diff --git a/CLAUDE.md b/CLAUDE.md index 00433c2..18d36ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,7 +81,7 @@ Examora is a monorepo with three logical layers: ## Judge Flow (planned, not yet implemented) -1. `POST /api/client/programming/submissions` → inserts `submissions` + `judge_tasks` rows, enqueues to Redis Stream +1. `POST /api/v1/submissions` → inserts `submissions` + `judge_tasks` rows, enqueues to Redis Stream 2. `judge-worker` consumes from Redis Stream → reads submission + test cases from PostgreSQL → calls `sandbox-runner` → writes `judge_case_results` → updates `submissions.status` 3. Candidate polls status until terminal state diff --git a/apps/admin/src/access.ts b/apps/admin/src/access.ts index d67ead0..b872b56 100644 --- a/apps/admin/src/access.ts +++ b/apps/admin/src/access.ts @@ -8,7 +8,7 @@ export default function access( ) { const { currentUser, forbidden } = initialState ?? {}; - // If the user is forbidden (403 from /api/auth/me), deny access + // If the user is forbidden (403 from /api/v1/auth/me), deny access if (forbidden) { return { canAdmin: false }; } diff --git a/apps/admin/src/app.tsx b/apps/admin/src/app.tsx index 9518d63..0a1db66 100644 --- a/apps/admin/src/app.tsx +++ b/apps/admin/src/app.tsx @@ -2,8 +2,9 @@ import { type Settings as LayoutSettings, SettingDrawer, } from '@ant-design/pro-components'; +import { API_PATHS } from '@examora/types'; import type { RunTimeLayoutConfig } from '@umijs/max'; -import { history, Link } from '@umijs/max'; +import { history, Link, useIntl } from '@umijs/max'; import { Avatar, Button, ConfigProvider, Result } from 'antd'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -86,7 +87,7 @@ export async function getInitialState(): Promise<{ if (!token) return null; try { - const response = await fetch('/api/auth/me', { + const response = await fetch(API_PATHS.auth.me, { headers: { Authorization: `Bearer ${token}` }, }); const data = await response.json(); @@ -255,13 +256,14 @@ export const request: any = { // 无权限页面组件 const ForbiddenPage: React.FC = () => { + const intl = useIntl(); const [loading, setLoading] = React.useState(false); const handleLogout = async () => { setLoading(true); try { const token = getAccessToken(); - await fetch('/api/auth/logout', { + await fetch(API_PATHS.auth.logout, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }); @@ -275,11 +277,11 @@ const ForbiddenPage: React.FC = () => { return ( - 重新登录 + {intl.formatMessage({ id: 'pages.forbidden.relogin' })} } /> diff --git a/apps/admin/src/auth/config.ts b/apps/admin/src/auth/config.ts index 6395fa7..baf5e0f 100644 --- a/apps/admin/src/auth/config.ts +++ b/apps/admin/src/auth/config.ts @@ -1,8 +1,9 @@ +import { API_PATHS } from '@examora/types'; import { request } from '@umijs/max'; import type { AuthConfig } from './token'; export const fetchAuthConfig = async (): Promise => { - const response = await request('/api/auth/config', { + const response = await request(API_PATHS.auth.config, { method: 'GET', }); return response; diff --git a/apps/admin/src/components/RightContent/AvatarDropdown.tsx b/apps/admin/src/components/RightContent/AvatarDropdown.tsx index 8cf38b9..57bf6a0 100644 --- a/apps/admin/src/components/RightContent/AvatarDropdown.tsx +++ b/apps/admin/src/components/RightContent/AvatarDropdown.tsx @@ -1,4 +1,5 @@ import { LogoutOutlined, SettingOutlined } from '@ant-design/icons'; +import { API_PATHS } from '@examora/types'; import { history, request, useIntl, useModel } from '@umijs/max'; import type { MenuProps } from 'antd'; import { Spin } from 'antd'; @@ -48,7 +49,7 @@ export const AvatarDropdown: React.FC = ({ const intl = useIntl(); const loginOut = async () => { try { - await request('/api/auth/logout', { method: 'POST' }); + await request(API_PATHS.auth.logout, { method: 'POST' }); } catch (_) { // ignore logout errors } diff --git a/apps/admin/src/components/RightContent/index.tsx b/apps/admin/src/components/RightContent/index.tsx index f7fb406..f1a038f 100644 --- a/apps/admin/src/components/RightContent/index.tsx +++ b/apps/admin/src/components/RightContent/index.tsx @@ -18,6 +18,7 @@ const useStyles = createStyles(() => ({ fontSize: 18, color: 'inherit', transition: 'background 0.2s', + borderRadius: 8, '&:hover': { background: 'rgba(0,0,0,0.04)' }, }, })); diff --git a/apps/admin/src/components/TagSelect/index.tsx b/apps/admin/src/components/TagSelect/index.tsx index 4b4a7b4..a5339f5 100644 --- a/apps/admin/src/components/TagSelect/index.tsx +++ b/apps/admin/src/components/TagSelect/index.tsx @@ -1,5 +1,6 @@ import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { useMergedState } from '@rc-component/util'; +import { useIntl } from '@umijs/max'; import { Tag } from 'antd'; import { clsx } from 'clsx'; import React, { type FC, useMemo, useState } from 'react'; @@ -51,6 +52,7 @@ export interface TagSelectProps { const TagSelect: FC & { Option: typeof TagSelectOption; } = (props) => { + const intl = useIntl(); const { styles } = useStyles(); const { children, @@ -103,9 +105,9 @@ const TagSelect: FC & { }; const checkedAll = allTags.length === value?.length && allTags.length > 0; const { - expandText = '展开', - collapseText = '收起', - selectAllText = '全部', + expandText = intl.formatMessage({ id: 'common.expand' }), + collapseText = intl.formatMessage({ id: 'common.collapse' }), + selectAllText = intl.formatMessage({ id: 'common.selectAll' }), } = actionsText; const cls = clsx(styles.tagSelect, className, { [styles.hasExpandTag]: expandable, diff --git a/apps/admin/src/locales/en-US/pages.ts b/apps/admin/src/locales/en-US/pages.ts index a90fdee..7a80ed9 100644 --- a/apps/admin/src/locales/en-US/pages.ts +++ b/apps/admin/src/locales/en-US/pages.ts @@ -1,5 +1,8 @@ export default { 'common.actions': 'Actions', + 'common.expand': 'Expand', + 'common.collapse': 'Collapse', + 'common.selectAll': 'All', 'pages.404.subTitle': 'Sorry, the page you visited does not exist.', 'pages.404.buttonText': 'Back to Home', 'pages.admin.subPage.title': 'Admin-only page', @@ -442,6 +445,131 @@ export default { 'To-do tasks will be notified via in-site messages', 'pages.account.settings.switch.on': 'On', 'pages.account.settings.switch.off': 'Off', + // Dashboard + 'pages.dashboard.week.mon': 'Mon', + 'pages.dashboard.week.tue': 'Tue', + 'pages.dashboard.week.wed': 'Wed', + 'pages.dashboard.week.thu': 'Thu', + 'pages.dashboard.week.fri': 'Fri', + 'pages.dashboard.week.sat': 'Sat', + 'pages.dashboard.week.sun': 'Sun', + 'pages.dashboard.hero.title': 'Exam Operations Dashboard', + 'pages.dashboard.hero.kicker': 'Live Operations', + 'pages.dashboard.hero.subtitle': + "Aggregate exam progress, proctoring events, question assets, and judge queues to help admins identify today's key risks quickly.", + 'pages.dashboard.hero.online': 'Online Candidates', + 'pages.dashboard.hero.risk': 'Risk Alerts', + 'pages.dashboard.hero.queue': 'Pending Judges', + 'pages.dashboard.actions.createExam': 'New Exam', + 'pages.dashboard.actions.viewMonitoring': 'View Monitoring', + 'pages.dashboard.stats.candidates.label': 'Registered Candidates', + 'pages.dashboard.stats.candidates.caption': '146 new candidates this week', + 'pages.dashboard.stats.activeExams.label': 'Active Exams', + 'pages.dashboard.stats.activeExams.trend': '45 online', + 'pages.dashboard.stats.activeExams.caption': 'Across 6 rooms', + 'pages.dashboard.stats.papers.label': 'Total Papers', + 'pages.dashboard.stats.papers.caption': 'Published this month', + 'pages.dashboard.stats.questions.label': 'Total Questions', + 'pages.dashboard.stats.questions.caption': 'Question reuse rate', + 'pages.dashboard.chart.weekAria': '{label} 7-day trend', + 'pages.dashboard.chart.empty': 'No trend data', + 'pages.dashboard.series.online': 'Online Candidates', + 'pages.dashboard.series.queue': 'Judge Queue', + 'pages.dashboard.series.risk': 'Risk Events', + 'pages.dashboard.load.title': 'Exam Load Trend', + 'pages.dashboard.load.range': 'Last 12 hours', + 'pages.dashboard.load.peakOnline': 'Peak Online', + 'pages.dashboard.load.peakOnlineCaption': 'Peaked at 16:00', + 'pages.dashboard.load.peakQueue': 'Queue Peak', + 'pages.dashboard.load.peakQueueCaption': 'Average 42 seconds', + 'pages.dashboard.load.riskEvents': 'Risk Events', + 'pages.dashboard.load.riskEventsCaption': 'Down 18% from yesterday', + 'pages.dashboard.load.aria': + 'Online candidates, judge queue, and risk event trend in the last 12 hours', + 'pages.dashboard.progress.title': 'Exam Progress', + 'pages.dashboard.progress.allExams': 'All Exams', + 'pages.dashboard.progress.activeCount': '{count} active', + 'pages.dashboard.progress.duration': '{count} min', + 'pages.dashboard.exams.math': '2026 Spring Advanced Mathematics Final', + 'pages.dashboard.exams.python': 'Python Programming Skills Test', + 'pages.dashboard.exams.physics': 'College Physics Mock Test', + 'pages.dashboard.status.running': 'Running', + 'pages.dashboard.status.closed': 'Closed', + 'pages.dashboard.risk.low': 'Low Risk', + 'pages.dashboard.risk.watch': 'Watch', + 'pages.dashboard.risk.normal': 'Normal', + 'pages.dashboard.risk.title': 'Proctoring Risk', + 'pages.dashboard.risk.watchCount': '9 need attention', + 'pages.dashboard.risk.healthLabel': 'Exam Health Today', + 'pages.dashboard.risk.healthTitle': 'Stable', + 'pages.dashboard.risk.healthCopy': + 'Auto-submit, off-screen, and network fluctuation events are under control.', + 'pages.dashboard.risk.offscreen': 'Off-screen Events', + 'pages.dashboard.risk.network': 'Network Fluctuations', + 'pages.dashboard.risk.review': 'Manual Reviews', + 'pages.dashboard.queue.title': 'Judge Queue', + 'pages.dashboard.queue.waiting': 'Waiting', + 'pages.dashboard.queue.running': 'Running', + 'pages.dashboard.queue.failed': 'Needs Review', + 'pages.dashboard.activities.title': 'Exam Activity', + 'pages.dashboard.activities.a1': + 'Li Na submitted the Python Programming Skills Test', + 'pages.dashboard.activities.a2': "Wang Lei's exam was auto-submitted", + 'pages.dashboard.activities.a3': + 'Zhang Wei started the 2026 Spring Advanced Mathematics exam', + 'pages.dashboard.activities.a4': + 'Chen Jing submitted the Data Structures and Algorithms test', + 'pages.dashboard.activities.5m': '5 minutes ago', + 'pages.dashboard.activities.8m': '8 minutes ago', + 'pages.dashboard.activities.12m': '12 minutes ago', + 'pages.dashboard.activities.15m': '15 minutes ago', + 'pages.dashboard.shortcuts.title': 'Quick Access', + 'pages.dashboard.shortcuts.users': 'User Management', + 'pages.dashboard.shortcuts.exams': 'Exam Management', + 'pages.dashboard.shortcuts.papers': 'Paper Management', + 'pages.dashboard.shortcuts.questions': 'Question Bank', + 'pages.dashboard.shortcuts.programming': 'Programming Bank', + 'pages.dashboard.shortcuts.submissions': 'Submissions', + // Coming soon + 'pages.comingSoon.title': 'Module Under Construction', + 'pages.comingSoon.description': + 'This admin module is reserved in the navigation and will be connected in a future iteration.', + 'pages.comingSoon.shortTitle': 'Coming Soon', + 'pages.comingSoon.shortDescription': + 'This feature is under development. Please check back later.', + 'pages.comingSoon.backDashboard': 'Back to Dashboard', + 'pages.comingSoon.viewExams': 'View Exam Management', + 'pages.comingSoon.note': + 'This entry keeps the admin information architecture stable while business pages are added.', + 'pages.comingSoon.questions.title': 'Question Bank', + 'pages.comingSoon.questions.description': + 'Question lists, type settings, answers, and explanations will be connected here.', + 'pages.comingSoon.programming.title': 'Programming Questions and Test Cases', + 'pages.comingSoon.programming.description': + 'Programming templates, sample cases, hidden cases, and execution limits will be managed here.', + 'pages.comingSoon.papers.title': 'Paper Management', + 'pages.comingSoon.papers.description': + 'Paper composition, question ordering, score settings, and status flow will be connected here.', + 'pages.comingSoon.examCreate.title': 'Create Exam', + 'pages.comingSoon.examCreate.description': + 'Exam details, paper binding, and pre-publish configuration will be connected here.', + 'pages.comingSoon.candidates.title': 'Candidate Management', + 'pages.comingSoon.candidates.description': + 'Candidate accounts, exam authorization, groups, and import/export will be connected here.', + 'pages.comingSoon.events.title': 'Proctoring Audit', + 'pages.comingSoon.events.description': + 'Desktop events, device binding, and abnormal behavior records will be viewed here.', + 'pages.comingSoon.submissions.title': 'Submission Records', + 'pages.comingSoon.submissions.description': + 'Candidate papers, programming submissions, and scoring status will be summarized here.', + 'pages.comingSoon.judgeTasks.title': 'Judge Tasks', + 'pages.comingSoon.judgeTasks.description': + 'Async judge tasks, retry status, and sandbox results will be tracked here.', + // Forbidden + 'pages.forbidden.title': 'Admin Access Denied', + 'pages.forbidden.subTitle': + 'Your account is inactive or does not have admin access. Contact an administrator to enable access.', + 'pages.forbidden.relogin': 'Sign In Again', 'navbar.settings': 'Settings', 'navbar.logout': 'Logout', }; diff --git a/apps/admin/src/locales/zh-CN/pages.ts b/apps/admin/src/locales/zh-CN/pages.ts index f2c3d0b..d49366f 100644 --- a/apps/admin/src/locales/zh-CN/pages.ts +++ b/apps/admin/src/locales/zh-CN/pages.ts @@ -1,5 +1,8 @@ export default { 'common.actions': '操作', + 'common.expand': '展开', + 'common.collapse': '收起', + 'common.selectAll': '全部', 'pages.404.subTitle': '抱歉,您访问的页面不存在。', 'pages.404.buttonText': '返回首页', 'pages.admin.subPage.title': '管理员专属页面', @@ -37,22 +40,19 @@ export default { 'pages.login.sso': '使用企业 SSO 登录', 'pages.login.footnote': '访问权限由系统角色与账号状态控制。', 'pages.login.ssoSuccess': 'SSO 登录成功', - 'pages.login.sectionLabel': 'Secure access', + 'pages.login.sectionLabel': '安全访问', 'pages.login.or': '或', // Login brand section - 'pages.login.brand.eyebrow': 'Admin Console', + 'pages.login.brand.eyebrow': '管理后台', 'pages.login.brand.headline': '考试运营与评测管理后台', 'pages.login.brand.description': '集中管理题库、试卷、考试发布、考生记录与评测结果。', - 'pages.login.brand.signal1.title': 'Exam Lifecycle', - 'pages.login.brand.signal1.desc': - 'Publish, schedule, and track exam operations.', - 'pages.login.brand.signal2.title': 'Judge Pipeline', - 'pages.login.brand.signal2.desc': - 'Review submissions and monitor execution status.', - 'pages.login.brand.signal3.title': 'Access Control', - 'pages.login.brand.signal3.desc': - 'Manage admin roles and protected workflows.', + 'pages.login.brand.signal1.title': '考试全生命周期', + 'pages.login.brand.signal1.desc': '发布、排期并追踪考试运营状态。', + 'pages.login.brand.signal2.title': '判题流水线', + 'pages.login.brand.signal2.desc': '查看提交记录并监控评测执行状态。', + 'pages.login.brand.signal3.title': '访问控制', + 'pages.login.brand.signal3.desc': '管理后台角色与受保护操作流程。', // Users page 'pages.users.title': '用户列表', 'pages.users.description': @@ -418,6 +418,126 @@ export default { '待办任务将以站内信的形式通知', 'pages.account.settings.switch.on': '开', 'pages.account.settings.switch.off': '关', + // Dashboard + 'pages.dashboard.week.mon': '周一', + 'pages.dashboard.week.tue': '周二', + 'pages.dashboard.week.wed': '周三', + 'pages.dashboard.week.thu': '周四', + 'pages.dashboard.week.fri': '周五', + 'pages.dashboard.week.sat': '周六', + 'pages.dashboard.week.sun': '周日', + 'pages.dashboard.hero.title': '考试运营工作台', + 'pages.dashboard.hero.kicker': '实时运营', + 'pages.dashboard.hero.subtitle': + '聚合考试进度、监考事件、题库资产与判题队列,帮助管理员快速判断今天的关键风险。', + 'pages.dashboard.hero.online': '在线考生', + 'pages.dashboard.hero.risk': '风险关注', + 'pages.dashboard.hero.queue': '等待判题', + 'pages.dashboard.actions.createExam': '新建考试', + 'pages.dashboard.actions.viewMonitoring': '查看监控', + 'pages.dashboard.stats.candidates.label': '注册考生', + 'pages.dashboard.stats.candidates.caption': '较上周新增 146 人', + 'pages.dashboard.stats.activeExams.label': '进行中考试', + 'pages.dashboard.stats.activeExams.trend': '45 人在线', + 'pages.dashboard.stats.activeExams.caption': '覆盖 6 个考场', + 'pages.dashboard.stats.papers.label': '试卷总数', + 'pages.dashboard.stats.papers.caption': '本月发布试卷', + 'pages.dashboard.stats.questions.label': '题目总数', + 'pages.dashboard.stats.questions.caption': '题库可复用率', + 'pages.dashboard.chart.weekAria': '{label} 7 日趋势', + 'pages.dashboard.chart.empty': '暂无趋势数据', + 'pages.dashboard.series.online': '在线考生', + 'pages.dashboard.series.queue': '判题队列', + 'pages.dashboard.series.risk': '风险事件', + 'pages.dashboard.load.title': '考试负载趋势', + 'pages.dashboard.load.range': '近 12 小时', + 'pages.dashboard.load.peakOnline': '峰值在线', + 'pages.dashboard.load.peakOnlineCaption': '16:00 达到峰值', + 'pages.dashboard.load.peakQueue': '队列峰值', + 'pages.dashboard.load.peakQueueCaption': '平均处理 42 秒', + 'pages.dashboard.load.riskEvents': '风险事件', + 'pages.dashboard.load.riskEventsCaption': '较昨日下降 18%', + 'pages.dashboard.load.aria': '近 12 小时在线考生、判题队列和风险事件趋势', + 'pages.dashboard.progress.title': '考试进度', + 'pages.dashboard.progress.allExams': '全部考试', + 'pages.dashboard.progress.activeCount': '{count} 人在考', + 'pages.dashboard.progress.duration': '{count} 分钟', + 'pages.dashboard.exams.math': '2026 春季高等数学期末考试', + 'pages.dashboard.exams.python': 'Python 程序设计能力测试', + 'pages.dashboard.exams.physics': '大学物理模拟测试', + 'pages.dashboard.status.running': '进行中', + 'pages.dashboard.status.closed': '已结束', + 'pages.dashboard.risk.low': '低风险', + 'pages.dashboard.risk.watch': '关注', + 'pages.dashboard.risk.normal': '正常', + 'pages.dashboard.risk.title': '监考风险', + 'pages.dashboard.risk.watchCount': '需关注 9', + 'pages.dashboard.risk.healthLabel': '今日考试健康度', + 'pages.dashboard.risk.healthTitle': '整体稳定', + 'pages.dashboard.risk.healthCopy': + '自动提交、离屏与网络波动事件均处于可控范围。', + 'pages.dashboard.risk.offscreen': '离屏事件', + 'pages.dashboard.risk.network': '网络波动', + 'pages.dashboard.risk.review': '人工复核', + 'pages.dashboard.queue.title': '判题队列', + 'pages.dashboard.queue.waiting': '等待判题', + 'pages.dashboard.queue.running': '执行中', + 'pages.dashboard.queue.failed': '需复核', + 'pages.dashboard.activities.title': '考试动态', + 'pages.dashboard.activities.a1': '李娜提交了 Python 程序设计能力测试', + 'pages.dashboard.activities.a2': '王磊的考试被系统自动提交', + 'pages.dashboard.activities.a3': '张伟开始作答 2026 春季高等数学', + 'pages.dashboard.activities.a4': '陈静提交了数据结构与算法测试', + 'pages.dashboard.activities.5m': '5 分钟前', + 'pages.dashboard.activities.8m': '8 分钟前', + 'pages.dashboard.activities.12m': '12 分钟前', + 'pages.dashboard.activities.15m': '15 分钟前', + 'pages.dashboard.shortcuts.title': '快捷入口', + 'pages.dashboard.shortcuts.users': '用户管理', + 'pages.dashboard.shortcuts.exams': '考试管理', + 'pages.dashboard.shortcuts.papers': '试卷管理', + 'pages.dashboard.shortcuts.questions': '题库管理', + 'pages.dashboard.shortcuts.programming': '编程题库', + 'pages.dashboard.shortcuts.submissions': '提交记录', + // Coming soon + 'pages.comingSoon.title': '模块建设中', + 'pages.comingSoon.description': + '该管理模块已预留导航入口,业务页面会在后续迭代接入。', + 'pages.comingSoon.shortTitle': '即将上线', + 'pages.comingSoon.shortDescription': '该功能正在开发中,敬请期待。', + 'pages.comingSoon.backDashboard': '返回工作台', + 'pages.comingSoon.viewExams': '查看考试管理', + 'pages.comingSoon.note': + '当前入口用于固定后台信息架构,避免后续业务页面上线时反复调整主菜单。', + 'pages.comingSoon.questions.title': '题库管理', + 'pages.comingSoon.questions.description': + '题目列表、题型配置、答案与解析维护即将接入。', + 'pages.comingSoon.programming.title': '编程题与测试用例', + 'pages.comingSoon.programming.description': + '编程题模板、样例用例、隐藏用例和运行限制将在这里管理。', + 'pages.comingSoon.papers.title': '试卷管理', + 'pages.comingSoon.papers.description': + '组卷、题目排序、分值配置和试卷状态流转即将接入。', + 'pages.comingSoon.examCreate.title': '创建考试', + 'pages.comingSoon.examCreate.description': + '考试基本信息、试卷绑定和发布前配置将在这里接入。', + 'pages.comingSoon.candidates.title': '考生管理', + 'pages.comingSoon.candidates.description': + '考生账号、考试授权、分组和导入导出能力即将接入。', + 'pages.comingSoon.events.title': '监考审计', + 'pages.comingSoon.events.description': + '桌面端事件、设备绑定、异常行为记录将在这里查看。', + 'pages.comingSoon.submissions.title': '提交记录', + 'pages.comingSoon.submissions.description': + '候选人答卷、编程提交和判分状态将在这里汇总。', + 'pages.comingSoon.judgeTasks.title': '判题任务', + 'pages.comingSoon.judgeTasks.description': + '异步判题任务、重试状态和沙箱结果将在这里跟踪。', + // Forbidden + 'pages.forbidden.title': '无权访问后台', + 'pages.forbidden.subTitle': + '您的账户尚未激活或没有后台访问权限,请联系管理员开通权限。', + 'pages.forbidden.relogin': '重新登录', 'navbar.settings': '设置', 'navbar.logout': '退出登录', }; diff --git a/apps/admin/src/pages/Assessment/Results/JudgeTasks/index.tsx b/apps/admin/src/pages/Assessment/Results/JudgeTasks/index.tsx index 6839cbb..cef70ad 100644 --- a/apps/admin/src/pages/Assessment/Results/JudgeTasks/index.tsx +++ b/apps/admin/src/pages/Assessment/Results/JudgeTasks/index.tsx @@ -1,19 +1,30 @@ import { PageContainer } from '@ant-design/pro-components'; -import { history } from '@umijs/max'; +import { history, useIntl } from '@umijs/max'; import { Button, Card, Result } from 'antd'; import React from 'react'; const Index: React.FC = () => { + const intl = useIntl(); + return ( - + history.push('/')}> - 返回首页 + } /> diff --git a/apps/admin/src/pages/Assessment/Results/Submissions/index.tsx b/apps/admin/src/pages/Assessment/Results/Submissions/index.tsx index 918a149..4cd3883 100644 --- a/apps/admin/src/pages/Assessment/Results/Submissions/index.tsx +++ b/apps/admin/src/pages/Assessment/Results/Submissions/index.tsx @@ -1,19 +1,30 @@ import { PageContainer } from '@ant-design/pro-components'; -import { history } from '@umijs/max'; +import { history, useIntl } from '@umijs/max'; import { Button, Card, Result } from 'antd'; import React from 'react'; const Index: React.FC = () => { + const intl = useIntl(); + return ( - + history.push('/')}> - 返回首页 + } /> diff --git a/apps/admin/src/pages/ComingSoon/index.tsx b/apps/admin/src/pages/ComingSoon/index.tsx index 1dfb086..2eeb945 100644 --- a/apps/admin/src/pages/ComingSoon/index.tsx +++ b/apps/admin/src/pages/ComingSoon/index.tsx @@ -7,86 +7,92 @@ import { TrophyOutlined, } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; import { Button, Card, Result, Space, Typography } from 'antd'; import React from 'react'; const moduleMeta: Record< string, { - title: string; - description: string; + titleId: string; + descriptionId: string; icon: React.ReactNode; } > = { - '/library/questions': { - title: '题库管理', - description: '题目列表、题型配置、答案与解析维护即将接入。', + '/content/library/questions': { + titleId: 'pages.comingSoon.questions.title', + descriptionId: 'pages.comingSoon.questions.description', icon: , }, - '/library/programming': { - title: '编程题与测试用例', - description: '编程题模板、样例用例、隐藏用例和运行限制将在这里管理。', + '/content/library/programming': { + titleId: 'pages.comingSoon.programming.title', + descriptionId: 'pages.comingSoon.programming.description', icon: , }, - '/papers': { - title: '试卷管理', - description: '组卷、题目排序、分值配置和试卷状态流转即将接入。', + '/content/papers': { + titleId: 'pages.comingSoon.papers.title', + descriptionId: 'pages.comingSoon.papers.description', icon: , }, - '/exams/create': { - title: '创建考试', - description: '考试基本信息、试卷绑定和发布前配置将在这里接入。', + '/examination/exams/create': { + titleId: 'pages.comingSoon.examCreate.title', + descriptionId: 'pages.comingSoon.examCreate.description', icon: , }, - '/candidates': { - title: '考生管理', - description: '考生账号、考试授权、分组和导入导出能力即将接入。', + '/examination/candidates': { + titleId: 'pages.comingSoon.candidates.title', + descriptionId: 'pages.comingSoon.candidates.description', icon: , }, - '/proctoring/events': { - title: '监考审计', - description: '桌面端事件、设备绑定、异常行为记录将在这里查看。', + '/monitoring/proctoring/events': { + titleId: 'pages.comingSoon.events.title', + descriptionId: 'pages.comingSoon.events.description', icon: , }, - '/results/submissions': { - title: '提交记录', - description: '候选人答卷、编程提交和判分状态将在这里汇总。', + '/assessment/results/submissions': { + titleId: 'pages.comingSoon.submissions.title', + descriptionId: 'pages.comingSoon.submissions.description', icon: , }, - '/results/judge-tasks': { - title: '判题任务', - description: '异步判题任务、重试状态和沙箱结果将在这里跟踪。', + '/assessment/results/judge-tasks': { + titleId: 'pages.comingSoon.judgeTasks.title', + descriptionId: 'pages.comingSoon.judgeTasks.description', icon: , }, }; const fallbackMeta = { - title: '模块建设中', - description: '该管理模块已预留导航入口,业务页面会在后续迭代接入。', + titleId: 'pages.comingSoon.title', + descriptionId: 'pages.comingSoon.description', icon: , }; const ComingSoon: React.FC = () => { + const intl = useIntl(); const meta = moduleMeta[window.location.pathname] ?? fallbackMeta; + const title = intl.formatMessage({ id: meta.titleId }); + const description = intl.formatMessage({ id: meta.descriptionId }); return ( - + - + - } /> - 当前入口用于固定后台信息架构,避免后续业务页面上线时反复调整主菜单。 + {intl.formatMessage({ id: 'pages.comingSoon.note' })} diff --git a/apps/admin/src/pages/Content/Library/Questions/Detail.tsx b/apps/admin/src/pages/Content/Library/Questions/Detail.tsx index ffd5efe..bb64cb6 100644 --- a/apps/admin/src/pages/Content/Library/Questions/Detail.tsx +++ b/apps/admin/src/pages/Content/Library/Questions/Detail.tsx @@ -26,6 +26,7 @@ import { } from '@dnd-kit/sortable'; import type { AdminQuestion, QuestionType } from '@examora/types'; import { + API_PATHS, DIFFICULTY_OPTIONS, QUESTION_STATUS_OPTIONS, QUESTION_TYPE_OPTIONS, @@ -1212,7 +1213,7 @@ const QuestionsDetailContent: React.FC = () => { setLoading(true); try { const response = await request( - `/api/admin/questions/${questionId}`, + API_PATHS.admin.question(questionId), ); setQuestion(response.data); } catch (_error) { @@ -1234,7 +1235,6 @@ const QuestionsDetailContent: React.FC = () => { status: 'DRAFT', time_limit_ms: 2000, memory_limit_mb: 256, - content: { text: '' }, ...(initialType ? { type: initialType, @@ -1291,7 +1291,7 @@ const QuestionsDetailContent: React.FC = () => { } setSaving(true); const response = await request( - '/api/admin/questions', + API_PATHS.admin.questions, { method: 'POST', data: payload, skipErrorHandler: true }, ); message.success( @@ -1317,7 +1317,7 @@ const QuestionsDetailContent: React.FC = () => { delete payload.test_cases; } setSaving(true); - await request(`/api/admin/questions/${question.id}`, { + await request(API_PATHS.admin.question(question.id), { method: 'PUT', data: payload, skipErrorHandler: true, @@ -1381,7 +1381,7 @@ const QuestionsDetailContent: React.FC = () => { const deleteQuestion = async () => { if (!question) return; try { - await request(`/api/admin/questions/${question.id}`, { + await request(API_PATHS.admin.question(question.id), { method: 'DELETE', skipErrorHandler: true, }); diff --git a/apps/admin/src/pages/Content/Library/Questions/index.tsx b/apps/admin/src/pages/Content/Library/Questions/index.tsx index 6f8748b..9366a6b 100644 --- a/apps/admin/src/pages/Content/Library/Questions/index.tsx +++ b/apps/admin/src/pages/Content/Library/Questions/index.tsx @@ -16,6 +16,7 @@ import { } from '@ant-design/pro-components'; import type { AdminQuestion, QuestionType } from '@examora/types'; import { + API_PATHS, DIFFICULTY_OPTIONS, QUESTION_STATUS_OPTIONS, QUESTION_TYPE_OPTIONS, @@ -178,7 +179,7 @@ export const QuestionsPageContent: React.FC = ({ }), onOk: async () => { try { - await request(`/api/admin/questions/${question.id}`, { + await request(API_PATHS.admin.question(question.id), { method: 'DELETE', skipErrorHandler: true, }); @@ -260,7 +261,7 @@ export const QuestionsPageContent: React.FC = ({ }), onOk: async () => { try { - await request(`/api/admin/questions/${question.id}`, { + await request(API_PATHS.admin.question(question.id), { method: 'PATCH', data: { status: next }, skipErrorHandler: true, @@ -371,7 +372,7 @@ export const QuestionsPageContent: React.FC = ({ const response = await request<{ code: number; data: BatchActionResult; - }>('/api/admin/questions/batch/status', { + }>(API_PATHS.admin.questionBatchStatus, { method: 'PATCH', data: { ids: selectedRows.map((item) => item.id), status }, skipErrorHandler: true, @@ -419,7 +420,7 @@ export const QuestionsPageContent: React.FC = ({ const response = await request<{ code: number; data: BatchActionResult; - }>('/api/admin/questions/batch', { + }>(API_PATHS.admin.questionBatch, { method: 'DELETE', data: { ids: selectedRows.map((item) => item.id) }, skipErrorHandler: true, @@ -719,7 +720,7 @@ export const QuestionsPageContent: React.FC = ({ const response = await request<{ code: number; data: { items: AdminQuestion[]; total: number }; - }>('/api/admin/questions', { + }>(API_PATHS.admin.questions, { params: { page: params.current, page_size: params.pageSize, diff --git a/apps/admin/src/pages/Content/Papers/Detail/index.tsx b/apps/admin/src/pages/Content/Papers/Detail/index.tsx index d9ee00d..8b73f54 100644 --- a/apps/admin/src/pages/Content/Papers/Detail/index.tsx +++ b/apps/admin/src/pages/Content/Papers/Detail/index.tsx @@ -30,7 +30,11 @@ import { verticalListSortingStrategy, } from '@dnd-kit/sortable'; import type { AdminPaperOutline, AdminQuestion } from '@examora/types'; -import { DIFFICULTY_OPTIONS, QUESTION_TYPE_OPTIONS } from '@examora/types'; +import { + API_PATHS, + DIFFICULTY_OPTIONS, + QUESTION_TYPE_OPTIONS, +} from '@examora/types'; import { history, request, useIntl } from '@umijs/max'; import { App as AntdApp, @@ -282,7 +286,7 @@ const PaperDetailContent: React.FC = () => { const response = await request<{ code: number; data: AdminPaperOutline; - }>(`/api/admin/papers/${paperId}/outline`); + }>(API_PATHS.admin.paperOutline(paperId)); if (cancelled) return; const outline = response.data; setPaper(outline.paper); @@ -535,7 +539,7 @@ const PaperDetailContent: React.FC = () => { const saveOutline = async (targetPaperID: number) => { const response = await request<{ code: number; data: AdminPaperOutline }>( - `/api/admin/papers/${targetPaperID}/outline`, + API_PATHS.admin.paperOutline(targetPaperID), { method: 'PUT', skipErrorHandler: true, @@ -586,7 +590,7 @@ const PaperDetailContent: React.FC = () => { const values = await form.validateFields(); setSaving(true); const paperResponse = await request<{ code: number; data: Paper }>( - isCreate ? '/api/admin/papers' : `/api/admin/papers/${paperId}`, + isCreate ? API_PATHS.admin.papers : API_PATHS.admin.paper(paperId), { method: isCreate ? 'POST' : 'PUT', skipErrorHandler: true, @@ -1327,7 +1331,7 @@ const PaperDetailContent: React.FC = () => { const response = await request<{ code: number; data: { items: AdminQuestion[]; total: number }; - }>('/api/admin/questions', { + }>(API_PATHS.admin.questions, { params: { page: params.current, page_size: params.pageSize, diff --git a/apps/admin/src/pages/Content/Papers/index.tsx b/apps/admin/src/pages/Content/Papers/index.tsx index 0575ba6..a64ed72 100644 --- a/apps/admin/src/pages/Content/Papers/index.tsx +++ b/apps/admin/src/pages/Content/Papers/index.tsx @@ -11,6 +11,7 @@ import { type ProColumns, ProTable, } from '@ant-design/pro-components'; +import { API_PATHS } from '@examora/types'; import { history, request, useIntl } from '@umijs/max'; import { App as AntdApp, Button, Dropdown, Space, Tag, Tooltip } from 'antd'; import dayjs from 'dayjs'; @@ -91,7 +92,7 @@ const PapersPageContent: React.FC = () => { }), onOk: async () => { try { - await request(`/api/admin/papers/${paper.id}`, { + await request(API_PATHS.admin.paper(paper.id), { method: 'DELETE', skipErrorHandler: true, }); @@ -190,7 +191,7 @@ const PapersPageContent: React.FC = () => { const response = await request<{ code: number; data: BatchActionResult; - }>('/api/admin/papers/batch', { + }>(API_PATHS.admin.paperBatch, { method: 'DELETE', data: { ids: selectedRows.map((item) => item.id) }, skipErrorHandler: true, @@ -454,7 +455,7 @@ const PapersPageContent: React.FC = () => { const response = await request<{ code: number; data: { items: Paper[]; total: number }; - }>('/api/admin/papers', { + }>(API_PATHS.admin.papers, { params: { page: params.current, page_size: params.pageSize, diff --git a/apps/admin/src/pages/Examination/Candidates/index.tsx b/apps/admin/src/pages/Examination/Candidates/index.tsx index 64e1232..542a8d3 100644 --- a/apps/admin/src/pages/Examination/Candidates/index.tsx +++ b/apps/admin/src/pages/Examination/Candidates/index.tsx @@ -1,19 +1,30 @@ import { PageContainer } from '@ant-design/pro-components'; -import { history } from '@umijs/max'; +import { history, useIntl } from '@umijs/max'; import { Button, Card, Result } from 'antd'; import React from 'react'; const Index: React.FC = () => { + const intl = useIntl(); + return ( - + history.push('/')}> - 返回首页 + } /> diff --git a/apps/admin/src/pages/Examination/ExamList/index.tsx b/apps/admin/src/pages/Examination/ExamList/index.tsx index 1a9af71..5a0ceeb 100644 --- a/apps/admin/src/pages/Examination/ExamList/index.tsx +++ b/apps/admin/src/pages/Examination/ExamList/index.tsx @@ -6,6 +6,7 @@ import { type ProColumns, ProTable, } from '@ant-design/pro-components'; +import { API_PATHS } from '@examora/types'; import { history, request, useIntl } from '@umijs/max'; import { App as AntdApp, Button, Dropdown, Space, Tag } from 'antd'; import dayjs from 'dayjs'; @@ -163,7 +164,7 @@ const ExamListContent: React.FC = () => { const response = await request<{ code: number; data: BatchActionResult; - }>('/api/admin/exams/batch/close', { + }>(API_PATHS.admin.examBatchClose, { method: 'POST', data: { ids: closableSelectedRows.map((item) => item.id) }, skipErrorHandler: true, @@ -370,7 +371,7 @@ const ExamListContent: React.FC = () => { const response = await request<{ code: number; data: { items: Exam[]; total: number }; - }>('/api/admin/exams', { + }>(API_PATHS.admin.exams, { skipErrorHandler: true, params: { page: params.current, diff --git a/apps/admin/src/pages/Examination/ExamPublish/index.tsx b/apps/admin/src/pages/Examination/ExamPublish/index.tsx index c61d2e6..b643a91 100644 --- a/apps/admin/src/pages/Examination/ExamPublish/index.tsx +++ b/apps/admin/src/pages/Examination/ExamPublish/index.tsx @@ -1,4 +1,5 @@ import { PageContainer } from '@ant-design/pro-components'; +import { API_PATHS } from '@examora/types'; import { history, request, useIntl } from '@umijs/max'; import { Alert, @@ -39,7 +40,7 @@ const ExamPublishContent: React.FC = () => { if (!examId) return; setLoading(true); - request<{ code: number; data: Exam }>(`/api/admin/exams/${examId}`, { + request<{ code: number; data: Exam }>(API_PATHS.admin.exam(examId), { method: 'GET', skipErrorHandler: true, }) @@ -73,7 +74,7 @@ const ExamPublishContent: React.FC = () => { setSubmitting(true); try { const response = await request<{ code: number; message: string }>( - `/api/admin/exams/${examId}/publish`, + API_PATHS.admin.examPublish(examId), { method: 'POST', skipErrorHandler: true, diff --git a/apps/admin/src/pages/Login/Login.less b/apps/admin/src/pages/Login/Login.less index 705d6a3..ccd8d37 100644 --- a/apps/admin/src/pages/Login/Login.less +++ b/apps/admin/src/pages/Login/Login.less @@ -1,4 +1,5 @@ .examora-login { + position: relative; min-height: 100vh; display: grid; grid-template-columns: minmax(420px, 0.92fr) minmax(420px, 1fr); @@ -6,6 +7,14 @@ color: #262626; } +.examora-login__lang { + position: absolute; + top: 20px; + right: 24px; + z-index: 3; + color: #262626; +} + .examora-login__brand { position: relative; display: flex; diff --git a/apps/admin/src/pages/Login/Login.tsx b/apps/admin/src/pages/Login/Login.tsx index ff755eb..613b46d 100644 --- a/apps/admin/src/pages/Login/Login.tsx +++ b/apps/admin/src/pages/Login/Login.tsx @@ -5,6 +5,7 @@ import { SafetyCertificateOutlined, UserOutlined, } from '@ant-design/icons'; +import { API_PATHS } from '@examora/types'; import { request, useIntl } from '@umijs/max'; import { Alert, @@ -18,6 +19,7 @@ import { import { useEffect, useState } from 'react'; import type { AuthConfig, LoginResponse } from '@/auth/token'; import { setAccessToken, setLocalProfile } from '@/auth/token'; +import { SelectLang } from '@/components'; import './Login.less'; const { Text, Title } = Typography; @@ -66,7 +68,7 @@ const Login: React.FC = () => { const loadConfig = async () => { try { const response = await request>( - '/api/auth/config', + API_PATHS.auth.config, { method: 'GET', }, @@ -98,7 +100,7 @@ const Login: React.FC = () => { try { const response = await request< LoginResponse | ApiEnvelope - >('/api/auth/login', { + >(API_PATHS.auth.login, { method: 'POST', data: values, skipErrorHandler: true, @@ -129,11 +131,14 @@ const Login: React.FC = () => { }; const handleLogtoLogin = () => { - window.location.href = '/api/auth/logto/login'; + window.location.href = API_PATHS.auth.logtoLogin; }; return (
+
+ +
Examora diff --git a/apps/admin/src/pages/Monitoring/Proctoring/Events/index.tsx b/apps/admin/src/pages/Monitoring/Proctoring/Events/index.tsx index 852e7a2..fc0177c 100644 --- a/apps/admin/src/pages/Monitoring/Proctoring/Events/index.tsx +++ b/apps/admin/src/pages/Monitoring/Proctoring/Events/index.tsx @@ -1,19 +1,30 @@ import { PageContainer } from '@ant-design/pro-components'; -import { history } from '@umijs/max'; +import { history, useIntl } from '@umijs/max'; import { Button, Card, Result } from 'antd'; import React from 'react'; const Index: React.FC = () => { + const intl = useIntl(); + return ( - + history.push('/')}> - 返回首页 + } /> diff --git a/apps/admin/src/pages/System/Settings/Users/index.tsx b/apps/admin/src/pages/System/Settings/Users/index.tsx index db12d1a..2795f7a 100644 --- a/apps/admin/src/pages/System/Settings/Users/index.tsx +++ b/apps/admin/src/pages/System/Settings/Users/index.tsx @@ -10,6 +10,7 @@ import { type ProColumns, ProTable, } from '@ant-design/pro-components'; +import { API_PATHS } from '@examora/types'; import { request, useIntl } from '@umijs/max'; import { App as AntdApp, @@ -51,6 +52,14 @@ interface UserFormValues { const ROLE_KEYS = ['ADMIN', 'TEACHER', 'STUDENT'] as const; const STATUS_KEYS = ['ACTIVE', 'INACTIVE', 'SUSPENDED'] as const; +const normalizeRole = (role?: string) => { + const normalized = role?.toUpperCase(); + if (normalized === 'CLIENT') return 'STUDENT'; + return normalized || ''; +}; + +const normalizeStatus = (status?: string) => status?.toUpperCase() || ''; + const UserListContent: React.FC = () => { const intl = useIntl(); const { message: antdMessage } = AntdApp.useApp(); @@ -137,8 +146,8 @@ const UserListContent: React.FC = () => { username: user.username, display_name: user.display_name, email: user.email, - role: user.role as UserFormValues['role'], - status: user.status as UserFormValues['status'], + role: normalizeRole(user.role) as UserFormValues['role'], + status: normalizeStatus(user.status) as UserFormValues['status'], }); setDrawerOpen(true); }; @@ -148,7 +157,7 @@ const UserListContent: React.FC = () => { const values = await userForm.validateFields(); setSaving(true); await request( - editing ? `/api/admin/users/${editing.id}` : '/api/admin/users', + editing ? API_PATHS.admin.user(editing.id) : API_PATHS.admin.users, { method: editing ? 'PUT' : 'POST', data: values, @@ -212,7 +221,7 @@ const UserListContent: React.FC = () => { }), onOk: async () => { try { - await request(`/api/admin/users/${user.id}`, { + await request(API_PATHS.admin.user(user.id), { method: 'DELETE', }); antdMessage.success( @@ -303,11 +312,14 @@ const UserListContent: React.FC = () => { width: 90, search: false, valueEnum: roleValueEnum, - render: (_, user) => ( - - {roleLabelMap[user.role] || user.role} - - ), + render: (_, user) => { + const roleKey = normalizeRole(user.role); + return ( + + {roleLabelMap[roleKey] || user.role} + + ); + }, }, { title: intl.formatMessage({ @@ -319,13 +331,16 @@ const UserListContent: React.FC = () => { width: 90, search: false, valueEnum: statusValueEnum, - render: (_, user) => ( - - {statusLabelMap[user.status] || user.status} - - ), + render: (_, user) => { + const statusKey = normalizeStatus(user.status); + return ( + + {statusLabelMap[statusKey] || user.status} + + ); + }, }, { title: intl.formatMessage({ @@ -482,7 +497,7 @@ const UserListContent: React.FC = () => { const response = await request<{ code: number; data: { items: User[]; total: number }; - }>('/api/admin/users', { + }>(API_PATHS.admin.users, { params: { page: current, page_size: pageSize, @@ -566,7 +581,7 @@ const UserListContent: React.FC = () => { try { await userForm.validateFields(); setSaving(true); - await request('/api/admin/users', { + await request(API_PATHS.admin.users, { method: 'POST', data: userForm.getFieldsValue(), }); diff --git a/apps/admin/src/pages/Welcome/TrendLineChart.tsx b/apps/admin/src/pages/Welcome/TrendLineChart.tsx index 1aaeddf..83c468d 100644 --- a/apps/admin/src/pages/Welcome/TrendLineChart.tsx +++ b/apps/admin/src/pages/Welcome/TrendLineChart.tsx @@ -1,3 +1,4 @@ +import { useIntl } from '@umijs/max'; import { curveMonotoneX, area as d3Area, @@ -51,7 +52,7 @@ const toSeries = ({ return [ { key: 'trend', - label: '趋势', + label: 'Trend', color, data: data ?? [], showArea, @@ -84,6 +85,7 @@ const TrendLineChart: React.FC = ({ showLegend = false, compact = false, }) => { + const intl = useIntl(); const mergedSeries = toSeries({ data, series, color, showArea }); const values = getChartValues(mergedSeries); const labels = getLabels(mergedSeries); @@ -91,7 +93,7 @@ const TrendLineChart: React.FC = ({ if (!values.length || !labels.length) { return (
- 暂无趋势数据 + {intl.formatMessage({ id: 'pages.dashboard.chart.empty' })}
); } diff --git a/apps/admin/src/pages/Welcome/index.tsx b/apps/admin/src/pages/Welcome/index.tsx index 7aabda4..cd37cf2 100644 --- a/apps/admin/src/pages/Welcome/index.tsx +++ b/apps/admin/src/pages/Welcome/index.tsx @@ -18,6 +18,7 @@ import { UserOutlined, } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; import { Badge, Button, @@ -38,81 +39,94 @@ import './welcome.less'; const { Text, Title } = Typography; const Welcome: React.FC = () => { + const intl = useIntl(); + const f = (id: string, values?: Record) => + intl.formatMessage({ id }, values); + const weekDays = [ + f('pages.dashboard.week.mon'), + f('pages.dashboard.week.tue'), + f('pages.dashboard.week.wed'), + f('pages.dashboard.week.thu'), + f('pages.dashboard.week.fri'), + f('pages.dashboard.week.sat'), + f('pages.dashboard.week.sun'), + ]; + const stats = [ { key: 'candidates', - label: '注册考生', + label: f('pages.dashboard.stats.candidates.label'), value: 1247, trend: '+12.8%', - caption: '较上周新增 146 人', + caption: f('pages.dashboard.stats.candidates.caption'), icon: , tone: 'blue', chartColor: '#18181b', trendData: [ - { label: '周一', value: 1018 }, - { label: '周二', value: 1042 }, - { label: '周三', value: 1096 }, - { label: '周四', value: 1124 }, - { label: '周五', value: 1190 }, - { label: '周六', value: 1216 }, - { label: '周日', value: 1247 }, + { label: weekDays[0], value: 1018 }, + { label: weekDays[1], value: 1042 }, + { label: weekDays[2], value: 1096 }, + { label: weekDays[3], value: 1124 }, + { label: weekDays[4], value: 1190 }, + { label: weekDays[5], value: 1216 }, + { label: weekDays[6], value: 1247 }, ], }, { key: 'activeExams', - label: '进行中考试', + label: f('pages.dashboard.stats.activeExams.label'), value: 38, - trend: '45 人在线', - caption: '覆盖 6 个考场', + trend: f('pages.dashboard.stats.activeExams.trend'), + caption: f('pages.dashboard.stats.activeExams.caption'), icon: , tone: 'amber', chartColor: '#f97316', trendData: [ - { label: '周一', value: 24 }, - { label: '周二', value: 31 }, - { label: '周三', value: 28 }, - { label: '周四', value: 34 }, - { label: '周五', value: 38 }, - { label: '周六', value: 33 }, - { label: '周日', value: 38 }, + { label: weekDays[0], value: 24 }, + { label: weekDays[1], value: 31 }, + { label: weekDays[2], value: 28 }, + { label: weekDays[3], value: 34 }, + { label: weekDays[4], value: 38 }, + { label: weekDays[5], value: 33 }, + { label: weekDays[6], value: 38 }, ], }, { key: 'papers', - label: '试卷总数', + label: f('pages.dashboard.stats.papers.label'), value: 156, trend: '+8', - caption: '本月发布试卷', + caption: f('pages.dashboard.stats.papers.caption'), icon: , tone: 'green', chartColor: '#22c55e', trendData: [ - { label: '周一', value: 132 }, - { label: '周二', value: 138 }, - { label: '周三', value: 139 }, - { label: '周四', value: 144 }, - { label: '周五', value: 150 }, - { label: '周六', value: 152 }, - { label: '周日', value: 156 }, + { label: weekDays[0], value: 132 }, + { label: weekDays[1], value: 138 }, + { label: weekDays[2], value: 139 }, + { label: weekDays[3], value: 144 }, + { label: weekDays[4], value: 150 }, + { label: weekDays[5], value: 152 }, + { label: weekDays[6], value: 156 }, ], }, { key: 'questions', - label: '题目总数', + label: f('pages.dashboard.stats.questions.label'), value: 2843, trend: '92%', - caption: '题库可复用率', + caption: f('pages.dashboard.stats.questions.caption'), icon: , tone: 'violet', chartColor: '#404040', trendData: [ - { label: '周一', value: 2660 }, - { label: '周二', value: 2704 }, - { label: '周三', value: 2748 }, - { label: '周四', value: 2768 }, - { label: '周五', value: 2812 }, - { label: '周六', value: 2824 }, - { label: '周日', value: 2843 }, + { label: weekDays[0], value: 2660 }, + { label: weekDays[1], value: 2704 }, + { label: weekDays[2], value: 2748 }, + { label: weekDays[3], value: 2768 }, + { label: weekDays[4], value: 2812 }, + { label: weekDays[5], value: 2824 }, + { label: weekDays[6], value: 2843 }, ], }, ]; @@ -120,7 +134,7 @@ const Welcome: React.FC = () => { const loadTrendSeries = [ { key: 'online', - label: '在线考生', + label: f('pages.dashboard.series.online'), color: '#18181b', showArea: true, data: [ @@ -135,7 +149,7 @@ const Welcome: React.FC = () => { }, { key: 'queue', - label: '判题队列', + label: f('pages.dashboard.series.queue'), color: '#f97316', dashed: true, data: [ @@ -150,7 +164,7 @@ const Welcome: React.FC = () => { }, { key: 'risk', - label: '风险事件', + label: f('pages.dashboard.series.risk'), color: '#ef4444', data: [ { label: '08:00', value: 2 }, @@ -167,56 +181,59 @@ const Welcome: React.FC = () => { const exams = [ { key: 'math-final', - name: '2026 春季高等数学期末考试', + name: f('pages.dashboard.exams.math'), progress: 78, active: 45, total: 120, - status: '进行中', - risk: '低风险', + status: f('pages.dashboard.status.running'), + risk: f('pages.dashboard.risk.low'), + riskLevel: 'low', }, { key: 'python', - name: 'Python 程序设计能力测试', + name: f('pages.dashboard.exams.python'), progress: 45, active: 12, total: 60, - status: '进行中', - risk: '关注', + status: f('pages.dashboard.status.running'), + risk: f('pages.dashboard.risk.watch'), + riskLevel: 'watch', }, { key: 'physics', - name: '大学物理模拟测试', + name: f('pages.dashboard.exams.physics'), progress: 100, active: 0, total: 80, - status: '已结束', - risk: '正常', + status: f('pages.dashboard.status.closed'), + risk: f('pages.dashboard.risk.normal'), + riskLevel: 'normal', }, ]; const activities = [ { key: 'a1', - text: '李娜提交了 Python 程序设计能力测试', - meta: '5 分钟前', + text: f('pages.dashboard.activities.a1'), + meta: f('pages.dashboard.activities.5m'), color: '#262626', }, { key: 'a2', - text: '王磊的考试被系统自动提交', - meta: '8 分钟前', + text: f('pages.dashboard.activities.a2'), + meta: f('pages.dashboard.activities.8m'), color: '#f97316', }, { key: 'a3', - text: '张伟开始作答 2026 春季高等数学', - meta: '12 分钟前', + text: f('pages.dashboard.activities.a3'), + meta: f('pages.dashboard.activities.12m'), color: '#22c55e', }, { key: 'a4', - text: '陈静提交了数据结构与算法测试', - meta: '15 分钟前', + text: f('pages.dashboard.activities.a4'), + meta: f('pages.dashboard.activities.15m'), color: '#262626', }, ]; @@ -224,55 +241,61 @@ const Welcome: React.FC = () => { const judgeQueues = [ { key: 'waiting', - label: '等待判题', + label: f('pages.dashboard.queue.waiting'), value: 126, percent: 64, color: '#18181b', }, { key: 'running', - label: '执行中', + label: f('pages.dashboard.queue.running'), value: 18, percent: 42, color: '#f97316', }, - { key: 'failed', label: '需复核', value: 7, percent: 18, color: '#ef4444' }, + { + key: 'failed', + label: f('pages.dashboard.queue.failed'), + value: 7, + percent: 18, + color: '#ef4444', + }, ]; const shortcuts = [ { key: 'users', - label: '用户管理', + label: f('pages.dashboard.shortcuts.users'), path: '/system/settings/users', icon: , }, { key: 'exams', - label: '考试管理', + label: f('pages.dashboard.shortcuts.exams'), path: '/examination/exams', icon: , }, { key: 'papers', - label: '试卷管理', + label: f('pages.dashboard.shortcuts.papers'), path: '/content/papers', icon: , }, { key: 'questions', - label: '题库管理', + label: f('pages.dashboard.shortcuts.questions'), path: '/content/library/questions', icon: , }, { key: 'programming', - label: '编程题库', + label: f('pages.dashboard.shortcuts.programming'), path: '/content/library/programming', icon: , }, { key: 'submissions', - label: '提交记录', + label: f('pages.dashboard.shortcuts.submissions'), path: '/assessment/results/submissions', icon: , }, @@ -285,31 +308,31 @@ const Welcome: React.FC = () => {
- 考试运营工作台 + {f('pages.dashboard.hero.title')} - 实时运营 + {f('pages.dashboard.hero.kicker')} Examora Admin
- 聚合考试进度、监考事件、题库资产与判题队列,帮助管理员快速判断今天的关键风险。 + {f('pages.dashboard.hero.subtitle')}
45 - 在线考生 + {f('pages.dashboard.hero.online')} 9 - 风险关注 + {f('pages.dashboard.hero.risk')} 126 - 等待判题 + {f('pages.dashboard.hero.queue')}
@@ -319,13 +342,13 @@ const Welcome: React.FC = () => { type="primary" icon={} > - 新建考试 + {f('pages.dashboard.actions.createExam')}
@@ -349,7 +372,9 @@ const Welcome: React.FC = () => { {stat.caption} { title={ - 考试负载趋势 + {f('pages.dashboard.load.title')} } - extra={近 12 小时} + extra={ + {f('pages.dashboard.load.range')} + } >
- 峰值在线 + + {f('pages.dashboard.load.peakOnline')} + 84 - 16:00 达到峰值 + {f('pages.dashboard.load.peakOnlineCaption')}
- 队列峰值 + + {f('pages.dashboard.load.peakQueue')} + 126 - 平均处理 42 秒 + {f('pages.dashboard.load.peakQueueCaption')}
- 风险事件 + + {f('pages.dashboard.load.riskEvents')} + 9 - 较昨日下降 18% + {f('pages.dashboard.load.riskEventsCaption')}
{ title={ - 考试进度 + {f('pages.dashboard.progress.title')} } extra={ @@ -420,7 +453,7 @@ const Welcome: React.FC = () => { icon={} iconPlacement="end" > - 全部考试 + {f('pages.dashboard.progress.allExams')} } > @@ -433,20 +466,30 @@ const Welcome: React.FC = () => {
- {exam.active} 人在考 + {' '} + {f('pages.dashboard.progress.activeCount', { + count: exam.active, + })} - {exam.total} 分钟 + {' '} + {f('pages.dashboard.progress.duration', { + count: exam.total, + })}
{exam.status} - + {exam.risk} @@ -468,12 +511,12 @@ const Welcome: React.FC = () => { title={ - 监考风险 + {f('pages.dashboard.risk.title')} } extra={ - 需关注 9 + {f('pages.dashboard.risk.watchCount')} } > @@ -486,26 +529,34 @@ const Welcome: React.FC = () => { railColor="#f4f4f5" />
- 今日考试健康度 + + {f('pages.dashboard.risk.healthLabel')} + - 整体稳定 + {f('pages.dashboard.risk.healthTitle')} - 自动提交、离屏与网络波动事件均处于可控范围。 + {f('pages.dashboard.risk.healthCopy')}
- 离屏事件 + + {f('pages.dashboard.risk.offscreen')} + 6
- 网络波动 + + {f('pages.dashboard.risk.network')} + 3
- 人工复核 + + {f('pages.dashboard.risk.review')} + 2
@@ -520,7 +571,7 @@ const Welcome: React.FC = () => { title={ - 判题队列 + {f('pages.dashboard.queue.title')} } > @@ -549,10 +600,15 @@ const Welcome: React.FC = () => { title={ - 考试动态 + {f('pages.dashboard.activities.title')} } - extra={} + extra={ + + } > { title={ - 快捷入口 + {f('pages.dashboard.shortcuts.title')} } > diff --git a/apps/admin/src/services/api.ts b/apps/admin/src/services/api.ts index 0f86029..d2b988f 100644 --- a/apps/admin/src/services/api.ts +++ b/apps/admin/src/services/api.ts @@ -1,10 +1,11 @@ // @ts-ignore /* eslint-disable */ +import { API_PATHS } from "@examora/types"; import { request } from "@umijs/max"; -/** 获取当前的用户 GET /api/auth/me */ +/** 获取当前的用户 GET /api/v1/auth/me */ export async function currentUser(options?: { [key: string]: any }) { - return request("/api/auth/me", { + return request(API_PATHS.auth.me, { method: "GET", ...(options || {}), }); diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 2f79831..9509dfd 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1,49 +1,19 @@