优化UI体验和修复Markdown渲染问题

## 主要改进

### 1. 修复AI解析Markdown渲染混乱
- 安装remark-gfm插件支持GitHub Flavored Markdown
- 优化ReactMarkdown组件样式配置
- 支持表格、代码块、引用、列表等完整Markdown语法
- 改进代码块样式,区分行内代码和代码块
- 优化排版,提升可读性

### 2. 用户界面优化
- 修改个人信息中"昵称"改为"姓名"
- 调整答题设置提示位置到第一行
- 去除答题设置提示标题,使界面更简洁

### 3. 新增用户功能
- 添加修改个人信息功能(姓名、身份类型)
- 添加修改密码功能
- 优化用户信息显示和编辑体验

### 4. 技术改进
- 修复Home.tsx中handleLogout函数声明顺序问题
- 添加更完善的Markdown样式定义
- 优化流式渲染体验

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
燕陇琪 2025-11-07 23:14:03 +08:00
parent b30647d81b
commit 45299b2d7e
7 changed files with 911 additions and 51 deletions

View File

@ -244,3 +244,145 @@ func UpdateUserType(c *gin.Context) {
"data": userInfo, "data": userInfo,
}) })
} }
// UpdateProfileRequest 更新用户信息请求
type UpdateProfileRequest struct {
Nickname string `json:"nickname" binding:"required"`
UserType string `json:"user_type" binding:"required,oneof=ordinary-person management-person"`
}
// UpdateProfile 更新用户信息
func UpdateProfile(c *gin.Context) {
var req UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
// 从上下文获取用户信息(由认证中间件设置)
username, exists := c.Get("username")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未授权访问",
})
return
}
db := database.GetDB()
var user models.User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "用户不存在",
})
return
}
// 更新用户信息
user.Nickname = req.Nickname
user.UserType = req.UserType
if err := db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "更新用户信息失败",
"error": err.Error(),
})
return
}
// 返回更新后的用户信息
userInfo := models.UserInfoResponse{
Username: user.Username,
Avatar: user.Avatar,
Nickname: user.Nickname,
UserType: user.UserType,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "用户信息更新成功",
"data": userInfo,
})
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
// ChangePassword 修改密码
func ChangePassword(c *gin.Context) {
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误新密码长度至少为6位",
"error": err.Error(),
})
return
}
// 从上下文获取用户信息(由认证中间件设置)
username, exists := c.Get("username")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未授权访问",
})
return
}
db := database.GetDB()
var user models.User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "用户不存在",
})
return
}
// 验证旧密码
if !user.CheckPassword(req.OldPassword) {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "当前密码错误",
})
return
}
// 更新密码
if err := user.HashPassword(req.NewPassword); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "密码加密失败",
"error": err.Error(),
})
return
}
// 清除旧的token强制重新登录
user.Token = ""
if err := db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "密码更新失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "密码修改成功,请重新登录",
})
}

View File

