81 lines
5.6 KiB
TypeScript
81 lines
5.6 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { useState } from 'react'
|
|
import { Bell, ChevronDown, CircleUserRound, Folder, Grid2X2, Home, Layers3, LogOut, Menu, MessageCircle, Palette, Search, Settings2, Sparkles, WandSparkles } from 'lucide-react'
|
|
import { Button } from '../primitives/Button'
|
|
import { IconButton } from '../primitives/IconButton'
|
|
import { SearchInput } from '../primitives/FormControls'
|
|
import styles from './AppShell.module.css'
|
|
import avatarImage from '../../assets/holopix/avatar.png'
|
|
import loginRewardAnimation from '../../assets/holopix/login-reward.gif'
|
|
import loginRewardLabel from '../../assets/holopix/login-reward-label.svg'
|
|
import loginRewardPanel from '../../assets/holopix/login-reward-panel.webp'
|
|
import visitorPanel from '../../assets/holopix/visitor-panel.png'
|
|
|
|
export type AppRoute = 'home' | 'components' | 'models' | 'create' | 'canvas' | 'assets'
|
|
|
|
interface AppShellProps { route: AppRoute; onRouteChange: (route: AppRoute) => void; children: ReactNode }
|
|
|
|
const primaryNav: Array<{ id: AppRoute; label: string; icon: typeof Home; badge?: string }> = [
|
|
{ id: 'home', label: '首页', icon: Home },
|
|
{ id: 'models', label: '模型广场', icon: Grid2X2 },
|
|
{ id: 'create', label: 'AI 创作', icon: WandSparkles },
|
|
{ id: 'canvas', label: '自由画布', icon: Layers3, badge: '热门' },
|
|
{ id: 'assets', label: '我的资产', icon: Folder },
|
|
]
|
|
|
|
export function AppShell({ route, onRouteChange, children }: AppShellProps) {
|
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
const [search, setSearch] = useState('')
|
|
|
|
return <div className={styles.shell}>
|
|
<header className={styles.header}>
|
|
<div className={styles.brand}><span className={styles.brandMark}><i /><i /></span><strong>Miragen <em>Flow</em></strong></div>
|
|
<IconButton className={styles.mobileMenu} label="打开导航" icon={<Menu />} onClick={() => setSidebarOpen(value => !value)} />
|
|
<div className={styles.headerSearch}><SearchInput value={search} onChange={event => setSearch(event.target.value)} placeholder="搜索作品、模型或风格" /><Button size="sm" variant="primary" leadingIcon={<Search />}>找灵感</Button></div>
|
|
<div className={styles.headerActions}>
|
|
<div className={`${styles.headerAction} ${styles.noticeAction}`}>
|
|
<IconButton className={styles.noticeButton} label="通知" icon={<Bell />} aria-haspopup="dialog" />
|
|
<div className={`${styles.headerPopover} ${styles.noticePopover}`} role="dialog" aria-label="通知消息">
|
|
<div className={styles.noticeHeader}><strong>通知</strong><button type="button" disabled>全部已读</button></div>
|
|
<div className={styles.noticeContent}><span>暂无通知</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`${styles.headerAction} ${styles.loginAction}`}>
|
|
<button type="button" className={styles.loginButton} style={{ backgroundImage: `url(${loginRewardAnimation})` }} aria-label="登录,免费领算力" aria-haspopup="dialog"><img src={loginRewardLabel} alt="登录 免费领算力" /></button>
|
|
<div className={`${styles.headerPopover} ${styles.rewardPopover}`} role="dialog" aria-label="登录领取算力">
|
|
<img className={styles.loginRewardPanel} src={loginRewardPanel} alt="注册即送专属算力" />
|
|
<button type="button" className={styles.loginRewardHitArea} aria-label="立即登录领取" />
|
|
</div>
|
|
</div>
|
|
|
|
<span className={styles.headerDivider} />
|
|
|
|
<div className={`${styles.headerAction} ${styles.profileAction}`}>
|
|
<button type="button" className={styles.avatarButton} aria-label="用户菜单" aria-haspopup="dialog">
|
|
<img className={styles.headerAvatar} src={avatarImage} alt="用户头像" />
|
|
<ChevronDown className={styles.chevron} />
|
|
</button>
|
|
<div className={`${styles.headerPopover} ${styles.profilePopover}`} role="dialog" aria-label="用户菜单">
|
|
<div className={styles.visitorBody}>
|
|
<div className={styles.visitorInfo}><img src={avatarImage} alt="用户头像" /><span>游客 5ebe1365e9fdd2b7</span></div>
|
|
<img className={styles.visitorPanel} src={visitorPanel} alt="注册领取新手大礼包" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<aside className={`${styles.sidebar} ${sidebarOpen ? styles.sidebarOpen : ''}`}>
|
|
<nav className={styles.nav} aria-label="主导航">
|
|
<p className={styles.navLabel}>工作台</p>
|
|
{primaryNav.map(item => { const Icon = item.icon; return <button key={item.id} type="button" className={`${styles.navItem} ${route === item.id ? styles.active : ''}`} onClick={() => { onRouteChange(item.id); setSidebarOpen(false) }}><Icon /><span>{item.label}</span>{item.badge && <small>{item.badge}</small>}</button> })}
|
|
<p className={styles.navLabel}>资源</p>
|
|
<button type="button" className={styles.navItem}><Palette /><span>模型定制</span></button>
|
|
<button type="button" className={styles.navItem}><Sparkles /><span>AI 学院</span></button>
|
|
</nav>
|
|
<div className={styles.sidebarBottom}><div className={styles.community}><MessageCircle /><span><strong>加入设计交流群</strong><small>与创作者一起交流</small></span><ChevronDown /></div><div className={styles.sideLinks}><button type="button" onClick={() => onRouteChange('components')}><Settings2 />组件中心</button><button type="button"><CircleUserRound />个人主页</button><button type="button"><LogOut />退出登录</button></div><p className={styles.legal}>©2026 Miragen Flow<br />用户协议 · 隐私协议</p></div>
|
|
</aside>
|
|
<main className={styles.main}>{children}</main>
|
|
</div>
|
|
}
|