From 7a33038af87a4e022f6748ec8a0f763fac0bacdf Mon Sep 17 00:00:00 2001 From: cdn0x12 Date: Fri, 21 Feb 2025 20:04:30 +0800 Subject: [PATCH] [feature/frontend] update person info --- frontend/data/i18n/en.json | 63 ++++- frontend/data/i18n/zh-Hans.json | 63 ++++- frontend/data/i18n/zh-Hant.json | 67 ++++- frontend/src/App.tsx | 68 +---- frontend/src/components/LoadingSpinner.tsx | 32 +++ .../src/components/admin/TableActions.tsx | 27 ++ frontend/src/components/layout/Header.tsx | 4 +- frontend/src/contexts/UserContext.tsx | 81 ++++++ frontend/src/pages/Article.tsx | 7 + .../admin/categories/CategoriesManagement.tsx | 32 ++- .../contributors/ContributorsManagement.tsx | 107 +++++--- .../src/pages/admin/daily/DailyManagement.tsx | 47 ++++ .../src/pages/admin/dashboard/Dashboard.tsx | 51 ++++ .../src/pages/admin/layout/AdminLayout.tsx | 46 +++- frontend/src/pages/admin/login.tsx | 243 ++++++++++++++++++ .../pages/admin/medias/MediasManagement.tsx | 52 ++++ .../src/pages/admin/posts/PostsManagement.tsx | 62 ++--- .../src/pages/admin/settings/Settings.tsx | 224 ++++++++++++++++ .../src/pages/admin/users/UsersManagement.tsx | 162 ++++++++---- frontend/src/router.tsx | 206 ++++++++++++++- frontend/vite.config.ts | 9 + 21 files changed, 1436 insertions(+), 217 deletions(-) create mode 100644 frontend/src/components/LoadingSpinner.tsx create mode 100644 frontend/src/components/admin/TableActions.tsx create mode 100644 frontend/src/contexts/UserContext.tsx create mode 100644 frontend/src/pages/admin/daily/DailyManagement.tsx create mode 100644 frontend/src/pages/admin/dashboard/Dashboard.tsx create mode 100644 frontend/src/pages/admin/login.tsx create mode 100644 frontend/src/pages/admin/medias/MediasManagement.tsx create mode 100644 frontend/src/pages/admin/settings/Settings.tsx diff --git a/frontend/data/i18n/en.json b/frontend/data/i18n/en.json index defb85f..06015e4 100644 --- a/frontend/data/i18n/en.json +++ b/frontend/data/i18n/en.json @@ -14,19 +14,23 @@ "search": "Search" }, "theme": { - "light": "Light Mode", - "dark": "Dark Mode", - "system": "System Mode" + "light": "Light", + "dark": "Dark", + "system": "System" }, "footer": { "copyright": "TSS Rocks. All rights reserved." }, "admin": { "common": { + "loading": "Loading...", "search": "Search", "create": "Create", "edit": "Edit", "delete": "Delete", + "upload": "Upload", + "save": "Save", + "saving": "Saving...", "status": "Status", "actions": "Actions", "published": "Published", @@ -43,19 +47,68 @@ "lastLogin": "Last Login", "joinDate": "Join Date", "username": "Username", - "logout": "Logout" + "logout": "Logout", + "language": "Language", + "theme": { + "light": "Light Mode", + "dark": "Dark Mode", + "system": "System" + } + }, + "dashboard": { + "totalPosts": "Total Posts", + "totalCategories": "Total Categories", + "totalUsers": "Total Users", + "totalContributors": "Total Contributors" + }, + "login": { + "title": "Admin Login", + "username": "Username", + "password": "Password", + "remember": "Remember me", + "submit": "Sign in", + "loading": "Signing in...", + "error": { + "failed": "Login failed", + "retry": "Login failed, please try again later" + } }, "nav": { "dashboard": "Dashboard", "posts": "Posts", + "daily": "Daily Quotes", + "medias": "Media", "categories": "Categories", "users": "Users", - "contributors": "Contributors" + "contributors": "Contributors", + "settings": "Settings" + }, + "daily": { + "title": "Title", + "publishDate": "Publish Date", + "status": "Status" + }, + "medias": { + "name": "File Name", + "type": "Type", + "size": "Size", + "uploadDate": "Upload Date" }, "roles": { "admin": "Administrator", "user": "User", "contributor": "Contributor" + }, + "settings": { + "username": "Username", + "displayName": "Display Name", + "email": "Email", + "changePassword": "Change Password", + "currentPassword": "Current Password", + "newPassword": "New Password", + "confirmPassword": "Confirm Password", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 8 characters long" } } } diff --git a/frontend/data/i18n/zh-Hans.json b/frontend/data/i18n/zh-Hans.json index 749c864..296d279 100644 --- a/frontend/data/i18n/zh-Hans.json +++ b/frontend/data/i18n/zh-Hans.json @@ -14,8 +14,8 @@ "search": "搜索" }, "theme": { - "light": "浅色模式", - "dark": "深色模式", + "light": "浅色", + "dark": "深色", "system": "跟随系统" }, "footer": { @@ -23,10 +23,12 @@ }, "admin": { "common": { + "loading": "正在加载...", "search": "搜索", "create": "新建", "edit": "编辑", "delete": "删除", + "upload": "上传", "status": "状态", "actions": "操作", "published": "已发布", @@ -43,19 +45,64 @@ "lastLogin": "最后登录", "joinDate": "加入时间", "username": "用户名", - "logout": "退出" + "logout": "退出登录", + "save": "保存", + "saving": "保存中..." + }, + "dashboard": { + "totalPosts": "文章总数", + "totalCategories": "分类总数", + "totalUsers": "用户总数", + "totalContributors": "贡献者总数" + }, + "login": { + "title": "管理员登录", + "username": "用户名", + "password": "密码", + "remember": "记住我", + "submit": "登录", + "loading": "登录中...", + "error": { + "failed": "登录失败", + "retry": "登录失败,请稍后重试" + } }, "nav": { - "dashboard": "仪表盘", - "posts": "文章", - "categories": "分类", - "users": "用户", - "contributors": "作者" + "dashboard": "仪表板", + "posts": "文章管理", + "daily": "每日一句", + "medias": "媒体管理", + "categories": "分类管理", + "users": "用户管理", + "contributors": "贡献者管理", + "settings": "个人设置" + }, + "daily": { + "title": "标题", + "publishDate": "发布日期", + "status": "状态" + }, + "medias": { + "name": "文件名", + "type": "类型", + "size": "大小", + "uploadDate": "上传日期" }, "roles": { "admin": "管理员", "user": "普通用户", "contributor": "作者" + }, + "settings": { + "username": "用户名", + "displayName": "显示名称", + "email": "邮箱", + "changePassword": "修改密码", + "currentPassword": "当前密码", + "newPassword": "新密码", + "confirmPassword": "确认密码", + "passwordMismatch": "两次输入的密码不一致", + "passwordTooShort": "密码长度不能少于8个字符" } } } diff --git a/frontend/data/i18n/zh-Hant.json b/frontend/data/i18n/zh-Hant.json index 6377964..7a89534 100644 --- a/frontend/data/i18n/zh-Hant.json +++ b/frontend/data/i18n/zh-Hant.json @@ -14,8 +14,8 @@ "search": "搜尋" }, "theme": { - "light": "淺色模式", - "dark": "深色模式", + "light": "淺色", + "dark": "深色", "system": "跟隨系統" }, "footer": { @@ -23,10 +23,12 @@ }, "admin": { "common": { + "loading": "正在載入...", "search": "搜尋", "create": "新建", "edit": "編輯", "delete": "刪除", + "upload": "上傳", "status": "狀態", "actions": "操作", "published": "已發布", @@ -43,19 +45,70 @@ "lastLogin": "最後登入", "joinDate": "加入時間", "username": "用戶名", - "logout": "退出" + "logout": "退出登錄", + "language": "語言", + "theme": { + "light": "淺色模式", + "dark": "深色模式", + "system": "跟隨系統" + }, + "save": "保存", + "saving": "保存中..." }, "nav": { "dashboard": "儀表板", - "posts": "文章", - "categories": "分類", - "users": "用戶", - "contributors": "作者" + "posts": "文章管理", + "daily": "每日一句", + "medias": "媒體管理", + "categories": "分類管理", + "users": "用戶管理", + "contributors": "貢獻者管理", + "settings": "個人設置" + }, + "dashboard": { + "totalPosts": "文章總數", + "totalCategories": "分類總數", + "totalUsers": "用戶總數", + "totalContributors": "貢獻者總數" + }, + "daily": { + "title": "標題", + "publishDate": "發布日期", + "status": "狀態" + }, + "medias": { + "name": "檔案名", + "type": "類型", + "size": "大小", + "uploadDate": "上傳日期" + }, + "login": { + "title": "管理員登錄", + "username": "用戶名", + "password": "密碼", + "remember": "記住我", + "submit": "登錄", + "loading": "登錄中...", + "error": { + "failed": "登錄失敗", + "retry": "登錄失敗,請稍後重試" + } }, "roles": { "admin": "管理員", "user": "普通用戶", "contributor": "作者" + }, + "settings": { + "username": "用戶名", + "displayName": "顯示名稱", + "email": "郵箱", + "changePassword": "修改密碼", + "currentPassword": "當前密碼", + "newPassword": "新密碼", + "confirmPassword": "確認密碼", + "passwordMismatch": "兩次輸入的密碼不一致", + "passwordTooShort": "密碼長度不能少於8個字符" } } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c5f2ca5..9fa788f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,63 +1,19 @@ -import React from 'react'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { Header } from './components/layout/Header'; -import { Suspense, lazy } from 'react'; -import Footer from './components/Footer'; +import { RouterProvider } from 'react-router-dom'; +import { Suspense } from 'react'; import { AuthProvider } from './contexts/AuthContext'; - -// Lazy load pages -const Home = lazy(() => import('./pages/Home')); -const Daily = lazy(() => import('./pages/Daily')); -const Article = lazy(() => import('./pages/Article')); -const AdminLayout = lazy(() => import('./pages/admin/layout/AdminLayout')); - -// 管理页面懒加载 -const PostsManagement = lazy(() => import('./pages/admin/posts/PostsManagement')); -const CategoriesManagement = lazy(() => import('./pages/admin/categories/CategoriesManagement')); -const UsersManagement = lazy(() => import('./pages/admin/users/UsersManagement')); -const ContributorsManagement = lazy(() => import('./pages/admin/contributors/ContributorsManagement')); +import { UserProvider } from './contexts/UserContext'; +import router from './router'; +import LoadingSpinner from './components/LoadingSpinner'; function App() { return ( - - -
- Loading...
}> - - {/* Admin routes */} - }> - } /> - } /> - } /> - } /> - - - {/* Public routes */} - -
- {/* 页眉分隔线 */} -
-
-
-
- - } /> - } /> - } /> - -
-
- - } - /> - - -
- - + }> + + + + + + ); } diff --git a/frontend/src/components/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..f6742fc --- /dev/null +++ b/frontend/src/components/LoadingSpinner.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next'; + +interface LoadingSpinnerProps { + fullScreen?: boolean; +} + +export default function LoadingSpinner({ fullScreen = false }: LoadingSpinnerProps) { + const { t } = useTranslation(); + + const content = ( +
+
+
+ {t('admin.common.loading')} +
+
+ ); + + if (fullScreen) { + return ( +
+ {content} +
+ ); + } + + return ( +
+ {content} +
+ ); +} diff --git a/frontend/src/components/admin/TableActions.tsx b/frontend/src/components/admin/TableActions.tsx new file mode 100644 index 0000000..7d0bded --- /dev/null +++ b/frontend/src/components/admin/TableActions.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from 'react-i18next'; + +interface TableActionsProps { + /** + * 按钮文本的翻译键,默认为 'create' + */ + actionKey?: 'create' | 'upload'; + /** + * 点击按钮时的回调函数 + */ + onClick?: () => void; +} + +export default function TableActions({ actionKey = 'create', onClick }: TableActionsProps) { + const { t } = useTranslation(); + + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index abbf3b1..96f2e28 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -12,7 +12,7 @@ const LANGUAGES = [ { code: 'zh-Hant', nativeName: '繁體中文' }, ]; -export function Header() { +function Header() { const { t, i18n } = useTranslation(); const { theme, setTheme } = useTheme(); const [isSearchOpen, setIsSearchOpen] = useState(false); @@ -238,3 +238,5 @@ export function Header() {
); } + +export default Header; diff --git a/frontend/src/contexts/UserContext.tsx b/frontend/src/contexts/UserContext.tsx new file mode 100644 index 0000000..0dab4c0 --- /dev/null +++ b/frontend/src/contexts/UserContext.tsx @@ -0,0 +1,81 @@ +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +interface User { + id: number; + username: string; + display_name?: string; + email: string; + status: string; + created_at: string; + updated_at: string; + edges: Record; +} + +interface UserContextType { + user: User | null; + loading: boolean; + error: string | null; + fetchUser: () => Promise; +} + +const UserContext = createContext({ + user: null, + loading: false, + error: null, + fetchUser: async () => {}, +}); + +export function UserProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchUser = async () => { + setLoading(true); + setError(null); + try { + console.log('Fetching current user info...'); + const response = await fetch('/api/v1/users/me', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch user info: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + console.log('API Response:', result); + + if (result) { + console.log('Setting user data:', result); + setUser(result); + } else { + throw new Error('No user data received'); + } + } catch (err) { + console.error('Error fetching user info:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch user info'); + setUser(null); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (localStorage.getItem('token')) { + fetchUser(); + } + }, []); + + return ( + + {children} + + ); +} + +export function useUser() { + return useContext(UserContext); +} diff --git a/frontend/src/pages/Article.tsx b/frontend/src/pages/Article.tsx index ecb089c..0d86795 100644 --- a/frontend/src/pages/Article.tsx +++ b/frontend/src/pages/Article.tsx @@ -1,6 +1,7 @@ import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useState, useEffect } from 'react'; +import LoadingSpinner from '../components/LoadingSpinner'; export default function Article() { const { articleId } = useParams(); @@ -9,13 +10,19 @@ export default function Article() { content: string; metadata: any; } | null>(null); + const [loading, setLoading] = useState(true); useEffect(() => { // In a real application, we would fetch the article content here // based on the articleId and current language console.log(`Fetching article ${articleId} in ${i18n.language}`); + setLoading(false); }, [articleId, i18n.language]); + if (loading) { + return ; + } + if (!article) { return
Loading...
; } diff --git a/frontend/src/pages/admin/categories/CategoriesManagement.tsx b/frontend/src/pages/admin/categories/CategoriesManagement.tsx index cdd1db3..926fc23 100644 --- a/frontend/src/pages/admin/categories/CategoriesManagement.tsx +++ b/frontend/src/pages/admin/categories/CategoriesManagement.tsx @@ -1,17 +1,19 @@ -import { FC, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { RiAddLine, RiSearchLine } from 'react-icons/ri'; import Table from '../../../components/admin/Table'; +import TableActions from '../../../components/admin/TableActions'; interface Category { id: string; name: string; - description: string; + slug: string; + postCount: number; } const CategoriesManagement: FC = () => { const [searchTerm, setSearchTerm] = useState(''); - const [loading, setLoading] = useState(false); + const [loading] = useState(false); const { t } = useTranslation(); // 这里后续会通过 API 获取数据 @@ -19,7 +21,8 @@ const CategoriesManagement: FC = () => { { id: '1', name: '新闻', - description: '新闻分类', + slug: 'news', + postCount: 10, }, ]; @@ -31,14 +34,22 @@ const CategoriesManagement: FC = () => { console.log('Delete category:', category); }; + const handleCreate = () => { + // TODO: 实现创建分类的逻辑 + }; + const columns = [ { - key: 'name', + key: 'name' as keyof Category, title: t('admin.common.name'), }, { - key: 'description', - title: t('admin.common.description'), + key: 'slug' as keyof Category, + title: t('admin.common.slug'), + }, + { + key: 'postCount' as keyof Category, + title: t('admin.common.postCount'), }, ]; @@ -56,15 +67,12 @@ const CategoriesManagement: FC = () => { /> - +
- columns={columns} data={categories} loading={loading} diff --git a/frontend/src/pages/admin/contributors/ContributorsManagement.tsx b/frontend/src/pages/admin/contributors/ContributorsManagement.tsx index 8ace271..0eb1338 100644 --- a/frontend/src/pages/admin/contributors/ContributorsManagement.tsx +++ b/frontend/src/pages/admin/contributors/ContributorsManagement.tsx @@ -2,13 +2,15 @@ import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { RiAddLine, RiSearchLine } from 'react-icons/ri'; import Table from '../../../components/admin/Table'; +import TableActions from '../../../components/admin/TableActions'; interface Contributor { id: string; name: string; - bio: string; - articles: number; - joinDate: string; + email: string; + role: 'editor' | 'contributor'; + status: 'active' | 'inactive' | 'banned'; + postCount: number; } const ContributorsManagement: FC = () => { @@ -21,9 +23,10 @@ const ContributorsManagement: FC = () => { { id: '1', name: '李四', - bio: '这是李四的简介', - articles: 5, - joinDate: '2024-02-20', + email: 'lisi@example.com', + role: 'contributor', + status: 'active', + postCount: 5, }, ]; @@ -37,28 +40,55 @@ const ContributorsManagement: FC = () => { const columns = [ { - key: 'name', - title: t('admin.common.name'), + key: 'name' as keyof Contributor, + title: t('admin.contributors.name'), }, { - key: 'bio', - title: t('admin.common.bio'), + key: 'email' as keyof Contributor, + title: t('admin.contributors.email'), }, { - key: 'articles', - title: t('admin.common.articles'), - render: (value: number) => ( - - {value} + key: 'role' as keyof Contributor, + title: t('admin.contributors.role'), + render: (value: Contributor['role']) => ( + + {t(`admin.roles.${value}`)} ), }, { - key: 'joinDate', - title: t('admin.common.joinDate'), + key: 'status' as keyof Contributor, + title: t('admin.contributors.status'), + render: (value: Contributor['status']) => ( + + {t(`admin.common.${value}`)} + + ), + }, + { + key: 'postCount' as keyof Contributor, + title: t('admin.contributors.postCount'), }, ]; + const handleCreate = () => { + // TODO: 实现创建贡献者的逻辑 + }; + return (
@@ -73,15 +103,12 @@ const ContributorsManagement: FC = () => { />
- +
-
columns={columns} data={contributors} loading={loading} @@ -92,10 +119,11 @@ const ContributorsManagement: FC = () => {
- - - - + + + + + @@ -103,13 +131,32 @@ const ContributorsManagement: FC = () => { {data.map((contributor) => ( - + - + +
{t('admin.common.name')}{t('admin.common.bio')}{t('admin.common.articles')}{t('admin.common.joinDate')}{t('admin.contributors.name')}{t('admin.contributors.email')}{t('admin.contributors.role')}{t('admin.contributors.status')}{t('admin.contributors.postCount')} {t('admin.common.actions')}
{contributor.name}{contributor.bio}{contributor.email} - - {contributor.articles} + + {t(`admin.roles.${contributor.role}`)} {contributor.joinDate} + + {t(`admin.common.${contributor.status}`)} + + {contributor.postCount}