@ -41,6 +41,8 @@ func main() {
{ {
// 用户相关API // 用户相关API
auth.PUT("/user/type", handlers.UpdateUserType) // 更新用户类型 auth.PUT("/user/type", handlers.UpdateUserType) // 更新用户类型
auth.PUT("/user/profile", handlers.UpdateProfile) // 更新用户信息
auth.PUT("/user/password", handlers.ChangePassword) // 修改密码
// 练习题相关API需要登录 // 练习题相关API需要登录
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表 auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表

View File

@ -16,7 +16,8 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^6.21.3" "react-router-dom": "^6.21.3",
"remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.5", "@types/node": "^20.11.5",

View File

@ -2,6 +2,7 @@ import React, { useState } from 'react'
import { Alert, Typography, Card, Space, Progress, Button, Spin } from 'antd' import { Alert, Typography, Card, Space, Progress, Button, Spin } from 'antd'
import { CheckOutlined, CloseOutlined, TrophyOutlined, CommentOutlined, BulbOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons' import { CheckOutlined, CloseOutlined, TrophyOutlined, CommentOutlined, BulbOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { fetchWithAuth } from '../utils/request' import { fetchWithAuth } from '../utils/request'
import type { AnswerResult as AnswerResultType } from '../types/question' import type { AnswerResult as AnswerResultType } from '../types/question'
@ -218,21 +219,175 @@ const AnswerResult: React.FC<AnswerResultProps> = ({
</div> </div>
} }
> >
<div style={{ color: '#595959' }}> <div style={{ color: '#595959', lineHeight: '1.8' }}>
{explanation ? ( {explanation ? (
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{ components={{
// 自定义markdown组件样式 // 自定义markdown组件样式
p: ({ children }) => <p style={{ marginBottom: '0.5em', lineHeight: '1.6' }}>{children}</p>, p: ({ children }) => (
h1: ({ children }) => <h1 style={{ fontSize: '1.5em', marginTop: '0.5em', marginBottom: '0.5em' }}>{children}</h1>, <p style={{
h2: ({ children }) => <h2 style={{ fontSize: '1.3em', marginTop: '0.5em', marginBottom: '0.5em' }}>{children}</h2>, marginBottom: '1em',
h3: ({ children }) => <h3 style={{ fontSize: '1.1em', marginTop: '0.5em', marginBottom: '0.5em' }}>{children}</h3>, lineHeight: '1.8',
ul: ({ children }) => <ul style={{ marginLeft: '1.5em', marginBottom: '0.5em' }}>{children}</ul>, wordWrap: 'break-word',
ol: ({ children }) => <ol style={{ marginLeft: '1.5em', marginBottom: '0.5em' }}>{children}</ol>, whiteSpace: 'pre-wrap'
li: ({ children }) => <li style={{ marginBottom: '0.3em', lineHeight: '1.6' }}>{children}</li>, }}>
code: ({ children }) => <code style={{ backgroundColor: '#f5f5f5', padding: '2px 6px', borderRadius: '3px', fontSize: '0.9em' }}>{children}</code>, {children}
pre: ({ children }) => <pre style={{ backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '4px', overflow: 'auto' }}>{children}</pre>, </p>
blockquote: ({ children }) => <blockquote style={{ borderLeft: '4px solid #1890ff', paddingLeft: '12px', margin: '0.5em 0', color: '#666' }}>{children}</blockquote>, ),
h1: ({ children }) => (
<h1 style={{
fontSize: '1.75em',
fontWeight: 'bold',
marginTop: '1em',
marginBottom: '0.6em',
borderBottom: '2px solid #e8e8e8',
paddingBottom: '0.3em'
}}>
{children}
</h1>
),
h2: ({ children }) => (
<h2 style={{
fontSize: '1.5em',
fontWeight: 'bold',
marginTop: '1em',
marginBottom: '0.5em',
color: '#262626'
}}>
{children}
</h2>
),
h3: ({ children }) => (
<h3 style={{
fontSize: '1.25em',
fontWeight: 'bold',
marginTop: '0.8em',
marginBottom: '0.5em',
color: '#262626'
}}>
{children}
</h3>
),
ul: ({ children }) => (
<ul style={{
marginLeft: '1.5em',
marginBottom: '1em',
paddingLeft: '0.5em',
lineHeight: '1.8'
}}>
{children}
</ul>
),
ol: ({ children }) => (
<ol style={{
marginLeft: '1.5em',
marginBottom: '1em',
paddingLeft: '0.5em',
lineHeight: '1.8'
}}>
{children}
</ol>
),
li: ({ children }) => (
<li style={{
marginBottom: '0.4em',
lineHeight: '1.8'
}}>
{children}
</li>
),
code: ({ children, className }) => {
const isInline = !className
return isInline ? (
<code style={{
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '0.9em',
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
color: '#c7254e'
}}>
{children}
</code>
) : (
<code className={className} style={{
display: 'block',
fontFamily: 'Consolas, Monaco, "Courier New", monospace'
}}>
{children}
</code>
)
},
pre: ({ children }) => (
<pre style={{
backgroundColor: '#f5f5f5',
padding: '12px 16px',
borderRadius: '4px',
overflow: 'auto',
marginBottom: '1em',
border: '1px solid #e8e8e8'
}}>
{children}
</pre>
),
blockquote: ({ children }) => (
<blockquote style={{
borderLeft: '4px solid #1890ff',
paddingLeft: '16px',
margin: '1em 0',
color: '#666',
fontStyle: 'italic',
backgroundColor: '#f0f9ff',
padding: '12px 16px',
borderRadius: '0 4px 4px 0'
}}>
{children}
</blockquote>
),
table: ({ children }) => (
<div style={{ overflowX: 'auto', marginBottom: '1em' }}>
<table style={{
borderCollapse: 'collapse',
width: '100%',
border: '1px solid #e8e8e8'
}}>
{children}
</table>
</div>
),
th: ({ children }) => (
<th style={{
border: '1px solid #e8e8e8',
padding: '8px 12px',
backgroundColor: '#fafafa',
textAlign: 'left',
fontWeight: 'bold'
}}>
{children}
</th>
),
td: ({ children }) => (
<td style={{
border: '1px solid #e8e8e8',
padding: '8px 12px'
}}>
{children}
</td>
),
strong: ({ children }) => (
<strong style={{ fontWeight: 'bold', color: '#262626' }}>{children}</strong>
),
em: ({ children }) => (
<em style={{ fontStyle: 'italic', color: '#595959' }}>{children}</em>
),
hr: () => (
<hr style={{
border: 'none',
borderTop: '1px solid #e8e8e8',
margin: '1.5em 0'
}} />
),
}} }}
> >
{explanation} {explanation}

View File

@ -43,7 +43,7 @@
.userInfo { .userInfo {
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
padding: 10px 16px; padding: 12px 16px;
border-radius: 16px; border-radius: 16px;
backdrop-filter: blur(30px) saturate(180%); backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%); -webkit-backdrop-filter: blur(30px) saturate(180%);
@ -52,21 +52,39 @@
0 1px 3px rgba(0, 0, 0, 0.02), 0 1px 3px rgba(0, 0, 0, 0.02),
0 0 0 1px rgba(0, 0, 0, 0.03); 0 0 0 1px rgba(0, 0, 0, 0.03);
border: 0.5px solid rgba(0, 0, 0, 0.04); border: 0.5px solid rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&: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);
}
.userDetails { .userDetails {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 3px;
.userNickname { .userNickname {
color: #1d1d1f !important; color: #1d1d1f !important;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
line-height: 1.2;
} }
.userUsername { .userUsername {
color: #6e6e73 !important; color: #6e6e73 !important;
font-size: 12px; font-size: 12px;
line-height: 1.2;
}
.userType {
font-size: 11px;
color: #6e6e73;
} }
} }
} }

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal, Form, Radio, Alert } from 'antd' import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal, Form, Radio, Alert, Input, Switch, InputNumber, Divider, Badge, Dropdown } from 'antd'
import type { MenuProps } from 'antd'
import { import {
FileTextOutlined, FileTextOutlined,
CheckCircleOutlined, CheckCircleOutlined,
@ -12,6 +13,9 @@ import {
LogoutOutlined, LogoutOutlined,
SettingOutlined, SettingOutlined,
UnorderedListOutlined as ListOutlined, UnorderedListOutlined as ListOutlined,
LockOutlined,
IdcardOutlined,
DownOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import * as questionApi from '../api/question' import * as questionApi from '../api/question'
import { fetchWithAuth } from '../utils/request' import { fetchWithAuth } from '../utils/request'
@ -77,8 +81,13 @@ const Home: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [userInfo, setUserInfo] = useState<UserInfo | null>(null) const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [userTypeModalVisible, setUserTypeModalVisible] = useState(false) // 用户类型补充模态框 const [userTypeModalVisible, setUserTypeModalVisible] = useState(false) // 用户类型补充模态框
const [editProfileVisible, setEditProfileVisible] = useState(false) // 修改用户信息模态框
const [changePasswordVisible, setChangePasswordVisible] = useState(false) // 修改密码模态框
const [practiceSettingsVisible, setPracticeSettingsVisible] = useState(false) // 答题设置模态框
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [userTypeForm] = Form.useForm() const [userTypeForm] = Form.useForm()
const [editProfileForm] = Form.useForm()
const [changePasswordForm] = Form.useForm()
const [statistics, setStatistics] = useState<Statistics>({ const [statistics, setStatistics] = useState<Statistics>({
total_questions: 0, total_questions: 0,
answered_questions: 0, answered_questions: 0,
@ -87,6 +96,22 @@ const Home: React.FC = () => {
accuracy: 0, accuracy: 0,
}) })
// 答题设置状态
const [autoNext, setAutoNext] = useState(() => {
const saved = localStorage.getItem('autoNextEnabled')
return saved !== null ? saved === 'true' : true
})
const [autoNextDelay, setAutoNextDelay] = useState(() => {
const saved = localStorage.getItem('autoNextDelay')
return saved !== null ? parseInt(saved, 10) : 2
})
const [randomMode, setRandomMode] = useState(() => {
const saved = localStorage.getItem('randomModeEnabled')
return saved !== null ? saved === 'true' : false
})
// 加载统计数据 // 加载统计数据
const loadStatistics = async () => { const loadStatistics = async () => {
try { try {
@ -154,6 +179,152 @@ const Home: React.FC = () => {
} }
} }
// 修改用户信息
const handleEditProfile = () => {
if (userInfo) {
editProfileForm.setFieldsValue({
nickname: userInfo.nickname,
user_type: userInfo.user_type,
})
setEditProfileVisible(true)
}
}
const handleUpdateProfile = async (values: { nickname: string; user_type: string }) => {
setLoading(true)
try {
const response = await fetchWithAuth('/api/user/profile', {
method: 'PUT',
body: JSON.stringify(values),
})
const data = await response.json()
if (data.success) {
// 更新本地存储的用户信息
const user = JSON.parse(localStorage.getItem('user') || '{}')
user.nickname = data.data.nickname
user.user_type = data.data.user_type
localStorage.setItem('user', JSON.stringify(user))
setUserInfo(user)
message.success('个人信息更新成功')
setEditProfileVisible(false)
} else {
message.error(data.message || '更新失败')
}
} catch (err) {
message.error('网络错误,请稍后重试')
console.error('更新用户信息错误:', err)
} finally {
setLoading(false)
}
}
// 修改密码
const handleChangePassword = async (values: { old_password: string; new_password: string }) => {
setLoading(true)
try {
const response = await fetchWithAuth('/api/user/password', {
method: 'PUT',
body: JSON.stringify(values),
})
const data = await response.json()
if (data.success) {
message.success('密码修改成功,请重新登录')
changePasswordForm.resetFields()
setChangePasswordVisible(false)
// 清除登录信息并跳转到登录页
setTimeout(() => {
localStorage.removeItem('token')
localStorage.removeItem('user')
navigate('/login')
}, 1000)
} else {
message.error(data.message || '修改失败')
}
} catch (err) {
message.error('网络错误,请稍后重试')
console.error('修改密码错误:', err)
} finally {
setLoading(false)
}
}
// 答题设置相关函数
const toggleAutoNext = () => {
const newValue = !autoNext
setAutoNext(newValue)
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) {
setAutoNextDelay(value)
localStorage.setItem('autoNextDelay', String(value))
}
}
// 获取用户类型显示文本
const getUserTypeText = (type?: string) => {
if (!type) return '未设置'
return type === 'ordinary-person' ? '普通涉密人员' : '保密管理人员'
}
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: '确定要退出登录吗?',
onOk: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUserInfo(null)
message.success('已退出登录')
navigate('/login')
},
})
}
// 用户菜单项
const userMenuItems: MenuProps['items'] = [
{
key: 'edit-profile',
icon: <IdcardOutlined />,
label: '修改个人信息',
onClick: handleEditProfile,
},
{
key: 'change-password',
icon: <LockOutlined />,
label: '修改密码',
onClick: () => setChangePasswordVisible(true),
},
{
key: 'practice-settings',
icon: <SettingOutlined />,
label: '答题设置',
onClick: () => setPracticeSettingsVisible(true),
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
danger: true,
onClick: handleLogout,
},
]
// 点击题型卡片 // 点击题型卡片
const handleTypeClick = async (type: string) => { const handleTypeClick = async (type: string) => {
try { try {
@ -181,20 +352,6 @@ const Home: React.FC = () => {
} }
} }
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: '确定要退出登录吗?',
onOk: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUserInfo(null)
message.success('已退出登录')
navigate('/login')
},
})
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* 头部 */} {/* 头部 */}
@ -210,27 +367,30 @@ const Home: React.FC = () => {
</div> </div>
{/* 用户信息 */} {/* 用户信息 */}
{userInfo && ( {userInfo && (
<Dropdown menu={{ items: userMenuItems }} trigger={['click']}>
<div className={styles.userInfo}> <div className={styles.userInfo}>
<Space size="middle"> <Space size="middle">
<Avatar <Avatar
src={userInfo.avatar || undefined} src={userInfo.avatar || undefined}
size={40} size={44}
icon={<UserOutlined />} icon={<UserOutlined />}
/> />
<div className={styles.userDetails}> <div className={styles.userDetails}>
<Text strong className={styles.userNickname}>{userInfo.nickname}</Text> <Text strong className={styles.userNickname}>{userInfo.nickname}</Text>
<Text type="secondary" className={styles.userUsername}>@{userInfo.username}</Text> <Text type="secondary" className={styles.userUsername}>@{userInfo.username}</Text>
<Badge
color={userInfo.user_type ? '#52c41a' : '#ff4d4f'}
text={
<Text className={styles.userType}>
{getUserTypeText(userInfo.user_type)}
</Text>
}
/>
</div> </div>
<Button <DownOutlined style={{ fontSize: '12px', color: '#8c8c8c' }} />
type="text"
danger
icon={<LogoutOutlined />}
onClick={handleLogout}
>
退
</Button>
</Space> </Space>
</div> </div>
</Dropdown>
)} )}
</div> </div>
@ -406,6 +566,203 @@ const Home: React.FC = () => {
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
{/* 修改个人信息模态框 */}
<Modal
title="修改个人信息"
open={editProfileVisible}
onCancel={() => setEditProfileVisible(false)}
footer={null}
destroyOnClose
>
<Form
form={editProfileForm}
name="editProfile"
onFinish={handleUpdateProfile}
autoComplete="off"
layout="vertical"
>
<Form.Item
name="nickname"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input prefix={<UserOutlined />} placeholder="请输入姓名" />
</Form.Item>
<Form.Item
name="user_type"
label="身份类型"
rules={[{ required: true, message: '请选择身份类型' }]}
>
<Radio.Group>
<Radio value="ordinary-person"></Radio>
<Radio value="management-person"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setEditProfileVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 修改密码模态框 */}
<Modal
title="修改密码"
open={changePasswordVisible}
onCancel={() => setChangePasswordVisible(false)}
footer={null}
destroyOnClose
>
<Alert
message="密码修改后需要重新登录"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<Form
form={changePasswordForm}
name="changePassword"
onFinish={handleChangePassword}
autoComplete="off"
layout="vertical"
>
<Form.Item
name="old_password"
label="当前密码"
rules={[{ required: true, message: '请输入当前密码' }]}
>
<Input.Password prefix={<LockOutlined />} placeholder="请输入当前密码" />
</Form.Item>
<Form.Item
name="new_password"
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度至少为6位' },
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="请输入新密码至少6位" />
</Form.Item>
<Form.Item
name="confirm_password"
label="确认新密码"
dependencies={['new_password']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('new_password') === value) {
return Promise.resolve()
}
return Promise.reject(new Error('两次输入的密码不一致'))
},
}),
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="请再次输入新密码" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setChangePasswordVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* 答题设置模态框 */}
<Modal
title="答题设置"
open={practiceSettingsVisible}
onCancel={() => setPracticeSettingsVisible(false)}
footer={[
<Button key="close" type="primary" onClick={() => setPracticeSettingsVisible(false)}>
</Button>
]}
width={480}
>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Alert
description="这些设置会在答题页面中生效,帮助您更高效地刷题。"
type="info"
showIcon
/>
<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={autoNext}
onChange={toggleAutoNext}
/>
<span style={{ fontSize: 14, color: autoNext ? '#52c41a' : '#8c8c8c' }}>
{autoNext ? '已开启' : '已关闭'}
</span>
</div>
</div>
{autoNext && (
<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 }}>
<InputNumber
min={1}
max={10}
value={autoNextDelay}
onChange={handleDelayChange}
style={{ width: 80 }}
/>
<span style={{ fontSize: 14 }}></span>
</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> </div>
) )
} }

