feat: 默认页面背景联动加载页,官网入口与线路面板体验优化
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m0s
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:
parent
d3f300d8b1
commit
9922d20141
24
index.html
24
index.html
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"menuCollapse": false,
|
||||
"footer": false,
|
||||
"themeColor": "#054f74",
|
||||
"pageBackgroundColor": "#f5f4f1",
|
||||
"pageBackgroundColor": "#f0f3f4",
|
||||
"menuWidth": 220,
|
||||
"globalSettings": false,
|
||||
"device": "desktop",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': '发布确认',
|
||||
|
||||
@ -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
50
src/utils/auditFields.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
@ -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' },
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
590
src/views/channel/salesProducts/components/PriceListHotTable.vue
Normal file
590
src/views/channel/salesProducts/components/PriceListHotTable.vue
Normal 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
@ -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,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -695,8 +695,7 @@ watch(adminCustomerId, () => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
loadCounts()
|
||||
void Promise.all([loadList(), loadCounts()])
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user