From ba448946c9107778cb209807e46484d8a3d0f46f Mon Sep 17 00:00:00 2001 From: root Date: Sat, 17 Oct 2026 17:48:04 +0800 Subject: [PATCH] fix(assessment): clickable rules panel at top, print-layout word doc polish - scoring rules panel moved above the sheet and auto-scrolls into view on toggle (was rendered at the very bottom, felt unresponsive) - word export: unified single spec table with numbered section rows (one/two/three...), subtotal and total rows, editorial heading fonts - standardized fonts and metrics: body SimSun 10.5pt, headings SimHei, numerals Times New Roman, line-height, A4 margins 72/78/66pt - footer with PAGE/NUMPAGES fields via mso-element footer, Word opens in Print layout view - evidence captions and rules annex restyled to match; www rebuilt --- app/src/pages/assessment/AssessmentPage.tsx | 287 ++++++++++-------- ...Page-D5_FcgCw.js => AdminPage-BqF0Bkw4.js} | 6 +- .../www/assets/AssessmentPage-BwKrhTyG.js | 27 -- .../www/assets/AssessmentPage-CIzQ48Ah.js | 27 ++ ...-BT2qJRZm.js => DashboardPage-BQFeFR8x.js} | 2 +- ...age-B1r83Bh8.js => GradesPage-BUHN14mZ.js} | 2 +- ...ge-D7oTzsex.js => ProfilePage-Blh11W5U.js} | 2 +- ...ge-9zb37KCi.js => RecordsPage-P1KGDld8.js} | 2 +- ...age-C6SFKwuP.js => ResumePage-D6LTsd6o.js} | 2 +- ...e-Dhhd76dy.js => TaxonomyPage-C83kP-EQ.js} | 2 +- ...Page-D7BFLt1r.js => TypesPage-DxXV3aWW.js} | 2 +- .../{card-BtFEK_ax.js => card-Diwu5WOM.js} | 2 +- .../{index-BhG5FvHJ.js => index-Bmyzyg8t.js} | 4 +- sync-server/www/assets/index-CRJEsjf-.css | 1 - sync-server/www/assets/index-D9E2kUSE.css | 1 + ...fufUH0v.js => mammoth.browser-BLgWCU1X.js} | 2 +- ...ccw-BDKjoLu6.js => rotate-ccw-DBQnyfoK.js} | 2 +- ...Chart-BfaP0epD.js => useChart-rSzOuKiQ.js} | 2 +- sync-server/www/index.html | 4 +- 19 files changed, 200 insertions(+), 179 deletions(-) rename sync-server/www/assets/{AdminPage-D5_FcgCw.js => AdminPage-BqF0Bkw4.js} (98%) delete mode 100644 sync-server/www/assets/AssessmentPage-BwKrhTyG.js create mode 100644 sync-server/www/assets/AssessmentPage-CIzQ48Ah.js rename sync-server/www/assets/{DashboardPage-BT2qJRZm.js => DashboardPage-BQFeFR8x.js} (94%) rename sync-server/www/assets/{GradesPage-B1r83Bh8.js => GradesPage-BUHN14mZ.js} (99%) rename sync-server/www/assets/{ProfilePage-D7oTzsex.js => ProfilePage-Blh11W5U.js} (99%) rename sync-server/www/assets/{RecordsPage-9zb37KCi.js => RecordsPage-P1KGDld8.js} (99%) rename sync-server/www/assets/{ResumePage-C6SFKwuP.js => ResumePage-D6LTsd6o.js} (99%) rename sync-server/www/assets/{TaxonomyPage-Dhhd76dy.js => TaxonomyPage-C83kP-EQ.js} (97%) rename sync-server/www/assets/{TypesPage-D7BFLt1r.js => TypesPage-DxXV3aWW.js} (98%) rename sync-server/www/assets/{card-BtFEK_ax.js => card-Diwu5WOM.js} (89%) rename sync-server/www/assets/{index-BhG5FvHJ.js => index-Bmyzyg8t.js} (99%) delete mode 100644 sync-server/www/assets/index-CRJEsjf-.css create mode 100644 sync-server/www/assets/index-D9E2kUSE.css rename sync-server/www/assets/{mammoth.browser-BfufUH0v.js => mammoth.browser-BLgWCU1X.js} (99%) rename sync-server/www/assets/{rotate-ccw-BDKjoLu6.js => rotate-ccw-DBQnyfoK.js} (95%) rename sync-server/www/assets/{useChart-BfaP0epD.js => useChart-rSzOuKiQ.js} (92%) diff --git a/app/src/pages/assessment/AssessmentPage.tsx b/app/src/pages/assessment/AssessmentPage.tsx index 9094128..f20ae8b 100644 --- a/app/src/pages/assessment/AssessmentPage.tsx +++ b/app/src/pages/assessment/AssessmentPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, type ReactNode } from 'react'; +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { ChevronLeft, ChevronRight, Download, FileText, Images, Printer, RotateCcw } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { attachmentList, attachmentThumb, footprintList, gradesOverview, profileGet, profileUpsert } from '@/lib/api'; @@ -38,8 +38,14 @@ function buildMhtml(html: string, images: { name: string; dataUri: string }[]): return new Blob([parts.join('')], { type: 'multipart/related; type="text/html"; boundary="' + boundary + '"' }); } -const W_TD = 'border:0.75pt solid #4a4a4a;padding:4pt 7pt;font-size:10.5pt;vertical-align:middle'; -const W_TH = W_TD + ';background:#e8eaf0;font-weight:bold;font-family:SimHei;letter-spacing:1pt'; +const W_TD = 'border:0.75pt solid #3a3a3a;padding:5pt 8pt;font-size:10.5pt;vertical-align:middle'; +const W_TH = 'border:0.75pt solid #3a3a3a;padding:5pt 8pt;font-size:10.5pt;font-family:"黑体",SimHei;letter-spacing:1pt;vertical-align:middle;background:#eef1f6'; +const W_SEC = 'border:0.75pt solid #3a3a3a;padding:5pt 8pt;font-size:11pt;font-family:"黑体",SimHei;letter-spacing:1.5pt;background:#f5f6f9;text-align:left'; +// Word 域代码(页码等):条件注释 + field-begin/separator/end,Word 打开时自动计算 +const W_FIELD = (name: string, shown: string) => + '' + shown + + ''; /** 评分细则参考(附在 Word 末尾,与页面「评分细则参考」同一数据源) */ function rulesAnnexHtml(r: ZcRules): string { @@ -79,35 +85,44 @@ function rulesAnnexHtml(r: ZcRules): string { ); } -/** 综合素质测评表:A4 纵向、黑体标题、表格化分项、附图栅格 + 细则参考附表 */ +/** 综合素质测评表:A4 纵向、宋体正文/黑体标题、统一表格版式、页脚页码、附1 材料图、附2 细则 */ function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial, evidence: Evidence[], rules: ZcRules): Blob { const f = (s: unknown) => esc(s); - const schoolLine = profile.school ? '
' + f(profile.school) + '
' : ''; + const schoolLine = profile.school ? '
' + f(profile.school) + '
' : ''; const infoCell = (v: unknown, label: string) => - '' + label + '' + f(v || '') + ''; + '' + label + '' + f(v || '') + ''; - // 分板块明细表 - const body = result.sections.map(sec => { + // 统一分项表:各板块内嵌为整表的小节行 + 小计行,末尾总分行 + const cnNum = ['一', '二', '三', '四', '五', '六', '七', '八']; + const rows: string[] = []; + result.sections.forEach((sec, si) => { const subtotal = sec.key === 'rewards' ? sec.subtotal : Math.min(sec.subtotal, sec.cap); - const rows = sec.items.map(it => - '' + - '' + f(it.title) + (it.overridden ? '(已手动修正)' : '') + '' + - '' + f(it.note || it.source) + '' + - '' + f(it.score) + '', - ).join(''); - return ( - '' + - '' + - '' + - '' + - rows + - '' + - '' + - '
考评项目评分依据分值
' + f(sec.label) + '小计(板块上限 ' + f(sec.cap) + ' 分)' + f(subtotal) + '
' + rows.push('' + f(cnNum[si] ?? si + 1) + '、' + f(sec.label) + '(上限 ' + f(sec.cap) + ' 分)'); + for (const it of sec.items) { + rows.push( + '' + f(it.title) + (it.overridden ? '(已手动修正)' : '') + '' + + '' + f(it.note || it.source) + '' + + '' + f(it.score) + '', + ); + } + rows.push( + '' + f(sec.label) + '小计' + + '' + f(subtotal) + '', ); - }).join(''); + }); + rows.push( + '综合素质测评总分' + + '' + f(result.total) + '', + ); + const specTable = + '' + + '' + + '' + + '' + + rows.join('') + + '
考评项目评分依据分值
'; - // 附 1:证明材料——每条有附件的活动 / 竞赛记录成组,图片纵向排布 + // 附1:证明材料——每条有附件的活动 / 竞赛记录成组,图片纵向排布并编图号 const images: { name: string; dataUri: string }[] = []; let figNo = 0; const grids = evidence.map(ev => { @@ -115,21 +130,22 @@ function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial

