更新前端
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,4 +9,8 @@
|
||||
/frontend/.vscode/
|
||||
/frontend/.idea/
|
||||
/开发路径.md
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
**/logs/
|
||||
**/logs/*.log
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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",
|
||||
|
||||
14
frontend/src/api/category.ts
Normal file
14
frontend/src/api/category.ts
Normal 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}`)
|
||||
},
|
||||
}
|
||||
59
frontend/src/components/MarkdownEditor.vue
Normal file
59
frontend/src/components/MarkdownEditor.vue
Normal 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
10
frontend/src/types/v-md-editor.d.ts
vendored
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user