feat: add post buy count
This commit is contained in:
@@ -38,3 +38,22 @@ func (ctl *medias) Show(ctx fiber.Ctx, id int64) error {
|
|||||||
|
|
||||||
return ctx.Redirect().To(url)
|
return ctx.Redirect().To(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @Router /v1/admin/medias/:id [delete]
|
||||||
|
// @Bind id path
|
||||||
|
func (ctl *medias) Delete(ctx fiber.Ctx, id int64) error {
|
||||||
|
media, err := models.Medias.GetByID(ctx.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.SendString("Media not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctl.oss.Delete(ctx.Context(), media.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.Medias.Delete(ctx.Context(), id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,32 @@ type posts struct{}
|
|||||||
// @Bind query query
|
// @Bind query query
|
||||||
func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery) (*requests.Pager, error) {
|
func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery) (*requests.Pager, error) {
|
||||||
cond := models.Posts.BuildConditionWithKey(query.Keyword)
|
cond := models.Posts.BuildConditionWithKey(query.Keyword)
|
||||||
return models.Posts.List(ctx.Context(), pagination, cond)
|
pager, err := models.Posts.List(ctx.Context(), pagination, cond)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
postIds := lo.Map(pager.Items.([]model.Posts), func(item model.Posts, _ int) int64 {
|
||||||
|
return item.ID
|
||||||
|
})
|
||||||
|
if len(postIds) > 0 {
|
||||||
|
postCntMap, err := models.Posts.BoughtStatistics(ctx.Context(), postIds)
|
||||||
|
if err != nil {
|
||||||
|
return pager, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := lo.Map(pager.Items.([]model.Posts), func(item model.Posts, _ int) PostItem {
|
||||||
|
cnt := int64(0)
|
||||||
|
if v, ok := postCntMap[item.ID]; ok {
|
||||||
|
cnt = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostItem{Posts: &item, BoughtCount: cnt}
|
||||||
|
})
|
||||||
|
|
||||||
|
pager.Items = items
|
||||||
|
}
|
||||||
|
return pager, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostForm struct {
|
type PostForm struct {
|
||||||
@@ -142,7 +167,8 @@ func (ctl *posts) Delete(ctx fiber.Ctx, id int64) error {
|
|||||||
|
|
||||||
type PostItem struct {
|
type PostItem struct {
|
||||||
*model.Posts
|
*model.Posts
|
||||||
Medias []*model.Medias `json:"medias"`
|
Medias []*model.Medias `json:"medias"`
|
||||||
|
BoughtCount int64 `json:"bought_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show posts by id
|
// Show posts by id
|
||||||
@@ -165,3 +191,22 @@ func (ctl *posts) Show(ctx fiber.Ctx, id int64) (*PostItem, error) {
|
|||||||
Medias: medias,
|
Medias: medias,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTo
|
||||||
|
// @Router /v1/admin/posts/:id/send-to/:userId [post]
|
||||||
|
// @Bind id path
|
||||||
|
// @Bind userId path
|
||||||
|
func (ctl *posts) SendTo(ctx fiber.Ctx, id, userId int64) error {
|
||||||
|
if _, err := models.Posts.GetByID(ctx.Context(), id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := models.Users.GetByID(ctx.Context(), userId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.Posts.SendTo(ctx.Context(), id, userId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
PathParam[int64]("id"),
|
PathParam[int64]("id"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
router.Delete("/v1/admin/medias/:id", Func1(
|
||||||
|
r.medias.Delete,
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
))
|
||||||
|
|
||||||
// 注册路由组: orders
|
// 注册路由组: orders
|
||||||
router.Get("/v1/admin/orders", DataFunc2(
|
router.Get("/v1/admin/orders", DataFunc2(
|
||||||
r.orders.List,
|
r.orders.List,
|
||||||
@@ -85,6 +90,12 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
PathParam[int64]("id"),
|
PathParam[int64]("id"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
router.Post("/v1/admin/posts/:id/send-to/:userId", Func2(
|
||||||
|
r.posts.SendTo,
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
PathParam[int64]("userId"),
|
||||||
|
))
|
||||||
|
|
||||||
// 注册路由组: uploads
|
// 注册路由组: uploads
|
||||||
router.Get("/v1/admin/uploads/pre-uploaded-check/:md5.:ext", DataFunc3(
|
router.Get("/v1/admin/uploads/pre-uploaded-check/:md5.:ext", DataFunc3(
|
||||||
r.uploads.PreUploadCheck,
|
r.uploads.PreUploadCheck,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/log"
|
"github.com/gofiber/fiber/v3/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListQuery struct {
|
type ListQuery struct {
|
||||||
@@ -25,6 +26,11 @@ type posts struct {
|
|||||||
wepay *wepay.Client
|
wepay *wepay.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostItem struct {
|
||||||
|
model.Posts
|
||||||
|
BoughtCount int64 `json:"bought_count"`
|
||||||
|
}
|
||||||
|
|
||||||
// List posts
|
// List posts
|
||||||
// @Router /api/posts [get]
|
// @Router /api/posts [get]
|
||||||
// @Bind pagination query
|
// @Bind pagination query
|
||||||
@@ -32,11 +38,34 @@ type posts struct {
|
|||||||
// @Bind user local
|
// @Bind user local
|
||||||
func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery, user *model.Users) (*requests.Pager, error) {
|
func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery, user *model.Users) (*requests.Pager, error) {
|
||||||
cond := models.Posts.BuildConditionWithKey(query.Keyword)
|
cond := models.Posts.BuildConditionWithKey(query.Keyword)
|
||||||
return models.Posts.List(ctx.Context(), pagination, cond, func(item model.Posts) model.Posts {
|
pager, err := models.Posts.List(ctx.Context(), pagination, cond, func(item model.Posts) model.Posts {
|
||||||
item.Assets = fields.ToJson([]fields.MediaAsset{})
|
item.Assets = fields.ToJson([]fields.MediaAsset{})
|
||||||
item.Content = ""
|
item.Content = ""
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
|
|
||||||
|
postIds := lo.Map(pager.Items.([]model.Posts), func(item model.Posts, _ int) int64 {
|
||||||
|
return item.ID
|
||||||
|
})
|
||||||
|
if len(postIds) > 0 {
|
||||||
|
postCntMap, err := models.Posts.BoughtStatistics(ctx.Context(), postIds)
|
||||||
|
if err != nil {
|
||||||
|
return pager, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := lo.Map(pager.Items.([]model.Posts), func(item model.Posts, _ int) PostItem {
|
||||||
|
cnt := int64(0)
|
||||||
|
if v, ok := postCntMap[item.ID]; ok {
|
||||||
|
cnt = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostItem{Posts: item, BoughtCount: cnt}
|
||||||
|
})
|
||||||
|
|
||||||
|
pager.Items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
return pager, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show
|
// Show
|
||||||
|
|||||||
@@ -198,3 +198,20 @@ func (m *mediasModel) GetByID(ctx context.Context, id int64) (*model.Medias, err
|
|||||||
|
|
||||||
return &media, nil
|
return &media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
func (m *mediasModel) Delete(ctx context.Context, id int64) error {
|
||||||
|
tbl := table.Medias
|
||||||
|
stmt := tbl.
|
||||||
|
DELETE().
|
||||||
|
WHERE(tbl.ID.EQ(Int64(id)))
|
||||||
|
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||||
|
|
||||||
|
if _, err := stmt.ExecContext(ctx, db); err != nil {
|
||||||
|
m.log.Errorf("error deleting media item: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.log.Infof("media item deleted successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,3 +233,56 @@ func (m *postsModel) DeleteByID(ctx context.Context, id int64) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendTo
|
||||||
|
func (m *postsModel) SendTo(ctx context.Context, userId, postId int64) error {
|
||||||
|
// add record to user_posts
|
||||||
|
tbl := table.UserPosts
|
||||||
|
stmt := tbl.INSERT(tbl.MutableColumns).MODEL(model.UserPosts{
|
||||||
|
UserID: userId,
|
||||||
|
PostID: postId,
|
||||||
|
})
|
||||||
|
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||||
|
if _, err := stmt.ExecContext(ctx, db); err != nil {
|
||||||
|
m.log.Errorf("error sending post to user: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostBoughtStatistics 获取指定文件 ID 的购买次数
|
||||||
|
func (m *postsModel) BoughtStatistics(ctx context.Context, postIds []int64) (map[int64]int64, error) {
|
||||||
|
tbl := table.UserPosts
|
||||||
|
|
||||||
|
// select count(user_id), post_id from user_posts up where post_id in (1, 2,3,4,5,6,7,8,9,10) group by post_id
|
||||||
|
stmt := tbl.
|
||||||
|
SELECT(
|
||||||
|
COUNT(tbl.UserID).AS("cnt"),
|
||||||
|
tbl.PostID.AS("post_id"),
|
||||||
|
).
|
||||||
|
WHERE(
|
||||||
|
tbl.PostID.IN(lo.Map(postIds, func(id int64, _ int) Expression { return Int64(id) })...),
|
||||||
|
).
|
||||||
|
GROUP_BY(
|
||||||
|
tbl.PostID,
|
||||||
|
)
|
||||||
|
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||||
|
|
||||||
|
var result []struct {
|
||||||
|
Cnt int64
|
||||||
|
PostId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stmt.QueryContext(ctx, db, &result); err != nil {
|
||||||
|
m.log.Errorf("error getting post bought statistics: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to map
|
||||||
|
resultMap := make(map[int64]int64)
|
||||||
|
for _, item := range result {
|
||||||
|
resultMap[item.PostId] = item.Cnt
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,13 +36,20 @@ func Provide(opts ...opt.Option) error {
|
|||||||
|
|
||||||
return container.Container.Provide(func() (*OSSClient, error) {
|
return container.Container.Provide(func() (*OSSClient, error) {
|
||||||
cred := credentials.NewStaticCredentialsProvider(config.AccessKeyId, config.AccessKeySecret)
|
cred := credentials.NewStaticCredentialsProvider(config.AccessKeyId, config.AccessKeySecret)
|
||||||
|
|
||||||
cfg := oss.LoadDefaultConfig().
|
cfg := oss.LoadDefaultConfig().
|
||||||
|
WithCredentialsProvider(cred).
|
||||||
|
WithRegion(config.Region).
|
||||||
|
WithUseCName(true).
|
||||||
|
WithEndpoint(*config.Host)
|
||||||
|
|
||||||
|
cfgInternal := oss.LoadDefaultConfig().
|
||||||
WithCredentialsProvider(cred).
|
WithCredentialsProvider(cred).
|
||||||
WithRegion(config.Region)
|
WithRegion(config.Region)
|
||||||
|
|
||||||
return &OSSClient{
|
return &OSSClient{
|
||||||
client: oss.NewClient(cfg.WithUseCName(true).WithEndpoint(*config.Host)),
|
client: oss.NewClient(cfg),
|
||||||
internalClient: oss.NewClient(cfg.WithUseInternalEndpoint(true)),
|
internalClient: oss.NewClient(cfgInternal),
|
||||||
config: &config,
|
config: &config,
|
||||||
}, nil
|
}, nil
|
||||||
}, o.DiOptions()...)
|
}, o.DiOptions()...)
|
||||||
|
|||||||
@@ -53,3 +53,17 @@ func (c *OSSClient) GetSignedUrl(ctx context.Context, path string) (string, erro
|
|||||||
}
|
}
|
||||||
return preSign.URL, nil
|
return preSign.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
func (c *OSSClient) Delete(ctx context.Context, path string) error {
|
||||||
|
request := &oss.DeleteObjectRequest{
|
||||||
|
Bucket: oss.Ptr(c.config.Bucket),
|
||||||
|
Key: oss.Ptr(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.internalClient.DeleteObject(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ GET {{host}}/v1/admin/medias HTTP/1.1
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
### get posts
|
### get posts
|
||||||
GET {{host}}/v1/admin/posts HTTP/1.1
|
GET {{host}}/v1/admin/posts?page=10 HTTP/1.1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
Authorization: {{token}}
|
||||||
|
|
||||||
### get posts with keyword
|
### get posts with keyword
|
||||||
GET {{host}}/v1/admin/posts?page=1&limit=10&keyword=99123 HTTP/1.1
|
GET {{host}}/v1/admin/posts?page=1&limit=10&keyword=99123 HTTP/1.1
|
||||||
|
|||||||
@@ -23,4 +23,7 @@ export const postService = {
|
|||||||
deletePost(id) {
|
deletePost(id) {
|
||||||
return httpClient.delete(`/admin/posts/${id}`);
|
return httpClient.delete(`/admin/posts/${id}`);
|
||||||
},
|
},
|
||||||
|
sendTo(id, userId) {
|
||||||
|
return httpClient.post(`/admin/posts/${id}/send-to/${userId}`);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,9 @@ export const userService = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
searchUser(id) {
|
||||||
|
return httpClient.get(`/admin/users/${id}`);
|
||||||
|
},
|
||||||
getUser(id) {
|
getUser(id) {
|
||||||
return httpClient.get(`/admin/users/${id}`);
|
return httpClient.get(`/admin/users/${id}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { InputText } from "primevue";
|
|||||||
import Badge from "primevue/badge";
|
import Badge from "primevue/badge";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
import Column from "primevue/column";
|
import Column from "primevue/column";
|
||||||
|
import ConfirmDialog from 'primevue/confirmdialog';
|
||||||
import DataTable from "primevue/datatable";
|
import DataTable from "primevue/datatable";
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
import Dropdown from "primevue/dropdown";
|
import Dropdown from "primevue/dropdown";
|
||||||
@@ -172,10 +173,45 @@ const previewFile = (file) => {
|
|||||||
|
|
||||||
previewDialogVisible.value = true;
|
previewDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add delete related methods
|
||||||
|
const confirmDelete = (file) => {
|
||||||
|
confirm.require({
|
||||||
|
message: `确定要删除文件 "${file.name}" 吗?`,
|
||||||
|
header: '确认删除',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
acceptClass: 'p-button-danger',
|
||||||
|
accept: () => handleDelete(file),
|
||||||
|
reject: () => { },
|
||||||
|
acceptLabel: '删除',
|
||||||
|
rejectLabel: '取消'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (file) => {
|
||||||
|
try {
|
||||||
|
await mediaService.delete(file.id);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: '成功',
|
||||||
|
detail: '文件已删除',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
fetchMediaFiles();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: '错误',
|
||||||
|
detail: '删除文件失败',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Toast />
|
<Toast />
|
||||||
|
<ConfirmDialog />
|
||||||
<!-- Remove ConfirmDialog -->
|
<!-- Remove ConfirmDialog -->
|
||||||
|
|
||||||
<!-- Add Dialog component -->
|
<!-- Add Dialog component -->
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { getFileTypeByMimeCN } from "@/utils/filetype";
|
|||||||
import { InputText } from 'primevue';
|
import { InputText } from 'primevue';
|
||||||
import Badge from 'primevue/badge';
|
import Badge from 'primevue/badge';
|
||||||
|
|
||||||
|
import { userService } from '@/api/userService';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import Column from 'primevue/column';
|
import Column from 'primevue/column';
|
||||||
import ConfirmDialog from 'primevue/confirmdialog';
|
import ConfirmDialog from 'primevue/confirmdialog';
|
||||||
import DataTable from 'primevue/datatable';
|
import DataTable from 'primevue/datatable';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
import Dropdown from 'primevue/dropdown';
|
import Dropdown from 'primevue/dropdown';
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
@@ -193,12 +195,184 @@ const formatPrice = (price) => {
|
|||||||
const formatMediaTypes = (assets) => {
|
const formatMediaTypes = (assets) => {
|
||||||
return assets.map(asset => getFileTypeByMimeCN(asset.type)).join(', ');
|
return assets.map(asset => getFileTypeByMimeCN(asset.type)).join(', ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add user selection dialog state and methods
|
||||||
|
const sendDialogVisible = ref(false);
|
||||||
|
const selectedPost = ref(null);
|
||||||
|
const selectedUser = ref(null);
|
||||||
|
|
||||||
|
// 修改用户列表相关变量
|
||||||
|
const users = ref({
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 10
|
||||||
|
});
|
||||||
|
const userFirst = ref(0);
|
||||||
|
const userRows = ref(10);
|
||||||
|
const userLoading = ref(false);
|
||||||
|
|
||||||
|
const sendToUser = (post) => {
|
||||||
|
selectedPost.value = post;
|
||||||
|
sendDialogVisible.value = true;
|
||||||
|
searchUsers(''); // 初始加载用户列表
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改 searchUsers 函数
|
||||||
|
const searchUsers = async (query = '') => {
|
||||||
|
userLoading.value = true;
|
||||||
|
try {
|
||||||
|
const currentPage = (userFirst.value / userRows.value) + 1;
|
||||||
|
const response = await userService.getUsers({
|
||||||
|
page: currentPage,
|
||||||
|
limit: userRows.value,
|
||||||
|
keyword: userFilter.value
|
||||||
|
});
|
||||||
|
users.value = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: '错误',
|
||||||
|
detail: '加载用户列表失败',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
userLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改用户分页事件处理
|
||||||
|
const onUserPage = (event) => {
|
||||||
|
userFirst.value = event.first;
|
||||||
|
userRows.value = event.rows;
|
||||||
|
searchUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加用户选择相关变量
|
||||||
|
const userFilter = ref('');
|
||||||
|
const userSearchTimeout = ref(null);
|
||||||
|
|
||||||
|
// 修改用户搜索处理函数
|
||||||
|
const onUserSearch = (event) => {
|
||||||
|
if (userSearchTimeout.value) {
|
||||||
|
clearTimeout(userSearchTimeout.value);
|
||||||
|
}
|
||||||
|
userSearchTimeout.value = setTimeout(() => {
|
||||||
|
userFirst.value = 0; // 重置到第一页
|
||||||
|
searchUsers();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendConfirm = async () => {
|
||||||
|
if (!selectedUser.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: '提示',
|
||||||
|
detail: '请选择用户',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postService.sendTo(selectedPost.value.id, selectedUser.value.id);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: '成功',
|
||||||
|
detail: '赠送成功',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
sendDialogVisible.value = false;
|
||||||
|
selectedUser.value = null;
|
||||||
|
selectedPost.value = null;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: '错误',
|
||||||
|
detail: '赠送失败',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUserLabel = (user) => {
|
||||||
|
return `${user.nickname} (${user.phone})`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Toast />
|
<Toast />
|
||||||
<ConfirmDialog />
|
<ConfirmDialog />
|
||||||
|
|
||||||
|
<!-- Add user selection dialog -->
|
||||||
|
<Dialog v-model:visible="sendDialogVisible" modal header="选择用户" :style="{ width: '80vw' }">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<span class="font-bold">文章:</span>
|
||||||
|
{{ selectedPost?.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-4">
|
||||||
|
<InputText v-model="userFilter" placeholder="搜索用户..." class="w-full" @input="onUserSearch" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改 Dialog 中的 DataTable 部分 -->
|
||||||
|
<DataTable v-model:selection="selectedUser" :value="users.items" selectionMode="single"
|
||||||
|
:loading="userLoading" :paginator="true" :rows="userRows" :totalRecords="users.total" :lazy="true"
|
||||||
|
:first="userFirst" @page="onUserPage" dataKey="id" class="p-datatable-sm" responsiveLayout="scroll"
|
||||||
|
style="max-height: 60vh" scrollable>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center p-4">未找到用户</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #loading>
|
||||||
|
<div class="flex flex-col items-center justify-center p-4">
|
||||||
|
<ProgressSpinner style="width:50px;height:50px" />
|
||||||
|
<span class="mt-2">加载用户数据...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column selectionMode="single" style="width: 3rem" />
|
||||||
|
|
||||||
|
<Column field="username" header="用户名">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="mask mask-squircle w-12 h-12">
|
||||||
|
<img :src="data.avatar" :alt="data.username" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-bold">{{ data.username }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="phone" header="手机号" />
|
||||||
|
|
||||||
|
<Column field="status" header="状态">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Badge :value="data.status === 0 ? '活跃' : '禁用'"
|
||||||
|
:severity="data.status === 0 ? 'success' : 'danger'" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="created_at" header="注册时间">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" @click="sendDialogVisible = false" class="p-button-text" />
|
||||||
|
<Button label="确认赠送" icon="pi pi-check" @click="handleSendConfirm" :disabled="!selectedUser" autofocus
|
||||||
|
severity="primary" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex justify-between items-center mb-6 gap-4">
|
<div class="flex justify-between items-center mb-6 gap-4">
|
||||||
<h1 class="text-2xl font-semibold text-gray-800 text-nowrap">文章列表</h1>
|
<h1 class="text-2xl font-semibold text-gray-800 text-nowrap">文章列表</h1>
|
||||||
@@ -252,6 +426,14 @@ const formatMediaTypes = (assets) => {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
<Column field="bought_count" header="购买数量" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-gray-500">{{ data.bought_count }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
<Column field="updated_at" header="时间信息" sortable>
|
<Column field="updated_at" header="时间信息" sortable>
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -295,15 +477,13 @@ const formatMediaTypes = (assets) => {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column header="操作" :exportable="false" style="min-width:8rem">
|
<Column header="" :exportable="false" style="min-width:8rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="flex justify-center space-x-2">
|
<div class="flex justify-center space-x-2">
|
||||||
|
<Button icon="pi pi-shopping-cart" rounded text severity="info" @click="sendToUser(data)"
|
||||||
|
aria-label="赠送" />
|
||||||
<Button icon="pi pi-pencil" rounded text severity="info" @click="navigateToEditPost(data)"
|
<Button icon="pi pi-pencil" rounded text severity="info" @click="navigateToEditPost(data)"
|
||||||
aria-label="编辑" />
|
aria-label="编辑" />
|
||||||
<Button icon="pi pi-eye" rounded text severity="secondary" @click="viewPost(data)"
|
|
||||||
aria-label="查看" />
|
|
||||||
<Button icon="pi pi-trash" rounded text severity="danger" @click="confirmDelete(data)"
|
|
||||||
aria-label="删除" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|||||||
Reference in New Issue
Block a user