front/src/views/api/upstreamInterface/components/EnteredMap.vue
2026-01-31 15:32:12 +08:00

599 lines
20 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 setup lang="ts">
import { computed, onMounted, ref, unref, watch, type PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import {
getEditMapDetails,
getGetMapParameterCascadedList,
} from '@/api/InterfaceMappingServices/interfaceMap';
import { getParameterGetParameterList } from '@/api/ParameterServices/parameter';
import AddParameter from './AddParameter.vue';
const { t } = useI18n();
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']);
const tableRef = ref();
const tableRef1 = ref();
const active = ref(0);
const AddParameterRef = ref();
const formsTypeList_new = ref<any[]>([]);
const maxCompletedIndex = ref<number>(-1);
interface Indexable {
[key: string]: any;
}
// 请求去重:记录正在请求的接口ID
const loadingParameterId = ref<number | null>(null);
async function formsValueInfoGetList() {
// 如果正在加载相同的接口,直接返回
if (loadingParameterId.value === props.interfaceId) {
return;
}
// 如果没有接口ID,直接返回
if (!props.interfaceId) {
return;
}
loadingParameterId.value = props.interfaceId;
try {
const { data } = await getParameterGetParameterList({
isInPara: true,
id: Number(props.interfaceId),
onlyLeafs: true,
});
formsTypeList_new.value = data;
formsTypeList.value = data;
} catch (error) {
} finally {
loadingParameterId.value = null;
}
}
// 监听 interfaceId 变化,只在变化时请求
watch(() => props.interfaceId, (newId) => {
if (newId) {
formsValueInfoGetList();
}
}, { immediate: true });
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: 'cnName',
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: 'mapCnName',
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: 'cnName',
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: {
edit: 'fixedValueEdit',
},
editRender: {},
},
{
field: 'mapAlias',
title: t('system.interface.mapping.table.inputParam'),
slots: {
edit: 'mapAliasEdit',
},
minWidth: 120,
editRender: {},
},
{
field: 'mapCnName',
title: t('system.interface.mapping.table.inputParamCn'),
minWidth: 130,
},
],
tableConfig: {
// 让表格根据内容自适应,由外层容器负责滚动
height: undefined,
maxHeight: undefined,
},
};
});
const inParaMapList = ref<API.MapSummary[]>([]);
const mapDetails = ref<API.InterfaceParameterMapOutputValueObject[]>([]);
function setActive(index: number) {
active.value = index;
}
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,
};
});
};
onMounted(() => {
restaurants.value = loadAll();
});
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.cnName
? `${item.alias}-${item.cnName}`
: item.alias;
});
return data;
}
const mapAliasEditRef = ref();
function getOptions(item: any, isReverse = false) {
if (isReverse)
return setlabel(cascadedList.value.slice(1, cascadedList.value.length));
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]];
} else {
data = [cascadedList.value[0]].concat(
cascadedList.value.filter((t) => {
return ids.slice(0, findI).includes(t.interfaceId!);
})
);
}
return setlabel(data);
}
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(
type === 0 ? item.prefixScript : item.postfixScript
);
}
}
// 前置-后置脚本保存
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');
}
const formsTypeList = ref<any[]>([]);
// 支持多关键词(空格分隔)、大小写不敏感,匹配别名/中文名/名称/代码等字段
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.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) {
tableRef.value?.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 resultsFn() {
formsTypeList_new.value = [];
formsTypeList.value = [];
mapDetails.value = [];
active.value = 0;
maxCompletedIndex.value = -1;
inParaMapList.value = [];
}
async function initData(data: any) {
resultsFn();
getEditMapDetails(data).then((resp) => {
const detail = (resp as any)?.data || resp || {};
inParaMapList.value = detail.inParaMapList || [];
mapDetails.value = detail.mapDetails || [];
});
getCascadedList(data);
setActive(active.value);
}
defineExpose({ tableRef, getdata, initData, getVali, getReturnVali, advanceAfterSave });
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
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 class="step-item" :class="{ active: active === -1, done: maxCompletedIndex >= inParaMapList.length }" @click="setActive(-1)">
<div class="circle">{{ inParaMapList.length + 1 }}</div>
<div class="label">{{ t('system.interface.mapping.outputMappingLabel') }}</div>
</div>
</div>
<div class="content-pane">
<div class="content-scroll">
<div class="content-body">
<template v-for="(item, index) in inParaMapList" :key="index">
<div v-show="active === index" class="map-table-wrapper">
<div class="map-scroll">
<TablePro
ref="tableRef"
v-bind="tableProps"
class="map-table"
:table-config="{ data: item?.mapDetails || [] }"
>
<template #toolbar_right>
<slot name="toolbar_right" />
</template>
<template #toolbar_buttons>
<div class="px-12px flex py-12px pl-0!">
<a-button @click="interfaceClick(item, 0)">{{ t('system.interface.mapping.actions.prefixScript') }} </a-button>
<a-button class="ml-5px" @click="interfaceClick(item, 1)"
>{{ t('system.interface.mapping.actions.postfixScript') }}
</a-button>
<div class="flex items-center ml-12px">
{{ t('system.interface.mapping.actions.allowUnequalMapping') }}
<a-switch v-model="item.freeMap" />
</div>
</div>
</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="getOptions(item, false)"
: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>
</template>
<template v-if="active === -1">
<div class="map-table-wrapper">
<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="getOptions({ interfaceId: 0 }, true)"
: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>
</template>
</div>
</div>
</div>
<AddParameter ref="AddParameterRef" @seva="save" />
</div>
</template>
<style scoped>
.entered-map { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; overflow: hidden; }
.steps-bar { display: flex; align-items: center; padding: 10px 12px; gap: 12px; border-bottom: 1px solid #eee; overflow-x: auto; }
.step-item { position: relative; display: flex; align-items: center; color: #8c8c8c; cursor: pointer; }
.step-item .circle { width: 22px; height: 22px; border-radius: 50%; background: #f0f0f0; color: #8c8c8c; 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; }
.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>