feat: update page
This commit is contained in:
@@ -78,3 +78,57 @@ func (ctl *posts) Create(ctx fiber.Ctx, form *PostForm) error {
|
|||||||
func (ctl *posts) Update(ctx fiber.Ctx, id int64, form *model.Posts) error {
|
func (ctl *posts) Update(ctx fiber.Ctx, id int64, form *model.Posts) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete posts
|
||||||
|
// @Router /v1/admin/posts/:id [delete]
|
||||||
|
// @Bind id path
|
||||||
|
func (ctl *posts) Delete(ctx fiber.Ctx, id int64) error {
|
||||||
|
post, err := models.Posts.GetByID(ctx.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if post == nil {
|
||||||
|
return fiber.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.Posts.DeleteByID(ctx.Context(), id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostItem struct {
|
||||||
|
*model.Posts
|
||||||
|
Medias []*models.MediaItem `json:"medias"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show posts by id
|
||||||
|
// @Router /v1/admin/posts/:id [get]
|
||||||
|
// @Bind id path
|
||||||
|
func (ctl *posts) Show(ctx fiber.Ctx, id int64) (*PostItem, error) {
|
||||||
|
post, err := models.Posts.GetByID(ctx.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
medias, err := models.Medias.GetByIds(ctx.Context(), lo.Map(post.Assets.Data, func(asset fields.MediaAsset, _ int) int64 {
|
||||||
|
return asset.Media
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &PostItem{
|
||||||
|
Posts: post,
|
||||||
|
Medias: lo.Map(medias, func(media *model.Medias, _ int) *models.MediaItem {
|
||||||
|
return &models.MediaItem{
|
||||||
|
ID: media.ID,
|
||||||
|
Name: media.Name,
|
||||||
|
UploadTime: media.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
FileSize: media.Size,
|
||||||
|
MimeType: media.MimeType,
|
||||||
|
FileType: models.Medias.ConvertFileTypeByMimeType(media.MimeType),
|
||||||
|
ThumbnailUrl: "",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
Body[model.Posts]("form"),
|
Body[model.Posts]("form"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
router.Delete("/v1/admin/posts/:id", Func1(
|
||||||
|
r.posts.Delete,
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
))
|
||||||
|
|
||||||
|
router.Get("/v1/admin/posts/:id", DataFunc1(
|
||||||
|
r.posts.Show,
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
))
|
||||||
|
|
||||||
// 注册路由组: uploads
|
// 注册路由组: uploads
|
||||||
router.Post("/v1/admin/uploads/:md5/chunks/:idx", Func3(
|
router.Post("/v1/admin/uploads/:md5/chunks/:idx", Func3(
|
||||||
r.uploads.Chunks,
|
r.uploads.Chunks,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"quyun/app/requests"
|
"quyun/app/requests"
|
||||||
"quyun/database/fields"
|
|
||||||
"quyun/database/schemas/public/model"
|
"quyun/database/schemas/public/model"
|
||||||
"quyun/database/schemas/public/table"
|
"quyun/database/schemas/public/table"
|
||||||
|
|
||||||
@@ -29,9 +28,7 @@ func (m *postsModel) Prepare() error {
|
|||||||
func (m *postsModel) BuildConditionWithKey(key *string) BoolExpression {
|
func (m *postsModel) BuildConditionWithKey(key *string) BoolExpression {
|
||||||
tbl := table.Posts
|
tbl := table.Posts
|
||||||
|
|
||||||
cond := tbl.DeletedAt.IS_NULL().AND(
|
cond := tbl.DeletedAt.IS_NULL()
|
||||||
tbl.Status.EQ(Int32(int32(fields.PostStatusPublished))),
|
|
||||||
)
|
|
||||||
|
|
||||||
if key == nil || *key == "" {
|
if key == nil || *key == "" {
|
||||||
return cond
|
return cond
|
||||||
@@ -58,8 +55,6 @@ func (m *postsModel) GetByID(ctx context.Context, id int64) (*model.Posts, error
|
|||||||
WHERE(
|
WHERE(
|
||||||
tbl.ID.EQ(Int64(id)).AND(
|
tbl.ID.EQ(Int64(id)).AND(
|
||||||
tbl.DeletedAt.IS_NULL(),
|
tbl.DeletedAt.IS_NULL(),
|
||||||
).AND(
|
|
||||||
tbl.Status.EQ(Int32(int32(fields.PostStatusPublished))),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
m.log.Infof("sql: %s", stmt.DebugSql())
|
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||||
@@ -211,3 +206,21 @@ func (m *postsModel) Buy(ctx context.Context, userId, postId int64) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteByID soft delete item
|
||||||
|
func (m *postsModel) DeleteByID(ctx context.Context, id int64) error {
|
||||||
|
tbl := table.Posts
|
||||||
|
stmt := tbl.
|
||||||
|
UPDATE(tbl.DeletedAt).
|
||||||
|
SET(TimestampT(time.Now())).
|
||||||
|
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 post: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,4 +43,7 @@ Content-Type: application/json
|
|||||||
|
|
||||||
### 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
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
|
### delete posts
|
||||||
|
DELETE {{host}}/v1/admin/posts/103 HTTP/1.1
|
||||||
25
frontend/admin/src/api/posts_item.json
Normal file
25
frontend/admin/src/api/posts_item.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"id": 102,
|
||||||
|
"created_at": "2025-04-09T20:25:00.118963Z",
|
||||||
|
"updated_at": "2025-04-09T20:25:00.118963Z",
|
||||||
|
"deleted_at": null,
|
||||||
|
"status": 0,
|
||||||
|
"title": "adsfafd",
|
||||||
|
"description": "afda",
|
||||||
|
"content": "",
|
||||||
|
"price": 123123,
|
||||||
|
"discount": 100,
|
||||||
|
"views": 0,
|
||||||
|
"likes": 0,
|
||||||
|
"tags": null,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"type": "unknown",
|
||||||
|
"media": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "document",
|
||||||
|
"media": 48
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { postService } from '@/api/postService';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -9,7 +10,6 @@ import Button from 'primevue/button';
|
|||||||
import Column from 'primevue/column';
|
import Column from 'primevue/column';
|
||||||
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 InputNumber from 'primevue/inputnumber';
|
import InputNumber from 'primevue/inputnumber';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
@@ -26,23 +26,25 @@ const post = reactive({
|
|||||||
id: null,
|
id: null,
|
||||||
title: '',
|
title: '',
|
||||||
price: 0,
|
price: 0,
|
||||||
|
discount: 100,
|
||||||
introduction: '',
|
introduction: '',
|
||||||
status: '',
|
|
||||||
selectedMedia: [],
|
selectedMedia: [],
|
||||||
author: '',
|
medias: [],
|
||||||
publishedAt: '',
|
status: 0,
|
||||||
viewCount: 0,
|
|
||||||
mediaTypes: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status options
|
// Status options
|
||||||
const statusOptions = ref(['已发布', '草稿', '已下架']);
|
const statusOptions = [
|
||||||
|
{ label: '发布', value: 1 },
|
||||||
|
{ label: '草稿', value: 0 }
|
||||||
|
];
|
||||||
|
|
||||||
// Validation state
|
// Validation state
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
introduction: '',
|
introduction: '',
|
||||||
selectedMedia: ''
|
selectedMedia: '',
|
||||||
|
discount: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Media selection dialog state
|
// Media selection dialog state
|
||||||
@@ -99,65 +101,22 @@ const mediaItems = ref([
|
|||||||
const fetchPost = async (id) => {
|
const fetchPost = async (id) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// In a real app, you would call an API to get the post
|
const response = await postService.getPost(id);
|
||||||
// const response = await postApi.getPostById(id);
|
if (response.status !== 200) {
|
||||||
// Object.assign(post, response.data);
|
toast.add({ severity: 'error', summary: '错误', detail: response.message, life: 3000 });
|
||||||
|
|
||||||
// For demo, we'll use some sample data
|
|
||||||
const samplePosts = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: '如何高效学习编程',
|
|
||||||
author: '张三',
|
|
||||||
thumbnail: 'https://via.placeholder.com/150',
|
|
||||||
price: 29.99,
|
|
||||||
publishedAt: '2023-06-15 14:30',
|
|
||||||
status: '已发布',
|
|
||||||
mediaTypes: ['文章', '视频'],
|
|
||||||
viewCount: 1254,
|
|
||||||
introduction: '这是一篇关于高效学习编程的文章,包含了多种学习方法和技巧。',
|
|
||||||
selectedMedia: [mediaItems.value[0], mediaItems.value[2]]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '前端开发最佳实践',
|
|
||||||
author: '李四',
|
|
||||||
thumbnail: 'https://via.placeholder.com/150',
|
|
||||||
price: 49.99,
|
|
||||||
publishedAt: '2023-06-10 09:15',
|
|
||||||
status: '草稿',
|
|
||||||
mediaTypes: ['文章'],
|
|
||||||
viewCount: 789,
|
|
||||||
introduction: '探讨现代前端开发的各种最佳实践和设计模式。',
|
|
||||||
selectedMedia: [mediaItems.value[1]]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: '数据分析入门指南',
|
|
||||||
author: '王五',
|
|
||||||
thumbnail: 'https://via.placeholder.com/150',
|
|
||||||
price: 0.00,
|
|
||||||
publishedAt: '2023-06-05 16:45',
|
|
||||||
status: '已下架',
|
|
||||||
mediaTypes: ['文章', '音频'],
|
|
||||||
viewCount: 2567,
|
|
||||||
introduction: '介绍数据分析的基础知识和常用工具。',
|
|
||||||
selectedMedia: [mediaItems.value[3], mediaItems.value[4]]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const foundPost = samplePosts.find(p => p.id === parseInt(id));
|
|
||||||
if (foundPost) {
|
|
||||||
Object.assign(post, foundPost);
|
|
||||||
// Initialize selectedMediaItems with the post's media for the dialog
|
|
||||||
selectedMediaItems.value = [...post.selectedMedia];
|
|
||||||
} else {
|
|
||||||
toast.add({ severity: 'error', summary: '错误', detail: '未找到该文章', life: 3000 });
|
|
||||||
router.push('/posts');
|
router.push('/posts');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate API delay
|
const postData = response.data;
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
post.id = postData.id;
|
||||||
|
post.title = postData.title;
|
||||||
|
post.price = postData.price;
|
||||||
|
post.discount = postData.discount || 100;
|
||||||
|
post.introduction = postData.introduction;
|
||||||
|
post.status = postData.status;
|
||||||
|
post.selectedMedia = postData.medias || [];
|
||||||
|
post.medias = postData.medias?.map(media => media.id) || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
|
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
|
||||||
router.push('/posts');
|
router.push('/posts');
|
||||||
@@ -235,21 +194,26 @@ const savePost = async () => {
|
|||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (post.discount < 0 || post.discount > 100) {
|
||||||
|
errors.discount = '折扣必须在0到100之间';
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
|
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// In a real app, you would call an API to update the post
|
post.medias = post.selectedMedia.map(media => media.id);
|
||||||
// await postApi.updatePost(post.id, post);
|
const resp = await postService.updatePost(post.id, post);
|
||||||
|
|
||||||
// Simulate API delay
|
if (resp.status !== 200) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
toast.add({ severity: 'error', summary: '错误', detail: resp.message, life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功更新', life: 3000 });
|
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功更新', life: 3000 });
|
||||||
|
|
||||||
// Navigate back to the post list
|
|
||||||
router.push('/posts');
|
router.push('/posts');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({ severity: 'error', summary: '错误', detail: '更新文章失败', life: 3000 });
|
toast.add({ severity: 'error', summary: '错误', detail: '更新文章失败', life: 3000 });
|
||||||
@@ -264,11 +228,11 @@ const cancelEdit = () => {
|
|||||||
// File type badge severity mapping
|
// File type badge severity mapping
|
||||||
const getBadgeSeverity = (fileType) => {
|
const getBadgeSeverity = (fileType) => {
|
||||||
const map = {
|
const map = {
|
||||||
'Image': 'info',
|
'image': 'info',
|
||||||
'PDF': 'danger',
|
'pdf': 'danger',
|
||||||
'Video': 'warning',
|
'video': 'warning',
|
||||||
'Document': 'primary',
|
'document': 'primary',
|
||||||
'Audio': 'success'
|
'audio': 'success'
|
||||||
};
|
};
|
||||||
return map[fileType] || 'info';
|
return map[fileType] || 'info';
|
||||||
};
|
};
|
||||||
@@ -276,11 +240,11 @@ const getBadgeSeverity = (fileType) => {
|
|||||||
// File type icon mapping
|
// File type icon mapping
|
||||||
const getFileIcon = (file) => {
|
const getFileIcon = (file) => {
|
||||||
const map = {
|
const map = {
|
||||||
'Image': 'pi-image',
|
'image': 'pi-image',
|
||||||
'PDF': 'pi-file-pdf',
|
'pdf': 'pi-file-pdf',
|
||||||
'Video': 'pi-video',
|
'video': 'pi-video',
|
||||||
'Document': 'pi-file',
|
'document': 'pi-file',
|
||||||
'Audio': 'pi-volume-up'
|
'audio': 'pi-volume-up'
|
||||||
};
|
};
|
||||||
return `pi ${map[file.fileType] || 'pi-file'}`;
|
return `pi ${map[file.fileType] || 'pi-file'}`;
|
||||||
};
|
};
|
||||||
@@ -315,15 +279,14 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-0!">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">标题</label>
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">标题</label>
|
||||||
<InputText id="title" v-model="post.title" class="w-full p-inputtext-lg" placeholder="输入文章标题" />
|
<InputText id="title" v-model="post.title" class="w-full p-inputtext-lg" placeholder="输入文章标题" />
|
||||||
<small v-if="errors.title" class="p-error">{{ errors.title }}</small>
|
<small v-if="errors.title" class="text-red-500">{{ errors.title }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Price -->
|
<!-- Price -->
|
||||||
<div class="col-span-1">
|
<div class="col-span-1">
|
||||||
<label for="price" class="block text-sm font-medium text-gray-700 mb-1">价格 (¥)</label>
|
<label for="price" class="block text-sm font-medium text-gray-700 mb-1">价格 (¥)</label>
|
||||||
@@ -331,10 +294,24 @@ onMounted(() => {
|
|||||||
class="w-full" />
|
class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Discount -->
|
||||||
<div class="col-span-1">
|
<div class="col-span-1">
|
||||||
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
<label for="discount" class="block text-sm font-medium text-gray-700 mb-1">折扣 (%)</label>
|
||||||
<Dropdown id="status" v-model="post.status" :options="statusOptions" class="w-full" />
|
<InputNumber id="discount" v-model="post.discount" :min="0" :max="100" :minFractionDigits="0"
|
||||||
|
:maxFractionDigits="0" class="w-full" placeholder="输入折扣百分比" />
|
||||||
|
<small v-if="errors.discount" class="text-red-500">{{ errors.discount }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div v-for="option in statusOptions" :key="option.value" class="flex items-center">
|
||||||
|
<RadioButton :value="option.value" v-model="post.status"
|
||||||
|
:inputId="'status_' + option.value" />
|
||||||
|
<label :for="'status_' + option.value" class="ml-2">{{ option.label }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Introduction -->
|
<!-- Introduction -->
|
||||||
@@ -365,16 +342,16 @@ onMounted(() => {
|
|||||||
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
||||||
<div v-if="media.thumbnailUrl" class="flex-shrink-0 h-10 w-10 mr-3">
|
<div v-if="media.thumbnailUrl" class="flex-shrink-0 h-10 w-10 mr-3">
|
||||||
<img class="h-10 w-10 object-cover rounded" :src="media.thumbnailUrl"
|
<img class="h-10 w-10 object-cover rounded" :src="media.thumbnailUrl"
|
||||||
:alt="media.fileName">
|
:alt="media.name">
|
||||||
</div>
|
</div>
|
||||||
<div v-else
|
<div v-else
|
||||||
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
||||||
<i :class="getFileIcon(media)" class="text-2xl"></i>
|
<i :class="getFileIcon(media)" class="text-2xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="text-sm font-medium text-gray-900 truncate">{{ media.fileName }}
|
<div class="text-sm font-medium text-gray-900 truncate">{{ media.name }}
|
||||||
</div>
|
</div>
|
||||||
<Badge :value="media.fileType" :severity="getBadgeSeverity(media.fileType)"
|
<Badge :value="media.file_type" :severity="getBadgeSeverity(media.file_type)"
|
||||||
class="text-xs" />
|
class="text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ const mediaTypeOptions = ref([
|
|||||||
const globalFilterValue = ref('');
|
const globalFilterValue = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const searchTimeout = ref(null);
|
const searchTimeout = ref(null);
|
||||||
|
const filters = ref({
|
||||||
|
global: { value: null, matchMode: 'contains' },
|
||||||
|
status: { value: null, matchMode: 'equals' },
|
||||||
|
mediaTypes: { value: null, matchMode: 'equals' }
|
||||||
|
});
|
||||||
|
|
||||||
// Sample data - in a real app, this would come from an API
|
// Sample data - in a real app, this would come from an API
|
||||||
const posts = ref([]);
|
const posts = ref([]);
|
||||||
@@ -50,9 +55,8 @@ const total = ref(0); // 总记录数
|
|||||||
|
|
||||||
// Status mapping
|
// Status mapping
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
1: '已发布',
|
0: '发布',
|
||||||
2: '草稿',
|
1: '草稿',
|
||||||
3: '已下架'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transform assets to media types
|
// Transform assets to media types
|
||||||
@@ -90,9 +94,22 @@ const confirmDelete = (post) => {
|
|||||||
icon: 'pi pi-exclamation-triangle',
|
icon: 'pi pi-exclamation-triangle',
|
||||||
acceptClass: 'p-button-danger',
|
acceptClass: 'p-button-danger',
|
||||||
accept: () => {
|
accept: () => {
|
||||||
// In a real app, you would call an API to delete the post
|
// // In a real app, you would call an API to delete the post
|
||||||
posts.value = posts.value.filter(p => p.id !== post.id);
|
// posts.value = posts.value.filter(p => p.id !== post.id);
|
||||||
toast.add({ severity: 'success', summary: '成功', detail: '文章已删除', life: 3000 });
|
// toast.add({ severity: 'success', summary: '成功', detail: '文章已删除', life: 3000 });
|
||||||
|
|
||||||
|
// call remote delete
|
||||||
|
postService.deletePost(post.id)
|
||||||
|
.then(() => {
|
||||||
|
// toast success
|
||||||
|
toast.add({ severity: 'success', summary: '成功', detail: '文章已删除', life: 3000 });
|
||||||
|
fetchPosts();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Delete error:', error); // Debug log
|
||||||
|
toast.add({ severity: 'error', summary: '错误', detail: '删除文章失败', life: 3000 });
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -217,9 +234,8 @@ const formatMediaTypes = (mediaTypes) => {
|
|||||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
:rowsPerPageOptions="[10, 20, 50]"
|
:rowsPerPageOptions="[10, 20, 50]"
|
||||||
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
|
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
|
||||||
:globalFilterFields="['title', 'description', 'status']"
|
:globalFilterFields="['title', 'description', 'status']" :filters="filters.value" stripedRows
|
||||||
:filters="{ global: { value: globalFilterValue, matchMode: 'contains' } }" stripedRows removableSort
|
removableSort class="p-datatable-sm" responsiveLayout="scroll">
|
||||||
class="p-datatable-sm" responsiveLayout="scroll">
|
|
||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="text-center p-4">未找到文章。</div>
|
<div class="text-center p-4">未找到文章。</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user