From 11cfeeb3a678c712988baa983998251ba13f53a5 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 17 Oct 2026 17:33:49 +0800 Subject: [PATCH] feat(assessment): unified GPA source, full evidence gallery, editable scoring rules, 3x3 info with phone - fix per-year computation: termYearOf parses semester terms (2023-2024-1) so year Scots no longer skipped the semester filter (previous bug made yearly score empty) - academic score now uses semester.gpa shown on grades page (single source of truth); fallback recalculates with the same band-linear + category weights formula - gradeLabel infers grade year (freshman/sophomore) from eduStart, falls back to earliest semester year - info grid 3x3 with phone number as the 9th cell (page + word export) - evidence gallery shows every activity/competition record with attachments in scope (no 2-image cap), records marked scored/unscored - scoring rules reference panel: default rules editable and persisted with profile; restore-default button; no college/school names in citations - word export mirrors all of the above, adds rules annex table; removed dead zongceWord.ts with school citation - sync-server/www rebuilt --- app/src/lib/zongce.ts | 51 ++-- app/src/lib/zongceWord.ts | 41 --- app/src/pages/assessment/AssessmentPage.tsx | 283 ++++++++++++++---- ...Page-C6Laaees.js => AdminPage-D5_FcgCw.js} | 6 +- .../www/assets/AssessmentPage-BwKrhTyG.js | 27 ++ .../www/assets/AssessmentPage-WNZm2OFJ.js | 27 -- ...-BwOCt12m.js => DashboardPage-BT2qJRZm.js} | 2 +- ...age-DyWyMHzn.js => GradesPage-B1r83Bh8.js} | 2 +- ...ge-BEX6Djoz.js => ProfilePage-D7oTzsex.js} | 2 +- ...ge-DsT-7b0h.js => RecordsPage-9zb37KCi.js} | 2 +- ...age-Bpogo_02.js => ResumePage-C6SFKwuP.js} | 2 +- ...e-DWJsm_Rl.js => TaxonomyPage-Dhhd76dy.js} | 2 +- ...Page-Bpwaq9zp.js => TypesPage-D7BFLt1r.js} | 2 +- .../{card-C932WdWm.js => card-BtFEK_ax.js} | 2 +- .../{index-Dplf5hVV.js => index-BhG5FvHJ.js} | 4 +- sync-server/www/assets/index-C8H8oQlD.css | 1 - sync-server/www/assets/index-CRJEsjf-.css | 1 + ...1iHtrXW.js => mammoth.browser-BfufUH0v.js} | 2 +- ...ccw-DFLp9voo.js => rotate-ccw-BDKjoLu6.js} | 2 +- ...Chart-D3ki8T58.js => useChart-BfaP0epD.js} | 2 +- sync-server/www/index.html | 4 +- 21 files changed, 311 insertions(+), 156 deletions(-) delete mode 100644 app/src/lib/zongceWord.ts rename sync-server/www/assets/{AdminPage-C6Laaees.js => AdminPage-D5_FcgCw.js} (98%) create mode 100644 sync-server/www/assets/AssessmentPage-BwKrhTyG.js delete mode 100644 sync-server/www/assets/AssessmentPage-WNZm2OFJ.js rename sync-server/www/assets/{DashboardPage-BwOCt12m.js => DashboardPage-BT2qJRZm.js} (94%) rename sync-server/www/assets/{GradesPage-DyWyMHzn.js => GradesPage-B1r83Bh8.js} (99%) rename sync-server/www/assets/{ProfilePage-BEX6Djoz.js => ProfilePage-D7oTzsex.js} (99%) rename sync-server/www/assets/{RecordsPage-DsT-7b0h.js => RecordsPage-9zb37KCi.js} (99%) rename sync-server/www/assets/{ResumePage-Bpogo_02.js => ResumePage-C6SFKwuP.js} (99%) rename sync-server/www/assets/{TaxonomyPage-DWJsm_Rl.js => TaxonomyPage-Dhhd76dy.js} (97%) rename sync-server/www/assets/{TypesPage-Bpwaq9zp.js => TypesPage-D7BFLt1r.js} (98%) rename sync-server/www/assets/{card-C932WdWm.js => card-BtFEK_ax.js} (89%) rename sync-server/www/assets/{index-Dplf5hVV.js => index-BhG5FvHJ.js} (99%) delete mode 100644 sync-server/www/assets/index-C8H8oQlD.css create mode 100644 sync-server/www/assets/index-CRJEsjf-.css rename sync-server/www/assets/{mammoth.browser-D1iHtrXW.js => mammoth.browser-BfufUH0v.js} (99%) rename sync-server/www/assets/{rotate-ccw-DFLp9voo.js => rotate-ccw-BDKjoLu6.js} (95%) rename sync-server/www/assets/{useChart-D3ki8T58.js => useChart-BfaP0epD.js} (92%) diff --git a/app/src/lib/zongce.ts b/app/src/lib/zongce.ts index 842c8d4..6619712 100644 --- a/app/src/lib/zongce.ts +++ b/app/src/lib/zongce.ts @@ -38,6 +38,12 @@ export interface ZcOverrides { manual?: { title: string; section: string; score: number }[]; } +// 学期名('2023-2024-1')或学年名('2023-2024')→ 学年名;无法解析返回 null +export function termYearOf(term: string): string | null { + const m = /^(\d{4})-(\d{4})/.exec(String(term ?? '').trim()); + return m ? m[1] + '-' + m[2] : null; +} + // 学年('2024-2025')→ 统计时间窗 [2024-09-01, 2025-09-01) export function yearRange(year: string): [string, string] | null { const m = /^(\d{4})-(\d{4})$/.exec(year); @@ -45,14 +51,16 @@ export function yearRange(year: string): [string, string] | null { return [m[1] + '-09-01', m[2] + '-09-01']; } -// 学年 → 年级名(按个人信息入学年份推算:大一~大四) +// 学年 → 年级名(按个人信息入学年份推算:大一~大四);无入学年份时退回学年名 export function gradeLabel(year: string, eduStart?: string): string { - const m = /^(\d{4})-/.exec(year); + const y = termYearOf(year) ?? year; + const m = /^(\d{4})/.exec(y); const e = /^(\d{4})/.exec(eduStart ?? ''); - if (!m || !e) return year + ' 学年'; + if (!m || !e) return y + ' 学年'; const n = Number(m[1]) - Number(e[1]) + 1; - const names = ['大一', '大二', '大三', '大四', '大五']; - return (names[n - 1] ?? n + '年级') + '(' + year + ' 学年)'; + const names = ['大一', '大二', '大三', '大四', '大五', '大六', '大七']; + if (n < 1) return y + ' 学年'; + return (names[n - 1] ?? n + '年级') + '(' + y + ' 学年)'; } function inRange(d: string | null | undefined, range: [string, string] | null): boolean { @@ -141,14 +149,8 @@ export function computeZongce(opts: { ])); // ---- 学业成绩(60):学年平均学分绩点 × 10 ---- - const scopedCourses: CourseGrade[] = []; - for (const x of semesters) { - if (range && !yearRange(x.term)) continue; - const rng = yearRange(x.term); - const hit = range === null || (rng && !(rng[1] <= range[0] || rng[0] >= range[1])); - if (hit) scopedCourses.push(...x.courses); - } - let gpa: number | null = null; + // 口径与「成绩」页统一:优先取学期卡展示的 semester.gpa;未存 gpa 的学期回退到 + // 与「自动绩点」按钮相同的推算公式(档内线性折算 × 分类权重),保证两处数字一致。 const gpOf = (c: CourseGrade): number | null => { if (c.grade_point != null) return c.grade_point; if (bands && c.score != null) { @@ -165,14 +167,25 @@ export function computeZongce(opts: { return null; }; const wOf = (c: CourseGrade): number => (weights ? (weights[c.category ?? '其他'] ?? weights['其他'] ?? 1) : 1); - const gpCourses = scopedCourses.map(c => ({ ...c, grade_point: gpOf(c) })).filter(c => c.grade_point != null && c.credit > 0); - if (gpCourses.length) { - const credits = gpCourses.reduce((a, c) => a + c.credit, 0); - gpa = round2(gpCourses.reduce((a, c) => a + (c.grade_point as number) * wOf(c) * c.credit, 0) / credits); + const yearKey = termYearOf(year); + const scopedSemesters = semesters.filter(x => !yearKey || termYearOf(x.term) === yearKey); + const scopedCourses: CourseGrade[] = []; + const semGpas: { term: string; gpa: number; fallback: boolean }[] = []; + for (const s of scopedSemesters) { + scopedCourses.push(...(s.courses ?? [])); + if (s.gpa != null) { semGpas.push({ term: s.term, gpa: s.gpa, fallback: false }); continue; } + const withGp = (s.courses ?? []).map(x => ({ ...x, grade_point: gpOf(x) })).filter(x => x.grade_point != null && x.credit > 0); + if (!withGp.length) continue; + const credits = withGp.reduce((a, x) => a + x.credit, 0); + semGpas.push({ term: s.term, gpa: round2(withGp.reduce((a, x) => a + (x.grade_point as number) * wOf(x) * x.credit, 0) / credits), fallback: true }); } - const gpaScore = gpa != null ? Math.min(round2(gpa * r.gpaFactor), 60) : 0; + const gpa = semGpas.length ? round2(semGpas.reduce((a, x) => a + x.gpa, 0) / semGpas.length) : null; + const gpaNote = gpa == null + ? '该学年暂无学期绩点数据(先在「成绩」页录入学期或课程)' + : semGpas.some(x => x.fallback) ? '部分学期绩点未录入,按课程推算' : undefined; + const gpaScore = gpa != null ? round2(gpa * r.gpaFactor) : 0; sections.push(sec('academic', '学业成绩', 60, [ - { itemKey: 'gpa', title: '平均学分绩点 ' + (gpa != null ? gpa.toFixed(2) : '—') + ' × 10', source: '成绩模块 · ' + scopedCourses.length + ' 门课程', score: gpaScore, note: gpa == null ? '该学期暂无课程成绩' : undefined }, + { itemKey: 'gpa', title: '学年平均学分绩点 ' + (gpa != null ? gpa.toFixed(2) : '—') + ' × ' + r.gpaFactor, source: '成绩模块 · ' + scopedSemesters.length + ' 学期' + (semGpas.length ? '(绩点均值)' : ''), score: gpaScore, note: gpaNote || (gpa == null ? undefined : scopedCourses.length + ' 门课程') }, ])); // ---- 文体素质(10):体育 + 文体类加分(表2,cap 5) ---- diff --git a/app/src/lib/zongceWord.ts b/app/src/lib/zongceWord.ts deleted file mode 100644 index 4ea42e2..0000000 --- a/app/src/lib/zongceWord.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ProfileInfo } from './types'; -import type { ZcSection } from './zongce'; - -const esc = (t: unknown) => String(t ?? '').replace(/&/g, '&').replace(//g, '>'); - -export function zongceToWordHtml( - termLabel: string, - sections: ZcSection[], - total: number, - profile: Partial -): string { - const body = sections.map(sec => { - const subtotal = sec.key === 'rewards' ? sec.subtotal : Math.min(sec.subtotal, sec.cap); - const rows = sec.items.map(it => - '' + - '' + esc(it.title) + '' + - '' + esc(it.note || it.source) + '' + - '' + esc(it.score) + '' + - '').join(''); - return '

