From 0c9804f283b36018a532823d53d04219dd046761 Mon Sep 17 00:00:00 2001 From: lsy Date: Mon, 16 Mar 2026 01:43:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin-ui):=20=E5=8F=B3=E4=B8=8A=E8=A7=92Toa?= =?UTF-8?q?st=E9=80=9A=E7=9F=A5=E4=B8=8E=E5=BC=B9=E7=AA=97=E9=81=AE?= =?UTF-8?q?=E7=BD=A9=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .helloagents/CHANGELOG.md | 21 + .helloagents/modules/admin-ui.md | 5 +- .helloagents/modules/dashboard.md | 1 + apps/ui/src/App.tsx | 366 ++++++----- apps/ui/src/core/utils.ts | 4 +- apps/ui/src/features/AppLayout.tsx | 78 +-- apps/ui/src/features/DashboardView.tsx | 242 +++---- apps/ui/src/features/LoginView.tsx | 41 +- apps/ui/src/features/ModelsView.tsx | 40 +- apps/ui/src/features/SettingsView.tsx | 32 +- apps/ui/src/features/SitesView.tsx | 859 ++++++++++++------------- apps/ui/src/features/TokensView.tsx | 474 +++++++------- apps/ui/src/features/UsageView.tsx | 658 +++++++++---------- apps/ui/src/styles.css | 434 ++++++++++++- 14 files changed, 1779 insertions(+), 1476 deletions(-) diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 7ac114b..e2411ab 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -4,6 +4,9 @@ ### 变更 +- **[admin-ui/dashboard]**: Dashboard 样板页引入 Apple 风主题(bento grid 卡片/表格/按钮重绘) + - 方案: [202603152243_apple-ui-sample-dashboard](archive/2026-03/202603152243_apple-ui-sample-dashboard/) + - 文件: apps/ui/src/features/DashboardView.tsx, apps/ui/src/styles.css, .helloagents/modules/admin-ui.md, .helloagents/modules/dashboard.md - **[sites]**: 站点类型接口字段由 claude 改为 Anthropic,并新增数据迁移脚本 - 类型: 变更(无方案包) - 文件: apps/worker/src/services/site-metadata.ts, apps/worker/src/routes/sites.ts, apps/worker/src/services/channel-metadata.ts, apps/worker/src/services/provider-transform.ts, apps/worker/src/routes/proxy.ts, apps/worker/migrations/0003_rename_claude_anthropic.sql, apps/ui/src/core/types.ts, apps/ui/src/core/sites.ts, apps/ui/src/features/SitesView.tsx, apps/ui/src/App.tsx, tests/worker/provider-transform.test.ts, tests/worker/channel-metadata.test.ts, helloagents/modules/admin-ui.md, helloagents/modules/sites.md, helloagents/modules/channels.md, helloagents/modules/proxy.md @@ -171,6 +174,24 @@ - 类型: 微调(无方案包) - 文件: apps/worker/src/routes/proxy.ts +### 修复 + +- **[admin-ui]**: Toast 通知固定右上并带进度条,弹窗改为全屏遮罩,筛选标题横排 — by lsy + - 方案: [202603160049_modal-toast-search-fixes](archive/2026-03/202603160049_modal-toast-search-fixes/) + - 决策: modal-toast-search-fixes#D001(通知统一为右上 Toast) + +## [0.9.6] - 2026-03-15 + +### 修复 +- **[opencode-config]**: 配置 Gemini + Claude 模型并补充鉴权头 — by lsy + - 方案: [202603152216_opencode-models-gemini-claude](archive/2026-03/202603152216_opencode-models-gemini-claude/) + +## [0.9.5] - 2026-03-15 + +### 修复 +- **[opencode-config]**: 更新 OpenCode 模型列表与默认模型 — by lsy + - 方案: [202603152138_opencode-model-config](archive/2026-03/202603152138_opencode-model-config/) + ### 快速修改 - **[admin-ui]**: 修复模型广场渲染语法错误 — by lsy diff --git a/.helloagents/modules/admin-ui.md b/.helloagents/modules/admin-ui.md index 956fc39..fae6713 100644 --- a/.helloagents/modules/admin-ui.md +++ b/.helloagents/modules/admin-ui.md @@ -15,6 +15,7 @@ - 入口文件为 `apps/ui/src/App.tsx`(`main.tsx` 已移除) - 按 `core/`(类型/常量/API)、`features/`(业务视图 + 布局/登录)组织前端模块 - 样式基于 Tailwind v4(结合全局主题变量) +- Dashboard 样板页引入 `apple-demo` 主题类(Apple 风格、bento grid) - 前端状态集中在单页 - 站点管理采用列表视图,支持分页(默认 10 条),新增/编辑使用弹窗,保留启禁/连通测试操作 - 站点管理支持名称/URL 搜索与列头排序,搜索/排序后再分页 @@ -37,10 +38,10 @@ - 移动端使用顶部栏 + 抽屉导航,侧边栏在桌面端保持固定 - 渠道/令牌列表在移动端以卡片形式展示,操作区改为网格按钮排列 - 数据面板、模型广场、使用日志表格在移动端支持横向滚动与字号收紧 -- 全局通知支持成功/提示/错误/信息分级展示,可关闭并自动消退 +- 全局通知采用右上角 Toast 展示,支持关闭与进度条自动消退 - 删除类操作必须经过确认弹窗 - 主要异步操作提供 loading 与按钮禁用态 -- 弹窗支持遮罩/ESC 关闭并自动聚焦首字段 +- 弹窗支持全屏遮罩/ESC 关闭,不强制自动聚焦首字段 - 空状态提供清晰 CTA 引导下一步 - 全局字体通过外部字体源加载,背景包含暖调渐变与轻纹理 diff --git a/.helloagents/modules/dashboard.md b/.helloagents/modules/dashboard.md index fb75726..32b7cff 100644 --- a/.helloagents/modules/dashboard.md +++ b/.helloagents/modules/dashboard.md @@ -10,6 +10,7 @@ ## 行为规范 - 以 `usage_logs` 为统计来源 - 默认返回最近 30 条趋势数据 +- 前端 DashboardView 使用 `apple-demo` 样板主题(Apple 风格) ## 依赖关系 - `usage_logs` 表 diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index cc05db9..bb43d8b 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -35,11 +35,15 @@ import type { SiteType, TabId, Token, + TokenForm, UsageQuery, UsageResponse, - TokenForm, } from "./core/types"; -import { toChinaDateTimeInput, toChinaIsoFromInput, toggleStatus } from "./core/utils"; +import { + toChinaDateTimeInput, + toChinaIsoFromInput, + toggleStatus, +} from "./core/utils"; import { AppLayout } from "./features/AppLayout"; import { DashboardView } from "./features/DashboardView"; import { LoginView } from "./features/LoginView"; @@ -136,15 +140,13 @@ const App = () => { const [tokenPage, setTokenPage] = useState(1); const [tokenPageSize, setTokenPageSize] = useState(10); const [editingToken, setEditingToken] = useState(null); - const [tokenForm, setTokenForm] = - useState(initialTokenForm); + const [tokenForm, setTokenForm] = useState(initialTokenForm); const [usagePage, setUsagePage] = useState(1); const [usagePageSize, setUsagePageSize] = useState(50); const [usageTotal, setUsageTotal] = useState(0); const [usageFilters, setUsageFilters] = useState(initialUsageQuery); - const [usageQuery, setUsageQuery] = - useState(initialUsageQuery); + const [usageQuery, setUsageQuery] = useState(initialUsageQuery); const [editingSite, setEditingSite] = useState(null); const [siteForm, setSiteForm] = useState(() => ({ ...initialSiteForm, @@ -169,9 +171,12 @@ const App = () => { } }, []); - const pushNotice = useCallback((tone: NoticeTone, message: string) => { - setNotice({ tone, message, id: Date.now() }); - }, []); + const pushNotice = useCallback( + (tone: NoticeTone, message: string, durationMs?: number) => { + setNotice({ tone, message, id: Date.now(), durationMs }); + }, + [], + ); const dismissNotice = useCallback(() => { setNotice(null); @@ -181,9 +186,10 @@ const App = () => { if (!notice) { return; } + const durationMs = notice.durationMs ?? 4500; const timer = window.setTimeout(() => { setNotice(null); - }, 4500); + }, durationMs); return () => window.clearTimeout(timer); }, [notice]); @@ -524,14 +530,7 @@ const App = () => { endAction(actionKey); } }, - [ - endAction, - isActionPending, - loadUsage, - pushNotice, - startAction, - usagePage, - ], + [endAction, isActionPending, loadUsage, pushNotice, startAction, usagePage], ); const handleUsagePageSizeChange = useCallback( @@ -554,12 +553,9 @@ const App = () => { [endAction, isActionPending, loadUsage, pushNotice, startAction], ); - const handleUsageFiltersChange = useCallback( - (patch: Partial) => { - setUsageFilters((prev) => ({ ...prev, ...patch })); - }, - [], - ); + const handleUsageFiltersChange = useCallback((patch: Partial) => { + setUsageFilters((prev) => ({ ...prev, ...patch })); + }, []); const handleUsageSearch = useCallback(async () => { const actionKey = buildActionKey("usage:load"); @@ -613,15 +609,18 @@ const App = () => { } }, [endAction, isActionPending, loadUsage, pushNotice, startAction]); - const handleTabChange = useCallback((tabId: TabId) => { - const nextPath = tabToPath[tabId]; - const normalized = normalizePath(window.location.pathname); - if (normalized !== nextPath) { - history.pushState(null, "", nextPath); - } - dismissNotice(); - setActiveTab(tabId); - }, [dismissNotice]); + const handleTabChange = useCallback( + (tabId: TabId) => { + const nextPath = tabToPath[tabId]; + const normalized = normalizePath(window.location.pathname); + if (normalized !== nextPath) { + history.pushState(null, "", nextPath); + } + dismissNotice(); + setActiveTab(tabId); + }, + [dismissNotice], + ); const closeSiteModal = useCallback(() => { setEditingSite(null); @@ -643,48 +642,51 @@ const App = () => { dismissNotice(); }, [dismissNotice]); - const startSiteEdit = useCallback((site: Site) => { - setEditingSite(site); - const callTokens = - site.call_tokens && site.call_tokens.length > 0 - ? site.call_tokens - : site.api_key - ? [ + const startSiteEdit = useCallback( + (site: Site) => { + setEditingSite(site); + const callTokens = + site.call_tokens && site.call_tokens.length > 0 + ? site.call_tokens + : site.api_key + ? [ + { + id: "", + name: "主调用令牌", + api_key: site.api_key, + }, + ] + : []; + const tokenForms = + callTokens.length > 0 + ? callTokens.map((token) => ({ + id: token.id, + name: token.name, + api_key: token.api_key, + })) + : [ { - id: "", name: "主调用令牌", - api_key: site.api_key, + api_key: "", }, - ] - : []; - const tokenForms = - callTokens.length > 0 - ? callTokens.map((token) => ({ - id: token.id, - name: token.name, - api_key: token.api_key, - })) - : [ - { - name: "主调用令牌", - api_key: "", - }, - ]; - setSiteForm({ - name: site.name ?? "", - base_url: site.base_url ?? "", - weight: site.weight ?? 1, - status: site.status ?? "active", - site_type: site.site_type ?? "new-api", - checkin_url: site.checkin_url ?? "", - system_token: site.system_token ?? "", - system_userid: site.system_userid ?? "", - checkin_enabled: Boolean(site.checkin_enabled ?? false), - call_tokens: tokenForms, - }); - setSiteModalOpen(true); - dismissNotice(); - }, [dismissNotice]); + ]; + setSiteForm({ + name: site.name ?? "", + base_url: site.base_url ?? "", + weight: site.weight ?? 1, + status: site.status ?? "active", + site_type: site.site_type ?? "new-api", + checkin_url: site.checkin_url ?? "", + system_token: site.system_token ?? "", + system_userid: site.system_userid ?? "", + checkin_enabled: Boolean(site.checkin_enabled ?? false), + call_tokens: tokenForms, + }); + setSiteModalOpen(true); + dismissNotice(); + }, + [dismissNotice], + ); const closeTokenModal = useCallback(() => { setTokenModalOpen(false); @@ -692,21 +694,24 @@ const App = () => { setTokenForm({ ...initialTokenForm }); }, []); - const openTokenEdit = useCallback((tokenItem: Token) => { - setEditingToken(tokenItem); - setTokenForm({ - name: tokenItem.name ?? "", - quota_total: - tokenItem.quota_total === null || tokenItem.quota_total === undefined - ? "" - : String(tokenItem.quota_total), - status: tokenItem.status ?? "active", - expires_at: toChinaDateTimeInput(tokenItem.expires_at ?? null), - allowed_channels: tokenItem.allowed_channels ?? [], - }); - setTokenModalOpen(true); - dismissNotice(); - }, [dismissNotice]); + const openTokenEdit = useCallback( + (tokenItem: Token) => { + setEditingToken(tokenItem); + setTokenForm({ + name: tokenItem.name ?? "", + quota_total: + tokenItem.quota_total === null || tokenItem.quota_total === undefined + ? "" + : String(tokenItem.quota_total), + status: tokenItem.status ?? "active", + expires_at: toChinaDateTimeInput(tokenItem.expires_at ?? null), + allowed_channels: tokenItem.allowed_channels ?? [], + }); + setTokenModalOpen(true); + dismissNotice(); + }, + [dismissNotice], + ); const handleSiteTest = useCallback( async (id: string) => { @@ -775,7 +780,10 @@ const App = () => { const summary = summarizeSiteTests(results); const testedTotal = summary.success + summary.failed; if (testedTotal === 0) { - pushNotice("info", `已跳过 ${summary.skipped} 个禁用站点,暂无可测试站点。`); + pushNotice( + "info", + `已跳过 ${summary.skipped} 个禁用站点,暂无可测试站点。`, + ); return; } let message = @@ -789,7 +797,15 @@ const App = () => { } finally { endAction(actionKey); } - }, [apiFetch, data.sites, endAction, isActionPending, loadSites, pushNotice, startAction]); + }, [ + apiFetch, + data.sites, + endAction, + isActionPending, + loadSites, + pushNotice, + startAction, + ]); const handleSiteSubmit = useCallback( async (event: Event) => { @@ -906,8 +922,7 @@ const App = () => { return; } const quotaInput = tokenForm.quota_total.trim(); - const quotaTotal = - quotaInput.length === 0 ? null : Number(quotaInput); + const quotaTotal = quotaInput.length === 0 ? null : Number(quotaInput); if (quotaInput.length > 0 && Number.isNaN(quotaTotal)) { pushNotice("warning", "额度需为数字"); return; @@ -965,23 +980,23 @@ const App = () => { } finally { endAction(actionKey); } - }, - [ - apiFetch, - editingToken, - endAction, - initialTokenForm, - isActionPending, - loadTokens, - pushNotice, - startAction, - tokenForm.expires_at, - tokenForm.allowed_channels, - tokenForm.name, - tokenForm.quota_total, - tokenForm.status, - ], -); + }, + [ + apiFetch, + editingToken, + endAction, + initialTokenForm, + isActionPending, + loadTokens, + pushNotice, + startAction, + tokenForm.expires_at, + tokenForm.allowed_channels, + tokenForm.name, + tokenForm.quota_total, + tokenForm.status, + ], + ); const handleSettingsSubmit = useCallback( async (event: Event) => { @@ -1004,10 +1019,7 @@ const App = () => { pushNotice("warning", "请填写有效的保留天数与会话时长"); return; } - if ( - Number.isNaN(failureCooldownMinutes) || - failureCooldownMinutes < 1 - ) { + if (Number.isNaN(failureCooldownMinutes) || failureCooldownMinutes < 1) { pushNotice("warning", "失败冷却需为正整数"); return; } @@ -1238,7 +1250,14 @@ const App = () => { } finally { endAction(actionKey); } - }, [apiFetch, endAction, isActionPending, loadSites, pushNotice, startAction]); + }, [ + apiFetch, + endAction, + isActionPending, + loadSites, + pushNotice, + startAction, + ]); const handleUsageRefresh = useCallback(async () => { const actionKey = buildActionKey("usage:refresh"); @@ -1311,15 +1330,15 @@ const App = () => { const renderContent = () => { if (loading) { return ( -
-
- +
+
+ 正在加载数据...
-
-
-
+
+
+
); @@ -1427,15 +1446,11 @@ const App = () => { /> ); } - return ( -
- 未知模块 -
- ); + return
未知模块
; }; return ( -
+
- {notice && ( -
+ {children} + + {notice && ( + +
- + {noticeToneLabel[notice.tone]} -
+
{notice.message}
+
- )} - {children} - + + )}
-
- 暂无数据,请先产生调用或刷新面板。 +
+
空状态
+

+ 暂无数据,请先产生调用或刷新面板。 +

-
-
- - 总请求 - -
- {dashboard.summary.total_requests} -
- - 最近窗口 - +
+
+ 总请求 +
{dashboard.summary.total_requests}
+ 最近窗口
-
- - 总 Tokens - -
- {dashboard.summary.total_tokens} -
- - 累计消耗 - +
+ 总 Tokens +
{dashboard.summary.total_tokens}
+ 累计消耗
-
- - 成功率 - -
- {successRate}% -
- - 错误率 {errorRate}% - +
+ 成功率 +
{successRate}%
+ 错误率 {errorRate}%
-
- - 单次消耗 - -
- {avgTokensPerRequest} -
- +
+ 单次消耗 +
{avgTokensPerRequest}
+ 平均 {Math.round(dashboard.summary.avg_latency)}ms 延迟
-
-
+
+
-

- 按日趋势 -

+

按日趋势

- +
- - - + + + {dashboard.byDay.map((row) => ( - - - - + + + + ))}
- 日期 - - 请求 - - Tokens - 日期请求Tokens
- {row.day} - - {row.requests} - - {row.tokens} -
{row.day}{row.requests}{row.tokens}
-
+
-

- 模型排行 -

+

模型排行

- +
- - - + + + {dashboard.byModel.map((row) => ( - - - - + + + + ))} @@ -217,85 +165,51 @@ export const DashboardView = ({ -
-
+
+
-

- 渠道贡献 -

+

渠道贡献

-
- 模型 - - 请求 - - Tokens - 模型请求Tokens
- {row.model ?? "-"} - - {row.requests} - - {row.tokens} -
{row.model ?? "-"}{row.requests}{row.tokens}
+
- - - + + + {dashboard.byChannel.map((row) => ( - - - - + + + + ))}
- 渠道 - - 请求 - - Tokens - 渠道请求Tokens
- {row.channel_name ?? "-"} - - {row.requests} - - {row.tokens} -
{row.channel_name ?? "-"}{row.requests}{row.tokens}
-
+
-

- 令牌贡献 -

+

令牌贡献

- +
- - - + + + {dashboard.byToken.map((row) => ( - - - - + + + + ))} diff --git a/apps/ui/src/features/LoginView.tsx b/apps/ui/src/features/LoginView.tsx index 33c690e..878a28a 100644 --- a/apps/ui/src/features/LoginView.tsx +++ b/apps/ui/src/features/LoginView.tsx @@ -15,51 +15,52 @@ type LoginViewProps = { * Returns: * Login JSX element. */ -export const LoginView = ({ notice, isSubmitting, onSubmit }: LoginViewProps) => { +export const LoginView = ({ + notice, + isSubmitting, + onSubmit, +}: LoginViewProps) => { const toneStyles: Record = { - success: "border-emerald-200 bg-emerald-50 text-emerald-800", - warning: "border-amber-200 bg-amber-50 text-amber-800", - error: "border-rose-200 bg-rose-50 text-rose-700", - info: "border-sky-200 bg-sky-50 text-sky-800", + success: "app-notice app-notice--success", + warning: "app-notice app-notice--warning", + error: "app-notice app-notice--error", + info: "app-notice app-notice--info", }; return ( -
-

- api-workers -

-

请输入管理员密码登录管理台。

+
+

api-workers

+

+ 请输入管理员密码登录管理台。 +

-

按回车键可快速提交。

+

+ 按回车键可快速提交。 +

{notice && ( -
- {notice.message} -
+
{notice.message}
)}
); diff --git a/apps/ui/src/features/ModelsView.tsx b/apps/ui/src/features/ModelsView.tsx index 5cdb29e..89aa0f0 100644 --- a/apps/ui/src/features/ModelsView.tsx +++ b/apps/ui/src/features/ModelsView.tsx @@ -18,49 +18,35 @@ export const ModelsView = ({ models }: ModelsViewProps) => { models.flatMap((model) => model.channels.map((channel) => channel.id)), ).size; return ( -
+
-

- 模型广场 -

-

- 当前聚合的模型与所属渠道清单。 -

+

模型广场

+

当前聚合的模型与所属渠道清单。

-
- - {models.length} 个模型 - - - {channelCount} 个渠道 - +
+ {models.length} 个模型 + {channelCount} 个渠道
{models.length === 0 ? ( -
+
暂无模型,请先在站点管理配置可用渠道。
) : (
-
- 令牌 - - 请求 - - Tokens - 令牌请求Tokens
- {row.token_name ?? "-"} - - {row.requests} - - {row.tokens} -
{row.token_name ?? "-"}{row.requests}{row.tokens}
+
- - + + {models.map((model) => ( - - - + + diff --git a/apps/ui/src/features/SettingsView.tsx b/apps/ui/src/features/SettingsView.tsx index 6909094..ba3b451 100644 --- a/apps/ui/src/features/SettingsView.tsx +++ b/apps/ui/src/features/SettingsView.tsx @@ -24,22 +24,20 @@ export const SettingsView = ({ onSubmit, onFormChange, }: SettingsViewProps) => ( -
+
-

- 系统设置 -

+

系统设置

-

+

同一模型失败后在该时间内跳过对应渠道。

-

+

密码状态:{adminPasswordSet ? "已设置" : "未设置"}

-
- 模型 - - 渠道 - 模型渠道
- {model.id} - +
{model.id} {model.channels.map((channel) => channel.name).join(" / ")}
- - - - - - - - - - - - - - - - - {usage.length === 0 ? ( +
+
+
- 时间 - - 模型 - - 渠道 - - 令牌 - - 输入 Tokens - - 输出 Tokens - - 用时 (s) - - 首 token 延迟 (s) - - 流式 - - 推理强度 - - 状态码 -
+ - + + + + + + + + + + + - ) : ( - usage.map((log) => { - const statusDetail = buildUsageStatusDetail(log); - const hasDetail = Boolean( - log.error_message || log.error_code, - ); - return ( - - - - - - - - - - - - + + {usage.length === 0 ? ( + + - ); - }) - )} - -
-
- 暂无日志,先完成一次调用吧。 - -
-
时间模型渠道令牌输入 Tokens输出 Tokens用时 (s)首 token 延迟 (s)流式推理强度状态码
- {formatDateTime(log.created_at)} - - {log.model ?? "-"} - - {log.channel_name ?? log.channel_id ?? "-"} - - {log.token_name ?? log.token_id ?? "-"} - - {formatTokens(log.prompt_tokens)} - - {formatTokens(log.completion_tokens)} - - {formatSeconds(log.latency_ms)} - - {formatSeconds(log.first_token_latency_ms)} - - {formatStream(log.stream)} - - {log.reasoning_effort ?? "-"} - -
- {statusDetail.label} -
- {hasDetail ? ( +
+
+ 暂无日志,先完成一次调用吧。 - ) : null} +
+ ) : ( + usage.map((log) => { + const statusDetail = buildUsageStatusDetail(log); + const hasDetail = Boolean( + log.error_message || log.error_code, + ); + return ( + + + {formatDateTime(log.created_at)} + + + {log.model ?? "-"} + + + {log.channel_name ?? log.channel_id ?? "-"} + + + {log.token_name ?? log.token_id ?? "-"} + + + {formatTokens(log.prompt_tokens)} + + + {formatTokens(log.completion_tokens)} + + + {formatSeconds(log.latency_ms)} + + + {formatSeconds(log.first_token_latency_ms)} + + + {formatStream(log.stream)} + + + {log.reasoning_effort ?? "-"} + + + + {statusDetail.label} + + {hasDetail ? ( + + ) : null} + + + ); + }) + )} + + +
-
-
-
- - 共 {totalPages} 页 · {total} 条 - - - {pageItems.map((item, index) => - item === "ellipsis" ? ( - - ... - - ) : ( - - ), - )} - +
+
+ + 共 {totalPages} 页 · {total} 条 + + + {pageItems.map((item, index) => + item === "ellipsis" ? ( + + ... + + ) : ( + + ), + )} + +
+
-
{activeErrorLog ? ( -
-