diff --git a/api/schemas/paths/users.yaml b/api/schemas/paths/users.yaml index da741eb..897adad 100644 --- a/api/schemas/paths/users.yaml +++ b/api/schemas/paths/users.yaml @@ -216,23 +216,14 @@ user_me: schema: type: object properties: - username: - type: string - minLength: 3 - maxLength: 32 - display_name: - type: string - maxLength: 64 email: type: string format: email current_password: type: string - description: 当修改密码时必填 new_password: type: string minLength: 8 - description: 新密码,如果要修改密码则必填 responses: '200': description: 用户信息更新成功 diff --git a/backend/internal/handler/user.go b/backend/internal/handler/user.go index bd9f8fb..b96603b 100644 --- a/backend/internal/handler/user.go +++ b/backend/internal/handler/user.go @@ -11,7 +11,6 @@ import ( ) type UpdateCurrentUserRequest struct { - Username string `json:"username,omitempty" binding:"omitempty,min=3,max=32"` Email string `json:"email,omitempty" binding:"omitempty,email"` CurrentPassword string `json:"current_password,omitempty"` NewPassword string `json:"new_password,omitempty" binding:"omitempty,min=8"` @@ -212,20 +211,12 @@ func (h *Handler) GetCurrentUser(c *gin.Context) { // UpdateCurrentUser updates the current user's information func (h *Handler) UpdateCurrentUser(c *gin.Context) { // 从上下文中获取用户ID(由认证中间件设置) - userIDStr, exists := c.Get("user_id") + userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } - // 将用户ID转换为整数 - userID, err := strconv.Atoi(userIDStr.(string)) - if err != nil { - log.Error().Err(err).Msg("Failed to convert user ID to integer") - c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID format"}) - return - } - var req UpdateCurrentUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -240,25 +231,20 @@ func (h *Handler) UpdateCurrentUser(c *gin.Context) { } // 验证当前密码 - if err := h.service.VerifyPassword(c.Request.Context(), userID, req.CurrentPassword); err != nil { + if err := h.service.VerifyPassword(c.Request.Context(), userID.(int), req.CurrentPassword); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid current password"}) return } } // 更新用户信息 - user, err := h.service.UpdateUser(c.Request.Context(), userID, &types.UpdateUserInput{ - Username: req.Username, + user, err := h.service.UpdateUser(c.Request.Context(), userID.(int), &types.UpdateUserInput{ Email: req.Email, Password: req.NewPassword, DisplayName: req.DisplayName, }) if err != nil { log.Error().Err(err).Msg("Failed to update user") - if err.Error() == "username already taken" { - c.JSON(http.StatusConflict, gin.H{"error": "Username already taken"}) - return - } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user information"}) return } diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go index 9d1d015..0ae658b 100644 --- a/backend/internal/service/user.go +++ b/backend/internal/service/user.go @@ -40,25 +40,6 @@ func (s *serviceImpl) UpdateUser(ctx context.Context, userID int, input *types.U // Start building the update update := s.client.User.UpdateOneID(userID) - // Update username if provided - if input.Username != "" { - // Check if username is already taken - exists, err := s.client.User.Query(). - Where(user.And( - user.UsernameEQ(input.Username), - user.IDNEQ(userID), - )). - Exist(ctx) - if err != nil { - log.Error().Err(err).Msg("Failed to check username availability") - return nil, fmt.Errorf("failed to check username availability: %w", err) - } - if exists { - return nil, fmt.Errorf("username already taken") - } - update.SetUsername(input.Username) - } - // Update email if provided if input.Email != "" { update.SetEmail(input.Email) diff --git a/backend/internal/types/user.go b/backend/internal/types/user.go index 45a0b9e..1eef0b6 100644 --- a/backend/internal/types/user.go +++ b/backend/internal/types/user.go @@ -2,7 +2,6 @@ package types // UpdateUserInput defines the input for updating a user type UpdateUserInput struct { - Username string Email string Password string Role string diff --git a/frontend/data/i18n/en.json b/frontend/data/i18n/en.json index 06015e4..defb85f 100644 --- a/frontend/data/i18n/en.json +++ b/frontend/data/i18n/en.json @@ -14,23 +14,19 @@ "search": "Search" }, "theme": { - "light": "Light", - "dark": "Dark", - "system": "System" + "light": "Light Mode", + "dark": "Dark Mode", + "system": "System Mode" }, "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", @@ -47,68 +43,19 @@ "lastLogin": "Last Login", "joinDate": "Join Date", "username": "Username", - "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" - } + "logout": "Logout" }, "nav": { "dashboard": "Dashboard", "posts": "Posts", - "daily": "Daily Quotes", - "medias": "Media", "categories": "Categories", "users": "Users", - "contributors": "Contributors", - "settings": "Settings" - }, - "daily": { - "title": "Title", - "publishDate": "Publish Date", - "status": "Status" - }, - "medias": { - "name": "File Name", - "type": "Type", - "size": "Size", - "uploadDate": "Upload Date" + "contributors": "Contributors" }, "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 296d279..749c864 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,12 +23,10 @@ }, "admin": { "common": { - "loading": "正在加载...", "search": "搜索", "create": "新建", "edit": "编辑", "delete": "删除", - "upload": "上传", "status": "状态", "actions": "操作", "published": "已发布", @@ -45,64 +43,19 @@ "lastLogin": "最后登录", "joinDate": "加入时间", "username": "用户名", - "logout": "退出登录", - "save": "保存", - "saving": "保存中..." - }, - "dashboard": { - "totalPosts": "文章总数", - "totalCategories": "分类总数", - "totalUsers": "用户总数", - "totalContributors": "贡献者总数" - }, - "login": { - "title": "管理员登录", - "username": "用户名", - "password": "密码", - "remember": "记住我", - "submit": "登录", - "loading": "登录中...", - "error": { - "failed": "登录失败", - "retry": "登录失败,请稍后重试" - } + "logout": "退出" }, "nav": { - "dashboard": "仪表板", - "posts": "文章管理", - "daily": "每日一句", - "medias": "媒体管理", - "categories": "分类管理", - "users": "用户管理", - "contributors": "贡献者管理", - "settings": "个人设置" - }, - "daily": { - "title": "标题", - "publishDate": "发布日期", - "status": "状态" - }, - "medias": { - "name": "文件名", - "type": "类型", - "size": "大小", - "uploadDate": "上传日期" + "dashboard": "仪表盘", + "posts": "文章", + "categories": "分类", + "users": "用户", + "contributors": "作者" }, "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 7a89534..6377964 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,12 +23,10 @@ }, "admin": { "common": { - "loading": "正在載入...", "search": "搜尋", "create": "新建", "edit": "編輯", "delete": "刪除", - "upload": "上傳", "status": "狀態", "actions": "操作", "published": "已發布", @@ -45,70 +43,19 @@ "lastLogin": "最後登入", "joinDate": "加入時間", "username": "用戶名", - "logout": "退出登錄", - "language": "語言", - "theme": { - "light": "淺色模式", - "dark": "深色模式", - "system": "跟隨系統" - }, - "save": "保存", - "saving": "保存中..." + "logout": "退出" }, "nav": { "dashboard": "儀表板", - "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": "登錄失敗,請稍後重試" - } + "posts": "文章", + "categories": "分類", + "users": "用戶", + "contributors": "作者" }, "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 9fa788f..c5f2ca5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,19 +1,63 @@ -import { RouterProvider } from 'react-router-dom'; -import { Suspense } from 'react'; +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 { AuthProvider } from './contexts/AuthContext'; -import { UserProvider } from './contexts/UserContext'; -import router from './router'; -import LoadingSpinner from './components/LoadingSpinner'; + +// 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')); function App() { return ( - }> - - - - - - + + +
+ Loading...
}> + + {/* Admin routes */} + }> + } /> + } /> + } /> + } /> + + + {/* Public routes */} + +
+ {/* 页眉分隔线 */} +
+
+
+
+ + } /> + } /> + } /> + +
+
+ + } + /> + + +
+ + ); } diff --git a/frontend/src/components/LoadingSpinner.tsx b/frontend/src/components/LoadingSpinner.tsx deleted file mode 100644 index f6742fc..0000000 --- a/frontend/src/components/LoadingSpinner.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 7d0bded..0000000 --- a/frontend/src/components/admin/TableActions.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 96f2e28..abbf3b1 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: '繁體中文' }, ]; -function Header() { +export function Header() { const { t, i18n } = useTranslation(); const { theme, setTheme } = useTheme(); const [isSearchOpen, setIsSearchOpen] = useState(false); @@ -238,5 +238,3 @@ function Header() {
); } - -export default Header; diff --git a/frontend/src/contexts/UserContext.tsx b/frontend/src/contexts/UserContext.tsx deleted file mode 100644 index 0dab4c0..0000000 --- a/frontend/src/contexts/UserContext.tsx +++ /dev/null @@ -1,81 +0,0 @@ -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 0d86795..ecb089c 100644 --- a/frontend/src/pages/Article.tsx +++ b/frontend/src/pages/Article.tsx @@ -1,7 +1,6 @@ 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(); @@ -10,19 +9,13 @@ 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 926fc23..cdd1db3 100644 --- a/frontend/src/pages/admin/categories/CategoriesManagement.tsx +++ b/frontend/src/pages/admin/categories/CategoriesManagement.tsx @@ -1,19 +1,17 @@ -import { useState } from 'react'; +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 Category { id: string; name: string; - slug: string; - postCount: number; + description: string; } const CategoriesManagement: FC = () => { const [searchTerm, setSearchTerm] = useState(''); - const [loading] = useState(false); + const [loading, setLoading] = useState(false); const { t } = useTranslation(); // 这里后续会通过 API 获取数据 @@ -21,8 +19,7 @@ const CategoriesManagement: FC = () => { { id: '1', name: '新闻', - slug: 'news', - postCount: 10, + description: '新闻分类', }, ]; @@ -34,22 +31,14 @@ const CategoriesManagement: FC = () => { console.log('Delete category:', category); }; - const handleCreate = () => { - // TODO: 实现创建分类的逻辑 - }; - const columns = [ { - key: 'name' as keyof Category, + key: 'name', title: t('admin.common.name'), }, { - key: 'slug' as keyof Category, - title: t('admin.common.slug'), - }, - { - key: 'postCount' as keyof Category, - title: t('admin.common.postCount'), + key: 'description', + title: t('admin.common.description'), }, ]; @@ -67,12 +56,15 @@ const CategoriesManagement: FC = () => { /> - +
- + { @@ -23,10 +21,9 @@ const ContributorsManagement: FC = () => { { id: '1', name: '李四', - email: 'lisi@example.com', - role: 'contributor', - status: 'active', - postCount: 5, + bio: '这是李四的简介', + articles: 5, + joinDate: '2024-02-20', }, ]; @@ -40,55 +37,28 @@ const ContributorsManagement: FC = () => { const columns = [ { - key: 'name' as keyof Contributor, - title: t('admin.contributors.name'), + key: 'name', + title: t('admin.common.name'), }, { - key: 'email' as keyof Contributor, - title: t('admin.contributors.email'), + key: 'bio', + title: t('admin.common.bio'), }, { - key: 'role' as keyof Contributor, - title: t('admin.contributors.role'), - render: (value: Contributor['role']) => ( - - {t(`admin.roles.${value}`)} + key: 'articles', + title: t('admin.common.articles'), + render: (value: number) => ( + + {value} ), }, { - 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'), + key: 'joinDate', + title: t('admin.common.joinDate'), }, ]; - const handleCreate = () => { - // TODO: 实现创建贡献者的逻辑 - }; - return (
@@ -103,12 +73,15 @@ const ContributorsManagement: FC = () => { />
- +
- +
{
- - - - - + + + + @@ -131,32 +103,13 @@ const ContributorsManagement: FC = () => { {data.map((contributor) => ( - + - - +
{t('admin.contributors.name')}{t('admin.contributors.email')}{t('admin.contributors.role')}{t('admin.contributors.status')}{t('admin.contributors.postCount')}{t('admin.common.name')}{t('admin.common.bio')}{t('admin.common.articles')}{t('admin.common.joinDate')} {t('admin.common.actions')}
{contributor.name}{contributor.email}{contributor.bio} - - {t(`admin.roles.${contributor.role}`)} + + {contributor.articles} - - {t(`admin.common.${contributor.status}`)} - - {contributor.postCount}{contributor.joinDate}