feat(resume): v3 flexibility - custom list sections, item reorder, info field visibility, header toggles, config io, migration persistence fix
This commit is contained in:
+41
-24
@@ -54,15 +54,23 @@ export function defaultResumeConfig(): ResumeConfig {
|
|||||||
export function migrateResumeConfig(raw: unknown): ResumeConfig {
|
export function migrateResumeConfig(raw: unknown): ResumeConfig {
|
||||||
const def = defaultResumeConfig();
|
const def = defaultResumeConfig();
|
||||||
if (!raw || typeof raw !== 'object') return def;
|
if (!raw || typeof raw !== 'object') return def;
|
||||||
const r = raw as { version?: number; template?: ResumeTemplate; accent?: string; fontScale?: number; density?: ResumeConfig['density']; onePage?: boolean; sections?: { key: string; enabled?: boolean; title?: string }[] };
|
const r = raw as { version?: number; template?: ResumeTemplate; accent?: string; fontScale?: number; density?: ResumeConfig['density']; onePage?: boolean; header?: ResumeConfig['header']; sections?: ResumeSectionCfg[] };
|
||||||
const oldSections = Array.isArray(r.sections) ? r.sections : [];
|
const oldSections = Array.isArray(r.sections) ? r.sections : [];
|
||||||
const sections = def.sections.map(d => {
|
const sections = def.sections.map(d => {
|
||||||
const old = oldSections.find(o => o.key === d.key);
|
const old = oldSections.find(o => o.key === d.key);
|
||||||
const dd = RESUME_SECTION_DEFS.find(x => x.key === d.key);
|
const dd = RESUME_SECTION_DEFS.find(x => x.key === d.key);
|
||||||
const base: ResumeSectionCfg = { ...d, enabled: old?.enabled ?? true, title: old?.title ?? dd?.defaultTitle ?? d.title };
|
// 关键:以「已存储的板块」为底展开 —— 保留用户的自定义条目/文本/覆盖/排序/显隐
|
||||||
if (d.key === 'skills' && !base.text) base.text = SEED.skills.text;
|
const base: ResumeSectionCfg = {
|
||||||
|
...d,
|
||||||
|
...(old ?? {}),
|
||||||
|
enabled: old?.enabled ?? true,
|
||||||
|
title: old?.title ?? dd?.defaultTitle ?? d.title,
|
||||||
|
kind: dd ? d.kind : (old?.kind ?? d.kind),
|
||||||
|
types: dd?.types ?? old?.types,
|
||||||
|
};
|
||||||
|
if (d.key === 'skills' && base.text == null) base.text = SEED.skills.text;
|
||||||
if (d.key === 'campus' && !(base.items?.length)) base.items = SEED.campus.items;
|
if (d.key === 'campus' && !(base.items?.length)) base.items = SEED.campus.items;
|
||||||
if (d.key === 'self' && !base.text) base.text = SEED.self.text;
|
if (d.key === 'self' && base.text == null) base.text = SEED.self.text;
|
||||||
return base;
|
return base;
|
||||||
});
|
});
|
||||||
// 旧版本已被取代的板块 key:直接丢弃,避免残留造成内容重复
|
// 旧版本已被取代的板块 key:直接丢弃,避免残留造成内容重复
|
||||||
@@ -88,6 +96,7 @@ export function migrateResumeConfig(raw: unknown): ResumeConfig {
|
|||||||
// ---------- 装配 ----------
|
// ---------- 装配 ----------
|
||||||
|
|
||||||
export interface AssembledItem {
|
export interface AssembledItem {
|
||||||
|
itemKey: string;
|
||||||
title: string; subtitle?: string; date?: string; desc?: string;
|
title: string; subtitle?: string; date?: string; desc?: string;
|
||||||
fromRecord?: number; excluded?: boolean;
|
fromRecord?: number; excluded?: boolean;
|
||||||
}
|
}
|
||||||
@@ -95,6 +104,7 @@ export interface AssembledItem {
|
|||||||
export interface AssembledSection {
|
export interface AssembledSection {
|
||||||
key: string; title: string; kind: 'info' | 'edu' | 'list' | 'text';
|
key: string; title: string; kind: 'info' | 'edu' | 'list' | 'text';
|
||||||
items?: AssembledItem[]; text?: string;
|
items?: AssembledItem[]; text?: string;
|
||||||
|
hiddenFields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimedTypes(): string[] {
|
function claimedTypes(): string[] {
|
||||||
@@ -125,7 +135,7 @@ export function assembleSections(
|
|||||||
if (!s.enabled) continue;
|
if (!s.enabled) continue;
|
||||||
const def = RESUME_SECTION_DEFS.find(d => d.key === s.key);
|
const def = RESUME_SECTION_DEFS.find(d => d.key === s.key);
|
||||||
|
|
||||||
if (s.key === 'info') { out.push({ key: s.key, title: s.title, kind: 'info' }); continue; }
|
if (s.key === 'info') { out.push({ key: s.key, title: s.title, kind: 'info', hiddenFields: s.hiddenFields ?? [] }); continue; }
|
||||||
if (s.key === 'self' || s.kind === 'text') {
|
if (s.key === 'self' || s.kind === 'text') {
|
||||||
const txt = s.text ?? '';
|
const txt = s.text ?? '';
|
||||||
if (!txt.trim()) continue; // 空板块不渲染
|
if (!txt.trim()) continue; // 空板块不渲染
|
||||||
@@ -143,14 +153,21 @@ export function assembleSections(
|
|||||||
// 其他桶:未归类的类型
|
// 其他桶:未归类的类型
|
||||||
derived = records.filter(r => r.status !== 'draft' && !excluded.has(r.id) && !usedRecordIds.has(r.id) && !claimed.includes(r.type_name));
|
derived = records.filter(r => r.status !== 'draft' && !excluded.has(r.id) && !usedRecordIds.has(r.id) && !claimed.includes(r.type_name));
|
||||||
}
|
}
|
||||||
const items: AssembledItem[] = derived.map(recordToItem).filter(it => !it.fromRecord || !excluded.has(it.fromRecord))
|
// 手动排序:itemOrder 优先,未排序的按日期排在后面
|
||||||
.map(it => ({ ...it, ...(s.overrides?.[String(it.fromRecord)] ?? {}) }));
|
const orderIdx = new Map((s.itemOrder ?? []).map((k, i) => [k, i]));
|
||||||
items.forEach(it => { if (it.fromRecord) usedRecordIds.add(it.fromRecord); });
|
const items: (AssembledItem & { _ord: number })[] = [];
|
||||||
for (const c of s.items ?? []) items.push({ ...c });
|
for (const r of derived) {
|
||||||
|
const it: AssembledItem = { itemKey: 'r' + r.id, ...recordToItem(r), ...(s.overrides?.[String(r.id)] ?? {}) };
|
||||||
items.sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''));
|
items.push({ ...it, _ord: orderIdx.get(it.itemKey) ?? 9999 });
|
||||||
|
usedRecordIds.add(r.id);
|
||||||
|
}
|
||||||
|
for (const c of s.items ?? []) {
|
||||||
|
const k = c.id ?? ('c' + (s.items ?? []).indexOf(c));
|
||||||
|
items.push({ itemKey: k, ...c, _ord: orderIdx.get(k) ?? 9999 });
|
||||||
|
}
|
||||||
|
items.sort((a, b) => (a._ord - b._ord) || (b.date ?? '').localeCompare(a.date ?? ''));
|
||||||
if (!items.length) continue;
|
if (!items.length) continue;
|
||||||
out.push({ key: s.key, title: s.title, kind: 'list', items });
|
out.push({ key: s.key, title: s.title, kind: 'list', items: items.map(it => { const { _ord, ...rest } = it; return rest; }) });
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -191,7 +208,7 @@ export function resumeToWordHtml(
|
|||||||
const nameColor = cfg.template === 'tech' ? accent : '#111';
|
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 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 contactLine = [P.phone, P.email, P.homepage].filter(v => v !== '').map(v => esc(v)).join(' · ');
|
||||||
const photo = photoUrl && P.photoUrl
|
const photo = (cfg.header?.showPhoto ?? true) && photoUrl && P.photoUrl
|
||||||
? '<td style="width:' + (120 * scale).toFixed(0) + 'px;text-align:right;vertical-align:top">' +
|
? '<td style="width:' + (120 * scale).toFixed(0) + 'px;text-align:right;vertical-align:top">' +
|
||||||
'<img src="' + esc(P.photoUrl) + '" alt="证件照" width="' + Math.round(120 * scale) + '" height="' + Math.round(160 * scale) + '" style="width:' + Math.round(120 * scale) + 'px;height:' + Math.round(160 * scale) + 'px" /></td>'
|
'<img src="' + esc(P.photoUrl) + '" alt="证件照" width="' + Math.round(120 * scale) + '" height="' + Math.round(160 * scale) + '" style="width:' + Math.round(120 * scale) + 'px;height:' + Math.round(160 * scale) + 'px" /></td>'
|
||||||
: '';
|
: '';
|
||||||
@@ -199,8 +216,8 @@ export function resumeToWordHtml(
|
|||||||
const head = '<table style="width:100%;border-collapse:collapse"><tr>' +
|
const head = '<table style="width:100%;border-collapse:collapse"><tr>' +
|
||||||
'<td style="vertical-align:top">' +
|
'<td style="vertical-align:top">' +
|
||||||
'<div style="font-size:' + pt(20) + ';font-weight:bold;color:' + nameColor + '">' + esc(P.name || '个人简历') + '</div>' +
|
'<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>' : '') +
|
((cfg.header?.showSchool ?? true) && 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>' : '') +
|
((cfg.header?.showContact ?? true) && contactLine ? '<div style="color:#555;font-size:' + pt(11) + ';margin-top:2pt">' + contactLine + '</div>' : '') +
|
||||||
'</td>' + photo + '</tr></table>';
|
'</td>' + photo + '</tr></table>';
|
||||||
|
|
||||||
// 条目:标题(左) + 日期(右) 表格行 + 副标题 + 描述
|
// 条目:标题(左) + 日期(右) 表格行 + 副标题 + 描述
|
||||||
@@ -224,17 +241,17 @@ export function resumeToWordHtml(
|
|||||||
'<td style="padding:3px 6px">' + esc(kv[1]) + '</td></tr>').join('') + '</table>' +
|
'<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>' : '');
|
(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') {
|
} else if (sec.kind === 'info') {
|
||||||
const rows: [string, string][] = ([
|
const infoRows: [string, string, string][] = ([
|
||||||
['姓名', P.name ?? ''], ['性别', P.gender ?? ''], ['出生年月', P.birth ?? ''],
|
['name', '姓名', P.name ?? ''], ['gender', '性别', P.gender ?? ''], ['birth', '出生年月', P.birth ?? ''],
|
||||||
['民族', P.nation ?? ''], ['籍贯', P.native ?? ''], ['政治面貌', P.political ?? ''],
|
['nation', '民族', P.nation ?? ''], ['native', '籍贯', P.native ?? ''], ['political', '政治面貌', P.political ?? ''],
|
||||||
['班级', P.className ?? ''], ['学号', P.studentId ?? ''], ['学历', P.degree ?? ''],
|
['className', '班级', P.className ?? ''], ['studentId', '学号', P.studentId ?? ''], ['degree', '学历', P.degree ?? ''],
|
||||||
['手机', P.phone ?? ''], ['邮箱', P.email ?? ''],
|
['phone', '手机', P.phone ?? ''], ['email', '邮箱', P.email ?? ''],
|
||||||
] as [string, string][]).filter(r => r[1] !== '');
|
] as [string, string, string][]).filter(r => r[2] !== '' && !(sec.hiddenFields ?? []).includes(r[0]));
|
||||||
inner = '<table style="width:100%;border-collapse:collapse;font-size:' + pt(12) + '">' +
|
inner = '<table style="width:100%;border-collapse:collapse;font-size:' + pt(12) + '">' +
|
||||||
rows.map((kv, i) =>
|
infoRows.map((kv, i) =>
|
||||||
(i % 2 === 0 ? '<tr>' : '') +
|
(i % 2 === 0 ? '<tr>' : '') +
|
||||||
'<td style="width:80px;padding:3px 6px;color:#555">' + esc(kv[0]) + '</td>' +
|
'<td style="width:80px;padding:3px 6px;color:#555">' + esc(kv[1]) + '</td>' +
|
||||||
'<td style="padding:3px 6px">' + esc(kv[1]) + '</td>' +
|
'<td style="padding:3px 6px">' + esc(kv[2]) + '</td>' +
|
||||||
(i % 2 === 1 ? '</tr>' : '')).join('') + '</table>';
|
(i % 2 === 1 ? '</tr>' : '')).join('') + '</table>';
|
||||||
} else if (sec.kind === 'list') {
|
} else if (sec.kind === 'list') {
|
||||||
inner = '<div>' + (sec.items ?? []).map(itemHtml).join('') + '</div>';
|
inner = '<div>' + (sec.items ?? []).map(itemHtml).join('') + '</div>';
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export interface ProfileInfo {
|
|||||||
phone: string; email: string; homepage: string; photoUrl: string;
|
phone: string; email: string; homepage: string; photoUrl: string;
|
||||||
selfIntro: string;
|
selfIntro: string;
|
||||||
}
|
}
|
||||||
export interface ResumeItemCfg { title: string; subtitle?: string; date?: string; desc?: string; fromRecord?: number }
|
export interface ResumeItemCfg { id?: string; title: string; subtitle?: string; date?: string; desc?: string; fromRecord?: number }
|
||||||
|
|
||||||
export interface ResumeSectionCfg {
|
export interface ResumeSectionCfg {
|
||||||
key: string; title: string; enabled: boolean;
|
key: string; title: string; enabled: boolean;
|
||||||
@@ -250,6 +250,8 @@ export interface ResumeSectionCfg {
|
|||||||
items?: ResumeItemCfg[];
|
items?: ResumeItemCfg[];
|
||||||
text?: string;
|
text?: string;
|
||||||
overrides?: Record<string, Partial<ResumeItemCfg>>;
|
overrides?: Record<string, Partial<ResumeItemCfg>>;
|
||||||
|
itemOrder?: string[];
|
||||||
|
hiddenFields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResumeConfig {
|
export interface ResumeConfig {
|
||||||
@@ -259,5 +261,6 @@ export interface ResumeConfig {
|
|||||||
fontScale: number;
|
fontScale: number;
|
||||||
density: 'compact' | 'normal' | 'relaxed';
|
density: 'compact' | 'normal' | 'relaxed';
|
||||||
onePage: boolean;
|
onePage: boolean;
|
||||||
|
header?: { showPhoto?: boolean; showContact?: boolean; showSchool?: boolean };
|
||||||
sections: ResumeSectionCfg[];
|
sections: ResumeSectionCfg[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ import {
|
|||||||
} from '@/lib/resume';
|
} from '@/lib/resume';
|
||||||
import { Button, Card, CardBody, CardHeader, Field, Input, Select, Textarea, useToast } from '@/components/ui';
|
import { Button, Card, CardBody, CardHeader, Field, Input, Select, Textarea, useToast } from '@/components/ui';
|
||||||
|
|
||||||
|
const INFO_KEY: Record<string, string> = {
|
||||||
|
姓名: 'name', 性别: 'gender', 出生年月: 'birth', 民族: 'nation', 籍贯: 'native',
|
||||||
|
政治面貌: 'political', 班级: 'className', 学号: 'studentId', 手机: 'phone', 邮箱: 'email',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 基本信息字段(可显隐)
|
||||||
|
const INFO_FIELDS: [string, string][] = [
|
||||||
|
['name', '姓名'], ['gender', '性别'], ['birth', '出生年月'], ['nation', '民族'],
|
||||||
|
['native', '籍贯'], ['political', '政治面貌'], ['className', '班级'], ['studentId', '学号'],
|
||||||
|
['phone', '手机'], ['email', '邮箱'],
|
||||||
|
];
|
||||||
|
|
||||||
// 2023-09 → 2023.9
|
// 2023-09 → 2023.9
|
||||||
function fmtEduDate(v: string | undefined): string {
|
function fmtEduDate(v: string | undefined): string {
|
||||||
const m = /^(\d{4})-(\d{1,2})/.exec(v ?? '');
|
const m = /^(\d{4})-(\d{1,2})/.exec(v ?? '');
|
||||||
@@ -129,8 +141,38 @@ export function ResumePage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addSection = (kind: 'text' | 'list') => {
|
||||||
|
const key = 'custom_' + Date.now();
|
||||||
|
persist({
|
||||||
|
...cfg,
|
||||||
|
sections: [...cfg.sections, kind === 'text'
|
||||||
|
? { key, title: '自定义板块', enabled: true, kind: 'text', text: '' }
|
||||||
|
: { key, title: '自定义经历', enabled: true, kind: 'list', items: [] }],
|
||||||
|
});
|
||||||
|
setOpenEditors(prev => new Set(prev).add(key));
|
||||||
|
};
|
||||||
|
|
||||||
const sectionTypes = (key: string) => RESUME_SECTION_DEFS.find(d => d.key === key)?.types?.join(' / ') ?? '固定板块';
|
const sectionTypes = (key: string) => RESUME_SECTION_DEFS.find(d => d.key === key)?.types?.join(' / ') ?? '固定板块';
|
||||||
|
|
||||||
|
const exportConfig = () => {
|
||||||
|
const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url; a.download = '简历配置.json'; a.click();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||||
|
toast('success', '简历配置已导出');
|
||||||
|
};
|
||||||
|
const importConfig = (f: File | undefined) => {
|
||||||
|
if (!f) return;
|
||||||
|
void f.text().then(txt => {
|
||||||
|
try {
|
||||||
|
const next = migrateResumeConfig(JSON.parse(txt));
|
||||||
|
persist(next);
|
||||||
|
toast('success', '配置已导入');
|
||||||
|
} catch { toast('error', '配置文件格式不正确'); }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-[1280px] px-3 py-4 sm:px-6 sm:py-5">
|
<div className="mx-auto max-w-[1280px] px-3 py-4 sm:px-6 sm:py-5">
|
||||||
<div className="no-print mb-4 flex flex-wrap items-center justify-between gap-2">
|
<div className="no-print mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
@@ -162,14 +204,18 @@ export function ResumePage() {
|
|||||||
<h1 className={'text-[26px] font-bold leading-tight ' + (cfg.template === 'tech' ? 'text-[var(--accent)]' : 'text-neutral-900')}>
|
<h1 className={'text-[26px] font-bold leading-tight ' + (cfg.template === 'tech' ? 'text-[var(--accent)]' : 'text-neutral-900')}>
|
||||||
{profile.name || '姓名'}
|
{profile.name || '姓名'}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-1.5 text-[12.5px] text-neutral-500">
|
{(cfg.header?.showSchool ?? true) && (
|
||||||
{[profile.school, profile.college, profile.major, profile.degree].filter(Boolean).join(' · ') || '完善个人信息后自动展示'}
|
<div className="mt-1.5 text-[12.5px] text-neutral-500">
|
||||||
</div>
|
{[profile.school, profile.college, profile.major, profile.degree].filter(Boolean).join(' · ') || '完善个人信息后自动展示'}
|
||||||
<div className="mt-1 text-[12px] text-neutral-500">
|
</div>
|
||||||
{[profile.phone, profile.email, profile.homepage].filter(Boolean).join(' · ')}
|
)}
|
||||||
</div>
|
{(cfg.header?.showContact ?? true) && (
|
||||||
|
<div className="mt-1 text-[12px] text-neutral-500">
|
||||||
|
{[profile.phone, profile.email, profile.homepage].filter(Boolean).join(' · ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{profile.photoUrl && (
|
{(cfg.header?.showPhoto ?? true) && profile.photoUrl && (
|
||||||
<img src={profile.photoUrl} alt="证件照" className="h-[110px] w-[82px] rounded border border-neutral-200 object-cover" />
|
<img src={profile.photoUrl} alt="证件照" className="h-[110px] w-[82px] rounded border border-neutral-200 object-cover" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +255,7 @@ export function ResumePage() {
|
|||||||
<div className="grid grid-cols-1 gap-x-8 gap-y-1 text-[12.5px] sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-x-8 gap-y-1 text-[12.5px] sm:grid-cols-2">
|
||||||
{([['姓名', profile.name], ['性别', profile.gender], ['出生年月', profile.birth], ['民族', profile.nation],
|
{([['姓名', profile.name], ['性别', profile.gender], ['出生年月', profile.birth], ['民族', profile.nation],
|
||||||
['籍贯', profile.native], ['政治面貌', profile.political], ['班级', profile.className], ['学号', profile.studentId],
|
['籍贯', profile.native], ['政治面貌', profile.political], ['班级', profile.className], ['学号', profile.studentId],
|
||||||
['手机', profile.phone], ['邮箱', profile.email]] as [string, string][]).filter(r => r[1]).map(r => (
|
['手机', profile.phone], ['邮箱', profile.email]] as [string, string][]).filter(r => r[1] && !(sec.hiddenFields ?? []).includes(INFO_KEY[r[0]])).map(r => (
|
||||||
<div key={r[0]} className="flex gap-2"><span className="w-16 shrink-0 text-neutral-500">{r[0]}</span><span>{r[1]}</span></div>
|
<div key={r[0]} className="flex gap-2"><span className="w-16 shrink-0 text-neutral-500">{r[0]}</span><span>{r[1]}</span></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +266,7 @@ export function ResumePage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className={cfg.density === 'compact' ? 'flex flex-col gap-1.5' : 'flex flex-col gap-3'}>
|
<div className={cfg.density === 'compact' ? 'flex flex-col gap-1.5' : 'flex flex-col gap-3'}>
|
||||||
{sec.items?.map((it, itemIdx) => (
|
{sec.items?.map((it, itemIdx) => (
|
||||||
<div key={(it.title ?? '') + (it.date ?? '') + itemIdx}>
|
<div key={it.itemKey ?? (it.title ?? '') + itemIdx}>
|
||||||
<div className="flex items-baseline justify-between gap-3">
|
<div className="flex items-baseline justify-between gap-3">
|
||||||
<span className="text-[13px] font-semibold text-neutral-900 r-item-title">
|
<span className="text-[13px] font-semibold text-neutral-900 r-item-title">
|
||||||
<Ed html={it.title} onCommit={v => handleEdit({ secKey: sec.key, field: 'title', value: v, itemIdx, fromRecord: it.fromRecord })} />
|
<Ed html={it.title} onCommit={v => handleEdit({ secKey: sec.key, field: 'title', value: v, itemIdx, fromRecord: it.fromRecord })} />
|
||||||
@@ -282,6 +328,20 @@ export function ResumePage() {
|
|||||||
options={[{ value: '0.9', label: '小' }, { value: '1', label: '标准' }, { value: '1.1', label: '大' }]} />
|
options={[{ value: '0.9', label: '小' }, { value: '1', label: '标准' }, { value: '1.1', label: '大' }]} />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
<Field label="头部布局">
|
||||||
|
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||||
|
{([['showPhoto', '证件照'], ['showContact', '联系行'], ['showSchool', '学校行']] as [string, string][]).map(t => {
|
||||||
|
const on = cfg.header?.[t[0] as 'showPhoto' | 'showContact' | 'showSchool'] ?? true;
|
||||||
|
return (
|
||||||
|
<button key={t[0]}
|
||||||
|
className={'rounded-md border px-2.5 py-1 text-[11.5px] transition-colors ' + (on ? 'border-brand bg-brand-subtle text-brand-text' : 'border-line bg-surface text-ink-3')}
|
||||||
|
onClick={() => persist({ ...cfg, header: { ...cfg.header, [t[0]]: !on } })}>
|
||||||
|
{t[1]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Field label="间距">
|
<Field label="间距">
|
||||||
<Select value={cfg.density} onChange={e => persist({ ...cfg, density: e.target.value as ResumeConfig['density'] })}
|
<Select value={cfg.density} onChange={e => persist({ ...cfg, density: e.target.value as ResumeConfig['density'] })}
|
||||||
@@ -294,6 +354,13 @@ export function ResumePage() {
|
|||||||
</button>
|
</button>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 border-t border-line pt-3">
|
||||||
|
<Button size="sm" variant="secondary" onClick={exportConfig}>导出配置</Button>
|
||||||
|
<label className="flex h-8 cursor-pointer items-center justify-center rounded-md border border-line bg-surface text-[12px] text-ink-2 transition-colors hover:border-line-strong">
|
||||||
|
导入配置
|
||||||
|
<input type="file" accept="application/json" className="hidden" onChange={e => { importConfig(e.target.files?.[0]); e.target.value = ''; }} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -347,6 +414,23 @@ export function ResumePage() {
|
|||||||
{s.kind === 'text' ? (
|
{s.kind === 'text' ? (
|
||||||
<Textarea rows={4} value={s.text ?? ''} placeholder="输入该板块内容…"
|
<Textarea rows={4} value={s.text ?? ''} placeholder="输入该板块内容…"
|
||||||
onChange={e => persist({ ...cfg, sections: cfg.sections.map(x => x.key === s.key ? { ...x, text: e.target.value } : x) })} />
|
onChange={e => persist({ ...cfg, sections: cfg.sections.map(x => x.key === s.key ? { ...x, text: e.target.value } : x) })} />
|
||||||
|
) : s.kind === 'info' ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="mb-0.5 text-[11px] text-ink-3">勾选 = 在基本信息中显示</div>
|
||||||
|
{INFO_FIELDS.map(f => (
|
||||||
|
<label key={f[0]} className="flex cursor-pointer items-center gap-2 text-[12px]">
|
||||||
|
<input type="checkbox" className="size-3.5"
|
||||||
|
checked={!(s.hiddenFields ?? []).includes(f[0])}
|
||||||
|
onChange={() => persist({ ...cfg, sections: cfg.sections.map(x => {
|
||||||
|
if (x.key !== s.key) return x;
|
||||||
|
const hid = new Set(x.hiddenFields ?? []);
|
||||||
|
if (hid.has(f[0])) hid.delete(f[0]); else hid.add(f[0]);
|
||||||
|
return { ...x, hiddenFields: [...hid] };
|
||||||
|
}) })} />
|
||||||
|
{f[1]}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
) : s.kind === 'list' ? (
|
) : s.kind === 'list' ? (
|
||||||
<ListSectionEditor
|
<ListSectionEditor
|
||||||
cfg={cfg} section={s} records={records} persist={persist}
|
cfg={cfg} section={s} records={records} persist={persist}
|
||||||
@@ -361,17 +445,12 @@ export function ResumePage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="p-3">
|
<div className="grid grid-cols-2 gap-2 p-3">
|
||||||
<Button size="sm" variant="secondary" className="w-full"
|
<Button size="sm" variant="secondary" onClick={() => addSection('text')}>
|
||||||
onClick={() => {
|
<Plus size={13} /> 文本板块
|
||||||
const key = 'custom_' + Date.now();
|
</Button>
|
||||||
persist({
|
<Button size="sm" variant="secondary" onClick={() => addSection('list')}>
|
||||||
...cfg,
|
<Plus size={13} /> 列表板块
|
||||||
sections: [...cfg.sections, { key, title: '自定义板块', enabled: true, kind: 'text', text: '' }],
|
|
||||||
});
|
|
||||||
setOpenEditors(prev => new Set(prev).add(key));
|
|
||||||
}}>
|
|
||||||
<Plus size={13} /> 新增文本板块
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -408,16 +487,54 @@ function ListSectionEditor({ cfg, section, records, persist, sectionTypes }: {
|
|||||||
setSection({ excluded: [...next] });
|
setSection({ excluded: [...next] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 合并视图:记录 + 自定义条目统一排序(itemOrder 优先,未排序按追加顺序)
|
||||||
|
const allKeys: string[] = [...(section.itemOrder ?? [])];
|
||||||
|
const seen = new Set(allKeys);
|
||||||
|
const rowMap = new Map<string, { type: 'record'; r: FootprintRow } | { type: 'custom'; c: ResumeItemCfg; idx: number }>();
|
||||||
|
derived.forEach(r => { const k = 'r' + r.id; rowMap.set(k, { type: 'record', r }); if (!seen.has(k)) { allKeys.push(k); seen.add(k); } });
|
||||||
|
customs.forEach((c, i) => { const k = c.id ?? 'c' + i; rowMap.set(k, { type: 'custom', c, idx: i }); if (!seen.has(k)) { allKeys.push(k); seen.add(k); } });
|
||||||
|
const moveKey = (k: string, dir: -1 | 1) => {
|
||||||
|
const i = allKeys.indexOf(k); const j = i + dir;
|
||||||
|
if (i < 0 || j < 0 || j >= allKeys.length) return;
|
||||||
|
[allKeys[i], allKeys[j]] = [allKeys[j], allKeys[i]];
|
||||||
|
setSection({ itemOrder: allKeys });
|
||||||
|
};
|
||||||
|
const ArrowBtns = ({ k }: { k: string }) => (
|
||||||
|
<span className="flex shrink-0 flex-col">
|
||||||
|
<button className="text-ink-3 hover:text-ink-1" aria-label="上移" onClick={() => moveKey(k, -1)}><ArrowUp size={11} /></button>
|
||||||
|
<button className="text-ink-3 hover:text-ink-1" aria-label="下移" onClick={() => moveKey(k, 1)}><ArrowDown size={11} /></button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
<div className="text-[11px] text-ink-3">来自记录的条目(勾选 = 显示)· 另可添加自定义条目</div>
|
<div className="text-[11px] text-ink-3">条目排序(↑↓)· 记录勾选显隐 · 自定义条目可增删</div>
|
||||||
{derived.map(r => (
|
{allKeys.map(k => {
|
||||||
<label key={r.id} className="flex cursor-pointer items-center gap-2 text-[12px]">
|
const row = rowMap.get(k);
|
||||||
<input type="checkbox" className="size-3.5" checked={!excluded.has(r.id)} onChange={() => toggleRecord(r.id)} />
|
if (!row) return null;
|
||||||
<span className="min-w-0 flex-1 truncate text-ink-1">{r.title}</span>
|
if (row.type === 'record') {
|
||||||
<span className="num shrink-0 text-[11px] text-ink-3">{(r.start_date ?? '').slice(0, 7)}</span>
|
const r = row.r;
|
||||||
</label>
|
return (
|
||||||
))}
|
<div key={k} className="flex items-center gap-2 text-[12px]">
|
||||||
|
<ArrowBtns k={k} />
|
||||||
|
<label className="flex min-w-0 flex-1 cursor-pointer items-center gap-2">
|
||||||
|
<input type="checkbox" className="size-3.5" checked={!excluded.has(r.id)} onChange={() => toggleRecord(r.id)} />
|
||||||
|
<span className="min-w-0 flex-1 truncate text-ink-1">{r.title}</span>
|
||||||
|
</label>
|
||||||
|
<span className="num shrink-0 text-[11px] text-ink-3">{(r.start_date ?? '').slice(0, 7)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={k} className="flex items-center gap-2 rounded-md border border-line bg-surface p-1.5 text-[12px]">
|
||||||
|
<ArrowBtns k={k} />
|
||||||
|
<span className="min-w-0 flex-1 truncate text-ink-1">{row.c.title}<span className="text-ink-3"> · {row.c.date ?? ''}</span></span>
|
||||||
|
<button className="text-ink-3 hover:text-danger" aria-label="删除条目"
|
||||||
|
onClick={() => setSection({ items: customs.filter((_, x) => x !== row.idx) })}>
|
||||||
|
<Trash2 size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<div className="mt-1 border-t border-line pt-2">
|
<div className="mt-1 border-t border-line pt-2">
|
||||||
<div className="mb-1.5 flex items-center justify-between">
|
<div className="mb-1.5 flex items-center justify-between">
|
||||||
@@ -438,7 +555,7 @@ function ListSectionEditor({ cfg, section, records, persist, sectionTypes }: {
|
|||||||
<Button size="sm" variant="primary" disabled={!adding.title.trim()}
|
<Button size="sm" variant="primary" disabled={!adding.title.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!adding.title.trim()) return;
|
if (!adding.title.trim()) return;
|
||||||
setSection({ items: [...customs, adding] });
|
setSection({ items: [...customs, { ...adding, id: 'c' + Date.now() }] });
|
||||||
setAdding(null);
|
setAdding(null);
|
||||||
}}>添加</Button>
|
}}>添加</Button>
|
||||||
<Button size="sm" variant="ghost" onClick={() => setAdding(null)}>取消</Button>
|
<Button size="sm" variant="ghost" onClick={() => setAdding(null)}>取消</Button>
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{r as g,f as v,j as t,S as j,t as S}from"./index-BCAZXiEu.js";import{C as c,a as d,b as x}from"./card-DaQiiTAq.js";import{u as N,i as u,a as m}from"./useChart-BXGoMcGj.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"lg:col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
import{r as g,f as v,j as t,S as j,t as S}from"./index-j5Xhw0AZ.js";import{C as c,a as d,b as x}from"./card-G_DTWQFR.js";import{u as N,i as u,a as m}from"./useChart-CcPk8-yZ.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-3 py-4 sm:px-6 sm:py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"lg:col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as I,f as T,j as e,E as $,w as H,u as N,d as f,r as p,g as b,x as W,y as J,B as j,P as F,C as A,z as U,A as B,D as Y,m as k,l as M,T as _,I as g,F as V,H as w,J as E,K as D}from"./index-BCAZXiEu.js";import{C,a as G,b as q}from"./card-DaQiiTAq.js";import{u as X,i as K,a as P}from"./useChart-BXGoMcGj.js";import{u as Z,i as ee,b as se,a as te,c as ae,d as re,e as ne,f as le}from"./echarts-BIWefrIc.js";/**
|
import{c as I,f as T,j as e,E as $,w as H,u as N,d as f,r as p,g as b,x as W,y as J,B as j,P as F,C as A,z as U,A as B,D as Y,m as k,l as M,T as _,I as g,F as V,H as w,J as E,K as D}from"./index-j5Xhw0AZ.js";import{C,a as G,b as q}from"./card-G_DTWQFR.js";import{u as X,i as K,a as P}from"./useChart-CcPk8-yZ.js";import{u as Z,i as ee,b as se,a as te,c as ae,d as re,e as ne,f as le}from"./echarts-BIWefrIc.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as E,u as P,d as S,r as p,f as F,g as q,j as e,B as f,l as B,ab as L,F as z,H as k,S as M,I as Q,ac as _,ad as K,ae as v,af as T}from"./index-BCAZXiEu.js";/**
|
import{c as E,u as P,d as S,r as p,f as F,g as q,j as e,B as f,l as B,ab as L,F as z,H as k,S as M,I as Q,ac as _,ad as K,ae as v,af as T}from"./index-j5Xhw0AZ.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as fe,j as e,a as N,r as s,b as he,u as ve,d as ge,e as je,f as L,g as ye,I as be,S as k,B as w,P as Q,h as te,L as se,G as ae,R as Ne,i as ne,k as ke,l as we,T as Ce,E as le,m as Ee,n as Se,o as _e,C as ze,p as qe,q as Le,s as Re,t as De,v as Ie}from"./index-BCAZXiEu.js";/**
|
import{c as fe,j as e,a as N,r as s,b as he,u as ve,d as ge,e as je,f as L,g as ye,I as be,S as k,B as w,P as Q,h as te,L as se,G as ae,R as Ne,i as ne,k as ke,l as we,T as Ce,E as le,m as Ee,n as Se,o as _e,C as ze,p as qe,q as Le,s as Re,t as De,v as Ie}from"./index-j5Xhw0AZ.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as D,u as K,d as F,f as L,r,g as I,j as e,B as p,P as q,M as O,m as _,l as H,T as P,C as R,N as G,F as U,H as i,I as j,O as W,S as z,Q as J,U as V,q as X}from"./index-BCAZXiEu.js";import{C as Y}from"./card-DaQiiTAq.js";/**
|
import{c as D,u as K,d as F,f as L,r,g as I,j as e,B as p,P as q,M as O,m as _,l as H,T as P,C as R,N as G,F as U,H as i,I as j,O as W,S as z,Q as J,U as V,q as X}from"./index-j5Xhw0AZ.js";import{C as Y}from"./card-G_DTWQFR.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
@@ -1 +1 @@
|
|||||||
import{j as s,a}from"./index-BCAZXiEu.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
import{j as s,a}from"./index-j5Xhw0AZ.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{r}from"./index-BCAZXiEu.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
import{r}from"./index-j5Xhw0AZ.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<link rel="icon" href="/icons/pwa-192.png" />
|
<link rel="icon" href="/icons/pwa-192.png" />
|
||||||
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
|
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
|
||||||
<title>足迹 Footprints</title>
|
<title>足迹 Footprints</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BCAZXiEu.js"></script>
|
<script type="module" crossorigin src="/assets/index-j5Xhw0AZ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B4Ww6089.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B4Ww6089.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user