feat: polish BI dashboards and bump version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
lingniu
2026-06-27 21:59:33 +08:00
parent 5377d2c225
commit b0caa5afcb
33 changed files with 2363 additions and 483 deletions

View File

@@ -1,13 +1,17 @@
import { useState, useEffect, useMemo, type ComponentType } from 'react';
import { useState, useEffect, useMemo, type ComponentType, type ElementType, Suspense } from 'react';
import { motion } from 'motion/react';
import { Building2, ShieldCheck } from 'lucide-react';
import { useAuth } from '../auth/useAuth';
import { DemoModeProvider } from './Blur';
import FeedbackFab from './FeedbackFab';
import { cn } from '../lib/cn';
import { LoadingState } from './ui/surface';
export interface ModuleConfig {
id: string;
label: string;
icon: ComponentType<{ size?: number; className?: string }>;
component: ComponentType;
component: ElementType;
}
/** hash 一级段(`#<id>` 或 `#<id>/<sub>` 都只取 id */
@@ -58,6 +62,7 @@ export function Shell({ modules }: { modules: ModuleConfig[] }) {
const ActiveComponent = modules.find((m) => m.id === activeModule)?.component ?? modules[0]?.component;
const { user } = useAuth();
const activeLabel = modules.find((m) => m.id === activeModule)?.label ?? '业务看板';
const watermarkText = useMemo(() => {
const name = user?.userName || '未登录';
const time = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-');
@@ -66,7 +71,7 @@ export function Shell({ modules }: { modules: ModuleConfig[] }) {
return (
<DemoModeProvider enabled={false}>
<div className="flex min-h-screen">
<div className="enterprise-grid-bg flex min-h-screen">
{/* 全局水印 */}
<div className="fixed inset-0 pointer-events-none z-[9999] overflow-hidden" style={{ opacity: 0.06 }}>
<div className="absolute inset-0" style={{
@@ -75,35 +80,63 @@ export function Shell({ modules }: { modules: ModuleConfig[] }) {
}} />
</div>
{/* Web 侧边栏 (md 及以上) */}
<nav className="hidden md:flex flex-col items-center w-16 bg-white border-r border-gray-100 fixed top-0 left-0 h-full z-50 py-6 gap-2">
{modules.map((m) => {
const Icon = m.icon;
const isActive = m.id === activeModule;
return (
<button
key={m.id}
onClick={() => switchModule(m.id)}
className={`flex flex-col items-center justify-center w-14 h-14 rounded-xl transition-colors ${
isActive
? 'bg-blue-50 text-blue-600'
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-50'
}`}
>
<Icon size={22} />
<span className="text-[10px] mt-1 leading-tight">{m.label}</span>
</button>
);
})}
<nav className="fixed left-0 top-0 z-50 hidden h-full w-20 flex-col items-center border-r border-white/10 bg-slate-950 px-2 py-4 text-white shadow-[12px_0_40px_rgba(15,23,42,0.16)] md:flex">
<div className="mb-5 flex h-12 w-12 items-center justify-center rounded-2xl bg-white text-blue-600 shadow-lg shadow-blue-950/20">
<Building2 size={22} />
</div>
<div className="flex w-full flex-1 flex-col items-center gap-2">
{modules.map((m) => {
const Icon = m.icon;
const isActive = m.id === activeModule;
return (
<button
key={m.id}
onClick={() => switchModule(m.id)}
className={cn(
'group relative flex h-16 w-16 flex-col items-center justify-center rounded-2xl text-[10px] font-black transition-all',
isActive
? 'text-white'
: 'text-slate-400 hover:bg-white/8 hover:text-white',
)}
title={m.label}
>
{isActive ? (
<motion.span
layoutId="desktop-shell-active"
className="absolute inset-0 rounded-2xl bg-blue-600 shadow-lg shadow-blue-950/30"
transition={{ type: 'spring', stiffness: 430, damping: 34 }}
/>
) : null}
<Icon size={21} className="relative mb-1" />
<span className="relative leading-tight">{m.label}</span>
</button>
);
})}
</div>
<div className="flex w-full flex-col items-center gap-2 border-t border-white/10 pt-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-400/10 text-emerald-300 ring-1 ring-emerald-300/20" title={user?.userName || '当前用户'}>
<ShieldCheck size={18} />
</div>
<div className="max-w-16 truncate text-center text-[9px] font-bold text-slate-400">{user?.userName || '未登录'}</div>
</div>
</nav>
{/* 内容区 */}
<main className="flex-1 md:ml-16 pb-16 md:pb-0 min-w-0" style={{ overflowX: 'clip' }}>
{ActiveComponent && <ActiveComponent />}
<main className="min-w-0 flex-1 pb-16 md:ml-20 md:pb-0" style={{ overflowX: 'clip' }}>
<div className="pointer-events-none fixed left-20 right-0 top-0 z-20 hidden h-12 items-center border-b border-white/60 bg-white/55 px-6 text-xs font-bold text-slate-400 backdrop-blur-xl md:flex">
<span className="text-slate-600"> BI</span>
<span className="mx-2 text-slate-300">/</span>
<span>{activeLabel}</span>
</div>
<Suspense fallback={<div className="p-3 md:p-6"><LoadingState label="正在加载业务模块" /></div>}>
{ActiveComponent && <ActiveComponent />}
</Suspense>
<FeedbackFab module={activeModule} />
</main>
{/* 移动端底部导航 (md 以下) */}
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-2 flex justify-around items-center md:hidden z-50">
<nav className="fixed inset-x-0 bottom-0 z-50 border-t border-white/70 bg-white/92 px-3 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 shadow-[0_-18px_40px_rgba(15,23,42,0.08)] backdrop-blur-xl md:hidden">
<div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${modules.length}, minmax(0, 1fr))` }}>
{modules.map((m) => {
const Icon = m.icon;
const isActive = m.id === activeModule;
@@ -111,13 +144,24 @@ export function Shell({ modules }: { modules: ModuleConfig[] }) {
<button
key={m.id}
onClick={() => switchModule(m.id)}
className={`flex flex-col items-center ${isActive ? 'text-blue-600' : 'text-gray-400'}`}
className={cn(
'relative flex min-h-12 flex-col items-center justify-center gap-0.5 rounded-2xl text-[10px] font-black transition-colors',
isActive ? 'text-blue-700' : 'text-slate-400',
)}
>
<Icon size={20} />
<span className="text-[10px] mt-1">{m.label}</span>
{isActive ? (
<motion.span
layoutId="mobile-shell-active"
className="absolute inset-0 rounded-2xl bg-blue-50 ring-1 ring-blue-100"
transition={{ type: 'spring', stiffness: 430, damping: 34 }}
/>
) : null}
<Icon size={20} className="relative" />
<span className="relative">{m.label}</span>
</button>
);
})}
</div>
</nav>
</div>
</DemoModeProvider>