# 前端架构重构与去重方案 日期:2026-11-06。状态:本轮前端结构重构已完成静态验收与本地前后端浏览器回归;未部署、不变更后端接口或数据库。判题进行态、真实过期会话与真实 ZIP 上传仍需在隔离测试数据上继续验证。 ## 当前实施状态(2026-11-06) 本文件不再把已经完成的改动写成“待实施”。本轮按可独立验证的批次推进,保留既有路由、接口契约和已确认的页面行为。 | 范围 | 实施状态 | 已落地内容 | 仍需验证或后续处理 | | --- | --- | --- | --- | | 鉴权与下载 | 已完成 | 单飞刷新、刷新失败时拒绝全部等待者、刷新失败只广播一次跨标签登出;Word/PDF/Zip 下载统一走带 Bearer Header 的 Blob 请求,不再在 URL 中暴露令牌 | 本地教师会话已验证带鉴权 ZIP 导出;真实过期凭据与 Word/PDF 下载仍需回归 | | 后台分页 | 已完成试点 | 算法题、数据库题后台列表接入 `usePagedQuery`,包含最新请求优先与页码回退 | 客观题、用户、题集等列表按各自查询语义分批接入,不强行泛化 | | 题目候选检索 | 已完成 | 题集、考试、课练三处接入 `useQuestionCandidates`;支持精确题号、分页、竞态防护并保留客观题自定义分值 | 本地题集新建页已验证 `#1` 精确检索;三种角色和长题库数据仍需回归 | | 判题结果 | 已完成 | 四个结果页接入 `useSubmissionMonitor`,使用链式调度避免慢请求重叠;考试结果返回路径改为命名路由 | 实际判题中的持续状态、失败状态和快速切换提交需浏览器回归 | | 编辑草稿 | 已完成 | 四个代码/SQL 提交页接入 `useCodeDraft`;按用户、题目/考试场次和语言隔离,旧键静默迁移,考试后端暂存仍优先 | 登录前旧草稿迁移和浏览器存储配额错误需手工验证 | | 倒计时与客观题答案 | 已完成 | `useCountdown` 接入两类考试页和课练页;`objectiveAnswer` 统一选项编号、多选答案排序、判断题兼容与选中状态 | 真实截止时刻、断网恢复与不同题型自动保存需浏览器回归 | | 测试用例管理 | 已完成 | `useQuestionTestCases` 收敛测试点统计、公开样例策略与 ZIP 导入导出;题目编辑页只负责表单编排 | 本地教师会话已验证测试点统计、Zip 导出与空文件时禁用导入;真实 ZIP 上传及失败提示仍需隔离测试数据回归 | | 未接入原型 | 已清理 | 删除没有真实调用方的 `ObjectiveInput` 及其测试,避免形成第二套客观题输入实现 | 未来以“页面行为测试 + 一个真实调用方”作为重新抽取的前置条件 | 本轮静态验证结果:`npm run lint` 为 0 error、0 warning;`npm run test:unit -- --run` 为 18 个测试文件、88 项通过;`npm run build` 已通过。生产构建仍提示 Monaco 编辑器分包为 3.5 MB(gzip 896 KB),这是可量化的后续性能优化项,不通过调高阈值掩盖。 本地运行时回归(2026-11-06):使用本机 Vite 前端与已启动的本机后端,以种子教师账号验证登录、前台题目详情、后台编程题列表的题号精确检索、题集新建页的 `#1` 候选检索、题目编辑页的测试点统计和带鉴权 ZIP 导出、提交监控表格。上述页面控制台无 error/warn。没有创建代码提交、重判、题集或测试数据;ZIP 导入仅验证了弹窗和未选择文件时的禁用状态,未上传文件。 ## 1. 结论 采用「按业务归组、复用稳定逻辑、分批替换」的重构方式,继续使用 Vue 3、TypeScript、Pinia、Vue Router 和现有样式体系。 当前主要问题是同一行为散落在不同页面、页面同时承担太多职责,而不只是文件数量多。重构后文件数量可能增加,但修改一次分页、判题状态或下载错误处理,应能覆盖所有使用者。 优先顺序:请求与鉴权 → 查询分页 → 判题结果与轮询 → 大页面拆分 → 样式及残留清理。目录迁移跟随业务批次进行,不先全站搬家。 ## 2. 当前代码基线 对 `frontend/src` 的当前工作区统计,包含尚未提交的题解等改动: | 项目 | 实际情况 | | --- | --- | | 源目录文件 | 138 个 | | Vue 文件 | 77 个 | | Vue/TS/CSS 总行数 | 约 49,978 行,含空行、注释和测试,不是有效逻辑行数 | | 超过 1000 行的源文件 | 9 个 | | 单元测试 | 18 个文件、88 项测试,本轮 `npm run test:unit -- --run` 全部通过 | 主要大文件:`PracticeDetailView.vue` 约 2265 行、`ProfileView.vue` 2038 行、`admin/QuestionEditView.vue` 1357 行、`AppSidebar.vue` 1337 行、`HomeView.vue` 1246 行、`ExamSessionView.vue` 1245 行。 这些行数用于确定检查顺序,不等同于重复率。本轮没有运行克隆检测工具,不承诺一个未经测量的「重复率下降百分比」。测试通过也不代表关键流程已覆盖:现有测试集中在侧栏权限、用户 store、分页、Markdown、SQL 解析、题集编辑和公告接口。 ## 3. 已确认的优先改造点 ### 3.1 请求、登录刷新和文件下载(P0) 代码证据: - `frontend/src/api/request.ts:64` 与 `:142` 在正常响应和 HTTP 异常响应中分别处理凭据刷新,存在相似但不一致的逻辑。 - `request.ts:54` 的刷新失败处理只清空订阅数组;`:126` 为等待者创建的 Promise 没有在这个失败分支被 reject。代码检查提示并发请求可能一直等待,实施前必须用可重复测试确认并修复。 - `api/question.ts:95` 与 `api/exam.ts:293` 分别处理鉴权、fetch、Blob、临时链接和回收;还有直接拼接 token 下载 URL 的分支。 方案细化与落地方案: 1. **共享刷新 Single-flight Promise 与挂起死锁修复**: 保留现有 `http.get/post/...` 调用接口,统一底层 `executeWithAuth`。 ```typescript // 结构化刷新状态锁 let refreshPromise: Promise | null = null; let refreshSubscribers: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void }> = []; async function getValidAccessToken(): Promise { if (!refreshPromise) { refreshPromise = (async () => { try { const newToken = await authApi.refreshToken(); refreshSubscribers.forEach(cb => cb.resolve(newToken)); return newToken; } catch (err) { // 关键:逐一拒绝所有排队等待的并发请求,消除死锁 Promise refreshSubscribers.forEach(cb => cb.reject(err)); // 触发全局登出与广播同步 userStore.logout(); notifyCrossTabLogout(); throw err; } finally { refreshSubscribers = []; refreshPromise = null; } })(); } return new Promise((resolve, reject) => { refreshSubscribers.push({ resolve, reject }); }); } ``` 2. **多标签页跨 Tab 同步登出广播(BroadcastChannel / Storage)**: ```typescript const authChannel = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('kece_auth_channel') : null; export function notifyCrossTabLogout(): void { authChannel?.postMessage({ type: 'LOGOUT' }); localStorage.setItem('kece:logout_event', String(Date.now())); } // 在 main.ts 启动监听:收到后清空内存 store 并重定向到 /login,中断所有后台静默轮询。 ``` 3. **HTTP 状态与业务 Code 归一化**: 所有错误包装为统一定义的 `ApiError`,携带 `status`(HTTP 状态码)、`code`(服务端业务码)、`message`。继续尊重项目 `code=200` 为成功。 4. **统一下载器封装 (`downloadFile`)**: 抽取 `shared/http/download.ts`: ```typescript export interface DownloadOptions { url: string; method?: 'GET' | 'POST'; body?: unknown; defaultFilename?: string; } export async function downloadFile(options: DownloadOptions): Promise { // 1. 带 Bearer Token 发起 fetch // 2. 检查 Content-Type:若为 application/json,解析为错误体抛出 ApiError,禁止将 JSON 错误页下载为 Zip/Docx // 3. 从 Content-Disposition 解析 filename*=UTF-8''... 或 filename=...,缺省用 defaultFilename // 4. 创建 window.URL.createObjectURL(blob),触发 a 标签点击后立即在 microtask 中 revokeObjectURL } ``` 5. **安全与限制**: 普通接口不引入自动重试 POST;下载接口全面去除 URL Query 拼接 token,统一由 Bearer Header 传递。 验收:并发过期时只刷新一次;刷新失败所有调用方结束等待;退出登录期间不得因迟到刷新重新登录;下载失败不得把 JSON 错误页保存为 Zip。 ### 3.2 搜索、分页和异步列表(P1) 代码证据:后台三类题目列表、用户/题集/公告列表重复维护 `rows/total/loading/error/page`、防抖和筛选重置;`AdminQuestionsView.vue:44` 已有请求序号防串页,`QuestionSolutionList.vue:22` 的加载逻辑没有同等保护。`RankingsView.vue` 则已有 URL 驱动查询,不能统一成丢失 URL 的本地状态。 方案细化与落地方案: 1. **`usePagedQuery` 核心实现规格**: 抽取 `shared/composables/usePagedQuery.ts`,严格控制单一职责: ```typescript export interface PagedQueryOptions { fetcher: (params: P & { page: number; size: number }) => Promise<{ list: T[]; total: number }>; defaultParams?: P; defaultSize?: number; immediate?: boolean; } export function usePagedQuery>(options: PagedQueryOptions) { const page = ref(1); const size = ref(options.defaultSize ?? 20); const params = reactive

