front/src/views/api/downstreamInterface/index.vue
zzh a5ef7e542d
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m38s
feat: 服务商公共参数配置并回落到接口明细
新增服务商级公共参数入口与弹窗,按位置只读落入 Query/Header/Body,并完善授权入参与调试页按钮样式。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 19:50:51 +08:00

2305 lines
66 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { ref, reactive, computed, nextTick, onMounted, onActivated, unref, watch } from 'vue';
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
import useModal from '@/components/ModalPro/useModal';
import ModalPro from '@/components/ModalPro/index.vue';
import {
getServiceProviderGetServiceProvider,
postServiceProviderAdd,
postServiceProviderUpdate,
postServiceProviderDeleteByIds,
} from '@/api/interface-serviceProvider/serviceProvider';
import { Message, Spin, Empty, Button, Input, Select, Option, Checkbox, Textarea, InputNumber, Space, Dropdown, Doption } from '@arco-design/web-vue';
import { $baseMessage } from '@/utils/message';
import { $baseConfirm } from '@/utils/confirm';
import {
postServiceProviderCopy,
postServiceProviderExport,
postServiceProviderImport,
} from '@/api/interface-serviceProvider/serviceProviderImportExport';
import ParameterConfig from '@/views/api/downstreamInterface/comps/ParameterConfig.vue';
import FixedParameterConfig from '@/views/api/downstreamInterface/comps/FixedParameterConfig.vue';
import FormPro from '@/components/FormPro/index.vue';
import TableButtons from '@/components/TableButtons/index.vue';
import {
postServiceProviderPaginatedList,
} from '@/api/ServiceProviderInterface/serviceProvider';
import {
postInterfaceBatchDeleteServiceProviderInterfaceByIds,
} from '@/api/InterfaceServices/serviceProviderInterface';
import {
postInterfaceInterfacePaginatedList,
} from '@/api/InterfaceServices/interface';
import dayjs from 'dayjs';
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
import { getAppGetDropDownList } from '@/api/ApplicationServices/apps';
import { getFixedParameterGetFixedParameterList } from '@/api/ParameterServices/fixedParameter';
import ServiceProviderInterfaceModal from './components/ServiceProviderInterfaceModal.vue';
import AccountList from './comps/AccountList.vue';
import ProviderMapPublishModal from './comps/ProviderMapPublishModal.vue';
import ApiDebugger from '@/views/api/upstreamInterface/components/ApiDebugger.vue';
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
import {
IconEdit,
IconSettings,
IconCopy,
IconUser,
IconDownload,
IconSearch,
IconPlus,
IconRefresh,
IconMore,
IconDelete,
IconList,
} from '@arco-design/web-vue/es/icon';
import { useI18n } from 'vue-i18n';
defineOptions({
name: 'DownstreamInterface',
});
const { t } = useI18n();
/** 保存成功后引导去发布映射缓存 */
const providerPublishRef = ref<InstanceType<typeof ProviderMapPublishModal> | null>(null);
function resolveProviderInfo(input?: any) {
const fromSelected = selectedProvider.value;
const id = Number(input?.id || input?.serviceProviderId || fromSelected?.id || 0);
const name =
input?.name ||
input?.serviceProviderName ||
fromSelected?.name ||
'';
const code = input?.code || input?.serviceProviderCode || fromSelected?.code;
return id ? { id, name, code } : null;
}
function askGoPublish(providerLike?: any) {
const info = resolveProviderInfo(providerLike);
if (!info) {
Message.success(t('system.serviceProviders.publish.saveOkOnly'));
return;
}
$baseConfirm({
title: t('system.interface.modal.warningTitle'),
content: t('system.serviceProviders.publish.confirmAfterSave'),
okText: t('system.serviceProviders.publish.goPublish'),
cancelText: t('system.serviceProviders.publish.later'),
onOk: () => {
providerPublishRef.value?.open(info);
},
});
}
/** 保存成功统一提示,并引导发布 */
function tipSaveOk(action = '保存', providerLike?: any) {
Message.success(
t('system.serviceProviders.publish.saveSuccessAction', { action }),
);
const info = resolveProviderInfo(providerLike);
if (info?.id) askGoPublish(info);
}
const route = useRoute();
const router = useRouter();
const useModalResult = useModal();
const register = useModalResult[0];
const modal = useModalResult[1] as any;
const applicationType = ref<number | undefined>(0);
const appList = ref<API.AppDropDownItem[]>([]);
// 新增的响应式数据
const searchKeyword = ref('');
const serviceProviderList = ref<any[]>([]);
const selectedProvider = ref<any>(null);
const commonParamsCount = ref<number | null>(null);
const commonParamsLoading = ref(false);
const loading = ref(false);
// 接口弹窗相关
const interfaceModalVisible = ref(false);
const interfaceModalRef = ref();
const interfaceModalTitle = ref('服务商接口配置');
// 接口列表相关
const interfaceList = ref<any[]>([]);
const selectedInterface = ref<any>(null);
const interfaceLoading = ref<boolean>(false);
// 接口详情表单和表格相关
const interfaceDetailFormRef = ref();
const interfaceDetailTableRef = ref();
const interfaceDetailTableRef1 = ref();
const interfaceDetailModalRef = ref();
const apiDebuggerRefreshKey = ref(0);
// 接口明细tab切换
const interfaceDetailTab = ref('detail');
const interfaceDetailTabItems = computed(() => [
{ key: 'detail', label: t('system.serviceProviders.detail.tab.detail') },
{ key: 'debug', label: t('system.serviceProviders.detail.tab.debug') },
]);
const interfaceDetailLoading = ref(false); // 接口详情加载状态
const detailRefreshKey = ref(0); // 用于控制组件重新渲染的key
// 初始化接口明细组件的函数
async function initInterfaceDetail() {
if (!selectedInterface.value || interfaceDetailTab.value !== 'detail') {
return;
}
// 等待组件完全挂载(v-if 条件渲染需要时间)
await nextTick();
await nextTick();
// 使用轮询方式等待组件实例创建,最多等待 2 秒
let retryCount = 0;
const maxRetries = 20;
const checkInterval = 100;
const checkAndInit = () => {
// 检查组件是否还存在且处于 detail tab
if (interfaceDetailTab.value !== 'detail' || !selectedInterface.value) {
return; // 如果已经切换了 tab,停止初始化
}
if (interfaceDetailModalRef.value && interfaceDetailModalRef.value.init) {
try {
const interfaceId = selectedInterface.value.interfaceId || selectedInterface.value.id;
interfaceDetailModalRef.value.init({
id: selectedInterface.value.id,
interfaceId: interfaceId,
serviceProviderId: selectedProvider.value?.id,
serviceProviderName: selectedProvider.value?.name,
mode: 'edit',
forceRefresh: false
});
} catch {
// 初始化失败,静默处理
}
} else {
retryCount++;
if (retryCount < maxRetries) {
setTimeout(checkAndInit, checkInterval);
}
}
};
// 延迟一下再开始检查,给组件更多时间挂载
setTimeout(checkAndInit, 200);
}
// 监听tab切换,切换回detail tab时检查是否需要初始化组件
watch(interfaceDetailTab, async (newTab) => {
if (newTab === 'detail' && selectedInterface.value) {
// 等待 DOM 更新完成
await nextTick();
await nextTick();
// 由于使用了 v-show,组件不会被销毁,数据会保留
// 但如果组件还没有初始化(比如第一次切换到 detail tab),需要初始化
// 检查组件是否存在,如果不存在则初始化
setTimeout(() => {
if (!interfaceDetailModalRef.value && selectedInterface.value) {
// 组件不存在,需要初始化
initInterfaceDetail();
}
// 如果组件已存在,说明可能是切换tab回来的,v-show 会保留数据,不需要重新初始化
}, 100);
}
});
const interfaceDetailForm = reactive<any>({
optionType: 1,
id: 0,
name: '',
abbreviation: '',
requestProtocol: '',
requestMethod: 'POST',
requestAddress: '',
contentType: '',
responseContentType: '',
isDisableAuth: false,
qps: 1,
mappedParamType: '',
description: '',
serviceProviderId: 0,
inParameterList: [],
outParameterList: [],
});
// 参数类型列表
const ParameterTypeList = ref<any[]>([]);
// 导入:打开导入弹窗(复用已有新建接口弹窗或实现你有的导入逻辑)
function openImportModal() {
// 直接选择文件并调用导入接口
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.xlsx,.xls,.csv,.txt';
input.onchange = async () => {
const file = input.files && input.files[0];
if (!file) return;
try {
loading.value = true;
const ok = await postServiceProviderImport({}, file);
if (ok) {
$baseMessage(t('system.serviceProviders.interfaceList.refresh'), 'success');
await loadServiceProviderList();
} else {
$baseMessage(t('system.serviceProviders.list.deleteFail'), 'error');
}
} catch (e) {
$baseMessage(t('system.serviceProviders.list.deleteFail'), 'error');
} finally {
loading.value = false;
}
};
input.click();
}
// 表格配置
const interfaceDetailTableProps = reactive({
data: [] as any[],
columns: [
{ field: 'drag', title: '按住拖动', width: 80, slots: { default: 'drag' } },
{ field: 'name', title: '参数名', width: 120, slots: { default: 'name' } },
{ field: 'cnName', title: '参数名(中文)', width: 120, slots: { default: 'cnName' } },
{ field: 'type', title: '参数类型', width: 100, slots: { default: 'type' } },
{ field: 'description', title: '参数描述', width: 150, slots: { default: 'description' } },
{ field: 'isRequired', title: '必填', width: 80, slots: { default: 'isRequired' } },
{ field: 'action', title: '操作', width: 120, slots: { default: 'action' } },
],
});
const interfaceDetailTableProps1 = reactive({
data: [] as any[],
columns: [
{ field: 'drag', title: '按住拖动', width: 80, slots: { default: 'drag' } },
{ field: 'name', title: '参数名', width: 120, slots: { default: 'name' } },
{ field: 'cnName', title: '参数名(中文)', width: 120, slots: { default: 'cnName' } },
{ field: 'type', title: '参数类型', width: 100, slots: { default: 'type' } },
{ field: 'description', title: '参数描述', width: 150, slots: { default: 'description' } },
{ field: 'isRequired', title: '必填', width: 80, slots: { default: 'isRequired' } },
{ field: 'action', title: '操作', width: 120, slots: { default: 'action' } },
],
});
// 编辑状态
const editingRows = ref<Set<number>>(new Set());
const editingRows1 = ref<Set<number>>(new Set());
// 表单配置
const serviceProviderFormItems = [
{
comp: 'a-input',
field: 'name',
label: '',
span: 5,
compProps: {
placeholder: '请填写接口中文名称',
},
},
{
comp: 'a-input',
field: 'abbreviation',
label: '',
span: 5,
required: true,
compProps: {
placeholder: '请填写接口code',
},
},
{
comp: 'SelectPro',
field: 'requestProtocol',
label: '',
span: 4,
compProps: {
placeholder: '请选择请求协议',
api: getDictionaryGetList,
extra: { type: 'RequestProtocol' },
labelKeys: ['value'],
idKey: 'key',
isPage: false,
showHeader: false,
immediate: true, // 立即加载字典数据,确保能显示字典值
},
},
{
comp: 'SelectPro',
field: 'requestMethod',
label: '',
span: 4,
compProps: {
placeholder: '请选择请求方式',
api: getDictionaryGetList,
extra: { type: 'RequestMethod' },
labelKeys: ['value'],
idKey: 'key',
isPage: false,
showHeader: false,
immediate: true, // 立即加载字典数据,确保能显示字典值
},
},
{
comp: 'a-input',
field: 'requestAddress',
label: '',
span: 6,
compProps: {
placeholder: '例: http://xx/api/CreateOrder',
},
},
{
comp: 'SelectPro',
field: 'contentType',
label: '',
span: 4,
compProps: {
placeholder: '请选择Content-Type',
api: getDictionaryGetList,
extra: { type: 'ContentType' },
labelKeys: ['value'],
idKey: 'key',
isPage: false,
showHeader: false,
immediate: true, // 立即加载字典数据,确保能显示字典值
},
},
{
comp: 'SelectPro',
field: 'responseContentType',
label: '',
span: 4,
compProps: {
placeholder: '请选择Response-Content-Type',
api: getDictionaryGetList,
extra: { type: 'ResponseContentType' },
labelKeys: ['value'],
idKey: 'key',
isPage: false,
showHeader: false,
immediate: true, // 立即加载字典数据,确保能显示字典值
},
},
{
comp: 'SelectPro',
field: 'isDisableAuth',
label: '',
span: 4,
compProps: {
options: [
{ value: true, label: '是' },
{ value: false, label: '否' },
],
labelKeys: ['label'],
idKey: 'value',
isPage: false,
showHeader: false,
},
},
{
comp: 'a-input-number',
field: 'qps',
label: '',
span: 4,
compProps: {
min: 0,
precision: 0,
controlsPosition: 'right',
valueOnClear: 0,
controls: false,
placeholder: 'QPS',
},
},
{
comp: 'SelectPro',
field: 'mappedParamType',
label: '',
span: 4,
compProps: {
api: getDictionaryGetList,
extra: { type: 'ParameterType' },
placeholder: '请选择根节点',
labelKeys: ['value'],
idKey: 'key',
isPage: false,
showHeader: false,
immediate: true, // 立即加载字典数据,确保能显示字典值
},
},
{
comp: 'a-input',
field: 'description',
label: '',
span: 6,
compProps: {
placeholder: '请输入接口备注',
allowClear: true,
},
},
];
const serviceProviderFormRules = {
name: [{ required: true, message: '输入不能为空', trigger: 'change' }],
abbreviation: [
{ required: true, message: '输入不能为空', trigger: 'change' },
{
max: 30,
message: '请控制长度在30个字符之内',
trigger: 'change',
},
],
requestAddress: [
{ required: true, message: '输入不能为空', trigger: 'change' },
],
requestProtocol: [
{ required: true, message: '请选择请求协议', trigger: 'change' },
],
requestMethod: [
{ required: true, message: '请选择请求方式', trigger: 'change' },
],
contentType: [
{ required: true, message: '请选择Content-Type', trigger: 'change' },
],
description: [
{
max: 200,
message: '请控制长度在200个字符之内',
trigger: 'change',
},
],
};
const tableProps = computed(() => {
return {
isToolsExport: false,
tableConfig: {
immediate: true,
columns: [
{ field: 'checkbox', type: 'checkbox', width: 60 },
{ field: 'seq', type: 'seq', width: 60, title: '序号' },
{
field: 'name',
title: '服务商名称',
},
{
field: 'type',
title: '服务商类型',
},
{
field: 'code',
title: '服务商代码',
},
{
field: 'interfaceCount',
title: '接口数量',
slots: { default: 'interfaceCount' },
},
{
field: 'description',
title: '描述',
},
{
field: 'accountCount',
title: '账户数量',
slots: { default: 'accountCount' },
},
{
field: 'updateTime',
title: '更新时间',
slots: {
default: 'updateTime',
},
},
{
field: 'action',
title: '操作',
fixed: 'right',
width: 180,
slots: {
default: 'action',
},
},
],
},
searchInfo: {
appId: applicationType.value,
},
api: postServiceProviderPaginatedList,
formConfig: {
layout: 'inline',
itemList: [
{
label: '',
field: 'nameOrCode',
comp: 'AInput',
componentProps: {
placeholder: '请输入服务商名称或者服务商代码',
allowClear: true,
size: 'small',
},
compProps: {
placeholder: '请输入服务商名称或者服务商代码',
allowClear: true,
size: 'small',
},
span: 6,
},
],
buttonConfig: {
showBtn: true,
showReset: false,
showOk: true,
okText: '查询',
span: 2,
},
},
};
});
const dialogFormRef = ref();
const dialogConfig = reactive({
formLayout: 'vertical',
defaultSpan: 12,
rowGutter: 16,
buttonConfig: {
showBtn: false,
},
itemList: [
{
field: 'sectionBasic',
label: ' ',
hideLabel: true,
hideColon: true,
comp: 'slot',
span: 24,
},
{
label: '服务商名称',
field: 'name',
comp: 'AInput',
tooltip: '用于列表展示与检索,建议与业务侧命名一致',
compProps: {
placeholder: '如:某某物流正式环境',
allowClear: true,
},
rules: [
{
required: true,
message: '请输入服务商名称',
trigger: 'change',
},
{
max: 200,
message: '请控制长度在200个字符之内',
trigger: 'change',
},
],
span: 12,
},
{
label: '服务商类型',
field: 'types',
comp: 'SelectPro',
tooltip: '可多选,用于分类筛选与权限场景',
compProps: {
api: getDictionaryGetList,
placeholder: '请选择服务商类型',
extra: { type: 'ServiceProviderType' },
labelKeys: ['value'],
idKey: 'key',
isPage: false,
multiple: true,
showHeader: false,
immediate: true,
},
rules: [
{
required: true,
message: '请选择服务商类型',
trigger: 'change',
},
],
span: 12,
},
{
label: '服务商代码',
field: 'code',
comp: 'AInput',
tooltip: '唯一编码,仅字母与数字,对接脚本常按此识别',
compProps: {
placeholder: '如:DHL01',
allowClear: true,
},
rules: [
{
required: true,
message: '请输入服务商代码',
trigger: 'change',
},
{
max: 30,
message: '请控制长度在30个字符之内',
trigger: 'change',
},
{
pattern: /^[a-zA-Z0-9]+$/,
message: '只能输入字母和数字组合',
trigger: 'change',
},
],
span: 12,
},
{
label: '时区',
field: 'traceTimeZone',
comp: 'AInput',
tooltip: '轨迹时间换算时区,如 UTC+8 / Asia/Shanghai',
compProps: {
maxlength: 50,
placeholder: '如:UTC+8',
allowClear: true,
},
span: 12,
},
{
field: 'sectionRuntime',
label: ' ',
hideLabel: true,
hideColon: true,
comp: 'slot',
span: 24,
},
{
label: '流程Code',
field: 'flowCode',
comp: 'AInput',
tooltip: '对应编排/工作流编码,可按下游约定填写',
compProps: {
placeholder: '可选,按对接约定填写',
allowClear: true,
},
span: 12,
},
{
label: '最大超时',
field: 'timeout',
comp: 'AInput',
tooltip: '调用下游接口的超时时间(秒),须为正整数',
compProps: {
placeholder: '如:30(单位:秒)',
maxlength: 50,
allowClear: true,
},
rules: [
{
pattern: /^[1-9]\d*$/,
message: '只能输入正整数',
trigger: 'change',
},
],
span: 12,
},
{
label: '描述',
field: 'description',
comp: 'ATextarea',
tooltip: '补充说明用途、环境或注意事项',
compProps: {
placeholder: '可选,补充对接说明',
autoSize: { minRows: 3, maxRows: 5 },
allowClear: true,
maxlength: 200,
showWordLimit: true,
},
rules: [
{
max: 200,
message: '请控制长度在200个字符之内',
trigger: 'change',
},
],
span: 24,
},
],
});
const edId = ref(0);
const isCopy = ref(false);
// 用于存储待设置的表单数据
const pendingEditFormData = ref<any>(null);
// 监听 Modal 打开状态,当 Modal 打开且 FormPro 已挂载时,设置表单值
watch(() => modal.getVisible, async (visible) => {
if (visible && pendingEditFormData.value) {
// 等待组件初始化
await nextTick();
await nextTick();
// 重试机制:最多尝试 10 次,每次间隔 100ms
let retries = 0;
const maxRetries = 10;
const checkAndSet = async () => {
if (dialogFormRef.value && pendingEditFormData.value) {
try {
if (dialogFormRef.value.setFormValue) {
dialogFormRef.value.setFormValue(pendingEditFormData.value);
} else if (dialogFormRef.value.setValues) {
dialogFormRef.value.setValues(pendingEditFormData.value);
}
pendingEditFormData.value = null;
return true;
} catch {
// 静默忽略设置表单值失败
}
}
return false;
};
while (retries < maxRetries) {
if (await checkAndSet()) {
break;
}
await new Promise(resolve => setTimeout(resolve, 100));
retries++;
}
}
});
function addClick() {
modal.setModalProps({ title: '新增' });
isCopy.value = false;
edId.value = 0;
modal.openModal();
}
async function saveFn() {
const d = await dialogFormRef.value.validate();
if (!d) {
const fromValue = dialogFormRef.value.getValues();
if (!isCopy.value) {
if (!edId.value) {
await postServiceProviderAdd({
...fromValue,
id: 0,
Id: undefined,
}).then(() => {
tipSaveOk('新增', { id: 0, name: fromValue?.name, code: fromValue?.code });
loadServiceProviderList();
modal.closeModal();
});
} else {
await postServiceProviderUpdate({
...fromValue,
Id: undefined,
id: edId.value,
}).then(() => {
tipSaveOk('修改', {
id: edId.value,
name: fromValue?.name || selectedProvider.value?.name,
code: fromValue?.code || selectedProvider.value?.code,
});
loadServiceProviderList();
modal.closeModal();
});
}
} else {
await postServiceProviderCopy({
...fromValue,
Id: undefined,
id: edId.value,
}).then(() => {
tipSaveOk('创建', { name: fromValue?.name, code: fromValue?.code });
loadServiceProviderList();
modal.closeModal();
});
}
}
}
async function deleteClick() {
// 由于新布局没有表格选择功能,这里暂时禁用删除功能
// 或者可以添加一个删除按钮到每个服务商项目中
Message.warning(t('system.serviceProviders.list.deleteTip'));
}
// 删除服务商
async function deleteProviderClick(provider: any) {
if (!provider || !provider.id) {
$baseMessage(t('system.serviceProviders.list.invalid'), 'warning');
return;
}
$baseConfirm({
title: '温馨提示',
content: `确定要删除服务商"${provider.name}"吗?删除后该服务商下的所有接口也将被删除。`,
onOk: async () => {
try {
await postServiceProviderDeleteByIds([provider.id]);
$baseMessage(t('system.serviceProviders.list.deleteSuccess'), 'success');
// 重新加载服务商列表
loadServiceProviderList();
// 如果删除的是当前选中的服务商,清空选中状态
if (selectedProvider.value?.id === provider.id) {
selectedProvider.value = null;
interfaceList.value = [];
selectedInterface.value = null;
}
} catch (error) {
$baseMessage(t('system.serviceProviders.list.deleteFail'), 'error');
}
},
});
}
async function editClick(row: any, isCopy2: boolean) {
if (!row || !row.id) {
$baseMessage('请先选择服务商', 'warning');
return;
}
isCopy.value = isCopy2;
edId.value = row.id;
// 创建副本模式
if (isCopy2) {
modal.setModalProps({ title: '创建服务商副本' });
modal.openModal();
await nextTick();
await nextTick();
setTimeout(() => {
if (dialogFormRef.value) {
dialogFormRef.value.setValues({ name: row.name });
}
}, 300);
return;
}
// 编辑模式:先加载数据,再打开 Modal
try {
const response = await getServiceProviderGetServiceProvider({
serviceProviderId: row.id,
});
// request.ts 的响应拦截器已经处理了数据,直接使用返回的数据
// 如果返回的是 { data: {...} } 格式,则使用 data;否则直接使用 response
const apiData = (response as any)?.data || response;
// 仅设置表单存在的字段,避免无效字段导致 setValues 抛错(例如已删除的“时区”)
const formData = {
name: apiData?.name || '',
types: apiData?.types || [],
code: apiData?.code || '',
flowCode: apiData?.flowCode || '',
traceTimeZone: apiData?.traceTimeZone || '',
timeout: apiData?.timeout || '',
description: apiData?.description || '',
};
// 存储待设置的数据
pendingEditFormData.value = formData;
// 打开 Modal,watch 会自动处理数据设置
modal.setModalProps({ title: '修改服务商列表' });
modal.openModal();
// 备用方案:如果 watch 没有及时触发,这里也会设置
await nextTick();
await nextTick();
await new Promise(resolve => setTimeout(resolve, 500));
if (dialogFormRef.value && pendingEditFormData.value) {
if (dialogFormRef.value.setFormValue) {
dialogFormRef.value.setFormValue(pendingEditFormData.value);
} else if (dialogFormRef.value.setValues) {
dialogFormRef.value.setValues(pendingEditFormData.value);
}
pendingEditFormData.value = null;
}
} catch (error) {
pendingEditFormData.value = null;
$baseMessage('加载服务商数据失败', 'error');
modal.closeModal();
}
}
const parameterConfigFlagShow = ref(false);
const parameterConfigFlag = ref();
// 包装函数:先选中服务商,然后延迟执行编辑操作
function handleEditWithSelect(provider: any, isCopy: boolean) {
handleProviderSelect(provider);
setTimeout(() => {
editClick(provider, isCopy);
}, 150);
}
// 包装函数:先选中服务商,然后延迟执行授权配置操作
function handleParameterConfigWithSelect(provider: any) {
handleProviderSelect(provider);
setTimeout(() => {
parameterConfigClick(provider);
}, 150);
}
async function parameterConfigClick(row: any) {
if (!row || !row.id) {
$baseMessage('请先选择服务商', 'warning');
return;
}
// 注意:选中操作已经在模板的 @click 中处理了
parameterConfigFlagShow.value = true;
// 等待组件完全挂载,使用多个 nextTick 确保 ModalPro 也完全初始化
await nextTick();
await nextTick();
// 额外等待一小段时间,确保 ModalPro 的 useModal 完全初始化
setTimeout(() => {
if (parameterConfigFlag.value) {
parameterConfigFlag.value.init(row);
} else {
$baseMessage('组件初始化失败,请重试', 'error');
}
}, 100);
}
const accountList = ref();
const fixedParameterConfigRef = ref<InstanceType<typeof FixedParameterConfig> | null>(null);
function accountManageClick(row: any) {
accountList.value?.init?.(row);
}
function handleFixedParamsWithSelect(provider: any) {
handleProviderSelect(provider);
setTimeout(() => {
fixedParameterConfigRef.value?.init?.(provider);
}, 150);
}
async function refreshCommonParamsCount(providerLike?: any) {
const id = Number(providerLike?.id || selectedProvider.value?.id || 0);
if (!Number.isFinite(id) || id <= 0) {
commonParamsCount.value = null;
return;
}
commonParamsLoading.value = true;
try {
const res: any = await getFixedParameterGetFixedParameterList({
serviceProviderId: id,
});
const data = res?.data || res || [];
commonParamsCount.value = Array.isArray(data) ? data.length : 0;
} catch {
commonParamsCount.value = null;
} finally {
commonParamsLoading.value = false;
}
}
watch(
() => selectedProvider.value?.id,
() => {
refreshCommonParamsCount();
},
{ immediate: true },
);
function openCommonParamsFromTabs() {
const provider = selectedProvider.value;
if (!provider?.id) {
$baseMessage('请先选择服务商', 'warning');
return;
}
fixedParameterConfigRef.value?.init?.(provider);
}
function onFixedParamsSaved(payload: any) {
refreshCommonParamsCount(payload);
interfaceDetailModalRef.value?.refreshInheritedCommonParams?.();
interfaceModalRef.value?.refreshInheritedCommonParams?.();
askGoPublish(payload);
}
function mapAccountClick(row: { id: any; code: any; name?: string }) {
accountList.value?.init?.(row);
}
function exportFileJSON(data = {}, filename = 'dataJSON.json') {
if (typeof data === 'object') data = JSON.stringify(data, null, 4);
// 导出数据
const blob = new Blob([data as any], { type: 'text/json' });
const e = new MouseEvent('click');
const a = document.createElement('a');
a.download = filename;
a.href = window.URL.createObjectURL(blob);
a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
a.dispatchEvent(e);
}
// 导出数据
function onDerive(row: any) {
postServiceProviderExport({ serviceProviderId: row.id }).then(
(res: any) => {
exportFileJSON(res, `${row.name}.json`);
}
);
}
function getAppList() {
getAppGetDropDownList({ keyword: '' }).then((response: any) => {
const data = response?.data || response || [];
appList.value = Array.isArray(data) ? data : [];
}).catch(() => {
appList.value = [];
});
}
async function tabClick(key: number) {
applicationType.value = key;
await nextTick();
// 重新加载服务商列表
loadServiceProviderList();
}
// 其他页面跳转过来如果query中有type就打开服务商列表弹窗
onActivated(() => {
const { type, id } = route.query;
// type === 'closeReturnConfig')
if (type && type === 'goRouter') {
// 重新加载服务商列表
loadServiceProviderList();
}
if (type && type === 'closeReturnConfig') {
// 重新加载服务商列表
loadServiceProviderList();
}
});
// 离开页面就把type删掉,不弹框
onBeforeRouteLeave(() => {
// route.query.type = ''
delete route.query.type;
delete route.query.id;
});
// 左侧菜单:将服务商列表转为树数据(单分类 + 子节点)
const providerTreeData = computed(() => {
const list = serviceProviderList.value || [];
if (list.length === 0) return [];
return [
{
key: 'category-1',
title: '服务商',
children: list.map((p: any) => ({
key: `item-${p.id}`,
title: p.name,
isLeaf: true,
id: p.id,
name: p.name,
code: p.code || '-',
...p,
})),
},
];
});
const providerSelectedKeys = computed(() =>
selectedProvider.value ? [`item-${selectedProvider.value.id}`] : []
);
const providerExpandedKeys = ref(['category-1']);
const interfaceSearchKeyword = ref('');
/** 中间接口列表:转成 CustomLeftMenu 树数据(展示结构与左侧/上游映射列表一致) */
const interfaceTreeData = computed(() => {
const list = interfaceList.value || [];
if (!selectedProvider.value || list.length === 0) return [];
const kw = interfaceSearchKeyword.value.trim().toLowerCase();
return list
.filter((item: any) => {
if (!kw) return true;
const name = String(item.name || '').toLowerCase();
const code = String(item.abbreviation || item.code || item.requestAddress || '').toLowerCase();
return name.includes(kw) || code.includes(kw);
})
.map((item: any, index: number) => ({
...item,
key: `iface-${item.id}`,
title: item.name,
isLeaf: true,
id: item.id,
name: item.name,
code: item.abbreviation || item.code || item.requestAddress || '-',
method: formatRequestMethod(item.requestMethod),
index,
}));
});
function handleInterfaceSearch() {
// 本地过滤,interfaceTreeData 已依赖 interfaceSearchKeyword
}
const interfaceSelectedKeys = computed(() =>
selectedInterface.value?.id != null ? [`iface-${selectedInterface.value.id}`] : []
);
function handleInterfaceSelectFromTree(keys: string[]) {
const key = keys?.[0];
if (!key?.startsWith('iface-')) return;
const id = Number(key.slice(6));
const item = interfaceList.value.find((x: any) => x.id === id);
if (item) selectInterface(item);
}
function handleInterfaceNodeClick(key: string) {
handleInterfaceSelectFromTree([key]);
}
function handleInterfaceDeleteNode(nodeData: any) {
if (!nodeData) return;
const id = nodeData.id;
const item = interfaceList.value.find((x: any) => x.id === id) || nodeData;
deleteInterfaceClick(item);
}
function handleProviderSelectFromTree(keys: string[], info: any) {
const node = info?.node;
if (!node || !node.isLeaf) return;
const key = node.key;
const id = key.startsWith('item-') ? Number(key.slice(5)) : null;
if (id == null) return;
const provider = serviceProviderList.value.find((p: any) => p.id === id);
if (provider) handleProviderSelect(provider);
}
// 新增的函数
async function loadServiceProviderList() {
try {
loading.value = true;
const params = {
nameOrCode: searchKeyword.value,
appId: applicationType.value,
pageIndex: 1,
pageSize: 1000
} as any;
const response = await postServiceProviderPaginatedList(params);
const data = (response as any)?.items || [];
serviceProviderList.value = data;
} catch (error) {
$baseMessage(t('system.serviceProviders.list.loadFail'), 'error');
} finally {
loading.value = false;
}
}
// 加载服务商接口列表
async function loadInterfaceList() {
if (!selectedProvider.value) return;
interfaceLoading.value = true;
try {
const response = await postInterfaceInterfacePaginatedList({
pageIndex: 1,
pageSize: 1000,
serviceProviderId: selectedProvider.value.id,
isUpstream: false,
});
const data = (response as any)?.items || (response as any)?.data?.items || (response as any)?.list || [];
interfaceList.value = data;
} catch (error) {
$baseMessage(t('system.serviceProviders.interfaceList.loadFail'), 'error');
} finally {
interfaceLoading.value = false;
}
}
function handleProviderSelect(provider: any) {
selectedProvider.value = provider;
selectedInterface.value = null; // 清空选中的接口
loadInterfaceList(); // 加载接口列表
// 使用 requestAnimationFrame 确保在下一帧渲染时选中效果已应用
nextTick(() => {
requestAnimationFrame(() => {
// 再次等待一帧,确保 CSS 过渡效果已应用
requestAnimationFrame(() => {
const selectedElement = document.querySelector(`.provider-item.selected`);
if (selectedElement) {
selectedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
});
});
}
// 选择接口
async function selectInterface(interfaceItem: any) {
// 检查是否选择了不同的接口
const isDifferentInterface = !selectedInterface.value || selectedInterface.value.id !== interfaceItem.id;
if (isDifferentInterface && interfaceDetailTab.value === 'detail') {
const canLeave = await confirmLeaveInterfaceDetail();
if (!canLeave) return;
}
selectedInterface.value = interfaceItem;
// 确保切换到详情tab
interfaceDetailTab.value = 'detail';
// 只有在选择不同接口时才重新初始化组件
if (isDifferentInterface) {
interfaceDetailLoading.value = true;
detailRefreshKey.value++; // 改变key触发组件重新渲染
try {
// 等待组件渲染完成
await nextTick();
await nextTick();
setTimeout(() => {
if (interfaceDetailModalRef.value && interfaceDetailTab.value === 'detail') {
const interfaceId = interfaceItem.interfaceId || interfaceItem.id;
interfaceDetailModalRef.value.init({
id: interfaceItem.id,
interfaceId: interfaceId,
serviceProviderId: selectedProvider.value?.id,
serviceProviderName: selectedProvider.value?.name,
mode: 'edit',
forceRefresh: false
});
}
}, 100);
} finally {
interfaceDetailLoading.value = false;
}
}
}
async function confirmLeaveInterfaceDetail(): Promise<boolean> {
if (interfaceDetailModalRef.value?.confirmLeaveUnsaved) {
return interfaceDetailModalRef.value.confirmLeaveUnsaved();
}
if (!interfaceDetailModalRef.value?.hasUnsavedChanges?.()) return true;
return new Promise((resolve) => {
$baseConfirm({
title: '提示',
content: '当前接口明细有未保存的更改,离开后将丢失。保存成功后如需生效还需发布。是否继续离开?',
okText: '继续离开',
cancelText: '继续编辑',
onOk: () => resolve(true),
onCancel: () => resolve(false),
});
});
}
// 中间接口列表切换且当前在调试 tab:刷新调试面板并重新导入 cURL
watch(
() => selectedInterface.value?.id,
async (newId, oldId) => {
if (!newId || newId === oldId) return;
if (interfaceDetailTab.value === 'debug') {
apiDebuggerRefreshKey.value++;
await nextTick();
importCurlToDebugger();
}
}
);
// 接口列表操作函数
function addInterface() {
if (!selectedProvider.value) {
$baseMessage(t('system.serviceProviders.interface.needProvider'), 'warning');
return;
}
// 设置弹窗标题
interfaceModalTitle.value = '新增服务商接口';
// 打开新增接口弹窗
interfaceModalVisible.value = true;
nextTick(() => {
interfaceModalRef.value?.init({
serviceProviderId: selectedProvider.value.id,
serviceProviderName: selectedProvider.value.name,
mode: 'add'
});
});
}
function deleteInterface() {
// 在新的布局中,删除功能通过单个接口的删除按钮实现
$baseMessage('请通过接口列表中的删除按钮进行删除', 'warning');
}
function editInterface(row: any) {
if (!selectedProvider.value) {
$baseMessage(t('system.serviceProviders.interface.needProvider'), 'warning');
return;
}
// 设置弹窗标题
interfaceModalTitle.value = '修改服务商接口';
// 打开修改接口弹窗
interfaceModalVisible.value = true;
nextTick(() => {
interfaceModalRef.value?.init({
id: row.id,
serviceProviderId: selectedProvider.value.id,
serviceProviderName: selectedProvider.value.name,
mode: 'edit'
});
});
}
function deleteSingleInterface(row: any) {
$baseConfirm({
title: t('system.serviceProviders.interface.deleteConfirmTitle'),
content: t('system.serviceProviders.interface.deleteConfirmContent', { name: row.name }),
onOk: async () => {
try {
await postInterfaceBatchDeleteServiceProviderInterfaceByIds([row.id]);
$baseMessage('删除成功', 'success');
// 重新加载接口列表
loadInterfaceList();
} catch (error) {
$baseMessage('删除失败', 'error');
}
}
});
}
// 删除接口
async function deleteInterfaceClick(interfaceItem: any) {
if (!interfaceItem || !interfaceItem.id) {
$baseMessage(t('system.serviceProviders.interface.invalid'), 'warning');
return;
}
$baseConfirm({
title: t('system.serviceProviders.interface.deleteConfirmTitle'),
content: t('system.serviceProviders.interface.deleteConfirmContent', { name: interfaceItem.name }),
onOk: async () => {
try {
await postInterfaceBatchDeleteServiceProviderInterfaceByIds([interfaceItem.id]);
$baseMessage('删除成功', 'success');
// 重新加载接口列表
loadInterfaceList();
// 如果删除的是当前选中的接口,清空选中状态
if (selectedInterface.value?.id === interfaceItem.id) {
selectedInterface.value = null;
}
} catch (error) {
$baseMessage('删除失败', 'error');
}
},
});
}
// 编辑接口
function editInterfaceClick(interfaceItem: any) {
selectInterface(interfaceItem);
}
function handleSearch() {
loadServiceProviderList();
}
function handleAddProvider() {
addClick();
}
function handleRefresh() {
loadServiceProviderList();
}
// 接口弹窗关闭
function handleInterfaceModalClose() {
interfaceModalVisible.value = false;
}
// 接口保存成功(关弹窗、刷新,并引导发布)
function handleInterfaceSaved() {
interfaceModalVisible.value = false;
loadInterfaceList();
loadServiceProviderList();
if (selectedInterface.value && interfaceDetailTab.value === 'detail' && interfaceDetailModalRef.value) {
nextTick(() => {
const interfaceId = selectedInterface.value.interfaceId || selectedInterface.value.id;
interfaceDetailModalRef.value.init({
id: selectedInterface.value.id,
interfaceId: interfaceId,
serviceProviderId: selectedProvider.value?.id,
serviceProviderName: selectedProvider.value?.name,
mode: 'edit',
forceRefresh: true,
});
});
}
askGoPublish(selectedProvider.value);
}
// ModalPro确定按钮点击
async function handleInterfaceSave() {
if (interfaceModalRef.value) {
try {
await interfaceModalRef.value.saveInterface({ skipSuccessToast: true });
return true; // 返回 true 表示成功,允许弹窗关闭
} catch (error) {
return false; // 返回 false 表示失败,阻止弹窗关闭
}
}
return false;
}
// 表格操作方法
function isRowEditing(row: any) {
return editingRows.value.has(row.id);
}
function isRowEditing1(row: any) {
return editingRows1.value.has(row.id);
}
function editRow(row: any) {
editingRows.value.add(row.id);
}
function editRow1(row: any) {
editingRows1.value.add(row.id);
}
function saveRow(row: any) {
editingRows.value.delete(row.id);
}
function saveRow1(row: any) {
editingRows1.value.delete(row.id);
}
function deleteRow(row: any) {
const index = interfaceDetailTableProps.data.findIndex((item: any) => item.id === row.id);
if (index > -1) {
interfaceDetailTableProps.data.splice(index, 1);
}
}
function deleteRow1(row: any) {
const index = interfaceDetailTableProps1.data.findIndex((item: any) => item.id === row.id);
if (index > -1) {
interfaceDetailTableProps1.data.splice(index, 1);
}
}
function addNewRequestParam() {
const newParam: any = {
id: Date.now(),
name: '',
cnName: '',
type: 'String',
description: '',
isRequired: false,
};
interfaceDetailTableProps.data.push(newParam);
editingRows.value.add(newParam.id);
}
function addNewResponseParam() {
const newParam: any = {
id: Date.now(),
name: '',
cnName: '',
type: 'String',
description: '',
isRequired: false,
};
interfaceDetailTableProps1.data.push(newParam);
editingRows1.value.add(newParam.id);
}
function getParameterTypeName(type: string) {
const typeMap: { [key: string]: string } = {
'String': '字符串',
'Integer': '整数',
'Boolean': '布尔值',
'Array': '数组',
'Object': '对象',
};
return typeMap[type] || type;
}
function drawerClick() {
// 解析请求参数
}
function drawerClick1() {
// 解析返回参数
}
// 加载参数类型列表
async function loadParameterTypes() {
try {
const response: any = await getDictionaryGetList({ type: 'ParameterType' });
const responseData = response?.data || response;
ParameterTypeList.value = Array.isArray(responseData) ? responseData : [];
} catch (error) {
}
}
// 接口详情操作函数
function handleInterfaceCancel() {
// 重置表单数据
Object.assign(interfaceDetailForm, {
optionType: 1,
id: 0,
name: '',
abbreviation: '',
requestProtocol: '',
requestMethod: 'POST',
requestAddress: '',
contentType: '',
responseContentType: '',
isDisableAuth: false,
qps: 1,
mappedParamType: '',
description: '',
serviceProviderId: 0,
inParameterList: [],
outParameterList: [],
});
// 清空表格数据
interfaceDetailTableProps.data = [];
interfaceDetailTableProps1.data = [];
// 清空编辑状态
editingRows.value.clear();
editingRows1.value.clear();
// 清空选中的接口
selectedInterface.value = null;
}
// 格式化请求方式
function formatRequestMethod(method: any) {
if (!method) return 'POST';
// 如果是数字,转换为对应的HTTP方法
if (typeof method === 'number') {
const methodMap: { [key: number]: string } = {
1: 'GET',
2: 'POST',
3: 'PUT',
4: 'DELETE',
5: 'PATCH',
6: 'HEAD',
7: 'OPTIONS'
};
return methodMap[method] || 'POST';
}
// 如果是字符串,直接返回大写形式
return String(method).toUpperCase();
}
// 获取请求方式的CSS类名
function getMethodClass(method: any) {
const formattedMethod = formatRequestMethod(method);
return `method-${formattedMethod.toLowerCase()}`;
}
async function handleInterfaceDetailSave() {
if (!interfaceDetailModalRef.value) return;
// 发布确认已含「保存成功」;刷新 / 引导发布由 saveInterface → @saved 统一处理
try {
await interfaceDetailModalRef.value.saveInterface({ skipSuccessToast: true });
} catch {
/* 子组件已提示错误 */
}
}
function handleInterfaceDetailCancel() {
// 取消编辑,重新加载接口详情
if (selectedInterface.value && interfaceDetailModalRef.value) {
nextTick(() => {
interfaceDetailModalRef.value.init({
id: selectedInterface.value.id,
serviceProviderId: selectedProvider.value?.id,
serviceProviderName: selectedProvider.value?.name,
mode: 'edit'
});
});
}
}
// 点击调试 / 切换 API 调试 tab:获取请求容器的 cURL 并导入到 API 调试
const curlToImport = ref('');
function importCurlToDebugger() {
const curl = interfaceDetailModalRef.value?.getRequestExample?.() || '';
if (curl && curl.trim()) {
curlToImport.value = curl;
nextTick(() => {
setTimeout(() => { curlToImport.value = ''; }, 500);
});
}
}
async function handleDebugClick() {
const canLeave = await confirmLeaveInterfaceDetail();
if (!canLeave) return;
interfaceDetailTab.value = 'debug';
}
async function switchInterfaceDetailTab(tab: string) {
if (tab === interfaceDetailTab.value) return;
if (interfaceDetailTab.value === 'detail' && tab !== 'detail') {
const canLeave = await confirmLeaveInterfaceDetail();
if (!canLeave) return;
}
interfaceDetailTab.value = tab;
}
// 切换至 API 调试 tab 时也解析 cURL
watch(interfaceDetailTab, (tab) => {
if (tab === 'debug') importCurlToDebugger();
});
onMounted(() => {
getAppList();
loadServiceProviderList();
loadParameterTypes();
});
</script>
<template>
<div class="service-provider-layout">
<!-- 左侧服务商列表:自定义左菜单 -->
<CustomLeftMenu
class="left-sidebar nav-panel"
:tree-data="providerTreeData"
:selected-keys="providerSelectedKeys"
v-model:expanded-keys="providerExpandedKeys"
:loading="loading"
v-model:search-keyword="searchKeyword"
:search-placeholder="t('system.serviceProviders.search.placeholder')"
:refresh-title="t('system.serviceProviders.search.refresh')"
:add-title="t('system.serviceProviders.search.addProvider')"
loading-text="正在加载服务商列表..."
:actions-title="t('common.actions')"
:show-method="false"
:show-map-count="false"
@search="handleSearch"
@refresh="handleRefresh"
@add="handleAddProvider"
@select="handleProviderSelectFromTree"
@node-click="(key) => { const id = key.startsWith('item-') ? Number(key.slice(5)) : null; if (id != null) { const p = serviceProviderList.find((x: any) => x.id === id); if (p) handleProviderSelect(p); } }"
>
<template #node-menu="{ nodeData }">
<a-doption @click="handleParameterConfigWithSelect(nodeData)">
<IconSettings style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.authConfig') }}
</a-doption>
<a-doption @click="handleFixedParamsWithSelect(nodeData)">
<IconList style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.fixedParams') }}
</a-doption>
<a-doption @click="accountManageClick(nodeData)">
<IconUser style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.accountManage') }}
</a-doption>
<a-doption @click="handleEditWithSelect(nodeData, false)">
<IconEdit style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.edit') }}
</a-doption>
<a-doption @click="handleEditWithSelect(nodeData, true)">
<IconCopy style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.copy') }}
</a-doption>
<a-doption @click="onDerive(nodeData)">
<IconDownload style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.export') }}
</a-doption>
<a-doption @click="deleteProviderClick(nodeData)">
<IconDelete style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.delete') }}
</a-doption>
</template>
</CustomLeftMenu>
<!-- 中间接口列表:壳常驻,未选服务商时列表区为空提示 -->
<div class="middle-content">
<CustomLeftMenu
class="middle-sidebar"
width="100%"
:tree-data="interfaceTreeData"
:selected-keys="interfaceSelectedKeys"
:loading="interfaceLoading"
v-model:search-keyword="interfaceSearchKeyword"
:search-placeholder="'请输入接口名称'"
:show-method="false"
:show-map-count="false"
:show-pagination="false"
:show-node-index="true"
:show-node-code="true"
:refresh-title="t('system.serviceProviders.interfaceList.refresh')"
:add-title="t('system.serviceProviders.interfaceList.add')"
:loading-text="t('system.serviceProviders.interfaceList.loading')"
:empty-text="
selectedProvider
? '暂无数据'
: t('system.serviceProviders.interfaceList.selectProvider')
"
actions-title="操作"
delete-label="删除"
@search="handleInterfaceSearch"
@refresh="loadInterfaceList"
@add="addInterface"
@select="handleInterfaceSelectFromTree"
@node-click="handleInterfaceNodeClick"
@delete-node="handleInterfaceDeleteNode"
>
<template #node-menu="{ nodeData }">
<a-doption @click="handleInterfaceDeleteNode(nodeData)">
<IconDelete style="margin-right: 8px;" />
删除
</a-doption>
</template>
</CustomLeftMenu>
</div>
<!-- 右侧接口详情区域 -->
<div class="right-content content-panel">
<div class="interface-detail-container">
<!-- Tab 壳常驻,未选接口时内容区仍为空提示 -->
<div class="interface-detail-tabs">
<div class="tabs-header">
<SlidingTabs
:model-value="interfaceDetailTab"
:items="interfaceDetailTabItems"
@change="switchInterfaceDetailTab"
/>
<div v-if="selectedProvider" class="tabs-extra">
<a-button
size="small"
class="common-params-entry"
title="服务商公共参数,发布映射后对该服务商下接口生效"
@click="openCommonParamsFromTabs"
>
公共参数{{
commonParamsLoading
? ''
: commonParamsCount != null
? ` · ${commonParamsCount}`
: ''
}}
</a-button>
</div>
</div>
</div>
<div class="interface-detail-tab-content">
<template v-if="selectedInterface">
<!-- 接口明细内容 -->
<div v-show="interfaceDetailTab === 'detail'" class="detail-content">
<a-spin :loading="interfaceDetailLoading" tip="加载接口详情中..." class="detail-spin-wrapper">
<ServiceProviderInterfaceModal
:key="`interface-detail-${selectedInterface?.id}-${detailRefreshKey}`"
ref="interfaceDetailModalRef"
@saved="handleInterfaceSaved"
@return-config-saved="askGoPublish"
>
<template #headerActions>
<a-button
size="small"
title="将当前 cURL 带入 API 调试"
@click="handleDebugClick"
>
发送
</a-button>
<a-button
size="small"
class="app-btn-primary-soft"
@click="handleInterfaceDetailSave"
>
{{ t('system.serviceProviders.detail.actions.save') }}
</a-button>
</template>
</ServiceProviderInterfaceModal>
</a-spin>
</div>
<!-- API调试组件 -->
<div v-show="interfaceDetailTab === 'debug'" class="debug-content">
<ApiDebugger
:key="`api-debugger-${selectedProvider?.id || ''}-${selectedInterface?.id || ''}-${apiDebuggerRefreshKey}`"
:selected-interface="selectedInterface"
:selected-service-provider="selectedProvider"
:enable-defaults="false"
:curl-to-import="curlToImport"
/>
</div>
</template>
<div v-else class="empty-state">
<a-empty :description="t('system.serviceProviders.interfaceList.empty')" />
</div>
</div>
</div>
</div>
<!-- 模态框 -->
<ModalPro :width="760" @ok="saveFn" @register="register">
<div class="sp-modal-form">
<a-alert type="info" :show-icon="false" class="modal-form-tip">
维护下游服务商基础信息与调用超时;代码创建后请勿随意变更,以免影响已有对接。
</a-alert>
<FormPro ref="dialogFormRef" v-bind="dialogConfig" :required-position="'none'">
<template #sectionBasic>
<div class="form-section-title">基础信息</div>
</template>
<template #sectionRuntime>
<div class="form-section-title">调用与说明</div>
</template>
</FormPro>
</div>
</ModalPro>
<!-- 服务商接口弹窗 -->
<ModalPro
class="interface-modal-tall"
:title="interfaceModalTitle"
:width="1500"
:modal-style="{ height: '900px' }"
:visible="interfaceModalVisible"
:footer="true"
@ok="handleInterfaceSave"
@cancel="handleInterfaceModalClose"
@close="handleInterfaceModalClose"
>
<ServiceProviderInterfaceModal
ref="interfaceModalRef"
@close="handleInterfaceModalClose"
@saved="handleInterfaceSaved"
@return-config-saved="askGoPublish"
/>
</ModalPro>
<ParameterConfig
v-if="parameterConfigFlagShow"
ref="parameterConfigFlag"
@saved="askGoPublish"
/>
<FixedParameterConfig ref="fixedParameterConfigRef" @saved="onFixedParamsSaved" />
<AccountList ref="accountList" @saved="askGoPublish" />
<ProviderMapPublishModal ref="providerPublishRef" />
</div>
</template>
<style lang="less" scoped>
.sp-modal-form {
.modal-form-tip {
margin-bottom: 16px;
}
.form-section-title {
margin: 0 0 12px;
padding: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-2);
}
:deep(.arco-form-item-label-col) {
.arco-form-item-label-required,
.arco-form-item-label-asterisk,
.arco-form-item-label-required-symbol {
display: none !important;
}
.arco-form-item-label {
padding-left: 0 !important;
}
}
:deep(.arco-input-wrapper):not(.sidebar-search-input-wrapper),
:deep(.arco-select-view):not(.sidebar-search-input-wrapper),
:deep(.arco-input-number):not(.sidebar-search-input-wrapper) {
height: 32px;
}
:deep(.arco-textarea-wrapper) {
height: auto !important;
min-height: 72px;
}
:deep(.arco-input):not(.sidebar-search-input) {
border-color: var(--color-neutral-3);
&:hover,
&:focus {
border-color: var(--app-primary-color);
}
}
:deep(.arco-select):not(.sidebar-search-input-wrapper) {
width: 100%;
.arco-select-view {
border-color: var(--color-neutral-3);
&:hover {
border-color: var(--app-primary-color);
}
}
}
:deep(.arco-input-number):not(.sidebar-search-input-wrapper) {
border-color: var(--color-neutral-3);
&:hover,
&:focus-within {
border-color: var(--app-primary-color);
}
}
}
.service-provider-layout {
display: flex;
height: calc(100vh - 86px);
max-height: calc(100vh - 86px);
padding: 16px;
gap: 16px;
background-color: var(--app-page-bg);
overflow: hidden;
box-sizing: border-box;
width: 100%;
}
/* 左侧使用自定义左菜单组件,仅保留布局 */
.left-sidebar {
width: 280px;
flex-shrink: 0;
height: 100%;
}
.action-btn {
width: 28px;
height: 28px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.vertical-dots-icon {
display: flex;
flex-direction: column;
gap: 2px;
span {
width: 3px;
height: 3px;
background: rgba(255, 255, 255, 0.7);
border-radius: 50%;
}
}
.middle-content {
flex: 1;
max-width: 280px;
min-width: 0;
height: 100%;
min-height: 0;
align-self: stretch;
display: flex;
flex-direction: column;
overflow: hidden;
background: transparent;
border: none;
box-shadow: none;
}
.middle-sidebar {
width: 100% !important;
flex: 1 1 auto !important;
flex-shrink: 1 !important;
min-height: 0 !important;
height: 100% !important;
}
.middle-empty {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--app-card-bg);
border: 1px solid rgba(15, 16, 17, 0.08);
border-radius: 12px;
}
.right-content {
flex: 1;
background-color: var(--app-card-bg);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.interface-detail-container {
display: flex;
flex-direction: column;
height: 100%;
.interface-detail-tabs {
margin-bottom: 16px;
flex-shrink: 0;
border-bottom: 1px solid var(--color-border-2);
.tabs-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 20px;
min-height: 48px;
.tabs-extra {
margin-left: auto;
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
.common-params-entry {
color: var(--color-text-2);
}
}
}
}
.interface-detail-tab-content {
flex: 1;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
.detail-content {
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
box-sizing: border-box;
min-height: 0;
.detail-spin-wrapper {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
:deep(.arco-spin) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
}
:deep(.arco-spin-container) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
}
}
}
.debug-content {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px;
box-sizing: border-box;
min-height: 0;
}
}
}
.tab-actions {
padding: 16px 0;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 16px;
flex-shrink: 0;
}
.detail-content {
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
box-sizing: border-box;
min-height: 0;
}
.debug-content {
flex: 1;
min-height: 0;
overflow: hidden;
padding: 12px;
box-sizing: border-box;
}
.empty-state {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
width: 100%;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-text-3);
p {
margin-top: 12px;
font-size: 14px;
}
}
:deep(.arco-modal-body) {
padding: 12px;
height: auto !important;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
:deep(.arco-form) {
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
}
/* Form items in details */
.interface-detail-container :deep(.arco-form-item) {
margin-bottom: 16px; /* Standard spacing */
overflow: visible;
flex-shrink: 0; /* Prevent item compression */
}
/* Ensure form fields are visible */
:deep(.arco-form-item-label-col) {
overflow: visible;
}
:deep(.arco-form-item-control) {
overflow: visible;
min-height: auto;
}
/* 使用上游接口弹窗的样式风格 */
.detail-content :deep(.arco-input-wrapper),
.detail-content :deep(.arco-select-view),
.detail-content :deep(.arco-input-number),
.detail-content :deep(.arco-textarea-wrapper) {
height: 32px;
}
.detail-content :deep(.arco-textarea-wrapper) {
min-height: 32px;
}
.detail-content :deep(.arco-input) {
border-color: var(--color-neutral-3);
&:hover,
&:focus {
border-color: var(--app-primary-color);
}
}
.detail-content :deep(.arco-select) {
width: 100%;
.arco-select-view {
border-color: var(--color-neutral-3);
&:hover {
border-color: var(--app-primary-color);
}
}
}
.detail-content :deep(.arco-input-number) {
border-color: var(--color-neutral-3);
&:hover,
&:focus-within {
border-color: var(--app-primary-color);
}
}
/* 使用上游接口弹窗的样式风格 */
:deep(.ServiceProviderInterfaceModal .arco-input-wrapper),
:deep(.ServiceProviderInterfaceModal .arco-select-view),
:deep(.ServiceProviderInterfaceModal .arco-input-number),
:deep(.ServiceProviderInterfaceModal .arco-textarea-wrapper) {
height: 32px;
}
:deep(.ServiceProviderInterfaceModal .arco-textarea-wrapper) {
min-height: 32px;
}
:deep(.ServiceProviderInterfaceModal .arco-input) {
border-color: var(--color-neutral-3);
&:hover,
&:focus {
border-color: var(--app-primary-color);
}
}
:deep(.ServiceProviderInterfaceModal .arco-select) {
width: 100%;
.arco-select-view {
border-color: var(--color-neutral-3);
&:hover {
border-color: var(--app-primary-color);
}
}
}
:deep(.ServiceProviderInterfaceModal .arco-input-number) {
border-color: var(--color-neutral-3);
&:hover,
&:focus-within {
border-color: var(--app-primary-color);
}
}
</style>