feat: 企业级设计系统 v3 — 高级侧边栏/排版/阴影/动效

This commit is contained in:
root
2026-10-16 17:46:03 +08:00
parent aea94de9e5
commit 13c3f19691
14 changed files with 203 additions and 193 deletions
+57 -52
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { CalendarDays, Rows3, ChartColumn, GraduationCap, Shapes, SlidersHorizontal, LogIn, ShieldCheck, Cloud } from 'lucide-react'; import { CalendarDays, Rows3, ChartColumn, GraduationCap, Shapes, SlidersHorizontal, LogIn, LogOut, ShieldCheck, Cloud } from 'lucide-react';
import { serverMe, clearToken, getToken } from '@/lib/api'; import { serverMe, clearToken, getToken } from '@/lib/api';
import { LoginDialog } from '@/components/auth/LoginDialog'; import { LoginDialog } from '@/components/auth/LoginDialog';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -13,25 +13,26 @@ const NAV = [
{ to: '/timeline', label: '时间线', icon: CalendarDays }, { to: '/timeline', label: '时间线', icon: CalendarDays },
{ to: '/records', label: '记录', icon: Rows3 }, { to: '/records', label: '记录', icon: Rows3 },
{ to: '/grades', label: '成绩', icon: GraduationCap }, { to: '/grades', label: '成绩', icon: GraduationCap },
{ to: '/dashboard', label: '统计', icon: ChartColumn } { to: '/dashboard', label: '统计', icon: ChartColumn },
] ],
}, },
{ {
section: '配置', section: '配置',
items: [ items: [
{ to: '/admin', label: '管理', icon: ShieldCheck }, { to: '/admin', label: '管理', icon: ShieldCheck },
{ to: '/types', label: '记录类型', icon: Shapes }, { to: '/types', label: '记录类型', icon: Shapes },
{ to: '/taxonomy', label: '字典与维度', icon: SlidersHorizontal } { to: '/taxonomy', label: '字典与维度', icon: SlidersHorizontal },
] ],
} },
]; ];
function LogoMark() { function LogoMark() {
return ( return (
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" aria-hidden> <svg width='28' height='28' viewBox='0 0 26 26' fill='none' aria-hidden>
<rect x="1" y="1" width="24" height="24" rx="6" fill="var(--brand)" /> <defs><linearGradient id='lg' x1='0' y1='0' x2='0' y2='26'><stop stopColor='#4359e8'/><stop offset='1' stopColor='#2a3ab8'/></linearGradient></defs>
<ellipse cx="11" cy="10" rx="4.2" ry="5.2" fill="#fff" opacity="0.95" transform="rotate(-18 11 10)" /> <rect x='1' y='1' width='24' height='24' rx='7' fill='url(#lg)'/>
<circle cx="17.5" cy="17.5" r="3.1" fill="#fff" opacity="0.7" /> <ellipse cx='11' cy='10' rx='4.2' ry='5.2' fill='#fff' opacity='.95' transform='rotate(-18 11 10)'/>
<circle cx='17.5' cy='17.5' r='3.1' fill='#fff' opacity='.7'/>
</svg> </svg>
); );
} }
@@ -42,47 +43,51 @@ export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void
queryKey: ['me'], queryKey: ['me'],
queryFn: () => serverMe(), queryFn: () => serverMe(),
enabled: !!getToken(), enabled: !!getToken(),
retry: 0 retry: 0,
}); });
const [loginOpen, setLoginOpen] = useState(false);
const me = meQ.data ?? null; const me = meQ.data ?? null;
const [loginOpen, setLoginOpen] = useState(false);
return ( return (
<aside <aside
className={ className={cn(
'flex w-[224px] shrink-0 flex-col border-r border-line bg-surface' + 'flex w-[236px] shrink-0 flex-col border-r border-line bg-surface',
' max-lg:fixed max-lg:inset-y-0 max-lg:left-0 max-lg:z-40 max-lg:transition-transform' + 'max-lg:fixed max-lg:inset-y-0 max-lg:left-0 max-lg:z-40 max-lg:transition-transform',
(open ? ' max-lg:translate-x-0' : ' max-lg:-translate-x-full') open ? 'max-lg:translate-x-0' : 'max-lg:-translate-x-full',
} )}
> >
<div className="flex h-14 items-center gap-2.5 border-b border-line px-4"> <LoginDialog open={loginOpen} onClose={() => setLoginOpen(false)} defaultUrl='https://footpr.linyux.cn' />
{/* Logo */}
<div className="flex h-[60px] items-center gap-3 border-b border-line px-5">
<LogoMark /> <LogoMark />
<div className="leading-tight"> <div className="leading-tight">
<div className="text-[14px] font-semibold text-ink-1">足迹</div> <div className="text-[15px] font-bold tracking-tight text-ink-1">足迹</div>
<div className="text-[10px] font-medium tracking-[0.18em] text-ink-3">FOOTPRINTS</div> <div className="text-[9px] font-medium tracking-[0.24em] text-ink-3">FOOTPRINTS</div>
</div> </div>
</div> </div>
<LoginDialog open={loginOpen} onClose={() => setLoginOpen(false)} defaultUrl={'https://footpr.linyux.cn'} />
<nav className="flex-1 overflow-y-auto px-2 py-3"> {/* 导航 */}
{NAV.map(group => ( <nav className="flex-1 overflow-y-auto px-2.5 py-4">
<div key={group.section} className="mb-4"> {NAV.map((group) => (
<div className="px-2 pb-1.5 text-[11px] font-medium text-ink-3">{group.section}</div> <div key={group.section} className="mb-5">
<div className="px-2.5 pb-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-ink-3">{group.section}</div>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
{group.items.map(item => ( {group.items.map((item) => (
<NavLink <NavLink
key={item.to} key={item.to}
to={item.to} to={item.to}
onClick={onClose} onClick={onClose}
className={({ isActive }) => className={({ isActive }) =>
cn( cn(
'flex h-8 items-center gap-2.5 rounded-md px-2 text-[13px] transition-colors', 'relative flex h-[34px] items-center gap-3 rounded-lg px-3 text-[13px] font-medium transition-all duration-150',
isActive isActive
? 'bg-brand-subtle font-medium text-brand-text' ? 'bg-brand-subtle text-brand-text nav-active'
: 'text-ink-2 hover:bg-surface-3 hover:text-ink-1' : 'text-ink-2 hover:bg-surface-3 hover:text-ink-1',
) )
} }
> >
<item.icon size={15} strokeWidth={2} /> <item.icon size={16} strokeWidth={1.8} />
{item.label} {item.label}
</NavLink> </NavLink>
))} ))}
@@ -91,43 +96,43 @@ export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void
))} ))}
</nav> </nav>
<div className="border-t border-line px-4 py-3"> {/* 左下角:账号区 */}
{/* 左下角:账号区(云端模式) */} <div className="border-t border-line px-3 py-3">
{getToken() && me ? ( {getToken() && me ? (
<div className="flex flex-col gap-1"> <div className="rounded-lg bg-surface-2 px-3 py-2.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<Cloud size={13} className="shrink-0 text-success" /> <div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-brand text-[10px] font-bold text-white">
<div className="min-w-0 truncate text-[12px] font-medium text-ink-1" title={me.username}> {me.username.charAt(0).toUpperCase()}
{me.is_admin && <span className="mr-0.5 text-brand-text">管理员</span>} </div>
{me.username} <div className="min-w-0">
<div className="truncate text-[12px] font-semibold text-ink-1" title={me.username}>{me.username}</div>
{me.is_admin && <div className='text-[10px] text-brand-text'>管理员</div>}
</div> </div>
</div> </div>
<button <button
className="shrink-0 text-[11px] text-ink-3 underline transition-colors hover:text-danger" className="shrink-0 rounded p-1 text-ink-3 transition-colors hover:bg-surface-3 hover:text-danger"
title="退出登录"
onClick={() => { clearToken(); qc.clear(); window.location.reload(); }} onClick={() => { clearToken(); qc.clear(); window.location.reload(); }}
> >
退出 <LogOut size={13} />
</button> </button>
</div> </div>
<div className="flex items-center gap-1 text-[10px] text-success"> <div className="mt-1.5 flex items-center gap-1 text-[10px] text-success">
<Cloud size={10} /> <Cloud size={10} />
云端同步已开启 云端同步已开启
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-1.5"> <button
<button className="flex w-full items-center justify-center gap-2 rounded-lg border border-brand/30 bg-brand-subtle px-3 py-2 text-[13px] font-medium text-brand-text transition-all hover:shadow-brand"
className="flex w-full items-center gap-1.5 rounded-md bg-brand-subtle px-2 py-1.5 text-[12.5px] font-medium text-brand-text transition-colors hover:brightness-[0.97]" onClick={() => setLoginOpen(true)}
onClick={() => setLoginOpen(true)} >
> <LogIn size={14} />
<LogIn size={13} /> 登录我的档案
登录账号 </button>
</button>
<div className="text-[10px] text-ink-3">登录后数据云端互通</div>
</div>
)} )}
</div> </div>
</aside> </aside>
); );
} }
+77 -77
View File
@@ -1,10 +1,5 @@
@import 'tailwindcss'; @import 'tailwindcss';
/* ============================================
Footprints 设计系统 v2 —— 企业级视觉规范
栅格 4px · 控件高 28px · 正文 14px · 圆角 6/8
============================================ */
@theme inline { @theme inline {
--color-bg: var(--bg); --color-bg: var(--bg);
--color-surface: var(--surface); --color-surface: var(--surface);
@@ -26,58 +21,61 @@
--color-danger: var(--danger); --color-danger: var(--danger);
--color-danger-subtle: var(--danger-subtle); --color-danger-subtle: var(--danger-subtle);
--radius-sm: 4px; --radius-sm: 5px;
--radius-md: 6px; --radius-md: 8px;
--radius-lg: 8px; --radius-lg: 12px;
--shadow-card: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.05); --shadow-card: 0 1px 2px rgba(0,0,0,.03), 0 2px 8px rgba(0,0,0,.04);
--shadow-pop: 0 12px 32px rgba(16, 24, 40, 0.16), 0 2px 8px rgba(16, 24, 40, 0.08); --shadow-pop: 0 4px 12px rgba(0,0,0,.08), 0 16px 48px rgba(0,0,0,.12);
--shadow-brand: 0 2px 8px rgba(48,86,211,.28);
} }
/* ---------- 亮色 ---------- */ /* ── 亮色 ── */
:root { :root {
--bg: #f4f5f7; --bg: #f3f4f6;
--surface: #ffffff; --surface: #ffffff;
--surface-2: #f9fafb; --surface-2: #f8f9fa;
--surface-3: #eff1f4; --surface-3: #eceef1;
--border: #e5e8ec; --border: #e3e6ea;
--border-strong: #ccd3dc; --border-strong: #c8cdd6;
--text-1: #15181d; --text-1: #111318;
--text-2: #454e5c; --text-2: #3d434f;
--text-3: #7e8798; --text-3: #747c8a;
--brand: #3056d3; --brand: #3346d3;
--brand-hover: #2748b8; --brand-hover: #2a3ab8;
--brand-subtle: #eef2ff; --brand-subtle: #eef1ff;
--brand-text: #2748b8; --brand-text: #2a3ab8;
--success: #15803d; --success: #177245;
--success-subtle: #ecfdf3; --success-subtle: #ebfaf1;
--warning: #b45309; --warning: #a86509;
--warning-subtle: #fffaeb; --warning-subtle: #fef8eb;
--danger: #dc2626; --danger: #d5304f;
--danger-subtle: #fef3f2; --danger-subtle: #fef0f2;
color-scheme: light;
} }
/* ---------- 暗色 ---------- */ /* ── 暗色 ── */
.dark { .dark {
--bg: #0e1013; --bg: #0c0d10;
--surface: #15181e; --surface: #141519;
--surface-2: #1a1e25; --surface-2: #1a1c22;
--surface-3: #22272f; --surface-3: #23262e;
--border: #262c35; --border: #262932;
--border-strong: #3a424f; --border-strong: #363b47;
--text-1: #e8ebf1; --text-1: #e9ebf0;
--text-2: #a2abb9; --text-2: #a0a8b8;
--text-3: #6d7684; --text-3: #68707e;
--brand: #7c93f5; --brand: #6d8bfa;
--brand-hover: #94a8f8; --brand-hover: #8aa2fb;
--brand-subtle: #1c2340; --brand-subtle: #1a2144;
--brand-text: #a3b4f8; --brand-text: #96adfa;
--success: #4ade80; --success: #34d399;
--success-subtle: #12251a; --success-subtle: #0d2921;
--warning: #fbbf24; --warning: #fbbf24;
--warning-subtle: #2a2213; --warning-subtle: #2b2109;
--danger: #f87171; --danger: #f87171;
--danger-subtle: #2c1618; --danger-subtle: #2d1317;
color-scheme: dark;
} }
html, body, #root { height: 100%; } html, body, #root { height: 100%; }
@@ -85,51 +83,53 @@ html, body, #root { height: 100%; }
body { body {
background: var(--bg); background: var(--bg);
color: var(--text-1); color: var(--text-1);
font-family: system-ui, 'Segoe UI Variable Text', 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', 'PingFang SC', sans-serif; font-family: 'Inter Variable', 'Inter', system-ui, -apple-system, 'Segoe UI Variable Text', 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', 'PingFang SC', sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.55; line-height: 1.55;
letter-spacing: 0.005em; letter-spacing: 0.003em;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
} }
/* 标题排版:紧凑字距 */ h1, h2, h3 { letter-spacing: -0.022em; text-wrap: balance; }
h1, h2, h3 {
letter-spacing: -0.01em;
text-wrap: balance;
}
/* 数字统一等宽,对齐敏感场景(表格/指标卡) */ .num { font-variant-numeric: tabular-nums lining-nums; font-feature-settings: 'tnum' 1, 'lnum' 1; }
.num {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1;
}
::selection { background: color-mix(in srgb, var(--brand) 22%, transparent); } ::selection { background: color-mix(in srgb, var(--brand) 18%, transparent); }
/* 滚动条:更纤细,悬停加深 */ * { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--text-3) 30%, transparent) transparent; }
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } *::-webkit-scrollbar { width: 5px; height: 5px; }
*::-webkit-scrollbar { width: 6px; height: 6px; } *::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--text-3) 25%, transparent); border-radius: 3px; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } *::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--text-3) 45%, transparent); }
*::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
*::-webkit-scrollbar-track { background: transparent; } *::-webkit-scrollbar-track { background: transparent; }
:focus-visible { :focus-visible {
outline: 2px solid color-mix(in srgb, var(--brand) 55%, transparent); outline: 2px solid color-mix(in srgb, var(--brand) 50%, transparent);
outline-offset: 1px; outline-offset: 2px;
border-radius: inherit;
} }
.animate-in { animation: fp-fade-slide-in 0.18s ease-out; } /* 微动效 */
.animate-in { animation: fp-in .22s cubic-bezier(.16,1,.3,1); }
@keyframes fp-in { from { opacity: 0; transform: translateY(6px) scale(.98); } to { opacity: 1; transform: none; } }
@keyframes fp-fade-slide-in { .animate-fade { animation: fp-fade .3s ease-out; }
from { opacity: 0; transform: translateY(4px); } @keyframes fp-fade { from { opacity: 0; } to { opacity: 1; } }
to { opacity: 1; transform: none; }
}
/* 数字输入框去掉浏览器步进按钮的视觉噪音 */ /* 数字输入去噪 */
input[type='number']::-webkit-outer-spin-button, input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button { input[type='number']::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
-webkit-appearance: none;
margin: 0;
}
input[type='number'] { -moz-appearance: textfield; appearance: textfield; } input[type='number'] { -moz-appearance: textfield; appearance: textfield; }
/* 卡片悬停微抬 */
.hover-lift { transition: transform .2s cubic-bezier(.16,1,.3,1), box-shadow .2s ease; }
.hover-lift:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,.04); }
/* 侧边栏激活指示条 */
.nav-active::before {
content: '';
position: absolute; left: 0; top: 6px; bottom: 6px; width: 3px;
border-radius: 0 3px 3px 0; background: var(--brand);
}
@@ -1,5 +1,5 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-BPypTgG9.js","assets/index-D8_RHt64.css"])))=>i.map(i=>d[i]); const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/index-DfnNvRlO.js","assets/index-CY5BkXpe.css"])))=>i.map(i=>d[i]);
import{c as Q,u as M,r as w,e as d,Y as r,f as N,j as e,E as c,B as t,P as B,A as l,i as F,T as K,Z as P,_ as R,$ as S,a0 as L,a1 as U,a2 as A}from"./index-BPypTgG9.js";import{C,a as q,b as z}from"./card-DtBTl1yb.js";/** import{c as Q,u as M,r as w,e as d,Y as r,f as N,j as e,E as c,B as t,P as B,A as l,i as F,T as K,Z as P,_ as R,$ as S,a0 as L,a1 as U,a2 as A}from"./index-DfnNvRlO.js";import{C,a as q,b as z}from"./card-D7onCjYF.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.
@@ -9,4 +9,4 @@ import{c as Q,u as M,r as w,e as d,Y as r,f as N,j as e,E as c,B as t,P as B,A a
* *
* This source code is licensed under the ISC license. * This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree. * See the LICENSE file in the root directory of this source tree.
*/const V=[["path",{d:"M18 21a8 8 0 0 0-16 0",key:"3ypg7q"}],["circle",{cx:"10",cy:"8",r:"5",key:"o932ke"}],["path",{d:"M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3",key:"10s06x"}]],W=Q("UsersRound",V);function a(){return localStorage.getItem("fp-server-url")||""}function O(){var p,y,j,k,f,g,_,b;const i=M(),[o,n]=w.useState(!1),m=d({queryKey:["me"],queryFn:()=>L(()=>import("./index-BPypTgG9.js").then(s=>s.a3),__vite__mapDeps([0,1])).then(s=>s.serverMe(a())),enabled:!!r()}),x=d({queryKey:["admin-users"],queryFn:()=>U(a()),enabled:!!r()}),u=d({queryKey:["admin-invites"],queryFn:()=>A(a()),enabled:!!r()}),T=N({mutationFn:s=>R(s,a()),onSuccess:()=>{i.invalidateQueries({queryKey:["admin-invites"]}),n(!1)},onError:()=>n(!1)}),E=N({mutationFn:s=>S(s,a()),onSuccess:()=>void i.invalidateQueries({queryKey:["admin-invites"]})});if(m.data&&!m.data.is_admin)return e.jsx(c,{title:"需要管理员账号",description:"当前登录账号不是管理员。"});const v=((y=(p=u.data)==null?void 0:p.data)==null?void 0:y.invites)??[],h=((k=(j=x.data)==null?void 0:j.data)==null?void 0:k.users)??[],I=((g=(f=u.data)==null?void 0:f.data)==null?void 0:g.total)??0;return e.jsxs("div",{className:"mx-auto max-w-[1080px] px-6 py-5",children:[e.jsxs("div",{className:"mb-4 flex items-center justify-between",children:[e.jsx("h2",{className:"text-[15px] font-semibold text-ink-1",children:"管理"}),e.jsx(t,{variant:"secondary",onClick:()=>{i.invalidateQueries()},children:"刷新"})]}),e.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[e.jsxs(C,{children:[e.jsx(q,{title:"邀请码("+I+")",extra:e.jsxs(t,{size:"sm",variant:"primary",disabled:o,onClick:()=>{n(!0),T.mutate(5)},children:[e.jsx(B,{size:13})," 批量生成 5 个"]})}),e.jsx(z,{className:"p-0",children:v.length?e.jsx("div",{className:"divide-y divide-line max-h-[420px] overflow-y-auto",children:v.map(s=>e.jsxs("div",{className:"flex items-center gap-2 px-4 py-2 text-[12.5px]",children:[e.jsx("code",{className:"num rounded-sm px-1.5 py-0.5 "+(s.valid?"bg-success-subtle text-success":"bg-surface-3 text-ink-3"),children:s.code}),s.valid?e.jsx(l,{color:"var(--success)",children:"可用"}):e.jsx(l,{color:"var(--text-3)",children:"已使用"}),s.used_by_name&&e.jsxs("span",{className:"text-ink-3 truncate",children:["被 ",s.used_by_name," 使用于 ",F(s.used_at)]}),!s.used_by&&e.jsxs("span",{className:"ml-auto flex gap-1",children:[e.jsx(t,{size:"sm",variant:"secondary",onClick:()=>{navigator.clipboard.writeText(s.code)},children:"复制"}),!s.valid&&e.jsx(t,{size:"icon-sm",variant:"ghost","aria-label":"作废",className:"text-ink-3",onClick:()=>E.mutate(s.id),children:e.jsx(K,{size:13})})]})]},s.id))}):e.jsx(c,{icon:e.jsx(H,{size:32,strokeWidth:1.5}),title:"暂无邀请码",description:"生成后发给需要注册的用户。"})})]}),e.jsxs(C,{children:[e.jsx(q,{title:"用户("+(((b=(_=x.data)==null?void 0:_.data)==null?void 0:b.total)??0)+")"}),e.jsx(z,{className:"p-0",children:h.length?e.jsx("div",{className:"divide-y divide-line max-h-[420px] overflow-y-auto",children:h.map(s=>e.jsxs("div",{className:"flex items-center gap-2 px-4 py-2 text-[12.5px]",children:[e.jsx("span",{className:"font-medium text-ink-1",children:s.username}),s.is_admin&&e.jsx(l,{color:"var(--brand-text)",children:"管理员"}),e.jsxs("span",{className:"num ml-auto text-ink-3",children:[s.devices," 设备 · ",s.records," 记录 · ",P(s.attachment_bytes)]})]},s.id))}):e.jsx(c,{icon:e.jsx(W,{size:32,strokeWidth:1.5}),title:"暂无用户"})})]})]}),o&&e.jsx("div",{className:"mt-3 text-[12px] text-ink-3",children:"生成中…"})]})}export{O as AdminPage}; */const V=[["path",{d:"M18 21a8 8 0 0 0-16 0",key:"3ypg7q"}],["circle",{cx:"10",cy:"8",r:"5",key:"o932ke"}],["path",{d:"M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3",key:"10s06x"}]],W=Q("UsersRound",V);function a(){return localStorage.getItem("fp-server-url")||""}function O(){var p,y,j,k,f,g,_,b;const i=M(),[o,n]=w.useState(!1),m=d({queryKey:["me"],queryFn:()=>L(()=>import("./index-DfnNvRlO.js").then(s=>s.a3),__vite__mapDeps([0,1])).then(s=>s.serverMe(a())),enabled:!!r()}),x=d({queryKey:["admin-users"],queryFn:()=>U(a()),enabled:!!r()}),u=d({queryKey:["admin-invites"],queryFn:()=>A(a()),enabled:!!r()}),T=N({mutationFn:s=>R(s,a()),onSuccess:()=>{i.invalidateQueries({queryKey:["admin-invites"]}),n(!1)},onError:()=>n(!1)}),E=N({mutationFn:s=>S(s,a()),onSuccess:()=>void i.invalidateQueries({queryKey:["admin-invites"]})});if(m.data&&!m.data.is_admin)return e.jsx(c,{title:"需要管理员账号",description:"当前登录账号不是管理员。"});const v=((y=(p=u.data)==null?void 0:p.data)==null?void 0:y.invites)??[],h=((k=(j=x.data)==null?void 0:j.data)==null?void 0:k.users)??[],I=((g=(f=u.data)==null?void 0:f.data)==null?void 0:g.total)??0;return e.jsxs("div",{className:"mx-auto max-w-[1080px] px-6 py-5",children:[e.jsxs("div",{className:"mb-4 flex items-center justify-between",children:[e.jsx("h2",{className:"text-[15px] font-semibold text-ink-1",children:"管理"}),e.jsx(t,{variant:"secondary",onClick:()=>{i.invalidateQueries()},children:"刷新"})]}),e.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[e.jsxs(C,{children:[e.jsx(q,{title:"邀请码("+I+")",extra:e.jsxs(t,{size:"sm",variant:"primary",disabled:o,onClick:()=>{n(!0),T.mutate(5)},children:[e.jsx(B,{size:13})," 批量生成 5 个"]})}),e.jsx(z,{className:"p-0",children:v.length?e.jsx("div",{className:"divide-y divide-line max-h-[420px] overflow-y-auto",children:v.map(s=>e.jsxs("div",{className:"flex items-center gap-2 px-4 py-2 text-[12.5px]",children:[e.jsx("code",{className:"num rounded-sm px-1.5 py-0.5 "+(s.valid?"bg-success-subtle text-success":"bg-surface-3 text-ink-3"),children:s.code}),s.valid?e.jsx(l,{color:"var(--success)",children:"可用"}):e.jsx(l,{color:"var(--text-3)",children:"已使用"}),s.used_by_name&&e.jsxs("span",{className:"text-ink-3 truncate",children:["被 ",s.used_by_name," 使用于 ",F(s.used_at)]}),!s.used_by&&e.jsxs("span",{className:"ml-auto flex gap-1",children:[e.jsx(t,{size:"sm",variant:"secondary",onClick:()=>{navigator.clipboard.writeText(s.code)},children:"复制"}),!s.valid&&e.jsx(t,{size:"icon-sm",variant:"ghost","aria-label":"作废",className:"text-ink-3",onClick:()=>E.mutate(s.id),children:e.jsx(K,{size:13})})]})]},s.id))}):e.jsx(c,{icon:e.jsx(H,{size:32,strokeWidth:1.5}),title:"暂无邀请码",description:"生成后发给需要注册的用户。"})})]}),e.jsxs(C,{children:[e.jsx(q,{title:"用户("+(((b=(_=x.data)==null?void 0:_.data)==null?void 0:b.total)??0)+")"}),e.jsx(z,{className:"p-0",children:h.length?e.jsx("div",{className:"divide-y divide-line max-h-[420px] overflow-y-auto",children:h.map(s=>e.jsxs("div",{className:"flex items-center gap-2 px-4 py-2 text-[12.5px]",children:[e.jsx("span",{className:"font-medium text-ink-1",children:s.username}),s.is_admin&&e.jsx(l,{color:"var(--brand-text)",children:"管理员"}),e.jsxs("span",{className:"num ml-auto text-ink-3",children:[s.devices," 设备 · ",s.records," 记录 · ",P(s.attachment_bytes)]})]},s.id))}):e.jsx(c,{icon:e.jsx(W,{size:32,strokeWidth:1.5}),title:"暂无用户"})})]})]}),o&&e.jsx("div",{className:"mt-3 text-[12px] text-ink-3",children:"生成中…"})]})}export{O as AdminPage};
@@ -1 +1 @@
import{r as g,e as v,j as t,S as j,s as S}from"./index-BPypTgG9.js";import{C as c,a as d,b as x}from"./card-DtBTl1yb.js";import{u as N,i as u,a as m}from"./useChart-ODBxrUQE.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-6 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,e as v,j as t,S as j,s as S}from"./index-DfnNvRlO.js";import{C as c,a as d,b as x}from"./card-D7onCjYF.js";import{u as N,i as u,a as m}from"./useChart-Dn2X0nrY.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-6 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 K,e as I,j as e,E as O,t as $,u as k,b as S,r as u,f as y,v as H,w as W,B as g,P as q,C as Q,x as J,y as T,z as U,A as N,k as G,T as E,I as j,D as Y,F as C,H as B,J as V}from"./index-BPypTgG9.js";import{C as f,a as z,b as _}from"./card-DtBTl1yb.js";import{u as X,i as M,a as L}from"./useChart-ODBxrUQE.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 K,e as I,j as e,E as O,t as $,u as k,b as S,r as u,f as y,v as H,w as W,B as g,P as q,C as Q,x as J,y as T,z as U,A as N,k as G,T as E,I as j,D as Y,F as C,H as B,J as V}from"./index-DfnNvRlO.js";import{C as f,a as z,b as _}from"./card-D7onCjYF.js";import{u as X,i as M,a as L}from"./useChart-Dn2X0nrY.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 re,j as e,a as u,r as t,u as oe,b as ce,d as de,e as C,f as ue,I as xe,S as m,B as b,P as Y,g as pe,L as me,G as ve,R as he,h as je,i as ge,k as fe,T as ye,E as be,l as Ne,m as ke,C as Se,n as we,o as Ce,p as Ee,s as qe,q as _e}from"./index-BPypTgG9.js";/** import{c as re,j as e,a as u,r as t,u as oe,b as ce,d as de,e as C,f as ue,I as xe,S as m,B as b,P as Y,g as pe,L as me,G as ve,R as he,h as je,i as ge,k as fe,T as ye,E as be,l as Ne,m as ke,C as Se,n as we,o as Ce,p as Ee,s as qe,q as _e}from"./index-DfnNvRlO.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 P,u as Q,b as z,e as B,r as i,f as T,j as e,B as p,P as F,K as q,A as E,k as D,T as I,C as K,M as L,D as G,F as r,I as j,N as H,S as R,O as W,o as J}from"./index-BPypTgG9.js";import{C as U}from"./card-DtBTl1yb.js";/** import{c as P,u as Q,b as z,e as B,r as i,f as T,j as e,B as p,P as F,K as q,A as E,k as D,T as I,C as K,M as L,D as G,F as r,I as j,N as H,S as R,O as W,o as J}from"./index-DfnNvRlO.js";import{C as U}from"./card-D7onCjYF.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-BPypTgG9.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-DfnNvRlO.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
@@ -1 +1 @@
import{r}from"./index-BPypTgG9.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-DfnNvRlO.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-BPypTgG9.js"></script> <script type="module" crossorigin src="/assets/index-DfnNvRlO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D8_RHt64.css"> <link rel="stylesheet" crossorigin href="/assets/index-CY5BkXpe.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>