From 0dfad675b8612b067aff77e75b36408503c58465 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 4 Oct 2026 12:30:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin-web):=20=E5=88=A0=E6=8E=89=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E7=A1=AC=E7=BC=96=E7=A0=81=E5=81=87=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E6=88=90=E8=AF=9A=E5=AE=9E=E7=9A=84=E7=A9=BA?= =?UTF-8?q?=E6=80=81/=E9=94=99=E8=AF=AF=E6=80=81=EF=BC=88P0-24=EF=BC=8C?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=20=C2=A74.3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Automations.tsx / Llm.tsx 原有的 sample 回退("后端在线但列表为空 → 渲样例数据")整段删除 - 后端不可达 → 明确错误态;后端可达但没有数据 → 空列表 + 引导文案(对齐 Users.tsx 已修做法) - 复核:grep sample 零命中;npx tsc --noEmit 0 错误;npm run build 成功 --- .../web/src/admin/modules/Automations.tsx | 127 ++++++++++++++---- frontend/web/src/admin/modules/Llm.tsx | 95 ++++++++++--- 2 files changed, 175 insertions(+), 47 deletions(-) diff --git a/frontend/web/src/admin/modules/Automations.tsx b/frontend/web/src/admin/modules/Automations.tsx index bd67a0c..2763ea0 100644 --- a/frontend/web/src/admin/modules/Automations.tsx +++ b/frontend/web/src/admin/modules/Automations.tsx @@ -10,20 +10,101 @@ import { import type { AdminAutomationRecord } from "../../lib/types"; import { useAuditStore } from "../audit"; import { Notice, PageHeader } from "../components"; -import { Button, Surface, Switch, Tag } from "../../components/primitives"; +import { Button, EmptyState, Surface, Switch, Tag } from "../../components/primitives"; import { Icon } from "../../components/Icon"; -const sample: AdminAutomationRecord[] = [ - { id: "r1", name: "睡前总结", kind: "reminder", cron: "0 22 * * *", enabled: true }, - { id: "r2", name: "晨间问候", kind: "cron", cron: "0 8 * * *", enabled: true }, - { id: "r3", name: "设备待机提醒", kind: "queue", cron: "—", enabled: false }, -]; +interface AutomationRowProps { + r: AdminAutomationRecord; + busy: boolean; + onRun: (id: string, label: string) => void; + onDelete: (id: string, label: string) => void; +} + +/** 一条自动化任务行:字段全部来自接口返回值,动作直接打在真实端点上。 */ +function AutomationRow({ r, busy, onRun, onDelete }: AutomationRowProps): React.ReactNode { + return ( +
+ {r.name} + {r.kind} + {r.cron || "—"} + + {}} label="启用" /> + + + +
+ ); +} + +interface AutomationListProps { + rows: AdminAutomationRecord[]; + loading: boolean; + error: string | null; + /** 后端明确表示未提供该端点(404/501)—— 与「请求失败」「真为空」是三种状态。 */ + unimplemented: boolean; + busy: boolean; + onRun: (id: string, label: string) => void; + onDelete: (id: string, label: string) => void; +} + +/** 列表区:把「加载中 / 端点未实现 / 请求失败 / 真为空 / 有数据」分开渲染。 + * 每种状态都只反映接口的真实结果,绝不回退到示例条目。 */ +function AutomationList({ + rows, + loading, + error, + unimplemented, + busy, + onRun, + onDelete, +}: AutomationListProps): React.ReactNode { + if (loading) { + return

加载中…

; + } + if (rows.length === 0) { + if (unimplemented) { + return ( + + 后端未提供 GET /api/v1/admin/automations(404/501):列表读不到。本页只渲染接口返回的真实数据,不显示任何示例条目。 + + ); + } + if (error) { + return ( + + 加载自动化列表失败(后端不可达或接口报错):{error} —— 这是错误态,不是空列表。 + + ); + } + return ( + } + title="还没有自动化任务" + hint="用上方表单新增第一条提醒 / cron / 队列事件。" + /> + ); + } + return ( +
+ {error ? 刷新失败,下方仍是上次成功读取的数据:{error} : null} + {rows.map((r) => ( + + ))} +
+ ); +} export function AutomationsModule(): React.ReactNode { - const { data, error, reload } = useAsyncData(adminListAutomations, []); + const { data, error, loading, reload } = useAsyncData(adminListAutomations, []); const log = useAuditStore((s) => s.log); - const backend = !isNotImplemented(error); - const rows = backend && data && data.length > 0 ? data : sample; + // 端点未实现 / 请求失败必须分开:前者是 404/501,后者是断连或 5xx。 + const unimplemented = isNotImplemented(error); + // 只渲染接口真的返回过的数据;没有数据就是空列表,绝不拿样例顶替。 + const rows = data ?? []; const [name, setName] = useState(""); const [kind, setKind] = useState("cron"); @@ -93,9 +174,6 @@ export function AutomationsModule(): React.ReactNode { /> {msg ? {msg} : null} - {backend && !error ? 后端已返回自动化配置(数据以接口为准)。 : null} - {error && !backend ? 后端未提供 /api/v1/admin/automations,以下为示例壳(M10 提供真实数据)。 : null} - {error && backend ? {error} : null}

提醒 / CRON / 队列事件

@@ -117,22 +195,15 @@ export function AutomationsModule(): React.ReactNode { - {rows.map((r) => ( -
- {r.name} - {r.kind} - {r.cron || "—"} - - {}} label="启用" /> - - - -
- ))} +
diff --git a/frontend/web/src/admin/modules/Llm.tsx b/frontend/web/src/admin/modules/Llm.tsx index 8d90218..5a2a6be 100644 --- a/frontend/web/src/admin/modules/Llm.tsx +++ b/frontend/web/src/admin/modules/Llm.tsx @@ -1,20 +1,14 @@ import { useState } from "react"; import { useAsyncData } from "../../lib/useAsyncData"; import { adminGetLlm, adminTestLlm, isNotImplemented } from "../../lib/api"; -import type { AdminLlmProvider, AdminLlmConfig } from "../../lib/types"; +import type { AdminLlmProvider } from "../../lib/types"; import { useAuditStore } from "../audit"; import { Notice, PageHeader } from "../components"; -import { Button, Surface, Tag } from "../../components/primitives"; +import { Button, EmptyState, Surface, Tag } from "../../components/primitives"; +import { Icon } from "../../components/Icon"; import { LlmProviderForm } from "./llm/LlmProviderForm"; -const sample: AdminLlmConfig = { - providers: [ - { id: "p1", name: "主模型", provider: "openai", baseUrl: "https://api.example.com/v1", model: "example-ultra", multimodal: true, enabled: true, maxTokens: 8192 }, - { id: "p2", name: "备选模型", provider: "openai", baseUrl: "https://api.backup.example.com/v1", model: "example-fast", multimodal: false, enabled: true }, - ], -}; - -/** One provider group row: identity + current token budget + test/edit actions. */ +/** 一个供应商分组行:身份 + 当前 token 预算 + 测试/编辑动作。 */ function ProviderRow({ p, busy, @@ -48,11 +42,71 @@ function ProviderRow({ ); } +interface ProviderListProps { + providers: AdminLlmProvider[]; + loading: boolean; + error: string | null; + /** 后端明确表示未提供该端点(404/501)—— 与「请求失败」「真为空」是三种状态。 */ + unimplemented: boolean; + busy: boolean; + onTest: (p: AdminLlmProvider) => void; + onEdit: (p: AdminLlmProvider) => void; +} + +/** 供应商列表:把「加载中 / 端点未实现 / 请求失败 / 真为空 / 有数据」分开渲染。 + * 每种状态都只反映接口的真实结果,绝不回退到示例供应商。 */ +function ProviderList({ + providers, + loading, + error, + unimplemented, + busy, + onTest, + onEdit, +}: ProviderListProps): React.ReactNode { + if (loading) { + return

加载中…

; + } + if (providers.length === 0) { + if (unimplemented) { + return ( + + 后端未提供 GET /api/v1/admin/llm(404/501):配置读不到。本页只渲染接口返回的真实数据,不显示任何示例供应商。 + + ); + } + if (error) { + return ( + + 加载 LLM 配置失败(后端不可达或接口报错):{error} —— 这是错误态,不是空配置。 + + ); + } + return ( + } + title="还没有配置 LLM 供应商" + hint="用下方表单新增第一个供应商 / 模型分组。" + /> + ); + } + return ( +
+ {error ? 刷新失败,下方仍是上次成功读取的配置:{error} : null} + {providers.map((r) => ( + + ))} +
+ ); +} + export function LlmModule(): React.ReactNode { - const { data, error, reload } = useAsyncData(adminGetLlm, []); + const { data, error, loading, reload } = useAsyncData(adminGetLlm, []); const log = useAuditStore((s) => s.log); - const backend = !isNotImplemented(error); - const providers = backend && data ? data.providers : sample.providers; + // 端点未实现 / 请求失败必须分开:前者是 404/501,后者是断连或 5xx。 + const unimplemented = isNotImplemented(error); + // 只渲染接口真的返回过的供应商;没有就是空列表,绝不拿示例配置顶替。 + const providers = data?.providers ?? []; const [editing, setEditing] = useState(null); const [busy, setBusy] = useState(false); @@ -83,17 +137,20 @@ export function LlmModule(): React.ReactNode { subtitle="供应商 + 模型配置(base/key/多模态检测/token 预算) + 测试(真实调用) + fallback + 按角色路由 + 用量" /> - {backend && !error ? 后端已返回 LLM 配置(数据以接口为准)。 : null} - {error && !backend ? 后端未提供 /api/v1/admin/llm,以下为示例壳(M10 提供真实数据)。 : null} - {error && backend ? {error} : null} {fail ? {fail} : null} {result ? {result} : null}

供应商 / 模型

- {providers.map((r) => ( - - ))} +