feat: editable gpa formula via bands, resume edu format per template, dedupe sections, word photo size, text justify

This commit is contained in:
root
2026-10-17 14:17:43 +08:00
parent b48c36fdb3
commit 40661aa685
22 changed files with 127 additions and 88 deletions
+8 -5
View File
@@ -338,6 +338,9 @@ function openDb(dbPath) {
d.exec(fs.readFileSync(path.join(__dirname, '..', 'schema.sql'), 'utf8'));
// 个人信息表兜底(确保任何挂载形态下都存在)
d.exec("CREATE TABLE IF NOT EXISTS profile (id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT, content TEXT NOT NULL DEFAULT '{}', deleted_at TEXT, origin TEXT, hlc TEXT)");
// 绩点公式:梯度带携带每档绩点(gp),各校规则不同可自行修改
const hasGp = d.prepare("SELECT COUNT(*) AS c FROM pragma_table_info('grade_band') WHERE name='gp'").get().c;
if (Number(hasGp) === 0) d.exec("ALTER TABLE grade_band ADD COLUMN gp REAL");
migrateSync(d);
ensureBuiltin(d);
alignBuiltinUuids(d);
@@ -889,7 +892,7 @@ function listFootprints(d, query) {
if (score == null) return null;
let hit = bands.find(b => score >= b.min && score < b.max);
if (!hit) hit = bands.find(b => score >= b.min && score <= b.max);
return hit ? { id: hit.id, label: hit.label, color: hit.color } : null;
return hit ? { id: hit.id, label: hit.label, color: hit.color, gp: hit.gp ?? null } : null;
};
const semesters = d.prepare('SELECT * FROM semester WHERE ' + DEL + ' ORDER BY term DESC').all();
const activeSemIds = new Set(semesters.map(s => s.id));
@@ -1007,14 +1010,14 @@ function listFootprints(d, query) {
withTx(d, () => {
if (id) {
const h = hlcNext(d);
d.prepare('UPDATE grade_band SET label=?, color=?, min=?, max=?, sort=?, hlc=? WHERE id=?')
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, h, id);
d.prepare('UPDATE grade_band SET label=?, color=?, min=?, max=?, sort=?, gp=?, hlc=? WHERE id=?')
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, p.gp ?? null, h, id);
touchOplog(d, 'grade_band', getUuid(d, 'grade_band', id), 'upsert', h);
} else {
const h = hlcNext(d);
const uuid = uuidv7();
id = Number(d.prepare('INSERT INTO grade_band (label, color, min, max, sort, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?)')
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, uuid, h, getDeviceId(d)).lastInsertRowid);
id = Number(d.prepare('INSERT INTO grade_band (label, color, min, max, sort, gp, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?,?)')
.run(label, p.color ?? '#64748b', p.min, p.max, p.sort ?? 100, p.gp ?? null, uuid, h, getDeviceId(d)).lastInsertRowid);
touchOplog(d, 'grade_band', uuid, 'upsert', h);
}
});
+14 -7
View File
@@ -65,8 +65,11 @@ export function migrateResumeConfig(raw: unknown): ResumeConfig {
if (d.key === 'self' && !base.text) base.text = SEED.self.text;
return base;
});
// v1 里用户新增的板块(不在默认定义中)→ 追加为自定义文本板块
// 旧版本已被取代的板块 key:直接丢弃,避免残留造成内容重复
const KNOWN_LEGACY = new Set(['awards', 'competition', 'practice', 'research']);
// 用户新增的自定义板块(不在默认与遗留定义中)→ 追加为自定义文本板块
for (const o of oldSections) {
if (KNOWN_LEGACY.has(o.key)) continue;
if (!sections.some(s => s.key === o.key)) {
sections.push({ key: o.key, title: o.title || o.key, enabled: o.enabled ?? true, kind: 'text', text: '' });
}
@@ -116,6 +119,7 @@ export function assembleSections(
): AssembledSection[] {
const out: AssembledSection[] = [];
const claimed = claimedTypes();
const usedRecordIds = new Set<number>(); // 全局去重:一条记录只出现一次
for (const s of config.sections) {
if (!s.enabled) continue;
@@ -123,7 +127,9 @@ export function assembleSections(
if (s.key === 'info') { out.push({ key: s.key, title: s.title, kind: 'info' }); continue; }
if (s.key === 'self' || s.kind === 'text') {
out.push({ key: s.key, title: s.title, kind: 'text', text: s.text ?? '' });
const txt = s.text ?? '';
if (!txt.trim()) continue; // 空板块不渲染
out.push({ key: s.key, title: s.title, kind: 'text', text: txt });
continue;
}
@@ -132,13 +138,14 @@ export function assembleSections(
let derived: FootprintRow[] = [];
const types = def?.types;
if (types) {
derived = records.filter(r => types.includes(r.type_name) && r.status !== 'draft' && !excluded.has(r.id));
derived = records.filter(r => types.includes(r.type_name) && r.status !== 'draft' && !excluded.has(r.id) && !usedRecordIds.has(r.id));
} else {
// 其他桶:未归类的类型
derived = records.filter(r => r.status !== 'draft' && !excluded.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))
.map(it => ({ ...it, ...(s.overrides?.[String(it.fromRecord)] ?? {}) }));
items.forEach(it => { if (it.fromRecord) usedRecordIds.add(it.fromRecord); });
for (const c of s.items ?? []) items.push({ ...c });
items.sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''));
@@ -185,8 +192,8 @@ export function resumeToWordHtml(
const schoolLine = [P.school, P.college, P.major, P.degree].filter(v => v !== '').map(v => esc(v)).join(' · ');
const contactLine = [P.phone, P.email, P.homepage].filter(v => v !== '').map(v => esc(v)).join(' · ');
const photo = photoUrl && P.photoUrl
? '<td style="width:' + (90 * scale).toFixed(0) + 'pt;text-align:right;vertical-align:top">' +
'<img src="' + esc(P.photoUrl) + '" alt="证件照" style="width:' + (90 * scale).toFixed(0) + 'pt;height:' + (120 * scale).toFixed(0) + 'pt;object-fit:cover" /></td>'
? '<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>'
: '';
const head = '<table style="width:100%;border-collapse:collapse"><tr>' +
@@ -232,7 +239,7 @@ export function resumeToWordHtml(
} else if (sec.kind === 'list') {
inner = '<div>' + (sec.items ?? []).map(itemHtml).join('') + '</div>';
} else {
inner = '<p style="font-size:' + pt(12) + ';line-height:1.7;margin:0">' + esc(sec.text ?? '').replace(/\n/g, '<br/>') + '</p>';
inner = '<p style="font-size:' + pt(12) + ';line-height:1.7;margin:0;text-align:justify">' + esc(sec.text ?? '').replace(/\n/g, '<br/>') + '</p>';
}
return '<div style="' + titleCss[cfg.template] + '">' + esc(sec.title) + '</div>' + inner;
}).join('');
+1
View File
@@ -195,6 +195,7 @@ export interface GradeBand {
min: number;
max: number;
sort: number;
gp?: number | null;
}
export interface BandInfo { id: number; label: string; color: string; }
+17 -5
View File
@@ -17,8 +17,17 @@ function termYear(term: string): string {
return m ? m[1] + '~' + m[2] + ' 学年' : '其他学期';
}
function autoGpa(courses: CourseGrade[]): number | null {
const withGp = courses.filter(c => c.grade_point != null && c.credit > 0);
function autoGpa(courses: CourseGrade[], bands?: { min: number; max: number; gp?: number | null }[]): number | null {
// 按梯度公式补算缺失绩点:分数落在哪一档 → 该档绩点(各校规则可在梯度设置中修改)
const gpOf = (c: CourseGrade): number | null => {
if (c.grade_point != null) return c.grade_point;
if (bands && c.score != null) {
const hit = bands.find(b => c.score! >= b.min && (c.score! < b.max || (b.max >= 100 && c.score! <= b.max)));
if (hit && hit.gp != null) return hit.gp;
}
return null;
};
const withGp = courses.map(c => ({ ...c, grade_point: gpOf(c) })).filter(c => c.grade_point != null && c.credit > 0);
if (!withGp.length) return null;
const credits = withGp.reduce((a, c) => a + c.credit, 0);
const weighted = withGp.reduce((a, c) => a + (c.grade_point as number) * c.credit, 0);
@@ -169,7 +178,7 @@ function BandEditor({ bands }: { bands: GradeBand[] }) {
<Card>
<CardHeader
title="成绩梯度"
extra={<span className="text-[11px] text-ink-3">按 分数 ≥ min 且 &lt; max 定级,满分档含上限</span>}
extra={<span className="text-[11px] text-ink-3">各校规则不同:每档可设定级与绩点,「自动绩点」按此计算</span>}
/>
<CardBody className="p-0">
<div className="divide-y divide-line">
@@ -187,7 +196,7 @@ function BandEditor({ bands }: { bands: GradeBand[] }) {
onCancel={() => setAdding(null)}
/>
) : (
<Button size="sm" variant="secondary" onClick={() => setAdding({ id: 0, label: '', color: '#3056d3', min: 0, max: 60, sort: (bands.at(-1)?.sort ?? 0) + 10 })}>
<Button size="sm" variant="secondary" onClick={() => setAdding({ id: 0, label: '', color: '#3056d3', min: 0, max: 60, gp: null, sort: (bands.at(-1)?.sort ?? 0) + 10 })}>
<Plus size={13} /> 添加等级
</Button>
)}
@@ -233,6 +242,8 @@ function BandRow({ band, onSave, onDelete, onChange, onCancel, isNew }: {
<Input type="number" className="num h-6 w-[64px] border-0 bg-transparent p-0 text-right" value={String(value.min)} onChange={e => set({ min: Number(e.target.value) })} />
<span className="text-[11.5px] text-ink-3">—</span>
<Input type="number" className="num h-6 w-[64px] border-0 bg-transparent p-0" value={String(value.max)} onChange={e => set({ max: Number(e.target.value) })} />
<span className="ml-1 text-[11px] text-ink-3">绩点</span>
<Input type="number" step="0.1" className="num h-6 w-[56px] border-0 bg-transparent p-0" value={value.gp != null ? String(value.gp) : ''} placeholder="—" onChange={e => set({ gp: e.target.value === '' ? null : Number(e.target.value) })} />
</div>
<div className="flex items-center gap-1.5 sm:ml-auto">
{isNew ? (
@@ -258,6 +269,7 @@ function BandRow({ band, onSave, onDelete, onChange, onCancel, isNew }: {
function SemesterCard({ sem }: { sem: SemesterGrades }) {
const qc = useQueryClient();
const toast = useToast();
const overviewQ = useQuery({ queryKey: ['grades'], queryFn: gradesOverview });
const [editingId, setEditingId] = useState<number | null>(null);
const [deleting, setDeleting] = useState<CourseGrade | null>(null);
const [semDialog, setSemDialog] = useState(false);
@@ -271,7 +283,7 @@ function SemesterCard({ sem }: { sem: SemesterGrades }) {
const autoGpaMut = useMutation({
mutationFn: () => {
const gpa = autoGpa(sem.courses);
const gpa = autoGpa(sem.courses, overviewQ.data?.bands);
if (gpa == null) throw new Error('请先为课程填写绩点');
return semesterUpsert({ id: sem.id, term: sem.term, gpa, rank: sem.rank ?? null, total_students: sem.total_students ?? null });
},
+26 -10
View File
@@ -9,6 +9,12 @@ import {
} from '@/lib/resume';
import { Button, Card, CardBody, CardHeader, Field, Input, Select, Textarea, useToast } from '@/components/ui';
// 2023-09 → 2023.9
function fmtEduDate(v: string | undefined): string {
const m = /^(\d{4})-(\d{1,2})/.exec(v ?? '');
return m ? m[1] + '.' + Number(m[2]) : (v ?? '');
}
// 预览内联编辑组件:点击即改,失焦保存
function Ed({ html, onCommit, className }: { html: string; onCommit: (v: string) => void; className?: string }) {
const esc = html.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -51,6 +57,7 @@ export function ResumePage() {
const records: FootprintRow[] = listQ.data?.items ?? [];
const cfg: ResumeConfig = config ?? defaultResumeConfig();
const previewProfile: Partial<ProfileInfo> = { ...profile, selfIntro };
const eduRange = [fmtEduDate(profile.eduStart), fmtEduDate(profile.eduEnd)].filter(Boolean).join('-');
const sections = useMemo(() => assembleSections(records, cfg), [records, cfg]);
const move = (i: number, dir: -1 | 1) => setConfig(pc => {
@@ -174,16 +181,25 @@ export function ResumePage() {
</div>
{sec.kind === 'edu' ? (
<div>
<div className="grid grid-cols-1 gap-x-8 gap-y-1 text-[12.5px] sm:grid-cols-2">
{([['学校', profile.school, 'school'], ['专业', profile.major, 'major'], ['学制', profile.eduLength, 'eduLength'], ['学历', profile.degree, 'degree'], ['入学', profile.eduStart, 'eduStart'], ['毕业', profile.eduEnd, 'eduEnd']] as [string, string | undefined, keyof ProfileInfo][])
.filter(r => r[1]).map(r => (
<div key={r[0]} className="flex gap-2"><span className="w-16 shrink-0 text-neutral-500">{r[0]}</span>
<Ed html={r[1] ?? ''} onCommit={v => saveProfileField(r[2], v)} />
</div>
))}
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-0.5 text-[13px]">
<span className="num shrink-0 text-neutral-500">
<Ed html={eduRange} onCommit={v => {
const m = /(\d{4})\.(\d{1,2})\s*[-—~至]\s*(\d{4})\.(\d{1,2})/.exec(v);
if (!m) { toast('error', '时间格式:2023.9-2027.6'); return; }
void profileUpsert({ info: { ...previewProfile, eduStart: m[1] + '-' + m[2].padStart(2, '0'), eduEnd: m[3] + '-' + m[4].padStart(2, '0') } })
.then(() => profileQ.refetch()).catch(e => toast('error', e.message));
}} />
</span>
<span className="min-w-0 font-semibold text-neutral-900">
<Ed html={profile.school ?? ''} onCommit={v => saveProfileField('school', v)} />
</span>
<span className="shrink-0 text-neutral-600">
<Ed html={profile.major ?? ''} onCommit={v => saveProfileField('major', v)} />
{profile.degree ? <span> | <Ed html={profile.degree} onCommit={v => saveProfileField('degree', v)} /></span> : null}
</span>
</div>
{profile.majorCourses && (
<p className="mt-2.5 text-[12.5px] leading-relaxed text-neutral-700">
<p className="mt-1.5 text-[12px] leading-relaxed text-justify text-neutral-700">
<b>主修课程:</b>
<Ed html={profile.majorCourses} onCommit={v => saveProfileField('majorCourses', v)} />
</p>
@@ -198,7 +214,7 @@ export function ResumePage() {
))}
</div>
) : sec.kind === 'text' ? (
<p className={'whitespace-pre-wrap text-neutral-700 ' + (cfg.density === 'compact' ? '' : 'leading-relaxed')}>
<p className={'whitespace-pre-wrap text-justify text-neutral-700 ' + (cfg.density === 'compact' ? '' : 'leading-relaxed')}>
<Ed html={sec.text ?? ''} onCommit={v => handleEdit({ secKey: sec.key, field: 'text', value: v })} />
</p>
) : (
@@ -215,7 +231,7 @@ export function ResumePage() {
</div>
{it.subtitle && <div className="text-[12px] text-neutral-600">{it.subtitle}</div>}
{it.desc && (
<p className={'text-[12px] text-neutral-500 ' + (cfg.density === 'compact' ? '' : 'mt-0.5')}>
<p className={'text-justify text-[12px] text-neutral-500 ' + (cfg.density === 'compact' ? '' : 'mt-0.5')}>
<Ed html={it.desc} onCommit={v => handleEdit({ secKey: sec.key, field: 'desc', value: v, itemIdx, fromRecord: it.fromRecord })} />
</p>
)}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as g,f as v,j as t,S as j,t as S}from"./index-CIeBS4lG.js";import{C as c,a as d,b as x}from"./card-_C0OnRIz.js";import{u as N,i as u,a as m}from"./useChart-Cowo5MEh.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-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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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-CIeBS4lG.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-BCAZXiEu.js";/**
* @license lucide-react v0.475.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -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-CIeBS4lG.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-BCAZXiEu.js";/**
* @license lucide-react v0.475.0 - ISC
*
* 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
File diff suppressed because one or more lines are too long
@@ -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-CIeBS4lG.js";import{C as Y}from"./card-_C0OnRIz.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-BCAZXiEu.js";import{C as Y}from"./card-DaQiiTAq.js";/**
* @license lucide-react v0.475.0 - ISC
*
* This source code is licensed under the ISC license.
@@ -1 +1 @@
import{j as s,a}from"./index-CIeBS4lG.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-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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r}from"./index-CIeBS4lG.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-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};
+2 -2
View File
@@ -9,8 +9,8 @@
<link rel="icon" href="/icons/pwa-192.png" />
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
<title>足迹 Footprints</title>
<script type="module" crossorigin src="/assets/index-CIeBS4lG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-tIg5eBzC.css">
<script type="module" crossorigin src="/assets/index-BCAZXiEu.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B4Ww6089.css">
</head>
<body>
<div id="root"></div>