' + esc(sec.label) + - '(上限 ' + esc(sec.cap) + ' 分,小计 ' + esc(subtotal) + ' 分)

' + - '' + rows + '
'; - }).join(''); - - const head = '
' + - esc(profile.name ?? '') + ' · ' + esc(profile.className ?? '') + ' · 统计周期:' + esc(termLabel) + '
'; - - return '' + - '综合素质测评' + - '' + - '' + - '

学生综合素质测评

' + head + - '

综合素质总分:' + esc(total) + ' 分

' + - body + - '
计分依据:《计算机与网络安全学院学生综合素质测评评分细则》。以上自动匹配条目均经过人工核对。
' + - '' + - '' + - '' + - '
学生签名:______辅导员签名:______日期:______
' + - ''; -} diff --git a/app/src/pages/assessment/AssessmentPage.tsx b/app/src/pages/assessment/AssessmentPage.tsx index cfdfdaf..9094128 100644 --- a/app/src/pages/assessment/AssessmentPage.tsx +++ b/app/src/pages/assessment/AssessmentPage.tsx @@ -1,9 +1,9 @@ -import { useEffect, useMemo, useState } from 'react'; -import { ChevronLeft, ChevronRight, Download, Images, Printer, RotateCcw } from 'lucide-react'; +import { useEffect, useMemo, 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'; import type { FootprintRow, ProfileInfo, ZcOverrides } from '@/lib/types'; -import { computeZongce, gradeLabel, ZC_DEFAULT_RULES, type ZcRules, type ZcResult } from '@/lib/zongce'; +import { computeZongce, gradeLabel, termYearOf, yearRange, ZC_DEFAULT_RULES, type ZcRules, type ZcResult } from '@/lib/zongce'; import { Badge, Button, Dialog, Select, useToast } from '@/components/ui'; interface Evidence { recordId: number; title: string; meta: string; images: string[] } @@ -41,12 +41,50 @@ function buildMhtml(html: string, images: { name: string; dataUri: string }[]): 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'; -/** 综合素质测评表:A4 纵向、黑体标题、表格化分项、附图两列栅格 */ -function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial, evidence: Evidence[]): Blob { +/** 评分细则参考(附在 Word 末尾,与页面「评分细则参考」同一数据源) */ +function rulesAnnexHtml(r: ZcRules): string { + const basics = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
基础项分值 / 规则
政治面貌分' + esc(Object.entries(r.politics).map(([k, v]) => k + ' ' + v + ' 分').join(';')) + '
操行考评 / 诚信分默认' + esc(r.conductDefault) + ' 分 / ' + esc(r.integrityDefault) + ' 分
学业成绩学年平均学分绩点 × ' + esc(r.gpaFactor) + '(上限 60 分)
体育加分体育课成绩之和 × ' + esc(r.sportsFactor) + '(上限 5 分)
公益服务' + esc(Object.entries(r.publicGood).map(([k, v]) => k + ' ' + v).join(';')) + '
'; + const t2 = (r.table2 as unknown as Record>); + const t4 = (r.table4 as unknown as Record>); + const rw = (r.rewards as unknown as Record>); + const levels = Object.keys(t2); + const grades = ['一等奖', '二等奖', '三等奖', '优秀奖']; + const matrix = + '' + + '' + grades.map(g => '').join('') + '' + + levels.map(lv => '' + grades.map(g => '').join('') + '').join('') + + '
获奖级别' + g + '
' + esc(lv) + '' + esc(t2[lv]?.[g] ?? '—') + '
'; + const work = + '' + + '' + + Object.keys(t4).map(lv => '').join('') + + '
学生工作级别岗位分值
' + esc(lv) + '' + esc(Object.entries(t4[lv]).map(([k, v]) => k + ' ' + v).join(';')) + '
'; + const others = + '' + + '' + + ['荣誉称号', '英语等级', '计算机等级', '职业资格'].map(k => '').join('') + + '' + + '
类别分值
' + esc(k) + '' + esc(Object.entries(rw[k] ?? {}).map(([a, v]) => a + ' ' + v).join(';')) + '
违纪扣分' + esc(Object.entries(r.penalties).map(([k, v]) => k + ' -' + v).join(';')) + '
'; + return ( + '
附 2:评分细则参考(默认值,可自行调整)
' + + basics + matrix + work + others + ); +} + +/** 综合素质测评表:A4 纵向、黑体标题、表格化分项、附图栅格 + 细则参考附表 */ +function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial, evidence: Evidence[], rules: ZcRules): Blob { const f = (s: unknown) => esc(s); - const school = f(profile.school || '云川理工学院'); - const infoCell = (v: unknown, label: string, half = false) => - '' + label + '' + f(v || '') + ''; + const schoolLine = profile.school ? '
' + f(profile.school) + '
' : ''; + const infoCell = (v: unknown, label: string) => + '' + label + '' + f(v || '') + ''; // 分板块明细表 const body = result.sections.map(sec => { @@ -69,7 +107,7 @@ function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial

