Commit Graph

13 Commits

Author SHA1 Message Date
042e0a8e1f feat(frontend): SortableJS 改为本地依赖,移除公网 CDN 运行时依赖(R33)
- package.json: 新增 sortablejs@^1.15.0(dependencies)
- src/main.js: import Sortable + 挂载 window.Sortable
  (保留全局挂载形式 => Admin.vue 的 window.Sortable.* 零改动)
- index.html: 删除 cdn.jsdelivr.net 的 <script>,留注释说明原因
- 原因: 内网/离线部署时 CDN 不可达 => window.Sortable 为 undefined,
  而 Admin.vue:3089/3276 的 `if (!window.Sortable) return` 会静默吞掉
  新闻/轮播的拖拽排序(界面零提示)
- 验证: 真 chromium 16/16 PASS(含整站 0 次 CDN 请求、宿主元素 expando
  Sortable<数字> 探针、news/carousel handle 正确、0 pageerror/0 console.error);
  构建 ✓ built in 18.57s / VITE_EXIT=0; TDZ src 38 + dist 26 全 PASS
2026-09-29 09:57:48 +08:00
cb672b621a feat(admin): 导入结果提示区分「忽略非参赛选手」条数(R32)
- onExcelImport 读取 result.ignored 并在成功提示中单列展示
- 控制台 console.info 输出 ignoredBy 身份明细(报名人/指导教师/领队)
- 仅账号导入这一处;客观题导入提示未动
2026-09-29 09:40:51 +08:00
2ca82d0da3 refactor(admin): 评分入口收敛 + 大题小问得分可见(R30)
- 文案:「考生大题作答查证」提示由「作品评分」改为「大题评分」
  (保存后将写入该考生「大题评分」,总分 = 客观题得分 + 大题评分)
- 删除「考生答题记录」的评分操作:操作列(评分按钮)+ 评分弹窗 +
  examScoreDialog* / openExamScoreDialog / saveExamScore + .exam-score-form 样式
  ⇒ 大题评分统一由「考生大题作答查证」按小问给分,word_score 不再有两条口径
  ⛔ 刻意保留:adminApi.setScores(作品管理 batchSaveWorkScores 仍在用)
     与后端 PUT /api/admin/scores/:group(10+ 门禁脚本依赖:verify_full_e2e /
     exam_regression_test / verify_large_scale_17 / verify_agg_key_mismatch 等)
- 新增「小问得分」列(「大题得分」之后):后端 listAnswersForAdmin 本就返回
  score1/2/3 ⇒ 纯前端改动、未动后端。新增 subScoresOf / hasSubScores / fmtSubScores;
  hasSubScores 用于区分「未评分」与「三问都给 0 分」(二者不可都显示成 0)

验证:
- TDZ 顺序扫描:src 38 文件 + dist 打包产物 26 文件 全 PASS
- 真 chromium(playwright)ui-runtime-check.mjs:16/16 断言全过 / 致命错误 0 / 告警 0
  (含 expectText "10 / 8 / 9" 直证小问得分列渲染;DOM dump 证实答题记录表头
   已无「操作」列、大题查证表头含「小问得分」)
- dist 重建 built in 13.00s / VITE_EXIT=0;产物「作品评分」= 0,「小问得分」「大题评分」在
2026-09-28 17:44:55 +08:00
e5579f034e fix(admin): 修复成绩管理页 TDZ 崩溃——watch 早于 ref 声明(R29 回归)
浏览器实测报错:
  ReferenceError: Cannot access 'scoreExportGroup' before initialization
  at setup (Admin.vue:2559:7)

根因:R29 板块迁移时把 `watch(scoreExportGroup, …)` 写在 2559 行,而
`const scoreExportGroup = ref('')` 在 2810 行。**watch() 会立即执行一次
source 取值来收集依赖**,故 setup 阶段即触发 TDZ。
(对照:watch(questionGroup,…) 安全,其声明在 1784 行。)

修法:把该 watch 下移到 `scoreExportGroup` / `isExamScoreGroup` 声明之后
(成绩管理节内),原位置留注释说明原因,避免后人再次上移。

验证(本轮新增两道门禁工具,均在 .workbuddy/tools/,未入仓库):
- AST 顺序扫描 verify_frontend_tdz.mjs:对已提交的坏版本 c470e79 精确命中
  Admin.vue:2559(变异测试);修复后 src 38 文件 + dist 打包产物 26 文件全 PASS。
- 真 chromium(playwright)ui-runtime-check.mjs:致命运行错误 0、告警 0、
  18/18 断言通过(中职→本科→高职切组、迁移板块显隐、导出Excel、
  批量下载作品按组显隐、评分弹窗「客观分(卷面)」单行左对齐)。
- dist 已重建:built in 12.36s、VITE_EXIT=0。

