f1b282f3cd
- 合并 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。
335 lines
10 KiB
TypeScript
335 lines
10 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
import {
|
|
formatAdminApiError,
|
|
getAdminMe,
|
|
isAdminApiError,
|
|
loginAdmin,
|
|
} from '../api/adminApiClient';
|
|
import type {
|
|
AdminSessionPayload,
|
|
ProfileRechargeProductConfigAdminResponse,
|
|
ProfileTaskConfigAdminResponse,
|
|
ProfileWalletConfigAdminResponse,
|
|
} from '../api/adminApiTypes';
|
|
import {
|
|
clearStoredAdminToken,
|
|
getStoredAdminToken,
|
|
setStoredAdminToken,
|
|
} from '../auth/adminAuthStore';
|
|
import { AdminAccountsPage } from '../pages/AdminAccountsPage';
|
|
import { AdminAgcModelsPage } from '../pages/AdminAgcModelsPage';
|
|
import { AdminAgcTemplatesPage } from '../pages/AdminAgcTemplatesPage';
|
|
import { AdminDashboardPage } from '../pages/AdminDashboardPage';
|
|
import { AdminDatabaseTablesPage } from '../pages/AdminDatabaseTablesPage';
|
|
import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage';
|
|
import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage';
|
|
import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage';
|
|
import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage';
|
|
import { AdminErrorReportsPage } from '../pages/AdminErrorReportsPage';
|
|
import { AdminGameDistributionReviewPage } from '../pages/AdminGameDistributionReviewPage';
|
|
import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage';
|
|
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
|
|
import { AdminLoginPage } from '../pages/AdminLoginPage';
|
|
import { AdminOverviewPage } from '../pages/AdminOverviewPage';
|
|
import { AdminProfileWalletConfigPage } from '../pages/AdminProfileWalletConfigPage';
|
|
import { AdminProjectSnapshotsPage } from '../pages/AdminProjectSnapshotsPage';
|
|
import { AdminRechargeOrderPage } from '../pages/AdminRechargeOrderPage';
|
|
import { AdminRechargeProductPage } from '../pages/AdminRechargeProductPage';
|
|
import { AdminRedeemCodePage } from '../pages/AdminRedeemCodePage';
|
|
import { AdminTaskConfigPage } from '../pages/AdminTaskConfigPage';
|
|
import { AdminTrackingEventsPage } from '../pages/AdminTrackingEventsPage';
|
|
import type { AdminRouteId } from './adminRoutes';
|
|
import {
|
|
getAccessibleAdminRoutes,
|
|
resolveAccessibleAdminRoute,
|
|
resolveAdminRoute,
|
|
routeHash,
|
|
} from './adminRoutes';
|
|
import { AdminShell } from './AdminShell';
|
|
|
|
type SessionStatus = 'checking' | 'guest' | 'authenticated';
|
|
|
|
export function AdminApp() {
|
|
const [status, setStatus] = useState<SessionStatus>('checking');
|
|
const [admin, setAdmin] = useState<AdminSessionPayload | null>(null);
|
|
const [token, setToken] = useState('');
|
|
const [routeId, setRouteId] = useState<AdminRouteId>(() =>
|
|
resolveAdminRoute(window.location.hash),
|
|
);
|
|
const [loginNotice, setLoginNotice] = useState('');
|
|
const [taskConfigResult, setTaskConfigResult] =
|
|
useState<ProfileTaskConfigAdminResponse | null>(null);
|
|
const [profileWalletConfigResult, setProfileWalletConfigResult] =
|
|
useState<ProfileWalletConfigAdminResponse | null>(null);
|
|
const [rechargeProductResult, setRechargeProductResult] =
|
|
useState<ProfileRechargeProductConfigAdminResponse | null>(null);
|
|
const accessibleRoutes = useMemo(
|
|
() => (admin ? getAccessibleAdminRoutes(admin) : []),
|
|
[admin],
|
|
);
|
|
const activeRouteId = accessibleRoutes.some((route) => route.id === routeId)
|
|
? routeId
|
|
: null;
|
|
|
|
const clearSession = useCallback((message = '') => {
|
|
clearStoredAdminToken();
|
|
setToken('');
|
|
setAdmin(null);
|
|
setTaskConfigResult(null);
|
|
setProfileWalletConfigResult(null);
|
|
setRechargeProductResult(null);
|
|
setStatus('guest');
|
|
setLoginNotice(message);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
const storedToken = getStoredAdminToken();
|
|
|
|
if (!storedToken) {
|
|
setStatus('guest');
|
|
return;
|
|
}
|
|
|
|
void getAdminMe(storedToken)
|
|
.then((response) => {
|
|
if (!isMounted) {
|
|
return;
|
|
}
|
|
|
|
setToken(storedToken);
|
|
setAdmin(response.admin);
|
|
setStatus('authenticated');
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!isMounted) {
|
|
return;
|
|
}
|
|
|
|
clearStoredAdminToken();
|
|
setToken('');
|
|
setAdmin(null);
|
|
setStatus('guest');
|
|
setLoginNotice(
|
|
isAdminApiError(error) && error.status === 401
|
|
? '登录状态已失效'
|
|
: formatAdminApiError(error),
|
|
);
|
|
});
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (status !== 'authenticated' || !admin) {
|
|
return;
|
|
}
|
|
|
|
const nextRouteId = resolveAccessibleAdminRoute(
|
|
window.location.hash,
|
|
accessibleRoutes,
|
|
);
|
|
if (!nextRouteId) {
|
|
return;
|
|
}
|
|
|
|
setRouteId(nextRouteId);
|
|
const nextHash = routeHash(nextRouteId);
|
|
if (window.location.hash !== nextHash) {
|
|
window.history.replaceState(null, '', nextHash);
|
|
}
|
|
}, [accessibleRoutes, admin, routeId, status]);
|
|
|
|
useEffect(() => {
|
|
const handleHashChange = () => {
|
|
setRouteId(resolveAdminRoute(window.location.hash));
|
|
};
|
|
|
|
window.addEventListener('hashchange', handleHashChange);
|
|
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
}, []);
|
|
|
|
const handleRouteChange = useCallback((nextRouteId: AdminRouteId) => {
|
|
setRouteId(nextRouteId);
|
|
const nextHash = routeHash(nextRouteId);
|
|
if (window.location.hash !== nextHash) {
|
|
window.location.hash = nextHash;
|
|
}
|
|
}, []);
|
|
|
|
const handleLogin = useCallback(
|
|
async (username: string, password: string) => {
|
|
const response = await loginAdmin(username, password);
|
|
setStoredAdminToken(response.token);
|
|
setToken(response.token);
|
|
setAdmin(response.admin);
|
|
setTaskConfigResult(null);
|
|
setProfileWalletConfigResult(null);
|
|
setRechargeProductResult(null);
|
|
setLoginNotice('');
|
|
setStatus('authenticated');
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleUnauthorized = useCallback(
|
|
(message = '登录状态已失效') => {
|
|
clearSession(message);
|
|
},
|
|
[clearSession],
|
|
);
|
|
|
|
const handleLogout = useCallback(() => {
|
|
clearSession('');
|
|
}, [clearSession]);
|
|
|
|
if (status === 'checking') {
|
|
return (
|
|
<main className="admin-loading-screen">
|
|
<div className="admin-loading-mark" />
|
|
<span>正在校验会话</span>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (status === 'guest' || !admin || !token) {
|
|
return <AdminLoginPage notice={loginNotice} onLogin={handleLogin} />;
|
|
}
|
|
|
|
return (
|
|
<AdminShell
|
|
admin={admin}
|
|
routeId={activeRouteId}
|
|
routes={accessibleRoutes}
|
|
onLogout={handleLogout}
|
|
onRouteChange={handleRouteChange}
|
|
>
|
|
{activeRouteId === null ? (
|
|
<section className="admin-panel admin-zero-permission-state">
|
|
<h2>暂无访问权限</h2>
|
|
</section>
|
|
) : null}
|
|
{activeRouteId === 'dashboard' ? (
|
|
<AdminDashboardPage token={token} onUnauthorized={handleUnauthorized} />
|
|
) : null}
|
|
{activeRouteId === 'overview' ? (
|
|
<AdminOverviewPage token={token} onUnauthorized={handleUnauthorized} />
|
|
) : null}
|
|
{activeRouteId === 'tables' ? (
|
|
<AdminDatabaseTablesPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'debug' ? (
|
|
<AdminDebugHttpPage token={token} onUnauthorized={handleUnauthorized} />
|
|
) : null}
|
|
{activeRouteId === 'tracking' ? (
|
|
<AdminTrackingEventsPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'error-reports' ? (
|
|
<AdminErrorReportsPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'gray-release' ? (
|
|
<AdminGrayReleaseConfigPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'redeem' ? (
|
|
<AdminRedeemCodePage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'invite' ? (
|
|
<AdminInviteCodePage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'tasks' ? (
|
|
<AdminTaskConfigPage
|
|
result={taskConfigResult}
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
onResultChange={setTaskConfigResult}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'profile-wallet' ? (
|
|
<AdminProfileWalletConfigPage
|
|
result={profileWalletConfigResult}
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
onResultChange={setProfileWalletConfigResult}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'recharge-products' ? (
|
|
<AdminRechargeProductPage
|
|
result={rechargeProductResult}
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
onResultChange={setRechargeProductResult}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'recharge-orders' ? (
|
|
<AdminRechargeOrderPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'editor-generation-pricing' ? (
|
|
<AdminEditorGenerationPricingPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'agc-models' ? (
|
|
<AdminAgcModelsPage token={token} onUnauthorized={handleUnauthorized} />
|
|
) : null}
|
|
{activeRouteId === 'agc-templates' ? (
|
|
<AdminAgcTemplatesPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'editor-showcase' ? (
|
|
<AdminEditorShowcaseReviewPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'game-distribution' ? (
|
|
<AdminGameDistributionReviewPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'editor-assets' ? (
|
|
<AdminEditorAssetQueryPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
{activeRouteId === 'accounts' ? (
|
|
<AdminAccountsPage token={token} onUnauthorized={handleUnauthorized} />
|
|
) : null}
|
|
{activeRouteId === 'project-snapshots' ? (
|
|
<AdminProjectSnapshotsPage
|
|
token={token}
|
|
onUnauthorized={handleUnauthorized}
|
|
/>
|
|
) : null}
|
|
</AdminShell>
|
|
);
|
|
}
|