Initial CMPP frontend prototype
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user