AnCao/web/src/pages/UserDetail.tsx
yanlongqi 3ecc1c6a18 添加用户管理功能
新增功能:
- 用户管理页面:展示所有用户及答题统计
- 用户详情页面:查看单个用户的详细答题数据
- 管理员权限中间件:仅yanlongqi用户可访问
- 后端API接口:用户列表和详情统计

后端更新:
- 新增 admin_handler.go:用户管理相关处理器
- 新增 admin.go 中间件:管理员权限验证
- 新增 user_stats.go 模型:用户统计数据结构
- 更新 main.go:注册用户管理API路由

前端更新:
- 新增 UserManagement 页面:用户列表和统计卡片
- 新增 UserDetail 页面:用户详细信息和题型统计
- 更新 Home 页面:添加用户管理入口(仅管理员可见)
- 更新 App.tsx:添加用户管理路由和权限保护
- 更新 API 接口:添加用户管理相关接口定义

UI优化:
- 用户管理页面标题居中显示,参考错题本设计
- 统计卡片使用16px padding
- 返回按钮使用绝对定位,标题居中
- 字体大小统一为18px,字重700

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 06:21:15 +08:00

292 lines
8.9 KiB
TypeScript

import React, { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
Card,
Button,
Typography,
Space,
message,
Tag,
Row,
Col,
Avatar,
Descriptions,
Progress,
Table,
Spin,
} from 'antd'
import {
ArrowLeftOutlined,
UserOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
} from '@ant-design/icons'
import * as questionApi from '../api/question'
import type { UserDetailStats } from '../api/question'
import styles from './UserDetail.module.less'
const { Title, Text } = Typography
const UserDetail: React.FC = () => {
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const [loading, setLoading] = useState(false)
const [userDetail, setUserDetail] = useState<UserDetailStats | null>(null)
// 加载用户详情
const loadUserDetail = async () => {
if (!id) return
try {
setLoading(true)
const res = await questionApi.getUserDetailStats(Number(id))
if (res.success && res.data) {
setUserDetail(res.data)
}
} catch (error: any) {
console.error('加载用户详情失败:', error)
if (error.response?.status === 403) {
message.error('无权访问')
navigate('/user-management')
} else if (error.response?.status === 401) {
message.error('请先登录')
navigate('/login')
} else {
message.error('加载用户详情失败')
}
} finally {
setLoading(false)
}
}
useEffect(() => {
loadUserDetail()
}, [id])
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
// 获取用户类型显示文本
const getUserTypeText = (type?: string) => {
if (!type) return '未设置'
return type === 'ordinary-person' ? '普通涉密人员' : '保密管理人员'
}
if (loading) {
return (
<div className={styles.loadingContainer}>
<Spin size="large" tip="加载中..." />
</div>
)
}
if (!userDetail) {
return null
}
const { user_info, type_stats } = userDetail
return (
<div className={styles.container}>
{/* 返回按钮 */}
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/user-management')}
className={styles.backButton}
>
</Button>
{/* 用户信息卡片 */}
<Card className={styles.userInfoCard}>
<div className={styles.userHeader}>
<Avatar
size={80}
src={user_info.avatar || undefined}
icon={<UserOutlined />}
className={styles.avatar}
/>
<div className={styles.userBasicInfo}>
<Title level={3} style={{ margin: 0 }}>
{user_info.nickname || user_info.username}
</Title>
<Text type="secondary" style={{ fontSize: 16 }}>
@{user_info.username}
</Text>
<Tag
color={user_info.user_type === 'ordinary-person' ? 'blue' : 'green'}
style={{ marginTop: 8 }}
>
{getUserTypeText(user_info.user_type)}
</Tag>
</div>
</div>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginTop: 24 }}>
<Col xs={24} sm={12} md={6}>
<Card className={styles.statCard}>
<div className={styles.statCardContent}>
<Text type="secondary"></Text>
<Text strong style={{ fontSize: 24 }}>
{user_info.total_answers}
</Text>
</div>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card className={styles.statCard}>
<div className={styles.statCardContent}>
<Text type="secondary"></Text>
<Text strong style={{ fontSize: 24, color: '#52c41a' }}>
<CheckCircleOutlined /> {user_info.correct_count}
</Text>
</div>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card className={styles.statCard}>
<div className={styles.statCardContent}>
<Text type="secondary"></Text>
<Text strong style={{ fontSize: 24, color: '#ff4d4f' }}>
<CloseCircleOutlined /> {user_info.wrong_count}
</Text>
</div>
</Card>
</Col>
<Col xs={24} sm={12} md={6}>
<Card className={styles.statCard}>
<div className={styles.statCardContent}>
<Text type="secondary"></Text>
<Text
strong
style={{
fontSize: 24,
color:
user_info.accuracy >= 80
? '#52c41a'
: user_info.accuracy >= 60
? '#1890ff'
: '#faad14',
}}
>
{user_info.accuracy.toFixed(1)}%
</Text>
</div>
</Card>
</Col>
</Row>
{/* 正确率进度条 */}
<div style={{ marginTop: 24 }}>
<Text type="secondary" style={{ marginBottom: 8, display: 'block' }}>
</Text>
<Progress
percent={user_info.accuracy}
strokeColor={
user_info.accuracy >= 80
? '#52c41a'
: user_info.accuracy >= 60
? '#1890ff'
: '#faad14'
}
strokeWidth={12}
format={(percent) => `${percent?.toFixed(1)}%`}
/>
</div>
</Card>
{/* 详细信息 */}
<Card title="详细信息" className={styles.detailCard}>
<Descriptions bordered column={{ xs: 1, sm: 2 }}>
<Descriptions.Item label="用户名">{user_info.username}</Descriptions.Item>
<Descriptions.Item label="姓名">{user_info.nickname || '-'}</Descriptions.Item>
<Descriptions.Item label="用户类型">
<Tag color={user_info.user_type === 'ordinary-person' ? 'blue' : 'green'}>
{getUserTypeText(user_info.user_type)}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="总答题数">{user_info.total_answers}</Descriptions.Item>
<Descriptions.Item label="答对数">
<Tag color="success">{user_info.correct_count}</Tag>
</Descriptions.Item>
<Descriptions.Item label="答错数">
<Tag color="error">{user_info.wrong_count}</Tag>
</Descriptions.Item>
<Descriptions.Item label="注册时间">
{formatDate(user_info.created_at)}
</Descriptions.Item>
<Descriptions.Item label="最后答题">
{formatDate(user_info.last_answer_at)}
</Descriptions.Item>
</Descriptions>
</Card>
{/* 题型统计 */}
{type_stats.length > 0 && (
<Card title="题型统计" className={styles.typeStatsCard}>
<Table
dataSource={type_stats}
rowKey="question_type"
pagination={false}
columns={[
{
title: '题型',
dataIndex: 'question_type_name',
key: 'question_type_name',
render: (text: string) => <Text strong>{text}</Text>,
},
{
title: '答题数',
dataIndex: 'total_answers',
key: 'total_answers',
align: 'center',
sorter: (a, b) => a.total_answers - b.total_answers,
},
{
title: '答对数',
dataIndex: 'correct_count',
key: 'correct_count',
align: 'center',
render: (val: number) => <Tag color="success">{val}</Tag>,
},
{
title: '答错数',
key: 'wrong_count',
align: 'center',
render: (_, record) => (
<Tag color="error">{record.total_answers - record.correct_count}</Tag>
),
},
{
title: '正确率',
dataIndex: 'accuracy',
key: 'accuracy',
align: 'center',
sorter: (a, b) => a.accuracy - b.accuracy,
render: (val: number) => (
<Tag color={val >= 80 ? 'success' : val >= 60 ? 'processing' : 'warning'}>
{val.toFixed(1)}%
</Tag>
),
},
]}
/>
</Card>
)}
</div>
)
}
export default UserDetail