添加会话模型和API Key配置功能

- 在模型模块中新增会话开关状态字段,支持会话模型的管理。
- 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。
- 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。
- 更新相关样式以增强界面可读性和美观性。
This commit is contained in:
2026-05-11 20:01:03 +08:00
parent 0a42e700e2
commit 29838b030f
19 changed files with 1296 additions and 274 deletions

View File

@@ -79,6 +79,8 @@ export interface ModelModuleItem {
apiKey?: string;
isPrivate?: number;
isChatModel?: number;
/** 会话开关状态列表接口返回0 关 1 开;会话开关接口就绪后生效) */
chatSessionEnabled?: number;
enabled: number;
maxConcurrency: number;
queueLimit: number;
@@ -210,3 +212,8 @@ export function getModelModuleDetail(id: number | string) {
params: { id },
});
}
// TODO: 列表「会话开关」提交接口确定后在此封装,例如:
// export function updateModelChatSessionSwitch(data: { id: number | string; chatSessionEnabled: 0 | 1 }) {
// return request({ url: '/model-gateway/model/...', method: 'post', data });
// }

View File

@@ -19,12 +19,15 @@
v-for="model in modelList"
:key="model.id"
class="model-card"
:class="{ selected: selectedModel?.id === model.id }"
:class="{ selected: selectedModel?.id === model.id, 'system-model': model.tenantId === 1 }"
@click="handleSelectModel(model)"
>
<div class="model-card-header">
<div class="model-type">{{ getModelTypeName(model.modelsType) }}</div>
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a"><CircleCheck /></el-icon>
<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>
</div>
</div>
<div class="model-card-body">
<h3 class="model-name">{{ model.modelName }}</h3>
@@ -58,23 +61,65 @@
<!-- 新建模型弹窗 -->
<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>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
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';
interface ModelItem {
id: string;
tenantId?: number;
modelName: string;
modelsType: number;
baseUrl: string;
route: string;
httpMethod: string;
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;
}
@@ -103,6 +148,20 @@ const loading = ref(false);
const selectedModel = ref<ModelItem | null>(null);
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(
() => props.modelValue,
(val) => {
@@ -122,9 +181,9 @@ watch(visible, (val) => {
const getModelTypeName = (type: number) => {
const typeMap: Record<number, string> = {
1: '图片模型',
2: '语音模型',
3: '推理模型',
1: '推理模型',
2: '图片模型',
3: '音频模型',
};
return typeMap[type] || '未知类型';
};
@@ -137,10 +196,11 @@ const fetchModelList = async () => {
pageSize: pagination.pageSize,
modelName: searchParams.modelName || undefined,
};
const res = await getModelModuleList(params, { errorMode: 'message' });
const res: any = await getModelModuleList(params);
modelList.value = res.data?.list || [];
pagination.total = res.data?.total || 0;
} catch (error) {
} catch {
// 接口错误由 request 全局提示后端 message
modelList.value = [];
pagination.total = 0;
} finally {
@@ -158,7 +218,77 @@ const handlePageChange = () => {
};
const handleSelectModel = (model: ModelItem) => {
selectedModel.value = model;
// 判断是否是系统模型tenantId === 1
if (model.tenantId === 1) {
// 系统模型,需要用户配置 API Key
systemModelToClone.value = model;
apiKeyForm.modelName = `${model.modelName} - 副本`;
apiKeyForm.apiKey = '';
apiKeyDialogVisible.value = true;
} else {
// 非系统模型,直接选中
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 = () => {
@@ -179,6 +309,8 @@ const handleConfirm = () => {
const handleClose = () => {
visible.value = false;
selectedModel.value = null;
apiKeyDialogVisible.value = false;
systemModelToClone.value = null;
};
</script>
@@ -228,6 +360,15 @@ const handleClose = () => {
background: #f0f9ff;
}
.model-card.system-model {
border-color: #fbbf24;
background: #fffbeb;
}
.model-card.system-model:hover {
border-color: #f59e0b;
}
.model-card-header {
display: flex;
justify-content: space-between;
@@ -245,6 +386,12 @@ const handleClose = () => {
font-weight: 600;
}
.model-badges {
display: flex;
align-items: center;
gap: 8px;
}
.check-icon {
font-size: 20px;
}

View File

@@ -152,9 +152,7 @@ const fetchSkillList = async () => {
try {
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
const res =
activeTab.value === 'system'
? await getSkillList(params, { errorMode: 'message' })
: await getUserSkilllistUser(params, { errorMode: 'message' });
activeTab.value === 'system' ? await getSkillList(params) : await getUserSkilllistUser(params);
skillList.value = res.data?.list || [];
pagination.total = res.data?.total || 0;
} catch (error) {

View File

@@ -97,7 +97,7 @@ const fetchSkillList = async () => {
loading.value = true;
try {
const params = { pageNum: pagination.pageNum, pageSize: pagination.pageSize, keyword: searchParams.keyword || undefined };
const res = await getUserSkilllistUser(params, { errorMode: 'message' });
const res = await getUserSkilllistUser(params);
skillList.value = res.data?.list || [];
pagination.total = res.data?.total || 0;
} catch (error) {

View File

@@ -7,10 +7,10 @@ import { getChangedFields } from '/@/utils/diffUtils';
import { handleModuleNotEnabled } from '/@/utils/assetSubscribe';
/**
* 控制一次请求的错误提示归属:
* - global: 交给 request.ts 统一弹错,适合绝大多数接口
* - page: 页面自己在 catch 中决定提示文案,避免与全局重复
* - silent: 完全静默,适合轮询、后台刷新等不希望打扰用户的请求
* 控制一次请求的错误提示归属(默认 global
* - global: 由拦截器统一弹出后端返回的 message含 HTTP 与业务 JSON
* - page: 不自动弹窗,仅 reject请在页面 catch 内自行处理(应与全局择一,避免重复
* - silent: 完全静默(轮询等)
*/
export interface RequestOptions {
errorMode?: 'global' | 'page' | 'silent';
@@ -39,6 +39,26 @@ const ERROR_MESSAGE_INTERVAL = 2000;
const getErrorMode = (config?: InternalAxiosRequestConfig) => config?.requestOptions?.errorMode ?? 'global';
const shouldShowGlobalError = (config?: InternalAxiosRequestConfig) => getErrorMode(config) === 'global';
/**
* 从接口响应体解析可读错误文案JSON API、Spring 风格等)
*/
export function extractBackendMessage(data: unknown): string | undefined {
if (data == null) return undefined;
if (typeof data === 'string') {
const t = data.trim();
return t.length > 0 && t.length < 2000 ? t : undefined;
}
if (typeof data !== 'object') return undefined;
const o = data as Record<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 = () => {
activeErrorMessage?.close();
activeErrorMessage = null;
@@ -174,7 +194,7 @@ const responseInterceptor = (response: AxiosResponse) => {
const res = response.data;
const httpStatus = response.status;
const code = res?.code;
const message = res?.message;
const message = extractBackendMessage(res);
const config = response.config;
if (isTokenExpiredError(httpStatus, code, message)) {
@@ -207,8 +227,8 @@ const responseInterceptor = (response: AxiosResponse) => {
if (knownErrorCodes.includes(code)) {
errorMsg = message || `请求失败(${code})`;
} else {
// 未知的 code,统一提示后端异常
errorMsg = '后端异常,请联系管理员';
// 未知的 code:优先使用后端 message便于排查业务含义
errorMsg = message || '后端异常,请联系管理员';
}
showErrorMessage(errorMsg, config);
@@ -221,7 +241,8 @@ const responseInterceptor = (response: AxiosResponse) => {
const responseErrorHandler = (error: any) => {
const config = error.config as InternalAxiosRequestConfig | undefined;
const httpStatus = error.response?.status;
const responseMessage = error.response?.data?.message;
const responseData = error.response?.data;
const responseMessage = extractBackendMessage(responseData);
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
showErrorMessage('请求超时,请检查网络连接', config);
@@ -232,7 +253,7 @@ const responseErrorHandler = (error: any) => {
return Promise.reject(error);
}
if (isTokenExpiredError(httpStatus, error.response?.data?.code, responseMessage)) {
if (isTokenExpiredError(httpStatus, error.response?.data?.code as number | undefined, responseMessage)) {
handleTokenExpired();
return Promise.reject(new Error('登录状态已过期'));
}
@@ -245,7 +266,7 @@ const responseErrorHandler = (error: any) => {
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
const now = Date.now();
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面', config);
showErrorMessage(responseMessage ?? '服务开通中,请稍后刷新页面', config);
return Promise.reject(new Error('模块开通中'));
}
@@ -253,30 +274,30 @@ const responseErrorHandler = (error: any) => {
handleModuleNotEnabled(currentPath);
return Promise.reject(new Error('模块未开通'));
}
showErrorMessage(responseMessage || '服务未开通', config);
showErrorMessage(responseMessage ?? '服务未开通', config);
break;
case 403:
showErrorMessage(responseMessage || '没有权限访问该资源', config);
showErrorMessage(responseMessage ?? '没有权限访问该资源', config);
break;
case 404:
showErrorMessage(responseMessage || '请求的资源不存在', config);
showErrorMessage(responseMessage ?? '请求的资源不存在', config);
break;
case 429:
// 429 是限流,不等于登录过期,这里只保留频率提示。
showErrorMessage(responseMessage || '请求过于频繁,请稍后再试', config);
showErrorMessage(responseMessage ?? '请求过于频繁,请稍后再试', config);
break;
case 500:
showErrorMessage(responseMessage || '服务器内部错误', config);
showErrorMessage(responseMessage ?? '服务器内部错误', config);
break;
case 502:
showErrorMessage(responseMessage || '网关错误', config);
showErrorMessage(responseMessage ?? '网关错误', config);
break;
case 503:
showErrorMessage(responseMessage || '服务不可用', config);
showErrorMessage(responseMessage ?? '服务不可用', config);
break;
default:
if (httpStatus >= 400) {
showErrorMessage(responseMessage || `请求失败(${httpStatus})`, config);
showErrorMessage(responseMessage ?? `请求失败(${httpStatus})`, config);
}
}
@@ -286,5 +307,27 @@ const responseErrorHandler = (error: any) => {
service.interceptors.request.use(requestInterceptor, requestErrorHandler);
service.interceptors.response.use(responseInterceptor, responseErrorHandler);
/**
* 从 axios / 业务 reject 中取出后端返回的提示文案与全局拦截器同源逻辑silent / 特殊场景下可在页面使用)。
*/
export function getApiErrorMessage(error: unknown, fallback = '操作失败'): string {
const e = error as any;
const fromBody = extractBackendMessage(e?.response?.data);
if (fromBody != null && fromBody !== '') {
return fromBody;
}
const msg = e?.message;
if (typeof msg === 'string' && msg.trim() !== '') {
if (/^Request failed with status code \d+$/i.test(msg)) {
return extractBackendMessage(e?.response?.data) ?? fallback;
}
if (msg === 'Network Error') {
return '网络异常,请检查网络连接';
}
return msg;
}
return fallback;
}
export default service;
export { closeActiveErrorMessage, showErrorMessage };

View File

@@ -138,7 +138,7 @@ const openDialog = async (row?: DialogFormData) => {
}
} catch (error) {
console.error('获取账号详情失败:', error);
ElMessage.error('获取账号详情失败');
// 错误已由全局拦截器处理
} finally {
state.loading = false;
}

View File

@@ -98,7 +98,7 @@ const loadDatasets = async () => {
}));
}
} catch (error) {
ElMessage.error('加载数据集列表失败');
// 错误已由全局拦截器处理
}
};

View File

@@ -790,10 +790,11 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
const getList = async () => {
treeLoading.value = true;
try {
const res = await getExecutionList({ errorMode: 'page' });
const res = await getExecutionList();
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
treeNodes.value = buildTreeNodes(res.data?.tree || []);
} catch {
// 错误已由全局拦截器处理
treeNodes.value = [];
imgAddressPrefix.value = '';
} finally {
@@ -802,9 +803,10 @@ const getList = async () => {
};
const getNodeLibrary = async () => {
try {
const res = await getNodeLibraryList({ errorMode: 'page' });
const res = await getNodeLibraryList();
nodeLibraryGroups.value = res.data?.groups || [];
} catch {
// 错误已由全局拦截器处理
nodeLibraryGroups.value = [];
}
};
@@ -812,7 +814,7 @@ const getNodeLibrary = async () => {
const fetchWorkflowList = async () => {
workflowListLoading.value = true;
try {
const res = await getWorkflowList({ errorMode: 'page' });
const res = await getWorkflowList();
// 分别处理用户工作流和模板工作流
const userWorkflows = res.data?.listFlowUserRes?.list || [];
@@ -833,6 +835,7 @@ const fetchWorkflowList = async () => {
const templateEnd = templateStart + templateWorkflowPagination.pageSize;
templateWorkflowList.value = templateWorkflows.slice(templateStart, templateEnd);
} catch {
// 错误已由全局拦截器处理
userWorkflowList.value = [];
templateWorkflowList.value = [];
userWorkflowPagination.total = 0;
@@ -892,7 +895,7 @@ const handleRemoveModel = () => {
const useWorkflow = async (workflow: WorkflowItem) => {
try {
// 调用详情接口获取最新的工作流数据
const res = await getWorkflowDetail(workflow.id, { errorMode: 'page' });
const res = await getWorkflowDetail(workflow.id);
if (res.data) {
// 切换到创作模式
isCreationMode.value = true;
@@ -946,7 +949,7 @@ const useWorkflow = async (workflow: WorkflowItem) => {
const editWorkflow = async (workflow: WorkflowItem) => {
try {
// 调用详情接口获取最新的工作流数据
const res = await getWorkflowDetail(workflow.id, { errorMode: 'page' });
const res = await getWorkflowDetail(workflow.id);
if (res.data?.flowContent) {
// 切换回画布编辑模式
isCreationMode.value = false;
@@ -994,7 +997,7 @@ const deleteWorkflowAction = async (workflow: WorkflowItem) => {
type: 'warning',
});
await deleteWorkflow(workflow.id, { errorMode: 'page' });
await deleteWorkflow(workflow.id);
ElMessage.success('工作流删除成功');
// 如果删除的是当前正在编辑的工作流,清空编辑状态
@@ -1038,15 +1041,10 @@ const sendMessage = async () => {
const fileUrls: string[] = [];
if (selectedFiles.value.length > 0) {
for (const file of selectedFiles.value) {
try {
const uploadRes = await uploadFile(file, { errorMode: 'page' });
// 拼接完整的文件地址
const fullUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
fileUrls.push(fullUrl);
} catch (error) {
ElMessage.error(`文件 ${file.name} 上传失败`);
throw error;
}
const uploadRes = await uploadFile(file);
// 拼接完整的文件地址
const fullUrl = uploadRes.data.fileAddressPrefix ? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}` : uploadRes.data.fileURL;
fileUrls.push(fullUrl);
}
}
@@ -1109,7 +1107,7 @@ const sendMessage = async () => {
};
// 5. 调用执行接口(不再使用 FormData直接传 JSON
await executeFlow(params, { errorMode: 'page' });
await executeFlow(params);
ElMessage.success('创作完成!');
@@ -1117,8 +1115,8 @@ const sendMessage = async () => {
userInput.value = '';
selectedFiles.value = [];
selectedCreationSkill.value = null;
} catch (error) {
ElMessage.error('创作失败,请重试');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
isCreating.value = false;
}
@@ -1149,7 +1147,7 @@ const downloadNode = async (d: TreeNode) => {
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
try {
// 下载失败时希望展示更贴近页面语义的提示,因此改为 page 模式。
const r = await downloadToFile({ fileURL: d.fileUrl }, { errorMode: 'page' });
const r = await downloadToFile({ fileURL: d.fileUrl });
const blob = r instanceof Blob ? r : r?.data;
if (!(blob instanceof Blob)) throw new Error('invalid blob');
const name = decodeURIComponent(d.fileUrl.split('/').pop() || `${d.label}.${d.nodeType === 'html' ? 'html' : 'png'}`);
@@ -1163,7 +1161,7 @@ const downloadNode = async (d: TreeNode) => {
URL.revokeObjectURL(u);
ElMessage.success('下载成功');
} catch {
// 下载接口使用 errorMode: 'page',后端错误会自动显示
// 下载失败由 request 全局提示后端 message
}
};
const syncDsl = () => {
@@ -1847,26 +1845,20 @@ const confirmSaveWorkflow = async () => {
// 判断是新建还是更新
if (currentEditingWorkflowId.value) {
// 更新现有工作流
await updateWorkflow(
{
id: currentEditingWorkflowId.value,
flowName: saveForm.flowName,
description: saveForm.description,
flowContent: workflowDsl.value,
},
{ errorMode: 'page' }
);
await updateWorkflow({
id: currentEditingWorkflowId.value,
flowName: saveForm.flowName,
description: saveForm.description,
flowContent: workflowDsl.value,
});
ElMessage.success('工作流更新成功');
} else {
// 创建新工作流
await saveWorkflow(
{
flowName: saveForm.flowName,
description: saveForm.description,
flowContent: workflowDsl.value,
},
{ errorMode: 'page' }
);
await saveWorkflow({
flowName: saveForm.flowName,
description: saveForm.description,
flowContent: workflowDsl.value,
});
ElMessage.success('工作流保存成功');
}
saveDialogVisible.value = false;

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="system-edit-module-container">
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
<el-form
@@ -19,12 +19,7 @@
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="模型类型" prop="modelsType">
<el-select v-model="state.ruleForm.modelsType" placeholder="请选择模型类型" clearable style="width: 100%">
<el-option
v-for="t in modelTypeOptions"
:key="String(t.id)"
:label="t.label"
:value="typeOptionValue(t.id)"
></el-option>
<el-option v-for="t in modelTypeOptions" :key="String(t.id)" :label="t.label" :value="typeOptionValue(t.id)"></el-option>
</el-select>
</el-form-item>
</el-col>
@@ -53,13 +48,7 @@
</el-col>
<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-input
v-model="state.ruleForm.apiKey"
type="password"
show-password
placeholder="请输入 API 密钥字符串"
clearable
></el-input>
<el-input v-model="state.ruleForm.apiKey" type="password" show-password placeholder="请输入 API 密钥字符串" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -139,29 +128,21 @@
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<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-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="请求映射" prop="requestMappingJson">
<el-input
v-model="state.ruleForm.requestMappingJson"
type="textarea"
:rows="4"
placeholder='JSON 对象,例如 {}'
clearable
></el-input>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求映射" prop="requestMapping">
<el-button @click="showRequestMappingDialog = true" style="width: 100%">
配置请求映射 ({{ state.requestMappingFields.length }})
</el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="响应映射" prop="responseMappingJson">
<el-input
v-model="state.ruleForm.responseMappingJson"
type="textarea"
:rows="4"
placeholder='JSON 对象,例如 {}'
clearable
></el-input>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="响应映射" prop="responseMapping">
<el-button @click="showResponseMappingDialog = true" style="width: 100%">
配置响应映射 ({{ state.responseMappingFields.length }})
</el-button>
</el-form-item>
</el-col>
</el-row>
@@ -170,13 +151,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button
type="primary"
@click="onSubmit"
size="default"
:loading="state.dialog.loading"
:disabled="state.dialog.detailLoading"
>
<el-button type="primary" @click="onSubmit" size="default" :loading="state.dialog.loading" :disabled="state.dialog.detailLoading">
{{ state.dialog.submitTxt }}
</el-button>
</span>
@@ -219,6 +194,51 @@
</span>
</template>
</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>
</template>
@@ -227,12 +247,7 @@ import { reactive, ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
import {
addModelModule,
updateModelModule,
getModelModuleDetail,
type ModelFormEntry,
} from '/@/api/digitalHuman/modelConfig/modelModule/index';
import { addModelModule, updateModelModule, getModelModuleDetail, type ModelFormEntry } from '/@/api/digitalHuman/modelConfig/modelModule/index';
export type ModelTypeOption = { id: number | string; label: string };
@@ -254,6 +269,8 @@ const editModuleFormRef = ref();
const emit = defineEmits(['refresh']);
const showHeaderDialog = ref(false);
const showFormDialog = ref(false);
const showRequestMappingDialog = ref(false);
const showResponseMappingDialog = ref(false);
const state = reactive({
ruleForm: {
id: '',
@@ -274,8 +291,6 @@ const state = reactive({
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
requestMappingJson: '{}',
responseMappingJson: '{}',
},
rules: {
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
@@ -293,48 +308,6 @@ const state = reactive({
],
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
requestMappingJson: [
{
validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请输入请求映射 JSON'));
return;
}
try {
const o = JSON.parse(value);
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
callback(new Error('请求映射须为 JSON 对象'));
return;
}
callback();
} catch {
callback(new Error('请求映射 JSON 格式无效'));
}
},
trigger: 'blur',
},
],
responseMappingJson: [
{
validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请输入响应映射 JSON'));
return;
}
try {
const o = JSON.parse(value);
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
callback(new Error('响应映射须为 JSON 对象'));
return;
}
callback();
} catch {
callback(new Error('响应映射 JSON 格式无效'));
}
},
trigger: 'blur',
},
],
maxConcurrency: [{ required: true, message: '请输入最大并发数', trigger: 'blur' }],
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
@@ -351,8 +324,22 @@ const state = reactive({
showAdvanced: false,
headers: [] as Array<{ key: string; value: string }>,
formFields: [] as Array<{ key: string; value: string }>,
requestMappingFields: [] as Array<{ key: string; value: string }>,
responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean }>,
mainBodyIndex: -1, // 记录哪一行被设置为返回主体
});
// 将数组转换为对象
const fieldsToObject = (fields: Array<{ key: string; value: string }>) => {
const obj: Record<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);
// 解析 form支持数组 [{ key, value }] 或历史对象 { k: { value } }
const parseFormFields = (form: unknown) => {
@@ -368,15 +355,25 @@ const parseFormFields = (form: unknown) => {
if (typeof form === 'object') {
const fields: Array<{ key: string; value: string }> = [];
Object.keys(form as Record<string, unknown>).forEach((key) => {
const v = (form as Record<string, { value?: string }>)[key];
if (v && typeof v === 'object' && v.value !== undefined) {
fields.push({ key, value: String(v.value) });
}
const v = (form as Record<string, unknown>)[key];
const value = String(v || '');
fields.push({ key, value });
});
return fields;
}
return [];
};
// 解析 requestMapping 对象为数组
const parseRequestMappingFields = (mapping: unknown) => {
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
};
// 解析 responseMapping 对象为数组
const parseResponseMappingFields = (mapping: unknown) => {
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
};
const buildFormArray = (): ModelFormEntry[] => {
return state.formFields
@@ -454,9 +451,48 @@ const removeFormField = (index: number) => {
const confirmFormFields = () => {
showFormDialog.value = false;
};
// 请求映射字段操作
const addRequestMappingField = () => {
state.requestMappingFields.push({ key: '', value: '' });
};
const removeRequestMappingField = (index: number) => {
state.requestMappingFields.splice(index, 1);
};
const confirmRequestMappingFields = () => {
showRequestMappingDialog.value = false;
};
// 响应映射字段操作
const addResponseMappingField = () => {
state.responseMappingFields.push({ key: '', value: '', isMainBody: false });
};
const removeResponseMappingField = (index: number) => {
state.responseMappingFields.splice(index, 1);
};
// 设置返回主体(单选)
const setMainBody = (index: number) => {
// 清除所有字段的返回主体标记
state.responseMappingFields.forEach((field, i) => {
field.isMainBody = i === index;
});
state.mainBodyIndex = index;
};
const confirmResponseMappingFields = () => {
showResponseMappingDialog.value = false;
};
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean }>) => {
if (!rows.length) return [{ key: '', value: '', isMainBody: false }];
return rows.map(row => ({ ...row, isMainBody: row.isMainBody || false }));
};
/** 从 getModel 返回的 data 中取出单条模型对象 */
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
if (data == null) return null;
@@ -487,10 +523,7 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
state.ruleForm = {
id: row.id as string,
modelName: String(row.modelName ?? ''),
modelsType:
row.modelsType !== undefined && row.modelsType !== null
? typeOptionValue(row.modelsType as number | string)
: null,
modelsType: row.modelsType !== undefined && row.modelsType !== null ? typeOptionValue(row.modelsType as number | string) : null,
baseUrl: String(row.baseUrl ?? ''),
httpMethod: String(row.httpMethod || 'POST'),
headMsg: String(row.headMsg || ''),
@@ -506,21 +539,25 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
remark: String(row.remark || ''),
requestMappingJson: JSON.stringify(
row.requestMapping && typeof row.requestMapping === 'object' ? row.requestMapping : {},
null,
2
),
responseMappingJson: JSON.stringify(
row.responseMapping && typeof row.responseMapping === 'object' ? row.responseMapping : {},
null,
2
),
};
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
// 解析请求映射和响应映射
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
// 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value})
if (row.responseBody && typeof row.responseBody === 'object') {
const responseBodyKey = Object.keys(row.responseBody)[0];
if (responseBodyKey) {
const mainBodyIndex = state.responseMappingFields.findIndex(f => f.key === responseBodyKey);
if (mainBodyIndex !== -1) {
state.responseMappingFields[mainBodyIndex].isMainBody = true;
state.mainBodyIndex = mainBodyIndex;
}
}
}
};
// 打开弹窗(编辑时会请求 /model/getModel 详情)
const openDialog = async (type: string, row?: Record<string, unknown>) => {
state.dialog.type = type;
@@ -553,7 +590,7 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
}
fillFormFromDetailRow(detail as Record<string, unknown>);
} catch {
ElMessage.error('获取模型详情失败');
// 接口错误由 request 全局提示后端 message
state.dialog.isShowDialog = false;
} finally {
state.dialog.detailLoading = false;
@@ -578,11 +615,11 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
requestMappingJson: '{}',
responseMappingJson: '{}',
};
state.headers = [{ key: '', value: '' }];
state.formFields = [{ key: '', value: '' }];
state.requestMappingFields = [{ key: '', value: '' }];
state.responseMappingFields = [{ key: '', value: '', isMainBody: false }];
state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增';
}
@@ -608,8 +645,11 @@ const onSubmit = () => {
state.dialog.loading = true;
try {
state.ruleForm.headMsg = stringifyHeaders();
const requestMapping = parseJsonObjectField(state.ruleForm.requestMappingJson, {});
const responseMapping = parseJsonObjectField(state.ruleForm.responseMappingJson, {});
const requestMapping = fieldsToObject(state.requestMappingFields);
const responseMapping = fieldsToObject(state.responseMappingFields);
// 获取被设置为返回主体的字段 {key: value}
const responseBodyField = state.responseMappingFields.find(f => f.isMainBody);
const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {};
const submitData = {
modelName: state.ruleForm.modelName,
modelsType: state.ruleForm.modelsType as number | string,
@@ -620,9 +660,10 @@ const onSubmit = () => {
enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel,
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
form: buildFormArray(),
form: fieldsToObject(state.formFields),
requestMapping,
responseMapping,
responseBody,
maxConcurrency: state.ruleForm.maxConcurrency,
queueLimit: state.ruleForm.queueLimit,
timeoutSeconds: state.ruleForm.timeoutSeconds,
@@ -642,8 +683,8 @@ const onSubmit = () => {
}
closeDialog();
emit('refresh');
} catch (error) {
ElMessage.error('保存失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
state.dialog.loading = false;
}
@@ -657,6 +698,19 @@ defineExpose({
</script>
<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 {
max-height: 400px;
overflow-y: auto;
@@ -689,3 +743,50 @@ defineExpose({
color: #606266;
}
</style>

View File

@@ -32,6 +32,25 @@
</template>
</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">
<template #default="scope">
<el-tag :type="scope.row.enabled === 1 ? 'success' : 'danger'">{{ scope.row.enabled === 1 ? '启用' : '禁用' }}</el-tag>
@@ -94,6 +113,16 @@ const state = reactive({
},
});
/** 列表行与会话开关接口约定字段 chatSessionEnabled0/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) => {
if (modelsType === undefined || modelsType === null || modelsType === '') {
return '—';
@@ -108,8 +137,8 @@ const loadModelTypes = async () => {
if (res.code === 0) {
state.modelTypes = normalizeModelTypeOptions(res);
}
} catch (e) {
ElMessage.error('获取模型类型失败:');
} catch {
// 接口错误由 request 全局提示后端 message
}
};
@@ -122,8 +151,8 @@ const getTableData = async () => {
state.tableData.data = res.data.list || [];
state.tableData.total = res.data.total || 0;
}
} catch (error) {
ElMessage.error('获取模型列表失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
state.tableData.loading = false;
}
@@ -151,8 +180,8 @@ const onRowDel = (row: any) => {
await deleteModelModule(row.id);
ElMessage.success('删除成功');
getTableData();
} catch (error) {
ElMessage.error('删除失败');
} catch {
// 接口错误由 request 全局提示后端 message
}
})
.catch(() => {});
@@ -178,6 +207,10 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
.text-muted {
color: var(--el-text-color-placeholder);
}
.system-user-container {
:deep(.el-card__body) {
display: flex;

View File

@@ -162,7 +162,7 @@ const handleFileChange: UploadProps['onChange'] = async (uploadFile) => {
}
try {
ElMessage.info('正在上传文件到 OSS...');
const uploadRes = await uploadFileToOss(uploadFile.raw, { errorMode: 'page' });
const uploadRes = await uploadFileToOss(uploadFile.raw);
formData.fileName = uploadRes.data.fileName;
formData.fileUrl = uploadRes.data.fileURL;
fileList.value = [uploadFile];
@@ -180,7 +180,7 @@ const fetchSkillList = async () => {
loading.value = true;
try {
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 || [];
pagination.total = res.data?.total || 0;
} catch (error) {
@@ -222,21 +222,18 @@ const handleSubmit = async () => {
}
submitting.value = true;
try {
await createUserSkill(
{
name: formData.name,
description: formData.description,
category: formData.category,
fileName: formData.fileName,
fileUrl: formData.fileUrl,
},
{ errorMode: 'page' }
);
await createUserSkill({
name: formData.name,
description: formData.description,
category: formData.category,
fileName: formData.fileName,
fileUrl: formData.fileUrl,
});
ElMessage.success('创建成功');
dialogVisible.value = false;
fetchSkillList();
} catch (error) {
// 错误errorMode: 'page' 处理
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
submitting.value = false;
}
@@ -251,12 +248,12 @@ const handleCommand = async (command: string, skill: SkillItem) => {
cancelButtonText: '取消',
type: 'warning',
});
await deleteUserSkill(skill.id, { errorMode: 'page' });
await deleteUserSkill(skill.id);
ElMessage.success('删除成功');
fetchSkillList();
} catch (error) {
if (error !== 'cancel') {
// 错误errorMode: 'page' 处理
// 接口错误由 request 全局提示后端 message
}
}
}

View File

@@ -558,7 +558,7 @@ const getknowledgeList = async () => {
});
knowledgeList.value = response.data.list || [];
} catch (_error) {
ElMessage.error('获取知识库列表失败');
// 错误已由全局拦截器处理
} finally {
knowledgeLoading.value = false;
}
@@ -648,7 +648,7 @@ const onSaveknowledge = async () => {
showknowledgeDialog.value = false;
getknowledgeList();
} catch (_error) {
ElMessage.error('保存失败,请重试');
// 错误已由全局拦截器处理
} finally {
knowledgeSaving.value = false;
}
@@ -673,7 +673,7 @@ const getFileList = async () => {
statusEnabled: item.status === 1,
}));
} catch (_error) {
ElMessage.error('获取文件列表失败');
// 错误已由全局拦截器处理
} finally {
fileLoading.value = false;
}
@@ -745,7 +745,7 @@ const onConfirmUpload = async () => {
showUploadDialog.value = false;
getFileList();
} catch (_error) {
ElMessage.error('创建文档失败,请重试');
// 错误已由全局拦截器处理
} finally {
uploading.value = false;
}
@@ -764,7 +764,7 @@ const onFileStatusChange = async (row: any) => {
} catch (error) {
// 失败时恢复原状态
row.statusEnabled = !row.statusEnabled;
ElMessage.error('状态更新失败');
// 错误已由全局拦截器处理
}
};
@@ -779,7 +779,7 @@ const onGenerateVector = async (row: any) => {
getFileList();
}, 1000);
} catch (error) {
ElMessage.error('生成向量失败');
// 错误已由全局拦截器处理
}
};
@@ -791,7 +791,7 @@ const onViewDocumentDetail = async (row: any) => {
currentDocument.value = response.data;
showDocumentDetailDialog.value = true;
} catch (error) {
ElMessage.error('获取文件详情失败');
// 错误已由全局拦截器处理
}
};
@@ -917,7 +917,7 @@ const onEditModelConfig = async (row: any) => {
// 打开弹窗
showCreateModelDialog.value = true;
} catch (error) {
ElMessage.error('获取模型配置详情失败');
// 错误已由全局拦截器处理
}
};
@@ -928,7 +928,7 @@ const getModelEnums = async () => {
const response = await getAllModelEnums();
modelEnums.value = response.data?.options || [];
} catch (error) {
ElMessage.error('获取模型类型枚举失败');
// 错误已由全局拦截器处理
modelEnums.value = [];
} finally {
modelEnumsLoading.value = false;
@@ -975,7 +975,7 @@ const getModelFormFields = async () => {
}
});
} catch (error) {
ElMessage.error('获取模型表单字段失败');
// 错误已由全局拦截器处理
modelFormFields.value = [];
} finally {
modelFormLoading.value = false;
@@ -1016,7 +1016,7 @@ const onSaveModelConfig = async () => {
showCreateModelDialog.value = false;
getModelConfigList();
} catch (error) {
ElMessage.error(isEditMode.value ? '更新模型配置失败' : '创建模型配置失败');
// 错误已由全局拦截器处理
}
};
@@ -1040,7 +1040,7 @@ const getModelConfigList = async () => {
});
modelConfigList.value = response.data?.list || [];
} catch (error) {
ElMessage.error('获取模型配置列表失败');
// 错误已由全局拦截器处理
modelConfigList.value = [];
} finally {
modelConfigLoading.value = false;
@@ -1128,7 +1128,7 @@ const getTaskList = async () => {
const response = await listTasks();
taskList.value = response.data?.list || [];
} catch (error) {
ElMessage.error('获取任务列表失败');
// 错误已由全局拦截器处理
taskList.value = [];
} finally {
taskListLoading.value = false;
@@ -1149,7 +1149,7 @@ const onReexecuteTask = async (task: any) => {
// 重新获取任务列表
await getTaskList();
} catch (error) {
ElMessage.error('重新执行任务失败,请重试');
// 错误已由全局拦截器处理
}
})
.catch(() => {});

View File

@@ -114,7 +114,7 @@ const openDialog = async (row?: DialogFormData) => {
};
}
} catch (error) {
ElMessage.error('获取主播详情失败');
// 错误已由全局拦截器处理
} finally {
state.loading = false;
}
@@ -160,7 +160,7 @@ const onSubmit = async () => {
closeDialog();
emit('refresh');
} catch (error) {
ElMessage.error('操作失败');
// 错误已由全局拦截器处理
} finally {
state.loading = false;
}

View File

@@ -166,7 +166,7 @@ const getList = async () => {
tableData.total = res.data.total || 0;
}
} catch (error) {
ElMessage.error('获取主播列表失败');
// 错误已由全局拦截器处理
} finally {
tableData.loading = false;
}
@@ -192,7 +192,7 @@ const handleDelete = async (row: TableDataItem) => {
getList();
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
// 错误已由全局拦截器处理
}
}
};

View File

@@ -101,14 +101,13 @@ const openDialog = async (row?: { id?: string }) => {
try {
loading.value = true;
// 详情加载失败时由当前弹窗给出更易懂的业务提示。
const res = await getLiveAccountDetail({ id: String(row.id) }, { errorMode: 'page' });
const res = await getLiveAccountDetail({ id: String(row.id) });
if (res?.data) {
fillForm(res.data);
}
dialogVisible.value = true;
} catch (error) {
ElMessage.error('获取直播账号详情失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
loading.value = false;
}
@@ -130,19 +129,18 @@ const handleSubmit = async () => {
remark: formData.remark,
};
// 提交失败提示交给当前弹窗自己处理,避免和 request.ts 的统一报错重复。
if (isEdit.value) {
await updateLiveAccount(payload, { errorMode: 'page' });
await updateLiveAccount(payload);
ElMessage.success('修改成功');
} else {
await createLiveAccount(payload, { errorMode: 'page' });
await createLiveAccount(payload);
ElMessage.success('新增成功');
}
dialogVisible.value = false;
emit('refresh');
} catch (error) {
ElMessage.error(isEdit.value ? '修改失败' : '新增失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
loading.value = false;
}

View File

@@ -131,17 +131,13 @@ const tableData = reactive({
const getList = async () => {
try {
tableData.loading = true;
// 列表失败文案由当前页面决定,避免和全局请求报错同时出现。
const res = await getLiveAccountList(
{
...tableData.param,
platform: searchForm.platform || undefined,
accountName: searchForm.accountName || undefined,
accountId: searchForm.accountId || undefined,
status: searchForm.status,
},
{ errorMode: 'page' }
);
const res = await getLiveAccountList({
...tableData.param,
platform: searchForm.platform || undefined,
accountName: searchForm.accountName || undefined,
accountId: searchForm.accountId || undefined,
status: searchForm.status,
});
if (res && res.data) {
tableData.data = (res.data.list || []).map((item: any) => ({
...item,
@@ -149,8 +145,8 @@ const getList = async () => {
}));
tableData.total = res.data.total || 0;
}
} catch (error) {
ElMessage.error('获取直播账号列表失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
tableData.loading = false;
}
@@ -195,12 +191,12 @@ const handleDelete = async (row: LiveAccountItem) => {
cancelButtonText: '取消',
type: 'warning',
});
await deleteLiveAccount({ id: row.id }, { errorMode: 'page' });
await deleteLiveAccount({ id: row.id });
ElMessage.success('删除成功');
getList();
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
// 接口错误由 request 全局提示后端 message
}
}
};

View File

@@ -153,8 +153,7 @@ const openDialog = async (row?: { id?: string }) => {
await loadOptions();
if (row?.id) {
// 详情请求失败时,这个弹窗希望给出更明确的页面语义提示。
const res = await getScheduleDetail({ id: String(row.id) }, { errorMode: 'page' });
const res = await getScheduleDetail({ id: String(row.id) });
const detail = res?.data;
if (detail) {
formData.id = String(detail.id);
@@ -170,8 +169,8 @@ const openDialog = async (row?: { id?: string }) => {
}
dialogVisible.value = true;
} catch (error) {
ElMessage.error(isEdit.value ? '获取排班详情失败' : '加载排班基础数据失败');
} catch {
// 接口错误由 request 全局提示后端 message表单校验错误由表单项展示
} finally {
loading.value = false;
}
@@ -196,19 +195,18 @@ const handleSubmit = async () => {
remark: formData.remark,
};
// 提交失败文案由弹窗自己控制,避免接口层和弹窗层重复报错。
if (isEdit.value) {
await updateSchedule(payload, { errorMode: 'page' });
await updateSchedule(payload);
ElMessage.success('修改排班成功');
} else {
await createSchedule(payload, { errorMode: 'page' });
await createSchedule(payload);
ElMessage.success('新增排班成功');
}
dialogVisible.value = false;
emit('refresh');
} catch (error) {
ElMessage.error(isEdit.value ? '修改排班失败' : '新增排班失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
loading.value = false;
}

View File

@@ -144,16 +144,12 @@ const getStatusTagType = (status: number): 'success' | 'info' | 'warning' => {
const getList = async () => {
try {
tableData.loading = true;
// 列表失败文案由当前页面决定,避免和 request.ts 的全局错误提示重复。
const res = await getScheduleList(
{
...tableData.param,
anchorName: searchForm.anchorName || undefined,
accountName: searchForm.accountName || undefined,
status: searchForm.status,
} as any,
{ errorMode: 'page' }
);
const res = await getScheduleList({
...tableData.param,
anchorName: searchForm.anchorName || undefined,
accountName: searchForm.accountName || undefined,
status: searchForm.status,
} as any);
const scheduleData = res?.data;
if (scheduleData) {
tableData.data = (scheduleData.list || []).map((item: any) => ({
@@ -166,8 +162,8 @@ const getList = async () => {
}));
tableData.total = scheduleData.total || 0;
}
} catch (error) {
ElMessage.error('获取排班列表失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
tableData.loading = false;
}
@@ -211,12 +207,12 @@ const handleDelete = async (row: ScheduleItem) => {
cancelButtonText: '取消',
type: 'warning',
});
await deleteSchedule({ id: row.id }, { errorMode: 'page' });
await deleteSchedule({ id: row.id });
ElMessage.success('删除成功');
getList();
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
// 接口错误由 request 全局提示后端 message
}
}
};