From eec3ea2238c897ac90a20f8269c37033812b858c Mon Sep 17 00:00:00 2001 From: MomoWen Date: Tue, 13 Jan 2026 23:12:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E9=A1=B5=E9=9D=A2=E5=B0=9D?= =?UTF-8?q?=E8=AF=95=E7=94=A8UI=20UX=E4=BC=98=E5=8C=96=EF=BC=8C=E6=9C=89?= =?UTF-8?q?=E7=82=B9=E6=84=8F=E6=80=9D~?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/__pycache__/core.cpython-311.pyc | Bin 0 -> 13349 bytes src/App.tsx | 2 + src/components/common/GlassCard.tsx | 29 + src/contexts/QuizContext.tsx | 10 +- src/contexts/index.ts | 2 +- src/layouts/AdminLayout.tsx | 170 ++++-- src/pages/BankingLandingPage.tsx | 300 +++++++++++ src/pages/QuizPage.tsx | 244 ++++++--- src/pages/ResultPage.tsx | 26 +- src/pages/admin/AdminDashboardPage.tsx | 504 ++++++------------ src/pages/admin/AdminLoginPage.tsx | 223 ++++---- src/pages/admin/BackupRestorePage.tsx | 20 +- src/pages/admin/ExamSubjectPage.tsx | 29 +- src/pages/admin/ExamTaskPage.tsx | 21 +- src/pages/admin/QuestionCategoryPage.tsx | 10 +- src/pages/admin/QuestionManagePage.tsx | 10 +- src/pages/admin/QuestionTextImportPage.tsx | 23 +- src/pages/admin/QuizConfigPage.tsx | 105 ++-- src/pages/admin/RecordDetailPage.tsx | 94 ++-- src/pages/admin/StatisticsPage.tsx | 73 +-- src/pages/admin/UserGroupManage.tsx | 11 +- src/pages/admin/UserManagePage.tsx | 23 +- src/pages/admin/UserRecordsPage.tsx | 3 +- src/pages/quiz/components/OptionList.tsx | 6 +- src/pages/quiz/components/QuizFooter.tsx | 33 +- src/pages/quiz/components/QuizHeader.tsx | 34 +- src/pages/quiz/components/QuizProgress.tsx | 18 +- 27 files changed, 1228 insertions(+), 795 deletions(-) create mode 100644 .shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc create mode 100644 src/components/common/GlassCard.tsx create mode 100644 src/pages/BankingLandingPage.tsx diff --git a/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc b/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2495f37e2cd8480d1a0e46201d78b68a1332334 GIT binary patch literal 13349 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 /> } /> } /> + } /> {/* 管理端路由 */} } /> diff --git a/src/components/common/GlassCard.tsx b/src/components/common/GlassCard.tsx new file mode 100644 index 0000000..08089c2 --- /dev/null +++ b/src/components/common/GlassCard.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Card, CardProps } from 'antd'; + +interface GlassCardProps extends CardProps { + children: React.ReactNode; + className?: string; +} + +const GlassCard: React.FC = ({ children, className = '', style, ...props }) => { + return ( + + {children} + + ); +}; + +export default GlassCard; diff --git a/src/contexts/QuizContext.tsx b/src/contexts/QuizContext.tsx index 7e1d251..48b01f2 100644 --- a/src/contexts/QuizContext.tsx +++ b/src/contexts/QuizContext.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useState, ReactNode } from 'react'; -interface Question { +export interface QuizQuestion { id: string; content: string; type: 'single' | 'multiple' | 'judgment' | 'text'; @@ -8,13 +8,13 @@ interface Question { answer: string | string[]; analysis?: string; score: number; - createdAt: string; + createdAt?: string; category?: string; } interface QuizContextType { - questions: Question[]; - setQuestions: (questions: Question[]) => void; + questions: QuizQuestion[]; + setQuestions: (questions: QuizQuestion[]) => void; currentQuestionIndex: number; setCurrentQuestionIndex: (index: number) => void; answers: Record; @@ -26,7 +26,7 @@ interface QuizContextType { const QuizContext = createContext(undefined); export const QuizProvider = ({ children }: { children: ReactNode }) => { - const [questions, setQuestions] = useState([]); + const [questions, setQuestions] = useState([]); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [answers, setAnswers] = useState>({}); diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 4d3ca7f..fecbeb8 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,3 +1,3 @@ export { UserProvider, useUser } from './UserContext'; export { AdminProvider, useAdmin } from './AdminContext'; -export { QuizProvider, useQuiz } from './QuizContext'; \ No newline at end of file +export { QuizProvider, useQuiz, type QuizQuestion } from './QuizContext'; diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index c13337a..e29bf26 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Layout, Menu, Button, Avatar, Dropdown, message } from 'antd'; +import { Layout, Menu, Button, Avatar, Dropdown, App, ConfigProvider, theme } from 'antd'; import { useNavigate, useLocation } from 'react-router-dom'; import { DashboardOutlined, @@ -25,6 +25,7 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => { const navigate = useNavigate(); const location = useLocation(); const { admin, clearAdmin } = useAdmin(); + const { message } = App.useApp(); const [collapsed, setCollapsed] = useState(false); const menuItems = [ @@ -91,65 +92,122 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => { ]; return ( - - -
- {collapsed ? ( - 正方形LOGO - ) : ( - 主要LOGO - )} + + + {/* Ambient Background Effects */} +
+
+
- - - - -
-
- - - {children} - -
-
- © {new Date().getFullYear()} Boonlive OA System. All Rights Reserved. + +
+ Logo
-
+ + + + +
+
+ + +
+ {children} +
+
+ +
+ Survey System ©{new Date().getFullYear()} Created by BLV +
+
- + ); }; diff --git a/src/pages/BankingLandingPage.tsx b/src/pages/BankingLandingPage.tsx new file mode 100644 index 0000000..b1b516b --- /dev/null +++ b/src/pages/BankingLandingPage.tsx @@ -0,0 +1,300 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; + +// Icons as components to avoid dependency issues +const MenuIcon = () => ( + +); +const ShieldIcon = () => ( + +); +const LockIcon = () => ( + +); +const ZapIcon = () => ( + +); +const GlobeIcon = () => ( + +); +const ArrowRightIcon = () => ( + +); +const CheckIcon = () => ( + +); +const CreditCardIcon = () => ( + +); + +const BankingLandingPage = () => { + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setScrolled(window.scrollY > 50); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return ( +
+ {/* Background Gradients */} +
+
+
+
+
+ + {/* Navbar */} + + + {/* Hero Section */} +
+
+
+
+
+ + Banking 3.0 is here +
+

+ Banking for the
+ + Digital Age + +

+

+ Experience the future of finance with instant transactions, bank-grade security, and beautiful analytics. No hidden fees, ever. +

+
+ + +
+ +
+
+ {[1,2,3,4].map(i => ( +
+ ))} +
+

Trusted by 100,000+ users

+
+
+ +
+ {/* Glass Card - Main */} +
+
+
+

Total Balance

+

$124,500.80

+
+
+ +
+
+ +
+
+
+
+ +
+
+

Netflix

+

Subscription

+
+
+ -$15.99 +
+ +
+
+
+ +
+
+

Salary

+

Deposit

+
+
+ +$4,250.00 +
+
+ +
+ **** 4582 + VISA +
+
+ + {/* Decorative elements behind card */} +
+
+
+
+
+
+ + {/* Features Grid */} +
+
+
+

Everything you need

+

We've built a platform that handles all your financial needs with precision and style.

+
+ +
+ {[ + { + icon: , + title: "Instant Transfers", + desc: "Send money to anyone, anywhere in the world in seconds. No waiting days." + }, + { + icon: , + title: "Bank-Grade Security", + desc: "Your data is protected by 256-bit encryption and advanced fraud detection." + }, + { + icon: , + title: "Global Spending", + desc: "Spend in over 150 currencies with the real exchange rate and no hidden markups." + } + ].map((feature, idx) => ( +
+
+ {feature.icon} +
+

{feature.title}

+

{feature.desc}

+
+ ))} +
+
+
+ + {/* Security Section */} +
+
+ +
+
+
+
+
+
+
+
+ +
+
+

Security Alert

+

Just now

+
+
+

We noticed a login from a new device. Was this you?

+
+ + +
+
+
+
+ +
+
+ Unbreakable Security +
+

Your money is safe with us

+

+ We use state-of-the-art encryption and biometric verification to ensure your account is impenetrable. +

+
    + {['Biometric Authentication', 'Instant Freeze & Unfreeze', 'Real-time Transaction Alerts', '24/7 Fraud Monitoring'].map((item, i) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+
+
+
+
+ + {/* CTA Section */} +
+
+
+
+ +

Ready to start?

+

+ Join over 100,000 users who are managing their finances smarter, faster, and better. +

+ +
+ + +
+
+
+
+ + {/* Footer */} + +
+ ); +}; + +export default BankingLandingPage; diff --git a/src/pages/QuizPage.tsx b/src/pages/QuizPage.tsx index 163ef7a..a8f7731 100644 --- a/src/pages/QuizPage.tsx +++ b/src/pages/QuizPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useRef, type TouchEventHandler } from 're import { Card, Button, Modal, App } from 'antd'; import { useNavigate, useLocation } from 'react-router-dom'; import { useUser, useQuiz } from '../contexts'; +import type { QuizQuestion } from '../contexts'; import { quizAPI } from '../services/api'; import { questionTypeMap } from '../utils/validation'; import { detectHorizontalSwipe } from '../utils/swipe'; @@ -11,19 +12,10 @@ import { QuizProgress } from './quiz/components/QuizProgress'; import { OptionList } from './quiz/components/OptionList'; import { QuizFooter } from './quiz/components/QuizFooter'; -interface Question { - id: string; - content: string; - type: 'single' | 'multiple' | 'judgment' | 'text'; - options?: string[]; - answer: string | string[]; - analysis?: string; - score: number; - category: string; -} +type Question = QuizQuestion; interface LocationState { - questions?: Question[]; + questions?: QuizQuestion[]; totalScore?: number; timeLimit?: number; subjectId?: string; @@ -456,7 +448,7 @@ const QuizPage = () => { }; return ( -
+
{ taskName={taskName} /> - + {/* Mobile Progress Bar */} +
+ +
-
-
-
-
-
- +
+
+ {/* Left Column: Question Area */} +
+
+ {/* Question Meta */} +
+ + {(currentQuestionIndex + 1).toString().padStart(2, '0')} + + {questionTypeMap[currentQuestion.type]} + + / 共 {questions.length} 题 + {currentQuestion.category && ( - + {currentQuestion.category} )}
+ {/* Question Content */}

{currentQuestion.content}

- handleAnswerChange(currentQuestion.id, val)} - /> + {/* Options */} +
+ handleAnswerChange(currentQuestion.id, val)} + /> +
+ + {/* Desktop Navigation Buttons */} +
+ + + {currentQuestionIndex === questions.length - 1 ? ( + + ) : ( + + )} +
+
+
+ + {/* Right Column: Sidebar (Desktop Only) */} +
+ {/* User Info Card */} +
+
+
+ {user?.name?.[0] || 'U'} +
+
+
{user?.name}
+
考生
+
+
+
+ 已完成 + {answeredCount} / {questions.length} +
+
+ + {/* Answer Sheet Card */} +
+

答题卡

+
+ {questions.map((q, idx) => { + const isCurrent = idx === currentQuestionIndex; + const isAnswered = !!answers[q.id]; + let className = 'h-10 w-10 rounded-lg flex items-center justify-center text-sm font-medium border transition-all duration-200 '; + + if (isCurrent) { + className += 'border-[#008C8C] bg-[#E0F7FA] text-[#006064] ring-2 ring-[#008C8C] ring-offset-2'; + } else if (isAnswered) { + className += 'border-[#008C8C] bg-[#008C8C] text-white'; + } else { + className += 'border-gray-200 text-gray-600 bg-gray-50 hover:border-[#008C8C] hover:bg-white'; + } + + return ( + + ); + })} +
+ +
+
+
+ 已作答 +
+
+
+ 当前 +
+
+
+ 未作答 +
+
+ +
+
+ + {/* Mobile Footer */} +
+ handleSubmit()} + onOpenSheet={() => setAnswerSheetOpen(true)} + answeredCount={answeredCount} + />
- handleSubmit()} - onOpenSheet={() => setAnswerSheetOpen(true)} - answeredCount={answeredCount} - /> - + {/* Mobile Answer Sheet Modal */} { centered width={340} destroyOnClose + className="mobile-sheet-modal" > -
-
- 已答 {answeredCount} / {questions.length} +
+
+ 进度:{answeredCount} / {questions.length}
-
+
{questions.map((q, idx) => { const isCurrent = idx === currentQuestionIndex; const isAnswered = !!answers[q.id]; - let className = 'h-9 w-9 rounded-full flex items-center justify-center text-xs font-medium border transition-colors '; + let className = 'h-10 w-10 rounded-lg flex items-center justify-center text-sm font-medium border transition-colors '; if (isCurrent) { - className += 'border-[#00897B] bg-[#E0F2F1] text-[#00695C]'; + className += 'border-[#008C8C] bg-[#E0F7FA] text-[#006064]'; } else if (isAnswered) { - className += 'border-[#00897B] bg-[#00897B] text-white'; + className += 'border-[#008C8C] bg-[#008C8C] text-white'; } else { - className += 'border-gray-200 text-gray-600 bg-white hover:border-[#00897B]'; + className += 'border-gray-200 text-gray-600 bg-white hover:border-[#008C8C]'; } return ( diff --git a/src/pages/ResultPage.tsx b/src/pages/ResultPage.tsx index 6528af5..8d13371 100644 --- a/src/pages/ResultPage.tsx +++ b/src/pages/ResultPage.tsx @@ -241,7 +241,7 @@ const ResultPage = () => { case '不及格': return 'bg-red-100 text-red-800 border-red-200'; case '合格': - return 'bg-blue-100 text-blue-800 border-blue-200'; + return 'bg-teal-50 text-[#008C8C] border-teal-100'; case '优秀': return 'bg-green-100 text-green-800 border-green-200'; default: @@ -251,26 +251,26 @@ const ResultPage = () => { return ( -
+
{/* 结果概览 */} - -
+ +
-
+
答题完成!您的得分是 {record.totalScore} 分
-
+
{record.status} - + 正确率 {correctRate}% ({record.correctCount}/{record.totalCount})
-
@@ -278,8 +278,8 @@ const ResultPage = () => { {/* 基本信息 */} - -

答题信息

+ +

答题信息

{formatDateTime(record.createdAt)} {record.totalCount} 题 @@ -291,9 +291,9 @@ const ResultPage = () => {
{/* 答案详情 */} - -

答案详情

-
+ +

答案详情

+
{answers.map((answer, index) => (
{ const navigate = useNavigate(); + const { message } = App.useApp(); const [overview, setOverview] = useState(null); const [recentRecords, setRecentRecords] = useState([]); const [taskStats, setTaskStats] = useState([]); @@ -101,405 +103,231 @@ const AdminDashboardPage = () => { const fetchDashboardData = async () => { try { setLoading(true); - const [overviewResponse, recordsResponse, taskStatsResponse] = - await Promise.all([ - adminAPI.getDashboardOverview(), - fetchRecentRecords(), - adminAPI.getAllTaskStats(buildTaskStatsParams(1, taskStatusFilter, endAtRange)), - ]); + const [overviewRes, recentRecordsRes, taskStatsRes] = await Promise.all([ + adminAPI.getDashboardOverview(), + quizAPI.getAllRecords({ page: 1, limit: 10 }), + adminAPI.getAllTaskStats(buildTaskStatsParams(1, taskStatusFilter, endAtRange)), + ]); - setOverview(overviewResponse.data); - setRecentRecords(recordsResponse); - setTaskStats((taskStatsResponse as any).data || []); - if ((taskStatsResponse as any).pagination) { + if (overviewRes.data) setOverview(overviewRes.data); + if (Array.isArray((recentRecordsRes as any).data)) setRecentRecords((recentRecordsRes as any).data); + if (taskStatsRes.data) { + setTaskStats(taskStatsRes.data.list); setTaskStatsPagination({ - page: (taskStatsResponse as any).pagination.page, - limit: (taskStatsResponse as any).pagination.limit, - total: (taskStatsResponse as any).pagination.total, + page: 1, + limit: 5, + total: taskStatsRes.data.total, }); } - } catch (error: any) { - message.error(error.message || '获取数据失败'); + } catch (error) { + console.error('Failed to fetch dashboard data:', error); + message.error('获取仪表盘数据失败'); } finally { setLoading(false); } }; - const fetchTaskStats = async ( - page: number, - next: { status?: '' | 'completed' | 'ongoing' | 'notStarted'; range?: [Dayjs | null, Dayjs | null] | null } = {}, - ) => { + const fetchTaskStats = async (page: number) => { try { - setLoading(true); - const status = next.status ?? taskStatusFilter; - const range = next.range ?? endAtRange; - const response = (await adminAPI.getAllTaskStats(buildTaskStatsParams(page, status, range))) as any; - setTaskStats(response.data || []); - if (response.pagination) { - setTaskStatsPagination({ - page: response.pagination.page, - limit: response.pagination.limit, - total: response.pagination.total, - }); + const res = await adminAPI.getAllTaskStats( + buildTaskStatsParams(page, taskStatusFilter, endAtRange) + ); + if (res.data) { + setTaskStats(res.data.list); + setTaskStatsPagination(prev => ({ ...prev, page, total: res.data.total })); } - } catch (error: any) { - message.error(error.message || '获取考试任务统计失败'); - } finally { - setLoading(false); + } catch (error) { + console.error('Failed to fetch task stats:', error); + message.error('获取任务统计失败'); } }; - const fetchRecentRecords = async () => { - const response = await quizAPI.getAllRecords({ page: 1, limit: 10 }) as any; - return response.data || []; + const handleTaskFilterChange = () => { + fetchTaskStats(1); }; - const totalQuestions = - overview?.questionCategoryStats?.reduce((sum, item) => sum + (Number(item.count) || 0), 0) || 0; + useEffect(() => { + handleTaskFilterChange(); + }, [taskStatusFilter, endAtRange]); - const totalTasks = overview - ? Number(overview.taskStatusDistribution.completed || 0) + - Number(overview.taskStatusDistribution.ongoing || 0) + - Number(overview.taskStatusDistribution.notStarted || 0) - : 0; + const COLORS = ['#22D3EE', '#F472B6', '#A78BFA', '#34D399', '#FBBF24', '#60A5FA']; - const getStatusColor = (status: string) => { - switch (status) { - case '不及格': - return 'red'; - case '合格': - return 'blue'; - case '优秀': - return 'green'; - default: - return 'default'; - } -}; - - const columns = [ + const recordColumns = [ { - title: '姓名', + title: '用户', dataIndex: 'userName', key: 'userName', + render: (text: string, record: RecentRecord) => ( + +
+ {text.charAt(0)} +
+
+
{text}
+
{record.userPhone}
+
+
+ ), }, { - title: '手机号', - dataIndex: 'userPhone', - key: 'userPhone', + title: '考试科目', + dataIndex: 'subjectName', + key: 'subjectName', + render: (text: string) => {text || '-'}, }, { title: '得分', - dataIndex: 'totalScore', - key: 'totalScore', - render: (score: number) => {score} 分, + key: 'score', + render: (_: any, record: RecentRecord) => ( + + {record.totalScore} / {record.totalCount} + + ), }, { title: '状态', dataIndex: 'status', key: 'status', render: (status: string) => { - const color = getStatusColor(status); - return {status}; + let color = 'default'; + let bg = 'rgba(255,255,255,0.1)'; + if (status === '优秀') { color = '#34D399'; bg = 'rgba(52, 211, 153, 0.1)'; } + if (status === '合格') { color = '#22D3EE'; bg = 'rgba(34, 211, 238, 0.1)'; } + if (status === '不及格') { color = '#F87171'; bg = 'rgba(248, 113, 113, 0.1)'; } + return ( + + {status} + + ); }, }, { - title: '正确率', - key: 'correctRate', - render: (_: any, record: RecentRecord) => { - const rate = record.totalCount > 0 - ? ((record.correctCount / record.totalCount) * 100).toFixed(1) - : '0.0'; - return {rate}%; - }, - }, - { - title: '考试科目', - dataIndex: 'subjectName', - key: 'subjectName', - render: (subjectName?: string) => subjectName || '', - }, - { - title: '考试人数', - dataIndex: 'examCount', - key: 'examCount', - render: (examCount?: number) => examCount || '', - }, - { - title: '答题时间', + title: '时间', dataIndex: 'createdAt', key: 'createdAt', - render: (date: string) => formatDateTime(date), - }, - ]; - - const taskStatsColumns = [ - { - title: '状态', - dataIndex: 'status', - key: 'status', - }, - { - title: '任务名称', - dataIndex: 'taskName', - key: 'taskName', - }, - { - title: '科目', - dataIndex: 'subjectName', - key: 'subjectName', - }, - { - title: '指定考试人数', - dataIndex: 'totalUsers', - key: 'totalUsers', - }, - { - title: '考试进度', - key: 'progress', - render: (_: any, record: ActiveTaskStat) => { - const now = new Date(); - const start = parseUtcDateTime(record.startAt) ?? new Date(record.startAt); - const end = parseUtcDateTime(record.endAt) ?? new Date(record.endAt); - - const totalDuration = end.getTime() - start.getTime(); - const elapsedDuration = now.getTime() - start.getTime(); - const progress = - totalDuration > 0 - ? Math.max(0, Math.min(100, Math.round((elapsedDuration / totalDuration) * 100))) - : 0; - - return ( -
-
-
-
- {progress}% -
- ); - }, - }, - { - title: '考试人数统计', - key: 'statistics', - render: (_: any, record: ActiveTaskStat) => { - const total = record.totalUsers; - const completed = record.completedUsers; - - const passedTotal = Math.round(completed * (record.passRate / 100)); - const excellentTotal = Math.round(completed * (record.excellentRate / 100)); - - const incomplete = total - completed; - const failed = completed - passedTotal; - const passedOnly = passedTotal - excellentTotal; - const excellent = excellentTotal; - - const pieData = [ - { name: '优秀', value: excellent, color: '#008C8C' }, - { name: '合格', value: passedOnly, color: '#00A3A3' }, - { name: '不及格', value: failed, color: '#ff4d4f' }, - { name: '未完成', value: incomplete, color: '#f0f0f0' }, - ]; - - const filteredData = pieData.filter((item) => item.value > 0); - const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0; - - return ( -
- - - - {filteredData.map((entry, index) => ( - - ))} - - [`${value} 人`, '数量']} - contentStyle={{ - borderRadius: '8px', - border: 'none', - boxShadow: '0 2px 8px rgba(0,0,0,0.15)', - fontSize: '12px', - padding: '8px', - }} - /> - ( - - {value} {entry.payload.value} - - )} - /> - - -
- ); - }, + render: (text: string) => {formatDateTime(text)}, }, ]; return ( -
-
-

管理仪表盘

+
+
+
+

仪表盘

+

系统运行状态概览

+
- {/* 统计卡片 */} - - - navigate('/admin/users')} - styles={{ body: { padding: 16 } }} - > + {/* Top Stats Cards */} + + + 总用户数} value={overview?.totalUsers || 0} - prefix={} - valueStyle={{ color: '#008C8C' }} + prefix={} + valueStyle={{ color: '#fff', fontWeight: 'bold' }} /> - + - - - navigate('/admin/question-bank')} - styles={{ body: { padding: 16 } }} - > + + } - valueStyle={{ color: '#008C8C' }} - suffix="题" - /> - - - - - navigate('/admin/subjects')} - styles={{ body: { padding: 16 } }} - > - 活跃科目} value={overview?.activeSubjectCount || 0} - prefix={} - valueStyle={{ color: '#008C8C' }} - suffix="个" + prefix={} + valueStyle={{ color: '#fff', fontWeight: 'bold' }} /> - + - - - navigate('/admin/exam-tasks')} - styles={{ body: { padding: 16 } }} - > + + } - valueStyle={{ color: '#008C8C' }} - suffix="个" + title={进行中任务} + value={overview?.taskStatusDistribution?.ongoing || 0} + prefix={} + valueStyle={{ color: '#fff', fontWeight: 'bold' }} /> - + + + + + 题库总数} + value={overview?.questionCategoryStats?.reduce((acc, curr) => acc + curr.count, 0) || 0} + prefix={} + valueStyle={{ color: '#fff', fontWeight: 'bold' }} + /> + - - { - const record = loginRecords.find(r => `${r.username}-${r.timestamp}` === value); - if (record) { - handleSelectRecord(record); - } - }} - options={loginRecords.map(record => ({ - value: `${record.username}-${record.timestamp}`, - label: ( -
- {record.username} - - {new Date(record.timestamp).toLocaleString()} - -
- ) - }))} +
+ +
+

管理员登录

+

请输入管理员账号密码

+
+ +
+ {/* 最近登录记录下拉选择 */} + {loginRecords.length > 0 && ( + 最近登录} className="mb-4"> + - )} - - - - - - - - - - - -
+ + - - -
- + + + + -
-
- © {new Date().getFullYear()} Boonlive OA System. All Rights Reserved. -
-
- + + +
+ + +
+
+ © {new Date().getFullYear()} Boonlive OA System. All Rights Reserved. +
+
+ + ); }; diff --git a/src/pages/admin/BackupRestorePage.tsx b/src/pages/admin/BackupRestorePage.tsx index d8abdbb..167835f 100644 --- a/src/pages/admin/BackupRestorePage.tsx +++ b/src/pages/admin/BackupRestorePage.tsx @@ -1,9 +1,11 @@ import { useState, useEffect } from 'react'; -import { Card, Table, Button, message, Upload, Modal } from 'antd'; +import { Card, Table, Button, App, Upload, Modal } from 'antd'; import { UploadOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons'; import * as XLSX from 'xlsx'; +import GlassCard from '../../components/common/GlassCard'; const BackupRestorePage = () => { + const { message } = App.useApp(); const [loading, setLoading] = useState(false); // 数据备份 @@ -154,14 +156,14 @@ const BackupRestorePage = () => { return (
-

数据备份与恢复

+

数据备份与恢复

{/* 数据备份 */} - + 数据备份}>
-

+

备份所有数据到Excel文件,包括用户信息、题库、答题记录等。

-
+ {/* 数据恢复 */} - + 数据恢复}>
-

+

从Excel文件恢复数据,将覆盖现有数据,请谨慎操作。

{
-
+
{/* 注意事项 */} @@ -216,4 +218,4 @@ const BackupRestorePage = () => { ); }; -export default BackupRestorePage; \ No newline at end of file +export default BackupRestorePage; diff --git a/src/pages/admin/ExamSubjectPage.tsx b/src/pages/admin/ExamSubjectPage.tsx index f4c710b..524d887 100644 --- a/src/pages/admin/ExamSubjectPage.tsx +++ b/src/pages/admin/ExamSubjectPage.tsx @@ -4,6 +4,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-de import api from '../../services/api'; import { getCategoryColorHex } from '../../lib/categoryColors'; import { parseUtcDateTime } from '../../utils/validation'; +import GlassCard from '../../components/common/GlassCard'; interface Question { id: string; @@ -344,8 +345,8 @@ const ExamSubjectPage = () => { const ratioMode = isRatioMode(ratios || {}); const total = sumValues(ratios || {}); return ( -
-
+
+
{ratios && Object.entries(ratios).map(([type, value]) => { const typeConfig = questionTypes.find(t => t.key === type); const widthPercent = ratioMode ? value : (total > 0 ? (value / total) * 100 : 0); @@ -371,8 +372,8 @@ const ExamSubjectPage = () => { className="inline-block w-3 h-3 mr-2 rounded-full" style={{ backgroundColor: typeConfig?.color || '#1890ff' }} > - {typeConfig?.label || type} - {ratioMode ? `${value}%` : `${value}题(${percent}%)`} + {typeConfig?.label || type} + {ratioMode ? `${value}%` : `${value}题(${percent}%)`}
); })} @@ -392,8 +393,8 @@ const ExamSubjectPage = () => { : []; const total = entries.reduce((s, [, v]) => s + (Number(v) || 0), 0); return ( -
-
+
+
{entries.map(([category, value]) => (
{ className="inline-block w-3 h-3 mr-2 rounded-full" style={{ backgroundColor: getCategoryColorHex(category) }} > - {category} - {ratioMode ? `${value}%` : `${value}题(${percent}%)`} + {category} + {ratioMode ? `${value}%` : `${value}题(${percent}%)`}
); })} @@ -433,7 +434,7 @@ const ExamSubjectPage = () => { return (
{date.toLocaleDateString()}
-
{date.toLocaleTimeString()}
+
{date.toLocaleTimeString()}
); }, @@ -476,9 +477,9 @@ const ExamSubjectPage = () => { ]; return ( -
+
-

考试科目管理

+

考试科目管理

@@ -753,19 +754,19 @@ const ExamSubjectPage = () => {
解析 - {question.analysis || ''} + {question.analysis || ''}
))}
) : ( -
+
暂无考题数据
)} -
+ ); }; diff --git a/src/pages/admin/ExamTaskPage.tsx b/src/pages/admin/ExamTaskPage.tsx index cb127ce..8d75729 100644 --- a/src/pages/admin/ExamTaskPage.tsx +++ b/src/pages/admin/ExamTaskPage.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, DatePicker, Select, Tabs, Tag } from 'antd'; +import { Table, Button, Input, Space, App, Popconfirm, Modal, Form, DatePicker, Select, Tabs, Tag } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons'; import api, { userGroupAPI } from '../../services/api'; import dayjs from 'dayjs'; import { formatDateTime, parseUtcDateTime } from '../../utils/validation'; +import GlassCard from '../../components/common/GlassCard'; interface ExamTask { id: string; @@ -38,6 +39,7 @@ interface UserGroup { } const ExamTaskPage = () => { + const { message } = App.useApp(); const [tasks, setTasks] = useState([]); const [subjects, setSubjects] = useState([]); const [users, setUsers] = useState([]); @@ -264,9 +266,9 @@ const ExamTaskPage = () => {
{progress}%
-
+
@@ -347,15 +349,16 @@ const ExamTaskPage = () => { ]; return ( -
+
-

考试任务管理

+

考试任务管理

{ /> -
+

任务分配对象

{ -
- 实际分配人数(去重后):{uniqueUserCount} 人 +
+ 实际分配人数(去重后):{uniqueUserCount}
@@ -548,7 +551,7 @@ const ExamTaskPage = () => {
)} -
+ ); }; diff --git a/src/pages/admin/QuestionCategoryPage.tsx b/src/pages/admin/QuestionCategoryPage.tsx index d531d02..7f4d5c4 100644 --- a/src/pages/admin/QuestionCategoryPage.tsx +++ b/src/pages/admin/QuestionCategoryPage.tsx @@ -1,10 +1,11 @@ import React, { useState, useEffect } from 'react'; -import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, Tag, Tooltip } from 'antd'; +import { Table, Button, Input, Space, App, Popconfirm, Modal, Form, Tag, Tooltip } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { useLocation } from 'react-router-dom'; import api from '../../services/api'; import { getCategoryColorHex } from '../../lib/categoryColors'; import { formatDateTime } from '../../utils/validation'; +import GlassCard from '../../components/common/GlassCard'; interface QuestionCategory { id: string; @@ -14,6 +15,7 @@ interface QuestionCategory { } const QuestionCategoryPage = () => { + const { message } = App.useApp(); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); @@ -152,9 +154,9 @@ const QuestionCategoryPage = () => { ]; return ( -
+
-

题目类别管理

+

题目类别管理

+
); }; diff --git a/src/pages/admin/QuestionManagePage.tsx b/src/pages/admin/QuestionManagePage.tsx index 1e787e1..b67cc0f 100644 --- a/src/pages/admin/QuestionManagePage.tsx +++ b/src/pages/admin/QuestionManagePage.tsx @@ -9,7 +9,7 @@ import { Input, Select, Upload, - message, + App, Tag, Popconfirm, Row, @@ -35,6 +35,7 @@ import { useNavigate } from 'react-router-dom'; import { questionAPI } from '../../services/api'; import { formatDate, questionTypeMap, questionTypeColors } from '../../utils/validation'; import { getCategoryColorHex } from '../../lib/categoryColors'; +import GlassCard from '../../components/common/GlassCard'; const { Option } = Select; const { TextArea } = Input; @@ -52,6 +53,7 @@ interface Question { const QuestionManagePage = () => { const navigate = useNavigate(); + const { message } = App.useApp(); const [questions, setQuestions] = useState([]); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); @@ -477,9 +479,9 @@ const QuestionManagePage = () => { ]; return ( -
+
-

题库管理

+

题库管理

{/* 题型筛选 - 动态生成选项 */} @@ -697,7 +699,7 @@ const QuestionManagePage = () => { -
+ ); }; diff --git a/src/pages/admin/QuestionTextImportPage.tsx b/src/pages/admin/QuestionTextImportPage.tsx index a90713c..7a6c711 100644 --- a/src/pages/admin/QuestionTextImportPage.tsx +++ b/src/pages/admin/QuestionTextImportPage.tsx @@ -1,16 +1,18 @@ import { useState } from 'react'; -import { Alert, Button, Card, Input, Modal, Radio, Space, Statistic, Table, Tag, Typography, message } from 'antd'; +import { Alert, Button, Card, Input, Modal, Radio, Space, Statistic, Table, Tag, Typography, App } from 'antd'; import { ArrowLeftOutlined, DeleteOutlined, FileTextOutlined, ImportOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { questionAPI } from '../../services/api'; import { questionTypeMap } from '../../utils/validation'; import { getCategoryColorHex } from '../../lib/categoryColors'; import { type ImportMode, type ImportQuestion, parseTextQuestions } from '../../utils/questionTextImport'; +import GlassCard from '../../components/common/GlassCard'; const { TextArea } = Input; const QuestionTextImportPage = () => { const navigate = useNavigate(); + const { message } = App.useApp(); const [rawText, setRawText] = useState(''); const [mode, setMode] = useState('incremental'); const [parseErrors, setParseErrors] = useState([]); @@ -154,9 +156,9 @@ const QuestionTextImportPage = () => { - +

文本导入题库 - +

@@ -176,7 +178,7 @@ const QuestionTextImportPage = () => {
- +
@@ -188,10 +190,10 @@ const QuestionTextImportPage = () => { - - + 本次导入题目总数} value={totalCount} valueStyle={{ color: '#1677ff', fontWeight: 700 }} /> + 有效题目数量} value={validCount} valueStyle={{ color: '#52c41a', fontWeight: 700 }} /> 无效题目数量} value={invalidCount} valueStyle={{ color: invalidCount > 0 ? '#ff4d4f' : '#8c8c8c', fontWeight: 700 }} /> @@ -203,6 +205,7 @@ const QuestionTextImportPage = () => { onChange={(e) => setRawText(e.target.value)} placeholder={exampleText} rows={12} + className="bg-white/90" /> {parseErrors.length > 0 && ( @@ -220,9 +223,9 @@ const QuestionTextImportPage = () => { /> )} - + - +
({ ...q, key: `${q.content}-${idx}` }))} @@ -238,7 +241,7 @@ const QuestionTextImportPage = () => { }) } /> - + ); }; diff --git a/src/pages/admin/QuizConfigPage.tsx b/src/pages/admin/QuizConfigPage.tsx index 0b47b09..c2114c8 100644 --- a/src/pages/admin/QuizConfigPage.tsx +++ b/src/pages/admin/QuizConfigPage.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; -import { Card, Form, InputNumber, Button, Row, Col, Progress, message } from 'antd'; +import { Form, InputNumber, Button, Row, Col, Progress, App } from 'antd'; import { adminAPI } from '../../services/api'; +import GlassCard from '../../components/common/GlassCard'; interface QuizConfig { singleRatio: number; @@ -12,6 +13,7 @@ interface QuizConfig { const QuizConfigPage = () => { const [form] = Form.useForm(); + const { message } = App.useApp(); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); // 使用state来跟踪实时配置值 @@ -82,24 +84,20 @@ const QuizConfigPage = () => { }; return ( -
+
-

抽题配置

-

设置各题型的比例和试卷总分

+

抽题配置

+

设置各题型的比例和试卷总分

- - 抽题配置 - - 比例总和:{configValues.singleRatio + configValues.multipleRatio + configValues.judgmentRatio + configValues.textRatio}% - -
- } - > +
+
+ 抽题配置 + + 比例总和:{configValues.singleRatio + configValues.multipleRatio + configValues.judgmentRatio + configValues.textRatio}% + +
+
{
单选题比例 (%)} name="singleRatio" rules={[{ required: true, message: '请输入单选题比例' }]} > @@ -130,18 +128,21 @@ const QuizConfigPage = () => { - +
+ +
多选题比例 (%)} name="multipleRatio" rules={[{ required: true, message: '请输入多选题比例' }]} > @@ -155,18 +156,21 @@ const QuizConfigPage = () => { - +
+ +
判断题比例 (%)} name="judgmentRatio" rules={[{ required: true, message: '请输入判断题比例' }]} > @@ -180,18 +184,21 @@ const QuizConfigPage = () => { - +
+ +
文字题比例 (%)} name="textRatio" rules={[{ required: true, message: '请输入文字题比例' }]} > @@ -205,18 +212,21 @@ const QuizConfigPage = () => { - +
+ +
试卷总分} name="totalScore" rules={[{ required: true, message: '请输入试卷总分' }]} > @@ -230,7 +240,7 @@ const QuizConfigPage = () => { -
+
建议设置:100-150分
@@ -247,19 +257,20 @@ const QuizConfigPage = () => { - +
{/* 配置说明 */} - -
+
+

配置说明

+

• 各题型比例总和必须为100%

• 系统会根据比例和总分自动计算各题型的题目数量

• 建议单选题比例不低于30%,确保试卷的覆盖面

• 文字题比例建议不超过20%,避免评分主观性过强

• 总分设置建议为100分,便于成绩统计和分析

- -
+
+ ); }; diff --git a/src/pages/admin/RecordDetailPage.tsx b/src/pages/admin/RecordDetailPage.tsx index 1845154..a598b2c 100644 --- a/src/pages/admin/RecordDetailPage.tsx +++ b/src/pages/admin/RecordDetailPage.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Table, Card, Row, Col, Statistic, Button, message } from 'antd'; +import { Table, Card, Row, Col, Statistic, Button, App } from 'antd'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; import api from '../../services/api'; import { formatDateTime } from '../../utils/validation'; +import GlassCard from '../../components/common/GlassCard'; interface Answer { id: string; @@ -45,6 +46,7 @@ const getStatusColor = (status: string) => { }; const RecordDetailPage = ({ recordId }: { recordId: string }) => { + const { message } = App.useApp(); const [record, setRecord] = useState(null); const [loading, setLoading] = useState(false); @@ -131,7 +133,7 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => { key: 'score', width: '5%', render: (score: number, record: Answer) => ( - + {score} / {record.questionScore} ), @@ -142,7 +144,7 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => { key: 'isCorrect', width: '5%', render: (isCorrect: boolean) => ( - + {isCorrect ? '✓' : '✗'} ), @@ -152,60 +154,64 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => { return (
-

答题记录详情

-

+

答题记录详情

+

答题时间:{formatDateTime(record.createdAt, { includeSeconds: true })}

- -
- - - + + + + 考试状态} value={record.status} valueStyle={{ color: getStatusColor(record.status) === 'red' ? '#F87171' : getStatusColor(record.status) === 'green' ? '#34D399' : '#22D3EE' }} /> + - - - - + + + 总得分} value={record.totalScore} suffix="分" valueStyle={{ color: '#fff' }} /> + - - - - + + + 正确题数} value={record.correctCount} valueStyle={{ color: '#34D399' }} /> + - - - - + + + 总题数} value={record.totalCount} valueStyle={{ color: '#fff' }} /> + - - + + 正确率} value={record.totalCount > 0 ? ((record.correctCount / record.totalCount) * 100).toFixed(1) : 0} suffix="%" + valueStyle={{ color: '#FBBF24' }} /> - + - - - + + + 题型正确率}> - - - - + + + + - + - - + + 答题结果分布}> { innerRadius={60} outerRadius={120} dataKey="value" + stroke="none" > {pieData.map((entry, index) => ( ))} - +
@@ -230,15 +240,15 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => { className="w-4 h-4 rounded mr-2" style={{ backgroundColor: item.color }} /> - {item.name}: {item.value} + {item.name}: {item.value}
))} - +
- + 答题详情}>
{ showTotal: (total) => `共 ${total} 条`, }} /> - + ); }; -export default RecordDetailPage; \ No newline at end of file +export default RecordDetailPage; diff --git a/src/pages/admin/StatisticsPage.tsx b/src/pages/admin/StatisticsPage.tsx index 2d2652c..c54a41d 100644 --- a/src/pages/admin/StatisticsPage.tsx +++ b/src/pages/admin/StatisticsPage.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react'; -import { Card, Row, Col, Statistic, Table, DatePicker, Button, message, Tabs, Select } from 'antd'; +import { Row, Col, Statistic, Table, DatePicker, Button, App, Tabs, Select } from 'antd'; import { Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; import api, { adminAPI, quizAPI } from '../../services/api'; import { formatDateTime } from '../../utils/validation'; +import GlassCard from '../../components/common/GlassCard'; const { RangePicker } = DatePicker; const { TabPane } = Tabs; @@ -70,6 +71,7 @@ const getStatusColor = (status: string) => { }; const StatisticsPage = () => { + const { message } = App.useApp(); const [statistics, setStatistics] = useState(null); const [records, setRecords] = useState([]); const [userStats, setUserStats] = useState([]); @@ -212,10 +214,10 @@ const StatisticsPage = () => { key: 'status', render: (status: string) => { const color = getStatusColor(status); - return {status}; + return {status}; }, }, - { title: '得分', dataIndex: 'totalScore', key: 'totalScore', render: (score: number) => {score} 分 }, + { title: '得分', dataIndex: 'totalScore', key: 'totalScore', render: (score: number) => {score} 分 }, { title: '正确率', key: 'correctRate', render: (record: QuizRecord) => { const rate = ((record.correctCount / record.totalCount) * 100).toFixed(1); return {rate}%; @@ -246,9 +248,9 @@ const StatisticsPage = () => { ]; return ( -
+
-

