feat: update page
This commit is contained in:
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>
|
||||
import { postService } from '@/api/postService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -9,7 +10,6 @@ import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
@@ -26,23 +26,25 @@ const post = reactive({
|
||||
id: null,
|
||||
title: '',
|
||||
price: 0,
|
||||
discount: 100,
|
||||
introduction: '',
|
||||
status: '',
|
||||
selectedMedia: [],
|
||||
author: '',
|
||||
publishedAt: '',
|
||||
viewCount: 0,
|
||||
mediaTypes: [],
|
||||
medias: [],
|
||||
status: 0,
|
||||
});
|
||||
|
||||
// Status options
|
||||
const statusOptions = ref(['已发布', '草稿', '已下架']);
|
||||
const statusOptions = [
|
||||
{ label: '发布', value: 1 },
|
||||
{ label: '草稿', value: 0 }
|
||||
];
|
||||
|
||||
// Validation state
|
||||
const errors = reactive({
|
||||
title: '',
|
||||
introduction: '',
|
||||
selectedMedia: ''
|
||||
selectedMedia: '',
|
||||
discount: ''
|
||||
});
|
||||
|
||||
// Media selection dialog state
|
||||
@@ -99,65 +101,22 @@ const mediaItems = ref([
|
||||
const fetchPost = async (id) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// In a real app, you would call an API to get the post
|
||||
// const response = await postApi.getPostById(id);
|
||||
// Object.assign(post, response.data);
|
||||
|
||||
// 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 });
|
||||
const response = await postService.getPost(id);
|
||||
if (response.status !== 200) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: response.message, life: 3000 });
|
||||
router.push('/posts');
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const postData = response.data;
|
||||
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) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
|
||||
router.push('/posts');
|
||||
@@ -235,21 +194,26 @@ const savePost = async () => {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (post.discount < 0 || post.discount > 100) {
|
||||
errors.discount = '折扣必须在0到100之间';
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// In a real app, you would call an API to update the post
|
||||
// await postApi.updatePost(post.id, post);
|
||||
post.medias = post.selectedMedia.map(media => media.id);
|
||||
const resp = await postService.updatePost(post.id, post);
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (resp.status !== 200) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: resp.message, life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功更新', life: 3000 });
|
||||
|
||||
// Navigate back to the post list
|
||||
router.push('/posts');
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: '更新文章失败', life: 3000 });
|
||||
@@ -264,11 +228,11 @@ const cancelEdit = () => {
|
||||
// File type badge severity mapping
|
||||
const getBadgeSeverity = (fileType) => {
|
||||
const map = {
|
||||
'Image': 'info',
|
||||
'PDF': 'danger',
|
||||
'Video': 'warning',
|
||||
'Document': 'primary',
|
||||
'Audio': 'success'
|
||||
'image': 'info',
|
||||
'pdf': 'danger',
|
||||
'video': 'warning',
|
||||
'document': 'primary',
|
||||
'audio': 'success'
|
||||
};
|
||||
return map[fileType] || 'info';
|
||||
};
|
||||
@@ -276,11 +240,11 @@ const getBadgeSeverity = (fileType) => {
|
||||
// File type icon mapping
|
||||
const getFileIcon = (file) => {
|
||||
const map = {
|
||||
'Image': 'pi-image',
|
||||
'PDF': 'pi-file-pdf',
|
||||
'Video': 'pi-video',
|
||||
'Document': 'pi-file',
|
||||
'Audio': 'pi-volume-up'
|
||||
'image': 'pi-image',
|
||||
'pdf': 'pi-file-pdf',
|
||||
'video': 'pi-video',
|
||||
'document': 'pi-file',
|
||||
'audio': 'pi-volume-up'
|
||||
};
|
||||
return `pi ${map[file.fileType] || 'pi-file'}`;
|
||||
};
|
||||
@@ -315,15 +279,14 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
<div class="col-span-2">
|
||||
<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="输入文章标题" />
|
||||
<small v-if="errors.title" class="p-error">{{ errors.title }}</small>
|
||||
<small v-if="errors.title" class="text-red-500">{{ errors.title }}</small>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Price -->
|
||||
<div class="col-span-1">
|
||||
<label for="price" class="block text-sm font-medium text-gray-700 mb-1">价格 (¥)</label>
|
||||
@@ -331,10 +294,24 @@ onMounted(() => {
|
||||
class="w-full" />
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<!-- Discount -->
|
||||
<div class="col-span-1">
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
||||
<Dropdown id="status" v-model="post.status" :options="statusOptions" class="w-full" />
|
||||
<label for="discount" class="block text-sm font-medium text-gray-700 mb-1">折扣 (%)</label>
|
||||
<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>
|
||||
|
||||
<!-- Introduction -->
|
||||
@@ -365,16 +342,16 @@ onMounted(() => {
|
||||
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">
|
||||
<img class="h-10 w-10 object-cover rounded" :src="media.thumbnailUrl"
|
||||
:alt="media.fileName">
|
||||
:alt="media.name">
|
||||
</div>
|
||||
<div v-else
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
<Badge :value="media.fileType" :severity="getBadgeSeverity(media.fileType)"
|
||||
<Badge :value="media.file_type" :severity="getBadgeSeverity(media.file_type)"
|
||||
class="text-xs" />
|
||||
</div>
|
||||
<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 loading = ref(false);
|
||||
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
|
||||
const posts = ref([]);
|
||||
@@ -50,9 +55,8 @@ const total = ref(0); // 总记录数
|
||||
|
||||
// Status mapping
|
||||
const statusMap = {
|
||||
1: '已发布',
|
||||
2: '草稿',
|
||||
3: '已下架'
|
||||
0: '发布',
|
||||
1: '草稿',
|
||||
};
|
||||
|
||||
// Transform assets to media types
|
||||
@@ -90,9 +94,22 @@ const confirmDelete = (post) => {
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: () => {
|
||||
// In a real app, you would call an API to delete the post
|
||||
posts.value = posts.value.filter(p => p.id !== post.id);
|
||||
toast.add({ severity: 'success', summary: '成功', detail: '文章已删除', life: 3000 });
|
||||
// // In a real app, you would call an API to delete the post
|
||||
// posts.value = posts.value.filter(p => p.id !== post.id);
|
||||
// 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"
|
||||
:rowsPerPageOptions="[10, 20, 50]"
|
||||
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
|
||||
:globalFilterFields="['title', 'description', 'status']"
|
||||
:filters="{ global: { value: globalFilterValue, matchMode: 'contains' } }" stripedRows removableSort
|
||||
class="p-datatable-sm" responsiveLayout="scroll">
|
||||
:globalFilterFields="['title', 'description', 'status']" :filters="filters.value" stripedRows
|
||||
removableSort class="p-datatable-sm" responsiveLayout="scroll">
|
||||
|
||||
<template #empty>
|
||||
<div class="text-center p-4">未找到文章。</div>
|
||||
|
||||
Reference in New Issue
Block a user