feat: update admin

This commit is contained in:
Rogee
2025-05-08 14:35:20 +08:00
parent eda360e398
commit 3ded1ddd60
8 changed files with 42 additions and 43 deletions

View File

@@ -19,7 +19,7 @@ const navItems = ref([
command: () => router.push('/medias') command: () => router.push('/medias')
}, },
{ {
label: '文章', label: '曲谱',
icon: 'pi pi-file', icon: 'pi pi-file',
command: () => router.push('/posts') command: () => router.push('/posts')
}, },

View File

@@ -73,7 +73,7 @@ onMounted(() => {
<template #header> <template #header>
<div class="border border-primary"></div> <div class="border border-primary"></div>
</template> </template>
<template #title>文章数量</template> <template #title>曲谱数量</template>
<template #content> <template #content>
<div class="text-4xl font-bold mb-2 text-primary"> <div class="text-4xl font-bold mb-2 text-primary">
{{ stats.post_published }}/{{ stats.post_draft }} {{ stats.post_published }}/{{ stats.post_draft }}

View File

@@ -192,7 +192,7 @@ onMounted(() => {
</div> </div>
</template> </template>
</Column> </Column>
<Column field="post_id" header="文章ID" sortable> <Column field="post_id" header="曲谱ID" sortable>
<template #body="{ data }"> <template #body="{ data }">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-gray-700"> 标题: {{ data.post_title }}</span> <span class="text-gray-700"> 标题: {{ data.post_title }}</span>
@@ -207,7 +207,7 @@ onMounted(() => {
<span class="text-orange-500">优惠: -¥{{ formatPrice(getDiscountAmount(data.price, <span class="text-orange-500">优惠: -¥{{ formatPrice(getDiscountAmount(data.price,
data.discount)) }}</span> data.discount)) }}</span>
<span class="font-bold">实付: ¥{{ formatPrice(getFinalPrice(data.price, data.discount)) <span class="font-bold">实付: ¥{{ formatPrice(getFinalPrice(data.price, data.discount))
}}</span> }}</span>
</div> </div>
</template> </template>
</Column> </Column>

View File

@@ -191,12 +191,12 @@ const savePost = async () => {
let valid = true; let valid = true;
if (!post.title.trim()) { if (!post.title.trim()) {
errors.title = '请填写文章标题'; errors.title = '请填写曲谱标题';
valid = false; valid = false;
} }
// if (!post.introduction.trim()) { // if (!post.introduction.trim()) {
// errors.introduction = '请填写文章介绍'; // errors.introduction = '请填写曲谱介绍';
// valid = false; // valid = false;
// } // }
@@ -230,12 +230,12 @@ const savePost = async () => {
toast.add({ severity: 'error', summary: '错误', detail: resp.message, life: 3000 }); toast.add({ severity: 'error', summary: '错误', detail: resp.message, life: 3000 });
return; return;
} }
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功创建', life: 3000 }); toast.add({ severity: 'success', summary: '成功', detail: '曲谱已成功创建', life: 3000 });
// Navigate back to the post list // 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 });
} }
}; };
@@ -263,7 +263,7 @@ const loadHeadImagePreviews = async () => {
<div class="w-full max-w-6xl mx-auto"> <div class="w-full max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">创建文章</h1> <h1 class="text-2xl font-bold text-gray-800">创建曲谱</h1>
<div class="flex gap-2"> <div class="flex gap-2">
<Button label="取消" icon="pi pi-times" severity="secondary" @click="cancelCreate" /> <Button label="取消" icon="pi pi-times" severity="secondary" @click="cancelCreate" />
<Button label="保存" icon="pi pi-check" severity="primary" @click="savePost" /> <Button label="保存" icon="pi pi-check" severity="primary" @click="savePost" />
@@ -310,7 +310,7 @@ const loadHeadImagePreviews = async () => {
<!-- 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="text-red-500">{{ errors.title }}</small> <small v-if="errors.title" class="text-red-500">{{ errors.title }}</small>
</div> </div>
@@ -343,9 +343,9 @@ const loadHeadImagePreviews = async () => {
<!-- Introduction --> <!-- Introduction -->
<div class="col-span-2"> <div class="col-span-2">
<label for="introduction" class="block text-sm font-medium text-gray-700 mb-1">文章介绍</label> <label for="introduction" class="block text-sm font-medium text-gray-700 mb-1">曲谱介绍</label>
<Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full" <Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full"
placeholder="输入文章介绍内容" /> placeholder="输入曲谱介绍内容" />
<small v-if="errors.introduction" class="text-red-500">{{ errors.introduction }}</small> <small v-if="errors.introduction" class="text-red-500">{{ errors.introduction }}</small>
</div> </div>

View File

@@ -130,7 +130,7 @@ const fetchPost = async (id) => {
post.head_images = postData.head_images || []; // Add head images post.head_images = postData.head_images || []; // Add head images
loadHeadImagePreviews(); // Load head image previews loadHeadImagePreviews(); // Load head image previews
} 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');
} finally { } finally {
loading.value = false; loading.value = false;
@@ -231,12 +231,12 @@ const savePost = async () => {
let valid = true; let valid = true;
if (!post.title.trim()) { if (!post.title.trim()) {
errors.title = '请填写文章标题'; errors.title = '请填写曲谱标题';
valid = false; valid = false;
} }
// if (!post.introduction.trim()) { // if (!post.introduction.trim()) {
// errors.introduction = '请填写文章介绍'; // errors.introduction = '请填写曲谱介绍';
// valid = false; // valid = false;
// } // }
@@ -270,10 +270,10 @@ const savePost = async () => {
return; return;
} }
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功更新', life: 3000 }); toast.add({ severity: 'success', summary: '成功', detail: '曲谱已成功更新', life: 3000 });
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 });
} }
}; };
@@ -297,7 +297,7 @@ onMounted(() => {
if (postId) { if (postId) {
fetchPost(postId); fetchPost(postId);
} else { } else {
toast.add({ severity: 'error', summary: '错误', detail: '未提供文章ID', life: 3000 }); toast.add({ severity: 'error', summary: '错误', detail: '未提供曲谱ID', life: 3000 });
router.push('/posts'); router.push('/posts');
} }
}); });
@@ -307,7 +307,7 @@ onMounted(() => {
<div class="w-full max-w-6xl mx-auto"> <div class="w-full max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">编辑文章</h1> <h1 class="text-2xl font-bold text-gray-800">编辑曲谱</h1>
<div class="flex gap-2"> <div class="flex gap-2">
<Button label="取消" icon="pi pi-times" severity="secondary" @click="cancelEdit" /> <Button label="取消" icon="pi pi-times" severity="secondary" @click="cancelEdit" />
<Button label="保存" icon="pi pi-check" severity="primary" @click="savePost" /> <Button label="保存" icon="pi pi-check" severity="primary" @click="savePost" />
@@ -316,7 +316,7 @@ onMounted(() => {
<div v-if="loading" class="flex flex-col items-center justify-center py-12"> <div v-if="loading" class="flex flex-col items-center justify-center py-12">
<ProgressSpinner style="width:50px;height:50px" /> <ProgressSpinner style="width:50px;height:50px" />
<span class="mt-4">加载文章数据...</span> <span class="mt-4">加载曲谱数据...</span>
</div> </div>
<div v-else> <div v-else>
@@ -359,7 +359,7 @@ onMounted(() => {
<!-- 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="text-red-500">{{ errors.title }}</small> <small v-if="errors.title" class="text-red-500">{{ errors.title }}</small>
</div> </div>
@@ -392,9 +392,9 @@ onMounted(() => {
<!-- Introduction --> <!-- Introduction -->
<div class="col-span-2"> <div class="col-span-2">
<label for="introduction" class="block text-sm font-medium text-gray-700 mb-1">文章介绍</label> <label for="introduction" class="block text-sm font-medium text-gray-700 mb-1">曲谱介绍</label>
<Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full" <Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full"
placeholder="输入文章介绍内容" /> placeholder="输入曲谱介绍内容" />
<small v-if="errors.introduction" class="p-error">{{ errors.introduction }}</small> <small v-if="errors.introduction" class="p-error">{{ errors.introduction }}</small>
</div> </div>

View File

@@ -35,7 +35,7 @@ const statusOptions = ref([
// Media types for filtering // Media types for filtering
const mediaTypeOptions = ref([ const mediaTypeOptions = ref([
{ name: '所有类型', value: null }, { name: '所有类型', value: null },
{ name: '文章', value: '文章' }, { name: '曲谱', value: '曲谱' },
{ name: '视频', value: '视频' }, { name: '视频', value: '视频' },
{ name: '音频', value: '音频' } { name: '音频', value: '音频' }
]); ]);
@@ -92,12 +92,12 @@ const confirmDelete = (post) => {
postService.deletePost(post.id) postService.deletePost(post.id)
.then(() => { .then(() => {
// toast success // toast success
toast.add({ severity: 'success', summary: '成功', detail: '文章已删除', life: 3000 }); toast.add({ severity: 'success', summary: '成功', detail: '曲谱已删除', life: 3000 });
fetchPosts(); fetchPosts();
}) })
.catch(error => { .catch(error => {
console.error('Delete error:', error); // Debug log console.error('Delete error:', error); // Debug log
toast.add({ severity: 'error', summary: '错误', detail: '删除文章失败', life: 3000 }); toast.add({ severity: 'error', summary: '错误', detail: '删除曲谱失败', life: 3000 });
}); });
} }
@@ -140,7 +140,7 @@ const fetchPosts = async () => {
total.value = response.data.total; total.value = response.data.total;
} catch (error) { } catch (error) {
console.error('Fetch error:', error); // Debug log console.error('Fetch error:', error); // Debug log
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 }); toast.add({ severity: 'error', summary: '错误', detail: '加载曲谱失败', life: 3000 });
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -173,11 +173,10 @@ onMounted(() => {
// Status badge severity mapping // Status badge severity mapping
const getBadgeSeverity = (status) => { const getBadgeSeverity = (status) => {
const map = { const map = {
'发布': 'success', '发布': 'success',
'草稿': 'warning', '草稿': 'secondary',
'已下架': 'danger'
}; };
return map[status] || 'info'; return map[status] || 'warn';
}; };
// Format price to display ¥ symbol // Format price to display ¥ symbol
@@ -299,7 +298,7 @@ const handleSendConfirm = async () => {
<Dialog v-model:visible="sendDialogVisible" modal header="选择用户" :style="{ width: '80vw' }"> <Dialog v-model:visible="sendDialogVisible" modal header="选择用户" :style="{ width: '80vw' }">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="mb-4"> <div class="mb-4">
<span class="font-bold">文章</span> <span class="font-bold">曲谱</span>
{{ selectedPost?.title }} {{ selectedPost?.title }}
</div> </div>
@@ -366,16 +365,16 @@ const handleSendConfirm = async () => {
<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>
<Button class="text-nowrap !px-8" icon="pi pi-plus" label="创建文章" severity="primary" <Button class="text-nowrap !px-8" icon="pi pi-plus" label="创建曲谱" severity="primary"
@click="navigateToCreatePost" /> @click="navigateToCreatePost" />
</div> </div>
<!-- Posts Table --> <!-- Posts Table -->
<div class="card mt-10"> <div class="card mt-10">
<div class="pb-10 flex"> <div class="pb-10 flex">
<InputText v-model="globalFilterValue" placeholder="搜索文章..." class="flex-1" @input="onSearch" /> <InputText v-model="globalFilterValue" placeholder="搜索曲谱..." class="flex-1" @input="onSearch" />
</div> </div>
<DataTable v-model:filters="filters" :value="posts" :paginator="true" :rows="rows" :totalRecords="total" <DataTable v-model:filters="filters" :value="posts" :paginator="true" :rows="rows" :totalRecords="total"
@@ -387,13 +386,13 @@ const handleSendConfirm = async () => {
removableSort class="p-datatable-sm" responsiveLayout="scroll"> removableSort class="p-datatable-sm" responsiveLayout="scroll">
<template #empty> <template #empty>
<div class="text-center p-4">未找到文章</div> <div class="text-center p-4">未找到曲谱</div>
</template> </template>
<template #loading> <template #loading>
<div class="flex flex-col items-center justify-center p-4"> <div class="flex flex-col items-center justify-center p-4">
<ProgressSpinner style="width:50px;height:50px" /> <ProgressSpinner style="width:50px;height:50px" />
<span class="mt-2">加载文章数据...</span> <span class="mt-2">加载曲谱数据...</span>
</div> </div>
</template> </template>
@@ -412,12 +411,12 @@ const handleSendConfirm = async () => {
<span class="text-orange-500">优惠: -{{ formatPrice(getDiscountAmount(data.price, <span class="text-orange-500">优惠: -{{ formatPrice(getDiscountAmount(data.price,
data.discount)) }}</span> data.discount)) }}</span>
<span class="font-bold">实付: {{ formatPrice(getFinalPrice(data.price, data.discount)) <span class="font-bold">实付: {{ formatPrice(getFinalPrice(data.price, data.discount))
}}</span> }}</span>
</div> </div>
</template> </template>
</Column> </Column>
<Column field="bought_count" header="购买数量" sortable> <Column field="bought_count" header="销售数量" sortable>
<template #body="{ data }"> <template #body="{ data }">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-gray-500">{{ data.bought_count }}</span> <span class="text-gray-500">{{ data.bought_count }}</span>

View File

@@ -113,9 +113,9 @@ const formatPrice = (price) => {
</div> </div>
</div> </div>
<!-- 用户购买的文章列表 --> <!-- 用户购买的曲谱列表 -->
<div class="card"> <div class="card">
<h3 class="text-xl font-semibold mb-4">购买的文章</h3> <h3 class="text-xl font-semibold mb-4">购买的曲谱</h3>
<DataTable :value="userArticles" stripedRows class="p-datatable-sm" responsiveLayout="scroll" <DataTable :value="userArticles" stripedRows class="p-datatable-sm" responsiveLayout="scroll"
:lazy="true" :totalRecords="totalArticles" :rows="lazyParams.limit" :loading="loading" :lazy="true" :totalRecords="totalArticles" :rows="lazyParams.limit" :loading="loading"
@page="onPage" paginator :rows-per-page-options="[10, 20, 50]"> @page="onPage" paginator :rows-per-page-options="[10, 20, 50]">

View File

@@ -72,7 +72,7 @@ onMounted(() => {
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
<div class="flex-none bg-white border-b border-gray-200 z-50 shadow"> <div class="flex-none bg-white border-b border-gray-200 z-50 shadow">
<div class="p-4"> <div class="p-4">
<input type="search" v-model="searchInput" @keyup="handleKeyup" placeholder="搜索文章" <input type="search" v-model="searchInput" @keyup="handleKeyup" placeholder="搜索"
class="w-full px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"> class="w-full px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
</div> </div>
</div> </div>
@@ -91,7 +91,7 @@ onMounted(() => {
</div> </div>
<div v-if="!hasMore && articles.length > 0" class="text-center text-gray-500 py-4"> <div v-if="!hasMore && articles.length > 0" class="text-center text-gray-500 py-4">
没有更多文章 没有更多了
</div> </div>
</div> </div>
</div> </div>