diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index f9294a7a5..8caad4598 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -1,3 +1,4 @@ +import { AdminPanel } from '@genarrative/shared/components'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { @@ -210,9 +211,9 @@ export function AdminApp() { onRouteChange={handleRouteChange} > {activeRouteId === null ? ( -
+

暂无访问权限

-
+ ) : null} {activeRouteId === 'dashboard' ? ( diff --git a/apps/admin-web/src/app/AdminShell.tsx b/apps/admin-web/src/app/AdminShell.tsx index 680193254..9db6e1c66 100644 --- a/apps/admin-web/src/app/AdminShell.tsx +++ b/apps/admin-web/src/app/AdminShell.tsx @@ -1,3 +1,4 @@ +import { AdminButton } from '@genarrative/shared/components'; import { Activity, BadgeDollarSign, @@ -108,15 +109,15 @@ export function AdminShell({ {admin.displayName || admin.username} {admin.accountRole === 'owner' ? 'owner' : 'member'} - +
{children}
diff --git a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx index ca06149f2..67792f933 100644 --- a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx +++ b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx @@ -1,4 +1,5 @@ -import { Pause, Play, X } from 'lucide-react'; +import { AdminDialog } from '@genarrative/shared/components'; +import { Pause, Play } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { @@ -85,35 +86,22 @@ export function AdminEditorAssetPreviewDialog({ : null; return ( -
-
-
-
-

{entry.label || entry.assetId}

- {entry.assetId} -
- + + {sourceEntry ? ( +
+

原图

+
- {sourceEntry ? ( -
-

原图

- -
- ) : null} - -
-
+ ) : null} + + ); } diff --git a/apps/admin-web/src/components/AdminUserDetailDialog.tsx b/apps/admin-web/src/components/AdminUserDetailDialog.tsx index 66725d952..3fadef888 100644 --- a/apps/admin-web/src/components/AdminUserDetailDialog.tsx +++ b/apps/admin-web/src/components/AdminUserDetailDialog.tsx @@ -1,3 +1,15 @@ +import { + AdminAlert, + AdminButton, + AdminDialog, + AdminField, + AdminInfoList, + AdminListPanel, + AdminPanelHeading, + AdminStatusPill, + AdminTag, + AdminTagList, +} from '@genarrative/shared/components'; import { RefreshCcw, ShieldAlert, UserRound, X } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -62,24 +74,6 @@ export function AdminUserDetailDialog({ }; }, []); - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === 'Escape' && - !isSavingRestriction && - !isReconcilingConsumption && - !isConfirming - ) { - event.preventDefault(); - onClose(); - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [isConfirming, isReconcilingConsumption, isSavingRestriction, onClose]); - async function loadDetail() { const requestVersion = requestVersionRef.current + 1; requestVersionRef.current = requestVersion; @@ -200,210 +194,206 @@ export function AdminUserDetailDialog({ } return createPortal( -
{ - if ( - event.target === event.currentTarget && - !isSavingRestriction && - !isReconcilingConsumption && - !isConfirming - ) { - onClose(); - } - }} - > -
-
-
-

用户详情

- - {detail?.publicUserCode || publicUserCode || userId || '-'} - -
-
- - -
+ + void loadDetail()} + > + + +
+ } + > + {isLoading ? ( +
+
+ 正在读取用户信息 +
+ ) : errorMessage && !detail ? ( +
+ {errorMessage} + void loadDetail()} + > + +
+ ) : detail ? ( + <> + + {errorMessage ? {errorMessage} : null} + void handleConsumptionReconcile()} + /> - {isLoading ? ( -
-
- 正在读取用户信息 -
- ) : errorMessage && !detail ? ( -
-
- {errorMessage} -
- -
- ) : detail ? ( - <> - - {errorMessage ? ( -
- {errorMessage} +
+ + {detail.wallet.manualRestriction ? ( +
+ + {detail.wallet.manualRestriction.reason || '未填写原因'} + + + {formatMicros( + detail.wallet.manualRestriction.updatedAtMicros, + )}{' '} + / {detail.wallet.manualRestriction.updatedByAdminDisplayName} +
) : null} - void handleConsumptionReconcile()} - /> - -
-
-

人工冻结

+ {detail.wallet.manualFrozen && detail.wallet.refundDebtFrozen ? ( + + + ) : null} +
+ + setRestrictionReason(event.target.value)} + /> + +
- {detail.wallet.manualRestriction ? ( -
- - {detail.wallet.manualRestriction.reason || '未填写原因'} - - - {formatMicros( - detail.wallet.manualRestriction.updatedAtMicros, - )}{' '} - /{' '} - {detail.wallet.manualRestriction.updatedByAdminDisplayName} - -
- ) : null} - {detail.wallet.manualFrozen && detail.wallet.refundDebtFrozen ? ( -
-
- ) : null} -
- - -
-
+ +
+
-
-
-

充值订单

- {detail.rechargeOrders.length} 笔 -
- {detail.rechargeOrders.length ? ( -
- - - - - - - - - - - - - {detail.rechargeOrders.map((order) => ( - - - - - - - - - ))} - -
订单商品实付发放泥点退款状态
- - {order.orderId} - - {formatMicros(order.createdAtMicros)} - {order.productTitle || order.productId} - {order.paidAmountCents > 0 - ? formatMoney(order.paidAmountCents) - : '未支付'} - {order.pointsDelta} 泥点 - {formatMoney(order.cumulativeSuccessRefundCents)} - 欠账 {order.unrecoveredPoints} 泥点 - {formatOrderStatus(order.status)}
-
- ) : ( -
暂无充值订单
+
+ {detail.rechargeOrders.length} 笔} + /> + order.createdAtMicros, + }, + { + key: 'product', + label: '商品', + sortable: true, + sortValue: (order) => order.productTitle || order.productId, + }, + { + key: 'paidAmount', + label: '实付', + sortable: true, + sortValue: (order) => order.paidAmountCents, + }, + { + key: 'pointsDelta', + label: '发放泥点', + sortable: true, + sortValue: (order) => order.pointsDelta, + }, + { + key: 'refund', + label: '退款', + sortable: true, + sortValue: (order) => order.cumulativeSuccessRefundCents, + }, + { key: 'status', label: '状态', sortable: true }, + ]} + emptyText="暂无充值订单" + renderRow={(order) => ( + + + {order.orderId} + {formatMicros(order.createdAtMicros)} + + {order.productTitle || order.productId} + + {order.paidAmountCents > 0 + ? formatMoney(order.paidAmountCents) + : '未支付'} + + {order.pointsDelta} 泥点 + + {formatMoney(order.cumulativeSuccessRefundCents)} + 欠账 {order.unrecoveredPoints} 泥点 + + {formatOrderStatus(order.status)} + )} -
- - ) : null} -
+ rows={detail.rechargeOrders} + surface="plain" + tableClassName="admin-user-recharge-table" + /> + + + ) : null} {confirmDialog} -
, + , document.body, ); } @@ -425,36 +415,43 @@ function UserIdentityHeader({ detail }: { detail: AdminUserDetailResponse }) { {detail.displayName || '未设置昵称'} {detail.publicUserCode || '未分配陶泥号'} -
-
-
内部 ID
-
{detail.userId}
-
-
-
脱敏手机号
-
{detail.phoneNumberMasked || '未绑定'}
-
-
-
登录方式
-
{detail.loginMethod || '-'}
-
-
-
累计充值
-
- {typeof detail.cumulativeRechargedCents === 'number' - ? formatMoney(detail.cumulativeRechargedCents) - : '未知'} -
-
-
-
绑定状态
-
- {detail.bindingStatus || '-'} / 手机 - {detail.phoneBound ? '已绑定' : '未绑定'} / 微信 - {detail.wechatBound ? '已绑定' : '未绑定'} -
-
-
+ 内部 ID, + value: <>{detail.userId}, + }, + { + label: <>脱敏手机号, + value: <>{detail.phoneNumberMasked || '未绑定'}, + }, + { + label: <>登录方式, + value: <>{detail.loginMethod || '-'}, + }, + { + label: <>累计充值, + value: ( + <> + {typeof detail.cumulativeRechargedCents === 'number' + ? formatMoney(detail.cumulativeRechargedCents) + : '未知'} + + ), + }, + { + label: <>绑定状态, + value: ( + <> + {detail.bindingStatus || '-'} / 手机 + {detail.phoneBound ? '已绑定' : '未绑定'} / 微信 + {detail.wechatBound ? '已绑定' : '未绑定'} + + ), + }, + ]} + className="admin-user-identity-list" + /> ); } @@ -488,39 +485,36 @@ function WalletSection({ ] as const; return (
-
-

钱包

-
- {canReconcileConsumption ? ( - - ) : null} -
- {wallet.manualFrozen ? ( - 人工冻结 - ) : null} - {wallet.refundDebtFrozen ? ( - 退款欠账限制 - ) : null} - {!wallet.walletFrozen ? ( - 正常 + + {canReconcileConsumption ? ( + + ) : null} + + {wallet.manualFrozen ? 人工冻结 : null} + {wallet.refundDebtFrozen ? ( + 退款欠账限制 + ) : null} + {!wallet.walletFrozen ? ( + 正常 + ) : null} +
-
-
- {reconcileMessage ? ( -
- {reconcileMessage} -
- ) : null} + } + /> + {reconcileMessage ? {reconcileMessage} : null}
{metrics.map(([label, value]) => (
diff --git a/apps/admin-web/src/components/AdminUserReferenceButton.tsx b/apps/admin-web/src/components/AdminUserReferenceButton.tsx index 3141a808b..edc045d0f 100644 --- a/apps/admin-web/src/components/AdminUserReferenceButton.tsx +++ b/apps/admin-web/src/components/AdminUserReferenceButton.tsx @@ -1,3 +1,4 @@ +import { AdminButton } from '@genarrative/shared/components'; import { UserRoundSearch } from 'lucide-react'; import { MouseEvent, useRef, useState } from 'react'; @@ -42,16 +43,17 @@ export function AdminUserReferenceButton({ return ( <> - + {open ? ( { - if (event.key === 'Escape') { - event.preventDefault(); - closeConfirm(false); - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [closeConfirm, pendingConfirm]); + }, [pendingConfirm]); const confirmDialog = pendingConfirm ? ( -
{ - if (event.target === event.currentTarget) { - closeConfirm(false); - } - }} + closeConfirm(false)} > -
-
-

确认操作

- {pendingConfirm.action} -
-
-
-
操作
-
{pendingConfirm.action}
-
-
-
对象
-
{pendingConfirm.target}
-
-
-
该操作会立即影响线上数据
-
- - -
-
-
+ 操作, + value: <>{pendingConfirm.action}, + }, + { + label: <>对象, + value: <>{pendingConfirm.target}, + }, + ]} + /> +
该操作会立即影响线上数据
+
+ closeConfirm(false)} + > + 取消 + + closeConfirm(true)} + > + 确认 + +
+ ) : null; return { diff --git a/apps/admin-web/src/pages/AdminAccountsPage.tsx b/apps/admin-web/src/pages/AdminAccountsPage.tsx index 47c343c94..1e6af7e1b 100644 --- a/apps/admin-web/src/pages/AdminAccountsPage.tsx +++ b/apps/admin-web/src/pages/AdminAccountsPage.tsx @@ -1,3 +1,14 @@ +import { + AdminActionRow, + AdminAlert, + AdminButton, + AdminField, + AdminFormRow, + AdminListPanel, + AdminPage, + AdminPageHeading, + AdminPanel, +} from '@genarrative/shared/components'; import { Plus, RefreshCcw, Save } from 'lucide-react'; import { type FormEvent, useEffect, useState } from 'react'; @@ -159,117 +170,111 @@ export function AdminAccountsPage({ } return ( -
-
-
-

账号管理

-

后台成员

-
-
- - -
-
+ + + + + + + + } + /> - {errorMessage ? ( -
- {errorMessage} -
- ) : null} + {errorMessage ? {errorMessage} : null}
-
-
-

后台账号

- {accounts.length} -
- {accounts.length ? ( -
- {accounts.map((account) => ( - - ))} -
- ) : ( -
- {isLoading ? '加载中' : '暂无成员账号'} -
- )} -
- -
-
-

{selectedAccountId ? '编辑账号' : '添加账号'}

- + }} + > + + {account.displayName || account.username} + {account.username} + + + {account.accountRole === 'owner' + ? 'owner' + : account.enabled + ? '启用' + : '停用'} + + + ))}
+ -
- - -
- - - +
Tab 访问权限
@@ -296,7 +300,6 @@ export function AdminAccountsPage({ ))}
-
独立操作权限
@@ -318,18 +321,13 @@ export function AdminAccountsPage({
- - -
+ +
{confirmDialog} -
+ ); } diff --git a/apps/admin-web/src/pages/AdminAgcModelsPage.tsx b/apps/admin-web/src/pages/AdminAgcModelsPage.tsx index 2a3618fc1..0697eec0a 100644 --- a/apps/admin-web/src/pages/AdminAgcModelsPage.tsx +++ b/apps/admin-web/src/pages/AdminAgcModelsPage.tsx @@ -1,3 +1,8 @@ +import { + AdminListPanel, + AdminPage, + AdminPageHeading, +} from '@genarrative/shared/components'; import { CircleHelp, Plus, RefreshCcw, Save, Trash2 } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -74,16 +79,16 @@ export function AdminAgcModelsPage({ } return ( -
-
-
-

AGC 模型

-

管理客户端可用模型与用户看到的名称

-
- - 版本 v{catalog?.revision ?? '-'} - -
+ + + 版本 v{catalog?.revision ?? '-'} + + } + />
已启用 @@ -106,150 +111,161 @@ export function AdminAgcModelsPage({ {busy ? '处理中' : saved ? '已保存' : '待修改'}
-
-
-
-

模型目录

- 客户端仅显示别名,实际模型名仅在这里维护 -
- -
-
- - + - -
- {error ?

{error}

: null} - {saved ?

已保存

: null} - {busy ?

正在处理

: null} -
- - - - - - - - - - - - {catalog?.models.map((model, index) => ( - - - - - - - - ))} - -
别名实际模型名启用默认操作
- - update(model.id, { alias: e.target.value }) - } - /> - - - update(model.id, { modelId: e.target.value }) - } - /> - - - update(model.id, { enabled: e.target.checked }) - } - /> - - { - setSaved(false); - setCatalog({ ...catalog, defaultModelId: model.id }); - }} - /> - - -
-
-
+ ); + }} + > + + + +
+ {error ?

{error}

: null} + {saved ?

已保存

: null} + {busy ?

正在处理

: null} + + } + renderRow={(model, index) => { + if (!catalog) { + return null; + } + return ( + + + + update(model.id, { + alias: e.target.value, + }) + } + /> + + + + update(model.id, { + modelId: e.target.value, + }) + } + /> + + + + update(model.id, { + enabled: e.target.checked, + }) + } + /> + + + { + setSaved(false); + setCatalog({ + ...catalog, + defaultModelId: model.id, + }); + }} + /> + + + + + + ); + }} + tableClassName="admin-agc-models-table-grid" + wrapClassName="admin-agc-models-table" + /> {confirmDialog} -
+ ); } diff --git a/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx b/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx index 37c3d8809..b6a6a0f11 100644 --- a/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx +++ b/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx @@ -1,4 +1,7 @@ import { + AdminListPanel, + AdminPage, + AdminPageHeading, Button, Modal, SelectField, @@ -294,28 +297,30 @@ function AdminAgcTemplatesSession({ token, onUnauthorized }: PageProps) { const writesDisabled = busy || loading || conflict || !snapshot?.writable; return ( -
-
-

模板管理

-
- - -
-
+ + + + + + } + /> {readOnly ? ( 当前为只读模式,无法保存或上下架 ) : null} @@ -334,133 +339,155 @@ function AdminAgcTemplatesSession({ token, onUnauthorized }: PageProps) { ) : null} {notice ? {notice} : null} -
- setQuery(event.currentTarget.value)} - /> - setRuntime(event.currentTarget.value)} - > - - {runtimes.map((value) => ( - - ))} - - setVisibility(event.currentTarget.value)} - > - - - - -
- {loading ? 正在加载模板 : null} - {snapshot ? ( - - - - 封面 - 模板 - 引擎 / 版本 - 包大小 - 状态 - 操作 - - - - {entries.map((entry) => ( - - - {`${entry.title}封面`} - - - {entry.title} - {entry.id} -

{entry.summary}

-
- {entry.tags.map((tag) => ( - {tag} - ))} -
-
- - {entry.engine || - runtimeLabels[entry.runtime] || - entry.runtime}{' '} - {entry.engineVersion} - 模板 {entry.templateVersion} - - {formatTemplateSize(entry.zipSizeBytes)} - {entry.enabled ? '已上架' : '已下架'} - -
- - -
-
-
- ))} - {!entries.length ? ( - - - {snapshot.templates.length - ? '没有符合筛选条件的模板' - : '暂无模板'} - - - ) : null} -
-
- ) : null} + + setQuery(event.currentTarget.value)} + /> + setRuntime(event.currentTarget.value)} + > + + {runtimes.map((value) => ( + + ))} + + setVisibility(event.currentTarget.value)} + > + + + + + + } + columns={[ + { key: 'cover', label: '封面' }, + { + key: 'title', + label: '模板', + sortable: true, + sortValue: (entry) => entry.title, + }, + { + key: 'engine', + label: '引擎 / 版本', + sortable: true, + sortValue: (entry) => entry.engine || entry.engineVersion, + }, + { + key: 'zipSizeBytes', + label: '包大小', + sortable: true, + sortValue: (entry) => entry.zipSizeBytes, + }, + { + key: 'enabled', + label: '状态', + sortable: true, + sortDescription: '上架状态', + sortValue: (entry) => (entry.enabled ? 1 : 0), + }, + { key: 'actions', label: '操作' }, + ]} + rows={snapshot ? entries : []} + showEmpty={Boolean(snapshot && entries.length === 0)} + emptyText={ + snapshot && snapshot.templates.length + ? '没有符合筛选条件的模板' + : '暂无模板' + } + emptyVariant="row" + renderRow={(entry) => { + if (!snapshot) { + return null; + } + return ( + + + {`${entry.title}封面`} + + + {entry.title} + {entry.id} +

{entry.summary}

+
+ {entry.tags.map((tag) => ( + {tag} + ))} +
+ + + {entry.engine || runtimeLabels[entry.runtime] || entry.runtime}{' '} + {entry.engineVersion} + 模板 {entry.templateVersion} + + {formatTemplateSize(entry.zipSizeBytes)} + {entry.enabled ? '已上架' : '已下架'} + +
+ + +
+ + + ); + }} + tableClassName="admin-agc-template-table" + /> {editing ? ( ) : null} -
+ ); } diff --git a/apps/admin-web/src/pages/AdminAgcTrackingPage.tsx b/apps/admin-web/src/pages/AdminAgcTrackingPage.tsx index 239270c42..28fd2ff29 100644 --- a/apps/admin-web/src/pages/AdminAgcTrackingPage.tsx +++ b/apps/admin-web/src/pages/AdminAgcTrackingPage.tsx @@ -1,4 +1,15 @@ -import { Modal } from '@genarrative/shared/components'; +import { + AdminActionRow, + AdminButton, + AdminField, + AdminListPanel, + AdminPage, + AdminPageHeading, + AdminPagination, + AdminPanel, + formatAdminDateTime, + Modal, +} from '@genarrative/shared/components'; import { FormEvent, useEffect, useRef, useState } from 'react'; import { listAdminAgcTrackingEvents } from '../api/adminApiClient'; @@ -45,11 +56,6 @@ const fieldLabels: Record = { receivedAt: '入库时间', }; -function formatTime(value: string) { - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN'); -} - export function AdminAgcTrackingPage({ token, onUnauthorized, @@ -86,8 +92,8 @@ export function AdminAgcTrackingPage({ useEffect(() => { const saved = firstPage.current; if (!cursor && saved?.token === token && saved.query === query) { - setEntries(saved.response.entries); - setNextCursor(saved.response.nextCursor); + setEntries(saved.response.entries ?? []); + setNextCursor(saved.response.nextCursor ?? null); setError(''); setLoading(false); return; @@ -101,8 +107,9 @@ export function AdminAgcTrackingPage({ .then((response) => { if (!active) return; if (!cursor) firstPage.current = { token, query, response }; - setEntries(response.entries); - setNextCursor(response.nextCursor); + // 接口形状异常时退回空列表,避免整页渲染因 undefined.length 崩掉。 + setEntries(response.entries ?? []); + setNextCursor(response.nextCursor ?? null); }) .catch((failure: unknown) => { if (active) handlePageError(failure, onUnauthorized, setError); @@ -162,43 +169,47 @@ export function AdminAgcTrackingPage({ } return ( -
-
-
-

客户端埋点

-

按发生时间倒序展示

-
- -
-
+ + { + resetPages(); + setRefresh((value) => value + 1); + }} + > + 刷新 + + } + /> +
{(['userId', 'projectId'] as const).map((key) => ( - + ))} - + {(['startTime', 'endTime'] as const).map((key) => ( - + ))}
-
- -
+ + {(['creativeTaskId', 'agentRunId', 'clientVersion'] as const) .filter((key) => query[key]) .map((key) => ( @@ -242,93 +254,81 @@ export function AdminAgcTrackingPage({ {fieldLabels[key]}:{query[key]}(重新查询可清除)

))} - +
{error ? (

{error}

) : null} -
-
- - - - {[ - '入库时间', - '发生时间', - '用户', - '事件名称', - '项目', - '来源', - '结果', - '客户端版本', - '详情', - ].map((label) => ( - - ))} - - - - {entries.map((entry) => ( - - - - - - - - - - - - ))} - -
{label}
{formatTime(entry.receivedAt)}{formatTime(entry.eventTime)} - {entry.userId} - - {eventLabels[entry.eventName] ?? entry.eventName}{entry.projectId ?? '—'}{entry.source}{entry.status ?? '—'}{entry.clientVersion} - -
-
- {loading ? ( -

加载中…

- ) : !error && entries.length === 0 ? ( -

暂无客户端埋点数据

- ) : null} -
- - 第 {page + 1} 页 - -
-
+ next={{ + disabled: !nextCursor, + onClick: () => { + if (!nextCursor) return; + setCursors([...cursors.slice(0, page + 1), nextCursor]); + setPage(page + 1); + }, + }} + /> + } + columns={[ + { key: 'receivedAt', label: '入库时间' }, + { key: 'eventTime', label: '发生时间' }, + { key: 'userId', label: '用户' }, + { key: 'eventName', label: '事件名称' }, + { key: 'projectId', label: '项目' }, + { key: 'source', label: '来源' }, + { key: 'status', label: '结果' }, + { key: 'clientVersion', label: '客户端版本' }, + { key: 'actions', label: '详情' }, + ]} + renderRow={(entry) => ( + + {formatAdminDateTime(entry.receivedAt)} + {formatAdminDateTime(entry.eventTime)} + + {entry.userId} + + + {eventLabels[entry.eventName] ?? entry.eventName} + {entry.projectId ?? '—'} + {entry.source} + {entry.status ?? '—'} + {entry.clientVersion} + + setDetail(entry)} + > + 查看详情 + + + + )} + size="wide" + /> {detail ? ( (
{fieldLabels[key]}
-
+
{detail[key] == null ? '—' : String(detail[key])}
))} -
+ {(['creativeTaskId', 'agentRunId', 'clientVersion'] as const).map( (key) => detail[key] ? ( - + ) : null, )} -
+

