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 0000000..b2495f3 Binary files /dev/null and b/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc differ diff --git a/src/App.tsx b/src/App.tsx index ead6d5e..158635b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import QuizPage from './pages/QuizPage'; import ResultPage from './pages/ResultPage'; import { SubjectSelectionPage } from './pages/SubjectSelectionPage'; import { UserTaskPage } from './pages/UserTaskPage'; +import BankingLandingPage from './pages/BankingLandingPage'; // 管理端页面 import AdminLoginPage from './pages/admin/AdminLoginPage'; @@ -41,6 +42,7 @@ function App() { } /> } /> } /> + } /> {/* 管理端路由 */} } /> 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 ( -
+