Files
nl-blogs/client/src/pages/admin/Posts.vue

153 lines
6.5 KiB
Vue
Raw Normal View History

2026-01-15 13:51:44 +08:00
<template>
2026-01-16 11:06:59 +08:00
<div class="w-full animate-reveal">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-serif italic text-white">文章管理</h1>
<router-link to="/admin/posts/create" class="admin-btn-primary flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
新增文章
2026-01-15 13:51:44 +08:00
</router-link>
</div>
2026-01-16 11:06:59 +08:00
<div class="admin-card overflow-hidden">
<div class="overflow-x-auto">
<table class="admin-table">
<thead>
<tr>
<th class="w-20">ID</th>
<th>标题</th>
<th>分类</th>
<th>发布日期</th>
<th>状态</th>
<th class="text-right">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.id" class="group transition-colors duration-200">
<td class="font-mono text-xs text-white/40">#{{ post.id }}</td>
<td class="font-medium text-white group-hover:text-art-accent transition-colors">{{ post.title }}</td>
2026-01-16 17:03:34 +08:00
<td><span class="px-2 py-1 rounded bg-white/5 border border-white/5 text-xs text-art-muted">{{ post.categoryName || post.category?.name || '未分类' }}</span></td>
2026-01-16 11:06:59 +08:00
<td class="text-art-muted text-xs">{{ post.date }}</td>
<td>
2026-01-16 17:03:34 +08:00
<button
@click="handleToggleStatus(post)"
:class="['px-2 py-1 rounded-full text-xs font-medium border transition-all hover:opacity-80', post.isPublished === 1 ? 'bg-green-500/10 text-green-400 border-green-500/20' : 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20']"
title="点击切换状态"
>
2026-01-16 11:06:59 +08:00
{{ post.isPublished === 1 ? '已发布' : '草稿' }}
2026-01-16 17:03:34 +08:00
</button>
2026-01-16 11:06:59 +08:00
</td>
<td class="text-right">
<div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<router-link :to="`/admin/posts/${post.id}/edit`" class="admin-btn-secondary py-1 px-3 text-xs">
编辑
</router-link>
<button @click="deletePost(post.id)" class="admin-btn-danger py-1 px-3 text-xs">
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="posts.length === 0" class="p-16 text-center">
<div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl">📝</div>
<h3 class="text-white font-medium mb-2">暂无文章</h3>
<p class="text-art-muted text-sm mb-6">开始您的创作之旅吧</p>
<router-link to="/admin/posts/create" class="admin-btn-secondary inline-flex items-center gap-2">
+ 新增文章
</router-link>
</div>
2026-01-15 13:51:44 +08:00
</div>
2026-01-16 17:03:34 +08:00
<!-- Category Modal -->
<div v-if="isCategoryModalOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="closeCategoryModal">
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-md overflow-hidden animate-reveal">
<div class="p-4 border-b border-white/10 flex justify-between items-center">
<h3 class="text-lg font-serif italic text-white">修改文章分类</h3>
<button @click="closeCategoryModal" class="text-white/50 hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="p-6 space-y-4">
<p class="text-sm text-art-muted">正在修改文章: <span class="text-white">{{ editingPost?.title }}</span></p>
<div class="space-y-2">
<label class="block text-xs font-medium text-art-muted uppercase tracking-wider">选择分类</label>
<CustomSelect
v-model="selectedCategoryId"
:options="categories"
placeholder="请选择分类"
/>
</div>
</div>
<div class="p-4 border-t border-white/10 flex justify-end gap-3 bg-white/5">
<button @click="closeCategoryModal" class="px-4 py-2 text-sm text-white/70 hover:text-white transition-colors">取消</button>
<button @click="saveCategory" class="admin-btn-primary py-1.5 px-4 text-sm">保存</button>
</div>
</div>
</div>
2026-01-15 13:51:44 +08:00
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
2026-01-16 17:03:34 +08:00
import { getAdminPosts, deletePost as deletePostApi, Post, togglePostStatus } from '../../services/api'
2026-01-15 13:51:44 +08:00
import { useToast } from '../../composables/useToast'
const router = useRouter()
const toast = useToast()
const posts = ref<Post[]>([])
const fetchPosts = async () => {
try {
2026-01-16 17:03:34 +08:00
const response = await getAdminPosts()
// Handle PaginationResponse structure
if ('list' in response) {
posts.value = response.list
} else {
// Fallback if API returns array directly (legacy)
posts.value = response as any
}
2026-01-15 13:51:44 +08:00
} catch (error) {
console.error('Error fetching posts:', error)
2026-01-16 11:06:59 +08:00
toast.showToast('获取文章列表失败', 'error')
2026-01-15 13:51:44 +08:00
}
}
2026-01-16 17:03:34 +08:00
const handleToggleStatus = async (post: Post) => {
const newStatus = post.isPublished === 1 ? 0 : 1
try {
await togglePostStatus(post.id, newStatus)
post.isPublished = newStatus
toast.showToast(newStatus === 1 ? '文章已发布' : '文章已转为草稿', 'success')
} catch (error) {
console.error('Error toggling status:', error)
toast.showToast('更新状态失败', 'error')
}
}
2026-01-15 17:03:59 +08:00
const deletePost = async (id: number) => {
2026-01-15 13:51:44 +08:00
if (confirm('确定要删除这篇文章吗?')) {
try {
await deletePostApi(id)
2026-01-16 11:06:59 +08:00
toast.showToast('文章删除成功', 'success')
2026-01-15 13:51:44 +08:00
fetchPosts()
} catch (error) {
console.error('Error deleting post:', error)
2026-01-16 11:06:59 +08:00
toast.showToast('删除文章失败', 'error')
2026-01-15 13:51:44 +08:00
}
}
}
onMounted(() => {
fetchPosts()
})
</script>
<style scoped>
2026-01-16 11:06:59 +08:00
/* Scoped styles removed, using Tailwind classes */
</style>