统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。 Co-authored-by: Cursor <cursoragent@cursor.com>
692 lines
19 KiB
Vue
692 lines
19 KiB
Vue
<script lang="ts" setup>
|
|
import { ref, computed } 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 dayjs from 'dayjs';
|
|
import { $baseMessage, $baseConfirm } from '@/utils';
|
|
import { diffJson } from 'diff';
|
|
// import { mapserviceproviderColumns } from '../models/tableColumns';
|
|
import RollBack from './RollBack.vue';
|
|
import MonacoCom from './monacoCom.vue';
|
|
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
|
|
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
|
|
// import { IconSearch } from '@arco-design/web-vue/es/icon';
|
|
|
|
const { t } = useI18n();
|
|
|
|
defineOptions({
|
|
name: 'MapServiceProvider',
|
|
});
|
|
|
|
type IProps = {
|
|
id: number;
|
|
};
|
|
|
|
const props = defineProps<IProps>();
|
|
|
|
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();
|
|
}
|
|
|
|
async function loadList() {
|
|
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 (
|
|
selectedId.value !== null &&
|
|
!list.value.some((row: any, index: number) => getRowKey(row, index) === selectedId.value)
|
|
) {
|
|
selectedId.value = null;
|
|
}
|
|
} catch (e) {
|
|
list.value = [];
|
|
selectedId.value = null;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
watch(() => props.id, () => {
|
|
if (props.id) loadList();
|
|
}, { immediate: true });
|
|
|
|
// 暴露与旧用法兼容的 reload 接口
|
|
tableRef.value = { reload: loadList };
|
|
|
|
function setSearchKeyword(val: string) {
|
|
searchKeyword.value = val || '';
|
|
loadList();
|
|
}
|
|
|
|
defineExpose({ onRelease, tableRef, setSearchKeyword });
|
|
|
|
// 右侧内容
|
|
const modifiedModel = ref<string>();
|
|
const originalValue = ref<string | undefined>();
|
|
|
|
const differenceContent = ref('');
|
|
|
|
|
|
// 删除
|
|
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();
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
// 发布
|
|
const [registerRelease, registerReleaseMethods] = useModal() as [any, any];
|
|
|
|
const codeRef = ref();
|
|
// 差异数量
|
|
const diffLength = ref(0);
|
|
// 接口映射id
|
|
const interfaceMapId = ref(0);
|
|
// 服务商名称接口名称
|
|
const serverInfo = {
|
|
serviceProviderName: '',
|
|
interfaceName: '',
|
|
};
|
|
const commsData = ref<any>({});
|
|
|
|
const getOriginalValue = computed(() => {
|
|
if (originalValue.value) {
|
|
return JSON.stringify(JSON.parse(originalValue.value || '{}'), null, 2);
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
const getModifiedModel = computed(() => {
|
|
if (modifiedModel.value) {
|
|
return JSON.stringify(JSON.parse(modifiedModel.value || '{}'), null, 2);
|
|
}
|
|
return '';
|
|
});
|
|
|
|
// 简单差异行列表(无语法高亮,仅标记增删)
|
|
const simpleDiffLines = computed(() => {
|
|
try {
|
|
const originObj = JSON.parse(originalValue.value || '{}');
|
|
const newObj = JSON.parse(modifiedModel.value || '{}');
|
|
const diffParts =
|
|
diffJson(originObj, newObj)?.filter((p) => p && p.value !== undefined) ||
|
|
[];
|
|
const lines: Array<{ text: string; added?: boolean; removed?: boolean }> =
|
|
[];
|
|
diffParts.forEach((part) => {
|
|
const segments = (part.value || '').split('\n');
|
|
segments.forEach((seg, idx) => {
|
|
// 跳过最后一个空行
|
|
if (idx === segments.length - 1 && seg === '') return;
|
|
lines.push({ text: seg, added: part.added, removed: part.removed });
|
|
});
|
|
});
|
|
return lines;
|
|
} catch (err) {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const showOnlyDiff = ref(false);
|
|
// 将 jsdiff 输出转成行级结构,并尝试把“删+增”配对为“修改”
|
|
const structuredDiff = computed(() => {
|
|
try {
|
|
const originObj = JSON.parse(originalValue.value || '{}');
|
|
const newObj = JSON.parse(modifiedModel.value || '{}');
|
|
const parts =
|
|
diffJson(originObj, newObj)?.filter((p) => p && p.value !== undefined) ||
|
|
[];
|
|
const flat: Array<{ text: string; status: 'add' | 'remove' | 'same' }> = [];
|
|
parts.forEach((p) => {
|
|
const status = p.added ? 'add' : p.removed ? 'remove' : 'same';
|
|
const lines = (p.value || '').split('\n');
|
|
lines.forEach((line, idx) => {
|
|
if (idx === lines.length - 1 && line === '') return;
|
|
flat.push({ text: line, status });
|
|
});
|
|
});
|
|
const merged: Array<
|
|
| { type: 'add' | 'remove' | 'same'; text: string }
|
|
| { type: 'change'; oldText: string; newText: string }
|
|
> = [];
|
|
let i = 0;
|
|
while (i < flat.length) {
|
|
const curr = flat[i];
|
|
const next = flat[i + 1];
|
|
if (curr?.status === 'remove' && next?.status === 'add') {
|
|
merged.push({ type: 'change', oldText: curr.text, newText: next.text });
|
|
i += 2;
|
|
} else {
|
|
merged.push({ type: curr.status, text: curr.text });
|
|
i += 1;
|
|
}
|
|
}
|
|
return merged;
|
|
} catch (err) {
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const visibleDiffRows = computed(() => {
|
|
if (!showOnlyDiff.value) return structuredDiff.value;
|
|
return structuredDiff.value.filter((row) => row.type !== 'same');
|
|
});
|
|
|
|
async function onRelease(row: any) {
|
|
commsData.value = row;
|
|
interfaceMapId.value = row.interfaceMapId!;
|
|
serverInfo.serviceProviderName = row.serviceProviderName;
|
|
serverInfo.interfaceName = row.interfaceName;
|
|
|
|
try {
|
|
const resp = await getGetPublishCompare({
|
|
serviceProviderId: row.serviceProviderId!,
|
|
interfaceCode: row.systemInterfaceCode,
|
|
});
|
|
const res = (resp as any)?.data || resp || {};
|
|
|
|
modifiedModel.value = res.dbContent || '';
|
|
originalValue.value = res.publishedContent || '';
|
|
differenceContent.value = res.differenceContent || '';
|
|
|
|
// 先打开弹窗
|
|
registerReleaseMethods.openModal();
|
|
|
|
// 等待弹窗渲染完成
|
|
await nextTick();
|
|
|
|
// MonacoCom 组件会自动响应 props 变化,无需手动初始化
|
|
|
|
// 计算差异数量
|
|
const len =
|
|
diffJson(
|
|
JSON.parse(originalValue.value || '{}'),
|
|
JSON.parse(modifiedModel.value || '{}')
|
|
)?.filter((item) => item.added || item.removed).length || 0;
|
|
diffLength.value = len;
|
|
} catch (error) {
|
|
$baseMessage(t('system.interface.mapping.message.getPublishCompareFailed'), 'error');
|
|
}
|
|
}
|
|
|
|
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 (!diffLength.value)
|
|
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.serviceProviderName,
|
|
interfaceName: serverInfo.interfaceName,
|
|
count: diffLength.value,
|
|
}),
|
|
'success'
|
|
);
|
|
|
|
registerReleaseMethods.closeModal();
|
|
},
|
|
});
|
|
|
|
/**
|
|
* @description: 切换下一个差异
|
|
* @return {*}
|
|
*/
|
|
|
|
function changeNext() {
|
|
codeRef.value.handleNext();
|
|
}
|
|
|
|
/**
|
|
* @description: 切换上一个差异
|
|
* @return {*}
|
|
*/
|
|
|
|
function changePre() {
|
|
codeRef.value.handlePre();
|
|
}
|
|
|
|
// 回滚
|
|
const rollBackRef = ref();
|
|
|
|
/**
|
|
* @description: 打开回滚弹窗
|
|
* @return {*}
|
|
*/
|
|
|
|
function onBack(row: any) {
|
|
interfaceMapId.value = row.interfaceMapId;
|
|
rollBackRef.value?.handleOpenBack(row);
|
|
}
|
|
|
|
// 处理取消操作
|
|
function handleCancel() {
|
|
visible.value = false;
|
|
}
|
|
|
|
</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')"
|
|
:show-method="false"
|
|
:show-map-count="false"
|
|
:show-pagination="false"
|
|
:show-node-index="true"
|
|
:show-node-code="true"
|
|
@search="handleSearch"
|
|
@refresh="loadList"
|
|
@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="1280"
|
|
:footer="false"
|
|
@register="registerRelease"
|
|
>
|
|
<div class="release-content slim">
|
|
<div class="diff-header">
|
|
<div class="diff-title">{{ t('system.interface.mapping.modal.updateCache') }}</div>
|
|
<div class="diff-actions">
|
|
<a-button
|
|
type="primary"
|
|
size="small"
|
|
@click="handlerSendMap"
|
|
:disabled="diffLength === 0"
|
|
>
|
|
{{ t('system.interface.mapping.actions.publish') }}
|
|
</a-button>
|
|
<a-checkbox v-model="showOnlyDiff" size="small">
|
|
只看差异
|
|
</a-checkbox>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="diff-boxes">
|
|
<div class="diff-box">
|
|
<div class="diff-box-title">{{ t('system.interface.mapping.modal.onlineCache') }}</div>
|
|
<pre class="diff-pre">{{ getOriginalValue || '{}' }}</pre>
|
|
</div>
|
|
<div class="diff-box">
|
|
<div class="diff-box-title">{{ t('system.interface.mapping.modal.updateCache', { count: diffLength }) }}</div>
|
|
<pre class="diff-pre">{{ getModifiedModel || '{}' }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<span class="legend-item add">🟢 新增</span>
|
|
<span class="legend-item remove">🔴 删除</span>
|
|
<span class="legend-item change">🟡 修改</span>
|
|
<span class="legend-item same">⚪ 不变</span>
|
|
</div>
|
|
|
|
<div class="merged-diff">
|
|
<div class="diff-box-title">{{ t('system.interface.mapping.modal.updateCache') }}</div>
|
|
<div class="merged-diff-body">
|
|
<template v-for="(row, idx) in visibleDiffRows" :key="idx">
|
|
<template v-if="row.type === 'change'">
|
|
<div class="merged-line change">
|
|
<span class="line-mark">🟡</span>
|
|
<span class="line-text">
|
|
{{ row.oldText }} → {{ row.newText }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div
|
|
class="merged-line"
|
|
:class="{
|
|
added: row.type === 'add',
|
|
removed: row.type === 'remove',
|
|
same: row.type === 'same'
|
|
}"
|
|
>
|
|
<span class="line-mark">
|
|
{{ row.type === 'add' ? '🟢' : row.type === 'remove' ? '🔴' : '⚪' }}
|
|
</span>
|
|
<span class="line-text">{{ row.text }}</span>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<div v-if="!visibleDiffRows.length" class="diff-empty">
|
|
{{ t('system.interface.mapping.message.noContentToPublish') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ModalPro>
|
|
|
|
<RollBack ref="rollBackRef" :server-info="serverInfo" />
|
|
|
|
<a-modal
|
|
v-model:visible="visible"
|
|
:title="t('system.interface.modal.warningTitle')"
|
|
@cancel="handleCancel"
|
|
@ok="sendMapOk"
|
|
>
|
|
<div class="mb-15px">{{ t('system.interface.mapping.modal.publishWarning') }}</div>
|
|
<a-input v-model="description" :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;
|
|
}
|
|
|
|
.release-content.slim {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.diff-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.diff-title {
|
|
font-size: 13px;
|
|
color: #232A33;
|
|
}
|
|
|
|
.diff-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.diff-boxes {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.diff-box {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.diff-box-title {
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.diff-pre {
|
|
margin: 0;
|
|
padding: 10px 12px;
|
|
background: var(--color-fill-2);
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 6px;
|
|
min-height: 160px;
|
|
max-height: 280px;
|
|
overflow: auto;
|
|
font-family: Menlo, Consolas, monospace;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.diff-actions :deep(.arco-btn) {
|
|
padding: 0 8px;
|
|
min-width: 32px;
|
|
}
|
|
|
|
.merged-diff {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.merged-diff-body {
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 6px;
|
|
background: var(--app-card-bg);
|
|
max-height: 600px;
|
|
overflow: auto;
|
|
font-family: Menlo, Consolas, monospace;
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
padding: 10px 12px;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.merged-seg.added {
|
|
background: #e6ffed;
|
|
}
|
|
|
|
.merged-seg.removed {
|
|
background: #ffecec;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.merged-line {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 4px 0;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.merged-line.added {
|
|
background: #e6ffed;
|
|
}
|
|
|
|
.merged-line.removed {
|
|
background: #ffecec;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.merged-line.change {
|
|
background: #fffbe6;
|
|
}
|
|
|
|
.merged-line.same {
|
|
color: #666;
|
|
}
|
|
|
|
.legend {
|
|
display: flex;
|
|
gap: 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.legend-item.add { color: #16a34a; }
|
|
.legend-item.remove { color: #dc2626; }
|
|
.legend-item.change { color: #ca8a04; }
|
|
.legend-item.same { color: #666; }
|
|
|
|
.diff-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.control-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 4px;
|
|
background-color: var(--app-card-bg);
|
|
|
|
&:hover {
|
|
border-color: var(--app-primary-color);
|
|
color: var(--app-primary-color);
|
|
}
|
|
}
|
|
</style>
|