feat: 默认页面背景联动加载页,官网入口与线路面板体验优化
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m0s

页面背景默认改为默认灰并同步启动屏;顶栏/登录头可回官网;线路卡片与话题面板顾问入口改为图标。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-07 15:08:57 +08:00
parent d3f300d8b1
commit 9922d20141
39 changed files with 5304 additions and 2065 deletions

View File

@ -56,14 +56,24 @@
var isH5 = isH5Path;
var isEn = false;
var pageBg = '#f0f3f4';
try {
var locale =
localStorage.getItem('arco-locale') || localStorage.getItem('locale') || '';
isEn = /^en/i.test(locale);
var storedBg = localStorage.getItem('app-page-background-color');
if (storedBg && /^#?[0-9a-fA-F]{6}$/.test(storedBg)) {
pageBg = storedBg.charAt(0) === '#' ? storedBg : '#' + storedBg;
}
} catch (e) {}
var root = document.documentElement;
if (isH5) root.classList.add('app-boot-h5');
if (isEn) root.classList.add('app-boot-en');
/* 加载页与 OMS/官网共用页面背景配置,首屏前写入避免闪色 */
root.style.setProperty('--app-page-bg', pageBg);
root.style.setProperty('--kc-page-bg', pageBg);
root.style.setProperty('--ai-bg', pageBg);
root.style.setProperty('--ai-canvas', pageBg);
try {
document.title = isH5
? isEn
@ -100,7 +110,7 @@
* 常驻 stable 会在右侧留出约 15px 空缝,贯穿 fixed 顶栏。
*/
html {
background: #f5f4f1;
background: var(--app-page-bg, #f0f3f4);
}
html.app-boot-site #app-boot {
display: none !important;
@ -118,7 +128,7 @@
#app {
margin: 0;
min-height: 100%;
background: #f5f4f1;
background: var(--app-page-bg, #f0f3f4);
}
#app-boot {
position: fixed;
@ -133,7 +143,7 @@
display: flex;
align-items: center;
justify-content: center;
background: #f5f4f1;
background: var(--app-page-bg, #f0f3f4);
/*
* 必须与 global.less 首屏后状态一致:
* Vue 加载后 html 会带上 antialiased,若 boot 首帧没有,中文会突然变细像「字号缩小」。
@ -272,9 +282,9 @@
#app-boot.is-timeout .boot-sub {
color: #86909c;
}
/* H5:样式挂在 html.app-boot-h5 上,首屏即正确,不再二次改字号/Logo */
/* H5:样式挂在 html.app-boot-h5 上,首屏即正确,不再二次改字号/Logo;背景随页面背景配置 */
html.app-boot-h5 {
background: #f5f6f8;
background: var(--app-page-bg, #f0f3f4);
}
html.app-boot-h5.app-booting {
scrollbar-gutter: stable;
@ -282,10 +292,10 @@
html.app-boot-h5,
html.app-boot-h5 body,
html.app-boot-h5 #app {
background: #f5f6f8;
background: var(--app-page-bg, #f0f3f4);
}
html.app-boot-h5 #app-boot {
background: #f5f6f8;
background: var(--app-page-bg, #f0f3f4);
padding: env(safe-area-inset-top, 0) 24px env(safe-area-inset-bottom, 0);
}
html.app-boot-h5 #app-boot .boot-panel {

View File

@ -81,15 +81,29 @@ export async function saveSalesProductCustomerGuide(body: {
assertBusinessOk(res.data)
}
/** 卖价 */
export async function getSalesProductPriceList(salesProductId: number | string) {
/** 卖价列表;列表页请传 includeDetails:false,避免拉回整张价格矩阵导致卡顿 */
export async function getSalesProductPriceList(
salesProductId: number | string,
opts?: { includeDetails?: boolean },
) {
const res = await axiosInstance.get(omsUrl('/channel/sales-product/price/list'), {
params: { salesProductId },
params: {
salesProductId,
includeDetails: opts?.includeDetails ?? true,
},
})
const { items } = parseListPayload(res.data)
return items ?? []
}
/** 单条卖价(含 factors/details),编辑弹窗按需加载 */
export async function getSalesProductPrice(id: number | string) {
const res = await axiosInstance.get(omsUrl('/channel/sales-product/price/get'), {
params: { id },
})
return unwrapData<any>(res.data) ?? res.data?.data ?? res.data
}
export async function createSalesProductPrice(body: Record<string, any>) {
const res = await axiosInstance.post(omsUrl('/channel/sales-product/price/create'), body)
assertBusinessOk(res.data)
@ -150,6 +164,8 @@ export async function getSalesProductPublishPriceList(params: {
salesProductId: number | string
page?: number
pageSize?: number
/** false 时后端不返回快照正文,用于版本头展示 */
includeContent?: boolean
}) {
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/price/list'), { params })
return parseListPayload(res.data)
@ -159,11 +175,21 @@ export async function getSalesProductPublishRulesList(params: {
salesProductId: number | string
page?: number
pageSize?: number
/** false 时只返回版本头,不带快照正文 */
includeContent?: boolean
}) {
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/rules/list'), { params })
return parseListPayload(res.data)
}
/** 单条发布记录(含快照正文) */
export async function getSalesProductPublishRecord(id: number | string) {
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/get'), {
params: { id },
})
return unwrapData<any>(res.data) ?? res.data?.data ?? res.data
}
/** 预览即将发布的快照(不落库),用于发布弹窗与发布记录核对 */
export async function getSalesProductPublishPreview(params: {
salesProductId: number | string

View File

@ -1,7 +1,22 @@
import { axiosInstance } from '@/api/rbac'
import { assertBusinessOk, omsUrl, parseListPayload } from '@/api/oms/common'
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
import { ENABLE_STATUS, isEnableStatusEnabled, toEnableStatus } from '@/utils/enableStatus'
/** 渠道下拉:全部名称,不分页 */
export async function getServiceChannelOptions() {
const res = await axiosInstance.get(omsUrl('/channel/service-channel/options'))
const body = res.data
if (Array.isArray(body)) return body
assertBusinessOk(body)
const raw = body?.data ?? body?.Data ?? body?.result ?? body?.Result ?? body
if (Array.isArray(raw)) return raw
if (raw && typeof raw === 'object') {
const items = (raw as any).items ?? (raw as any).Items ?? (raw as any).list ?? (raw as any).List
if (Array.isArray(items)) return items
}
return []
}
export async function getServiceChannelList(params: {
page?: number
pageSize?: number
@ -181,15 +196,30 @@ export async function deleteServiceChannelGroup(id: number | string) {
assertBusinessOk(res.data)
}
export async function getServiceChannelPriceList(serviceChannelId: number | string) {
/** 买价列表;列表页请传 includeDetails:false,避免拉回整张价格矩阵导致卡顿 */
export async function getServiceChannelPriceList(
serviceChannelId: number | string,
opts?: { includeDetails?: boolean },
) {
const res = await axiosInstance.get(omsUrl('/channel/service-channel/price/list'), {
params: { serviceChannelId },
params: {
serviceChannelId,
includeDetails: opts?.includeDetails ?? true,
},
})
// 与 parseListPayload 一致:兼容 data 为数组、{ items }、{ list } 等
const { items } = parseListPayload(res.data)
return items
}
/** 单条买价(含 factors/details),编辑弹窗按需加载 */
export async function getServiceChannelPrice(id: number | string) {
const res = await axiosInstance.get(omsUrl('/channel/service-channel/price/get'), {
params: { id },
})
return unwrapData<any>(res.data) ?? res.data?.data ?? res.data
}
export async function createServiceChannelPrice(body: Record<string, any>) {
const res = await axiosInstance.post(omsUrl('/channel/service-channel/price/create'), body)
assertBusinessOk(res.data)

View File

@ -5,7 +5,7 @@
--app-primary-color-light: #eef3f6;
--app-navbar-bg: #054f74;
--app-sidebar-bg: #054f74;
--app-page-bg: #f5f4f1;
--app-page-bg: #f0f3f4;
--app-card-bg: #ffffff;
--app-card-border: #e5e6eb;
--app-elevated-bg: #ffffff;

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, reactive, computed, nextTick, watch, h } from 'vue';
import { ref, reactive, computed, nextTick, watch, h, defineAsyncComponent } from 'vue';
import type { PropType } from 'vue';
import { IconDelete } from '@arco-design/web-vue/es/icon';
import { Message, Modal } from '@arco-design/web-vue';
@ -14,9 +14,11 @@
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
import { getMethodText } from '@/views/api/upstreamInterface/utils';
import { useTabRowCounts } from '@/utils/useTabRowCounts';
import RequestLog from '@/views/api/requestLog/index.vue';
import { prodApiOrigin } from '@/utils/rewriteBrowserApiPath';
/** 请求日志页含 Handsontable,仅打开「查看报文」时再加载 */
const RequestLog = defineAsyncComponent(() => import('@/views/api/requestLog/index.vue'));
const { t } = useI18n();
defineOptions({

View File

@ -603,6 +603,8 @@
--clm-node-second-line-height: 16px;
--clm-node-padding-y: 5px;
--clm-node-height: 60px;
/* 菜单项垂直间距 */
--clm-node-gap: 8px;
position: relative;
display: flex;
@ -858,7 +860,7 @@
}
.tree-node {
margin: 2px 0;
margin: calc(var(--clm-node-gap) / 2) 0;
padding: var(--clm-node-padding-y) 8px;
border-radius: var(--clm-radius);
border: 1px solid rgba(15, 16, 17, 0.06);
@ -879,7 +881,7 @@
height: auto;
min-height: 34px;
max-height: none;
margin: 8px 0 2px;
margin: calc(var(--clm-node-gap) + 4px) 0 calc(var(--clm-node-gap) / 2);
padding: 6px 8px;
border-color: transparent;
background: transparent;
@ -1098,7 +1100,7 @@
box-sizing: border-box;
border: 1px solid rgba(15, 16, 17, 0.06);
padding: var(--clm-node-padding-y) 8px;
margin: 2px 0;
margin: calc(var(--clm-node-gap) / 2) 0;
border-radius: var(--clm-radius);
transition: background-color 0.12s ease, border-color 0.12s ease;
cursor: pointer;
@ -1120,7 +1122,7 @@
height: auto;
min-height: 34px;
max-height: none;
margin: 10px 0 4px;
margin: calc(var(--clm-node-gap) + 4px) 0 calc(var(--clm-node-gap) / 2);
padding: 6px 8px;
border-color: transparent;
background: transparent;
@ -1184,10 +1186,11 @@
}
}
/* 实体叶子:保持卡片感 */
/* 实体叶子:固定高度,内容块垂直居中避免贴底 */
&.tree-node--leaf {
background: var(--color-fill-1);
border-color: rgba(15, 16, 17, 0.07);
align-items: center;
}
&:hover {
@ -1250,7 +1253,11 @@
width: 100%;
border-collapse: collapse;
table-layout: fixed;
height: 100%;
height: auto;
}
.node-second-row > td {
padding-top: 2px;
}
.node-content-cell {

View File

@ -1824,8 +1824,14 @@
if (!hotInstance) return;
const editor = hotInstance.getActiveEditor() as any;
if (!editor) return;
// TextEditor:只有传入 string 才会用该初值;数字会被忽略并回退 originalValue。
// 显式传入 String(cell) 避免 originalValue 未就绪时点进编辑被清空。
const raw = hotInstance.getDataAtCell(r, c);
const initial = raw === null || raw === undefined ? '' : String(raw);
if (!editor.isOpened?.()) {
editor.beginEditing?.();
editor.beginEditing?.(initial);
} else if (editor.TEXTAREA && (editor.TEXTAREA.value == null || editor.TEXTAREA.value === '')) {
editor.setValue?.(initial);
}
// 下拉/自动完成:点开即展开选项,避免用户不知道还能点箭头
const meta = hotInstance.getCellMeta(r, c);
@ -1833,8 +1839,12 @@
if (t === 'dropdown' || t === 'autocomplete') {
window.setTimeout(() => {
try {
if (typeof editor.open === 'function') editor.open();
else if (typeof editor.queryChoices === 'function') editor.queryChoices(editor.TEXTAREA?.value ?? '');
if (editor.TEXTAREA && !editor.TEXTAREA.value && initial) {
editor.setValue?.(initial);
}
// 用空 query 拉全量选项,避免「当前值与 source 不完全一致」时列表被滤空
if (typeof editor.queryChoices === 'function') editor.queryChoices('');
else if (typeof editor.open === 'function') editor.open();
} catch (_) {
/* ignore */
}
@ -2406,8 +2416,7 @@
}
}
// 监听数据变化
watch(() => props.data, () => {
function refreshHandsontableData() {
if (ignoreDataWatch) return;
// 编辑中勿 loadData,否则输入框/下拉会被立刻拆掉
if (hotInstance && props.readOnly === false) {
@ -2422,7 +2431,29 @@
void initHandsontableWhenReady({ finalize: false });
});
}
}, { deep: true });
}
/**
* 数据刷新策略:
* - 可编辑表:afterChange 会写回行对象;若 deep watch,上千行一改一格就全表 loadData → 卡死。
* 因此只在数组引用/长度变化时刷新。
* - 只读表:仍 deep watch,兼容外部原地改字段后的展示刷新。
*/
watch(
() => [props.data, props.data?.length] as const,
() => {
refreshHandsontableData();
},
);
watch(
() => props.data,
() => {
if (props.readOnly === false) return;
refreshHandsontableData();
},
{ deep: true },
);
// 监听分页变化
watch([() => props.currentPage, () => props.pageSize], () => {
@ -2442,10 +2473,26 @@
{ deep: true }
);
// 列定义变化时,重新对齐顺序并重建表格
function columnsStructuralKey(cols: ColumnDef[]): string {
return (cols || [])
.map((c) =>
[
c.key,
c.label,
c.type || '',
c.readOnly ? 1 : 0,
c.width || '',
Array.isArray(c.source) ? c.source.join('\u0001') : '',
].join('\u0002'),
)
.join('\u0003');
}
// 列定义变化时,仅在结构真正变化时重建(避免 computed 每次新对象引用就整表销毁)
watch(
() => props.columns,
() => {
() => columnsStructuralKey(props.columns),
(next, prev) => {
if (next === prev) return;
internalColumnOrder.value = normalizeColumnOrder(props.columns, internalColumnOrder.value);
if (hotInstance) {
nextTick(() => {
@ -2454,7 +2501,6 @@
});
}
},
{ deep: true }
);
// 窗口大小变化处理函数
@ -2502,12 +2548,15 @@
// 容器尺寸变化时同步表格高度,使列表高度与容器一致
let resizeObserver: ResizeObserver | null = null;
let lastObservedTableHeight = 0;
let resizeRaf = 0;
const setupResizeObserver = () => {
if (!containerRef.value) return;
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
lastObservedTableHeight = 0;
resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
if (!entry || !hotInstance) return;
@ -2515,9 +2564,14 @@
// 高度以本组件根容器实测为准;勿用外层 props.height 覆盖,否则与 flex 内真实高度不一致,stretchH 会把列压满导致横向条消失
if (h <= 0) return;
const fh = Math.max(Math.floor(h - customScrollbarSize), 120);
hotInstance.updateSettings({ height: fh });
nextTick(() => {
hotInstance?.refreshDimensions();
// 高度几乎不变时跳过,避免 updateSettings→再触发 RO 的死循环把页面卡死
if (Math.abs(fh - lastObservedTableHeight) < 2) return;
lastObservedTableHeight = fh;
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => {
resizeRaf = 0;
if (!hotInstance) return;
hotInstance.updateSettings({ height: fh }, false);
ensureHorizontalScrollbarVisible();
syncHorizontalScrollbarMetrics();
syncVerticalScrollbarMetrics();
@ -3382,7 +3436,8 @@
.autocompleteEditor.handsontable.listbox,
.ht-theme-classic .handsontable.listbox,
.ht-theme-classic-dark .handsontable.listbox {
z-index: 5600 !important;
/* 高于 Arco Modal(约 1001+),避免弹窗内下拉被挡住 */
z-index: 11000 !important;
border: 1px solid var(--color-border-2, #e5e6eb) !important;
border-radius: 6px !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;

View File

@ -1,14 +1,14 @@
<template>
<div id="top-nav" class="top-nav" :class="{ tabNavEnable: tabNavEnable }">
<div class="top-nav-header">
<!-- Logo 区域 -->
<div class="top-nav-logo">
<!-- Logo 区域:点击跳转官网首页 -->
<a class="top-nav-logo" href="/" :title="t('brand.fullName')" @click.prevent="goOfficialSite">
<img :src="navLogo" :alt="t('brand.name')" />
<div class="top-nav-logo__brand">
<span class="top-nav-logo__name">{{ t('brand.name') }}</span>
<span class="top-nav-logo__full">{{ t('brand.fullName') }}</span>
</div>
</div>
</a>
<!-- 顶部水平菜单:磁吸滑块 + 极速 hover 下拉 -->
<div
@ -298,6 +298,11 @@ const topNavEnable = computed(() => true); // 启用顶部导航
const tabNavEnable = computed(() => appStore.tabBar);
const printConnectionState = useHiprintClientState();
/** 顶栏 Logo → 官网首页(开发环境即 http://localhost:5173/) */
function goOfficialSite() {
window.location.href = '/';
}
const menuItemEls = new Map<string, HTMLElement>();
const indicatorReady = ref(false);
const indicatorVisible = ref(false);
@ -307,6 +312,8 @@ const indicatorStyle = ref<Record<string, string>>({
});
/** 磁吸:当前指针悬停的一级菜单;空则吸回路由选中项 */
const hoverMenuKey = ref('');
/** 跳转中目标一级(路由尚未变时避免滑块先吸回旧项再弹到新项) */
const pendingNavTopKey = ref('');
/** 下拉进入/离开延迟(ms):偏快,横向扫菜单时几乎无感 */
const SUBMENU_ENTER_DELAY = 20;
const SUBMENU_LEAVE_DELAY = 120;
@ -336,6 +343,7 @@ function setMenuItemRef(key: string, el: Element | ComponentPublicInstance | nul
function resolveIndicatorKey() {
if (hoverMenuKey.value) return hoverMenuKey.value;
if (openedSubmenuKey.value) return openedSubmenuKey.value;
if (pendingNavTopKey.value) return pendingNavTopKey.value;
return isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value;
}
@ -385,6 +393,7 @@ function onMenuItemPointerEnter(key: string) {
}
function onMenuBarPointerLeave(e: PointerEvent) {
if (navPaintLock.value) return;
const next = e.relatedTarget as Node | null;
// 指针进入二级浮层时保持当前一级高亮,不吸回
if (
@ -398,6 +407,7 @@ function onMenuBarPointerLeave(e: PointerEvent) {
hoverMenuKey.value = '';
moveIndicatorToKey(
openedSubmenuKey.value ||
pendingNavTopKey.value ||
(isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value),
);
}
@ -417,7 +427,14 @@ async function onTopMenuClick(e: MouseEvent, item: any) {
e.preventDefault();
const children = filterPage(item.children);
if (children.length > 0) {
paintNavThenGo(children[0]);
// 有二级时只展开面板,不抢先进第一项(否则再点目标页等于连续加载两次)
if (openedSubmenuKey.value === MORE_MENU_KEY) {
return;
}
const key = menuKey(item);
openedSubmenuKey.value = key;
hoverMenuKey.value = key;
moveIndicatorToKey(key);
return;
}
paintNavThenGo(item);
@ -771,6 +788,19 @@ function onSubmenuPopupVisibleChange(key: string, visible: boolean) {
openedSubmenuKey.value = key;
hoverMenuKey.value = key;
moveIndicatorToKey(key);
// 展开一级时预取二级路由 chunk,缩短点击后白屏
if (key === MORE_MENU_KEY) {
overflowTopMenus.value.forEach((item) => {
filterPage(item.children).forEach((child) => prefetchMenuRoute(child));
});
} else {
const parent =
fittedTopMenus.value.find((m) => menuKey(m) === key) ||
overflowTopMenus.value.find((m) => menuKey(m) === key);
if (parent) {
filterPage(parent.children).forEach((child) => prefetchMenuRoute(child));
}
}
nextTick(() => {
requestAnimationFrame(() => {
const el = document.querySelector(
@ -1058,15 +1088,44 @@ function previewMenuNavigation(item: any) {
}
}
/** 二级项所属一级(溢出则落到「更多」) */
function findTopMenuKeyForItem(item: any): string {
const targetKey = menuKey(item);
const targetPath = String(item?.path || item?.route || '');
if (!targetKey && !targetPath) return '';
for (const menu of visibleTopMenus.value) {
const topKey = menuKey(menu);
if (topKey && topKey === targetKey) {
return overflowTopMenus.value.some((m) => menuKey(m) === topKey)
? MORE_MENU_KEY
: topKey;
}
for (const child of filterPage(menu.children) || []) {
const childPath = String(child.path || child.route || '');
const childMatched =
(targetKey && menuKey(child) === targetKey) ||
(targetPath && childPath === targetPath);
if (!childMatched) continue;
return overflowTopMenus.value.some((m) => menuKey(m) === topKey)
? MORE_MENU_KEY
: topKey;
}
}
return '';
}
/** 先关弹层、亮标签,下一帧再走路由,避免页面重渲染把顶栏一起卡住 */
let navGoingTo = '';
function paintNavThenGo(item: any) {
const rawPath = String(item?.path || item?.route || '');
if (navGoingTo && navGoingTo === rawPath) return;
navGoingTo = rawPath;
// 先锁再关弹层,避免 leave/关闭回调把滑块吸回旧一级
navPaintLock.value = true;
pendingNavTopKey.value = findTopMenuKeyForItem(item);
openedSubmenuKey.value = '';
hoverMenuKey.value = '';
navPaintLock.value = true;
if (navPaintUnlockTimer) {
window.clearTimeout(navPaintUnlockTimer);
}
@ -1077,8 +1136,17 @@ function paintNavThenGo(item: any) {
goto(item);
navPaintUnlockTimer = window.setTimeout(() => {
navPaintLock.value = false;
// 路由已切到目标时清掉 pending;否则留到 belongTopMenu 对齐再清
if (
!pendingNavTopKey.value ||
pendingNavTopKey.value === belongTopMenu.value ||
(pendingNavTopKey.value === MORE_MENU_KEY && isOverflowActive.value)
) {
pendingNavTopKey.value = '';
}
navPaintUnlockTimer = 0;
navGoingTo = '';
updateActiveIndicator();
}, 280);
});
});
@ -1179,6 +1247,13 @@ function handleCustomerBalanceChanged() {
watch(
() => [belongTopMenu.value, menuDataReady.value, visibleTopMenus.value.length] as const,
async () => {
if (
pendingNavTopKey.value &&
(pendingNavTopKey.value === belongTopMenu.value ||
(pendingNavTopKey.value === MORE_MENU_KEY && isOverflowActive.value))
) {
pendingNavTopKey.value = '';
}
await nextTick();
await recalcMenuOverflow();
},
@ -1241,7 +1316,10 @@ onUnmounted(() => {
gap: 10px;
padding: 0 16px;
flex-shrink: 0;
color: inherit;
text-decoration: none;
cursor: pointer;
img {
height: 36px;
width: 36px;

View File

@ -8,7 +8,7 @@
"menuCollapse": false,
"footer": false,
"themeColor": "#054f74",
"pageBackgroundColor": "#f5f4f1",
"pageBackgroundColor": "#f0f3f4",
"menuWidth": 220,
"globalSettings": false,
"device": "desktop",

View File

@ -4995,6 +4995,8 @@ export default {
'channel.salesProducts.empty.selectProduct': 'Select a product from the left',
'channel.salesProducts.context.product': 'Product',
'channel.salesProducts.context.group': 'Group',
'channel.salesProducts.context.updateTime': 'Updated',
'channel.salesProducts.context.updateBy': 'Updated by',
'channel.salesProducts.context.unsaved': 'Unsaved',
'channel.salesProducts.context.publishPending': 'Config pending',
'channel.salesProducts.context.rulesPublishPending': 'Rules pending',
@ -5092,6 +5094,13 @@ export default {
'channel.salesProducts.publish.tabs.rules': 'Rules',
'channel.salesProducts.publish.versionRemark': 'Version note',
'channel.salesProducts.publish.noRemark': 'No notes',
'channel.salesProducts.publish.recordsHint': 'Select a version to view its snapshot; rollback is available',
'channel.salesProducts.publish.versionLabel': 'Version: {version}',
'channel.salesProducts.publish.status.publish': 'Published',
'channel.salesProducts.publish.status.rollback': 'Rolled back',
'channel.salesProducts.publish.emptyList': 'No publish records',
'channel.salesProducts.publish.emptyDetail': 'Select a version on the left to view details',
'channel.salesProducts.publish.emptyContent': 'No snapshot content',
'channel.salesProducts.ai.open': 'AI analysis',
'channel.salesProducts.ai.openRequired': 'Required: AI analysis',
@ -5377,6 +5386,11 @@ export default {
'channel.serviceChannels.record.versionRemark': 'Version note',
'channel.serviceChannels.record.noRemark': 'No notes',
'channel.serviceChannels.record.versionNumber': 'Version: {version}',
'channel.serviceChannels.record.recordsHint': 'Select a version to view its snapshot; rollback is available',
'channel.serviceChannels.record.status.rollback': 'Rolled back',
'channel.serviceChannels.record.emptyList': 'No publish records',
'channel.serviceChannels.record.emptyDetail': 'Select a version on the left to view details',
'channel.serviceChannels.record.emptyContent': 'No snapshot content',
'channel.serviceChannels.modal.publishDateTitle': 'Select publish date',
'channel.serviceChannels.modal.publishDate.placeholder': 'Please select date',
'channel.serviceChannels.modal.actions.cancel': 'Cancel',
@ -5411,11 +5425,14 @@ export default {
'channel.serviceChannels.drawer.priceList.batchFillSelected': '{count} rows selected',
'channel.serviceChannels.drawer.priceList.batchFillValue': 'Unit price',
'channel.serviceChannels.createTime': 'Created: {time}',
'channel.serviceChannels.updateTime': 'Updated: {time}',
'channel.serviceChannels.empty.selectChannel': 'Select a channel from the left',
'channel.serviceChannels.context.channel': 'Channel',
'channel.serviceChannels.context.group': 'Provider group',
'channel.serviceChannels.context.app': 'Application',
'channel.serviceChannels.context.apiProvider': 'API provider',
'channel.serviceChannels.context.updateTime': 'Updated',
'channel.serviceChannels.context.updateBy': 'Updated by',
'channel.serviceChannels.context.unsaved': 'Unsaved',
'channel.serviceChannels.context.rulesPublishPending': 'Config pending',
'channel.serviceChannels.publishConfirm.title': 'Publish confirmation',

View File

@ -5031,6 +5031,8 @@ export default {
'channel.salesProducts.empty.selectProduct': '请从左侧选择销售产品',
'channel.salesProducts.context.product': '当前产品',
'channel.salesProducts.context.group': '产品分组',
'channel.salesProducts.context.updateTime': '修改时间',
'channel.salesProducts.context.updateBy': '修改人',
'channel.salesProducts.context.unsaved': '未保存',
'channel.salesProducts.context.publishPending': '待发布配置',
'channel.salesProducts.context.rulesPublishPending': '待发布规则',
@ -5127,6 +5129,13 @@ export default {
'channel.salesProducts.publish.tabs.rules': '规则',
'channel.salesProducts.publish.versionRemark': '版本备注',
'channel.salesProducts.publish.noRemark': '暂无备注',
'channel.salesProducts.publish.recordsHint': '选择版本查看快照,可回滚到历史发布',
'channel.salesProducts.publish.versionLabel': '版本号:{version}',
'channel.salesProducts.publish.status.publish': '发布',
'channel.salesProducts.publish.status.rollback': '回滚',
'channel.salesProducts.publish.emptyList': '暂无发布记录',
'channel.salesProducts.publish.emptyDetail': '请选择左侧版本查看详情',
'channel.salesProducts.publish.emptyContent': '暂无快照内容',
'channel.salesProducts.ai.open': 'AI 分析',
'channel.salesProducts.ai.openRequired': '必做:AI 分析',
@ -5320,7 +5329,7 @@ export default {
'channel.salesProducts.drawer.footer.generatePriceList': '生成价格表',
'channel.salesProducts.drawer.footer.syncPriceList': '同步价格表',
'channel.salesProducts.drawer.footer.save': '保存',
'channel.salesProducts.drawer.priceList.hint': '已有组合会保留价格;勾选行可批量填价',
'channel.salesProducts.drawer.priceList.hint': '已有组合会保留价格;选中行可批量填价',
'channel.salesProducts.drawer.priceList.batchFill': '批量填价',
'channel.salesProducts.drawer.priceList.batchFillEmpty': '只填空白/0',
'channel.salesProducts.drawer.priceList.copyPrev': '复制上一行',
@ -5412,6 +5421,11 @@ export default {
'channel.serviceChannels.record.versionRemark': '版本备注',
'channel.serviceChannels.record.noRemark': '暂无备注',
'channel.serviceChannels.record.versionNumber': '版本号:{version}',
'channel.serviceChannels.record.recordsHint': '选择版本查看快照,可回滚到历史发布',
'channel.serviceChannels.record.status.rollback': '回滚',
'channel.serviceChannels.record.emptyList': '暂无发布记录',
'channel.serviceChannels.record.emptyDetail': '请选择左侧版本查看详情',
'channel.serviceChannels.record.emptyContent': '暂无快照内容',
'channel.serviceChannels.modal.publishDateTitle': '选择发布日期',
'channel.serviceChannels.modal.publishDate.placeholder': '请选择日期',
'channel.serviceChannels.modal.actions.cancel': '取消',
@ -5440,18 +5454,21 @@ export default {
'channel.serviceChannels.drawer.footer.generatePriceList': '生成价格表',
'channel.serviceChannels.drawer.footer.syncPriceList': '同步价格表',
'channel.serviceChannels.drawer.footer.save': '保存',
'channel.serviceChannels.drawer.priceList.hint': '已有组合会保留单价;勾选行可批量填价',
'channel.serviceChannels.drawer.priceList.hint': '已有组合会保留单价;选中行可批量填价',
'channel.serviceChannels.drawer.priceList.batchFill': '批量填价',
'channel.serviceChannels.drawer.priceList.batchFillEmpty': '只填空白/0',
'channel.serviceChannels.drawer.priceList.copyPrev': '复制上一行',
'channel.serviceChannels.drawer.priceList.batchFillSelected': '已选 {count} 行',
'channel.serviceChannels.drawer.priceList.batchFillValue': '统一单价',
'channel.serviceChannels.createTime': '创建: {time}',
'channel.serviceChannels.updateTime': '修改: {time}',
'channel.serviceChannels.empty.selectChannel': '请从左侧选择渠道',
'channel.serviceChannels.context.channel': '当前渠道',
'channel.serviceChannels.context.group': '服务商分组',
'channel.serviceChannels.context.app': '应用',
'channel.serviceChannels.context.apiProvider': 'API 服务商',
'channel.serviceChannels.context.updateTime': '修改时间',
'channel.serviceChannels.context.updateBy': '修改人',
'channel.serviceChannels.context.unsaved': '未保存',
'channel.serviceChannels.context.rulesPublishPending': '待发布配置',
'channel.serviceChannels.publishConfirm.title': '发布确认',

View File

@ -4,7 +4,7 @@
*/
:root {
--app-page-bg: #f5f4f1;
--app-page-bg: #f0f3f4;
--app-card-bg: #ffffff;
--app-card-border: #e5e6eb;
--app-elevated-bg: #ffffff;

50
src/utils/auditFields.ts Normal file
View File

@ -0,0 +1,50 @@
/** 后端未写入修改字段时的占位时间(首次新增常见) */
export function isPlaceholderAuditTime(val: unknown): boolean {
if (val == null || val === '') return true;
const s = String(val).trim();
if (!s || s === '--' || s === '-') return true;
return (
s.startsWith('1900-01-01') ||
s.startsWith('0001-01-01') ||
s.startsWith('0000-00-00')
);
}
function normalizeTimeText(val: unknown): string {
if (val == null) return '';
const s = String(val).trim();
if (!s) return '';
return s.replace('T', ' ').slice(0, 19);
}
/** 修改时间为空/占位时回退创建时间 */
export function resolveAuditTime(updateTime?: unknown, createTime?: unknown): string {
const updated = normalizeTimeText(updateTime);
if (!isPlaceholderAuditTime(updated)) return updated;
const created = normalizeTimeText(createTime);
if (!isPlaceholderAuditTime(created)) return created;
return '';
}
/** 修改人为空时回退创建人 */
export function resolveAuditBy(updateBy?: unknown, createBy?: unknown): string {
const u = updateBy != null ? String(updateBy).trim() : '';
if (u) return u;
const c = createBy != null ? String(createBy).trim() : '';
return c;
}
/** 从接口行统一取出创建/修改字段,并处理好首次新增占位 */
export function pickResolvedAudit(row: Record<string, any> | null | undefined) {
const createTime = row?.createTime ?? row?.CreateTime;
const updateTime =
row?.updateTime ?? row?.UpdateTime ?? row?.modifyTime ?? row?.ModifyTime;
const createBy = row?.createBy ?? row?.CreateBy;
const updateBy = row?.updateBy ?? row?.UpdateBy ?? row?.modifier;
return {
createTime: resolveAuditTime(createTime, createTime),
updateTime: resolveAuditTime(updateTime, createTime),
createBy: resolveAuditBy(createBy, createBy),
updateBy: resolveAuditBy(updateBy, createBy),
};
}

View File

@ -4,7 +4,7 @@
export const PAGE_BACKGROUND_STORAGE_KEY = 'app-page-background-color';
export const DEFAULT_PAGE_BACKGROUND = '#f5f4f1';
export const DEFAULT_PAGE_BACKGROUND = '#f0f3f4';
export const PAGE_BACKGROUND_PRESETS = [
{ color: '#f0f3f4', labelKey: 'settings.pageBg.default' },

View File

@ -231,6 +231,9 @@ function flattenDiffParts(diff: ReturnType<typeof diffJson>) {
return merged
}
/** 超过该字符量时跳过 diffJson 逐行展开,避免卖价大快照卡死主线程 */
const MAX_INLINE_DIFF_CHARS = 400_000
export function computeSnapshotDiff(
originalRaw: unknown,
modifiedRaw: unknown
@ -246,6 +249,20 @@ export function computeSnapshotDiff(
if (originalText === modifiedText) {
return { length: 0, rows: [], equal: true }
}
// 大快照(如上千行卖价明细)做逐行 diff 会分配海量行对象,直接卡死页面
if (originalText.length + modifiedText.length > MAX_INLINE_DIFF_CHARS) {
return {
length: 1,
rows: [
{
type: 'change',
oldText: '(快照较大,已跳过逐行对比以免页面卡死)',
newText: '请展开下方原始 JSON 查看完整差异',
},
],
equal: false,
}
}
const diff = diffJson(original, modified)
const rows = flattenDiffParts(diff)
const length = rows.filter((row) => row.type !== 'same').length

View File

@ -1,5 +1,15 @@
<script lang="ts" setup>
import { ref, reactive, computed, nextTick, onMounted, onActivated, unref, watch } from 'vue';
import {
ref,
reactive,
computed,
nextTick,
onMounted,
onActivated,
unref,
watch,
defineAsyncComponent,
} from 'vue';
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
import useModal from '@/components/ModalPro/useModal';
import ModalPro from '@/components/ModalPro/index.vue';
@ -17,8 +27,6 @@
postServiceProviderExport,
postServiceProviderImport,
} from '@/api/interface-serviceProvider/serviceProviderImportExport';
import ParameterConfig from '@/views/api/downstreamInterface/comps/ParameterConfig.vue';
import FixedParameterConfig from '@/views/api/downstreamInterface/comps/FixedParameterConfig.vue';
import FormPro from '@/components/FormPro/index.vue';
import TableButtons from '@/components/TableButtons/index.vue';
import {
@ -34,11 +42,25 @@
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
import { getAppGetDropDownList } from '@/api/ApplicationServices/apps';
import { getFixedParameterGetFixedParameterList } from '@/api/ParameterServices/fixedParameter';
import ServiceProviderInterfaceModal from './components/ServiceProviderInterfaceModal.vue';
import AccountList from './comps/AccountList.vue';
import ProviderMapPublishModal from './comps/ProviderMapPublishModal.vue';
import ApiDebugger from '@/views/api/upstreamInterface/components/ApiDebugger.vue';
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
/** 弹层/调试器按需加载,避免首屏拖入 CodeMirror / 请求日志 / Handsontable */
const ParameterConfig = defineAsyncComponent(
() => import('@/views/api/downstreamInterface/comps/ParameterConfig.vue'),
);
const FixedParameterConfig = defineAsyncComponent(
() => import('@/views/api/downstreamInterface/comps/FixedParameterConfig.vue'),
);
const ServiceProviderInterfaceModal = defineAsyncComponent(
() => import('./components/ServiceProviderInterfaceModal.vue'),
);
const AccountList = defineAsyncComponent(() => import('./comps/AccountList.vue'));
const ProviderMapPublishModal = defineAsyncComponent(
() => import('./comps/ProviderMapPublishModal.vue'),
);
const ApiDebugger = defineAsyncComponent(
() => import('@/views/api/upstreamInterface/components/ApiDebugger.vue'),
);
import {
IconEdit,
IconSettings,
@ -1842,7 +1864,7 @@
</div>
<!-- API调试组件 -->
<div v-show="interfaceDetailTab === 'debug'" class="debug-content">
<div v-if="interfaceDetailTab === 'debug'" class="debug-content">
<ApiDebugger
:key="`api-debugger-${selectedProvider?.id || ''}-${selectedInterface?.id || ''}-${apiDebuggerRefreshKey}`"
:selected-interface="selectedInterface"

View File

@ -1,20 +1,29 @@
<script lang="ts" setup>
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue';
import {
ref,
reactive,
computed,
onMounted,
nextTick,
watch,
defineAsyncComponent,
} from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import xmlFormat from 'xml-formatter';
import { $baseMessage } from '@/utils';
import useModal from '@/components/ModalPro/useModal';
import { getLogGetById, getLogGetList } from '@/api/ApplicationServices/log';
import { IconCopy } from '@arco-design/web-vue/es/icon';
import CodeMirror from '@/components/CodeMirror/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { parseFilterValue, toServerSearchValue } from '@/components/HandsontablePro/filterValue';
import type { ColumnDef } from '@/components/HandsontablePro/types';
/** 详情报文编辑器按需加载,避免进页就拉 CodeMirror */
const CodeMirror = defineAsyncComponent(() => import('@/components/CodeMirror/index.vue'));
const { t } = useI18n();
const route = useRoute();
@ -328,6 +337,7 @@
headers.value = '';
content.value = '';
exceptionType.value = '';
activeName.value = 'first';
openModal();
dialogVisible.value = true;
@ -335,10 +345,16 @@
const data: any = await getLogGetById({ id: row.id });
response.value = formatContent(data.response || '');
response2.value = formatContent(data.response2 || '');
headers.value = formatContent(data.headers || '');
content.value = formatContent(data.content || '');
const [body, hdr, resp, resp2] = await Promise.all([
formatContent(data.content || ''),
formatContent(data.headers || ''),
formatContent(data.response || ''),
formatContent(data.response2 || ''),
]);
content.value = body;
headers.value = hdr;
response.value = resp;
response2.value = resp2;
exceptionType.value = String(data.exception || '');
} catch (error) {
$baseMessage(t('system.log.message.loadDetailFailed'), 'error');
@ -398,7 +414,7 @@
return str.trim().startsWith('<?xml') || str.trim().startsWith('<');
}
function formatContent(str: string): string {
async function formatContent(str: string): Promise<string> {
if (!str) return '';
const strValue = String(str);
if (isJSON(strValue)) {
@ -410,6 +426,7 @@
}
if (isXML(strValue)) {
try {
const { default: xmlFormat } = await import('xml-formatter');
return xmlFormat(strValue);
} catch {
return strValue;
@ -521,11 +538,12 @@
@register="register"
@cancel="dialogVisible = false"
>
<div class="tabs-container">
<div v-if="dialogVisible" class="tabs-container">
<a-tabs v-model="activeName" class="mb-20px" @change="handleTabChange">
<a-tab-pane key="first" :title="$t('system.log.modal.tabs.requestBody')">
<div class="json-content">
<CodeMirror
v-if="activeName === 'first'"
v-model="content"
:language="getContentLanguage(content)"
theme="vs"
@ -545,6 +563,7 @@
<a-tab-pane key="second" :title="$t('system.log.modal.tabs.requestHeaders')">
<div class="json-content">
<CodeMirror
v-if="activeName === 'second'"
v-model="headers"
:language="getContentLanguage(headers)"
theme="vs"
@ -564,6 +583,7 @@
<a-tab-pane key="third" :title="$t('system.log.modal.tabs.response')">
<div class="json-content">
<CodeMirror
v-if="activeName === 'third'"
v-model="response"
:language="getContentLanguage(response)"
theme="vs"
@ -583,6 +603,7 @@
<a-tab-pane key="fourth" :title="$t('system.log.modal.tabs.exception')">
<div class="json-content">
<CodeMirror
v-if="activeName === 'fourth'"
v-model="exceptionType"
language="text"
theme="vs"
@ -602,6 +623,7 @@
<a-tab-pane key="fifth" :title="$t('system.log.modal.tabs.queueResult')">
<div class="json-content">
<CodeMirror
v-if="activeName === 'fifth'"
v-model="response2"
:language="getContentLanguage(response2)"
theme="vs"

View File

@ -2,17 +2,26 @@
import { useTabBarStore } from '@/store';
import { Modal, Message as $message } from '@arco-design/web-vue';
import { useI18n } from 'vue-i18n';
import { nextTick, onMounted, onActivated, ref, watch, computed, unref } from 'vue';
import {
nextTick,
onMounted,
onActivated,
ref,
watch,
computed,
unref,
reactive,
defineAsyncComponent,
} from 'vue';
import {
postInterfaceInterfacePaginatedList,
postInterfaceBatchDeleteInterfaceByIds,
} from '@/api/InterfaceServices/interface';
import CascaderPro from '@/components/CascaderPro/Cascader.vue';
import { getServiceProviderGetServiceProviderInterfaceCascadedList } from '@/api/interface-serviceProvider/serviceProvider';
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRefresh } from '@arco-design/web-vue/es/icon';
import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRefresh } from '@arco-design/web-vue/es/icon';
import dayjs from 'dayjs';
import { useRouter, useRoute } from 'vue-router';
@ -22,13 +31,20 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
} from '@/api/InterfaceMappingServices/interfaceMap';
import useModal from '@/components/ModalPro/useModal';
import { $baseMessage, $baseConfirm } from '@/utils';
import EnteredMap from './components/EnteredMap.vue';
import MapServiceProvider from './components/map-service-provider.vue';
import ApiDebugger from './components/ApiDebugger.vue';
import { systemInterfaceColumns } from './models/tableColumns';
import UpstreamInterfaceModal from './components/UpstreamInterfaceModal.vue';
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
/** 明细含 InterfaceDetail(~200KB)、调试含 CodeMirror:按需分包,避免进页就解析 */
const EnteredMap = defineAsyncComponent(() => import('./components/EnteredMap.vue'));
const MapServiceProvider = defineAsyncComponent(
() => import('./components/map-service-provider.vue'),
);
const ApiDebugger = defineAsyncComponent(() => import('./components/ApiDebugger.vue'));
const UpstreamInterfaceModal = defineAsyncComponent(
() => import('./components/UpstreamInterfaceModal.vue'),
);
const { t } = useI18n();
defineOptions({
@ -70,6 +86,8 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
const drawerFormRef = ref<any>();
const upStreamIdRef = ref<number>();
const enteredMapRef = ref<any>();
/** 弹窗内 EnteredMap,与右侧 Tab 实例分开,避免 ref 互相覆盖 */
const enterMapModalRef = ref<any>();
const drawerConfig = reactive({
itemList: [
{
@ -179,9 +197,47 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
const [register, registerMethods] = useModal() as any;
const [registerEnter, registerEnterMethods] = useModal() as any;
/** 入参映射弹窗打开后再挂 EnteredMap,避免进页就拉映射分包 */
const enterMapModalOpen = ref(false);
const mapId = ref();
// 映射服务商
const mapName = ref();
function activeEnteredMap() {
return (enterMapModalOpen.value ? unref(enterMapModalRef) : unref(enteredMapRef)) as any;
}
async function waitForEnteredMap() {
for (let i = 0; i < 40; i++) {
await nextTick();
const map = activeEnteredMap();
if (map?.initData) return map;
await new Promise((r) => setTimeout(r, 40));
}
return activeEnteredMap();
}
async function openEnterMapModal(opts?: {
title?: string;
init?: { serviceProviderId: number; interfaceId?: number };
}) {
enterMapModalOpen.value = true;
registerEnterMethods.setModalProps({
title: opts?.title || t('system.interface.modal.edit'),
width: 1600,
height: 1600,
});
registerEnterMethods.openModal();
if (opts?.init) {
const map = await waitForEnteredMap();
await map?.initData?.(opts.init);
}
}
function closeEnterMapModal() {
enterMapModalOpen.value = false;
registerEnterMethods.closeModal?.();
}
async function mapClick(row: any) {
newmapClickROW.value = row;
mapName.value = row.name;
@ -797,18 +853,12 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
intId: id,
};
registerEnterMethods.openModal();
await nextTick();
registerEnterMethods.setModalProps({
await openEnterMapModal({
title: t('system.interface.modal.add'),
width: 1600,
height: 1600,
});
(unref(enteredMapRef) as any)?.initData?.({
serviceProviderId: serviceProviderId.value,
interfaceId: upStreamIdRef.value,
init: {
serviceProviderId: serviceProviderId.value,
interfaceId: upStreamIdRef.value,
},
});
active.value = 1;
@ -831,28 +881,19 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
const fullScreen = ref(false);
const serviceProviderName = ref('');
async function handlerEditMap(data: any) {
async function handlerEditMap(data: any) {
editMapIds.value = data;
serviceProviderId.value = data.serviceProviderId;
serviceProviderName.value = data.serviceProviderName;
upStreamIdRef.value = data.addId;
InputParameterMappingList.value = data.inputParameterMappingList;
registerEnterMethods.setModalProps({
await openEnterMapModal({
title: t('system.interface.modal.edit'),
width: 1600,
height: 1600,
});
registerEnterMethods.openModal();
await nextTick();
await (unref(enteredMapRef) as any)?.initData?.({
init: {
serviceProviderId: data.serviceProviderId,
interfaceId: data.addId,
}
);
},
});
}
const MapServiceProviderROW = ref<any>();
const MapServiceProviderRef = ref<any>();
@ -885,7 +926,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
}
async function saveModelEnter() {
const mapComp = unref(enteredMapRef) as any;
const mapComp = activeEnteredMap();
const guard = mapComp?.guardBeforeSave?.() as 'ok' | 'goto-output' | 'required' | undefined;
if (guard === 'goto-output') {
$baseMessage(t('system.interface.mapping.outputMappingGuide'), 'warning');
@ -927,7 +968,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
try {
await postSaveParameterMaps(data);
loadingSubmit.value = false;
(unref(enteredMapRef) as any)?.initData?.(
activeEnteredMap()?.initData?.(
{
serviceProviderId: serviceProviderId.value,
interfaceId: upStreamIdRef.value,
@ -939,7 +980,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
title: t('system.interface.modal.warningTitle'),
content: t('system.interface.modal.publishConfirm'),
onOk: async () => {
await registerEnterMethods.closeModal?.();
closeEnterMapModal();
const currentRow = MapServiceProviderROW.value;
if (currentRow) {
(unref(MapServiceProviderRef) as any)?.onRelease?.(currentRow);
@ -1398,7 +1439,6 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
title: category,
isLeaf: false,
depth: 1,
subtitle: `${items.length} 个接口`,
children: items
};
treeData.push(categoryNode);
@ -1679,22 +1719,19 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
class="mapping-modal"
@register="registerEnter"
@ok="saveModelEnter"
@cancel="enterMapModalOpen = false"
@reset="() => {}"
>
<div >
<EnteredMap
ref="enteredMapRef"
class="h-full w-full"
:service-provider-id="serviceProviderId"
<div v-if="enterMapModalOpen">
<EnteredMap
ref="enterMapModalRef"
class="h-full w-full"
:service-provider-id="serviceProviderId"
:interface-id="upStreamIdRef"
:service-provider-name="serviceProviderName"
@save-model-enter="saveModelEnter"
/>
</div>
:service-provider-name="serviceProviderName"
@save-model-enter="saveModelEnter"
/>
</div>
</ModalPro>
</div>
</template>

View File

@ -3,9 +3,11 @@
* 卖价/买价「计费条件」紧凑编辑:
* 默认只占输入区,不铺开字段芯片;
* AI 写公式走侧栏抽屉;手动插字段走 Popover。
* 传入 label 时:标签与工具栏同一行,工具栏在右侧。
*/
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon'
import FormulaAiAssistDrawer from '@/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue'
import {
FORMULA_MAX_LENGTH,
@ -23,9 +25,18 @@ import {
buildBillingConditionAiSystemPrompt,
} from '../billingConditionAiSkill'
const props = defineProps<{
modelValue: string
}>()
const props = withDefaults(
defineProps<{
modelValue: string
/** 有值时:标签与工具栏同一行,工具栏在右侧 */
label?: string
labelTip?: string
}>(),
{
label: '',
labelTip: '',
},
)
const emit = defineEmits<{
'update:modelValue': [string]
@ -47,6 +58,7 @@ const displayValue = computed(() => {
const v = props.modelValue ?? ''
return v === '无' ? '' : v
})
const toolbarInline = computed(() => !!props.label)
function updateValue(next: string) {
emit('update:modelValue', next.slice(0, FORMULA_MAX_LENGTH))
@ -88,7 +100,14 @@ watch(
</script>
<template>
<div class="billing-condition-editor">
<div class="billing-condition-editor" :class="{ 'has-inline-toolbar': toolbarInline }">
<div v-if="toolbarInline" class="editor-label">
{{ label }}
<a-tooltip v-if="labelTip" :content="labelTip">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</div>
<div class="formula-text-wrap">
<textarea
ref="textareaRef"
@ -102,9 +121,13 @@ watch(
<div class="char-count">{{ charCount }}/{{ FORMULA_MAX_LENGTH }}</div>
</div>
<div class="editor-footer">
<div class="editor-footer" :class="{ 'editor-footer--inline': toolbarInline }">
<div class="editor-actions">
<a-button type="secondary" size="small" @click="aiVisible = true">
<a-button
type="secondary"
:size="toolbarInline ? 'mini' : 'small'"
@click="aiVisible = true"
>
{{ t('channel.salesProducts.billingCondition.aiWrite') }}
</a-button>
<a-popover
@ -113,7 +136,7 @@ watch(
position="bl"
:content-style="{ padding: '16px', width: '520px', maxWidth: '92vw' }"
>
<a-button type="secondary" size="small">
<a-button type="secondary" :size="toolbarInline ? 'mini' : 'small'">
{{ t('channel.salesProducts.billingCondition.insertField') }}
</a-button>
<template #content>
@ -181,6 +204,48 @@ watch(
<style lang="less" scoped>
.billing-condition-editor {
width: 100%;
&.has-inline-toolbar {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-areas:
'label footer'
'text text';
column-gap: 12px;
row-gap: 6px;
align-items: center;
.editor-label {
grid-area: label;
}
.editor-footer {
grid-area: footer;
margin-top: 0;
justify-content: flex-end;
min-width: 0;
}
.formula-text-wrap {
grid-area: text;
}
}
}
.editor-label {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: var(--color-text-2);
white-space: nowrap;
line-height: 22px;
}
.tip-icon {
color: var(--color-text-3);
cursor: help;
font-size: 14px;
}
.formula-text-wrap {
@ -228,6 +293,11 @@ watch(
align-items: center;
gap: 8px 12px;
margin-top: 6px;
&--inline {
flex-wrap: nowrap;
margin-top: 0;
}
}
.editor-actions {
@ -239,8 +309,12 @@ watch(
.editor-hint {
font-size: 12px;
color: var(--color-border-3);
color: var(--color-text-3);
line-height: 1.4;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.field-popover {

View File

@ -15,7 +15,7 @@
<a-form :model="form" layout="vertical" class="sim-form">
<a-row :gutter="12">
<a-col :span="12">
<a-form-item :label="t('channel.salesProducts.billingSimulate.country')" required>
<a-form-item :label="t('channel.salesProducts.billingSimulate.destCountry')" required>
<a-select
v-model="form.countryCode"
allow-search

View File

@ -0,0 +1,590 @@
<template>
<div ref="wrapRef" class="price-list-hot-wrap" :style="{ height: `${height}px` }">
<div
ref="hostRef"
class="price-list-hot"
:class="{ 'is-ready': ready }"
></div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import Handsontable from 'handsontable';
import { registerAllModules } from 'handsontable/registry';
import { registerLanguageDictionary, zhCN } from 'handsontable/i18n';
import 'handsontable/styles/handsontable.min.css';
import 'handsontable/styles/ht-theme-classic.min.css';
registerAllModules();
registerLanguageDictionary(zhCN);
export type PriceListHotColumn = {
key: string;
label: string;
width?: number;
readOnly?: boolean;
type?: 'text' | 'numeric' | 'dropdown';
source?: string[];
formatter?: (row: any, value: any) => string;
};
const props = withDefaults(
defineProps<{
columns: PriceListHotColumn[];
data: any[];
height?: number;
/** 计费因素列:同一上级分组内、连续相同的展示值纵向合并 */
mergeSameValues?: boolean;
}>(),
{
height: 500,
mergeSameValues: false,
},
);
const emit = defineEmits<{
'selection-change': [ids: Array<number | string>];
}>();
const wrapRef = ref<HTMLElement | null>(null);
const hostRef = ref<HTMLElement | null>(null);
const ready = ref(false);
let hot: Handsontable | null = null;
let ignoreChange = false;
/** 合并插件会把被盖住的单元格写成空,这里留一份因素列原值,关闭合并时写回 */
let factorSnapshot: Array<Record<string, any>> | null = null;
let selectedRowIndexes: number[] = [];
let selectedRowSet = new Set<number>();
let selectionLock = 0;
let resizeObserver: ResizeObserver | null = null;
let lastWidth = 0;
let resizeRaf = 0;
let mountTimer: ReturnType<typeof setTimeout> | null = null;
/** 量外层容器宽度(不要量 HOT 根节点,否则会被写死的 px 宽度锁死) */
function readWrapWidth() {
const el = wrapRef.value;
if (!el) return 0;
const w = Math.floor(el.clientWidth || el.getBoundingClientRect().width);
return w > 0 ? w : 0;
}
function buildColumns(): any[] {
const cols: any[] = [
{
data: '_serial',
readOnly: true,
width: 56,
className: 'htCenter',
},
];
for (const col of props.columns) {
const conf: any = {
data: col.key,
readOnly: !!col.readOnly,
width: col.width || 120,
className: col.readOnly ? 'htLeft price-factor-cell' : 'htRight',
};
if (col.type === 'numeric') {
conf.type = 'numeric';
conf.numericFormat = { pattern: '0.00' };
conf.className = 'htRight';
} else if (col.type === 'dropdown') {
conf.type = 'dropdown';
conf.source = col.source || [];
conf.strict = true;
conf.allowInvalid = false;
conf.className = 'htLeft';
} else {
conf.type = 'text';
}
if (col.formatter) {
const fmt = col.formatter;
conf.renderer = (
instance: Handsontable,
td: HTMLTableCellElement,
row: number,
_col: number,
_prop: any,
value: any,
) => {
const rowData = instance.getSourceDataAtRow(row) as any;
td.className = conf.className || '';
td.textContent = fmt(rowData, value);
};
}
cols.push(conf);
}
return cols;
}
function buildColHeaders(): string[] {
return ['序号', ...props.columns.map((c) => c.label)];
}
function factorCellText(row: any, col: PriceListHotColumn): string {
const raw = row?.[col.key];
if (col.formatter) return String(col.formatter(row, raw) ?? '').trim();
return String(raw ?? '').trim();
}
/** 只合并计费因素列,且必须和左侧因素同属一组,避免跨仓库把相同货物类型并到一起 */
function buildMergeCells() {
if (!props.mergeSameValues) return [];
const rows = props.data || [];
const count = rows.length;
if (count < 2) return [];
const factors = props.columns
.map((col, index) => ({ col, hotCol: index + 1 }))
.filter(({ col }) => col.key.startsWith('factor_'));
const merges: { row: number; col: number; rowspan: number; colspan: number }[] = [];
for (let factorIndex = 0; factorIndex < factors.length; factorIndex += 1) {
const keys = factors.slice(0, factorIndex + 1);
let start = 0;
while (start < count) {
let end = start;
while (end + 1 < count && sameFactorGroup(rows, start, end + 1, keys)) {
end += 1;
}
const text = factorCellText(rows[start], factors[factorIndex].col);
if (text && end > start) {
merges.push({
row: start,
col: factors[factorIndex].hotCol,
rowspan: end - start + 1,
colspan: 1,
});
}
start = end + 1;
}
}
return merges;
}
function sameFactorGroup(
rows: any[],
left: number,
right: number,
keys: { col: PriceListHotColumn }[],
) {
return keys.every(
({ col }) => factorCellText(rows[left], col) === factorCellText(rows[right], col),
);
}
function mergeCellsOption() {
const cells = buildMergeCells();
if (!cells.length) return false;
return { virtualized: true, cells };
}
function factorColumnKeys() {
return props.columns.filter((col) => col.key.startsWith('factor_')).map((col) => col.key);
}
function rememberFactorValues() {
const keys = factorColumnKeys();
const rows = props.data || [];
if (!factorSnapshot || factorSnapshot.length !== rows.length) {
factorSnapshot = rows.map(() => ({}));
}
rows.forEach((row, index) => {
const snap = factorSnapshot![index];
for (const key of keys) {
const value = row?.[key];
if (value != null && String(value).trim() !== '') snap[key] = value;
}
});
}
function restoreFactorValues() {
if (!factorSnapshot) return;
const keys = factorColumnKeys();
(props.data || []).forEach((row, index) => {
const snap = factorSnapshot![index];
if (!row || !snap) return;
for (const key of keys) {
if (key in snap && row[key] !== snap[key]) row[key] = snap[key];
}
});
}
function applyMergeCells() {
if (!hot) return;
rememberFactorValues();
const option = mergeCellsOption();
if (!option) {
const plugin = hot.getPlugin('mergeCells') as { clearCollections?: () => void } | undefined;
plugin?.clearCollections?.();
}
hot.updateSettings({ mergeCells: option }, false);
restoreFactorValues();
hot.render();
}
function decorateRows(rows: any[]): any[] {
return (rows || []).map((row, idx) => {
if (row && typeof row === 'object') {
row._serial = idx + 1;
}
return row;
});
}
/** 序号列不参与拉伸,只拉宽数据列 */
function beforeStretchingColumnWidth(stretchedWidth: number, column: number) {
if (column === 0) return 56;
return stretchedWidth;
}
function collectSelectedRowIndexes() {
if (!hot) return [] as number[];
const selected = hot.getSelected();
if (!selected?.length) return [] as number[];
const max = Math.max((props.data?.length || 1) - 1, 0);
const set = new Set<number>();
for (const [r1, , r2] of selected) {
if (r1 == null || r2 == null || (r1 < 0 && r2 < 0)) continue;
const from = Math.max(0, Math.min(r1, r2));
const to = Math.min(max, Math.max(r1, r2));
for (let row = from; row <= to; row += 1) set.add(row);
}
return [...set].sort((a, b) => a - b);
}
function publishSelection(indexes = collectSelectedRowIndexes()) {
selectedRowIndexes = indexes;
selectedRowSet = new Set(indexes);
const ids = indexes
.map((index) => props.data?.[index]?.id)
.filter((id) => id != null);
emit('selection-change', ids);
}
function groupConsecutive(indexes: number[]) {
const ranges: Array<[number, number]> = [];
for (const index of indexes) {
const last = ranges[ranges.length - 1];
if (last && last[1] === index - 1) last[1] = index;
else ranges.push([index, index]);
}
return ranges;
}
function applyRowSelection(indexes: number[]) {
if (!hot) return;
selectionLock += 1;
try {
if (!indexes.length) {
hot.deselectCell();
} else {
const lastCol = Math.max(hot.countCols() - 1, 0);
const specs = groupConsecutive(indexes).map(
([from, to]) => [from, 0, to, lastCol] as [number, number, number, number],
);
hot.selectCells(specs, false, false);
}
} finally {
selectionLock -= 1;
}
publishSelection(indexes);
hot.render();
}
function selectAll() {
const count = props.data?.length || 0;
if (!count) return;
applyRowSelection(Array.from({ length: count }, (_, index) => index));
}
function clearSelection() {
applyRowSelection([]);
}
function destroyHot() {
if (mountTimer) {
clearTimeout(mountTimer);
mountTimer = null;
}
if (hot) {
hot.destroy();
hot = null;
}
ready.value = false;
}
function mountHot() {
if (!hostRef.value || !wrapRef.value) return;
destroyHot();
let width = readWrapWidth();
// 弹窗动画未完成时宽度可能为 0,先用父级估算,稍后校正
if (width < 200) {
const parent = wrapRef.value.parentElement;
width = Math.max(Math.floor(parent?.clientWidth || 0), 800);
}
lastWidth = width;
rememberFactorValues();
hot = new Handsontable(hostRef.value, {
data: decorateRows(props.data),
columns: buildColumns(),
colHeaders: buildColHeaders(),
rowHeaders: false,
width,
height: props.height,
licenseKey: 'non-commercial-and-evaluation',
themeName: 'ht-theme-classic',
language: 'zh-CN',
stretchH: 'all',
beforeStretchingColumnWidth,
autoRowSize: false,
autoColumnSize: false,
rowHeights: 32,
columnHeaderHeight: 36,
renderAllRows: false,
viewportRowRenderingOffset: 20,
viewportColumnRenderingOffset: 4,
enterBeginsEditing: true,
fragmentSelection: false,
selectionMode: 'multiple',
outsideClickDeselects(event: MouseEvent) {
const target = event.target as HTMLElement | null;
return !target?.closest('.sales-price-config-modal, .arco-modal, .arco-trigger-popup');
},
allowInsertRow: false,
allowInsertColumn: false,
allowRemoveRow: false,
allowRemoveColumn: false,
contextMenu: false,
manualColumnResize: true,
mergeCells: mergeCellsOption(),
afterGetColHeader(col, th) {
// 价格列靠右,其余居中;垂直居中(不依赖 instance,避免挂载期 this 问题)
th.style.verticalAlign = 'middle';
if (col < 1) {
th.style.textAlign = 'center';
return;
}
const meta = props.columns[col - 1];
const isPriceCol = !!meta && !meta.readOnly && meta.type !== 'dropdown';
th.style.textAlign = isPriceCol ? 'right' : 'center';
},
afterChange(changes, source) {
if (!changes || source === 'loadData' || source === 'MergeCells' || ignoreChange) return;
for (const [rowIndex, prop, _oldVal, newVal] of changes) {
const row = hot?.getSourceDataAtRow(rowIndex) as any;
if (!row || typeof prop !== 'string') continue;
if (prop === '_serial') continue;
row[prop] = newVal;
}
},
afterSelectionEnd() {
if (selectionLock) return;
publishSelection();
hot?.render();
},
afterDeselect() {
if (selectionLock) return;
selectedRowIndexes = [];
selectedRowSet = new Set();
emit('selection-change', []);
hot?.render();
},
afterRenderer(td, row) {
td.parentElement?.classList.toggle('price-row-selected', selectedRowSet.has(row));
},
});
restoreFactorValues();
hot.render();
ready.value = true;
// 首次挂载后再按真实宽度拉一次(不重挂)
requestAnimationFrame(() => syncWidth(true));
}
function reloadData() {
if (!hot) {
mountHot();
return;
}
ignoreChange = true;
const keepSelection = selectedRowIndexes.slice();
selectionLock += 1;
try {
rememberFactorValues();
hot.loadData(decorateRows(props.data));
applyMergeCells();
const max = (props.data?.length || 0) - 1;
applyRowSelection(keepSelection.filter((index) => index <= max));
} finally {
selectionLock -= 1;
requestAnimationFrame(() => {
ignoreChange = false;
});
}
}
function syncWidth(force = false) {
if (!hot) return;
const width = readWrapWidth();
if (width < 200) return;
if (!force && Math.abs(width - lastWidth) < 2) return;
lastWidth = width;
hot.updateSettings(
{
width,
height: props.height,
stretchH: 'all',
},
false,
);
hot.refreshDimensions();
}
function setupResizeObserver() {
if (!wrapRef.value) return;
resizeObserver?.disconnect();
resizeObserver = new ResizeObserver(() => {
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => {
resizeRaf = 0;
syncWidth();
});
});
resizeObserver.observe(wrapRef.value);
}
onMounted(() => {
// 等弹窗/Tab 布局完成再挂载,效果等同「切走再切回」后的正确首屏
void nextTick(async () => {
await new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
// 再让出一帧给弹窗动画/flex 定高
await new Promise<void>((resolve) => {
mountTimer = setTimeout(() => {
mountTimer = null;
resolve();
}, 50);
});
if (!wrapRef.value || !hostRef.value) return;
mountHot();
setupResizeObserver();
});
});
onBeforeUnmount(() => {
if (resizeRaf) cancelAnimationFrame(resizeRaf);
if (mountTimer) {
clearTimeout(mountTimer);
mountTimer = null;
}
resizeObserver?.disconnect();
resizeObserver = null;
destroyHot();
});
watch(
() => props.height,
(h) => {
if (!hot || !h) return;
hot.updateSettings({ height: h }, false);
hot.refreshDimensions();
},
);
watch(
() =>
props.columns
.map((c) => `${c.key}:${c.label}:${c.type}:${(c.source || []).join(',')}`)
.join('|'),
() => {
void nextTick(() => mountHot());
},
);
watch(
() => [props.data, props.data?.length] as const,
() => {
reloadData();
},
);
watch(
() => props.mergeSameValues,
() => {
applyMergeCells();
},
);
defineExpose({ selectAll, clearSelection });
</script>
<style scoped lang="less">
.price-list-hot-wrap {
box-sizing: border-box;
width: 100%;
min-width: 0;
}
.price-list-hot {
width: 100%;
height: 100%;
overflow: hidden;
border: none;
border-radius: 6px;
opacity: 0;
transition: opacity 0.12s ease;
&.is-ready {
opacity: 1;
}
:deep(tr.price-row-selected td) {
box-shadow: inset 0 0 0 9999px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
}
:deep(.price-factor-cell) {
color: var(--color-text-2);
background: color-mix(in srgb, var(--app-primary-color) 4%, #fff);
vertical-align: middle;
}
:deep(.handsontable) {
font-size: 13px;
color: var(--color-text-1);
}
:deep(.htCore th) {
background: var(--color-fill-1) !important;
color: var(--color-text-2) !important;
font-weight: 600;
border-color: var(--color-border-2) !important;
vertical-align: middle !important;
}
:deep(.htCore td) {
border-color: var(--color-border-2) !important;
}
:deep(.htCore tr:hover td) {
background: color-mix(in srgb, var(--app-primary-color) 5%, #fff) !important;
}
/* 防止 HOT 写死宽度后外层出现空白 */
:deep(.ht-wrapper),
:deep(.handsontable) {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style lang="less">
.handsontable.listbox,
.autocompleteEditor.handsontable.listbox {
z-index: 11000 !important;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,11 @@
<tr>
<td class="product-row-left">
<span class="update-time">
{{ t('channel.weightRounding.updateTime', { time: nodeData?.updateTime ?? '' }) }}
{{
t('channel.weightRounding.updateTime', {
time: nodeData?.updateTime || '--',
})
}}
</span>
</td>
<td class="product-row-right"></td>
@ -159,6 +163,7 @@
import ChannelConfigHotTable from '@/views/channel/components/ChannelConfigHotTable.vue';
import type { ChannelHotColumn } from '@/views/channel/components/channelHotTable.types';
import { syncChannelLeftSelection } from '@/views/channel/composables/channelLeftSelection';
import { pickResolvedAudit } from '@/utils/auditFields';
const { t } = useI18n();
@ -505,14 +510,19 @@
{
key: 'category-list',
title: t('channel.weightRounding.listTitle'),
children: leftMenuData.value.map((item) => ({
key: `item-${item.id}`,
title: item.name,
isLeaf: true,
id: item.id,
name: item.name,
...item,
})),
children: leftMenuData.value.map((item) => {
const audit = pickResolvedAudit(item);
return {
key: `item-${item.id}`,
title: item.name,
isLeaf: true,
id: item.id,
name: item.name,
...item,
updateTime: audit.updateTime,
updateBy: audit.updateBy,
};
}),
},
]);

View File

@ -41,7 +41,7 @@
<tr>
<td class="product-row-left">
<span class="update-time">
{{ t('channel.weightRules.updateTime', { time: nodeData?.updateTime ?? '' }) }}
{{ t('channel.weightRules.updateTime', { time: nodeData?.updateTime || '--' }) }}
</span>
</td>
<td class="product-row-right"></td>
@ -161,6 +161,7 @@
import ChannelConfigHotTable from '@/views/channel/components/ChannelConfigHotTable.vue';
import type { ChannelHotColumn } from '@/views/channel/components/channelHotTable.types';
import { syncChannelLeftSelection } from '@/views/channel/composables/channelLeftSelection';
import { pickResolvedAudit } from '@/utils/auditFields';
const { t } = useI18n();
@ -489,14 +490,19 @@
{
key: 'category-list',
title: t('channel.weightRules.listTitle'),
children: leftMenuData.value.map((item) => ({
key: `item-${item.id}`,
title: item.name,
isLeaf: true,
id: item.id,
name: item.name,
...item,
})),
children: leftMenuData.value.map((item) => {
const audit = pickResolvedAudit(item);
return {
key: `item-${item.id}`,
title: item.name,
isLeaf: true,
id: item.id,
name: item.name,
...item,
updateTime: audit.updateTime,
updateBy: audit.updateBy,
};
}),
},
]);

View File

@ -1,9 +1,14 @@
<template>
<div class="nova-login">
<div class="nova-login__header">
<a
class="nova-login__header"
href="/"
:title="brandHint"
@click.prevent="goOfficialSite"
>
<LoginLockLogo />
<p class="nova-login__brand-hint">{{ brandHint }}</p>
</div>
</a>
<form class="nova-login__form" autocomplete="off" @submit.prevent="handleSubmit">
<div class="nova-login__input-group">
@ -162,6 +167,11 @@ const isCustomerLogin = computed(() => props.loginType === OmsLoginType.Customer
const brandHint = computed(() =>
t(isCustomerLogin.value ? 'login.form.brandHint.customer' : 'login.form.brandHint.admin'),
);
/** Logo / 欢迎文案 → 官网首页 */
function goOfficialSite() {
window.location.href = '/';
}
const accountFieldLabel = computed(() => t('login.field.account'));
const accountPlaceholder = computed(() =>
isCustomerLogin.value
@ -403,8 +413,12 @@ async function handleSubmit() {
<style lang="less" scoped>
.nova-login__header {
display: block;
text-align: center;
margin-bottom: 28px;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.nova-login__brand-hint {

View File

@ -164,6 +164,7 @@ provide(LOGIN_EXIT_KEY, {
if (isExiting.value) {
return Promise.resolve();
}
// Flips login-container--exit: hides admin overlays outside pane clip-paths
isExiting.value = true;
const leftTargets = collectExitTargets('left');
@ -335,6 +336,15 @@ onBeforeUnmount(() => {
.login-container--exit {
pointer-events: none;
/* Safety net: admin overlays are outside pane clip-paths */
.login-hero-panel--admin,
.login-characters-panel--admin,
:deep(.login-care-banner) {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none;
}
}
.login-split {

View File

@ -695,8 +695,7 @@ watch(adminCustomerId, () => {
})
onMounted(() => {
loadList()
loadCounts()
void Promise.all([loadList(), loadCounts()])
})
</script>

View File

@ -1,11 +1,20 @@
<script lang="ts" setup>
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
import {
ref,
computed,
onMounted,
nextTick,
watch,
reactive,
defineAsyncComponent,
} from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { IconCalendar, IconDown, IconQuestionCircleFill } from '@arco-design/web-vue/es/icon';
import { Message, Modal } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { useI18n } from 'vue-i18n';
import {
@ -40,13 +49,6 @@
} from '@/api/order';
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
import OrderPayConfirmModal from './components/OrderPayConfirmModal.vue';
import CreatePickupOrderModal from './components/CreatePickupOrderModal.vue';
import MarkProblematicModal from './components/MarkProblematicModal.vue';
import MarkClaimedModal from './components/MarkClaimedModal.vue';
import StockBindModal from './components/StockBindModal.vue';
import ColumnConfigModal from '../components/ColumnConfigModal.vue';
import {
MY_ORDERS_COLUMN_CATALOG,
MY_ORDERS_COLUMN_STORAGE_KEY,
@ -60,9 +62,32 @@
type ColumnScheme,
} from './columnConfig';
import { filterMyOrdersColumns, filterMyOrdersFieldKeys } from './customerFieldMask';
import OrderPrintModal from '@/components/PrintTemplate/OrderPrintModal.vue';
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
import { fetchPrintUrlsForOrders, getCachedPrintUrl } from '@/utils/printSessionCache';
/** 详情/弹层按需分包,代发订单与我的订单共用本页 */
const OrderDetailDrawer = defineAsyncComponent(
() => import('./components/OrderDetailDrawer.vue'),
);
const OrderPayConfirmModal = defineAsyncComponent(
() => import('./components/OrderPayConfirmModal.vue'),
);
const CreatePickupOrderModal = defineAsyncComponent(
() => import('./components/CreatePickupOrderModal.vue'),
);
const MarkProblematicModal = defineAsyncComponent(
() => import('./components/MarkProblematicModal.vue'),
);
const MarkClaimedModal = defineAsyncComponent(
() => import('./components/MarkClaimedModal.vue'),
);
const StockBindModal = defineAsyncComponent(() => import('./components/StockBindModal.vue'));
const ColumnConfigModal = defineAsyncComponent(
() => import('../components/ColumnConfigModal.vue'),
);
const OrderPrintModal = defineAsyncComponent(
() => import('@/components/PrintTemplate/OrderPrintModal.vue'),
);
import {
ORDER_STATUS_TABS,
ORDER_STATUS_ALL,
@ -106,9 +131,48 @@
);
const { tableContainerRef, tableHeight } = useTableContainerHeight();
// 订单详情抽屉
const detailDrawerRef = ref<InstanceType<typeof OrderDetailDrawer>>();
// 订单详情抽屉(首次打开再挂载)
const detailDrawerMounted = ref(false);
const detailDrawerRef = ref<{
openDrawer?: (opts?: { tab?: string }) => void;
startBasicEdit?: () => void;
} | null>(null);
const currentOrder = ref<any>(null);
const columnConfigMounted = ref(false);
const createPickupMounted = ref(false);
const columnConfigModalRef = ref<{ open?: () => void } | null>(null);
const createPickupModalRef = ref<{
open?: (
rows: Array<{
id: number | string;
serviceNo?: string;
warehouseId?: number;
customerId?: number;
}>,
) => void;
} | null>(null);
async function waitForRef<T>(
get: () => T | null | undefined,
ready: (v: T) => boolean,
retries = 40,
): Promise<T | null> {
for (let i = 0; i < retries; i++) {
await nextTick();
const v = get();
if (v != null && ready(v)) return v;
await new Promise((r) => setTimeout(r, 40));
}
return null;
}
async function ensureDetailDrawer() {
detailDrawerMounted.value = true;
return waitForRef(
() => detailDrawerRef.value,
(v) => typeof v.openDrawer === 'function',
);
}
/** 兼容后端 camelCase / PascalCase */
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
@ -216,10 +280,11 @@
}
}
currentOrder.value = nextRow;
detailDrawerRef.value?.openDrawer({ tab: opts?.tab || 'basic' });
const drawer = await ensureDetailDrawer();
drawer?.openDrawer?.({ tab: opts?.tab || 'basic' });
if (opts?.editBasic) {
await nextTick();
detailDrawerRef.value?.startBasicEdit?.();
drawer?.startBasicEdit?.();
}
}
@ -1061,9 +1126,7 @@
openPayConfirm(rows);
};
const createPickupModalRef = ref<InstanceType<typeof CreatePickupOrderModal> | null>(null);
const handleCreatePickup = () => {
async function handleCreatePickup() {
const rows = requireSelectedRows();
if (!rows.length) return;
const notPredeclared = rows.filter((r) => resolveOrderStatusCode(r) !== 'predeclared');
@ -1076,7 +1139,12 @@
Message.warning(t('order.myOrders.createPickup.alreadyBound'));
return;
}
createPickupModalRef.value?.open(
createPickupMounted.value = true;
const modal = await waitForRef(
() => createPickupModalRef.value,
(v) => typeof v.open === 'function',
);
modal?.open?.(
rows.map((r) => ({
id: getOrderRowId(r)!,
serviceNo: r?.serviceNo || r?.ServiceNo,
@ -1084,7 +1152,7 @@
customerId: r?.customerId ?? r?.CustomerId,
})),
);
};
}
async function handleCreatePickupSuccess() {
await refreshListAndTabCounts();
@ -1530,7 +1598,6 @@
t('order.myOrders.columnConfig.defaultScheme'),
),
);
const columnConfigModalRef = ref<InstanceType<typeof ColumnConfigModal>>();
const columnSchemeOptions = computed(() =>
columnConfigState.value.schemes.map((s) => ({ label: s.name, value: s.id })),
@ -1607,8 +1674,13 @@
saveColumnConfigState(listColumnStorageKey.value, columnConfigState.value);
}
function openColumnConfig() {
columnConfigModalRef.value?.open();
async function openColumnConfig() {
columnConfigMounted.value = true;
const modal = await waitForRef(
() => columnConfigModalRef.value,
(v) => typeof v.open === 'function',
);
modal?.open?.();
}
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
@ -1792,8 +1864,7 @@
onMounted(() => {
nextTick(async () => {
await loadOrderList();
loadTabCounts();
await Promise.all([loadOrderList(), loadTabCounts()]);
await tryOpenOrderFromQuery();
});
});
@ -2021,7 +2092,7 @@
<div ref="tableContainerRef" class="list-wrap">
<a-spin :loading="listLoading" class="table-spin">
<HandsontablePro
:key="`${activeTab}-${currentPage}-${orderList.length}-${tableColumnsKey}`"
:key="`${activeTab}-${tableColumnsKey}`"
:columns="hotColumns"
:data="processedTableData"
:total="totalCount"
@ -2045,13 +2116,15 @@
</div>
</div>
<!-- 订单详情抽屉(左侧) -->
<!-- 订单详情抽屉(左侧):首次打开再挂载 -->
<OrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:order="currentOrder"
@updated="handleOrderDetailUpdated"
/>
<ColumnConfigModal
v-if="columnConfigMounted"
ref="columnConfigModalRef"
:catalog="listColumnCatalog"
:schemes="columnConfigState.schemes"
@ -2059,8 +2132,14 @@
:default-keys="MY_ORDERS_DEFAULT_KEYS"
@apply="handleColumnConfigApply"
/>
<OrderPrintModal v-model:visible="singlePrintVisible" mode="single" :items="singlePrintItems" />
<OrderPrintModal
v-if="singlePrintVisible"
v-model:visible="singlePrintVisible"
mode="single"
:items="singlePrintItems"
/>
<OrderPrintModal
v-if="batchPrintVisible"
v-model:visible="batchPrintVisible"
mode="batch"
:items="batchPrintItems"
@ -2068,11 +2147,13 @@
/>
<MarkProblematicModal
v-if="problematicModalVisible"
v-model:visible="problematicModalVisible"
:order-ids="exceptionOrderIds"
@success="handleExceptionActionSuccess"
/>
<MarkClaimedModal
v-if="claimedModalVisible"
v-model:visible="claimedModalVisible"
:order-ids="exceptionOrderIds"
:has-insurance="claimContext.hasInsurance"
@ -2081,16 +2162,22 @@
@success="handleExceptionActionSuccess"
/>
<StockBindModal
v-if="stockBindModalVisible"
v-model:visible="stockBindModalVisible"
:order-ids="stockBindOrderIds"
@success="handleStockBindSuccess"
/>
<OrderPayConfirmModal
v-if="payConfirmVisible"
v-model:visible="payConfirmVisible"
:orders="payConfirmOrders"
@success="handlePayConfirmSuccess"
/>
<CreatePickupOrderModal ref="createPickupModalRef" @success="handleCreatePickupSuccess" />
<CreatePickupOrderModal
v-if="createPickupMounted"
ref="createPickupModalRef"
@success="handleCreatePickupSuccess"
/>
</div>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, h, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { computed, defineAsyncComponent, h, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import dayjs from 'dayjs'
@ -10,7 +10,6 @@ import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
import SlidingTabs from '@/components/SlidingTabs/index.vue'
import useModal from '@/components/ModalPro/useModal'
import PickupOrderDetailDrawer from './PickupOrderDetailDrawer.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import {
@ -44,12 +43,20 @@ import {
loadDomesticPickupProductOptions,
type DomesticPickupProductOption,
} from '@/utils/domesticPickupProducts'
import CompleteContactModal from '@/components/CompleteContactModal/index.vue'
import type { CustomerProfileCompleteness } from '@/api/customer'
import CreatePickupSheetModal from './components/CreatePickupSheetModal.vue'
import type { CreatePickupSuccessPayload } from './components/CreatePickupSheetModal.vue'
import BindableOrderPicker from './components/BindableOrderPicker.vue'
import type { BindableOrderRow } from './components/BindableOrderPicker.vue'
const PickupOrderDetailDrawer = defineAsyncComponent(() => import('./PickupOrderDetailDrawer.vue'))
const CompleteContactModal = defineAsyncComponent(
() => import('@/components/CompleteContactModal/index.vue'),
)
const CreatePickupSheetModal = defineAsyncComponent(
() => import('./components/CreatePickupSheetModal.vue'),
)
const BindableOrderPicker = defineAsyncComponent(
() => import('./components/BindableOrderPicker.vue'),
)
import { getOrderStatusMeta, resolveOrderStatusCode } from '@/constants/orderStatus'
import { useRbacUserStore } from '@/store/modules/rbac-user'
import { getMapIdFromAccessToken } from '@/utils/jwt'
@ -68,10 +75,28 @@ const {
withCustomerId,
} = useWarehouseAdminCustomer()
const createModalRef = ref<InstanceType<typeof CreatePickupSheetModal> | null>(null)
const createModalMounted = ref(false)
const detailDrawerMounted = ref(false)
const createModalRef = ref<{
open?: (customerId?: number, direction?: 'forward' | 'reverse') => void
} | null>(null)
const contactVisible = ref(false)
const contactProfile = ref<CustomerProfileCompleteness | null>(null)
async function waitForPickupRef<T>(
get: () => T | null | undefined,
ready: (v: T) => boolean,
retries = 40,
): Promise<T | null> {
for (let i = 0; i < retries; i++) {
await nextTick()
const v = get()
if (v != null && ready(v)) return v
await new Promise((r) => setTimeout(r, 40))
}
return null
}
const keyword = ref('')
const STATUS_TAB_ALL = 'all'
const DIRECTION_FORWARD = 'forward'
@ -405,12 +430,21 @@ function handleSearch() {
void loadList()
}
async function openCreateModal() {
createModalMounted.value = true
const modal = await waitForPickupRef(
() => createModalRef.value,
(v) => typeof v.open === 'function',
)
modal?.open?.(adminCustomerId.value, directionFilter.value)
}
function handleOpenCreate() {
if (!hasPermission('Order.Pickup.Create')) {
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
return
}
createModalRef.value?.open(adminCustomerId.value, directionFilter.value)
void openCreateModal()
}
function onCreateSuccess(payload: CreatePickupSuccessPayload = {}) {
@ -502,7 +536,7 @@ function onNeedContact(profile: CustomerProfileCompleteness | null) {
function onContactSuccess() {
contactVisible.value = false
createModalRef.value?.open(adminCustomerId.value, directionFilter.value)
void openCreateModal()
}
function handleDirectionTabChange() {
@ -868,12 +902,17 @@ function handleRowAction(row: any, _rowIndex: number, action?: string) {
async function openDetail(id: number | string) {
detailLoading.value = true
detail.value = null
detailDrawerRef.value?.open()
detailDrawerMounted.value = true
const drawer = await waitForPickupRef(
() => detailDrawerRef.value,
(v) => typeof v.open === 'function',
)
drawer?.open()
try {
detail.value = await getPickupOrderDetail(id)
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
detailDrawerRef.value?.close()
drawer?.close()
} finally {
detailLoading.value = false
}
@ -1336,8 +1375,9 @@ watch(
},
)
onMounted(async () => {
await ensureCarrierServiceOptions()
onMounted(() => {
// 承运选项不阻塞列表首屏
void ensureCarrierServiceOptions()
void loadList()
})
</script>
@ -1345,11 +1385,13 @@ onMounted(async () => {
<template>
<div class="pickup-order-container warehouse-list-page">
<CompleteContactModal
v-if="contactVisible"
v-model:visible="contactVisible"
:profile="contactProfile"
@success="onContactSuccess"
/>
<CreatePickupSheetModal
v-if="createModalMounted"
ref="createModalRef"
@success="onCreateSuccess"
@need-contact="onNeedContact"
@ -1488,6 +1530,7 @@ onMounted(async () => {
</div>
<PickupOrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:loading="detailLoading"
:detail="detail"

View File

@ -1,10 +1,11 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import SlidingTabs from '@/components/SlidingTabs/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import { getSalesProductList } from '@/api/channel/salesProduct'
@ -32,9 +33,11 @@ import {
} from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import { buildStoreOrderStatusTagHtml } from '@/constants/storeOrderStatus'
import PullOrdersModal from './PullOrdersModal.vue'
import StoreOrderDetailDrawer from './StoreOrderDetailDrawer.vue'
import CreateLogisticsModal, { type CreateLogisticsMode } from './CreateLogisticsModal.vue'
import type { CreateLogisticsMode } from './CreateLogisticsModal.vue'
const PullOrdersModal = defineAsyncComponent(() => import('./PullOrdersModal.vue'))
const StoreOrderDetailDrawer = defineAsyncComponent(() => import('./StoreOrderDetailDrawer.vue'))
const CreateLogisticsModal = defineAsyncComponent(() => import('./CreateLogisticsModal.vue'))
defineOptions({ name: 'PlatformStoreOrder' })
@ -93,8 +96,21 @@ const pullVisible = ref(false)
const actionLoading = ref(false)
const matchWarehouseId = ref<number>()
const matchProductCode = ref<string>()
const detailDrawerRef = ref<InstanceType<typeof StoreOrderDetailDrawer>>()
const detailDrawerMounted = ref(false)
const detailDrawerRef = ref<{
open?: (opts: { id: number; customerId?: number }) => void
} | null>(null)
const createModalVisible = ref(false)
async function ensureStoreDetailDrawer() {
detailDrawerMounted.value = true
for (let i = 0; i < 40; i++) {
await nextTick()
if (detailDrawerRef.value?.open) return detailDrawerRef.value
await new Promise((r) => setTimeout(r, 40))
}
return detailDrawerRef.value
}
const createModalMode = ref<CreateLogisticsMode>('logistics')
const createModalIds = ref<number[]>([])
@ -321,7 +337,8 @@ function handleSelectionChange(rows: any[]) {
async function showDetail(row: any) {
const id = Number(row._raw?.id || row.id)
if (!id) return
detailDrawerRef.value?.open({
const drawer = await ensureStoreDetailDrawer()
drawer?.open?.({
id,
customerId: withCustomerId({}).customerId,
})
@ -639,15 +656,19 @@ function handleActionClick(row: any, _i: number, action?: string) {
}
onMounted(async () => {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] }))
warehouseOptions.value = (items || []).map((w: any) => ({
label: w.warehouseName || `#${w.id}`,
value: Number(w.id),
}))
if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value
await loadProducts()
await loadSummary()
await loadList()
// 仓库下拉与列表/汇总/产品并行,避免串行等三轮接口
const warehouseTask = getWarehouseList({ page: 1, pageSize: 200 })
.then(({ items }) => {
warehouseOptions.value = (items || []).map((w: any) => ({
label: w.warehouseName || `#${w.id}`,
value: Number(w.id),
}))
if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value
})
.catch(() => {
warehouseOptions.value = []
})
await Promise.all([warehouseTask, loadProducts(), loadSummary(), loadList()])
})
watch(
@ -790,17 +811,20 @@ watch(
</div>
<StoreOrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:warehouse-name-map="warehouseNameMap"
/>
<PullOrdersModal
v-if="pullVisible"
v-model:visible="pullVisible"
:customer-id="withCustomerId({}).customerId"
@success="() => { loadSummary(); loadList() }"
/>
<CreateLogisticsModal
v-if="createModalVisible"
v-model:visible="createModalVisible"
:mode="createModalMode"
:store-order-ids="createModalIds"

View File

@ -1,11 +1,20 @@
<script lang="ts" setup>
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue';
import {
ref,
computed,
onMounted,
nextTick,
watch,
reactive,
defineAsyncComponent,
} from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { IconCalendar, IconDown, IconQuestionCircleFill, IconSettings } from '@arco-design/web-vue/es/icon';
import { Message, Modal } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { useI18n } from 'vue-i18n';
import {
@ -47,13 +56,6 @@
} from '@/api/order';
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
import OrderPayConfirmModal from './components/OrderPayConfirmModal.vue';
import MarkProblematicModal from './components/MarkProblematicModal.vue';
import MarkClaimedModal from './components/MarkClaimedModal.vue';
import ReallocateModal from './components/ReallocateModal.vue';
import DownstreamWeightImportModal from './components/DownstreamWeightImportModal.vue';
import ColumnConfigModal from '../components/ColumnConfigModal.vue';
import {
buildHotColumns,
getActiveScheme,
@ -68,7 +70,6 @@
WAYBILL_MANAGE_DEFAULT_KEYS,
getWaybillBaseCatalog,
} from './columnConfig';
import OrderPrintModal from '@/components/PrintTemplate/OrderPrintModal.vue';
import {
fetchPrintUrlForOrder,
fetchPrintUrlsForOrders,
@ -100,6 +101,32 @@
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
import { buildWaybillContextFromRow, openAskAdvisor } from '@/composables/useAskAdvisor';
/** 详情/弹层按需分包,避免进页就解析 100KB+ 抽屉与打印组件 */
const OrderDetailDrawer = defineAsyncComponent(
() => import('./components/OrderDetailDrawer.vue'),
);
const OrderPayConfirmModal = defineAsyncComponent(
() => import('./components/OrderPayConfirmModal.vue'),
);
const MarkProblematicModal = defineAsyncComponent(
() => import('./components/MarkProblematicModal.vue'),
);
const MarkClaimedModal = defineAsyncComponent(
() => import('./components/MarkClaimedModal.vue'),
);
const ReallocateModal = defineAsyncComponent(
() => import('./components/ReallocateModal.vue'),
);
const DownstreamWeightImportModal = defineAsyncComponent(
() => import('./components/DownstreamWeightImportModal.vue'),
);
const ColumnConfigModal = defineAsyncComponent(
() => import('../components/ColumnConfigModal.vue'),
);
const OrderPrintModal = defineAsyncComponent(
() => import('@/components/PrintTemplate/OrderPrintModal.vue'),
);
defineOptions({
name: 'WaybillManage',
});
@ -108,9 +135,39 @@
const { tableContainerRef, tableHeight } = useTableContainerHeight();
const { customerOptions, withCustomerId } = useWarehouseAdminCustomer();
// 订单详情抽屉
const detailDrawerRef = ref<InstanceType<typeof OrderDetailDrawer>>();
// 订单详情抽屉(首次打开再挂载,避免进页就拉抽屉分包)
const detailDrawerMounted = ref(false);
const detailDrawerRef = ref<{
openDrawer?: (opts?: { tab?: string }) => void;
startBasicEdit?: () => void;
} | null>(null);
const currentOrder = ref<any>(null);
const columnConfigMounted = ref(false);
const downstreamWeightImportMounted = ref(false);
const columnConfigModalRef = ref<{ open?: () => void } | null>(null);
const downstreamWeightImportModalRef = ref<{ open?: () => void } | null>(null);
async function waitForRef<T>(
get: () => T | null | undefined,
ready: (v: T) => boolean,
retries = 40,
): Promise<T | null> {
for (let i = 0; i < retries; i++) {
await nextTick();
const v = get();
if (v != null && ready(v)) return v;
await new Promise((r) => setTimeout(r, 40));
}
return null;
}
async function ensureDetailDrawer() {
detailDrawerMounted.value = true;
return waitForRef(
() => detailDrawerRef.value,
(v) => typeof v.openDrawer === 'function',
);
}
/** 兼容后端 camelCase / PascalCase */
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
@ -274,10 +331,11 @@
}
}
currentOrder.value = nextRow;
detailDrawerRef.value?.openDrawer({ tab: opts?.tab || 'basic' });
const drawer = await ensureDetailDrawer();
drawer?.openDrawer?.({ tab: opts?.tab || 'basic' });
if (opts?.editBasic) {
await nextTick();
detailDrawerRef.value?.startBasicEdit?.();
drawer?.startBasicEdit?.();
}
}
@ -1356,10 +1414,14 @@
reallocateModalVisible.value = true;
};
const downstreamWeightImportModalRef = ref<InstanceType<typeof DownstreamWeightImportModal> | null>(null);
const openDownstreamWeightImport = () => {
downstreamWeightImportModalRef.value?.open();
};
async function openDownstreamWeightImport() {
downstreamWeightImportMounted.value = true;
const modal = await waitForRef(
() => downstreamWeightImportModalRef.value,
(v) => typeof v.open === 'function',
);
modal?.open?.();
}
function resolveClaimContextFromRow(row: any) {
let hasInsurance: boolean | null = null;
@ -1487,8 +1549,6 @@
t('order.myOrders.columnConfig.defaultScheme'),
),
);
const columnConfigModalRef = ref<InstanceType<typeof ColumnConfigModal>>();
const activeColumnKeys = computed(() => getActiveScheme(columnConfigState.value).keys);
const hotColumns = computed(() => {
@ -1549,8 +1609,13 @@
columnCatalog.value = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
}
function openColumnConfig() {
columnConfigModalRef.value?.open();
async function openColumnConfig() {
columnConfigMounted.value = true;
const modal = await waitForRef(
() => columnConfigModalRef.value,
(v) => typeof v.open === 'function',
);
modal?.open?.();
}
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
@ -1730,8 +1795,8 @@
onMounted(() => {
nextTick(async () => {
await loadOrderList();
loadTabCounts();
// 列表与角标并行,避免串行多等一整轮接口
await Promise.all([loadOrderList(), loadTabCounts()]);
await tryOpenOrderFromQuery();
});
});
@ -2017,7 +2082,7 @@
<div ref="tableContainerRef" class="list-wrap">
<a-spin :loading="listLoading" class="table-spin">
<HandsontablePro
:key="`${activeTab}-${currentPage}-${orderList.length}-${tableColumnsKey}`"
:key="`${activeTab}-${tableColumnsKey}`"
:columns="hotColumns"
:data="processedTableData"
:total="totalCount"
@ -2041,13 +2106,15 @@
</div>
</div>
<!-- 订单详情抽屉(左侧) -->
<!-- 订单详情抽屉(左侧):首次打开再挂载 -->
<OrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:order="currentOrder"
@updated="handleOrderDetailUpdated"
/>
<ColumnConfigModal
v-if="columnConfigMounted"
ref="columnConfigModalRef"
:catalog="columnCatalog"
:schemes="columnConfigState.schemes"
@ -2056,12 +2123,19 @@
@apply="handleColumnConfigApply"
/>
<OrderPrintModal
v-if="singlePrintVisible"
v-model:visible="singlePrintVisible"
mode="single"
:items="singlePrintItems"
:preparing="singlePrintPreparing"
/>
<OrderPrintModal v-model:visible="batchPrintVisible" mode="batch" :items="batchPrintItems" :preparing="batchPrintPreparing" />
<OrderPrintModal
v-if="batchPrintVisible"
v-model:visible="batchPrintVisible"
mode="batch"
:items="batchPrintItems"
:preparing="batchPrintPreparing"
/>
<a-modal
v-model:visible="keywordBatchVisible"
@ -2109,11 +2183,13 @@
</a-modal>
<MarkProblematicModal
v-if="problematicModalVisible"
v-model:visible="problematicModalVisible"
:order-ids="exceptionOrderIds"
@success="handleExceptionActionSuccess"
/>
<MarkClaimedModal
v-if="claimedModalVisible"
v-model:visible="claimedModalVisible"
:order-ids="exceptionOrderIds"
:has-insurance="claimContext.hasInsurance"
@ -2122,15 +2198,18 @@
@success="handleExceptionActionSuccess"
/>
<ReallocateModal
v-if="reallocateModalVisible"
v-model:visible="reallocateModalVisible"
:order-ids="exceptionOrderIds"
@success="handleExceptionActionSuccess"
/>
<DownstreamWeightImportModal
v-if="downstreamWeightImportMounted"
ref="downstreamWeightImportModalRef"
@success="handleExceptionActionSuccess"
/>
<OrderPayConfirmModal
v-if="payConfirmVisible"
v-model:visible="payConfirmVisible"
:orders="payConfirmOrders"
@success="handlePayConfirmSuccess"

View File

@ -34,6 +34,7 @@
</a>
</li>
<li class="site-nav-tools">
<span class="site-nav-tools__glass" aria-hidden="true"></span>
<a
v-if="!signedIn"
class="site-nav-login"

View File

@ -104,7 +104,10 @@
<article v-for="item in visibleItems" :key="String(item.id)" class="ai-route-card">
<div class="ai-route-card__head">
<div class="ai-route-card__titles">
<span class="ai-route-card__group">{{ item.productGroupName || item.productGroup }}</span>
<span
v-if="item.productGroupName || item.productGroup"
class="ai-route-card__group"
>{{ item.productGroupName || item.productGroup }}</span>
<h4 class="ai-route-card__name">{{ item.productName }}</h4>
</div>
<p v-if="formatRouteTimeliness(item)" class="ai-route-card__eta">
@ -134,10 +137,37 @@
{{ expanded[String(item.id)] ? copy.collapse : copy.expand }}
</button>
<div class="ai-route-card__actions">
<button type="button" class="ai-route-card__ask" @click="askItem(item)">{{ copy.ask }}</button>
<a class="ai-route-card__reg" :href="officialLoginUrl(destLabel(destFilter, lang) || undefined)">
{{ copy.register }}
</a>
<button
type="button"
class="ai-route-card__ask"
:title="copy.ask"
:aria-label="copy.ask"
@click="askItem(item)"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 14.5V12a8 8 0 0 1 16 0v2.5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<path
d="M4 14h2.8v5.2H4A1.6 1.6 0 0 1 2.4 17.6v-2A1.6 1.6 0 0 1 4 14z"
stroke="currentColor"
stroke-width="1.8"
stroke-linejoin="round"
/>
<path
d="M20 14h-2.8v5.2H20a1.6 1.6 0 0 0 1.6-1.6v-2A1.6 1.6 0 0 0 20 14z"
stroke="currentColor"
stroke-width="1.8"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</article>
<div v-if="visibleItems.length < filtered.length" class="ai-routes-more">

View File

@ -62,11 +62,65 @@
<template v-if="topic === 'pricing'">
<a class="site-topic-btn is-primary" :href="loginUrl">{{ copy.register }}</a>
<button type="button" class="site-topic-btn" @click="$emit('open-panel', 'routes')">{{ copy.lanes }}</button>
<button type="button" class="site-topic-btn is-ghost" @click="askPricing">{{ copy.ask }}</button>
<button
type="button"
class="site-topic-btn is-icon"
:title="copy.ask"
:aria-label="copy.ask"
@click="askPricing"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 14.5V12a8 8 0 0 1 16 0v2.5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<path
d="M4 14h2.8v5.2H4A1.6 1.6 0 0 1 2.4 17.6v-2A1.6 1.6 0 0 1 4 14z"
stroke="currentColor"
stroke-width="1.8"
stroke-linejoin="round"
/>
<path
d="M20 14h-2.8v5.2H20a1.6 1.6 0 0 0 1.6-1.6v-2A1.6 1.6 0 0 0 20 14z"
stroke="currentColor"
stroke-width="1.8"
stroke-linejoin="round"
/>
</svg>
</button>
</template>
<template v-else-if="topic === 'about'">
<a class="site-topic-btn is-primary" :href="loginUrl">{{ copy.register }}</a>
<button type="button" class="site-topic-btn" @click="$emit('open-panel', 'contact')">{{ copy.contact }}</button>
<button
type="button"
class="site-topic-btn is-icon"
:title="copy.contact"
:aria-label="copy.contact"
@click="$emit('open-panel', 'contact')"
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 14.5V12a8 8 0 0 1 16 0v2.5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<path
d="M4 14h2.8v5.2H4A1.6 1.6 0 0 1 2.4 17.6v-2A1.6 1.6 0 0 1 4 14z"
stroke="currentColor"
stroke-width="1.8"
stroke-linejoin="round"
/>
<path
d="M20 14h-2.8v5.2H20a1.6 1.6 0 0 0 1.6-1.6v-2A1.6 1.6 0 0 0 20 14z"
stroke="currentColor"
stroke-width="1.8"
stroke-linejoin="round"
/>
</svg>
</button>
</template>
<template v-else>
<button type="button" class="site-topic-btn is-primary" @click="$emit('wechat')">{{ copy.live }}</button>

View File

@ -371,7 +371,7 @@
inset: 0;
z-index: 995;
border: none;
background: color-mix(in srgb, var(--ai-bg, #f5f4f1) 55%, rgba(18, 24, 32, 0.18));
background: color-mix(in srgb, var(--ai-bg, #f0f3f4) 55%, rgba(18, 24, 32, 0.18));
backdrop-filter: blur(6px) saturate(140%);
-webkit-backdrop-filter: blur(6px) saturate(140%);
cursor: pointer;

View File

@ -3,7 +3,7 @@
:root {
--ai-header-h: 72px;
--ai-ease: cubic-bezier(0.22, 1, 0.36, 1);
--ai-bg: #f5f4f1;
--ai-bg: #f0f3f4;
--ai-bg-soft: #faf8f6;
--ai-surface: #ffffff;
--ai-text: #121820;
@ -110,10 +110,16 @@ body.command-home #header .container-xl {
}
body.command-home #navbar > ul > li.site-nav-tools {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
gap: 2px;
isolation: isolate;
}
body.command-home .site-nav-tools__glass {
display: none;
}
body.command-home .site-nav-dock {
@ -132,7 +138,8 @@ body.command-home #navbar .site-nav-dock a {
}
body.command-home .site-nav-dock__glass,
body.command-home .site-nav-dock__slider {
body.command-home .site-nav-dock__slider,
body.command-home .site-nav-tools__glass {
display: none;
}
@ -357,15 +364,123 @@ body.command-home #navbar .site-nav-dock a {
}
body.command-home #navbar > ul > li.site-nav-tools {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
height: 60px;
padding: 5px;
overflow: hidden;
border: none;
border-radius: 28px;
background: transparent;
box-shadow:
0 12px 40px rgba(15, 23, 42, 0.1),
0 2px 8px rgba(15, 23, 42, 0.04);
}
body.command-home .site-nav-tools__glass {
display: block;
position: absolute;
inset: 0;
z-index: 0;
border-radius: inherit;
overflow: hidden;
pointer-events: none;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.38) 0%, rgba(255, 255, 255, 0.16) 100%);
backdrop-filter: blur(32px) saturate(190%) brightness(1.08);
-webkit-backdrop-filter: blur(32px) saturate(190%) brightness(1.08);
}
body.command-home .site-nav-tools__glass::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
165deg,
rgba(255, 255, 255, 0.62) 0%,
rgba(255, 255, 255, 0.12) 32%,
rgba(255, 255, 255, 0) 58%
);
}
body.command-home .site-nav-tools__glass::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.92) 0%,
rgba(255, 255, 255, 0.28) 42%,
rgba(255, 255, 255, 0.4) 100%
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
}
body.command-home #navbar .site-nav-tools .site-nav-login,
body.command-home #navbar .site-nav-tools .site-nav-login:focus,
body.command-home #navbar .site-nav-tools .site-nav-login:hover,
body.command-home #navbar .site-nav-tools .site-nav-login:visited,
body.command-home #navbar .site-nav-tools .language-toggle-link,
body.command-home #navbar .site-nav-tools .language-toggle-link:focus,
body.command-home #navbar .site-nav-tools .language-toggle-link:hover,
body.command-home #navbar .site-nav-tools .language-toggle-link.active,
body.command-home #navbar .site-nav-tools .language-toggle-link.active:focus {
position: relative;
z-index: 1;
height: 100%;
min-height: 0;
padding: 0 14px;
border: none;
border-radius: 23px;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: none;
transition:
background 0.22s ease,
box-shadow 0.22s ease,
color 0.22s ease;
}
body.command-home #navbar .site-nav-tools .site-nav-login:hover,
body.command-home #navbar .site-nav-tools .site-nav-login:focus,
body.command-home #navbar .site-nav-tools .language-toggle-link:hover,
body.command-home #navbar .site-nav-tools .language-toggle-link.active,
body.command-home #navbar .site-nav-tools .language-toggle-link.active:focus {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0.42) 100%);
box-shadow:
0 4px 16px rgba(15, 23, 42, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.95),
inset 0 -1px 0 rgba(255, 255, 255, 0.2);
}
body.command-home #navbar .site-nav-tools .site-nav-avatar,
body.command-home #navbar .site-nav-tools .site-nav-avatar:visited,
body.command-home #navbar .site-nav-tools .site-nav-avatar:hover {
position: relative;
z-index: 1;
width: 50px;
height: 50px;
border: none;
background: transparent;
box-shadow: none;
}
body.command-home #navbar .site-nav-tools .site-nav-avatar img {
width: 36px;
height: 36px;
border-radius: 50%;
}
body.command-home #navbar .language-toggle-link,
body.command-home #navbar .language-toggle-link:focus {
padding: 0 10px 0 8px;
margin-left: 0;
}
}
@ -462,10 +577,10 @@ body.command-home #navbar .site-nav-login:visited {
align-items: center;
justify-content: center;
gap: 6px;
box-sizing: border-box;
width: auto;
height: 32px;
margin: 0;
padding: 0 10px 0 8px;
border: 1px solid var(--ai-glass-border);
border-radius: var(--ai-radius-sm);
background: var(--ai-glass-fill);
@ -480,6 +595,26 @@ body.command-home #navbar .site-nav-login:visited {
color 0.2s var(--ai-ease);
}
body.command-home #navbar .language-toggle-link,
body.command-home #navbar .language-toggle-link:focus,
body.command-home #navbar .language-toggle-link:hover,
body.command-home #navbar .language-toggle-link.active,
body.command-home #navbar .language-toggle-link.active:focus {
padding: 0 10px 0 8px;
}
body.command-home #navbar .site-nav-login,
body.command-home #navbar .site-nav-login:focus,
body.command-home #navbar .site-nav-login:hover,
body.command-home #navbar .site-nav-login:visited {
padding: 0 12px;
text-decoration: none;
font-size: 13px;
font-weight: 700;
line-height: 1;
color: var(--ai-action);
}
body.command-home .language-toggle-icon {
width: 1em;
height: 1em;
@ -1561,6 +1696,21 @@ body.command-home .ai-stage-map--eu {
background: transparent;
}
.site-topic-btn.is-icon {
width: 36px;
min-width: 36px;
padding: 0;
border-color: transparent;
background: transparent;
color: var(--ai-text-3);
}
.site-topic-btn.is-icon svg {
display: block;
width: 18px;
height: 18px;
}
.site-topic-btn:hover {
filter: brightness(1.03);
}
@ -1569,6 +1719,12 @@ body.command-home .ai-stage-map--eu {
background: color-mix(in srgb, var(--ai-action) 8%, #fff);
}
.site-topic-btn.is-icon:hover {
filter: none;
color: var(--ai-action);
background: color-mix(in srgb, var(--ai-action) 8%, transparent);
}
@media (max-width: 640px) {
.site-topic-contact {
grid-template-columns: minmax(0, 1fr);
@ -1782,23 +1938,36 @@ body.command-home .ai-stage-map--eu {
.ai-route-card__titles {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
gap: 0;
}
.ai-route-card__group {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--ai-gold);
flex-shrink: 0;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1.3;
color: var(--ai-muted);
}
.ai-route-card__group::after {
content: '·';
margin: 0 6px;
font-weight: 500;
color: color-mix(in srgb, var(--ai-muted) 55%, transparent);
}
.ai-route-card__name {
margin: 0;
font-size: 1.02rem;
font-weight: 750;
min-width: 0;
font-size: 1.05rem;
font-weight: 800;
line-height: 1.3;
color: var(--ai-ink);
letter-spacing: -0.01em;
color: var(--ai-action);
}
.ai-route-card__eta {
@ -1888,12 +2057,39 @@ body.command-home .ai-stage-map--eu {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
justify-content: flex-end;
gap: 8px;
margin-top: auto;
padding-top: 2px;
padding-top: 4px;
}
.ai-route-card__ask {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin: 0;
padding: 0;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--ai-text-3);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
}
.ai-route-card__ask svg {
display: block;
width: 16px;
height: 16px;
}
.ai-route-card__ask:hover {
color: var(--ai-action);
background: color-mix(in srgb, var(--ai-action) 8%, transparent);
}
.ai-route-card__ask,
.ai-routes-retry {
display: inline-flex;
align-items: center;
@ -1909,7 +2105,6 @@ body.command-home .ai-stage-map--eu {
cursor: pointer;
}
.ai-route-card__ask:hover,
.ai-routes-retry:hover {
filter: brightness(1.06);
}
@ -1919,19 +2114,20 @@ body.command-home .ai-stage-map--eu {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0 4px;
min-height: 28px;
padding: 0 2px;
border: 0;
background: none;
color: var(--ai-action);
font-size: 0.82rem;
font-weight: 650;
color: var(--ai-text-3);
font-size: 0.78rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
}
.ai-route-card__reg:hover,
.ai-routes-ghost:hover {
color: var(--ai-action);
text-decoration: underline;
}
@ -2207,14 +2403,6 @@ body.command-home .ai-stage-map--eu {
display: none;
}
body.command-home #navbar .site-nav-login {
text-decoration: none;
font-size: 13px;
font-weight: 700;
color: var(--ai-action);
padding: 0 12px;
}
body.command-home #navbar .site-nav-avatar,
body.command-home #navbar .site-nav-avatar:visited {
display: inline-flex;

View File

@ -8,7 +8,7 @@
--kc-secondary: #3d6678;
--kc-tertiary: #054f74;
--kc-tertiary-rgb: 5, 79, 116;
--kc-page-bg: #f5f4f1;
--kc-page-bg: #f0f3f4;
--kc-neutral: #faf8f6;
--kc-surface: #ffffff;
--kc-border: #e0d9cc;

View File

@ -1,7 +1,16 @@
<script setup lang="ts">
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import { useI18n } from 'vue-i18n';
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, unref } from 'vue';
import {
ref,
reactive,
computed,
onMounted,
onUnmounted,
nextTick,
unref,
defineAsyncComponent,
} from 'vue';
import { useRoute } from 'vue-router';
import dayjs from 'dayjs';
import { $baseMessage } from '@/utils';
@ -13,7 +22,7 @@ import { useI18n } from 'vue-i18n';
import { useUserStore } from '@/store';
import { eventBus } from '@/utils/pub';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import TrackOrderDetailDrawer from './components/TrackOrderDetailDrawer.vue';
import SlidingTabs from '@/components/SlidingTabs/index.vue';
import { useTrackOrderForm } from './composables/useTrackOrderForm';
import { useTableColumns } from './config/tableColumns';
import { useTrackOrderData } from './composables/useTrackOrderData';
@ -21,6 +30,10 @@ import { useI18n } from 'vue-i18n';
import { useContentHeight } from '@/composables/table/useContentHeight';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
const TrackOrderDetailDrawer = defineAsyncComponent(
() => import('./components/TrackOrderDetailDrawer.vue'),
);
const { t } = useI18n();
const route = useRoute();
const { tableContainerRef, tableHeight } = useTableContainerHeight();
@ -64,14 +77,18 @@ import { useI18n } from 'vue-i18n';
} from '@/api/TrackOrder/trackOrder';
import { getGetList } from '@/api/ApplicationServicesProvider/dictionary';
import { getAppGetList } from '@/api/ApplicationServices/apps';
import TimeAxis from './components/TimeAxis.vue';
import TimeZoneConversion from './components/TimeZoneConversion.vue';
import { IconEye, IconTool, IconDown } from '@arco-design/web-vue/es/icon';
import PaginationPro from '@/components/PaginationPro/index.vue';
const TimeAxis = defineAsyncComponent(() => import('./components/TimeAxis.vue'));
const TimeZoneConversion = defineAsyncComponent(
() => import('./components/TimeZoneConversion.vue'),
);
defineOptions({ name: 'TrackOrder' });
const formRef = ref();
const detailDrawerMounted = ref(false);
const [register, modalMethods] = useModal();
const [registerTemplate, templateMethods] = useModal();
@ -81,7 +98,22 @@ import { useI18n } from 'vue-i18n';
// 详情抽屉相关状态
const currentDetailRow = ref<any>(null);
const detailDrawerRef = ref<InstanceType<typeof TrackOrderDetailDrawer>>();
const detailDrawerRef = ref<{
setDrawerProps?: (props: Record<string, unknown>) => void;
openDrawer?: () => void;
updateDetailData?: () => void;
getSelectedRows?: () => any[];
} | null>(null);
async function ensureTrackDetailDrawer() {
detailDrawerMounted.value = true;
for (let i = 0; i < 40; i++) {
await nextTick();
if (detailDrawerRef.value?.openDrawer) return detailDrawerRef.value;
await new Promise((r) => setTimeout(r, 40));
}
return detailDrawerRef.value;
}
const orderId = ref<number | undefined>();
const tableRef = ref();
@ -777,12 +809,13 @@ import { useI18n } from 'vue-i18n';
orderId: row.id,
});
// 打开详情抽屉
detailDrawerRef.value?.setDrawerProps({
// 打开详情抽屉(首次再挂载分包)
const drawer = await ensureTrackDetailDrawer();
drawer?.setDrawerProps?.({
content: '订单详情',
width: 1200,
});
detailDrawerRef.value?.openDrawer();
drawer?.openDrawer?.();
}
// 关闭详情抽屉
@ -2027,8 +2060,9 @@ import { useI18n } from 'vue-i18n';
</div>
</div>
</ModalPro>
<!-- 详情抽屉 -->
<!-- 详情抽屉:首次打开再挂载 -->
<TrackOrderDetailDrawer
v-if="detailDrawerMounted"
ref="detailDrawerRef"
:orderId="currentDetailRow?.id"
:orderNumber="currentDetailRow?.orderNumber"