数据统计分析

+

数据统计分析

- +
总用户数} value={statistics?.totalUsers || 0} valueStyle={{ color: '#1890ff' }} /> - +
- +
总答题数} value={statistics?.totalRecords || 0} valueStyle={{ color: '#52c41a' }} /> - +
- +
平均分} value={statistics?.averageScore || 0} precision={1} valueStyle={{ color: '#faad14' }} suffix="分" /> - +
- +
活跃率} value={statistics?.totalUsers ? ((statistics.totalRecords / statistics.totalUsers) * 100).toFixed(1) : 0} precision={1} valueStyle={{ color: '#f5222d' }} suffix="%" /> - +
- + {/* 图表 */}
- +
+

分数分布

{ ))} - + - +
{/* 详细记录 */} - +
+

答题记录明细

{ size: 'small', }} /> - + - +
+

用户答题统计

{ size: 'small', }} /> - + - +
+

科目答题统计

{ size: 'small', }} /> - + - +
+

考试任务统计

{ size: 'small', }} /> - + - + ); }; diff --git a/src/pages/admin/UserGroupManage.tsx b/src/pages/admin/UserGroupManage.tsx index 060cf82..0a96633 100644 --- a/src/pages/admin/UserGroupManage.tsx +++ b/src/pages/admin/UserGroupManage.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, Tag } from 'antd'; +import { Table, Button, Input, Space, App, Popconfirm, Modal, Form, Tag } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, TeamOutlined } from '@ant-design/icons'; import { userGroupAPI } from '../../services/api'; import { formatDateTime } from '../../utils/validation'; +import GlassCard from '../../components/common/GlassCard'; interface UserGroup { id: string; @@ -14,6 +15,7 @@ interface UserGroup { } const UserGroupManage = () => { + const { message } = App.useApp(); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(false); const [modalVisible, setModalVisible] = useState(false); @@ -150,8 +152,9 @@ const UserGroupManage = () => { ]; return ( -
-
+ +
+

用户组管理

@@ -193,7 +196,7 @@ const UserGroupManage = () => { -
+
); }; diff --git a/src/pages/admin/UserManagePage.tsx b/src/pages/admin/UserManagePage.tsx index 6cc8883..7239b0d 100644 --- a/src/pages/admin/UserManagePage.tsx +++ b/src/pages/admin/UserManagePage.tsx @@ -1,10 +1,11 @@ import { formatDateTime } from '../../utils/validation'; import React, { useState, useEffect } from 'react'; -import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, Switch, Upload, Tabs, Select, Tag, Descriptions, Divider, Typography } from 'antd'; +import { Table, Button, Input, Space, App, Popconfirm, Modal, Form, Switch, Upload, Tabs, Select, Tag, Descriptions, Divider, Typography } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, ExportOutlined, ImportOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import api, { userGroupAPI } from '../../services/api'; import type { UploadProps } from 'antd'; import UserGroupManage from './UserGroupManage'; +import GlassCard from '../../components/common/GlassCard'; interface User { id: string; @@ -66,6 +67,7 @@ const getStatusColor = (status: string) => { }; const UserManagePage = () => { + const { message } = App.useApp(); const [users, setUsers] = useState([]); const [userGroups, setUserGroups] = useState([]); const [loading, setLoading] = useState(false); @@ -431,7 +433,7 @@ const UserManagePage = () => { }; const UserListContent = () => ( -
+
{ {/* 答题记录面板 */} {selectedUser && (
-

+

{selectedUser.name}的答题记录

{ ); })()} - + + ); + + const UserGroupContent = () => ( + + + ); return (
-

用户管理

+

用户管理

{ { key: '2', label: '用户组管理', - children: , + children: , }, ]} /> diff --git a/src/pages/admin/UserRecordsPage.tsx b/src/pages/admin/UserRecordsPage.tsx index 00b1aed..5025218 100644 --- a/src/pages/admin/UserRecordsPage.tsx +++ b/src/pages/admin/UserRecordsPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Table, Card, Row, Col, Statistic, Button, message } from 'antd'; +import { Table, Card, Row, Col, Statistic, Button, App } from 'antd'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; import api from '../../services/api'; import { formatDateTime } from '../../utils/validation'; @@ -33,6 +33,7 @@ const getStatusColor = (status: string) => { }; const UserRecordsPage = ({ userId }: { userId: string }) => { + const { message } = App.useApp(); const [records, setRecords] = useState([]); const [loading, setLoading] = useState(false); const [pagination, setPagination] = useState({ diff --git a/src/pages/quiz/components/OptionList.tsx b/src/pages/quiz/components/OptionList.tsx index bfbdc12..bd3f8a8 100644 --- a/src/pages/quiz/components/OptionList.tsx +++ b/src/pages/quiz/components/OptionList.tsx @@ -36,14 +36,14 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL if (type === 'text') { return ( -
+