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 { NavLink } from 'react-router-dom';
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 { LoginDialog } from '@/components/auth/LoginDialog';
import { cn } from '@/lib/utils';
@@ -13,25 +13,26 @@ const NAV = [
{ to: '/timeline', label: '时间线', icon: CalendarDays },
{ to: '/records', label: '记录', icon: Rows3 },
{ to: '/grades', label: '成绩', icon: GraduationCap },
{ to: '/dashboard', label: '统计', icon: ChartColumn }
]
{ to: '/dashboard', label: '统计', icon: ChartColumn },
],
},
{
section: '配置',
items: [
{ to: '/admin', label: '管理', icon: ShieldCheck },
{ to: '/types', label: '记录类型', icon: Shapes },
{ to: '/taxonomy', label: '字典与维度', icon: SlidersHorizontal }
]
}
{ to: '/taxonomy', label: '字典与维度', icon: SlidersHorizontal },
],
},
];
function LogoMark() {
return (
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" aria-hidden>
<rect x="1" y="1" width="24" height="24" rx="6" fill="var(--brand)" />
<ellipse cx="11" cy="10" rx="4.2" ry="5.2" fill="#fff" opacity="0.95" transform="rotate(-18 11 10)" />
<circle cx="17.5" cy="17.5" r="3.1" fill="#fff" opacity="0.7" />
<svg width='28' height='28' viewBox='0 0 26 26' fill='none' aria-hidden>
<defs><linearGradient id='lg' x1='0' y1='0' x2='0' y2='26'><stop stopColor='#4359e8'/><stop offset='1' stopColor='#2a3ab8'/></linearGradient></defs>
<rect x='1' y='1' width='24' height='24' rx='7' fill='url(#lg)'/>
<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>
);
}
@@ -42,47 +43,51 @@ export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void
queryKey: ['me'],
queryFn: () => serverMe(),
enabled: !!getToken(),
retry: 0
retry: 0,
});
const [loginOpen, setLoginOpen] = useState(false);
const me = meQ.data ?? null;
const [loginOpen, setLoginOpen] = useState(false);
return (
<aside
className={
'flex w-[224px] 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' +
(open ? ' max-lg:translate-x-0' : ' max-lg:-translate-x-full')
}
className={cn(
'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',
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 />
<div className="leading-tight">
<div className="text-[14px] font-semibold text-ink-1">足迹</div>
<div className="text-[10px] font-medium tracking-[0.18em] text-ink-3">FOOTPRINTS</div>
<div className="text-[15px] font-bold tracking-tight text-ink-1">足迹</div>
<div className="text-[9px] font-medium tracking-[0.24em] text-ink-3">FOOTPRINTS</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 => (
<div key={group.section} className="mb-4">
<div className="px-2 pb-1.5 text-[11px] font-medium text-ink-3">{group.section}</div>
{/* 导航 */}
<nav className="flex-1 overflow-y-auto px-2.5 py-4">
{NAV.map((group) => (
<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">
{group.items.map(item => (
{group.items.map((item) => (
<NavLink
key={item.to}
to={item.to}
onClick={onClose}
className={({ isActive }) =>
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
? 'bg-brand-subtle font-medium text-brand-text'
: 'text-ink-2 hover:bg-surface-3 hover:text-ink-1'
? 'bg-brand-subtle text-brand-text nav-active'
: '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}
</NavLink>
))}
@@ -91,43 +96,43 @@ export function Sidebar({ open, onClose }: { open: boolean; onClose: () => void
))}
</nav>
<div className="border-t border-line px-4 py-3">
{/* 左下角:账号区(云端模式) */}
{/* 左下角:账号区 */}
<div className="border-t border-line px-3 py-3">
{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 gap-1.5 min-w-0">
<Cloud size={13} className="shrink-0 text-success" />
<div className="min-w-0 truncate text-[12px] font-medium text-ink-1" title={me.username}>
{me.is_admin && <span className="mr-0.5 text-brand-text">管理员</span>}
{me.username}
<div className="flex items-center gap-2 min-w-0">
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-brand text-[10px] font-bold text-white">
{me.username.charAt(0).toUpperCase()}
</div>
<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>
<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(); }}
>
退出
<LogOut size={13} />
</button>
</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} />
云端同步已开启
</div>
</div>
) : (
<div className="flex flex-col gap-1.5">
<button
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)}
>
<LogIn size={13} />
登录账号
</button>
<div className="text-[10px] text-ink-3">登录后数据云端互通</div>
</div>
<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"
onClick={() => setLoginOpen(true)}
>
<LogIn size={14} />
登录我的档案
</button>
)}
</div>
</aside>
);
}
}
+77 -77
View File
@@ -1,10 +1,5 @@
@import 'tailwindcss';
/* ============================================
Footprints 设计系统 v2 —— 企业级视觉规范
栅格 4px · 控件高 28px · 正文 14px · 圆角 6/8
============================================ */
@theme inline {
--color-bg: var(--bg);
--color-surface: var(--surface);
@@ -26,58 +21,61 @@
--color-danger: var(--danger);
--color-danger-subtle: var(--danger-subtle);
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-sm: 5px;
--radius-md: 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-pop: 0 12px 32px rgba(16, 24, 40, 0.16), 0 2px 8px rgba(16, 24, 40, 0.08);
--shadow-card: 0 1px 2px rgba(0,0,0,.03), 0 2px 8px rgba(0,0,0,.04);
--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 {
--bg: #f4f5f7;
--bg: #f3f4f6;
--surface: #ffffff;
--surface-2: #f9fafb;
--surface-3: #eff1f4;
--border: #e5e8ec;
--border-strong: #ccd3dc;
--text-1: #15181d;
--text-2: #454e5c;
--text-3: #7e8798;
--brand: #3056d3;
--brand-hover: #2748b8;
--brand-subtle: #eef2ff;
--brand-text: #2748b8;
--success: #15803d;
--success-subtle: #ecfdf3;
--warning: #b45309;
--warning-subtle: #fffaeb;
--danger: #dc2626;
--danger-subtle: #fef3f2;
--surface-2: #f8f9fa;
--surface-3: #eceef1;
--border: #e3e6ea;
--border-strong: #c8cdd6;
--text-1: #111318;
--text-2: #3d434f;
--text-3: #747c8a;
--brand: #3346d3;
--brand-hover: #2a3ab8;
--brand-subtle: #eef1ff;
--brand-text: #2a3ab8;
--success: #177245;
--success-subtle: #ebfaf1;
--warning: #a86509;
--warning-subtle: #fef8eb;
--danger: #d5304f;
--danger-subtle: #fef0f2;
color-scheme: light;
}
/* ---------- 暗色 ---------- */
/* ── 暗色 ── */
.dark {
--bg: #0e1013;
--surface: #15181e;
--surface-2: #1a1e25;
--surface-3: #22272f;
--border: #262c35;
--border-strong: #3a424f;
--text-1: #e8ebf1;
--text-2: #a2abb9;
--text-3: #6d7684;
--brand: #7c93f5;
--brand-hover: #94a8f8;
--brand-subtle: #1c2340;
--brand-text: #a3b4f8;
--success: #4ade80;
--success-subtle: #12251a;
--bg: #0c0d10;
--surface: #141519;
--surface-2: #1a1c22;
--surface-3: #23262e;
--border: #262932;
--border-strong: #363b47;
--text-1: #e9ebf0;
--text-2: #a0a8b8;
--text-3: #68707e;
--brand: #6d8bfa;
--brand-hover: #8aa2fb;
--brand-subtle: #1a2144;
--brand-text: #96adfa;
--success: #34d399;
--success-subtle: #0d2921;
--warning: #fbbf24;
--warning-subtle: #2a2213;
--warning-subtle: #2b2109;
--danger: #f87171;
--danger-subtle: #2c1618;
--danger-subtle: #2d1317;
color-scheme: dark;
}
html, body, #root { height: 100%; }
@@ -85,51 +83,53 @@ html, body, #root { height: 100%; }
body {
background: var(--bg);
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;
line-height: 1.55;
letter-spacing: 0.005em;
letter-spacing: 0.003em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
/* 标题排版:紧凑字距 */
h1, h2, h3 {
letter-spacing: -0.01em;
text-wrap: balance;
}
h1, h2, h3 { letter-spacing: -0.022em; text-wrap: balance; }
/* 数字统一等宽,对齐敏感场景(表格/指标卡) */
.num {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1;
}
.num { font-variant-numeric: tabular-nums lining-nums; font-feature-settings: 'tnum' 1, 'lnum' 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: var(--border-strong) transparent; }
*::-webkit-scrollbar { width: 6px; height: 6px; }
*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
*::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
* { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--text-3) 30%, transparent) transparent; }
*::-webkit-scrollbar { width: 5px; height: 5px; }
*::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--text-3) 25%, transparent); border-radius: 3px; }
*::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--text-3) 45%, transparent); }
*::-webkit-scrollbar-track { background: transparent; }
:focus-visible {
outline: 2px solid color-mix(in srgb, var(--brand) 55%, transparent);
outline-offset: 1px;
outline: 2px solid color-mix(in srgb, var(--brand) 50%, transparent);
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 {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
.animate-fade { animation: fp-fade .3s ease-out; }
@keyframes fp-fade { from { opacity: 0; } to { opacity: 1; } }
/* 数字输入框去掉浏览器步进按钮的视觉噪音 */
/* 数字输入去噪 */
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number']::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
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);
}