From 29838b030fcfcf92cda3f262e62b63b2d12b0840 Mon Sep 17 00:00:00 2001
From: 2910410219 <2910410219@qq.com>
Date: Mon, 11 May 2026 20:01:03 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AF=9D=E6=A8=A1?=
=?UTF-8?q?=E5=9E=8B=E5=92=8CAPI=20Key=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在模型模块中新增会话开关状态字段,支持会话模型的管理。
- 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。
- 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。
- 更新相关样式以增强界面可读性和美观性。
---
docs/API-ERROR-HANDLING.md | 716 ++++++++++++++++++
.../modelConfig/modelModule/index.ts | 7 +
src/components/model/ModelSelector.vue | 165 +++-
src/components/skill/NodeSkillSelector.vue | 4 +-
src/components/skill/SkillSelector.vue | 2 +-
src/utils/request.ts | 79 +-
.../account/component/editAccount.vue | 2 +-
.../script/component/editRole.vue | 2 +-
src/views/digitalHuman/creation/index.vue | 66 +-
.../modelModule/component/editModule.vue | 335 +++++---
.../modelConfig/modelModule/index.vue | 45 +-
src/views/digitalHuman/skill/index.vue | 29 +-
src/views/knowledge/index.vue | 28 +-
.../setting/anchor/component/editAnchor.vue | 4 +-
.../trade/operation/setting/anchor/index.vue | 4 +-
.../component/editLiveAccount.vue | 16 +-
.../operation/setting/live-account/index.vue | 26 +-
.../scheduling/component/editSchedule.vue | 16 +-
.../operation/setting/scheduling/index.vue | 24 +-
19 files changed, 1296 insertions(+), 274 deletions(-)
create mode 100644 docs/API-ERROR-HANDLING.md
diff --git a/docs/API-ERROR-HANDLING.md b/docs/API-ERROR-HANDLING.md
new file mode 100644
index 0000000..ce75e46
--- /dev/null
+++ b/docs/API-ERROR-HANDLING.md
@@ -0,0 +1,716 @@
+# API 错误处理规范
+
+> 本文档定义了项目中 API 请求的错误处理标准,确保错误提示的一致性和用户体验。
+
+## 📋 目录
+
+- [核心原则](#核心原则)
+- [全局拦截器机制](#全局拦截器机制)
+- [API 层规范](#api-层规范)
+- [页面层规范](#页面层规范)
+- [完整示例](#完整示例)
+- [常见问题](#常见问题)
+
+---
+
+## 核心原则
+
+### ✅ 避免重复提示
+- **全局拦截器**已经处理了大部分错误提示
+- **页面层**不应再显示固定的错误提示
+- 只在需要自定义错误处理时使用 `errorMode: 'page'`
+
+### ✅ 优先使用后端 message
+- 全局拦截器会自动提取后端返回的 `message` 字段
+- 页面层使用 `getApiErrorMessage` 工具函数提取错误信息
+- 避免写死前端错误文案
+
+### ✅ 业务逻辑与错误提示分离
+- `catch` 块只处理必要的业务逻辑(数据清空、状态重置等)
+- 错误提示交给全局拦截器或使用 `getApiErrorMessage`
+
+---
+
+## 全局拦截器机制
+
+### 位置
+`src/utils/request.ts`
+
+### 错误处理流程
+
+```typescript
+// 响应拦截器
+service.interceptors.response.use(
+ (response) => {
+ const res = response.data;
+ const code = res.code;
+
+ // 业务成功
+ if (code === 200 || code === 0) {
+ return res;
+ }
+
+ // 业务失败
+ const errorMode = response.config.requestOptions?.errorMode || 'global';
+
+ if (errorMode === 'global') {
+ // 全局模式:自动显示后端 message
+ ElMessage.error(res.message || res.msg || '操作失败');
+ }
+
+ // 抛出错误供页面 catch
+ return Promise.reject(new Error(res.message || res.msg));
+ },
+ (error) => {
+ // 网络错误、超时等
+ ElMessage.error('网络请求失败,请稍后重试');
+ return Promise.reject(error);
+ }
+);
+```
+
+### 错误模式
+
+| 模式 | 说明 | 使用场景 |
+|------|------|----------|
+| `global`(默认) | 全局拦截器自动显示错误 | 大部分接口(推荐) |
+| `page` | 页面自己处理错误 | 需要自定义错误处理时 |
+
+---
+
+## API 层规范
+
+### 1. 默认配置(推荐 95% 的场景)
+
+```typescript
+// src/api/xxx/index.ts
+
+/**
+ * 获取列表
+ * 使用默认配置,全局拦截器自动处理错误
+ */
+export function getList(params: ListParams) {
+ return request({
+ url: '/api/xxx/list',
+ method: 'get',
+ params
+ });
+}
+
+/**
+ * 创建数据
+ * 使用默认配置
+ */
+export function createItem(data: CreateParams) {
+ return request({
+ url: '/api/xxx/create',
+ method: 'post',
+ data
+ });
+}
+
+/**
+ * 更新数据
+ * 使用默认配置
+ */
+export function updateItem(data: UpdateParams) {
+ return request({
+ url: '/api/xxx/update',
+ method: 'put',
+ data
+ });
+}
+
+/**
+ * 删除数据
+ * 使用默认配置
+ */
+export function deleteItem(id: string) {
+ return request({
+ url: `/api/xxx/delete/${id}`,
+ method: 'delete'
+ });
+}
+```
+
+### 2. 页面自定义错误处理(特殊场景)
+
+```typescript
+// src/api/xxx/index.ts
+
+/**
+ * 批量导入
+ * 需要页面自定义错误处理(显示详细的导入结果)
+ */
+export function batchImport(data: ImportParams) {
+ return request({
+ url: '/api/xxx/import',
+ method: 'post',
+ data,
+ requestOptions: {
+ errorMode: 'page' // 页面自己处理错误
+ }
+ });
+}
+
+/**
+ * 复杂表单提交
+ * 需要根据不同错误类型做不同处理
+ */
+export function submitComplexForm(data: FormData) {
+ return request({
+ url: '/api/xxx/submit',
+ method: 'post',
+ data,
+ requestOptions: {
+ errorMode: 'page'
+ }
+ });
+}
+```
+
+### 3. 何时使用 `errorMode: 'page'`?
+
+仅在以下情况使用:
+
+- ✅ 需要根据不同错误码做不同处理
+- ✅ 需要自定义错误提示格式
+- ✅ 需要在错误后执行特殊业务逻辑
+- ✅ 需要显示详细的错误信息(如批量操作结果)
+
+---
+
+## 页面层规范
+
+### 1. 默认场景 - 全局错误处理
+
+```typescript
+// ✅ 推荐写法
+const getList = async () => {
+ loading.value = true;
+ try {
+ const res = await listApi(params);
+ tableData.value = res.data?.list || [];
+ total.value = res.data?.total || 0;
+ } catch (error) {
+ // 错误已由全局拦截器处理
+ // 这里只处理必要的业务逻辑
+ tableData.value = [];
+ total.value = 0;
+ } finally {
+ loading.value = false;
+ }
+};
+
+// ✅ 简单场景可以不写 catch
+const deleteItem = async (id: string) => {
+ try {
+ await deleteApi(id);
+ ElMessage.success('删除成功');
+ getList(); // 刷新列表
+ } catch (error) {
+ // 错误已由全局拦截器处理
+ }
+};
+
+// ✅ 更简洁的写法(如果不需要处理错误)
+const deleteItem = async (id: string) => {
+ await deleteApi(id);
+ ElMessage.success('删除成功');
+ getList();
+};
+```
+
+### 2. 页面自定义错误处理
+
+```typescript
+import { getApiErrorMessage } from '/@/utils/request';
+
+// ⚠️ 仅在 API 设置了 errorMode: 'page' 时使用
+const saveData = async () => {
+ loading.value = true;
+ try {
+ await saveApi(formData);
+ ElMessage.success('保存成功');
+ closeDialog();
+ } catch (error) {
+ // 使用 getApiErrorMessage 提取后端错误信息
+ ElMessage.error(getApiErrorMessage(error, '保存失败'));
+ } finally {
+ loading.value = false;
+ }
+};
+
+// 根据不同错误做不同处理
+const deleteItem = async (id: string) => {
+ try {
+ await deleteApi(id);
+ ElMessage.success('删除成功');
+ getList();
+ } catch (error) {
+ const msg = getApiErrorMessage(error, '删除失败');
+
+ // 根据错误信息做不同处理
+ if (msg.includes('被引用')) {
+ ElMessage.warning('该数据已被其他数据引用,无法删除');
+ showRelatedData(id);
+ } else if (msg.includes('权限')) {
+ ElMessage.error('您没有删除权限');
+ } else {
+ ElMessage.error(msg);
+ }
+ }
+};
+
+// 批量操作显示详细结果
+const batchImport = async (file: File) => {
+ try {
+ const res = await importApi(file);
+ ElMessage.success(`导入成功 ${res.data.successCount} 条,失败 ${res.data.failCount} 条`);
+ if (res.data.failCount > 0) {
+ showFailDetails(res.data.failList);
+ }
+ } catch (error) {
+ ElMessage.error(getApiErrorMessage(error, '导入失败'));
+ }
+};
+```
+
+### 3. getApiErrorMessage 工具函数
+
+```typescript
+/**
+ * 从错误对象中提取错误信息
+ * @param error - 错误对象
+ * @param fallback - 默认错误信息
+ * @returns 错误信息字符串
+ */
+export function getApiErrorMessage(error: any, fallback: string = '操作失败'): string {
+ // 优先从 response.data 中获取
+ if (error?.response?.data?.message) {
+ return error.response.data.message;
+ }
+ if (error?.response?.data?.msg) {
+ return error.response.data.msg;
+ }
+
+ // 从 Error.message 中获取
+ if (error?.message && error.message !== 'Network Error') {
+ return error.message;
+ }
+
+ // 返回默认值
+ return fallback;
+}
+```
+
+**使用示例:**
+
+```typescript
+import { getApiErrorMessage } from '/@/utils/request';
+
+try {
+ await someApi();
+} catch (error) {
+ // 使用后端返回的 message,如果没有则显示 '操作失败'
+ ElMessage.error(getApiErrorMessage(error, '操作失败'));
+}
+```
+
+---
+
+## 完整示例
+
+### 示例 1:标准 CRUD 操作
+
+```typescript
+// ==================== API 层 ====================
+// src/api/user/index.ts
+
+export function getUserList(params: ListParams) {
+ return request({
+ url: '/api/user/list',
+ method: 'get',
+ params
+ });
+}
+
+export function createUser(data: UserForm) {
+ return request({
+ url: '/api/user/create',
+ method: 'post',
+ data
+ });
+}
+
+export function updateUser(data: UserForm) {
+ return request({
+ url: '/api/user/update',
+ method: 'put',
+ data
+ });
+}
+
+export function deleteUser(id: string) {
+ return request({
+ url: `/api/user/delete/${id}`,
+ method: 'delete'
+ });
+}
+
+// ==================== 页面层 ====================
+// src/views/user/index.vue
+
+import { getUserList, createUser, updateUser, deleteUser } from '/@/api/user';
+
+// 获取列表
+const getList = async () => {
+ loading.value = true;
+ try {
+ const res = await getUserList(queryParams);
+ tableData.value = res.data?.list || [];
+ total.value = res.data?.total || 0;
+ } catch (error) {
+ // 错误已由全局拦截器处理
+ tableData.value = [];
+ total.value = 0;
+ } finally {
+ loading.value = false;
+ }
+};
+
+// 新增/编辑
+const onSubmit = async () => {
+ try {
+ await formRef.value?.validate();
+
+ if (isEdit.value) {
+ await updateUser(formData);
+ ElMessage.success('修改成功');
+ } else {
+ await createUser(formData);
+ ElMessage.success('添加成功');
+ }
+
+ dialogVisible.value = false;
+ getList();
+ } catch (error) {
+ // 错误已由全局拦截器处理
+ }
+};
+
+// 删除
+const onDelete = async (row: User) => {
+ try {
+ await ElMessageBox.confirm(`确定要删除用户"${row.name}"吗?`, '提示', {
+ type: 'warning'
+ });
+
+ await deleteUser(row.id);
+ ElMessage.success('删除成功');
+ getList();
+ } catch (error) {
+ if (error === 'cancel') {
+ // 用户取消操作
+ return;
+ }
+ // 错误已由全局拦截器处理
+ }
+};
+```
+
+### 示例 2:需要自定义错误处理
+
+```typescript
+// ==================== API 层 ====================
+// src/api/model/index.ts
+
+export function listModel(params: ListParams) {
+ return request({
+ url: '/api/model/list',
+ method: 'get',
+ params,
+ requestOptions: { errorMode: 'page' } // 页面自己处理错误
+ });
+}
+
+export function createModel(data: ModelForm) {
+ return request({
+ url: '/api/model/create',
+ method: 'post',
+ data,
+ requestOptions: { errorMode: 'page' }
+ });
+}
+
+// ==================== 页面层 ====================
+// src/views/model/index.vue
+
+import { getApiErrorMessage } from '/@/utils/request';
+import { listModel, createModel } from '/@/api/model';
+
+// 获取列表
+const getList = async () => {
+ loading.value = true;
+ try {
+ const res = await listModel(queryParams);
+ tableData.value = res.data?.list || [];
+ total.value = res.data?.total || 0;
+ } catch (error) {
+ // 使用 getApiErrorMessage 提取后端错误
+ ElMessage.error(getApiErrorMessage(error, '获取列表失败'));
+ tableData.value = [];
+ total.value = 0;
+ } finally {
+ loading.value = false;
+ }
+};
+
+// 创建
+const onCreate = async () => {
+ try {
+ await createModel(formData);
+ ElMessage.success('创建成功');
+ dialogVisible.value = false;
+ getList();
+ } catch (error) {
+ // 使用 getApiErrorMessage 提取后端错误
+ ElMessage.error(getApiErrorMessage(error, '创建失败'));
+ }
+};
+```
+
+---
+
+## 常见问题
+
+### Q1: 什么时候使用 `errorMode: 'page'`?
+
+**A:** 仅在以下情况使用:
+- 需要根据不同错误码做不同处理
+- 需要自定义错误提示格式
+- 需要在错误后执行特殊业务逻辑
+- 需要显示详细的错误信息
+
+**大部分情况(95%)使用默认的全局错误处理即可。**
+
+---
+
+### Q2: 为什么不能在页面写固定的错误提示?
+
+**A:** 因为全局拦截器已经显示了错误,页面再显示会导致**重复提示**:
+
+```typescript
+// ❌ 错误写法 - 会重复提示
+try {
+ await getList();
+} catch (error) {
+ ElMessage.error('获取列表失败'); // 全局拦截器已经显示过了
+}
+
+// ✅ 正确写法
+try {
+ await getList();
+} catch (error) {
+ // 错误已由全局拦截器处理
+ tableData.value = [];
+}
+```
+
+---
+
+### Q3: 如何显示后端返回的错误信息?
+
+**A:** 有两种方式:
+
+1. **使用默认全局处理(推荐)**
+```typescript
+// API 层不设置 errorMode
+export function getList() {
+ return request({ url: '/api/list', method: 'get' });
+}
+
+// 页面层不写错误提示
+try {
+ await getList();
+} catch (error) {
+ // 全局拦截器会自动显示后端的 message
+}
+```
+
+2. **使用 getApiErrorMessage**
+```typescript
+// API 层设置 errorMode: 'page'
+export function getList() {
+ return request({
+ url: '/api/list',
+ method: 'get',
+ requestOptions: { errorMode: 'page' }
+ });
+}
+
+// 页面层使用 getApiErrorMessage
+import { getApiErrorMessage } from '/@/utils/request';
+
+try {
+ await getList();
+} catch (error) {
+ ElMessage.error(getApiErrorMessage(error, '获取列表失败'));
+}
+```
+
+---
+
+### Q4: catch 块应该写什么?
+
+**A:** 根据场景决定:
+
+```typescript
+// 场景 1:只需要清空数据
+try {
+ const res = await getList();
+ tableData.value = res.data?.list || [];
+} catch (error) {
+ // 错误已由全局拦截器处理
+ tableData.value = [];
+}
+
+// 场景 2:需要重置状态
+try {
+ await uploadFile(file);
+} catch (error) {
+ // 错误已由全局拦截器处理
+ resetUploadState();
+ fileList.value = [];
+}
+
+// 场景 3:不需要任何处理
+try {
+ await deleteItem(id);
+ ElMessage.success('删除成功');
+ getList();
+} catch (error) {
+ // 错误已由全局拦截器处理
+}
+
+// 场景 4:需要自定义错误处理(API 设置了 errorMode: 'page')
+try {
+ await saveData();
+ ElMessage.success('保存成功');
+} catch (error) {
+ ElMessage.error(getApiErrorMessage(error, '保存失败'));
+}
+```
+
+---
+
+### Q5: 如何处理用户取消操作?
+
+**A:** 使用 `if (error === 'cancel')` 判断:
+
+```typescript
+const onDelete = async (row: any) => {
+ try {
+ await ElMessageBox.confirm('确定要删除吗?', '提示', {
+ type: 'warning'
+ });
+
+ await deleteApi(row.id);
+ ElMessage.success('删除成功');
+ getList();
+ } catch (error) {
+ if (error === 'cancel') {
+ // 用户取消操作,不显示错误
+ return;
+ }
+ // 其他错误已由全局拦截器处理
+ }
+};
+```
+
+---
+
+### Q6: 如何处理表单验证失败?
+
+**A:** 表单验证失败不会进入 catch,无需特殊处理:
+
+```typescript
+const onSubmit = async () => {
+ try {
+ // 表单验证失败会直接 return,不会进入 catch
+ await formRef.value?.validate();
+
+ await saveApi(formData);
+ ElMessage.success('保存成功');
+ } catch (error) {
+ // 这里只会捕获 API 请求错误
+ // 错误已由全局拦截器处理
+ }
+};
+```
+
+---
+
+## 快速检查清单
+
+写新接口时,检查以下几点:
+
+- [ ] **API 层**:是否需要设置 `errorMode: 'page'`?
+ - 大部分情况不需要
+ - 只在需要自定义错误处理时设置
+
+- [ ] **页面层**:catch 块是否正确?
+ - ✅ 只处理业务逻辑(数据清空、状态重置)
+ - ❌ 不写固定的 `ElMessage.error('xxx失败')`
+ - ✅ 如果 API 设置了 `errorMode: 'page'`,使用 `getApiErrorMessage`
+
+- [ ] **是否避免了重复提示?**
+ - ✅ 全局拦截器 OR 页面 getApiErrorMessage
+ - ❌ 全局拦截器 + 页面固定提示
+
+---
+
+## 工具函数导入
+
+```typescript
+// 导入错误提取工具
+import { getApiErrorMessage } from '/@/utils/request';
+
+// 使用示例
+try {
+ await someApi();
+} catch (error) {
+ ElMessage.error(getApiErrorMessage(error, '操作失败'));
+}
+```
+
+---
+
+## 总结
+
+### 核心规则
+
+1. **默认使用全局错误处理**(95% 的场景)
+ - API 层不设置 `errorMode`
+ - 页面层 catch 不写固定错误提示
+
+2. **特殊场景使用页面自定义处理**(5% 的场景)
+ - API 层设置 `errorMode: 'page'`
+ - 页面层使用 `getApiErrorMessage`
+
+3. **避免重复提示**
+ - 全局拦截器已经处理了错误
+ - 页面不要再显示固定错误
+
+### 记住这个公式
+
+```
+全局错误处理(默认) = 不设置 errorMode + catch 不写错误提示
+页面自定义处理(特殊) = errorMode: 'page' + getApiErrorMessage
+```
+
+---
+
+**文档版本:** v1.0
+**最后更新:** 2026-05-11
+**维护者:** 开发团队
diff --git a/src/api/digitalHuman/modelConfig/modelModule/index.ts b/src/api/digitalHuman/modelConfig/modelModule/index.ts
index 6a431fb..2113952 100644
--- a/src/api/digitalHuman/modelConfig/modelModule/index.ts
+++ b/src/api/digitalHuman/modelConfig/modelModule/index.ts
@@ -79,6 +79,8 @@ export interface ModelModuleItem {
apiKey?: string;
isPrivate?: number;
isChatModel?: number;
+ /** 会话开关状态(列表接口返回,0 关 1 开;会话开关接口就绪后生效) */
+ chatSessionEnabled?: number;
enabled: number;
maxConcurrency: number;
queueLimit: number;
@@ -210,3 +212,8 @@ export function getModelModuleDetail(id: number | string) {
params: { id },
});
}
+
+// TODO: 列表「会话开关」提交接口确定后在此封装,例如:
+// export function updateModelChatSessionSwitch(data: { id: number | string; chatSessionEnabled: 0 | 1 }) {
+// return request({ url: '/model-gateway/model/...', method: 'post', data });
+// }
diff --git a/src/components/model/ModelSelector.vue b/src/components/model/ModelSelector.vue
index 032b779..cfee934 100644
--- a/src/components/model/ModelSelector.vue
+++ b/src/components/model/ModelSelector.vue
@@ -19,12 +19,15 @@
v-for="model in modelList"
:key="model.id"
class="model-card"
- :class="{ selected: selectedModel?.id === model.id }"
+ :class="{ selected: selectedModel?.id === model.id, 'system-model': model.tenantId === 1 }"
@click="handleSelectModel(model)"
>
{{ model.modelName }}
@@ -58,23 +61,65 @@
+
+
+
+
+
+
+ 您选择的是系统模型,需要配置您自己的 API Key。
+ 系统将为您创建一个模型副本。
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
@@ -228,6 +360,15 @@ const handleClose = () => {
background: #f0f9ff;
}
+.model-card.system-model {
+ border-color: #fbbf24;
+ background: #fffbeb;
+}
+
+.model-card.system-model:hover {
+ border-color: #f59e0b;
+}
+
.model-card-header {
display: flex;
justify-content: space-between;
@@ -245,6 +386,12 @@ const handleClose = () => {
font-weight: 600;
}
+.model-badges {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
.check-icon {
font-size: 20px;
}
diff --git a/src/components/skill/NodeSkillSelector.vue b/src/components/skill/NodeSkillSelector.vue
index fd9a7b2..826f101 100644
--- a/src/components/skill/NodeSkillSelector.vue
+++ b/src/components/skill/NodeSkillSelector.vue
@@ -152,9 +152,7 @@ const fetchSkillList = async () => {
try {
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
const res =
- activeTab.value === 'system'
- ? await getSkillList(params, { errorMode: 'message' })
- : await getUserSkilllistUser(params, { errorMode: 'message' });
+ activeTab.value === 'system' ? await getSkillList(params) : await getUserSkilllistUser(params);
skillList.value = res.data?.list || [];
pagination.total = res.data?.total || 0;
} catch (error) {
diff --git a/src/components/skill/SkillSelector.vue b/src/components/skill/SkillSelector.vue
index a38e670..ecf4759 100644
--- a/src/components/skill/SkillSelector.vue
+++ b/src/components/skill/SkillSelector.vue
@@ -97,7 +97,7 @@ const fetchSkillList = async () => {
loading.value = true;
try {
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
- const res = await getUserSkilllistUser(params, { errorMode: 'message' });
+ const res = await getUserSkilllistUser(params);
skillList.value = res.data?.list || [];
pagination.total = res.data?.total || 0;
} catch (error) {
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 623b400..d4c1fcf 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -7,10 +7,10 @@ import { getChangedFields } from '/@/utils/diffUtils';
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
/**
- * 控制一次请求的错误提示归属:
- * - global: 交给 request.ts 统一弹错,适合绝大多数接口
- * - page: 页面自己在 catch 中决定提示文案,避免与全局重复
- * - silent: 完全静默,适合轮询、后台刷新等不希望打扰用户的请求
+ * 控制一次请求的错误提示归属(默认 global):
+ * - global: 由拦截器统一弹出后端返回的 message(含 HTTP 与业务 JSON)
+ * - page: 不自动弹窗,仅 reject;请在页面 catch 内自行处理(应与全局择一,避免重复)
+ * - silent: 完全静默(轮询等)
*/
export interface RequestOptions {
errorMode?: 'global' | 'page' | 'silent';
@@ -39,6 +39,26 @@ const ERROR_MESSAGE_INTERVAL = 2000;
const getErrorMode = (config?: InternalAxiosRequestConfig) => config?.requestOptions?.errorMode ?? 'global';
const shouldShowGlobalError = (config?: InternalAxiosRequestConfig) => getErrorMode(config) === 'global';
+/**
+ * 从接口响应体解析可读错误文案(JSON API、Spring 风格等)
+ */
+export function extractBackendMessage(data: unknown): string | undefined {
+ if (data == null) return undefined;
+ if (typeof data === 'string') {
+ const t = data.trim();
+ return t.length > 0 && t.length < 2000 ? t : undefined;
+ }
+ if (typeof data !== 'object') return undefined;
+ const o = data as Record
;
+ const pick = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
+ return (
+ pick(o.message) ||
+ pick(o.msg) ||
+ pick(o.error) ||
+ (typeof o.detail === 'string' ? pick(o.detail) : undefined)
+ );
+}
+
const closeActiveErrorMessage = () => {
activeErrorMessage?.close();
activeErrorMessage = null;
@@ -174,7 +194,7 @@ const responseInterceptor = (response: AxiosResponse) => {
const res = response.data;
const httpStatus = response.status;
const code = res?.code;
- const message = res?.message;
+ const message = extractBackendMessage(res);
const config = response.config;
if (isTokenExpiredError(httpStatus, code, message)) {
@@ -207,8 +227,8 @@ const responseInterceptor = (response: AxiosResponse) => {
if (knownErrorCodes.includes(code)) {
errorMsg = message || `请求失败(${code})`;
} else {
- // 未知的 code,统一提示后端异常
- errorMsg = '后端异常,请联系管理员';
+ // 未知的 code:优先使用后端 message,便于排查业务含义
+ errorMsg = message || '后端异常,请联系管理员';
}
showErrorMessage(errorMsg, config);
@@ -221,7 +241,8 @@ const responseInterceptor = (response: AxiosResponse) => {
const responseErrorHandler = (error: any) => {
const config = error.config as InternalAxiosRequestConfig | undefined;
const httpStatus = error.response?.status;
- const responseMessage = error.response?.data?.message;
+ const responseData = error.response?.data;
+ const responseMessage = extractBackendMessage(responseData);
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
showErrorMessage('请求超时,请检查网络连接', config);
@@ -232,7 +253,7 @@ const responseErrorHandler = (error: any) => {
return Promise.reject(error);
}
- if (isTokenExpiredError(httpStatus, error.response?.data?.code, responseMessage)) {
+ if (isTokenExpiredError(httpStatus, error.response?.data?.code as number | undefined, responseMessage)) {
handleTokenExpired();
return Promise.reject(new Error('登录状态已过期'));
}
@@ -245,7 +266,7 @@ const responseErrorHandler = (error: any) => {
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
const now = Date.now();
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
- showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面', config);
+ showErrorMessage(responseMessage ?? '服务开通中,请稍后刷新页面', config);
return Promise.reject(new Error('模块开通中'));
}
@@ -253,30 +274,30 @@ const responseErrorHandler = (error: any) => {
handleModuleNotEnabled(currentPath);
return Promise.reject(new Error('模块未开通'));
}
- showErrorMessage(responseMessage || '服务未开通', config);
+ showErrorMessage(responseMessage ?? '服务未开通', config);
break;
case 403:
- showErrorMessage(responseMessage || '没有权限访问该资源', config);
+ showErrorMessage(responseMessage ?? '没有权限访问该资源', config);
break;
case 404:
- showErrorMessage(responseMessage || '请求的资源不存在', config);
+ showErrorMessage(responseMessage ?? '请求的资源不存在', config);
break;
case 429:
// 429 是限流,不等于登录过期,这里只保留频率提示。
- showErrorMessage(responseMessage || '请求过于频繁,请稍后再试', config);
+ showErrorMessage(responseMessage ?? '请求过于频繁,请稍后再试', config);
break;
case 500:
- showErrorMessage(responseMessage || '服务器内部错误', config);
+ showErrorMessage(responseMessage ?? '服务器内部错误', config);
break;
case 502:
- showErrorMessage(responseMessage || '网关错误', config);
+ showErrorMessage(responseMessage ?? '网关错误', config);
break;
case 503:
- showErrorMessage(responseMessage || '服务不可用', config);
+ showErrorMessage(responseMessage ?? '服务不可用', config);
break;
default:
if (httpStatus >= 400) {
- showErrorMessage(responseMessage || `请求失败(${httpStatus})`, config);
+ showErrorMessage(responseMessage ?? `请求失败(${httpStatus})`, config);
}
}
@@ -286,5 +307,27 @@ const responseErrorHandler = (error: any) => {
service.interceptors.request.use(requestInterceptor, requestErrorHandler);
service.interceptors.response.use(responseInterceptor, responseErrorHandler);
+/**
+ * 从 axios / 业务 reject 中取出后端返回的提示文案(与全局拦截器同源逻辑;silent / 特殊场景下可在页面使用)。
+ */
+export function getApiErrorMessage(error: unknown, fallback = '操作失败'): string {
+ const e = error as any;
+ const fromBody = extractBackendMessage(e?.response?.data);
+ if (fromBody != null && fromBody !== '') {
+ return fromBody;
+ }
+ const msg = e?.message;
+ if (typeof msg === 'string' && msg.trim() !== '') {
+ if (/^Request failed with status code \d+$/i.test(msg)) {
+ return extractBackendMessage(e?.response?.data) ?? fallback;
+ }
+ if (msg === 'Network Error') {
+ return '网络异常,请检查网络连接';
+ }
+ return msg;
+ }
+ return fallback;
+}
+
export default service;
export { closeActiveErrorMessage, showErrorMessage };
diff --git a/src/views/customerService/account/component/editAccount.vue b/src/views/customerService/account/component/editAccount.vue
index c0a7f8a..5cbb952 100644
--- a/src/views/customerService/account/component/editAccount.vue
+++ b/src/views/customerService/account/component/editAccount.vue
@@ -138,7 +138,7 @@ const openDialog = async (row?: DialogFormData) => {
}
} catch (error) {
console.error('获取账号详情失败:', error);
- ElMessage.error('获取账号详情失败');
+ // 错误已由全局拦截器处理
} finally {
state.loading = false;
}
diff --git a/src/views/customerService/script/component/editRole.vue b/src/views/customerService/script/component/editRole.vue
index 92223ba..e3825a9 100644
--- a/src/views/customerService/script/component/editRole.vue
+++ b/src/views/customerService/script/component/editRole.vue
@@ -98,7 +98,7 @@ const loadDatasets = async () => {
}));
}
} catch (error) {
- ElMessage.error('加载数据集列表失败');
+ // 错误已由全局拦截器处理
}
};
diff --git a/src/views/digitalHuman/creation/index.vue b/src/views/digitalHuman/creation/index.vue
index 4969f29..bad11ce 100644
--- a/src/views/digitalHuman/creation/index.vue
+++ b/src/views/digitalHuman/creation/index.vue
@@ -790,10 +790,11 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
const getList = async () => {
treeLoading.value = true;
try {
- const res = await getExecutionList({ errorMode: 'page' });
+ const res = await getExecutionList();
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
treeNodes.value = buildTreeNodes(res.data?.tree || []);
} catch {
+ // 错误已由全局拦截器处理
treeNodes.value = [];
imgAddressPrefix.value = '';
} finally {
@@ -802,9 +803,10 @@ const getList = async () => {
};
const getNodeLibrary = async () => {
try {
- const res = await getNodeLibraryList({ errorMode: 'page' });
+ const res = await getNodeLibraryList();
nodeLibraryGroups.value = res.data?.groups || [];
} catch {
+ // 错误已由全局拦截器处理
nodeLibraryGroups.value = [];
}
};
@@ -812,7 +814,7 @@ const getNodeLibrary = async () => {
const fetchWorkflowList = async () => {
workflowListLoading.value = true;
try {
- const res = await getWorkflowList({ errorMode: 'page' });
+ const res = await getWorkflowList();
// 分别处理用户工作流和模板工作流
const userWorkflows = res.data?.listFlowUserRes?.list || [];
@@ -833,6 +835,7 @@ const fetchWorkflowList = async () => {
const templateEnd = templateStart + templateWorkflowPagination.pageSize;
templateWorkflowList.value = templateWorkflows.slice(templateStart, templateEnd);
} catch {
+ // 错误已由全局拦截器处理
userWorkflowList.value = [];
templateWorkflowList.value = [];
userWorkflowPagination.total = 0;
@@ -892,7 +895,7 @@ const handleRemoveModel = () => {
const useWorkflow = async (workflow: WorkflowItem) => {
try {
// 调用详情接口获取最新的工作流数据
- const res = await getWorkflowDetail(workflow.id, { errorMode: 'page' });
+ const res = await getWorkflowDetail(workflow.id);
if (res.data) {
// 切换到创作模式
isCreationMode.value = true;
@@ -946,7 +949,7 @@ const useWorkflow = async (workflow: WorkflowItem) => {
const editWorkflow = async (workflow: WorkflowItem) => {
try {
// 调用详情接口获取最新的工作流数据
- const res = await getWorkflowDetail(workflow.id, { errorMode: 'page' });
+ const res = await getWorkflowDetail(workflow.id);
if (res.data?.flowContent) {
// 切换回画布编辑模式
isCreationMode.value = false;
@@ -994,7 +997,7 @@ const deleteWorkflowAction = async (workflow: WorkflowItem) => {
type: 'warning',
});
- await deleteWorkflow(workflow.id, { errorMode: 'page' });
+ await deleteWorkflow(workflow.id);
ElMessage.success('工作流删除成功');
// 如果删除的是当前正在编辑的工作流,清空编辑状态
@@ -1038,15 +1041,10 @@ const sendMessage = async () => {
const fileUrls: string[] = [];
if (selectedFiles.value.length > 0) {
for (const file of selectedFiles.value) {
- try {
- const uploadRes = await uploadFile(file, { errorMode: 'page' });
- // 拼接完整的文件地址
- const fullUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
- fileUrls.push(fullUrl);
- } catch (error) {
- ElMessage.error(`文件 ${file.name} 上传失败`);
- throw error;
- }
+ const uploadRes = await uploadFile(file);
+ // 拼接完整的文件地址
+ const fullUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
+ fileUrls.push(fullUrl);
}
}
@@ -1109,7 +1107,7 @@ const sendMessage = async () => {
};
// 5. 调用执行接口(不再使用 FormData,直接传 JSON)
- await executeFlow(params, { errorMode: 'page' });
+ await executeFlow(params);
ElMessage.success('创作完成!');
@@ -1117,8 +1115,8 @@ const sendMessage = async () => {
userInput.value = '';
selectedFiles.value = [];
selectedCreationSkill.value = null;
- } catch (error) {
- ElMessage.error('创作失败,请重试');
+ } catch {
+ // 接口错误由 request 全局提示后端 message
} finally {
isCreating.value = false;
}
@@ -1149,7 +1147,7 @@ const downloadNode = async (d: TreeNode) => {
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
try {
// 下载失败时希望展示更贴近页面语义的提示,因此改为 page 模式。
- const r = await downloadToFile({ fileURL: d.fileUrl }, { errorMode: 'page' });
+ const r = await downloadToFile({ fileURL: d.fileUrl });
const blob = r instanceof Blob ? r : r?.data;
if (!(blob instanceof Blob)) throw new Error('invalid blob');
const name = decodeURIComponent(d.fileUrl.split('/').pop() || `${d.label}.${d.nodeType === 'html' ? 'html' : 'png'}`);
@@ -1163,7 +1161,7 @@ const downloadNode = async (d: TreeNode) => {
URL.revokeObjectURL(u);
ElMessage.success('下载成功');
} catch {
- // 下载接口使用 errorMode: 'page',后端错误会自动显示
+ // 下载失败由 request 全局提示后端 message
}
};
const syncDsl = () => {
@@ -1847,26 +1845,20 @@ const confirmSaveWorkflow = async () => {
// 判断是新建还是更新
if (currentEditingWorkflowId.value) {
// 更新现有工作流
- await updateWorkflow(
- {
- id: currentEditingWorkflowId.value,
- flowName: saveForm.flowName,
- description: saveForm.description,
- flowContent: workflowDsl.value,
- },
- { errorMode: 'page' }
- );
+ await updateWorkflow({
+ id: currentEditingWorkflowId.value,
+ flowName: saveForm.flowName,
+ description: saveForm.description,
+ flowContent: workflowDsl.value,
+ });
ElMessage.success('工作流更新成功');
} else {
// 创建新工作流
- await saveWorkflow(
- {
- flowName: saveForm.flowName,
- description: saveForm.description,
- flowContent: workflowDsl.value,
- },
- { errorMode: 'page' }
- );
+ await saveWorkflow({
+ flowName: saveForm.flowName,
+ description: saveForm.description,
+ flowContent: workflowDsl.value,
+ });
ElMessage.success('工作流保存成功');
}
saveDialogVisible.value = false;
diff --git a/src/views/digitalHuman/modelConfig/modelModule/component/editModule.vue b/src/views/digitalHuman/modelConfig/modelModule/component/editModule.vue
index 4e6c4d5..640904d 100644
--- a/src/views/digitalHuman/modelConfig/modelModule/component/editModule.vue
+++ b/src/views/digitalHuman/modelConfig/modelModule/component/editModule.vue
@@ -1,4 +1,4 @@
-
+
-
+
@@ -53,13 +48,7 @@
-
+
@@ -139,29 +128,21 @@
-
+
-
-
-
+
+
+
+ 配置请求映射 ({{ state.requestMappingFields.length }})
+
-
-
-
+
+
+
+ 配置响应映射 ({{ state.responseMappingFields.length }})
+
@@ -170,13 +151,7 @@
@@ -219,6 +194,51 @@
+
+
+
+
+
+ =
+
+ 删除
+
+
+ 添加字段
+
+
+
+
+
+
+
+
+
+
+
+ =
+
+
+ {{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
+
+ 删除
+
+
+ 添加字段
+
+
+
+
+
@@ -227,12 +247,7 @@ import { reactive, ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
-import {
- addModelModule,
- updateModelModule,
- getModelModuleDetail,
- type ModelFormEntry,
-} from '/@/api/digitalHuman/modelConfig/modelModule/index';
+import { addModelModule, updateModelModule, getModelModuleDetail, type ModelFormEntry } from '/@/api/digitalHuman/modelConfig/modelModule/index';
export type ModelTypeOption = { id: number | string; label: string };
@@ -254,6 +269,8 @@ const editModuleFormRef = ref();
const emit = defineEmits(['refresh']);
const showHeaderDialog = ref(false);
const showFormDialog = ref(false);
+const showRequestMappingDialog = ref(false);
+const showResponseMappingDialog = ref(false);
const state = reactive({
ruleForm: {
id: '',
@@ -274,8 +291,6 @@ const state = reactive({
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
- requestMappingJson: '{}',
- responseMappingJson: '{}',
},
rules: {
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
@@ -293,48 +308,6 @@ const state = reactive({
],
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
- requestMappingJson: [
- {
- validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
- if (!value || !String(value).trim()) {
- callback(new Error('请输入请求映射 JSON'));
- return;
- }
- try {
- const o = JSON.parse(value);
- if (o === null || typeof o !== 'object' || Array.isArray(o)) {
- callback(new Error('请求映射须为 JSON 对象'));
- return;
- }
- callback();
- } catch {
- callback(new Error('请求映射 JSON 格式无效'));
- }
- },
- trigger: 'blur',
- },
- ],
- responseMappingJson: [
- {
- validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
- if (!value || !String(value).trim()) {
- callback(new Error('请输入响应映射 JSON'));
- return;
- }
- try {
- const o = JSON.parse(value);
- if (o === null || typeof o !== 'object' || Array.isArray(o)) {
- callback(new Error('响应映射须为 JSON 对象'));
- return;
- }
- callback();
- } catch {
- callback(new Error('响应映射 JSON 格式无效'));
- }
- },
- trigger: 'blur',
- },
- ],
maxConcurrency: [{ required: true, message: '请输入最大并发数', trigger: 'blur' }],
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
@@ -351,8 +324,22 @@ const state = reactive({
showAdvanced: false,
headers: [] as Array<{ key: string; value: string }>,
formFields: [] as Array<{ key: string; value: string }>,
+ requestMappingFields: [] as Array<{ key: string; value: string }>,
+ responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean }>,
+ mainBodyIndex: -1, // 记录哪一行被设置为返回主体
});
+// 将数组转换为对象
+const fieldsToObject = (fields: Array<{ key: string; value: string }>) => {
+ const obj: Record = {};
+ fields.forEach((f) => {
+ if (f.key && f.key.trim()) {
+ obj[f.key.trim()] = f.value || '';
+ }
+ });
+ return obj;
+};
+
const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg);
// 解析 form:支持数组 [{ key, value }] 或历史对象 { k: { value } }
const parseFormFields = (form: unknown) => {
@@ -368,15 +355,25 @@ const parseFormFields = (form: unknown) => {
if (typeof form === 'object') {
const fields: Array<{ key: string; value: string }> = [];
Object.keys(form as Record).forEach((key) => {
- const v = (form as Record)[key];
- if (v && typeof v === 'object' && v.value !== undefined) {
- fields.push({ key, value: String(v.value) });
- }
+ const v = (form as Record)[key];
+ const value = String(v || '');
+ fields.push({ key, value });
});
return fields;
}
return [];
};
+// 解析 requestMapping 对象为数组
+const parseRequestMappingFields = (mapping: unknown) => {
+ if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
+ return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
+};
+
+// 解析 responseMapping 对象为数组
+const parseResponseMappingFields = (mapping: unknown) => {
+ if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
+ return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
+};
const buildFormArray = (): ModelFormEntry[] => {
return state.formFields
@@ -454,9 +451,48 @@ const removeFormField = (index: number) => {
const confirmFormFields = () => {
showFormDialog.value = false;
};
+// 请求映射字段操作
+const addRequestMappingField = () => {
+ state.requestMappingFields.push({ key: '', value: '' });
+};
+
+const removeRequestMappingField = (index: number) => {
+ state.requestMappingFields.splice(index, 1);
+};
+
+const confirmRequestMappingFields = () => {
+ showRequestMappingDialog.value = false;
+};
+
+// 响应映射字段操作
+const addResponseMappingField = () => {
+ state.responseMappingFields.push({ key: '', value: '', isMainBody: false });
+};
+
+const removeResponseMappingField = (index: number) => {
+ state.responseMappingFields.splice(index, 1);
+};
+
+// 设置返回主体(单选)
+const setMainBody = (index: number) => {
+ // 清除所有字段的返回主体标记
+ state.responseMappingFields.forEach((field, i) => {
+ field.isMainBody = i === index;
+ });
+ state.mainBodyIndex = index;
+};
+
+const confirmResponseMappingFields = () => {
+ showResponseMappingDialog.value = false;
+};
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
+const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean }>) => {
+ if (!rows.length) return [{ key: '', value: '', isMainBody: false }];
+ return rows.map(row => ({ ...row, isMainBody: row.isMainBody || false }));
+};
+
/** 从 getModel 返回的 data 中取出单条模型对象 */
const unwrapModelDetailPayload = (data: unknown): Record | null => {
if (data == null) return null;
@@ -487,10 +523,7 @@ const fillFormFromDetailRow = (row: Record) => {
state.ruleForm = {
id: row.id as string,
modelName: String(row.modelName ?? ''),
- modelsType:
- row.modelsType !== undefined && row.modelsType !== null
- ? typeOptionValue(row.modelsType as number | string)
- : null,
+ modelsType: row.modelsType !== undefined && row.modelsType !== null ? typeOptionValue(row.modelsType as number | string) : null,
baseUrl: String(row.baseUrl ?? ''),
httpMethod: String(row.httpMethod || 'POST'),
headMsg: String(row.headMsg || ''),
@@ -506,21 +539,25 @@ const fillFormFromDetailRow = (row: Record) => {
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
remark: String(row.remark || ''),
- requestMappingJson: JSON.stringify(
- row.requestMapping && typeof row.requestMapping === 'object' ? row.requestMapping : {},
- null,
- 2
- ),
- responseMappingJson: JSON.stringify(
- row.responseMapping && typeof row.responseMapping === 'object' ? row.responseMapping : {},
- null,
- 2
- ),
};
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
+ // 解析请求映射和响应映射
+ state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
+ state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
+
+ // 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value})
+ if (row.responseBody && typeof row.responseBody === 'object') {
+ const responseBodyKey = Object.keys(row.responseBody)[0];
+ if (responseBodyKey) {
+ const mainBodyIndex = state.responseMappingFields.findIndex(f => f.key === responseBodyKey);
+ if (mainBodyIndex !== -1) {
+ state.responseMappingFields[mainBodyIndex].isMainBody = true;
+ state.mainBodyIndex = mainBodyIndex;
+ }
+ }
+ }
};
-
// 打开弹窗(编辑时会请求 /model/getModel 详情)
const openDialog = async (type: string, row?: Record) => {
state.dialog.type = type;
@@ -553,7 +590,7 @@ const openDialog = async (type: string, row?: Record) => {
}
fillFormFromDetailRow(detail as Record);
} catch {
- ElMessage.error('获取模型详情失败');
+ // 接口错误由 request 全局提示后端 message
state.dialog.isShowDialog = false;
} finally {
state.dialog.detailLoading = false;
@@ -578,11 +615,11 @@ const openDialog = async (type: string, row?: Record) => {
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
- requestMappingJson: '{}',
- responseMappingJson: '{}',
};
state.headers = [{ key: '', value: '' }];
state.formFields = [{ key: '', value: '' }];
+ state.requestMappingFields = [{ key: '', value: '' }];
+ state.responseMappingFields = [{ key: '', value: '', isMainBody: false }];
state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增';
}
@@ -608,8 +645,11 @@ const onSubmit = () => {
state.dialog.loading = true;
try {
state.ruleForm.headMsg = stringifyHeaders();
- const requestMapping = parseJsonObjectField(state.ruleForm.requestMappingJson, {});
- const responseMapping = parseJsonObjectField(state.ruleForm.responseMappingJson, {});
+ const requestMapping = fieldsToObject(state.requestMappingFields);
+ const responseMapping = fieldsToObject(state.responseMappingFields);
+ // 获取被设置为返回主体的字段 {key: value}
+ const responseBodyField = state.responseMappingFields.find(f => f.isMainBody);
+ const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {};
const submitData = {
modelName: state.ruleForm.modelName,
modelsType: state.ruleForm.modelsType as number | string,
@@ -620,9 +660,10 @@ const onSubmit = () => {
enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel,
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
- form: buildFormArray(),
+ form: fieldsToObject(state.formFields),
requestMapping,
responseMapping,
+ responseBody,
maxConcurrency: state.ruleForm.maxConcurrency,
queueLimit: state.ruleForm.queueLimit,
timeoutSeconds: state.ruleForm.timeoutSeconds,
@@ -642,8 +683,8 @@ const onSubmit = () => {
}
closeDialog();
emit('refresh');
- } catch (error) {
- ElMessage.error('保存失败');
+ } catch {
+ // 接口错误由 request 全局提示后端 message
} finally {
state.dialog.loading = false;
}
@@ -657,6 +698,19 @@ defineExpose({
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/digitalHuman/modelConfig/modelModule/index.vue b/src/views/digitalHuman/modelConfig/modelModule/index.vue
index a8c7d67..215510f 100644
--- a/src/views/digitalHuman/modelConfig/modelModule/index.vue
+++ b/src/views/digitalHuman/modelConfig/modelModule/index.vue
@@ -32,6 +32,25 @@
+
+
+
+ {{ Number(row.isChatModel) === 1 ? '是' : '否' }}
+
+
+
+
+
+
+
+
+ —
+
+
{{ scope.row.enabled === 1 ? '启用' : '禁用' }}
@@ -94,6 +113,16 @@ const state = reactive({
},
});
+/** 列表行与会话开关接口约定字段 chatSessionEnabled(0/1);接口未就绪前占位 */
+const chatSessionSwitchOn = (row: { chatSessionEnabled?: number }) => Number(row.chatSessionEnabled) === 1;
+
+const onChatSessionSwitchRequest = (_row: { id?: number | string }) => {
+ return new Promise((resolve) => {
+ ElMessage.info('会话开关接口接入后即可生效');
+ resolve(false);
+ });
+};
+
const resolveModelTypeLabel = (modelsType: number | string | undefined | null) => {
if (modelsType === undefined || modelsType === null || modelsType === '') {
return '—';
@@ -108,8 +137,8 @@ const loadModelTypes = async () => {
if (res.code === 0) {
state.modelTypes = normalizeModelTypeOptions(res);
}
- } catch (e) {
- ElMessage.error('获取模型类型失败:');
+ } catch {
+ // 接口错误由 request 全局提示后端 message
}
};
@@ -122,8 +151,8 @@ const getTableData = async () => {
state.tableData.data = res.data.list || [];
state.tableData.total = res.data.total || 0;
}
- } catch (error) {
- ElMessage.error('获取模型列表失败');
+ } catch {
+ // 接口错误由 request 全局提示后端 message
} finally {
state.tableData.loading = false;
}
@@ -151,8 +180,8 @@ const onRowDel = (row: any) => {
await deleteModelModule(row.id);
ElMessage.success('删除成功');
getTableData();
- } catch (error) {
- ElMessage.error('删除失败');
+ } catch {
+ // 接口错误由 request 全局提示后端 message
}
})
.catch(() => {});
@@ -178,6 +207,10 @@ onMounted(async () => {