事件属性

-
+          
             {JSON.stringify(detail.properties, null, 2)}
           
) : null} -
+ ); } diff --git a/apps/admin-web/src/pages/AdminDashboardPage.tsx b/apps/admin-web/src/pages/AdminDashboardPage.tsx index 111e7e9a0..2e218139c 100644 --- a/apps/admin-web/src/pages/AdminDashboardPage.tsx +++ b/apps/admin-web/src/pages/AdminDashboardPage.tsx @@ -1,3 +1,12 @@ +import { + AdminAlert, + AdminButton, + AdminEmptyState, + AdminField, + AdminPage, + AdminPageHeading, + AdminPanel, +} from '@genarrative/shared/components'; import { RefreshCw } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -157,64 +166,68 @@ export function AdminDashboardPage({ ); return ( -
-
-
-

Dashboard

-

{dashboard?.range.periodLabel ?? '运营指标'}

-
-
-
- - -
-
- {granularityOptions.map((option) => ( - - ))} + + handleDateRangeChange('startDate', event.target.value) + } + /> + + + + handleDateRangeChange('endDate', event.target.value) + } + /> + +
+
+ {granularityOptions.map((option) => ( + + ))} +
+ void loadDashboard()} + > +
- -
- + } + />
- {errorMessage ? ( -
- {errorMessage} -
- ) : null} + {errorMessage ? {errorMessage} : null} {dashboard?.warnings.length ? ( -
-
-

读取告警

- {dashboard.warnings.length} -
+
    {dashboard.warnings.map((message) => (
  • {message}
  • ))}
-
+ ) : null} {activeTab === 'metrics' ? ( <> -
-
-

总计数据

- 全站累计 -
+
{totalMetricCards.map((metric) => ( ))}
-
+ -
-
-

时段数据

- {dashboard?.range.periodLabel ?? '-'} -
+
{periodMetricCards.map((metric) => ( ))}
-
+ -
-
-

新增用户转化与留存

- {dashboard?.range.periodLabel ?? '-'} -
+
-
+
{(dashboard?.charts ?? []).map((chart) => ( @@ -342,17 +345,16 @@ export function AdminDashboardPage({ /> ))} {dashboard && dashboard.charts.length === 0 ? ( -
暂无图表
+ 暂无图表 ) : null}
) : (
-
-
-

运营指标

- {dashboard?.range.periodLabel ?? '-'} -
+
{(dashboard?.operations.cards ?? []).map((card) => ( ))}
-
+
)} -
+ ); function handleGranularityChange( @@ -426,14 +428,11 @@ function MetricCard({ compact?: boolean; }) { return ( -
+ {label} {formatNumber(value)} {unit} -
+ ); } @@ -498,14 +497,16 @@ function ChartPanel({ }, [chart.buckets.length, scrollLeft]); return ( -
-
-

{chart.title}

- + {chart.id === 'visit-users' ? '时段去重 ' : ''} {formatNumber(chart.total)} {chart.unit} - -
+ + } + className="admin-dashboard-chart-card" + >
))}
-
+ ); } @@ -550,11 +551,11 @@ function BreakdownPanel({ }) { const maxValue = Math.max(1, ...rows.map((row) => row.value)); return ( -
-
-

{title}

- {rows.length} -
+
{rows.map((row) => (
@@ -565,15 +566,17 @@ function BreakdownPanel({
- +
))} - {rows.length === 0 ? ( -
暂无数据
- ) : null} + {rows.length === 0 ? 暂无数据 : null} -
+ ); } diff --git a/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx b/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx index a032d4889..b8d05c742 100644 --- a/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx +++ b/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx @@ -1,9 +1,21 @@ +import { + AdminActionRow, + AdminAlert, + AdminButton, + AdminDialog, + AdminField, + AdminJsonPreview, + AdminListPanel, + AdminPage, + AdminPageHeading, + AdminPagination, + AdminPanel, + AdminToast, +} from '@genarrative/shared/components'; import { ArrowDown, ArrowUp, ArrowUpDown, - ChevronLeft, - ChevronRight, Eye, Filter, Plus, @@ -547,84 +559,76 @@ export function AdminDatabaseTablesPage({ if (isExternalApiKeyTable) { return ( -
-
-
-

表查询

-

external_api_key 使用专用安全查询

-
-
- - -
-
- -
- ); - } + + + + + } + /> - return ( -
-
-
-

表查询

-

SpacetimeDB 行数据

-
-
- - -
-
- -
+
- - - - +
{conditions.length ? (
- + {operatorOption?.listValue ? (
- +
) : operatorOption?.needsValue ? ( - + ) : (
)} - + {operatorOption?.listValue && condition.values.length ? (
@@ -900,26 +904,27 @@ export function AdminDatabaseTablesPage({ )}
) : null} -
- -
-
- +
已选表:{selectedTableHeader.optionLabel} @@ -934,272 +939,217 @@ export function AdminDatabaseTablesPage({ ) : null}
-
- + +
- {errorMessage ? ( -
- {errorMessage} -
- ) : null} + {errorMessage ? {errorMessage} : null} {result?.scanLimitReached ? ( -
+ 当前表超过 {result.scanLimit} 条扫描与浏览上限,本次已扫描{' '} {result.scannedCount} 条;当前分页结果和匹配总数可能不完整。 -
+ ) : null} -
-
-

- {resultTableHeader.label} -

- + { + const isSorted = sortColumn === column; + return { + key: column, + header: ( +
+ + +
+ ), + headerProps: { + 'aria-sort': isSorted + ? sortDirection === 'asc' + ? 'ascending' + : 'descending' + : 'none', + }, + }; + })} + count={ + <> 匹配 {totalMatched} 条,本页 {result?.totalReturned ?? 0} 条 -
-
-
- - - - {columnHeaders.map(({ column, label, description }) => { - const isSorted = sortColumn === column; - return ( - setDetailRow(row)} + onKeyDown={(event) => handleRowKeyDown(event, row)} + > + {visibleColumns.map((column) => { + const rawValue = row.cells[column]; + const cellValue = formatCellValue(rawValue, column); + const userReference = resolveAdminDatabaseUserReference( + result?.tableName ?? '', + column, + row.cells[column], + ); + return ( + - - - {result?.rows.length ? ( - result.rows.map((row, rowIndex) => ( - setDetailRow(row)} - onKeyDown={(event) => handleRowKeyDown(event, row)} - > - {visibleColumns.map((column) => { - const rawValue = row.cells[column]; - const cellValue = formatCellValue(rawValue, column); - const userReference = resolveAdminDatabaseUserReference( - result.tableName, - column, - row.cells[column], - ); - return ( - - ); - })} - - )) - ) : ( - - - - )} - -
+ } + emptyText="暂无数据" + emptyVariant="row" + renderRow={(row, rowIndex) => ( +
+
+ -
- - -
- - ); - })} -
-
- - {getJsonTablePreviewText(rawValue) ? ( - - {renderJsonSyntax( - getJsonTablePreviewText(rawValue) ?? '', - )} - - ) : ( - cellValue.content - )} - - {userReference ? ( - - ) : null} -
-
暂无数据
-
-
+ + ) : ( + cellValue.content + )} + + {userReference ? ( + + ) : null} + + + ); + })} + + )} + rows={result?.rows ?? []} + showEmpty={!result?.rows.length} + size="wide" + tableClassName="admin-database-table" + title={resultTableHeader.label} + titleProps={{ + title: resultTableHeader.description, + }} + /> {result ? ( - + + 第 {currentPage} / {totalPages} 页,共 {totalMatched} 条 + + } + busy={isLoadingRows} + previous={{ + disabled: currentPage <= 1, + onClick: () => handlePageChange(currentPage - 1), + }} + next={{ + disabled: currentPage >= totalPages, + onClick: () => handlePageChange(currentPage + 1), + }} + /> ) : null} {copyToast ? ( -
- {copyToast.message} -
+ {copyToast.message} ) : null} {detailRow ? ( -
-
-
-

行详情

-
- -
+ setDetailRow(null)}> +
+
+ 字段 + 值 + 操作
-
-
- 字段 - 值 - 操作 -
-
- {detailFields.map( - ({ column, label, description, cellValue, jsonPreview }) => ( +
+ {detailFields.map( + ({ column, label, description, cellValue, jsonPreview }) => ( +
-
- {label} -
-
- {jsonPreview ? ( -
-                            {renderJsonSyntax(jsonPreview)}
-                          
- ) : ( - cellValue.content - )} -
-
-
- -
+ {label} +
+
+ {jsonPreview ? ( + + {renderJsonSyntax(jsonPreview)} + + ) : ( + cellValue.content + )} +
+
+
+ + void handleCopyDetailField( + column, + detailRow?.cells[column], + ) + } + > + 复制 +
- ), - )} -
+
+ ), + )}
-
-
+ + ) : null} -
+ ); } @@ -1311,50 +1261,44 @@ function AdminExternalApiKeysPanel({ return ( <> -
+
- - - - - - - - - - - +
-
- - + 必须提供 owner、公开用户编号、keyId 或精确前缀;只返回安全元数据。 -
- + +
- {errorMessage ? ( -
- {errorMessage} -
- ) : null} + {errorMessage ? {errorMessage} : null} -
-
-

External API Key 安全视图

- {result ? `匹配 ${result.total} 条` : '尚未查询'} -
-
- - - - - - - - - - - - - - - {result?.keys.length ? ( - result.keys.map((key) => ( - - - - - - - - - - - )) - ) : ( - - - - )} - -
Key IDOwner名称前缀Scope创建时间状态详情
{key.keyId}{key.ownerUserId}{key.name}{key.keyPrefix}{key.scopes.join(', ')}{formatSafeDate(key.createdAt)}{key.status} - -
- {result ? '暂无数据' : '请先提供精确范围并查询'} -
-
- {result?.scanLimitReached ? ( -
- 当前查询达到 {result.scanLimit} 条安全扫描上限,已扫描{' '} - {result.scannedCount} 条; 总数和分页可能不完整,请缩小 owner、Key - ID 或精确前缀范围。 -
- ) : null} - {result ? ( - - ) : null} -
+ next={{ + disabled: !canGoToNextKeyPage, + onClick: () => + void submit(undefined, resultOffset + resultLimit), + }} + /> + ) : null} + + } + /> {selectedKey ? ( -
-
-
-

API Key 安全详情

- -
-
-
Key ID
-
{selectedKey.keyId}
-
Owner
-
{selectedKey.ownerUserId}
-
名称
-
{selectedKey.name}
-
前缀
-
{selectedKey.keyPrefix}
-
Scope
-
{selectedKey.scopes.join(', ') || '-'}
-
创建时间
-
{formatSafeDate(selectedKey.createdAt)}
-
最近使用
-
{formatSafeDate(selectedKey.lastUsedAt)}
-
撤销时间
-
{formatSafeDate(selectedKey.revokedAt)}
-
更新时间
-
{formatSafeDate(selectedKey.updatedAt)}
-
状态
-
{selectedKey.status}
-
-
-
+ setSelectedKey(null)} + > +
+
Key ID
+
{selectedKey.keyId}
+
Owner
+
{selectedKey.ownerUserId}
+
名称
+
{selectedKey.name}
+
前缀
+
{selectedKey.keyPrefix}
+
Scope
+
{selectedKey.scopes.join(', ') || '-'}
+
创建时间
+
{formatSafeDate(selectedKey.createdAt)}
+
最近使用
+
{formatSafeDate(selectedKey.lastUsedAt)}
+
撤销时间
+
{formatSafeDate(selectedKey.revokedAt)}
+
更新时间
+
{formatSafeDate(selectedKey.updatedAt)}
+
状态
+
{selectedKey.status}
+
+
) : null} ); diff --git a/apps/admin-web/src/pages/AdminDebugHttpPage.tsx b/apps/admin-web/src/pages/AdminDebugHttpPage.tsx index 33148e2e7..5600eb506 100644 --- a/apps/admin-web/src/pages/AdminDebugHttpPage.tsx +++ b/apps/admin-web/src/pages/AdminDebugHttpPage.tsx @@ -1,3 +1,14 @@ +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'; @@ -75,19 +86,13 @@ export function AdminDebugHttpPage({ } return ( -
-
-
-

API 调试

-

受控同源请求

-
-
+ +
-
-
- - -
+ +
Headers - +
{headers.map((header, index) => ( @@ -132,7 +142,10 @@ export function AdminDebugHttpPage({ setHeaders((current) => current.map((item, itemIndex) => itemIndex === index - ? { ...item, name: event.target.value } + ? { + ...item, + name: event.target.value, + } : item, ), ) @@ -144,14 +157,17 @@ export function AdminDebugHttpPage({ setHeaders((current) => current.map((item, itemIndex) => itemIndex === index - ? { ...item, value: event.target.value } + ? { + ...item, + value: event.target.value, + } : item, ), ) } /> - +
))}
-