Files
freedom-web/public/resume.html
T
张家训 8337e4223c
CI/CD Deploy (Web) / 构建前端镜像并部署 (push) Successful in 21s
feat(resume): 新增个人能力图/简历页单文件(resume.html)+域名server块
2026-10-23 16:42:30 +08:00

576 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#4f6ef7">
<title>张家训 · 个人能力图 / 简历</title>
<style>
/* ===== 主题变量(浅色靛蓝系,与 freedom 平台主色同源) ===== */
:root {
--indigo: #4f6ef7;
--indigo-2: #7c5cf0;
--ink: #161d33;
--text: #2b3448;
--muted: #6b7690;
--line: #e4e8f2;
--line-strong: #d3d9e8;
--surface: #ffffff;
--surface-2: #f6f8fc;
--surface-3: #eef2fa;
--accent-soft: rgba(79, 110, 247, .1);
--radius: 14px;
--shadow-sm: 0 1px 3px rgba(20, 27, 48, .06);
--shadow-md: 0 6px 20px rgba(20, 27, 48, .09);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--surface-2);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", sans-serif;
font-size: 14px; line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3 { margin: 0; color: var(--ink); }
p { margin: 0; }
/* ===== 页面骨架 ===== */
.page { max-width: 940px; margin: 0 auto; padding: 28px 20px 56px; }
.card {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
box-shadow: var(--shadow-sm); padding: 22px 24px; margin-bottom: 18px;
}
.sec-title {
display: flex; align-items: center; gap: 9px;
font-size: 15.5px; font-weight: 700; letter-spacing: .3px; margin-bottom: 16px;
}
.sec-title::before {
content: ''; width: 4px; height: 16px; border-radius: 2px;
background: linear-gradient(180deg, var(--indigo), var(--indigo-2));
}
/* ===== 页头 ===== */
.hero { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.avatar {
width: 78px; height: 78px; flex-shrink: 0; border-radius: 20px;
background: linear-gradient(135deg, var(--indigo), var(--indigo-2));
color: #fff; font-size: 30px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 6px 18px rgba(79, 110, 247, .32);
}
.hero-main { flex: 1; min-width: 240px; }
.hero-name { font-size: 26px; font-weight: 800; letter-spacing: .5px; }
.hero-role { font-size: 14.5px; color: var(--indigo); font-weight: 600; margin-top: 2px; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 8px; color: var(--muted); font-size: 13px; }
.hero-actions { display: flex; gap: 10px; align-items: center; }
.btn {
border: 1px solid var(--line-strong); background: var(--surface); color: var(--text);
padding: 8px 16px; border-radius: 10px; font-size: 13.5px; cursor: pointer;
font-family: inherit; transition: all .18s; display: inline-flex; align-items: center; gap: 7px;
}
.btn:hover { border-color: var(--indigo); color: var(--indigo); }
.btn-primary {
background: linear-gradient(135deg, var(--indigo), var(--indigo-2));
border-color: transparent; color: #fff; font-weight: 600;
box-shadow: 0 4px 12px rgba(79, 110, 247, .28);
}
.btn-primary:hover { color: #fff; filter: brightness(1.06); }
/* ===== 能力雷达图 ===== */
.radar-wrap { display: flex; gap: 26px; align-items: center; flex-wrap: wrap; }
.radar-box { flex: 0 0 320px; max-width: 100%; }
.radar-note { flex: 1; min-width: 260px; }
.radar-note p { color: var(--text); margin-bottom: 10px; }
.radar-note .tags { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
.tag {
background: var(--accent-soft); color: var(--indigo); border-radius: 20px;
padding: 3px 11px; font-size: 12.5px; font-weight: 600;
}
svg.radar { width: 100%; height: auto; display: block; }
.radar-grid { fill: none; stroke: var(--line-strong); stroke-width: 1; }
.radar-axis { stroke: var(--line); stroke-width: 1; }
.radar-area {
fill: rgba(79, 110, 247, .22); stroke: var(--indigo); stroke-width: 2; stroke-linejoin: round;
}
.radar-dot { fill: #fff; stroke: var(--indigo); stroke-width: 2; }
.radar-label { fill: var(--text); font-size: 12px; font-weight: 600; }
.radar-score { fill: var(--muted); font-size: 10.5px; }
/* ===== 技能栈 ===== */
.skill-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px 26px; }
.skill-cat h4 {
margin: 0 0 10px; font-size: 13.5px; font-weight: 700; color: var(--ink);
display: flex; align-items: center; gap: 7px;
}
.skill-cat h4 .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--indigo); }
.skill-row { margin-bottom: 9px; }
.skill-row .top { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 4px; }
.skill-row .name { color: var(--text); font-weight: 500; }
.skill-row .lv { color: var(--muted); font-size: 12px; }
.bar { height: 6px; background: var(--surface-3); border-radius: 4px; overflow: hidden; }
.bar > i {
display: block; height: 100%; border-radius: 4px; width: 0;
background: linear-gradient(90deg, var(--indigo), var(--indigo-2));
transition: width .9s cubic-bezier(.2, .8, .3, 1);
}
/* ===== 时间线 ===== */
.timeline { position: relative; padding-left: 22px; }
.timeline::before {
content: ''; position: absolute; left: 5px; top: 6px; bottom: 6px; width: 2px;
background: linear-gradient(180deg, var(--indigo), var(--line));
}
.tl-item { position: relative; padding-bottom: 18px; break-inside: avoid; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item::before {
content: ''; position: absolute; left: -22px; top: 7px; width: 12px; height: 12px;
border-radius: 50%; background: #fff; border: 2px solid var(--indigo);
}
.tl-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.tl-title { font-size: 14.5px; font-weight: 700; color: var(--ink); }
.tl-role { font-size: 13px; color: var(--indigo); font-weight: 600; }
.tl-time { font-size: 12.5px; color: var(--muted); margin-left: auto; }
.tl-desc { margin-top: 6px; color: var(--text); }
.tl-points { margin: 8px 0 0; padding-left: 18px; }
.tl-points li { margin-bottom: 4px; }
.tl-stack { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
background: var(--surface-3); color: var(--muted); border-radius: 6px;
padding: 2px 8px; font-size: 12px; font-family: ui-monospace, Menlo, Consolas, monospace;
}
.todo-tip {
background: #fff8e6; border: 1px dashed #f0c674; color: #92620a;
border-radius: 10px; padding: 10px 14px; font-size: 13px; margin-bottom: 14px;
}
.foot { text-align: center; color: var(--muted); font-size: 12.5px; margin-top: 26px; }
/* ===== 提示条 ===== */
.toast {
position: fixed; left: 50%; bottom: 34px; transform: translate(-50%, 20px);
background: var(--ink); color: #fff; padding: 10px 20px; border-radius: 10px;
font-size: 13.5px; opacity: 0; pointer-events: none; transition: all .25s; z-index: 99;
}
.toast.on { opacity: 1; transform: translate(-50%, 0); }
/* ===== 响应式 ===== */
@media (max-width: 640px) {
.page { padding: 18px 14px 40px; }
.card { padding: 18px; }
.hero { gap: 16px; }
.avatar { width: 62px; height: 62px; font-size: 24px; border-radius: 16px; }
.hero-name { font-size: 22px; }
.radar-box { flex: 1 1 100%; }
.tl-time { margin-left: 0; }
}
/* ===== 打印(A4 投递) ===== */
@media print {
@page { size: A4; margin: 12mm; }
body { background: #fff; font-size: 12.5px; }
.page { max-width: 100%; padding: 0; }
.card { border-color: #dfe3ee; box-shadow: none; margin-bottom: 10px; padding: 14px 16px; }
.no-print, .toast { display: none !important; }
.tl-item, .skill-cat, .card { break-inside: avoid; }
.hero { gap: 14px; }
.bar > i { transition: none; }
a { color: inherit; text-decoration: none; }
}
</style>
</head>
<body>
<div class="page">
<!-- 页头 -->
<div class="card hero-card">
<div class="hero">
<div class="avatar" id="avatar">张</div>
<div class="hero-main">
<h1 class="hero-name" id="name">张家训</h1>
<div class="hero-role" id="role">Java 全栈开发工程师</div>
<div class="hero-meta" id="meta"></div>
</div>
<div class="hero-actions no-print">
<button class="btn btn-primary" id="exportBtn">导出 PDF</button>
</div>
</div>
</div>
<!-- 能力雷达图 -->
<div class="card">
<h2 class="sec-title">能力图</h2>
<div class="radar-wrap">
<div class="radar-box">
<svg class="radar" id="radar" viewBox="0 0 340 320"></svg>
</div>
<div class="radar-note" id="radarNote"></div>
</div>
</div>
<!-- 技能栈 -->
<div class="card">
<h2 class="sec-title">技能栈</h2>
<div class="skill-grid" id="skillGrid"></div>
</div>
<!-- 工作经历 -->
<div class="card">
<h2 class="sec-title">工作经历</h2>
<div id="workBox"></div>
</div>
<!-- 项目经历 -->
<div class="card">
<h2 class="sec-title">项目经历</h2>
<div class="timeline" id="projectList"></div>
</div>
<div class="foot" id="foot"></div>
</div>
<div class="toast" id="toast"></div>
<script>
/* ==================================================================
* 数据区:所有内容集中在这里,改这里即可,不用动下面的渲染代码
* 评分统一 0-100;时间用字符串,按你需要展示
* ================================================================== */
var DATA = {
profile: {
name: '张家训',
initial: '张', // 头像显示的字
role: 'Java 全栈开发工程师 · 工具与效率平台建设',
meta: [
'Gitea:gitea.jiaxun.site(yiciheng)',
'邮箱:待补充',
'电话:待补充',
'所在城市:待补充'
]
},
// 能力雷达:维度名 + 评分(0-100),建议 4-8 项
radar: {
dims: [
{ name: '后端开发', score: 92 },
{ name: '前端开发', score: 76 },
{ name: '数据库与建模', score: 82 },
{ name: '架构与工程化', score: 86 },
{ name: '运维与部署', score: 83 },
{ name: '工具与自动化', score: 90 }
],
summary: '以 Java 后端为主线,向上能独立交付前端页面,向下能自己搭 CI/CD 与服务器环境;' +
'擅长把重复劳动沉淀成工具(桌面工具平台、JAR 裁剪分析、本地语音转写工具),' +
'偏好多用工程化手段一次性解决问题。',
tags: ['Spring Boot', 'MyBatis-Plus', 'Vue2', 'Docker + CI/CD', 'JavaFX 桌面工具', 'Python 自动化']
},
// 技能栈:分类 + 每项熟练度(0-100)
skills: [
{
cat: '编程语言',
items: [
{ name: 'Java', lv: 95, note: '主力语言,熟悉 JDK 8 / 17 差异' },
{ name: 'Python', lv: 72, note: '自动化脚本与桌面小工具' },
{ name: 'JavaScript', lv: 74, note: '原生 JS + Vue2' },
{ name: 'SQL', lv: 80, note: '复杂查询、建模与迁移' }
]
},
{
cat: '后端框架',
items: [
{ name: 'Spring Boot', lv: 92, note: '2.7.x,含 Security + JWT 鉴权' },
{ name: 'MyBatis-Plus', lv: 88, note: '分页插件、自定义 SQL、代码生成' },
{ name: 'Dubbo', lv: 68, note: '接口文档梳理与调用链路分析' },
{ name: 'Maven / 多模块', lv: 80, note: '依赖治理与打包体积优化' }
]
},
{
cat: '前端与界面',
items: [
{ name: 'Vue2 + vue-cli', lv: 80, note: '路由守卫、动态菜单、组件封装' },
{ name: 'view-design', lv: 78, note: '表格/表单/弹窗,含样式与交互定制' },
{ name: '原生 HTML/CSS/JS', lv: 86, note: '零依赖单文件页面(看板、能力图)' },
{ name: 'JavaFX', lv: 82, note: '桌面工具界面与响应式布局' }
]
},
{
cat: '数据与文件',
items: [
{ name: 'MySQL 8', lv: 85, note: '索引、时区与字符集坑位排查' },
{ name: 'EasyExcel / POI', lv: 80, note: '大批量导出与模板填充' },
{ name: '数据建模', lv: 78, note: '权限模型、状态机表设计' }
]
},
{
cat: '工程化 · 运维',
items: [
{ name: 'Docker / Compose', lv: 85, note: '自建服务编排与容器排障' },
{ name: 'Nginx / OpenResty', lv: 78, note: '反向代理、多域名、gzip' },
{ name: 'Gitea + Actions CI/CD', lv: 84, note: 'push master 自动构建部署' },
{ name: 'frp 内网穿透', lv: 75, note: '公网入口与连接池调优' },
{ name: 'Linux(Ubuntu)', lv: 80, note: '1Panel 运维、日志与容器排障' }
]
},
{
cat: '自动化与工具',
items: [
{ name: 'launch4j / jlink', lv: 76, note: '桌面应用打包与体积裁剪' },
{ name: 'faster-whisper 本地 ASR', lv: 66, note: '语音转写 + 规则引擎增强' },
{ name: 'PyQt6 / sounddevice', lv: 68, note: '悬浮窗与音频采集' },
{ name: 'AI 辅助工程实践', lv: 85, note: '用大模型提效开发与排障' }
]
}
],
// 工作经历(任职信息待补充)
work: [
{
company: '(公司名待补充)',
role: 'Java 开发工程师',
time: '(起止时间待补充)',
desc: '负责业务系统开发与维护,参与需求拆解、编码实现与线上问题排查。',
points: [
'(请补充:核心职责与量化成果,例如承接哪个系统、日/月处理量级、性能提升幅度)',
'(请补充:团队角色,如带几个人、负责哪块模块)'
]
}
],
// 项目经历
projects: [
{
name: 'freedom 管理系统平台',
role: '全栈(独立开发)',
time: '2026',
desc: '自用的管理系统平台,前后端分离,覆盖系统管理与待办场景,部署在家里的服务器并经内网穿透对外提供服务。',
points: [
'后端 Spring Boot + MyBatis-Plus,实现用户/角色/权限/部门体系,权限统一由 JWT 解析,不依赖前端传参',
'前端 Vue2 + view-design,动态菜单、路由守卫、标签页导航',
'自建 Gitea + Actions 流水线,push master 自动构建镜像并部署,前后端各自独立发布',
'登录页首屏 JS 从 4.5MB 优化到 248KB(gzip + 清理无用依赖),公网加载约 2s',
'清理历史冗余模块(改枪码、kpi 等)与零使用依赖,保持代码库精简'
],
stack: ['Spring Boot', 'MyBatis-Plus', 'Vue2', 'view-design', 'Docker', 'Gitea Actions', 'Nginx']
},
{
name: '公开待办看板',
role: '全栈(独立开发)',
time: '2026',
desc: '免登录对外分享的待办看板,单文件零依赖页面,支持富文本备注与主题切换。',
points: [
'纯原生实现自绘下拉与日历组件,避开原生控件无法美化的问题',
'自研轻量富文本编辑器(contenteditable + execCommand),保存与渲染双次白名单净化防存储型 XSS',
'DATE 字段改用字符串映射,根治 JDBC 时区换算导致的日期差一天问题',
'全操作加 loading 与防重复提交,弱网下弱感知延迟'
],
stack: ['原生 JS', 'CSS 变量主题', 'Spring Boot', 'MySQL']
},
{
name: 'WorkUtils 桌面工具平台',
role: '全栈(独立开发)',
time: '2026',
desc: '基于 Spring Boot + JavaFX 的桌面辅助工具,插件化承载多个开发提效能力。',
points: [
'平台层与业务层分离,内置 ApiDoc、Scanner、MyBatis 导出、Diff、GitSync 等插件',
'解决 Windows .git 只读属性导致删除失败、readLine 阻塞 waitFor 等并发与文件系统问题',
'用 launch4j 打包为独立 EXE(内嵌 JRE),依赖裁剪后体积从 88.6MB 降到 61.5MB(-30.6%)',
'修复多项 UI 响应式问题(导航栏自适应、弹窗空白、字体跟随系统)'
],
stack: ['Java 17', 'JavaFX', 'Spring Boot', 'EasyExcel', 'launch4j', 'jlink']
},
{
name: '《三角洲行动》报点增强器',
role: '独立开发',
time: '2026',
desc: '游戏语音报点的实时增强工具:热键录音 → 本地语音识别 → 规则引擎增强 → 悬浮窗展示。',
points: [
'CapsLock 全局热键触发录音,faster-whisper 本地 ASR 转写,无需联网',
'规则引擎做术语与别名互认,内置地图知识库',
'PyQt6 无边框悬浮窗展示结果,低干扰'
],
stack: ['Python', 'faster-whisper', 'sounddevice', 'PyQt6']
},
{
name: 'ApiTrimmer / 拨款状态机重构',
role: '独立开发',
time: '2026',
desc: '面向既有代码的分析与重构类工作。',
points: [
'ApiTrimmer:分析 JAR 包依赖与裁剪空间的工具',
'拨款(disburse)状态机模块向决策表驱动架构转型,降低分支维护成本',
'输出 Dubbo 接口文档与代码调用链路,支撑存量系统梳理'
],
stack: ['Java', 'Dubbo', '状态机', '决策表']
}
],
foot: '本页为单文件静态页面,数据集中在 HTML 顶部的 DATA 区,修改后刷新即可生效'
};
/* ==================== 渲染逻辑(一般无需修改) ==================== */
var $ = function (id) { return document.getElementById(id) };
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
/* ---------- 页头 ---------- */
function renderProfile() {
var p = DATA.profile;
$('avatar').textContent = p.initial || (p.name || ' ').charAt(0);
$('name').textContent = p.name;
$('role').textContent = p.role;
$('meta').innerHTML = (p.meta || []).map(function (m) {
return '<span>' + esc(m) + '</span>';
}).join('');
}
/* ---------- 雷达图(纯 SVG 自绘,不引图表库) ---------- */
function renderRadar() {
var dims = DATA.radar.dims;
var cx = 170, cy = 150, r = 105, n = dims.length;
var svg = $('radar');
var parts = [];
// 背景网格:4 层同心多边形
for (var g = 4; g >= 1; g--) {
var pts = [];
for (var i = 0; i < n; i++) {
var a = -Math.PI / 2 + i * 2 * Math.PI / n;
var rr = r * g / 4;
pts.push((cx + rr * Math.cos(a)).toFixed(1) + ',' + (cy + rr * Math.sin(a)).toFixed(1));
}
parts.push('<polygon class="radar-grid" points="' + pts.join(' ') + '"/>');
}
// 轴线与标签
for (var j = 0; j < n; j++) {
var ang = -Math.PI / 2 + j * 2 * Math.PI / n;
var ex = cx + r * Math.cos(ang), ey = cy + r * Math.sin(ang);
parts.push('<line class="radar-axis" x1="' + cx + '" y1="' + cy + '" x2="' + ex.toFixed(1) + '" y2="' + ey.toFixed(1) + '"/>');
// 标签位置:按角度决定对齐方式,避免贴在边上
var lx = cx + (r + 26) * Math.cos(ang), ly = cy + (r + 26) * Math.sin(ang);
var anchor = Math.abs(Math.cos(ang)) < 0.2 ? 'middle' : (Math.cos(ang) > 0 ? 'start' : 'end');
parts.push('<text class="radar-label" x="' + lx.toFixed(1) + '" y="' + (ly + 4).toFixed(1) + '" text-anchor="' + anchor + '">' +
esc(dims[j].name) + '</text>');
parts.push('<text class="radar-score" x="' + lx.toFixed(1) + '" y="' + (ly + 18).toFixed(1) + '" text-anchor="' + anchor + '">' +
dims[j].score + '</text>');
}
// 数据区多边形 + 顶点
var vals = [], dots = [];
for (var k = 0; k < n; k++) {
var ak = -Math.PI / 2 + k * 2 * Math.PI / n;
var rk = r * (dims[k].score / 100);
var px = cx + rk * Math.cos(ak), py = cy + rk * Math.sin(ak);
vals.push(px.toFixed(1) + ',' + py.toFixed(1));
dots.push('<circle class="radar-dot" cx="' + px.toFixed(1) + '" cy="' + py.toFixed(1) + '" r="3.5"/>');
}
parts.push('<polygon class="radar-area" points="' + vals.join(' ') + '"/>');
parts.push(dots.join(''));
svg.innerHTML = parts.join('');
// 右侧概述
var note = DATA.radar;
$('radarNote').innerHTML =
'<p>' + esc(note.summary) + '</p>' +
'<div class="tags">' + (note.tags || []).map(function (t) {
return '<span class="tag">' + esc(t) + '</span>';
}).join('') + '</div>';
}
/* ---------- 技能栈 ---------- */
function renderSkills() {
$('skillGrid').innerHTML = DATA.skills.map(function (cat) {
var rows = cat.items.map(function (it) {
return '<div class="skill-row">'
+ '<div class="top"><span class="name">' + esc(it.name) + '</span>'
+ '<span class="lv">' + (it.note ? esc(it.note) + ' · ' : '') + it.lv + '</span></div>'
+ '<div class="bar"><i data-w="' + it.lv + '"></i></div>'
+ '</div>';
}).join('');
return '<div class="skill-cat"><h4><span class="dot"></span>' + esc(cat.cat) + '</h4>' + rows + '</div>';
}).join('');
// 进度条动画:下一帧再赋宽度才有过渡效果
requestAnimationFrame(function () {
Array.prototype.forEach.call(document.querySelectorAll('.bar > i'), function (el) {
el.style.width = el.dataset.w + '%';
});
});
}
/* ---------- 工作经历 ---------- */
function renderWork() {
var pending = DATA.work.some(function (w) { return /待补充/.test(w.company + w.time) });
var tip = pending
? '<div class="todo-tip no-print">任职信息(公司、起止时间、量化成果)待补充,改 DATA.work 即可;打印时本提示不会输出。</div>'
: '';
$('workBox').innerHTML = tip + '<div class="timeline">' + DATA.work.map(function (w) {
return '<div class="tl-item">'
+ '<div class="tl-head"><span class="tl-title">' + esc(w.company) + '</span>'
+ '<span class="tl-role">' + esc(w.role) + '</span>'
+ '<span class="tl-time">' + esc(w.time) + '</span></div>'
+ '<div class="tl-desc">' + esc(w.desc) + '</div>'
+ '<ul class="tl-points">' + (w.points || []).map(function (p) {
return '<li>' + esc(p) + '</li>';
}).join('') + '</ul>'
+ (w.stack ? '<div class="tl-stack">' + w.stack.map(function (s) {
return '<span class="chip">' + esc(s) + '</span>';
}).join('') + '</div>' : '')
+ '</div>';
}).join('') + '</div>';
}
/* ---------- 项目经历 ---------- */
function renderProjects() {
$('projectList').innerHTML = DATA.projects.map(function (p) {
return '<div class="tl-item">'
+ '<div class="tl-head"><span class="tl-title">' + esc(p.name) + '</span>'
+ '<span class="tl-role">' + esc(p.role) + '</span>'
+ '<span class="tl-time">' + esc(p.time) + '</span></div>'
+ '<div class="tl-desc">' + esc(p.desc) + '</div>'
+ '<ul class="tl-points">' + (p.points || []).map(function (x) {
return '<li>' + esc(x) + '</li>';
}).join('') + '</ul>'
+ '<div class="tl-stack">' + p.stack.map(function (s) {
return '<span class="chip">' + esc(s) + '</span>';
}).join('') + '</div>'
+ '</div>';
}).join('');
}
/* ---------- 提示与导出 ---------- */
var toastTimer = null;
function toast(msg) {
var el = $('toast');
el.textContent = msg;
el.classList.add('on');
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.classList.remove('on') }, 2200);
}
$('exportBtn').addEventListener('click', function () {
toast('已唤起打印,目标选择「另存为 PDF」即可');
setTimeout(function () { window.print() }, 320);
});
/* ---------- 启动 ---------- */
renderProfile();
renderRadar();
renderSkills();
renderWork();
renderProjects();
$('foot').textContent = DATA.foot;
document.title = DATA.profile.name + ' · 个人能力图 / 简历';
</script>
</body>
</html>