From 141f5f886f1a630b5602a933e2e8ef22d2e57c49 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 22 Apr 2026 11:36:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BE=9B=E5=BA=94?= =?UTF-8?q?=E5=95=86=E7=AE=A1=E7=90=86=E5=BC=B9=E7=AA=97=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 导入 TDesign react-19-adapter 修复 MessagePlugin 在 React 19 下的渲染错误 - Dialog 禁用蒙版点击和 ESC 键关闭,防止误操作丢失表单数据 - 重构弹窗关闭逻辑,使用 mutateAsync 替代 useEffect 监听 isSuccess - 成功后自动关闭弹窗,失败后保持弹窗打开并显示错误提示 --- frontend/src/main.tsx | 1 + frontend/src/pages/Providers/ModelForm.tsx | 4 +- frontend/src/pages/Providers/ProviderForm.tsx | 4 +- frontend/src/pages/Providers/index.tsx | 62 +++++++++---------- openspec/specs/frontend/spec.md | 42 ++++++++++--- 5 files changed, 72 insertions(+), 41 deletions(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 806f900..dc9aed9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import 'tdesign-react/es/style/index.css' +import 'tdesign-react/es/_util/react-19-adapter' import './index.scss' import App from './App' diff --git a/frontend/src/pages/Providers/ModelForm.tsx b/frontend/src/pages/Providers/ModelForm.tsx index 043fd1e..c737f34 100644 --- a/frontend/src/pages/Providers/ModelForm.tsx +++ b/frontend/src/pages/Providers/ModelForm.tsx @@ -14,7 +14,7 @@ interface ModelFormProps { model?: Model; providerId: string; providers: Provider[]; - onSave: (values: ModelFormValues) => void; + onSave: (values: ModelFormValues) => Promise | void; onCancel: () => void; loading: boolean; } @@ -63,6 +63,8 @@ export function ModelForm({ { form?.submit(); return false; }} onClose={onCancel} confirmLoading={loading} diff --git a/frontend/src/pages/Providers/ProviderForm.tsx b/frontend/src/pages/Providers/ProviderForm.tsx index f3f2f91..f434739 100644 --- a/frontend/src/pages/Providers/ProviderForm.tsx +++ b/frontend/src/pages/Providers/ProviderForm.tsx @@ -15,7 +15,7 @@ interface ProviderFormValues { interface ProviderFormProps { open: boolean; provider?: Provider; - onSave: (values: ProviderFormValues) => void; + onSave: (values: ProviderFormValues) => Promise | void; onCancel: () => void; loading: boolean; } @@ -59,6 +59,8 @@ export function ProviderForm({ { form?.submit(); return false; }} onClose={onCancel} confirmLoading={loading} diff --git a/frontend/src/pages/Providers/index.tsx b/frontend/src/pages/Providers/index.tsx index fd73dd6..ca88f94 100644 --- a/frontend/src/pages/Providers/index.tsx +++ b/frontend/src/pages/Providers/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types'; import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders'; import { useCreateModel, useUpdateModel } from '@/hooks/useModels'; @@ -20,18 +20,6 @@ export function ProvidersPage() { const [editingModel, setEditingModel] = useState(); const [modelFormProviderId, setModelFormProviderId] = useState(''); - useEffect(() => { - if ((createProvider.isSuccess || updateProvider.isSuccess) && providerFormOpen) { - setProviderFormOpen(false); - } - }, [createProvider.isSuccess, updateProvider.isSuccess, providerFormOpen]); - - useEffect(() => { - if ((createModel.isSuccess || updateModel.isSuccess) && modelFormOpen) { - setModelFormOpen(false); - } - }, [createModel.isSuccess, updateModel.isSuccess, modelFormOpen]); - return (
{ - if (editingProvider) { - const input: Partial = {}; - if (values.name !== editingProvider.name) input.name = values.name; - if (values.apiKey) input.apiKey = values.apiKey; - if (values.baseUrl !== editingProvider.baseUrl) input.baseUrl = values.baseUrl; - if (values.enabled !== editingProvider.enabled) input.enabled = values.enabled; - updateProvider.mutate({ id: editingProvider.id, input }); - } else { - createProvider.mutate(values); + onSave={async (values) => { + try { + if (editingProvider) { + const input: Partial = {}; + if (values.name !== editingProvider.name) input.name = values.name; + if (values.apiKey) input.apiKey = values.apiKey; + if (values.baseUrl !== editingProvider.baseUrl) input.baseUrl = values.baseUrl; + if (values.enabled !== editingProvider.enabled) input.enabled = values.enabled; + await updateProvider.mutateAsync({ id: editingProvider.id, input }); + } else { + await createProvider.mutateAsync(values); + } + setProviderFormOpen(false); + } catch { + // 错误已由 hooks 的 onError 处理 } }} onCancel={() => setProviderFormOpen(false)} @@ -83,15 +76,20 @@ export function ProvidersPage() { providerId={modelFormProviderId} providers={providers} loading={createModel.isPending || updateModel.isPending} - onSave={(values) => { - if (editingModel) { - const input: Partial = {}; - if (values.providerId !== editingModel.providerId) input.providerId = values.providerId; - if (values.modelName !== editingModel.modelName) input.modelName = values.modelName; - if (values.enabled !== editingModel.enabled) input.enabled = values.enabled; - updateModel.mutate({ id: editingModel.id, input }); - } else { - createModel.mutate(values); + onSave={async (values) => { + try { + if (editingModel) { + const input: Partial = {}; + if (values.providerId !== editingModel.providerId) input.providerId = values.providerId; + if (values.modelName !== editingModel.modelName) input.modelName = values.modelName; + if (values.enabled !== editingModel.enabled) input.enabled = values.enabled; + await updateModel.mutateAsync({ id: editingModel.id, input }); + } else { + await createModel.mutateAsync(values); + } + setModelFormOpen(false); + } catch { + // 错误已由 hooks 的 onError 处理 } }} onCancel={() => setModelFormOpen(false)} diff --git a/openspec/specs/frontend/spec.md b/openspec/specs/frontend/spec.md index 9eb8423..d05dd50 100644 --- a/openspec/specs/frontend/spec.md +++ b/openspec/specs/frontend/spec.md @@ -37,18 +37,23 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **WHEN** 用户点击"添加供应商"按钮 - **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示输入表单 - **THEN** 表单 SHALL 包含 id、name、api_key、base_url 字段,带校验规则 +- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false}) +- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false}) - **WHEN** 用户提交包含有效数据的表单 -- **THEN** 前端 SHALL 通过 useMutation 调用创建 API +- **THEN** 前端 SHALL 通过 mutateAsync 调用创建 API - **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表 -- **THEN** 失败 SHALL 显示错误提示 +- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error) #### Scenario: 编辑现有供应商 - **WHEN** 用户点击供应商的"编辑"按钮 - **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示预填充数据的表单 +- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false}) +- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false}) - **WHEN** 用户提交包含更新数据的表单 -- **THEN** 前端 SHALL 通过 useMutation 调用更新 API +- **THEN** 前端 SHALL 通过 mutateAsync 调用更新 API - **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表 +- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error) #### Scenario: 删除供应商 @@ -81,9 +86,12 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** provider_id SHALL 自动关联当前供应商 - **THEN** 供应商选择 SHALL 使用 `options` 属性 - **THEN** 创建表单 SHALL NOT 包含 ID 输入框(后端自动生成 UUID) +- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false}) +- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false}) - **WHEN** 用户提交表单 -- **THEN** 前端 SHALL 通过 useMutation 调用创建 API -- **THEN** 成功后 SHALL 刷新模型列表 +- **THEN** 前端 SHALL 通过 mutateAsync 调用创建 API +- **THEN** 成功后 SHALL 关闭 Dialog 并刷新模型列表 +- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error) #### Scenario: 编辑模型 @@ -91,9 +99,12 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** 前端 SHALL 显示编辑表单 - **THEN** 编辑表单 SHALL 显示统一模型 ID(只读) - **THEN** ID 字段 SHALL 为禁用状态 +- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false}) +- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false}) - **WHEN** 用户提交表单 -- **THEN** 前端 SHALL 通过 useMutation 调用更新 API -- **THEN** 成功后 SHALL 刷新模型列表 +- **THEN** 前端 SHALL 通过 mutateAsync 调用更新 API +- **THEN** 成功后 SHALL 关闭 Dialog 并刷新模型列表 +- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error) #### Scenario: 删除模型 @@ -333,6 +344,23 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **WHEN** 用户使用浏览器后退按钮 - **THEN** 前端 SHALL 正确导航到上一个页面 +### Requirement: React 19 适配器 + +前端 SHALL 导入 TDesign react-19-adapter 以支持 React 19。 + +#### Scenario: 导入适配器 + +- **WHEN** 应用启动 +- **THEN** main.tsx SHALL 导入 'tdesign-react/es/_util/react-19-adapter' +- **THEN** MessagePlugin、DialogPlugin 等插件式调用 SHALL 正常工作 + +#### Scenario: 错误提示显示 + +- **WHEN** API 请求失败 +- **THEN** MessagePlugin.error SHALL 正确渲染错误提示 +- **THEN** 错误提示 SHALL 显示在页面顶部(placement: top) +- **THEN** 错误提示 SHALL 在 3 秒后自动消失 + ### Requirement: 使用 React 和 TypeScript 前端 SHALL 使用 React 和 TypeScript 实现,遵循 strict 模式。