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 全部通过
238 lines
6.9 KiB
TypeScript
238 lines
6.9 KiB
TypeScript
import {
|
|
AdminAlert,
|
|
AdminButton,
|
|
AdminEmptyState,
|
|
AdminField,
|
|
AdminFormRow,
|
|
AdminInfoList,
|
|
AdminPage,
|
|
AdminPageHeading,
|
|
AdminPanel,
|
|
} from '@genarrative/shared/components';
|
|
import { Plus, Send, Trash2 } from 'lucide-react';
|
|
import { FormEvent, useMemo, useState } from 'react';
|
|
|
|
import { debugAdminHttp } from '../api/adminApiClient';
|
|
import type {
|
|
AdminDebugHeaderInput,
|
|
AdminDebugHttpMethod,
|
|
AdminDebugHttpResponse,
|
|
} from '../api/adminApiTypes';
|
|
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
|
import { formatUnknownJson, handlePageError } from './pageUtils';
|
|
|
|
interface AdminDebugHttpPageProps {
|
|
token: string;
|
|
onUnauthorized: (message?: string) => void;
|
|
}
|
|
|
|
const httpMethods: AdminDebugHttpMethod[] = [
|
|
'GET',
|
|
'POST',
|
|
'PUT',
|
|
'PATCH',
|
|
'DELETE',
|
|
];
|
|
|
|
export function AdminDebugHttpPage({
|
|
token,
|
|
onUnauthorized,
|
|
}: AdminDebugHttpPageProps) {
|
|
const [method, setMethod] = useState<AdminDebugHttpMethod>('GET');
|
|
const [path, setPath] = useState('/healthz');
|
|
const [body, setBody] = useState('');
|
|
const [headers, setHeaders] = useState<AdminDebugHeaderInput[]>([]);
|
|
const [result, setResult] = useState<AdminDebugHttpResponse | null>(null);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
|
|
|
const jsonPreview = useMemo(
|
|
() => formatUnknownJson(result?.bodyJson),
|
|
[result?.bodyJson],
|
|
);
|
|
|
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (isSubmitting) {
|
|
return;
|
|
}
|
|
|
|
setErrorMessage('');
|
|
if (method !== 'GET') {
|
|
const confirmed = await confirmWrite({
|
|
action: `${method} 调试请求`,
|
|
target: path.trim(),
|
|
});
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const response = await debugAdminHttp(token, {
|
|
method,
|
|
path: path.trim(),
|
|
headers: headers.filter((header) => header.name.trim()),
|
|
body,
|
|
});
|
|
setResult(response);
|
|
} catch (error: unknown) {
|
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AdminPage>
|
|
<AdminPageHeading title="API 调试" description="受控同源请求" />
|
|
|
|
<div className="admin-two-column">
|
|
<AdminPanel as="form" onSubmit={handleSubmit}>
|
|
<AdminFormRow>
|
|
<AdminField label="Method" compact>
|
|
<select
|
|
value={method}
|
|
onChange={(event) =>
|
|
setMethod(event.target.value as AdminDebugHttpMethod)
|
|
}
|
|
>
|
|
{httpMethods.map((item) => (
|
|
<option key={item} value={item}>
|
|
{item}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</AdminField>
|
|
<AdminField label="Path" fill>
|
|
<input
|
|
value={path}
|
|
onChange={(event) => setPath(event.target.value)}
|
|
/>
|
|
</AdminField>
|
|
</AdminFormRow>
|
|
|
|
<section className="admin-subsection">
|
|
<div className="admin-subsection-heading">
|
|
<span>Headers</span>
|
|
<AdminButton
|
|
variant="ghost"
|
|
type="button"
|
|
onClick={() =>
|
|
setHeaders((current) => [
|
|
...current,
|
|
{
|
|
name: '',
|
|
value: '',
|
|
},
|
|
])
|
|
}
|
|
>
|
|
<Plus size={16} aria-hidden="true" />
|
|
</AdminButton>
|
|
</div>
|
|
<div className="admin-header-editor">
|
|
{headers.map((header, index) => (
|
|
<div className="admin-header-row" key={index}>
|
|
<input
|
|
value={header.name}
|
|
onChange={(event) =>
|
|
setHeaders((current) =>
|
|
current.map((item, itemIndex) =>
|
|
itemIndex === index
|
|
? {
|
|
...item,
|
|
name: event.target.value,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
<input
|
|
value={header.value}
|
|
onChange={(event) =>
|
|
setHeaders((current) =>
|
|
current.map((item, itemIndex) =>
|
|
itemIndex === index
|
|
? {
|
|
...item,
|
|
value: event.target.value,
|
|
}
|
|
: item,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
<AdminButton
|
|
variant="ghost"
|
|
title="移除"
|
|
type="button"
|
|
onClick={() =>
|
|
setHeaders((current) =>
|
|
current.filter((_, itemIndex) => itemIndex !== index),
|
|
)
|
|
}
|
|
>
|
|
<Trash2 size={16} aria-hidden="true" />
|
|
</AdminButton>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<AdminField label="Body">
|
|
<textarea
|
|
rows={9}
|
|
value={body}
|
|
onChange={(event) => setBody(event.target.value)}
|
|
/>
|
|
</AdminField>
|
|
|
|
{errorMessage ? <AdminAlert>{errorMessage}</AdminAlert> : null}
|
|
|
|
<AdminButton
|
|
variant="primary"
|
|
disabled={isSubmitting || !path.trim().startsWith('/')}
|
|
type="submit"
|
|
>
|
|
<Send size={17} aria-hidden="true" />
|
|
<span>{isSubmitting ? '发送中' : '发送'}</span>
|
|
</AdminButton>
|
|
</AdminPanel>
|
|
|
|
<AdminPanel
|
|
title="结果"
|
|
count={result ? `${result.status} ${result.statusText}` : '-'}
|
|
className="admin-result-panel"
|
|
>
|
|
{result ? (
|
|
<>
|
|
<AdminInfoList
|
|
items={[
|
|
{
|
|
label: <>Status</>,
|
|
value: <>{result.status}</>,
|
|
},
|
|
{
|
|
label: <>Headers</>,
|
|
value: <>{result.headers.length}</>,
|
|
},
|
|
]}
|
|
/>
|
|
<pre className="admin-code-block">
|
|
{jsonPreview || result.bodyText || '(empty)'}
|
|
</pre>
|
|
</>
|
|
) : (
|
|
<AdminEmptyState>暂无结果</AdminEmptyState>
|
|
)}
|
|
</AdminPanel>
|
|
</div>
|
|
{confirmDialog}
|
|
</AdminPage>
|
|
);
|
|
}
|