feat(assessment): evidence image gallery with lightbox, word export redesigned (A4/tables/annex grid), normGrade maps gold/silver/bronze medals
This commit is contained in:
@@ -75,9 +75,9 @@ function normLevel(level?: string | null): keyof typeof ZC_DEFAULT_RULES.table2
|
||||
function normGrade(grade?: string | null): keyof typeof ZC_DEFAULT_RULES.table2['国家级'] | null {
|
||||
const v = (grade ?? '').trim();
|
||||
if (!v) return null;
|
||||
if (/特等|一等|冠军|第1名|第一名/.test(v)) return '一等奖';
|
||||
if (/二等|亚军|第2名|第3名|第二名|第三名/.test(v)) return '二等奖';
|
||||
if (/三等|第[4-6]名|第四名|第五名|第六名/.test(v)) return '三等奖';
|
||||
if (/特等|一等|金(色)?奖|冠军|第1名|第一名/.test(v)) return '一等奖';
|
||||
if (/二等|银(色)?奖|亚军|第2名|第3名|第二名|第三名/.test(v)) return '二等奖';
|
||||
if (/三等|铜(色)?奖|第[4-6]名|第四名|第五名|第六名/.test(v)) return '三等奖';
|
||||
if (/优秀|优胜|第[7-9]名|第七名/.test(v)) return '优秀奖';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,130 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Download, Images, Printer, RotateCcw } from 'lucide-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Download, Printer, RotateCcw } from 'lucide-react';
|
||||
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 { Button, Dialog, Select, useToast } from '@/components/ui';
|
||||
import { Badge, Button, Dialog, Select, useToast } from '@/components/ui';
|
||||
|
||||
interface Evidence { recordId: number; title: string; images: string[] }
|
||||
interface Evidence { recordId: number; title: string; meta: string; images: string[] }
|
||||
|
||||
function esc(t: unknown) { return String(t ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
||||
|
||||
// MHTML:把图片内嵌进 Word(.doc)的可靠方式
|
||||
// ---------------- Word 导出(MHTML 单文件 .doc,图片内嵌) ----------------
|
||||
function buildMhtml(html: string, images: { name: string; dataUri: string }[]): Blob {
|
||||
const boundary = '----=_fp_zongce';
|
||||
const parts: string[] = [];
|
||||
parts.push('--' + boundary + '\r\nContent-Type: text/html; charset="utf-8"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n' + html + '\r\n');
|
||||
parts.push(
|
||||
'MIME-Version: 1.0\r\n' +
|
||||
'Content-Type: multipart/related; type="text/html"; boundary="' + boundary + '"\r\n\r\n' +
|
||||
'--' + boundary + '\r\n' +
|
||||
'Content-Location: document.html\r\n' +
|
||||
'Content-Type: text/html; charset="utf-8"\r\n' +
|
||||
'Content-Transfer-Encoding: 8bit\r\n\r\n' + html + '\r\n',
|
||||
);
|
||||
for (const img of images) {
|
||||
const idx = img.dataUri.indexOf(',');
|
||||
const meta = img.dataUri.slice(0, idx);
|
||||
const b64 = img.dataUri.slice(idx + 1);
|
||||
const mime = /data:([^;]+)/.exec(meta)?.[1] ?? 'image/png';
|
||||
parts.push('--' + boundary + '\r\nContent-Type: ' + mime + '\r\nContent-Transfer-Encoding: base64\r\nContent-Location: ' + img.name + '\r\n\r\n' + b64 + '\r\n');
|
||||
parts.push(
|
||||
'--' + boundary + '\r\n' +
|
||||
'Content-Location: ' + img.name + '\r\n' +
|
||||
'Content-Type: ' + mime + '\r\n' +
|
||||
'Content-Transfer-Encoding: base64\r\n\r\n' + b64 + '\r\n',
|
||||
);
|
||||
}
|
||||
parts.push('--' + boundary + '--\r\n');
|
||||
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';
|
||||
|
||||
/** 综合素质测评表:A4 纵向、黑体标题、表格化分项、附图两列栅格 */
|
||||
function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial<ProfileInfo>, evidence: Evidence[]): Blob {
|
||||
const f = (s: unknown) => esc(s);
|
||||
const school = f(profile.school || '云川理工学院');
|
||||
const infoCell = (v: unknown, label: string, half = false) =>
|
||||
'<td style="' + W_TH + ';width:' + (half ? '16%' : '14%') + '">' + label + '</td><td style="' + W_TD + '">' + f(v || '') + '</td>';
|
||||
|
||||
// 分板块明细表
|
||||
const body = result.sections.map(sec => {
|
||||
const subtotal = sec.key === 'rewards' ? sec.subtotal : Math.min(sec.subtotal, sec.cap);
|
||||
const rows = sec.items.map(it =>
|
||||
'<tr>' +
|
||||
'<td style="padding:3pt 6pt;border:0.75pt solid #999">' + esc(it.title) + '</td>' +
|
||||
'<td style="padding:3pt 6pt;border:0.75pt solid #999;font-size:10pt;color:#555">' + esc(it.note || it.source) + '</td>' +
|
||||
'<td style="padding:3pt 6pt;border:0.75pt solid #999;text-align:right;width:70pt">' + esc(it.score) + '</td></tr>').join('');
|
||||
return '<h2 style="font-size:13pt;margin:12pt 0 5pt">' + esc(sec.label) + '(上限 ' + esc(sec.cap) + ' 分,小计 ' + esc(subtotal) + ' 分)</h2>' +
|
||||
'<table style="width:100%;border-collapse:collapse;font-size:11pt">' + rows + '</table>';
|
||||
'<td style="' + W_TD + '">' + f(it.title) + (it.overridden ? '(已手动修正)' : '') + '</td>' +
|
||||
'<td style="' + W_TD + ';color:#555;font-size:9.5pt">' + f(it.note || it.source) + '</td>' +
|
||||
'<td style="' + W_TD + ';text-align:right;width:64pt" class="num">' + f(it.score) + '</td></tr>',
|
||||
).join('');
|
||||
return (
|
||||
'<table style="border-collapse:collapse;width:100%;margin:6pt 0 0" border="1">' +
|
||||
'<tr><td style="' + W_TH + ';width:34%">考评项目</td>' +
|
||||
'<td style="' + W_TH + '">评分依据</td>' +
|
||||
'<td style="' + W_TH + ';width:64pt;text-align:center">分值</td></tr>' +
|
||||
rows +
|
||||
'<tr><td colspan="2" style="' + W_TD + ';text-align:right;background:#f4f5f7"><b>' + f(sec.label) + '小计</b>(板块上限 ' + f(sec.cap) + ' 分)</td>' +
|
||||
'<td style="' + W_TD + ';text-align:right;background:#f4f5f7"><b class="num">' + f(subtotal) + '</b></td></tr>' +
|
||||
'</table>'
|
||||
);
|
||||
}).join('');
|
||||
|
||||
// 证明材料附图:每条记录一个组块,图两列(约 200pt 宽)
|
||||
const images: { name: string; dataUri: string }[] = [];
|
||||
const annex = evidence.map(ev => {
|
||||
let imgs = '';
|
||||
ev.images.forEach((uri, i) => {
|
||||
const name = 'ev' + ev.recordId + '_' + i;
|
||||
let figNo = 0;
|
||||
const grids = evidence.map(ev => {
|
||||
const imgs = ev.images.map(uri => {
|
||||
figNo++;
|
||||
const name = 'ev' + ev.recordId + '_' + (images.length);
|
||||
images.push({ name, dataUri: uri });
|
||||
imgs += '<div style="font-size:10pt;color:#555">图 ' + (i + 1) + '</div><img src="' + name + '" style="width:300pt">';
|
||||
});
|
||||
return '<div style="margin-bottom:10pt"><b style="font-size:11pt">' + esc(ev.title) + '</b>' + imgs + '</div>';
|
||||
return '<div style="text-align:center"><img src="' + name + '" style="width:196pt;border:0.75pt solid #8a8a8a" alt="附图"><br>' +
|
||||
'<span style="font-size:9pt;color:#444">图 ' + figNo + ' ' + f(ev.title) + '</span></div>';
|
||||
}).join('');
|
||||
return '<tr><td style="' + W_TD + ';width:26%;vertical-align:top;background:#fafafa"><b style="font-size:10pt">' + f(ev.title) + '</b><br>' +
|
||||
'<span style="font-size:9pt;color:#666">' + f(ev.meta) + '</span></td>' +
|
||||
'<td style="' + W_TD + '"><table style="width:100%;border-collapse:collapse"><tr><td style="padding:2pt">' + imgs + '</td></tr></table></td></tr>';
|
||||
}).join('');
|
||||
const html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word">' +
|
||||
'<head><meta charset="utf-8"><title>综合素质测评</title></head>' +
|
||||
'<body style="font-family:SimSun,Calibri,sans-serif;margin:36pt;color:#222">' +
|
||||
'<h1 style="text-align:center;font-size:18pt;margin:0 0 6pt">学生综合素质测评</h1>' +
|
||||
'<div style="text-align:center;color:#555;font-size:11pt">' + esc(profile.name ?? '') + ' · ' + esc(profile.className ?? '') + ' · 统计周期:' + esc(termLabel) + '</div>' +
|
||||
'<p style="text-align:center;font-size:13pt;background:#f2f2f2;padding:6pt"><b>综合素质总分:' + esc(result.total) + ' 分</b></p>' +
|
||||
body + (annex ? '<h2 style="font-size:13pt;margin:16pt 0 6pt">附:证明材料</h2>' + annex : '') +
|
||||
'<table style="width:100%;border-collapse:collapse;margin-top:24pt;font-size:11pt"><tr>' +
|
||||
'<td style="width:33%">学生签名:______</td><td style="width:33%">辅导员签名:______</td><td>日期:______</td></tr></table>' +
|
||||
'</body></html>';
|
||||
const annex = grids
|
||||
? '<div style="font-family:SimHei;font-size:12pt;margin:14pt 0 4pt">附:加分证明材料(' + f(evidence.length) + ' 项记录 · ' + f(figNo) + ' 张材料图)</div>' +
|
||||
'<table style="border-collapse:collapse;width:100%" border="1">' + grids + '</table>'
|
||||
: '';
|
||||
|
||||
const html =
|
||||
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word">' +
|
||||
'<head><meta charset="utf-8"><title>学生综合素质测评表</title>' +
|
||||
'<style>' +
|
||||
'@page WordSection1 { size:595.3pt 841.9pt; margin:56.7pt 62pt 62pt; mso-footer: f1; }' +
|
||||
'div.WordSection1 { page:WordSection1; }' +
|
||||
'body { font-family:SimSun,serif; font-size:10.5pt; color:#1a1a1a; }' +
|
||||
'h1.title { font-family:SimHei; font-size:16pt; text-align:center; letter-spacing:2pt; margin:0 0 4pt; }' +
|
||||
'p.sub { text-align:center; font-size:10pt; color:#333; margin:0; }' +
|
||||
'table { border-collapse:collapse; }' +
|
||||
'.total td { background:#eef1f6; font-family:SimHei; font-size:12pt; }' +
|
||||
'.num { font-family:Calibri,SimSun; }' +
|
||||
'</style></head>' +
|
||||
'<body><div class="WordSection1">' +
|
||||
'<div style="text-align:center;font-size:10.5pt;letter-spacing:1pt;color:#333">' + school + '</div>' +
|
||||
'<h1 class="title">学生综合素质测评表</h1>' +
|
||||
'<p class="sub">测评学年:' + f(termLabel) + '</p>' +
|
||||
'<table style="border-collapse:collapse;width:100%;margin-top:10pt" border="1">' +
|
||||
'<tr>' + infoCell(profile.studentId, '学 号') + infoCell(profile.name, '姓 名') + infoCell(profile.gender, '性 别') + '</tr>' +
|
||||
'<tr>' + infoCell(profile.college, '学 院') + infoCell(profile.major, '专 业') + infoCell(profile.className, '班 级') + '</tr>' +
|
||||
'<tr>' + infoCell(profile.political, '政治面貌') +
|
||||
'<td style="' + W_TH + '">测评学年</td><td style="' + W_TD + '">' + f(termLabel) + '</td>' +
|
||||
'<td style="' + W_TH + '">测评总分</td><td style="' + W_TD + '"><b class="num" style="font-size:12pt">' + f(result.total) + '</b> / 100 分</td></tr>' +
|
||||
'</table>' +
|
||||
body +
|
||||
'<table style="border-collapse:collapse;width:100%;margin-top:8pt" border="1"><tr class="total">' +
|
||||
'<td style="' + W_TD + ';text-align:left;font-family:SimHei">综合素质测评总分</td>' +
|
||||
'<td style="' + W_TD + ';text-align:right;width:64pt" class="num">' + f(result.total) + '</td></tr></table>' +
|
||||
annex +
|
||||
'<table style="width:100%;border-collapse:collapse;margin-top:22pt;font-size:10.5pt"><tr>' +
|
||||
'<td style="width:25%">学生签名:_____</td>' +
|
||||
'<td style="width:25%">班长签名:_____</td>' +
|
||||
'<td style="width:25%">辅导员签名:_____</td>' +
|
||||
'<td>日期:______</td></tr></table>' +
|
||||
'<p style="font-size:9pt;color:#555;margin-top:10pt">说明:本表依据《计算机与网络安全学院学生综合素质测评评分细则》自动生成;带「已手动修正」标记的分值为辅导员核准后的手动结果。导出前请核对加分记录与证明材料一致。</p>' +
|
||||
'</div></body></html>';
|
||||
return buildMhtml(html, images);
|
||||
}
|
||||
|
||||
@@ -73,6 +141,7 @@ export function AssessmentPage() {
|
||||
const [ovMap, setOvMap] = useState<Record<string, ZcOverrides>>({});
|
||||
const [evidence, setEvidence] = useState<Evidence[]>([]);
|
||||
const [initOpen, setInitOpen] = useState(false);
|
||||
const [zoom, setZoom] = useState<{ src: string; title: string; index: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stored) return;
|
||||
@@ -90,10 +159,10 @@ export function AssessmentPage() {
|
||||
bands: gradesQ.data?.bands, weights: gradesQ.data?.weights,
|
||||
profile, rules, ov,
|
||||
}),
|
||||
[records, gradesQ.data, profile, rules, ov, year]
|
||||
[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); }));
|
||||
@@ -109,11 +178,14 @@ export function AssessmentPage() {
|
||||
try {
|
||||
const attList = await attachmentList(rid);
|
||||
const images: string[] = [];
|
||||
for (const att of attList.items.filter(x => x.id).slice(0, 3)) {
|
||||
for (const att of attList.items.filter(x => x.id).slice(0, 2)) {
|
||||
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);
|
||||
}
|
||||
if (images.length) out.push({ recordId: rid, title: rec?.title ?? '记录 ' + rid, images });
|
||||
if (images.length) {
|
||||
const meta = [rec?.type_name, rec?.level, rec?.award_grade].filter(Boolean).join(' · ');
|
||||
out.push({ recordId: rid, title: rec?.title ?? '记录 ' + rid, meta, images });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (alive) setEvidence(out);
|
||||
@@ -121,6 +193,13 @@ export function AssessmentPage() {
|
||||
return () => { alive = false; };
|
||||
}, [matchedIds.join(',')]);
|
||||
|
||||
// 全部放平的图序列(灯箱上一张/下一张用)
|
||||
const flatImages = useMemo(() => {
|
||||
const arr: { src: string; title: string }[] = [];
|
||||
for (const ev of evidence) for (const src of ev.images) arr.push({ src, title: ev.title });
|
||||
return arr;
|
||||
}, [evidence]);
|
||||
|
||||
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));
|
||||
@@ -133,47 +212,81 @@ export function AssessmentPage() {
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = '综合素质测评_' + termLabel + '.doc'; a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
toast('success', '综测材料已导出' + (evidence.length ? '(含 ' + evidence.length + ' 条证明材料图片)' : ''));
|
||||
toast('success', '综测表已导出(.doc,含证明材料附图 ' + flatImages.length + ' 张)');
|
||||
};
|
||||
|
||||
const zoomIdx = zoom ? flatImages.findIndex(x => x.src === zoom.src) : -1;
|
||||
const gotoZoom = (delta: number) => {
|
||||
const next = zoomIdx + delta;
|
||||
if (next >= 0 && next < flatImages.length) {
|
||||
setZoom({ src: flatImages[next].src, title: flatImages[next].title, index: next });
|
||||
}
|
||||
};
|
||||
|
||||
const totalImgs = flatImages.length;
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5">
|
||||
<div className="no-print mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="text-[15px] font-semibold text-ink-1">综合测评(按学年自动计算)</h2>
|
||||
<div>
|
||||
<h2 className="text-[15px] font-semibold text-ink-1">综合测评</h2>
|
||||
<p className="mt-0.5 text-[11.5px] text-ink-3">按学年依据学院细则自动计算,可手动修正,支持导出 Word 测评表</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select className="w-[190px]" value={year} onChange={e => setYear(e.target.value)}
|
||||
options={[...Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => x.term.split('-').slice(0, 2).join('-')))).map(y => ({ value: y, label: gradeLabel(y, profile.eduStart) })), { value: 'all', label: '全部学年' }]} />
|
||||
<Button variant="secondary" onClick={exportWord}><Download size={13} /> 导出材料</Button>
|
||||
<Button variant="secondary" onClick={exportWord}><Download size={13} /> 导出 Word</Button>
|
||||
<Button variant="secondary" onClick={() => setInitOpen(true)}><RotateCcw size={13} /> 初始化</Button>
|
||||
<Button variant="primary" onClick={() => window.print()}><Printer size={13} /> 打印</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="print-sheet mx-auto w-full max-w-[794px] rounded-lg border border-line bg-white p-5 shadow-card sm:p-8">
|
||||
<h1 className="text-center text-[18px] font-bold text-neutral-900">学生综合素质测评</h1>
|
||||
<div className="mt-1 text-center text-[12px] text-neutral-500">{profile.name ?? ''} · {profile.className ?? ''} · 统计周期:{termLabel}</div>
|
||||
<div className="print-sheet mx-auto w-full max-w-[820px] rounded-xl border border-line bg-white shadow-card sm:p-8">
|
||||
<div className="text-center text-[11px] tracking-widest text-neutral-500">{profile.school || '云川理工学院'}</div>
|
||||
<h1 className="mt-0.5 text-center text-[19px] font-bold tracking-wide text-neutral-900">学生综合素质测评表</h1>
|
||||
<div className="mt-1 text-center text-[12px] text-neutral-500">测评学年:{termLabel}</div>
|
||||
|
||||
<div className="mt-5 rounded-md bg-neutral-100 px-4 py-2 text-center text-[15px] font-bold text-neutral-900">
|
||||
综合素质总分:<span className="num">{result.total}</span> 分
|
||||
{/* 学生信息栏 */}
|
||||
<div className="mt-4 grid grid-cols-2 gap-px overflow-hidden rounded-lg border border-neutral-300 bg-neutral-300 text-[12.5px] sm:grid-cols-3">
|
||||
{[
|
||||
['学 号', profile.studentId], ['姓 名', profile.name], ['性 别', profile.gender],
|
||||
['学 院', profile.college], ['专 业', profile.major], ['班 级', profile.className],
|
||||
['政治面貌', profile.political], ['统计周期', termLabel],
|
||||
].map(([label, val]) => (
|
||||
<div key={label as string} className="flex bg-white">
|
||||
<span className="w-[62px] shrink-0 bg-neutral-50 px-2 py-1.5 font-medium text-neutral-500">{label}</span>
|
||||
<span className="min-w-0 flex-1 truncate px-2 py-1.5 text-neutral-900">{val ?? '—'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-center rounded-lg bg-gradient-to-r from-neutral-900 to-neutral-700 px-4 py-2.5 text-white">
|
||||
<span className="text-[12px] opacity-80">综合素质测评总分</span>
|
||||
<span className="num ml-3 text-[26px] font-bold leading-none">{result.total}</span>
|
||||
<span className="ml-1.5 text-[12px] opacity-80">/ 100 分</span>
|
||||
</div>
|
||||
|
||||
{result.sections.map(sec => (
|
||||
<div key={sec.key} className="mt-5">
|
||||
<div className="flex items-baseline justify-between border-b-2 border-neutral-800 pb-1">
|
||||
<span className="text-[13.5px] font-bold text-neutral-900">{sec.label}(上限 {sec.cap} 分)</span>
|
||||
<span className="num text-[12.5px] text-neutral-600">
|
||||
小计 {sec.key === 'rewards' ? sec.subtotal : Math.min(sec.subtotal, sec.cap)} 分
|
||||
<div key={sec.key} className="mt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-4 w-1 rounded-full bg-brand"></span>
|
||||
<span className="text-[13.5px] font-bold text-neutral-900">{sec.label}</span>
|
||||
<Badge className="no-print" color="var(--text-3)">上限 {sec.cap} 分</Badge>
|
||||
</div>
|
||||
<span className="num rounded-md bg-neutral-100 px-2 py-0.5 text-[12px] font-semibold text-neutral-700">
|
||||
小计 {sec.key === 'rewards' ? sec.subtotal : Math.min(sec.subtotal, sec.cap)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-1.5">
|
||||
<div className="mt-2 flex flex-col gap-1.5 rounded-lg border border-neutral-200 bg-neutral-50/60 p-3">
|
||||
{sec.items.map(it => (
|
||||
<div key={it.itemKey} className="flex items-center gap-2 text-[12.5px]">
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="text-neutral-900">{it.title}</span>
|
||||
<span className="ml-2 text-[11px] text-neutral-400">{it.source}</span>
|
||||
{it.note && <div className="text-[11px] text-neutral-400">{it.note}</div>}
|
||||
{it.note && <div className="text-[11px] leading-relaxed text-neutral-400">{it.note}</div>}
|
||||
</span>
|
||||
<input type="number" step="0.1" className="num w-[72px] rounded-sm border border-neutral-200 px-1 py-0.5 text-right text-[12px]"
|
||||
<input type="number" step="0.1" className="num w-[72px] rounded-md border border-neutral-200 bg-white px-1.5 py-0.5 text-right text-[12px] focus:border-brand focus:outline-none"
|
||||
value={it.score} onChange={e => {
|
||||
if (it.itemKey === 'conduct' || it.itemKey === 'integrity') {
|
||||
persistOv({ ...ov, [it.itemKey]: Number(e.target.value) });
|
||||
@@ -189,27 +302,59 @@ export function AssessmentPage() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{evidence.length > 0 && (
|
||||
<div className="mt-6 border-t border-neutral-300 pt-4">
|
||||
<div className="mb-3 text-[13.5px] font-bold text-neutral-900">附:证明材料</div>
|
||||
{evidence.map(ev => (
|
||||
<div key={ev.recordId} className="mb-4">
|
||||
<div className="text-[12px] font-semibold text-neutral-800">{ev.title}</div>
|
||||
<div className="mt-1.5 flex flex-wrap gap-3">
|
||||
{ev.images.map((src, i) => (
|
||||
<img key={i} src={src} alt={'证明 ' + (i + 1)} className="max-h-[180px] rounded border border-neutral-200" />
|
||||
))}
|
||||
</div>
|
||||
{/* 证明材料:图片画廊 */}
|
||||
{(evidence.length > 0 || totalImgs > 0) && (
|
||||
<div className="mt-7">
|
||||
<div className="flex items-center justify-between border-t border-neutral-200 pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Images size={15} className="text-neutral-500" />
|
||||
<span className="text-[13.5px] font-bold text-neutral-900">附:加分证明材料</span>
|
||||
<span className="text-[11.5px] text-neutral-400">共 {evidence.length} 项记录 · {totalImgs} 张图片(点击可放大)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{evidence.map(ev => (
|
||||
<div key={ev.recordId} className="rounded-lg border border-neutral-200 bg-white p-2.5 transition-shadow hover:shadow-md">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<div className="min-w-0 truncate text-[12.5px] font-semibold text-neutral-800">{ev.title}</div>
|
||||
<div className="shrink-0 text-[10.5px] text-neutral-400">{ev.meta}</div>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
{ev.images.map((src, i) => (
|
||||
<button key={i} onClick={() => setZoom({ src, title: ev.title, index: flatImages.findIndex(x => x.src === src) })}
|
||||
className="group overflow-hidden rounded-md border border-neutral-200 bg-neutral-50">
|
||||
<img src={src} alt={'证明 ' + (i + 1)} className="aspect-[4/3] w-full object-cover transition-transform group-hover:scale-[1.03]" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 border-t border-neutral-200 pt-3 text-[11px] leading-relaxed text-neutral-400">
|
||||
计分依据:《计算机与网络安全学院学生综合素质测评评分细则》。自动匹配条目均可手动修正分值;
|
||||
「恢复自动」清除本学年全部手动修正。导出材料前请核对每条加分记录与证明材料一致。
|
||||
「初始化」清除本学年全部手动修正。导出 Word / 打印前请核对加分记录与证明材料一致。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图片灯箱 */}
|
||||
<Dialog
|
||||
open={!!zoom}
|
||||
onClose={() => setZoom(null)}
|
||||
width={920}
|
||||
title={zoom?.title ?? '' + (zoom ? '(' + (zoomIdx + 1) + '/' + totalImgs + ')' : '')}
|
||||
footer={
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Button variant="secondary" disabled={zoomIdx <= 0} onClick={() => gotoZoom(-1)}><ChevronLeft size={13} /> 上一张</Button>
|
||||
<span className="num text-[12px] text-ink-3">{zoom ? (zoomIdx + 1) + ' / ' + totalImgs : ''}</span>
|
||||
<Button variant="primary" disabled={zoomIdx >= totalImgs - 1} onClick={() => gotoZoom(1)}>下一张 <ChevronRight size={13} /></Button>
|
||||
</div>
|
||||
}>
|
||||
{zoom && <img src={zoom.src} alt={zoom.title} className="mx-auto max-h-[74vh] rounded-md" />}
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={initOpen}
|
||||
onClose={() => setInitOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user