test: demo seed now 18 footprints across two academic years
This commit is contained in:
+60
-43
@@ -359,51 +359,59 @@ function openDb(dbPath) {
|
||||
// ---------- 演示数据(demo 账号播种 / 本地 seed 共用) ----------
|
||||
// 行格式: [类型, 标题, 角色, 级别, 获奖等级, 主办方, 开始, 结束, 状态, 描述, 结果, 标签, 维度, extras, metrics]
|
||||
export const DEMO_ROWS = [
|
||||
['竞赛', '2025 全国大学生数学建模竞赛', '主导', '国家级', '省市级二等奖', '中国工业与应用数学学会', '2025-09-05', '2025-09-08', 'finished',
|
||||
'担任队长,负责模型建立与论文主笔;协调三人分工与时间安排', '重庆赛区二等奖', ['建模', '团队协作'], ['学业深造'],
|
||||
{ my_work: '模型建立与论文主笔', deliverable: '参赛论文《基于优化模型的共享单车调度研究》', team_members: '张三、李四、王五' }, {}],
|
||||
['竞赛', '第十六届蓝桥杯程序设计大赛', '参加', '省市级', '省市级二等奖', '工业和信息化部人才交流中心', '2025-04-12', '2025-06-20', 'finished',
|
||||
'备战算法与数据结构,完成省赛', '重庆赛区 C/C++ B 组二等奖', ['算法'], ['职业发展', '学业深造'],
|
||||
{ my_work: '独立参赛', deliverable: '省赛获奖证书' }, {}],
|
||||
['竞赛', '全国大学生信息安全竞赛', '核心', '国家级', null, '教育部高等学校网络空间安全专业教学指导委员会', '2025-05-01', '2025-08-15', 'submitted',
|
||||
'负责 Web 漏洞挖掘与利用脚本编写,参与作品报告撰写', '已提交作品,等待国赛评审', ['安全', 'CTF'], ['职业发展'],
|
||||
{ my_work: 'Web 漏洞挖掘与利用脚本编写', deliverable: '参赛作品与报告' }, {}],
|
||||
['志愿公益', '校图书馆迎新志愿服务', '参加', '校级', null, '校图书馆', '2025-09-01', '2025-09-05', 'finished',
|
||||
'新生入馆引导与借阅咨询,共服务 5 天', '获馆方感谢信', ['志愿'], ['综合素养'],
|
||||
{ hours: 24, contact: '李老师' }, { volunteer_hours: 24 }],
|
||||
// ---- 大一(2023-2024 学年):学生工作 / 志愿 / 文体 / 证书 ----
|
||||
['学生工作', '院学生会宣传部干事', '干事', '院级', null, '院学生会', '2023-09-01', '2024-06-30', 'finished',
|
||||
'负责活动推文撰写与海报设计,全年产出推文 20 篇', '获优秀干事', ['宣传'], ['综合素养'],
|
||||
{ duties: '推文撰写、海报设计、活动现场摄影', term: '2023.09 - 2024.06' }, {}],
|
||||
['志愿公益', '校迎新志愿服务', '参加', '校级', null, '校青年志愿者联合会', '2023-09-01', '2023-09-03', 'finished',
|
||||
'新生入馆引导与借阅咨询', '获馆方感谢信', ['志愿'], ['综合素养'],
|
||||
{ hours: 24 }, { volunteer_hours: 24 }],
|
||||
['志愿公益', '无偿献血', '参加', null, null, '市中心血站', '2024-03-15', '2024-03-15', 'finished',
|
||||
'全血献血 400ml', '', ['公益'], ['综合素养'],
|
||||
{ hours: 2 }, { volunteer_hours: 2 }],
|
||||
['文体活动', '校运动会男子3000米', '参加', '校级', '三等奖', '校体育运动委员会', '2024-04-18', '2024-04-20', 'finished',
|
||||
'坚持训练一个月,决赛第三名', '', ['文体'], ['兴趣生活'],
|
||||
{ participation: '男子3000米决赛' }, {}],
|
||||
['证书技能', '大学英语四级(CET-4)', '参加', '国家级', null, '教育部教育考试院', '2024-06-15', '2024-08-20', 'finished',
|
||||
'笔试 562 分', 'CET-4 562 分', ['英语'], ['学业深造', '职业发展'],
|
||||
{ score: '562' }, {}],
|
||||
// ---- 大二(2024-2025 学年):竞赛 / 科研 / 荣誉 / 证书 ----
|
||||
['竞赛', '全国大学生数学建模竞赛', '主导', '省市级', '一等奖', '中国工业与应用数学学会', '2024-09-05', '2024-09-08', 'finished',
|
||||
'担任队长,负责模型建立与论文主笔;协调三人分工', '重庆赛区一等奖', ['建模', '团队协作'], ['学业深造'],
|
||||
{ my_work: '模型建立与论文主笔', team_members: '张三、李四' }, {}],
|
||||
['竞赛', '第十六届蓝桥杯程序设计大赛', '参加', '省市级', '二等奖', '工业和信息化部人才交流中心', '2025-04-12', '2025-06-01', 'finished',
|
||||
'备战算法与数据结构,完成省赛', '重庆赛区 C/C++ B 组二等奖', ['算法'], ['职业发展'],
|
||||
{ my_work: '独立参赛' }, {}],
|
||||
['竞赛', '全国大学生信息安全竞赛', '核心', '国家级', '三等奖', '网络空间安全专业教指委', '2025-05-01', '2025-08-15', 'finished',
|
||||
'负责 Web 漏洞挖掘与利用脚本编写', '全国总决赛三等奖', ['安全', 'CTF'], ['职业发展'],
|
||||
{ my_work: 'Web 漏洞挖掘与利用脚本' }, {}],
|
||||
['科研', '大学生创新创业训练计划项目', '主导', '校级', null, '校创新创业学院', '2025-03-01', null, 'ongoing',
|
||||
'主持校级大创项目「基于流量特征的入侵检测研究」,带 2 名成员', '中期检查通过', ['科研', '安全'], ['学业深造'],
|
||||
{ authors: '主持人', outcome: '研究报告' }, {}],
|
||||
['文体活动', '校园十佳歌手大赛', '参加', '校级', '二等奖', '校团委', '2024-12-01', '2024-12-15', 'finished',
|
||||
'初赛复赛决赛三轮', '校园十佳歌手', ['文体'], ['兴趣生活'],
|
||||
{}, {}],
|
||||
['志愿公益', '社区敬老院慰问活动', '参加', '区县级', null, '所在社区居委会', '2025-03-15', '2025-03-15', 'finished',
|
||||
'陪伴老人、协助整理活动室', '', ['志愿', '公益'], ['综合素养'],
|
||||
{ hours: 6 }, { volunteer_hours: 6 }],
|
||||
['学生工作', '院学生会宣传部干事', '参加', '院级', null, '院学生会', '2024-09-01', '2025-06-30', 'finished',
|
||||
'负责活动推文撰写与排版,全年产出推文 20 篇', '获优秀干事', ['宣传'], ['综合素养'],
|
||||
{ duties: '推文撰写与排版、活动现场摄影', term: '2024.09 - 2025.06' }, {}],
|
||||
['学生工作', '院学生会宣传部副部长', '主导', '院级', null, '院学生会', '2025-09-01', null, 'ongoing',
|
||||
'统筹部门工作,管理 8 名干事,主导院级活动宣传方案', '', ['管理'], ['综合素养', '职业发展'],
|
||||
{ duties: '部门统筹、活动宣传方案设计', term: '2025.09 - 至今' }, {}],
|
||||
['荣誉惩处', '校级二等奖学金', '参加', '校级', null, '云川理工学院', '2025-10-15', '2025-10-15', 'finished',
|
||||
'2024-2025 学年综合测评前列', '二等奖学金', ['奖学金'], ['学业深造'],
|
||||
{ issued_by: '云川理工学院' }, {}],
|
||||
['学生工作', '校学生会学习部干事', '干事', '校级', null, '校学生会', '2024-09-01', null, 'ongoing',
|
||||
'负责学风建设活动组织与学业帮扶配对', '', ['学风'], ['综合素养'],
|
||||
{ duties: '学风活动组织、学业帮扶配对', term: '2024.09 - 至今' }, {}],
|
||||
['荣誉惩处', '优秀共青团员', '参加', '校级', null, '校团委', '2025-05-04', '2025-05-04', 'finished',
|
||||
'五四表彰', '优秀共青团员', ['表彰'], ['综合素养'],
|
||||
{ issued_by: '共青团重庆城市科技学院委员会' }, {}],
|
||||
['证书技能', '大学英语六级', '参加', '国家级', null, '教育部教育考试院', '2025-06-14', '2025-08-20', 'finished',
|
||||
'笔试 528 分', 'CET-6 528 分', ['英语'], ['学业深造', '职业发展'],
|
||||
{ cert_no: '510001251200123', score: '528' }, {}],
|
||||
['证书技能', '计算机技术与软件专业技术资格(中级)', '参加', '国家级', null, '工业和信息化部', '2025-11-08', null, 'ongoing',
|
||||
'备考软件设计师', '', ['软考'], ['职业发展'],
|
||||
{ score: '' }, {}],
|
||||
['学业', '2024-2025 学年秋季学期', '参加', null, null, null, '2024-09-01', '2025-01-15', 'finished',
|
||||
'学期绩点 3.72,专业排名 12/156', '', ['绩点'], ['学业深造'],
|
||||
{ semester_label: '2024-2025-1' }, {}],
|
||||
['学业', '2024-2025 学年春季学期', '参加', null, null, null, '2025-02-24', '2025-07-05', 'finished',
|
||||
'学期绩点 3.85,专业排名 9/156', '', ['绩点'], ['学业深造'],
|
||||
{ semester_label: '2024-2025-2' }, {}],
|
||||
['文体活动', '校运动会开幕式方阵', '参加', '校级', null, '校体育运动委员会', '2025-04-18', '2025-04-20', 'finished',
|
||||
'学院方阵队员,参与排练与开幕式表演', '', ['文体'], ['兴趣生活'],
|
||||
{ participation: '方阵队员' }, {}],
|
||||
['科研', '大学生创新创业训练计划项目', '主导', '校级', null, '校创新创业学院', '2025-03-01', null, 'ongoing',
|
||||
'主持校级大创项目「基于流量特征的入侵检测研究」,带 2 名成员', '中期检查通过', ['科研', '安全'], ['学业深造', '职业发展'],
|
||||
{ authors: '主持人', outcome: '研究报告' }, {}],
|
||||
{}, {}],
|
||||
['荣誉惩处', '综合测评奖学金', '参加', '校级', null, '云川理工学院', '2025-10-15', '2025-10-15', 'finished',
|
||||
'2024-2025 学年综合测评前列(细则规定综测奖学金不加分,用于验证排除逻辑)', '二等奖学金', ['奖学金'], ['学业深造'],
|
||||
{}, {}],
|
||||
['证书技能', '全国计算机等级考试二级', '参加', '国家级', null, '教育部考试中心', '2025-03-22', '2025-05-10', 'finished',
|
||||
'MS Office 高级应用', '二级合格证书', ['计算机'], ['职业发展'],
|
||||
{}, {}],
|
||||
['证书技能', '机动车驾驶证(C1)', '参加', null, null, '市公安局交通管理局', '2024-07-01', '2024-08-10', 'finished',
|
||||
'科目一到科目四一次通过', 'C1 驾驶证', ['驾照'], ['生活技能'],
|
||||
{}, {}],
|
||||
['证书技能', '计算机技术与软件专业技术资格(软件设计师)', '参加', '国家级', null, '工业和信息化部', '2025-11-08', null, 'ongoing',
|
||||
'备考软件设计师(中级)', '', ['软考'], ['职业发展'],
|
||||
{}, {}],
|
||||
['实习实践', '某安全公司渗透测试实习生', '参加', null, null, '某网络安全科技有限公司', '2025-07-01', '2025-08-25', 'finished',
|
||||
'参与企业授权渗透测试项目 3 个,编写测试报告', '获实习优秀评价', ['实习', '安全'], ['职业发展'],
|
||||
{ position: '渗透测试实习生', mentor: '陈工' }, {}]
|
||||
@@ -421,7 +429,10 @@ export const DEMO_PROFILE = {
|
||||
}
|
||||
};
|
||||
|
||||
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点][] }
|
||||
// 演示附件用真实 PNG(1x1,可在预览查看并进入综测证明材料)
|
||||
const DEMO_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
||||
|
||||
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点, 类别][] }
|
||||
export const DEMO_SEMESTERS = [
|
||||
{ term: '2023-2024-1', gpa: 3.42, rank: 38, total: 156, courses: [
|
||||
['高等数学(上)', 5, 78, 3.0, '必修课'], ['大学英语(一)', 3, 85, 3.7, '必修课'], ['程序设计基础', 4, 91, 4.0, '必修课'],
|
||||
@@ -433,7 +444,7 @@ export const DEMO_SEMESTERS = [
|
||||
]},
|
||||
{ term: '2024-2025-1', gpa: 3.78, rank: 18, total: 156, courses: [
|
||||
['概率论与数理统计', 3, 88, 3.9, '必修课'], ['操作系统', 4, 92, 4.0, '必修课'], ['计算机网络', 4, 86, 3.8, '必修课'],
|
||||
['数据库原理', 3, 94, 4.0, '必修课'], ['马克思主义基本原理', 2, 81, 3.3, '必修课']
|
||||
['数据库原理', 3, 94, 4.0, '必修课'], ['马克思主义基本原理', 2, 81, 3.3, '必修课'], ['体育(三)', 1, 88, 3.9, '必修课']
|
||||
]},
|
||||
{ term: '2024-2025-2', gpa: 3.85, rank: 12, total: 156, courses: [
|
||||
['软件工程', 3, 90, 4.0, '必修课'], ['信息安全导论', 3, 95, 4.0, '必修课'], ['密码学基础', 3, 89, 3.9, '必修课'],
|
||||
@@ -1225,13 +1236,19 @@ function listFootprints(d, query) {
|
||||
for (const [t, title, role, level, grade, organizer, start, end, status, desc, result, tags, dimNames, extras, metrics] of DEMO_ROWS) {
|
||||
const typeId = typeIds[t];
|
||||
if (!typeId) continue;
|
||||
handleCommand('footprint_upsert', { item: {
|
||||
const fr = handleCommand('footprint_upsert', { item: {
|
||||
type_id: typeId, title, role, level, award_grade: grade, organizer,
|
||||
start_date: start, end_date: end, status, description: desc, result,
|
||||
tags, metrics, extras,
|
||||
dimension_ids: (dimNames || []).map((nm) => dimIds[nm]).filter(Boolean),
|
||||
}});
|
||||
n++;
|
||||
// 演示附件:竞赛/荣誉记录附带证书图片(可预览、可进综测证明材料)
|
||||
if (/竞赛|优秀共青团员/.test(String(title)) && status === 'finished') {
|
||||
try {
|
||||
handleCommand('attachment_add', { owner_id: fr.item.id, file_name: '获奖证书.png', mime: 'image/png', size: DEMO_PNG.length, data_b64: DEMO_PNG, thumb_b64: DEMO_PNG });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
let sems = 0;
|
||||
for (const s of DEMO_SEMESTERS) {
|
||||
|
||||
+29
-18
@@ -38,12 +38,21 @@ export interface ZcOverrides {
|
||||
manual?: { title: string; section: string; score: number }[];
|
||||
}
|
||||
|
||||
// 学期 → 统计时间窗 [start, end)
|
||||
export function termRange(term: string): [string, string] | null {
|
||||
const m = /^(\d{4})-(\d{4})-(1|2)$/.exec(term);
|
||||
// 学年('2024-2025')→ 统计时间窗 [2024-09-01, 2025-09-01)
|
||||
export function yearRange(year: string): [string, string] | null {
|
||||
const m = /^(\d{4})-(\d{4})$/.exec(year);
|
||||
if (!m) return null;
|
||||
const y1 = Number(m[1]);
|
||||
return m[3] === '1' ? [y1 + '-09-01', (y1 + 1) + '-03-01'] : [(y1 + 1) + '-03-01', (y1 + 1) + '-09-01'];
|
||||
return [m[1] + '-09-01', m[2] + '-09-01'];
|
||||
}
|
||||
|
||||
// 学年 → 年级名(按个人信息入学年份推算:大一~大四)
|
||||
export function gradeLabel(year: string, eduStart?: string): string {
|
||||
const m = /^(\d{4})-/.exec(year);
|
||||
const e = /^(\d{4})/.exec(eduStart ?? '');
|
||||
if (!m || !e) return year + ' 学年';
|
||||
const n = Number(m[1]) - Number(e[1]) + 1;
|
||||
const names = ['大一', '大二', '大三', '大四', '大五'];
|
||||
return (names[n - 1] ?? n + '年级') + '(' + year + ' 学年)';
|
||||
}
|
||||
|
||||
function inRange(d: string | null | undefined, range: [string, string] | null): boolean {
|
||||
@@ -80,6 +89,7 @@ export interface ZcItem {
|
||||
score: number;
|
||||
overridden?: boolean;
|
||||
note?: string;
|
||||
fromRecord?: number;
|
||||
}
|
||||
export interface ZcSection { key: string; label: string; cap: number; items: ZcItem[]; subtotal: number }
|
||||
export interface ZcResult { sections: ZcSection[]; total: number; gpa: number | null }
|
||||
@@ -94,7 +104,7 @@ function pickScore(r: ZcRules, levelKey: string | null, gradeKey: string | null)
|
||||
|
||||
// 计算某学期(term,或 'all' 全年)的综测
|
||||
export function computeZongce(opts: {
|
||||
term: string;
|
||||
year: string;
|
||||
records: FootprintRow[];
|
||||
semesters: { term: string; gpa?: number | null; courses: CourseGrade[] }[];
|
||||
bands?: GradeBand[];
|
||||
@@ -103,14 +113,14 @@ export function computeZongce(opts: {
|
||||
rules: ZcRules;
|
||||
ov: ZcOverrides;
|
||||
}): ZcResult {
|
||||
const { term, records, semesters, bands, weights, profile, rules: r, ov } = opts;
|
||||
const range = term === 'all' ? null : termRange(term);
|
||||
const { year, records, semesters, bands, weights, profile, rules: r, ov } = opts;
|
||||
const range = year === 'all' ? null : yearRange(year);
|
||||
const scoped = records.filter(x => x.status !== 'draft' && inRange(x.start_date ?? x.created_at, range));
|
||||
const ovItem = (k: string) => ov.itemOverrides?.[k];
|
||||
const mk = (k: string, title: string, source: string, auto: number | null, note?: string): ZcItem => {
|
||||
const mk = (k: string, title: string, source: string, auto: number | null, note?: string, fromRecord?: number): ZcItem => {
|
||||
const o = ovItem(k);
|
||||
const score = o?.score ?? (auto ?? 0);
|
||||
return { itemKey: k, title, source, score, overridden: o?.score != null, note };
|
||||
return { itemKey: k, title, source, score, overridden: o?.score != null, note, fromRecord };
|
||||
};
|
||||
const sec = (key: string, label: string, cap: number, items: ZcItem[]): ZcSection => ({
|
||||
key, label, cap, items,
|
||||
@@ -133,9 +143,9 @@ export function computeZongce(opts: {
|
||||
// ---- 学业成绩(60):学年平均学分绩点 × 10 ----
|
||||
const scopedCourses: CourseGrade[] = [];
|
||||
for (const x of semesters) {
|
||||
if (!inRange(null, null)) continue;
|
||||
const rng = term === 'all' ? null : termRange(x.term);
|
||||
const hit = term === 'all' || (range && rng && !(rng[1] <= range[0] || rng[0] >= range[1]));
|
||||
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;
|
||||
@@ -174,7 +184,7 @@ export function computeZongce(opts: {
|
||||
for (const rec of scoped.filter(x => x.type_name === '文体活动')) {
|
||||
const auto = pickScore(r, normLevel(rec.level), normGrade(rec.award_grade));
|
||||
if (auto == null && !ovItem('w' + rec.id)?.score) continue;
|
||||
wItems.push(mk('w' + rec.id, rec.title, rec.type_name + ' · ' + (rec.level ?? '') + ' ' + (rec.award_grade ?? ''), auto));
|
||||
wItems.push(mk('w' + rec.id, rec.title, rec.type_name + ' · ' + (rec.level ?? '') + ' ' + (rec.award_grade ?? ''), auto, undefined, rec.id));
|
||||
}
|
||||
const wCap = round2(Math.min(wItems.reduce((a, x) => a + x.score, 0), 5));
|
||||
sections.push(sec('sports', '文体素质', 10, [...sportsItems, { itemKey: 'wcap', title: '文体类加分(上限 5 分)', source: wItems.length + ' 条记录', score: wCap, note: wItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' }]));
|
||||
@@ -184,7 +194,7 @@ export function computeZongce(opts: {
|
||||
for (const rec of scoped.filter(x => x.type_name === '竞赛' || x.type_name === '科研')) {
|
||||
const auto = pickScore(r, normLevel(rec.level), normGrade(rec.award_grade));
|
||||
if (auto == null && !ovItem('c' + rec.id)?.score) continue;
|
||||
compItems.push(mk('c' + rec.id, rec.title, rec.type_name + ' · ' + (rec.level ?? '') + ' ' + (rec.award_grade ?? ''), auto));
|
||||
compItems.push(mk('c' + rec.id, rec.title, rec.type_name + ' · ' + (rec.level ?? '') + ' ' + (rec.award_grade ?? ''), auto, undefined, rec.id));
|
||||
}
|
||||
const compCap = round2(Math.min(compItems.reduce((a, x) => a + x.score, 0), 10));
|
||||
const workItems: ZcItem[] = [];
|
||||
@@ -195,7 +205,7 @@ export function computeZongce(opts: {
|
||||
let auto: number | null = null;
|
||||
for (const [k, v] of Object.entries(tbl)) { if (role.includes(k)) { auto = v; break; } }
|
||||
if (auto == null && !ovItem('k' + rec.id)?.score) continue;
|
||||
workItems.push(mk('k' + rec.id, rec.title, '学生工作 · ' + (rec.level ?? '院级') + ' ' + (rec.role ?? ''), auto, auto == null ? '请手动填写分值' : undefined));
|
||||
workItems.push(mk('k' + rec.id, rec.title, '学生工作 · ' + (rec.level ?? '院级') + ' ' + (rec.role ?? ''), auto, auto == null ? '请手动填写分值' : undefined, rec.id));
|
||||
}
|
||||
const workCap = round2(Math.min(workItems.reduce((a, x) => a + x.score, 0), 4));
|
||||
const pubItems: ZcItem[] = [];
|
||||
@@ -204,7 +214,7 @@ export function computeZongce(opts: {
|
||||
const blood = /献血/.test(rec.title);
|
||||
let auto: number | null = blood ? r.publicGood['无偿献血'] : (lvl ? r.publicGood[lvl] ?? null : null);
|
||||
if (auto == null && !ovItem('g' + rec.id)?.score) continue;
|
||||
pubItems.push(mk('g' + rec.id, rec.title, '公益 · ' + (blood ? '无偿献血' : rec.level ?? '校级'), auto));
|
||||
pubItems.push(mk('g' + rec.id, rec.title, '公益 · ' + (blood ? '无偿献血' : rec.level ?? '校级'), auto, undefined, rec.id));
|
||||
}
|
||||
const pubCap = round2(Math.min(pubItems.reduce((a, x) => a + x.score, 0), 3));
|
||||
sections.push(sec('expand', '拓展性素质', 17, [
|
||||
@@ -218,6 +228,7 @@ export function computeZongce(opts: {
|
||||
for (const rec of scoped.filter(x => x.type_name === '荣誉惩处' || x.type_name === '证书技能')) {
|
||||
const o = ovItem('h' + rec.id);
|
||||
const text = rec.title + ' ' + (rec.award_grade ?? '');
|
||||
if (/综合测评奖学金/.test(text)) continue; // 细则:综合测评奖学金不加分
|
||||
let auto: number | null = null;
|
||||
let source = rec.type_name;
|
||||
if (/留校察看/.test(text)) auto = -r.penalties['留校察看'];
|
||||
@@ -241,7 +252,7 @@ export function computeZongce(opts: {
|
||||
if (lkey && r.rewards['荣誉称号'][lkey] != null) { auto = r.rewards['荣誉称号'][lkey]; source = '荣誉 · ' + lkey; }
|
||||
}
|
||||
if (auto == null && !o?.score) continue;
|
||||
rwItems.push(mk('h' + rec.id, rec.title, source, auto));
|
||||
rwItems.push(mk('h' + rec.id, rec.title, source, auto, undefined, rec.id));
|
||||
}
|
||||
const rwCap = round2(Math.max(Math.min(rwItems.reduce((a, x) => a + x.score, 0), 5), -5));
|
||||
sections.push(sec('rewards', '奖惩分', 5, [{ itemKey: 'hcap', title: '奖惩合计(±5 分)', source: rwItems.length + ' 条记录', score: rwCap, note: rwItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' }]));
|
||||
|
||||
@@ -1,11 +1,65 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Download, Printer, RotateCcw } from 'lucide-react';
|
||||
import { footprintList, gradesOverview, profileGet, profileUpsert } from '@/lib/api';
|
||||
import { attachmentList, attachmentThumb, footprintList, gradesOverview, profileGet, profileUpsert } from '@/lib/api';
|
||||
import type { FootprintRow, ProfileInfo, ZcOverrides } from '@/lib/types';
|
||||
import { computeZongce, ZC_DEFAULT_RULES, type ZcRules } from '@/lib/zongce';
|
||||
import { computeZongce, gradeLabel, ZC_DEFAULT_RULES, type ZcRules, type ZcResult } from '@/lib/zongce';
|
||||
import { Button, Select, useToast } from '@/components/ui';
|
||||
|
||||
interface Evidence { recordId: number; title: string; images: string[] }
|
||||
|
||||
function esc(t: unknown) { return String(t ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
||||
|
||||
// MHTML:把图片内嵌进 Word(.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');
|
||||
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');
|
||||
return new Blob([parts.join('')], { type: 'multipart/related; type="text/html"; boundary="' + boundary + '"' });
|
||||
}
|
||||
|
||||
function zongceWordMhtml(termLabel: string, result: ZcResult, profile: Partial<ProfileInfo>, evidence: Evidence[]): Blob {
|
||||
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>';
|
||||
}).join('');
|
||||
const images: { name: string; dataUri: string }[] = [];
|
||||
const annex = evidence.map(ev => {
|
||||
let imgs = '';
|
||||
ev.images.forEach((uri, i) => {
|
||||
const name = 'ev' + ev.recordId + '_' + i;
|
||||
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>';
|
||||
}).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>';
|
||||
return buildMhtml(html, images);
|
||||
}
|
||||
|
||||
export function AssessmentPage() {
|
||||
const toast = useToast();
|
||||
const profileQ = useQuery({ queryKey: ['profile'], queryFn: profileGet });
|
||||
@@ -15,61 +69,86 @@ export function AssessmentPage() {
|
||||
const stored = profileQ.data?.content;
|
||||
const profile: Partial<ProfileInfo> = { ...(stored?.info ?? {}) };
|
||||
const rules: ZcRules = stored?.zongce?.rules ?? ZC_DEFAULT_RULES;
|
||||
const [term, setTerm] = useState('');
|
||||
const [year, setYear] = useState('');
|
||||
const [ovMap, setOvMap] = useState<Record<string, ZcOverrides>>({});
|
||||
const [evidence, setEvidence] = useState<Evidence[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stored) return;
|
||||
setOvMap(stored.zongce?.overrides ?? {});
|
||||
const sems = (gradesQ.data?.semesters ?? []).map(x => x.term);
|
||||
if (!term && sems.length) setTerm(sems[0]);
|
||||
const years = Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => x.term.slice(0, 7))));
|
||||
if (!year && years.length) setYear(years[0]);
|
||||
}, [stored, gradesQ.data]);
|
||||
|
||||
const records: FootprintRow[] = listQ.data?.items ?? [];
|
||||
const ov: ZcOverrides = ovMap[term] ?? {};
|
||||
const ov: ZcOverrides = ovMap[year] ?? {};
|
||||
const result = useMemo(
|
||||
() => computeZongce({
|
||||
term: term || 'all', records,
|
||||
year: year || 'all', records,
|
||||
semesters: gradesQ.data?.semesters ?? [],
|
||||
bands: gradesQ.data?.bands, weights: gradesQ.data?.weights,
|
||||
profile, rules, ov,
|
||||
}),
|
||||
[records, gradesQ.data, profile, rules, ov, term]
|
||||
[records, gradesQ.data, profile, rules, ov, year]
|
||||
);
|
||||
|
||||
const persistOv = (next: ZcOverrides) => {
|
||||
setOvMap(m => ({ ...m, [term]: next }));
|
||||
void profileUpsert({ info: profile, zongce: { rules, overrides: { ...ovMap, [term]: next } } }).catch(e => toast('error', e.message));
|
||||
};
|
||||
// 证明材料:匹配条目对应记录的附件缩略图
|
||||
const matchedIds = useMemo(() => {
|
||||
const ids: number[] = [];
|
||||
result.sections.forEach(sec => sec.items.forEach(it => { if (it.fromRecord) ids.push(it.fromRecord); }));
|
||||
return ids;
|
||||
}, [result]);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
(async () => {
|
||||
const out: Evidence[] = [];
|
||||
for (const rid of matchedIds) {
|
||||
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, 3)) {
|
||||
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 });
|
||||
} catch {}
|
||||
}
|
||||
if (alive) setEvidence(out);
|
||||
})();
|
||||
return () => { alive = false; };
|
||||
}, [matchedIds.join(',')]);
|
||||
|
||||
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));
|
||||
};
|
||||
const resetOverrides = () => {
|
||||
const next = { ...ovMap };
|
||||
delete next[term];
|
||||
delete next[year];
|
||||
setOvMap(next);
|
||||
void profileUpsert({ info: profile, zongce: { rules, overrides: next } }).then(() => toast('success', '已恢复自动计算')).catch(e => toast('error', e.message));
|
||||
};
|
||||
|
||||
const termLabel = term === 'all' ? '全学年' : term;
|
||||
const termLabel = year === 'all' ? '全部学年' : gradeLabel(year, profile.eduStart);
|
||||
|
||||
const exportWord = () => {
|
||||
void import('@/lib/zongceWord').then(({ zongceToWordHtml }) => {
|
||||
const html = zongceToWordHtml(termLabel, result.sections, result.total, profile);
|
||||
const blob = new Blob(['\ufeff' + html], { type: 'application/msword' });
|
||||
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', '综测材料已导出');
|
||||
});
|
||||
const blob = zongceWordMhtml(termLabel, result, profile, evidence);
|
||||
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', '综测材料已导出' + (evidence.length ? '(含 ' + evidence.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>
|
||||
<h2 className="text-[15px] font-semibold text-ink-1">综合测评(按学年自动计算)</h2>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select className="w-[150px]" value={term} onChange={e => setTerm(e.target.value)}
|
||||
options={[...(gradesQ.data?.semesters ?? []).map(x => ({ value: x.term, label: x.term })), { value: 'all', label: '全学年' }]} />
|
||||
<Select className="w-[190px]" value={year} onChange={e => setYear(e.target.value)}
|
||||
options={[...Array.from(new Set((gradesQ.data?.semesters ?? []).map(x => x.term.slice(0, 7)))).map(y => ({ value: y, label: gradeLabel(y, profile.eduStart) })), { value: 'all', label: '全部学年' }]} />
|
||||
<Button variant="secondary" onClick={resetOverrides}><RotateCcw size={13} /> 恢复自动</Button>
|
||||
<Button variant="secondary" onClick={exportWord}><Download size={13} /> 导出材料</Button>
|
||||
<Button variant="primary" onClick={() => window.print()}><Printer size={13} /> 打印</Button>
|
||||
@@ -116,9 +195,25 @@ 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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 border-t border-neutral-200 pt-3 text-[11px] leading-relaxed text-neutral-400">
|
||||
计分依据:《计算机与网络安全学院学生综合素质测评评分细则》。自动匹配条目均可手动修正分值;
|
||||
「恢复自动」清除本学期全部手动修正。导出材料前请核对每条加分记录与证明材料一致。
|
||||
「恢复自动」清除本学年全部手动修正。导出材料前请核对每条加分记录与证明材料一致。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ const rootMe = await api(A, 'GET', '/auth/me', undefined, T);
|
||||
ok(rootMe.body?.data?.username === 'root' && rootMe.body?.data?.is_admin === true, 'root 是管理员', rootMe.body);
|
||||
|
||||
const fp = await api(A, 'POST', '/api/footprint_list', { query: { page: 1, page_size: 50 } }, D);
|
||||
ok(fp.body?.data?.total === 16, '演示足迹 16 条(播种成功)', fp.body?.data?.total);
|
||||
ok(fp.body?.data?.total === 18, '演示足迹 18 条(播种成功)', fp.body?.data?.total);
|
||||
const grades = await api(A, 'POST', '/api/grades_overview', {}, D);
|
||||
ok(grades.body?.data?.semesters?.length === 4, '演示学期 4 个', grades.body?.data?.semesters?.length);
|
||||
const tl = await api(A, 'POST', '/api/timeline', { query: {} }, D);
|
||||
@@ -167,7 +167,7 @@ ok(reseed.body?.data?.seeded === false, '重复播种幂等跳过', reseed.body)
|
||||
|
||||
// demo 重建路由(管理员):重置 demo 数据为初始播种态
|
||||
const re2 = await api(A, 'POST', '/admin/demo-rebuild', {}, T);
|
||||
ok(re2.status === 200 && re2.body?.data?.seeded?.footprints === 16, 'demo 重建路由可达', re2.body);
|
||||
ok(re2.status === 200 && re2.body?.data?.seeded?.footprints === 18, 'demo 重建路由可达', re2.body);
|
||||
|
||||
// ---------- Phase B:同一数据目录「重启」(新模块实例 + 新端口) ----------
|
||||
console.log('\n== Phase B:重启后(持久化 + 不重复播种) ==');
|
||||
@@ -180,7 +180,7 @@ const rootLogin2 = await api(B, 'POST', '/auth/login', { username: 'root', passw
|
||||
ok(rootLogin2.status === 200, '重启后 root 可登录', rootLogin2.body);
|
||||
const T2 = rootLogin2.body.data.token;
|
||||
const fp2 = await api(B, 'POST', '/api/footprint_list', { query: { page: 1, page_size: 50 } }, D2);
|
||||
ok(fp2.body?.data?.total === 16, '重启后仍是 16 条(测试足迹已被级联删除)', fp2.body?.data?.total);
|
||||
ok(fp2.body?.data?.total === 18, '重启后仍是 18 条(测试足迹已被级联删除)', fp2.body?.data?.total);
|
||||
const list2 = (await api(B, 'GET', '/admin/invites', undefined, T2)).body.data.invites;
|
||||
ok(list2.some((i) => i.code === 'XIONG-FRIEND-2025'), '邀请码跨重启持久化');
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{r as g,f as v,j as t,S as j,t as S}from"./index-DYhSDydL.js";import{C as c,a as d,b as x}from"./card-jktDkcYF.js";import{u as N,i as u,a as m}from"./useChart-DgHaQNyb.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"lg:col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
||||
import{r as g,f as v,j as t,S as j,t as S}from"./index-DhGFbjWw.js";import{C as c,a as d,b as x}from"./card-65YyMNIi.js";import{u as N,i as u,a as m}from"./useChart-BbOzH_LJ.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"lg:col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as $,f as O,j as e,E as H,w as U,u as k,d as C,r as u,g as y,x as W,y as J,B as j,P as E,I as g,z as Y,C as A,A as V,D as B,F as X,m as S,l as Q,T as q,H as Z,J as z,K as M,S as T,M as D}from"./index-DYhSDydL.js";import{C as w,a as G,b as F}from"./card-jktDkcYF.js";import{u as ee,i as K,a as P}from"./useChart-DgHaQNyb.js";import{u as se,i as te,b as ae,a as re,c as ne,d as le,e as ie,f as oe}from"./echarts-BIWefrIc.js";/**
|
||||
import{c as $,f as O,j as e,E as H,w as U,u as k,d as C,r as u,g as y,x as W,y as J,B as j,P as E,I as g,z as Y,C as A,A as V,D as B,F as X,m as S,l as Q,T as q,H as Z,J as z,K as M,S as T,M as D}from"./index-DhGFbjWw.js";import{C as w,a as G,b as F}from"./card-65YyMNIi.js";import{u as ee,i as K,a as P}from"./useChart-BbOzH_LJ.js";import{u as se,i as te,b as ae,a as re,c as ne,d as le,e as ie,f as oe}from"./echarts-BIWefrIc.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as E,u as P,d as S,r as p,f as q,g as F,j as e,B as f,l as B,ac as L,H as z,J as k,S as M,I as Q,ad as _,ae as K,af as v,ag as T}from"./index-DYhSDydL.js";/**
|
||||
import{c as E,u as P,d as S,r as p,f as q,g as F,j as e,B as f,l as B,ac as L,H as z,J as k,S as M,I as Q,ad as _,ae as K,af as v,ag as T}from"./index-DhGFbjWw.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as fe,j as e,a as N,r as s,b as he,u as ve,d as ge,e as je,f as L,g as ye,I as be,S as k,B as w,P as Q,h as te,L as se,G as ae,R as Ne,i as ne,k as ke,l as we,T as Ce,E as le,m as Ee,n as Se,o as _e,C as ze,p as qe,q as Le,s as Re,t as De,v as Ie}from"./index-DYhSDydL.js";/**
|
||||
import{c as fe,j as e,a as N,r as s,b as he,u as ve,d as ge,e as je,f as L,g as ye,I as be,S as k,B as w,P as Q,h as te,L as se,G as ae,R as Ne,i as ne,k as ke,l as we,T as Ce,E as le,m as Ee,n as Se,o as _e,C as ze,p as qe,q as Le,s as Re,t as De,v as Ie}from"./index-DhGFbjWw.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as A,d as we,f as se,r as T,j as e,B as R,ah as Ce,af as M,J as B,S as ne,ad as de,P as $,H as Se,a9 as _e,T as ie,I as V,ag as ze,v as Re}from"./index-DYhSDydL.js";import{C as ae,a as le,b as re}from"./card-jktDkcYF.js";import{R as Pe}from"./refresh-cw-CP7uPCIx.js";import{R as oe,P as Ee}from"./rotate-ccw-CE3bwRx2.js";/**
|
||||
import{c as A,d as we,f as se,r as T,j as e,B as R,ah as Ce,af as M,J as B,S as ne,ad as de,P as $,H as Se,a9 as _e,T as ie,I as V,ag as ze,v as Re}from"./index-DhGFbjWw.js";import{C as ae,a as le,b as re}from"./card-65YyMNIi.js";import{R as Pe}from"./refresh-cw-C3HYho-f.js";import{R as oe,P as Ee}from"./rotate-ccw-DGANt55P.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as D,u as K,d as I,f as L,r,g as q,j as e,B as p,P as F,N as O,m as _,l as H,T as P,C as R,O as G,H as J,J as i,I as j,Q as U,S as z,U as V,V as W,q as X}from"./index-DYhSDydL.js";import{C as Y}from"./card-jktDkcYF.js";/**
|
||||
import{c as D,u as K,d as I,f as L,r,g as q,j as e,B as p,P as F,N as O,m as _,l as H,T as P,C as R,O as G,H as J,J as i,I as j,Q as U,S as z,U as V,V as W,q as X}from"./index-DhGFbjWw.js";import{C as Y}from"./card-65YyMNIi.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1 +1 @@
|
||||
import{j as s,a}from"./index-DYhSDydL.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
||||
import{j as s,a}from"./index-DhGFbjWw.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DYhSDydL.js";/**
|
||||
import{c as e}from"./index-DhGFbjWw.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-DYhSDydL.js";/**
|
||||
import{c as t}from"./index-DhGFbjWw.js";/**
|
||||
* @license lucide-react v0.475.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r}from"./index-DYhSDydL.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
||||
import{r}from"./index-DhGFbjWw.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
||||
@@ -1 +0,0 @@
|
||||
const t=l=>String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");function c(l,r,d,i){const n=r.map(e=>{const s=e.key==="rewards"?e.subtotal:Math.min(e.subtotal,e.cap),a=e.items.map(o=>'<tr><td style="padding:3pt 6pt;border:0.75pt solid #999">'+t(o.title)+'</td><td style="padding:3pt 6pt;border:0.75pt solid #999;font-size:10pt;color:#555">'+t(o.note||o.source)+'</td><td style="padding:3pt 6pt;border:0.75pt solid #999;text-align:right;width:70pt">'+t(o.score)+"</td></tr>").join("");return'<h2 style="font-size:13pt;margin:12pt 0 5pt;color:#111">'+t(e.label)+"(上限 "+t(e.cap)+" 分,小计 "+t(s)+' 分)</h2><table style="width:100%;border-collapse:collapse;font-size:11pt">'+a+"</table>"}).join("");return'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word"><head><meta charset="utf-8"><title>综合素质测评</title><!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View></w:WordDocument></xml><![endif]--></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="font-size:11pt;color:#555;margin-bottom:6pt">'+t(i.name??"")+" · "+t(i.className??"")+" · 统计周期:"+t(l)+"</div>")+'<p style="text-align:center;font-size:13pt;background:#f2f2f2;padding:6pt"><b>综合素质总分:'+t(d)+" 分</b></p>"+n+'<div style="margin-top:24pt;font-size:10.5pt;color:#555">计分依据:《计算机与网络安全学院学生综合素质测评评分细则》。以上自动匹配条目均经过人工核对。</div><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>'}export{c as zongceToWordHtml};
|
||||
@@ -9,8 +9,8 @@
|
||||
<link rel="icon" href="/icons/pwa-192.png" />
|
||||
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
|
||||
<title>足迹 Footprints</title>
|
||||
<script type="module" crossorigin src="/assets/index-DYhSDydL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cn3Zl9XQ.css">
|
||||
<script type="module" crossorigin src="/assets/index-DhGFbjWw.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9uCsgi6.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user