View File

@ -1338,6 +1338,11 @@ escape-string-regexp@^4.0.0:
resolved "https://mirrors.yuchat.top/repository/npmjs/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" resolved "https://mirrors.yuchat.top/repository/npmjs/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
escape-string-regexp@^5.0.0:
version "5.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-plugin-react-hooks@^4.6.0: eslint-plugin-react-hooks@^4.6.0:
version "4.6.2" version "4.6.2"
resolved "https://mirrors.yuchat.top/repository/npmjs/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz" resolved "https://mirrors.yuchat.top/repository/npmjs/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz"
@ -1922,11 +1927,26 @@ make-dir@^2.1.0:
pify "^4.0.1" pify "^4.0.1"
semver "^5.6.0" semver "^5.6.0"
markdown-table@^3.0.0:
version "3.0.4"
resolved "https://mirrors.yuchat.top/repository/npmjs/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a"
integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==
math-intrinsics@^1.1.0: math-intrinsics@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/math-intrinsics/-/math-intrinsics-1.1.0.tgz" resolved "https://mirrors.yuchat.top/repository/npmjs/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
mdast-util-find-and-replace@^3.0.0:
version "3.0.2"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df"
integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==
dependencies:
"@types/mdast" "^4.0.0"
escape-string-regexp "^5.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
mdast-util-from-markdown@^2.0.0: mdast-util-from-markdown@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a"
@ -1945,6 +1965,71 @@ mdast-util-from-markdown@^2.0.0:
micromark-util-types "^2.0.0" micromark-util-types "^2.0.0"
unist-util-stringify-position "^4.0.0" unist-util-stringify-position "^4.0.0"
mdast-util-gfm-autolink-literal@^2.0.0:
version "2.0.1"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz#abd557630337bd30a6d5a4bd8252e1c2dc0875d5"
integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==
dependencies:
"@types/mdast" "^4.0.0"
ccount "^2.0.0"
devlop "^1.0.0"
mdast-util-find-and-replace "^3.0.0"
micromark-util-character "^2.0.0"
mdast-util-gfm-footnote@^2.0.0:
version "2.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz#7778e9d9ca3df7238cc2bd3fa2b1bf6a65b19403"
integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==
dependencies:
"@types/mdast" "^4.0.0"
devlop "^1.1.0"
mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.0.0"
micromark-util-normalize-identifier "^2.0.0"
mdast-util-gfm-strikethrough@^2.0.0:
version "2.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16"
integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==
dependencies:
"@types/mdast" "^4.0.0"
mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.0.0"
mdast-util-gfm-table@^2.0.0:
version "2.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38"
integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==
dependencies:
"@types/mdast" "^4.0.0"
devlop "^1.0.0"
markdown-table "^3.0.0"
mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.0.0"
mdast-util-gfm-task-list-item@^2.0.0:
version "2.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936"
integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==
dependencies:
"@types/mdast" "^4.0.0"
devlop "^1.0.0"
mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.0.0"
mdast-util-gfm@^3.0.0:
version "3.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz#2cdf63b92c2a331406b0fb0db4c077c1b0331751"
integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==
dependencies:
mdast-util-from-markdown "^2.0.0"
mdast-util-gfm-autolink-literal "^2.0.0"
mdast-util-gfm-footnote "^2.0.0"
mdast-util-gfm-strikethrough "^2.0.0"
mdast-util-gfm-table "^2.0.0"
mdast-util-gfm-task-list-item "^2.0.0"
mdast-util-to-markdown "^2.0.0"
mdast-util-mdx-expression@^2.0.0: mdast-util-mdx-expression@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" resolved "https://mirrors.yuchat.top/repository/npmjs/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096"
@ -2059,6 +2144,85 @@ micromark-core-commonmark@^2.0.0:
micromark-util-symbol "^2.0.0" micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0" micromark-util-types "^2.0.0"
micromark-extension-gfm-autolink-literal@^2.0.0:
version "2.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935"
integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-sanitize-uri "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-extension-gfm-footnote@^2.0.0:
version "2.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750"
integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==
dependencies:
devlop "^1.0.0"
micromark-core-commonmark "^2.0.0"
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-normalize-identifier "^2.0.0"
micromark-util-sanitize-uri "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-extension-gfm-strikethrough@^2.0.0:
version "2.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz#86106df8b3a692b5f6a92280d3879be6be46d923"
integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==
dependencies:
devlop "^1.0.0"
micromark-util-chunked "^2.0.0"
micromark-util-classify-character "^2.0.0"
micromark-util-resolve-all "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-extension-gfm-table@^2.0.0:
version "2.1.1"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz#fac70bcbf51fe65f5f44033118d39be8a9b5940b"
integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==
dependencies:
devlop "^1.0.0"
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-extension-gfm-tagfilter@^2.0.0:
version "2.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57"
integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==
dependencies:
micromark-util-types "^2.0.0"
micromark-extension-gfm-task-list-item@^2.0.0:
version "2.1.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz#bcc34d805639829990ec175c3eea12bb5b781f2c"
integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==
dependencies:
devlop "^1.0.0"
micromark-factory-space "^2.0.0"
micromark-util-character "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-extension-gfm@^3.0.0:
version "3.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b"
integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==
dependencies:
micromark-extension-gfm-autolink-literal "^2.0.0"
micromark-extension-gfm-footnote "^2.0.0"
micromark-extension-gfm-strikethrough "^2.0.0"
micromark-extension-gfm-table "^2.0.0"
micromark-extension-gfm-tagfilter "^2.0.0"
micromark-extension-gfm-task-list-item "^2.0.0"
micromark-util-combine-extensions "^2.0.0"
micromark-util-types "^2.0.0"
micromark-factory-destination@^2.0.0: micromark-factory-destination@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" resolved "https://mirrors.yuchat.top/repository/npmjs/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639"
@ -2838,6 +3002,18 @@ react@^18.2.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
remark-gfm@^4.0.1:
version "4.0.1"
resolved "https://mirrors.yuchat.top/repository/npmjs/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b"
integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==
dependencies:
"@types/mdast" "^4.0.0"
mdast-util-gfm "^3.0.0"
micromark-extension-gfm "^3.0.0"
remark-parse "^11.0.0"
remark-stringify "^11.0.0"
unified "^11.0.0"
remark-parse@^11.0.0: remark-parse@^11.0.0:
version "11.0.0" version "11.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" resolved "https://mirrors.yuchat.top/repository/npmjs/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1"
@ -2859,6 +3035,15 @@ remark-rehype@^11.0.0:
unified "^11.0.0" unified "^11.0.0"
vfile "^6.0.0" vfile "^6.0.0"
remark-stringify@^11.0.0:
version "11.0.0"
resolved "https://mirrors.yuchat.top/repository/npmjs/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3"
integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==
dependencies:
"@types/mdast" "^4.0.0"
mdast-util-to-markdown "^2.0.0"
unified "^11.0.0"
resize-observer-polyfill@^1.5.1: resize-observer-polyfill@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://mirrors.yuchat.top/repository/npmjs/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" resolved "https://mirrors.yuchat.top/repository/npmjs/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"