Files
Genarrative/apps/admin-web/src/pages/AdminOverviewPage.tsx
T
kdletters 8315d5660a
Project CI / AI game creator shell Rust crates (push) Successful in 1m27s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m53s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
后台公共组件收口到共享组件库并统一列表实现
- packages/shared/src/components/admin/ 新增 16 个后台共享组件(AdminPage/AdminPanel/AdminButton/AdminAlert/AdminTable/AdminField/AdminInfoList/AdminTagList/AdminPagination/AdminListPanel/AdminDialog 等)与随组件的 admin.css,经 @genarrative/shared/components 导出
- AdminListPanel 统一列表外壳:columns + renderRow、加载两口径(empty/line)、空态三态(state/text/row)、toolbar 与 pagination 槽位、showEmpty 判据、surface=plain(弹窗内列表)
- AdminListPanel 新增列头排序(sortable/sortValue/sortDescription):正序→倒序→不排序循环、同步 th[aria-sort]、稳定排序
- AdminDialog 统一弹窗外壳(遮罩/标题行/关闭动作/role=dialog/aria-modal/Esc/点遮罩关闭/初始聚焦/as=form),12 处各页自写弹窗全部改用它
- AdminFormat 新增 formatAdminDateTime/formatAdminDate,删除 7 份重复的本地时间格式化实现,修掉列表里裸 ISO 时间
- 21 个文件里 26 处列表全部迁到 AdminListPanel(24 处 columns 形态、2 处弹窗内 plain),页内已无手写表头或 AdminTable 当 children 的写法;累计 45 个可排序列
- 页面壳归一:错误报告补 AdminPage wide + AdminPageHeading,详情弹窗移出列表面板;模板管理列表从共享 ui Table 换成 AdminTable
- 样式收口:公共组件规则随组件进入共享 styles,清理 admin.css 重复按钮/面板规则,.admin-tag 与 .admin-table th 改 nowrap,埋点用户列参考按钮紧跟本行并留行距,错误报告修版心
- 新增 scripts/admin-web-fake-api.mjs 本地假 API(含 overview/dashboard/tracking/error-reports/feature-gates/邀请码/兑换码/任务/充值/项目工程/AGC 模型与模板/游戏/账号/用户详情夹具),用于本地核对渲染
- docs 同步:共享基础组件库与展示页(后台组件层/列表排序/收敛范围/本地核对)、decision-log 与 pitfalls 记录迁移口径、排序边界与踩坑
- 验证:npm run admin-web:typecheck、npx vitest run apps/admin-web packages/shared/src/components/admin(26 files / 244 tests)、npm run check:encoding、git diff --check、prettier、eslint 全部通过
2026-09-29 22:39:41 +08:00

177 lines
4.7 KiB
TypeScript

import {
AdminAlert,
AdminButton,
AdminInfoList,
AdminListPanel,
AdminPage,
AdminPageHeading,
AdminPanel,
AdminStatusPill,
} from '@genarrative/shared/components';
import { RefreshCw } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { getAdminOverview } from '../api/adminApiClient';
import type {
AdminDatabaseTableStatPayload,
AdminOverviewResponse,
} from '../api/adminApiTypes';
import { handlePageError } from './pageUtils';
interface AdminOverviewPageProps {
token: string;
onUnauthorized: (message?: string) => void;
}
export function AdminOverviewPage({
token,
onUnauthorized,
}: AdminOverviewPageProps) {
const [overview, setOverview] = useState<AdminOverviewResponse | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const loadOverview = useCallback(async () => {
setIsLoading(true);
setErrorMessage('');
try {
const response = await getAdminOverview(token);
setOverview(response);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
}, [onUnauthorized, token]);
useEffect(() => {
void loadOverview();
}, [loadOverview]);
return (
<AdminPage>
<AdminPageHeading
title="总览"
description="服务与数据库状态"
actions={
<AdminButton
variant="secondary"
disabled={isLoading}
type="button"
onClick={() => void loadOverview()}
>
<RefreshCw size={17} aria-hidden="true" />
<span>{isLoading ? '刷新中' : '刷新'}</span>
</AdminButton>
}
/>
{errorMessage ? <AdminAlert>{errorMessage}</AdminAlert> : null}
<div className="admin-overview-grid">
<InfoPanel
title="服务"
rows={[
[
'监听',
overview
? `${overview.service.bindHost}:${overview.service.bindPort}`
: '-',
],
['JWT issuer', overview?.service.jwtIssuer ?? '-'],
['后台', overview?.service.adminEnabled ? '已启用' : '未启用'],
]}
/>
<InfoPanel
title="SpacetimeDB"
rows={[
['Server', overview?.service.spacetimeServerUrl ?? '-'],
['Database', overview?.service.spacetimeDatabase ?? '-'],
['Identity', overview?.database.databaseIdentity ?? '-'],
['Owner', overview?.database.ownerIdentity ?? '-'],
['Host', overview?.database.hostType ?? '-'],
]}
/>
</div>
<AdminListPanel
columns={[
{ key: 'table', label: '表' },
{ key: 'rowCount', label: '行数' },
{ key: 'status', label: '状态' },
]}
count={<>{overview?.database.schemaTableNames.length ?? 0} tables</>}
emptyText="暂无统计"
emptyVariant="row"
renderRow={(stat) => <TableStatRow key={stat.tableName} stat={stat} />}
rows={overview?.database.tableStats ?? []}
showEmpty={Boolean(
overview && overview.database.tableStats.length === 0,
)}
title="表统计"
/>
{overview?.database.fetchErrors.length ? (
<AdminPanel
tone="warning"
title="读取异常"
count={overview.database.fetchErrors.length}
>
<ul className="admin-error-list">
{overview.database.fetchErrors.map((message) => (
<li key={message}>{message}</li>
))}
</ul>
</AdminPanel>
) : null}
</AdminPage>
);
}
function InfoPanel({
title,
rows,
}: {
title: string;
rows: Array<[string, string]>;
}) {
return (
<AdminPanel title={title}>
<AdminInfoList>
{rows.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</AdminInfoList>
</AdminPanel>
);
}
function TableStatRow({ stat }: { stat: AdminDatabaseTableStatPayload }) {
return (
<tr>
<td>
<AdminButton
variant="text"
type="button"
onClick={() => {
window.location.hash = `#tables?table=${encodeURIComponent(stat.tableName)}`;
}}
>
{stat.tableName}
</AdminButton>
</td>
<td>{typeof stat.rowCount === 'number' ? stat.rowCount : '-'}</td>
<td>
{stat.errorMessage ? (
<AdminStatusPill tone="error">{stat.errorMessage}</AdminStatusPill>
) : (
<AdminStatusPill tone="ok">OK</AdminStatusPill>
)}
</td>
</tr>
);
}