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 isH5 = isH5Path;
|
||||||
var isEn = false;
|
var isEn = false;
|
||||||
|
var pageBg = '#f0f3f4';
|
||||||
try {
|
try {
|
||||||
var locale =
|
var locale =
|
||||||
localStorage.getItem('arco-locale') || localStorage.getItem('locale') || '';
|
localStorage.getItem('arco-locale') || localStorage.getItem('locale') || '';
|
||||||
isEn = /^en/i.test(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) {}
|
} catch (e) {}
|
||||||
var root = document.documentElement;
|
var root = document.documentElement;
|
||||||
if (isH5) root.classList.add('app-boot-h5');
|
if (isH5) root.classList.add('app-boot-h5');
|
||||||
if (isEn) root.classList.add('app-boot-en');
|
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 {
|
try {
|
||||||
document.title = isH5
|
document.title = isH5
|
||||||
? isEn
|
? isEn
|
||||||
@ -100,7 +110,7 @@
|
|||||||
* 常驻 stable 会在右侧留出约 15px 空缝,贯穿 fixed 顶栏。
|
* 常驻 stable 会在右侧留出约 15px 空缝,贯穿 fixed 顶栏。
|
||||||
*/
|
*/
|
||||||
html {
|
html {
|
||||||
background: #f5f4f1;
|
background: var(--app-page-bg, #f0f3f4);
|
||||||
}
|
}
|
||||||
html.app-boot-site #app-boot {
|
html.app-boot-site #app-boot {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@ -118,7 +128,7 @@
|
|||||||
#app {
|
#app {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: #f5f4f1;
|
background: var(--app-page-bg, #f0f3f4);
|
||||||
}
|
}
|
||||||
#app-boot {
|
#app-boot {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -133,7 +143,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #f5f4f1;
|
background: var(--app-page-bg, #f0f3f4);
|
||||||
/*
|
/*
|
||||||
* 必须与 global.less 首屏后状态一致:
|
* 必须与 global.less 首屏后状态一致:
|
||||||
* Vue 加载后 html 会带上 antialiased,若 boot 首帧没有,中文会突然变细像「字号缩小」。
|
* Vue 加载后 html 会带上 antialiased,若 boot 首帧没有,中文会突然变细像「字号缩小」。
|
||||||
@ -272,9 +282,9 @@
|
|||||||
#app-boot.is-timeout .boot-sub {
|
#app-boot.is-timeout .boot-sub {
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
}
|
}
|
||||||
/* H5:样式挂在 html.app-boot-h5 上,首屏即正确,不再二次改字号/Logo */
|
/* H5:样式挂在 html.app-boot-h5 上,首屏即正确,不再二次改字号/Logo;背景随页面背景配置 */
|
||||||
html.app-boot-h5 {
|
html.app-boot-h5 {
|
||||||
background: #f5f6f8;
|
background: var(--app-page-bg, #f0f3f4);
|
||||||
}
|
}
|
||||||
html.app-boot-h5.app-booting {
|
html.app-boot-h5.app-booting {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
@ -282,10 +292,10 @@
|
|||||||
html.app-boot-h5,
|
html.app-boot-h5,
|
||||||
html.app-boot-h5 body,
|
html.app-boot-h5 body,
|
||||||
html.app-boot-h5 #app {
|
html.app-boot-h5 #app {
|
||||||
background: #f5f6f8;
|
background: var(--app-page-bg, #f0f3f4);
|
||||||
}
|
}
|
||||||
html.app-boot-h5 #app-boot {
|
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);
|
padding: env(safe-area-inset-top, 0) 24px env(safe-area-inset-bottom, 0);
|
||||||
}
|
}
|
||||||
html.app-boot-h5 #app-boot .boot-panel {
|
html.app-boot-h5 #app-boot .boot-panel {
|
||||||
|
|||||||
@ -81,15 +81,29 @@ export async function saveSalesProductCustomerGuide(body: {
|
|||||||
assertBusinessOk(res.data)
|
assertBusinessOk(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 卖价 */
|
/** 卖价列表;列表页请传 includeDetails:false,避免拉回整张价格矩阵导致卡顿 */
|
||||||
export async function getSalesProductPriceList(salesProductId: number | string) {
|
export async function getSalesProductPriceList(
|
||||||
|
salesProductId: number | string,
|
||||||
|
opts?: { includeDetails?: boolean },
|
||||||
|
) {
|
||||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/price/list'), {
|
const res = await axiosInstance.get(omsUrl('/channel/sales-product/price/list'), {
|
||||||
params: { salesProductId },
|
params: {
|
||||||
|
salesProductId,
|
||||||
|
includeDetails: opts?.includeDetails ?? true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const { items } = parseListPayload(res.data)
|
const { items } = parseListPayload(res.data)
|
||||||
return items ?? []
|
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>) {
|
export async function createSalesProductPrice(body: Record<string, any>) {
|
||||||
const res = await axiosInstance.post(omsUrl('/channel/sales-product/price/create'), body)
|
const res = await axiosInstance.post(omsUrl('/channel/sales-product/price/create'), body)
|
||||||
assertBusinessOk(res.data)
|
assertBusinessOk(res.data)
|
||||||
@ -150,6 +164,8 @@ export async function getSalesProductPublishPriceList(params: {
|
|||||||
salesProductId: number | string
|
salesProductId: number | string
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
/** false 时后端不返回快照正文,用于版本头展示 */
|
||||||
|
includeContent?: boolean
|
||||||
}) {
|
}) {
|
||||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/price/list'), { params })
|
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/price/list'), { params })
|
||||||
return parseListPayload(res.data)
|
return parseListPayload(res.data)
|
||||||
@ -159,11 +175,21 @@ export async function getSalesProductPublishRulesList(params: {
|
|||||||
salesProductId: number | string
|
salesProductId: number | string
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
/** false 时只返回版本头,不带快照正文 */
|
||||||
|
includeContent?: boolean
|
||||||
}) {
|
}) {
|
||||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/rules/list'), { params })
|
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/rules/list'), { params })
|
||||||
return parseListPayload(res.data)
|
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: {
|
export async function getSalesProductPublishPreview(params: {
|
||||||
salesProductId: number | string
|
salesProductId: number | string
|
||||||
|
|||||||
@ -1,7 +1,22 @@
|
|||||||
import { axiosInstance } from '@/api/rbac'
|
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'
|
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: {
|
export async function getServiceChannelList(params: {
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
@ -181,15 +196,30 @@ export async function deleteServiceChannelGroup(id: number | string) {
|
|||||||
assertBusinessOk(res.data)
|
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'), {
|
const res = await axiosInstance.get(omsUrl('/channel/service-channel/price/list'), {
|
||||||
params: { serviceChannelId },
|
params: {
|
||||||
|
serviceChannelId,
|
||||||
|
includeDetails: opts?.includeDetails ?? true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// 与 parseListPayload 一致:兼容 data 为数组、{ items }、{ list } 等
|
// 与 parseListPayload 一致:兼容 data 为数组、{ items }、{ list } 等
|
||||||
const { items } = parseListPayload(res.data)
|
const { items } = parseListPayload(res.data)
|
||||||
return items
|
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>) {
|
export async function createServiceChannelPrice(body: Record<string, any>) {
|
||||||
const res = await axiosInstance.post(omsUrl('/channel/service-channel/price/create'), body)
|
const res = await axiosInstance.post(omsUrl('/channel/service-channel/price/create'), body)
|
||||||
assertBusinessOk(res.data)
|
assertBusinessOk(res.data)
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
--app-primary-color-light: #eef3f6;
|
--app-primary-color-light: #eef3f6;
|
||||||
--app-navbar-bg: #054f74;
|
--app-navbar-bg: #054f74;
|
||||||
--app-sidebar-bg: #054f74;
|
--app-sidebar-bg: #054f74;
|
||||||
--app-page-bg: #f5f4f1;
|
--app-page-bg: #f0f3f4;
|
||||||
--app-card-bg: #ffffff;
|
--app-card-bg: #ffffff;
|
||||||
--app-card-border: #e5e6eb;
|
--app-card-border: #e5e6eb;
|
||||||
--app-elevated-bg: #ffffff;
|
--app-elevated-bg: #ffffff;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<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 type { PropType } from 'vue';
|
||||||
import { IconDelete } from '@arco-design/web-vue/es/icon';
|
import { IconDelete } from '@arco-design/web-vue/es/icon';
|
||||||
import { Message, Modal } from '@arco-design/web-vue';
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
@ -14,9 +14,11 @@
|
|||||||
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
import { getInterfaceGetInterfaceById } from '@/api/InterfaceServices/interface';
|
||||||
import { getMethodText } from '@/views/api/upstreamInterface/utils';
|
import { getMethodText } from '@/views/api/upstreamInterface/utils';
|
||||||
import { useTabRowCounts } from '@/utils/useTabRowCounts';
|
import { useTabRowCounts } from '@/utils/useTabRowCounts';
|
||||||
import RequestLog from '@/views/api/requestLog/index.vue';
|
|
||||||
import { prodApiOrigin } from '@/utils/rewriteBrowserApiPath';
|
import { prodApiOrigin } from '@/utils/rewriteBrowserApiPath';
|
||||||
|
|
||||||
|
/** 请求日志页含 Handsontable,仅打开「查看报文」时再加载 */
|
||||||
|
const RequestLog = defineAsyncComponent(() => import('@/views/api/requestLog/index.vue'));
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@ -603,6 +603,8 @@
|
|||||||
--clm-node-second-line-height: 16px;
|
--clm-node-second-line-height: 16px;
|
||||||
--clm-node-padding-y: 5px;
|
--clm-node-padding-y: 5px;
|
||||||
--clm-node-height: 60px;
|
--clm-node-height: 60px;
|
||||||
|
/* 菜单项垂直间距 */
|
||||||
|
--clm-node-gap: 8px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -858,7 +860,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
margin: 2px 0;
|
margin: calc(var(--clm-node-gap) / 2) 0;
|
||||||
padding: var(--clm-node-padding-y) 8px;
|
padding: var(--clm-node-padding-y) 8px;
|
||||||
border-radius: var(--clm-radius);
|
border-radius: var(--clm-radius);
|
||||||
border: 1px solid rgba(15, 16, 17, 0.06);
|
border: 1px solid rgba(15, 16, 17, 0.06);
|
||||||
@ -879,7 +881,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
margin: 8px 0 2px;
|
margin: calc(var(--clm-node-gap) + 4px) 0 calc(var(--clm-node-gap) / 2);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -1098,7 +1100,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid rgba(15, 16, 17, 0.06);
|
border: 1px solid rgba(15, 16, 17, 0.06);
|
||||||
padding: var(--clm-node-padding-y) 8px;
|
padding: var(--clm-node-padding-y) 8px;
|
||||||
margin: 2px 0;
|
margin: calc(var(--clm-node-gap) / 2) 0;
|
||||||
border-radius: var(--clm-radius);
|
border-radius: var(--clm-radius);
|
||||||
transition: background-color 0.12s ease, border-color 0.12s ease;
|
transition: background-color 0.12s ease, border-color 0.12s ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -1120,7 +1122,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
margin: 10px 0 4px;
|
margin: calc(var(--clm-node-gap) + 4px) 0 calc(var(--clm-node-gap) / 2);
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -1184,10 +1186,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 实体叶子:保持卡片感 */
|
/* 实体叶子:固定高度,内容块垂直居中避免贴底 */
|
||||||
&.tree-node--leaf {
|
&.tree-node--leaf {
|
||||||
background: var(--color-fill-1);
|
background: var(--color-fill-1);
|
||||||
border-color: rgba(15, 16, 17, 0.07);
|
border-color: rgba(15, 16, 17, 0.07);
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -1250,7 +1253,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
height: 100%;
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-second-row > td {
|
||||||
|
padding-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-content-cell {
|
.node-content-cell {
|
||||||
|
|||||||
@ -1824,8 +1824,14 @@
|
|||||||
if (!hotInstance) return;
|
if (!hotInstance) return;
|
||||||
const editor = hotInstance.getActiveEditor() as any;
|
const editor = hotInstance.getActiveEditor() as any;
|
||||||
if (!editor) return;
|
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?.()) {
|
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);
|
const meta = hotInstance.getCellMeta(r, c);
|
||||||
@ -1833,8 +1839,12 @@
|
|||||||
if (t === 'dropdown' || t === 'autocomplete') {
|
if (t === 'dropdown' || t === 'autocomplete') {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
if (typeof editor.open === 'function') editor.open();
|
if (editor.TEXTAREA && !editor.TEXTAREA.value && initial) {
|
||||||
else if (typeof editor.queryChoices === 'function') editor.queryChoices(editor.TEXTAREA?.value ?? '');
|
editor.setValue?.(initial);
|
||||||
|
}
|
||||||
|
// 用空 query 拉全量选项,避免「当前值与 source 不完全一致」时列表被滤空
|
||||||
|
if (typeof editor.queryChoices === 'function') editor.queryChoices('');
|
||||||
|
else if (typeof editor.open === 'function') editor.open();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@ -2406,8 +2416,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听数据变化
|
function refreshHandsontableData() {
|
||||||
watch(() => props.data, () => {
|
|
||||||
if (ignoreDataWatch) return;
|
if (ignoreDataWatch) return;
|
||||||
// 编辑中勿 loadData,否则输入框/下拉会被立刻拆掉
|
// 编辑中勿 loadData,否则输入框/下拉会被立刻拆掉
|
||||||
if (hotInstance && props.readOnly === false) {
|
if (hotInstance && props.readOnly === false) {
|
||||||
@ -2422,7 +2431,29 @@
|
|||||||
void initHandsontableWhenReady({ finalize: false });
|
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], () => {
|
watch([() => props.currentPage, () => props.pageSize], () => {
|
||||||
@ -2442,10 +2473,26 @@
|
|||||||
{ deep: true }
|
{ 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(
|
watch(
|
||||||
() => props.columns,
|
() => columnsStructuralKey(props.columns),
|
||||||
() => {
|
(next, prev) => {
|
||||||
|
if (next === prev) return;
|
||||||
internalColumnOrder.value = normalizeColumnOrder(props.columns, internalColumnOrder.value);
|
internalColumnOrder.value = normalizeColumnOrder(props.columns, internalColumnOrder.value);
|
||||||
if (hotInstance) {
|
if (hotInstance) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@ -2454,7 +2501,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 窗口大小变化处理函数
|
// 窗口大小变化处理函数
|
||||||
@ -2502,12 +2548,15 @@
|
|||||||
|
|
||||||
// 容器尺寸变化时同步表格高度,使列表高度与容器一致
|
// 容器尺寸变化时同步表格高度,使列表高度与容器一致
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
let lastObservedTableHeight = 0;
|
||||||
|
let resizeRaf = 0;
|
||||||
const setupResizeObserver = () => {
|
const setupResizeObserver = () => {
|
||||||
if (!containerRef.value) return;
|
if (!containerRef.value) return;
|
||||||
if (resizeObserver) {
|
if (resizeObserver) {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
resizeObserver = null;
|
resizeObserver = null;
|
||||||
}
|
}
|
||||||
|
lastObservedTableHeight = 0;
|
||||||
resizeObserver = new ResizeObserver((entries) => {
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
const entry = entries[0];
|
const entry = entries[0];
|
||||||
if (!entry || !hotInstance) return;
|
if (!entry || !hotInstance) return;
|
||||||
@ -2515,9 +2564,14 @@
|
|||||||
// 高度以本组件根容器实测为准;勿用外层 props.height 覆盖,否则与 flex 内真实高度不一致,stretchH 会把列压满导致横向条消失
|
// 高度以本组件根容器实测为准;勿用外层 props.height 覆盖,否则与 flex 内真实高度不一致,stretchH 会把列压满导致横向条消失
|
||||||
if (h <= 0) return;
|
if (h <= 0) return;
|
||||||
const fh = Math.max(Math.floor(h - customScrollbarSize), 120);
|
const fh = Math.max(Math.floor(h - customScrollbarSize), 120);
|
||||||
hotInstance.updateSettings({ height: fh });
|
// 高度几乎不变时跳过,避免 updateSettings→再触发 RO 的死循环把页面卡死
|
||||||
nextTick(() => {
|
if (Math.abs(fh - lastObservedTableHeight) < 2) return;
|
||||||
hotInstance?.refreshDimensions();
|
lastObservedTableHeight = fh;
|
||||||
|
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||||
|
resizeRaf = requestAnimationFrame(() => {
|
||||||
|
resizeRaf = 0;
|
||||||
|
if (!hotInstance) return;
|
||||||
|
hotInstance.updateSettings({ height: fh }, false);
|
||||||
ensureHorizontalScrollbarVisible();
|
ensureHorizontalScrollbarVisible();
|
||||||
syncHorizontalScrollbarMetrics();
|
syncHorizontalScrollbarMetrics();
|
||||||
syncVerticalScrollbarMetrics();
|
syncVerticalScrollbarMetrics();
|
||||||
@ -3382,7 +3436,8 @@
|
|||||||
.autocompleteEditor.handsontable.listbox,
|
.autocompleteEditor.handsontable.listbox,
|
||||||
.ht-theme-classic .handsontable.listbox,
|
.ht-theme-classic .handsontable.listbox,
|
||||||
.ht-theme-classic-dark .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: 1px solid var(--color-border-2, #e5e6eb) !important;
|
||||||
border-radius: 6px !important;
|
border-radius: 6px !important;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="top-nav" class="top-nav" :class="{ tabNavEnable: tabNavEnable }">
|
<div id="top-nav" class="top-nav" :class="{ tabNavEnable: tabNavEnable }">
|
||||||
<div class="top-nav-header">
|
<div class="top-nav-header">
|
||||||
<!-- Logo 区域 -->
|
<!-- Logo 区域:点击跳转官网首页 -->
|
||||||
<div class="top-nav-logo">
|
<a class="top-nav-logo" href="/" :title="t('brand.fullName')" @click.prevent="goOfficialSite">
|
||||||
<img :src="navLogo" :alt="t('brand.name')" />
|
<img :src="navLogo" :alt="t('brand.name')" />
|
||||||
<div class="top-nav-logo__brand">
|
<div class="top-nav-logo__brand">
|
||||||
<span class="top-nav-logo__name">{{ t('brand.name') }}</span>
|
<span class="top-nav-logo__name">{{ t('brand.name') }}</span>
|
||||||
<span class="top-nav-logo__full">{{ t('brand.fullName') }}</span>
|
<span class="top-nav-logo__full">{{ t('brand.fullName') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<!-- 顶部水平菜单:磁吸滑块 + 极速 hover 下拉 -->
|
<!-- 顶部水平菜单:磁吸滑块 + 极速 hover 下拉 -->
|
||||||
<div
|
<div
|
||||||
@ -298,6 +298,11 @@ const topNavEnable = computed(() => true); // 启用顶部导航
|
|||||||
const tabNavEnable = computed(() => appStore.tabBar);
|
const tabNavEnable = computed(() => appStore.tabBar);
|
||||||
const printConnectionState = useHiprintClientState();
|
const printConnectionState = useHiprintClientState();
|
||||||
|
|
||||||
|
/** 顶栏 Logo → 官网首页(开发环境即 http://localhost:5173/) */
|
||||||
|
function goOfficialSite() {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
const menuItemEls = new Map<string, HTMLElement>();
|
const menuItemEls = new Map<string, HTMLElement>();
|
||||||
const indicatorReady = ref(false);
|
const indicatorReady = ref(false);
|
||||||
const indicatorVisible = ref(false);
|
const indicatorVisible = ref(false);
|
||||||
@ -307,6 +312,8 @@ const indicatorStyle = ref<Record<string, string>>({
|
|||||||
});
|
});
|
||||||
/** 磁吸:当前指针悬停的一级菜单;空则吸回路由选中项 */
|
/** 磁吸:当前指针悬停的一级菜单;空则吸回路由选中项 */
|
||||||
const hoverMenuKey = ref('');
|
const hoverMenuKey = ref('');
|
||||||
|
/** 跳转中目标一级(路由尚未变时避免滑块先吸回旧项再弹到新项) */
|
||||||
|
const pendingNavTopKey = ref('');
|
||||||
/** 下拉进入/离开延迟(ms):偏快,横向扫菜单时几乎无感 */
|
/** 下拉进入/离开延迟(ms):偏快,横向扫菜单时几乎无感 */
|
||||||
const SUBMENU_ENTER_DELAY = 20;
|
const SUBMENU_ENTER_DELAY = 20;
|
||||||
const SUBMENU_LEAVE_DELAY = 120;
|
const SUBMENU_LEAVE_DELAY = 120;
|
||||||
@ -336,6 +343,7 @@ function setMenuItemRef(key: string, el: Element | ComponentPublicInstance | nul
|
|||||||
function resolveIndicatorKey() {
|
function resolveIndicatorKey() {
|
||||||
if (hoverMenuKey.value) return hoverMenuKey.value;
|
if (hoverMenuKey.value) return hoverMenuKey.value;
|
||||||
if (openedSubmenuKey.value) return openedSubmenuKey.value;
|
if (openedSubmenuKey.value) return openedSubmenuKey.value;
|
||||||
|
if (pendingNavTopKey.value) return pendingNavTopKey.value;
|
||||||
return isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value;
|
return isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,6 +393,7 @@ function onMenuItemPointerEnter(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMenuBarPointerLeave(e: PointerEvent) {
|
function onMenuBarPointerLeave(e: PointerEvent) {
|
||||||
|
if (navPaintLock.value) return;
|
||||||
const next = e.relatedTarget as Node | null;
|
const next = e.relatedTarget as Node | null;
|
||||||
// 指针进入二级浮层时保持当前一级高亮,不吸回
|
// 指针进入二级浮层时保持当前一级高亮,不吸回
|
||||||
if (
|
if (
|
||||||
@ -398,6 +407,7 @@ function onMenuBarPointerLeave(e: PointerEvent) {
|
|||||||
hoverMenuKey.value = '';
|
hoverMenuKey.value = '';
|
||||||
moveIndicatorToKey(
|
moveIndicatorToKey(
|
||||||
openedSubmenuKey.value ||
|
openedSubmenuKey.value ||
|
||||||
|
pendingNavTopKey.value ||
|
||||||
(isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value),
|
(isOverflowActive.value ? MORE_MENU_KEY : belongTopMenu.value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -417,7 +427,14 @@ async function onTopMenuClick(e: MouseEvent, item: any) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const children = filterPage(item.children);
|
const children = filterPage(item.children);
|
||||||
if (children.length > 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
paintNavThenGo(item);
|
paintNavThenGo(item);
|
||||||
@ -771,6 +788,19 @@ function onSubmenuPopupVisibleChange(key: string, visible: boolean) {
|
|||||||
openedSubmenuKey.value = key;
|
openedSubmenuKey.value = key;
|
||||||
hoverMenuKey.value = key;
|
hoverMenuKey.value = key;
|
||||||
moveIndicatorToKey(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(() => {
|
nextTick(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const el = document.querySelector(
|
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 = '';
|
let navGoingTo = '';
|
||||||
function paintNavThenGo(item: any) {
|
function paintNavThenGo(item: any) {
|
||||||
const rawPath = String(item?.path || item?.route || '');
|
const rawPath = String(item?.path || item?.route || '');
|
||||||
if (navGoingTo && navGoingTo === rawPath) return;
|
if (navGoingTo && navGoingTo === rawPath) return;
|
||||||
navGoingTo = rawPath;
|
navGoingTo = rawPath;
|
||||||
|
// 先锁再关弹层,避免 leave/关闭回调把滑块吸回旧一级
|
||||||
|
navPaintLock.value = true;
|
||||||
|
pendingNavTopKey.value = findTopMenuKeyForItem(item);
|
||||||
openedSubmenuKey.value = '';
|
openedSubmenuKey.value = '';
|
||||||
hoverMenuKey.value = '';
|
hoverMenuKey.value = '';
|
||||||
navPaintLock.value = true;
|
|
||||||
if (navPaintUnlockTimer) {
|
if (navPaintUnlockTimer) {
|
||||||
window.clearTimeout(navPaintUnlockTimer);
|
window.clearTimeout(navPaintUnlockTimer);
|
||||||
}
|
}
|
||||||
@ -1077,8 +1136,17 @@ function paintNavThenGo(item: any) {
|
|||||||
goto(item);
|
goto(item);
|
||||||
navPaintUnlockTimer = window.setTimeout(() => {
|
navPaintUnlockTimer = window.setTimeout(() => {
|
||||||
navPaintLock.value = false;
|
navPaintLock.value = false;
|
||||||
|
// 路由已切到目标时清掉 pending;否则留到 belongTopMenu 对齐再清
|
||||||
|
if (
|
||||||
|
!pendingNavTopKey.value ||
|
||||||
|
pendingNavTopKey.value === belongTopMenu.value ||
|
||||||
|
(pendingNavTopKey.value === MORE_MENU_KEY && isOverflowActive.value)
|
||||||
|
) {
|
||||||
|
pendingNavTopKey.value = '';
|
||||||
|
}
|
||||||
navPaintUnlockTimer = 0;
|
navPaintUnlockTimer = 0;
|
||||||
navGoingTo = '';
|
navGoingTo = '';
|
||||||
|
updateActiveIndicator();
|
||||||
}, 280);
|
}, 280);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1179,6 +1247,13 @@ function handleCustomerBalanceChanged() {
|
|||||||
watch(
|
watch(
|
||||||
() => [belongTopMenu.value, menuDataReady.value, visibleTopMenus.value.length] as const,
|
() => [belongTopMenu.value, menuDataReady.value, visibleTopMenus.value.length] as const,
|
||||||
async () => {
|
async () => {
|
||||||
|
if (
|
||||||
|
pendingNavTopKey.value &&
|
||||||
|
(pendingNavTopKey.value === belongTopMenu.value ||
|
||||||
|
(pendingNavTopKey.value === MORE_MENU_KEY && isOverflowActive.value))
|
||||||
|
) {
|
||||||
|
pendingNavTopKey.value = '';
|
||||||
|
}
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await recalcMenuOverflow();
|
await recalcMenuOverflow();
|
||||||
},
|
},
|
||||||
@ -1241,6 +1316,9 @@ onUnmounted(() => {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"menuCollapse": false,
|
"menuCollapse": false,
|
||||||
"footer": false,
|
"footer": false,
|
||||||
"themeColor": "#054f74",
|
"themeColor": "#054f74",
|
||||||
"pageBackgroundColor": "#f5f4f1",
|
"pageBackgroundColor": "#f0f3f4",
|
||||||
"menuWidth": 220,
|
"menuWidth": 220,
|
||||||
"globalSettings": false,
|
"globalSettings": false,
|
||||||
"device": "desktop",
|
"device": "desktop",
|
||||||
|
|||||||
@ -4995,6 +4995,8 @@ export default {
|
|||||||
'channel.salesProducts.empty.selectProduct': 'Select a product from the left',
|
'channel.salesProducts.empty.selectProduct': 'Select a product from the left',
|
||||||
'channel.salesProducts.context.product': 'Product',
|
'channel.salesProducts.context.product': 'Product',
|
||||||
'channel.salesProducts.context.group': 'Group',
|
'channel.salesProducts.context.group': 'Group',
|
||||||
|
'channel.salesProducts.context.updateTime': 'Updated',
|
||||||
|
'channel.salesProducts.context.updateBy': 'Updated by',
|
||||||
'channel.salesProducts.context.unsaved': 'Unsaved',
|
'channel.salesProducts.context.unsaved': 'Unsaved',
|
||||||
'channel.salesProducts.context.publishPending': 'Config pending',
|
'channel.salesProducts.context.publishPending': 'Config pending',
|
||||||
'channel.salesProducts.context.rulesPublishPending': 'Rules pending',
|
'channel.salesProducts.context.rulesPublishPending': 'Rules pending',
|
||||||
@ -5092,6 +5094,13 @@ export default {
|
|||||||
'channel.salesProducts.publish.tabs.rules': 'Rules',
|
'channel.salesProducts.publish.tabs.rules': 'Rules',
|
||||||
'channel.salesProducts.publish.versionRemark': 'Version note',
|
'channel.salesProducts.publish.versionRemark': 'Version note',
|
||||||
'channel.salesProducts.publish.noRemark': 'No notes',
|
'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.open': 'AI analysis',
|
||||||
'channel.salesProducts.ai.openRequired': 'Required: AI analysis',
|
'channel.salesProducts.ai.openRequired': 'Required: AI analysis',
|
||||||
@ -5377,6 +5386,11 @@ export default {
|
|||||||
'channel.serviceChannels.record.versionRemark': 'Version note',
|
'channel.serviceChannels.record.versionRemark': 'Version note',
|
||||||
'channel.serviceChannels.record.noRemark': 'No notes',
|
'channel.serviceChannels.record.noRemark': 'No notes',
|
||||||
'channel.serviceChannels.record.versionNumber': 'Version: {version}',
|
'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.publishDateTitle': 'Select publish date',
|
||||||
'channel.serviceChannels.modal.publishDate.placeholder': 'Please select date',
|
'channel.serviceChannels.modal.publishDate.placeholder': 'Please select date',
|
||||||
'channel.serviceChannels.modal.actions.cancel': 'Cancel',
|
'channel.serviceChannels.modal.actions.cancel': 'Cancel',
|
||||||
@ -5411,11 +5425,14 @@ export default {
|
|||||||
'channel.serviceChannels.drawer.priceList.batchFillSelected': '{count} rows selected',
|
'channel.serviceChannels.drawer.priceList.batchFillSelected': '{count} rows selected',
|
||||||
'channel.serviceChannels.drawer.priceList.batchFillValue': 'Unit price',
|
'channel.serviceChannels.drawer.priceList.batchFillValue': 'Unit price',
|
||||||
'channel.serviceChannels.createTime': 'Created: {time}',
|
'channel.serviceChannels.createTime': 'Created: {time}',
|
||||||
|
'channel.serviceChannels.updateTime': 'Updated: {time}',
|
||||||
'channel.serviceChannels.empty.selectChannel': 'Select a channel from the left',
|
'channel.serviceChannels.empty.selectChannel': 'Select a channel from the left',
|
||||||
'channel.serviceChannels.context.channel': 'Channel',
|
'channel.serviceChannels.context.channel': 'Channel',
|
||||||
'channel.serviceChannels.context.group': 'Provider group',
|
'channel.serviceChannels.context.group': 'Provider group',
|
||||||
'channel.serviceChannels.context.app': 'Application',
|
'channel.serviceChannels.context.app': 'Application',
|
||||||
'channel.serviceChannels.context.apiProvider': 'API provider',
|
'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.unsaved': 'Unsaved',
|
||||||
'channel.serviceChannels.context.rulesPublishPending': 'Config pending',
|
'channel.serviceChannels.context.rulesPublishPending': 'Config pending',
|
||||||
'channel.serviceChannels.publishConfirm.title': 'Publish confirmation',
|
'channel.serviceChannels.publishConfirm.title': 'Publish confirmation',
|
||||||
|
|||||||
@ -5031,6 +5031,8 @@ export default {
|
|||||||
'channel.salesProducts.empty.selectProduct': '请从左侧选择销售产品',
|
'channel.salesProducts.empty.selectProduct': '请从左侧选择销售产品',
|
||||||
'channel.salesProducts.context.product': '当前产品',
|
'channel.salesProducts.context.product': '当前产品',
|
||||||
'channel.salesProducts.context.group': '产品分组',
|
'channel.salesProducts.context.group': '产品分组',
|
||||||
|
'channel.salesProducts.context.updateTime': '修改时间',
|
||||||
|
'channel.salesProducts.context.updateBy': '修改人',
|
||||||
'channel.salesProducts.context.unsaved': '未保存',
|
'channel.salesProducts.context.unsaved': '未保存',
|
||||||
'channel.salesProducts.context.publishPending': '待发布配置',
|
'channel.salesProducts.context.publishPending': '待发布配置',
|
||||||
'channel.salesProducts.context.rulesPublishPending': '待发布规则',
|
'channel.salesProducts.context.rulesPublishPending': '待发布规则',
|
||||||
@ -5127,6 +5129,13 @@ export default {
|
|||||||
'channel.salesProducts.publish.tabs.rules': '规则',
|
'channel.salesProducts.publish.tabs.rules': '规则',
|
||||||
'channel.salesProducts.publish.versionRemark': '版本备注',
|
'channel.salesProducts.publish.versionRemark': '版本备注',
|
||||||
'channel.salesProducts.publish.noRemark': '暂无备注',
|
'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.open': 'AI 分析',
|
||||||
'channel.salesProducts.ai.openRequired': '必做:AI 分析',
|
'channel.salesProducts.ai.openRequired': '必做:AI 分析',
|
||||||
@ -5320,7 +5329,7 @@ export default {
|
|||||||
'channel.salesProducts.drawer.footer.generatePriceList': '生成价格表',
|
'channel.salesProducts.drawer.footer.generatePriceList': '生成价格表',
|
||||||
'channel.salesProducts.drawer.footer.syncPriceList': '同步价格表',
|
'channel.salesProducts.drawer.footer.syncPriceList': '同步价格表',
|
||||||
'channel.salesProducts.drawer.footer.save': '保存',
|
'channel.salesProducts.drawer.footer.save': '保存',
|
||||||
'channel.salesProducts.drawer.priceList.hint': '已有组合会保留价格;勾选行可批量填价',
|
'channel.salesProducts.drawer.priceList.hint': '已有组合会保留价格;选中行可批量填价',
|
||||||
'channel.salesProducts.drawer.priceList.batchFill': '批量填价',
|
'channel.salesProducts.drawer.priceList.batchFill': '批量填价',
|
||||||
'channel.salesProducts.drawer.priceList.batchFillEmpty': '只填空白/0',
|
'channel.salesProducts.drawer.priceList.batchFillEmpty': '只填空白/0',
|
||||||
'channel.salesProducts.drawer.priceList.copyPrev': '复制上一行',
|
'channel.salesProducts.drawer.priceList.copyPrev': '复制上一行',
|
||||||
@ -5412,6 +5421,11 @@ export default {
|
|||||||
'channel.serviceChannels.record.versionRemark': '版本备注',
|
'channel.serviceChannels.record.versionRemark': '版本备注',
|
||||||
'channel.serviceChannels.record.noRemark': '暂无备注',
|
'channel.serviceChannels.record.noRemark': '暂无备注',
|
||||||
'channel.serviceChannels.record.versionNumber': '版本号:{version}',
|
'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.publishDateTitle': '选择发布日期',
|
||||||
'channel.serviceChannels.modal.publishDate.placeholder': '请选择日期',
|
'channel.serviceChannels.modal.publishDate.placeholder': '请选择日期',
|
||||||
'channel.serviceChannels.modal.actions.cancel': '取消',
|
'channel.serviceChannels.modal.actions.cancel': '取消',
|
||||||
@ -5440,18 +5454,21 @@ export default {
|
|||||||
'channel.serviceChannels.drawer.footer.generatePriceList': '生成价格表',
|
'channel.serviceChannels.drawer.footer.generatePriceList': '生成价格表',
|
||||||
'channel.serviceChannels.drawer.footer.syncPriceList': '同步价格表',
|
'channel.serviceChannels.drawer.footer.syncPriceList': '同步价格表',
|
||||||
'channel.serviceChannels.drawer.footer.save': '保存',
|
'channel.serviceChannels.drawer.footer.save': '保存',
|
||||||
'channel.serviceChannels.drawer.priceList.hint': '已有组合会保留单价;勾选行可批量填价',
|
'channel.serviceChannels.drawer.priceList.hint': '已有组合会保留单价;选中行可批量填价',
|
||||||
'channel.serviceChannels.drawer.priceList.batchFill': '批量填价',
|
'channel.serviceChannels.drawer.priceList.batchFill': '批量填价',
|
||||||
'channel.serviceChannels.drawer.priceList.batchFillEmpty': '只填空白/0',
|
'channel.serviceChannels.drawer.priceList.batchFillEmpty': '只填空白/0',
|
||||||
'channel.serviceChannels.drawer.priceList.copyPrev': '复制上一行',
|
'channel.serviceChannels.drawer.priceList.copyPrev': '复制上一行',
|
||||||
'channel.serviceChannels.drawer.priceList.batchFillSelected': '已选 {count} 行',
|
'channel.serviceChannels.drawer.priceList.batchFillSelected': '已选 {count} 行',
|
||||||
'channel.serviceChannels.drawer.priceList.batchFillValue': '统一单价',
|
'channel.serviceChannels.drawer.priceList.batchFillValue': '统一单价',
|
||||||
'channel.serviceChannels.createTime': '创建: {time}',
|
'channel.serviceChannels.createTime': '创建: {time}',
|
||||||
|
'channel.serviceChannels.updateTime': '修改: {time}',
|
||||||
'channel.serviceChannels.empty.selectChannel': '请从左侧选择渠道',
|
'channel.serviceChannels.empty.selectChannel': '请从左侧选择渠道',
|
||||||
'channel.serviceChannels.context.channel': '当前渠道',
|
'channel.serviceChannels.context.channel': '当前渠道',
|
||||||
'channel.serviceChannels.context.group': '服务商分组',
|
'channel.serviceChannels.context.group': '服务商分组',
|
||||||
'channel.serviceChannels.context.app': '应用',
|
'channel.serviceChannels.context.app': '应用',
|
||||||
'channel.serviceChannels.context.apiProvider': 'API 服务商',
|
'channel.serviceChannels.context.apiProvider': 'API 服务商',
|
||||||
|
'channel.serviceChannels.context.updateTime': '修改时间',
|
||||||
|
'channel.serviceChannels.context.updateBy': '修改人',
|
||||||
'channel.serviceChannels.context.unsaved': '未保存',
|
'channel.serviceChannels.context.unsaved': '未保存',
|
||||||
'channel.serviceChannels.context.rulesPublishPending': '待发布配置',
|
'channel.serviceChannels.context.rulesPublishPending': '待发布配置',
|
||||||
'channel.serviceChannels.publishConfirm.title': '发布确认',
|
'channel.serviceChannels.publishConfirm.title': '发布确认',
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--app-page-bg: #f5f4f1;
|
--app-page-bg: #f0f3f4;
|
||||||
--app-card-bg: #ffffff;
|
--app-card-bg: #ffffff;
|
||||||
--app-card-border: #e5e6eb;
|
--app-card-border: #e5e6eb;
|
||||||
--app-elevated-bg: #ffffff;
|
--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 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 = [
|
export const PAGE_BACKGROUND_PRESETS = [
|
||||||
{ color: '#f0f3f4', labelKey: 'settings.pageBg.default' },
|
{ color: '#f0f3f4', labelKey: 'settings.pageBg.default' },
|
||||||
|
|||||||
@ -231,6 +231,9 @@ function flattenDiffParts(diff: ReturnType<typeof diffJson>) {
|
|||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 超过该字符量时跳过 diffJson 逐行展开,避免卖价大快照卡死主线程 */
|
||||||
|
const MAX_INLINE_DIFF_CHARS = 400_000
|
||||||
|
|
||||||
export function computeSnapshotDiff(
|
export function computeSnapshotDiff(
|
||||||
originalRaw: unknown,
|
originalRaw: unknown,
|
||||||
modifiedRaw: unknown
|
modifiedRaw: unknown
|
||||||
@ -246,6 +249,20 @@ export function computeSnapshotDiff(
|
|||||||
if (originalText === modifiedText) {
|
if (originalText === modifiedText) {
|
||||||
return { length: 0, rows: [], equal: true }
|
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 diff = diffJson(original, modified)
|
||||||
const rows = flattenDiffParts(diff)
|
const rows = flattenDiffParts(diff)
|
||||||
const length = rows.filter((row) => row.type !== 'same').length
|
const length = rows.filter((row) => row.type !== 'same').length
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<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 { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
|
||||||
import useModal from '@/components/ModalPro/useModal';
|
import useModal from '@/components/ModalPro/useModal';
|
||||||
import ModalPro from '@/components/ModalPro/index.vue';
|
import ModalPro from '@/components/ModalPro/index.vue';
|
||||||
@ -17,8 +27,6 @@
|
|||||||
postServiceProviderExport,
|
postServiceProviderExport,
|
||||||
postServiceProviderImport,
|
postServiceProviderImport,
|
||||||
} from '@/api/interface-serviceProvider/serviceProviderImportExport';
|
} 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 FormPro from '@/components/FormPro/index.vue';
|
||||||
import TableButtons from '@/components/TableButtons/index.vue';
|
import TableButtons from '@/components/TableButtons/index.vue';
|
||||||
import {
|
import {
|
||||||
@ -34,11 +42,25 @@
|
|||||||
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
|
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
|
||||||
import { getAppGetDropDownList } from '@/api/ApplicationServices/apps';
|
import { getAppGetDropDownList } from '@/api/ApplicationServices/apps';
|
||||||
import { getFixedParameterGetFixedParameterList } from '@/api/ParameterServices/fixedParameter';
|
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';
|
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 {
|
import {
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
@ -1842,7 +1864,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API调试组件 -->
|
<!-- API调试组件 -->
|
||||||
<div v-show="interfaceDetailTab === 'debug'" class="debug-content">
|
<div v-if="interfaceDetailTab === 'debug'" class="debug-content">
|
||||||
<ApiDebugger
|
<ApiDebugger
|
||||||
:key="`api-debugger-${selectedProvider?.id || ''}-${selectedInterface?.id || ''}-${apiDebuggerRefreshKey}`"
|
:key="`api-debugger-${selectedProvider?.id || ''}-${selectedInterface?.id || ''}-${apiDebuggerRefreshKey}`"
|
||||||
:selected-interface="selectedInterface"
|
:selected-interface="selectedInterface"
|
||||||
|
|||||||
@ -1,20 +1,29 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
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 { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import xmlFormat from 'xml-formatter';
|
|
||||||
import { $baseMessage } from '@/utils';
|
import { $baseMessage } from '@/utils';
|
||||||
import useModal from '@/components/ModalPro/useModal';
|
import useModal from '@/components/ModalPro/useModal';
|
||||||
import { getLogGetById, getLogGetList } from '@/api/ApplicationServices/log';
|
import { getLogGetById, getLogGetList } from '@/api/ApplicationServices/log';
|
||||||
import { IconCopy } from '@arco-design/web-vue/es/icon';
|
import { IconCopy } from '@arco-design/web-vue/es/icon';
|
||||||
import CodeMirror from '@/components/CodeMirror/index.vue';
|
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||||||
import { parseFilterValue, toServerSearchValue } from '@/components/HandsontablePro/filterValue';
|
import { parseFilterValue, toServerSearchValue } from '@/components/HandsontablePro/filterValue';
|
||||||
import type { ColumnDef } from '@/components/HandsontablePro/types';
|
import type { ColumnDef } from '@/components/HandsontablePro/types';
|
||||||
|
|
||||||
|
/** 详情报文编辑器按需加载,避免进页就拉 CodeMirror */
|
||||||
|
const CodeMirror = defineAsyncComponent(() => import('@/components/CodeMirror/index.vue'));
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@ -328,6 +337,7 @@
|
|||||||
headers.value = '';
|
headers.value = '';
|
||||||
content.value = '';
|
content.value = '';
|
||||||
exceptionType.value = '';
|
exceptionType.value = '';
|
||||||
|
activeName.value = 'first';
|
||||||
|
|
||||||
openModal();
|
openModal();
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
@ -335,10 +345,16 @@
|
|||||||
|
|
||||||
const data: any = await getLogGetById({ id: row.id });
|
const data: any = await getLogGetById({ id: row.id });
|
||||||
|
|
||||||
response.value = formatContent(data.response || '');
|
const [body, hdr, resp, resp2] = await Promise.all([
|
||||||
response2.value = formatContent(data.response2 || '');
|
formatContent(data.content || ''),
|
||||||
headers.value = formatContent(data.headers || '');
|
formatContent(data.headers || ''),
|
||||||
content.value = formatContent(data.content || '');
|
formatContent(data.response || ''),
|
||||||
|
formatContent(data.response2 || ''),
|
||||||
|
]);
|
||||||
|
content.value = body;
|
||||||
|
headers.value = hdr;
|
||||||
|
response.value = resp;
|
||||||
|
response2.value = resp2;
|
||||||
exceptionType.value = String(data.exception || '');
|
exceptionType.value = String(data.exception || '');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$baseMessage(t('system.log.message.loadDetailFailed'), 'error');
|
$baseMessage(t('system.log.message.loadDetailFailed'), 'error');
|
||||||
@ -398,7 +414,7 @@
|
|||||||
return str.trim().startsWith('<?xml') || str.trim().startsWith('<');
|
return str.trim().startsWith('<?xml') || str.trim().startsWith('<');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatContent(str: string): string {
|
async function formatContent(str: string): Promise<string> {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
const strValue = String(str);
|
const strValue = String(str);
|
||||||
if (isJSON(strValue)) {
|
if (isJSON(strValue)) {
|
||||||
@ -410,6 +426,7 @@
|
|||||||
}
|
}
|
||||||
if (isXML(strValue)) {
|
if (isXML(strValue)) {
|
||||||
try {
|
try {
|
||||||
|
const { default: xmlFormat } = await import('xml-formatter');
|
||||||
return xmlFormat(strValue);
|
return xmlFormat(strValue);
|
||||||
} catch {
|
} catch {
|
||||||
return strValue;
|
return strValue;
|
||||||
@ -521,11 +538,12 @@
|
|||||||
@register="register"
|
@register="register"
|
||||||
@cancel="dialogVisible = false"
|
@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-tabs v-model="activeName" class="mb-20px" @change="handleTabChange">
|
||||||
<a-tab-pane key="first" :title="$t('system.log.modal.tabs.requestBody')">
|
<a-tab-pane key="first" :title="$t('system.log.modal.tabs.requestBody')">
|
||||||
<div class="json-content">
|
<div class="json-content">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
v-if="activeName === 'first'"
|
||||||
v-model="content"
|
v-model="content"
|
||||||
:language="getContentLanguage(content)"
|
:language="getContentLanguage(content)"
|
||||||
theme="vs"
|
theme="vs"
|
||||||
@ -545,6 +563,7 @@
|
|||||||
<a-tab-pane key="second" :title="$t('system.log.modal.tabs.requestHeaders')">
|
<a-tab-pane key="second" :title="$t('system.log.modal.tabs.requestHeaders')">
|
||||||
<div class="json-content">
|
<div class="json-content">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
v-if="activeName === 'second'"
|
||||||
v-model="headers"
|
v-model="headers"
|
||||||
:language="getContentLanguage(headers)"
|
:language="getContentLanguage(headers)"
|
||||||
theme="vs"
|
theme="vs"
|
||||||
@ -564,6 +583,7 @@
|
|||||||
<a-tab-pane key="third" :title="$t('system.log.modal.tabs.response')">
|
<a-tab-pane key="third" :title="$t('system.log.modal.tabs.response')">
|
||||||
<div class="json-content">
|
<div class="json-content">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
v-if="activeName === 'third'"
|
||||||
v-model="response"
|
v-model="response"
|
||||||
:language="getContentLanguage(response)"
|
:language="getContentLanguage(response)"
|
||||||
theme="vs"
|
theme="vs"
|
||||||
@ -583,6 +603,7 @@
|
|||||||
<a-tab-pane key="fourth" :title="$t('system.log.modal.tabs.exception')">
|
<a-tab-pane key="fourth" :title="$t('system.log.modal.tabs.exception')">
|
||||||
<div class="json-content">
|
<div class="json-content">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
v-if="activeName === 'fourth'"
|
||||||
v-model="exceptionType"
|
v-model="exceptionType"
|
||||||
language="text"
|
language="text"
|
||||||
theme="vs"
|
theme="vs"
|
||||||
@ -602,6 +623,7 @@
|
|||||||
<a-tab-pane key="fifth" :title="$t('system.log.modal.tabs.queueResult')">
|
<a-tab-pane key="fifth" :title="$t('system.log.modal.tabs.queueResult')">
|
||||||
<div class="json-content">
|
<div class="json-content">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
v-if="activeName === 'fifth'"
|
||||||
v-model="response2"
|
v-model="response2"
|
||||||
:language="getContentLanguage(response2)"
|
:language="getContentLanguage(response2)"
|
||||||
theme="vs"
|
theme="vs"
|
||||||
|
|||||||
@ -2,14 +2,23 @@
|
|||||||
import { useTabBarStore } from '@/store';
|
import { useTabBarStore } from '@/store';
|
||||||
import { Modal, Message as $message } from '@arco-design/web-vue';
|
import { Modal, Message as $message } from '@arco-design/web-vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
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 {
|
import {
|
||||||
postInterfaceInterfacePaginatedList,
|
postInterfaceInterfacePaginatedList,
|
||||||
postInterfaceBatchDeleteInterfaceByIds,
|
postInterfaceBatchDeleteInterfaceByIds,
|
||||||
} from '@/api/InterfaceServices/interface';
|
} from '@/api/InterfaceServices/interface';
|
||||||
|
|
||||||
import CascaderPro from '@/components/CascaderPro/Cascader.vue';
|
|
||||||
import { getServiceProviderGetServiceProviderInterfaceCascadedList } from '@/api/interface-serviceProvider/serviceProvider';
|
import { getServiceProviderGetServiceProviderInterfaceCascadedList } from '@/api/interface-serviceProvider/serviceProvider';
|
||||||
import { getDictionaryGetList } from '@/api/ApplicationServices/dictionary';
|
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';
|
||||||
@ -22,13 +31,20 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
} from '@/api/InterfaceMappingServices/interfaceMap';
|
} from '@/api/InterfaceMappingServices/interfaceMap';
|
||||||
import useModal from '@/components/ModalPro/useModal';
|
import useModal from '@/components/ModalPro/useModal';
|
||||||
import { $baseMessage, $baseConfirm } from '@/utils';
|
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 { systemInterfaceColumns } from './models/tableColumns';
|
||||||
import UpstreamInterfaceModal from './components/UpstreamInterfaceModal.vue';
|
|
||||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.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();
|
const { t } = useI18n();
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@ -70,6 +86,8 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
const drawerFormRef = ref<any>();
|
const drawerFormRef = ref<any>();
|
||||||
const upStreamIdRef = ref<number>();
|
const upStreamIdRef = ref<number>();
|
||||||
const enteredMapRef = ref<any>();
|
const enteredMapRef = ref<any>();
|
||||||
|
/** 弹窗内 EnteredMap,与右侧 Tab 实例分开,避免 ref 互相覆盖 */
|
||||||
|
const enterMapModalRef = ref<any>();
|
||||||
const drawerConfig = reactive({
|
const drawerConfig = reactive({
|
||||||
itemList: [
|
itemList: [
|
||||||
{
|
{
|
||||||
@ -179,9 +197,47 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
|
|
||||||
const [register, registerMethods] = useModal() as any;
|
const [register, registerMethods] = useModal() as any;
|
||||||
const [registerEnter, registerEnterMethods] = useModal() as any;
|
const [registerEnter, registerEnterMethods] = useModal() as any;
|
||||||
|
/** 入参映射弹窗打开后再挂 EnteredMap,避免进页就拉映射分包 */
|
||||||
|
const enterMapModalOpen = ref(false);
|
||||||
const mapId = ref();
|
const mapId = ref();
|
||||||
// 映射服务商
|
// 映射服务商
|
||||||
const mapName = 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) {
|
async function mapClick(row: any) {
|
||||||
newmapClickROW.value = row;
|
newmapClickROW.value = row;
|
||||||
mapName.value = row.name;
|
mapName.value = row.name;
|
||||||
@ -797,18 +853,12 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
intId: id,
|
intId: id,
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEnterMethods.openModal();
|
await openEnterMapModal({
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
registerEnterMethods.setModalProps({
|
|
||||||
title: t('system.interface.modal.add'),
|
title: t('system.interface.modal.add'),
|
||||||
width: 1600,
|
init: {
|
||||||
height: 1600,
|
|
||||||
});
|
|
||||||
|
|
||||||
(unref(enteredMapRef) as any)?.initData?.({
|
|
||||||
serviceProviderId: serviceProviderId.value,
|
serviceProviderId: serviceProviderId.value,
|
||||||
interfaceId: upStreamIdRef.value,
|
interfaceId: upStreamIdRef.value,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
active.value = 1;
|
active.value = 1;
|
||||||
|
|
||||||
@ -837,22 +887,13 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
serviceProviderName.value = data.serviceProviderName;
|
serviceProviderName.value = data.serviceProviderName;
|
||||||
upStreamIdRef.value = data.addId;
|
upStreamIdRef.value = data.addId;
|
||||||
InputParameterMappingList.value = data.inputParameterMappingList;
|
InputParameterMappingList.value = data.inputParameterMappingList;
|
||||||
registerEnterMethods.setModalProps({
|
await openEnterMapModal({
|
||||||
title: t('system.interface.modal.edit'),
|
title: t('system.interface.modal.edit'),
|
||||||
width: 1600,
|
init: {
|
||||||
height: 1600,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
registerEnterMethods.openModal();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
await (unref(enteredMapRef) as any)?.initData?.({
|
|
||||||
serviceProviderId: data.serviceProviderId,
|
serviceProviderId: data.serviceProviderId,
|
||||||
interfaceId: data.addId,
|
interfaceId: data.addId,
|
||||||
}
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
const MapServiceProviderROW = ref<any>();
|
const MapServiceProviderROW = ref<any>();
|
||||||
const MapServiceProviderRef = ref<any>();
|
const MapServiceProviderRef = ref<any>();
|
||||||
@ -885,7 +926,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveModelEnter() {
|
async function saveModelEnter() {
|
||||||
const mapComp = unref(enteredMapRef) as any;
|
const mapComp = activeEnteredMap();
|
||||||
const guard = mapComp?.guardBeforeSave?.() as 'ok' | 'goto-output' | 'required' | undefined;
|
const guard = mapComp?.guardBeforeSave?.() as 'ok' | 'goto-output' | 'required' | undefined;
|
||||||
if (guard === 'goto-output') {
|
if (guard === 'goto-output') {
|
||||||
$baseMessage(t('system.interface.mapping.outputMappingGuide'), 'warning');
|
$baseMessage(t('system.interface.mapping.outputMappingGuide'), 'warning');
|
||||||
@ -927,7 +968,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
try {
|
try {
|
||||||
await postSaveParameterMaps(data);
|
await postSaveParameterMaps(data);
|
||||||
loadingSubmit.value = false;
|
loadingSubmit.value = false;
|
||||||
(unref(enteredMapRef) as any)?.initData?.(
|
activeEnteredMap()?.initData?.(
|
||||||
{
|
{
|
||||||
serviceProviderId: serviceProviderId.value,
|
serviceProviderId: serviceProviderId.value,
|
||||||
interfaceId: upStreamIdRef.value,
|
interfaceId: upStreamIdRef.value,
|
||||||
@ -939,7 +980,7 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
title: t('system.interface.modal.warningTitle'),
|
title: t('system.interface.modal.warningTitle'),
|
||||||
content: t('system.interface.modal.publishConfirm'),
|
content: t('system.interface.modal.publishConfirm'),
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await registerEnterMethods.closeModal?.();
|
closeEnterMapModal();
|
||||||
const currentRow = MapServiceProviderROW.value;
|
const currentRow = MapServiceProviderROW.value;
|
||||||
if (currentRow) {
|
if (currentRow) {
|
||||||
(unref(MapServiceProviderRef) as any)?.onRelease?.(currentRow);
|
(unref(MapServiceProviderRef) as any)?.onRelease?.(currentRow);
|
||||||
@ -1398,7 +1439,6 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
title: category,
|
title: category,
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
subtitle: `${items.length} 个接口`,
|
|
||||||
children: items
|
children: items
|
||||||
};
|
};
|
||||||
treeData.push(categoryNode);
|
treeData.push(categoryNode);
|
||||||
@ -1679,14 +1719,12 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
class="mapping-modal"
|
class="mapping-modal"
|
||||||
@register="registerEnter"
|
@register="registerEnter"
|
||||||
@ok="saveModelEnter"
|
@ok="saveModelEnter"
|
||||||
|
@cancel="enterMapModalOpen = false"
|
||||||
@reset="() => {}"
|
@reset="() => {}"
|
||||||
>
|
>
|
||||||
|
<div v-if="enterMapModalOpen">
|
||||||
<div >
|
|
||||||
|
|
||||||
<EnteredMap
|
<EnteredMap
|
||||||
ref="enteredMapRef"
|
ref="enterMapModalRef"
|
||||||
|
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
:service-provider-id="serviceProviderId"
|
:service-provider-id="serviceProviderId"
|
||||||
:interface-id="upStreamIdRef"
|
:interface-id="upStreamIdRef"
|
||||||
@ -1694,7 +1732,6 @@ import { IconClose, IconEdit, IconPlus, IconSearch, IconMore, IconMinus, IconRef
|
|||||||
@save-model-enter="saveModelEnter"
|
@save-model-enter="saveModelEnter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ModalPro>
|
</ModalPro>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
* 卖价/买价「计费条件」紧凑编辑:
|
* 卖价/买价「计费条件」紧凑编辑:
|
||||||
* 默认只占输入区,不铺开字段芯片;
|
* 默认只占输入区,不铺开字段芯片;
|
||||||
* AI 写公式走侧栏抽屉;手动插字段走 Popover。
|
* AI 写公式走侧栏抽屉;手动插字段走 Popover。
|
||||||
|
* 传入 label 时:标签与工具栏同一行,工具栏在右侧。
|
||||||
*/
|
*/
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { IconQuestionCircle } from '@arco-design/web-vue/es/icon'
|
||||||
import FormulaAiAssistDrawer from '@/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue'
|
import FormulaAiAssistDrawer from '@/views/channel/autoTrigger/components/FormulaAiAssistDrawer.vue'
|
||||||
import {
|
import {
|
||||||
FORMULA_MAX_LENGTH,
|
FORMULA_MAX_LENGTH,
|
||||||
@ -23,9 +25,18 @@ import {
|
|||||||
buildBillingConditionAiSystemPrompt,
|
buildBillingConditionAiSystemPrompt,
|
||||||
} from '../billingConditionAiSkill'
|
} from '../billingConditionAiSkill'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
}>()
|
/** 有值时:标签与工具栏同一行,工具栏在右侧 */
|
||||||
|
label?: string
|
||||||
|
labelTip?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
labelTip: '',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [string]
|
'update:modelValue': [string]
|
||||||
@ -47,6 +58,7 @@ const displayValue = computed(() => {
|
|||||||
const v = props.modelValue ?? ''
|
const v = props.modelValue ?? ''
|
||||||
return v === '无' ? '' : v
|
return v === '无' ? '' : v
|
||||||
})
|
})
|
||||||
|
const toolbarInline = computed(() => !!props.label)
|
||||||
|
|
||||||
function updateValue(next: string) {
|
function updateValue(next: string) {
|
||||||
emit('update:modelValue', next.slice(0, FORMULA_MAX_LENGTH))
|
emit('update:modelValue', next.slice(0, FORMULA_MAX_LENGTH))
|
||||||
@ -88,7 +100,14 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="formula-text-wrap">
|
||||||
<textarea
|
<textarea
|
||||||
ref="textareaRef"
|
ref="textareaRef"
|
||||||
@ -102,9 +121,13 @@ watch(
|
|||||||
<div class="char-count">{{ charCount }}/{{ FORMULA_MAX_LENGTH }}</div>
|
<div class="char-count">{{ charCount }}/{{ FORMULA_MAX_LENGTH }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="editor-footer">
|
<div class="editor-footer" :class="{ 'editor-footer--inline': toolbarInline }">
|
||||||
<div class="editor-actions">
|
<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') }}
|
{{ t('channel.salesProducts.billingCondition.aiWrite') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-popover
|
<a-popover
|
||||||
@ -113,7 +136,7 @@ watch(
|
|||||||
position="bl"
|
position="bl"
|
||||||
:content-style="{ padding: '16px', width: '520px', maxWidth: '92vw' }"
|
: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') }}
|
{{ t('channel.salesProducts.billingCondition.insertField') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #content>
|
<template #content>
|
||||||
@ -181,6 +204,48 @@ watch(
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.billing-condition-editor {
|
.billing-condition-editor {
|
||||||
width: 100%;
|
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 {
|
.formula-text-wrap {
|
||||||
@ -228,6 +293,11 @@ watch(
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px 12px;
|
gap: 8px 12px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
|
||||||
|
&--inline {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-actions {
|
.editor-actions {
|
||||||
@ -239,8 +309,12 @@ watch(
|
|||||||
|
|
||||||
.editor-hint {
|
.editor-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-border-3);
|
color: var(--color-text-3);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-popover {
|
.field-popover {
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<a-form :model="form" layout="vertical" class="sim-form">
|
<a-form :model="form" layout="vertical" class="sim-form">
|
||||||
<a-row :gutter="12">
|
<a-row :gutter="12">
|
||||||
<a-col :span="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
|
<a-select
|
||||||
v-model="form.countryCode"
|
v-model="form.countryCode"
|
||||||
allow-search
|
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>
|
<tr>
|
||||||
<td class="product-row-left">
|
<td class="product-row-left">
|
||||||
<span class="update-time">
|
<span class="update-time">
|
||||||
{{ t('channel.weightRounding.updateTime', { time: nodeData?.updateTime ?? '' }) }}
|
{{
|
||||||
|
t('channel.weightRounding.updateTime', {
|
||||||
|
time: nodeData?.updateTime || '--',
|
||||||
|
})
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="product-row-right"></td>
|
<td class="product-row-right"></td>
|
||||||
@ -159,6 +163,7 @@
|
|||||||
import ChannelConfigHotTable from '@/views/channel/components/ChannelConfigHotTable.vue';
|
import ChannelConfigHotTable from '@/views/channel/components/ChannelConfigHotTable.vue';
|
||||||
import type { ChannelHotColumn } from '@/views/channel/components/channelHotTable.types';
|
import type { ChannelHotColumn } from '@/views/channel/components/channelHotTable.types';
|
||||||
import { syncChannelLeftSelection } from '@/views/channel/composables/channelLeftSelection';
|
import { syncChannelLeftSelection } from '@/views/channel/composables/channelLeftSelection';
|
||||||
|
import { pickResolvedAudit } from '@/utils/auditFields';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@ -505,14 +510,19 @@
|
|||||||
{
|
{
|
||||||
key: 'category-list',
|
key: 'category-list',
|
||||||
title: t('channel.weightRounding.listTitle'),
|
title: t('channel.weightRounding.listTitle'),
|
||||||
children: leftMenuData.value.map((item) => ({
|
children: leftMenuData.value.map((item) => {
|
||||||
|
const audit = pickResolvedAudit(item);
|
||||||
|
return {
|
||||||
key: `item-${item.id}`,
|
key: `item-${item.id}`,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
...item,
|
...item,
|
||||||
})),
|
updateTime: audit.updateTime,
|
||||||
|
updateBy: audit.updateBy,
|
||||||
|
};
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="product-row-left">
|
<td class="product-row-left">
|
||||||
<span class="update-time">
|
<span class="update-time">
|
||||||
{{ t('channel.weightRules.updateTime', { time: nodeData?.updateTime ?? '' }) }}
|
{{ t('channel.weightRules.updateTime', { time: nodeData?.updateTime || '--' }) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="product-row-right"></td>
|
<td class="product-row-right"></td>
|
||||||
@ -161,6 +161,7 @@
|
|||||||
import ChannelConfigHotTable from '@/views/channel/components/ChannelConfigHotTable.vue';
|
import ChannelConfigHotTable from '@/views/channel/components/ChannelConfigHotTable.vue';
|
||||||
import type { ChannelHotColumn } from '@/views/channel/components/channelHotTable.types';
|
import type { ChannelHotColumn } from '@/views/channel/components/channelHotTable.types';
|
||||||
import { syncChannelLeftSelection } from '@/views/channel/composables/channelLeftSelection';
|
import { syncChannelLeftSelection } from '@/views/channel/composables/channelLeftSelection';
|
||||||
|
import { pickResolvedAudit } from '@/utils/auditFields';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@ -489,14 +490,19 @@
|
|||||||
{
|
{
|
||||||
key: 'category-list',
|
key: 'category-list',
|
||||||
title: t('channel.weightRules.listTitle'),
|
title: t('channel.weightRules.listTitle'),
|
||||||
children: leftMenuData.value.map((item) => ({
|
children: leftMenuData.value.map((item) => {
|
||||||
|
const audit = pickResolvedAudit(item);
|
||||||
|
return {
|
||||||
key: `item-${item.id}`,
|
key: `item-${item.id}`,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
...item,
|
...item,
|
||||||
})),
|
updateTime: audit.updateTime,
|
||||||
|
updateBy: audit.updateBy,
|
||||||
|
};
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="nova-login">
|
<div class="nova-login">
|
||||||
<div class="nova-login__header">
|
<a
|
||||||
|
class="nova-login__header"
|
||||||
|
href="/"
|
||||||
|
:title="brandHint"
|
||||||
|
@click.prevent="goOfficialSite"
|
||||||
|
>
|
||||||
<LoginLockLogo />
|
<LoginLockLogo />
|
||||||
<p class="nova-login__brand-hint">{{ brandHint }}</p>
|
<p class="nova-login__brand-hint">{{ brandHint }}</p>
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
<form class="nova-login__form" autocomplete="off" @submit.prevent="handleSubmit">
|
<form class="nova-login__form" autocomplete="off" @submit.prevent="handleSubmit">
|
||||||
<div class="nova-login__input-group">
|
<div class="nova-login__input-group">
|
||||||
@ -162,6 +167,11 @@ const isCustomerLogin = computed(() => props.loginType === OmsLoginType.Customer
|
|||||||
const brandHint = computed(() =>
|
const brandHint = computed(() =>
|
||||||
t(isCustomerLogin.value ? 'login.form.brandHint.customer' : 'login.form.brandHint.admin'),
|
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 accountFieldLabel = computed(() => t('login.field.account'));
|
||||||
const accountPlaceholder = computed(() =>
|
const accountPlaceholder = computed(() =>
|
||||||
isCustomerLogin.value
|
isCustomerLogin.value
|
||||||
@ -403,8 +413,12 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.nova-login__header {
|
.nova-login__header {
|
||||||
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nova-login__brand-hint {
|
.nova-login__brand-hint {
|
||||||
|
|||||||
@ -164,6 +164,7 @@ provide(LOGIN_EXIT_KEY, {
|
|||||||
if (isExiting.value) {
|
if (isExiting.value) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
// Flips login-container--exit: hides admin overlays outside pane clip-paths
|
||||||
isExiting.value = true;
|
isExiting.value = true;
|
||||||
|
|
||||||
const leftTargets = collectExitTargets('left');
|
const leftTargets = collectExitTargets('left');
|
||||||
@ -335,6 +336,15 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.login-container--exit {
|
.login-container--exit {
|
||||||
pointer-events: none;
|
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 {
|
.login-split {
|
||||||
|
|||||||
@ -695,8 +695,7 @@ watch(adminCustomerId, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadList()
|
void Promise.all([loadList(), loadCounts()])
|
||||||
loadCounts()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<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 { useRoute, useRouter } from 'vue-router';
|
||||||
import { IconCalendar, IconDown, IconQuestionCircleFill } from '@arco-design/web-vue/es/icon';
|
import { IconCalendar, IconDown, IconQuestionCircleFill } from '@arco-design/web-vue/es/icon';
|
||||||
import { Message, Modal } from '@arco-design/web-vue';
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
@ -40,13 +49,6 @@
|
|||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
|
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
|
||||||
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
|
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 {
|
import {
|
||||||
MY_ORDERS_COLUMN_CATALOG,
|
MY_ORDERS_COLUMN_CATALOG,
|
||||||
MY_ORDERS_COLUMN_STORAGE_KEY,
|
MY_ORDERS_COLUMN_STORAGE_KEY,
|
||||||
@ -60,9 +62,32 @@
|
|||||||
type ColumnScheme,
|
type ColumnScheme,
|
||||||
} from './columnConfig';
|
} from './columnConfig';
|
||||||
import { filterMyOrdersColumns, filterMyOrdersFieldKeys } from './customerFieldMask';
|
import { filterMyOrdersColumns, filterMyOrdersFieldKeys } from './customerFieldMask';
|
||||||
import OrderPrintModal from '@/components/PrintTemplate/OrderPrintModal.vue';
|
|
||||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||||||
import { fetchPrintUrlsForOrders, getCachedPrintUrl } from '@/utils/printSessionCache';
|
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 {
|
import {
|
||||||
ORDER_STATUS_TABS,
|
ORDER_STATUS_TABS,
|
||||||
ORDER_STATUS_ALL,
|
ORDER_STATUS_ALL,
|
||||||
@ -106,9 +131,48 @@
|
|||||||
);
|
);
|
||||||
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
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 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 */
|
/** 兼容后端 camelCase / PascalCase */
|
||||||
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
||||||
@ -216,10 +280,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentOrder.value = nextRow;
|
currentOrder.value = nextRow;
|
||||||
detailDrawerRef.value?.openDrawer({ tab: opts?.tab || 'basic' });
|
const drawer = await ensureDetailDrawer();
|
||||||
|
drawer?.openDrawer?.({ tab: opts?.tab || 'basic' });
|
||||||
if (opts?.editBasic) {
|
if (opts?.editBasic) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
detailDrawerRef.value?.startBasicEdit?.();
|
drawer?.startBasicEdit?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1061,9 +1126,7 @@
|
|||||||
openPayConfirm(rows);
|
openPayConfirm(rows);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPickupModalRef = ref<InstanceType<typeof CreatePickupOrderModal> | null>(null);
|
async function handleCreatePickup() {
|
||||||
|
|
||||||
const handleCreatePickup = () => {
|
|
||||||
const rows = requireSelectedRows();
|
const rows = requireSelectedRows();
|
||||||
if (!rows.length) return;
|
if (!rows.length) return;
|
||||||
const notPredeclared = rows.filter((r) => resolveOrderStatusCode(r) !== 'predeclared');
|
const notPredeclared = rows.filter((r) => resolveOrderStatusCode(r) !== 'predeclared');
|
||||||
@ -1076,7 +1139,12 @@
|
|||||||
Message.warning(t('order.myOrders.createPickup.alreadyBound'));
|
Message.warning(t('order.myOrders.createPickup.alreadyBound'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createPickupModalRef.value?.open(
|
createPickupMounted.value = true;
|
||||||
|
const modal = await waitForRef(
|
||||||
|
() => createPickupModalRef.value,
|
||||||
|
(v) => typeof v.open === 'function',
|
||||||
|
);
|
||||||
|
modal?.open?.(
|
||||||
rows.map((r) => ({
|
rows.map((r) => ({
|
||||||
id: getOrderRowId(r)!,
|
id: getOrderRowId(r)!,
|
||||||
serviceNo: r?.serviceNo || r?.ServiceNo,
|
serviceNo: r?.serviceNo || r?.ServiceNo,
|
||||||
@ -1084,7 +1152,7 @@
|
|||||||
customerId: r?.customerId ?? r?.CustomerId,
|
customerId: r?.customerId ?? r?.CustomerId,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
async function handleCreatePickupSuccess() {
|
async function handleCreatePickupSuccess() {
|
||||||
await refreshListAndTabCounts();
|
await refreshListAndTabCounts();
|
||||||
@ -1530,7 +1598,6 @@
|
|||||||
t('order.myOrders.columnConfig.defaultScheme'),
|
t('order.myOrders.columnConfig.defaultScheme'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const columnConfigModalRef = ref<InstanceType<typeof ColumnConfigModal>>();
|
|
||||||
|
|
||||||
const columnSchemeOptions = computed(() =>
|
const columnSchemeOptions = computed(() =>
|
||||||
columnConfigState.value.schemes.map((s) => ({ label: s.name, value: s.id })),
|
columnConfigState.value.schemes.map((s) => ({ label: s.name, value: s.id })),
|
||||||
@ -1607,8 +1674,13 @@
|
|||||||
saveColumnConfigState(listColumnStorageKey.value, columnConfigState.value);
|
saveColumnConfigState(listColumnStorageKey.value, columnConfigState.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openColumnConfig() {
|
async function openColumnConfig() {
|
||||||
columnConfigModalRef.value?.open();
|
columnConfigMounted.value = true;
|
||||||
|
const modal = await waitForRef(
|
||||||
|
() => columnConfigModalRef.value,
|
||||||
|
(v) => typeof v.open === 'function',
|
||||||
|
);
|
||||||
|
modal?.open?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
|
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
|
||||||
@ -1792,8 +1864,7 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
await loadOrderList();
|
await Promise.all([loadOrderList(), loadTabCounts()]);
|
||||||
loadTabCounts();
|
|
||||||
await tryOpenOrderFromQuery();
|
await tryOpenOrderFromQuery();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -2021,7 +2092,7 @@
|
|||||||
<div ref="tableContainerRef" class="list-wrap">
|
<div ref="tableContainerRef" class="list-wrap">
|
||||||
<a-spin :loading="listLoading" class="table-spin">
|
<a-spin :loading="listLoading" class="table-spin">
|
||||||
<HandsontablePro
|
<HandsontablePro
|
||||||
:key="`${activeTab}-${currentPage}-${orderList.length}-${tableColumnsKey}`"
|
:key="`${activeTab}-${tableColumnsKey}`"
|
||||||
:columns="hotColumns"
|
:columns="hotColumns"
|
||||||
:data="processedTableData"
|
:data="processedTableData"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
@ -2045,13 +2116,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 订单详情抽屉(左侧) -->
|
<!-- 订单详情抽屉(左侧):首次打开再挂载 -->
|
||||||
<OrderDetailDrawer
|
<OrderDetailDrawer
|
||||||
|
v-if="detailDrawerMounted"
|
||||||
ref="detailDrawerRef"
|
ref="detailDrawerRef"
|
||||||
:order="currentOrder"
|
:order="currentOrder"
|
||||||
@updated="handleOrderDetailUpdated"
|
@updated="handleOrderDetailUpdated"
|
||||||
/>
|
/>
|
||||||
<ColumnConfigModal
|
<ColumnConfigModal
|
||||||
|
v-if="columnConfigMounted"
|
||||||
ref="columnConfigModalRef"
|
ref="columnConfigModalRef"
|
||||||
:catalog="listColumnCatalog"
|
:catalog="listColumnCatalog"
|
||||||
:schemes="columnConfigState.schemes"
|
:schemes="columnConfigState.schemes"
|
||||||
@ -2059,8 +2132,14 @@
|
|||||||
:default-keys="MY_ORDERS_DEFAULT_KEYS"
|
:default-keys="MY_ORDERS_DEFAULT_KEYS"
|
||||||
@apply="handleColumnConfigApply"
|
@apply="handleColumnConfigApply"
|
||||||
/>
|
/>
|
||||||
<OrderPrintModal v-model:visible="singlePrintVisible" mode="single" :items="singlePrintItems" />
|
|
||||||
<OrderPrintModal
|
<OrderPrintModal
|
||||||
|
v-if="singlePrintVisible"
|
||||||
|
v-model:visible="singlePrintVisible"
|
||||||
|
mode="single"
|
||||||
|
:items="singlePrintItems"
|
||||||
|
/>
|
||||||
|
<OrderPrintModal
|
||||||
|
v-if="batchPrintVisible"
|
||||||
v-model:visible="batchPrintVisible"
|
v-model:visible="batchPrintVisible"
|
||||||
mode="batch"
|
mode="batch"
|
||||||
:items="batchPrintItems"
|
:items="batchPrintItems"
|
||||||
@ -2068,11 +2147,13 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MarkProblematicModal
|
<MarkProblematicModal
|
||||||
|
v-if="problematicModalVisible"
|
||||||
v-model:visible="problematicModalVisible"
|
v-model:visible="problematicModalVisible"
|
||||||
:order-ids="exceptionOrderIds"
|
:order-ids="exceptionOrderIds"
|
||||||
@success="handleExceptionActionSuccess"
|
@success="handleExceptionActionSuccess"
|
||||||
/>
|
/>
|
||||||
<MarkClaimedModal
|
<MarkClaimedModal
|
||||||
|
v-if="claimedModalVisible"
|
||||||
v-model:visible="claimedModalVisible"
|
v-model:visible="claimedModalVisible"
|
||||||
:order-ids="exceptionOrderIds"
|
:order-ids="exceptionOrderIds"
|
||||||
:has-insurance="claimContext.hasInsurance"
|
:has-insurance="claimContext.hasInsurance"
|
||||||
@ -2081,16 +2162,22 @@
|
|||||||
@success="handleExceptionActionSuccess"
|
@success="handleExceptionActionSuccess"
|
||||||
/>
|
/>
|
||||||
<StockBindModal
|
<StockBindModal
|
||||||
|
v-if="stockBindModalVisible"
|
||||||
v-model:visible="stockBindModalVisible"
|
v-model:visible="stockBindModalVisible"
|
||||||
:order-ids="stockBindOrderIds"
|
:order-ids="stockBindOrderIds"
|
||||||
@success="handleStockBindSuccess"
|
@success="handleStockBindSuccess"
|
||||||
/>
|
/>
|
||||||
<OrderPayConfirmModal
|
<OrderPayConfirmModal
|
||||||
|
v-if="payConfirmVisible"
|
||||||
v-model:visible="payConfirmVisible"
|
v-model:visible="payConfirmVisible"
|
||||||
:orders="payConfirmOrders"
|
:orders="payConfirmOrders"
|
||||||
@success="handlePayConfirmSuccess"
|
@success="handlePayConfirmSuccess"
|
||||||
/>
|
/>
|
||||||
<CreatePickupOrderModal ref="createPickupModalRef" @success="handleCreatePickupSuccess" />
|
<CreatePickupOrderModal
|
||||||
|
v-if="createPickupMounted"
|
||||||
|
ref="createPickupModalRef"
|
||||||
|
@success="handleCreatePickupSuccess"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@ -10,7 +10,6 @@ import PaginationPro from '@/components/PaginationPro/index.vue'
|
|||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import useModal from '@/components/ModalPro/useModal'
|
import useModal from '@/components/ModalPro/useModal'
|
||||||
import PickupOrderDetailDrawer from './PickupOrderDetailDrawer.vue'
|
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||||
import {
|
import {
|
||||||
@ -44,12 +43,20 @@ import {
|
|||||||
loadDomesticPickupProductOptions,
|
loadDomesticPickupProductOptions,
|
||||||
type DomesticPickupProductOption,
|
type DomesticPickupProductOption,
|
||||||
} from '@/utils/domesticPickupProducts'
|
} from '@/utils/domesticPickupProducts'
|
||||||
import CompleteContactModal from '@/components/CompleteContactModal/index.vue'
|
|
||||||
import type { CustomerProfileCompleteness } from '@/api/customer'
|
import type { CustomerProfileCompleteness } from '@/api/customer'
|
||||||
import CreatePickupSheetModal from './components/CreatePickupSheetModal.vue'
|
|
||||||
import type { CreatePickupSuccessPayload } from './components/CreatePickupSheetModal.vue'
|
import type { CreatePickupSuccessPayload } from './components/CreatePickupSheetModal.vue'
|
||||||
import BindableOrderPicker from './components/BindableOrderPicker.vue'
|
|
||||||
import type { BindableOrderRow } 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 { getOrderStatusMeta, resolveOrderStatusCode } from '@/constants/orderStatus'
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||||
import { getMapIdFromAccessToken } from '@/utils/jwt'
|
import { getMapIdFromAccessToken } from '@/utils/jwt'
|
||||||
@ -68,10 +75,28 @@ const {
|
|||||||
withCustomerId,
|
withCustomerId,
|
||||||
} = useWarehouseAdminCustomer()
|
} = 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 contactVisible = ref(false)
|
||||||
const contactProfile = ref<CustomerProfileCompleteness | null>(null)
|
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 keyword = ref('')
|
||||||
const STATUS_TAB_ALL = 'all'
|
const STATUS_TAB_ALL = 'all'
|
||||||
const DIRECTION_FORWARD = 'forward'
|
const DIRECTION_FORWARD = 'forward'
|
||||||
@ -405,12 +430,21 @@ function handleSearch() {
|
|||||||
void loadList()
|
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() {
|
function handleOpenCreate() {
|
||||||
if (!hasPermission('Order.Pickup.Create')) {
|
if (!hasPermission('Order.Pickup.Create')) {
|
||||||
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
|
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
createModalRef.value?.open(adminCustomerId.value, directionFilter.value)
|
void openCreateModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreateSuccess(payload: CreatePickupSuccessPayload = {}) {
|
function onCreateSuccess(payload: CreatePickupSuccessPayload = {}) {
|
||||||
@ -502,7 +536,7 @@ function onNeedContact(profile: CustomerProfileCompleteness | null) {
|
|||||||
|
|
||||||
function onContactSuccess() {
|
function onContactSuccess() {
|
||||||
contactVisible.value = false
|
contactVisible.value = false
|
||||||
createModalRef.value?.open(adminCustomerId.value, directionFilter.value)
|
void openCreateModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDirectionTabChange() {
|
function handleDirectionTabChange() {
|
||||||
@ -868,12 +902,17 @@ function handleRowAction(row: any, _rowIndex: number, action?: string) {
|
|||||||
async function openDetail(id: number | string) {
|
async function openDetail(id: number | string) {
|
||||||
detailLoading.value = true
|
detailLoading.value = true
|
||||||
detail.value = null
|
detail.value = null
|
||||||
detailDrawerRef.value?.open()
|
detailDrawerMounted.value = true
|
||||||
|
const drawer = await waitForPickupRef(
|
||||||
|
() => detailDrawerRef.value,
|
||||||
|
(v) => typeof v.open === 'function',
|
||||||
|
)
|
||||||
|
drawer?.open()
|
||||||
try {
|
try {
|
||||||
detail.value = await getPickupOrderDetail(id)
|
detail.value = await getPickupOrderDetail(id)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
|
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
|
||||||
detailDrawerRef.value?.close()
|
drawer?.close()
|
||||||
} finally {
|
} finally {
|
||||||
detailLoading.value = false
|
detailLoading.value = false
|
||||||
}
|
}
|
||||||
@ -1336,8 +1375,9 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
await ensureCarrierServiceOptions()
|
// 承运选项不阻塞列表首屏
|
||||||
|
void ensureCarrierServiceOptions()
|
||||||
void loadList()
|
void loadList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -1345,11 +1385,13 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="pickup-order-container warehouse-list-page">
|
<div class="pickup-order-container warehouse-list-page">
|
||||||
<CompleteContactModal
|
<CompleteContactModal
|
||||||
|
v-if="contactVisible"
|
||||||
v-model:visible="contactVisible"
|
v-model:visible="contactVisible"
|
||||||
:profile="contactProfile"
|
:profile="contactProfile"
|
||||||
@success="onContactSuccess"
|
@success="onContactSuccess"
|
||||||
/>
|
/>
|
||||||
<CreatePickupSheetModal
|
<CreatePickupSheetModal
|
||||||
|
v-if="createModalMounted"
|
||||||
ref="createModalRef"
|
ref="createModalRef"
|
||||||
@success="onCreateSuccess"
|
@success="onCreateSuccess"
|
||||||
@need-contact="onNeedContact"
|
@need-contact="onNeedContact"
|
||||||
@ -1488,6 +1530,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PickupOrderDetailDrawer
|
<PickupOrderDetailDrawer
|
||||||
|
v-if="detailDrawerMounted"
|
||||||
ref="detailDrawerRef"
|
ref="detailDrawerRef"
|
||||||
:loading="detailLoading"
|
:loading="detailLoading"
|
||||||
:detail="detail"
|
:detail="detail"
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<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 { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Modal } from '@arco-design/web-vue'
|
import { Modal } from '@arco-design/web-vue'
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||||
import { getSalesProductList } from '@/api/channel/salesProduct'
|
import { getSalesProductList } from '@/api/channel/salesProduct'
|
||||||
@ -32,9 +33,11 @@ import {
|
|||||||
} from '@/utils/warehouseListMapper'
|
} from '@/utils/warehouseListMapper'
|
||||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||||
import { buildStoreOrderStatusTagHtml } from '@/constants/storeOrderStatus'
|
import { buildStoreOrderStatusTagHtml } from '@/constants/storeOrderStatus'
|
||||||
import PullOrdersModal from './PullOrdersModal.vue'
|
import type { CreateLogisticsMode } from './CreateLogisticsModal.vue'
|
||||||
import StoreOrderDetailDrawer from './StoreOrderDetailDrawer.vue'
|
|
||||||
import CreateLogisticsModal, { 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' })
|
defineOptions({ name: 'PlatformStoreOrder' })
|
||||||
|
|
||||||
@ -93,8 +96,21 @@ const pullVisible = ref(false)
|
|||||||
const actionLoading = ref(false)
|
const actionLoading = ref(false)
|
||||||
const matchWarehouseId = ref<number>()
|
const matchWarehouseId = ref<number>()
|
||||||
const matchProductCode = ref<string>()
|
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)
|
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 createModalMode = ref<CreateLogisticsMode>('logistics')
|
||||||
const createModalIds = ref<number[]>([])
|
const createModalIds = ref<number[]>([])
|
||||||
|
|
||||||
@ -321,7 +337,8 @@ function handleSelectionChange(rows: any[]) {
|
|||||||
async function showDetail(row: any) {
|
async function showDetail(row: any) {
|
||||||
const id = Number(row._raw?.id || row.id)
|
const id = Number(row._raw?.id || row.id)
|
||||||
if (!id) return
|
if (!id) return
|
||||||
detailDrawerRef.value?.open({
|
const drawer = await ensureStoreDetailDrawer()
|
||||||
|
drawer?.open?.({
|
||||||
id,
|
id,
|
||||||
customerId: withCustomerId({}).customerId,
|
customerId: withCustomerId({}).customerId,
|
||||||
})
|
})
|
||||||
@ -639,15 +656,19 @@ function handleActionClick(row: any, _i: number, action?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const { items } = await getWarehouseList({ page: 1, pageSize: 200 }).catch(() => ({ items: [] }))
|
// 仓库下拉与列表/汇总/产品并行,避免串行等三轮接口
|
||||||
|
const warehouseTask = getWarehouseList({ page: 1, pageSize: 200 })
|
||||||
|
.then(({ items }) => {
|
||||||
warehouseOptions.value = (items || []).map((w: any) => ({
|
warehouseOptions.value = (items || []).map((w: any) => ({
|
||||||
label: w.warehouseName || `#${w.id}`,
|
label: w.warehouseName || `#${w.id}`,
|
||||||
value: Number(w.id),
|
value: Number(w.id),
|
||||||
}))
|
}))
|
||||||
if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value
|
if (warehouseOptions.value.length) matchWarehouseId.value = warehouseOptions.value[0].value
|
||||||
await loadProducts()
|
})
|
||||||
await loadSummary()
|
.catch(() => {
|
||||||
await loadList()
|
warehouseOptions.value = []
|
||||||
|
})
|
||||||
|
await Promise.all([warehouseTask, loadProducts(), loadSummary(), loadList()])
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -790,17 +811,20 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StoreOrderDetailDrawer
|
<StoreOrderDetailDrawer
|
||||||
|
v-if="detailDrawerMounted"
|
||||||
ref="detailDrawerRef"
|
ref="detailDrawerRef"
|
||||||
:warehouse-name-map="warehouseNameMap"
|
:warehouse-name-map="warehouseNameMap"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PullOrdersModal
|
<PullOrdersModal
|
||||||
|
v-if="pullVisible"
|
||||||
v-model:visible="pullVisible"
|
v-model:visible="pullVisible"
|
||||||
:customer-id="withCustomerId({}).customerId"
|
:customer-id="withCustomerId({}).customerId"
|
||||||
@success="() => { loadSummary(); loadList() }"
|
@success="() => { loadSummary(); loadList() }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CreateLogisticsModal
|
<CreateLogisticsModal
|
||||||
|
v-if="createModalVisible"
|
||||||
v-model:visible="createModalVisible"
|
v-model:visible="createModalVisible"
|
||||||
:mode="createModalMode"
|
:mode="createModalMode"
|
||||||
:store-order-ids="createModalIds"
|
:store-order-ids="createModalIds"
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<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 { useRoute, useRouter } from 'vue-router';
|
||||||
import { IconCalendar, IconDown, IconQuestionCircleFill, IconSettings } from '@arco-design/web-vue/es/icon';
|
import { IconCalendar, IconDown, IconQuestionCircleFill, IconSettings } from '@arco-design/web-vue/es/icon';
|
||||||
import { Message, Modal } from '@arco-design/web-vue';
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
@ -47,13 +56,6 @@
|
|||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
|
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow';
|
||||||
import { buildFailGuideFromReason } from '@/utils/parcelCreateFailGuide';
|
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 {
|
import {
|
||||||
buildHotColumns,
|
buildHotColumns,
|
||||||
getActiveScheme,
|
getActiveScheme,
|
||||||
@ -68,7 +70,6 @@
|
|||||||
WAYBILL_MANAGE_DEFAULT_KEYS,
|
WAYBILL_MANAGE_DEFAULT_KEYS,
|
||||||
getWaybillBaseCatalog,
|
getWaybillBaseCatalog,
|
||||||
} from './columnConfig';
|
} from './columnConfig';
|
||||||
import OrderPrintModal from '@/components/PrintTemplate/OrderPrintModal.vue';
|
|
||||||
import {
|
import {
|
||||||
fetchPrintUrlForOrder,
|
fetchPrintUrlForOrder,
|
||||||
fetchPrintUrlsForOrders,
|
fetchPrintUrlsForOrders,
|
||||||
@ -100,6 +101,32 @@
|
|||||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
|
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
|
||||||
import { buildWaybillContextFromRow, openAskAdvisor } from '@/composables/useAskAdvisor';
|
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({
|
defineOptions({
|
||||||
name: 'WaybillManage',
|
name: 'WaybillManage',
|
||||||
});
|
});
|
||||||
@ -108,9 +135,39 @@
|
|||||||
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
||||||
const { customerOptions, withCustomerId } = useWarehouseAdminCustomer();
|
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 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 */
|
/** 兼容后端 camelCase / PascalCase */
|
||||||
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
||||||
@ -274,10 +331,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentOrder.value = nextRow;
|
currentOrder.value = nextRow;
|
||||||
detailDrawerRef.value?.openDrawer({ tab: opts?.tab || 'basic' });
|
const drawer = await ensureDetailDrawer();
|
||||||
|
drawer?.openDrawer?.({ tab: opts?.tab || 'basic' });
|
||||||
if (opts?.editBasic) {
|
if (opts?.editBasic) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
detailDrawerRef.value?.startBasicEdit?.();
|
drawer?.startBasicEdit?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1356,10 +1414,14 @@
|
|||||||
reallocateModalVisible.value = true;
|
reallocateModalVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const downstreamWeightImportModalRef = ref<InstanceType<typeof DownstreamWeightImportModal> | null>(null);
|
async function openDownstreamWeightImport() {
|
||||||
const openDownstreamWeightImport = () => {
|
downstreamWeightImportMounted.value = true;
|
||||||
downstreamWeightImportModalRef.value?.open();
|
const modal = await waitForRef(
|
||||||
};
|
() => downstreamWeightImportModalRef.value,
|
||||||
|
(v) => typeof v.open === 'function',
|
||||||
|
);
|
||||||
|
modal?.open?.();
|
||||||
|
}
|
||||||
|
|
||||||
function resolveClaimContextFromRow(row: any) {
|
function resolveClaimContextFromRow(row: any) {
|
||||||
let hasInsurance: boolean | null = null;
|
let hasInsurance: boolean | null = null;
|
||||||
@ -1487,8 +1549,6 @@
|
|||||||
t('order.myOrders.columnConfig.defaultScheme'),
|
t('order.myOrders.columnConfig.defaultScheme'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const columnConfigModalRef = ref<InstanceType<typeof ColumnConfigModal>>();
|
|
||||||
|
|
||||||
const activeColumnKeys = computed(() => getActiveScheme(columnConfigState.value).keys);
|
const activeColumnKeys = computed(() => getActiveScheme(columnConfigState.value).keys);
|
||||||
|
|
||||||
const hotColumns = computed(() => {
|
const hotColumns = computed(() => {
|
||||||
@ -1549,8 +1609,13 @@
|
|||||||
columnCatalog.value = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
|
columnCatalog.value = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openColumnConfig() {
|
async function openColumnConfig() {
|
||||||
columnConfigModalRef.value?.open();
|
columnConfigMounted.value = true;
|
||||||
|
const modal = await waitForRef(
|
||||||
|
() => columnConfigModalRef.value,
|
||||||
|
(v) => typeof v.open === 'function',
|
||||||
|
);
|
||||||
|
modal?.open?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
|
function handleColumnConfigApply(payload: { activeId: string; schemes: ColumnScheme[] }) {
|
||||||
@ -1730,8 +1795,8 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
await loadOrderList();
|
// 列表与角标并行,避免串行多等一整轮接口
|
||||||
loadTabCounts();
|
await Promise.all([loadOrderList(), loadTabCounts()]);
|
||||||
await tryOpenOrderFromQuery();
|
await tryOpenOrderFromQuery();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -2017,7 +2082,7 @@
|
|||||||
<div ref="tableContainerRef" class="list-wrap">
|
<div ref="tableContainerRef" class="list-wrap">
|
||||||
<a-spin :loading="listLoading" class="table-spin">
|
<a-spin :loading="listLoading" class="table-spin">
|
||||||
<HandsontablePro
|
<HandsontablePro
|
||||||
:key="`${activeTab}-${currentPage}-${orderList.length}-${tableColumnsKey}`"
|
:key="`${activeTab}-${tableColumnsKey}`"
|
||||||
:columns="hotColumns"
|
:columns="hotColumns"
|
||||||
:data="processedTableData"
|
:data="processedTableData"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
@ -2041,13 +2106,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 订单详情抽屉(左侧) -->
|
<!-- 订单详情抽屉(左侧):首次打开再挂载 -->
|
||||||
<OrderDetailDrawer
|
<OrderDetailDrawer
|
||||||
|
v-if="detailDrawerMounted"
|
||||||
ref="detailDrawerRef"
|
ref="detailDrawerRef"
|
||||||
:order="currentOrder"
|
:order="currentOrder"
|
||||||
@updated="handleOrderDetailUpdated"
|
@updated="handleOrderDetailUpdated"
|
||||||
/>
|
/>
|
||||||
<ColumnConfigModal
|
<ColumnConfigModal
|
||||||
|
v-if="columnConfigMounted"
|
||||||
ref="columnConfigModalRef"
|
ref="columnConfigModalRef"
|
||||||
:catalog="columnCatalog"
|
:catalog="columnCatalog"
|
||||||
:schemes="columnConfigState.schemes"
|
:schemes="columnConfigState.schemes"
|
||||||
@ -2056,12 +2123,19 @@
|
|||||||
@apply="handleColumnConfigApply"
|
@apply="handleColumnConfigApply"
|
||||||
/>
|
/>
|
||||||
<OrderPrintModal
|
<OrderPrintModal
|
||||||
|
v-if="singlePrintVisible"
|
||||||
v-model:visible="singlePrintVisible"
|
v-model:visible="singlePrintVisible"
|
||||||
mode="single"
|
mode="single"
|
||||||
:items="singlePrintItems"
|
:items="singlePrintItems"
|
||||||
:preparing="singlePrintPreparing"
|
: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
|
<a-modal
|
||||||
v-model:visible="keywordBatchVisible"
|
v-model:visible="keywordBatchVisible"
|
||||||
@ -2109,11 +2183,13 @@
|
|||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<MarkProblematicModal
|
<MarkProblematicModal
|
||||||
|
v-if="problematicModalVisible"
|
||||||
v-model:visible="problematicModalVisible"
|
v-model:visible="problematicModalVisible"
|
||||||
:order-ids="exceptionOrderIds"
|
:order-ids="exceptionOrderIds"
|
||||||
@success="handleExceptionActionSuccess"
|
@success="handleExceptionActionSuccess"
|
||||||
/>
|
/>
|
||||||
<MarkClaimedModal
|
<MarkClaimedModal
|
||||||
|
v-if="claimedModalVisible"
|
||||||
v-model:visible="claimedModalVisible"
|
v-model:visible="claimedModalVisible"
|
||||||
:order-ids="exceptionOrderIds"
|
:order-ids="exceptionOrderIds"
|
||||||
:has-insurance="claimContext.hasInsurance"
|
:has-insurance="claimContext.hasInsurance"
|
||||||
@ -2122,15 +2198,18 @@
|
|||||||
@success="handleExceptionActionSuccess"
|
@success="handleExceptionActionSuccess"
|
||||||
/>
|
/>
|
||||||
<ReallocateModal
|
<ReallocateModal
|
||||||
|
v-if="reallocateModalVisible"
|
||||||
v-model:visible="reallocateModalVisible"
|
v-model:visible="reallocateModalVisible"
|
||||||
:order-ids="exceptionOrderIds"
|
:order-ids="exceptionOrderIds"
|
||||||
@success="handleExceptionActionSuccess"
|
@success="handleExceptionActionSuccess"
|
||||||
/>
|
/>
|
||||||
<DownstreamWeightImportModal
|
<DownstreamWeightImportModal
|
||||||
|
v-if="downstreamWeightImportMounted"
|
||||||
ref="downstreamWeightImportModalRef"
|
ref="downstreamWeightImportModalRef"
|
||||||
@success="handleExceptionActionSuccess"
|
@success="handleExceptionActionSuccess"
|
||||||
/>
|
/>
|
||||||
<OrderPayConfirmModal
|
<OrderPayConfirmModal
|
||||||
|
v-if="payConfirmVisible"
|
||||||
v-model:visible="payConfirmVisible"
|
v-model:visible="payConfirmVisible"
|
||||||
:orders="payConfirmOrders"
|
:orders="payConfirmOrders"
|
||||||
@success="handlePayConfirmSuccess"
|
@success="handlePayConfirmSuccess"
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="site-nav-tools">
|
<li class="site-nav-tools">
|
||||||
|
<span class="site-nav-tools__glass" aria-hidden="true"></span>
|
||||||
<a
|
<a
|
||||||
v-if="!signedIn"
|
v-if="!signedIn"
|
||||||
class="site-nav-login"
|
class="site-nav-login"
|
||||||
|
|||||||
@ -104,7 +104,10 @@
|
|||||||
<article v-for="item in visibleItems" :key="String(item.id)" class="ai-route-card">
|
<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__head">
|
||||||
<div class="ai-route-card__titles">
|
<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>
|
<h4 class="ai-route-card__name">{{ item.productName }}</h4>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="formatRouteTimeliness(item)" class="ai-route-card__eta">
|
<p v-if="formatRouteTimeliness(item)" class="ai-route-card__eta">
|
||||||
@ -134,10 +137,37 @@
|
|||||||
{{ expanded[String(item.id)] ? copy.collapse : copy.expand }}
|
{{ expanded[String(item.id)] ? copy.collapse : copy.expand }}
|
||||||
</button>
|
</button>
|
||||||
<div class="ai-route-card__actions">
|
<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)">
|
<a class="ai-route-card__reg" :href="officialLoginUrl(destLabel(destFilter, lang) || undefined)">
|
||||||
{{ copy.register }}
|
{{ copy.register }}
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<div v-if="visibleItems.length < filtered.length" class="ai-routes-more">
|
<div v-if="visibleItems.length < filtered.length" class="ai-routes-more">
|
||||||
|
|||||||
@ -62,11 +62,65 @@
|
|||||||
<template v-if="topic === 'pricing'">
|
<template v-if="topic === 'pricing'">
|
||||||
<a class="site-topic-btn is-primary" :href="loginUrl">{{ copy.register }}</a>
|
<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" @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>
|
||||||
<template v-else-if="topic === 'about'">
|
<template v-else-if="topic === 'about'">
|
||||||
<a class="site-topic-btn is-primary" :href="loginUrl">{{ copy.register }}</a>
|
<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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button type="button" class="site-topic-btn is-primary" @click="$emit('wechat')">{{ copy.live }}</button>
|
<button type="button" class="site-topic-btn is-primary" @click="$emit('wechat')">{{ copy.live }}</button>
|
||||||
|
|||||||
@ -371,7 +371,7 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 995;
|
z-index: 995;
|
||||||
border: none;
|
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%);
|
backdrop-filter: blur(6px) saturate(140%);
|
||||||
-webkit-backdrop-filter: blur(6px) saturate(140%);
|
-webkit-backdrop-filter: blur(6px) saturate(140%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--ai-header-h: 72px;
|
--ai-header-h: 72px;
|
||||||
--ai-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
--ai-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
--ai-bg: #f5f4f1;
|
--ai-bg: #f0f3f4;
|
||||||
--ai-bg-soft: #faf8f6;
|
--ai-bg-soft: #faf8f6;
|
||||||
--ai-surface: #ffffff;
|
--ai-surface: #ffffff;
|
||||||
--ai-text: #121820;
|
--ai-text: #121820;
|
||||||
@ -110,10 +110,16 @@ body.command-home #header .container-xl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.command-home #navbar > ul > li.site-nav-tools {
|
body.command-home #navbar > ul > li.site-nav-tools {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 8px;
|
gap: 2px;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.command-home .site-nav-tools__glass {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.command-home .site-nav-dock {
|
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__glass,
|
||||||
body.command-home .site-nav-dock__slider {
|
body.command-home .site-nav-dock__slider,
|
||||||
|
body.command-home .site-nav-tools__glass {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,15 +364,123 @@ body.command-home #navbar .site-nav-dock a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.command-home #navbar > ul > li.site-nav-tools {
|
body.command-home #navbar > ul > li.site-nav-tools {
|
||||||
display: flex;
|
height: 60px;
|
||||||
align-items: center;
|
padding: 5px;
|
||||||
flex-shrink: 0;
|
overflow: hidden;
|
||||||
gap: 8px;
|
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,
|
||||||
body.command-home #navbar .language-toggle-link:focus {
|
body.command-home #navbar .language-toggle-link:focus {
|
||||||
padding: 0 10px 0 8px;
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -462,10 +577,10 @@ body.command-home #navbar .site-nav-login:visited {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 10px 0 8px;
|
|
||||||
border: 1px solid var(--ai-glass-border);
|
border: 1px solid var(--ai-glass-border);
|
||||||
border-radius: var(--ai-radius-sm);
|
border-radius: var(--ai-radius-sm);
|
||||||
background: var(--ai-glass-fill);
|
background: var(--ai-glass-fill);
|
||||||
@ -480,6 +595,26 @@ body.command-home #navbar .site-nav-login:visited {
|
|||||||
color 0.2s var(--ai-ease);
|
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 {
|
body.command-home .language-toggle-icon {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
@ -1561,6 +1696,21 @@ body.command-home .ai-stage-map--eu {
|
|||||||
background: transparent;
|
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 {
|
.site-topic-btn:hover {
|
||||||
filter: brightness(1.03);
|
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);
|
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) {
|
@media (max-width: 640px) {
|
||||||
.site-topic-contact {
|
.site-topic-contact {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
@ -1782,23 +1938,36 @@ body.command-home .ai-stage-map--eu {
|
|||||||
.ai-route-card__titles {
|
.ai-route-card__titles {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: 4px;
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-route-card__group {
|
.ai-route-card__group {
|
||||||
font-size: 0.7rem;
|
flex-shrink: 0;
|
||||||
font-weight: 700;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.04em;
|
font-weight: 600;
|
||||||
color: var(--ai-gold);
|
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 {
|
.ai-route-card__name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.02rem;
|
min-width: 0;
|
||||||
font-weight: 750;
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
color: var(--ai-ink);
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--ai-action);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-route-card__eta {
|
.ai-route-card__eta {
|
||||||
@ -1888,12 +2057,39 @@ body.command-home .ai-stage-map--eu {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
margin-top: auto;
|
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 {
|
.ai-routes-retry {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1909,7 +2105,6 @@ body.command-home .ai-stage-map--eu {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-route-card__ask:hover,
|
|
||||||
.ai-routes-retry:hover {
|
.ai-routes-retry:hover {
|
||||||
filter: brightness(1.06);
|
filter: brightness(1.06);
|
||||||
}
|
}
|
||||||
@ -1919,19 +2114,20 @@ body.command-home .ai-stage-map--eu {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 34px;
|
min-height: 28px;
|
||||||
padding: 0 4px;
|
padding: 0 2px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--ai-action);
|
color: var(--ai-text-3);
|
||||||
font-size: 0.82rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-route-card__reg:hover,
|
.ai-route-card__reg:hover,
|
||||||
.ai-routes-ghost:hover {
|
.ai-routes-ghost:hover {
|
||||||
|
color: var(--ai-action);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2207,14 +2403,6 @@ body.command-home .ai-stage-map--eu {
|
|||||||
display: none;
|
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,
|
||||||
body.command-home #navbar .site-nav-avatar:visited {
|
body.command-home #navbar .site-nav-avatar:visited {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
--kc-secondary: #3d6678;
|
--kc-secondary: #3d6678;
|
||||||
--kc-tertiary: #054f74;
|
--kc-tertiary: #054f74;
|
||||||
--kc-tertiary-rgb: 5, 79, 116;
|
--kc-tertiary-rgb: 5, 79, 116;
|
||||||
--kc-page-bg: #f5f4f1;
|
--kc-page-bg: #f0f3f4;
|
||||||
--kc-neutral: #faf8f6;
|
--kc-neutral: #faf8f6;
|
||||||
--kc-surface: #ffffff;
|
--kc-surface: #ffffff;
|
||||||
--kc-border: #e0d9cc;
|
--kc-border: #e0d9cc;
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||||
import { useI18n } from 'vue-i18n';
|
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 { useRoute } from 'vue-router';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { $baseMessage } from '@/utils';
|
import { $baseMessage } from '@/utils';
|
||||||
@ -13,7 +22,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import { eventBus } from '@/utils/pub';
|
import { eventBus } from '@/utils/pub';
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue';
|
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 { useTrackOrderForm } from './composables/useTrackOrderForm';
|
||||||
import { useTableColumns } from './config/tableColumns';
|
import { useTableColumns } from './config/tableColumns';
|
||||||
import { useTrackOrderData } from './composables/useTrackOrderData';
|
import { useTrackOrderData } from './composables/useTrackOrderData';
|
||||||
@ -21,6 +30,10 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { useContentHeight } from '@/composables/table/useContentHeight';
|
import { useContentHeight } from '@/composables/table/useContentHeight';
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
|
||||||
|
|
||||||
|
const TrackOrderDetailDrawer = defineAsyncComponent(
|
||||||
|
() => import('./components/TrackOrderDetailDrawer.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
const { tableContainerRef, tableHeight } = useTableContainerHeight();
|
||||||
@ -64,14 +77,18 @@ import { useI18n } from 'vue-i18n';
|
|||||||
} from '@/api/TrackOrder/trackOrder';
|
} from '@/api/TrackOrder/trackOrder';
|
||||||
import { getGetList } from '@/api/ApplicationServicesProvider/dictionary';
|
import { getGetList } from '@/api/ApplicationServicesProvider/dictionary';
|
||||||
import { getAppGetList } from '@/api/ApplicationServices/apps';
|
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 { IconEye, IconTool, IconDown } from '@arco-design/web-vue/es/icon';
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue';
|
import PaginationPro from '@/components/PaginationPro/index.vue';
|
||||||
|
|
||||||
|
const TimeAxis = defineAsyncComponent(() => import('./components/TimeAxis.vue'));
|
||||||
|
const TimeZoneConversion = defineAsyncComponent(
|
||||||
|
() => import('./components/TimeZoneConversion.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
defineOptions({ name: 'TrackOrder' });
|
defineOptions({ name: 'TrackOrder' });
|
||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
|
const detailDrawerMounted = ref(false);
|
||||||
|
|
||||||
const [register, modalMethods] = useModal();
|
const [register, modalMethods] = useModal();
|
||||||
const [registerTemplate, templateMethods] = useModal();
|
const [registerTemplate, templateMethods] = useModal();
|
||||||
@ -81,7 +98,22 @@ import { useI18n } from 'vue-i18n';
|
|||||||
|
|
||||||
// 详情抽屉相关状态
|
// 详情抽屉相关状态
|
||||||
const currentDetailRow = ref<any>(null);
|
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 orderId = ref<number | undefined>();
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
@ -777,12 +809,13 @@ import { useI18n } from 'vue-i18n';
|
|||||||
orderId: row.id,
|
orderId: row.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 打开详情抽屉
|
// 打开详情抽屉(首次再挂载分包)
|
||||||
detailDrawerRef.value?.setDrawerProps({
|
const drawer = await ensureTrackDetailDrawer();
|
||||||
|
drawer?.setDrawerProps?.({
|
||||||
content: '订单详情',
|
content: '订单详情',
|
||||||
width: 1200,
|
width: 1200,
|
||||||
});
|
});
|
||||||
detailDrawerRef.value?.openDrawer();
|
drawer?.openDrawer?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭详情抽屉
|
// 关闭详情抽屉
|
||||||
@ -2027,8 +2060,9 @@ import { useI18n } from 'vue-i18n';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalPro>
|
</ModalPro>
|
||||||
<!-- 详情抽屉 -->
|
<!-- 详情抽屉:首次打开再挂载 -->
|
||||||
<TrackOrderDetailDrawer
|
<TrackOrderDetailDrawer
|
||||||
|
v-if="detailDrawerMounted"
|
||||||
ref="detailDrawerRef"
|
ref="detailDrawerRef"
|
||||||
:orderId="currentDetailRow?.id"
|
:orderId="currentDetailRow?.id"
|
||||||
:orderNumber="currentDetailRow?.orderNumber"
|
:orderNumber="currentDetailRow?.orderNumber"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user