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)" >
{{ getModelTypeName(model.modelsType) }}
- +
+ 系统模型 + +

{{ model.modelName }}

@@ -58,23 +61,65 @@ + + + + + + + + + + + + + + + + @@ -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 @@ - + + + + + +