15774a1c02
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m24s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m25s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m28s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m31s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / AI game creator shell Rust crates (push) Successful in 2m41s
Project CI / Repository checks (push) Successful in 4m10s
Project CI / Native shell tests (push) Successful in 11m20s
Project CI / Frontend tests (push) Successful in 10m30s
Project CI / AI game creator shell web tests (push) Successful in 5m41s
Project CI / Backend tests (push) Successful in 14m9s
接入真实项目打开、切换和关闭生命周期并修复退出等待竞态 新增后台项目列表及按原目录校验导出的 ZIP 下载 补充快照完整性元数据、后台权限和取消清理 固定默认 AGC 存储目标并支持空文件与特殊字符路径 补充自动化测试及真实 OSS 只读导出验证 记录本地数据库 404 导致完整 HTTP 联调和安装版实机验证待补
140 lines
3.6 KiB
TypeScript
140 lines
3.6 KiB
TypeScript
import {
|
|
Activity,
|
|
BadgeDollarSign,
|
|
Bug,
|
|
Coins,
|
|
Database,
|
|
FolderArchive,
|
|
GitBranch,
|
|
Images,
|
|
LayoutDashboard,
|
|
ListChecks,
|
|
LogOut,
|
|
ReceiptText,
|
|
ShieldCheck,
|
|
Star,
|
|
Table2,
|
|
TicketCheck,
|
|
TicketPercent,
|
|
Users,
|
|
WalletCards,
|
|
} from 'lucide-react';
|
|
import type { ReactNode } from 'react';
|
|
|
|
import type { AdminSessionPayload } from '../api/adminApiTypes';
|
|
import type { AdminRouteDefinition, AdminRouteId } from './adminRoutes';
|
|
|
|
interface AdminShellProps {
|
|
admin: AdminSessionPayload;
|
|
routeId: AdminRouteId | null;
|
|
routes: AdminRouteDefinition[];
|
|
children: ReactNode;
|
|
onRouteChange: (routeId: AdminRouteId) => void;
|
|
onLogout: () => void;
|
|
}
|
|
|
|
const routeIcons = {
|
|
dashboard: LayoutDashboard,
|
|
overview: Activity,
|
|
tables: Database,
|
|
debug: Bug,
|
|
tracking: Table2,
|
|
'error-reports': Bug,
|
|
'gray-release': GitBranch,
|
|
redeem: TicketPercent,
|
|
invite: TicketCheck,
|
|
'profile-wallet': WalletCards,
|
|
tasks: ListChecks,
|
|
'recharge-products': BadgeDollarSign,
|
|
'recharge-orders': ReceiptText,
|
|
'editor-generation-pricing': Coins,
|
|
'editor-showcase': Star,
|
|
'editor-assets': Images,
|
|
'project-snapshots': FolderArchive,
|
|
accounts: Users,
|
|
'agc-models': ListChecks,
|
|
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
|
|
|
export function AdminShell({
|
|
admin,
|
|
routeId,
|
|
routes,
|
|
children,
|
|
onRouteChange,
|
|
onLogout,
|
|
}: AdminShellProps) {
|
|
return (
|
|
<div className="admin-shell">
|
|
<aside className="admin-sidebar">
|
|
<div className="admin-brand">
|
|
<div className="admin-brand-icon">
|
|
<ShieldCheck size={20} aria-hidden="true" />
|
|
</div>
|
|
<div>
|
|
<strong>陶泥儿后台</strong>
|
|
<span>Admin</span>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="admin-nav" aria-label="后台导航">
|
|
{routes.map((route) => {
|
|
const Icon = routeIcons[route.id];
|
|
return (
|
|
<button
|
|
className="admin-nav-button"
|
|
data-active={route.id === routeId}
|
|
key={route.id}
|
|
title={route.label}
|
|
type="button"
|
|
onClick={() => onRouteChange(route.id)}
|
|
>
|
|
<Icon size={18} aria-hidden="true" />
|
|
<span>{route.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
</aside>
|
|
|
|
<div className="admin-main">
|
|
<header className="admin-topbar">
|
|
<div className="admin-user">
|
|
<span>{admin.displayName || admin.username}</span>
|
|
<small>{admin.accountRole === 'owner' ? 'owner' : 'member'}</small>
|
|
</div>
|
|
<button
|
|
className="admin-icon-button"
|
|
title="退出登录"
|
|
type="button"
|
|
onClick={onLogout}
|
|
>
|
|
<LogOut size={18} aria-hidden="true" />
|
|
<span>退出</span>
|
|
</button>
|
|
</header>
|
|
|
|
<main className="admin-content">{children}</main>
|
|
</div>
|
|
|
|
<nav className="admin-bottom-nav" aria-label="后台导航">
|
|
{routes.map((route) => {
|
|
const Icon = routeIcons[route.id];
|
|
return (
|
|
<button
|
|
className="admin-bottom-nav-button"
|
|
data-active={route.id === routeId}
|
|
key={route.id}
|
|
title={route.label}
|
|
type="button"
|
|
onClick={() => onRouteChange(route.id)}
|
|
>
|
|
<Icon size={19} aria-hidden="true" />
|
|
<span>{route.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|