fix: 修复供应商管理弹窗交互问题
- 导入 TDesign react-19-adapter 修复 MessagePlugin 在 React 19 下的渲染错误 - Dialog 禁用蒙版点击和 ESC 键关闭,防止误操作丢失表单数据 - 重构弹窗关闭逻辑,使用 mutateAsync 替代 useEffect 监听 isSuccess - 成功后自动关闭弹窗,失败后保持弹窗打开并显示错误提示
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import 'tdesign-react/es/style/index.css'
|
import 'tdesign-react/es/style/index.css'
|
||||||
|
import 'tdesign-react/es/_util/react-19-adapter'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface ModelFormProps {
|
|||||||
model?: Model;
|
model?: Model;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
providers: Provider[];
|
providers: Provider[];
|
||||||
onSave: (values: ModelFormValues) => void;
|
onSave: (values: ModelFormValues) => Promise<void> | void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,8 @@ export function ModelForm({
|
|||||||
<Dialog
|
<Dialog
|
||||||
header={isEdit ? '编辑模型' : '添加模型'}
|
header={isEdit ? '编辑模型' : '添加模型'}
|
||||||
visible={open}
|
visible={open}
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
closeOnEscKeydown={false}
|
||||||
onConfirm={() => { form?.submit(); return false; }}
|
onConfirm={() => { form?.submit(); return false; }}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface ProviderFormValues {
|
|||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
provider?: Provider;
|
provider?: Provider;
|
||||||
onSave: (values: ProviderFormValues) => void;
|
onSave: (values: ProviderFormValues) => Promise<void> | void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
@@ -59,6 +59,8 @@ export function ProviderForm({
|
|||||||
<Dialog
|
<Dialog
|
||||||
header={isEdit ? '编辑供应商' : '添加供应商'}
|
header={isEdit ? '编辑供应商' : '添加供应商'}
|
||||||
visible={open}
|
visible={open}
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
closeOnEscKeydown={false}
|
||||||
onConfirm={() => { form?.submit(); return false; }}
|
onConfirm={() => { form?.submit(); return false; }}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
confirmLoading={loading}
|
confirmLoading={loading}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types';
|
import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types';
|
||||||
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
|
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
|
||||||
import { useCreateModel, useUpdateModel } from '@/hooks/useModels';
|
import { useCreateModel, useUpdateModel } from '@/hooks/useModels';
|
||||||
@@ -20,18 +20,6 @@ export function ProvidersPage() {
|
|||||||
const [editingModel, setEditingModel] = useState<Model | undefined>();
|
const [editingModel, setEditingModel] = useState<Model | undefined>();
|
||||||
const [modelFormProviderId, setModelFormProviderId] = 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProviderTable
|
<ProviderTable
|
||||||
@@ -62,16 +50,21 @@ export function ProvidersPage() {
|
|||||||
open={providerFormOpen}
|
open={providerFormOpen}
|
||||||
provider={editingProvider}
|
provider={editingProvider}
|
||||||
loading={createProvider.isPending || updateProvider.isPending}
|
loading={createProvider.isPending || updateProvider.isPending}
|
||||||
onSave={(values) => {
|
onSave={async (values) => {
|
||||||
|
try {
|
||||||
if (editingProvider) {
|
if (editingProvider) {
|
||||||
const input: Partial<UpdateProviderInput> = {};
|
const input: Partial<UpdateProviderInput> = {};
|
||||||
if (values.name !== editingProvider.name) input.name = values.name;
|
if (values.name !== editingProvider.name) input.name = values.name;
|
||||||
if (values.apiKey) input.apiKey = values.apiKey;
|
if (values.apiKey) input.apiKey = values.apiKey;
|
||||||
if (values.baseUrl !== editingProvider.baseUrl) input.baseUrl = values.baseUrl;
|
if (values.baseUrl !== editingProvider.baseUrl) input.baseUrl = values.baseUrl;
|
||||||
if (values.enabled !== editingProvider.enabled) input.enabled = values.enabled;
|
if (values.enabled !== editingProvider.enabled) input.enabled = values.enabled;
|
||||||
updateProvider.mutate({ id: editingProvider.id, input });
|
await updateProvider.mutateAsync({ id: editingProvider.id, input });
|
||||||
} else {
|
} else {
|
||||||
createProvider.mutate(values);
|
await createProvider.mutateAsync(values);
|
||||||
|
}
|
||||||
|
setProviderFormOpen(false);
|
||||||
|
} catch {
|
||||||
|
// 错误已由 hooks 的 onError 处理
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCancel={() => setProviderFormOpen(false)}
|
onCancel={() => setProviderFormOpen(false)}
|
||||||
@@ -83,15 +76,20 @@ export function ProvidersPage() {
|
|||||||
providerId={modelFormProviderId}
|
providerId={modelFormProviderId}
|
||||||
providers={providers}
|
providers={providers}
|
||||||
loading={createModel.isPending || updateModel.isPending}
|
loading={createModel.isPending || updateModel.isPending}
|
||||||
onSave={(values) => {
|
onSave={async (values) => {
|
||||||
|
try {
|
||||||
if (editingModel) {
|
if (editingModel) {
|
||||||
const input: Partial<UpdateModelInput> = {};
|
const input: Partial<UpdateModelInput> = {};
|
||||||
if (values.providerId !== editingModel.providerId) input.providerId = values.providerId;
|
if (values.providerId !== editingModel.providerId) input.providerId = values.providerId;
|
||||||
if (values.modelName !== editingModel.modelName) input.modelName = values.modelName;
|
if (values.modelName !== editingModel.modelName) input.modelName = values.modelName;
|
||||||
if (values.enabled !== editingModel.enabled) input.enabled = values.enabled;
|
if (values.enabled !== editingModel.enabled) input.enabled = values.enabled;
|
||||||
updateModel.mutate({ id: editingModel.id, input });
|
await updateModel.mutateAsync({ id: editingModel.id, input });
|
||||||
} else {
|
} else {
|
||||||
createModel.mutate(values);
|
await createModel.mutateAsync(values);
|
||||||
|
}
|
||||||
|
setModelFormOpen(false);
|
||||||
|
} catch {
|
||||||
|
// 错误已由 hooks 的 onError 处理
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCancel={() => setModelFormOpen(false)}
|
onCancel={() => setModelFormOpen(false)}
|
||||||
|
|||||||
@@ -37,18 +37,23 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **WHEN** 用户点击"添加供应商"按钮
|
- **WHEN** 用户点击"添加供应商"按钮
|
||||||
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示输入表单
|
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示输入表单
|
||||||
- **THEN** 表单 SHALL 包含 id、name、api_key、base_url 字段,带校验规则
|
- **THEN** 表单 SHALL 包含 id、name、api_key、base_url 字段,带校验规则
|
||||||
|
- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false})
|
||||||
|
- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false})
|
||||||
- **WHEN** 用户提交包含有效数据的表单
|
- **WHEN** 用户提交包含有效数据的表单
|
||||||
- **THEN** 前端 SHALL 通过 useMutation 调用创建 API
|
- **THEN** 前端 SHALL 通过 mutateAsync 调用创建 API
|
||||||
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表
|
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表
|
||||||
- **THEN** 失败 SHALL 显示错误提示
|
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error)
|
||||||
|
|
||||||
#### Scenario: 编辑现有供应商
|
#### Scenario: 编辑现有供应商
|
||||||
|
|
||||||
- **WHEN** 用户点击供应商的"编辑"按钮
|
- **WHEN** 用户点击供应商的"编辑"按钮
|
||||||
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示预填充数据的表单
|
- **THEN** 前端 SHALL 使用 TDesign Dialog + Form 显示预填充数据的表单
|
||||||
|
- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false})
|
||||||
|
- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false})
|
||||||
- **WHEN** 用户提交包含更新数据的表单
|
- **WHEN** 用户提交包含更新数据的表单
|
||||||
- **THEN** 前端 SHALL 通过 useMutation 调用更新 API
|
- **THEN** 前端 SHALL 通过 mutateAsync 调用更新 API
|
||||||
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表
|
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新供应商列表
|
||||||
|
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error)
|
||||||
|
|
||||||
#### Scenario: 删除供应商
|
#### Scenario: 删除供应商
|
||||||
|
|
||||||
@@ -81,9 +86,12 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **THEN** provider_id SHALL 自动关联当前供应商
|
- **THEN** provider_id SHALL 自动关联当前供应商
|
||||||
- **THEN** 供应商选择 SHALL 使用 `options` 属性
|
- **THEN** 供应商选择 SHALL 使用 `options` 属性
|
||||||
- **THEN** 创建表单 SHALL NOT 包含 ID 输入框(后端自动生成 UUID)
|
- **THEN** 创建表单 SHALL NOT 包含 ID 输入框(后端自动生成 UUID)
|
||||||
|
- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false})
|
||||||
|
- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false})
|
||||||
- **WHEN** 用户提交表单
|
- **WHEN** 用户提交表单
|
||||||
- **THEN** 前端 SHALL 通过 useMutation 调用创建 API
|
- **THEN** 前端 SHALL 通过 mutateAsync 调用创建 API
|
||||||
- **THEN** 成功后 SHALL 刷新模型列表
|
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新模型列表
|
||||||
|
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error)
|
||||||
|
|
||||||
#### Scenario: 编辑模型
|
#### Scenario: 编辑模型
|
||||||
|
|
||||||
@@ -91,9 +99,12 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **THEN** 前端 SHALL 显示编辑表单
|
- **THEN** 前端 SHALL 显示编辑表单
|
||||||
- **THEN** 编辑表单 SHALL 显示统一模型 ID(只读)
|
- **THEN** 编辑表单 SHALL 显示统一模型 ID(只读)
|
||||||
- **THEN** ID 字段 SHALL 为禁用状态
|
- **THEN** ID 字段 SHALL 为禁用状态
|
||||||
|
- **THEN** Dialog SHALL 禁用蒙版点击关闭(closeOnOverlayClick={false})
|
||||||
|
- **THEN** Dialog SHALL 禁用 ESC 键关闭(closeOnEscKeydown={false})
|
||||||
- **WHEN** 用户提交表单
|
- **WHEN** 用户提交表单
|
||||||
- **THEN** 前端 SHALL 通过 useMutation 调用更新 API
|
- **THEN** 前端 SHALL 通过 mutateAsync 调用更新 API
|
||||||
- **THEN** 成功后 SHALL 刷新模型列表
|
- **THEN** 成功后 SHALL 关闭 Dialog 并刷新模型列表
|
||||||
|
- **THEN** 失败 SHALL 保持 Dialog 打开并显示错误提示(MessagePlugin.error)
|
||||||
|
|
||||||
#### Scenario: 删除模型
|
#### Scenario: 删除模型
|
||||||
|
|
||||||
@@ -333,6 +344,23 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
- **WHEN** 用户使用浏览器后退按钮
|
- **WHEN** 用户使用浏览器后退按钮
|
||||||
- **THEN** 前端 SHALL 正确导航到上一个页面
|
- **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
|
### Requirement: 使用 React 和 TypeScript
|
||||||
|
|
||||||
前端 SHALL 使用 React 和 TypeScript 实现,遵循 strict 模式。
|
前端 SHALL 使用 React 和 TypeScript 实现,遵循 strict 模式。
|
||||||
|
|||||||
Reference in New Issue
Block a user