重构题目练习模式,优化用户体验
主要变更: - 移除独立的随机题目 API 和快速开始卡片 - 添加应用图标 (icon.svg) 和品牌标识 - 优化首页布局,添加 logo 和"安全保密考试题库"标语 - 将随机模式改为答题页面内的可选开关(默认关闭) - 改进错题练习逻辑,单独处理随机错题功能 - 同步更新 README.md 文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ac3249a4a4
commit
b30647d81b
@ -64,7 +64,6 @@ yarn dev
|
||||
|
||||
#### 练习题相关
|
||||
- `GET /api/practice/questions` - 获取练习题目列表 (支持分页和类型过滤)
|
||||
- `GET /api/practice/questions/random` - 获取随机练习题目
|
||||
- `GET /api/practice/questions/:id` - 获取指定练习题目
|
||||
- `POST /api/practice/submit` - 提交练习答案 (简答题自动AI评分)
|
||||
- `GET /api/practice/types` - 获取题型列表
|
||||
@ -224,7 +223,7 @@ yarn build
|
||||
## 页面结构
|
||||
|
||||
- **登录页** (`/login`) - 用户登录和注册,支持密码可见性切换
|
||||
- **首页** (`/`) - 题目练习、随机题目、题目列表、筛选等功能
|
||||
- **首页** (`/`) - 题型选择、错题本、题目列表等功能
|
||||
- **我的** (`/profile`) - 用户信息、退出登录
|
||||
|
||||
## 特性
|
||||
@ -238,7 +237,6 @@ yarn build
|
||||
- 密码bcrypt加密存储
|
||||
- 练习题管理系统(236道练习题,5种题型)
|
||||
- 支持分页查询和题型筛选
|
||||
- 随机题目推送功能
|
||||
- **AI智能评分系统** - 使用deepseek-v3对简答题进行智能评分和反馈
|
||||
|
||||
### 前端特性
|
||||
|
||||
1
main.go
1
main.go
@ -44,7 +44,6 @@ func main() {
|
||||
|
||||
// 练习题相关API(需要登录)
|
||||
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表
|
||||
auth.GET("/practice/questions/random", handlers.GetRandomPracticeQuestion) // 获取随机练习题目
|
||||
auth.GET("/practice/questions/:id", handlers.GetPracticeQuestionByID) // 获取指定练习题目
|
||||
auth.POST("/practice/explain", handlers.ExplainQuestion) // 生成题目解析(AI)
|
||||
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="description" content="AnKao 移动端应用" />
|
||||
<title>AnKao</title>
|
||||
<meta name="description" content="AnKao - 安全保密考试系统" />
|
||||
<meta name="keywords" content="安全考试,保密考试,在线答题,考试系统" />
|
||||
<meta name="theme-color" content="#1890ff" />
|
||||
<title>AnKao - 安全保密考试</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
117
web/public/icon.svg
Normal file
117
web/public/icon.svg
Normal file
@ -0,0 +1,117 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<defs>
|
||||
<linearGradient id="shieldGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1890ff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#096dd9;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="lockGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#52c41a;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#389e0d;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="2" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.3"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 背景圆形 -->
|
||||
<circle cx="100" cy="100" r="95" fill="#f0f5ff" stroke="#d6e4ff" stroke-width="2"/>
|
||||
|
||||
<!-- 主盾牌形状 -->
|
||||
<path d="M 100 25
|
||||
C 80 25, 60 30, 45 40
|
||||
L 45 85
|
||||
C 45 120, 65 145, 100 165
|
||||
C 135 145, 155 120, 155 85
|
||||
L 155 40
|
||||
C 140 30, 120 25, 100 25 Z"
|
||||
fill="url(#shieldGradient)"
|
||||
stroke="#0050b3"
|
||||
stroke-width="2.5"
|
||||
filter="url(#shadow)"/>
|
||||
|
||||
<!-- 盾牌内部高光 -->
|
||||
<path d="M 100 30
|
||||
C 82 30, 65 34, 52 42
|
||||
L 52 85
|
||||
C 52 115, 70 138, 100 156"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.4)"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"/>
|
||||
|
||||
<!-- 文档/试卷图标 -->
|
||||
<rect x="75" y="60" width="50" height="65" rx="3" ry="3"
|
||||
fill="#ffffff"
|
||||
stroke="#0050b3"
|
||||
stroke-width="2"/>
|
||||
|
||||
<!-- 试卷标题线 -->
|
||||
<line x1="82" y1="70" x2="118" y2="70"
|
||||
stroke="#1890ff"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"/>
|
||||
|
||||
<!-- 试卷内容线条 -->
|
||||
<line x1="82" y1="80" x2="112" y2="80"
|
||||
stroke="#8cc5ff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"/>
|
||||
<line x1="82" y1="88" x2="115" y2="88"
|
||||
stroke="#8cc5ff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"/>
|
||||
<line x1="82" y1="96" x2="108" y2="96"
|
||||
stroke="#8cc5ff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"/>
|
||||
|
||||
<!-- 锁的主体 -->
|
||||
<rect x="88" y="105" width="24" height="15" rx="2" ry="2"
|
||||
fill="url(#lockGradient)"
|
||||
stroke="#237804"
|
||||
stroke-width="1.5"/>
|
||||
|
||||
<!-- 锁的U形环 -->
|
||||
<path d="M 93 105
|
||||
L 93 98
|
||||
A 7 7 0 0 1 107 98
|
||||
L 107 105"
|
||||
fill="none"
|
||||
stroke="url(#lockGradient)"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"/>
|
||||
|
||||
<!-- 锁孔 -->
|
||||
<circle cx="100" cy="110" r="2" fill="#ffffff"/>
|
||||
<rect x="99" y="110" width="2" height="4" rx="1" fill="#ffffff"/>
|
||||
|
||||
<!-- 装饰性星星(表示重要性) -->
|
||||
<path d="M 135 45 l 2 6 l 6 1 l -5 4 l 2 6 l -5 -3 l -5 3 l 2 -6 l -5 -4 l 6 -1 Z"
|
||||
fill="#faad14"
|
||||
stroke="#d48806"
|
||||
stroke-width="0.5"
|
||||
opacity="0.9"/>
|
||||
|
||||
<!-- 感叹号(警示标志) -->
|
||||
<g opacity="0.9">
|
||||
<rect x="140" y="130" width="3.5" height="15" rx="1.5" fill="#ff4d4f"/>
|
||||
<circle cx="141.75" cy="148" r="2" fill="#ff4d4f"/>
|
||||
</g>
|
||||
|
||||
<!-- 对勾标记(考试通过) -->
|
||||
<path d="M 60 135 L 65 142 L 75 128"
|
||||
fill="none"
|
||||
stroke="#52c41a"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
opacity="0.85"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@ -6,11 +6,6 @@ export const getQuestions = (params?: { type?: string; search?: string }) => {
|
||||
return request.get<ApiResponse<Question[]>>('/practice/questions', { params })
|
||||
}
|
||||
|
||||
// 获取随机题目
|
||||
export const getRandomQuestion = () => {
|
||||
return request.get<ApiResponse<Question>>('/practice/questions/random')
|
||||
}
|
||||
|
||||
// 获取指定题目
|
||||
export const getQuestionById = (id: number) => {
|
||||
return request.get<ApiResponse<Question>>(`/practice/questions/${id}`)
|
||||
|
||||
@ -15,6 +15,20 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.logoArea {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #1d1d1f !important;
|
||||
margin-bottom: 4px !important;
|
||||
@ -183,6 +197,15 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.logoArea {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
@ -200,8 +200,13 @@ const Home: React.FC = () => {
|
||||
{/* 头部 */}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<div className={styles.logoArea}>
|
||||
<img src="/icon.svg" alt="AnKao Logo" className={styles.logo} />
|
||||
<div>
|
||||
<Title level={2} className={styles.title}>AnKao 刷题</Title>
|
||||
<Paragraph className={styles.subtitle}>选择题型开始练习</Paragraph>
|
||||
<Paragraph className={styles.subtitle}>安全保密考试题库</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 用户信息 */}
|
||||
{userInfo && (
|
||||
@ -300,24 +305,6 @@ const Home: React.FC = () => {
|
||||
<RocketOutlined /> 快速开始
|
||||
</Title>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card
|
||||
hoverable
|
||||
className={styles.quickCard}
|
||||
onClick={() => navigate('/question')}
|
||||
>
|
||||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||||
<div className={styles.quickIcon}>
|
||||
<RocketOutlined style={{ fontSize: '32px', color: '#722ed1' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Title level={5} style={{ margin: 0 }}>随机练习</Title>
|
||||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>从所有题型中随机抽取</Paragraph>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Card
|
||||
hoverable
|
||||
|
||||
@ -48,6 +48,12 @@ const QuestionPage: React.FC = () => {
|
||||
return saved !== null ? parseInt(saved, 10) : 2;
|
||||
});
|
||||
|
||||
// 随机题目开关(默认关闭)
|
||||
const [randomMode, setRandomMode] = useState(() => {
|
||||
const saved = localStorage.getItem('randomModeEnabled');
|
||||
return saved !== null ? saved === 'true' : false;
|
||||
});
|
||||
|
||||
// 切换自动跳转开关
|
||||
const toggleAutoNext = () => {
|
||||
const newValue = !autoNext;
|
||||
@ -55,6 +61,13 @@ const QuestionPage: React.FC = () => {
|
||||
localStorage.setItem('autoNextEnabled', String(newValue));
|
||||
};
|
||||
|
||||
// 切换随机模式开关
|
||||
const toggleRandomMode = () => {
|
||||
const newValue = !randomMode;
|
||||
setRandomMode(newValue);
|
||||
localStorage.setItem('randomModeEnabled', String(newValue));
|
||||
};
|
||||
|
||||
// 修改自动跳转延迟时间
|
||||
const handleDelayChange = (value: number | null) => {
|
||||
if (value !== null && value >= 1 && value <= 10) {
|
||||
@ -121,16 +134,11 @@ const QuestionPage: React.FC = () => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 加载随机题目
|
||||
const loadRandomQuestion = async () => {
|
||||
// 加载随机错题
|
||||
const loadRandomWrongQuestion = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 检查是否是错题练习模式
|
||||
const mode = searchParams.get("mode");
|
||||
const res =
|
||||
mode === "wrong"
|
||||
? await questionApi.getRandomWrongQuestion()
|
||||
: await questionApi.getRandomQuestion();
|
||||
const res = await questionApi.getRandomWrongQuestion();
|
||||
|
||||
if (res.success && res.data) {
|
||||
setCurrentQuestion(res.data);
|
||||
@ -258,7 +266,16 @@ const QuestionPage: React.FC = () => {
|
||||
// 下一题
|
||||
const handleNext = () => {
|
||||
if (allQuestions.length > 0) {
|
||||
// 检查是否完成所有题目
|
||||
let nextIndex: number;
|
||||
|
||||
// 随机模式:从题库中随机选择一题
|
||||
if (randomMode) {
|
||||
// 生成一个不等于当前索引的随机索引
|
||||
do {
|
||||
nextIndex = Math.floor(Math.random() * allQuestions.length);
|
||||
} while (nextIndex === currentIndex && allQuestions.length > 1);
|
||||
} else {
|
||||
// 顺序模式:检查是否完成所有题目
|
||||
if (currentIndex + 1 >= allQuestions.length) {
|
||||
// 显示统计摘要
|
||||
setShowSummary(true);
|
||||
@ -266,8 +283,9 @@ const QuestionPage: React.FC = () => {
|
||||
localStorage.removeItem(getStorageKey());
|
||||
return;
|
||||
}
|
||||
nextIndex = currentIndex + 1;
|
||||
}
|
||||
|
||||
const nextIndex = currentIndex + 1;
|
||||
setCurrentIndex(nextIndex);
|
||||
setCurrentQuestion(allQuestions[nextIndex]);
|
||||
setSelectedAnswer(
|
||||
@ -310,7 +328,7 @@ const QuestionPage: React.FC = () => {
|
||||
|
||||
// 错题练习模式
|
||||
if (mode === "wrong") {
|
||||
loadRandomQuestion();
|
||||
loadRandomWrongQuestion();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -462,6 +480,24 @@ const QuestionPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 500 }}>随机题目</span>
|
||||
<span style={{ marginLeft: 8, fontSize: 13, color: '#8c8c8c' }}>
|
||||
开启后点击下一题时随机跳转
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||||
<Switch
|
||||
checked={randomMode}
|
||||
onChange={toggleRandomMode}
|
||||
/>
|
||||
<span style={{ fontSize: 14, color: randomMode ? '#52c41a' : '#8c8c8c' }}>
|
||||
{randomMode ? '已开启' : '已关闭'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@ -277,7 +277,6 @@ const QuestionList: React.FC = () => {
|
||||
<List
|
||||
dataSource={filteredQuestions}
|
||||
renderItem={(question, index) => {
|
||||
const typeConfig = questionTypeConfig[question.type]
|
||||
return (
|
||||
<Card
|
||||
key={question.id}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user