feat: update page

This commit is contained in:
yanghao05
2025-04-09 21:03:23 +08:00
parent 2346983d67
commit 1f27611dc7
7 changed files with 203 additions and 105 deletions

View 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
}
]
}

View File

@@ -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"

View File

@@ -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>