- © {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
+
+
+
-
+
+
+
+
+
+ : }
+ onClick={() => setCollapsed(!collapsed)}
+ style={{
+ fontSize: '16px',
+ width: 64,
+ height: 64,
+ color: 'rgba(255, 255, 255, 0.85)',
+ }}
+ />
+
+
+
+ 欢迎回来,{admin?.username}
+
+
+
+
}
+ size="small"
+ />
+
+ 管理员
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
-
+
);
};
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
+
+
+
+
+
+
+ **** 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'}
+
+
+
+
+ 已完成
+ {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 (
-
- );
- },
- },
- {
- 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',
- }}
- />
-
-
-
- );
- },
+ render: (text: string) => {formatDateTime(text)},
},
];
return (
-
-
-
管理仪表盘
+
+
+
}
- onClick={fetchDashboardData}
+ icon={
}
+ onClick={fetchDashboardData}
loading={loading}
- className="bg-mars-500 hover:bg-mars-600"
+ type="primary"
+ ghost
+ className="border-white/20 text-white hover:bg-white/10"
>
刷新数据
- {/* 统计卡片 */}
-
-
- 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' }}
+ />
+
-
-