front/src/views/api/upstreamInterface/components/EnteredMap.vue
zzh 640562654d
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m25s
feat: 仓库地址排序备注与映射明细性能、脏检查误报修复
下单/H5 仓库地址展示与排序备注;映射明细加载优化与发布后保留选中;接口明细加载期不再误报未保存。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-09 14:12:32 +08:00

1009 lines
33 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, 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>