附图
' + - '图 ' + figNo + ' ' + f(ev.title) + ''; + return '

附图
' + + '图 ' + figNo + ' ' + f(ev.title) + '
'; }).join(''); return '' + f(ev.title) + '
' + - '' + f(ev.meta) + '' + + '' + f(ev.meta) + '' + '' + imgs + ''; }).join(''); const annex = grids - ? '
附 1:加分证明材料 — ' + f(evidence.length) + ' 项记录 · ' + f(figNo) + ' 张材料图
' + + ? '
附 1:加分证明材料
' + + '
(' + f(evidence.length) + ' 项记录 · ' + f(figNo) + ' 张材料图)
' + '' + grids + '
' : ''; // 信息栏 3×3:末格为电话号码 const info = - '' + + '
' + '' + infoCell(profile.studentId, '学 号') + infoCell(profile.name, '姓 名') + infoCell(profile.gender, '性 别') + '' + '' + infoCell(profile.college, '学 院') + infoCell(profile.major, '专 业') + infoCell(profile.className, '班 级') + '' + '' + infoCell(profile.political, '政治面貌') + infoCell(termLabel, '统计周期') + infoCell(profile.phone, '电话号码') + '' + @@ -138,37 +154,38 @@ function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial

学生综合素质测评表' + + '' + '' + '