教训:vite build 只打包、不执行 setup,构建绿对 TDZ 是假绿。
2026-09-28 17:20:37 +08:00
c470e7922e feat(admin): 功能分布优化——板块迁入成绩管理 + 导出 Excel + 废弃按钮清理(R29)
- 试卷管理只留试卷相关:「考生答题记录」(含评分弹窗)与「考生大题作答查证」
  整段迁至成绩管理页,外层 <template v-if="isExamScoreGroup"> 仅中职/高职可见
- 组别绑定改跟随成绩页 scoreExportGroup(避免串组):新增 isExamScoreGroup /
  refreshExamCtxData / refreshScorePageData + watch(scoreExportGroup);ensureBigData 收窄为只刷题库
- 导出按钮:导出CSV → 导出Excel(GET /api/admin/export-excel/:group,fallback .xlsx)
- 中职/高职已取消 Word 作品上传:成绩页与作品管理页「批量下载作品」按组别 v-if 隐藏
- 评分弹窗文字错位修复:label-width 120px + label-position="left" + 标签左对齐不换行
- 5 个纯白按钮补 type(info/primary plain)提升可见度
2026-09-28 16:42:39 +08:00
fb9c48aaac fix(admin): 复核页选择题选项与答案显示 + 阅卷导出 Word 按钮 + 评分两位小数(R28)
- 新增 reviewOptionList() / reviewOptionTextOf() / reviewAnswerCell():
  防御性解包 options(非数组一律降级空数组),选项列表改为 v-for 遍历对象数组;
  选择题的「考生作答」「正确答案」显示为「A. GPS」,彻底消除「只显示字母不知指向哪个选项」的疑虑;
  判断题仍显示「正确/错误」。两列宽 96 -> 130。
- 阅卷区:el-table 增加 type="selection" + row-key="contestantId";
  新增「导出选中 Word(n)」「导出全部 Word」及行内「导出Word」,
  沿用 safeDownload 直接命中 GET /api/admin/big-answers/:group/export?ids=。
- fmtScore / bigScoreTotal 改两位小数;3 处 el-input-number 改 :precision="2" :step="0.25"。

构建验证:node ./node_modules/vite/bin/vite.js build => VITE_EXIT=0(built in 19.61s),
产物中含 review-options / 导出全部 Word / big-answers/{group}/export 三元 fallbackName(.docx|.zip)。
2026-09-28 15:50:56 +08:00
397356f141 feat(admin): 大题录入题干加「不得含参考答案」提示(R27)
- 题干表单项下新增 .big-form-tip:明确「不得出现参考答案 / 标准答案 / 评分标准,
  也不得给出带具体参数值的指令示例(如 >OUTMSG,COM1,GGA,1*3B)—— 考生可直接照抄」;
  并给出正确做法:只写格式定义(参数用方括号占位,例 >OUTMSG[,Port],Message,Period*CS)。
- 新增对应 CSS(.big-form-tip / .big-form-tip code)。
- 后端拦截消息经既有 ElMessage.error(e?.message) 展示(Admin.vue:1968),无需改动。

构建验证:vite build ✓ built in 11.32s,VITE_EXIT=0。
2026-09-28 11:58:17 +08:00
37d2641e49 feat(admin): 后台新增「成绩复核」——检索考生 + 全卷逐题明细弹窗(R26)
- api/admin.js: 新增 reviewSearch(姓名/身份证/电话任一,可选限定组别)与 getContestantScore(group, id)
- Admin.vue: 菜单新增「成绩复核」(Search 图标);检索区(关键词 + 可选组别下拉)+ 结果列表 + 全卷明细弹窗
- 明细含客观题逐题:题号 / 题型 / 题干 / 选项 / 考生答案 / 正确答案 / 判定 / 得分/满分
- 异常标记:题干缺失(questionMissing)、答案与判定矛盾(answerMatches≠isCorrect)
- 判断题答案 'true'/'false' 显示为「正确/错误」;未作答显示「未作答」且不判错
- 综合大题展示三小问作答与得分;本科/教师组展示作品文件清单并支持下载(不回传 file_path)
- 弹窗用考生自身组别请求接口,避免前端传错组照错人(后端 409 兜底)
- 回传正确答案仅管理员可见,与后端 adminAuth 门禁配套

构建验证:vite build 通过(✓ built in 14.80s,退出码 0)
2026-09-28 10:15:50 +08:00
bdfa4a9618 R20:Admin.vue —— 页面状态刷新修复 + 大题导出 Word + 评分一位小数
C 页面状态刷新(修复测试人员反馈的「切组 / 评分后需手动刷新」):
- onQuestionTypeChange:移除 `bigQuestions.length === 0` 守卫(切组后旧数组仍非空
  ⇒ 永不重新拉取,界面停留在上一组数据)
- saveBigScores / saveExamScore:原来各只刷新一侧列表(评分后「查看」侧仍是旧值)
- 收敛为唯一刷新入口 refreshQuestionData(),onMenuSelect / watch(questionGroup) /
  评分收尾 / 切组 全部走它
