更新前端

This commit is contained in:
2026-03-29 23:45:26 +08:00
parent 7fa24a893b
commit c1d76de658
42 changed files with 615 additions and 147 deletions

4
.gitignore vendored
View File

@@ -9,4 +9,8 @@
/frontend/.vscode/
/frontend/.idea/
/开发路径.md
**/__pycache__/
**/*.pyc
**/logs/
**/logs/*.log

View File

@@ -9,8 +9,10 @@
"preview": "vite preview"
},
"dependencies": {
"@kangc/v-md-editor": "^2.3.18",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.6",
"highlight.js": "^11.10.0",
"naive-ui": "^2.44.1",
"pinia": "^3.0.4",
"vue": "^3.5.30",

View File

@@ -0,0 +1,14 @@
import { http } from './index'
import type { Category } from '@/types'
export const categoryApi = {
// 获取所有分类
getAll() {
return http.get<Category[]>('/categories')
},
// 获取单个分类
getDetail(id: string) {
return http.get<Category>(`/categories/${id}`)
},
}

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import VMdEditor from '@kangc/v-md-editor'
import '@kangc/v-md-editor/lib/style/style.css'
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js'
import '@kangc/v-md-editor/lib/theme/style/github.css'
import hljs from 'highlight.js'
// 配置主题
VMdEditor.use(githubTheme, {
hljs,
})
const props = defineProps<{
modelValue: string
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const content = ref(props.modelValue)
watch(() => props.modelValue, (newVal) => {
content.value = newVal
})
watch(content, (newVal) => {
emit('update:modelValue', newVal)
})
function handleInput(value: string) {
content.value = value
}
</script>
<template>
<div class="markdown-editor">
<VMdEditor
v-model="content"
:placeholder="placeholder || '开始撰写...'"
height="400px"
mode="edit"
@change="handleInput"
/>
</div>
</template>
<style scoped>
.markdown-editor :deep(.v-md-editor) {
border: 1px solid var(--gray-300, #d1d5db);
border-radius: 0.5rem;
}
.markdown-editor :deep(.v-md-editor--fullscreen) {
z-index: 1000;
}
</style>

10
frontend/src/types/v-md-editor.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
declare module '@kangc/v-md-editor' {
import { DefineComponent } from 'vue'
const VMdEditor: DefineComponent<any, any, any>
export default VMdEditor
}
declare module '@kangc/v-md-editor/lib/theme/github.js' {
const theme: any
export default theme
}

View File

@@ -1,115 +1,54 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import Navbar from '@/components/Navbar.vue'
import Hero from '@/components/Hero.vue'
import PostCard from '@/components/PostCard.vue'
import Sidebar from '@/components/Sidebar.vue'
import Footer from '@/components/Footer.vue'
import type { Post } from '@/types'
// 模拟文章数据 - 后端完成后替换为真实API调用
const posts = ref<Post[]>([
{
id: '1',
title: '《原神》4.2版本前瞻:芙宁娜技能演示与剧情预测',
slug: 'genshin-4-2-preview',
content: '',
summary: '4.2版本即将到来,让我们提前了解水神芙宁娜的技能机制以及可能的剧情发展...',
cover_image: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=800',
author: { id: '1', username: 'Miyako', email: '', is_active: true, created_at: '', updated_at: '' },
category: { id: '1', name: '游戏攻略', slug: 'game', created_at: '' },
tags: [{ id: '1', name: '原神', created_at: '' }, { id: '2', name: '攻略', created_at: '' }],
view_count: 5200,
status: 'published',
created_at: '2024-03-15T10:00:00Z',
updated_at: '2024-03-15T10:00:00Z'
},
{
id: '2',
title: '2024年必追的10部春季新番推荐',
slug: 'spring-2024-anime',
content: '',
summary: '春天到了又到了补番的季节本期为大家带来2024年春季最值得期待的新番动画...',
cover_image: 'https://images.unsplash.com/photo-1535016120720-40c6874c3b1c?w=800',
author: { id: '2', username: 'Akari', email: '', is_active: true, created_at: '', updated_at: '' },
category: { id: '2', name: '动漫资讯', slug: 'anime', created_at: '' },
tags: [{ id: '3', name: '新番', created_at: '' }, { id: '4', name: '推荐', created_at: '' }],
view_count: 3800,
status: 'published',
created_at: '2024-03-14T08:00:00Z',
updated_at: '2024-03-14T08:00:00Z'
},
{
id: '3',
title: '《崩坏星穹铁道》角色强度榜 - 3月版',
slug: 'honkai-star-rail-tier-list',
content: '',
summary: '版本强势角色有哪些本期强度榜为你详细分析当前版本的T0、T1角色...',
cover_image: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=800',
author: { id: '1', username: 'Miyako', email: '', is_active: true, created_at: '', updated_at: '' },
category: { id: '1', name: '游戏攻略', slug: 'game', created_at: '' },
tags: [{ id: '2', name: '崩坏星穹铁道', created_at: '' }, { id: '5', name: '强度榜', created_at: '' }],
view_count: 2900,
status: 'published',
created_at: '2024-03-13T15:30:00Z',
updated_at: '2024-03-13T15:30:00Z'
},
{
id: '4',
title: '手办入坑指南:从萌新到进阶的全方位攻略',
slug: 'figure-beginner-guide',
content: '',
summary: '想入手第一只手办但不知道如何选择?这篇指南将带你了解手办的各种分类、选购渠道...',
cover_image: 'https://images.unsplash.com/photo-1608889175123-8ee362201f81?w=800',
author: { id: '3', username: 'Sakura', email: '', is_active: true, created_at: '', updated_at: '' },
category: { id: '3', name: '二次元美图', slug: 'pictures', created_at: '' },
tags: [{ id: '6', name: '手办', created_at: '' }, { id: '7', name: '教程', created_at: '' }],
view_count: 2100,
status: 'published',
created_at: '2024-03-12T12:00:00Z',
updated_at: '2024-03-12T12:00:00Z'
},
{
id: '5',
title: '同人创作分享:用画笔描绘心中的二次元世界',
slug: 'fanart-sharing',
content: '',
summary: '本期收录了多位优秀画师的作品,让我们一起欣赏这些充满想象力的同人创作...',
cover_image: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800',
author: { id: '2', username: 'Akari', email: '', is_active: true, created_at: '', updated_at: '' },
category: { id: '4', name: '同人创作', slug: 'fanwork', created_at: '' },
tags: [{ id: '8', name: '同人', created_at: '' }, { id: '9', name: '绘画', created_at: '' }],
view_count: 1800,
status: 'published',
created_at: '2024-03-11T09:00:00Z',
updated_at: '2024-03-11T09:00:00Z'
},
{
id: '6',
title: '《约定的梦幻岛》最新话解析:剧情走向深度分析',
slug: 'neverland-analysis',
content: '',
summary: '最新一话的剧情信息量巨大,让我们来深度分析一下剧情走向和角色心理...',
cover_image: 'https://images.unsplash.com/photo-1518709268805-4e9042af9f23?w=800',
author: { id: '4', username: 'Ken', email: '', is_active: true, created_at: '', updated_at: '' },
category: { id: '2', name: '动漫资讯', slug: 'anime', created_at: '' },
tags: [{ id: '10', name: '约定的梦幻岛', created_at: '' }, { id: '11', name: '分析', created_at: '' }],
view_count: 1500,
status: 'published',
created_at: '2024-03-10T20:00:00Z',
updated_at: '2024-03-10T20:00:00Z'
}
])
const categories = [
{ name: '全部', slug: '' },
{ name: '动漫资讯', slug: 'anime' },
{ name: '游戏攻略', slug: 'game' },
{ name: '二次元美图', slug: 'pictures' },
{ name: '同人创作', slug: 'fanwork' },
]
import { postApi } from '@/api/post'
import { categoryApi } from '@/api/category'
import type { Post, Category } from '@/types'
const posts = ref<Post[]>([])
const categories = ref<Category[]>([{ id: '', name: '全部', slug: '', created_at: '' }])
const selectedCategory = ref('')
const loading = ref(false)
async function fetchPosts(categoryId?: string) {
loading.value = true
try {
const params: any = { page: 1, page_size: 20 }
if (categoryId) {
params.category_id = categoryId
}
const response = await postApi.getList(params)
posts.value = response.data.items.filter(p => p.status === 'published')
} catch (error) {
console.error('Failed to fetch posts:', error)
} finally {
loading.value = false
}
}
async function fetchCategories() {
try {
const response = await categoryApi.getAll()
categories.value = [{ id: '', name: '全部', slug: '', created_at: '' }, ...response.data]
} catch (error) {
console.error('Failed to fetch categories:', error)
}
}
function handleCategoryChange(slug: string) {
selectedCategory.value = slug
const catId = slug ? categories.value.find(c => c.slug === slug)?.id : ''
fetchPosts(catId)
}
onMounted(() => {
fetchCategories()
fetchPosts()
})
</script>
<template>
@@ -123,8 +62,8 @@ const selectedCategory = ref('')
<div class="category-buttons">
<button
v-for="cat in categories"
:key="cat.slug"
@click="selectedCategory = cat.slug"
:key="cat.id"
@click="handleCategoryChange(cat.slug)"
class="category-btn"
:class="{ active: selectedCategory === cat.slug }"
>
@@ -138,7 +77,9 @@ const selectedCategory = ref('')
<!-- 文章列表 -->
<section class="posts-section">
<h2 class="section-title">最新文章</h2>
<div class="posts-grid">
<div v-if="loading" class="text-center py-8 text-gray-500">加载中...</div>
<div v-else-if="posts.length === 0" class="text-center py-8 text-gray-500">暂无文章</div>
<div v-else class="posts-grid">
<PostCard v-for="post in posts" :key="post.id" :post="post" />
</div>
</section>

View File

@@ -1,25 +1,100 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { postApi } from '@/api/post'
import type { Post } from '@/types'
import { useMessage } from 'naive-ui'
import Navbar from '@/components/Navbar.vue'
import Footer from '@/components/Footer.vue'
const message = useMessage()
const route = useRoute()
const router = useRouter()
const postId = route.params.id as string
const post = ref<Post | null>(null)
const loading = ref(true)
async function fetchPost() {
loading.value = true
try {
const response = await postApi.getDetail(postId)
post.value = response.data
// 增加浏览量
postApi.incrementView(postId).catch(() => {})
} catch (error) {
message.error('文章不存在或已被删除')
router.push('/')
} finally {
loading.value = false
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
onMounted(() => {
fetchPost()
})
</script>
<template>
<div class="post-detail">
<nav class="glass fixed top-0 left-0 right-0 z-50">
<div class="max-w-4xl mx-auto px-4 py-4">
<RouterLink to="/" class="text-acg-pink hover:underline"> 返回首页</RouterLink>
</div>
</nav>
<div class="post-detail min-h-screen">
<Navbar />
<main class="pt-20 px-4 max-w-4xl mx-auto">
<div class="glass rounded-xl p-8">
<h1 class="text-3xl font-bold mb-4">文章详情 - {{ postId }}</h1>
<div class="text-gray-600 dark:text-gray-400">
文章内容加载中...
<main class="pt-24 px-4 pb-12 max-w-4xl mx-auto">
<div v-if="loading" class="glass rounded-xl p-8 text-center">
<div class="text-gray-500">加载中...</div>
</div>
<article v-else-if="post" class="glass rounded-xl overflow-hidden">
<!-- 封面图 -->
<div v-if="post.cover_image" class="w-full h-64 md:h-80 overflow-hidden">
<img :src="post.cover_image" :alt="post.title" class="w-full h-full object-cover" />
</div>
<div class="p-6 md:p-8">
<!-- 标题 -->
<h1 class="text-3xl md:text-4xl font-bold mb-4">{{ post.title }}</h1>
<!-- 元信息 -->
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-6 pb-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-2">
<img
v-if="post.author.avatar"
:src="post.author.avatar"
:alt="post.author.username"
class="w-8 h-8 rounded-full"
/>
<span class="text-acg-pink">{{ post.author.username }}</span>
</div>
<span>发布于 {{ formatDate(post.created_at) }}</span>
<span v-if="post.category">{{ post.category.name }}</span>
<span>阅读 {{ post.view_count }}</span>
</div>
<!-- 标签 -->
<div v-if="post.tags?.length" class="flex flex-wrap gap-2 mb-6">
<span
v-for="tag in post.tags"
:key="tag.id"
class="px-3 py-1 text-sm rounded-full bg-acg-pink/20 text-acg-pink"
>
{{ tag.name }}
</span>
</div>
<!-- 文章内容 -->
<div class="prose prose-lg max-w-none dark:prose-invert" v-html="post.content"></div>
</div>
</article>
</main>
<Footer />
</div>
</template>

View File

@@ -1,8 +1,250 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useMessage } from 'naive-ui'
import { postApi } from '@/api/post'
import type { Post, PostCreateRequest } from '@/types'
import Navbar from '@/components/Navbar.vue'
import Footer from '@/components/Footer.vue'
const message = useMessage()
const posts = ref<Post[]>([])
const loading = ref(false)
const showEditor = ref(false)
const isEditing = ref(false)
const currentPostId = ref<string | null>(null)
// 表单数据
const formData = ref<PostCreateRequest>({
title: '',
content: '',
summary: '',
cover_image: '',
category_id: '',
tags: [],
status: 'draft',
})
const searchParams = ref({
page: 1,
page_size: 20,
})
async function fetchPosts() {
loading.value = true
try {
const response = await postApi.getList(searchParams.value)
posts.value = response.data.items
} catch (error) {
message.error('获取文章列表失败')
} finally {
loading.value = false
}
}
function openEditor(post?: Post) {
if (post) {
isEditing.value = true
currentPostId.value = post.id
formData.value = {
title: post.title,
content: post.content,
summary: post.summary || '',
cover_image: post.cover_image || '',
category_id: post.category?.id || '',
tags: [],
status: post.status === 'archived' ? 'draft' : post.status,
}
} else {
isEditing.value = false
currentPostId.value = null
formData.value = {
title: '',
content: '',
summary: '',
cover_image: '',
category_id: '',
tags: [],
status: 'draft',
}
}
showEditor.value = true
}
function closeEditor() {
showEditor.value = false
isEditing.value = false
currentPostId.value = null
}
async function savePost() {
if (!formData.value.title || !formData.value.content) {
message.warning('请填写标题和内容')
return
}
try {
if (isEditing.value && currentPostId.value) {
await postApi.update(currentPostId.value, formData.value)
message.success('文章更新成功')
} else {
await postApi.create(formData.value)
message.success('文章创建成功')
}
closeEditor()
fetchPosts()
} catch (error: any) {
message.error(error.response?.data?.message || '保存失败')
}
}
async function deletePost(id: string) {
try {
await postApi.delete(id)
message.success('删除成功')
fetchPosts()
} catch (error) {
message.error('删除失败')
}
}
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('zh-CN')
}
onMounted(() => {
fetchPosts()
})
</script>
<template>
<div class="post-manage">
<h1 class="text-2xl font-bold mb-6">文章管理</h1>
<div class="glass rounded-xl p-6">
<p class="text-gray-600 dark:text-gray-400">文章管理功能开发中...</p>
<div class="post-manage min-h-screen">
<Navbar />
<main class="pt-24 px-4 pb-12 max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">文章管理</h1>
<button @click="openEditor()" class="btn-acg">
新建文章
</button>
</div>
<div class="glass rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-gray-100 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left text-sm font-medium">标题</th>
<th class="px-4 py-3 text-left text-sm font-medium">分类</th>
<th class="px-4 py-3 text-left text-sm font-medium">状态</th>
<th class="px-4 py-3 text-left text-sm font-medium">发布时间</th>
<th class="px-4 py-3 text-right text-sm font-medium">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="post in posts" :key="post.id" class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3">
<RouterLink :to="`/post/${post.id}`" class="text-acg-pink hover:underline">
{{ post.title }}
</RouterLink>
</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ post.category?.name || '-' }}</td>
<td class="px-4 py-3">
<span
:class="{
'bg-green-500/20 text-green-500': post.status === 'published',
'bg-yellow-500/20 text-yellow-500': post.status === 'draft',
'bg-gray-500/20 text-gray-500': post.status === 'archived',
}"
class="px-2 py-1 text-xs rounded-full"
>
{{ post.status === 'published' ? '已发布' : post.status === 'draft' ? '草稿' : '归档' }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-500">{{ formatDate(post.created_at) }}</td>
<td class="px-4 py-3 text-right">
<button @click="openEditor(post)" class="text-acg-pink hover:underline mr-4">编辑</button>
<button @click="deletePost(post.id)" class="text-red-500 hover:underline">删除</button>
</td>
</tr>
</tbody>
</table>
<div v-if="loading" class="p-8 text-center text-gray-500">加载中...</div>
<div v-if="!loading && posts.length === 0" class="p-8 text-center text-gray-500">暂无文章</div>
</div>
</main>
<!-- 编辑器弹窗 -->
<div v-if="showEditor" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div class="glass rounded-xl w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div class="p-6">
<h2 class="text-xl font-bold mb-6">{{ isEditing ? '编辑文章' : '新建文章' }}</h2>
<form @submit.prevent="savePost" class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">标题</label>
<input
v-model="formData.title"
type="text"
placeholder="文章标题"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
required
/>
</div>
<div>
<label class="block text-sm font-medium mb-2">摘要</label>
<textarea
v-model="formData.summary"
placeholder="文章摘要(可选)"
rows="2"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
></textarea>
</div>
<div>
<label class="block text-sm font-medium mb-2">封面图 URL</label>
<input
v-model="formData.cover_image"
type="url"
placeholder="https://..."
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
/>
</div>
<div>
<label class="block text-sm font-medium mb-2">内容 (Markdown)</label>
<textarea
v-model="formData.content"
placeholder="使用 Markdown 编写文章内容..."
rows="12"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 font-mono text-sm"
></textarea>
</div>
<div>
<label class="block text-sm font-medium mb-2">状态</label>
<select
v-model="formData.status"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800"
>
<option value="draft">草稿</option>
<option value="published">发布</option>
</select>
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" @click="closeEditor" class="px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800">
取消
</button>
<button type="submit" class="btn-acg">
保存
</button>
</div>
</form>
</div>
</div>
</div>
<Footer />
</div>
</template>

View File

@@ -1,15 +1,32 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/store/user'
import { useMessage } from 'naive-ui'
const router = useRouter()
const userStore = useUserStore()
const message = useMessage()
const email = ref('')
const password = ref('')
const loading = ref(false)
async function handleLogin() {
// TODO: 实现登录逻辑
console.log('Login:', email.value, password.value)
if (!email.value || !password.value) {
message.warning('请填写邮箱和密码')
return
}
loading.value = true
try {
await userStore.login(email.value, password.value)
message.success('登录成功')
router.push('/')
} catch (error: any) {
message.error(error.response?.data?.message || '登录失败,请检查邮箱和密码')
} finally {
loading.value = false
}
}
</script>
@@ -41,8 +58,8 @@ async function handleLogin() {
/>
</div>
<button type="submit" class="w-full btn-acg">
登录
<button type="submit" class="w-full btn-acg" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
</form>

View File

@@ -1,16 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { authApi } from '@/api/auth'
import { useMessage } from 'naive-ui'
const router = useRouter()
const message = useMessage()
const username = ref('')
const email = ref('')
const password = ref('')
const loading = ref(false)
async function handleRegister() {
// TODO: 实现注册逻辑
console.log('Register:', username.value, email.value, password.value)
if (!username.value || !email.value || !password.value) {
message.warning('请填写所有字段')
return
}
if (password.value.length < 6) {
message.warning('密码长度至少为6位')
return
}
loading.value = true
try {
await authApi.register({
username: username.value,
email: email.value,
password: password.value,
})
message.success('注册成功,请登录')
router.push('/login')
} catch (error: any) {
message.error(error.response?.data?.message || '注册失败')
} finally {
loading.value = false
}
}
</script>
@@ -53,8 +78,8 @@ async function handleRegister() {
/>
</div>
<button type="submit" class="w-full btn-acg">
注册
<button type="submit" class="w-full btn-acg" :disabled="loading">
{{ loading ? '注册中...' : '注册' }}
</button>
</form>

View File

@@ -1,28 +1,107 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/store/user'
import { authApi } from '@/api/auth'
import { useMessage, useDialog } from 'naive-ui'
import Navbar from '@/components/Navbar.vue'
import Footer from '@/components/Footer.vue'
const router = useRouter()
const userStore = useUserStore()
const message = useMessage()
const dialog = useDialog()
function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
async function handleLogout() {
try {
await dialog.confirm({
title: '提示',
content: '确定要退出登录吗?',
confirmButtonText: '确定',
cancelButtonText: '取消',
})
await authApi.logout()
} catch {
// 用户取消
}
userStore.logout()
message.success('已退出登录')
router.push('/')
}
onMounted(() => {
if (!userStore.isLoggedIn) {
message.warning('请先登录')
router.push('/login')
}
})
</script>
<template>
<div class="profile">
<nav class="glass fixed top-0 left-0 right-0 z-50">
<div class="max-w-4xl mx-auto px-4 py-4">
<RouterLink to="/" class="text-acg-pink hover:underline"> 返回首页</RouterLink>
</div>
</nav>
<div class="profile min-h-screen">
<Navbar />
<main class="pt-20 px-4 max-w-4xl mx-auto">
<div class="glass rounded-xl p-8">
<h1 class="text-2xl font-bold mb-4">个人中心</h1>
<div v-if="userStore.user">
<p>用户名: {{ userStore.user.username }}</p>
<p>邮箱: {{ userStore.user.email }}</p>
<main class="pt-24 px-4 pb-12 max-w-2xl mx-auto">
<div class="glass rounded-xl p-6 md:p-8">
<h1 class="text-2xl font-bold mb-6">个人中心</h1>
<div v-if="userStore.user" class="space-y-6">
<!-- 头像和基本信息 -->
<div class="flex items-center gap-6">
<div class="w-20 h-20 rounded-full overflow-hidden bg-acg-pink/20 flex items-center justify-center">
<img
v-if="userStore.user.avatar"
:src="userStore.user.avatar"
:alt="userStore.user.username"
class="w-full h-full object-cover"
/>
<span v-else class="text-3xl text-acg-pink">{{ userStore.user.username[0].toUpperCase() }}</span>
</div>
<div v-else>
<div>
<h2 class="text-xl font-bold">{{ userStore.user.username }}</h2>
<p class="text-gray-500 text-sm">加入于 {{ formatDate(userStore.user.created_at) }}</p>
</div>
</div>
<!-- 详细信息 -->
<div class="space-y-4 py-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex justify-between">
<span class="text-gray-500">邮箱</span>
<span>{{ userStore.user.email }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">状态</span>
<span :class="userStore.user.is_active ? 'text-green-500' : 'text-gray-400'">
{{ userStore.user.is_active ? '已激活' : '未激活' }}
</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex gap-4 pt-4">
<button
@click="handleLogout"
class="px-6 py-2 rounded-lg bg-red-500/20 text-red-500 hover:bg-red-500/30 transition-colors"
>
退出登录
</button>
</div>
</div>
<div v-else class="text-center text-gray-500 py-8">
加载中...
</div>
</div>
</main>
<Footer />
</div>
</template>