更新前端
This commit is contained in:
@@ -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">
|
||||
文章内容加载中...
|
||||
</div>
|
||||
<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)
|
||||
router.push('/')
|
||||
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)
|
||||
router.push('/login')
|
||||
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>
|
||||
<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>
|
||||
|
||||
<div v-else class="text-center text-gray-500 py-8">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user