Initial CMPP frontend prototype

This commit is contained in:
hectorzhao
2026-06-30 16:09:46 +08:00
commit 2f3c274a30
98 changed files with 25255 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
import type { ComponentType } from 'react';
import { useState } from 'react';
import {
Bell,
ChevronDown,
ChevronRight,
CircleHelp,
PanelLeftClose,
PanelLeftOpen,
Search,
Sparkles,
} from 'lucide-react';
import { NavLink, Outlet } from 'react-router-dom';
export type ShellNavItem = {
label: string;
to: string;
icon: ComponentType<{ size?: number; strokeWidth?: number }>;
};
export type ShellNavSection = {
title: string;
icon?: ComponentType<{ size?: number; strokeWidth?: number }>;
items: ShellNavItem[];
};
type AppShellProps = {
title: string;
subtitle: string;
workspaceName: string;
userName: string;
userRole: string;
navSections: ShellNavSection[];
};
export function AppShell({
title,
subtitle,
workspaceName,
userName,
userRole,
navSections,
}: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
const [closedSections, setClosedSections] = useState<Record<string, boolean>>({});
const ToggleIcon = collapsed ? PanelLeftOpen : PanelLeftClose;
return (
<div className={['app-shell', collapsed ? 'app-shell--collapsed' : ''].filter(Boolean).join(' ')}>
<aside className="sidebar">
<div className="brand-block">
<div className="brand-icon">
<Sparkles size={18} />
</div>
<div className="brand-copy">
<div className="brand-mark">{title}</div>
<div className="brand-subtitle">{subtitle}</div>
</div>
</div>
<nav className="side-nav" aria-label="主导航">
{navSections.map((section) => (
<section className="side-nav-section" key={section.title}>
{section.icon ? (
<button
aria-expanded={!closedSections[section.title]}
className="side-nav-group-toggle"
onClick={() => setClosedSections((current) => ({ ...current, [section.title]: !current[section.title] }))}
type="button"
>
<section.icon size={18} strokeWidth={2.1} />
<span className="side-nav-group-label">{section.title}</span>
{closedSections[section.title] ? (
<ChevronRight className="side-nav-group-chevron" size={16} />
) : (
<ChevronDown className="side-nav-group-chevron" size={16} />
)}
</button>
) : (
<p>{section.title}</p>
)}
<div className={['side-nav-list', closedSections[section.title] ? 'side-nav-list--closed' : ''].join(' ')}>
{section.items.map((item) => {
const Icon = item.icon;
return (
<NavLink key={item.to} to={item.to} end title={item.label}>
<Icon size={17} strokeWidth={2.1} />
<span>{item.label}</span>
</NavLink>
);
})}
</div>
</section>
))}
</nav>
<div className="sidebar-footer">
<span></span>
<strong>{workspaceName}</strong>
</div>
</aside>
<main className="main-area">
<header className="topbar">
<div className="topbar-left">
<button
className="icon-button"
onClick={() => setCollapsed((value) => !value)}
type="button"
aria-label={collapsed ? '展开导航' : '收起导航'}
>
<ToggleIcon size={18} />
</button>
<label className="topbar-search">
<Search size={17} />
<input placeholder="搜索模板、客户、发送记录" />
</label>
</div>
<div className="topbar-actions">
<button className="icon-button" type="button" aria-label="帮助中心">
<CircleHelp size={18} />
</button>
<button className="icon-button has-dot" type="button" aria-label="通知">
<Bell size={18} />
</button>
<button className="user-menu" type="button">
<span className="user-avatar">{userName.slice(0, 1)}</span>
<span>
<strong>{userName}</strong>
<small>{userRole}</small>
</span>
<ChevronDown size={16} />
</button>
</div>
</header>
<div className="page-content">
<Outlet />
</div>
</main>
</div>
);
}