feat: update admin
This commit is contained in:
@@ -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')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user