All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m25s
下单/H5 仓库地址展示与排序备注;映射明细加载优化与发布后保留选中;接口明细加载期不再误报未保存。 Co-authored-by: Cursor <cursoragent@cursor.com>
1009 lines
33 KiB
Vue
1009 lines
33 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, ref, unref, watch, type PropType } from 'vue';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon';
|
||
|
||
import {
|
||
getEditMapDetails,
|
||
getGetMapParameterCascadedList,
|
||
} from '@/api/InterfaceMappingServices/interfaceMap';
|
||
|
||
import AddParameter from './AddParameter.vue';
|
||
import MapAiAssistModal from './MapAiAssistModal.vue';
|
||
|
||
const { t } = useI18n();
|
||
const mapAiVisible = ref(false);
|
||
|
||
const props = defineProps({
|
||
data: {
|
||
type: Object as PropType<any>,
|
||
default: () => {
|
||
return {};
|
||
},
|
||
},
|
||
serviceProviderName: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
|
||
serviceProviderId: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
interfaceId: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
});
|
||
const emits = defineEmits(['save-model-enter', 'step-state']);
|
||
const tableRef = ref();
|
||
const tableRef1 = ref();
|
||
const active = ref(0);
|
||
const AddParameterRef = ref();
|
||
const maxCompletedIndex = ref<number>(-1);
|
||
const outputAttention = ref(false);
|
||
const outputStepRef = ref<HTMLElement | null>(null);
|
||
let outputAttentionTimer: ReturnType<typeof setTimeout> | null = null;
|
||
/** 当前已加载的映射上下文,避免切 tab 重复拉取 */
|
||
const loadedContextKey = ref('');
|
||
let initSeq = 0;
|
||
let inflightInit: Promise<void> | null = null;
|
||
let inflightInitKey = '';
|
||
const detailLoading = ref(false);
|
||
|
||
interface Indexable {
|
||
[key: string]: any;
|
||
}
|
||
|
||
const tableProps1 = computed(() => {
|
||
return {
|
||
columnsKey: 'mapDetails111',
|
||
showPager: false,
|
||
useForm: false,
|
||
autoHeight: false,
|
||
tableClassName: 'px-0!',
|
||
editConfig: { trigger: 'click', mode: 'cell', autoClear: false },
|
||
columns: [
|
||
{
|
||
field: 'alias',
|
||
title: t('system.interface.mapping.table.systemParamName'),
|
||
minWidth: 120,
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: t('system.interface.mapping.table.systemParamNameCn'),
|
||
minWidth: 120,
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: t('system.interface.mapping.table.paramType'),
|
||
minWidth: 120,
|
||
slots: { default: 'typeResp' },
|
||
},
|
||
{
|
||
field: 'mapAlias',
|
||
title: t('system.interface.mapping.table.providerParamName'),
|
||
minWidth: 120,
|
||
slots: {
|
||
edit: 'mapAlias',
|
||
},
|
||
editRender: {},
|
||
},
|
||
{
|
||
field: 'mapDescription',
|
||
title: t('system.interface.mapping.table.providerParamNameCn'),
|
||
minWidth: 130,
|
||
},
|
||
// 去掉"必填/参数描述"列
|
||
],
|
||
tableConfig: {
|
||
data: mapDetails.value,
|
||
// 让表格根据内容自适应,由外层容器负责滚动
|
||
height: undefined,
|
||
maxHeight: undefined,
|
||
},
|
||
};
|
||
});
|
||
const tableProps = computed(() => {
|
||
return {
|
||
columnsKey: 'mapDetails2',
|
||
showPager: false,
|
||
useForm: false,
|
||
autoHeight: false,
|
||
showToolbar: true,
|
||
tableClassName: 'px-0!',
|
||
editConfig: { trigger: 'click', mode: 'cell', autoClear: false },
|
||
columns: [
|
||
{
|
||
field: 'alias',
|
||
title: t('system.interface.mapping.table.providerParamName'),
|
||
minWidth: 120,
|
||
},
|
||
{
|
||
field: 'description',
|
||
title: t('system.interface.mapping.table.providerParamNameCn'),
|
||
minWidth: 130,
|
||
},
|
||
{
|
||
field: 'type',
|
||
title: t('system.interface.mapping.table.paramType'),
|
||
minWidth: 60,
|
||
slots: { default: 'typeReq' },
|
||
},
|
||
// 去掉"必填/参数描述"列
|
||
{
|
||
field: 'fixedValue',
|
||
title: t('system.interface.mapping.table.paramValue'),
|
||
minWidth: 120,
|
||
slots: {
|
||
header: 'fixedValueHeader',
|
||
edit: 'fixedValueEdit',
|
||
},
|
||
editRender: {},
|
||
},
|
||
{
|
||
field: 'mapAlias',
|
||
title: t('system.interface.mapping.table.inputParam'),
|
||
slots: {
|
||
edit: 'mapAliasEdit',
|
||
},
|
||
minWidth: 120,
|
||
editRender: {},
|
||
},
|
||
{
|
||
field: 'mapDescription',
|
||
title: t('system.interface.mapping.table.inputParamCn'),
|
||
minWidth: 130,
|
||
},
|
||
],
|
||
tableConfig: {
|
||
// 让表格根据内容自适应,由外层容器负责滚动
|
||
height: undefined,
|
||
maxHeight: undefined,
|
||
},
|
||
};
|
||
});
|
||
const inParaMapList = ref<API.MapSummary[]>([]);
|
||
const mapDetails = ref<API.InterfaceParameterMapOutputValueObject[]>([]);
|
||
|
||
const activeInParaItem = computed(() => {
|
||
if (active.value < 0) return null;
|
||
return inParaMapList.value[active.value] || null;
|
||
});
|
||
|
||
function setActive(index: number) {
|
||
active.value = index;
|
||
}
|
||
|
||
function isOutputRowMapped(row: any): boolean {
|
||
const id = Number(row?.mapId);
|
||
if (Number.isFinite(id) && id > 0) return true;
|
||
return !!(row?.mapAlias && String(row.mapAlias).trim());
|
||
}
|
||
|
||
/** 返参一行都没映射,或必填返参还空着 */
|
||
const outputNeedsAttention = computed(() => {
|
||
const rows = mapDetails.value || [];
|
||
if (!rows.length) return false;
|
||
if (!rows.some((row) => isOutputRowMapped(row))) return true;
|
||
return rows.some((row) => isRequiredFlag(row.isRequired) && !isOutputRowMapped(row));
|
||
});
|
||
|
||
function focusOutputMapping() {
|
||
active.value = -1;
|
||
outputAttention.value = false;
|
||
nextTick(() => {
|
||
outputStepRef.value?.scrollIntoView?.({ inline: 'nearest', block: 'nearest' });
|
||
outputAttention.value = true;
|
||
if (outputAttentionTimer) clearTimeout(outputAttentionTimer);
|
||
outputAttentionTimer = setTimeout(() => {
|
||
outputAttention.value = false;
|
||
}, 1800);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 保存前检查返参。
|
||
* goto-output:人还在入参步骤,先切到返参,本次不保存。
|
||
* required:已在返参步骤,但必填未填。
|
||
*/
|
||
function guardBeforeSave(): 'ok' | 'goto-output' | 'required' {
|
||
if (active.value !== -1 && outputNeedsAttention.value) {
|
||
focusOutputMapping();
|
||
return 'goto-output';
|
||
}
|
||
const missing = (mapDetails.value || []).findIndex(
|
||
(row) => isRequiredFlag(row.isRequired) && !isOutputRowMapped(row)
|
||
);
|
||
if (missing >= 0) {
|
||
active.value = -1;
|
||
getReturnVali(missing);
|
||
return 'required';
|
||
}
|
||
return 'ok';
|
||
}
|
||
|
||
function getActiveIndex() {
|
||
return active.value;
|
||
}
|
||
|
||
watch(
|
||
[active, outputNeedsAttention],
|
||
() => {
|
||
emits('step-state', {
|
||
onOutput: active.value === -1,
|
||
needsAttention: outputNeedsAttention.value,
|
||
});
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
function loadAll() {
|
||
return [{ value: '$FromAccount', key: 1 }];
|
||
}
|
||
|
||
const cascadedList = ref<API.MapParameterCascadedOutput[]>([]);
|
||
|
||
const getCascadedList = async (parms: any) => {
|
||
const resp = await getGetMapParameterCascadedList(parms);
|
||
const list = Array.isArray((resp as any)?.data) ? (resp as any).data : Array.isArray(resp) ? (resp as any) : [];
|
||
|
||
cascadedList.value = list.map((e: API.MapParameterCascadedOutput) => {
|
||
return {
|
||
...e,
|
||
id: e.interfaceId,
|
||
};
|
||
});
|
||
invalidateCascaderOptionsCache();
|
||
};
|
||
onMounted(() => {
|
||
restaurants.value = loadAll();
|
||
// 面板首次挂载时自行拉数,避免依赖父级 setTimeout
|
||
if (props.serviceProviderId && props.interfaceId) {
|
||
void initData({
|
||
serviceProviderId: props.serviceProviderId,
|
||
interfaceId: props.interfaceId,
|
||
});
|
||
}
|
||
});
|
||
type RestaurantItem = {
|
||
value: string;
|
||
key: number;
|
||
};
|
||
const restaurants = ref<RestaurantItem[]>([]);
|
||
// 统一判断"必填"真值
|
||
function isRequiredFlag(v: any): boolean {
|
||
return v === true || v === t('common.yes') || v === 1 || v === '1';
|
||
}
|
||
function querySearch(queryString: string, cb: any) {
|
||
const results =
|
||
queryString?.length && queryString !== 'null'
|
||
? restaurants.value.filter(createFilter(queryString))
|
||
: restaurants.value;
|
||
// call callback function to return suggestions
|
||
cb(results);
|
||
}
|
||
function createFilter(queryString: string) {
|
||
return (restaurant: RestaurantItem) => {
|
||
return (
|
||
restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
|
||
);
|
||
};
|
||
}
|
||
function setlabel(data: any) {
|
||
data.forEach((item: any) => {
|
||
if (item.parameters) item.parameters = setlabel(item.parameters);
|
||
|
||
item.getlabel = item.parameters
|
||
? item.name
|
||
: item.description
|
||
? `${item.alias}-${item.description}`
|
||
: item.cnName
|
||
? `${item.alias}-${item.cnName}`
|
||
: item.alias;
|
||
});
|
||
return data;
|
||
}
|
||
const mapAliasEditRef = ref();
|
||
/** 级联 options 按步骤缓存,避免每行渲染都深拷贝/打标 */
|
||
const cascaderOptionsCache = ref<Map<string, any[]>>(new Map());
|
||
|
||
function invalidateCascaderOptionsCache() {
|
||
cascaderOptionsCache.value = new Map();
|
||
}
|
||
|
||
function getOptions(item: any, isReverse = false) {
|
||
const cacheKey = isReverse
|
||
? 'reverse'
|
||
: `fwd-${item?.interfaceId ?? 'unknown'}`;
|
||
const cached = cascaderOptionsCache.value.get(cacheKey);
|
||
if (cached) return cached;
|
||
|
||
let result: any[];
|
||
if (isReverse) {
|
||
result = setlabel(cascadedList.value.slice(1, cascadedList.value.length));
|
||
} else {
|
||
const ids: number[] = inParaMapList.value.map((t) => t.interfaceId!);
|
||
const findI = ids.findIndex((i) => i === item.interfaceId);
|
||
let data: any[] = [];
|
||
if (findI === -1 || findI === 0) {
|
||
data = cascadedList.value[0] ? [cascadedList.value[0]] : [];
|
||
} else {
|
||
data = [cascadedList.value[0]].concat(
|
||
cascadedList.value.filter((t) => {
|
||
return ids.slice(0, findI).includes(t.interfaceId!);
|
||
}),
|
||
).filter(Boolean);
|
||
}
|
||
result = setlabel(data);
|
||
}
|
||
cascaderOptionsCache.value.set(cacheKey, result);
|
||
return result;
|
||
}
|
||
|
||
/** 当前入参步骤 / 返参步骤共用一份 options,模板里不要对每行再算 */
|
||
const activeInputCascaderOptions = computed(() => {
|
||
if (!activeInParaItem.value) return [];
|
||
return getOptions(activeInParaItem.value, false);
|
||
});
|
||
const outputCascaderOptions = computed(() => getOptions({ interfaceId: 0 }, true));
|
||
function mapAliasEditCheng(
|
||
e: any,
|
||
row: API.InterfaceParameterMapOutputValueObject
|
||
) {
|
||
if (e) {
|
||
let arr: any[] = [];
|
||
cascadedList.value.forEach((t: any) => {
|
||
if (t.parameters) arr = arr.concat(t.parameters);
|
||
});
|
||
const item = arr.find((t: any) => t.id === e);
|
||
row.mapAlias = item?.alias;
|
||
row.mapId = item?.id;
|
||
row.mapCnName = item?.cnName;
|
||
row.mapDescription = item?.description;
|
||
} else {
|
||
row.mapAlias = '';
|
||
row.mapId = 0;
|
||
row.mapCnName = '';
|
||
row.mapDescription = '';
|
||
}
|
||
}
|
||
/** 临时储存item */
|
||
const temporaryItem = ref<any>({});
|
||
const temporaryItemType = ref<number>(1);
|
||
|
||
// 前置-后置脚本点击事件
|
||
function interfaceClick(item: any, type: number) {
|
||
if (item) {
|
||
temporaryItemType.value = type;
|
||
temporaryItem.value = item;
|
||
unref(AddParameterRef)?.open({
|
||
script: type === 0 ? item.prefixScript : item.postfixScript,
|
||
scriptType: type === 0 ? 'prefix' : 'postfix',
|
||
interfaceName: item.interfaceName,
|
||
interfaceId: item.interfaceId,
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 当前步骤级联来源:上游 + 已走过的下游步骤 */
|
||
const mapAiSourceInterfaceIds = computed(() => {
|
||
const item = activeInParaItem.value;
|
||
if (!item) return [];
|
||
const ids: number[] = inParaMapList.value.map((t) => t.interfaceId!);
|
||
const findI = ids.findIndex((i) => i === item.interfaceId);
|
||
if (findI === -1 || findI === 0) {
|
||
return cascadedList.value[0]?.interfaceId != null
|
||
? [cascadedList.value[0].interfaceId]
|
||
: [];
|
||
}
|
||
const upstreamId = cascadedList.value[0]?.interfaceId;
|
||
const prev = ids.slice(0, findI);
|
||
return [upstreamId, ...prev].filter((id): id is number => id != null);
|
||
});
|
||
|
||
function openMapAi() {
|
||
if (!activeInParaItem.value?.mapDetails?.length) {
|
||
// 无当前步骤映射行时不打开
|
||
return;
|
||
}
|
||
mapAiVisible.value = true;
|
||
}
|
||
|
||
function onMapAiApplied() {
|
||
// 触发表格刷新(原地改了 mapDetails)
|
||
inParaMapList.value = [...inParaMapList.value];
|
||
}
|
||
// 前置-后置脚本保存
|
||
function save(data: string) {
|
||
inParaMapList.value.forEach((t: any) => {
|
||
if (t.interfaceId === temporaryItem.value.interfaceId) {
|
||
if (temporaryItemType.value === 0) t.prefixScript = data;
|
||
if (temporaryItemType.value === 1) t.postfixScript = data;
|
||
}
|
||
});
|
||
temporaryItem.value = undefined;
|
||
emits('save-model-enter');
|
||
}
|
||
// 支持多关键词(空格分隔)、大小写不敏感,匹配别名/中文名/名称/代码等字段
|
||
function filterMethod(val: string, node: any) {
|
||
const query = (val ?? '').toString().trim().toLowerCase();
|
||
if (!query) return true;
|
||
const data = node?.data || node || {};
|
||
const haystack = [
|
||
data.getlabel,
|
||
data.alias,
|
||
data.cnName,
|
||
data.description,
|
||
data.name,
|
||
data.code,
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.toLowerCase();
|
||
const parts = query.split(/\s+/).filter(Boolean);
|
||
return parts.every((p: string) => haystack.includes(p));
|
||
}
|
||
function getdata() {
|
||
return {
|
||
inParaMapList: inParaMapList.value.map((t: any) => {
|
||
if (t.mapDetails.length) {
|
||
t.mapDetails = t.mapDetails.map(
|
||
(item: any) =>
|
||
(item = {
|
||
...item,
|
||
mappedParaId: item.mapId || 0,
|
||
paraId: item.id || 0,
|
||
})
|
||
);
|
||
}
|
||
return t;
|
||
}),
|
||
mapDetails: mapDetails.value.map(
|
||
(item: any) =>
|
||
(item = {
|
||
...item,
|
||
mappedParaId: item.mapId || 0,
|
||
paraId: item.id || 0,
|
||
})
|
||
),
|
||
};
|
||
}
|
||
|
||
function getVali(row: Indexable) {
|
||
const refs = tableRef.value;
|
||
const tables = Array.isArray(refs) ? refs : refs ? [refs] : [];
|
||
tables.forEach((element: any) => {
|
||
setTimeout(() => {
|
||
element?.editRowCellEvent(row, 'mapAlias');
|
||
element?.xGrid.scrollToRow(row, 'mapAlias');
|
||
}, 300);
|
||
});
|
||
}
|
||
function getReturnVali(index: number) {
|
||
setTimeout(() => {
|
||
tableRef1.value?.editRowCellEvent(mapDetails.value[index], 'mapAlias');
|
||
tableRef1.value?.xGrid.scrollToRow(mapDetails.value[index], 'mapAlias');
|
||
}, 300);
|
||
}
|
||
|
||
function buildContextKey(data: { serviceProviderId?: number; interfaceId?: number }) {
|
||
return `${data?.serviceProviderId ?? 0}:${data?.interfaceId ?? 0}`;
|
||
}
|
||
|
||
function hasLoadedData() {
|
||
return !!(inParaMapList.value.length || mapDetails.value.length);
|
||
}
|
||
|
||
async function initData(
|
||
data: any,
|
||
options?: { activeIndex?: number; force?: boolean },
|
||
) {
|
||
const ctx = buildContextKey(data);
|
||
const nextActive = options?.activeIndex;
|
||
const force = !!options?.force || typeof nextActive === 'number';
|
||
|
||
// 同服务商/接口已加载过:切回映射明细 tab 时跳过重复请求
|
||
if (!force && loadedContextKey.value === ctx && hasLoadedData()) {
|
||
return;
|
||
}
|
||
// 挂载与父级同时触发时合并为一次请求
|
||
if (!force && inflightInit && inflightInitKey === ctx) {
|
||
await inflightInit;
|
||
return;
|
||
}
|
||
|
||
const seq = ++initSeq;
|
||
detailLoading.value = true;
|
||
|
||
const run = (async () => {
|
||
try {
|
||
const [detailResp] = await Promise.all([
|
||
getEditMapDetails(data),
|
||
getCascadedList(data),
|
||
]);
|
||
if (seq !== initSeq) return;
|
||
|
||
const detail = (detailResp as any)?.data || detailResp || {};
|
||
inParaMapList.value = detail.inParaMapList || [];
|
||
mapDetails.value = detail.mapDetails || [];
|
||
loadedContextKey.value = ctx;
|
||
invalidateCascaderOptionsCache();
|
||
|
||
if (typeof nextActive === 'number') {
|
||
active.value = nextActive;
|
||
} else {
|
||
active.value = 0;
|
||
maxCompletedIndex.value = -1;
|
||
}
|
||
} finally {
|
||
if (seq === initSeq) detailLoading.value = false;
|
||
}
|
||
})();
|
||
|
||
inflightInit = run;
|
||
inflightInitKey = ctx;
|
||
try {
|
||
await run;
|
||
} finally {
|
||
if (inflightInit === run) {
|
||
inflightInit = null;
|
||
inflightInitKey = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
defineExpose({
|
||
tableRef,
|
||
getdata,
|
||
initData,
|
||
hasLoadedData,
|
||
getVali,
|
||
getReturnVali,
|
||
advanceAfterSave,
|
||
guardBeforeSave,
|
||
focusOutputMapping,
|
||
getActiveIndex,
|
||
});
|
||
function advanceAfterSave() {
|
||
const currentStepIndex = active.value === -1 ? inParaMapList.value.length : active.value;
|
||
maxCompletedIndex.value = Math.max(maxCompletedIndex.value, currentStepIndex);
|
||
const nextIndex = currentStepIndex + 1;
|
||
if (nextIndex < inParaMapList.value.length) {
|
||
active.value = nextIndex;
|
||
} else if (nextIndex === inParaMapList.value.length) {
|
||
active.value = -1;
|
||
}
|
||
}
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<div class="entered-map w-full h-full">
|
||
<div class="steps-bar">
|
||
<div class="steps-list">
|
||
<div
|
||
v-for="(item, index) in inParaMapList"
|
||
:key="item?.interfaceMapId || index"
|
||
class="step-item"
|
||
:class="{ active: active === index, done: index <= maxCompletedIndex }"
|
||
@click="setActive(index)"
|
||
>
|
||
<div class="circle">{{ index + 1 }}</div>
|
||
<div class="label">{{ t('system.interface.mapping.inputMapping') }}【{{ item.interfaceName }}】</div>
|
||
<div class="connector"></div>
|
||
</div>
|
||
<div
|
||
ref="outputStepRef"
|
||
class="step-item"
|
||
:class="{
|
||
active: active === -1,
|
||
done: maxCompletedIndex >= inParaMapList.length && !outputNeedsAttention,
|
||
pending: outputNeedsAttention,
|
||
attention: outputAttention,
|
||
}"
|
||
@click="setActive(-1)"
|
||
>
|
||
<div class="circle">{{ inParaMapList.length + 1 }}</div>
|
||
<div class="label">{{ t('system.interface.mapping.outputMappingLabel') }}</div>
|
||
<span v-if="outputNeedsAttention" class="step-pending-tag">
|
||
{{ t('system.interface.mapping.outputMappingPending') }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="activeInParaItem" class="steps-actions">
|
||
<div class="script-action">
|
||
<a-button size="small" @click="interfaceClick(activeInParaItem, 0)">
|
||
{{ t('system.interface.mapping.actions.prefixScript') }}
|
||
</a-button>
|
||
<a-tooltip position="bottom">
|
||
<template #content>
|
||
<div class="script-help-content">
|
||
<p><b>前置脚本</b>:下游 HTTP <b>发出前</b>执行(接口通 PrefixScript)。</p>
|
||
<p>典型用途:签名、鉴权头、动态时间戳/Token 写入 Query/Header/Body。</p>
|
||
<p>执行链路:入参重组 → 占位符替换 → <b>前置脚本</b> → 序列化 → 发请求。</p>
|
||
</div>
|
||
</template>
|
||
<IconQuestionCircle class="script-help-icon" />
|
||
</a-tooltip>
|
||
</div>
|
||
<div class="script-action">
|
||
<a-button size="small" @click="openMapAi">
|
||
{{ t('system.interface.mapping.actions.aiMap') }}
|
||
</a-button>
|
||
<a-tooltip position="bottom">
|
||
<template #content>
|
||
<div class="script-help-content">
|
||
<p><b>AI 映射</b>:按创建物流订单四模块自动初匹配上下游参数。</p>
|
||
<p>模块:订单基本信息、收件人信息、发件人信息、申报信息。</p>
|
||
<p>生成后可预览,确认再填入;仍需手动保存映射。</p>
|
||
</div>
|
||
</template>
|
||
<IconQuestionCircle class="script-help-icon" />
|
||
</a-tooltip>
|
||
</div>
|
||
<div class="script-action">
|
||
<a-button size="small" @click="interfaceClick(activeInParaItem, 1)">
|
||
{{ t('system.interface.mapping.actions.postfixScript') }}
|
||
</a-button>
|
||
<a-tooltip position="bottom">
|
||
<template #content>
|
||
<div class="script-help-content">
|
||
<p><b>后置脚本</b>:下游 HTTP <b>响应返回后</b>执行(接口通 PostfixScript)。</p>
|
||
<p>典型用途:解析响应、字段转换、统一错误码/报文结构。</p>
|
||
<p>执行链路:发请求 → 收响应 → <b>后置脚本</b> → 返参映射回上游。</p>
|
||
</div>
|
||
</template>
|
||
<IconQuestionCircle class="script-help-icon" />
|
||
</a-tooltip>
|
||
</div>
|
||
<div class="unequal-switch">
|
||
<span>{{ t('system.interface.mapping.actions.allowUnequalMapping') }}</span>
|
||
<a-tooltip position="bottom">
|
||
<template #content>
|
||
<div class="unequal-help-content">
|
||
<p>开启后,允许把「数组」映射到「对象」字段(结构可不对等)。</p>
|
||
<p><b>示例:</b>上游 <code>items[]</code> → 下游 <code>package</code>(对象),系统按映射关系取值/组装。</p>
|
||
<p>关闭时仅允许同结构映射:数组↔数组、对象↔对象。</p>
|
||
</div>
|
||
</template>
|
||
<IconQuestionCircle class="unequal-help-icon" />
|
||
</a-tooltip>
|
||
<a-switch v-model="activeInParaItem.freeMap" size="small" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
v-if="outputNeedsAttention && active !== -1"
|
||
type="button"
|
||
class="output-hint-bar"
|
||
@click="focusOutputMapping"
|
||
>
|
||
<span>{{ t('system.interface.mapping.outputMappingHint') }}</span>
|
||
<span class="output-hint-action">{{ t('system.interface.mapping.outputMappingGoFill') }}</span>
|
||
</button>
|
||
<div class="content-pane">
|
||
<a-spin :loading="detailLoading" class="map-detail-spin">
|
||
<div class="content-scroll">
|
||
<div class="content-body">
|
||
<!-- 只挂载当前步骤表格,避免多步骤×多参数同时渲染 -->
|
||
<div
|
||
v-if="activeInParaItem && active >= 0"
|
||
:key="`in-${activeInParaItem.interfaceMapId || active}`"
|
||
class="map-table-wrapper"
|
||
>
|
||
<div class="map-scroll">
|
||
<TablePro
|
||
ref="tableRef"
|
||
v-bind="tableProps"
|
||
class="map-table"
|
||
:table-config="{ data: activeInParaItem?.mapDetails || [] }"
|
||
>
|
||
<template #toolbar_right>
|
||
<slot name="toolbar_right" />
|
||
</template>
|
||
<template #fixedValueHeader>
|
||
<span class="param-value-header">
|
||
{{ t('system.interface.mapping.table.paramValue') }}
|
||
<a-tooltip position="top" :content-style="{ maxWidth: '320px' }">
|
||
<template #content>
|
||
<div class="param-value-help">
|
||
{{ t('system.interface.mapping.table.paramValueTip') }}
|
||
</div>
|
||
</template>
|
||
<IconQuestionCircle class="param-value-help-icon" @click.stop />
|
||
</a-tooltip>
|
||
</span>
|
||
</template>
|
||
<template #fixedValueEdit="{ row }">
|
||
<a-auto-complete
|
||
v-model="row.fixedValue"
|
||
:data="restaurants"
|
||
:filter-option="
|
||
(inputValue, option) =>
|
||
option.value
|
||
.toLowerCase()
|
||
.includes(inputValue.toLowerCase())
|
||
"
|
||
allow-clear
|
||
@select="(value) => (row.fixedValue = value)"
|
||
@change="
|
||
(value) => {
|
||
row.fixedValue = value;
|
||
}
|
||
"
|
||
/>
|
||
</template>
|
||
<template #mapAliasEdit="{ row }">
|
||
<a-cascader
|
||
ref="mapAliasEditRef"
|
||
v-model="row.mapId"
|
||
:options="activeInputCascaderOptions"
|
||
:filter-option="filterMethod"
|
||
allow-clear
|
||
allow-search
|
||
:field-names="{
|
||
value: 'id',
|
||
label: 'getlabel',
|
||
children: 'parameters',
|
||
}"
|
||
:show-all-levels="false"
|
||
@change="
|
||
(value) => {
|
||
mapAliasEditCheng(value, row);
|
||
}
|
||
"
|
||
>
|
||
<template #label="{ data: _data }">
|
||
{{ _data.label }}
|
||
</template>
|
||
</a-cascader>
|
||
</template>
|
||
|
||
<!-- 参数类型(请求侧)在必填时加红色星号 -->
|
||
<template #typeReq="{ row }">
|
||
<span :class="isRequiredFlag(row.isRequired) ? 'type-required' : ''">{{ row.type }}</span>
|
||
</template>
|
||
|
||
<template #isRequired="{ row }">
|
||
<span :class="{ 'text-red-500': row.isRequired === t('common.yes') || row.isRequired === true }">
|
||
{{ row.isRequired === t('common.yes') || row.isRequired === true ? t('common.yes') : t('common.no') }}
|
||
</span>
|
||
</template>
|
||
</TablePro>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="active === -1" class="map-table-wrapper">
|
||
<a-alert
|
||
v-if="outputNeedsAttention"
|
||
type="warning"
|
||
:show-icon="false"
|
||
class="output-map-alert"
|
||
>
|
||
{{ t('system.interface.mapping.outputMappingBanner') }}
|
||
</a-alert>
|
||
<div class="map-scroll">
|
||
<TablePro ref="tableRef1" v-bind="tableProps1" class="map-table">
|
||
<template #toolbar_right>
|
||
<slot name="toolbar_right" />
|
||
</template>
|
||
<template #isRequired="{ row }">
|
||
<span :class="{ 'text-red-500': row.isRequired === t('common.yes') || row.isRequired === true }">
|
||
{{ row.isRequired === t('common.yes') || row.isRequired === true ? t('common.yes') : t('common.no') }}
|
||
</span>
|
||
</template>
|
||
<template #mapAlias="{ row }">
|
||
<a-cascader
|
||
ref="mapAliasEditRef"
|
||
v-model="row.mapId"
|
||
:options="outputCascaderOptions"
|
||
:filter-option="filterMethod"
|
||
allow-search
|
||
allow-clear
|
||
:field-names="{
|
||
value: 'id',
|
||
label: 'getlabel',
|
||
children: 'parameters',
|
||
}"
|
||
:show-all-levels="false"
|
||
@change="
|
||
(value) => {
|
||
mapAliasEditCheng(value, row);
|
||
}
|
||
"
|
||
>
|
||
<template #label="{ data: _data }">
|
||
{{ _data.label }}
|
||
</template>
|
||
</a-cascader>
|
||
</template>
|
||
<!-- 参数类型(响应侧)在必填时加红色星号 -->
|
||
<template #typeResp="{ row }">
|
||
<span :class="isRequiredFlag(row.isRequired) ? 'type-required' : ''">{{ row.type }}</span>
|
||
</template>
|
||
</TablePro>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</div>
|
||
<AddParameter ref="AddParameterRef" @seva="save" />
|
||
<MapAiAssistModal
|
||
v-model:visible="mapAiVisible"
|
||
:interface-name="activeInParaItem?.interfaceName"
|
||
:map-details="activeInParaItem?.mapDetails || []"
|
||
:cascaded-list="cascadedList"
|
||
:source-interface-ids="mapAiSourceInterfaceIds"
|
||
@applied="onMapAiApplied"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<style scoped>
|
||
.entered-map { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; overflow: hidden; }
|
||
.map-detail-spin { display: block; width: 100%; height: 100%; min-height: 0; }
|
||
.map-detail-spin :deep(.arco-spin-children) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
.steps-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--color-border-2);
|
||
overflow: hidden;
|
||
}
|
||
.steps-list {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
flex: 1;
|
||
}
|
||
.steps-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
.script-action {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.unequal-switch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
color: var(--color-text-2);
|
||
white-space: nowrap;
|
||
}
|
||
.unequal-help-icon,
|
||
.script-help-icon,
|
||
.param-value-help-icon {
|
||
color: var(--color-text-3);
|
||
font-size: 14px;
|
||
cursor: help;
|
||
}
|
||
.param-value-header {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.param-value-help {
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
.unequal-help-icon:hover,
|
||
.script-help-icon:hover,
|
||
.param-value-help-icon:hover {
|
||
color: var(--app-primary-color);
|
||
}
|
||
.step-item { position: relative; display: flex; align-items: center; color: var(--color-text-3); cursor: pointer; flex-shrink: 0; }
|
||
.step-item .circle { width: 22px; height: 22px; border-radius: 50%; background: var(--color-fill-2); color: var(--color-text-3); display: flex; align-items: center; justify-content: center; font-size: 12px; margin-right: 6px; }
|
||
.step-item.active .circle { background: var(--app-primary-color); color: #fff; }
|
||
.step-item.done .circle { background: var(--app-primary-color); color: #fff; }
|
||
.step-item .label { font-size: 12px; white-space: nowrap; }
|
||
.step-item .connector { width: 48px; height: 2px; background: #e5e5e5; margin-left: 12px; margin-right: 0; }
|
||
.step-item.pending .circle {
|
||
background: #fff7e8;
|
||
color: #d46b08;
|
||
box-shadow: inset 0 0 0 1px #ffcf8b;
|
||
}
|
||
.step-item.pending .label { color: #d46b08; font-weight: 600; }
|
||
.step-item.pending.active .circle {
|
||
background: #d46b08;
|
||
color: #fff;
|
||
box-shadow: none;
|
||
}
|
||
.step-item.attention .circle {
|
||
animation: output-step-pulse 0.7s ease 3;
|
||
}
|
||
.step-pending-tag {
|
||
margin-left: 6px;
|
||
padding: 0 6px;
|
||
height: 18px;
|
||
line-height: 18px;
|
||
border-radius: 9px;
|
||
background: #fff7e8;
|
||
color: #d46b08;
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
.output-hint-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 8px 16px;
|
||
border: 0;
|
||
border-bottom: 1px solid #ffe4ba;
|
||
background: #fff7e8;
|
||
color: #ad4e00;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
.output-hint-action {
|
||
flex-shrink: 0;
|
||
color: var(--app-primary-color);
|
||
font-weight: 600;
|
||
}
|
||
.output-map-alert { margin-bottom: 8px; }
|
||
@keyframes output-step-pulse {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(212, 107, 8, 0.45); }
|
||
50% { box-shadow: 0 0 0 6px rgba(212, 107, 8, 0); }
|
||
}
|
||
.content-pane { padding: 12px 16px; flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||
.content-scroll { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||
.content-body { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
|
||
.map-table-wrapper { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||
.map-scroll { flex: 1; min-height: 0; max-height: calc(100vh - 260px); overflow: auto; }
|
||
.map-table { flex: 1; min-height: 0; }
|
||
|
||
/* 在“参数类型”后显示必填星号 */
|
||
:deep(.vxe-table) .type-required {
|
||
position: relative;
|
||
padding-right: 8px;
|
||
}
|
||
:deep(.vxe-table) .type-required::after {
|
||
content: '*';
|
||
color: #ff4d4f;
|
||
margin-left: 4px;
|
||
}
|
||
</style>
|
||
|
||
<style lang="less">
|
||
.unequal-help-content,
|
||
.script-help-content {
|
||
max-width: 340px;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
|
||
p {
|
||
margin: 0 0 6px;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
code {
|
||
padding: 0 4px;
|
||
border-radius: 2px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
}
|
||
}
|
||
</style>
|