- 新增 ensureBigData():按「数据属于哪个组」(bigDataGroup) 判定是否拉取
- fetchBigQuestions / fetchBigAnswers:固定发起时的组别,响应回来若组别已变则丢弃
  (防切组瞬间旧响应串组显示)

B 大题评分一位小数:
- 3 处 el-input-number 加 :step="0.5" :precision="1"
- 新增 fmtScore():整数不带小数点(27),非整数保留一位(27.4),空/非法回 '--'
- bigScoreTotal 收敛到一位小数(防 0.1+0.2 浮点脏值显示)

D 大题导出 Word:
- 工具栏新增「导出选中Word(N)」「导出全部Word」
- 大题表加多选(reserve-selection)+ 选中态跟踪
- exportBigQuestions():走 safeDownload(已含 JSON 错误分支,避免把错误信封存成
  .docx);过滤不属于当前组别的陈旧 ID

计划外修复:
- exportBigQuestions 边界:选中 ID 全失效时原会因 qs 为空而静默退化成「导出全部」,
  与用户意图相反;已改为提示而不导出

验证:VITE_EXIT=0(多次构建,Admin chunk hash 已更新)。
未 push:须先上线验证通过再单独授权。
2026-09-23 18:30:01 +08:00
cddf91d13a fix(admin): 中职/高职管理后台「Word」口径改为「大题分」并清理死列
问题(用户 2026-09-22 报障):
  管理后台 试卷管理 → 考生答题记录 中,
  ①「答题情况」显示的是「Word分」,与现行综合大题口径不符;
  ② 多出一列「Word 答案」(针对已废弃的 Word 上传通道,中职/高职恒为空)。

改动(5 处):
  ①「答题情况」列标签:Word分 → 大题分
  ② 删除整列「Word 答案」(含 hasWordFile 判断与下载按钮)
  ③ 评分弹窗 label:Word 答案分 → 大题分
  ④ 删除孤儿方法 downloadExamWord()(删除后已无调用者)
  ⑤ 成绩管理区中职/高职分支:删「Word文件」列,Word分 → 大题分
     (v-else 本科/教师分支的「文档分(Word)」保持不变)

影响面:仅中职/高职分支显示口径;本科/教师组 Word 通道不受影响。
   数据列仍为 contestants.word_score(历史列名沿用),仅文案口径调整。

验证:vite build EXIT=0(9.88s);产物指纹「大题分」1 次 /「Word分」0 /「Word 答案」0;
     GET /api/admin/submissions/zhongzhi 返回 6 行字段齐全,零回归。
2026-09-22 13:05:30 +08:00
c2551b64e8 feat(auth/exam): 登录重试同源 + 静默刷新透传 + 交卷链路加固(R5/R8)
前端重试与后端闸门同源(须成对上线)
- src/api/request.js: 新增 parseRetryAfterMs / computeLoginRetryDelayMs,
  退避取 max(指数退避, Retry-After) + ±20% 抖动 + 服务端值封顶;
  silent 透传(doSilentRefresh 不再吞掉 silent 标记)。
- src/api/auth.js / src/stores/auth.js: 登录 429 走同源重试;错误分类区分
  "TCP 未握手(可重试)"与"业务码失败(不可重试)"。
- src/stores/exam.js: 交卷链路顺序红线(取卷 -> 作答 -> submit-all 严格顺序),
  答案不丢 / 重复不脏 / 窗口不锁死。
- src/views/Competition.vue: 中职/高职综合大题区块(3 x textarea,2000 字上限
  与右下角计数);本科/教师组保留 Word 上传入口。

验证:源码抽取测试 8/8(含判别力自证:不读 header 的对照实现必 FAIL);
npm run build BUILD_RC=0(12.82s / 1711 modules);真实交卷端到端 code=0。
2026-09-22 09:17:06 +08:00
1bfe007149 feat: 综合大题改造(替代 Word 上传)+ MockExam 按组分支
中职/高职组以「综合大题」替代原 Word 文件上传:管理员在后台录入题干
(wangEditor 富文本,含表格),系统按组随机抽题,考生在 3 个小问内作答,
每小题 10 分、大题合计 30 分。

- api/admin.js / api/exam.js / stores/exam.js:大题题库 CRUD、抽题、作答与评分链路
- Competition.vue:Word 上传区块替换为大题作答区(3 小问 + 字数上限 + 富文本题干渲染)
- Admin.vue:大题题库管理、考生作答查看与人工评分
- MockExam.vue:按组分支——中职/高职显示综合大题,其余组保持 Word 答题;侧栏与提交提示同步
- RichEditor.vue + utils/richtext.js:题干富文本渲染前做白名单消毒(零依赖,纵深防御)

说明:big_questions / big_question_answers 建表迁移 023/024 随后端仓库提交。
2026-09-21 11:17:59 +08:00
WorkBuddy
e8f0195b61 chore: 前端首次基线提交
运行必须源码:src/、public/、index.html、package.json 及构建配置。
已忽略运行非必须:node_modules/、dist/、.env*。
2026-08-17 12:25:42 +08:00