From 2a866a2ad4e8e8468e87234fea3ddf6c14839e88 Mon Sep 17 00:00:00 2001 From: iorebuild Date: Sun, 7 Jun 2026 17:43:52 +0800 Subject: [PATCH] =?UTF-8?q?Add=20interactive=20resume=20builder=20?= =?UTF-8?q?=E2=80=94=2024=20colour=20themes=20=C3=97=2012=20layout=20prese?= =?UTF-8?q?ts,=20editable,=20print-to-PDF,=20XML=20import/export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resume/app.js | 564 +++++++++++++++++++++++++++++++++++++++ resume/index.html | 148 +++++++++++ resume/style.css | 665 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1377 insertions(+) create mode 100644 resume/app.js create mode 100644 resume/index.html create mode 100644 resume/style.css diff --git a/resume/app.js b/resume/app.js new file mode 100644 index 0000000..9ea697d --- /dev/null +++ b/resume/app.js @@ -0,0 +1,564 @@ +/** + * Resume — interactive resume builder + * 24 colour themes + 12 design/layout themes. All config in one XML. + */ + +const Resume = (() => { + 'use strict'; + + const STORAGE_KEY = 'resume_data'; + + // ============================================================ + // COLOUR THEMES — 24 presets + // ============================================================ + const COLOR_THEMES = [ + { name:'经典蓝', vars:{'--ink':'#1a1a1a','--ink-muted':'#555','--accent':'#1e5fa8','--accent-light':'#e8f0fa','--border':'#ddd','--bg':'#fff','--surface':'#f8f9fa','--body-bg':'#e8ecf1','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'暗夜黑', vars:{'--ink':'#e0e0e0','--ink-muted':'#999','--accent':'#5b9bd5','--accent-light':'#1e2a3a','--border':'#444','--bg':'#1e1e1e','--surface':'#2a2a2a','--body-bg':'#121212','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'墨绿', vars:{'--ink':'#1a2a1a','--ink-muted':'#5a7a5a','--accent':'#2d6a4f','--accent-light':'#d8f3dc','--border':'#b7c9b7','--bg':'#fafdfa','--surface':'#f0f7f0','--body-bg':'#dde8dd','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'酒红', vars:{'--ink':'#2a1a1a','--ink-muted':'#7a5555','--accent':'#8b1a2b','--accent-light':'#fce8ec','--border':'#d4b8b8','--bg':'#fefafa','--surface':'#faf0f0','--body-bg':'#ece0e0','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'石板灰', vars:{'--ink':'#1f1f1f','--ink-muted':'#666','--accent':'#4a4a4a','--accent-light':'#e8e8e8','--border':'#ccc','--bg':'#fafafa','--surface':'#f0f0f0','--body-bg':'#e0e0e0','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'琥珀暖', vars:{'--ink':'#2a2218','--ink-muted':'#7a6a50','--accent':'#b85c00','--accent-light':'#fff3e0','--border':'#d4c8b0','--bg':'#fffdf8','--surface':'#fff8ee','--body-bg':'#f0e8d8','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'青蓝', vars:{'--ink':'#1a2228','--ink-muted':'#5a7a88','--accent':'#0d7377','--accent-light':'#d4f4f5','--border':'#b8d4d6','--bg':'#f8fdfd','--surface':'#eef8f8','--body-bg':'#d8eaea','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'紫罗兰', vars:{'--ink':'#1f1a28','--ink-muted':'#6a5a88','--accent':'#6b3fa0','--accent-light':'#f0e8fc','--border':'#c8b8e0','--bg':'#fdfafe','--surface':'#f6eefc','--body-bg':'#e8daf0','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'极简黑白', vars:{'--ink':'#000','--ink-muted':'#555','--accent':'#222','--accent-light':'#eee','--border':'#ccc','--bg':'#fff','--surface':'#f8f8f8','--body-bg':'#f0f0f0','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'报纸风', vars:{'--ink':'#1a1a1a','--ink-muted':'#555','--accent':'#8b0000','--accent-light':'#fdf0f0','--border':'#ccc','--bg':'#fefef8','--surface':'#f8f8f0','--body-bg':'#e8e8d8','--font-sans':'"Noto Serif SC","Source Han Serif SC","SimSun",Georgia,serif'} }, + { name:'科技蓝', vars:{'--ink':'#e8f0ff','--ink-muted':'#8ab4f8','--accent':'#4da6ff','--accent-light':'#0d1b3e','--border':'#2a4480','--bg':'#0a1628','--surface':'#112240','--body-bg':'#060d1a','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'暖沙', vars:{'--ink':'#3a3028','--ink-muted':'#8a7a68','--accent':'#c07a40','--accent-light':'#fdf0e4','--border':'#d8c8b8','--bg':'#fefaf6','--surface':'#faf0e6','--body-bg':'#ede0d2','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'深海', vars:{'--ink':'#d0dce8','--ink-muted':'#7a96b0','--accent':'#5ec4d0','--accent-light':'#0f2838','--border':'#2a5068','--bg':'#0c1e2c','--surface':'#152a3c','--body-bg':'#06121c','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'日落橙', vars:{'--ink':'#2a2018','--ink-muted':'#8a6a48','--accent':'#e05530','--accent-light':'#fef0e8','--border':'#e0c8b0','--bg':'#fffaf6','--surface':'#fef4ea','--body-bg':'#f0e2d2','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'薄荷', vars:{'--ink':'#1a2822','--ink-muted':'#5a8a72','--accent':'#2eac7a','--accent-light':'#d8fcec','--border':'#b8d8c8','--bg':'#f8fefa','--surface':'#eefaf4','--body-bg':'#d8ece2','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'薰衣草', vars:{'--ink':'#2a2230','--ink-muted':'#8a7a98','--accent':'#8e6bb8','--accent-light':'#f4ecfc','--border':'#d8c8e8','--bg':'#fdf8ff','--surface':'#f8f0fc','--body-bg':'#ece0f8','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'木炭', vars:{'--ink':'#ddd','--ink-muted':'#999','--accent':'#a0a0a0','--accent-light':'#333','--border':'#555','--bg':'#2a2a2a','--surface':'#333','--body-bg':'#1a1a1a','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'枫叶', vars:{'--ink':'#2a1e18','--ink-muted':'#8a6048','--accent':'#c04020','--accent-light':'#fef0e8','--border':'#e0c0a8','--bg':'#fffaf6','--surface':'#fef4ea','--body-bg':'#f0e0d0','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'冰川', vars:{'--ink':'#1a2428','--ink-muted':'#5a7a88','--accent':'#4a9eb5','--accent-light':'#dff4f8','--border':'#b8d4dc','--bg':'#f8fcfd','--surface':'#eef8fa','--body-bg':'#d8eaf0','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'玫瑰金', vars:{'--ink':'#2a2022','--ink-muted':'#8a6870','--accent':'#c06078','--accent-light':'#fce8f0','--border':'#e8c0cc','--bg':'#fef8fa','--surface':'#fcf0f4','--body-bg':'#f0dce2','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'翡翠', vars:{'--ink':'#e0f0e8','--ink-muted':'#7ab898','--accent':'#4cdb9a','--accent-light':'#0f301e','--border':'#2a6840','--bg':'#0c1e12','--surface':'#152c1c','--body-bg':'#061208','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'午夜蓝', vars:{'--ink':'#c8d8f0','--ink-muted':'#6a8ab8','--accent':'#6b9ce0','--accent-light':'#102240','--border':'#304868','--bg':'#0e1628','--surface':'#182438','--body-bg':'#060c18','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'陶土', vars:{'--ink':'#2a201c','--ink-muted':'#8a6858','--accent':'#c06840','--accent-light':'#fef0e8','--border':'#e0c8b8','--bg':'#fefaf8','--surface':'#fcf2ec','--body-bg':'#f0e2d8','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + { name:'铂金', vars:{'--ink':'#2a2a2a','--ink-muted':'#888','--accent':'#7a8a9a','--accent-light':'#e8ecf0','--border':'#d0d4d8','--bg':'#fcfcfc','--surface':'#f4f4f6','--body-bg':'#e4e6e8','--font-sans':'"PingFang SC","Hiragino Sans GB","Microsoft YaHei",system-ui,sans-serif'} }, + ]; + + // ============================================================ + // DESIGN THEMES — 12 layout presets + // ============================================================ + const DESIGN_THEMES = [ + { name:'经典双栏', cls:'theme-classic' }, + { name:'极简单栏', cls:'theme-minimal' }, + { name:'右侧栏', cls:'theme-rightbar' }, + { name:'顶栏式', cls:'theme-topbar' }, + { name:'左色条', cls:'theme-accentbar' }, + { name:'宽侧栏', cls:'theme-wideside' }, + { name:'紧凑', cls:'theme-compact' }, + { name:'卡片式', cls:'theme-cards' }, + { name:'时间轴', cls:'theme-timeline' }, + { name:'双色块', cls:'theme-duotone' }, + { name:'线条风', cls:'theme-lines' }, + { name:'无框', cls:'theme-frameless' }, + ]; + + let colorIdx = 0; + let designIdx = 0; + + // ---- Colour theme helpers ---- + function applyColor(idx) { + idx = ((idx % COLOR_THEMES.length) + COLOR_THEMES.length) % COLOR_THEMES.length; + colorIdx = idx; + const t = COLOR_THEMES[idx]; + const root = document.documentElement; + Object.entries(t.vars).forEach(([k, v]) => root.style.setProperty(k, v)); + const label = document.getElementById('themeLabel'); + if (label) label.textContent = t.name; + save(); + } + function prevTheme() { applyColor(colorIdx - 1); } + function nextTheme() { applyColor(colorIdx + 1); } + function setColorByName(name) { + const idx = COLOR_THEMES.findIndex(t => t.name === name); + if (idx >= 0) applyColor(idx); + } + function getColorName() { return COLOR_THEMES[colorIdx].name; } + + // ---- Design theme helpers ---- + function applyDesign(idx) { + idx = ((idx % DESIGN_THEMES.length) + DESIGN_THEMES.length) % DESIGN_THEMES.length; + // Remove old design class + const page = document.querySelector('.page'); + if (page) { + DESIGN_THEMES.forEach(d => page.classList.remove(d.cls)); + designIdx = idx; + page.classList.add(DESIGN_THEMES[idx].cls); + } + const label = document.getElementById('designLabel'); + if (label) label.textContent = DESIGN_THEMES[idx].name; + save(); + } + function prevDesign() { applyDesign(designIdx - 1); } + function nextDesign() { applyDesign(designIdx + 1); } + function setDesignByName(name) { + const idx = DESIGN_THEMES.findIndex(d => d.name === name); + if (idx >= 0) applyDesign(idx); + } + function getDesignName() { return DESIGN_THEMES[designIdx].name; } + + // ============================================================ + // DATA MODEL + // ============================================================ + function snapshot() { + return { + _theme: getColorName(), + _design: getDesignName(), + name: qs('h1 > span:first-child')?.innerHTML || '', + title: qs('h1 > .sub')?.innerHTML || '', + contact: collect('.info-item span:last-child'), + skills: collect('#skillTags .skill-tag', el => el.textContent.replace('×','').trim()), + education: { + school: qs('.editable-block h3')?.innerHTML || '', + major: qs('.editable-block .section-sub')?.innerHTML || '', + date: qs('.editable-block .date')?.innerHTML || '', + }, + sidebarCustom: collectSidebarCustom(), + summary: qs('.summary-text')?.innerHTML || '', + experiences: collectCards('experienceList'), + projects: collectCards('projectList'), + extras: collectBullets('extraList'), + }; + } + + function collect(sel, fn) { + return Array.from(document.querySelectorAll(sel)).map(el => fn ? fn(el) : (el.innerHTML || '')); + } + + function collectCards(listId) { + const cards = []; + const list = document.getElementById(listId); + if (!list) return cards; + list.querySelectorAll('.section-card').forEach(card => { + cards.push({ + title: card.querySelector('h3')?.innerHTML || '', + date: card.querySelector('.date')?.innerHTML || '', + subtitle: card.querySelector('.section-sub')?.innerHTML || '', + bullets: collect(card.querySelectorAll('ul.bullets li')), + }); + }); + return cards; + } + + function collectBullets(listId) { + const list = document.getElementById(listId); + if (!list) return []; + return collect(list.querySelectorAll('ul.bullets li')); + } + + function collectSidebarCustom() { + const c = document.getElementById('sidebarCustom'); + if (!c) return []; + return Array.from(c.querySelectorAll('.sidebar-custom-section')).map(sec => ({ + title: sec.querySelector('.sidebar-custom-title')?.innerHTML || '', + content: sec.querySelector('.sidebar-custom-content')?.innerHTML || '', + })); + } + + // ============================================================ + // RESTORE + // ============================================================ + function restore(data) { + if (!data) return; + + if (data._theme) setColorByName(data._theme); + if (data._design) setDesignByName(data._design); + + setHTML('h1 > span:first-child', data.name); + setHTML('h1 > .sub', data.title); + + const cs = document.querySelectorAll('.info-item span:last-child'); + (data.contact || []).forEach((v, i) => { if (cs[i]) cs[i].innerHTML = v; }); + + if (data.education) { + setHTML('.editable-block h3', data.education.school); + setHTML('.editable-block .section-sub', data.education.major); + setHTML('.editable-block .date', data.education.date); + } + setHTML('.summary-text', data.summary); + + const tc = document.getElementById('skillTags'); + if (tc) { tc.innerHTML = ''; (data.skills || []).forEach(s => createSkillTag(tc, s)); } + + rebuildSidebarCustom(data.sidebarCustom || []); + rebuildCards('experienceList', data.experiences || [], createExperienceCard); + rebuildCards('projectList', data.projects || [], createProjectCard); + rebuildBullets('extraList', data.extras || []); + } + + function setHTML(sel, html) { + const el = qs(sel); + if (el && html !== undefined) el.innerHTML = html; + } + function qs(sel) { return document.querySelector(sel); } + + function rebuildCards(listId, items, fn) { + const list = document.getElementById(listId); + if (!list) return; + list.innerHTML = ''; + items.forEach(item => fn(list, item)); + } + function rebuildBullets(listId, items) { + const list = document.getElementById(listId); + if (!list) return; + list.innerHTML = ''; + const ul = mk('ul', { class:'bullets' }); + items.forEach(t => { + const li = mk('li', { contenteditable:'true', 'data-placeholder':'描述内容…' }); + li.innerHTML = t; ul.appendChild(li); + }); + list.appendChild(ul); + } + function rebuildSidebarCustom(items) { + const c = document.getElementById('sidebarCustom'); + if (!c) return; + c.innerHTML = ''; + items.forEach(item => createSidebarSection(c, item)); + } + + // ============================================================ + // PERSISTENCE + // ============================================================ + let saveTimer; + function save() { + clearTimeout(saveTimer); + saveTimer = setTimeout(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot())); + }, 400); + } + + function load() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + restore(JSON.parse(raw)); + return true; + } + } catch (e) { console.warn('load failed', e); } + return false; + } + + // ============================================================ + // XML EXPORT + // ============================================================ + function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + function tag(n, c, a) { + const as = a ? ' '+Object.entries(a).map(([k,v])=>`${k}="${esc(v)}"`).join(' ') : ''; + return c === '' ? `<${n}${as}/>` : `<${n}${as}>${c}`; + } + + function toXML(data) { + const L = ['']; + const theme = esc(data._theme || getColorName()); + const design = esc(data._design || getDesignName()); + L.push(``); + + L.push(tag('name', esc(data.name||''))); + L.push(tag('title', esc(data.title||''))); + + L.push(''); + ['邮箱','手机','城市','链接'].forEach((lb,i) => L.push(tag('item', esc((data.contact||[])[i]||''), {label:lb}))); + L.push(''); + + L.push(''); + (data.skills||[]).forEach(s => { if(s) L.push(tag('skill', esc(s))); }); + L.push(''); + + const edu = data.education||{}; + L.push(''); + L.push(tag('school', esc(edu.school||''))); + L.push(tag('major', esc(edu.major||''))); + L.push(tag('date', esc(edu.date||''))); + L.push(''); + + L.push(''); + (data.sidebarCustom||[]).forEach(sec => { + L.push('
'); + L.push(tag('title', esc(sec.title||''))); + L.push(tag('content', esc(sec.content||''))); + L.push('
'); + }); + L.push('
'); + + L.push(tag('summary', esc(data.summary||''))); + + L.push(''); + (data.experiences||[]).forEach(exp => { + L.push(''); + L.push(tag('title', esc(exp.title||''))); + L.push(tag('date', esc(exp.date||''))); + L.push(tag('subtitle', esc(exp.subtitle||''))); + L.push(''); + (exp.bullets||[]).forEach(b => { if(b) L.push(tag('bullet', esc(b))); }); + L.push(''); + }); + L.push(''); + + L.push(''); + (data.projects||[]).forEach(p => { + L.push(''); + L.push(tag('title', esc(p.title||''))); + L.push(tag('date', esc(p.date||''))); + L.push(''); + (p.bullets||[]).forEach(b => { if(b) L.push(tag('bullet', esc(b))); }); + L.push(''); + }); + L.push(''); + + L.push(''); + (data.extras||[]).forEach(e => { if(e) L.push(tag('extra', esc(e))); }); + L.push(''); + + L.push('
'); + return L.join('\n'); + } + + function exportXML() { + try { + const data = snapshot(); + const xml = toXML(data); + const blob = new Blob([xml], {type:'application/xml'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = 'resume.xml'; + document.body.appendChild(a); a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 1000); + toast('✅ XML 已导出'); + } catch(err) { + alert('导出失败:' + err.message); + console.error(err); + } + } + + // ============================================================ + // XML IMPORT + // ============================================================ + function importXML(input) { + const file = input.files[0]; + if (!file) return; + const r = new FileReader(); + r.onload = e => { + try { + const doc = new DOMParser().parseFromString(e.target.result, 'application/xml'); + const errNode = doc.querySelector('parsererror'); + if (errNode) { alert('XML 解析失败:' + errNode.textContent); return; } + const data = parseXML(doc); + restore(data); + save(); + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + toast('✅ XML 已导入'); + } catch(err) { + alert('导入失败:' + err.message); + console.error(err); + } + }; + r.onerror = () => { alert('文件读取失败'); }; + r.readAsText(file); + input.value = ''; + } + + function parseXML(doc) { + const t = (el, tag) => { const n = el.querySelector(tag); return n ? n.textContent : ''; }; + const ts = (el, tag) => Array.from(el.querySelectorAll(tag)).map(n => n.textContent||''); + const root = doc.documentElement; + + return { + _theme: root.getAttribute('theme') || '', + _design: root.getAttribute('design') || '', + name: t(root, 'name'), + title: t(root, 'title'), + contact: Array.from(root.querySelectorAll('contact > item')).map(n => n.textContent||''), + skills: ts(root, 'skills > skill'), + education:{ school:t(root,'education>school'), major:t(root,'education>major'), date:t(root,'education>date') }, + sidebarCustom: Array.from(root.querySelectorAll('sidebarSections > section')).map(sec => ({ + title: t(sec,'title'), content: t(sec,'content'), + })), + summary: t(root, 'summary'), + experiences: Array.from(root.querySelectorAll('experiences > experience')).map(exp => ({ + title:t(exp,'title'), date:t(exp,'date'), subtitle:t(exp,'subtitle'), bullets:ts(exp,'bullet'), + })), + projects: Array.from(root.querySelectorAll('projects > project')).map(p => ({ + title:t(p,'title'), date:t(p,'date'), bullets:ts(p,'bullet'), + })), + extras: ts(root, 'extras > extra'), + }; + } + + // ============================================================ + // TOAST + // ============================================================ + function toast(msg) { + const el = document.createElement('div'); + el.textContent = msg; + el.style.cssText = 'position:fixed;bottom:50px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.8);color:#fff;padding:8px 20px;border-radius:20px;font-size:13px;z-index:999;pointer-events:none;transition:opacity .3s;font-family:var(--font-sans)'; + document.body.appendChild(el); + setTimeout(() => { el.style.opacity = '0'; }, 1500); + setTimeout(() => { el.remove(); }, 2000); + } + + // ============================================================ + // DOM HELPERS + // ============================================================ + function mk(tag, a, h) { + const el = document.createElement(tag); + Object.entries(a||{}).forEach(([k,v]) => { + if (k==='class') el.className=v; + else if (k.startsWith('data-')) el.setAttribute(k,v); + else if (k==='contenteditable') el.setAttribute('contenteditable',v); + else el[k]=v; + }); + if (h) el.innerHTML=h; + return el; + } + + // ============================================================ + // SKILL TAGS + // ============================================================ + function createSkillTag(c, text) { + const tag = mk('span', {class:'skill-tag', contenteditable:'true', 'data-placeholder':'技能'}); + tag.textContent = text||''; + const del = mk('button', {class:'tag-delete no-print'}, '×'); + del.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); tag.remove(); save(); }); + tag.appendChild(del); c.appendChild(tag); + } + function addSkill() { + const c = document.getElementById('skillTags'); + createSkillTag(c, ''); + const tags = c.querySelectorAll('.skill-tag'); + const last = tags[tags.length-1]; + if (last) { last.focus(); last.textContent=''; } + save(); + } + + // ============================================================ + // SIDEBAR CUSTOM SECTIONS + // ============================================================ + function createSidebarSection(c, item) { + const sec = mk('div', {class:'sidebar-custom-section'}); + const del = mk('button', {class:'card-delete no-print'}, '×'); + del.addEventListener('click', () => { sec.remove(); save(); }); + sec.appendChild(del); + const ti = mk('div', {class:'sidebar-custom-title', contenteditable:'true', 'data-placeholder':'栏目名称'}); + ti.innerHTML = (item||{}).title||''; sec.appendChild(ti); + const co = mk('div', {class:'sidebar-custom-content', contenteditable:'true', 'data-placeholder':'内容…'}); + co.innerHTML = (item||{}).content||''; sec.appendChild(co); + c.appendChild(sec); + return sec; + } + function addSidebarSection() { + const c = document.getElementById('sidebarCustom'); + const sec = createSidebarSection(c, {}); + sec.querySelector('.sidebar-custom-title').focus(); + save(); + } + + // ============================================================ + // SECTION CARDS (Experience / Project) + // ============================================================ + function createSectionCard(list, item, hasSub) { + const card = mk('div', {class:'section-card'}); + const del = mk('button', {class:'card-delete no-print'}, '×'); + del.addEventListener('click', () => { card.remove(); save(); }); + card.appendChild(del); + + const hdr = mk('div', {class:'section-header'}); + const h3 = mk('h3', {contenteditable:'true', 'data-placeholder':'公司 / 项目名称'}); + h3.innerHTML = item.title||''; + const dt = mk('span', {class:'date', contenteditable:'true', 'data-placeholder':'时间'}); + dt.innerHTML = item.date||''; + hdr.appendChild(h3); hdr.appendChild(dt); + card.appendChild(hdr); + + if (hasSub) { + const sub = mk('div', {class:'section-sub', contenteditable:'true', 'data-placeholder':'职位 / 角色'}); + sub.innerHTML = item.subtitle||''; + card.appendChild(sub); + } + + const ul = mk('ul', {class:'bullets'}); + (item.bullets||['']).forEach(t => { + const li = mk('li', {contenteditable:'true', 'data-placeholder':'描述你做了什么…'}); + li.innerHTML = t; ul.appendChild(li); + }); + card.appendChild(ul); + + const addB = mk('button', {class:'add-btn no-print'}, '+ 添加要点'); + addB.addEventListener('click', () => { + const li = mk('li', {contenteditable:'true', 'data-placeholder':'描述你做了什么…'}); + ul.appendChild(li); li.focus(); save(); + }); + card.appendChild(addB); + list.appendChild(card); + return card; + } + function createExperienceCard(list, item) { return createSectionCard(list, item||{}, true); } + function createProjectCard(list, item) { return createSectionCard(list, item||{}, false); } + + function addExperience() { const l=document.getElementById('experienceList'); createExperienceCard(l,{bullets:['']}); save(); } + function addProject() { const l=document.getElementById('projectList'); createProjectCard(l,{bullets:['']}); save(); } + + // ============================================================ + // EXTRA BULLETS + // ============================================================ + function addExtra() { + const list = document.getElementById('extraList'); + let ul = list.querySelector('ul.bullets'); + if (!ul) { ul = mk('ul', {class:'bullets'}); list.appendChild(ul); } + const li = mk('li', {contenteditable:'true', 'data-placeholder':'其他信息…'}); + ul.appendChild(li); li.focus(); save(); + } + + // ============================================================ + // RESET + // ============================================================ + function reset() { + if (confirm('确定要清空所有内容吗?此操作不可恢复。')) { + localStorage.removeItem(STORAGE_KEY); + location.reload(); + } + } + + // ============================================================ + // AUTO-SAVE + // ============================================================ + function bindAutoSave() { + document.addEventListener('input', e => { if (e.target.isContentEditable) save(); }); + document.addEventListener('focusout', e => { if (e.target.isContentEditable) save(); }); + document.addEventListener('keyup', e => { + if (e.target.isContentEditable && (e.key==='Backspace'||e.key==='Delete')) save(); + }); + } + + // ============================================================ + // INIT + // ============================================================ + function init() { + bindAutoSave(); + applyColor(0); + applyDesign(0); + + const hasData = load(); + + if (!hasData) { + applyColor(0); + applyDesign(0); + if (!document.getElementById('experienceList').children.length) addExperience(); + if (!document.getElementById('projectList').children.length) addProject(); + if (!document.getElementById('skillTags').children.length) addSkill(); + } + } + + return { + addSkill, addSidebarSection, addExperience, addProject, addExtra, + reset, exportXML, importXML, + prevTheme, nextTheme, setColorByName, + prevDesign, nextDesign, setDesignByName, + init, + }; +})(); + +document.addEventListener('DOMContentLoaded', Resume.init); diff --git a/resume/index.html b/resume/index.html new file mode 100644 index 0000000..1d29bbf --- /dev/null +++ b/resume/index.html @@ -0,0 +1,148 @@ + + + + + +个人简历 + + + + + +
+ + + + + + 经典蓝 + + + + + 经典双栏 + + + + +
+
💡 点击文字直接编辑 · 点击 [+ 添加] 增条目 · 悬停条目右侧删 · 改完 Ctrl+P 出 PDF
+ + +
+ +
+ + + + + +
+ + +
+

个人概述

+

+
+ + +
+

工作经历

+
+ +
+
+ +
+
+ + +
+

项目经历

+
+ +
+
+ +
+
+ + +
+

其他

+
+ +
+
+ +
+
+ +
+
+ +
+ + + + \ No newline at end of file diff --git a/resume/style.css b/resume/style.css new file mode 100644 index 0000000..cf51d56 --- /dev/null +++ b/resume/style.css @@ -0,0 +1,665 @@ +/* ===== Reset & Variables ===== */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --ink: #1a1a1a; + --ink-muted: #555; + --accent: #1e5fa8; + --accent-light: #e8f0fa; + --border: #ddd; + --bg: #fff; + --surface: #f8f9fa; + --radius: 6px; + --font-sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", system-ui, -apple-system, sans-serif; + --font-mono: "JetBrains Mono", "Cascadia Code", "Fira Code", "SF Mono", "Monaco", monospace; +} + +/* ===== Print ===== */ +@media print { + @page { size: A4; margin: 0; } + body { + background: #fff; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + .page { + box-shadow: none; + margin: 0 auto; + padding: 36px 44px; + max-width: 210mm; + /* No min-height — natural multi-page flow */ + } + .section-card { break-inside: avoid; page-break-inside: avoid; } + h2 { break-after: avoid; page-break-after: avoid; } + /* Keep heading with the first card that follows */ + section > h2 + * { break-before: avoid; } + .no-print { display: none !important; } + [contenteditable]:focus { outline: none; } + [contenteditable]:empty::before { display: none; } + + /* Collapse sidebar into compact header on page 1; page 2+ is full-width main */ + .resume-grid { + grid-template-columns: 1fr; + gap: 0; + } + .sidebar { + position: static; + display: flex; + flex-wrap: wrap; + gap: 6px 20px; + align-items: flex-start; + padding-bottom: 14px; + margin-bottom: 20px; + border-bottom: 2px solid var(--accent); + } + .sidebar .name-section { + flex-basis: 100%; + margin-bottom: 4px; + } + .sidebar .name-section h1 { font-size: 22px; display: flex; gap: 12px; align-items: baseline; } + .sidebar .name-section h1 .sub { margin-top: 0; } + .sidebar section { + margin-bottom: 0; + min-width: 120px; + } + .sidebar h2 { + border-bottom: none; + margin-bottom: 2px; + padding-bottom: 0; + font-size: 11px; + } + .sidebar .info-item { margin-bottom: 1px; } + .skill-tags { gap: 3px; } + .skill-tag { font-size: 10px; padding: 1px 5px; } + .editable-block h3 { font-size: 12px; } + .sidebar .section-sub { font-size: 11px; } + .sidebar .date { font-size: 11px; } + + /* Sidebar custom sections in print */ + .sidebar-custom-section { min-width: 100px; } + .sidebar-custom-section h2 { font-size: 11px; margin-bottom: 2px; } + .sidebar-custom-section .sidebar-custom-content { font-size: 11px; } + + /* Main content full width */ + .main { width: 100%; } +} + +/* ===== Screen ===== */ +@media screen { + body { + background: var(--body-bg, #e8ecf1); + font-family: var(--font-sans); + color: var(--ink); + line-height: 1.6; + padding: 20px; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + } + .page { + background: var(--bg); + width: 210mm; + min-height: 297mm; + padding: 36px 44px; + box-shadow: 0 2px 20px rgba(0,0,0,.12); + position: relative; + } +} + +/* ===== Editable ===== */ +[contenteditable] { + outline: none; + border-radius: 3px; + transition: background .15s; + cursor: text; +} +[contenteditable]:hover { + background: rgba(30,95,168,.04); +} +[contenteditable]:focus { + background: rgba(30,95,168,.08); + box-shadow: 0 0 0 1.5px var(--accent); + border-radius: 3px; +} +[contenteditable]:empty::before { + content: attr(data-placeholder); + color: #bbb; + font-style: italic; +} + +/* ===== Layout ===== */ +.resume-grid { + display: grid; + grid-template-columns: 180px 1fr; + gap: 32px; + align-items: start; +} + +.sidebar { + position: sticky; + top: 36px; +} + +/* ===== Typography ===== */ +h1 { + font-size: 26px; + font-weight: 700; + letter-spacing: 1px; + line-height: 1.2; + margin-bottom: 4px; +} +h1 .sub { + display: block; + font-size: 13px; + font-weight: 400; + color: var(--ink-muted); + margin-top: 4px; + letter-spacing: 0; +} +h2 { + font-size: 14px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--accent); + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 2px solid var(--accent); +} +h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: 2px; +} +h3 .meta { + font-weight: 400; + color: var(--ink-muted); + font-size: 12px; +} + +/* ===== Sidebar ===== */ +.sidebar section { + margin-bottom: 22px; +} +.sidebar h2 { + border-bottom-color: var(--border); + color: var(--ink); + font-size: 12px; +} +.name-section { + margin-bottom: 18px; +} +.info-item { + font-size: 12px; + margin-bottom: 5px; + display: flex; + gap: 5px; +} +.info-label { + color: var(--ink-muted); + flex-shrink: 0; +} +.skill-tags { + display: flex; + flex-wrap: wrap; + gap: 5px; +} +.skill-tag { + font-size: 11px; + background: var(--accent-light); + color: var(--accent); + padding: 2px 8px; + border-radius: 3px; + font-weight: 500; + position: relative; + cursor: text; +} +.skill-tag .tag-delete { + display: none; + position: absolute; + top: -6px; + right: -6px; + width: 16px; + height: 16px; + background: #e74c3c; + color: #fff; + border-radius: 50%; + font-size: 10px; + line-height: 16px; + text-align: center; + cursor: pointer; + border: none; +} +.skill-tag:hover .tag-delete { + display: block; +} + +/* ===== Sidebar custom sections ===== */ +.sidebar-custom-section { + margin-bottom: 16px; + position: relative; + padding: 8px; + border: 1px dashed transparent; + border-radius: var(--radius); + transition: border-color .15s; +} +.sidebar-custom-section:hover { + border-color: var(--border); +} +.sidebar-custom-section .sidebar-custom-title { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.5px; + color: var(--ink); + margin-bottom: 4px; + padding-bottom: 4px; + border-bottom: 1.5px solid var(--border); + min-width: 2em; +} +.sidebar-custom-section .sidebar-custom-content { + font-size: 12px; + color: var(--ink-muted); + line-height: 1.5; + min-height: 1em; +} +.sidebar-custom-section .card-delete { + display: none; + position: absolute; + top: -4px; + right: -4px; + width: 18px; + height: 18px; + background: #e74c3c; + color: #fff; + border: none; + border-radius: 50%; + font-size: 10px; + line-height: 18px; + text-align: center; + cursor: pointer; + opacity: .7; +} +.sidebar-custom-section:hover .card-delete { + display: block; +} + +/* ===== Main ===== */ +.main section { + margin-bottom: 24px; +} + +/* Section card (experience, project) */ +.section-card { + position: relative; + margin-bottom: 14px; + padding-bottom: 14px; + border-bottom: 1px dashed var(--border); +} +.section-card:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} +.section-card .card-delete { + display: none; + position: absolute; + top: 0; + right: 0; + width: 22px; + height: 22px; + background: #e74c3c; + color: #fff; + border: none; + border-radius: 50%; + font-size: 12px; + line-height: 22px; + text-align: center; + cursor: pointer; + opacity: .7; + transition: opacity .15s; +} +.section-card:hover .card-delete { + display: block; +} +.section-card .card-delete:hover { + opacity: 1; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 2px; +} +.section-header .date { + font-size: 12px; + color: var(--ink-muted); + white-space: nowrap; + margin-left: 10px; +} +.section-sub { + font-size: 13px; + color: var(--ink-muted); + margin-bottom: 6px; +} + +/* Bullet list */ +ul.bullets { + list-style: none; + padding: 0; +} +ul.bullets li { + font-size: 13px; + margin-bottom: 3px; + padding-left: 14px; + position: relative; +} +ul.bullets li::before { + content: "—"; + position: absolute; + left: 0; + color: var(--accent); + font-weight: 700; +} + +p { + font-size: 13px; + margin-bottom: 4px; +} + +.date { + font-size: 12px; + color: var(--ink-muted); +} + +/* ===== Add Button ===== */ +.add-btn-group { + margin-top: 8px; +} +.add-btn { + font-size: 12px; + padding: 4px 12px; + border: 1px dashed var(--accent); + background: transparent; + color: var(--accent); + border-radius: var(--radius); + cursor: pointer; + font-family: var(--font-sans); + transition: all .15s; +} +.add-btn:hover { + background: var(--accent-light); + border-style: solid; +} + +/* ===== Toolbar ===== */ +.toolbar { + position: fixed; + top: 14px; + right: 14px; + display: flex; + gap: 8px; + z-index: 100; +} +.btn { + font-size: 12px; + padding: 7px 14px; + border: none; + border-radius: var(--radius); + cursor: pointer; + font-weight: 600; + transition: all .15s; + font-family: var(--font-sans); +} +.btn-print { + background: var(--accent); + color: #fff; +} +.btn-print:hover { background: #164a85; } +.btn-xml { + background: #fff; + color: var(--accent); + border: 1px solid var(--accent); +} +.btn-xml:hover { background: var(--accent-light); } +.theme-switcher { + display: flex; + align-items: center; + gap: 2px; +} +.btn-theme { + background: transparent; + color: var(--ink-muted); + border: 1px solid var(--border); + padding: 5px 8px; + font-size: 11px; + min-width: auto; +} +.btn-theme:hover { background: var(--surface); color: var(--ink); } +.theme-label { + font-size: 11px; + color: var(--accent); + font-weight: 600; + min-width: 48px; + text-align: center; + user-select: none; +} +.btn-reset { + background: #fff; + color: var(--ink-muted); + border: 1px solid var(--border); +} +.btn-reset:hover { background: var(--surface); } +.edit-hint { + position: fixed; + bottom: 14px; + left: 50%; + transform: translateX(-50%); + font-size: 12px; + background: rgba(0,0,0,.7); + color: #fff; + padding: 5px 16px; + border-radius: 20px; + z-index: 100; + pointer-events: none; +} + +/* ===== Editable block ===== */ +.editable-block { + position: relative; +} + +/* ===== Summary ===== */ +.summary-text { + min-height: 1.6em; +} + +/* ===== Bullet list item controls ===== */ +.bullet-li { + display: flex; + align-items: flex-start; + gap: 4px; + margin-bottom: 3px; +} +.bullet-li .li-delete { + display: none; + flex-shrink: 0; + width: 16px; + height: 16px; + background: #e74c3c; + color: #fff; + border: none; + border-radius: 50%; + font-size: 10px; + line-height: 16px; + text-align: center; + cursor: pointer; + opacity: .7; + margin-top: 2px; +} +.bullet-li:hover .li-delete { + display: block; +} + +/* ===== Responsive ===== */ +@media screen and (max-width: 800px) { + .page { width: 100%; padding: 20px 16px; } + .resume-grid { grid-template-columns: 1fr; gap: 20px; } + .sidebar { position: static; } + .toolbar { top: 6px; right: 6px; } + .btn { padding: 5px 10px; font-size: 11px; } +} + +/* ============================================================ + DESIGN THEMES — 12 layout presets + Applied via .page.theme-xxx + ============================================================ */ + +/* 1. Classic — default, no overrides needed */ + +/* 2. Minimal — single column, sidebar as compact top header */ +.page.theme-minimal .resume-grid { + grid-template-columns: 1fr; + gap: 24px; +} +.page.theme-minimal .sidebar { + position: static; + display: flex; + flex-wrap: wrap; + gap: 6px 24px; + align-items: flex-start; + padding-bottom: 12px; + margin-bottom: 18px; + border-bottom: 2px solid var(--border); +} +.page.theme-minimal .sidebar .name-section { flex-basis: 100%; margin-bottom: 2px; } +.page.theme-minimal .sidebar .name-section h1 { font-size: 24px; } +.page.theme-minimal .sidebar section { margin-bottom: 0; min-width: 100px; } +.page.theme-minimal .sidebar h2 { + border-bottom: none; margin-bottom: 2px; padding-bottom: 0; font-size: 11px; +} + +/* 3. Right bar — sidebar on the right */ +.page.theme-rightbar .resume-grid { + grid-template-columns: 1fr 180px; +} +.page.theme-rightbar .sidebar { order: 2; } + +/* 4. Top bar — sidebar as full-width header, single column body */ +.page.theme-topbar .resume-grid { + grid-template-columns: 1fr; + gap: 0; +} +.page.theme-topbar .sidebar { + position: static; + display: flex; + flex-wrap: wrap; + gap: 6px 28px; + align-items: flex-start; + padding-bottom: 14px; + margin-bottom: 22px; + border-bottom: 2px solid var(--accent); +} +.page.theme-topbar .sidebar .name-section { flex-basis: 100%; margin-bottom: 4px; } +.page.theme-topbar .sidebar .name-section h1 { font-size: 24px; } +.page.theme-topbar .sidebar section { margin-bottom: 0; min-width: 100px; } +.page.theme-topbar .sidebar h2 { + border-bottom: none; margin-bottom: 2px; padding-bottom: 0; font-size: 11px; +} + +/* 5. Accent bar — thick accent left border on sidebar */ +.page.theme-accentbar .sidebar { + border-left: 4px solid var(--accent); + padding-left: 14px; +} + +/* 6. Wide side — 240px sidebar */ +.page.theme-wideside .resume-grid { + grid-template-columns: 240px 1fr; +} + +/* 7. Compact — tighter spacing everywhere */ +.page.theme-compact .resume-grid { gap: 20px; } +.page.theme-compact .sidebar section { margin-bottom: 14px; } +.page.theme-compact .main section { margin-bottom: 16px; } +.page.theme-compact .section-card { margin-bottom: 8px; padding-bottom: 8px; } +.page.theme-compact h1 { font-size: 22px; } +.page.theme-compact h2 { font-size: 12px; margin-bottom: 6px; padding-bottom: 3px; } +.page.theme-compact h3 { font-size: 12px; } +.page.theme-compact ul.bullets li { font-size: 11px; margin-bottom: 1px; } +.page.theme-compact p { font-size: 11px; } +.page.theme-compact .page { padding: 28px 32px; } + +/* 8. Cards — sections wrapped in card containers */ +.page.theme-cards .main section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + margin-bottom: 16px; +} +.page.theme-cards .main section h2 { + margin-top: -4px; +} + +/* 9. Timeline — left-positioned date markers on experiences/projects */ +.page.theme-timeline .section-card { + padding-left: 80px; + position: relative; +} +.page.theme-timeline .section-card .date { + position: absolute; + left: 0; + top: 0; + font-size: 11px; + color: var(--accent); + font-weight: 600; + white-space: nowrap; +} +.page.theme-timeline .section-card::before { + content: ''; + position: absolute; + left: 68px; + top: 4px; + width: 6px; + height: 6px; + background: var(--accent); + border-radius: 50%; +} +.page.theme-timeline .section-header .date { display: none; } + +/* 10. Duotone — sidebar has a subtle background colour */ +.page.theme-duotone .sidebar { + background: var(--surface); + margin: -16px; + padding: 16px; + border-radius: var(--radius); +} +.page.theme-duotone .resume-grid { gap: 28px; } + +/* 11. Lines — bold horizontal rules between sections, no card borders */ +.page.theme-lines .main section { + padding-bottom: 18px; + margin-bottom: 18px; + border-bottom: 2px solid var(--accent); +} +.page.theme-lines .main section:last-child { border-bottom: none; } +.page.theme-lines .section-card { + border-bottom: 1px solid var(--border); +} +.page.theme-lines .section-card:last-child { border-bottom: none; } +.page.theme-lines .sidebar section { + padding-bottom: 14px; + border-bottom: 1px solid var(--border); +} +.page.theme-lines .sidebar section:last-child { border-bottom: none; } + +/* 12. Frameless — no borders, no backgrounds, pure whitespace */ +.page.theme-frameless .sidebar h2 { border-bottom: none; padding-bottom: 0; } +.page.theme-frameless .main section h2 { border-bottom: none; padding-bottom: 0; } +.page.theme-frameless .section-card { border-bottom: none; padding-bottom: 10px; margin-bottom: 10px; } +.page.theme-frameless .sidebar-custom-section { border: none; } +.page.theme-frameless .sidebar-custom-section .sidebar-custom-title { border-bottom: none; } +.page.theme-frameless .add-btn { border-style: solid; opacity: .5; }