Files
Genarrative/apps/admin-web/src/app/AdminShell.tsx
T
kdletters f1b282f3cd 合并 master 并接入 DirectProject 新聊天架构
- 合并 origin/master(304 个提交:DirectProject 聊天容器重构、Project Supervisor 退役、策划附件导入、CI 隔离编译缓存等)。
- 接受 master 对 ProjectSupervisorView / SupervisorChatOnlyView 的退役与预览快捷测试收敛;发布入口改由 DirectProject 聊天头承载。
- DirectProjectChatHeader 新增「发布到游戏广场」入口(无回调不渲染、回合忙态禁用),DirectProjectChatView 透传 onRequestGamePublish。
- App.tsx 继续由工作台壳持有试玩包导出与 GameDistributionPublishPanel,沿用 project.export_package 权限确认队列;check-config 把该命令从 native-only 清单移回 App invoke。
- 后台游戏审核 API / 类型 / 路由测试与 master 新增的 AGC 模板管理按双方保留合并,并修掉拼接造成的接口与用例闭合缺陷。
- 修正 master 自带的 viteProxyConfig 断言:/api/creation-entry 属退役路由,测试改为断言不进入代理。
- 记录合并踩坑:语法结构内部的冲突不能简单按「双方保留」拼接,必须按某一侧骨架重建并跑 tsc 与单文件测试。
- 验证:全量 vitest 393 文件 / 4374 用例通过,root / AGC / admin-web 三端 typecheck,cargo check 与游戏分发 Rust 测试,encoding、doc-index、rustfmt、SpacetimeDB schema guard。
2026-09-22 16:45:29 +08:00

143 lines
3.6 KiB
TypeScript

import {
Activity,
BadgeDollarSign,
Bug,
Coins,
Database,
FolderArchive,
Gamepad2,
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,
'game-distribution': Gamepad2,
'editor-assets': Images,
'project-snapshots': FolderArchive,
accounts: Users,
'agc-models': ListChecks,
'agc-templates': Images,
} 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>
);
}