From 92e8c9a94ce2db9c85730b134073d04b668e49c4 Mon Sep 17 00:00:00 2001 From: yanlongqi Date: Mon, 3 Nov 2025 23:56:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AE=8C=E6=95=B4=E7=9A=84?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现登录页面: - 添加登录和注册表单切换功能 - 使用antd-mobile组件(Form, Input, Button, Toast) - 白色背景设计,标题使用主题色 - 表单验证和错误提示 - 已登录用户自动重定向 - 完善用户认证: - 路由保护,未登录用户重定向到登录页 - 用户信息保存到localStorage - Profile页面支持退出登录 - 后端改进: - 启用CORS中间件支持跨域请求 - 更新开发规范: - 在CLAUDE.md中添加前端开发规范 - 明确UI组件使用原则:优先使用antd-mobile组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 22 ++ main.go | 1 + web/src/pages/Login.less | 222 -------------- web/src/pages/Login.module.less | 124 ++++++++ web/src/pages/Login.tsx | 285 ++++++++++-------- .../{Profile.less => Profile.module.less} | 14 +- web/src/pages/Profile.tsx | 26 +- web/src/pages/Question.less | 209 ------------- web/src/pages/Question.module.less | 212 +++++++++++++ web/src/pages/Question.tsx | 2 +- 10 files changed, 543 insertions(+), 574 deletions(-) delete mode 100644 web/src/pages/Login.less create mode 100644 web/src/pages/Login.module.less rename web/src/pages/{Profile.less => Profile.module.less} (79%) delete mode 100644 web/src/pages/Question.less create mode 100644 web/src/pages/Question.module.less diff --git a/CLAUDE.md b/CLAUDE.md index 86e7d3b..da61ff4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,3 +104,25 @@ go test -v ./... - 在 `internal/models/` 中添加模型 - 考虑使用 GORM 或类似的 ORM - 在 main.go 或单独的包中添加数据库初始化 + +## 前端开发规范 + +### UI 组件使用原则 +**重要**: 在开发前端页面时,必须优先使用 UI 框架的组件。 + +- **优先使用 antd-mobile 组件**: 项目使用 antd-mobile 作为 UI 框架,开发时应优先查找并使用框架提供的组件 +- **常用组件示例**: + - 表单输入: 使用 `Input` 组件,而非原生 `` + - 按钮: 使用 `Button` 组件,而非原生 ` - } + {isLogin ? '登录' : '注册'} + + + +
+ {isLogin ? ( +

+ 还没有账号? + - } + 立即注册 + +

+ ) : ( +

+ 已有账号? + +

+ )}
) diff --git a/web/src/pages/Profile.less b/web/src/pages/Profile.module.less similarity index 79% rename from web/src/pages/Profile.less rename to web/src/pages/Profile.module.less index 58e45d8..0fa1dcf 100644 --- a/web/src/pages/Profile.less +++ b/web/src/pages/Profile.module.less @@ -3,7 +3,7 @@ @text-secondary: #999; // 页面容器 -.profile-page { +.page { min-height: 100vh; background-color: @bg-color; padding: 16px; @@ -11,32 +11,32 @@ } // 用户卡片 -.user-card { +.userCard { margin-bottom: 16px; } -.user-info { +.userInfo { display: flex; align-items: center; gap: 16px; } -.user-details { +.userDetails { flex: 1; } -.user-nickname { +.userNickname { font-size: 20px; font-weight: bold; margin-bottom: 4px; } -.user-username { +.userUsername { font-size: 14px; color: @text-secondary; } // 登出容器 -.logout-container { +.logoutContainer { margin-top: 24px; } diff --git a/web/src/pages/Profile.tsx b/web/src/pages/Profile.tsx index ae8cc10..7763a9d 100644 --- a/web/src/pages/Profile.tsx +++ b/web/src/pages/Profile.tsx @@ -14,7 +14,7 @@ import { FileOutline, UserOutline, } from 'antd-mobile-icons' -import './Profile.less' +import styles from './Profile.module.less' interface UserInfo { username: string @@ -29,7 +29,7 @@ const Profile: React.FC = () => { useEffect(() => { // 从 localStorage 获取用户信息 const token = localStorage.getItem('token') - const savedUserInfo = localStorage.getItem('userInfo') + const savedUserInfo = localStorage.getItem('user') if (token && savedUserInfo) { try { @@ -47,7 +47,7 @@ const Profile: React.FC = () => { if (result) { localStorage.removeItem('token') - localStorage.removeItem('userInfo') + localStorage.removeItem('user') setUserInfo(null) Toast.show('已退出登录') navigate('/login') @@ -59,29 +59,29 @@ const Profile: React.FC = () => { } return ( -
+
{/* 用户信息卡片 */} - + {userInfo ? ( -
+
{!userInfo.avatar && } -
-
{userInfo.nickname}
-
@{userInfo.username}
+
+
{userInfo.nickname}
+
@{userInfo.username}
) : ( -
+
-
-
未登录
+
+
未登录
diff --git a/web/src/pages/Question.less b/web/src/pages/Question.less deleted file mode 100644 index 034e397..0000000 --- a/web/src/pages/Question.less +++ /dev/null @@ -1,209 +0,0 @@ -// 变量定义 -@bg-color: #f5f5f5; -@white: #fff; -@primary-color: #1677ff; -@text-color: #333; -@text-secondary: #666; -@text-tertiary: #888; -@border-color: #e8e8e8; -@shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06); -@shadow-medium: 0 2px 8px rgba(0, 0, 0, 0.08); -@success-bg: #f6ffed; -@success-border: #b7eb8f; -@success-color: #52c41a; -@error-bg: #fff2f0; -@error-border: #ffccc7; -@error-color: #ff4d4f; - -// 页面容器 -.question-page { - min-height: 100vh; - background: @bg-color; - padding-bottom: 80px; -} - -// 头部 -.header { - background: @white; - padding: 16px 20px; - display: flex; - justify-content: space-between; - align-items: center; - box-shadow: @shadow-light; - position: sticky; - top: 0; - z-index: 100; - - h1 { - margin: 0; - font-size: 20px; - font-weight: 600; - color: @primary-color; - } -} - -// 内容区域 -.content { - padding: 16px; -} - -// 题目部分 -.question-header { - display: flex; - gap: 8px; - margin-bottom: 12px; -} - -.question-number { - font-size: 14px; - color: @text-secondary; - margin-bottom: 12px; -} - -.question-content { - font-size: 18px; - font-weight: 500; - color: @text-color; - line-height: 1.6; - margin-bottom: 8px; -} - -// 答案结果 -.answer-result { - margin-top: 20px; - padding: 16px; - border-radius: 8px; - background: @success-bg; - border: 1px solid @success-border; - - &.wrong { - background: @error-bg; - border: 1px solid @error-border; - } - - .result-icon { - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 8px; - } - - &.correct .result-icon { - color: @success-color; - } - - &.wrong .result-icon { - color: @error-color; - } - - .result-text { - font-size: 16px; - font-weight: 600; - margin-bottom: 12px; - text-align: center; - } - - .correct-answer { - font-size: 14px; - color: @text-secondary; - margin-bottom: 8px; - } - - .explanation { - font-size: 14px; - color: @text-tertiary; - line-height: 1.5; - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid @border-color; - } -} - -// 按钮组 -.button-group { - margin-top: 24px; -} - -.action-buttons { - display: flex; - gap: 8px; - padding: 16px; - background: @white; - position: sticky; - bottom: 0; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06); - - button { - flex: 1; - } -} - -// 统计内容 -.stats-content { - padding: 20px; - - h2 { - margin: 0 0 20px 0; - text-align: center; - font-size: 20px; - } -} - -.stat-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 0; - border-bottom: 1px solid #f0f0f0; - - &:last-child { - border-bottom: none; - } - - span { - font-size: 16px; - color: @text-secondary; - } - - strong { - font-size: 20px; - color: @primary-color; - } -} - -// 筛选内容 -.filter-content { - padding: 20px; - - h2 { - margin: 0 0 20px 0; - text-align: center; - font-size: 20px; - } -} - -.filter-group { - margin-bottom: 20px; - - p { - margin: 0 0 12px 0; - font-size: 14px; - color: @text-secondary; - font-weight: 500; - } -} - -// 覆盖 antd-mobile 样式 -.adm-card { - border-radius: 12px; - box-shadow: @shadow-medium; -} - -.adm-list-item { - padding: 12px 16px; -} - -.adm-modal-body { - max-height: 70vh; - overflow-y: auto; -} diff --git a/web/src/pages/Question.module.less b/web/src/pages/Question.module.less new file mode 100644 index 0000000..4b2b79c --- /dev/null +++ b/web/src/pages/Question.module.less @@ -0,0 +1,212 @@ +// 变量定义 +@bg-color: #f5f5f5; +@white: #fff; +@primary-color: #1677ff; +@text-color: #333; +@text-secondary: #666; +@text-tertiary: #888; +@border-color: #e8e8e8; +@shadow-light: 0 2px 8px rgba(0, 0, 0, 0.06); +@shadow-medium: 0 2px 8px rgba(0, 0, 0, 0.08); +@success-bg: #f6ffed; +@success-border: #b7eb8f; +@success-color: #52c41a; +@error-bg: #fff2f0; +@error-border: #ffccc7; +@error-color: #ff4d4f; + +// 使用 :global 包裹所有样式,因为 Question 组件使用直接类名 +:global { + // 页面容器 + .question-page { + min-height: 100vh; + background: @bg-color; + padding-bottom: 80px; + } + + // 头部 + .header { + background: @white; + padding: 16px 20px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: @shadow-light; + position: sticky; + top: 0; + z-index: 100; + + h1 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: @primary-color; + } + } + + // 内容区域 + .content { + padding: 16px; + } + + // 题目部分 + .question-header { + display: flex; + gap: 8px; + margin-bottom: 12px; + } + + .question-number { + font-size: 14px; + color: @text-secondary; + margin-bottom: 12px; + } + + .question-content { + font-size: 18px; + font-weight: 500; + color: @text-color; + line-height: 1.6; + margin-bottom: 8px; + } + + // 答案结果 + .answer-result { + margin-top: 20px; + padding: 16px; + border-radius: 8px; + background: @success-bg; + border: 1px solid @success-border; + + &.wrong { + background: @error-bg; + border: 1px solid @error-border; + } + + .result-icon { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; + } + + &.correct .result-icon { + color: @success-color; + } + + &.wrong .result-icon { + color: @error-color; + } + + .result-text { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + text-align: center; + } + + .correct-answer { + font-size: 14px; + color: @text-secondary; + margin-bottom: 8px; + } + + .explanation { + font-size: 14px; + color: @text-tertiary; + line-height: 1.5; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid @border-color; + } + } + + // 按钮组 + .button-group { + margin-top: 24px; + } + + .action-buttons { + display: flex; + gap: 8px; + padding: 16px; + background: @white; + position: sticky; + bottom: 0; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06); + + button { + flex: 1; + } + } + + // 统计内容 + .stats-content { + padding: 20px; + + h2 { + margin: 0 0 20px 0; + text-align: center; + font-size: 20px; + } + } + + .stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } + + span { + font-size: 16px; + color: @text-secondary; + } + + strong { + font-size: 20px; + color: @primary-color; + } + } + + // 筛选内容 + .filter-content { + padding: 20px; + + h2 { + margin: 0 0 20px 0; + text-align: center; + font-size: 20px; + } + } + + .filter-group { + margin-bottom: 20px; + + p { + margin: 0 0 12px 0; + font-size: 14px; + color: @text-secondary; + font-weight: 500; + } + } + + // 覆盖 antd-mobile 样式 + .adm-card { + border-radius: 12px; + box-shadow: @shadow-medium; + } + + .adm-list-item { + padding: 12px 16px; + } + + .adm-modal-body { + max-height: 70vh; + overflow-y: auto; + } +} diff --git a/web/src/pages/Question.tsx b/web/src/pages/Question.tsx index bb3c384..7af1d31 100644 --- a/web/src/pages/Question.tsx +++ b/web/src/pages/Question.tsx @@ -22,7 +22,7 @@ import { } from 'antd-mobile-icons' import type { Question, AnswerResult } from '../types/question' import * as questionApi from '../api/question' -import './Question.less' +import './Question.module.less' const QuestionPage: React.FC = () => { const [currentQuestion, setCurrentQuestion] = useState(null)