AnCao/web/src/pages/Home.tsx
yanlongqi ea051e9380 添加AI评分系统和题目列表功能
新增功能:
1. AI智能评分系统
   - 集成OpenAI兼容API进行简答题评分
   - 提供分数、评语和改进建议
   - 支持自定义AI服务配置(BaseURL、APIKey、Model)

2. 题目列表页面
   - 展示所有题目和答案
   - Tab标签页形式的题型筛选(选择题、多选题、判断题、填空题、简答题)
   - 关键词搜索功能(支持题目内容和编号搜索)
   - 填空题特殊渲染:****显示为下划线
   - 判断题不显示选项,界面更简洁

3. UI优化
   - 答题结果组件重构,支持AI评分显示
   - 首页新增"题目列表"快速入口
   - 响应式设计,适配移动端和PC端

技术改进:
- 添加AI评分服务层(internal/services/ai_grading.go)
- 扩展题目模型支持AI评分结果
- 更新配置管理支持AI服务配置
- 添加AI评分测试脚本和文档

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 13:36:30 +08:00

324 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal } from 'antd'
import {
FileTextOutlined,
CheckCircleOutlined,
UnorderedListOutlined,
EditOutlined,
RocketOutlined,
BookOutlined,
UserOutlined,
LogoutOutlined,
SettingOutlined,
UnorderedListOutlined as ListOutlined,
} from '@ant-design/icons'
import * as questionApi from '../api/question'
import type { Statistics } from '../types/question'
import styles from './Home.module.less'
const { Title, Paragraph, Text } = Typography
// 题型配置 - 使用数据库中的实际类型
const questionTypes = [
{
key: 'multiple-choice',
title: '选择题',
icon: <CheckCircleOutlined />,
color: '#1677ff',
description: '基础知识考察',
},
{
key: 'multiple-selection',
title: '多选题',
icon: <UnorderedListOutlined />,
color: '#52c41a',
description: '综合能力提升',
},
{
key: 'true-false',
title: '判断题',
icon: <CheckCircleOutlined />,
color: '#fa8c16',
description: '快速判断训练',
},
{
key: 'fill-in-blank',
title: '填空题',
icon: <FileTextOutlined />,
color: '#722ed1',
description: '填空补充练习',
},
{
key: 'short-answer',
title: '简答题',
icon: <EditOutlined />,
color: '#eb2f96',
description: '深度理解练习',
},
]
interface UserInfo {
username: string
nickname: string
avatar: string
}
const Home: React.FC = () => {
const navigate = useNavigate()
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [statistics, setStatistics] = useState<Statistics>({
total_questions: 0,
answered_questions: 0,
correct_answers: 0,
wrong_questions: 0,
accuracy: 0,
})
// 加载统计数据
const loadStatistics = async () => {
try {
const res = await questionApi.getStatistics()
if (res.success && res.data) {
setStatistics(res.data)
}
} catch (error) {
console.error('加载统计失败:', error)
}
}
// 加载用户信息
useEffect(() => {
const token = localStorage.getItem('token')
const savedUserInfo = localStorage.getItem('user')
if (token && savedUserInfo) {
try {
setUserInfo(JSON.parse(savedUserInfo))
} catch (e) {
console.error('解析用户信息失败', e)
}
}
}, [])
useEffect(() => {
loadStatistics()
}, [])
// 点击题型卡片
const handleTypeClick = async (type: string) => {
try {
// 加载该题型的题目列表
const res = await questionApi.getQuestions({ type })
if (res.success && res.data && res.data.length > 0) {
// 跳转到答题页面,并传递题型参数
navigate(`/question?type=${type}`)
} else {
message.warning('该题型暂无题目')
}
} catch (error) {
message.error('加载题目失败')
}
}
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: '确定要退出登录吗?',
onOk: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUserInfo(null)
message.success('已退出登录')
navigate('/login')
},
})
}
return (
<div className={styles.container}>
{/* 头部 */}
<div className={styles.header}>
<div className={styles.headerLeft}>
<Title level={2} className={styles.title}>AnKao </Title>
<Paragraph className={styles.subtitle}></Paragraph>
</div>
{/* 用户信息 */}
{userInfo && (
<div className={styles.userInfo}>
<Space size="middle">
<Avatar
src={userInfo.avatar || undefined}
size={40}
icon={<UserOutlined />}
/>
<div className={styles.userDetails}>
<Text strong className={styles.userNickname}>{userInfo.nickname}</Text>
<Text type="secondary" className={styles.userUsername}>@{userInfo.username}</Text>
</div>
<Button
type="text"
danger
icon={<LogoutOutlined />}
onClick={handleLogout}
>
退
</Button>
</Space>
</div>
)}
</div>
{/* 统计卡片 */}
<Card className={styles.statsCard}>
<Row gutter={[16, 16]}>
<Col xs={12} sm={12} md={6}>
<Statistic
title="题库总数"
value={statistics.total_questions}
valueStyle={{ color: '#1677ff', fontSize: '24px' }}
/>
</Col>
<Col xs={12} sm={12} md={6}>
<Statistic
title="已答题数"
value={statistics.answered_questions}
valueStyle={{ color: '#52c41a', fontSize: '24px' }}
/>
</Col>
<Col xs={12} sm={12} md={6}>
<Statistic
title="错题数量"
value={statistics.wrong_questions}
valueStyle={{ color: '#ff4d4f', fontSize: '24px' }}
/>
</Col>
<Col xs={12} sm={12} md={6}>
<Statistic
title="正确率"
value={statistics.accuracy.toFixed(0)}
suffix="%"
valueStyle={{ color: '#fa8c16', fontSize: '24px' }}
/>
</Col>
</Row>
</Card>
{/* 题型选择 */}
<div className={styles.typeSection}>
<Title level={4} className={styles.sectionTitle}>
<FileTextOutlined />
</Title>
<Row gutter={[12, 12]}>
{questionTypes.map(type => (
<Col xs={12} sm={12} md={8} lg={6} xl={4} key={type.key}>
<Card
hoverable
className={styles.typeCard}
onClick={() => handleTypeClick(type.key)}
styles={{
body: {
textAlign: 'center',
padding: '20px 12px',
}
}}
>
<div className={styles.typeIcon} style={{ color: type.color, fontSize: '40px' }}>
{type.icon}
</div>
<Title level={5} className={styles.typeTitle}>{type.title}</Title>
<Paragraph type="secondary" className={styles.typeDesc}>{type.description}</Paragraph>
</Card>
</Col>
))}
</Row>
</div>
{/* 快速开始 */}
<div className={styles.quickStart}>
<Title level={4} className={styles.sectionTitle}>
<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
className={styles.quickCard}
onClick={() => navigate('/wrong-questions')}
>
<Space align="center" size="middle" style={{ width: '100%' }}>
<div className={styles.quickIcon}>
<BookOutlined style={{ fontSize: '32px', color: '#ff4d4f' }} />
</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
className={styles.quickCard}
onClick={() => navigate('/question-list')}
>
<Space align="center" size="middle" style={{ width: '100%' }}>
<div className={styles.quickIcon}>
<ListOutlined style={{ fontSize: '32px', color: '#1677ff' }} />
</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>
{/* 仅 yanlongqi 用户显示题库管理 */}
{userInfo?.username === 'yanlongqi' && (
<Col xs={24} sm={24} md={12} lg={8}>
<Card
hoverable
className={styles.quickCard}
onClick={() => navigate('/question-management')}
>
<Space align="center" size="middle" style={{ width: '100%' }}>
<div className={styles.quickIcon}>
<SettingOutlined style={{ fontSize: '32px', color: '#13c2c2' }} />
</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>
)}
</Row>
</div>
</div>
)
}
export default Home