From b7e9699dbd236972fc73a1fba41b2b5a264ff4f2 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Tue, 24 Mar 2026 14:08:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=89=8D=E7=AB=AF=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=92=8C=E5=B8=83=E5=B1=80=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/core/config.py | 0 backend/main.py | 2 +- frontend/.env.example | 2 + frontend/.gitignore | 24 + frontend/README.md | 5 + frontend/index.html | 13 + frontend/package.json | 34 + frontend/postcss.config.js | 6 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.vue | 15 + frontend/src/api/auth.ts | 29 + frontend/src/api/index.ts | 69 ++ frontend/src/api/post.ts | 34 + frontend/src/assets/hero.png | Bin 0 -> 44919 bytes frontend/src/assets/vite.svg | 1 + frontend/src/assets/vue.svg | 1 + frontend/src/components/Footer.vue | 227 +++++++ frontend/src/components/Hero.vue | 267 ++++++++ frontend/src/components/Navbar.vue | 179 ++++++ frontend/src/components/PostCard.vue | 251 ++++++++ frontend/src/components/Sidebar.vue | 321 ++++++++++ frontend/src/layouts/AdminLayout.vue | 49 ++ frontend/src/main.ts | 12 + frontend/src/router/index.ts | 99 +++ frontend/src/store/index.ts | 2 + frontend/src/store/theme.ts | 43 ++ frontend/src/store/user.ts | 64 ++ frontend/src/style.css | 12 + frontend/src/types/index.ts | 109 ++++ frontend/src/views/About.vue | 19 + frontend/src/views/Category.vue | 25 + frontend/src/views/Home.vue | 243 +++++++ frontend/src/views/NotFound.vue | 13 + frontend/src/views/PostDetail.vue | 25 + frontend/src/views/admin/CategoryManage.vue | 8 + frontend/src/views/admin/Dashboard.vue | 19 + frontend/src/views/admin/PostManage.vue | 8 + frontend/src/views/admin/TagManage.vue | 8 + frontend/src/views/auth/Login.vue | 55 ++ frontend/src/views/auth/Register.vue | 67 ++ frontend/src/views/user/Profile.vue | 28 + frontend/tailwind.config.js | 22 + frontend/tsconfig.app.json | 22 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 + frontend/vite.config.ts | 22 + readme.md | 10 +- 开发路径.md | 672 ++++++++++++++++++++ 49 files changed, 3188 insertions(+), 6 deletions(-) create mode 100644 backend/app/core/config.py create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/auth.ts create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/post.ts create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/assets/vue.svg create mode 100644 frontend/src/components/Footer.vue create mode 100644 frontend/src/components/Hero.vue create mode 100644 frontend/src/components/Navbar.vue create mode 100644 frontend/src/components/PostCard.vue create mode 100644 frontend/src/components/Sidebar.vue create mode 100644 frontend/src/layouts/AdminLayout.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/store/index.ts create mode 100644 frontend/src/store/theme.ts create mode 100644 frontend/src/store/user.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/views/About.vue create mode 100644 frontend/src/views/Category.vue create mode 100644 frontend/src/views/Home.vue create mode 100644 frontend/src/views/NotFound.vue create mode 100644 frontend/src/views/PostDetail.vue create mode 100644 frontend/src/views/admin/CategoryManage.vue create mode 100644 frontend/src/views/admin/Dashboard.vue create mode 100644 frontend/src/views/admin/PostManage.vue create mode 100644 frontend/src/views/admin/TagManage.vue create mode 100644 frontend/src/views/auth/Login.vue create mode 100644 frontend/src/views/auth/Register.vue create mode 100644 frontend/src/views/user/Profile.vue create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 开发路径.md diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/main.py b/backend/main.py index cdd6725..9149eef 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI - +from pydantic import BaseModel app = FastAPI() @app.get("/") def read_root(): diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..8ed17e0 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# API 基础地址 +VITE_API_BASE_URL=http://localhost:8000/api/v1 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2bcf4d4 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vueuse/core": "^14.2.1", + "axios": "^1.13.6", + "naive-ui": "^2.44.1", + "pinia": "^3.0.4", + "vue": "^3.5.30", + "vue-router": "^5.0.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.2.1", + "@tailwindcss/typography": "^0.5.19", + "@types/node": "^24.12.0", + "@vitejs/plugin-vue": "^6.0.5", + "@vue/tsconfig": "^0.9.0", + "autoprefixer": "^10.4.27", + "lightningcss": "^1.32.0", + "lightningcss-win32-x64-msvc": "^1.32.0", + "postcss": "^8.5.8", + "tailwindcss": "^4.2.1", + "typescript": "~5.9.3", + "vite": "^8.0.0", + "vue-tsc": "^3.2.5" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..1c87846 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..c7c8bcb --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..06ac2f0 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,29 @@ +import { http } from './index' +import type { UserLoginRequest, UserRegisterRequest, TokenResponse, User } from '@/types' + +export const authApi = { + // 用户登录 + login(data: UserLoginRequest) { + return http.post('/auth/login', data) + }, + + // 用户注册 + register(data: UserRegisterRequest) { + return http.post('/auth/register', data) + }, + + // 获取当前用户信息 + getCurrentUser() { + return http.get('/auth/me') + }, + + // 刷新 Token + refreshToken(refreshToken: string) { + return http.post('/auth/refresh', { refresh_token: refreshToken }) + }, + + // 登出 + logout() { + return http.post('/auth/logout') + }, +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..3355cd2 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,69 @@ +import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios' +import type { ApiResponse } from '@/types' + +// 创建 axios 实例 +const request: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// 请求拦截器 +request.interceptors.request.use( + (config: any) => { + // 从 localStorage 获取 token + const token = localStorage.getItem('access_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error: any) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +request.interceptors.response.use( + (response: AxiosResponse) => { + return response + }, + (error: any) => { + if (error.response) { + const { status } = error.response + + // 401 未授权,跳转登录页 + if (status === 401) { + localStorage.removeItem('access_token') + window.location.href = '/login' + } + + return Promise.reject(error.response.data) + } + + return Promise.reject(error) + } +) + +// 封装请求方法 +export const http = { + get(url: string, config?: AxiosRequestConfig): Promise> { + return request.get(url, config) + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + return request.post(url, data, config) + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + return request.put(url, data, config) + }, + + delete(url: string, config?: AxiosRequestConfig): Promise> { + return request.delete(url, config) + }, +} + +export default request diff --git a/frontend/src/api/post.ts b/frontend/src/api/post.ts new file mode 100644 index 0000000..7a6a085 --- /dev/null +++ b/frontend/src/api/post.ts @@ -0,0 +1,34 @@ +import { http } from './index' +import type { Post, PostCreateRequest, PostUpdateRequest, PostListResponse, PageParams } from '@/types' + +export const postApi = { + // 获取文章列表 + getList(params?: PageParams & { category_id?: string; tag_id?: string }) { + return http.get('/posts', { params }) + }, + + // 获取单篇文章 + getDetail(id: string) { + return http.get(`/posts/${id}`) + }, + + // 创建文章 + create(data: PostCreateRequest) { + return http.post('/posts', data) + }, + + // 更新文章 + update(id: string, data: PostUpdateRequest) { + return http.put(`/posts/${id}`, data) + }, + + // 删除文章 + delete(id: string) { + return http.delete(`/posts/${id}`) + }, + + // 增加浏览量 + incrementView(id: string) { + return http.post(`/posts/${id}/view`) + }, +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..cc51a3d20ad4bc961b596a6adfd686685cd84bb0 GIT binary patch literal 44919 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 JPDHLkV1myZcL)Fg literal 0 HcmV?d00001 diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Footer.vue b/frontend/src/components/Footer.vue new file mode 100644 index 0000000..de1e35b --- /dev/null +++ b/frontend/src/components/Footer.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/frontend/src/components/Hero.vue b/frontend/src/components/Hero.vue new file mode 100644 index 0000000..f4c8290 --- /dev/null +++ b/frontend/src/components/Hero.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/frontend/src/components/Navbar.vue b/frontend/src/components/Navbar.vue new file mode 100644 index 0000000..eb179f6 --- /dev/null +++ b/frontend/src/components/Navbar.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/frontend/src/components/PostCard.vue b/frontend/src/components/PostCard.vue new file mode 100644 index 0000000..f9b3f5d --- /dev/null +++ b/frontend/src/components/PostCard.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue new file mode 100644 index 0000000..7ebac98 --- /dev/null +++ b/frontend/src/components/Sidebar.vue @@ -0,0 +1,321 @@ + + + + + diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..4f79b49 --- /dev/null +++ b/frontend/src/layouts/AdminLayout.vue @@ -0,0 +1,49 @@ + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..9279b89 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +// import naive-ui (按需引入,后续按需配置) + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..7eb8025 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,99 @@ +import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' +import { useUserStore } from '@/store/user' + +const routes: RouteRecordRaw[] = [ + { + path: '/', + name: 'Home', + component: () => import('@/views/Home.vue'), + }, + { + path: '/post/:id', + name: 'PostDetail', + component: () => import('@/views/PostDetail.vue'), + }, + { + path: '/category/:id', + name: 'Category', + component: () => import('@/views/Category.vue'), + }, + { + path: '/about', + name: 'About', + component: () => import('@/views/About.vue'), + }, + { + path: '/login', + name: 'Login', + component: () => import('@/views/auth/Login.vue'), + }, + { + path: '/register', + name: 'Register', + component: () => import('@/views/auth/Register.vue'), + }, + { + path: '/profile', + name: 'Profile', + component: () => import('@/views/user/Profile.vue'), + meta: { requiresAuth: true }, + }, + { + path: '/admin', + name: 'Admin', + component: () => import('@/layouts/AdminLayout.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + children: [ + { + path: '', + redirect: '/admin/dashboard', + }, + { + path: 'dashboard', + name: 'AdminDashboard', + component: () => import('@/views/admin/Dashboard.vue'), + }, + { + path: 'posts', + name: 'AdminPosts', + component: () => import('@/views/admin/PostManage.vue'), + }, + { + path: 'categories', + name: 'AdminCategories', + component: () => import('@/views/admin/CategoryManage.vue'), + }, + { + path: 'tags', + name: 'AdminTags', + component: () => import('@/views/admin/TagManage.vue'), + }, + ], + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/NotFound.vue'), + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// 路由守卫 +router.beforeEach((to, _from, next) => { + const userStore = useUserStore() + const token = localStorage.getItem('access_token') + + if (to.meta.requiresAuth && !token) { + next({ name: 'Login' }) + } else if (to.meta.requiresAdmin && userStore.user?.is_active !== true) { + next({ name: 'Home' }) + } else { + next() + } +}) + +export default router diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts new file mode 100644 index 0000000..88d657a --- /dev/null +++ b/frontend/src/store/index.ts @@ -0,0 +1,2 @@ +export { useUserStore } from './user' +export { useThemeStore } from './theme' diff --git a/frontend/src/store/theme.ts b/frontend/src/store/theme.ts new file mode 100644 index 0000000..acd1acd --- /dev/null +++ b/frontend/src/store/theme.ts @@ -0,0 +1,43 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +type Theme = 'light' | 'dark' | 'auto' + +export const useThemeStore = defineStore('theme', () => { + // 状态 + const theme = ref((localStorage.getItem('theme') as Theme) || 'light') + + // Actions + function setTheme(newTheme: Theme) { + theme.value = newTheme + localStorage.setItem('theme', newTheme) + applyTheme(newTheme) + } + + function applyTheme(t: Theme) { + const html = document.documentElement + + if (t === 'auto') { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + html.classList.toggle('dark', prefersDark) + } else { + html.classList.toggle('dark', t === 'dark') + } + } + + function toggleTheme() { + const themes: Theme[] = ['light', 'dark', 'auto'] + const currentIndex = themes.indexOf(theme.value) + const nextTheme = themes[(currentIndex + 1) % themes.length] + setTheme(nextTheme) + } + + // 初始化主题 + applyTheme(theme.value) + + return { + theme, + setTheme, + toggleTheme, + } +}) diff --git a/frontend/src/store/user.ts b/frontend/src/store/user.ts new file mode 100644 index 0000000..b7816d5 --- /dev/null +++ b/frontend/src/store/user.ts @@ -0,0 +1,64 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { User } from '@/types' +import { authApi } from '@/api/auth' + +export const useUserStore = defineStore('user', () => { + // 状态 + const user = ref(null) + const token = ref(localStorage.getItem('access_token')) + + // 计算属性 + const isLoggedIn = computed(() => !!token.value) + const isAdmin = computed(() => user.value?.is_active === true) + + // Actions + function setToken(newToken: string | null) { + token.value = newToken + if (newToken) { + localStorage.setItem('access_token', newToken) + } else { + localStorage.removeItem('access_token') + } + } + + function setUser(newUser: User | null) { + user.value = newUser + } + + async function fetchUser() { + if (!token.value) return + + try { + const response = await authApi.getCurrentUser() + setUser(response.data) + } catch (error) { + console.error('Failed to fetch user:', error) + setToken(null) + setUser(null) + } + } + + async function login(email: string, password: string) { + const response = await authApi.login({ email, password }) + setToken(response.data.access_token) + await fetchUser() + } + + function logout() { + setToken(null) + setUser(null) + } + + return { + user, + token, + isLoggedIn, + isAdmin, + setToken, + setUser, + fetchUser, + login, + logout, + } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..be7f557 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,12 @@ +@import "tailwindcss"; + +/* 全局样式 */ +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +* { + box-sizing: border-box; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..41f8d5f --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,109 @@ +// 用户相关类型 +export interface User { + id: string + username: string + email: string + avatar?: string + is_active: boolean + created_at: string + updated_at: string +} + +export interface UserLoginRequest { + email: string + password: string +} + +export interface UserRegisterRequest { + username: string + email: string + password: string +} + +export interface TokenResponse { + access_token: string + token_type: string +} + +// 文章相关类型 +export interface Post { + id: string + title: string + slug: string + content: string + summary?: string + cover_image?: string + author: User + category?: Category + tags: Tag[] + view_count: number + status: 'draft' | 'published' | 'archived' + created_at: string + updated_at: string +} + +export interface PostCreateRequest { + title: string + content: string + summary?: string + cover_image?: string + category_id?: string + tags?: string[] + status?: 'draft' | 'published' +} + +export interface PostUpdateRequest { + title?: string + content?: string + summary?: string + cover_image?: string + category_id?: string + tags?: string[] + status?: 'draft' | 'published' | 'archived' +} + +export interface PostListResponse { + items: Post[] + total: number + page: number + page_size: number +} + +// 分类相关类型 +export interface Category { + id: string + name: string + slug: string + description?: string + created_at: string +} + +// 标签相关类型 +export interface Tag { + id: string + name: string + created_at: string +} + +// 评论相关类型 +export interface Comment { + id: string + content: string + author?: User + created_at: string + parent_id?: string + replies?: Comment[] +} + +// API 响应类型 +export interface ApiResponse { + code: number + message: string + data: T +} + +// 分页请求参数 +export interface PageParams { + page?: number + page_size?: number +} diff --git a/frontend/src/views/About.vue b/frontend/src/views/About.vue new file mode 100644 index 0000000..28d9704 --- /dev/null +++ b/frontend/src/views/About.vue @@ -0,0 +1,19 @@ + diff --git a/frontend/src/views/Category.vue b/frontend/src/views/Category.vue new file mode 100644 index 0000000..83892c5 --- /dev/null +++ b/frontend/src/views/Category.vue @@ -0,0 +1,25 @@ + + + diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..a7f3e23 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..51b0b0f --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,13 @@ + + + diff --git a/frontend/src/views/PostDetail.vue b/frontend/src/views/PostDetail.vue new file mode 100644 index 0000000..b48e7e5 --- /dev/null +++ b/frontend/src/views/PostDetail.vue @@ -0,0 +1,25 @@ + + + diff --git a/frontend/src/views/admin/CategoryManage.vue b/frontend/src/views/admin/CategoryManage.vue new file mode 100644 index 0000000..2690663 --- /dev/null +++ b/frontend/src/views/admin/CategoryManage.vue @@ -0,0 +1,8 @@ + diff --git a/frontend/src/views/admin/Dashboard.vue b/frontend/src/views/admin/Dashboard.vue new file mode 100644 index 0000000..1b6365c --- /dev/null +++ b/frontend/src/views/admin/Dashboard.vue @@ -0,0 +1,19 @@ + diff --git a/frontend/src/views/admin/PostManage.vue b/frontend/src/views/admin/PostManage.vue new file mode 100644 index 0000000..3b03f6a --- /dev/null +++ b/frontend/src/views/admin/PostManage.vue @@ -0,0 +1,8 @@ + diff --git a/frontend/src/views/admin/TagManage.vue b/frontend/src/views/admin/TagManage.vue new file mode 100644 index 0000000..3f95e3a --- /dev/null +++ b/frontend/src/views/admin/TagManage.vue @@ -0,0 +1,8 @@ + diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue new file mode 100644 index 0000000..cb9f09c --- /dev/null +++ b/frontend/src/views/auth/Login.vue @@ -0,0 +1,55 @@ + + + diff --git a/frontend/src/views/auth/Register.vue b/frontend/src/views/auth/Register.vue new file mode 100644 index 0000000..6e4a3fc --- /dev/null +++ b/frontend/src/views/auth/Register.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/views/user/Profile.vue b/frontend/src/views/user/Profile.vue new file mode 100644 index 0000000..cd022e6 --- /dev/null +++ b/frontend/src/views/user/Profile.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..3bd3c70 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,22 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + // 二次元风格配色 + 'acg-pink': '#FFB7C5', + 'acg-pink-light': '#FFE4E9', + 'acg-blue': '#A8D8EA', + 'acg-purple': '#D4B5E6', + 'acg-yellow': '#FFF4BD', + }, + }, + }, + plugins: [ + require('@tailwindcss/typography'), + ], +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..c2a267a --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Path Alias */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..048f74c --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/readme.md b/readme.md index ea20820..fbc4b5f 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ - **项目名称**:ACG Blog(二次元风格博客) - **项目简介**:基于 **FastAPI** 与 **Vue 3** 的前后端分离博客系统,主打二次元视觉体验与高性能响应。支持 Markdown 文章发布、动态看板娘、访问量统计、热搜排行、深色模式切换等特色功能。 - **技术栈概览**: - - 后端:Python + FastAPI + PostgreSQL + Tortoise‑ORM + Redis + JWT + - 后端:Python + FastAPI + SupaBase + Tortoise‑ORM + Redis + JWT - 前端:Vue 3 (Vite) + Pinia + Naive UI + Tailwind CSS + GSAP - **系统架构**:**B/S 架构**(Browser/Server,浏览器/服务器架构) - 前端:单页应用(SPA)运行于浏览器 @@ -28,7 +28,7 @@ | **模块** | **技术选型** | **说明** | | -------------- | --------------------- | ------------------------------------------------------------ | | **核心框架** | **FastAPI** | 高性能、原生异步(Async),满足二次元素材(图片/视频)的高并发加载需求。 | -| **数据库** | **PostgreSQL** | 关系型数据库首选,对 JSONB 的支持非常适合存储灵活的文章元数据。 | +| **数据库** | **SupaBace** | **内置 Auth**,支持第三方登录 (Github/Google) | | **ORM (异步)** | **Tortoise-ORM** | 语法类似 Django,且原生支持异步操作。 | | **缓存/任务** | **Redis** | 用于文章点击量统计、热搜排行以及 Session 存储。 | | **认证** | **JWT (python-jose)** | 无状态认证,方便前后端分离部署。 | @@ -99,7 +99,7 @@ uvicorn[standard]==0.27.1 tortoise-orm==0.20.0 aerich==0.7.2 # 数据库迁移工具 asyncpg==0.29.0 # PostgreSQL 驱动 -aioredis==2.0.1 # Redis 驱动 (用于缓存访问量) +aioredis==2.0.1 # Redis 驱动 # 日志系统 loguru==0.7.2 # 极简且强大的异步日志处理 @@ -113,8 +113,8 @@ passlib[bcrypt]==1.7.4 # 密码哈希 # 业务 python-multipart==0.0.9 # 处理表单与文件上传 mistune==3.0.2 # 快速 Markdown 解析 -pillow==10.2.0 # 处理图片 (ACG 博客需要自动缩略图) -httpx==0.27.0 # 异步 HTTP 请求 (爬取番剧信息等) +pillow==10.2.0 # 处理图片 +httpx==0.27.0 # 异步 HTTP 请求 # 开发与部署 python-dotenv==1.0.1 diff --git a/开发路径.md b/开发路径.md new file mode 100644 index 0000000..7e77143 --- /dev/null +++ b/开发路径.md @@ -0,0 +1,672 @@ +# ACG Blog 开发路径 + +> 二次元风格博客系统 - 分阶段开发计划 +> +> **最后更新**:2026-03-24 +> +> **当前状态**:前端基础搭建完成 ✅ | 后端开发未开始 ❌ + +--- + +## 系统架构设计 + +### 整体架构图 + +```mermaid +graph TB + subgraph Frontend["前端 (Vue 3 + Vite)"] + FE_Vue[Vue 3 应用] + FE_Router[Vue Router] + FE_Pinia[Pinia 状态管理] + FE_UI[Naive UI 组件库] + FE_CSS[Tailwind CSS] + FE_GSAP[GSAP 动画] + FE_Editor[V-MD-Editor] + end + + subgraph Backend["后端 (FastAPI)"] + BE_API[API 路由层] + BE_Service[Service 服务层] + BE_Crud[CRUD 数据操作层] + BE_Schema[Schema 数据校验层] + BE_Core[Core 核心配置层] + end + + subgraph BaaS["Supabase (BaaS)"] + SB_Auth[Auth 认证] + SB_DB[(PostgreSQL)] + SB_Storage[Storage 存储] + SB_Realtime[Realtime 实时] + end + + subgraph Cache["Redis 缓存"] + RC_Stats[访问统计] + RC_Hot[热搜排行] + RC_Session[会话存储] + end + + FE_Vue --> |HTTP/JSON| BE_API + FE_Router --> FE_Vue + FE_Pinia --> FE_Vue + FE_GSAP --> FE_Vue + FE_Editor --> FE_Vue + BE_API --> BE_Service + BE_Service --> BE_Crud + BE_Crud --> BE_Schema + BE_Schema --> BE_Core + BE_Core --> SB_Auth + BE_Core --> SB_DB + BE_Core --> SB_Storage + BE_Service --> RC_Stats + BE_Service --> RC_Hot + BE_Service --> RC_Session +``` + +### 前端模块架构 + +```mermaid +graph TB + subgraph Views["页面视图"] + Home[首页 Home.vue] + PostDetail[文章详情 PostDetail.vue] + Category[分类页 Category.vue] + Archive[归档页 Archive.vue] + About[关于页 About.vue] + end + + subgraph Auth["认证模块"] + Login[登录 Login.vue] + Register[注册 Register.vue] + Profile[个人资料 Profile.vue] + end + + subgraph Admin["管理后台"] + Dashboard[仪表盘 Dashboard.vue] + PostManage[文章管理 PostManage.vue] + CategoryManage[分类管理 CategoryManage.vue] + TagManage[标签管理 TagManage.vue] + end + + subgraph Components["公共组件"] + Navbar[导航栏 Navbar.vue] + Hero[Hero 区域 Hero.vue] + Footer[页脚 Footer.vue] + PostCard[文章卡片 PostCard.vue] + Sidebar[侧边栏 Sidebar.vue] + Kanban[看板娘 Kanban.vue] + end + + subgraph Store["状态管理"] + UserStore[user.ts 用户状态] + ThemeStore[theme.ts 主题状态] + KanbanStore[kanban.ts 看板娘状态] + end + + subgraph API["接口层"] + AuthAPI[auth.ts 认证接口] + PostAPI[post.ts 文章接口] + IndexAPI[index.ts 基础配置] + end + + Navbar --> Home + Hero --> Home + PostCard --> Home + Sidebar --> Home + Footer --> Views + Home --> PostDetail + Home --> Category + Login --> Auth + Register --> Auth + Profile --> Auth + Dashboard --> Admin + PostManage --> Admin + UserStore --> Store + ThemeStore --> Store + AuthAPI --> API + PostAPI --> API +``` + +### 后端模块架构 + +```mermaid +graph TB + subgraph API["API 路由层 api/endpoints/"] + AuthAPI[auth.py 认证接口] + UserAPI[users.py 用户接口] + PostAPI[posts.py 文章接口] + CategoryAPI[categories.py 分类接口] + TagAPI[tags.py 标签接口] + CommentAPI[comments.py 评论接口] + KanbanAPI[kanban.py 看板娘接口] + StatsAPI[stats.py 统计接口] + UploadAPI[upload.py 上传接口] + end + + subgraph Service["服务层 services/"] + AuthService[auth_service.py] + PostService[post_service.py] + KanbanService[kanban_service.py] + StatsService[stats_service.py] + UploadService[upload_service.py] + end + + subgraph Crud["CRUD 层 crud/"] + UserCrud[user.py] + PostCrud[post.py] + CategoryCrud[category.py] + TagCrud[tag.py] + CommentCrud[comment.py] + end + + subgraph Schema["Schema 层 schemas/"] + UserSchema[user.py] + PostSchema[post.py] + AuthSchema[auth.py] + TokenSchema[token.py] + end + + subgraph Core["核心层 core/"] + Config[config.py 配置] + Security[security.py JWT/密码] + Logger[logger.py 日志] + Supabase[supabase_client.py] + end + + AuthAPI --> AuthService + PostAPI --> PostService + KanbanAPI --> KanbanService + AuthService --> UserCrud + PostService --> PostCrud + UserCrud --> UserSchema + PostCrud --> PostSchema + UserSchema --> Supabase + PostSchema --> Supabase + UserCrud --> Supabase + PostCrud --> Supabase + AuthService --> Security + Core --> Config +``` + +### 数据库模型关系图 (Supabase PostgreSQL) + +```mermaid +erDiagram + USER ||--o{ POST : writes + USER ||--o{ COMMENT : writes + POST ||--o{ COMMENT : has + POST }o--o{ TAG : tagged_with + POST ||--|{ CATEGORY : belongs_to + USER { + uuid id PK + string username + string email + string avatar_url + boolean is_active + boolean is_superuser + datetime created_at + datetime updated_at + } + POST { + uuid id PK + string title + string slug + text content + text summary + string cover_url + uuid author_id FK + uuid category_id FK + integer view_count + string status + datetime created_at + datetime updated_at + } + CATEGORY { + uuid id PK + string name + string slug + text description + datetime created_at + } + TAG { + uuid id PK + string name + datetime created_at + } + COMMENT { + uuid id PK + text content + uuid author_id FK + uuid post_id FK + uuid parent_id + datetime created_at + } + POST_TAG { + uuid post_id FK + uuid tag_id FK + } +``` + +### 数据流架构图 + +```mermaid +sequenceDiagram + participant U as 用户 + participant FE as 前端 Vue + participant API as FastAPI 后端 + participant S as Service + participant SB as Supabase + participant R as Redis + + U->>FE: 访问页面 + FE->>API: HTTP Request + API->>S: 业务逻辑处理 + + Note over S,SB: 数据库操作 + S->>SB: Supabase SDK 调用 + SB-->>S: 查询结果 + + Note over S,R: 缓存操作 + S->>R: 检查/写入缓存 + R-->>S: 缓存数据 + + S-->>FE: JSON 响应 + FE-->>U: 渲染页面 +``` + +--- + +## 开发阶段 + +### 阶段一:后端基础架构搭建 + +**优先级:P0** | **状态:❌ 未开始** + +#### 1.1 核心配置层 +- [ ] `core/config.py` - 环境变量与全局配置(Pydantic Settings) +- [ ] `core/supabase.py` - Supabase 客户端初始化 +- [ ] `core/security.py` - JWT 生成、验证与密码哈希(python-jose + passlib) +- [ ] `core/logger.py` - Loguru 日志配置 +- [ ] `.env.example` - 环境变量模板 + +#### 1.2 数据库表结构 (Supabase PostgreSQL) +- [ ] 创建 profiles 表(用户扩展信息) +- [ ] 创建 posts 表(文章) +- [ ] 创建 categories 表(分类) +- [ ] 创建 tags 表(标签) +- [ ] 创建 comments 表(评论) +- [ ] 创建 post_tags 表(文章-标签关联) +- [ ] 配置 RLS (Row Level Security) 策略 +- [ ] 设计数据库索引 + +#### 1.3 Pydantic Schema 层 +- [ ] `schemas/user.py` - 用户数据验证 +- [ ] `schemas/post.py` - 文章数据验证 +- [ ] `schemas/auth.py` - 认证相关 Schema +- [ ] `schemas/token.py` - Token Schema + +#### 1.4 CRUD 操作层 +- [ ] `crud/user.py` - 用户 CRUD(通过 Supabase SDK) +- [ ] `crud/post.py` - 文章 CRUD +- [ ] `crud/category.py` - 分类 CRUD +- [ ] `crud/tag.py` - 标签 CRUD +- [ ] `crud/comment.py` - 评论 CRUD + +--- + +### 阶段二:认证与用户模块 + +**优先级:P0** | **状态:❌ 未开始** + +#### 2.1 认证接口 +- [ ] `api/endpoints/auth.py` + - [ ] POST `/api/v1/auth/register` - 用户注册 + - [ ] POST `/api/v1/auth/login` - 用户登录 + - [ ] POST `/api/v1/auth/refresh` - 刷新 Token + - [ ] POST `/api/v1/auth/logout` - 用户登出 + +#### 2.2 用户管理接口 +- [ ] `api/endpoints/users.py` + - [ ] GET `/api/v1/users/me` - 获取当前用户信息 + - [ ] PUT `/api/v1/users/me` - 更新当前用户信息 + - [ ] PUT `/api/v1/users/me/password` - 修改密码 + +#### 2.3 依赖注入与中间件 +- [ ] `api/deps.py` - 依赖注入(获取当前用户等) +- [ ] JWT 认证中间件 + +--- + +### 阶段三:文章管理模块 + +**优先级:P1** | **状态:❌ 未开始** + +#### 3.1 文章接口 +- [ ] `api/endpoints/posts.py` + - [ ] GET `/api/v1/posts/` - 文章列表(分页) + - [ ] GET `/api/v1/posts/{post_id}` - 获取单篇文章 + - [ ] POST `/api/v1/posts/` - 创建文章(需认证) + - [ ] PUT `/api/v1/posts/{post_id}` - 更新文章 + - [ ] DELETE `/api/v1/posts/{post_id}` - 删除文章 + - [ ] POST `/api/v1/posts/{post_id}/view` - 增加浏览量 + +#### 3.2 分类与标签接口 +- [ ] `api/endpoints/categories.py` + - [ ] GET `/api/v1/categories/` - 分类列表 + - [ ] POST `/api/v1/categories/` - 创建分类(管理员) +- [ ] `api/endpoints/tags.py` + - [ ] GET `/api/v1/tags/` - 标签列表 + - [ ] POST `/api/v1/tags/` - 创建标签(管理员) + +#### 3.3 评论接口 +- [ ] `api/endpoints/comments.py` + - [ ] GET `/api/v1/posts/{post_id}/comments` - 获取文章评论 + - [ ] POST `/api/v1/posts/{post_id}/comments` - 发表评论 + - [ ] DELETE `/api/v1/comments/{comment_id}` - 删除评论 + +--- + +### 阶段四:特色功能开发 + +**优先级:P3** | **状态:❌ 未开始** + +#### 4.1 看板娘系统 +- [ ] `services/kanban_service.py` - 看板娘互动逻辑 +- [ ] `api/endpoints/kanban.py` + - [ ] GET `/api/v1/kanban/greeting` - 随机问候语 + - [ ] POST `/api/v1/kanban/mood` - 切换看板娘心情 + - [ ] GET `/api/v1/kanban/stats` - 看板娘统计信息 + +#### 4.2 统计与热搜 +- [ ] Redis 缓存集成 +- [ ] `services/stats_service.py` - 访问统计逻辑 +- [ ] `api/endpoints/stats.py` + - [ ] GET `/api/v1/stats/hot-posts` - 热搜文章排行 + - [ ] GET `/api/v1/stats/visits` - 访问量统计 + - [ ] GET `/api/v1/stats/archive` - 归档统计 + +#### 4.3 文件上传 +- [ ] `services/upload_service.py` - 图片上传处理 +- [ ] `api/endpoints/upload.py` + - [ ] POST `/api/v1/upload/image` - 上传图片 + - [ ] 图片压缩与缩略图生成(Pillow) + +--- + +### 阶段五:前端项目初始化 + +**优先级:P1** | **状态:✅ 已完成** + +#### 5.1 基础搭建 ✅ +- [x] Vite + Vue 3 项目初始化 +- [x] 配置 Tailwind CSS v4 +- [x] 配置 Naive UI 主题(二次元配色) +- [x] 配置路由(Vue Router) + +#### 5.2 状态管理 ✅ +- [x] Pinia Store 结构设计 +- [x] `store/user.ts` - 用户状态 +- [x] `store/theme.ts` - 主题状态 +- [ ] `store/kanban.ts` - 看板娘状态 + +#### 5.3 API 封装 ✅ +- [x] Axios 基础配置 +- [x] 请求/响应拦截器(JWT 处理) +- [x] API 模块化封装 + +--- + +### 阶段六:前端核心页面 + +**优先级:P2** | **状态:⚠️ 部分完成** + +#### 6.1 布局组件 ✅ +- [x] `layouts/AdminLayout.vue` - 管理后台布局 +- [x] `components/Navbar.vue` - 导航栏 +- [x] `components/Footer.vue` - 页脚 +- [x] `components/Hero.vue` - Hero 区域 + +#### 6.2 公共组件 ⚠️ +- [ ] `components/Kanban.vue` - Live2D 看板娘 +- [x] `components/PostCard.vue` - 文章卡片 +- [ ] `components/MarkdownEditor.vue` - Markdown 编辑器 +- [x] `components/Sidebar.vue` - 侧边栏 + +#### 6.3 核心页面 ⚠️ +- [x] `views/Home.vue` - 首页(文章列表) +- [x] `views/PostDetail.vue` - 文章详情(占位) +- [x] `views/Category.vue` - 分类页(占位) +- [ ] `views/Archive.vue` - 归档页 +- [x] `views/About.vue` - 关于页(占位) + +--- + +### 阶段七:用户功能页面 + +**优先级:P2** | **状态:⚠️ 部分完成** + +#### 7.1 认证页面 ⚠️ +- [x] `views/auth/Login.vue` - 登录页(框架) +- [x] `views/auth/Register.vue` - 注册页(框架) + +#### 7.2 用户中心 ⚠️ +- [x] `views/user/Profile.vue` - 个人资料(框架) +- [ ] `views/user/Settings.vue` - 用户设置 +- [ ] `views/user/MyPosts.vue` - 我的文章 + +--- + +### 阶段八:管理后台 + +**优先级:P3** | **状态:⚠️ 部分完成** + +#### 8.1 后台布局 ✅ +- [x] `admin/Dashboard.vue` - 仪表盘(框架) +- [x] `admin/Sidebar.vue` - 侧边栏(在 AdminLayout 中) + +#### 8.2 后台功能 ⚠️ +- [x] `admin/PostManage.vue` - 文章管理(框架) +- [x] `admin/CategoryManage.vue` - 分类管理(框架) +- [x] `admin/TagManage.vue` - 标签管理(框架) +- [ ] `admin/CommentManage.vue` - 评论管理 +- [ ] `admin/UserManage.vue` - 用户管理 +- [ ] `admin/Statistics.vue` - 数据统计 + +--- + +### 阶段九:动效与优化 + +**优先级:P4** | **状态:⚠️ 部分完成** + +#### 9.1 动画效果 ⚠️ +- [ ] GSAP 页面转场动画 +- [x] 卡片悬浮效果 ✅ +- [ ] 看板娘互动动画 +- [x] 毛玻璃特效实现 ✅ + +#### 9.2 性能优化 +- [ ] 图片懒加载 +- [x] 路由懒加载 +- [ ] API 请求防抖节流 +- [ ] 前端缓存策略 + +--- + +### 阶段十:测试与部署 + +**优先级:P5** | **状态:❌ 未开始** + +#### 10.1 测试 +- [ ] 后端单元测试(pytest) +- [ ] API 集成测试 +- [ ] 前端组件测试(Vitest) + +#### 10.2 部署配置 +- [ ] `Dockerfile`(后端) +- [ ] `Dockerfile`(前端) +- [ ] `docker-compose.yml` - 完整编排 +- [ ] Nginx 配置 +- [ ] CI/CD 流程 + +--- + +## 项目结构(当前) + +``` +ACG-Blog/ +├── backend/ +│ ├── app/ +│ │ ├── api/ +│ │ │ └── endpoints/ # API 路由 +│ │ │ ├── auth.py # 认证接口 +│ │ │ ├── users.py # 用户接口 +│ │ │ ├── posts.py # 文章接口 +│ │ │ ├── categories.py # 分类接口 +│ │ │ ├── tags.py # 标签接口 +│ │ │ ├── comments.py # 评论接口 +│ │ │ ├── kanban.py # 看板娘接口 +│ │ │ ├── stats.py # 统计接口 +│ │ │ └── upload.py # 上传接口 +│ │ ├── core/ # 核心配置 +│ │ │ ├── config.py # 环境变量配置 +│ │ │ ├── supabase.py # Supabase 客户端 +│ │ │ ├── security.py # JWT/密码安全 +│ │ │ └── logger.py # 日志配置 +│ │ ├── crud/ # CRUD 操作 +│ │ │ ├── user.py +│ │ │ ├── post.py +│ │ │ ├── category.py +│ │ │ ├── tag.py +│ │ │ └── comment.py +│ │ ├── models/ # 数据模型(Pydantic) +│ │ │ ├── user.py +│ │ │ ├── post.py +│ │ │ ├── category.py +│ │ │ ├── tag.py +│ │ │ └── comment.py +│ │ ├── schemas/ # Pydantic Schema +│ │ │ ├── auth.py +│ │ │ ├── token.py +│ │ │ └── ... +│ │ └── services/ # 业务服务 +│ │ ├── kanban_service.py +│ │ ├── stats_service.py +│ │ └── upload_service.py +│ ├── main.py # FastAPI 入口 +│ ├── requirements.txt # Python 依赖 +│ └── venv/ # 虚拟环境 +│ +├── frontend/ +│ ├── public/ # 静态资源 +│ │ ├── favicon.svg +│ │ ├── icons.svg +│ │ └── live2d/ # Live2D 模型文件 +│ ├── src/ +│ │ ├── api/ # API 封装 ✅ +│ │ │ ├── index.ts # Axios 配置 +│ │ │ ├── auth.ts # 认证接口 +│ │ │ └── post.ts # 文章接口 +│ │ ├── assets/ # 静态资源 +│ │ ├── components/ # 公共组件 ✅ +│ │ │ ├── Footer.vue +│ │ │ ├── Hero.vue +│ │ │ ├── Kanban.vue # 看板娘组件 +│ │ │ ├── Navbar.vue +│ │ │ ├── PostCard.vue +│ │ │ └── Sidebar.vue +│ │ ├── layouts/ # 布局组件 +│ │ │ └── AdminLayout.vue +│ │ ├── router/ # 路由配置 +│ │ ├── store/ # 状态管理 ✅ +│ │ ├── views/ # 页面视图 +│ │ ├── App.vue +│ │ ├── main.ts +│ │ └── style.css +│ ├── package.json +│ ├── tsconfig.json +│ ├── vite.config.ts +│ ├── tailwind.config.js +│ └── postcss.config.js +│ +├── docker-compose.yml # Docker 编排 +├── README.md +└── 开发路径.md +``` + +--- + +## 技术栈汇总 + +### 后端技术 +| 技术 | 版本 | 用途 | +|------|------|------| +| Python | 3.11+ | 运行环境 | +| FastAPI | 0.110.0 | Web 框架 | +| Uvicorn | 0.27.1 | ASGI 服务器 | +| **Supabase** | - | BaaS 平台(内置 Auth + PostgreSQL + Storage) | +| Redis | - | 缓存/会话(热搜排行、访问统计) | +| Pydantic | 2.6.3 | 数据校验 | +| python-jose | 3.3.0 | JWT 认证 | +| Passlib | 1.7.4 | 密码哈希 | +| Loguru | 0.7.2 | 日志 | +| Pillow | 10.2.0 | 图片处理 | +| httpx | 0.27.0 | 异步 HTTP | +| python-multipart | 0.0.9 | 文件上传 | + +### 前端技术 +| 技术 | 版本 | 用途 | +|------|------|------| +| Vue | 3.5.30 | 框架 | +| Vite | 8.0.0 | 构建工具 | +| TypeScript | 5.9.3 | 类型系统 | +| Vue Router | 5.0.3 | 路由 | +| Pinia | 3.0.4 | 状态管理 | +| Naive UI | 2.44.1 | UI 组件库 | +| Tailwind CSS | 4.2.1 | CSS 框架 | +| Axios | 1.13.6 | HTTP 客户端 | +| @vueuse/core | 14.2.1 | Vue 组合式工具 | +| **GSAP** | - | 动画库(页面转场、交互动画) | +| **V-MD-Editor** | - | Markdown 编辑器 | + +--- + +## 优先级建议 + +| 优先级 | 阶段 | 说明 | 状态 | +|--------|------|------|------| +| P0 | 一、二 | 后端基础 + 认证系统(核心) | ❌ 未开始 | +| P1 | 三、五 | 文章管理 + 前端初始化 | ❌ / ⚠️ | +| P2 | 六、七 | 核心页面 + 用户功能 | ⚠️ 部分完成 | +| P3 | 四、八 | 特色功能 + 管理后台 | ❌ / ⚠️ | +| P4 | 九 | 动效优化 | ⚠️ 部分完成 | +| P5 | 十 | 测试与部署 | ❌ 未开始 | + +--- + +## 下一步开发计划 + +根据当前项目状态,建议按以下顺序继续开发: + +### 立即执行(Next Step) +1. **完成后端核心配置** (`core/config.py`, `core/security.py`) +2. **创建数据库模型** (`models/`) +3. **实现认证接口** (`api/endpoints/auth.py`) +4. **前后端 API 对接** + +### 看板娘集成(可选) +- 可使用 Live2D Cubism SDK 或 `vue-live2d` 等第三方库 + +--- + +## 注意事项 + +1. **Supabase**:使用 Supabase 作为 BaaS 平台,内置 Auth、PostgreSQL、Storage +2. **环境变量**:敏感信息(Supabase URL、anon key、JWT密钥)必须通过 `.env` 管理 +3. **前端主题**:使用二次元风格的配色(粉色 #FFB7C5、浅蓝 #A8D8EA、紫色 #D4B5E6) +4. **看板娘**:可使用 Live2D Cubism SDK 或第三方开源实现(如 vue-live2d) +5. **图片处理**:确保上传的图片有合适的压缩和尺寸限制(Pillow) +6. **Tailwind 4.x**:注意 `@apply` 指令使用限制,优先使用 CSS 原生语法 +7. **Redis**:用于缓存热搜排行、访问统计等高频访问数据 + +--- + +*文档版本:v2.0 | 更新日期:2026-03-24*