ui: uniform badge sizing across record rows + fixed-width right-aligned label column in detail meta grid

This commit is contained in:
root
2026-10-17 11:25:49 +08:00
parent e0fcca75b1
commit 5ad9e8db64
15 changed files with 94 additions and 91 deletions
+9 -6
View File
@@ -1,6 +1,9 @@
import { Badge, NeutralBadge } from '@/components/ui'; import { Badge, NeutralBadge } from '@/components/ui';
import type { FootprintRow } from '@/lib/types'; import type { FootprintRow } from '@/lib/types';
// 记录徽章统一尺寸:同高 22px、最小宽 52px、居中、同字号 —— 整行排列规整
export const ROW_BADGE_CLS = 'inline-flex h-[22px] min-w-[52px] items-center justify-center gap-1 rounded-sm border px-2 text-[11.5px] font-medium whitespace-nowrap';
const STATUS_MAP: Record<string, { label: string; color: string; bg: string }> = { const STATUS_MAP: Record<string, { label: string; color: string; bg: string }> = {
draft: { label: '草稿', color: 'var(--text-3)', bg: 'var(--surface-3)' }, draft: { label: '草稿', color: 'var(--text-3)', bg: 'var(--surface-3)' },
ongoing: { label: '进行中', color: 'var(--brand-text)', bg: 'var(--brand-subtle)' }, ongoing: { label: '进行中', color: 'var(--brand-text)', bg: 'var(--brand-subtle)' },
@@ -12,7 +15,7 @@ export function StatusBadge({ status }: { status: string }) {
const s = STATUS_MAP[status] ?? STATUS_MAP.draft; const s = STATUS_MAP[status] ?? STATUS_MAP.draft;
return ( return (
<span <span
className="inline-flex items-center rounded-sm px-1.5 py-px text-[11px] leading-[18px] font-medium whitespace-nowrap" className={ROW_BADGE_CLS + ' border-transparent'}
style={{ color: s.color, backgroundColor: s.bg }} style={{ color: s.color, backgroundColor: s.bg }}
> >
{s.label} {s.label}
@@ -31,7 +34,7 @@ const LEVEL_COLORS: Record<string, string> = {
export function LevelBadge({ level }: { level?: string | null }) { export function LevelBadge({ level }: { level?: string | null }) {
if (!level) return null; if (!level) return null;
return <Badge color={LEVEL_COLORS[level] ?? 'var(--text-3)'}>{level}</Badge>; return <Badge color={LEVEL_COLORS[level] ?? 'var(--text-3)'} className={ROW_BADGE_CLS}>{level}</Badge>;
} }
const GRADE_COLORS: Record<string, string> = { const GRADE_COLORS: Record<string, string> = {
@@ -44,21 +47,21 @@ const GRADE_COLORS: Record<string, string> = {
export function GradeBadge({ grade }: { grade?: string | null }) { export function GradeBadge({ grade }: { grade?: string | null }) {
if (!grade) return null; if (!grade) return null;
return <Badge color={GRADE_COLORS[grade] ?? 'var(--text-3)'}>{grade}</Badge>; return <Badge color={GRADE_COLORS[grade] ?? 'var(--text-3)'} className={ROW_BADGE_CLS}>{grade}</Badge>;
} }
export function RoleBadge({ role }: { role?: string | null }) { export function RoleBadge({ role }: { role?: string | null }) {
if (!role) return null; if (!role) return null;
return <NeutralBadge>{role}</NeutralBadge>; return <NeutralBadge className={ROW_BADGE_CLS}>{role}</NeutralBadge>;
} }
export function NegativeBadge() { export function NegativeBadge() {
return <Badge color="var(--danger)">负面</Badge>; return <Badge color="var(--danger)" className={ROW_BADGE_CLS}>负面</Badge>;
} }
export function TypeBadge({ row }: { row: FootprintRow }) { export function TypeBadge({ row }: { row: FootprintRow }) {
return ( return (
<Badge color={row.type_color}> <Badge color={row.type_color} className={ROW_BADGE_CLS}>
{row.type_name} {row.type_name}
</Badge> </Badge>
); );
@@ -4,7 +4,7 @@ import { Pencil, Trash2, Paperclip, FileText } from 'lucide-react';
import { attachmentRead, attachmentThumb, footprintGet, listDimensions, listTypes } from '@/lib/api'; import { attachmentRead, attachmentThumb, footprintGet, listDimensions, listTypes } from '@/lib/api';
import type { Attachment, FormField } from '@/lib/types'; import type { Attachment, FormField } from '@/lib/types';
import { Badge, Button, Confirm, Dialog, useToast } from '@/components/ui'; import { Badge, Button, Confirm, Dialog, useToast } from '@/components/ui';
import { StatusBadge, LevelBadge, GradeBadge, RoleBadge, NegativeBadge } from '@/components/common/badges'; import { StatusBadge, LevelBadge, GradeBadge, RoleBadge, NegativeBadge, ROW_BADGE_CLS } from '@/components/common/badges';
import { AttachmentPreview } from '@/components/common/AttachmentPreview'; import { AttachmentPreview } from '@/components/common/AttachmentPreview';
import { makeThumb } from '@/lib/img'; import { makeThumb } from '@/lib/img';
import { fmtRange, fmtDateTime, fmtSize } from '@/lib/format'; import { fmtRange, fmtDateTime, fmtSize } from '@/lib/format';
@@ -135,13 +135,13 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
<div> <div>
<h2 className="text-[20px] font-bold leading-tight tracking-tight text-ink-1" style={{ textWrap: 'balance' }}>{d.title}</h2> <h2 className="text-[20px] font-bold leading-tight tracking-tight text-ink-1" style={{ textWrap: 'balance' }}>{d.title}</h2>
<div className="mt-2.5 flex flex-wrap items-center gap-1.5"> <div className="mt-2.5 flex flex-wrap items-center gap-1.5">
<Badge color={d.type_color}>{d.type_name}</Badge> <Badge color={d.type_color} className={ROW_BADGE_CLS}>{d.type_name}</Badge>
<LevelBadge level={d.level} /> <LevelBadge level={d.level} />
<GradeBadge grade={d.award_grade} /> <GradeBadge grade={d.award_grade} />
<RoleBadge role={d.role} /> <RoleBadge role={d.role} />
<StatusBadge status={d.status} /> <StatusBadge status={d.status} />
{d.polarity === 'negative' && <NegativeBadge />} {d.polarity === 'negative' && <NegativeBadge />}
{d.rank != null && <Badge color="var(--text-3)">第 {d.rank} 名</Badge>} {d.rank != null && <Badge color="var(--text-3)" className={ROW_BADGE_CLS}>第 {d.rank} 名</Badge>}
</div> </div>
</div> </div>
@@ -272,8 +272,8 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
function MetaItem({ label, value }: { label: string; value: string }) { function MetaItem({ label, value }: { label: string; value: string }) {
return ( return (
<div className="flex min-w-0 gap-2"> <div className="grid min-w-0 grid-cols-[76px_1fr] gap-x-2.5">
<span className="shrink-0 text-ink-3">{label}</span> <span className="shrink-0 text-right text-ink-3">{label}</span>
<span className="min-w-0 truncate font-medium text-ink-1" title={value}>{value}</span> <span className="min-w-0 truncate font-medium text-ink-1" title={value}>{value}</span>
</div> </div>
); );
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-C4hukKDs.js";import{C as c,a as d,b as x}from"./card-DIOCCjdD.js";import{u as N,i as u,a as m}from"./useChart-Cd4ARk7y.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-D1QXTKzW.js";import{C as c,a as d,b as x}from"./card-6oyQXitQ.js";import{u as N,i as u,a as m}from"./useChart-QAOCakHi.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-C4hukKDs.js";import{C as f,a as z,b as _}from"./card-DIOCCjdD.js";import{u as X,i as M,a as L}from"./useChart-Cd4ARk7y.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-D1QXTKzW.js";import{C as f,a as z,b as _}from"./card-6oyQXitQ.js";import{u as X,i as M,a as L}from"./useChart-QAOCakHi.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,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-C4hukKDs.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-D1QXTKzW.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
@@ -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-C4hukKDs.js";import{C as Y}from"./card-DIOCCjdD.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-D1QXTKzW.js";import{C as Y}from"./card-6oyQXitQ.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-C4hukKDs.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-D1QXTKzW.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-C4hukKDs.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-D1QXTKzW.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="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-C4hukKDs.js"></script> <script type="module" crossorigin src="/assets/index-D1QXTKzW.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D9dgRd9m.css"> <link rel="stylesheet" crossorigin href="/assets/index-7hm-Qv_H.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>