添加会话模型和API Key配置功能
- 在模型模块中新增会话开关状态字段,支持会话模型的管理。 - 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。 - 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。 - 更新相关样式以增强界面可读性和美观性。
This commit is contained in:
716
docs/API-ERROR-HANDLING.md
Normal file
716
docs/API-ERROR-HANDLING.md
Normal file
@@ -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
|
||||||
|
**维护者:** 开发团队
|
||||||
@@ -79,6 +79,8 @@ export interface ModelModuleItem {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
isPrivate?: number;
|
isPrivate?: number;
|
||||||
isChatModel?: number;
|
isChatModel?: number;
|
||||||
|
/** 会话开关状态(列表接口返回,0 关 1 开;会话开关接口就绪后生效) */
|
||||||
|
chatSessionEnabled?: number;
|
||||||
enabled: number;
|
enabled: number;
|
||||||
maxConcurrency: number;
|
maxConcurrency: number;
|
||||||
queueLimit: number;
|
queueLimit: number;
|
||||||
@@ -210,3 +212,8 @@ export function getModelModuleDetail(id: number | string) {
|
|||||||
params: { id },
|
params: { id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 列表「会话开关」提交接口确定后在此封装,例如:
|
||||||
|
// export function updateModelChatSessionSwitch(data: { id: number | string; chatSessionEnabled: 0 | 1 }) {
|
||||||
|
// return request({ url: '/model-gateway/model/...', method: 'post', data });
|
||||||
|
// }
|
||||||
|
|||||||
@@ -19,13 +19,16 @@
|
|||||||
v-for="model in modelList"
|
v-for="model in modelList"
|
||||||
:key="model.id"
|
:key="model.id"
|
||||||
class="model-card"
|
class="model-card"
|
||||||
:class="{ selected: selectedModel?.id === model.id }"
|
:class="{ selected: selectedModel?.id === model.id, 'system-model': model.tenantId === 1 }"
|
||||||
@click="handleSelectModel(model)"
|
@click="handleSelectModel(model)"
|
||||||
>
|
>
|
||||||
<div class="model-card-header">
|
<div class="model-card-header">
|
||||||
<div class="model-type">{{ getModelTypeName(model.modelsType) }}</div>
|
<div class="model-type">{{ getModelTypeName(model.modelsType) }}</div>
|
||||||
|
<div class="model-badges">
|
||||||
|
<el-tag v-if="model.tenantId === 1" type="warning" size="small">系统模型</el-tag>
|
||||||
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a"><CircleCheck /></el-icon>
|
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a"><CircleCheck /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="model-card-body">
|
<div class="model-card-body">
|
||||||
<h3 class="model-name">{{ model.modelName }}</h3>
|
<h3 class="model-name">{{ model.modelName }}</h3>
|
||||||
<p class="model-url">{{ model.baseUrl }}</p>
|
<p class="model-url">{{ model.baseUrl }}</p>
|
||||||
@@ -58,23 +61,65 @@
|
|||||||
|
|
||||||
<!-- 新建模型弹窗 -->
|
<!-- 新建模型弹窗 -->
|
||||||
<EditModule ref="editModuleRef" @refresh="handleRefresh" />
|
<EditModule ref="editModuleRef" @refresh="handleRefresh" />
|
||||||
|
|
||||||
|
<!-- 系统模型 API Key 输入弹窗 -->
|
||||||
|
<el-dialog v-model="apiKeyDialogVisible" title="配置系统模型" width="500px" :close-on-click-modal="false" append-to-body>
|
||||||
|
<el-alert type="info" :closable="false" style="margin-bottom: 16px">
|
||||||
|
<template #title>
|
||||||
|
<div style="line-height: 1.6">
|
||||||
|
您选择的是系统模型,需要配置您自己的 API Key。<br />
|
||||||
|
系统将为您创建一个模型副本。
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
|
<el-form :model="apiKeyForm" :rules="apiKeyRules" ref="apiKeyFormRef" label-width="100px">
|
||||||
|
<el-form-item label="模型名称" prop="modelName">
|
||||||
|
<el-input v-model="apiKeyForm.modelName" placeholder="请输入模型名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="API Key" prop="apiKey">
|
||||||
|
<el-input v-model="apiKeyForm.apiKey" type="password" show-password placeholder="请输入您的 API Key" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="apiKeyDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleCreatePrivateModel" :loading="creatingModel">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, watch } from 'vue';
|
import { ref, reactive, watch } from 'vue';
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
||||||
import { getModelModuleList } from '/@/api/digitalHuman/modelConfig/modelModule';
|
import { getModelModuleList, addModelModule } from '/@/api/digitalHuman/modelConfig/modelModule';
|
||||||
|
import { getApiErrorMessage } from '/@/utils/request';
|
||||||
import EditModule from '/@/views/digitalHuman/modelConfig/modelModule/component/editModule.vue';
|
import EditModule from '/@/views/digitalHuman/modelConfig/modelModule/component/editModule.vue';
|
||||||
|
|
||||||
interface ModelItem {
|
interface ModelItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
tenantId?: number;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
modelsType: number;
|
modelsType: number;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
route: string;
|
route: string;
|
||||||
httpMethod: string;
|
httpMethod: string;
|
||||||
enabled: number;
|
enabled: number;
|
||||||
|
apiKey?: string;
|
||||||
|
isPrivate?: number;
|
||||||
|
isChatModel?: number;
|
||||||
|
headMsg?: string;
|
||||||
|
form?: any;
|
||||||
|
requestMapping?: any;
|
||||||
|
responseMapping?: any;
|
||||||
|
maxConcurrency?: number;
|
||||||
|
queueLimit?: number;
|
||||||
|
timeoutSeconds?: number;
|
||||||
|
expectedSeconds?: number;
|
||||||
|
retryTimes?: number;
|
||||||
|
retryQueueMaxSeconds?: number;
|
||||||
|
autoCleanSeconds?: number;
|
||||||
|
remark?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +148,20 @@ const loading = ref(false);
|
|||||||
const selectedModel = ref<ModelItem | null>(null);
|
const selectedModel = ref<ModelItem | null>(null);
|
||||||
const editModuleRef = ref();
|
const editModuleRef = ref();
|
||||||
|
|
||||||
|
// 系统模型 API Key 配置
|
||||||
|
const apiKeyDialogVisible = ref(false);
|
||||||
|
const apiKeyFormRef = ref<FormInstance>();
|
||||||
|
const apiKeyForm = reactive({
|
||||||
|
modelName: '',
|
||||||
|
apiKey: '',
|
||||||
|
});
|
||||||
|
const apiKeyRules: FormRules = {
|
||||||
|
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
|
||||||
|
apiKey: [{ required: true, message: '请输入 API Key', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
const creatingModel = ref(false);
|
||||||
|
const systemModelToClone = ref<ModelItem | null>(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -122,9 +181,9 @@ watch(visible, (val) => {
|
|||||||
|
|
||||||
const getModelTypeName = (type: number) => {
|
const getModelTypeName = (type: number) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '图片模型',
|
1: '推理模型',
|
||||||
2: '语音模型',
|
2: '图片模型',
|
||||||
3: '推理模型',
|
3: '音频模型',
|
||||||
};
|
};
|
||||||
return typeMap[type] || '未知类型';
|
return typeMap[type] || '未知类型';
|
||||||
};
|
};
|
||||||
@@ -137,10 +196,11 @@ const fetchModelList = async () => {
|
|||||||
pageSize: pagination.pageSize,
|
pageSize: pagination.pageSize,
|
||||||
modelName: searchParams.modelName || undefined,
|
modelName: searchParams.modelName || undefined,
|
||||||
};
|
};
|
||||||
const res = await getModelModuleList(params, { errorMode: 'message' });
|
const res: any = await getModelModuleList(params);
|
||||||
modelList.value = res.data?.list || [];
|
modelList.value = res.data?.list || [];
|
||||||
pagination.total = res.data?.total || 0;
|
pagination.total = res.data?.total || 0;
|
||||||
} catch (error) {
|
} catch {
|
||||||
|
// 接口错误由 request 全局提示后端 message
|
||||||
modelList.value = [];
|
modelList.value = [];
|
||||||
pagination.total = 0;
|
pagination.total = 0;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,7 +218,77 @@ const handlePageChange = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectModel = (model: ModelItem) => {
|
const handleSelectModel = (model: ModelItem) => {
|
||||||
|
// 判断是否是系统模型(tenantId === 1)
|
||||||
|
if (model.tenantId === 1) {
|
||||||
|
// 系统模型,需要用户配置 API Key
|
||||||
|
systemModelToClone.value = model;
|
||||||
|
apiKeyForm.modelName = `${model.modelName} - 副本`;
|
||||||
|
apiKeyForm.apiKey = '';
|
||||||
|
apiKeyDialogVisible.value = true;
|
||||||
|
} else {
|
||||||
|
// 非系统模型,直接选中
|
||||||
selectedModel.value = model;
|
selectedModel.value = model;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatePrivateModel = async () => {
|
||||||
|
if (!apiKeyFormRef.value || !systemModelToClone.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiKeyFormRef.value.validate();
|
||||||
|
|
||||||
|
creatingModel.value = true;
|
||||||
|
|
||||||
|
// 基于系统模型创建新模型(继承原模型的所有配置,只替换 apiKey)
|
||||||
|
const systemModel = systemModelToClone.value;
|
||||||
|
const createParams = {
|
||||||
|
modelName: apiKeyForm.modelName,
|
||||||
|
modelsType: systemModel.modelsType,
|
||||||
|
baseUrl: systemModel.baseUrl,
|
||||||
|
httpMethod: systemModel.httpMethod || 'POST',
|
||||||
|
headMsg: systemModel.headMsg || '',
|
||||||
|
isPrivate: systemModel.isPrivate ?? 1, // 继承原模型的公有/私有属性
|
||||||
|
enabled: systemModel.enabled ?? 1,
|
||||||
|
isChatModel: systemModel.isChatModel || 0,
|
||||||
|
apiKey: apiKeyForm.apiKey, // 使用用户输入的新 API Key
|
||||||
|
form: systemModel.form || {},
|
||||||
|
requestMapping: systemModel.requestMapping || {},
|
||||||
|
responseMapping: systemModel.responseMapping || {},
|
||||||
|
maxConcurrency: systemModel.maxConcurrency || 10,
|
||||||
|
queueLimit: systemModel.queueLimit || 100,
|
||||||
|
timeoutSeconds: systemModel.timeoutSeconds || 30,
|
||||||
|
expectedSeconds: systemModel.expectedSeconds || 15,
|
||||||
|
retryTimes: systemModel.retryTimes || 3,
|
||||||
|
retryQueueMaxSeconds: systemModel.retryQueueMaxSeconds || 60,
|
||||||
|
autoCleanSeconds: systemModel.autoCleanSeconds || 300,
|
||||||
|
remark: systemModel.remark || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res: any = await addModelModule(createParams);
|
||||||
|
|
||||||
|
ElMessage.success('模型创建成功');
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
apiKeyDialogVisible.value = false;
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
await fetchModelList();
|
||||||
|
|
||||||
|
// 选中新创建的模型
|
||||||
|
const newModelId = res.data?.id || res.data;
|
||||||
|
if (newModelId) {
|
||||||
|
const newModel = modelList.value.find((m) => m.id === String(newModelId));
|
||||||
|
if (newModel) {
|
||||||
|
selectedModel.value = newModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(getApiErrorMessage(error, '创建模型失败'));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
creatingModel.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddModel = () => {
|
const handleAddModel = () => {
|
||||||
@@ -179,6 +309,8 @@ const handleConfirm = () => {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
selectedModel.value = null;
|
selectedModel.value = null;
|
||||||
|
apiKeyDialogVisible.value = false;
|
||||||
|
systemModelToClone.value = null;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -228,6 +360,15 @@ const handleClose = () => {
|
|||||||
background: #f0f9ff;
|
background: #f0f9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-card.system-model {
|
||||||
|
border-color: #fbbf24;
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card.system-model:hover {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
.model-card-header {
|
.model-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -245,6 +386,12 @@ const handleClose = () => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-badges {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.check-icon {
|
.check-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,9 +152,7 @@ const fetchSkillList = async () => {
|
|||||||
try {
|
try {
|
||||||
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
|
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
|
||||||
const res =
|
const res =
|
||||||
activeTab.value === 'system'
|
activeTab.value === 'system' ? await getSkillList(params) : await getUserSkilllistUser(params);
|
||||||
? await getSkillList(params, { errorMode: 'message' })
|
|
||||||
: await getUserSkilllistUser(params, { errorMode: 'message' });
|
|
||||||
skillList.value = res.data?.list || [];
|
skillList.value = res.data?.list || [];
|
||||||
pagination.total = res.data?.total || 0;
|
pagination.total = res.data?.total || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const fetchSkillList = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
|
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 || [];
|
skillList.value = res.data?.list || [];
|
||||||
pagination.total = res.data?.total || 0;
|
pagination.total = res.data?.total || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { getChangedFields } from '/@/utils/diffUtils';
|
|||||||
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
|
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制一次请求的错误提示归属:
|
* 控制一次请求的错误提示归属(默认 global):
|
||||||
* - global: 交给 request.ts 统一弹错,适合绝大多数接口
|
* - global: 由拦截器统一弹出后端返回的 message(含 HTTP 与业务 JSON)
|
||||||
* - page: 页面自己在 catch 中决定提示文案,避免与全局重复
|
* - page: 不自动弹窗,仅 reject;请在页面 catch 内自行处理(应与全局择一,避免重复)
|
||||||
* - silent: 完全静默,适合轮询、后台刷新等不希望打扰用户的请求
|
* - silent: 完全静默(轮询等)
|
||||||
*/
|
*/
|
||||||
export interface RequestOptions {
|
export interface RequestOptions {
|
||||||
errorMode?: 'global' | 'page' | 'silent';
|
errorMode?: 'global' | 'page' | 'silent';
|
||||||
@@ -39,6 +39,26 @@ const ERROR_MESSAGE_INTERVAL = 2000;
|
|||||||
const getErrorMode = (config?: InternalAxiosRequestConfig) => config?.requestOptions?.errorMode ?? 'global';
|
const getErrorMode = (config?: InternalAxiosRequestConfig) => config?.requestOptions?.errorMode ?? 'global';
|
||||||
const shouldShowGlobalError = (config?: InternalAxiosRequestConfig) => getErrorMode(config) === '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<string, unknown>;
|
||||||
|
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 = () => {
|
const closeActiveErrorMessage = () => {
|
||||||
activeErrorMessage?.close();
|
activeErrorMessage?.close();
|
||||||
activeErrorMessage = null;
|
activeErrorMessage = null;
|
||||||
@@ -174,7 +194,7 @@ const responseInterceptor = (response: AxiosResponse) => {
|
|||||||
const res = response.data;
|
const res = response.data;
|
||||||
const httpStatus = response.status;
|
const httpStatus = response.status;
|
||||||
const code = res?.code;
|
const code = res?.code;
|
||||||
const message = res?.message;
|
const message = extractBackendMessage(res);
|
||||||
const config = response.config;
|
const config = response.config;
|
||||||
|
|
||||||
if (isTokenExpiredError(httpStatus, code, message)) {
|
if (isTokenExpiredError(httpStatus, code, message)) {
|
||||||
@@ -207,8 +227,8 @@ const responseInterceptor = (response: AxiosResponse) => {
|
|||||||
if (knownErrorCodes.includes(code)) {
|
if (knownErrorCodes.includes(code)) {
|
||||||
errorMsg = message || `请求失败(${code})`;
|
errorMsg = message || `请求失败(${code})`;
|
||||||
} else {
|
} else {
|
||||||
// 未知的 code,统一提示后端异常
|
// 未知的 code:优先使用后端 message,便于排查业务含义
|
||||||
errorMsg = '后端异常,请联系管理员';
|
errorMsg = message || '后端异常,请联系管理员';
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorMessage(errorMsg, config);
|
showErrorMessage(errorMsg, config);
|
||||||
@@ -221,7 +241,8 @@ const responseInterceptor = (response: AxiosResponse) => {
|
|||||||
const responseErrorHandler = (error: any) => {
|
const responseErrorHandler = (error: any) => {
|
||||||
const config = error.config as InternalAxiosRequestConfig | undefined;
|
const config = error.config as InternalAxiosRequestConfig | undefined;
|
||||||
const httpStatus = error.response?.status;
|
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')) {
|
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
|
||||||
showErrorMessage('请求超时,请检查网络连接', config);
|
showErrorMessage('请求超时,请检查网络连接', config);
|
||||||
@@ -232,7 +253,7 @@ const responseErrorHandler = (error: any) => {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTokenExpiredError(httpStatus, error.response?.data?.code, responseMessage)) {
|
if (isTokenExpiredError(httpStatus, error.response?.data?.code as number | undefined, responseMessage)) {
|
||||||
handleTokenExpired();
|
handleTokenExpired();
|
||||||
return Promise.reject(new Error('登录状态已过期'));
|
return Promise.reject(new Error('登录状态已过期'));
|
||||||
}
|
}
|
||||||
@@ -245,7 +266,7 @@ const responseErrorHandler = (error: any) => {
|
|||||||
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
|
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
|
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
|
||||||
showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面', config);
|
showErrorMessage(responseMessage ?? '服务开通中,请稍后刷新页面', config);
|
||||||
return Promise.reject(new Error('模块开通中'));
|
return Promise.reject(new Error('模块开通中'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,30 +274,30 @@ const responseErrorHandler = (error: any) => {
|
|||||||
handleModuleNotEnabled(currentPath);
|
handleModuleNotEnabled(currentPath);
|
||||||
return Promise.reject(new Error('模块未开通'));
|
return Promise.reject(new Error('模块未开通'));
|
||||||
}
|
}
|
||||||
showErrorMessage(responseMessage || '服务未开通', config);
|
showErrorMessage(responseMessage ?? '服务未开通', config);
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
showErrorMessage(responseMessage || '没有权限访问该资源', config);
|
showErrorMessage(responseMessage ?? '没有权限访问该资源', config);
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
showErrorMessage(responseMessage || '请求的资源不存在', config);
|
showErrorMessage(responseMessage ?? '请求的资源不存在', config);
|
||||||
break;
|
break;
|
||||||
case 429:
|
case 429:
|
||||||
// 429 是限流,不等于登录过期,这里只保留频率提示。
|
// 429 是限流,不等于登录过期,这里只保留频率提示。
|
||||||
showErrorMessage(responseMessage || '请求过于频繁,请稍后再试', config);
|
showErrorMessage(responseMessage ?? '请求过于频繁,请稍后再试', config);
|
||||||
break;
|
break;
|
||||||
case 500:
|
case 500:
|
||||||
showErrorMessage(responseMessage || '服务器内部错误', config);
|
showErrorMessage(responseMessage ?? '服务器内部错误', config);
|
||||||
break;
|
break;
|
||||||
case 502:
|
case 502:
|
||||||
showErrorMessage(responseMessage || '网关错误', config);
|
showErrorMessage(responseMessage ?? '网关错误', config);
|
||||||
break;
|
break;
|
||||||
case 503:
|
case 503:
|
||||||
showErrorMessage(responseMessage || '服务不可用', config);
|
showErrorMessage(responseMessage ?? '服务不可用', config);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (httpStatus >= 400) {
|
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.request.use(requestInterceptor, requestErrorHandler);
|
||||||
service.interceptors.response.use(responseInterceptor, responseErrorHandler);
|
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 default service;
|
||||||
export { closeActiveErrorMessage, showErrorMessage };
|
export { closeActiveErrorMessage, showErrorMessage };
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const openDialog = async (row?: DialogFormData) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取账号详情失败:', error);
|
console.error('获取账号详情失败:', error);
|
||||||
ElMessage.error('获取账号详情失败');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const loadDatasets = async () => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('加载数据集列表失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -790,10 +790,11 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
|||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
treeLoading.value = true;
|
treeLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getExecutionList({ errorMode: 'page' });
|
const res = await getExecutionList();
|
||||||
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
||||||
treeNodes.value = buildTreeNodes(res.data?.tree || []);
|
treeNodes.value = buildTreeNodes(res.data?.tree || []);
|
||||||
} catch {
|
} catch {
|
||||||
|
// 错误已由全局拦截器处理
|
||||||
treeNodes.value = [];
|
treeNodes.value = [];
|
||||||
imgAddressPrefix.value = '';
|
imgAddressPrefix.value = '';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -802,9 +803,10 @@ const getList = async () => {
|
|||||||
};
|
};
|
||||||
const getNodeLibrary = async () => {
|
const getNodeLibrary = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getNodeLibraryList({ errorMode: 'page' });
|
const res = await getNodeLibraryList();
|
||||||
nodeLibraryGroups.value = res.data?.groups || [];
|
nodeLibraryGroups.value = res.data?.groups || [];
|
||||||
} catch {
|
} catch {
|
||||||
|
// 错误已由全局拦截器处理
|
||||||
nodeLibraryGroups.value = [];
|
nodeLibraryGroups.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -812,7 +814,7 @@ const getNodeLibrary = async () => {
|
|||||||
const fetchWorkflowList = async () => {
|
const fetchWorkflowList = async () => {
|
||||||
workflowListLoading.value = true;
|
workflowListLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getWorkflowList({ errorMode: 'page' });
|
const res = await getWorkflowList();
|
||||||
|
|
||||||
// 分别处理用户工作流和模板工作流
|
// 分别处理用户工作流和模板工作流
|
||||||
const userWorkflows = res.data?.listFlowUserRes?.list || [];
|
const userWorkflows = res.data?.listFlowUserRes?.list || [];
|
||||||
@@ -833,6 +835,7 @@ const fetchWorkflowList = async () => {
|
|||||||
const templateEnd = templateStart + templateWorkflowPagination.pageSize;
|
const templateEnd = templateStart + templateWorkflowPagination.pageSize;
|
||||||
templateWorkflowList.value = templateWorkflows.slice(templateStart, templateEnd);
|
templateWorkflowList.value = templateWorkflows.slice(templateStart, templateEnd);
|
||||||
} catch {
|
} catch {
|
||||||
|
// 错误已由全局拦截器处理
|
||||||
userWorkflowList.value = [];
|
userWorkflowList.value = [];
|
||||||
templateWorkflowList.value = [];
|
templateWorkflowList.value = [];
|
||||||
userWorkflowPagination.total = 0;
|
userWorkflowPagination.total = 0;
|
||||||
@@ -892,7 +895,7 @@ const handleRemoveModel = () => {
|
|||||||
const useWorkflow = async (workflow: WorkflowItem) => {
|
const useWorkflow = async (workflow: WorkflowItem) => {
|
||||||
try {
|
try {
|
||||||
// 调用详情接口获取最新的工作流数据
|
// 调用详情接口获取最新的工作流数据
|
||||||
const res = await getWorkflowDetail(workflow.id, { errorMode: 'page' });
|
const res = await getWorkflowDetail(workflow.id);
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
// 切换到创作模式
|
// 切换到创作模式
|
||||||
isCreationMode.value = true;
|
isCreationMode.value = true;
|
||||||
@@ -946,7 +949,7 @@ const useWorkflow = async (workflow: WorkflowItem) => {
|
|||||||
const editWorkflow = async (workflow: WorkflowItem) => {
|
const editWorkflow = async (workflow: WorkflowItem) => {
|
||||||
try {
|
try {
|
||||||
// 调用详情接口获取最新的工作流数据
|
// 调用详情接口获取最新的工作流数据
|
||||||
const res = await getWorkflowDetail(workflow.id, { errorMode: 'page' });
|
const res = await getWorkflowDetail(workflow.id);
|
||||||
if (res.data?.flowContent) {
|
if (res.data?.flowContent) {
|
||||||
// 切换回画布编辑模式
|
// 切换回画布编辑模式
|
||||||
isCreationMode.value = false;
|
isCreationMode.value = false;
|
||||||
@@ -994,7 +997,7 @@ const deleteWorkflowAction = async (workflow: WorkflowItem) => {
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
|
|
||||||
await deleteWorkflow(workflow.id, { errorMode: 'page' });
|
await deleteWorkflow(workflow.id);
|
||||||
ElMessage.success('工作流删除成功');
|
ElMessage.success('工作流删除成功');
|
||||||
|
|
||||||
// 如果删除的是当前正在编辑的工作流,清空编辑状态
|
// 如果删除的是当前正在编辑的工作流,清空编辑状态
|
||||||
@@ -1038,15 +1041,10 @@ const sendMessage = async () => {
|
|||||||
const fileUrls: string[] = [];
|
const fileUrls: string[] = [];
|
||||||
if (selectedFiles.value.length > 0) {
|
if (selectedFiles.value.length > 0) {
|
||||||
for (const file of selectedFiles.value) {
|
for (const file of selectedFiles.value) {
|
||||||
try {
|
const uploadRes = await uploadFile(file);
|
||||||
const uploadRes = await uploadFile(file, { errorMode: 'page' });
|
|
||||||
// 拼接完整的文件地址
|
// 拼接完整的文件地址
|
||||||
const fullUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
|
const fullUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
|
||||||
fileUrls.push(fullUrl);
|
fileUrls.push(fullUrl);
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(`文件 ${file.name} 上传失败`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,7 +1107,7 @@ const sendMessage = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 5. 调用执行接口(不再使用 FormData,直接传 JSON)
|
// 5. 调用执行接口(不再使用 FormData,直接传 JSON)
|
||||||
await executeFlow(params, { errorMode: 'page' });
|
await executeFlow(params);
|
||||||
|
|
||||||
ElMessage.success('创作完成!');
|
ElMessage.success('创作完成!');
|
||||||
|
|
||||||
@@ -1117,8 +1115,8 @@ const sendMessage = async () => {
|
|||||||
userInput.value = '';
|
userInput.value = '';
|
||||||
selectedFiles.value = [];
|
selectedFiles.value = [];
|
||||||
selectedCreationSkill.value = null;
|
selectedCreationSkill.value = null;
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('创作失败,请重试');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
isCreating.value = false;
|
isCreating.value = false;
|
||||||
}
|
}
|
||||||
@@ -1149,7 +1147,7 @@ const downloadNode = async (d: TreeNode) => {
|
|||||||
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
|
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
|
||||||
try {
|
try {
|
||||||
// 下载失败时希望展示更贴近页面语义的提示,因此改为 page 模式。
|
// 下载失败时希望展示更贴近页面语义的提示,因此改为 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;
|
const blob = r instanceof Blob ? r : r?.data;
|
||||||
if (!(blob instanceof Blob)) throw new Error('invalid blob');
|
if (!(blob instanceof Blob)) throw new Error('invalid blob');
|
||||||
const name = decodeURIComponent(d.fileUrl.split('/').pop() || `${d.label}.${d.nodeType === 'html' ? 'html' : 'png'}`);
|
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);
|
URL.revokeObjectURL(u);
|
||||||
ElMessage.success('下载成功');
|
ElMessage.success('下载成功');
|
||||||
} catch {
|
} catch {
|
||||||
// 下载接口使用 errorMode: 'page',后端错误会自动显示
|
// 下载失败由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const syncDsl = () => {
|
const syncDsl = () => {
|
||||||
@@ -1847,26 +1845,20 @@ const confirmSaveWorkflow = async () => {
|
|||||||
// 判断是新建还是更新
|
// 判断是新建还是更新
|
||||||
if (currentEditingWorkflowId.value) {
|
if (currentEditingWorkflowId.value) {
|
||||||
// 更新现有工作流
|
// 更新现有工作流
|
||||||
await updateWorkflow(
|
await updateWorkflow({
|
||||||
{
|
|
||||||
id: currentEditingWorkflowId.value,
|
id: currentEditingWorkflowId.value,
|
||||||
flowName: saveForm.flowName,
|
flowName: saveForm.flowName,
|
||||||
description: saveForm.description,
|
description: saveForm.description,
|
||||||
flowContent: workflowDsl.value,
|
flowContent: workflowDsl.value,
|
||||||
},
|
});
|
||||||
{ errorMode: 'page' }
|
|
||||||
);
|
|
||||||
ElMessage.success('工作流更新成功');
|
ElMessage.success('工作流更新成功');
|
||||||
} else {
|
} else {
|
||||||
// 创建新工作流
|
// 创建新工作流
|
||||||
await saveWorkflow(
|
await saveWorkflow({
|
||||||
{
|
|
||||||
flowName: saveForm.flowName,
|
flowName: saveForm.flowName,
|
||||||
description: saveForm.description,
|
description: saveForm.description,
|
||||||
flowContent: workflowDsl.value,
|
flowContent: workflowDsl.value,
|
||||||
},
|
});
|
||||||
{ errorMode: 'page' }
|
|
||||||
);
|
|
||||||
ElMessage.success('工作流保存成功');
|
ElMessage.success('工作流保存成功');
|
||||||
}
|
}
|
||||||
saveDialogVisible.value = false;
|
saveDialogVisible.value = false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="system-edit-module-container">
|
<div class="system-edit-module-container">
|
||||||
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
|
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
|
||||||
<el-form
|
<el-form
|
||||||
@@ -19,12 +19,7 @@
|
|||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
<el-form-item label="模型类型" prop="modelsType">
|
<el-form-item label="模型类型" prop="modelsType">
|
||||||
<el-select v-model="state.ruleForm.modelsType" placeholder="请选择模型类型" clearable style="width: 100%">
|
<el-select v-model="state.ruleForm.modelsType" placeholder="请选择模型类型" clearable style="width: 100%">
|
||||||
<el-option
|
<el-option v-for="t in modelTypeOptions" :key="String(t.id)" :label="t.label" :value="typeOptionValue(t.id)"></el-option>
|
||||||
v-for="t in modelTypeOptions"
|
|
||||||
:key="String(t.id)"
|
|
||||||
:label="t.label"
|
|
||||||
:value="typeOptionValue(t.id)"
|
|
||||||
></el-option>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -53,13 +48,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-if="state.ruleForm.isPrivate === 1" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
<el-col v-if="state.ruleForm.isPrivate === 1" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||||
<el-form-item label="API 密钥" prop="apiKey">
|
<el-form-item label="API 密钥" prop="apiKey">
|
||||||
<el-input
|
<el-input v-model="state.ruleForm.apiKey" type="password" show-password placeholder="请输入 API 密钥字符串" clearable></el-input>
|
||||||
v-model="state.ruleForm.apiKey"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="请输入 API 密钥字符串"
|
|
||||||
clearable
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
@@ -139,29 +128,21 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
<el-form-item label="自动清理间隔(秒)" prop="autoCleanSeconds">
|
<el-form-item label="自动清理间隔(秒)" prop="autoCleanSeconds">
|
||||||
<el-input-number v-model="state.ruleForm.autoCleanSeconds" :min="0" :max="86400" style="width: 100%"></el-input-number>
|
<el-input-number v-model="state.ruleForm.autoCleanSeconds" :min="0" :max="86400" style="width: 100%"> </el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
<el-form-item label="请求映射" prop="requestMappingJson">
|
<el-form-item label="请求映射" prop="requestMapping">
|
||||||
<el-input
|
<el-button @click="showRequestMappingDialog = true" style="width: 100%">
|
||||||
v-model="state.ruleForm.requestMappingJson"
|
配置请求映射 ({{ state.requestMappingFields.length }})
|
||||||
type="textarea"
|
</el-button>
|
||||||
:rows="4"
|
|
||||||
placeholder='JSON 对象,例如 {}'
|
|
||||||
clearable
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
<el-form-item label="响应映射" prop="responseMappingJson">
|
<el-form-item label="响应映射" prop="responseMapping">
|
||||||
<el-input
|
<el-button @click="showResponseMappingDialog = true" style="width: 100%">
|
||||||
v-model="state.ruleForm.responseMappingJson"
|
配置响应映射 ({{ state.responseMappingFields.length }})
|
||||||
type="textarea"
|
</el-button>
|
||||||
:rows="4"
|
|
||||||
placeholder='JSON 对象,例如 {}'
|
|
||||||
clearable
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -170,13 +151,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="onCancel" size="default">取 消</el-button>
|
<el-button @click="onCancel" size="default">取 消</el-button>
|
||||||
<el-button
|
<el-button type="primary" @click="onSubmit" size="default" :loading="state.dialog.loading" :disabled="state.dialog.detailLoading">
|
||||||
type="primary"
|
|
||||||
@click="onSubmit"
|
|
||||||
size="default"
|
|
||||||
:loading="state.dialog.loading"
|
|
||||||
:disabled="state.dialog.detailLoading"
|
|
||||||
>
|
|
||||||
{{ state.dialog.submitTxt }}
|
{{ state.dialog.submitTxt }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
@@ -219,6 +194,51 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<!-- 请求映射配置弹窗 -->
|
||||||
|
<el-dialog v-model="showRequestMappingDialog" title="配置请求映射" width="600px" :close-on-click-modal="false">
|
||||||
|
<div class="mapping-config-container">
|
||||||
|
<div v-for="(field, index) in state.requestMappingFields" :key="index" class="mapping-field-item">
|
||||||
|
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
|
||||||
|
<span class="separator">=</span>
|
||||||
|
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
|
||||||
|
<el-button type="danger" link @click="removeRequestMappingField(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" link @click="addRequestMappingField" style="margin-top: 10px">+ 添加字段</el-button>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="showRequestMappingDialog = false" size="default">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmRequestMappingFields" size="default">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 响应映射配置弹窗 -->
|
||||||
|
<el-dialog v-model="showResponseMappingDialog" title="配置响应映射" width="700px" :close-on-click-modal="false">
|
||||||
|
<div class="mapping-config-container">
|
||||||
|
<div v-for="(field, index) in state.responseMappingFields" :key="index" class="mapping-field-item">
|
||||||
|
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 30%" clearable></el-input>
|
||||||
|
<span class="separator">=</span>
|
||||||
|
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 30%" clearable></el-input>
|
||||||
|
<el-button
|
||||||
|
:type="field.isMainBody ? 'success' : 'primary'"
|
||||||
|
:plain="!field.isMainBody"
|
||||||
|
@click="setMainBody(index)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link @click="removeResponseMappingField(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" link @click="addResponseMappingField" style="margin-top: 10px">+ 添加字段</el-button>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="showResponseMappingDialog = false" size="default">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmResponseMappingFields" size="default">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -227,12 +247,7 @@ import { reactive, ref, computed } from 'vue';
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
||||||
import {
|
import { addModelModule, updateModelModule, getModelModuleDetail, type ModelFormEntry } from '/@/api/digitalHuman/modelConfig/modelModule/index';
|
||||||
addModelModule,
|
|
||||||
updateModelModule,
|
|
||||||
getModelModuleDetail,
|
|
||||||
type ModelFormEntry,
|
|
||||||
} from '/@/api/digitalHuman/modelConfig/modelModule/index';
|
|
||||||
|
|
||||||
export type ModelTypeOption = { id: number | string; label: string };
|
export type ModelTypeOption = { id: number | string; label: string };
|
||||||
|
|
||||||
@@ -254,6 +269,8 @@ const editModuleFormRef = ref();
|
|||||||
const emit = defineEmits(['refresh']);
|
const emit = defineEmits(['refresh']);
|
||||||
const showHeaderDialog = ref(false);
|
const showHeaderDialog = ref(false);
|
||||||
const showFormDialog = ref(false);
|
const showFormDialog = ref(false);
|
||||||
|
const showRequestMappingDialog = ref(false);
|
||||||
|
const showResponseMappingDialog = ref(false);
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
ruleForm: {
|
ruleForm: {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -274,8 +291,6 @@ const state = reactive({
|
|||||||
retryQueueMaxSeconds: 60,
|
retryQueueMaxSeconds: 60,
|
||||||
autoCleanSeconds: 300,
|
autoCleanSeconds: 300,
|
||||||
remark: '',
|
remark: '',
|
||||||
requestMappingJson: '{}',
|
|
||||||
responseMappingJson: '{}',
|
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
|
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
|
||||||
@@ -293,48 +308,6 @@ const state = reactive({
|
|||||||
],
|
],
|
||||||
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
|
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
|
||||||
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
|
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' }],
|
maxConcurrency: [{ required: true, message: '请输入最大并发数', trigger: 'blur' }],
|
||||||
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
|
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
|
||||||
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
|
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
|
||||||
@@ -351,8 +324,22 @@ const state = reactive({
|
|||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
headers: [] as Array<{ key: string; value: string }>,
|
headers: [] as Array<{ key: string; value: string }>,
|
||||||
formFields: [] 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<string, string> = {};
|
||||||
|
fields.forEach((f) => {
|
||||||
|
if (f.key && f.key.trim()) {
|
||||||
|
obj[f.key.trim()] = f.value || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg);
|
const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg);
|
||||||
// 解析 form:支持数组 [{ key, value }] 或历史对象 { k: { value } }
|
// 解析 form:支持数组 [{ key, value }] 或历史对象 { k: { value } }
|
||||||
const parseFormFields = (form: unknown) => {
|
const parseFormFields = (form: unknown) => {
|
||||||
@@ -368,15 +355,25 @@ const parseFormFields = (form: unknown) => {
|
|||||||
if (typeof form === 'object') {
|
if (typeof form === 'object') {
|
||||||
const fields: Array<{ key: string; value: string }> = [];
|
const fields: Array<{ key: string; value: string }> = [];
|
||||||
Object.keys(form as Record<string, unknown>).forEach((key) => {
|
Object.keys(form as Record<string, unknown>).forEach((key) => {
|
||||||
const v = (form as Record<string, { value?: string }>)[key];
|
const v = (form as Record<string, unknown>)[key];
|
||||||
if (v && typeof v === 'object' && v.value !== undefined) {
|
const value = String(v || '');
|
||||||
fields.push({ key, value: String(v.value) });
|
fields.push({ key, value });
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
return [];
|
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[] => {
|
const buildFormArray = (): ModelFormEntry[] => {
|
||||||
return state.formFields
|
return state.formFields
|
||||||
@@ -454,9 +451,48 @@ const removeFormField = (index: number) => {
|
|||||||
const confirmFormFields = () => {
|
const confirmFormFields = () => {
|
||||||
showFormDialog.value = false;
|
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 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 中取出单条模型对象 */
|
/** 从 getModel 返回的 data 中取出单条模型对象 */
|
||||||
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
|
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
|
||||||
if (data == null) return null;
|
if (data == null) return null;
|
||||||
@@ -487,10 +523,7 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
|||||||
state.ruleForm = {
|
state.ruleForm = {
|
||||||
id: row.id as string,
|
id: row.id as string,
|
||||||
modelName: String(row.modelName ?? ''),
|
modelName: String(row.modelName ?? ''),
|
||||||
modelsType:
|
modelsType: row.modelsType !== undefined && row.modelsType !== null ? typeOptionValue(row.modelsType as number | string) : null,
|
||||||
row.modelsType !== undefined && row.modelsType !== null
|
|
||||||
? typeOptionValue(row.modelsType as number | string)
|
|
||||||
: null,
|
|
||||||
baseUrl: String(row.baseUrl ?? ''),
|
baseUrl: String(row.baseUrl ?? ''),
|
||||||
httpMethod: String(row.httpMethod || 'POST'),
|
httpMethod: String(row.httpMethod || 'POST'),
|
||||||
headMsg: String(row.headMsg || ''),
|
headMsg: String(row.headMsg || ''),
|
||||||
@@ -506,21 +539,25 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
|||||||
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
||||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||||
remark: String(row.remark || ''),
|
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.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
||||||
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
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 详情)
|
// 打开弹窗(编辑时会请求 /model/getModel 详情)
|
||||||
const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||||
state.dialog.type = type;
|
state.dialog.type = type;
|
||||||
@@ -553,7 +590,7 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
|||||||
}
|
}
|
||||||
fillFormFromDetailRow(detail as Record<string, unknown>);
|
fillFormFromDetailRow(detail as Record<string, unknown>);
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('获取模型详情失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
state.dialog.isShowDialog = false;
|
state.dialog.isShowDialog = false;
|
||||||
} finally {
|
} finally {
|
||||||
state.dialog.detailLoading = false;
|
state.dialog.detailLoading = false;
|
||||||
@@ -578,11 +615,11 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
|||||||
retryQueueMaxSeconds: 60,
|
retryQueueMaxSeconds: 60,
|
||||||
autoCleanSeconds: 300,
|
autoCleanSeconds: 300,
|
||||||
remark: '',
|
remark: '',
|
||||||
requestMappingJson: '{}',
|
|
||||||
responseMappingJson: '{}',
|
|
||||||
};
|
};
|
||||||
state.headers = [{ key: '', value: '' }];
|
state.headers = [{ key: '', value: '' }];
|
||||||
state.formFields = [{ key: '', value: '' }];
|
state.formFields = [{ key: '', value: '' }];
|
||||||
|
state.requestMappingFields = [{ key: '', value: '' }];
|
||||||
|
state.responseMappingFields = [{ key: '', value: '', isMainBody: false }];
|
||||||
state.dialog.title = '新增模型配置';
|
state.dialog.title = '新增模型配置';
|
||||||
state.dialog.submitTxt = '新 增';
|
state.dialog.submitTxt = '新 增';
|
||||||
}
|
}
|
||||||
@@ -608,8 +645,11 @@ const onSubmit = () => {
|
|||||||
state.dialog.loading = true;
|
state.dialog.loading = true;
|
||||||
try {
|
try {
|
||||||
state.ruleForm.headMsg = stringifyHeaders();
|
state.ruleForm.headMsg = stringifyHeaders();
|
||||||
const requestMapping = parseJsonObjectField(state.ruleForm.requestMappingJson, {});
|
const requestMapping = fieldsToObject(state.requestMappingFields);
|
||||||
const responseMapping = parseJsonObjectField(state.ruleForm.responseMappingJson, {});
|
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 = {
|
const submitData = {
|
||||||
modelName: state.ruleForm.modelName,
|
modelName: state.ruleForm.modelName,
|
||||||
modelsType: state.ruleForm.modelsType as number | string,
|
modelsType: state.ruleForm.modelsType as number | string,
|
||||||
@@ -620,9 +660,10 @@ const onSubmit = () => {
|
|||||||
enabled: state.ruleForm.enabled,
|
enabled: state.ruleForm.enabled,
|
||||||
isChatModel: state.ruleForm.isChatModel,
|
isChatModel: state.ruleForm.isChatModel,
|
||||||
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
|
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
|
||||||
form: buildFormArray(),
|
form: fieldsToObject(state.formFields),
|
||||||
requestMapping,
|
requestMapping,
|
||||||
responseMapping,
|
responseMapping,
|
||||||
|
responseBody,
|
||||||
maxConcurrency: state.ruleForm.maxConcurrency,
|
maxConcurrency: state.ruleForm.maxConcurrency,
|
||||||
queueLimit: state.ruleForm.queueLimit,
|
queueLimit: state.ruleForm.queueLimit,
|
||||||
timeoutSeconds: state.ruleForm.timeoutSeconds,
|
timeoutSeconds: state.ruleForm.timeoutSeconds,
|
||||||
@@ -642,8 +683,8 @@ const onSubmit = () => {
|
|||||||
}
|
}
|
||||||
closeDialog();
|
closeDialog();
|
||||||
emit('refresh');
|
emit('refresh');
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('保存失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
state.dialog.loading = false;
|
state.dialog.loading = false;
|
||||||
}
|
}
|
||||||
@@ -657,6 +698,19 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.mapping-config-container {
|
||||||
|
.mapping-field-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.form-config-container {
|
.form-config-container {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -689,3 +743,50 @@ defineExpose({
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,25 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="httpMethod" label="请求方式" width="100"></el-table-column>
|
<el-table-column prop="httpMethod" label="请求方式" width="100"></el-table-column>
|
||||||
|
<el-table-column label="会话模型" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="Number(row.isChatModel) === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ Number(row.isChatModel) === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="会话开关" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="Number(row.isChatModel) === 1">
|
||||||
|
<el-switch
|
||||||
|
size="small"
|
||||||
|
:model-value="chatSessionSwitchOn(row)"
|
||||||
|
:before-change="() => onChatSessionSwitchRequest(row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-muted">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="enabled" label="状态" width="100">
|
<el-table-column prop="enabled" label="状态" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.enabled === 1 ? 'success' : 'danger'">{{ scope.row.enabled === 1 ? '启用' : '禁用' }}</el-tag>
|
<el-tag :type="scope.row.enabled === 1 ? 'success' : 'danger'">{{ scope.row.enabled === 1 ? '启用' : '禁用' }}</el-tag>
|
||||||
@@ -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<boolean>((resolve) => {
|
||||||
|
ElMessage.info('会话开关接口接入后即可生效');
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resolveModelTypeLabel = (modelsType: number | string | undefined | null) => {
|
const resolveModelTypeLabel = (modelsType: number | string | undefined | null) => {
|
||||||
if (modelsType === undefined || modelsType === null || modelsType === '') {
|
if (modelsType === undefined || modelsType === null || modelsType === '') {
|
||||||
return '—';
|
return '—';
|
||||||
@@ -108,8 +137,8 @@ const loadModelTypes = async () => {
|
|||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
state.modelTypes = normalizeModelTypeOptions(res);
|
state.modelTypes = normalizeModelTypeOptions(res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
ElMessage.error('获取模型类型失败:');
|
// 接口错误由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,8 +151,8 @@ const getTableData = async () => {
|
|||||||
state.tableData.data = res.data.list || [];
|
state.tableData.data = res.data.list || [];
|
||||||
state.tableData.total = res.data.total || 0;
|
state.tableData.total = res.data.total || 0;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('获取模型列表失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
state.tableData.loading = false;
|
state.tableData.loading = false;
|
||||||
}
|
}
|
||||||
@@ -151,8 +180,8 @@ const onRowDel = (row: any) => {
|
|||||||
await deleteModelModule(row.id);
|
await deleteModelModule(row.id);
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
getTableData();
|
getTableData();
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('删除失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -178,6 +207,10 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.text-muted {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
.system-user-container {
|
.system-user-container {
|
||||||
:deep(.el-card__body) {
|
:deep(.el-card__body) {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ const handleFileChange: UploadProps['onChange'] = async (uploadFile) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ElMessage.info('正在上传文件到 OSS...');
|
ElMessage.info('正在上传文件到 OSS...');
|
||||||
const uploadRes = await uploadFileToOss(uploadFile.raw, { errorMode: 'page' });
|
const uploadRes = await uploadFileToOss(uploadFile.raw);
|
||||||
formData.fileName = uploadRes.data.fileName;
|
formData.fileName = uploadRes.data.fileName;
|
||||||
formData.fileUrl = uploadRes.data.fileURL;
|
formData.fileUrl = uploadRes.data.fileURL;
|
||||||
fileList.value = [uploadFile];
|
fileList.value = [uploadFile];
|
||||||
@@ -180,7 +180,7 @@ const fetchSkillList = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
|
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
|
||||||
const res = await getUserSkillList(params, { errorMode: 'page' });
|
const res = await getUserSkillList(params);
|
||||||
skillList.value = res.data?.list || [];
|
skillList.value = res.data?.list || [];
|
||||||
pagination.total = res.data?.total || 0;
|
pagination.total = res.data?.total || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -222,21 +222,18 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
await createUserSkill(
|
await createUserSkill({
|
||||||
{
|
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
category: formData.category,
|
category: formData.category,
|
||||||
fileName: formData.fileName,
|
fileName: formData.fileName,
|
||||||
fileUrl: formData.fileUrl,
|
fileUrl: formData.fileUrl,
|
||||||
},
|
});
|
||||||
{ errorMode: 'page' }
|
|
||||||
);
|
|
||||||
ElMessage.success('创建成功');
|
ElMessage.success('创建成功');
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
fetchSkillList();
|
fetchSkillList();
|
||||||
} catch (error) {
|
} catch {
|
||||||
// 错误已由 errorMode: 'page' 处理
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
@@ -251,12 +248,12 @@ const handleCommand = async (command: string, skill: SkillItem) => {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
await deleteUserSkill(skill.id, { errorMode: 'page' });
|
await deleteUserSkill(skill.id);
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
fetchSkillList();
|
fetchSkillList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
// 错误已由 errorMode: 'page' 处理
|
// 接口错误由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ const getknowledgeList = async () => {
|
|||||||
});
|
});
|
||||||
knowledgeList.value = response.data.list || [];
|
knowledgeList.value = response.data.list || [];
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
ElMessage.error('获取知识库列表失败');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
knowledgeLoading.value = false;
|
knowledgeLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -648,7 +648,7 @@ const onSaveknowledge = async () => {
|
|||||||
showknowledgeDialog.value = false;
|
showknowledgeDialog.value = false;
|
||||||
getknowledgeList();
|
getknowledgeList();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
ElMessage.error('保存失败,请重试');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
knowledgeSaving.value = false;
|
knowledgeSaving.value = false;
|
||||||
}
|
}
|
||||||
@@ -673,7 +673,7 @@ const getFileList = async () => {
|
|||||||
statusEnabled: item.status === 1,
|
statusEnabled: item.status === 1,
|
||||||
}));
|
}));
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
ElMessage.error('获取文件列表失败');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
fileLoading.value = false;
|
fileLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -745,7 +745,7 @@ const onConfirmUpload = async () => {
|
|||||||
showUploadDialog.value = false;
|
showUploadDialog.value = false;
|
||||||
getFileList();
|
getFileList();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
ElMessage.error('创建文档失败,请重试');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
uploading.value = false;
|
uploading.value = false;
|
||||||
}
|
}
|
||||||
@@ -764,7 +764,7 @@ const onFileStatusChange = async (row: any) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 失败时恢复原状态
|
// 失败时恢复原状态
|
||||||
row.statusEnabled = !row.statusEnabled;
|
row.statusEnabled = !row.statusEnabled;
|
||||||
ElMessage.error('状态更新失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -779,7 +779,7 @@ const onGenerateVector = async (row: any) => {
|
|||||||
getFileList();
|
getFileList();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('生成向量失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -791,7 +791,7 @@ const onViewDocumentDetail = async (row: any) => {
|
|||||||
currentDocument.value = response.data;
|
currentDocument.value = response.data;
|
||||||
showDocumentDetailDialog.value = true;
|
showDocumentDetailDialog.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取文件详情失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -917,7 +917,7 @@ const onEditModelConfig = async (row: any) => {
|
|||||||
// 打开弹窗
|
// 打开弹窗
|
||||||
showCreateModelDialog.value = true;
|
showCreateModelDialog.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取模型配置详情失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -928,7 +928,7 @@ const getModelEnums = async () => {
|
|||||||
const response = await getAllModelEnums();
|
const response = await getAllModelEnums();
|
||||||
modelEnums.value = response.data?.options || [];
|
modelEnums.value = response.data?.options || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取模型类型枚举失败');
|
// 错误已由全局拦截器处理
|
||||||
modelEnums.value = [];
|
modelEnums.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
modelEnumsLoading.value = false;
|
modelEnumsLoading.value = false;
|
||||||
@@ -975,7 +975,7 @@ const getModelFormFields = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取模型表单字段失败');
|
// 错误已由全局拦截器处理
|
||||||
modelFormFields.value = [];
|
modelFormFields.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
modelFormLoading.value = false;
|
modelFormLoading.value = false;
|
||||||
@@ -1016,7 +1016,7 @@ const onSaveModelConfig = async () => {
|
|||||||
showCreateModelDialog.value = false;
|
showCreateModelDialog.value = false;
|
||||||
getModelConfigList();
|
getModelConfigList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(isEditMode.value ? '更新模型配置失败' : '创建模型配置失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1040,7 +1040,7 @@ const getModelConfigList = async () => {
|
|||||||
});
|
});
|
||||||
modelConfigList.value = response.data?.list || [];
|
modelConfigList.value = response.data?.list || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取模型配置列表失败');
|
// 错误已由全局拦截器处理
|
||||||
modelConfigList.value = [];
|
modelConfigList.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
modelConfigLoading.value = false;
|
modelConfigLoading.value = false;
|
||||||
@@ -1128,7 +1128,7 @@ const getTaskList = async () => {
|
|||||||
const response = await listTasks();
|
const response = await listTasks();
|
||||||
taskList.value = response.data?.list || [];
|
taskList.value = response.data?.list || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取任务列表失败');
|
// 错误已由全局拦截器处理
|
||||||
taskList.value = [];
|
taskList.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
taskListLoading.value = false;
|
taskListLoading.value = false;
|
||||||
@@ -1149,7 +1149,7 @@ const onReexecuteTask = async (task: any) => {
|
|||||||
// 重新获取任务列表
|
// 重新获取任务列表
|
||||||
await getTaskList();
|
await getTaskList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('重新执行任务失败,请重试');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const openDialog = async (row?: DialogFormData) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取主播详情失败');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ const onSubmit = async () => {
|
|||||||
closeDialog();
|
closeDialog();
|
||||||
emit('refresh');
|
emit('refresh');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('操作失败');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ const getList = async () => {
|
|||||||
tableData.total = res.data.total || 0;
|
tableData.total = res.data.total || 0;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('获取主播列表失败');
|
// 错误已由全局拦截器处理
|
||||||
} finally {
|
} finally {
|
||||||
tableData.loading = false;
|
tableData.loading = false;
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ const handleDelete = async (row: TableDataItem) => {
|
|||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
ElMessage.error('删除失败');
|
// 错误已由全局拦截器处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,14 +101,13 @@ const openDialog = async (row?: { id?: string }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
// 详情加载失败时由当前弹窗给出更易懂的业务提示。
|
const res = await getLiveAccountDetail({ id: String(row.id) });
|
||||||
const res = await getLiveAccountDetail({ id: String(row.id) }, { errorMode: 'page' });
|
|
||||||
if (res?.data) {
|
if (res?.data) {
|
||||||
fillForm(res.data);
|
fillForm(res.data);
|
||||||
}
|
}
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('获取直播账号详情失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -130,19 +129,18 @@ const handleSubmit = async () => {
|
|||||||
remark: formData.remark,
|
remark: formData.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交失败提示交给当前弹窗自己处理,避免和 request.ts 的统一报错重复。
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateLiveAccount(payload, { errorMode: 'page' });
|
await updateLiveAccount(payload);
|
||||||
ElMessage.success('修改成功');
|
ElMessage.success('修改成功');
|
||||||
} else {
|
} else {
|
||||||
await createLiveAccount(payload, { errorMode: 'page' });
|
await createLiveAccount(payload);
|
||||||
ElMessage.success('新增成功');
|
ElMessage.success('新增成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
emit('refresh');
|
emit('refresh');
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error(isEdit.value ? '修改失败' : '新增失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,17 +131,13 @@ const tableData = reactive({
|
|||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
try {
|
try {
|
||||||
tableData.loading = true;
|
tableData.loading = true;
|
||||||
// 列表失败文案由当前页面决定,避免和全局请求报错同时出现。
|
const res = await getLiveAccountList({
|
||||||
const res = await getLiveAccountList(
|
|
||||||
{
|
|
||||||
...tableData.param,
|
...tableData.param,
|
||||||
platform: searchForm.platform || undefined,
|
platform: searchForm.platform || undefined,
|
||||||
accountName: searchForm.accountName || undefined,
|
accountName: searchForm.accountName || undefined,
|
||||||
accountId: searchForm.accountId || undefined,
|
accountId: searchForm.accountId || undefined,
|
||||||
status: searchForm.status,
|
status: searchForm.status,
|
||||||
},
|
});
|
||||||
{ errorMode: 'page' }
|
|
||||||
);
|
|
||||||
if (res && res.data) {
|
if (res && res.data) {
|
||||||
tableData.data = (res.data.list || []).map((item: any) => ({
|
tableData.data = (res.data.list || []).map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -149,8 +145,8 @@ const getList = async () => {
|
|||||||
}));
|
}));
|
||||||
tableData.total = res.data.total || 0;
|
tableData.total = res.data.total || 0;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('获取直播账号列表失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
tableData.loading = false;
|
tableData.loading = false;
|
||||||
}
|
}
|
||||||
@@ -195,12 +191,12 @@ const handleDelete = async (row: LiveAccountItem) => {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
await deleteLiveAccount({ id: row.id }, { errorMode: 'page' });
|
await deleteLiveAccount({ id: row.id });
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
ElMessage.error('删除失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,8 +153,7 @@ const openDialog = async (row?: { id?: string }) => {
|
|||||||
await loadOptions();
|
await loadOptions();
|
||||||
|
|
||||||
if (row?.id) {
|
if (row?.id) {
|
||||||
// 详情请求失败时,这个弹窗希望给出更明确的页面语义提示。
|
const res = await getScheduleDetail({ id: String(row.id) });
|
||||||
const res = await getScheduleDetail({ id: String(row.id) }, { errorMode: 'page' });
|
|
||||||
const detail = res?.data;
|
const detail = res?.data;
|
||||||
if (detail) {
|
if (detail) {
|
||||||
formData.id = String(detail.id);
|
formData.id = String(detail.id);
|
||||||
@@ -170,8 +169,8 @@ const openDialog = async (row?: { id?: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error(isEdit.value ? '获取排班详情失败' : '加载排班基础数据失败');
|
// 接口错误由 request 全局提示后端 message;表单校验错误由表单项展示
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -196,19 +195,18 @@ const handleSubmit = async () => {
|
|||||||
remark: formData.remark,
|
remark: formData.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交失败文案由弹窗自己控制,避免接口层和弹窗层重复报错。
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateSchedule(payload, { errorMode: 'page' });
|
await updateSchedule(payload);
|
||||||
ElMessage.success('修改排班成功');
|
ElMessage.success('修改排班成功');
|
||||||
} else {
|
} else {
|
||||||
await createSchedule(payload, { errorMode: 'page' });
|
await createSchedule(payload);
|
||||||
ElMessage.success('新增排班成功');
|
ElMessage.success('新增排班成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
emit('refresh');
|
emit('refresh');
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error(isEdit.value ? '修改排班失败' : '新增排班失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,16 +144,12 @@ const getStatusTagType = (status: number): 'success' | 'info' | 'warning' => {
|
|||||||
const getList = async () => {
|
const getList = async () => {
|
||||||
try {
|
try {
|
||||||
tableData.loading = true;
|
tableData.loading = true;
|
||||||
// 列表失败文案由当前页面决定,避免和 request.ts 的全局错误提示重复。
|
const res = await getScheduleList({
|
||||||
const res = await getScheduleList(
|
|
||||||
{
|
|
||||||
...tableData.param,
|
...tableData.param,
|
||||||
anchorName: searchForm.anchorName || undefined,
|
anchorName: searchForm.anchorName || undefined,
|
||||||
accountName: searchForm.accountName || undefined,
|
accountName: searchForm.accountName || undefined,
|
||||||
status: searchForm.status,
|
status: searchForm.status,
|
||||||
} as any,
|
} as any);
|
||||||
{ errorMode: 'page' }
|
|
||||||
);
|
|
||||||
const scheduleData = res?.data;
|
const scheduleData = res?.data;
|
||||||
if (scheduleData) {
|
if (scheduleData) {
|
||||||
tableData.data = (scheduleData.list || []).map((item: any) => ({
|
tableData.data = (scheduleData.list || []).map((item: any) => ({
|
||||||
@@ -166,8 +162,8 @@ const getList = async () => {
|
|||||||
}));
|
}));
|
||||||
tableData.total = scheduleData.total || 0;
|
tableData.total = scheduleData.total || 0;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
ElMessage.error('获取排班列表失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
} finally {
|
} finally {
|
||||||
tableData.loading = false;
|
tableData.loading = false;
|
||||||
}
|
}
|
||||||
@@ -211,12 +207,12 @@ const handleDelete = async (row: ScheduleItem) => {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
await deleteSchedule({ id: row.id }, { errorMode: 'page' });
|
await deleteSchedule({ id: row.id });
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
getList();
|
getList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
ElMessage.error('删除失败');
|
// 接口错误由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user