Files
CallMe/resume/app.js
T

565 lines
27 KiB
JavaScript
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.
/**
* 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
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}</${n}>`;
}
function toXML(data) {
const L = ['<?xml version="1.0" encoding="UTF-8"?>'];
const theme = esc(data._theme || getColorName());
const design = esc(data._design || getDesignName());
L.push(`<resume theme="${theme}" design="${design}">`);
L.push(tag('name', esc(data.name||'')));
L.push(tag('title', esc(data.title||'')));
L.push('<contact>');
['邮箱','手机','城市','链接'].forEach((lb,i) => L.push(tag('item', esc((data.contact||[])[i]||''), {label:lb})));
L.push('</contact>');
L.push('<skills>');
(data.skills||[]).forEach(s => { if(s) L.push(tag('skill', esc(s))); });
L.push('</skills>');
const edu = data.education||{};
L.push('<education>');
L.push(tag('school', esc(edu.school||'')));
L.push(tag('major', esc(edu.major||'')));
L.push(tag('date', esc(edu.date||'')));
L.push('</education>');
L.push('<sidebarSections>');
(data.sidebarCustom||[]).forEach(sec => {
L.push('<section>');
L.push(tag('title', esc(sec.title||'')));
L.push(tag('content', esc(sec.content||'')));
L.push('</section>');
});
L.push('</sidebarSections>');
L.push(tag('summary', esc(data.summary||'')));
L.push('<experiences>');
(data.experiences||[]).forEach(exp => {
L.push('<experience>');
L.push(tag('title', esc(exp.title||'')));
L.push(tag('date', esc(exp.date||'')));
L.push(tag('subtitle', esc(exp.subtitle||'')));
L.push('<bullets>');
(exp.bullets||[]).forEach(b => { if(b) L.push(tag('bullet', esc(b))); });
L.push('</bullets></experience>');
});
L.push('</experiences>');
L.push('<projects>');
(data.projects||[]).forEach(p => {
L.push('<project>');
L.push(tag('title', esc(p.title||'')));
L.push(tag('date', esc(p.date||'')));
L.push('<bullets>');
(p.bullets||[]).forEach(b => { if(b) L.push(tag('bullet', esc(b))); });
L.push('</bullets></project>');
});
L.push('</projects>');
L.push('<extras>');
(data.extras||[]).forEach(e => { if(e) L.push(tag('extra', esc(e))); });
L.push('</extras>');
L.push('</resume>');
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);