feat: 官网纸面对话与登录翻页过渡,波浪更稳,并同步面单打印优化
Some checks failed
Build and Deploy Front / deploy (push) Has been cancelled
Some checks failed
Build and Deploy Front / deploy (push) Has been cancelled
欢迎区/侧栏/聊天槽统一暖纸拟物;跟进改仓储代发;波浪降扰动关视差减少眩晕。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
95918100cf
commit
07b71ca565
268
src/App.vue
268
src/App.vue
@ -1,21 +1,216 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-config-provider :locale="locale">
|
<a-config-provider :locale="locale">
|
||||||
<router-view />
|
<div
|
||||||
|
class="app-portal-stage"
|
||||||
|
:class="{ 'app-portal-stage--flip': isPageFlip }"
|
||||||
|
>
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<Transition
|
||||||
|
:css="false"
|
||||||
|
@leave="onPortalLeave"
|
||||||
|
@enter="onPortalEnter"
|
||||||
|
@after-leave="clearPortalTransition"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
v-if="Component"
|
||||||
|
:key="portalViewKey(route)"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
<GlobalSetting v-if="showGlobalSetting" />
|
<GlobalSetting v-if="showGlobalSetting" />
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
|
||||||
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
|
||||||
import useLocale from '@/hooks/locale';
|
import useLocale from '@/hooks/locale';
|
||||||
import { isLoginRouteName } from '@/utils/login-portal';
|
import { isLoginRouteName } from '@/utils/login-portal';
|
||||||
|
import { consumePageFlip } from '@/utils/page-transition-flag';
|
||||||
import GlobalSetting from '@/components/global-setting/index.vue';
|
import GlobalSetting from '@/components/global-setting/index.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
/** '' | 'oms-page-flip' */
|
||||||
|
const portalTransition = ref('');
|
||||||
|
const isPageFlip = computed(() => portalTransition.value === 'oms-page-flip');
|
||||||
|
|
||||||
|
function clearPortalTransition() {
|
||||||
|
portalTransition.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用顶层路由名作 key,避免业务页切路由时整页 remount */
|
||||||
|
function portalViewKey(r: RouteLocationNormalizedLoaded) {
|
||||||
|
const top = r.matched[0];
|
||||||
|
if (top?.name) return String(top.name);
|
||||||
|
return top?.path || r.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSheet(node: HTMLElement, z: number) {
|
||||||
|
node.style.position = 'absolute';
|
||||||
|
node.style.inset = '0';
|
||||||
|
node.style.width = '100%';
|
||||||
|
node.style.minHeight = '100%';
|
||||||
|
node.style.zIndex = String(z);
|
||||||
|
node.style.transformStyle = 'preserve-3d';
|
||||||
|
node.style.backfaceVisibility = 'hidden';
|
||||||
|
node.style.willChange = 'transform, opacity, filter';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSheetStyles(node: HTMLElement) {
|
||||||
|
node.style.position = '';
|
||||||
|
node.style.inset = '';
|
||||||
|
node.style.width = '';
|
||||||
|
node.style.minHeight = '';
|
||||||
|
node.style.zIndex = '';
|
||||||
|
node.style.transformStyle = '';
|
||||||
|
node.style.backfaceVisibility = '';
|
||||||
|
node.style.willChange = '';
|
||||||
|
node.style.transform = '';
|
||||||
|
node.style.transformOrigin = '';
|
||||||
|
node.style.filter = '';
|
||||||
|
node.style.opacity = '';
|
||||||
|
node.style.boxShadow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真翻书:书脊在左固定,右上角往左掀。
|
||||||
|
* 多段缓动;新页在底下同步亮起。
|
||||||
|
*/
|
||||||
|
function playBookFlipLeave(node: HTMLElement, done: () => void) {
|
||||||
|
prepareSheet(node, 4);
|
||||||
|
node.style.transformOrigin = '0% 22%';
|
||||||
|
node.style.boxShadow = '-8px 12px 28px rgba(8, 18, 28, 0.16)';
|
||||||
|
|
||||||
|
const shade = document.createElement('div');
|
||||||
|
shade.className = 'oms-flip-shade';
|
||||||
|
node.appendChild(shade);
|
||||||
|
|
||||||
|
const anim = node.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
transform: 'rotateY(0deg)',
|
||||||
|
filter: 'brightness(1)',
|
||||||
|
opacity: 1,
|
||||||
|
boxShadow: '-6px 10px 24px rgba(8, 18, 28, 0.12)',
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'rotateY(-8deg)',
|
||||||
|
filter: 'brightness(0.99)',
|
||||||
|
opacity: 1,
|
||||||
|
boxShadow: '-10px 12px 28px rgba(8, 18, 28, 0.16)',
|
||||||
|
offset: 0.18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'rotateY(-28deg)',
|
||||||
|
filter: 'brightness(0.97)',
|
||||||
|
opacity: 1,
|
||||||
|
boxShadow: '-16px 14px 34px rgba(8, 18, 28, 0.2)',
|
||||||
|
offset: 0.36,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'rotateY(-58deg)',
|
||||||
|
filter: 'brightness(0.94)',
|
||||||
|
opacity: 1,
|
||||||
|
boxShadow: '-22px 12px 40px rgba(8, 18, 28, 0.24)',
|
||||||
|
offset: 0.55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'rotateY(-98deg)',
|
||||||
|
filter: 'brightness(0.9)',
|
||||||
|
opacity: 0.92,
|
||||||
|
boxShadow: '-26px 10px 44px rgba(8, 18, 28, 0.26)',
|
||||||
|
offset: 0.74,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'rotateY(-138deg)',
|
||||||
|
filter: 'brightness(0.84)',
|
||||||
|
opacity: 0.7,
|
||||||
|
boxShadow: '-18px 8px 32px rgba(8, 18, 28, 0.18)',
|
||||||
|
offset: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'rotateY(-162deg)',
|
||||||
|
filter: 'brightness(0.78)',
|
||||||
|
opacity: 0.28,
|
||||||
|
boxShadow: '-6px 6px 18px rgba(8, 18, 28, 0.1)',
|
||||||
|
offset: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 1480,
|
||||||
|
easing: 'cubic-bezier(0.33, 0.0, 0.2, 1)',
|
||||||
|
fill: 'forwards',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const shadeAnim = shade.animate(
|
||||||
|
[
|
||||||
|
{ opacity: 0, offset: 0 },
|
||||||
|
{ opacity: 0.25, offset: 0.18 },
|
||||||
|
{ opacity: 0.55, offset: 0.45 },
|
||||||
|
{ opacity: 0.7, offset: 0.7 },
|
||||||
|
{ opacity: 0.2, offset: 1 },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 1480,
|
||||||
|
easing: 'cubic-bezier(0.33, 0.0, 0.2, 1)',
|
||||||
|
fill: 'forwards',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Promise.all([anim.finished, shadeAnim.finished])
|
||||||
|
.catch(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
shade.remove();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function playBookFlipEnter(node: HTMLElement, done: () => void) {
|
||||||
|
prepareSheet(node, 1);
|
||||||
|
const anim = node.animate(
|
||||||
|
[
|
||||||
|
{ opacity: 0.88, filter: 'brightness(0.9)', offset: 0 },
|
||||||
|
{ opacity: 0.94, filter: 'brightness(0.94)', offset: 0.45 },
|
||||||
|
{ opacity: 1, filter: 'brightness(1)', offset: 1 },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 1480,
|
||||||
|
easing: 'cubic-bezier(0.33, 0.0, 0.2, 1)',
|
||||||
|
fill: 'forwards',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
anim.finished
|
||||||
|
.catch(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
clearSheetStyles(node);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPortalLeave(el: Element, done: () => void) {
|
||||||
|
if (portalTransition.value !== 'oms-page-flip') {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playBookFlipLeave(el as HTMLElement, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPortalEnter(el: Element, done: () => void) {
|
||||||
|
if (portalTransition.value !== 'oms-page-flip') {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playBookFlipEnter(el as HTMLElement, done);
|
||||||
|
}
|
||||||
|
|
||||||
function isStandalonePortalPath(path: string) {
|
function isStandalonePortalPath(path: string) {
|
||||||
return (
|
return (
|
||||||
@ -40,6 +235,29 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isPcLoginRouteName(name: unknown) {
|
||||||
|
return name === 'login' || name === 'adminLogin';
|
||||||
|
}
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
const armed = consumePageFlip();
|
||||||
|
const siteToLogin =
|
||||||
|
from.name === 'siteRoot' &&
|
||||||
|
(to.name === 'login' || to.name === 'H5Login') &&
|
||||||
|
to.query.from === 'official-site';
|
||||||
|
const loginPortalSwitch =
|
||||||
|
isPcLoginRouteName(from.name) &&
|
||||||
|
isPcLoginRouteName(to.name) &&
|
||||||
|
from.name !== to.name;
|
||||||
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
if ((armed || siteToLogin || loginPortalSwitch) && !reduceMotion) {
|
||||||
|
portalTransition.value = 'oms-page-flip';
|
||||||
|
} else {
|
||||||
|
portalTransition.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { currentLocale } = useLocale();
|
const { currentLocale } = useLocale();
|
||||||
const locale = computed(() => {
|
const locale = computed(() => {
|
||||||
switch (currentLocale.value) {
|
switch (currentLocale.value) {
|
||||||
@ -59,7 +277,6 @@
|
|||||||
background: var(--app-sidebar-bg) !important;
|
background: var(--app-sidebar-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 针对Vue scoped样式的强制覆盖 */
|
|
||||||
[data-v-eb552580].main-menu {
|
[data-v-eb552580].main-menu {
|
||||||
background: var(--app-sidebar-bg) !important;
|
background: var(--app-sidebar-bg) !important;
|
||||||
}
|
}
|
||||||
@ -72,18 +289,15 @@ div.main-menu[data-v-eb552580] {
|
|||||||
background: var(--app-sidebar-bg) !important;
|
background: var(--app-sidebar-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 使用最高优先级的选择器 */
|
|
||||||
html body .main-menu {
|
html body .main-menu {
|
||||||
background: var(--app-sidebar-bg) !important;
|
background: var(--app-sidebar-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 主菜单文字样式覆盖 */
|
|
||||||
.main-menu .menu-text {
|
.main-menu .menu-text {
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 针对Vue scoped样式的文字覆盖 */
|
|
||||||
[data-v-eb552580] .menu-text {
|
[data-v-eb552580] .menu-text {
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
@ -94,9 +308,45 @@ html body .main-menu {
|
|||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 最高优先级选择器 */
|
|
||||||
html body .main-menu .menu-text {
|
html body .main-menu .menu-text {
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-portal-stage {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-portal-stage--flip {
|
||||||
|
position: relative;
|
||||||
|
perspective: 2600px;
|
||||||
|
perspective-origin: 78% 16%;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
background: #0b1620;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oms-flip-shade {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(0, 0, 0, 0.28) 0%,
|
||||||
|
rgba(0, 0, 0, 0.1) 14%,
|
||||||
|
rgba(0, 0, 0, 0.02) 28%,
|
||||||
|
transparent 46%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
215deg,
|
||||||
|
rgba(255, 255, 255, 0.22) 0%,
|
||||||
|
rgba(255, 255, 255, 0.05) 18%,
|
||||||
|
transparent 42%
|
||||||
|
);
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -110,6 +110,8 @@
|
|||||||
onSelectionChange?: (selectedRows: any[]) => void;
|
onSelectionChange?: (selectedRows: any[]) => void;
|
||||||
// 自定义单元格渲染器
|
// 自定义单元格渲染器
|
||||||
customCellRenderer?: (row: number, col: number, value: any, rowData: any) => string | null;
|
customCellRenderer?: (row: number, col: number, value: any, rowData: any) => string | null;
|
||||||
|
/** 行 class(如状态底色),避免依赖 :has() 在虚拟滚动时闪烁 */
|
||||||
|
resolveRowClassName?: (rowData: any, rowIndex: number) => string | string[] | null | undefined;
|
||||||
// 高度
|
// 高度
|
||||||
height?: number;
|
height?: number;
|
||||||
// 是否只读
|
// 是否只读
|
||||||
@ -120,6 +122,11 @@
|
|||||||
onOpenMultiValueModal?: (field: string, label: string) => void;
|
onOpenMultiValueModal?: (field: string, label: string) => void;
|
||||||
// 列是否拉伸铺满容器宽度
|
// 列是否拉伸铺满容器宽度
|
||||||
stretchColumns?: boolean;
|
stretchColumns?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否对 props.data 做 deep watch。
|
||||||
|
* 只读列表整表替换时建议 false,避免 shallow + deep 双刷 loadData。
|
||||||
|
*/
|
||||||
|
deepWatchData?: boolean;
|
||||||
// 固定行高(像素),设置后禁用 autoRowSize
|
// 固定行高(像素),设置后禁用 autoRowSize
|
||||||
rowHeight?: number;
|
rowHeight?: number;
|
||||||
// 是否启用“序号表头-设置排序”入口
|
// 是否启用“序号表头-设置排序”入口
|
||||||
@ -167,6 +174,7 @@
|
|||||||
multiValueFields: () => [],
|
multiValueFields: () => [],
|
||||||
height: undefined,
|
height: undefined,
|
||||||
stretchColumns: false,
|
stretchColumns: false,
|
||||||
|
deepWatchData: true,
|
||||||
rowHeight: undefined,
|
rowHeight: undefined,
|
||||||
enableColumnOrderConfig: false,
|
enableColumnOrderConfig: false,
|
||||||
enableColumnReorder: false,
|
enableColumnReorder: false,
|
||||||
@ -319,6 +327,13 @@
|
|||||||
checkbox: CHECKBOX_COL_WIDTH,
|
checkbox: CHECKBOX_COL_WIDTH,
|
||||||
};
|
};
|
||||||
let remasuringActionWidth = false;
|
let remasuringActionWidth = false;
|
||||||
|
/** 操作列宽按按钮方案缓存,避免 afterRender 反复量 DOM */
|
||||||
|
let actionWidthSchemeKey = '';
|
||||||
|
let actionWidthCalibrated = false;
|
||||||
|
/** 滚动中跳过 afterRender 重活,停滚后再补一次,减轻闪白 */
|
||||||
|
let tableScrolling = false;
|
||||||
|
let scrollIdleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const ROW_STATUS_CLASS_RE = /\brow-status-\S+/g;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 固定列序:操作(可选) → 序号 → 复选框(可选) → 数据列
|
* 固定列序:操作(可选) → 序号 → 复选框(可选) → 数据列
|
||||||
@ -414,14 +429,30 @@
|
|||||||
return padX + buttons * btnSize + Math.max(0, buttons - 1) * gap;
|
return padX + buttons * btnSize + Math.max(0, buttons - 1) * gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActionWidthSchemeKey(): string {
|
||||||
|
if (!props.actionRenderer) return 'none';
|
||||||
|
const html = props.data.length
|
||||||
|
? props.actionRenderer(props.data[0], 0) || ''
|
||||||
|
: props.actionRenderer({}, 0) || '';
|
||||||
|
const actions = [...html.matchAll(/data-action=["']([^"']+)["']/g)].map((m) => m[1]);
|
||||||
|
if (actions.length) return actions.join('|');
|
||||||
|
return `btn:${countActionButtonsInHtml(html)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** 按操作按钮内容自适应操作列宽 */
|
/** 按操作按钮内容自适应操作列宽 */
|
||||||
function resolveActionColumnWidth(): number {
|
function resolveActionColumnWidth(): number {
|
||||||
let maxWidth = 48;
|
let maxWidth = 48;
|
||||||
if (!props.actionRenderer) {
|
if (!props.actionRenderer) {
|
||||||
layoutFixedWidths.action = maxWidth;
|
layoutFixedWidths.action = maxWidth;
|
||||||
|
actionWidthSchemeKey = 'none';
|
||||||
|
actionWidthCalibrated = true;
|
||||||
return maxWidth;
|
return maxWidth;
|
||||||
}
|
}
|
||||||
const sampleLen = Math.min(Math.max(props.data.length, 1), 40);
|
const scheme = getActionWidthSchemeKey();
|
||||||
|
if (scheme === actionWidthSchemeKey && layoutFixedWidths.action >= 48) {
|
||||||
|
return layoutFixedWidths.action;
|
||||||
|
}
|
||||||
|
const sampleLen = Math.min(Math.max(props.data.length, 1), 8);
|
||||||
const samples: string[] = [];
|
const samples: string[] = [];
|
||||||
if (!props.data.length) {
|
if (!props.data.length) {
|
||||||
samples.push(props.actionRenderer({}, 0) || '');
|
samples.push(props.actionRenderer({}, 0) || '');
|
||||||
@ -438,6 +469,8 @@
|
|||||||
});
|
});
|
||||||
maxWidth = Math.min(Math.max(48, Math.ceil(maxWidth)), 420);
|
maxWidth = Math.min(Math.max(48, Math.ceil(maxWidth)), 420);
|
||||||
layoutFixedWidths.action = maxWidth;
|
layoutFixedWidths.action = maxWidth;
|
||||||
|
actionWidthSchemeKey = scheme;
|
||||||
|
actionWidthCalibrated = false;
|
||||||
return maxWidth;
|
return maxWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,9 +483,11 @@
|
|||||||
return stretchedWidth;
|
return stretchedWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 渲染后用真实单元格再校准操作列宽(覆盖页面自定义 CSS) */
|
/** 渲染后用真实单元格再校准操作列宽(覆盖页面自定义 CSS);同操作方案只校准一次 */
|
||||||
function remasureActionColumnFromDom() {
|
function remasureActionColumnFromDom() {
|
||||||
if (!hotInstance || !props.showActionColumn || remasuringActionWidth) return;
|
if (!hotInstance || !props.showActionColumn || remasuringActionWidth) return;
|
||||||
|
const scheme = getActionWidthSchemeKey();
|
||||||
|
if (actionWidthCalibrated && scheme === actionWidthSchemeKey) return;
|
||||||
const { actionColIndex } = getLayoutIndices();
|
const { actionColIndex } = getLayoutIndices();
|
||||||
if (actionColIndex < 0 || !hotContainer.value) return;
|
if (actionColIndex < 0 || !hotContainer.value) return;
|
||||||
const cells = hotContainer.value.querySelectorAll('td.ht-action-cell');
|
const cells = hotContainer.value.querySelectorAll('td.ht-action-cell');
|
||||||
@ -484,6 +519,8 @@
|
|||||||
});
|
});
|
||||||
if (maxContent <= 0) return;
|
if (maxContent <= 0) return;
|
||||||
const next = Math.min(Math.max(48, maxContent + 16), 420);
|
const next = Math.min(Math.max(48, maxContent + 16), 420);
|
||||||
|
actionWidthSchemeKey = scheme;
|
||||||
|
actionWidthCalibrated = true;
|
||||||
if (Math.abs(next - layoutFixedWidths.action) < 4) return;
|
if (Math.abs(next - layoutFixedWidths.action) < 4) return;
|
||||||
remasuringActionWidth = true;
|
remasuringActionWidth = true;
|
||||||
layoutFixedWidths.action = next;
|
layoutFixedWidths.action = next;
|
||||||
@ -1497,8 +1534,9 @@
|
|||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
observeDOMVisibility: true,
|
observeDOMVisibility: true,
|
||||||
renderAllRows: false,
|
renderAllRows: false,
|
||||||
viewportColumnRenderingOffset: 'auto',
|
// 加大视口外预渲染,滚动时少出现「空一下再出字」
|
||||||
viewportRowRenderingOffset: 'auto',
|
viewportColumnRenderingOffset: 8,
|
||||||
|
viewportRowRenderingOffset: 40,
|
||||||
// 强制启用滚动条
|
// 强制启用滚动条
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
// 确保滚动条可见
|
// 确保滚动条可见
|
||||||
@ -1750,6 +1788,18 @@
|
|||||||
td.classList.remove('ht-select-cell', 'ht-select-cell--empty', 'ht-editable-cell');
|
td.classList.remove('ht-select-cell', 'ht-select-cell--empty', 'ht-editable-cell');
|
||||||
|
|
||||||
const layout = getLayoutIndices();
|
const layout = getLayoutIndices();
|
||||||
|
const applyRowClass = (rowIndex: number) => {
|
||||||
|
if (!props.resolveRowClassName || rowIndex < 0) return;
|
||||||
|
const tr = td.parentElement as HTMLTableRowElement | null;
|
||||||
|
if (!tr) return;
|
||||||
|
const rowData = rowAt(rowIndex);
|
||||||
|
const next = props.resolveRowClassName(rowData, rowIndex);
|
||||||
|
const classes = (Array.isArray(next) ? next : [next])
|
||||||
|
.map((x) => String(x || '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const cleaned = (tr.className || '').replace(ROW_STATUS_CLASS_RE, '').replace(/\s+/g, ' ').trim();
|
||||||
|
tr.className = classes.length ? `${cleaned} ${classes.join(' ')}`.trim() : cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
// 操作列(永远第一列,居中)
|
// 操作列(永远第一列,居中)
|
||||||
if (col === layout.actionColIndex && row >= 0 && row < props.data.length) {
|
if (col === layout.actionColIndex && row >= 0 && row < props.data.length) {
|
||||||
@ -1766,6 +1816,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
applyRowClass(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1776,6 +1827,8 @@
|
|||||||
td.innerHTML = value || '';
|
td.innerHTML = value || '';
|
||||||
td.style.color = '#8c8c8c';
|
td.style.color = '#8c8c8c';
|
||||||
td.style.fontWeight = '500';
|
td.style.fontWeight = '500';
|
||||||
|
// 无操作列时由序号列挂行 class
|
||||||
|
if (layout.actionColIndex < 0) applyRowClass(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2036,34 +2089,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
afterScrollVertically: markTableScrolling,
|
||||||
|
afterScrollHorizontally: markTableScrolling,
|
||||||
afterRender: function(isForced: boolean) {
|
afterRender: function(isForced: boolean) {
|
||||||
if (!hotInstance) return;
|
if (!hotInstance) return;
|
||||||
ensureHorizontalScrollbarVisible();
|
// 滚动过程中的增量 render:只保选中态,避免反复量 DOM / 同步表头造成闪白
|
||||||
|
if (!isForced && tableScrolling) {
|
||||||
// 移除操作列和序号列的筛选图标(这些列不需要筛选功能)
|
applySelectedRowStyles();
|
||||||
const removeFilterIconFromColumns = () => {
|
return;
|
||||||
const headerRows = hotContainer.value?.querySelectorAll('.ht_clone_top thead tr, .ht_master thead tr');
|
}
|
||||||
if (!headerRows?.length) return;
|
runAfterRenderMaintenance();
|
||||||
const { actionColIndex, serialColIndex, checkboxColIndex } = getLayoutIndices();
|
|
||||||
const noFilterCols = new Set(
|
|
||||||
[checkboxColIndex, actionColIndex, serialColIndex].filter((i) => i >= 0)
|
|
||||||
);
|
|
||||||
headerRows.forEach((row: Element) => {
|
|
||||||
const ths = row.querySelectorAll('th');
|
|
||||||
ths.forEach((th: Element, index: number) => {
|
|
||||||
if (noFilterCols.has(index)) {
|
|
||||||
const btn = th.querySelector('.changeType');
|
|
||||||
if (btn?.parentElement) btn.parentElement.removeChild(btn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
removeFilterIconFromColumns();
|
|
||||||
syncHybridHeaders();
|
|
||||||
applySelectedRowStyles();
|
|
||||||
remasureActionColumnFromDom();
|
|
||||||
// remasure/updateSettings 后 HOT 会按原生滚动条再次缩短 clone,需再对齐一次
|
|
||||||
scheduleCloneOverlaySync();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2077,6 +2112,44 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function runAfterRenderMaintenance() {
|
||||||
|
if (!hotInstance) return;
|
||||||
|
ensureHorizontalScrollbarVisible();
|
||||||
|
|
||||||
|
// 移除操作列和序号列的筛选图标(这些列不需要筛选功能)
|
||||||
|
const headerRows = hotContainer.value?.querySelectorAll('.ht_clone_top thead tr, .ht_master thead tr');
|
||||||
|
if (headerRows?.length) {
|
||||||
|
const { actionColIndex, serialColIndex, checkboxColIndex } = getLayoutIndices();
|
||||||
|
const noFilterCols = new Set(
|
||||||
|
[checkboxColIndex, actionColIndex, serialColIndex].filter((i) => i >= 0),
|
||||||
|
);
|
||||||
|
headerRows.forEach((row: Element) => {
|
||||||
|
const ths = row.querySelectorAll('th');
|
||||||
|
ths.forEach((th: Element, index: number) => {
|
||||||
|
if (noFilterCols.has(index)) {
|
||||||
|
const btn = th.querySelector('.changeType');
|
||||||
|
if (btn?.parentElement) btn.parentElement.removeChild(btn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
syncHybridHeaders();
|
||||||
|
applySelectedRowStyles();
|
||||||
|
remasureActionColumnFromDom();
|
||||||
|
// remasure/updateSettings 后 HOT 会按原生滚动条再次缩短 clone,需再对齐一次
|
||||||
|
scheduleCloneOverlaySync();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markTableScrolling() {
|
||||||
|
tableScrolling = true;
|
||||||
|
if (scrollIdleTimer) clearTimeout(scrollIdleTimer);
|
||||||
|
scrollIdleTimer = setTimeout(() => {
|
||||||
|
tableScrolling = false;
|
||||||
|
scrollIdleTimer = null;
|
||||||
|
runAfterRenderMaintenance();
|
||||||
|
}, 140);
|
||||||
|
}
|
||||||
|
|
||||||
const ensureHorizontalScrollbarVisible = () => {
|
const ensureHorizontalScrollbarVisible = () => {
|
||||||
if (!hotContainer.value) return;
|
if (!hotContainer.value) return;
|
||||||
const holder = hotContainer.value.querySelector('.ht_master .wtHolder') as HTMLElement | null;
|
const holder = hotContainer.value.querySelector('.ht_master .wtHolder') as HTMLElement | null;
|
||||||
@ -2541,7 +2614,7 @@
|
|||||||
* 数据刷新策略:
|
* 数据刷新策略:
|
||||||
* - 可编辑表:afterChange 会写回行对象;若 deep watch,上千行一改一格就全表 loadData → 卡死。
|
* - 可编辑表:afterChange 会写回行对象;若 deep watch,上千行一改一格就全表 loadData → 卡死。
|
||||||
* 因此只在数组引用/长度变化时刷新。
|
* 因此只在数组引用/长度变化时刷新。
|
||||||
* - 只读表:仍 deep watch,兼容外部原地改字段后的展示刷新。
|
* - 只读表:默认 deep watch;列表页可传 deepWatchData=false,整表替换时避免双刷。
|
||||||
*/
|
*/
|
||||||
watch(
|
watch(
|
||||||
() => [props.data, props.data?.length] as const,
|
() => [props.data, props.data?.length] as const,
|
||||||
@ -2554,6 +2627,7 @@
|
|||||||
() => props.data,
|
() => props.data,
|
||||||
() => {
|
() => {
|
||||||
if (props.readOnly === false) return;
|
if (props.readOnly === false) return;
|
||||||
|
if (props.deepWatchData === false) return;
|
||||||
refreshHandsontableData();
|
refreshHandsontableData();
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
@ -2713,6 +2787,11 @@
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
pauseHeaderSearchEmit();
|
pauseHeaderSearchEmit();
|
||||||
|
if (scrollIdleTimer) {
|
||||||
|
clearTimeout(scrollIdleTimer);
|
||||||
|
scrollIdleTimer = null;
|
||||||
|
}
|
||||||
|
tableScrolling = false;
|
||||||
if (resizeObserver && containerRef.value) {
|
if (resizeObserver && containerRef.value) {
|
||||||
try {
|
try {
|
||||||
resizeObserver.unobserve(containerRef.value);
|
resizeObserver.unobserve(containerRef.value);
|
||||||
|
|||||||
@ -10,8 +10,24 @@
|
|||||||
>
|
>
|
||||||
<div class="print-modal-body">
|
<div class="print-modal-body">
|
||||||
<div :class="['print-status-bar', `print-status-bar--${printerStatusTone}`]">
|
<div :class="['print-status-bar', `print-status-bar--${printerStatusTone}`]">
|
||||||
<span class="print-status-dot" />
|
<div class="print-status-bar__left">
|
||||||
<span class="print-status-text">{{ printerStatusText }}</span>
|
<span class="print-status-dot" />
|
||||||
|
<span class="print-status-text">{{ printerStatusText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="print-status-bar__right">
|
||||||
|
<a-select
|
||||||
|
v-model="selectedPrinter"
|
||||||
|
class="print-printer-select"
|
||||||
|
size="mini"
|
||||||
|
allow-search
|
||||||
|
:loading="printerListLoading"
|
||||||
|
:disabled="!canSelectPrinter"
|
||||||
|
:options="printerSelectOptions"
|
||||||
|
placeholder="默认打印机"
|
||||||
|
@change="onPrinterChange"
|
||||||
|
@popup-visible-change="onPrinterPopupVisible"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="print-options-card">
|
<div class="print-options-card">
|
||||||
@ -51,6 +67,27 @@
|
|||||||
>
|
>
|
||||||
查看面单
|
查看面单
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<span
|
||||||
|
v-if="getItemPrintResult(item.id) === 'success'"
|
||||||
|
class="batch-list-item-result is-success"
|
||||||
|
title="打印成功"
|
||||||
|
>
|
||||||
|
<icon-check :size="16" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="getItemPrintResult(item.id) === 'fail'"
|
||||||
|
class="batch-list-item-result is-fail"
|
||||||
|
title="打印失败"
|
||||||
|
>
|
||||||
|
<icon-close :size="16" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="getItemPrintResult(item.id) === 'printing'"
|
||||||
|
class="batch-list-item-result is-printing"
|
||||||
|
title="打印中"
|
||||||
|
>
|
||||||
|
<icon-loading :size="14" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<a-empty v-if="!listItems.length" description="暂无可打印内容" />
|
<a-empty v-if="!listItems.length" description="暂无可打印内容" />
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +111,7 @@
|
|||||||
:loading="printingAll"
|
:loading="printingAll"
|
||||||
@click="printAll"
|
@click="printAll"
|
||||||
>
|
>
|
||||||
{{ printingAll ? '发送中…' : '全部打印' }}
|
{{ printingAll ? '打印中…' : '全部打印' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -90,13 +127,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { IconCheck, IconClose, IconLoading } from '@arco-design/web-vue/es/icon';
|
||||||
import {
|
import {
|
||||||
fetchHtmlTextForHiprint,
|
fetchHtmlTextForHiprint,
|
||||||
fetchPdfBytesForHiprint,
|
fetchPdfBytesForHiprint,
|
||||||
|
hiprintRefreshPrinterList,
|
||||||
hiprintSendBlobPdf,
|
hiprintSendBlobPdf,
|
||||||
hiprintSendHtmlPrint,
|
hiprintSendHtmlPrint,
|
||||||
|
type HiprintPrinterInfo,
|
||||||
} from '@/utils/hiprint/hiprintElectronSocket';
|
} from '@/utils/hiprint/hiprintElectronSocket';
|
||||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||||
@ -106,7 +146,9 @@ import {
|
|||||||
buildHiprintBlobPrintExtras,
|
buildHiprintBlobPrintExtras,
|
||||||
HIPRINT_PAPER_PRESET_OPTIONS,
|
HIPRINT_PAPER_PRESET_OPTIONS,
|
||||||
readHiprintPrintFormFromStorage,
|
readHiprintPrintFormFromStorage,
|
||||||
|
readHiprintSelectedPrinterFromStorage,
|
||||||
writeHiprintPrintFormToStorage,
|
writeHiprintPrintFormToStorage,
|
||||||
|
writeHiprintSelectedPrinterToStorage,
|
||||||
} from '@/utils/hiprint/hiprintPrintOptions';
|
} from '@/utils/hiprint/hiprintPrintOptions';
|
||||||
|
|
||||||
interface PrintItem {
|
interface PrintItem {
|
||||||
@ -115,6 +157,8 @@ interface PrintItem {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemPrintResult = 'idle' | 'printing' | 'success' | 'fail';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -142,7 +186,31 @@ const advancedOpen = ref(false);
|
|||||||
const printingActive = ref(false);
|
const printingActive = ref(false);
|
||||||
const printingAll = ref(false);
|
const printingAll = ref(false);
|
||||||
const printJustSucceeded = ref(false);
|
const printJustSucceeded = ref(false);
|
||||||
|
const printerList = ref<HiprintPrinterInfo[]>([]);
|
||||||
|
const printerListLoading = ref(false);
|
||||||
|
const selectedPrinter = ref(readHiprintSelectedPrinterFromStorage());
|
||||||
|
/** 批量列表逐票打印结果:idle | printing | success | fail */
|
||||||
|
const itemPrintResultMap = reactive<Record<string, ItemPrintResult>>({});
|
||||||
let printSuccessTimer: ReturnType<typeof setTimeout> | null = null;
|
let printSuccessTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let printerListReqId = 0;
|
||||||
|
|
||||||
|
function itemResultKey(id: number | string) {
|
||||||
|
return String(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemPrintResult(id: number | string): ItemPrintResult {
|
||||||
|
return itemPrintResultMap[itemResultKey(id)] || 'idle';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemPrintResult(id: number | string, result: ItemPrintResult) {
|
||||||
|
itemPrintResultMap[itemResultKey(id)] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetItemPrintResults() {
|
||||||
|
Object.keys(itemPrintResultMap).forEach((key) => {
|
||||||
|
delete itemPrintResultMap[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const listItems = computed(() => props.items || []);
|
const listItems = computed(() => props.items || []);
|
||||||
const printableItems = computed(() => listItems.value.filter((x) => !!String(x?.url || '').trim()));
|
const printableItems = computed(() => listItems.value.filter((x) => !!String(x?.url || '').trim()));
|
||||||
@ -170,7 +238,7 @@ const printerStatusTone = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const printerStatusText = computed(() => {
|
const printerStatusText = computed(() => {
|
||||||
if (printingActive.value || printingAll.value) return '正在发送打印任务…';
|
if (printingActive.value || printingAll.value) return '正在打印,等待客户端回执…';
|
||||||
if (props.preparing) return '正在获取面单文件…';
|
if (props.preparing) return '正在获取面单文件…';
|
||||||
if (hiprintClientState.connecting) return '正在连接本机打印程序…';
|
if (hiprintClientState.connecting) return '正在连接本机打印程序…';
|
||||||
if (hiprintClientState.connected) return '本机打印已就绪,可一键自动打印';
|
if (hiprintClientState.connected) return '本机打印已就绪,可一键自动打印';
|
||||||
@ -180,6 +248,31 @@ const printerStatusText = computed(() => {
|
|||||||
|
|
||||||
const footerBusy = computed(() => printingActive.value || printingAll.value);
|
const footerBusy = computed(() => printingActive.value || printingAll.value);
|
||||||
|
|
||||||
|
const canSelectPrinter = computed(
|
||||||
|
() => hiprintClientState.enabled && hiprintClientState.connected && !footerBusy.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const printerSelectOptions = computed(() =>
|
||||||
|
printerList.value.map((p) => ({
|
||||||
|
value: p.name,
|
||||||
|
label: p.isDefault ? `${p.displayName || p.name}(默认)` : p.displayName || p.name,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
function pickDefaultPrinterName(list: HiprintPrinterInfo[]) {
|
||||||
|
return list.find((p) => p.isDefault)?.name || list[0]?.name || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPrinterSelection(list: HiprintPrinterInfo[]) {
|
||||||
|
const current = selectedPrinter.value;
|
||||||
|
const next =
|
||||||
|
current && list.some((p) => p.name === current) ? current : pickDefaultPrinterName(list);
|
||||||
|
if (next !== current) {
|
||||||
|
selectedPrinter.value = next;
|
||||||
|
}
|
||||||
|
if (next) writeHiprintSelectedPrinterToStorage(next);
|
||||||
|
}
|
||||||
|
|
||||||
const canViewLabel = computed(() => !!activeItem.value?.url && !props.preparing);
|
const canViewLabel = computed(() => !!activeItem.value?.url && !props.preparing);
|
||||||
|
|
||||||
const canPrint = computed(
|
const canPrint = computed(
|
||||||
@ -191,8 +284,8 @@ const canPrint = computed(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const printActiveLabel = computed(() => {
|
const printActiveLabel = computed(() => {
|
||||||
if (printJustSucceeded.value) return '已发送';
|
if (printJustSucceeded.value) return '已打印';
|
||||||
if (printingActive.value) return '发送中…';
|
if (printingActive.value) return '打印中…';
|
||||||
return props.mode === 'batch' ? '打印当前' : '打印';
|
return props.mode === 'batch' ? '打印当前' : '打印';
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,17 +302,59 @@ function flashPrintSuccess() {
|
|||||||
}, 1600);
|
}, 1600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadPrinterList(force = false) {
|
||||||
|
if (!hiprintClientState.enabled || !hiprintClientState.connected) {
|
||||||
|
printerList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (printerListLoading.value && !force) return;
|
||||||
|
const reqId = ++printerListReqId;
|
||||||
|
printerListLoading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await hiprintRefreshPrinterList();
|
||||||
|
if (reqId !== printerListReqId) return;
|
||||||
|
printerList.value = list;
|
||||||
|
applyPrinterSelection(list);
|
||||||
|
} catch {
|
||||||
|
if (reqId !== printerListReqId) return;
|
||||||
|
printerList.value = [];
|
||||||
|
} finally {
|
||||||
|
if (reqId === printerListReqId) printerListLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPrinterChange(value: string | undefined) {
|
||||||
|
writeHiprintSelectedPrinterToStorage(String(value || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPrinterPopupVisible(visible: boolean) {
|
||||||
|
if (visible) void loadPrinterList(true);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(visible) => {
|
(visible) => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
advancedOpen.value = false;
|
advancedOpen.value = false;
|
||||||
printJustSucceeded.value = false;
|
printJustSucceeded.value = false;
|
||||||
|
resetItemPrintResults();
|
||||||
if (printSuccessTimer) clearTimeout(printSuccessTimer);
|
if (printSuccessTimer) clearTimeout(printSuccessTimer);
|
||||||
printSuccessTimer = null;
|
printSuccessTimer = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeId.value = printableItems.value[0]?.id ?? null;
|
activeId.value = printableItems.value[0]?.id ?? null;
|
||||||
|
selectedPrinter.value = readHiprintSelectedPrinterFromStorage();
|
||||||
|
resetItemPrintResults();
|
||||||
|
void loadPrinterList(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => hiprintClientState.connected,
|
||||||
|
(connected) => {
|
||||||
|
if (!props.visible) return;
|
||||||
|
if (connected) void loadPrinterList(true);
|
||||||
|
else printerList.value = [];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -282,15 +417,16 @@ function openActiveInNative() {
|
|||||||
|
|
||||||
async function silentPrintByUrl(url: string) {
|
async function silentPrintByUrl(url: string) {
|
||||||
const extras = buildHiprintBlobPrintExtras(hiprintForm.value);
|
const extras = buildHiprintBlobPrintExtras(hiprintForm.value);
|
||||||
|
const printer = String(selectedPrinter.value || '').trim() || undefined;
|
||||||
const isPdf = /\.pdf(\?|#|$)/i.test(url);
|
const isPdf = /\.pdf(\?|#|$)/i.test(url);
|
||||||
try {
|
try {
|
||||||
const bytes = await fetchPrintAssetBytes(url);
|
const bytes = await fetchPrintAssetBytes(url);
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
await hiprintSendBlobPdf({ pdf_blob: bytes, printExtras: extras });
|
await hiprintSendBlobPdf({ pdf_blob: bytes, printer, printExtras: extras });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const html = new TextDecoder('utf-8').decode(bytes);
|
const html = new TextDecoder('utf-8').decode(bytes);
|
||||||
await hiprintSendHtmlPrint({ html, printExtras: extras });
|
await hiprintSendHtmlPrint({ html, printer, printExtras: extras });
|
||||||
return;
|
return;
|
||||||
} catch (primaryError) {
|
} catch (primaryError) {
|
||||||
const absolute = toAbsoluteUrl(url);
|
const absolute = toAbsoluteUrl(url);
|
||||||
@ -304,11 +440,23 @@ async function silentPrintByUrl(url: string) {
|
|||||||
}
|
}
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
const pdf_blob = await fetchPdfBytesForHiprint(toAbsoluteUrl(url));
|
const pdf_blob = await fetchPdfBytesForHiprint(toAbsoluteUrl(url));
|
||||||
await hiprintSendBlobPdf({ pdf_blob, printExtras: extras });
|
await hiprintSendBlobPdf({ pdf_blob, printer, printExtras: extras });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const html = await fetchHtmlTextForHiprint(toAbsoluteUrl(url));
|
const html = await fetchHtmlTextForHiprint(toAbsoluteUrl(url));
|
||||||
await hiprintSendHtmlPrint({ html, printExtras: extras });
|
await hiprintSendHtmlPrint({ html, printer, printExtras: extras });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function printOneItem(item: PrintItem) {
|
||||||
|
setItemPrintResult(item.id, 'printing');
|
||||||
|
try {
|
||||||
|
await silentPrintByUrl(item.url);
|
||||||
|
setItemPrintResult(item.id, 'success');
|
||||||
|
return true;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setItemPrintResult(item.id, 'fail');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printActive() {
|
async function printActive() {
|
||||||
@ -322,11 +470,11 @@ async function printActive() {
|
|||||||
}
|
}
|
||||||
printingActive.value = true;
|
printingActive.value = true;
|
||||||
try {
|
try {
|
||||||
await silentPrintByUrl(activeItem.value.url);
|
await printOneItem(activeItem.value);
|
||||||
flashPrintSuccess();
|
flashPrintSuccess();
|
||||||
Message.success('已发送到本机打印');
|
Message.success('打印成功');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Message.warning(e?.message || '自动打印失败,将尝试浏览器打开面单');
|
Message.warning(e?.message || '打印失败,将尝试浏览器打开面单');
|
||||||
await openItemInNative(activeItem.value);
|
await openItemInNative(activeItem.value);
|
||||||
} finally {
|
} finally {
|
||||||
printingActive.value = false;
|
printingActive.value = false;
|
||||||
@ -344,18 +492,22 @@ async function printAll() {
|
|||||||
}
|
}
|
||||||
printingAll.value = true;
|
printingAll.value = true;
|
||||||
try {
|
try {
|
||||||
const tasks = printableItems.value.map((item) =>
|
const tasks = printableItems.value.map(async (item) => {
|
||||||
silentPrintByUrl(item.url).catch(async () => {
|
try {
|
||||||
await openItemInNative(item);
|
await printOneItem(item);
|
||||||
}),
|
return true;
|
||||||
);
|
} catch {
|
||||||
const results = await Promise.allSettled(tasks);
|
return false;
|
||||||
const failed = results.filter((x) => x.status === 'rejected').length;
|
}
|
||||||
|
});
|
||||||
|
const results = await Promise.all(tasks);
|
||||||
|
const ok = results.filter(Boolean).length;
|
||||||
|
const failed = results.length - ok;
|
||||||
if (failed === 0) {
|
if (failed === 0) {
|
||||||
flashPrintSuccess();
|
flashPrintSuccess();
|
||||||
Message.success(`批量打印完成(${results.length} 单)`);
|
Message.success(`批量打印完成(${results.length} 单)`);
|
||||||
} else {
|
} else {
|
||||||
Message.warning(`批量打印完成:成功 ${results.length - failed},失败 ${failed}`);
|
Message.warning(`批量打印完成:成功 ${ok},失败 ${failed}`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
printingAll.value = false;
|
printingAll.value = false;
|
||||||
@ -373,7 +525,8 @@ async function printAll() {
|
|||||||
.print-status-bar {
|
.print-status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -381,6 +534,30 @@ async function printAll() {
|
|||||||
background: var(--color-fill-2);
|
background: var(--color-fill-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.print-status-bar__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-status-text {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-status-bar__right {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-printer-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.print-status-bar--success {
|
.print-status-bar--success {
|
||||||
background: rgba(0, 180, 42, 0.08);
|
background: rgba(0, 180, 42, 0.08);
|
||||||
border-color: rgba(0, 180, 42, 0.25);
|
border-color: rgba(0, 180, 42, 0.25);
|
||||||
@ -478,6 +655,27 @@ async function printAll() {
|
|||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-list-item-result {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&.is-success {
|
||||||
|
color: rgb(0, 180, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-fail {
|
||||||
|
color: rgb(245, 63, 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-printing {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.single-label-row {
|
.single-label-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from '@/utils/login-portal';
|
} from '@/utils/login-portal';
|
||||||
import { preferH5Portal } from '@/utils/device';
|
import { preferH5Portal } from '@/utils/device';
|
||||||
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
||||||
|
import { peekPageFlip } from '@/utils/page-transition-flag';
|
||||||
|
|
||||||
function buildLoginRedirect(to: {
|
function buildLoginRedirect(to: {
|
||||||
name?: string | symbol | null;
|
name?: string | symbol | null;
|
||||||
@ -63,7 +64,9 @@ export default function setupUserLoginInfoGuard(router: Router) {
|
|||||||
!isLoginRouteName(toName) &&
|
!isLoginRouteName(toName) &&
|
||||||
!isLoginRouteName(fromName) &&
|
!isLoginRouteName(fromName) &&
|
||||||
tabBarStore.getCacheList.includes(toName);
|
tabBarStore.getCacheList.includes(toName);
|
||||||
|
// 门户翻页:跳过顶栏横条,等待感交给翻页动画
|
||||||
if (
|
if (
|
||||||
|
!peekPageFlip() &&
|
||||||
!isCachedTabSwitch &&
|
!isCachedTabSwitch &&
|
||||||
!String(to.path || '').startsWith('/h5') &&
|
!String(to.path || '').startsWith('/h5') &&
|
||||||
to.path !== '/'
|
to.path !== '/'
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
|||||||
import {
|
import {
|
||||||
buildHiprintBlobPrintExtras,
|
buildHiprintBlobPrintExtras,
|
||||||
readHiprintPrintFormFromStorage,
|
readHiprintPrintFormFromStorage,
|
||||||
|
readHiprintSelectedPrinterFromStorage,
|
||||||
} from '@/utils/hiprint/hiprintPrintOptions';
|
} from '@/utils/hiprint/hiprintPrintOptions';
|
||||||
|
|
||||||
function toAbsoluteUrl(url: string) {
|
function toAbsoluteUrl(url: string) {
|
||||||
@ -22,15 +23,20 @@ function toAbsoluteUrl(url: string) {
|
|||||||
return `${window.location.origin}/${resolved}`;
|
return `${window.location.origin}/${resolved}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSelectedPrinter() {
|
||||||
|
return readHiprintSelectedPrinterFromStorage() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendBytesToHiprint(rawUrl: string, bytes: Uint8Array) {
|
async function sendBytesToHiprint(rawUrl: string, bytes: Uint8Array) {
|
||||||
const extras = buildHiprintBlobPrintExtras(readHiprintPrintFormFromStorage());
|
const extras = buildHiprintBlobPrintExtras(readHiprintPrintFormFromStorage());
|
||||||
|
const printer = resolveSelectedPrinter();
|
||||||
const isPdf = /\.pdf(\?|#|$)/i.test(rawUrl);
|
const isPdf = /\.pdf(\?|#|$)/i.test(rawUrl);
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
await hiprintSendBlobPdf({ pdf_blob: bytes, printExtras: extras });
|
await hiprintSendBlobPdf({ pdf_blob: bytes, printer, printExtras: extras });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const html = new TextDecoder('utf-8').decode(bytes);
|
const html = new TextDecoder('utf-8').decode(bytes);
|
||||||
await hiprintSendHtmlPrint({ html, printExtras: extras });
|
await hiprintSendHtmlPrint({ html, printer, printExtras: extras });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 本机 hiprint 静默打印,不打开新标签页 */
|
/** 本机 hiprint 静默打印,不打开新标签页 */
|
||||||
@ -60,14 +66,15 @@ export async function dispatchPrintUrl(rawUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const absolute = toAbsoluteUrl(url);
|
const absolute = toAbsoluteUrl(url);
|
||||||
|
const printer = resolveSelectedPrinter();
|
||||||
if (/\.pdf(\?|#|$)/i.test(url)) {
|
if (/\.pdf(\?|#|$)/i.test(url)) {
|
||||||
const pdf_blob = await fetchPdfBytesForHiprint(absolute);
|
const pdf_blob = await fetchPdfBytesForHiprint(absolute);
|
||||||
const extras = buildHiprintBlobPrintExtras(readHiprintPrintFormFromStorage());
|
const extras = buildHiprintBlobPrintExtras(readHiprintPrintFormFromStorage());
|
||||||
await hiprintSendBlobPdf({ pdf_blob, printExtras: extras });
|
await hiprintSendBlobPdf({ pdf_blob, printer, printExtras: extras });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = await fetchHtmlTextForHiprint(absolute);
|
const html = await fetchHtmlTextForHiprint(absolute);
|
||||||
const extras = buildHiprintBlobPrintExtras(readHiprintPrintFormFromStorage());
|
const extras = buildHiprintBlobPrintExtras(readHiprintPrintFormFromStorage());
|
||||||
await hiprintSendHtmlPrint({ html, printExtras: extras });
|
await hiprintSendHtmlPrint({ html, printer, printExtras: extras });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,15 +91,123 @@ export async function ensureHiprintSocketConnected(timeoutMs = 15000) {
|
|||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hiprintSendUrlPdf(payload: { pdf_path: string; printer?: string; templateId?: string }) {
|
export type HiprintPrinterInfo = {
|
||||||
if (!payload.pdf_path) throw new Error('pdf 地址为空');
|
name: string;
|
||||||
|
displayName?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
description?: string;
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 向本机打印客户端请求打印机列表(emit refreshPrinterList → on printerList) */
|
||||||
|
export async function hiprintRefreshPrinterList(timeoutMs = 8000): Promise<HiprintPrinterInfo[]> {
|
||||||
const socket = await ensureHiprintSocketConnected();
|
const socket = await ensureHiprintSocketConnected();
|
||||||
|
return new Promise<HiprintPrinterInfo[]>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
socket.off('printerList', onList);
|
||||||
|
reject(new Error('获取打印机列表超时'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const onList = (list: unknown) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
socket.off('printerList', onList);
|
||||||
|
const arr = Array.isArray(list) ? list : [];
|
||||||
|
resolve(
|
||||||
|
arr
|
||||||
|
.map((item) => {
|
||||||
|
const row = item as Partial<HiprintPrinterInfo>;
|
||||||
|
const name = String(row?.name || row?.displayName || '').trim();
|
||||||
|
if (!name) return null;
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
displayName: String(row?.displayName || name).trim() || name,
|
||||||
|
isDefault: !!row?.isDefault,
|
||||||
|
description: row?.description,
|
||||||
|
status: row?.status,
|
||||||
|
} as HiprintPrinterInfo;
|
||||||
|
})
|
||||||
|
.filter((x): x is HiprintPrinterInfo => !!x),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.once('printerList', onList);
|
||||||
|
socket.emit('refreshPrinterList');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHiprintTemplateId(prefix = 'oms') {
|
||||||
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchHiprintTemplateId(payload: unknown, templateId: string) {
|
||||||
|
if (payload == null) return false;
|
||||||
|
if (typeof payload === 'string') return payload === templateId;
|
||||||
|
if (typeof payload === 'object') {
|
||||||
|
const row = payload as Record<string, unknown>;
|
||||||
|
const id = row.templateId ?? row.id;
|
||||||
|
return String(id ?? '') === templateId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readHiprintErrorMessage(payload: unknown) {
|
||||||
|
if (typeof payload === 'string' && payload.trim()) return payload.trim();
|
||||||
|
if (payload && typeof payload === 'object') {
|
||||||
|
const row = payload as Record<string, unknown>;
|
||||||
|
const msg = row.msg ?? row.message ?? row.error ?? row.errMsg;
|
||||||
|
if (msg != null && String(msg).trim()) return String(msg).trim();
|
||||||
|
}
|
||||||
|
return '打印失败';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等待本机客户端按 templateId 回传 success / error */
|
||||||
|
function waitHiprintPrintResult(socket: Socket, templateId: string, timeoutMs = 90000) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error('等待打印结果超时'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
const onSuccess = (payload: unknown) => {
|
||||||
|
if (!matchHiprintTemplateId(payload, templateId)) return;
|
||||||
|
cleanup();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const onError = (payload: unknown) => {
|
||||||
|
if (!matchHiprintTemplateId(payload, templateId)) return;
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(readHiprintErrorMessage(payload)));
|
||||||
|
};
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
socket.off('success', onSuccess);
|
||||||
|
socket.off('error', onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('success', onSuccess);
|
||||||
|
socket.on('error', onError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hiprintSendUrlPdf(payload: {
|
||||||
|
pdf_path: string;
|
||||||
|
printer?: string;
|
||||||
|
templateId?: string;
|
||||||
|
/** 是否等待客户端 success/error,默认 true */
|
||||||
|
waitResult?: boolean;
|
||||||
|
}) {
|
||||||
|
if (!payload.pdf_path) throw new Error('pdf 地址为空');
|
||||||
|
const templateId = payload.templateId || createHiprintTemplateId();
|
||||||
|
const socket = await ensureHiprintSocketConnected();
|
||||||
|
const wait =
|
||||||
|
payload.waitResult === false ? Promise.resolve() : waitHiprintPrintResult(socket, templateId);
|
||||||
socket.emit('news', {
|
socket.emit('news', {
|
||||||
type: 'url_pdf',
|
type: 'url_pdf',
|
||||||
templateId: payload.templateId || `oms-${Date.now()}`,
|
templateId,
|
||||||
pdf_path: payload.pdf_path,
|
pdf_path: payload.pdf_path,
|
||||||
printer: payload.printer || undefined,
|
printer: payload.printer || undefined,
|
||||||
});
|
});
|
||||||
|
await wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBrowserFetchUrl(pathOrUrl: string) {
|
function resolveBrowserFetchUrl(pathOrUrl: string) {
|
||||||
@ -166,18 +274,24 @@ export async function hiprintSendBlobPdf(payload: {
|
|||||||
printer?: string;
|
printer?: string;
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
printExtras?: Record<string, unknown>;
|
printExtras?: Record<string, unknown>;
|
||||||
|
/** 是否等待客户端 success/error,默认 true */
|
||||||
|
waitResult?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (!(payload.pdf_blob instanceof Uint8Array) || payload.pdf_blob.byteLength === 0) {
|
if (!(payload.pdf_blob instanceof Uint8Array) || payload.pdf_blob.byteLength === 0) {
|
||||||
throw new Error('PDF 数据为空');
|
throw new Error('PDF 数据为空');
|
||||||
}
|
}
|
||||||
|
const templateId = payload.templateId || createHiprintTemplateId();
|
||||||
const socket = await ensureHiprintSocketConnected();
|
const socket = await ensureHiprintSocketConnected();
|
||||||
|
const wait =
|
||||||
|
payload.waitResult === false ? Promise.resolve() : waitHiprintPrintResult(socket, templateId);
|
||||||
socket.emit('news', {
|
socket.emit('news', {
|
||||||
type: 'blob_pdf',
|
type: 'blob_pdf',
|
||||||
templateId: payload.templateId || `oms-${Date.now()}`,
|
templateId,
|
||||||
pdf_blob: payload.pdf_blob,
|
pdf_blob: payload.pdf_blob,
|
||||||
printer: payload.printer || undefined,
|
printer: payload.printer || undefined,
|
||||||
...(payload.printExtras || {}),
|
...(payload.printExtras || {}),
|
||||||
});
|
});
|
||||||
|
await wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hiprintSendHtmlPrint(payload: {
|
export async function hiprintSendHtmlPrint(payload: {
|
||||||
@ -185,15 +299,21 @@ export async function hiprintSendHtmlPrint(payload: {
|
|||||||
printer?: string;
|
printer?: string;
|
||||||
templateId?: string;
|
templateId?: string;
|
||||||
printExtras?: Record<string, unknown>;
|
printExtras?: Record<string, unknown>;
|
||||||
|
/** 是否等待客户端 success/error,默认 true */
|
||||||
|
waitResult?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (!payload.html || !String(payload.html).trim()) throw new Error('HTML 内容为空');
|
if (!payload.html || !String(payload.html).trim()) throw new Error('HTML 内容为空');
|
||||||
|
const templateId = payload.templateId || createHiprintTemplateId();
|
||||||
const socket = await ensureHiprintSocketConnected();
|
const socket = await ensureHiprintSocketConnected();
|
||||||
|
const wait =
|
||||||
|
payload.waitResult === false ? Promise.resolve() : waitHiprintPrintResult(socket, templateId);
|
||||||
socket.emit('news', {
|
socket.emit('news', {
|
||||||
type: 'html',
|
type: 'html',
|
||||||
html: payload.html,
|
html: payload.html,
|
||||||
templateId: payload.templateId || `oms-${Date.now()}`,
|
templateId,
|
||||||
printer: payload.printer || undefined,
|
printer: payload.printer || undefined,
|
||||||
...(payload.printExtras || {}),
|
...(payload.printExtras || {}),
|
||||||
});
|
});
|
||||||
|
await wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const STORAGE_KEY = 'oms_hiprint_print_options_v1';
|
const STORAGE_KEY = 'oms_hiprint_print_options_v1';
|
||||||
|
const PRINTER_STORAGE_KEY = 'oms_hiprint_selected_printer_v1';
|
||||||
|
|
||||||
export interface HiprintPrintForm {
|
export interface HiprintPrintForm {
|
||||||
paperPreset: string;
|
paperPreset: string;
|
||||||
@ -9,6 +10,27 @@ export interface HiprintPrintForm {
|
|||||||
scaleFactor: number;
|
scaleFactor: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readHiprintSelectedPrinterFromStorage(): string {
|
||||||
|
try {
|
||||||
|
return String(window.localStorage.getItem(PRINTER_STORAGE_KEY) || '').trim();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeHiprintSelectedPrinterToStorage(printer: string) {
|
||||||
|
try {
|
||||||
|
const name = String(printer || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
window.localStorage.removeItem(PRINTER_STORAGE_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(PRINTER_STORAGE_KEY, name);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 物流面单常用尺寸(mm),走 pageSize 而非系统纸张名 */
|
/** 物流面单常用尺寸(mm),走 pageSize 而非系统纸张名 */
|
||||||
export const HIPRINT_LABEL_PAPER_MM: Record<string, { widthMm: number; heightMm: number }> = {
|
export const HIPRINT_LABEL_PAPER_MM: Record<string, { widthMm: number; heightMm: number }> = {
|
||||||
label_100x150: { widthMm: 100, heightMm: 150 },
|
label_100x150: { widthMm: 100, heightMm: 150 },
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
/** 登录进系统后首屏跳过 page-fade */
|
/** 登录进系统后首屏跳过 page-fade */
|
||||||
let skipOnce = false;
|
let skipOnce = false;
|
||||||
|
|
||||||
|
/** 门户翻页过渡(官网→登录 / 客户↔管理登录,单次消费) */
|
||||||
|
let pageFlipOnce = false;
|
||||||
|
|
||||||
export function skipNextPageTransition() {
|
export function skipNextPageTransition() {
|
||||||
skipOnce = true;
|
skipOnce = true;
|
||||||
}
|
}
|
||||||
@ -10,3 +13,34 @@ export function consumeSkipPageTransition(): boolean {
|
|||||||
skipOnce = false;
|
skipOnce = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 武装一次书页翻页过渡 */
|
||||||
|
export function armPageFlip() {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
pageFlipOnce = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated 同 armPageFlip */
|
||||||
|
export function armOfficialLoginFlip() {
|
||||||
|
armPageFlip();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumePageFlip(): boolean {
|
||||||
|
if (!pageFlipOnce) return false;
|
||||||
|
pageFlipOnce = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated 同 consumePageFlip */
|
||||||
|
export function consumeOfficialLoginFlip(): boolean {
|
||||||
|
return consumePageFlip();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function peekPageFlip(): boolean {
|
||||||
|
return pageFlipOnce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated 同 peekPageFlip */
|
||||||
|
export function peekOfficialLoginFlip(): boolean {
|
||||||
|
return peekPageFlip();
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
:to="to"
|
:to="to"
|
||||||
replace
|
replace
|
||||||
:aria-label="label"
|
:aria-label="label"
|
||||||
|
@click="onSwitchClick"
|
||||||
>
|
>
|
||||||
<icon-swap :size="16" />
|
<icon-swap :size="16" />
|
||||||
</router-link>
|
</router-link>
|
||||||
@ -16,6 +17,7 @@ import { computed } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { IconSwap } from '@arco-design/web-vue/es/icon';
|
import { IconSwap } from '@arco-design/web-vue/es/icon';
|
||||||
import { ADMIN_LOGIN_PATH, CUSTOMER_LOGIN_PATH } from '@/utils/login-portal';
|
import { ADMIN_LOGIN_PATH, CUSTOMER_LOGIN_PATH } from '@/utils/login-portal';
|
||||||
|
import { armPageFlip } from '@/utils/page-transition-flag';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 当前页面对侧:admin=去管理登录;customer=去客户登录 */
|
/** 当前页面对侧:admin=去管理登录;customer=去客户登录 */
|
||||||
@ -33,6 +35,19 @@ const label = computed(() =>
|
|||||||
? t('login.switchToAdmin')
|
? t('login.switchToAdmin')
|
||||||
: t('login.switchToCustomer'),
|
: t('login.switchToCustomer'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function onSwitchClick(e: MouseEvent) {
|
||||||
|
if (
|
||||||
|
e.button !== 0 ||
|
||||||
|
e.metaKey ||
|
||||||
|
e.ctrlKey ||
|
||||||
|
e.shiftKey ||
|
||||||
|
e.altKey
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
armPageFlip();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@ -144,7 +144,12 @@ const showCharacters = ref(false);
|
|||||||
const activeKey = ref<'pwd' | 'wechat'>('pwd');
|
const activeKey = ref<'pwd' | 'wechat'>('pwd');
|
||||||
const wechatMounted = ref(false);
|
const wechatMounted = ref(false);
|
||||||
const wechatLoginRef = ref<{ prefetch?: () => void } | null>(null);
|
const wechatLoginRef = ref<{ prefetch?: () => void } | null>(null);
|
||||||
const shellReady = ref(false);
|
/** 翻页进入时立刻出壳,避免过渡中空屏 */
|
||||||
|
const shellReady = ref(
|
||||||
|
route.query.from === 'official-site' ||
|
||||||
|
route.name === 'login' ||
|
||||||
|
route.name === 'adminLogin',
|
||||||
|
);
|
||||||
const isExiting = ref(false);
|
const isExiting = ref(false);
|
||||||
const loginCharactersApi = shallowRef<LoginCharactersApi | null>(null);
|
const loginCharactersApi = shallowRef<LoginCharactersApi | null>(null);
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
|||||||
@ -186,8 +186,25 @@
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表行与表格列对齐:接口用 serviceNo/clientNo/trackNo,表格用运单号/客户单号/跟踪号 */
|
function escapeHtml(text: string) {
|
||||||
function normalizeOrderRow(raw: any) {
|
return String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCustomerLabelMap() {
|
||||||
|
const map = new Map<number, string>();
|
||||||
|
for (const opt of customerOptions.value || []) {
|
||||||
|
const id = Number(opt.value);
|
||||||
|
if (Number.isFinite(id)) map.set(id, String(opt.label || ''));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表行与表格列对齐:只保留标量/展平字段,不保留嵌套树(Items/FeeList 等) */
|
||||||
|
function normalizeOrderRow(raw: any, customerLabelMap?: Map<number, string>) {
|
||||||
const r = raw?.receiver || raw?.Receiver || {};
|
const r = raw?.receiver || raw?.Receiver || {};
|
||||||
const s = raw?.sender || raw?.Sender || {};
|
const s = raw?.sender || raw?.Sender || {};
|
||||||
const wl = raw?.weightList || raw?.WeightList;
|
const wl = raw?.weightList || raw?.WeightList;
|
||||||
@ -204,15 +221,68 @@
|
|||||||
const channelName = pick(raw, 'channelName', 'ChannelName');
|
const channelName = pick(raw, 'channelName', 'ChannelName');
|
||||||
const isPaidRaw = pick(raw, 'isPaid', 'IsPaid');
|
const isPaidRaw = pick(raw, 'isPaid', 'IsPaid');
|
||||||
const customerId = pick(raw, 'customerId', 'CustomerId') ?? raw?.customerId;
|
const customerId = pick(raw, 'customerId', 'CustomerId') ?? raw?.customerId;
|
||||||
const customerFromOptions = customerOptions.value.find(
|
const customerLabel =
|
||||||
(opt) => Number(opt.value) === Number(customerId),
|
(customerId != null && customerLabelMap?.get(Number(customerId))) ||
|
||||||
);
|
customerOptions.value.find((opt) => Number(opt.value) === Number(customerId))?.label ||
|
||||||
|
'';
|
||||||
|
|
||||||
const boolish = (v: any) => v === true || v === 1 || v === '1';
|
const boolish = (v: any) => v === true || v === 1 || v === '1';
|
||||||
|
const hasInsuranceRaw = pick(raw, 'hasInsurance', 'HasInsurance');
|
||||||
|
let hasInsurance: boolean | undefined;
|
||||||
|
if (hasInsuranceRaw === true || hasInsuranceRaw === 1 || hasInsuranceRaw === '1') hasInsurance = true;
|
||||||
|
else if (hasInsuranceRaw === false || hasInsuranceRaw === 0 || hasInsuranceRaw === '0') {
|
||||||
|
hasInsurance = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusCode =
|
||||||
|
resolveOrderStatusCode(raw) || pick(raw, 'status', 'Status') || raw?.status || '';
|
||||||
|
const orderStatusText = pick(raw, 'orderStatus', 'OrderStatus') ?? raw?.orderStatus;
|
||||||
|
const handleStatus = pick(raw, 'handleStatus', 'HandleStatus') ?? raw?.handleStatus;
|
||||||
|
const handleStatusName = pick(raw, 'handleStatusName', 'HandleStatusName') ?? raw?.handleStatusName;
|
||||||
|
const predeclareFailReason = pick(raw, 'predeclareFailReason', 'PredeclareFailReason') ?? '';
|
||||||
|
const allocationPendingReason =
|
||||||
|
pick(raw, 'allocationPendingReason', 'AllocationPendingReason') ?? '';
|
||||||
|
const isPaid = boolish(isPaidRaw);
|
||||||
|
const isCod = boolish(pick(raw, 'isCod', 'IsCod'));
|
||||||
|
const isIntercept = boolish(pick(raw, 'isIntercept', 'IsIntercept'));
|
||||||
|
const isRemote = boolish(pick(raw, 'isRemote', 'IsRemote'));
|
||||||
|
const cargoAttribute = pick(raw, 'cargoAttribute', 'CargoAttribute') ?? raw?.cargoAttribute;
|
||||||
|
const fulfillmentType = pick(raw, 'fulfillmentType', 'FulfillmentType') ?? 'direct';
|
||||||
|
const stockBindStatus = pick(raw, 'stockBindStatus', 'StockBindStatus') ?? 'none';
|
||||||
|
const fulfillProgress = pick(raw, 'fulfillProgress', 'FulfillProgress') ?? '';
|
||||||
|
const domesticTrackingNos = pick(raw, 'domesticTrackingNos', 'DomesticTrackingNos') ?? '';
|
||||||
|
const primaryBinCode = pick(raw, 'primaryBinCode', 'PrimaryBinCode') ?? '';
|
||||||
|
const waybillNo = serviceNo ?? '';
|
||||||
|
const consigneeCountry =
|
||||||
|
pick(raw, 'consigneeCountry', 'ConsigneeCountry') ?? pick(r, 'country', 'Country') ?? '';
|
||||||
|
const consigneeCountryName = pick(raw, 'consigneeCountryName', 'ConsigneeCountryName') ?? '';
|
||||||
|
|
||||||
|
const statusMeta = getOrderStatusMeta(String(statusCode || ''));
|
||||||
|
const statusDisplay = statusMeta
|
||||||
|
? t(statusMeta.labelKey)
|
||||||
|
: orderStatusText || statusCode || '--';
|
||||||
|
const handleCode = resolveOrderHandleStatusCode({ handleStatus, status: statusCode });
|
||||||
|
const handleMeta = getOrderHandleStatusMeta(handleCode);
|
||||||
|
const handleText =
|
||||||
|
handleStatusName || (handleMeta ? t(handleMeta.labelKey) : '');
|
||||||
|
const showHandleTag = isOrderHandleSubTabParent(String(statusCode || '')) && handleText;
|
||||||
|
const orderStatusHtml = `<span style="display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;max-width:100%;">${buildOrderStatusTagHtml(String(statusDisplay), statusMeta)}${
|
||||||
|
showHandleTag ? buildOrderStatusSubTagHtml(String(handleText), handleMeta) : ''
|
||||||
|
}</span>`;
|
||||||
|
|
||||||
|
let predeclareFailReasonHtml: string | null = null;
|
||||||
|
if (predeclareFailReason) {
|
||||||
|
const guide = buildFailGuideFromReason(String(predeclareFailReason));
|
||||||
|
const display = guide.reason || String(predeclareFailReason);
|
||||||
|
predeclareFailReasonHtml = `<span title="${escapeHtml(guide.rawReason || String(predeclareFailReason))}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8a5c58;font-size:12px;">${escapeHtml(display)}</span>`;
|
||||||
|
}
|
||||||
|
const allocationPendingReasonHtml = allocationPendingReason
|
||||||
|
? `<span title="${escapeHtml(String(allocationPendingReason))}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#7a6a52;font-size:12px;">${escapeHtml(String(allocationPendingReason))}</span>`
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...raw,
|
id: pick(raw, 'id', 'Id') ?? raw?.id,
|
||||||
// 展平寄件人(便于列配置直接选接口字段)
|
// 展平寄件人
|
||||||
senderName: pick(s, 'senderName', 'SenderName') ?? '',
|
senderName: pick(s, 'senderName', 'SenderName') ?? '',
|
||||||
senderCompany: pick(s, 'company', 'Company') ?? '',
|
senderCompany: pick(s, 'company', 'Company') ?? '',
|
||||||
senderPhone: pick(s, 'phone', 'Phone') ?? '',
|
senderPhone: pick(s, 'phone', 'Phone') ?? '',
|
||||||
@ -226,7 +296,7 @@
|
|||||||
senderStreetAddress2: pick(s, 'streetAddress2', 'StreetAddress2') ?? '',
|
senderStreetAddress2: pick(s, 'streetAddress2', 'StreetAddress2') ?? '',
|
||||||
senderStreetAddress3: pick(s, 'streetAddress3', 'StreetAddress3') ?? '',
|
senderStreetAddress3: pick(s, 'streetAddress3', 'StreetAddress3') ?? '',
|
||||||
senderHouseNumber: pick(s, 'houseNumber', 'HouseNumber') ?? '',
|
senderHouseNumber: pick(s, 'houseNumber', 'HouseNumber') ?? '',
|
||||||
// 展平收件人原始字段
|
// 展平收件人
|
||||||
receiverRecipientsName: pick(r, 'recipientsName', 'RecipientsName') ?? '',
|
receiverRecipientsName: pick(r, 'recipientsName', 'RecipientsName') ?? '',
|
||||||
receiverCompanyName: pick(r, 'companyName', 'CompanyName') ?? '',
|
receiverCompanyName: pick(r, 'companyName', 'CompanyName') ?? '',
|
||||||
receiverPhone: pick(r, 'phone', 'Phone') ?? '',
|
receiverPhone: pick(r, 'phone', 'Phone') ?? '',
|
||||||
@ -247,7 +317,10 @@
|
|||||||
receiverEori: pick(r, 'eori', 'Eori') ?? '',
|
receiverEori: pick(r, 'eori', 'Eori') ?? '',
|
||||||
receiverVat: pick(r, 'vat', 'Vat') ?? '',
|
receiverVat: pick(r, 'vat', 'Vat') ?? '',
|
||||||
|
|
||||||
waybillNo: serviceNo ?? '',
|
serviceNo: serviceNo ?? '',
|
||||||
|
clientNo: clientNo ?? '',
|
||||||
|
trackNo: trackNo ?? '',
|
||||||
|
waybillNo,
|
||||||
customerOrderNo: clientNo ?? '',
|
customerOrderNo: clientNo ?? '',
|
||||||
trackingNo: trackNo ?? '',
|
trackingNo: trackNo ?? '',
|
||||||
downstreamServiceNo: downstreamServiceNo ?? '',
|
downstreamServiceNo: downstreamServiceNo ?? '',
|
||||||
@ -255,22 +328,33 @@
|
|||||||
productName: productName ?? productCode ?? '',
|
productName: productName ?? productCode ?? '',
|
||||||
channelCode: channelCode ?? '',
|
channelCode: channelCode ?? '',
|
||||||
channelName: channelName ?? channelCode ?? '',
|
channelName: channelName ?? channelCode ?? '',
|
||||||
downstreamSupplierName:
|
downstreamSupplierName: pick(raw, 'downstreamSupplierName', 'DownstreamSupplierName') ?? '',
|
||||||
pick(raw, 'downstreamSupplierName', 'DownstreamSupplierName') ?? '',
|
downstreamSupplierCode: pick(raw, 'downstreamSupplierCode', 'DownstreamSupplierCode') ?? '',
|
||||||
downstreamSupplierCode:
|
downstreamAppKey: pick(raw, 'downstreamAppKey', 'DownstreamAppKey') ?? '',
|
||||||
pick(raw, 'downstreamSupplierCode', 'DownstreamSupplierCode') ?? '',
|
downstreamAppId: pick(raw, 'downstreamAppId', 'DownstreamAppId') ?? '',
|
||||||
isPaid: boolish(isPaidRaw),
|
channelForecastCode: pick(raw, 'channelForecastCode', 'ChannelForecastCode') ?? '',
|
||||||
isCod: boolish(pick(raw, 'isCod', 'IsCod')),
|
maxTimeliness: pick(raw, 'maxTimeliness', 'MaxTimeliness') ?? null,
|
||||||
isIntercept: boolish(pick(raw, 'isIntercept', 'IsIntercept')),
|
salesProductRulesVersion: pick(raw, 'salesProductRulesVersion', 'SalesProductRulesVersion') ?? '',
|
||||||
isRemote: boolish(pick(raw, 'isRemote', 'IsRemote')),
|
salesProductPriceVersion: pick(raw, 'salesProductPriceVersion', 'SalesProductPriceVersion') ?? '',
|
||||||
consignee: pick(raw, 'consignee', 'Consignee') ?? pick(r, 'recipientsName', 'RecipientsName') ?? '',
|
isPaid,
|
||||||
consigneePhone: pick(raw, 'consigneePhone', 'ConsigneePhone') ?? pick(r, 'phone', 'Phone') ?? '',
|
isCod,
|
||||||
consigneeAddress: pick(raw, 'consigneeAddress', 'ConsigneeAddress') ?? pick(r, 'streetAddress1', 'StreetAddress1') ?? '',
|
isIntercept,
|
||||||
consigneePostcode: pick(raw, 'consigneePostcode', 'ConsigneePostcode') ?? pick(r, 'zipCode', 'ZipCode') ?? '',
|
isRemote,
|
||||||
consigneeState: pick(raw, 'consigneeState', 'ConsigneeState') ?? pick(r, 'province', 'Province') ?? '',
|
consignee:
|
||||||
|
pick(raw, 'consignee', 'Consignee') ?? pick(r, 'recipientsName', 'RecipientsName') ?? '',
|
||||||
|
consigneePhone:
|
||||||
|
pick(raw, 'consigneePhone', 'ConsigneePhone') ?? pick(r, 'phone', 'Phone') ?? '',
|
||||||
|
consigneeAddress:
|
||||||
|
pick(raw, 'consigneeAddress', 'ConsigneeAddress') ??
|
||||||
|
pick(r, 'streetAddress1', 'StreetAddress1') ??
|
||||||
|
'',
|
||||||
|
consigneePostcode:
|
||||||
|
pick(raw, 'consigneePostcode', 'ConsigneePostcode') ?? pick(r, 'zipCode', 'ZipCode') ?? '',
|
||||||
|
consigneeState:
|
||||||
|
pick(raw, 'consigneeState', 'ConsigneeState') ?? pick(r, 'province', 'Province') ?? '',
|
||||||
consigneeCity: pick(raw, 'consigneeCity', 'ConsigneeCity') ?? pick(r, 'city', 'City') ?? '',
|
consigneeCity: pick(raw, 'consigneeCity', 'ConsigneeCity') ?? pick(r, 'city', 'City') ?? '',
|
||||||
consigneeCountry: pick(raw, 'consigneeCountry', 'ConsigneeCountry') ?? pick(r, 'country', 'Country') ?? '',
|
consigneeCountry,
|
||||||
consigneeCountryName: pick(raw, 'consigneeCountryName', 'ConsigneeCountryName') ?? '',
|
consigneeCountryName,
|
||||||
ioss: pick(raw, 'ioss', 'Ioss', 'IOSS') ?? '',
|
ioss: pick(raw, 'ioss', 'Ioss', 'IOSS') ?? '',
|
||||||
eori: pick(raw, 'eori', 'Eori', 'EORI') ?? pick(r, 'eori', 'Eori') ?? '',
|
eori: pick(raw, 'eori', 'Eori', 'EORI') ?? pick(r, 'eori', 'Eori') ?? '',
|
||||||
shippingMethod: productName ?? productCode ?? '',
|
shippingMethod: productName ?? productCode ?? '',
|
||||||
@ -282,48 +366,100 @@
|
|||||||
receivableAmount: pick(raw, 'receivableAmount', 'ReceivableAmount') ?? '',
|
receivableAmount: pick(raw, 'receivableAmount', 'ReceivableAmount') ?? '',
|
||||||
declaredSum: pick(raw, 'declaredSum', 'DeclaredSum') ?? raw?.declaredSum ?? null,
|
declaredSum: pick(raw, 'declaredSum', 'DeclaredSum') ?? raw?.declaredSum ?? null,
|
||||||
declaredCurrency: pick(raw, 'declaredCurrency', 'DeclaredCurrency') ?? raw?.declaredCurrency ?? '',
|
declaredCurrency: pick(raw, 'declaredCurrency', 'DeclaredCurrency') ?? raw?.declaredCurrency ?? '',
|
||||||
|
quantity: pick(raw, 'quantity', 'Quantity') ?? null,
|
||||||
|
codCurrency: pick(raw, 'codCurrency', 'CodCurrency') ?? '',
|
||||||
|
codSum: pick(raw, 'codSum', 'CodSum') ?? null,
|
||||||
additionalServices: pick(raw, 'additionalServices', 'AdditionalServices') ?? raw?.additionalServices,
|
additionalServices: pick(raw, 'additionalServices', 'AdditionalServices') ?? raw?.additionalServices,
|
||||||
hasInsurance: (() => {
|
hasInsurance,
|
||||||
const hi = pick(raw, 'hasInsurance', 'HasInsurance');
|
hasAllocationTrace: boolish(pick(raw, 'hasAllocationTrace', 'HasAllocationTrace')),
|
||||||
if (hi === true || hi === 1 || hi === '1') return true;
|
allocationScheme: pick(raw, 'allocationScheme', 'AllocationScheme') ?? '',
|
||||||
if (hi === false || hi === 0 || hi === '0') return false;
|
|
||||||
return undefined;
|
|
||||||
})(),
|
|
||||||
forecastReceivableAmount:
|
forecastReceivableAmount:
|
||||||
pick(raw, 'forecastReceivableAmount', 'ForecastReceivableAmount') ?? null,
|
pick(raw, 'forecastReceivableAmount', 'ForecastReceivableAmount') ?? null,
|
||||||
forecastPayableAmount: pick(raw, 'forecastPayableAmount', 'ForecastPayableAmount') ?? null,
|
forecastPayableAmount: pick(raw, 'forecastPayableAmount', 'ForecastPayableAmount') ?? null,
|
||||||
orderStatus: pick(raw, 'orderStatus', 'OrderStatus') ?? raw?.orderStatus,
|
orderStatus: orderStatusText,
|
||||||
predeclareFailReason: pick(raw, 'predeclareFailReason', 'PredeclareFailReason') ?? '',
|
predeclareFailReason,
|
||||||
allocationPendingReason: pick(raw, 'allocationPendingReason', 'AllocationPendingReason') ?? '',
|
allocationPendingReason,
|
||||||
status: resolveOrderStatusCode(raw) || pick(raw, 'status', 'Status') || raw?.status,
|
status: statusCode,
|
||||||
handleStatus: pick(raw, 'handleStatus', 'HandleStatus') ?? raw?.handleStatus,
|
handleStatus,
|
||||||
handleStatusName: pick(raw, 'handleStatusName', 'HandleStatusName') ?? raw?.handleStatusName,
|
handleStatusName,
|
||||||
cargoAttribute: pick(raw, 'cargoAttribute', 'CargoAttribute') ?? raw?.cargoAttribute,
|
issueType: pick(raw, 'issueType', 'IssueType') ?? '',
|
||||||
|
issueTypeName: pick(raw, 'issueTypeName', 'IssueTypeName') ?? '',
|
||||||
|
issueTitle: pick(raw, 'issueTitle', 'IssueTitle') ?? '',
|
||||||
|
issueContent: pick(raw, 'issueContent', 'IssueContent') ?? '',
|
||||||
|
issueTemplateId: pick(raw, 'issueTemplateId', 'IssueTemplateId') ?? null,
|
||||||
|
issueRemark: pick(raw, 'issueRemark', 'IssueRemark') ?? '',
|
||||||
|
issueTime: pick(raw, 'issueTime', 'IssueTime') ?? null,
|
||||||
|
claimType: pick(raw, 'claimType', 'ClaimType') ?? '',
|
||||||
|
claimTypeName: pick(raw, 'claimTypeName', 'ClaimTypeName') ?? '',
|
||||||
|
claimAmount: pick(raw, 'claimAmount', 'ClaimAmount') ?? null,
|
||||||
|
claimCurrency: pick(raw, 'claimCurrency', 'ClaimCurrency') ?? '',
|
||||||
|
claimReason: pick(raw, 'claimReason', 'ClaimReason') ?? '',
|
||||||
|
claimTime: pick(raw, 'claimTime', 'ClaimTime') ?? null,
|
||||||
|
previousOrderStatus: pick(raw, 'previousOrderStatus', 'PreviousOrderStatus') ?? '',
|
||||||
|
cargoAttribute,
|
||||||
createTime: pick(raw, 'createTime', 'CreateTime') ?? raw?.createTime,
|
createTime: pick(raw, 'createTime', 'CreateTime') ?? raw?.createTime,
|
||||||
|
createBy: pick(raw, 'createBy', 'CreateBy') ?? '',
|
||||||
|
updateTime: pick(raw, 'updateTime', 'UpdateTime') ?? null,
|
||||||
|
updateBy: pick(raw, 'updateBy', 'UpdateBy') ?? '',
|
||||||
receiveTime: pick(raw, 'receiveTime', 'ReceiveTime') ?? raw?.receiveTime,
|
receiveTime: pick(raw, 'receiveTime', 'ReceiveTime') ?? raw?.receiveTime,
|
||||||
shipTime: pick(raw, 'shipTime', 'ShipTime') ?? raw?.shipTime,
|
shipTime: pick(raw, 'shipTime', 'ShipTime') ?? raw?.shipTime,
|
||||||
payTime: pick(raw, 'payTime', 'PayTime') ?? raw?.payTime,
|
payTime: pick(raw, 'payTime', 'PayTime') ?? raw?.payTime,
|
||||||
signTime: pick(raw, 'signTime', 'SignTime') ?? raw?.signTime,
|
signTime: pick(raw, 'signTime', 'SignTime') ?? raw?.signTime,
|
||||||
|
warehouseId: pick(raw, 'warehouseId', 'WarehouseId') ?? null,
|
||||||
warehouse: pick(raw, 'warehouse', 'Warehouse') ?? raw?.warehouse,
|
warehouse: pick(raw, 'warehouse', 'Warehouse') ?? raw?.warehouse,
|
||||||
customerId,
|
customerId,
|
||||||
customerName:
|
customerName:
|
||||||
pick(raw, 'customerName', 'CustomerName') ||
|
pick(raw, 'customerName', 'CustomerName') ||
|
||||||
customerFromOptions?.label?.replace(/\s*\([^)]*\)\s*$/, '') ||
|
String(customerLabel).replace(/\s*\([^)]*\)\s*$/, '') ||
|
||||||
'',
|
'',
|
||||||
customerCode:
|
customerCode:
|
||||||
pick(raw, 'customerCode', 'CustomerCode') ||
|
pick(raw, 'customerCode', 'CustomerCode') ||
|
||||||
(customerFromOptions?.label?.match(/\(([^)]+)\)\s*$/)?.[1] ?? '') ||
|
(String(customerLabel).match(/\(([^)]+)\)\s*$/)?.[1] ?? '') ||
|
||||||
'',
|
'',
|
||||||
|
orderSource: pick(raw, 'orderSource', 'OrderSource') ?? '',
|
||||||
|
pickupTime: pick(raw, 'pickupTime', 'PickupTime') ?? null,
|
||||||
|
pickupOrderId: pick(raw, 'pickupOrderId', 'PickupOrderId') ?? null,
|
||||||
carrierWaybillNo: pick(raw, 'carrierWaybillNo', 'CarrierWaybillNo') ?? '',
|
carrierWaybillNo: pick(raw, 'carrierWaybillNo', 'CarrierWaybillNo') ?? '',
|
||||||
pickupNo: pick(raw, 'pickupNo', 'PickupNo') ?? '',
|
pickupNo: pick(raw, 'pickupNo', 'PickupNo') ?? '',
|
||||||
inboundMethod: pick(raw, 'inboundMethod', 'InboundMethod') ?? '',
|
inboundMethod: pick(raw, 'inboundMethod', 'InboundMethod') ?? '',
|
||||||
fulfillmentType: pick(raw, 'fulfillmentType', 'FulfillmentType') ?? 'direct',
|
fulfillmentType,
|
||||||
fulfillmentSource: pick(raw, 'fulfillmentSource', 'FulfillmentSource') ?? '',
|
fulfillmentSource: pick(raw, 'fulfillmentSource', 'FulfillmentSource') ?? '',
|
||||||
stockBindStatus: pick(raw, 'stockBindStatus', 'StockBindStatus') ?? 'none',
|
stockBindStatus,
|
||||||
|
stockBindLineCount: pick(raw, 'stockBindLineCount', 'StockBindLineCount') ?? 0,
|
||||||
stockBindQtyTotal: pick(raw, 'stockBindQtyTotal', 'StockBindQtyTotal') ?? 0,
|
stockBindQtyTotal: pick(raw, 'stockBindQtyTotal', 'StockBindQtyTotal') ?? 0,
|
||||||
domesticTrackingNos: pick(raw, 'domesticTrackingNos', 'DomesticTrackingNos') ?? '',
|
domesticTrackingNos,
|
||||||
fulfillProgress: pick(raw, 'fulfillProgress', 'FulfillProgress') ?? '',
|
fulfillProgress,
|
||||||
primaryBinCode: pick(raw, 'primaryBinCode', 'PrimaryBinCode') ?? '',
|
primaryBinCode,
|
||||||
|
primarySkuCodes: pick(raw, 'primarySkuCodes', 'PrimarySkuCodes') ?? '',
|
||||||
|
customerSenderAddressId: pick(raw, 'customerSenderAddressId', 'CustomerSenderAddressId') ?? null,
|
||||||
|
requestId: pick(raw, 'requestId', 'RequestId') ?? '',
|
||||||
|
exportCustomsMode: pick(raw, 'exportCustomsMode', 'ExportCustomsMode') ?? '',
|
||||||
|
exportQualificationId: pick(raw, 'exportQualificationId', 'ExportQualificationId') ?? null,
|
||||||
|
// 预计算 HTML,减轻 Handsontable 每格重算
|
||||||
|
_orderStatusHtml: orderStatusHtml,
|
||||||
|
_predeclareFailReasonHtml: predeclareFailReasonHtml,
|
||||||
|
_allocationPendingReasonHtml: allocationPendingReasonHtml,
|
||||||
|
_isPaidHtml: buildBooleanTagHtml(
|
||||||
|
isPaid ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
|
||||||
|
isPaid,
|
||||||
|
),
|
||||||
|
_isCodHtml: buildBooleanTagHtml(
|
||||||
|
isCod ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
|
||||||
|
isCod,
|
||||||
|
),
|
||||||
|
_isInterceptHtml: buildBooleanTagHtml(
|
||||||
|
isIntercept ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
|
||||||
|
isIntercept,
|
||||||
|
),
|
||||||
|
_isRemoteHtml: buildBooleanTagHtml(
|
||||||
|
isRemote ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no'),
|
||||||
|
isRemote,
|
||||||
|
),
|
||||||
|
_waybillNoHtml: `<span data-row-status="${escapeHtml(String(statusCode || ''))}">${escapeHtml(String(waybillNo || '--'))}</span>`,
|
||||||
|
_consigneeCountryText:
|
||||||
|
consigneeCountry && consigneeCountryName
|
||||||
|
? `${consigneeCountry} ${consigneeCountryName}`
|
||||||
|
: consigneeCountryName || consigneeCountry || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +472,7 @@
|
|||||||
if (id != null && id !== '') {
|
if (id != null && id !== '') {
|
||||||
try {
|
try {
|
||||||
const detail = await getOrderDetail(id);
|
const detail = await getOrderDetail(id);
|
||||||
nextRow = normalizeOrderRow({ ...row, ...detail });
|
nextRow = normalizeOrderRow({ ...row, ...detail }, buildCustomerLabelMap());
|
||||||
} catch {
|
} catch {
|
||||||
/* 保留列表行数据 */
|
/* 保留列表行数据 */
|
||||||
}
|
}
|
||||||
@ -351,10 +487,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleOrderDetailUpdated(row: any) {
|
function handleOrderDetailUpdated(row: any) {
|
||||||
currentOrder.value = normalizeOrderRow(row);
|
const customerLabelMap = buildCustomerLabelMap();
|
||||||
|
currentOrder.value = normalizeOrderRow(row, customerLabelMap);
|
||||||
const idx = orderList.value.findIndex((it) => Number(it.id) === Number(row?.id));
|
const idx = orderList.value.findIndex((it) => Number(it.id) === Number(row?.id));
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
orderList.value[idx] = normalizeOrderRow({ ...orderList.value[idx], ...row });
|
// 替换数组引用,配合 deepWatchData=false 触发 Handsontable 刷新
|
||||||
|
const next = orderList.value.slice();
|
||||||
|
next[idx] = normalizeOrderRow({ ...orderList.value[idx], ...row }, customerLabelMap);
|
||||||
|
orderList.value = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,14 +855,6 @@
|
|||||||
const listLoading = ref(false);
|
const listLoading = ref(false);
|
||||||
let listRequestSeq = 0;
|
let listRequestSeq = 0;
|
||||||
|
|
||||||
function escapeHtml(text: string) {
|
|
||||||
return String(text)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedRowCount = ref(0);
|
const selectedRowCount = ref(0);
|
||||||
/** 表格选中行(用于批量操作) */
|
/** 表格选中行(用于批量操作) */
|
||||||
const selectedOrderRows = ref<any[]>([]);
|
const selectedOrderRows = ref<any[]>([]);
|
||||||
@ -1076,7 +1208,8 @@
|
|||||||
if (requestSeq !== listRequestSeq) {
|
if (requestSeq !== listRequestSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mapped = (items || []).map((row) => normalizeOrderRow(row));
|
const customerLabelMap = buildCustomerLabelMap();
|
||||||
|
const mapped = (items || []).map((row) => normalizeOrderRow(row, customerLabelMap));
|
||||||
orderList.value = mapped;
|
orderList.value = mapped;
|
||||||
totalCount.value = total;
|
totalCount.value = total;
|
||||||
tabCounts.value[status] = total;
|
tabCounts.value[status] = total;
|
||||||
@ -1748,7 +1881,15 @@
|
|||||||
const tableColumnsKey = computed(() => activeColumnKeys.value.join('|'));
|
const tableColumnsKey = computed(() => activeColumnKeys.value.join('|'));
|
||||||
|
|
||||||
function syncColumnCatalogFromRows(rows: Record<string, any>[]) {
|
function syncColumnCatalogFromRows(rows: Record<string, any>[]) {
|
||||||
columnCatalog.value = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
|
const next = mergeDiscoveredColumns(getWaybillBaseCatalog(), rows);
|
||||||
|
const prev = columnCatalog.value;
|
||||||
|
if (
|
||||||
|
prev.length === next.length &&
|
||||||
|
prev.every((col, idx) => col.key === next[idx]?.key)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
columnCatalog.value = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openColumnConfig() {
|
async function openColumnConfig() {
|
||||||
@ -1788,48 +1929,30 @@
|
|||||||
return wrapWarehouseActions(parts, rowIndex);
|
return wrapWarehouseActions(parts, rowIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 行底色用 class,避免 :has() 在虚拟滚动复用行时闪一下 */
|
||||||
|
function resolveWaybillRowClassName(rowData: any) {
|
||||||
|
const code = resolveOrderStatusCode(rowData);
|
||||||
|
if (code === 'problematic' || code === 'pendingPayment' || code === 'pendingAllocation') {
|
||||||
|
return `row-status-${code}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const customCellRenderer = (row: number, col: number, value: any, rowData: any) => {
|
const customCellRenderer = (row: number, col: number, value: any, rowData: any) => {
|
||||||
const colOffset = 2; // action + serialNo
|
const colOffset = 2; // action + serialNo
|
||||||
const dataColIndex = col - colOffset;
|
const dataColIndex = col - colOffset;
|
||||||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null;
|
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null;
|
||||||
const colKey = hotColumns.value[dataColIndex].key;
|
const colKey = hotColumns.value[dataColIndex].key;
|
||||||
|
|
||||||
if (colKey === 'orderStatus') {
|
if (colKey === 'orderStatus') return rowData?._orderStatusHtml || null;
|
||||||
const code = resolveOrderStatusCode(rowData);
|
if (colKey === 'predeclareFailReason') return rowData?._predeclareFailReasonHtml || null;
|
||||||
const meta = getOrderStatusMeta(code);
|
if (colKey === 'allocationPendingReason') return rowData?._allocationPendingReasonHtml || null;
|
||||||
const statusText = meta ? t(meta.labelKey) : (rowData?.orderStatus || code || '--');
|
if (colKey === 'isPaid') return rowData?._isPaidHtml || null;
|
||||||
const handleCode = resolveOrderHandleStatusCode(rowData);
|
if (colKey === 'isCod') return rowData?._isCodHtml || null;
|
||||||
const handleMeta = getOrderHandleStatusMeta(handleCode);
|
if (colKey === 'isIntercept') return rowData?._isInterceptHtml || null;
|
||||||
const handleText =
|
if (colKey === 'isRemote') return rowData?._isRemoteHtml || null;
|
||||||
rowData?.handleStatusName ||
|
if (colKey === 'waybillNo') return rowData?._waybillNoHtml || null;
|
||||||
(handleMeta ? t(handleMeta.labelKey) : '');
|
if (colKey === 'consigneeCountry') return rowData?._consigneeCountryText || null;
|
||||||
const showHandleTag = isOrderHandleSubTabParent(code) && handleText;
|
|
||||||
const handleTagHtml = showHandleTag
|
|
||||||
? buildOrderStatusSubTagHtml(String(handleText), handleMeta)
|
|
||||||
: '';
|
|
||||||
return `<span style="display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;max-width:100%;">${buildOrderStatusTagHtml(String(statusText), meta)}${handleTagHtml}</span>`;
|
|
||||||
}
|
|
||||||
if (colKey === 'predeclareFailReason') {
|
|
||||||
const reason = rowData?.predeclareFailReason || value || '';
|
|
||||||
if (!reason) return null;
|
|
||||||
const guide = buildFailGuideFromReason(String(reason));
|
|
||||||
const display = guide.reason || String(reason);
|
|
||||||
const escaped = escapeHtml(display);
|
|
||||||
const title = escapeHtml(guide.rawReason || String(reason));
|
|
||||||
return `<span title="${title}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8a5c58;font-size:12px;">${escaped}</span>`;
|
|
||||||
}
|
|
||||||
if (colKey === 'allocationPendingReason') {
|
|
||||||
const reason = rowData?.allocationPendingReason || value || '';
|
|
||||||
if (!reason) return null;
|
|
||||||
const escaped = escapeHtml(String(reason));
|
|
||||||
return `<span title="${escaped}" style="display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#7a6a52;font-size:12px;">${escaped}</span>`;
|
|
||||||
}
|
|
||||||
if (['isPaid', 'isCod', 'isIntercept', 'isRemote'].includes(colKey)) {
|
|
||||||
const rawVal = rowData?.[colKey] ?? value;
|
|
||||||
const yes = rawVal === true || rawVal === 1 || rawVal === '1';
|
|
||||||
const text = yes ? t('order.myOrders.drawer.yes') : t('order.myOrders.drawer.no');
|
|
||||||
return buildBooleanTagHtml(text, yes);
|
|
||||||
}
|
|
||||||
if (colKey === 'cargoAttribute') {
|
if (colKey === 'cargoAttribute') {
|
||||||
const raw = String(rowData?.cargoAttribute || value || '').trim().toLowerCase();
|
const raw = String(rowData?.cargoAttribute || value || '').trim().toLowerCase();
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
@ -1844,12 +1967,6 @@
|
|||||||
const tone = raw === 'sensitive' ? 'wait' : raw === 'general' ? 'mute' : 'neutral';
|
const tone = raw === 'sensitive' ? 'wait' : raw === 'general' ? 'mute' : 'neutral';
|
||||||
return buildStatusTagHtml(text, tone);
|
return buildStatusTagHtml(text, tone);
|
||||||
}
|
}
|
||||||
if (colKey === 'consigneeCountry') {
|
|
||||||
const code = rowData?.consigneeCountry || value || '';
|
|
||||||
const name = rowData?.consigneeCountryName || '';
|
|
||||||
if (code && name) return `${code} ${name}`;
|
|
||||||
return name || code || null;
|
|
||||||
}
|
|
||||||
if (['chargeableWeight', 'receivableAmount', 'declaredSum', 'codSum', 'claimAmount'].includes(colKey)) {
|
if (['chargeableWeight', 'receivableAmount', 'declaredSum', 'codSum', 'claimAmount'].includes(colKey)) {
|
||||||
return value != null && value !== '' ? Number(value).toLocaleString() : null;
|
return value != null && value !== '' ? Number(value).toLocaleString() : null;
|
||||||
}
|
}
|
||||||
@ -1901,12 +2018,6 @@
|
|||||||
const code = String(rowData?.primaryBinCode || value || '').trim();
|
const code = String(rowData?.primaryBinCode || value || '').trim();
|
||||||
return code || '--';
|
return code || '--';
|
||||||
}
|
}
|
||||||
// 首列添加 data-row-status 用于行背景样式
|
|
||||||
if (colKey === 'waybillNo') {
|
|
||||||
const statusCode = resolveOrderStatusCode(rowData);
|
|
||||||
const escaped = escapeHtml(String(value || '--'));
|
|
||||||
return `<span data-row-status="${statusCode}">${escaped}</span>`;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2252,7 +2363,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}-${tableColumnsKey}`"
|
:key="tableColumnsKey"
|
||||||
:columns="hotColumns"
|
:columns="hotColumns"
|
||||||
:data="processedTableData"
|
:data="processedTableData"
|
||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
@ -2261,7 +2372,9 @@
|
|||||||
:show-action-column="true"
|
:show-action-column="true"
|
||||||
:action-renderer="actionRenderer"
|
:action-renderer="actionRenderer"
|
||||||
:custom-cell-renderer="customCellRenderer"
|
:custom-cell-renderer="customCellRenderer"
|
||||||
:stretch-columns="true"
|
:resolve-row-class-name="resolveWaybillRowClassName"
|
||||||
|
:stretch-columns="false"
|
||||||
|
:deep-watch-data="false"
|
||||||
:height="tableHeight"
|
:height="tableHeight"
|
||||||
:search-params="headerSearchParams"
|
:search-params="headerSearchParams"
|
||||||
sort-mode="remote"
|
sort-mode="remote"
|
||||||
@ -2847,14 +2960,14 @@
|
|||||||
border-left: none !important;
|
border-left: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据订单状态设置行背景
|
// 根据订单状态设置行背景(class 由 resolveRowClassName 写入)
|
||||||
tbody tr:has([data-row-status="problematic"]) td {
|
tbody tr.row-status-problematic td {
|
||||||
background-color: #fff1f0 !important;
|
background-color: #fff1f0 !important;
|
||||||
}
|
}
|
||||||
tbody tr:has([data-row-status="pendingPayment"]) td {
|
tbody tr.row-status-pendingPayment td {
|
||||||
background-color: #ffece8 !important;
|
background-color: #ffece8 !important;
|
||||||
}
|
}
|
||||||
tbody tr:has([data-row-status="pendingAllocation"]) td {
|
tbody tr.row-status-pendingAllocation td {
|
||||||
background-color: #fff7e6 !important;
|
background-color: #fff7e6 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,26 +3,26 @@
|
|||||||
<div class="ai-shell-bg" aria-hidden="true">
|
<div class="ai-shell-bg" aria-hidden="true">
|
||||||
<GradientWaves
|
<GradientWaves
|
||||||
class="ai-shell-bg__waves"
|
class="ai-shell-bg__waves"
|
||||||
horizon-color="#023e8a"
|
horizon-color="#0369a1"
|
||||||
wave-color="#0077b6"
|
wave-color="#0ea5c9"
|
||||||
crest-color="#caf0f8"
|
crest-color="#e0f7fc"
|
||||||
:speed="0.34"
|
:speed="waveMotion ? 0.2 : 0"
|
||||||
:amplitude="3.3"
|
:amplitude="waveMotion ? 2.7 : 0"
|
||||||
:wave-scale="0.72"
|
:wave-scale="0.64"
|
||||||
:wave-ratio="0.82"
|
:wave-ratio="0.85"
|
||||||
:swell="42"
|
:swell="30"
|
||||||
:turbulence="26"
|
:turbulence="waveMotion ? 10 : 0"
|
||||||
:tilt="1.08"
|
:tilt="1.06"
|
||||||
:zoom="0.92"
|
:zoom="0.92"
|
||||||
:height="4.8"
|
:height="4.8"
|
||||||
:fog-depth="22"
|
:fog-depth="22"
|
||||||
detail="low"
|
detail="low"
|
||||||
:brightness="1.08"
|
:brightness="1.02"
|
||||||
:opacity="1.0"
|
:opacity="0.82"
|
||||||
:mouse-interaction="true"
|
:mouse-interaction="false"
|
||||||
:parallax-strength="0.4"
|
:parallax-strength="0"
|
||||||
:grain="false"
|
:grain="false"
|
||||||
:fps="30"
|
:fps="waveMotion ? 28 : 12"
|
||||||
:render-scale="0.72"
|
:render-scale="0.72"
|
||||||
:max-dpr="1.25"
|
:max-dpr="1.25"
|
||||||
/>
|
/>
|
||||||
@ -60,42 +60,42 @@
|
|||||||
IDE<span class="ai-welcome-title__dot">.</span>
|
IDE<span class="ai-welcome-title__dot">.</span>
|
||||||
</span>
|
</span>
|
||||||
<TextType
|
<TextType
|
||||||
|
v-if="titleAnimate"
|
||||||
as="span"
|
as="span"
|
||||||
class-name="ai-welcome-title__type"
|
class-name="ai-welcome-title__type"
|
||||||
:text="shortTitle ? copy.titleRestShort : copy.titleRest"
|
:text="shortTitle ? copy.titleRestShort : copy.titleRest"
|
||||||
:typing-speed="75"
|
:typing-speed="36"
|
||||||
:pause-duration="1500"
|
:pause-duration="900"
|
||||||
:show-cursor="true"
|
:show-cursor="false"
|
||||||
:loop="false"
|
:loop="false"
|
||||||
cursor-character="|"
|
|
||||||
cursor-class-name="ai-welcome-title__cursor"
|
|
||||||
/>
|
/>
|
||||||
|
<span v-else class="ai-welcome-title__type">{{
|
||||||
|
shortTitle ? copy.titleRestShort : copy.titleRest
|
||||||
|
}}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="ai-welcome-sub">{{ copy.sub }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-welcome__foot">
|
<div class="ai-welcome__meta">
|
||||||
<div class="ai-welcome-stats-block">
|
<ul class="ai-welcome-stats ai-welcome-stats--compact" :aria-label="copy.statsAria">
|
||||||
<ul class="ai-welcome-stats" :aria-label="copy.statsAria">
|
<li
|
||||||
<li
|
v-for="stat in HERO_STATS"
|
||||||
v-for="stat in HERO_STATS"
|
:key="stat.label.zh"
|
||||||
:key="stat.label.zh"
|
class="ai-welcome-stats__item"
|
||||||
class="ai-welcome-stats__item"
|
>
|
||||||
>
|
<strong class="ai-welcome-stats__value">{{ isZh ? stat.value.zh : stat.value.en }}</strong>
|
||||||
<strong class="ai-welcome-stats__value">{{ isZh ? stat.value.zh : stat.value.en }}</strong>
|
<span class="ai-welcome-stats__label">{{ isZh ? stat.label.zh : stat.label.en }}</span>
|
||||||
<span class="ai-welcome-stats__label">{{ isZh ? stat.label.zh : stat.label.en }}</span>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="ai-welcome__actions">
|
|
||||||
<a class="ai-welcome-cta" :href="loginUrl" @pointerenter="$emit('warm-login')" @focus="$emit('warm-login')">{{
|
|
||||||
copy.heroCta
|
|
||||||
}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-welcome__command">
|
<div class="ai-welcome__command">
|
||||||
<div class="ai-welcome-compose">
|
<div class="ai-welcome-compose-panel">
|
||||||
<p class="ai-welcome-compose__lead" :hidden="chatting">{{ copy.composeLead }}</p>
|
<a
|
||||||
|
class="ai-welcome-cta-link"
|
||||||
|
:href="loginUrl"
|
||||||
|
@pointerenter="$emit('warm-login')"
|
||||||
|
@focus="$emit('warm-login')"
|
||||||
|
>{{ copy.heroCta }}</a>
|
||||||
|
<p class="ai-welcome-compose__lead">{{ copy.composeLead }}</p>
|
||||||
<div class="ai-home-dests" role="group" :aria-label="copy.destAria">
|
<div class="ai-home-dests" role="group" :aria-label="copy.destAria">
|
||||||
<button
|
<button
|
||||||
v-for="dest in homeDests"
|
v-for="dest in homeDests"
|
||||||
@ -119,23 +119,49 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-intent-grid">
|
<div class="ai-intent-section">
|
||||||
<button
|
<div class="ai-intent-group">
|
||||||
v-for="door in HOME_DOORS"
|
<p class="ai-intent-group__label">{{ copy.groupFulfill }}</p>
|
||||||
:key="door.id"
|
<div class="ai-intent-grid ai-intent-grid--primary">
|
||||||
type="button"
|
<button
|
||||||
class="ai-intent-door"
|
v-for="door in primaryDoors"
|
||||||
:disabled="busy"
|
:key="door.id"
|
||||||
@click="onHomeDoor(door)"
|
type="button"
|
||||||
>
|
class="ai-intent-door is-primary"
|
||||||
<span class="ai-intent-door__icon">
|
:disabled="busy"
|
||||||
<SiteIllusIcon :name="door.icon" />
|
@click="onHomeDoor(door)"
|
||||||
</span>
|
>
|
||||||
<span class="ai-intent-door__copy">
|
<span class="ai-intent-door__icon">
|
||||||
<span class="ai-intent-door__title">{{ isZh ? door.zh.title : door.en.title }}</span>
|
<SiteIllusIcon :name="door.icon" />
|
||||||
<span class="ai-intent-door__hint">{{ isZh ? door.zh.hint : door.en.hint }}</span>
|
</span>
|
||||||
</span>
|
<span class="ai-intent-door__copy">
|
||||||
</button>
|
<span class="ai-intent-door__title">{{ isZh ? door.zh.title : door.en.title }}</span>
|
||||||
|
<span class="ai-intent-door__hint">{{ isZh ? door.zh.hint : door.en.hint }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ai-intent-group ai-intent-group--growth">
|
||||||
|
<p class="ai-intent-group__label">{{ copy.groupGrowth }}</p>
|
||||||
|
<div class="ai-intent-grid ai-intent-grid--growth">
|
||||||
|
<button
|
||||||
|
v-for="door in growthDoors"
|
||||||
|
:key="door.id"
|
||||||
|
type="button"
|
||||||
|
class="ai-intent-door"
|
||||||
|
:disabled="busy"
|
||||||
|
@click="onHomeDoor(door)"
|
||||||
|
>
|
||||||
|
<span class="ai-intent-door__icon">
|
||||||
|
<SiteIllusIcon :name="door.icon" />
|
||||||
|
</span>
|
||||||
|
<span class="ai-intent-door__copy">
|
||||||
|
<span class="ai-intent-door__title">{{ isZh ? door.zh.title : door.en.title }}</span>
|
||||||
|
<span class="ai-intent-door__hint">{{ isZh ? door.zh.hint : door.en.hint }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -309,7 +335,11 @@
|
|||||||
|
|
||||||
const copy = computed(() => (props.isZh ? WELCOME_COPY.zh : WELCOME_COPY.en));
|
const copy = computed(() => (props.isZh ? WELCOME_COPY.zh : WELCOME_COPY.en));
|
||||||
const homeDests = ROUTE_DEST_PRESETS.filter((item) => item.id !== 'all');
|
const homeDests = ROUTE_DEST_PRESETS.filter((item) => item.id !== 'all');
|
||||||
|
const primaryDoors = HOME_DOORS.filter((d) => d.primary);
|
||||||
|
const growthDoors = HOME_DOORS.filter((d) => !d.primary);
|
||||||
const shortTitle = ref(false);
|
const shortTitle = ref(false);
|
||||||
|
const titleAnimate = ref(true);
|
||||||
|
const waveMotion = ref(true);
|
||||||
const draft = computed({
|
const draft = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit('update:modelValue', value),
|
set: (value) => emit('update:modelValue', value),
|
||||||
@ -346,18 +376,25 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let mq: MediaQueryList | null = null;
|
let mqNarrow: MediaQueryList | null = null;
|
||||||
function syncTitle() {
|
let mqReduce: MediaQueryList | null = null;
|
||||||
shortTitle.value = !!mq?.matches;
|
|
||||||
|
function syncMediaPrefs() {
|
||||||
|
shortTitle.value = !!mqNarrow?.matches;
|
||||||
|
waveMotion.value = !mqReduce?.matches;
|
||||||
|
titleAnimate.value = !mqReduce?.matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
mq = window.matchMedia('(max-width: 640px)');
|
mqNarrow = window.matchMedia('(max-width: 640px)');
|
||||||
syncTitle();
|
mqReduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
mq.addEventListener('change', syncTitle);
|
syncMediaPrefs();
|
||||||
|
mqNarrow.addEventListener('change', syncMediaPrefs);
|
||||||
|
mqReduce.addEventListener('change', syncMediaPrefs);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
mq?.removeEventListener('change', syncTitle);
|
mqNarrow?.removeEventListener('change', syncMediaPrefs);
|
||||||
|
mqReduce?.removeEventListener('change', syncMediaPrefs);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
:href="loginUrl"
|
:href="loginUrl"
|
||||||
@pointerenter="$emit('warm-login')"
|
@pointerenter="$emit('warm-login')"
|
||||||
@focus="$emit('warm-login')"
|
@focus="$emit('warm-login')"
|
||||||
|
@click="onLoginClick"
|
||||||
>{{ isZh ? '登录' : 'Log in' }}</a>
|
>{{ isZh ? '登录' : 'Log in' }}</a>
|
||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
@ -69,6 +70,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import {
|
import {
|
||||||
@ -78,7 +80,7 @@
|
|||||||
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
import { applyRbacUserToUserStore } from '@/utils/enter-system';
|
||||||
import { DEFAULT_AVATAR_URL, isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
import { DEFAULT_AVATAR_URL, isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
||||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||||
import { officialAppHomeUrl } from './lib/site-urls';
|
import { goOfficialLogin, officialAppHomeUrl } from './lib/site-urls';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
nav: string;
|
nav: string;
|
||||||
@ -95,10 +97,15 @@
|
|||||||
'warm-login': [];
|
'warm-login': [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
const signedIn = computed(() => rbacUserStore.isLoggedIn);
|
const signedIn = computed(() => rbacUserStore.isLoggedIn);
|
||||||
const homeUrl = officialAppHomeUrl();
|
const homeUrl = officialAppHomeUrl();
|
||||||
|
|
||||||
|
function onLoginClick(e: MouseEvent) {
|
||||||
|
goOfficialLogin(router, undefined, e);
|
||||||
|
}
|
||||||
const avatarUrl = ref(DEFAULT_AVATAR_URL);
|
const avatarUrl = ref(DEFAULT_AVATAR_URL);
|
||||||
const displayName = computed(
|
const displayName = computed(
|
||||||
() =>
|
() =>
|
||||||
@ -196,12 +203,10 @@
|
|||||||
opacity: '0',
|
opacity: '0',
|
||||||
});
|
});
|
||||||
const sliderPos = { x: 0, w: 0 };
|
const sliderPos = { x: 0, w: 0 };
|
||||||
let settleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let sliderAnimating = false;
|
|
||||||
let resizeObserver: ResizeObserver | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
let sliderRaf = 0;
|
||||||
|
|
||||||
const SLIDER_INSET = 4;
|
const SLIDER_INSET = 4;
|
||||||
const LIQUID_STRETCH_MS = 120;
|
|
||||||
|
|
||||||
function applySlider(x: number, w: number, opacity = '1') {
|
function applySlider(x: number, w: number, opacity = '1') {
|
||||||
sliderPos.x = x;
|
sliderPos.x = x;
|
||||||
@ -213,74 +218,47 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readLiveSlider() {
|
|
||||||
const slider = dockRef.value?.querySelector<HTMLElement>('.site-nav-dock__slider');
|
|
||||||
if (!slider) return { ...sliderPos };
|
|
||||||
const cs = getComputedStyle(slider);
|
|
||||||
return {
|
|
||||||
x: Number.parseFloat(cs.left) || sliderPos.x,
|
|
||||||
w: Number.parseFloat(cs.width) || sliderPos.w,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSettleTimer() {
|
|
||||||
if (settleTimer) {
|
|
||||||
clearTimeout(settleTimer);
|
|
||||||
settleTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSlider(animate: boolean) {
|
function updateSlider(animate: boolean) {
|
||||||
const dock = dockRef.value;
|
const dock = dockRef.value;
|
||||||
const active = dock?.querySelector<HTMLElement>('.site-nav-dock__item.is-active');
|
const active = dock?.querySelector<HTMLElement>('.site-nav-dock__item.is-active');
|
||||||
if (!dock || !active || window.matchMedia('(max-width: 1279px)').matches) {
|
if (!dock || !active || window.matchMedia('(max-width: 1279px)').matches) {
|
||||||
sliderStyle.value = { ...sliderStyle.value, opacity: '0' };
|
sliderStyle.value = { ...sliderStyle.value, opacity: '0' };
|
||||||
sliderAnimating = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = active.offsetLeft + SLIDER_INSET;
|
const x = active.offsetLeft + SLIDER_INSET;
|
||||||
const w = Math.max(active.offsetWidth - SLIDER_INSET * 2, 0);
|
const w = Math.max(active.offsetWidth - SLIDER_INSET * 2, 0);
|
||||||
clearSettleTimer();
|
|
||||||
|
|
||||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
// 首帧或关动画:先贴到位再开 transition,避免初次跳动
|
||||||
if (!animate || !sliderReady.value || sliderPos.w <= 0 || reduceMotion) {
|
if (!animate || !sliderReady.value || sliderPos.w <= 0 || reduceMotion) {
|
||||||
sliderReady.value = false;
|
sliderReady.value = false;
|
||||||
applySlider(x, w);
|
applySlider(x, w);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
sliderReady.value = true;
|
sliderReady.value = true;
|
||||||
});
|
});
|
||||||
sliderAnimating = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = readLiveSlider();
|
if (Math.abs(sliderPos.x - x) < 0.5 && Math.abs(sliderPos.w - w) < 0.5) {
|
||||||
if (Math.abs(from.x - x) < 1 && Math.abs(from.w - w) < 1) {
|
|
||||||
applySlider(x, w);
|
applySlider(x, w);
|
||||||
sliderAnimating = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverLeft = Math.min(from.x, x);
|
// 单段滑移:left/width 一次到位,交给 CSS 缓动
|
||||||
const coverWidth = Math.max(from.x + from.w, x + w) - coverLeft;
|
applySlider(x, w);
|
||||||
sliderAnimating = true;
|
|
||||||
applySlider(coverLeft, coverWidth);
|
|
||||||
|
|
||||||
settleTimer = setTimeout(() => {
|
|
||||||
applySlider(x, w);
|
|
||||||
sliderAnimating = false;
|
|
||||||
settleTimer = null;
|
|
||||||
}, LIQUID_STRETCH_MS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncSlider(animate: boolean) {
|
async function syncSlider(animate: boolean) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateSlider(animate);
|
if (sliderRaf) cancelAnimationFrame(sliderRaf);
|
||||||
|
sliderRaf = requestAnimationFrame(() => {
|
||||||
|
sliderRaf = 0;
|
||||||
|
updateSlider(animate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onViewportResize() {
|
function onViewportResize() {
|
||||||
if (sliderAnimating) return;
|
|
||||||
updateSlider(false);
|
updateSlider(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +273,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearSettleTimer();
|
if (sliderRaf) cancelAnimationFrame(sliderRaf);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
window.removeEventListener('resize', onViewportResize);
|
window.removeEventListener('resize', onViewportResize);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -72,10 +72,35 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="ai-routes-toolbar">
|
<div class="ai-routes-toolbar">
|
||||||
<p v-if="metaText" class="ai-routes-meta">{{ metaText }}</p>
|
<p v-if="metaText" class="ai-routes-meta">{{ metaText }}</p>
|
||||||
|
<nav
|
||||||
|
v-if="loadState === 'ready' && filtered.length > 0 && pageCount > 1"
|
||||||
|
class="ai-routes-pager"
|
||||||
|
:aria-label="isZh ? '线路分页' : 'Route pagination'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ai-routes-pager__btn"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
:aria-label="isZh ? '上一页' : 'Previous page'"
|
||||||
|
@click="goPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<span class="ai-routes-pager__label">{{ currentPage }} / {{ pageCount }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ai-routes-pager__btn"
|
||||||
|
:disabled="currentPage >= pageCount"
|
||||||
|
:aria-label="isZh ? '下一页' : 'Next page'"
|
||||||
|
@click="goPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
<button type="button" class="ai-routes-sticky" @click="askDirect">{{ stickyLabel }}</button>
|
<button type="button" class="ai-routes-sticky" @click="askDirect">{{ stickyLabel }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-routes-panel__body">
|
<div ref="bodyRef" class="ai-routes-panel__body">
|
||||||
<div v-if="loadState === 'loading'" class="ai-routes-state">
|
<div v-if="loadState === 'loading'" class="ai-routes-state">
|
||||||
<div class="ai-routes-skeleton"></div>
|
<div class="ai-routes-skeleton"></div>
|
||||||
<div class="ai-routes-skeleton"></div>
|
<div class="ai-routes-skeleton"></div>
|
||||||
@ -101,7 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<article v-for="item in visibleItems" :key="String(item.id)" class="ai-route-card">
|
<article v-for="item in pageItems" :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
|
<span
|
||||||
@ -120,7 +145,6 @@
|
|||||||
<span v-if="item.tradeTerms === 'DDP'"> · {{ copy.ddpHint }}</span>
|
<span v-if="item.tradeTerms === 'DDP'"> · {{ copy.ddpHint }}</span>
|
||||||
<span v-else-if="item.tradeTerms === 'DDU'"> · {{ copy.dduHint }}</span>
|
<span v-else-if="item.tradeTerms === 'DDU'"> · {{ copy.dduHint }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="ai-route-card__cargo">{{ routeCargoHint(item, catalog, lang) }}</p>
|
|
||||||
<p
|
<p
|
||||||
v-if="item.productDescription"
|
v-if="item.productDescription"
|
||||||
class="ai-route-card__desc"
|
class="ai-route-card__desc"
|
||||||
@ -170,18 +194,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<div v-if="visibleItems.length < filtered.length" class="ai-routes-more">
|
|
||||||
<button type="button" class="ai-routes-more__btn" @click="visibleCount += ROUTE_PAGE_SIZE">
|
|
||||||
{{ copy.loadMore }}({{ filtered.length - visibleItems.length }})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { officialLoginUrl } from './lib/site-urls';
|
import { officialLoginUrl } from './lib/site-urls';
|
||||||
import {
|
import {
|
||||||
ROUTE_DEST_PRESETS,
|
ROUTE_DEST_PRESETS,
|
||||||
@ -193,7 +212,6 @@
|
|||||||
fetchRoutesCatalog,
|
fetchRoutesCatalog,
|
||||||
filterRouteItems,
|
filterRouteItems,
|
||||||
formatRouteTimeliness,
|
formatRouteTimeliness,
|
||||||
routeCargoHint,
|
|
||||||
routeCoverageLabel,
|
routeCoverageLabel,
|
||||||
type RoutesCatalog,
|
type RoutesCatalog,
|
||||||
type RouteItem,
|
type RouteItem,
|
||||||
@ -217,7 +235,8 @@
|
|||||||
const groupFilter = ref('all');
|
const groupFilter = ref('all');
|
||||||
const termsFilter = ref('all');
|
const termsFilter = ref('all');
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
const visibleCount = ref(ROUTE_PAGE_SIZE);
|
const currentPage = ref(1);
|
||||||
|
const bodyRef = ref<HTMLElement | null>(null);
|
||||||
const expanded = ref<Record<string, boolean>>({});
|
const expanded = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
const groupChips = computed(() => {
|
const groupChips = computed(() => {
|
||||||
@ -241,7 +260,14 @@
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibleItems = computed(() => filtered.value.slice(0, visibleCount.value));
|
const pageCount = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(filtered.value.length / ROUTE_PAGE_SIZE) || 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageItems = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * ROUTE_PAGE_SIZE;
|
||||||
|
return filtered.value.slice(start, start + ROUTE_PAGE_SIZE);
|
||||||
|
});
|
||||||
|
|
||||||
const hasFilters = computed(
|
const hasFilters = computed(
|
||||||
() =>
|
() =>
|
||||||
@ -253,12 +279,7 @@
|
|||||||
|
|
||||||
const metaText = computed(() => {
|
const metaText = computed(() => {
|
||||||
if (loadState.value !== 'ready') return '';
|
if (loadState.value !== 'ready') return '';
|
||||||
const total = filtered.value.length;
|
return copy.value.matchCount.replace('{n}', String(filtered.value.length));
|
||||||
const shown = visibleItems.value.length;
|
|
||||||
if (shown < total) {
|
|
||||||
return copy.value.matchShown.replace('{shown}', String(shown)).replace('{total}', String(total));
|
|
||||||
}
|
|
||||||
return copy.value.matchCount.replace('{n}', String(total));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const stickyLabel = computed(() => {
|
const stickyLabel = computed(() => {
|
||||||
@ -268,7 +289,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function resetPage() {
|
function resetPage() {
|
||||||
visibleCount.value = ROUTE_PAGE_SIZE;
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goPage(page: number) {
|
||||||
|
const next = Math.min(pageCount.value, Math.max(1, page));
|
||||||
|
if (next === currentPage.value) return;
|
||||||
|
currentPage.value = next;
|
||||||
|
void nextTick(() => {
|
||||||
|
if (bodyRef.value) bodyRef.value.scrollTop = 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDest(id: string) {
|
function setDest(id: string) {
|
||||||
@ -324,6 +354,10 @@
|
|||||||
|
|
||||||
watch(search, () => resetPage());
|
watch(search, () => resetPage());
|
||||||
|
|
||||||
|
watch(pageCount, (count) => {
|
||||||
|
if (currentPage.value > count) currentPage.value = count;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void reload();
|
void reload();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
'is-services-open': panel === 'services',
|
'is-services-open': panel === 'services',
|
||||||
'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact',
|
'is-topic-open': panel === 'pricing' || panel === 'about' || panel === 'contact',
|
||||||
}"
|
}"
|
||||||
|
@click.capture="onOfficialLoginCapture"
|
||||||
>
|
>
|
||||||
<SiteHeader
|
<SiteHeader
|
||||||
:nav="nav"
|
:nav="nav"
|
||||||
@ -57,7 +58,7 @@
|
|||||||
@click="closePanel"
|
@click="closePanel"
|
||||||
></button>
|
></button>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition name="site-sheet" mode="out-in">
|
<Transition name="site-sheet">
|
||||||
<SiteRoutesPanel
|
<SiteRoutesPanel
|
||||||
v-if="panel === 'routes'"
|
v-if="panel === 'routes'"
|
||||||
key="routes"
|
key="routes"
|
||||||
@ -91,13 +92,18 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import SiteHeader from './SiteHeader.vue';
|
import SiteHeader from './SiteHeader.vue';
|
||||||
import SiteAdvisorShell from './SiteAdvisorShell.vue';
|
import SiteAdvisorShell from './SiteAdvisorShell.vue';
|
||||||
import SiteRoutesPanel from './SiteRoutesPanel.vue';
|
import SiteRoutesPanel from './SiteRoutesPanel.vue';
|
||||||
import SiteServicesPanel from './SiteServicesPanel.vue';
|
import SiteServicesPanel from './SiteServicesPanel.vue';
|
||||||
import SiteTopicPanel from './SiteTopicPanel.vue';
|
import SiteTopicPanel from './SiteTopicPanel.vue';
|
||||||
import SiteWecomModal from './SiteWecomModal.vue';
|
import SiteWecomModal from './SiteWecomModal.vue';
|
||||||
import { officialLoginUrl, warmupOfficialLogin } from './lib/site-urls';
|
import {
|
||||||
|
goOfficialLoginFromHref,
|
||||||
|
officialLoginUrl,
|
||||||
|
warmupOfficialLogin,
|
||||||
|
} from './lib/site-urls';
|
||||||
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
|
import { shouldOpenWecomDirect, WECOM_KF_URL } from './lib/site-wecom';
|
||||||
import { useSiteAdvisor } from './useSiteAdvisor';
|
import { useSiteAdvisor } from './useSiteAdvisor';
|
||||||
import './styles/vendor.css';
|
import './styles/vendor.css';
|
||||||
@ -112,6 +118,7 @@
|
|||||||
const DESCRIPTION =
|
const DESCRIPTION =
|
||||||
'IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。';
|
'IDE 面向中小卖家的跨境物流。全球数字化一站式跨境物流服务商。注册登记后即可询价,一口价不砍,专属顾问跟到签收。';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const lang = ref<'zh' | 'en'>('zh');
|
const lang = ref<'zh' | 'en'>('zh');
|
||||||
const nav = ref('home');
|
const nav = ref('home');
|
||||||
const panel = ref<PanelId>(null);
|
const panel = ref<PanelId>(null);
|
||||||
@ -122,6 +129,16 @@
|
|||||||
const isZh = computed(() => lang.value === 'zh');
|
const isZh = computed(() => lang.value === 'zh');
|
||||||
const loginUrl = computed(() => officialLoginUrl());
|
const loginUrl = computed(() => officialLoginUrl());
|
||||||
|
|
||||||
|
/** 顾问卡片等其它「去登录」链接统一走 SPA 翻页 */
|
||||||
|
function onOfficialLoginCapture(e: MouseEvent) {
|
||||||
|
const target = e.target as Element | null;
|
||||||
|
const anchor = target?.closest?.('a[href]') as HTMLAnchorElement | null;
|
||||||
|
if (!anchor || anchor.classList.contains('site-nav-login')) return;
|
||||||
|
const href = anchor.getAttribute('href') || '';
|
||||||
|
if (!href.includes('from=official-site')) return;
|
||||||
|
goOfficialLoginFromHref(router, href, e);
|
||||||
|
}
|
||||||
|
|
||||||
function focusComposer() {
|
function focusComposer() {
|
||||||
const input = document.querySelector('.ai-composer-input') as HTMLTextAreaElement | null;
|
const input = document.querySelector('.ai-composer-input') as HTMLTextAreaElement | null;
|
||||||
input?.focus();
|
input?.focus();
|
||||||
@ -375,7 +392,7 @@
|
|||||||
|
|
||||||
.site-fade-enter-active,
|
.site-fade-enter-active,
|
||||||
.site-fade-leave-active {
|
.site-fade-leave-active {
|
||||||
transition: opacity 0.22s cubic-bezier(0.22, 1, 0.36, 1);
|
transition: opacity 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-fade-enter-from,
|
.site-fade-enter-from,
|
||||||
@ -386,20 +403,45 @@
|
|||||||
.site-sheet-enter-active,
|
.site-sheet-enter-active,
|
||||||
.site-sheet-leave-active {
|
.site-sheet-leave-active {
|
||||||
transition:
|
transition:
|
||||||
opacity 0.24s cubic-bezier(0.22, 1, 0.36, 1),
|
opacity 0.34s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
transform 0.24s cubic-bezier(0.22, 1, 0.36, 1);
|
transform 0.4s cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
|
filter 0.34s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-sheet-leave-active {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-sheet-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(2px);
|
||||||
|
transform: translateX(-50%) translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-sheet-enter-from,
|
|
||||||
.site-sheet-leave-to {
|
.site-sheet-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-50%) translateY(-12px) scale(0.985);
|
filter: blur(1px);
|
||||||
|
transform: translateX(-50%) translateY(-6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 780px) {
|
@media (max-width: 780px) {
|
||||||
.site-sheet-enter-from,
|
.site-sheet-enter-from {
|
||||||
|
filter: none;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
|
||||||
.site-sheet-leave-to {
|
.site-sheet-leave-to {
|
||||||
transform: translateY(18px) scale(1);
|
filter: none;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.site-fade-enter-active,
|
||||||
|
.site-fade-leave-active,
|
||||||
|
.site-sheet-enter-active,
|
||||||
|
.site-sheet-leave-active {
|
||||||
|
transition-duration: 0.01ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,18 +1,93 @@
|
|||||||
export function officialAppHomeUrl() {
|
import type { Router } from 'vue-router';
|
||||||
|
import { armOfficialLoginFlip } from '@/utils/page-transition-flag';
|
||||||
|
|
||||||
|
function preferMobilePortal() {
|
||||||
const ua = navigator.userAgent || '';
|
const ua = navigator.userAgent || '';
|
||||||
const mobile = /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua);
|
return /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua);
|
||||||
return new URL(mobile ? '/h5' : '/home/workplace', window.location.origin).toString();
|
}
|
||||||
|
|
||||||
|
export function officialAppHomeUrl() {
|
||||||
|
return new URL(
|
||||||
|
preferMobilePortal() ? '/h5' : '/home/workplace',
|
||||||
|
window.location.origin,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function officialLoginLocation(dest?: string) {
|
||||||
|
const path = preferMobilePortal() ? '/h5/login' : '/login';
|
||||||
|
const query: Record<string, string> = { from: 'official-site' };
|
||||||
|
if (dest) query.dest = dest;
|
||||||
|
return { path, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function officialLoginUrl(dest?: string) {
|
export function officialLoginUrl(dest?: string) {
|
||||||
const ua = navigator.userAgent || '';
|
const { path, query } = officialLoginLocation(dest);
|
||||||
const mobile = /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua);
|
const url = new URL(path, window.location.origin);
|
||||||
const url = new URL(mobile ? '/h5/login' : '/login', window.location.origin);
|
Object.entries(query).forEach(([key, value]) => {
|
||||||
url.searchParams.set('from', 'official-site');
|
url.searchParams.set(key, value);
|
||||||
if (dest) url.searchParams.set('dest', dest);
|
});
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 同域 SPA 进登录;普通左键走翻页,修饰键仍允许新标签打开 */
|
||||||
|
export function goOfficialLogin(
|
||||||
|
router: Router,
|
||||||
|
dest?: string,
|
||||||
|
event?: MouseEvent,
|
||||||
|
) {
|
||||||
|
if (event) {
|
||||||
|
if (event.defaultPrevented) return false;
|
||||||
|
if (
|
||||||
|
event.button !== 0 ||
|
||||||
|
event.metaKey ||
|
||||||
|
event.ctrlKey ||
|
||||||
|
event.shiftKey ||
|
||||||
|
event.altKey
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
if (!reduceMotion) {
|
||||||
|
armOfficialLoginFlip();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先确保登录分包就绪,再开翻页,避免顶栏进度条干等
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
if (preferMobilePortal()) {
|
||||||
|
await import('@/views/h5/customer-center/login/index.vue');
|
||||||
|
} else {
|
||||||
|
await import('@/views/login/index.vue');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore:仍继续跳转 */
|
||||||
|
}
|
||||||
|
await router.push(officialLoginLocation(dest));
|
||||||
|
})();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从已有 login URL 解析 dest 后 SPA 跳转 */
|
||||||
|
export function goOfficialLoginFromHref(
|
||||||
|
router: Router,
|
||||||
|
href: string,
|
||||||
|
event?: MouseEvent,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const url = new URL(href, window.location.origin);
|
||||||
|
if (url.origin !== window.location.origin) return false;
|
||||||
|
if (url.pathname !== '/login' && url.pathname !== '/h5/login') return false;
|
||||||
|
if (url.searchParams.get('from') !== 'official-site') return false;
|
||||||
|
const dest = url.searchParams.get('dest') || undefined;
|
||||||
|
return goOfficialLogin(router, dest, event);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function warmupOfficialLogin() {
|
export function warmupOfficialLogin() {
|
||||||
const warmed = document.documentElement.dataset.omsLoginWarm;
|
const warmed = document.documentElement.dataset.omsLoginWarm;
|
||||||
if (warmed) return;
|
if (warmed) return;
|
||||||
@ -28,16 +103,22 @@ export function warmupOfficialLogin() {
|
|||||||
prefetch.rel = 'prefetch';
|
prefetch.rel = 'prefetch';
|
||||||
prefetch.href = url;
|
prefetch.href = url;
|
||||||
document.head.appendChild(prefetch);
|
document.head.appendChild(prefetch);
|
||||||
|
// 预拉取登录分包,翻页进入时少等 chunk
|
||||||
|
void import('@/views/login/index.vue');
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function officialTrackLoginUrl(number?: string) {
|
export function officialTrackLoginUrl(number?: string) {
|
||||||
const ua = navigator.userAgent || '';
|
const url = new URL(
|
||||||
const mobile = /MicroMessenger|Android|iPhone|iPod|iPad|Mobile/i.test(ua);
|
preferMobilePortal() ? '/h5/login' : '/login',
|
||||||
const url = new URL(mobile ? '/h5/login' : '/login', window.location.origin);
|
window.location.origin,
|
||||||
url.searchParams.set('redirect', mobile ? '/h5/orders' : '/home/workplace');
|
);
|
||||||
|
url.searchParams.set(
|
||||||
|
'redirect',
|
||||||
|
preferMobilePortal() ? '/h5/orders' : '/home/workplace',
|
||||||
|
);
|
||||||
if (number) url.searchParams.set('track', number);
|
if (number) url.searchParams.set('track', number);
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ export const WELCOME_COPY = {
|
|||||||
titleShort: 'IDE. Logistics advisor',
|
titleShort: 'IDE. Logistics advisor',
|
||||||
titleRest: ' Your cross-border logistics advisor',
|
titleRest: ' Your cross-border logistics advisor',
|
||||||
titleRestShort: ' Logistics advisor',
|
titleRestShort: ' Logistics advisor',
|
||||||
sub: 'US, EU and Southeast Asia parcels. List rates after you register. One advisor through to delivery.',
|
|
||||||
placeholder: 'e.g. 5 kg general cargo to the US, how many days?',
|
placeholder: 'e.g. 5 kg general cargo to the US, how many days?',
|
||||||
sendLabel: 'Ask',
|
sendLabel: 'Ask',
|
||||||
human: 'Talk to an advisor',
|
human: 'Talk to an advisor',
|
||||||
@ -18,6 +17,8 @@ export const WELCOME_COPY = {
|
|||||||
statsAria: 'Service highlights',
|
statsAria: 'Service highlights',
|
||||||
composeLead: 'Where are you shipping? Pick a destination, or ask below.',
|
composeLead: 'Where are you shipping? Pick a destination, or ask below.',
|
||||||
destAria: 'Pick a destination',
|
destAria: 'Pick a destination',
|
||||||
|
groupFulfill: 'Shipping',
|
||||||
|
groupGrowth: 'Benefits',
|
||||||
partnerTag: 'Partners',
|
partnerTag: 'Partners',
|
||||||
partnerAria: 'Connected platforms and carriers',
|
partnerAria: 'Connected platforms and carriers',
|
||||||
trustIcp: '粤ICP备2025463970号-2',
|
trustIcp: '粤ICP备2025463970号-2',
|
||||||
@ -29,7 +30,6 @@ export const WELCOME_COPY = {
|
|||||||
titleShort: 'IDE.跨境物流顾问',
|
titleShort: 'IDE.跨境物流顾问',
|
||||||
titleRest: '您身边的跨境物流顾问',
|
titleRest: '您身边的跨境物流顾问',
|
||||||
titleRestShort: '跨境物流顾问',
|
titleRestShort: '跨境物流顾问',
|
||||||
sub: '中小卖家也能按线路看一口价。不砍价、不拉大群,一个人从报价跟到签收。',
|
|
||||||
placeholder: '例如:发美国普货,5kg,要几天',
|
placeholder: '例如:发美国普货,5kg,要几天',
|
||||||
sendLabel: '提问',
|
sendLabel: '提问',
|
||||||
human: '联系顾问',
|
human: '联系顾问',
|
||||||
@ -43,6 +43,8 @@ export const WELCOME_COPY = {
|
|||||||
statsAria: '服务数据',
|
statsAria: '服务数据',
|
||||||
composeLead: '发到哪?点目的地看线路,或直接提问。',
|
composeLead: '发到哪?点目的地看线路,或直接提问。',
|
||||||
destAria: '选择目的地',
|
destAria: '选择目的地',
|
||||||
|
groupFulfill: '履约',
|
||||||
|
groupGrowth: '权益',
|
||||||
partnerTag: '合作伙伴',
|
partnerTag: '合作伙伴',
|
||||||
partnerAria: '对接平台与服务商',
|
partnerAria: '对接平台与服务商',
|
||||||
trustIcp: '粤ICP备2025463970号-2',
|
trustIcp: '粤ICP备2025463970号-2',
|
||||||
@ -97,6 +99,7 @@ export const SITE_INTENTS = [
|
|||||||
{
|
{
|
||||||
id: 'track',
|
id: 'track',
|
||||||
icon: 'track',
|
icon: 'track',
|
||||||
|
followup: false,
|
||||||
en: {
|
en: {
|
||||||
title: 'Where is my parcel?',
|
title: 'Where is my parcel?',
|
||||||
hint: 'Sign in to see live scans from pickup to delivery',
|
hint: 'Sign in to see live scans from pickup to delivery',
|
||||||
@ -108,6 +111,20 @@ export const SITE_INTENTS = [
|
|||||||
label: '这票货从揽收到派送卡在哪',
|
label: '这票货从揽收到派送卡在哪',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'warehouse',
|
||||||
|
icon: 'track',
|
||||||
|
en: {
|
||||||
|
title: 'Warehouse fulfillment',
|
||||||
|
hint: 'Inbound, pick & pack — ship when orders land',
|
||||||
|
label: 'I need warehouse dropshipping',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
title: '仓储代发',
|
||||||
|
hint: '入仓拣货打包,订单来了就发出去',
|
||||||
|
label: '我要仓储一件代发',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'hard',
|
id: 'hard',
|
||||||
icon: 'hard',
|
icon: 'hard',
|
||||||
@ -207,43 +224,49 @@ export const HOME_DOORS = [
|
|||||||
id: 'hard',
|
id: 'hard',
|
||||||
icon: 'hard',
|
icon: 'hard',
|
||||||
intentId: 'hard' as const,
|
intentId: 'hard' as const,
|
||||||
zh: { title: '带电、别人不接也能问', hint: '先帮你看出得去的线路,再谈税。' },
|
primary: true,
|
||||||
en: { title: 'Battery or declined cargo', hint: 'We find a lane that can leave, then the tax path.' },
|
zh: { title: '敏感难发', hint: '先帮你看出得去的线路,再谈税。' },
|
||||||
|
en: { title: 'Sensitive cargo', hint: 'We find a lane that can leave, then the tax path.' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'track',
|
id: 'warehouse',
|
||||||
icon: 'track',
|
icon: 'track',
|
||||||
intentId: 'track' as const,
|
intentId: 'warehouse' as const,
|
||||||
zh: { title: '货到哪了', hint: '登录后看揽收到派送的真实节点。' },
|
primary: true,
|
||||||
en: { title: 'Where is my parcel?', hint: 'Sign in to see scans from pickup to delivery.' },
|
zh: { title: '仓储代发', hint: '入仓拣货打包,订单来了就发出去。' },
|
||||||
|
en: { title: 'Warehouse fulfillment', hint: 'Inbound, pick & pack — ship when orders land.' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'clearance',
|
id: 'clearance',
|
||||||
icon: 'clearance',
|
icon: 'clearance',
|
||||||
intentId: 'compliance' as const,
|
intentId: 'compliance' as const,
|
||||||
zh: { title: '出口报关 / 退税', hint: '正式报关、退税,或没有专票怎么出。' },
|
primary: true,
|
||||||
en: { title: 'Export paperwork', hint: 'Declaration, VAT refund, or shipping without invoices.' },
|
zh: { title: '报关退税', hint: '正式报关、退税,或没有专票怎么出。' },
|
||||||
|
en: { title: 'Customs & VAT', hint: 'Declaration, refund, or shipping without invoices.' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'member',
|
id: 'member',
|
||||||
icon: 'growth',
|
icon: 'growth',
|
||||||
intentId: 'member' as const,
|
intentId: 'member' as const,
|
||||||
zh: { title: '会员成长体系', hint: '近 12 个月发货积分定级,基础运费最高 95 折。' },
|
primary: false,
|
||||||
en: { title: 'Member growth', hint: 'Rolling 12-month shipping points — up to 95% on base freight.' },
|
zh: { title: '会员成长', hint: '近 12 个月发货积分定级,基础运费最高 95 折。' },
|
||||||
|
en: { title: 'Member tiers', hint: '12-month shipping points — up to 95% on base freight.' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'invite',
|
id: 'invite',
|
||||||
icon: 'coin',
|
icon: 'coin',
|
||||||
intentId: 'invite' as const,
|
intentId: 'invite' as const,
|
||||||
|
primary: false,
|
||||||
zh: { title: '邀新分成', hint: '好友派送成功,你拿运费余额 + 20 元券。' },
|
zh: { title: '邀新分成', hint: '好友派送成功,你拿运费余额 + 20 元券。' },
|
||||||
en: { title: 'Invite & earn', hint: 'When friends deliver, you get freight credit + a ¥20 coupon.' },
|
en: { title: 'Invite & earn', hint: 'Friends deliver → freight credit + ¥20 coupon.' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'newcomer',
|
id: 'newcomer',
|
||||||
icon: 'coupon',
|
icon: 'coupon',
|
||||||
intentId: 'newcomer' as const,
|
intentId: 'newcomer' as const,
|
||||||
zh: { title: '新人注册福利', hint: '注册送券:首单、备货、第二单都有。' },
|
primary: false,
|
||||||
en: { title: 'New-seller coupons', hint: 'Sign up for first-ship, stock-up, and second-ship coupons.' },
|
zh: { title: '新人福利', hint: '注册送券:首单、备货、第二单都有。' },
|
||||||
|
en: { title: 'New-seller deals', hint: 'Coupons for first ship, stock-up, and second ship.' },
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user