front/src/views/api/downstreamInterface/index.vue
zzh ffe8679f82
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m42s
chore: 清理无用接口表页与构建优化,次要列表改 a-table
保留核心 InterfaceDetail/ApiTest 的 vxe;去掉死代码映射页,预警/账户等次要表迁 a-table;构建默认跳过 brotli、mermaid 分包;面单按需 EP;修复标签刷新竞态与 COS 打印鉴权。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 15:46:57 +08:00

2118 lines
60 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 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 ServiceProviderInterfaceModal from './components/ServiceProviderInterfaceModal.vue';
import AccountList from './comps/AccountList.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,
} from '@arco-design/web-vue/es/icon';
import { useI18n } from 'vue-i18n';
defineOptions({
name: 'DownstreamInterface',
});
const { t } = useI18n();
/** 保存成功统一提示(含发布提醒,避免 success + warning 连弹) */
function tipSaveOk(action = '保存') {
Message.success(`${action}成功,如需生效请去发布`);
}
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 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({
itemList: [
{
label: '服务商名称',
field: 'name',
comp: 'AInput',
compProps: {
placeholder: '* 请填写服务商名称',
},
rules: [
{
required: true,
message: '输入不能为空',
trigger: 'change',
},
{
max: 200,
message: '请控制长度在200个字符之内',
trigger: 'change',
},
],
span: 6,
},
{
label: '服务商类型',
field: 'types',
comp: 'SelectPro',
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: 6,
},
{
label: '服务商代码',
field: 'code',
comp: 'AInput',
compProps: {
placeholder: '* 请填写服务商代码',
// disabled: !flag.value,
},
rules: [
{
required: true,
message: '输入不能为空',
trigger: 'change',
},
{
max: 30,
message: '请控制长度在30个字符之内',
trigger: 'change',
},
{
pattern: /^[a-z,A-Z,0-9]+$/,
message: '只能输入字母和数字组合',
trigger: 'change',
},
],
span: 6,
},
{
label: '时区',
field: 'traceTimeZone',
comp: 'AInput',
compProps: {
maxlength: 50,
placeholder: '请输入时区',
},
span: 6,
},
{
label: '流程Code',
field: 'flowCode',
comp: 'AInput',
compProps: {
placeholder: '请输入流程Code',
},
span: 6,
},
{
label: '最大超时',
field: 'timeout',
comp: 'AInput',
compProps: {
placeholder: '请输入最大超时(秒)',
maxlength: 50,
},
rules: [
{
pattern: /^[1-9]\d*$/,
message: '只能输入正整数',
trigger: 'change',
},
],
span: 6,
},
{
label: '描述',
field: 'description',
comp: 'AInput',
compProps: {
placeholder: '请输入接口备注',
type: 'textarea',
rows: 4,
allowClear: true,
},
rules: [
{
max: 200,
message: '请控制长度在200个字符之内',
trigger: 'change',
},
],
span: 6,
},
],
formLayout: 'vertical',
buttonConfig: {
showBtn: false,
},
});
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('新增');
loadServiceProviderList();
modal.closeModal();
});
} else {
await postServiceProviderUpdate({
...fromValue,
Id: undefined,
id: edId.value,
}).then(() => {
tipSaveOk('修改');
loadServiceProviderList();
modal.closeModal();
});
}
} else {
await postServiceProviderCopy({
...fromValue,
Id: undefined,
id: edId.value,
}).then(() => {
tipSaveOk('创建');
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();
function accountManageClick(row: any) {
accountList.value.init(row.id);
}
function mapAccountClick(row: { id: any; code: any }) {
accountList.value.init(row.id);
}
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,
});
});
}
}
// ModalPro确定按钮点击
async function handleInterfaceSave() {
if (interfaceModalRef.value) {
try {
await interfaceModalRef.value.saveInterface();
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 统一处理,避免连弹 2~3 次
try {
await interfaceDetailModalRef.value.saveInterface();
} 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="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="handleParameterConfigWithSelect(nodeData)">
<IconSettings style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.authConfig') }}
</a-doption>
<a-doption @click="accountManageClick(nodeData)">
<IconUser style="margin-right: 8px;" />
{{ t('system.serviceProviders.dropdown.accountManage') }}
</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 class="tabs-extra" v-if="selectedInterface && interfaceDetailTab !== 'detail'"></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"
>
<template #headerActions>
<a-button
size="small"
title="将当前 cURL 带入 API 调试"
@click="handleDebugClick"
>
调试
</a-button>
<a-button size="small" type="primary" @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="50%" @ok="saveFn" @register="register">
<div class="sp-modal-form">
<FormPro ref="dialogFormRef" v-bind="dialogConfig" :required-position="'none'" />
</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"
/>
</ModalPro>
<ParameterConfig v-if="parameterConfigFlagShow" ref="parameterConfigFlag" />
<AccountList ref="accountList" />
</div>
</template>
<style lang="less" scoped>
.sp-modal-form {
: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),
:deep(.arco-textarea-wrapper):not(.sidebar-search-input-wrapper) {
height: 32px;
}
:deep(.arco-textarea-wrapper):not(.sidebar-search-input-wrapper) {
min-height: 32px;
}
: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;
}
}
}
.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>