feat: add edit page

This commit is contained in:
yanghao05
2025-04-01 19:52:35 +08:00
parent 6aa61a7497
commit 4918bcee16
5 changed files with 1130 additions and 1 deletions

View File

@@ -0,0 +1,268 @@
<script setup>
import { InputText } from 'primevue';
import Badge from 'primevue/badge';
import Button from 'primevue/button';
import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import Dropdown from 'primevue/dropdown';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
// Import useRouter for navigation
const router = useRouter();
const confirm = useConfirm();
const toast = useToast();
// State for edit dialog (removed "create" functionality since we now have a dedicated page)
const postDialog = ref(false);
const postDialogTitle = ref('编辑文章');
const editMode = ref(true); // Always true now since we only use dialog for editing
const currentPost = ref({
id: null,
title: '',
author: '',
thumbnail: '',
price: 0,
publishedAt: '',
status: '',
mediaTypes: [],
viewCount: 0
});
// Post statuses for filtering
const statusOptions = ref([
{ name: '所有状态', value: null },
{ name: '已发布', value: '已发布' },
{ name: '草稿', value: '草稿' },
{ name: '已下架', value: '已下架' }
]);
// Media types for filtering
const mediaTypeOptions = ref([
{ name: '所有类型', value: null },
{ name: '文章', value: '文章' },
{ name: '视频', value: '视频' },
{ name: '音频', value: '音频' }
]);
const selectedStatus = ref(statusOptions.value[0]);
const globalFilterValue = ref('');
const loading = ref(false);
// Sample data - in a real app, this would come from an API
const posts = ref([
{
id: 1,
title: '如何高效学习编程',
author: '张三',
thumbnail: 'https://via.placeholder.com/150',
price: 29.99,
publishedAt: '2023-06-15 14:30',
status: '已发布',
mediaTypes: ['文章', '视频'],
viewCount: 1254
},
{
id: 2,
title: '前端开发最佳实践',
author: '李四',
thumbnail: 'https://via.placeholder.com/150',
price: 49.99,
publishedAt: '2023-06-10 09:15',
status: '草稿',
mediaTypes: ['文章'],
viewCount: 789
},
{
id: 3,
title: '数据分析入门指南',
author: '王五',
thumbnail: 'https://via.placeholder.com/150',
price: 0.00,
publishedAt: '2023-06-05 16:45',
status: '已下架',
mediaTypes: ['文章', '音频'],
viewCount: 2567
}
]);
// Navigate to post creation page
const navigateToCreatePost = () => {
router.push('/posts/create');
};
// Navigate to post edit page
const navigateToEditPost = (post) => {
router.push(`/posts/edit/${post.id}`);
};
// View post details
const viewPost = (post) => {
// In a real app, this would navigate to a post detail page
toast.add({ severity: 'info', summary: '查看', detail: `查看文章: ${post.title}`, life: 3000 });
};
// Delete post
const confirmDelete = (post) => {
confirm.require({
message: `确定要删除 "${post.title}" 吗?`,
header: '确认删除',
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 });
}
});
};
// Fetch posts data
const fetchPosts = async () => {
loading.value = true;
try {
// In a real app, this would be an API call
// const response = await postApi.getPosts();
// posts.value = response.data;
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
// Using sample data already defined above
} catch (error) {
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchPosts();
});
// Status badge severity mapping
const getBadgeSeverity = (status) => {
const map = {
'已发布': 'success',
'草稿': 'warning',
'已下架': 'danger'
};
return map[status] || 'info';
};
// Format price to display ¥ symbol
const formatPrice = (price) => {
return `¥${price.toFixed(2)}`;
};
// Format media types to display as comma-separated string
const formatMediaTypes = (mediaTypes) => {
return mediaTypes.join(', ');
};
</script>
<template>
<Toast />
<ConfirmDialog />
<div class="w-full">
<div class="flex justify-between items-center mb-6 gap-4">
<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"
@click="navigateToCreatePost" />
</div>
<!-- Posts Table -->
<div class="card mt-10">
<div class="pb-10 flex">
<InputText v-model="globalFilterValue" placeholder="搜索文章..." class="flex-1" />
</div>
<DataTable v-model:filters="filters" :value="posts" :paginator="true" :rows="5"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rowsPerPageOptions="[5, 10, 25]"
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" :loading="loading" dataKey="id"
:globalFilterFields="['title', 'author', 'status', 'mediaTypes']"
:filters="{ global: { value: globalFilterValue, matchMode: 'contains' } }" stripedRows removableSort
class="p-datatable-sm" responsiveLayout="scroll">
<template #empty>
<div class="text-center p-4">未找到文章</div>
</template>
<template #loading>
<div class="flex flex-col items-center justify-center p-4">
<ProgressSpinner style="width:50px;height:50px" />
<span class="mt-2">加载文章数据...</span>
</div>
</template>
<Column field="title" header="标题" sortable>
<template #body="{ data }">
<div class="text-sm font-medium text-gray-900">
{{ data.title }}
</div>
</template>
</Column>
<Column field="price" header="价格" sortable>
<template #body="{ data }">
<div class="text-sm text-gray-900">{{ formatPrice(data.price) }}</div>
</template>
</Column>
<Column field="publishedAt" header="发布时间" sortable></Column>
<Column field="status" header="发布状态" sortable>
<template #body="{ data }">
<Badge :value="data.status" :severity="getBadgeSeverity(data.status)" />
</template>
<template #filter="{ filterModel, filterCallback }">
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="statusOptions"
optionLabel="name" optionValue="value" placeholder="所有状态" class="p-column-filter"
showClear />
</template>
</Column>
<Column field="mediaTypes" header="媒体类型" sortable>
<template #body="{ data }">
<div class="text-sm text-gray-900">{{ formatMediaTypes(data.mediaTypes) }}</div>
</template>
<template #filter="{ filterModel, filterCallback }">
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="mediaTypeOptions"
optionLabel="name" optionValue="value" placeholder="所有类型" class="p-column-filter"
showClear />
</template>
</Column>
<Column field="viewCount" header="观看次数" sortable>
<template #body="{ data }">
<div class="text-sm text-gray-500">{{ data.viewCount }}</div>
</template>
</Column>
<Column header="操作" :exportable="false" style="min-width:8rem">
<template #body="{ data }">
<div class="flex justify-center space-x-2">
<Button icon="pi pi-pencil" rounded text severity="info" @click="navigateToEditPost(data)"
aria-label="编辑" />
<Button icon="pi pi-eye" rounded text severity="secondary" @click="viewPost(data)"
aria-label="查看" />
<Button icon="pi pi-trash" rounded text severity="danger" @click="confirmDelete(data)"
aria-label="删除" />
</div>
</template>
</Column>
</DataTable>
</div>
</div>
</template>
<style scoped>
/* Additional styling if needed */
</style>