feat: fictional demo persona (profile seed), word export mirrors preview layout, print scrollbar fix
This commit is contained in:
+17
-2
@@ -370,9 +370,9 @@ export const DEMO_ROWS = [
|
||||
['学生工作', '院学生会宣传部副部长', '主导', '院级', null, '院学生会', '2025-09-01', null, 'ongoing',
|
||||
'统筹部门工作,管理 8 名干事,主导院级活动宣传方案', '', ['管理'], ['综合素养', '职业发展'],
|
||||
{ duties: '部门统筹、活动宣传方案设计', term: '2025.09 - 至今' }, {}],
|
||||
['荣誉惩处', '校级二等奖学金', '参加', '校级', null, '重庆城市科技学院', '2025-10-15', '2025-10-15', 'finished',
|
||||
['荣誉惩处', '校级二等奖学金', '参加', '校级', null, '云川理工学院', '2025-10-15', '2025-10-15', 'finished',
|
||||
'2024-2025 学年综合测评前列', '二等奖学金', ['奖学金'], ['学业深造'],
|
||||
{ issued_by: '重庆城市科技学院' }, {}],
|
||||
{ issued_by: '云川理工学院' }, {}],
|
||||
['荣誉惩处', '优秀共青团员', '参加', '校级', null, '校团委', '2025-05-04', '2025-05-04', 'finished',
|
||||
'五四表彰', '优秀共青团员', ['表彰'], ['综合素养'],
|
||||
{ issued_by: '共青团重庆城市科技学院委员会' }, {}],
|
||||
@@ -398,6 +398,19 @@ export const DEMO_ROWS = [
|
||||
'参与企业授权渗透测试项目 3 个,编写测试报告', '获实习优秀评价', ['实习', '安全'], ['职业发展'],
|
||||
{ position: '渗透测试实习生', mentor: '陈工' }, {}]
|
||||
];
|
||||
// 演示个人档案(虚构人设:云川理工学院 · 信息安全 · 林晓阳)
|
||||
export const DEMO_PROFILE = {
|
||||
info: {
|
||||
name: '林晓阳', gender: '男', birth: '2004-06', nation: '汉族', native: '四川成都',
|
||||
political: '共青团员', school: '云川理工学院', college: '信息工程学院', major: '信息安全',
|
||||
className: '信安2201', studentId: '2023110245', degree: '本科', eduLength: '四年',
|
||||
eduStart: '2023-09', eduEnd: '2027-06',
|
||||
majorCourses: '计算机网络、数据库技术、操作系统、信息安全导论、密码学基础、网络安全、云计算与虚拟化技术',
|
||||
phone: '138****8000', email: 'linxy.demo@example.com', homepage: 'https://blog.example.com/linxy',
|
||||
photoUrl: '', selfIntro: '性格沉稳踏实,做事有计划性,习惯提前预判风险并准备预案;对网络安全方向有持续热情,动手能力强,能独立完成从方案设计到落地验证的完整流程;责任心强,注重细节与条理,是可靠的技术执行者。'
|
||||
}
|
||||
};
|
||||
|
||||
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点][] }
|
||||
export const DEMO_SEMESTERS = [
|
||||
{ term: '2023-2024-1', gpa: 3.42, rank: 38, total: 156, courses: [
|
||||
@@ -1173,6 +1186,8 @@ function listFootprints(d, query) {
|
||||
const fpCnt = d.prepare('SELECT COUNT(*) AS c FROM footprint WHERE deleted_at IS NULL').get().c;
|
||||
const semCnt = d.prepare('SELECT COUNT(*) AS c FROM semester WHERE deleted_at IS NULL').get().c;
|
||||
if (fpCnt > 0 || semCnt > 0) return { seeded: false, footprints: fpCnt, semesters: semCnt };
|
||||
// 个人档案(简历与个人信息页展示用)
|
||||
handleCommand('profile_upsert', { content: DEMO_PROFILE });
|
||||
const typeIds = Object.fromEntries(d.prepare('SELECT id, name FROM footprint_type').all().map((r) => [r.name, r.id]));
|
||||
const dimIds = Object.fromEntries(d.prepare('SELECT id, name FROM dimension').all().map((r) => [r.name, r.id]));
|
||||
let n = 0;
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AppShell() {
|
||||
<Sidebar open={navOpen} onClose={() => setNavOpen(false)} />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<Header onMenu={() => setNavOpen(true)} />
|
||||
<main className="min-h-0 flex-1 overflow-y-auto">
|
||||
<main className="app-scroll min-h-0 flex-1 overflow-y-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
+62
-60
@@ -23,14 +23,14 @@ export const RESUME_SECTION_DEFS: ResumeSectionDef[] = [
|
||||
{ key: 'self', defaultTitle: '自我评价', kind: 'text', fixed: true },
|
||||
];
|
||||
|
||||
// 首次使用种子内容(IT/网安方向示例,可自由修改)
|
||||
// 首次使用种子内容(虚构示例,可自由修改)
|
||||
const SEED: Record<string, { text?: string; items?: { title: string; subtitle?: string; date?: string; desc?: string }[] }> = {
|
||||
skills: { text: '路由交换:熟悉TCP/IP协议栈与基础路由交换,掌握VLAN、链路聚合、MSTP、VRRP/VRRP6等园区网冗余技术,熟悉OSPF、RIP、BGP等动态路由协议,具备IPv4/IPv6双栈网络的规划与部署能力。\n网络安全:熟悉防火墙安全区域、ACL访问控制与安全策略配置,掌握NAPT、NAT Server、NAT64地址转换,了解GRE over IPsec隧道与防火墙双机热备。\n网络设备:熟悉华为交换机、路由器、防火墙CLI操作与配置,掌握GVRP、MSTP、VRRP、链路聚合及BFD联动快速收敛,熟悉Wireshark抓包分析。\n运维基础:熟悉RedHat、Ubuntu等Linux系统部署与日常维护,了解Docker、K8s、Prometheus等云原生运维工具。' },
|
||||
skills: { text: '程序设计:熟悉 Python 与 C/C++,掌握常用数据结构与算法,能独立完成中小型工具与脚本开发。\n网络安全:熟悉常见 Web 漏洞原理(SQL 注入、XSS、CSRF 等)与防护方案,掌握 Burp Suite、Nmap、Wireshark 等常用工具。\n网络基础:熟悉 TCP/IP 协议栈与交换路由基础配置,理解 VLAN、NAT、ACL 等核心技术。\n运维基础:熟悉 Linux 常用操作与 Shell 脚本,了解 Docker 容器化部署与 Nginx 服务配置。' },
|
||||
campus: { items: [
|
||||
{ title: '学校图书馆 勤工俭学固定岗', subtitle: '图书管理员', date: '2024.12 - 2026.7', desc: '负责图书上架、整理和维护,定时进行馆内安全巡检,协助读者检索和借阅服务,累计服务读者超2000人次,零服务投诉。这段经历培养了高度的责任意识与服务意识,锻炼了高效执行和多任务时间管理能力。' },
|
||||
{ title: '学院团总支学生会宣传部 干事', subtitle: '视觉设计', date: '2023.9 - 2024.9', desc: '独立完成学院活动海报、邀请函、KT板、易拉宝等40余份宣传物料设计,参与换届大会、迎新、毕业季等重大活动视觉设计与宣传品制作。负责报告厅后台多媒体设备控制,保障12场大型活动视觉呈现零失误,锻炼了应急处置能力与谨慎细致的工作态度。' },
|
||||
{ title: '校图书馆学生助理', subtitle: '读者服务岗', date: '2024.9 - 2025.6', desc: '负责书库整理与借阅服务,协助开展入馆培训与阅读推广活动,累计服务读者千余人次。培养了责任意识与服务意识,锻炼了多任务时间管理能力。' },
|
||||
{ title: '院学生会宣传部干事', subtitle: '平面设计与文案', date: '2023.9 - 2024.6', desc: '参与学院活动的海报设计与推文撰写,负责活动现场的后勤与设备保障。锻炼了审美表达、协作沟通与临场应变能力。' },
|
||||
]},
|
||||
self: { text: '心态沉稳、抗压能力较好,遇到突发状况能冷静应对、稳住局面。做事有计划性,习惯提前预判风险并做好预案,追求稳妥与确定性,执行质量有保障,能在压力下保持稳定输出。责任心强,凡事有始有终,注重细节与条理,能在复杂环境中稳步推进工作,是一名可靠、值得信赖的执行者。' },
|
||||
self: { text: '性格沉稳踏实,做事有计划性,习惯提前预判风险并准备预案;学习能力强,能快速上手新技术与工具;责任心强,注重细节与条理,凡事有始有终,能独立推进任务并在压力下保持稳定输出。' },
|
||||
};
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ export const TEMPLATES: { key: ResumeTemplate; name: string; desc: string }[] =
|
||||
|
||||
export const ACCENT_CHOICES = ['#3056d3', '#0f766e', '#7c3aed', '#c2410c', '#0e7490', '#1f2937'];
|
||||
|
||||
// ---------- Word 导出(内联样式,按模板着色) ----------
|
||||
// ---------- Word 导出(按预览布局生成,Word/WPS 直接打开) ----------
|
||||
|
||||
const esc = (t: unknown) => String(t ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
@@ -171,73 +171,75 @@ export function resumeToWordHtml(
|
||||
const scale = cfg.fontScale;
|
||||
const pt = (base: number) => (base * scale).toFixed(1) + 'pt';
|
||||
const accent = cfg.accent;
|
||||
const titleStyle: Record<ResumeTemplate, string> = {
|
||||
classic: 'font-size:' + pt(14) + ';border-bottom:1.5pt solid #333;padding-bottom:3pt;margin:12pt 0 6pt;color:#111',
|
||||
tech: 'font-size:' + pt(14) + ';color:' + accent + ';border-left:3pt solid ' + accent + ';padding-left:6pt;margin:12pt 0 6pt',
|
||||
minimal: 'font-size:' + pt(11.5) + ';color:#555;letter-spacing:1.5pt;text-transform:uppercase;margin:14pt 0 6pt',
|
||||
const P = profile;
|
||||
|
||||
// 模板标题样式
|
||||
const titleCss: Record<ResumeTemplate, string> = {
|
||||
classic: 'font-size:' + pt(14) + ';color:#111;border-bottom:1.5pt solid #333;padding-bottom:3pt;margin:14pt 0 7pt',
|
||||
tech: 'font-size:' + pt(14) + ';color:' + accent + ';border-bottom:1pt solid ' + accent + ';padding-bottom:3pt;margin:14pt 0 7pt',
|
||||
minimal: 'font-size:' + pt(11.5) + ';color:#555;letter-spacing:1.5pt;text-transform:uppercase;border-bottom:0.75pt solid #e0e0e0;padding-bottom:3pt;margin:14pt 0 7pt',
|
||||
};
|
||||
|
||||
// 头部:姓名 + 校园信息 + 联系方式(左) + 证件照(右)
|
||||
const nameColor = cfg.template === 'tech' ? accent : '#111';
|
||||
const schoolLine = [P.school, P.college, P.major, P.degree].filter(v => v !== '').map(v => esc(v)).join(' · ');
|
||||
const contactLine = [P.phone, P.email, P.homepage].filter(v => v !== '').map(v => esc(v)).join(' · ');
|
||||
const photo = photoUrl && P.photoUrl
|
||||
? '<td style="width:' + (90 * scale).toFixed(0) + 'pt;text-align:right;vertical-align:top">' +
|
||||
'<img src="' + esc(P.photoUrl) + '" alt="证件照" style="width:' + (90 * scale).toFixed(0) + 'pt;height:' + (120 * scale).toFixed(0) + 'pt;object-fit:cover" /></td>'
|
||||
: '';
|
||||
|
||||
const head = '<table style="width:100%;border-collapse:collapse"><tr>' +
|
||||
'<td style="vertical-align:top">' +
|
||||
'<div style="font-size:' + pt(20) + ';font-weight:bold;color:' + nameColor + '">' + esc(P.name || '个人简历') + '</div>' +
|
||||
(schoolLine ? '<div style="color:#555;font-size:' + pt(11) + ';margin-top:3pt">' + schoolLine + '</div>' : '') +
|
||||
(contactLine ? '<div style="color:#555;font-size:' + pt(11) + ';margin-top:2pt">' + contactLine + '</div>' : '') +
|
||||
'</td>' + photo + '</tr></table>';
|
||||
|
||||
// 条目:标题(左) + 日期(右) 表格行 + 副标题 + 描述
|
||||
const itemHtml = (it: { title: string; subtitle?: string; date?: string; desc?: string }) =>
|
||||
'<table style="width:100%;border-collapse:collapse;margin-top:' + (cfg.density === 'compact' ? 4 : 7) + 'pt"><tr>' +
|
||||
'<td style="font-weight:bold;font-size:' + pt(12) + '">' + esc(it.title) + '</td>' +
|
||||
'<td style="text-align:right;color:#666;font-size:' + pt(10.5) + ';white-space:nowrap">' + esc(it.date ?? '') + '</td></tr></table>' +
|
||||
(it.subtitle ? '<div style="color:#666;font-size:' + pt(10.5) + '">' + esc(it.subtitle) + '</div>' : '') +
|
||||
(it.desc ? '<div style="color:#444;font-size:' + pt(11) + ';line-height:1.55;margin-top:2pt">' + esc(it.desc) + '</div>' : '');
|
||||
|
||||
const secHtml = sections.map(sec => {
|
||||
let inner = '';
|
||||
if (sec.kind === 'info') {
|
||||
if (sec.kind === 'edu') {
|
||||
const rows: [string, string][] = ([
|
||||
['姓名', P0(profile).name], ['性别', P0(profile).gender], ['出生年月', P0(profile).birth],
|
||||
['民族', P0(profile).nation], ['籍贯', P0(profile).native], ['政治面貌', P0(profile).political],
|
||||
['学校', P0(profile).school], ['学院', P0(profile).college], ['专业', P0(profile).major],
|
||||
['班级', P0(profile).className], ['学号', P0(profile).studentId], ['学历', P0(profile).degree],
|
||||
['手机', P0(profile).phone], ['邮箱', P0(profile).email],
|
||||
['学校', P.school ?? ''], ['专业', P.major ?? ''], ['学制', P.eduLength ?? ''],
|
||||
['学历', P.degree ?? ''], ['入学', P.eduStart ?? ''], ['毕业', P.eduEnd ?? ''],
|
||||
] as [string, string][]).filter(r => r[1] !== '');
|
||||
inner = '<table style="width:100%;border-collapse:collapse;font-size:' + pt(12) + '">' +
|
||||
rows.map(([k, v], i) =>
|
||||
rows.map(kv =>
|
||||
'<tr><td style="width:80px;padding:3px 6px;color:#555">' + esc(kv[0]) + '</td>' +
|
||||
'<td style="padding:3px 6px">' + esc(kv[1]) + '</td></tr>').join('') + '</table>' +
|
||||
(P.majorCourses ? '<div style="margin-top:6pt;font-size:' + pt(11.5) + ';line-height:1.6"><b>主修课程:</b>' + esc(P.majorCourses) + '</div>' : '');
|
||||
} else if (sec.kind === 'info') {
|
||||
const rows: [string, string][] = ([
|
||||
['姓名', P.name ?? ''], ['性别', P.gender ?? ''], ['出生年月', P.birth ?? ''],
|
||||
['民族', P.nation ?? ''], ['籍贯', P.native ?? ''], ['政治面貌', P.political ?? ''],
|
||||
['班级', P.className ?? ''], ['学号', P.studentId ?? ''], ['学历', P.degree ?? ''],
|
||||
['手机', P.phone ?? ''], ['邮箱', P.email ?? ''],
|
||||
] as [string, string][]).filter(r => r[1] !== '');
|
||||
inner = '<table style="width:100%;border-collapse:collapse;font-size:' + pt(12) + '">' +
|
||||
rows.map((kv, i) =>
|
||||
(i % 2 === 0 ? '<tr>' : '') +
|
||||
'<td style="width:80px;padding:3px 6px;color:#555">' + esc(k) + '</td>' +
|
||||
'<td style="padding:3px 6px">' + esc(v) + '</td>' +
|
||||
(i % 2 === 1 ? '</tr>' : '')
|
||||
).join('') + '</table>';
|
||||
} else if (sec.kind === 'edu') {
|
||||
const rows: [string, string][] = ([
|
||||
['学校', profile.school ?? ''], ['专业', profile.major ?? ''], ['学制', profile.eduLength ?? ''],
|
||||
['学历', profile.degree ?? ''], ['入学', profile.eduStart ?? ''], ['毕业', profile.eduEnd ?? ''],
|
||||
] as [string, string][]).filter(r => r[1] !== '');
|
||||
inner = '<table style="width:100%;border-collapse:collapse;font-size:' + pt(12) + '">' +
|
||||
rows.map(function (kv) {
|
||||
return '<tr><td style="width:80px;padding:3px 6px;color:#555">' + esc(kv[0]) + '</td>' +
|
||||
'<td style="padding:3px 6px">' + esc(kv[1]) + '</td></tr>';
|
||||
}).join('') + '</table>' +
|
||||
(profile.majorCourses ? '<div style="margin-top:6pt"><b style="font-size:' + pt(11) + '">主修课程:</b>' +
|
||||
'<span style="font-size:' + pt(11.5) + '">' + esc(profile.majorCourses) + '</span></div>' : '');
|
||||
'<td style="width:80px;padding:3px 6px;color:#555">' + esc(kv[0]) + '</td>' +
|
||||
'<td style="padding:3px 6px">' + esc(kv[1]) + '</td>' +
|
||||
(i % 2 === 1 ? '</tr>' : '')).join('') + '</table>';
|
||||
} else if (sec.kind === 'list') {
|
||||
inner = '<div style="margin-bottom:6pt">' + (sec.items ?? []).map(it => {
|
||||
const sub = it.subtitle ? '<div style="color:#666;font-size:' + pt(10.5) + '">' + esc(it.subtitle) + '</div>' : '';
|
||||
const desc = it.desc ? '<div style="color:#444;font-size:' + pt(11) + ';line-height:1.5">' + esc(it.desc) + '</div>' : '';
|
||||
return '<div style="margin-bottom:7pt">' +
|
||||
'<div style="display:flex;justify-content:space-between"><b style="font-size:' + pt(12) + '">' + esc(it.title) + '</b>' +
|
||||
'<span style="color:#666;font-size:' + pt(10.5) + '">' + esc(it.date ?? '') + '</span></div>' +
|
||||
sub + desc + '</div>';
|
||||
}).join('') + '</div>';
|
||||
inner = '<div>' + (sec.items ?? []).map(itemHtml).join('') + '</div>';
|
||||
} else {
|
||||
inner = '<p style="font-size:' + pt(12) + ';line-height:1.7;margin:0">' + esc(sec.text ?? '') + '</p>';
|
||||
inner = '<p style="font-size:' + pt(12) + ';line-height:1.7;margin:0">' + esc(sec.text ?? '').replace(/\n/g, '<br/>') + '</p>';
|
||||
}
|
||||
return '<h2 style="' + titleStyle[cfg.template] + '">' + esc(sec.title) + '</h2>' + inner;
|
||||
return '<div style="' + titleCss[cfg.template] + '">' + esc(sec.title) + '</div>' + inner;
|
||||
}).join('');
|
||||
|
||||
const photo = photoUrl && profile.photoUrl
|
||||
? '<div style="text-align:right"><img src="' + esc(profile.photoUrl) + '" alt="证件照" style="width:' + (90 * scale).toFixed(0) + 'pt;height:' + (120 * scale).toFixed(0) + 'pt;object-fit:cover" /></div>'
|
||||
: '';
|
||||
|
||||
const headSub = [P0(profile).school, P0(profile).college, P0(profile).major, P0(profile).degree].filter(v => v !== '').map(v => esc(v)).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>' + esc(P0(profile).name || '个人简历') + '</title></head>' +
|
||||
'<body style="font-family:SimSun,Calibri,sans-serif;margin:36pt;color:#222">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:flex-start">' +
|
||||
'<h1 style="font-size:' + pt(20) + ';margin:0 0 4pt;color:' + (cfg.template === 'tech' ? accent : '#111') + '">' + esc(P0(profile).name || '个人简历') + '</h1>' + photo + '</div>' +
|
||||
(headSub ? '<div style="color:#555;font-size:' + pt(11) + ';margin-bottom:8pt">' + headSub + '</div>' : '') +
|
||||
secHtml +
|
||||
'</body></html>';
|
||||
'<head><meta charset="utf-8"><title>' + esc(P.name || '个人简历') + '</title>' +
|
||||
'<!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View></w:WordDocument></xml><![endif]--></head>' +
|
||||
'<body style="font-family:Calibri,SimSun,sans-serif;margin:36pt;color:#222">' + head + secHtml + '</body></html>';
|
||||
}
|
||||
|
||||
// 避免循环引用的小工具
|
||||
function P0(p: Partial<ProfileInfo> | undefined): Partial<ProfileInfo> {
|
||||
return p ?? {};
|
||||
}
|
||||
|
||||
@@ -157,12 +157,15 @@ input[type='number'] { -moz-appearance: textfield; appearance: textfield; }
|
||||
}
|
||||
|
||||
/* ---------- 简历打印 / 导出 ---------- */
|
||||
@page { size: A4; margin: 10mm; }
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
html, body, #root { height: auto !important; background: #fff !important; }
|
||||
body { padding: 0 !important; }
|
||||
.print-scroll { overflow: visible !important; height: auto !important; }
|
||||
.print-sheet { border: none !important; box-shadow: none !important; margin: 0 !important; max-width: none !important; width: 100% !important; padding: 0 !important; }
|
||||
.app-scroll, .print-scroll { overflow: visible !important; height: auto !important; max-height: none !important; }
|
||||
.print-sheet { border: none !important; box-shadow: none !important; margin: 0 !important; max-width: none !important; width: 100% !important; padding: 0 !important; border-radius: 0 !important; }
|
||||
* { scrollbar-width: none !important; }
|
||||
*::-webkit-scrollbar { width: 0 !important; height: 0 !important; display: none !important; }
|
||||
}
|
||||
|
||||
/* ---------- 简历模板 ---------- */
|
||||
|
||||
Reference in New Issue
Block a user