{ @@ -77,18 +115,26 @@ function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial

附图
' + + return '

附图
' + '图 ' + figNo + ' ' + f(ev.title) + '
'; }).join(''); return '' + f(ev.title) + '
' + '' + f(ev.meta) + '' + - '
' + imgs + '
'; + '' + imgs + ''; }).join(''); const annex = grids - ? '
附:加分证明材料(' + 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, '电话号码') + '' + + '
'; + const html = '' + '学生综合素质测评表' + @@ -103,31 +149,41 @@ function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial

' + '

' + - '
' + school + '
' + + schoolLine + '

学生综合素质测评表

' + '

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

' + - '' + - '' + infoCell(profile.studentId, '学 号') + infoCell(profile.name, '姓 名') + infoCell(profile.gender, '性 别') + '' + - '' + infoCell(profile.college, '学 院') + infoCell(profile.major, '专 业') + infoCell(profile.className, '班 级') + '' + - '' + infoCell(profile.political, '政治面貌') + - '' + - '' + - '
测评学年' + f(termLabel) + '测评总分' + f(result.total) + ' / 100 分
' + + info + body + '' + '' + '
综合素质测评总分' + f(result.total) + '
' + annex + + rulesAnnexHtml(rules) + '' + '' + '' + '' + '
学生签名:_____班长签名:_____辅导员签名:_____日期:______
' + - '

说明:本表依据《计算机与网络安全学院学生综合素质测评评分细则》自动生成;带「已手动修正」标记的分值为辅导员核准后的手动结果。导出前请核对加分记录与证明材料一致。

' + + '

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

' + '
'; return buildMhtml(html, images); } +// ---------------- 评分细则参考编辑 UI ---------------- +const NumCell = ({ value, onChange, wide }: { value: number; onChange: (v: number) => void; wide?: boolean }) => ( + onChange(e.target.value === '' ? 0 : Number(e.target.value))} /> +); + +const MiniTable = ({ head, children }: { head: string[]; children: ReactNode }) => ( + + {head.map((h, i) => )} + {children} +
{h}
+); + export function AssessmentPage() { const toast = useToast(); const profileQ = useQuery({ queryKey: ['profile'], queryFn: profileGet }); @@ -136,17 +192,24 @@ export function AssessmentPage() { const stored = profileQ.data?.content; const profile: Partial = { ...(stored?.info ?? {}) }; - const rules: ZcRules = stored?.zongce?.rules ?? ZC_DEFAULT_RULES; const [year, setYear] = useState(''); const [ovMap, setOvMap] = useState>({}); const [evidence, setEvidence] = useState([]); const [initOpen, setInitOpen] = useState(false); + const [rulesOpen, setRulesOpen] = useState(false); + const [rules, setRules] = useState(ZC_DEFAULT_RULES); const [zoom, setZoom] = useState<{ src: string; title: string; index: number } | null>(null); useEffect(() => { if (!stored) return; setOvMap(stored.zongce?.overrides ?? {}); - const years = Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => x.term.slice(0, 7)))); + if (stored.zongce?.rules) setRules(stored.zongce.rules); + }, [stored]); + + 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]); }, [stored, gradesQ.data]); @@ -162,23 +225,39 @@ export function AssessmentPage() { [records, gradesQ.data, profile, rules, ov, year], ); - // 证明材料:匹配条目对应记录的附件缩略图(每条最多 2 张) - const matchedIds = useMemo(() => { - const ids: number[] = []; - result.sections.forEach(sec => sec.items.forEach(it => { if (it.fromRecord) ids.push(it.fromRecord); })); - return ids; + // 学年 → 大几:优先用个人信息「入学时间」,缺省时以最早学期推算 + 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 scopeIds = useMemo(() => { + const range = year && year !== 'all' ? yearRange(year) : null; + return records.filter(x => { + if (x.status === 'draft') return false; + if (!range) return true; + const d = (x.start_date ?? x.created_at ?? '').slice(0, 10); + return d >= range[0] && d < range[1]; + }).map(x => x.id); + }, [records, year]); + + // 参与计分的记录 id(画廊中标注「计分」) + const matchedSet = useMemo(() => { + const s = new Set(); + result.sections.forEach(sec => sec.items.forEach(it => { if (it.fromRecord) s.add(it.fromRecord); })); + return s; }, [result]); useEffect(() => { let alive = true; (async () => { const out: Evidence[] = []; - for (const rid of matchedIds) { + for (const rid of scopeIds) { const rec = records.find(r => r.id === rid); try { const attList = await attachmentList(rid); const images: string[] = []; - for (const att of attList.items.filter(x => x.id).slice(0, 2)) { + for (const att of attList.items.filter(x => x.id && (x.mime ?? '').startsWith('image/'))) { const th = await attachmentThumb(att.id); if (th.data_b64) images.push('data:' + (att.mime === 'image/jpeg' ? 'image/jpeg' : 'image/webp') + ';base64,' + th.data_b64); } @@ -191,7 +270,7 @@ export function AssessmentPage() { if (alive) setEvidence(out); })(); return () => { alive = false; }; - }, [matchedIds.join(',')]); + }, [scopeIds.join(',')]); // 全部放平的图序列(灯箱上一张/下一张用) const flatImages = useMemo(() => { @@ -200,19 +279,32 @@ export function AssessmentPage() { return arr; }, [evidence]); + const persist = (p2: { rules?: ZcRules; overrides?: Record }) => + void profileUpsert({ info: profile, zongce: { rules: p2.rules ?? rules, overrides: p2.overrides ?? ovMap } }).catch(e => toast('error', e.message)); const persistOv = (next: ZcOverrides) => { setOvMap(m => ({ ...m, [year]: next })); - void profileUpsert({ info: profile, zongce: { rules, overrides: { ...ovMap, [year]: next } } }).catch(e => toast('error', e.message)); + persist({ overrides: { ...ovMap, [year]: next } }); + }; + const editRule = (fn: (r: ZcRules) => void) => { + const next: ZcRules = JSON.parse(JSON.stringify(rules)); + fn(next); + setRules(next); + persist({ rules: next }); + }; + const resetRules = () => { + const d: ZcRules = JSON.parse(JSON.stringify(ZC_DEFAULT_RULES)); + setRules(d); + persist({ rules: d }); + toast('success', '评分细则已恢复默认'); }; - const termLabel = year === 'all' ? '全部学年' : gradeLabel(year, profile.eduStart); const exportWord = () => { - const blob = zongceWordMhtml(termLabel, result, profile, evidence); + const blob = zongceWordMhtml(termLabel, result, profile, evidence, rules); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '综合素质测评_' + termLabel + '.doc'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 60000); - toast('success', '综测表已导出(.doc,含证明材料附图 ' + flatImages.length + ' 张)'); + toast('success', '综测表已导出(.doc,附证明材料图 ' + flatImages.length + ' 张 + 细则参考)'); }; const zoomIdx = zoom ? flatImages.findIndex(x => x.src === zoom.src) : -1; @@ -230,11 +322,12 @@ export function AssessmentPage() {

综合测评

-

按学年依据学院细则自动计算,可手动修正,支持导出 Word 测评表

+

按学年依据评分细则自动计算,可手动修正分值,支持导出 Word 测评表