' + schoolLine + '

学生综合素质测评表

' + '

测评学年:' + f(termLabel) + '

' + info + - body + - '
' + - '' + - '
综合素质测评总分' + f(result.total) + '
' + + specTable + annex + rulesAnnexHtml(rules) + - '' + + '
' + '' + '' + '' + '
学生签名:_____班长签名:_____辅导员签名:_____日期:______
' + - '

说明:本表依据《学生综合素质测评评分细则》自动生成(细则参考默认值见附 2,可在综合测评页自行调整);带「已手动修正」标记的分值为手动核定结果。导出前请核对加分记录与证明材料一致。

' + + '

说明:本表依据《学生综合素质测评评分细则》自动生成(细则默认值见附 2,可自行调整);带「已手动修正」标记的分值为手动核定结果。导出前请核对加分记录与证明材料一致。

' + + '
' + + '

第 ' + W_FIELD('PAGE', '1') + ' 页(共 ' + W_FIELD('NUMPAGES', '1') + ' 页)

' + + '
' + ''; return buildMhtml(html, images); } + // ---------------- 评分细则参考编辑 UI ---------------- const NumCell = ({ value, onChange, wide }: { value: number; onChange: (v: number) => void; wide?: boolean }) => ( ([]); const [initOpen, setInitOpen] = useState(false); const [rulesOpen, setRulesOpen] = useState(false); + const rulesRef = useRef(null); const [rules, setRules] = useState(ZC_DEFAULT_RULES); const [zoom, setZoom] = useState<{ src: string; title: string; index: number } | null>(null); @@ -206,14 +224,14 @@ export function AssessmentPage() { if (stored.zongce?.rules) setRules(stored.zongce.rules); }, [stored]); + const records: FootprintRow[] = listQ.data?.items ?? []; + const termYears = useMemo(() => Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => termYearOf(x.term)).filter(Boolean) as string[])).sort(), [gradesQ.data]); useEffect(() => { if (!stored) return; setOvMap(stored.zongce?.overrides ?? {}); - const years = Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => termYearOf(x.term)).filter(Boolean))) as string[]; - if (!year && years.length) setYear(years[0]); + if (!year && termYears.length) setYear(termYears[0]); }, [stored, gradesQ.data]); - const records: FootprintRow[] = listQ.data?.items ?? []; const ov: ZcOverrides = ovMap[year] ?? {}; const result = useMemo( () => computeZongce({ @@ -226,9 +244,8 @@ export function AssessmentPage() { ); // 学年 → 大几:优先用个人信息「入学时间」,缺省时以最早学期推算 - const years = useMemo(() => Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => termYearOf(x.term)).filter(Boolean) as string[])).sort(), [gradesQ.data]); - const eduStartEff = profile.eduStart || (years.length ? years[0] + '-09' : ''); - const termLabel = year === 'all' || !year ? '全部学年' : gradeLabel(year, eduStartEff); + const eduStartEff = profile.eduStart || (termYears.length ? termYears[0] + '-09' : ''); + const termLabel = year === 'all' || !year ? '全部学年' : gradeLabel(year, eduStartEff); // 本学年范围内全部记录(证据画廊不再只看计分记录——有附件就展示) const scopeIds = useMemo(() => { @@ -326,14 +343,101 @@ export function AssessmentPage() {