({ ...(options.defaultParams as P) }); const list = ref([]) as Ref; const total = ref(0); const loading = ref(false); const error = ref(null); let requestSeq = 0; // 自增请求序号,杜绝迟到响应串页(Race Condition 防御) async function load(targetPage?: number) { if (targetPage !== undefined) page.value = targetPage; const currentSeq = ++requestSeq; loading.value = true; error.value = null; try { const res = await options.fetcher({ ...params, page: page.value, size: size.value }); if (currentSeq === requestSeq) { // 仅当是最新请求时采纳 list.value = res.list; total.value = res.total; // 自动回退保护:删除最后一项后如果当前页超出最大页码,自动回退有效页 const maxPage = Math.max(1, Math.ceil(res.total / size.value)); if (page.value > maxPage) { load(maxPage); } } } catch (err: unknown) { if (currentSeq === requestSeq) { error.value = err.message || '加载列表失败'; } } finally { if (currentSeq === requestSeq) loading.value = false; } } function reset() { page.value = 1; load(); } if (options.immediate ?? true) load(); return { page, size, params, list, total, loading, error, load, reset }; } ``` 2. **防抖与触发源隔离**: 页面上的搜索框 `keyword` 通过独立防抖驱动 `load(1)`,不监听已解构的属性产生级联重复请求。 3. **复用现有 `DataTable.vue` 和 `Pagination.vue`**:不再制造第二套通用表格/分页组件。 验收:三类后台题目筛选结果不变;题号精确查询、关键词、防抖、空状态、失败重试和删除回页正确;排名 URL、浏览器前进后退行为保留。 组卷候选检索也是这一批的重要复用点:`admin/ExamEditView.vue:74` 与 `admin/PracticeEditView.vue:87` 的三题库加载、标签切换、防抖和候选映射近似;已有 `components/problem-sets/QuestionPicker.vue:24` 具备分页、`#ID` 查询、竞态保护和卸载清理。 当前考试/课练候选加载还只取第一页 100 条。迁移到分页检索可以解决这一实际限制,但它属于明确的行为改进,应单独写验收用例,而不是声称完全等价搬迁。 从现有 QuestionPicker 抽取 `useQuestionCandidates` 检索内核,供题集、考试和课练的选择界面使用。候选统一携带题型、ID、标题、必要评分元数据,联合键使用题型+ID。题集的章节目标与单一题型限制、考试的添加操作、课练的选择/取消操作及分值策略保留在各自编排页面;不直接把三套页面替换成同一个大选择器。 标准化候选实体类型接口定义(解决当前分页与元数据不统一限制): ```typescript export interface StandardCandidate { uniqueKey: string; // 标准复合主键 `${type}_${id}`,避免 ALGO 和 DB 题号重叠冲突 id: number; type: 'ALGO' | 'DB' | 'OBJECTIVE'; title: string; subType?: string; // 针对客观题细分: SINGLE / MULTIPLE / JUDGE / FILL difficulty?: string; // EASY / MEDIUM / HARD defaultScore?: number; // 题库默认分值,供组卷赋分预填 } ``` ### 3.3 判题状态、结果呈现和轮询(P1) 代码证据:`QuestionResultView.vue:31`、`DatabaseQuestionResultView.vue:31`、`ExamQuestionResultView.vue:25`、`ExamDbQuestionResultView.vue:24` 重复声明终态;现有 `utils/submission.ts` 已包含终态判断、语言、默认代码、状态文字和内存格式化。四个结果页还分别实现定时轮询。 方案细化与落地方案: 1. **统一判题轮询控制器 `useSubmissionMonitor`**: 抽取 `features/judging/composables/useSubmissionMonitor.ts`,基于链式 `setTimeout` 实现自适应、无重叠轮询: ```typescript export interface SubmissionMonitorOptions { submissionId: Ref; fetcher: (id: number) => Promise; isTerminal: (result: T) => boolean; onSuccess?: (result: T) => void; intervalMs?: number; // 默认 1500ms maxTimeoutMs?: number; // 默认 120000ms(防无限挂起超时) } export function useSubmissionMonitor(options: SubmissionMonitorOptions) { const result = ref(null) as Ref; const polling = ref(false); const error = ref(null); let timer: number | null = null; let activeId: number | null = null; const stop = () => { if (timer !== null) { clearTimeout(timer); timer = null; } polling.value = false; }; const scheduleNext = () => { if (!polling.value) return; timer = window.setTimeout(async () => { if (!options.submissionId.value || options.submissionId.value !== activeId) return; try { const res = await options.fetcher(activeId); if (options.submissionId.value !== activeId) return; // 切换 ID 丢弃迟到响应 result.value = res; if (options.isTerminal(res)) { stop(); options.onSuccess?.(res); } else { scheduleNext(); // 只有本次请求返回后才安排下一次,绝对避免慢网络请求堆叠 } } catch (err: unknown) { error.value = err instanceof Error ? err.message : '获取评测结果异常'; stop(); } }, options.intervalMs ?? 1500); }; watch(options.submissionId, (newId) => { stop(); result.value = null; error.value = null; activeId = newId; if (newId) { polling.value = true; // 立即首查一次 options.fetcher(newId).then((res) => { if (activeId !== newId) return; result.value = res; if (!options.isTerminal(res)) scheduleNext(); else { stop(); options.onSuccess?.(res); } }).catch(err => { error.value = err.message; stop(); }); } }, { immediate: true }); onBeforeUnmount(stop); return { result, polling, error, stop }; } ``` 2. **应用范围覆盖**: - 4 个独立结果页(算法结果、数据库结果、考试算法结果、考试数据库结果)。 - 随堂课练详情页 [`PracticeDetailView.vue`](file:///c:/Users/zco/Desktop/KECE/frontend/src/views/PracticeDetailView.vue):监听正在评测的实操题,彻底消除内部 `pendingJudgePollTimer` 的手动定时器。 3. **表现层与数据层分离**: 抽取通用的 `SubmissionStatusBadge.vue` 和 `TestCaseResultList.vue`,考试成绩披露规则通过属性传递,不写死在轮询控制器中。 验收:4 个结果页与课练内联轮询的成功/失败/终态一致;慢请求不重叠;路由切换不显示上一次提交;组件卸载后立即停止网络调度。 ### 3.4 返回上下文、草稿与编辑器(P1/P2) 题集、课练、普通刷题页面重复组装 `setId/itemId/practiceCode/pqId`;可抽出白名单参数解析与返回地址生成,避免遗漏上下文。考试场次使用单独的上下文模型,不能与普通题目路由混用。 编辑器继续使用 `CodeEditor.vue`;草稿存取、默认代码和未保存状态从页面移到所属业务模块。草稿键要明确账号、题型、题目或考试场次、语言,实施前设计旧草稿兼容策略,不能改完 key 后让用户原草稿消失。 方案细化与落地方案: 1. **题目返回上下文统筹工具 (`useQuestionNavContext`)**: 抽取 `features/questions/composables/useQuestionNavContext.ts`: - 提取规范 query 白名单:`setId`(题集)、`itemId`(章节项)、`practiceCode`(课练口令)、`pqId`(课练题目项)。 - 自动生成返回 URL: - 若有 `practiceCode`:返回 `/practice/:practiceCode` 或携带 query 返回; - 若有 `setId`:返回 `/problem-sets/:setId`; - 否则默认返回 `/questions` 或 `/database` 题库大厅。 - 解决各页面手工拼接字符串导致的参数漏传与返回错乱。 2. **草稿前向静默迁移体系 (`useCodeDraft`)**: ```typescript export function useCodeDraft(options: { targetKey: string; // 新规范 Key,如 kece:draft:${userId}:${type}:${questionId}:${lang} legacyKeys?: string[]; // 历史规范 Key 列表,如 ['kece:code_draft:q_1_cpp', 'kece:db_draft_1'] }) { function getDraft(defaultValue = ''): string { const current = localStorage.getItem(options.targetKey); if (current !== null) return current; // 遍历旧 Key 回退探测 if (options.legacyKeys) { for (const lk of options.legacyKeys) { const val = localStorage.getItem(lk); if (val !== null) { localStorage.setItem(options.targetKey, val); // 静默迁移至新键 localStorage.removeItem(lk); // 清理旧键 return val; } } } return defaultValue; } function saveDraft(code: string): void { if (!code) localStorage.removeItem(options.targetKey); else localStorage.setItem(options.targetKey, code); } function clearDraft(): void { localStorage.removeItem(options.targetKey); } return { getDraft, saveDraft, clearDraft }; } ``` 3. **保留业务边界差异**: - 考试恢复时后端暂存与本地草稿的现有优先级不变; - ACM 保存后评测、OI 暂存的区别不变; - 教师预览、已交卷课练不误绑课练成绩; - Monaco / Prism 初始化副作用与语言 worker 保持独立加载。 ### 3.5 大页面拆分(P2) 本节是后续页面组件拆分的设计约束,不表示其中列出的组件已经创建。没有真实调用方的 `ObjectiveInput` 原型已从当前工作区移除;`useCountdown` 已连同三处真实调用与单元测试落地。 | 现有页面 | 建议内部职责 | 拆分后的局部子组件/Composables 建议 | | --- | --- | --- | | `PracticeDetailView.vue` (2265 行) | 课练会话管理、三类题目导航、实时榜单、客观题即时答题抽屉 | `PracticeHeader.vue`、`PracticeQuestionNav.vue`、`PracticeScoreboard.vue`、`ObjectiveDrawer.vue`、`usePracticeSession.ts` | | `ProfileView.vue` (2038 行) | 用户资料卡片、个人 AC/做题热力统计、提交历史记录、考试历史、我的题解列表 | `ProfileHeader.vue`、`ProfileHeatmap.vue`、`ProfileSubmissions.vue`、`ProfileExams.vue`、`ProfileSolutions.vue` | | `admin/QuestionEditView.vue` (1357 行) | 基础题干表单、Markdown 内容编辑、评测环境限制、测试用例导入/手动列表 | `QuestionBaseForm.vue`、`QuestionLimitsForm.vue`、`QuestionTestCasesManager.vue`、`useQuestionEditor.ts` | | `admin/AdminPracticeOverviewView.vue` | 考务核心指标卡、逐题正确率分析、学生签到/提交名单、逐题代码作答审计抽屉 | `PracticeMetricCards.vue`、`PracticeQuestionStats.vue`、`PracticeStudentRoster.vue`、`StudentAuditDrawer.vue` | | `admin/AdminSubmissionsView.vue` | 算法/SQL/客观题评测流过滤、重判动作、代码抽屉展示 | `SubmissionFilterBar.vue`、`SubmissionDetailDrawer.vue`、`useAdminSubmissions.ts` | | `ExamSessionView.vue` (1245 行) | 考试生命周期、交卷防误触、多题快速切换、暂存与评测 | `ExamSessionHeader.vue`、`ExamQuestionTabs.vue`、`ExamSubmitDialog.vue`、`useExamSession.ts` | | `HomeView.vue` (1246 行) | 动态打字主视觉、系统实时节点监控、练习板入口、实时评测沙箱演示动效 | `HomeHeroSection.vue`、`HomeClusterStatus.vue`、`HomePracticeBoard.vue`、`HomeSandboxDemo.vue` | | `AppSidebar.vue` (1337 行) | 侧边栏折叠收放、菜单分组权限过滤、底栏登出与个人中心 | `SidebarMenuGroups.vue`、`SidebarUserBadge.vue`、`useSidebarNav.ts` | 大页面拆分原则与落地方案: 1. **单一页面私有组件就近存放**: 拆分出的子组件存放在各自业务目录下的子目录中(例如 `features/practice/components/detail/`),不提升到全局 `shared/`,避免破坏业务封装性。 2. **状态归属清晰**: 页面根 View 充当编排协调者(Orchestrator),负责提供 props 与响应 events;非跨路由共享的数据一律不丢入全局 Pinia,只保存在页面专属的组合式函数(如 `usePracticeSession.ts`)内。 3. **客观题渲染与输入解耦**: 当前已抽取 `features/questions/lib/objectiveAnswer.ts`,统一选项文本/字母、答案归一化和多选排序;继续保留三处页面各自的提交与自动保存时机。只有三处输入交互能够完全同构时,才引入受控 `ObjectiveInput.vue`,避免制造含大量场景开关的万能组件。 4. **计时器逻辑纯粹化 (`useCountdown`)**: 抽离 `shared/composables/useCountdown.ts`:以服务端结束时间 `endTime` 与客户端对齐时间戳计算剩余秒数,不依赖 `setInterval` 简单 `-1`,杜绝浏览器切后台休眠带来的倒计时变慢或误差,支持 `durationMinutes === 0` 不限时场景。 建议抽取接口的范围: | 模块 | 调用方提供 | 模块负责 | 不负责 | | --- | --- | --- | --- | | `usePagedQuery` | 查询参数、分页请求函数 | 异步状态、最新结果、重载、清理 | 列定义、删除/重判、权限 | | `useQuestionCandidates` | 题型、关键词/题号、页码 | 三类候选查询、标准化身份与元数据 | 章节、分值策略、选中/取消规则 | | `useSubmissionMonitor` | 当前提交 ID、读取结果函数 | 终态、进度、无重叠轮询、错误与清理 | 提交代码、考试计分、导航 | | 草稿模块 | 稳定存储身份、内容、恢复策略 | 恢复/保存/重置与存储失败处理 | 交卷、后端自动保存策略 | 这几个模块已有多个真实使用场景,值得形成较小的公共 interface;无需再抽一层只有转发调用的万能业务 service。 ## 4. 目标目录与依赖规则 以下为目标示意,按批次创建实际需要的目录,不一次生成空壳: ```text frontend/src/ app/ router/ # 路由组装、权限守卫 layouts/ # 前台/后台布局与导航 features/ questions/ # 三类题目、管理页、检索候选 judging/ # 提交状态与结果监控 solutions/ # 题解阅读、写作、管理 practice/ # 课练会话、榜单、管理 exams/ # 考试场次、答卷、组卷 problem-sets/ # 题集编排与学习进度 profile/ # 个人主页 home/ # 首页演示与练习入口 shared/ http/ # HTTP、刷新、二进制下载 ui/ # 分页、表格、对话框等通用 UI composables/ # 无业务归属的可复用状态逻辑 lib/ # Markdown、纯格式化等基础能力 styles/ # token、reset、公共样式 types/ # PageResult 等真正通用的契约 ``` 业务目录内部按需要放置 `pages/components/composables/api.ts/types.ts`,小模块可直接用少量文件,不强制层层套目录。 依赖方向:`app → features → shared`。`shared` 不导入业务页面;不同 feature 只使用显式开放的能力,例如考试可调用 judging 的结果监控,不能访问另一个 feature 的页面私有状态。两处出现相似代码后才考虑公共接口,不预先造插件框架。 迁移规则: 1. 保留现有路由 URL、name、query、角色配置与懒加载;隐藏管理菜单只是体验,权限依然由后端校验。 2. 每个业务批次先整理逻辑,再以独立可审查变更移动文件和更新 import。类型跟随所属业务,公共 `PageResult` 等留共享目录。 3. `types/index.ts`、旧 API 文件可临时定向 re-export,记录剩余调用;迁移完成删除兼容出口,不能长期存在两份实现。 4. 避免大而全的 `export *` 入口让首页间接加载 Monaco 或所有业务页面。Monaco/Prism 分包必须做生产构建后的冷启动验证。 5. 路由表拆成前台/后台/考试等静态分组;守卫独立测试。导航元数据可共享名称和权限,但导航结构不必机械等同于路由结构。 ## 5. 样式和死代码清理规则 继续保留当前黑白、直角、白底代码块的视觉风格。本次结构重构不同时重设计 UI。 - 先整理现有 `style.css` 的 token、基础元素、通用按钮/输入框、正文排版;明确一个视觉规则的所有者。 - 页面只保留布局和专用样式;先用一对页面验证共享按钮、筛选栏、状态提示,再逐页替换。 - 不把各页面的 `.header/.title/.panel` 直接搬成全局同名选择器,避免覆盖关系变化;可以使用组件作用域或业务前缀。 - `AdminLayout.vue:484` 附近使用深层选择器与 `!important` 覆盖页面类名,布局壳和业务页面已经耦合。先将重复的页面头/筛选栏样式迁移到有明确所有权的组件,再逐条移除覆盖;不能先删覆盖让布局失效。 - 重复颜色、字号和间距先归并为语义变量;确实有不同语义的危险操作、主按钮与次按钮保持区别。 - 保留用户已经确认的掉落动画、乱码复原、减少动态效果偏好,以及 100% 缩放下检索器的滚动与分页可见性。 可直接验证的样式试点:全局 `style.css:366`、`:384` 与算法/考试结果页的 loading 样式;`AdminQuestionsView.vue:251` 与 `AdminDbQuestionsView.vue:224` 一带的页面头、筛选和操作区域。先比较 DOM、尺寸、状态语义,再决定共用组件或共享样式,不以 CSS 看起来相似作为唯一依据。 删除前使用「引用证据 → 路由/动态使用检查 → 构建与交互验证」流程。`questionApi.exportTestCasesZipUrl`、`downloadInputUrl`、`downloadOutputUrl` 已确认无调用,并因会把令牌拼入 URL 而移除;导出 ZIP 统一保留已鉴权的 Blob 下载。旧的 `AppHeader.vue` 也已确认不在路由、布局、显式 import 或自动组件注册链路中,并通过构建后删除;其余静态搜索候选仍须逐项确认动态 class、样式副作用、懒加载和类型声明,不能只凭文件名删除。 `ComingSoonView.vue` 仍被路由动态导入,不能按文件名判定为残留。已有 `FormField`、`useConfirm/useToast`、`clipboard`、`sqlSchemaParser`、`userCache` 应优先扩用,避免建立平行工具。Markdown 的专用 CSS 与全局 `.prose` 要一起做级联验证。 先迁移调用方,再删除旧实现和失效样式;同一提交保留 Git 可恢复记录。不能根据文件名相似就删除两份侧栏测试,先确认各自覆盖的菜单行为与路由权限测试是否重复。 ## 6. 实施批次与验收 | 批次 | 状态 | 交付范围 | 关键验收 | | --- | --- | --- | | 0:建立基线 | 已完成 | 记录当前未提交改动、路由/截图/构建清单;补关键行为测试 | 当前正常行为与既有缺陷分开记录,不把用户改动覆盖掉;未提交的新功能(如进行中的题解模块)须先独立闭环或建立 stash/分支隔离,确保重构在稳定代码基线上启动 | | 1:请求基础设施 | 已完成 | 合并刷新流程、统一下载错误处理 | 刷新成功/失败/退出竞争;Zip、Word、PDF 行为一致 | | 2:列表试点 | 已完成 | 算法题、数据库题后台列表接入公共查询能力;题集、考试、课练候选检索入口迁移 | 快速输入、跨页、迟到响应、删除最后一项、权限与题型限制正确 | | 3:判题试点 | 已完成 | 普通算法/SQL 与考试结果页迁移至统一轮询控制器 | 状态一致、轮询不重叠、不串提交、不丢上下文 | | 4:业务页面逻辑收敛 | 已完成 | 四类提交页草稿、三处倒计时、三处客观题答案规则、编程题测试用例管理完成抽取;不新增空壳 UI 组件 | 草稿、自动保存、截止交卷、题型差异、测试点 ZIP 的真实环境回归 | | 5:类型与残留收尾 | 已完成 | `DataTable` 改为行泛型契约;提交监控页用类型守卫收窄三类提交;移除临时脚本、截图和日志;修复全量 lint warning | 冷加载、所有深链接、视口回归仍依赖可用后端 | 每批单独提交、单独验收,可独立回退。预计约 8–12 个工作日为初步安排,其中考试/课练回归占比最高;实施后根据首个列表和判题试点的实际规模调整。时间不是上线承诺,不为了进度跳过验收。 每批最少执行:相关行为测试、全量 `npm run test:unit -- --run`、类型检查与生产构建、lint。若存在既有 lint/build 问题先记录基线,不能全局关闭规则或借机大规模格式化无关文件。 交互回归重点: - 普通算法、SQL、客观题;题集进入与返回;课练/考试提交;题解阅读、发布和管理。 - USER/TEACHER/ADMIN 角色;过期凭据;请求失败;快速路由切换;并发保存。 - 390px 移动端及 1280/1440/1920px 桌面、100% 缩放,长标题和长代码。 - 本地生产构建预览,首次进入代码编辑器与 SQL/Java 代码块,无 `Prism is not defined`、worker 或 chunk 加载错误。 - 同一查询状态只触发一次主动加载;每个提交监控实例至多一个结果请求在途,组件卸载后不再调度。 ## 7. 完成标准 1. 鉴权刷新、判题终态和下载核心处理各有一个实现来源,并有行为测试。 2. 公共抽取至少有两个真实调用方;页面不再复制已经迁移的旧实现。 3. 大页面按职责拆分,业务差异清晰,没有新增含大量模式开关的万能组件。 4. 重构前后路由、权限、接口请求结构、草稿兼容和已确认 UI 行为不变;有意修复缺陷单独列明。 5. 清理项有证据,兼容出口有删除终点;没有 `any`、忽略类型检查或关闭 lint 来掩盖迁移问题。 6. 记录构建产物和请求次数的前后对比。减少重复源码不能直接宣称提升服务端并发或让页面更快。 本轮不再为了“文件更小”强行拆分 `PracticeDetailView.vue` 或 `ExamSessionView.vue`:它们还承载不同题型、自动保存与考试状态机,先抽出并验证共同逻辑比制造只转发 props 的组件更安全。下一步应在可用测试后端执行浏览器级回归,并以真实问题为依据继续拆分。