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 全部通过
177 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|