ui: type-aware editor form + detail reordered by importance

This commit is contained in:
root
2026-10-17 10:39:01 +08:00
parent 4a73fddd0c
commit 79dada4226
17 changed files with 519 additions and 465 deletions
@@ -131,7 +131,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
<div className="py-10 text-center text-[13px] text-ink-3">加载中…</div>
) : (
<div className="flex flex-col gap-4">
{/* 徽章行 */}
{/* 徽章行:类型 · 级别 · 奖等 · 角色 · 状态 · 名次 */}
<div className="flex flex-wrap items-center gap-1.5">
<Badge color={d.type_color}>{d.type_name}</Badge>
<LevelBadge level={d.level} />
@@ -142,15 +142,69 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
{d.rank != null && <Badge color="var(--text-3)">第 {d.rank} 名</Badge>}
</div>
{/* 基本信息网格 */}
<div className="grid grid-cols-2 gap-x-4 gap-y-2 rounded-md border border-line bg-surface-2 px-3 py-2.5 text-[12.5px]">
<MetaItem label="时间" value={fmtRange(d.start_date, d.end_date) || '—'} />
<MetaItem label="组织方" value={d.organizer || '—'} />
{d.group_id && <MetaItem label="事件组" value={d.group_id} />}
{d.parent_id != null && <MetaItem label="上级记录" value={'#' + d.parent_id} />}
<MetaItem label="创建于" value={fmtDateTime(d.created_at)} />
<MetaItem label="更新于" value={fmtDateTime(d.updated_at)} />
</div>
{/* 事项要素:时间/组织方 + 类型专属字段(按模板定义顺序 = 重要性顺序) */}
<Section title="事项要素">
<div className="grid grid-cols-2 gap-x-4 gap-y-2 rounded-md border border-line bg-surface-2 px-3 py-2.5 text-[12.5px]">
<MetaItem label="时间" value={fmtRange(d.start_date, d.end_date) || '—'} />
<MetaItem label="组织方" value={d.organizer || '—'} />
{fields.map(f => <MetaItem key={f.label} label={f.label} value={f.value} />)}
</div>
</Section>
{/* 附件佐证 */}
{(d.attachments?.length ?? 0) > 0 && (
<Section title={'附件(' + (d.attachments?.length ?? 0) + ')'}>
{atts.length ? (
<div className="flex flex-col gap-2.5">
{images.length > 0 && (
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
{images.map(a => (
<button
key={a.id}
className="group relative aspect-square overflow-hidden rounded-md border border-line bg-surface-2"
onClick={() => setPreviewAtt(a)}
title={'点击预览:' + a.file_name}
>
{imgMap[a.id] ? (
<img
src={imgMap[a.id]}
alt={a.file_name}
loading="lazy"
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.04]"
/>
) : (
<div className="h-full w-full animate-pulse bg-surface-3" />
)}
<span className="absolute inset-x-0 bottom-0 truncate bg-gradient-to-t from-black/60 to-transparent px-1.5 pb-1 pt-3 text-[10px] text-white opacity-0 transition-opacity group-hover:opacity-100">
{a.file_name}
</span>
</button>
))}
</div>
)}
{docs.length > 0 && (
<div className="flex flex-col gap-1">
{docs.map(a => (
<button
key={a.id}
className="flex items-center gap-2 rounded-md border border-line bg-surface px-2.5 py-2 text-left text-[12.5px] transition-colors hover:border-line-strong"
onClick={() => setPreviewAtt(a)}
>
<FileText size={14} className="shrink-0 text-ink-3" />
<span className="truncate text-ink-1">{a.file_name}</span>
<span className="num ml-auto shrink-0 text-ink-3">{fmtSize(a.size)}</span>
<Paperclip size={12} className="shrink-0 text-ink-3" />
</button>
))}
</div>
)}
<div className="text-[11px] text-ink-3">点击图片 / 文件可在线预览(图片 · PDF · 文本 · Word · Excel · 音视频)</div>
</div>
) : (
<p className="text-[12px] text-ink-3">暂无佐证附件</p>
)}
</Section>
)}
{d.description && (
<Section title="工作内容 / 描述">
@@ -164,14 +218,6 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
</Section>
)}
{fields.length > 0 && (
<Section title={type ? '类型字段 · ' + type.name : '类型字段'}>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2">
{fields.map(f => <MetaItem key={f.label} label={f.label} value={f.value} />)}
</div>
</Section>
)}
{(d.metrics && Object.keys(d.metrics).length > 0) && (
<Section title="量化指标">
<div className="flex flex-wrap gap-1.5">
@@ -197,57 +243,13 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
</Section>
)}
<Section title={'附件(' + (d.attachments?.length ?? 0) + ')'}>
{atts.length ? (
<div className="flex flex-col gap-2.5">
{images.length > 0 && (
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
{images.map(a => (
<button
key={a.id}
className="group relative aspect-square overflow-hidden rounded-md border border-line bg-surface-2"
onClick={() => setPreviewAtt(a)}
title={'点击预览:' + a.file_name}
>
{imgMap[a.id] ? (
<img
src={imgMap[a.id]}
alt={a.file_name}
loading="lazy"
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.04]"
/>
) : (
<div className="h-full w-full animate-pulse bg-surface-3" />
)}
<span className="absolute inset-x-0 bottom-0 truncate bg-gradient-to-t from-black/60 to-transparent px-1.5 pb-1 pt-3 text-[10px] text-white opacity-0 transition-opacity group-hover:opacity-100">
{a.file_name}
</span>
</button>
))}
</div>
)}
{docs.length > 0 && (
<div className="flex flex-col gap-1">
{docs.map(a => (
<button
key={a.id}
className="flex items-center gap-2 rounded-md border border-line bg-surface px-2.5 py-2 text-left text-[12.5px] transition-colors hover:border-line-strong"
onClick={() => setPreviewAtt(a)}
>
<FileText size={14} className="shrink-0 text-ink-3" />
<span className="truncate text-ink-1">{a.file_name}</span>
<span className="num ml-auto shrink-0 text-ink-3">{fmtSize(a.size)}</span>
<Paperclip size={12} className="shrink-0 text-ink-3" />
</button>
))}
</div>
)}
<div className="text-[11px] text-ink-3">点击图片 / 文件可在线预览(图片 · PDF · 文本 · Word · Excel · 音视频)</div>
</div>
) : (
<p className="text-[12px] text-ink-3">暂无佐证附件</p>
)}
</Section>
{/* 系统信息(最末位) */}
<div className="flex flex-wrap gap-x-4 gap-y-1 border-t border-line pt-3 text-[11.5px] text-ink-3">
<span>创建于 {fmtDateTime(d.created_at)}</span>
<span>更新于 {fmtDateTime(d.updated_at)}</span>
{d.group_id && <span>事件组 {d.group_id}</span>}
{d.parent_id != null && <span>上级 #{d.parent_id}</span>}
</div>
</div>
)}
@@ -11,6 +11,7 @@ import { Button, Dialog, Field, Input, Select, Textarea, useToast } from '@/comp
import { TagInput } from '@/components/common/TagInput';
import { DynamicForm, type TaxonomyOptionsMap } from '@/components/common/DynamicForm';
import { AttachmentPicker, fileToPending, type PendingAtt } from '@/components/common/AttachmentPicker';
import { getTypeProfile } from '@/lib/typeProfiles';
interface FormState {
type_id: number | null;
@@ -83,6 +84,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
() => typesQ.data?.items.find(t => t.id === form.type_id),
[typesQ.data, form.type_id]
);
const profile = getTypeProfile(selectedType?.name);
const parentQ = useQuery({
queryKey: ['parent-candidates', form.type_id],
@@ -182,6 +184,38 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
onSuccess: () => void qc.invalidateQueries({ queryKey: ['attachments', footprintId] })
});
const renderCoreField = (key: string) => {
if (key === 'organizer') return (
<Field label="组织方 / 所在组织" key="organizer">
<Input value={form.organizer} onChange={e => set('organizer', e.target.value)} placeholder="如:中国工业与应用数学学会" />
</Field>
);
if (key === 'role') return (
<Field label="角色" key="role">
<Select value={form.role} onChange={e => set('role', e.target.value)}
options={[{ value: '', label: '未设置' }, ...(taxonomies['role'] ?? [])]} />
</Field>
);
if (key === 'level') return (
<Field label="级别" key="level">
<Select value={form.level} onChange={e => set('level', e.target.value)}
options={[{ value: '', label: '未设置' }, ...(taxonomies['level'] ?? [])]} />
</Field>
);
if (key === 'award_grade') return (
<Field label="奖等" key="award_grade">
<Select value={form.award_grade} onChange={e => set('award_grade', e.target.value)}
options={[{ value: '', label: '未设置' }, ...GRADES.map(g => ({ value: g, label: g }))]} />
</Field>
);
if (key === 'rank') return (
<Field label="名次" key="rank">
<Input type="number" className="num" value={form.rank} onChange={e => set('rank', e.target.value)} placeholder="如 2" />
</Field>
);
return null;
};
return (
<Dialog
open={open}
@@ -216,60 +250,39 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-x-3 gap-y-3.5">
<Field label="组织方 / 所在组织">
<Input value={form.organizer} onChange={e => set('organizer', e.target.value)} placeholder="如:中国工业与应用数学学会" />
</Field>
<Field label="开始日期">
<Input type="date" value={form.start_date} onChange={e => set('start_date', e.target.value)} />
</Field>
<Field label="结束日期">
<Input type="date" value={form.end_date} onChange={e => set('end_date', e.target.value)} />
</Field>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-x-3 gap-y-3.5">
<Field label="角色">
<Select value={form.role} onChange={e => set('role', e.target.value)}
options={[{ value: '', label: '未设置' }, ...(taxonomies['role'] ?? [])]} />
</Field>
<Field label="级别">
<Select value={form.level} onChange={e => set('level', e.target.value)}
options={[{ value: '', label: '未设置' }, ...(taxonomies['level'] ?? [])]} />
</Field>
<Field label="状态">
<Select value={form.status} onChange={e => set('status', e.target.value)} options={STATUS_OPTS} />
</Field>
<Field label="重要度">
<Select value={form.weight} onChange={e => set('weight', e.target.value)}
options={[1, 2, 3, 4, 5].map(n => ({ value: String(n), label: String(n) }))} />
</Field>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-x-3 gap-y-3.5">
<Field label="奖等">
<Select value={form.award_grade} onChange={e => set('award_grade', e.target.value)}
options={[{ value: '', label: '未设置' }, ...GRADES.map(g => ({ value: g, label: g }))]} />
</Field>
<Field label="名次">
<Input type="number" className="num" value={form.rank} onChange={e => set('rank', e.target.value)} placeholder="如 2" />
</Field>
<Field label="性质">
<Select value={form.polarity} onChange={e => set('polarity', e.target.value)}
options={[{ value: 'positive', label: '正面' }, { value: 'negative', label: '负面' }]} />
</Field>
<Field label="上级记录" hint="晋升链">
<Select value={form.parent_id} onChange={e => set('parent_id', e.target.value)}
options={[
{ value: '', label: '无' },
...(parentQ.data?.items ?? [])
.filter(p => p.id !== footprintId)
.map(p => ({ value: String(p.id), label: p.title.slice(0, 24) }))
]}
/>
</Field>
<div className="border-t border-line pt-4">
<div className="mb-3 flex items-center gap-2 text-[12px] font-semibold text-ink-2">
类型要素{selectedType ? ' · ' + selectedType.name : ''}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-3.5">
{profile.core.map(k => renderCoreField(k))}
</div>
{selectedType && selectedType.form_template.length > 0 && (
<div className="mt-3.5">
<div className="mb-2 text-[11.5px] text-ink-3">专项信息</div>
<DynamicForm
fields={selectedType.form_template}
value={form.extras}
onChange={v => set('extras', v)}
taxonomies={taxonomies}
/>
</div>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-3.5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-3.5 border-t border-line pt-4">
<Field label="工作内容 / 描述">
<Textarea value={form.description} onChange={e => set('description', e.target.value)} placeholder="具体做了什么、承担什么分工" />
</Field>
@@ -278,7 +291,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
</Field>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-3.5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-3.5 border-t border-line pt-4">
<Field label="标签">
<TagInput value={form.tags} onChange={v => set('tags', v)} />
</Field>
@@ -311,25 +324,39 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
</Field>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-3">
<Field label="事件组 ID" hint="同一场事件的多条记录共享">
<Input value={form.group_id} onChange={e => set('group_id', e.target.value)} placeholder="如 g2025001(可选)" />
</Field>
</div>
{selectedType && selectedType.form_template.length > 0 && (
<div className="border-t border-line pt-4">
<div className="mb-3 text-[12px] font-semibold text-ink-2">
类型字段 · {selectedType.name}
</div>
<DynamicForm
fields={selectedType.form_template}
value={form.extras}
onChange={v => set('extras', v)}
taxonomies={taxonomies}
/>
<details className="border-t border-line pt-4">
<summary className="cursor-pointer select-none text-[12px] text-ink-3 transition-colors hover:text-ink-2">高级选项(重要度 · 名次 · 性质 · 关联)</summary>
<div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-x-3 gap-y-3.5">
<Field label="重要度">
<Select value={form.weight} onChange={e => set('weight', e.target.value)}
options={[1, 2, 3, 4, 5].map(n => ({ value: String(n), label: String(n) }))} />
</Field>
{!profile.core.includes('rank') && (
<Field label="名次">
<Input type="number" className="num" value={form.rank} onChange={e => set('rank', e.target.value)} placeholder="如 2" />
</Field>
)}
<Field label="性质">
<Select value={form.polarity} onChange={e => set('polarity', e.target.value)}
options={[{ value: 'positive', label: '正面' }, { value: 'negative', label: '负面' }]} />
</Field>
<Field label="上级记录" hint="晋升链">
<Select value={form.parent_id} onChange={e => set('parent_id', e.target.value)}
options={[
{ value: '', label: '无' },
...(parentQ.data?.items ?? [])
.filter(p => p.id !== footprintId)
.map(p => ({ value: String(p.id), label: p.title.slice(0, 24) }))
]}
/>
</Field>
</div>
)}
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-3">
<Field label="事件组 ID" hint="同一场事件的多条记录共享">
<Input value={form.group_id} onChange={e => set('group_id', e.target.value)} placeholder="如 g2025001(可选)" />
</Field>
</div>
</details>
{footprintId ? (
<div className="border-t border-line pt-4">
+25
View File
@@ -0,0 +1,25 @@
// 类型感知字段配置:不同类型的编辑表单与详情展示按此配置裁剪、排序
export interface TypeProfile {
/** 核心静态字段(类型要素区,按重要性排序) */
core: ('organizer' | 'role' | 'level' | 'award_grade' | 'rank')[];
hint?: string;
}
const PROFILES: Record<string, TypeProfile> = {
'竞赛': { core: ['level', 'award_grade', 'role', 'organizer'] },
'科研': { core: ['level', 'role', 'organizer'] },
'实习实践': { core: ['organizer'] },
'学生工作': { core: ['role', 'organizer'] },
'志愿公益': { core: ['organizer'] },
'文体活动': { core: ['level', 'organizer'] },
'证书技能': { core: ['level', 'organizer'] },
'荣誉惩处': { core: ['award_grade', 'level', 'organizer'] },
'学业': { core: ['rank'] },
};
const DEFAULT_PROFILE: TypeProfile = { core: ['level', 'role', 'award_grade', 'organizer'] };
export function getTypeProfile(typeName?: string | null): TypeProfile {
if (!typeName) return DEFAULT_PROFILE;
return PROFILES[typeName] ?? DEFAULT_PROFILE;
}
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-BxEr63go.js";import{C as c,a as d,b as x}from"./card-DASBiK00.js";import{u as N,i as u,a as m}from"./useChart-eUS_Zp3L.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:"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-BpXGbBE-.js";import{C as c,a as d,b as x}from"./card-DMi0z8Hh.js";import{u as N,i as u,a as m}from"./useChart-D4kPoPsx.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:"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,4 +1,4 @@
import{c as A,f as I,j as e,E as O,w as $,u as k,d as S,r as u,g as y,x as H,y as W,B as g,P as q,C as Q,z as J,A as T,D as U,m as N,l as G,T as E,I as j,F as Y,H as C,J as B,K as V}from"./index-BxEr63go.js";import{C as f,a as z,b as _}from"./card-DASBiK00.js";import{u as X,i as M,a as L}from"./useChart-eUS_Zp3L.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 A,f as I,j as e,E as O,w as $,u as k,d as S,r as u,g as y,x as H,y as W,B as g,P as q,C as Q,z as J,A as T,D as U,m as N,l as G,T as E,I as j,F as Y,H as C,J as B,K as V}from"./index-BpXGbBE-.js";import{C as f,a as z,b as _}from"./card-DMi0z8Hh.js";import{u as X,i as M,a as L}from"./useChart-D4kPoPsx.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
*
* This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
import{c as pe,j as e,a as b,r as s,b as fe,u as he,d as ve,e as ge,f as L,g as je,I as ye,S as N,B as k,P as Q,h as ee,L as te,G as se,R as be,i as ae,k as Ne,l as ke,T as we,E as ne,m as Ce,n as Ee,o as Se,C as _e,p as ze,q as qe,s as Le,t as Re,v as De}from"./index-BxEr63go.js";/**
import{c as pe,j as e,a as b,r as s,b as fe,u as he,d as ve,e as ge,f as L,g as je,I as ye,S as N,B as k,P as Q,h as ee,L as te,G as se,R as be,i as ae,k as Ne,l as ke,T as we,E as ne,m as Ce,n as Ee,o as Se,C as _e,p as ze,q as qe,s as Le,t as Re,v as De}from"./index-BpXGbBE-.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
@@ -1,4 +1,4 @@
import{c as Q,u as A,d as z,f as B,r as i,g as T,j as e,B as p,P as q,M as F,m as E,l as D,T as I,C as K,N as H,F as L,H as r,I as j,O as G,S as R,Q as W,q as J}from"./index-BxEr63go.js";import{C as U}from"./card-DASBiK00.js";/**
import{c as Q,u as A,d as z,f as B,r as i,g as T,j as e,B as p,P as q,M as F,m as E,l as D,T as I,C as K,N as H,F as L,H as r,I as j,O as G,S as R,Q as W,q as J}from"./index-BpXGbBE-.js";import{C as U}from"./card-DMi0z8Hh.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-BxEr63go.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-BpXGbBE-.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
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r}from"./index-BxEr63go.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-BpXGbBE-.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
@@ -8,8 +8,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-BxEr63go.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B60u283l.css">
<script type="module" crossorigin src="/assets/index-BpXGbBE-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D6K8zE2F.css">
</head>
<body>
<div id="root"></div>