All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m47s
优化运单详情抽屉操作栏与申报表编辑观感,限制我的订单入库后不可编辑;完善接口参数全量替换/脏检查与全局 Message;修复价格表批量填价选中被清与弹窗被遮挡。 Co-authored-by: Cursor <cursoragent@cursor.com>
566 lines
17 KiB
Vue
566 lines
17 KiB
Vue
<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>
|