front/src/views/api/upstreamInterface/components/map-service-provider.vue
zzh b9702cd0bc
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
fix: 订单详情编辑体验、接口参数与价格表批量填价
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 16:27:22 +08:00

566 lines
17 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, computed, watch, nextTick } from 'vue';
import {
postDelete,
getGetMapList,
} from '@/api/InterfaceMappingServices/interfaceMap';
import {
getGetPublishCompare,
postPublish,
} from '@/api/InterfaceMappingServices/interfaceMapPublish';
import useModal from '@/components/ModalPro/useModal';
import { useI18n } from 'vue-i18n';
import { useRequest } from 'vue-hooks-plus';
import { $baseMessage, $baseConfirm } from '@/utils';
import {
computeSnapshotDiff,
formatSnapshotJson,
} from '@/utils/publishSnapshotDiff';
import RollBack from './RollBack.vue';
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
import PublishDiffPanel from '@/components/PublishDiff/index.vue';
const { t } = useI18n();
defineOptions({
name: 'MapServiceProvider',
});
type IProps = {
id?: number | null;
/** 未选接口或列表为空时的提示 */
emptyText?: string;
};
const props = withDefaults(defineProps<IProps>(), {
id: null,
emptyText: '暂无数据',
});
const emit = defineEmits(['edit-map', 'row-click', 'add']);
const tableRef = ref<any>();
const list = ref<any[]>([]);
const loading = ref(false);
const searchKeyword = ref('');
const selectedId = ref<number | string | null>(null);
function getRowKey(row: any, index: number) {
return row?.interfaceMapId ?? row?.id ?? index;
}
const mappingTreeData = computed<CustomLeftMenuTreeNode[]>(() =>
(list.value || []).map((row: any, index: number) => ({
key: `map-${getRowKey(row, index)}`,
title: row.serviceProviderName || '-',
isLeaf: true,
id: getRowKey(row, index),
name: row.serviceProviderName,
code: row.serviceProviderCode || '',
raw: row,
index,
})),
);
const selectedKeys = computed(() =>
selectedId.value != null ? [`map-${selectedId.value}`] : [],
);
function handleSelectRow(row: any, index: number) {
selectedId.value = getRowKey(row, index);
emit('row-click', row);
}
function handleMenuSelect(keys: string[]) {
const key = keys?.[0];
if (!key?.startsWith('map-')) return;
const id = key.slice(4);
const index = list.value.findIndex(
(row: any, i: number) => String(getRowKey(row, i)) === String(id),
);
if (index < 0) return;
handleSelectRow(list.value[index], index);
}
function handleNodeClick(key: string) {
handleMenuSelect([key]);
}
function resolveRow(nodeData: any) {
if (!nodeData) return null;
if (nodeData.raw) return nodeData.raw;
const id = nodeData.id;
return (
list.value.find(
(row: any, i: number) => String(getRowKey(row, i)) === String(id),
) || null
);
}
function handleDeleteNode(nodeData: any) {
const row = resolveRow(nodeData);
if (row) delRow(row);
}
function handlePublishNode(nodeData: any) {
const row = resolveRow(nodeData);
if (row) onRelease(row);
}
function handleRollbackNode(nodeData: any) {
const row = resolveRow(nodeData);
if (row) onBack(row);
}
function handleAdd() {
emit('add');
}
function handleSearch() {
loadList({ keepSelection: true });
}
async function loadList(options?: { keepSelection?: boolean }) {
if (!props.id) {
list.value = [];
if (selectedId.value != null) {
selectedId.value = null;
emit('row-click', null);
}
return;
}
try {
loading.value = true;
const requestParams: any = {
InterfaceId: props.id,
NameOrCode: searchKeyword.value || undefined,
PageIndex: 1,
PageSize: 1000,
};
const response: any = await getGetMapList(requestParams);
// 兼容各种返回结构,规避 TS 报错
const raw: any = response || {};
const data: any = raw.items ?? raw.data ?? raw.list ?? raw.records ?? raw;
list.value = Array.isArray(data) ? data : [];
if (!options?.keepSelection) {
// 刷新 / 切换上游接口:清空选中
const hadSelection = selectedId.value != null;
selectedId.value = null;
if (hadSelection) emit('row-click', null);
} else if (
selectedId.value !== null &&
!list.value.some(
(row: any, index: number) => String(getRowKey(row, index)) === String(selectedId.value),
)
) {
selectedId.value = null;
emit('row-click', null);
}
} catch (e) {
list.value = [];
selectedId.value = null;
emit('row-click', null);
} finally {
loading.value = false;
}
}
watch(() => props.id, (id) => {
// 换接口先清选中,避免沿用上一次的服务商高亮
selectedId.value = null;
if (id) loadList({ keepSelection: false });
else list.value = [];
}, { immediate: true });
// 暴露与旧用法兼容的 reload:默认保留当前选中(发布/保存后刷新不丢高亮)
tableRef.value = {
reload: (options?: { keepSelection?: boolean }) =>
loadList({ keepSelection: options?.keepSelection !== false }),
};
function setSearchKeyword(val: string) {
searchKeyword.value = val || '';
loadList({ keepSelection: true });
}
// 右侧内容
const modifiedModel = ref<string>('');
const originalValue = ref<string>('');
const differenceContent = ref('');
const publishLoading = ref(false);
const diffRows = ref<ReturnType<typeof computeSnapshotDiff>['rows']>([]);
const hasOnlineSnapshot = ref(false);
// 删除
function delRow(row: any) {
$baseConfirm({
title: t('system.interface.modal.warningTitle'),
content: t('system.interface.mapping.modal.deleteConfirm'),
onOk: async () => {
await postDelete({
interfaceId: row.systemInterfaceId,
serviceProviderId: row.serviceProviderId,
}).then(() => {
$baseMessage(t('system.interface.message.deleteSuccess'), 'success');
loadList({ keepSelection: false });
});
},
});
}
// 发布
const [registerRelease, registerReleaseMethods] = useModal() as [any, any];
const diffLength = ref(0);
const interfaceMapId = ref(0);
const serverInfo = ref({
serviceProviderName: '',
interfaceName: '',
});
const commsData = ref<any>({});
const diffViewMode = ref<'all' | 'diff'>('diff');
const publishDiffStatus = computed(() => {
if (!hasOnlineSnapshot.value) return 'first';
if (diffLength.value === 0) return 'synced';
return 'changed';
});
const publishStatusTitle = computed(() => {
if (publishDiffStatus.value === 'synced') {
return t('system.interface.mapping.modal.status.synced');
}
if (publishDiffStatus.value === 'first') {
return t('system.interface.mapping.modal.status.first');
}
return t('system.interface.mapping.modal.status.changed', {
count: diffLength.value,
});
});
const publishLegendLabels = computed(() => ({
add: t('system.interface.mapping.modal.legend.add'),
remove: t('system.interface.mapping.modal.legend.remove'),
change: t('system.interface.mapping.modal.legend.change'),
same: t('system.interface.mapping.modal.legend.same'),
}));
const getOriginalValue = computed(() =>
formatSnapshotJson(originalValue.value || '{}'),
);
const getModifiedModel = computed(() =>
formatSnapshotJson(modifiedModel.value || '{}'),
);
function detectHasOnline(raw: unknown) {
const text = String(raw ?? '').trim();
if (!text || text === '{}' || text === 'null') return false;
try {
const parsed = JSON.parse(text);
return (
parsed != null &&
typeof parsed === 'object' &&
Object.keys(parsed).length > 0
);
} catch {
return true;
}
}
function applyPublishDiff(originalRaw: unknown, modifiedRaw: unknown) {
originalValue.value =
typeof originalRaw === 'string'
? originalRaw
: JSON.stringify(originalRaw || {});
modifiedModel.value =
typeof modifiedRaw === 'string'
? modifiedRaw
: JSON.stringify(modifiedRaw || {});
hasOnlineSnapshot.value = detectHasOnline(originalRaw);
const { length, rows } = computeSnapshotDiff(originalRaw, modifiedRaw);
diffLength.value = Math.max(length, 0);
diffRows.value = rows;
diffViewMode.value = diffLength.value > 0 ? 'diff' : 'all';
}
function fillServerInfo(row: any) {
serverInfo.value = {
serviceProviderName:
row?.serviceProviderName || serverInfo.value.serviceProviderName || '',
interfaceName:
row?.interfaceName ||
row?.systemInterfaceName ||
serverInfo.value.interfaceName ||
'',
};
}
async function onRelease(row: any) {
commsData.value = row;
interfaceMapId.value = row.interfaceMapId!;
fillServerInfo(row);
publishLoading.value = true;
registerReleaseMethods.openModal();
try {
const resp = await getGetPublishCompare({
serviceProviderId: row.serviceProviderId!,
interfaceCode: row.systemInterfaceCode,
});
const res = (resp as any)?.data || resp || {};
differenceContent.value = res.differenceContent || '';
applyPublishDiff(res.publishedContent || '', res.dbContent || '');
await nextTick();
} catch (error) {
registerReleaseMethods.closeModal();
$baseMessage(
t('system.interface.mapping.message.getPublishCompareFailed'),
'error',
);
} finally {
publishLoading.value = false;
}
}
const visible = ref(false);
const description = ref('');
async function sendMapOk() {
await run({
interfaceCode: commsData.value.systemInterfaceCode,
serviceProviderId: commsData.value.serviceProviderId,
dbContent: modifiedModel.value || '',
differenceContent: differenceContent.value,
description: description.value,
});
description.value = '';
visible.value = false;
}
function handlerSendMap() {
if (publishDiffStatus.value === 'synced') {
return $baseMessage(
t('system.interface.mapping.message.noContentToPublish'),
'warning',
);
}
visible.value = true;
}
const { run } = useRequest(postPublish, {
manual: true,
onSuccess() {
$baseMessage(
t('system.interface.mapping.message.publishCacheSuccess', {
providerName: serverInfo.value.serviceProviderName,
interfaceName: serverInfo.value.interfaceName,
count: diffLength.value,
}),
'success',
);
registerReleaseMethods.closeModal();
// 发布后刷新列表,保留当前服务商选中
void loadList({ keepSelection: true });
},
});
const rollBackRef = ref();
function onBack(row: any) {
interfaceMapId.value = row.interfaceMapId;
fillServerInfo(row);
rollBackRef.value?.handleOpenBack(row);
}
function handleCancel() {
visible.value = false;
}
defineExpose({ onRelease, tableRef, setSearchKeyword });
</script>
<template>
<div class="map-service-provider">
<CustomLeftMenu
class="mapping-menu"
width="100%"
:tree-data="mappingTreeData"
:selected-keys="selectedKeys"
:loading="loading"
v-model:search-keyword="searchKeyword"
:search-placeholder="t('system.interface.search.placeholder.providerName')"
:refresh-title="t('system.interface.actions.refresh')"
:add-title="t('system.interface.modal.addMapping')"
:actions-title="t('system.interface.mapping.actions.actions')"
:delete-label="t('system.interface.mapping.actions.delete')"
:empty-text="emptyText"
:show-method="false"
:show-map-count="false"
:show-pagination="false"
:show-node-index="true"
:show-node-code="true"
@search="handleSearch"
@refresh="() => loadList({ keepSelection: false })"
@add="handleAdd"
@select="handleMenuSelect"
@node-click="handleNodeClick"
@delete-node="handleDeleteNode"
>
<template #node-menu="{ nodeData }">
<a-doption @click="handleDeleteNode(nodeData)">
{{ t('system.interface.mapping.actions.delete') }}
</a-doption>
<a-doption @click="handlePublishNode(nodeData)">
{{ t('system.interface.mapping.actions.publish') }}
</a-doption>
<a-doption @click="handleRollbackNode(nodeData)">
{{ t('system.interface.mapping.actions.rollback') }}
</a-doption>
</template>
</CustomLeftMenu>
<ModalPro
:title="t('system.interface.mapping.modal.publishTitle')"
:width="1120"
:footer="false"
@register="registerRelease"
>
<a-spin :loading="publishLoading" class="publish-modal-spin">
<div class="release-content slim">
<a-alert type="info" :show-icon="false" class="modal-form-tip">
{{ t('system.interface.mapping.modal.publishTip') }}
</a-alert>
<PublishDiffPanel
:status="publishDiffStatus"
:rows="diffRows"
:diff-length="diffLength"
:status-title="publishStatusTitle"
filter-mode="tabs"
v-model:diff-view-mode="diffViewMode"
:show-all-label="t('system.interface.mapping.modal.showAll')"
:show-only-diff-label="t('system.interface.mapping.modal.showOnlyDiff')"
:legend-labels="publishLegendLabels"
:empty-text="t('system.interface.mapping.message.noContentToPublish')"
:header-title="t('system.interface.mapping.modal.diffSummary')"
:header-count-text="
diffLength > 0
? t('system.interface.mapping.modal.updateCacheWithCount', {
count: diffLength,
})
: undefined
"
raw-mode="details"
:raw-summary-text="t('system.interface.mapping.modal.advancedRawJson')"
:original-title="t('system.interface.mapping.modal.onlineCache')"
:modified-title="
t('system.interface.mapping.modal.updateCacheWithCount', {
count: diffLength,
})
"
:original-json="getOriginalValue"
:modified-json="getModifiedModel"
>
<template #header-actions>
<a-button
type="primary"
:disabled="publishDiffStatus === 'synced'"
@click="handlerSendMap"
>
{{
publishDiffStatus === 'synced'
? t('system.interface.mapping.modal.noNeedPublish')
: t('system.interface.mapping.modal.confirmPublish')
}}
</a-button>
</template>
</PublishDiffPanel>
</div>
</a-spin>
</ModalPro>
<RollBack ref="rollBackRef" :server-info="serverInfo" />
<a-modal
v-model:visible="visible"
:title="t('system.interface.mapping.modal.confirmPublishTitle')"
:ok-text="t('system.interface.mapping.modal.confirmPublish')"
:mask-closable="false"
unmount-on-close
@cancel="handleCancel"
@ok="sendMapOk"
>
<a-alert type="warning" :show-icon="false" class="modal-form-tip modal-form-tip--warn">
{{ t('system.interface.mapping.modal.publishWarning') }}
</a-alert>
<div class="form-section-title">
{{ t('system.interface.mapping.modal.versionRemarkLabel') }}
</div>
<a-input
v-model="description"
allow-clear
:placeholder="t('system.interface.mapping.modal.versionRemark')"
/>
</a-modal>
</div>
</template>
<style lang="less" scoped>
.map-service-provider {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
.mapping-menu {
width: 100% !important;
height: 100%;
flex: 1;
min-height: 0;
}
.modal-form-tip {
margin-bottom: 12px;
border-radius: 6px;
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, transparent);
color: var(--color-text-2);
font-size: 13px;
line-height: 1.5;
&--warn {
background: #fff7e8;
border-color: #ffe4a3;
color: #ad6800;
}
}
.form-section-title {
margin: 0 0 8px;
padding: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-2);
}
.publish-modal-spin {
width: 100%;
display: block;
}
.release-content.slim {
display: flex;
flex-direction: column;
gap: 12px;
max-height: calc(90vh - 120px);
overflow: auto;
}
</style>