diff --git a/internal/handlers/practice_handler.go b/internal/handlers/practice_handler.go index d686ce5..4912026 100644 --- a/internal/handlers/practice_handler.go +++ b/internal/handlers/practice_handler.go @@ -661,23 +661,25 @@ func GetStatistics(c *gin.Context) { Where("user_id = ?", uid). Count(&wrongQuestions) - // 计算正确率 + // 计算正确率和总答题数 var accuracy float64 - if answeredQuestions > 0 { + var totalAnswers int64 + db.Model(&models.UserAnswerRecord{}). + Where("user_id = ?", uid). + Count(&totalAnswers) + + if totalAnswers > 0 { // 正确率 = 答对题数 / 总答题数 - var totalAnswers int64 - db.Model(&models.UserAnswerRecord{}). - Where("user_id = ?", uid). - Count(&totalAnswers) accuracy = float64(correctAnswers) / float64(totalAnswers) * 100 } - stats := models.UserStatistics{ - TotalQuestions: int(totalQuestions), - AnsweredQuestions: int(answeredQuestions), - CorrectAnswers: int(correctAnswers), - WrongQuestions: int(wrongQuestions), - Accuracy: accuracy, + stats := gin.H{ + "total_questions": int(totalQuestions), + "answered_questions": int(answeredQuestions), + "correct_answers": int(correctAnswers), + "wrong_questions": int(wrongQuestions), + "total_answers": int(totalAnswers), // 刷题次数 + "accuracy": accuracy, } c.JSON(http.StatusOK, gin.H{ diff --git a/internal/services/ai_grading.go b/internal/services/ai_grading.go index 0311639..3d04beb 100644 --- a/internal/services/ai_grading.go +++ b/internal/services/ai_grading.go @@ -36,12 +36,12 @@ func NewAIGradingService() *AIGradingService { // AIGradingResult AI评分结果 type AIGradingResult struct { - Score float64 `json:"score"` // 得分 (0-100) - IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确) - Feedback string `json:"feedback"` // 评语 - Suggestion string `json:"suggestion"` // 改进建议 - ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题) - ScoringRationale string `json:"scoring_rationale"` // 评分依据 + Score float64 `json:"score"` // 得分 (0-100) + IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确) + Feedback string `json:"feedback"` // 评语 + Suggestion string `json:"suggestion"` // 改进建议 + ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题) + ScoringRationale string `json:"scoring_rationale"` // 评分依据 } // GradeEssay 对论述题进行AI评分(不需要标准答案) @@ -275,7 +275,6 @@ func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, que 1. **必须基于保密法规**:解析时必须引用相关法规条文,说明依据哪些具体法律法规 2. **必须实事求是**:只基于题目内容、标准答案和实际法规进行解析 3. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造法规条文 -4. **使用Markdown格式**:使用标题、列表、加粗等markdown语法使内容更清晰易读 解析内容要求: - **知识点**:说明题目考查的核心知识点,指出涉及哪些保密法规 diff --git a/pkg/config/config.go b/pkg/config/config.go index fa3d2d5..b192a7b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -78,7 +78,7 @@ func (c *DatabaseConfig) GetDSN() string { // GetAIConfig 获取AI服务配置 // 优先使用环境变量,如果没有设置则使用默认值 func GetAIConfig() *AIConfig { - baseURL := getEnv("AI_BASE_URL", "https://ai.yuchat.top") + baseURL := getEnv("AI_BASE_URL", "http://172.20.0.117") apiKey := getEnv("AI_API_KEY", "sk-OKBmOpJx855juSOPU14cWG6Iz87tZQuv3Xg9PiaJYXdHoKcN") model := getEnv("AI_MODEL", "deepseek-v3") diff --git a/web/src/pages/Home.module.less b/web/src/pages/Home.module.less index 12a03b0..ff49900 100644 --- a/web/src/pages/Home.module.less +++ b/web/src/pages/Home.module.less @@ -8,7 +8,7 @@ .header { display: flex; justify-content: space-between; - align-items: flex-start; + align-items: center; margin-bottom: 24px; .headerLeft { @@ -29,62 +29,196 @@ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08)); } + .titleRow { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 4px; + } + .title { color: #1d1d1f !important; - margin-bottom: 4px !important; + margin-bottom: 0 !important; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.02); font-weight: 700; } + .totalBadge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%); + border: 1px solid #91d5ff; + border-radius: 20px; + font-size: 14px; + font-weight: 600; + color: #0958d9; + line-height: 1; + box-shadow: 0 2px 4px rgba(9, 88, 217, 0.1); + } + .subtitle { color: #6e6e73; font-size: 14px; } .userInfo { - background: rgba(255, 255, 255, 0.85); - padding: 12px 16px; + background: #fff; + padding: 20px; border-radius: 16px; - backdrop-filter: blur(30px) saturate(180%); - -webkit-backdrop-filter: blur(30px) saturate(180%); - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.04), - 0 1px 3px rgba(0, 0, 0, 0.02), - 0 0 0 1px rgba(0, 0, 0, 0.03); - border: 0.5px solid rgba(0, 0, 0, 0.04); - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02); + border: 1px solid rgba(0, 0, 0, 0.06); + transition: box-shadow 0.2s ease; + min-width: 520px; - &:hover { - background: rgba(255, 255, 255, 0.95); - box-shadow: - 0 4px 12px rgba(0, 0, 0, 0.06), - 0 2px 6px rgba(0, 0, 0, 0.04), - 0 0 0 1px rgba(0, 0, 0, 0.04); - transform: translateY(-2px); + .userInfoContent { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + } + + .avatarWrapper { + position: relative; + flex-shrink: 0; + + .userAvatar { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border: 2px solid #fff; + } + + .avatarBadge { + position: absolute; + bottom: -2px; + right: -2px; + width: 22px; + height: 22px; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + font-size: 12px; + + :global { + .anticon { + &.anticon-check-circle { + color: #52c41a; + } + &.anticon-setting { + color: #ff4d4f; + } + } + } + } } .userDetails { display: flex; flex-direction: column; - gap: 3px; + gap: 4px; + flex: 1; + min-width: 0; .userNickname { color: #1d1d1f !important; - font-size: 15px; + font-size: 16px; font-weight: 600; - line-height: 1.2; + line-height: 1.3; } .userUsername { color: #6e6e73 !important; - font-size: 12px; - line-height: 1.2; + font-size: 13px; + line-height: 1.3; } - .userType { - font-size: 11px; - color: #6e6e73; + .userTypeBadge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 12px; + background: linear-gradient(135deg, #e6f7ff 0%, #d6f0ff 100%); + border-radius: 8px; + border: 1px solid #91caff; + width: fit-content; + margin-top: 2px; + + .badgeIcon { + font-size: 12px; + color: #1890ff; + } + + .userTypeText { + font-size: 12px; + color: #1890ff; + font-weight: 500; + line-height: 1; + } + } + } + + .dropdownTrigger { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: transparent; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + + &:hover { + background: #f5f5f5; + } + + &:active { + background: #e8e8e8; + } + + .dropdownIcon { + font-size: 12px; + color: #8c8c8c; + } + } + + .userStatsRow { + display: flex; + align-items: center; + justify-content: space-around; + gap: 12px; + + .statItem { + flex: 1; + text-align: center; + padding: 8px 4px; + border-radius: 8px; + background: #fafafa; + + .statValue { + font-size: 18px; + font-weight: 700; + color: #1890ff; + line-height: 1.3; + margin-bottom: 2px; + } + + .statLabel { + font-size: 11px; + color: #8c8c8c; + font-weight: 500; + } + } + + .statDivider { + width: 1px; + height: 24px; + background: #e8e8e8; } } } @@ -121,6 +255,83 @@ gap: 8px; font-weight: 700; font-size: 18px !important; + + :global { + .anticon { + font-size: 20px; + color: #1890ff; + } + } + } +} + +.progressSection { + margin-bottom: 24px; + + .sectionTitle { + color: #1d1d1f !important; + margin-bottom: 16px !important; + display: flex; + align-items: center; + gap: 8px; + font-weight: 700; + font-size: 18px !important; + } + + .progressCard { + border-radius: 16px; + background: #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02); + border: 1px solid rgba(0, 0, 0, 0.06); + transition: all 0.3s ease; + height: 100%; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + } + + :global { + .ant-card-body { + padding: 20px; + } + } + + .progressCardContent { + display: flex; + align-items: center; + gap: 16px; + } + + .progressIcon { + width: 56px; + height: 56px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + .progressInfo { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + + .progressLabel { + font-size: 12px; + line-height: 1; + } + + .progressValue { + margin: 4px 0 0 0 !important; + font-size: 28px !important; + font-weight: 700; + line-height: 1; + color: #1d1d1f; + } + } } } @@ -146,8 +357,23 @@ 0 0 0 1px rgba(0, 0, 0, 0.04); } + .typeIconWrapper { + width: 64px; + height: 64px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 12px; + border: 1.5px solid; + transition: all 0.3s ease; + } + .typeIcon { - margin-bottom: 10px; + font-size: 32px; + display: flex; + align-items: center; + justify-content: center; } .typeTitle { @@ -173,6 +399,13 @@ gap: 8px; font-weight: 700; font-size: 18px !important; + + :global { + .anticon { + font-size: 20px; + color: #73d13d; + } + } } .quickCard { @@ -197,6 +430,22 @@ 0 0 0 1px rgba(0, 0, 0, 0.04); } } + + .quickIconWrapper { + width: 56px; + height: 56px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 1.5px solid; + transition: all 0.3s ease; + } + + .quickIcon { + font-size: 28px; + } } // 响应式设计 - 移动端 (< 768px) @@ -224,27 +473,104 @@ height: 48px; } + .titleRow { + gap: 8px; + flex-wrap: wrap; + } + .title { font-size: 22px !important; } + .totalBadge { + padding: 3px 8px; + font-size: 12px; + border-radius: 16px; + } + .subtitle { font-size: 13px; } .userInfo { width: 100%; - padding: 8px 12px; + min-width: auto; + padding: 16px; + border-radius: 12px; - :global { - .ant-space { - width: 100%; - justify-content: space-between; + .userInfoContent { + gap: 12px; + margin-bottom: 12px; + padding-bottom: 12px; + } + + .avatarWrapper { + .userAvatar { + width: 48px !important; + height: 48px !important; } - .ant-btn { - font-size: 12px; - padding: 4px 8px; + .avatarBadge { + width: 20px; + height: 20px; + font-size: 11px; + bottom: -1px; + right: -1px; + } + } + + .userDetails { + gap: 3px; + + .userNickname { + font-size: 15px !important; + } + + .userUsername { + font-size: 12px !important; + } + + .userTypeBadge { + padding: 3px 8px; + margin-top: 1px; + + .badgeIcon { + font-size: 10px; + } + + .userTypeText { + font-size: 10px; + } + } + } + + .dropdownTrigger { + width: 24px; + height: 24px; + + .dropdownIcon { + font-size: 11px; + } + } + + .userStatsRow { + gap: 8px; + + .statItem { + padding: 6px 4px; + border-radius: 6px; + + .statValue { + font-size: 16px; + } + + .statLabel { + font-size: 10px; + } + } + + .statDivider { + height: 20px; } } } @@ -273,11 +599,59 @@ } } + .progressSection { + margin-bottom: 16px; + + .sectionTitle { + font-size: 16px !important; + margin-bottom: 12px !important; + } + + .progressCard { + border-radius: 12px; + + :global { + .ant-card-body { + padding: 16px; + } + } + + .progressIcon { + width: 48px; + height: 48px; + border-radius: 10px; + + :global { + svg { + font-size: 20px !important; + } + } + } + + .progressInfo { + .progressLabel { + font-size: 11px; + } + + .progressValue { + font-size: 22px !important; + } + } + } + } + .typeCard { border-radius: 12px; + .typeIconWrapper { + width: 56px; + height: 56px; + border-radius: 12px; + margin-bottom: 10px; + } + .typeIcon { - font-size: 32px !important; + font-size: 28px; } .typeTitle { @@ -312,6 +686,16 @@ } } } + + .quickIconWrapper { + width: 48px; + height: 48px; + border-radius: 12px; + } + + .quickIcon { + font-size: 24px; + } } } @@ -326,11 +710,38 @@ .title { font-size: 28px !important; } + + .userInfo { + min-width: 450px; + + .avatarWrapper .userAvatar { + width: 52px !important; + height: 52px !important; + } + + .userStatsRow .statItem { + .statValue { + font-size: 17px; + } + } + } } .typeCard { + .typeIconWrapper { + width: 60px; + height: 60px; + border-radius: 14px; + } + .typeIcon { - font-size: 36px !important; + font-size: 30px; + } + } + + .progressSection { + .sectionTitle { + font-size: 17px !important; } } } @@ -354,6 +765,47 @@ .subtitle { font-size: 15px; } + + .userInfo { + min-width: 560px; + + .avatarWrapper .userAvatar { + width: 60px !important; + height: 60px !important; + } + + .userDetails { + .userNickname { + font-size: 17px !important; + } + + .userUsername { + font-size: 14px !important; + } + + .userTypeBadge { + padding: 5px 12px; + + .badgeIcon { + font-size: 12px; + } + + .userTypeText { + font-size: 12px; + } + } + } + + .userStatsRow .statItem { + .statValue { + font-size: 20px; + } + + .statLabel { + font-size: 12px; + } + } + } } .statsCard { @@ -368,6 +820,14 @@ } } + .progressSection { + margin-bottom: 32px; + + .sectionTitle { + font-size: 20px !important; + } + } + .quickStart { .sectionTitle { font-size: 20px !important; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 68d34fd..0d61554 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal, Form, Radio, Alert, Input, Switch, InputNumber, Divider, Badge, Dropdown } from 'antd' +import { Typography, message, Space, Avatar, Button, Modal, Form, Radio, Alert, Input, Switch, InputNumber, Divider, Dropdown, Row, Col, Card } from 'antd' import type { MenuProps } from 'antd' import { FileTextOutlined, @@ -15,7 +15,10 @@ import { UnorderedListOutlined as ListOutlined, LockOutlined, IdcardOutlined, - DownOutlined, + MoreOutlined, + CloseCircleOutlined, + FormOutlined, + FileMarkdownOutlined, } from '@ant-design/icons' import * as questionApi from '../api/question' import { fetchWithAuth } from '../utils/request' @@ -24,48 +27,48 @@ import styles from './Home.module.less' const { Title, Paragraph, Text } = Typography -// 题型配置 - 使用数据库中的实际类型 +// 题型配置 - 使用数据库中的实际类型,采用明快的配色方案 const questionTypes = [ { key: 'multiple-choice', title: '选择题', icon: , - color: '#1677ff', + color: '#1890ff', // 明亮的蓝色 description: '基础知识考察', }, { key: 'multiple-selection', title: '多选题', icon: , - color: '#52c41a', + color: '#52c41a', // 明亮的绿色 description: '综合能力提升', }, { key: 'true-false', title: '判断题', - icon: , - color: '#fa8c16', + icon: , + color: '#fa8c16', // 明亮的橙色 description: '快速判断训练', }, { key: 'fill-in-blank', title: '填空题', - icon: , - color: '#722ed1', + icon: , + color: '#faad14', // 明亮的金色 description: '填空补充练习', }, { key: 'short-answer', title: '简答题', icon: , - color: '#eb2f96', + color: '#722ed1', // 明亮的紫色 description: '深度理解练习', }, { key: 'essay', // 特殊标识,根据用户类型动态路由 title: '论述题', - icon: , - color: '#f759ab', + icon: , + color: '#eb2f96', // 明亮的粉色 description: '深度分析与表达', }, ] @@ -93,6 +96,7 @@ const Home: React.FC = () => { answered_questions: 0, correct_answers: 0, wrong_questions: 0, + total_answers: 0, accuracy: 0, }) @@ -360,75 +364,74 @@ const Home: React.FC = () => {
AnKao Logo
- AnKao 刷题 +
+ AnKao 刷题 + {statistics.total_questions} 题 +
安全保密考试题库
{/* 用户信息 */} {userInfo && ( - -
- +
+
+
} + className={styles.userAvatar} /> -
- {userInfo.nickname} - @{userInfo.username} - - {getUserTypeText(userInfo.user_type)} - - } - /> +
+ {userInfo.user_type ? ( + + ) : ( + + )}
- - +
+
+ {userInfo.nickname} + @{userInfo.username} +
+ + + {getUserTypeText(userInfo.user_type)} + +
+
+ +
+ +
+
- +
+
+
{statistics.total_answers}
+
刷题次数
+
+
+
+
{statistics.answered_questions}
+
已刷
+
+
+
+
{statistics.wrong_questions}
+
错题本数
+
+
+
+
{statistics.accuracy.toFixed(0)}%
+
准确率
+
+
+
)}
- {/* 统计卡片 */} - - - - - - - - - - - - - - - - - {/* 题型选择 */}
@@ -448,8 +451,16 @@ const Home: React.FC = () => { } }} > - <div className={styles.typeIcon} style={{ color: type.color, fontSize: '40px' }}> - {type.icon} + <div + className={styles.typeIconWrapper} + style={{ + background: `linear-gradient(135deg, ${type.color}15 0%, ${type.color}08 100%)`, + borderColor: `${type.color}30` + }} + > + <div className={styles.typeIcon} style={{ color: type.color }}> + {type.icon} + </div> </div> <Title level={5} className={styles.typeTitle}>{type.title} {type.description} @@ -472,8 +483,14 @@ const Home: React.FC = () => { onClick={() => navigate('/wrong-questions')} > -
- +
+
错题本 @@ -490,8 +507,14 @@ const Home: React.FC = () => { onClick={() => navigate('/question-list')} > -
- +
+
题目列表 @@ -510,8 +533,14 @@ const Home: React.FC = () => { onClick={() => navigate('/question-management')} > -
- +
+
题库管理 diff --git a/web/src/types/question.ts b/web/src/types/question.ts index bdb9f1a..aba8925 100644 --- a/web/src/types/question.ts +++ b/web/src/types/question.ts @@ -48,6 +48,7 @@ export interface Statistics { answered_questions: number correct_answers: number wrong_questions: number + total_answers: number // 刷题次数 accuracy: number }