feat: 客户门户可折叠侧栏导航,去掉页签并区分打印入口提示
All checks were successful
Build and Deploy Front / deploy (push) Successful in 17m52s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 17m52s
运营端顶栏布局不变;客户侧栏按常用/仓储/更多分组,余额与打印沉底,打印提示按门户区分。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fbf1064ab8
commit
88909258cc
@ -27,19 +27,31 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
a-layout (垂直)
|
a-layout (垂直)
|
||||||
├── TopNav(顶部导航,fixed)
|
├── TopNav(仅运营端,fixed)
|
||||||
├── a-layout-content
|
├── a-layout-content
|
||||||
│ ├── paddingTop: 顶部导航高度 + Tab 栏高度(可选)
|
│ ├── 运营端:paddingTop = 顶栏 + Tab;PageLayout
|
||||||
│ └── PageLayout(router-view + keep-alive)
|
│ └── 客户门户:CustomerSideRail(全高)+ PageLayout(无顶栏、无多页签)
|
||||||
└── Footer(可选)
|
└── Footer(可选)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
运营端布局 DOM/样式与改前一致;客户门户不挂载 TopNav / TabBar。
|
||||||
|
|
||||||
**约定:**
|
**约定:**
|
||||||
|
|
||||||
- 主内容背景色:`#f0f3f4`
|
- 主内容背景色:`#f0f3f4`
|
||||||
- 页面容器使用 `[class$='-container']`,统一 `padding: 16px`
|
- 页面容器使用 `[class$='-container']`,统一 `padding: 16px`
|
||||||
- 内容区域使用 `overflow: hidden`、`height: 100%` 保证高度正确
|
- 内容区域使用 `overflow: hidden`、`height: 100%` 保证高度正确
|
||||||
|
|
||||||
|
**客户门户 vs 运营端导航(差异约定):**
|
||||||
|
|
||||||
|
| | 运营端 | 客户门户 |
|
||||||
|
|---|---|---|
|
||||||
|
| 顶栏 | 一级模块 + hover 二级 + 右侧余额/打印/用户 | **无顶栏** |
|
||||||
|
| 页签 | TabBar 多页签 | **无多页签**(当前页仅由侧栏高亮表达) |
|
||||||
|
| 侧栏 | 无全局侧栏 | **全高可折叠左栏**(默认展开):顶 Logo;中按「常用 / 仓储 / 更多」浅分组;**资产与账户收进用户下拉**;底沉余额/打印(含下载)/消息/仓库;**语言收进用户菜单** |
|
||||||
|
| 高亮 | 路由最长路径匹配 | 侧栏最长路径匹配;账户类路由高亮底栏用户区 |
|
||||||
|
| 数据 | RBAC 菜单树原样 | 仍用同一菜单树,`flatMap` 二级即可,不改表结构 |
|
||||||
|
|
||||||
### 2.2 页面内容结构
|
### 2.2 页面内容结构
|
||||||
|
|
||||||
单页内容推荐结构:
|
单页内容推荐结构:
|
||||||
|
|||||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@ -18,6 +18,7 @@ declare module 'vue' {
|
|||||||
CodeMirror: typeof import('./components/CodeMirror/index.vue')['default']
|
CodeMirror: typeof import('./components/CodeMirror/index.vue')['default']
|
||||||
CompleteContactModal: typeof import('./components/CompleteContactModal/index.vue')['default']
|
CompleteContactModal: typeof import('./components/CompleteContactModal/index.vue')['default']
|
||||||
ContactService: typeof import('./components/ContactService/index.vue')['default']
|
ContactService: typeof import('./components/ContactService/index.vue')['default']
|
||||||
|
CustomerSideRail: typeof import('./components/customer-side-rail/index.vue')['default']
|
||||||
CustomLeftMenu: typeof import('./components/CustomLeftMenu/index.vue')['default']
|
CustomLeftMenu: typeof import('./components/CustomLeftMenu/index.vue')['default']
|
||||||
CustomList: typeof import('./components/CustomList/index.vue')['default']
|
CustomList: typeof import('./components/CustomList/index.vue')['default']
|
||||||
DrawerPro: typeof import('./components/DrawerPro/index.vue')['default']
|
DrawerPro: typeof import('./components/DrawerPro/index.vue')['default']
|
||||||
|
|||||||
@ -4,12 +4,18 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { getWarehouseList } from '@/api/system/warehouse'
|
import { getWarehouseList } from '@/api/system/warehouse'
|
||||||
import { useCurrentWarehouseStore } from '@/store'
|
import { useCurrentWarehouseStore } from '@/store'
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
variant?: 'navbar' | 'login'
|
variant?: 'navbar' | 'login' | 'sider'
|
||||||
|
/** 下拉位置;侧栏沉底用 rt */
|
||||||
|
dropdownPosition?: 'br' | 'bl' | 'tr' | 'tl' | 'rt' | 'rb'
|
||||||
|
/** 侧栏收起:窄宽展示缩略文案(无图标) */
|
||||||
|
compact?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
variant: 'navbar',
|
variant: 'navbar',
|
||||||
|
dropdownPosition: 'br',
|
||||||
|
compact: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,6 +28,17 @@ const displayName = computed(
|
|||||||
t('nav.contactService.selectWarehouse'),
|
t('nav.contactService.selectWarehouse'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 收起时只露一字,避免无图标时空块 */
|
||||||
|
const labelText = computed(() => {
|
||||||
|
const name = displayName.value
|
||||||
|
if (props.variant === 'sider' && props.compact) {
|
||||||
|
return name.slice(0, 1) || '仓'
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
})
|
||||||
|
|
||||||
|
const useDropdown = computed(() => props.variant === 'navbar' || props.variant === 'sider')
|
||||||
|
|
||||||
async function loadServiceDataList() {
|
async function loadServiceDataList() {
|
||||||
try {
|
try {
|
||||||
const { items } = await getWarehouseList({
|
const { items } = await getWarehouseList({
|
||||||
@ -51,15 +68,22 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="contact-service" :class="`contact-service--${variant}`">
|
<div
|
||||||
<!-- 顶栏:与语言切换一致,悬停出菜单,无箭头/无 icon -->
|
class="contact-service"
|
||||||
|
:class="[
|
||||||
|
`contact-service--${variant}`,
|
||||||
|
{ 'is-compact': compact },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<a-dropdown
|
<a-dropdown
|
||||||
v-if="variant === 'navbar'"
|
v-if="useDropdown"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
position="br"
|
:position="dropdownPosition"
|
||||||
:popup-max-height="320"
|
:popup-max-height="320"
|
||||||
>
|
>
|
||||||
<span class="contact-service__label">{{ displayName }}</span>
|
<span class="contact-service__label" :title="displayName">
|
||||||
|
<span class="contact-service__text">{{ labelText }}</span>
|
||||||
|
</span>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption
|
<a-doption
|
||||||
v-for="opt in warehouseStore.options"
|
v-for="opt in warehouseStore.options"
|
||||||
@ -73,7 +97,6 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
|
|
||||||
<!-- 登录页等:保留选择器 -->
|
|
||||||
<a-select
|
<a-select
|
||||||
v-else
|
v-else
|
||||||
:model-value="warehouseStore.warehouseId"
|
:model-value="warehouseStore.warehouseId"
|
||||||
@ -98,11 +121,18 @@ onMounted(() => {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
&__select {
|
&__select {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
&--navbar {
|
&--navbar {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
@ -128,6 +158,38 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--sider {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.contact-service__label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.14s ease, color 0.14s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-compact .contact-service__label {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 2px;
|
||||||
|
min-width: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&--login {
|
&--login {
|
||||||
:deep(.arco-select-view) {
|
:deep(.arco-select-view) {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<a-modal
|
<a-modal
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
:width="mode === 'batch' ? 640 : 480"
|
:width="mode === 'batch' ? 760 : 600"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
unmount-on-close
|
unmount-on-close
|
||||||
modal-class="order-print-modal"
|
modal-class="order-print-modal"
|
||||||
@ -139,6 +139,7 @@ import {
|
|||||||
type HiprintPrinterInfo,
|
type HiprintPrinterInfo,
|
||||||
} from '@/utils/hiprint/hiprintElectronSocket';
|
} from '@/utils/hiprint/hiprintElectronSocket';
|
||||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||||||
|
import { tPrintControlHint } from '@/utils/hiprint/printControlHints';
|
||||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||||
import PrintOptionsForm from '@/components/PrintTemplate/PrintOptionsForm.vue';
|
import PrintOptionsForm from '@/components/PrintTemplate/PrintOptionsForm.vue';
|
||||||
import { fetchPrintAssetBytes, openPrintAssetInNewTab } from '@/utils/printPreview';
|
import { fetchPrintAssetBytes, openPrintAssetInNewTab } from '@/utils/printPreview';
|
||||||
@ -243,7 +244,7 @@ const printerStatusText = computed(() => {
|
|||||||
if (hiprintClientState.connecting) return '正在连接本机打印程序…';
|
if (hiprintClientState.connecting) return '正在连接本机打印程序…';
|
||||||
if (hiprintClientState.connected) return '本机打印已就绪,可一键自动打印';
|
if (hiprintClientState.connected) return '本机打印已就绪,可一键自动打印';
|
||||||
if (hiprintClientState.enabled) return '打印程序连接中,请稍候…';
|
if (hiprintClientState.enabled) return '打印程序连接中,请稍候…';
|
||||||
return '未连接,请先打开页面顶部「打印」开关';
|
return tPrintControlHint('modalOff');
|
||||||
});
|
});
|
||||||
|
|
||||||
const footerBusy = computed(() => printingActive.value || printingAll.value);
|
const footerBusy = computed(() => printingActive.value || printingAll.value);
|
||||||
@ -551,7 +552,7 @@ async function printAll() {
|
|||||||
|
|
||||||
.print-status-bar__right {
|
.print-status-bar__right {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 180px;
|
width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.print-printer-select {
|
.print-printer-select {
|
||||||
|
|||||||
1115
src/components/customer-side-rail/index.vue
Normal file
1115
src/components/customer-side-rail/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Top-nav inbox / announcement entry (unread counts stay here, not on Nana FAB). -->
|
<!-- Top-nav inbox / announcement entry (unread counts stay here, not on Nana FAB). -->
|
||||||
<a-dropdown trigger="hover" placement="bottomRight" @visible-change="onMessageDropdownVisibleChange">
|
<a-dropdown
|
||||||
|
trigger="hover"
|
||||||
|
:position="dropdownPosition"
|
||||||
|
@visible-change="onMessageDropdownVisibleChange"
|
||||||
|
>
|
||||||
<div class="message-dropdown-trigger" :title="t('navbar.message')">
|
<div class="message-dropdown-trigger" :title="t('navbar.message')">
|
||||||
<IconNotification class="message-icon" :class="totalCount > 0 ? 'theMessage' : ''" />
|
<IconNotification class="message-icon" :class="totalCount > 0 ? 'theMessage' : ''" />
|
||||||
<span v-if="totalCount > 0" class="message-count">({{ displayCount }})</span>
|
<span v-if="totalCount > 0" class="message-count">({{ displayCount }})</span>
|
||||||
@ -17,11 +21,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { IconNotification } from '@arco-design/web-vue/es/icon';
|
import { IconNotification } from '@arco-design/web-vue/es/icon';
|
||||||
import { useInboxUnread } from '@/composables/useInboxUnread';
|
import { useInboxUnread } from '@/composables/useInboxUnread';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
dropdownPosition?: 'br' | 'bl' | 'tr' | 'tl' | 'rt' | 'rb';
|
||||||
|
}>(),
|
||||||
|
{ dropdownPosition: 'br' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const dropdownPosition = computed(() => props.dropdownPosition);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { notificationCount, noticeCount, totalCount, displayCount, refreshUnread } = useInboxUnread();
|
const { notificationCount, noticeCount, totalCount, displayCount, refreshUnread } = useInboxUnread();
|
||||||
|
|||||||
@ -1,20 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-dropdown
|
<a-dropdown
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
position="br"
|
:position="dropdownPosition"
|
||||||
@select="onDropdownSelect"
|
@select="onDropdownSelect"
|
||||||
>
|
>
|
||||||
<div class="user-dropdown-trigger">
|
<div class="user-dropdown-trigger" :class="{ 'is-compact': compact }">
|
||||||
<img
|
<img
|
||||||
v-if="showCustomerAvatar"
|
v-if="showCustomerAvatar"
|
||||||
:src="customerAvatarUrl"
|
:src="customerAvatarUrl"
|
||||||
alt=""
|
alt=""
|
||||||
class="user-avatar"
|
class="user-avatar"
|
||||||
/>
|
/>
|
||||||
<span class="user-name">{{ displayName }}</span>
|
<span v-else class="user-avatar user-avatar--fallback">{{ displayNameInitial }}</span>
|
||||||
|
<span v-if="!compact" class="user-name">{{ displayName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption v-if="isCustomerPortal" value="userinfo">
|
<!-- 侧栏传入的资产与账户入口(替代侧栏「资产与账户」分组) -->
|
||||||
|
<a-doption
|
||||||
|
v-for="item in accountMenus"
|
||||||
|
:key="accountMenuKey(item)"
|
||||||
|
:value="accountMenuKey(item)"
|
||||||
|
>
|
||||||
|
<span class="user-menu-item">
|
||||||
|
<span class="user-menu-item__icon">
|
||||||
|
<component :is="getSubMenuIcon(item, undefined, MENU_NAME_TO_LOCALE_KEY)" />
|
||||||
|
</span>
|
||||||
|
<span class="user-menu-item__label">{{ formatAccountTitle(item) }}</span>
|
||||||
|
</span>
|
||||||
|
</a-doption>
|
||||||
|
<a-doption v-if="isCustomerPortal && !hasAccountProfileItem" value="userinfo">
|
||||||
<span class="user-menu-item">
|
<span class="user-menu-item">
|
||||||
<span class="user-menu-item__icon"><i class="fa fa-user" /></span>
|
<span class="user-menu-item__icon"><i class="fa fa-user" /></span>
|
||||||
<span class="user-menu-item__label">{{ t('menu.user.profile') }}</span>
|
<span class="user-menu-item__label">{{ t('menu.user.profile') }}</span>
|
||||||
@ -38,6 +52,21 @@
|
|||||||
<span class="user-menu-item__label">页面配置</span>
|
<span class="user-menu-item__label">页面配置</span>
|
||||||
</span>
|
</span>
|
||||||
</a-doption>
|
</a-doption>
|
||||||
|
<template v-if="showLanguageMenu">
|
||||||
|
<a-doption
|
||||||
|
v-for="item in languageOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:value="`lang:${item.value}`"
|
||||||
|
>
|
||||||
|
<span class="user-menu-item">
|
||||||
|
<span class="user-menu-item__icon">{{ item.icon }}</span>
|
||||||
|
<span class="user-menu-item__label">
|
||||||
|
{{ item.label }}
|
||||||
|
<span v-if="locale === item.value" class="user-menu-item__hint">✓</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a-doption>
|
||||||
|
</template>
|
||||||
<a-doption value="logout">
|
<a-doption value="logout">
|
||||||
<span class="user-menu-item is-danger">
|
<span class="user-menu-item is-danger">
|
||||||
<span class="user-menu-item__icon"><i class="fa fa-sign-in" /></span>
|
<span class="user-menu-item__icon"><i class="fa fa-sign-in" /></span>
|
||||||
@ -116,21 +145,97 @@ import {
|
|||||||
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
||||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||||
import { isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
import { isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
||||||
|
import useLocale from '@/hooks/locale';
|
||||||
import { resolvePostLogoutLogin } from '@/utils/login-portal';
|
import { resolvePostLogoutLogin } from '@/utils/login-portal';
|
||||||
import { openPortalSideDrawer } from '@/views/user/portal-drawer/usePortalSideDrawer';
|
import { openPortalSideDrawer } from '@/views/user/portal-drawer/usePortalSideDrawer';
|
||||||
|
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
||||||
|
import { getSubMenuIcon } from '@/utils/menu-icon';
|
||||||
|
import { MENU_NAME_TO_LOCALE_KEY, translateMenuTitle } from '@/utils/menu-i18n';
|
||||||
|
import {
|
||||||
|
getCustomerNavPath,
|
||||||
|
type CustomerNavItem,
|
||||||
|
} from '@/utils/customer-portal-nav';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 侧栏收起等场景只显示头像 */
|
||||||
|
compact?: boolean
|
||||||
|
/** 下拉相对触发器位置,侧栏用 rt */
|
||||||
|
dropdownPosition?: 'br' | 'bl' | 'tr' | 'tl' | 'rt' | 'rb'
|
||||||
|
/** 客户侧栏:资产与账户菜单,收进用户下拉 */
|
||||||
|
accountMenus?: CustomerNavItem[]
|
||||||
|
/** 客户侧栏:语言收进用户菜单,不再单独占底栏 */
|
||||||
|
showLanguageMenu?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
compact: false,
|
||||||
|
dropdownPosition: 'br',
|
||||||
|
accountMenus: () => [],
|
||||||
|
showLanguageMenu: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
|
const { changeLocale } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
|
|
||||||
|
const showLanguageMenu = computed(() => props.showLanguageMenu);
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'zh-CN', label: '中文', icon: '🇨🇳' },
|
||||||
|
{ value: 'en-US', label: 'English', icon: '🇺🇸' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
const isCustomerPortal = computed(() => rbacUserStore.isCustomerPortal());
|
const isCustomerPortal = computed(() => rbacUserStore.isCustomerPortal());
|
||||||
|
|
||||||
const displayName = computed(() => {
|
const displayName = computed(() => {
|
||||||
return userStore.name || '--';
|
return userStore.name || '--';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const displayNameInitial = computed(() => {
|
||||||
|
const name = String(displayName.value || '').trim();
|
||||||
|
return name ? name.slice(0, 1).toUpperCase() : '?';
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropdownPosition = computed(() => props.dropdownPosition);
|
||||||
|
const compact = computed(() => props.compact);
|
||||||
|
const accountMenus = computed(() => props.accountMenus || []);
|
||||||
|
|
||||||
|
const hasAccountProfileItem = computed(() =>
|
||||||
|
accountMenus.value.some((item) => {
|
||||||
|
const path = getCustomerNavPath(item);
|
||||||
|
return path === '/user/profile' || resolvePortalDrawerPanel(path) === 'profile';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
function accountMenuKey(item: CustomerNavItem) {
|
||||||
|
return `account:${getCustomerNavPath(item) || item.id || item.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAccountTitle(item: CustomerNavItem) {
|
||||||
|
return translateMenuTitle(t, {
|
||||||
|
name: item.name,
|
||||||
|
route: item.route || item.path,
|
||||||
|
path: item.path || item.route,
|
||||||
|
meta: item.meta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAccountMenuItem(item: CustomerNavItem) {
|
||||||
|
let targetPath = getCustomerNavPath(item);
|
||||||
|
if (!targetPath) return;
|
||||||
|
if (!targetPath.startsWith('/')) targetPath = `/${targetPath}`;
|
||||||
|
const portalPanel = resolvePortalDrawerPanel(targetPath);
|
||||||
|
if (portalPanel) {
|
||||||
|
openPortalSideDrawer(portalPanel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (router.currentRoute.value.path === targetPath) return;
|
||||||
|
router.push(targetPath).catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
const customerAvatarRaw = ref('');
|
const customerAvatarRaw = ref('');
|
||||||
|
|
||||||
const customerAvatarUrl = computed(() =>
|
const customerAvatarUrl = computed(() =>
|
||||||
@ -272,6 +377,20 @@ const onDropdownSelect = (value: string | number | Record<string, unknown> | und
|
|||||||
const onMenuClick = async (event: { key: string }) => {
|
const onMenuClick = async (event: { key: string }) => {
|
||||||
const { key } = event;
|
const { key } = event;
|
||||||
|
|
||||||
|
if (key.startsWith('account:')) {
|
||||||
|
const path = key.slice('account:'.length);
|
||||||
|
const item = accountMenus.value.find(
|
||||||
|
(m) => getCustomerNavPath(m) === path || String(m.id || m.name) === path,
|
||||||
|
);
|
||||||
|
if (item) openAccountMenuItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.startsWith('lang:')) {
|
||||||
|
changeLocale(key.slice('lang:'.length));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (key === 'logout') {
|
if (key === 'logout') {
|
||||||
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
|
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
|
||||||
const currentPath = router.currentRoute.value.fullPath;
|
const currentPath = router.currentRoute.value.fullPath;
|
||||||
@ -309,6 +428,14 @@ const onMenuClick = async (event: { key: string }) => {
|
|||||||
transition: background-color 0.15s ease, opacity 0.15s ease;
|
transition: background-color 0.15s ease, opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-dropdown-trigger.is-compact {
|
||||||
|
max-width: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.user-dropdown-trigger:hover {
|
.user-dropdown-trigger:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.12);
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
@ -322,6 +449,16 @@ const onMenuClick = async (event: { key: string }) => {
|
|||||||
background: rgba(255, 255, 255, 0.16);
|
background: rgba(255, 255, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-avatar--fallback {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1, #1d2129);
|
||||||
|
background: var(--color-fill-3, #e5e6eb);
|
||||||
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -334,7 +471,8 @@ const onMenuClick = async (event: { key: string }) => {
|
|||||||
.user-menu-item {
|
.user-menu-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 132px;
|
min-width: 148px;
|
||||||
|
max-width: 220px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,6 +484,7 @@ const onMenuClick = async (event: { key: string }) => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
color: var(--color-text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-menu-item__label {
|
.user-menu-item__label {
|
||||||
@ -355,6 +494,12 @@ const onMenuClick = async (event: { key: string }) => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-menu-item__hint {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.user-menu-item.is-danger .user-menu-item__label {
|
.user-menu-item.is-danger .user-menu-item__label {
|
||||||
color: rgb(var(--red-6));
|
color: rgb(var(--red-6));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-dropdown trigger="hover" placement="bottomRight">
|
<a-dropdown trigger="hover" :position="dropdownPosition">
|
||||||
<span class="dropDown" :title="$t('app.language', '语言')">
|
<span class="dropDown" :title="$t('app.language', '语言')">
|
||||||
<icon-language class="lang-icon" />
|
<icon-language class="lang-icon" />
|
||||||
</span>
|
</span>
|
||||||
@ -20,6 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { IconLanguage } from '@arco-design/web-vue/es/icon';
|
import { IconLanguage } from '@arco-design/web-vue/es/icon';
|
||||||
import useLocale from '@/hooks/locale';
|
import useLocale from '@/hooks/locale';
|
||||||
@ -28,10 +29,14 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
/** 为 true 时不弹出「已切换语言」提示,适合登录页等需要无感切换的场景 */
|
/** 为 true 时不弹出「已切换语言」提示,适合登录页等需要无感切换的场景 */
|
||||||
quiet?: boolean;
|
quiet?: boolean;
|
||||||
|
/** 下拉位置;侧栏沉底用 rt */
|
||||||
|
dropdownPosition?: 'br' | 'bl' | 'tr' | 'tl' | 'rt' | 'rb';
|
||||||
}>(),
|
}>(),
|
||||||
{ quiet: false },
|
{ quiet: false, dropdownPosition: 'br' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dropdownPosition = computed(() => props.dropdownPosition);
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const { changeLocale } = useLocale();
|
const { changeLocale } = useLocale();
|
||||||
|
|
||||||
|
|||||||
@ -122,7 +122,24 @@
|
|||||||
更多
|
更多
|
||||||
</span>
|
</span>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="nav-more-panel">
|
<!-- 客户扁平导航:溢出项直接列表,不按一级分组 -->
|
||||||
|
<div v-if="isCustomerFlatNav" class="nav-more-panel nav-more-panel--flat">
|
||||||
|
<div
|
||||||
|
v-for="item in overflowTopMenus"
|
||||||
|
:key="menuKey(item)"
|
||||||
|
class="nav-more-item"
|
||||||
|
:class="{ active: isTopMenuActive(item) }"
|
||||||
|
@pointerenter="prefetchMenuRoute(item)"
|
||||||
|
@pointerdown.prevent="gotoFromSubmenu(item)"
|
||||||
|
@click.prevent="gotoFromSubmenu(item)"
|
||||||
|
>
|
||||||
|
<span class="nav-more-item__icon">
|
||||||
|
<component :is="getSubMenuIcon(item, undefined, menuNameToLocaleKeyMap)" />
|
||||||
|
</span>
|
||||||
|
<span class="nav-more-item__text">{{ formatMenuTitle(item) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="nav-more-panel">
|
||||||
<div
|
<div
|
||||||
v-for="item in overflowTopMenus"
|
v-for="item in overflowTopMenus"
|
||||||
:key="menuKey(item)"
|
:key="menuKey(item)"
|
||||||
@ -281,6 +298,10 @@ import { getMapIdFromAccessToken } from '@/utils/jwt';
|
|||||||
import { getSubMenuIcon } from '@/utils/menu-icon';
|
import { getSubMenuIcon } from '@/utils/menu-icon';
|
||||||
import { MENU_NAME_TO_LOCALE_KEY, translateMenuTitle } from '@/utils/menu-i18n';
|
import { MENU_NAME_TO_LOCALE_KEY, translateMenuTitle } from '@/utils/menu-i18n';
|
||||||
import { canAccessCustomerPortalRoute, isAdminPortalOnlyPath, isCustomerPortalOnlyPath } from '@/utils/login-portal';
|
import { canAccessCustomerPortalRoute, isAdminPortalOnlyPath, isCustomerPortalOnlyPath } from '@/utils/login-portal';
|
||||||
|
import {
|
||||||
|
buildCustomerFlatNavMenus,
|
||||||
|
pickCustomerTopNavMenus,
|
||||||
|
} from '@/utils/customer-portal-nav';
|
||||||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||||||
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
||||||
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
||||||
@ -602,9 +623,33 @@ const menuData = computed(() => {
|
|||||||
return convertMenuTreeToNavMenu(userMenus);
|
return convertMenuTreeToNavMenu(userMenus);
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleTopMenus = computed(() =>
|
/** 客户门户:压平后的全量二级(侧栏索引同源) */
|
||||||
menuData.value.filter((item: any) => !item.meta?.isHide),
|
const customerMenuSource = computed(() => {
|
||||||
);
|
const userMenus = userMenuTreeData.value;
|
||||||
|
if (!userMenus.length) return [];
|
||||||
|
if (allMenuTreeData.value.length > 0) {
|
||||||
|
return filterMenuByUserPermission(allMenuTreeData.value, userMenus);
|
||||||
|
}
|
||||||
|
return userMenus;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customerFlatMenus = computed(() => {
|
||||||
|
if (!rbacUserStore.isCustomerPortal()) return [];
|
||||||
|
return buildCustomerFlatNavMenus(
|
||||||
|
customerMenuSource.value,
|
||||||
|
rbacUserStore.loginPortalType,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCustomerFlatNav = computed(() => rbacUserStore.isCustomerPortal());
|
||||||
|
|
||||||
|
const visibleTopMenus = computed(() => {
|
||||||
|
// 客户 P0/P1:顶栏只平铺高频二级,不再展示「订单/仓储/系统」等一级模块名
|
||||||
|
if (isCustomerFlatNav.value) {
|
||||||
|
return pickCustomerTopNavMenus(customerFlatMenus.value);
|
||||||
|
}
|
||||||
|
return menuData.value.filter((item: any) => !item.meta?.isHide);
|
||||||
|
});
|
||||||
|
|
||||||
const MORE_MENU_KEY = '__more__';
|
const MORE_MENU_KEY = '__more__';
|
||||||
const MORE_BTN_RESERVE = 88;
|
const MORE_BTN_RESERVE = 88;
|
||||||
@ -1675,6 +1720,18 @@ onUnmounted(() => {
|
|||||||
background: var(--app-elevated-bg, var(--app-card-bg));
|
background: var(--app-elevated-bg, var(--app-card-bg));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
&--flat {
|
||||||
|
min-width: 180px;
|
||||||
|
|
||||||
|
.nav-more-item {
|
||||||
|
padding-left: 10px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-more-group {
|
.nav-more-group {
|
||||||
|
|||||||
@ -1,15 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-layout class="layout">
|
<a-layout class="layout">
|
||||||
<!-- 顶部导航 -->
|
<!-- 运营端顶栏;客户门户无顶栏(能力沉底侧栏) -->
|
||||||
<TopNav />
|
<TopNav v-if="!isCustomerShell" />
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 客户门户:全高侧栏 + 主区(无多页签,当前页由侧栏高亮) -->
|
||||||
<a-layout :style="contentStyle" class="layout-content">
|
<a-layout
|
||||||
|
v-if="isCustomerShell"
|
||||||
|
class="layout-content layout-content--customer"
|
||||||
|
>
|
||||||
|
<CustomerSideRail />
|
||||||
|
<a-layout class="layout-main-wrap">
|
||||||
|
<a-layout-content class="layout-main">
|
||||||
|
<PageLayout />
|
||||||
|
</a-layout-content>
|
||||||
|
<Footer v-if="footer" />
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
|
||||||
|
<!-- 运营端:与改前一致 -->
|
||||||
|
<a-layout v-else :style="adminContentStyle" class="layout-content">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<PageLayout />
|
<PageLayout />
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
<Footer v-if="footer" />
|
<Footer v-if="footer" />
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<NanaAssistant />
|
<NanaAssistant />
|
||||||
<PortalSideDrawer />
|
<PortalSideDrawer />
|
||||||
<HelpSideDrawer />
|
<HelpSideDrawer />
|
||||||
@ -20,7 +35,9 @@
|
|||||||
import { computed, onMounted } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import TopNav from '@/components/top-nav/index.vue';
|
import TopNav from '@/components/top-nav/index.vue';
|
||||||
|
import CustomerSideRail from '@/components/customer-side-rail/index.vue';
|
||||||
import Footer from '@/components/footer/index.vue';
|
import Footer from '@/components/footer/index.vue';
|
||||||
import NanaAssistant from '@/components/NanaAssistant/index.vue';
|
import NanaAssistant from '@/components/NanaAssistant/index.vue';
|
||||||
import PortalSideDrawer from '@/views/user/portal-drawer/PortalSideDrawer.vue';
|
import PortalSideDrawer from '@/views/user/portal-drawer/PortalSideDrawer.vue';
|
||||||
@ -31,6 +48,7 @@
|
|||||||
import { setRapidPackageHostCookie } from '@/utils/rapidPackagePluginHost';
|
import { setRapidPackageHostCookie } from '@/utils/rapidPackagePluginHost';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const rbacUserStore = useRbacUserStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const permission = usePermission();
|
const permission = usePermission();
|
||||||
useResponsive(true);
|
useResponsive(true);
|
||||||
@ -38,15 +56,18 @@
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setRapidPackageHostCookie();
|
setRapidPackageHostCookie();
|
||||||
});
|
});
|
||||||
|
|
||||||
const footer = computed(() => appStore.footer && !route.path.startsWith('/assistant'));
|
const footer = computed(() => appStore.footer && !route.path.startsWith('/assistant'));
|
||||||
|
|
||||||
// 内容区域样式:顶部留出导航栏空间
|
const isCustomerShell = computed(
|
||||||
const contentStyle = computed(() => {
|
() => rbacUserStore.isCustomerPortal() && rbacUserStore.isLoggedIn,
|
||||||
const headerHeight = 50; // 顶部导航高度
|
);
|
||||||
const tabNavHeight = appStore.tabBar ? 36 : 0; // Tab导航高度
|
|
||||||
const paddingTop = headerHeight + tabNavHeight;
|
// 仅运营端需要为 fixed 顶栏留白
|
||||||
return { paddingTop: `${paddingTop}px` };
|
const adminContentStyle = computed(() => {
|
||||||
|
const headerHeight = 50;
|
||||||
|
const tabNavHeight = appStore.tabBar ? 36 : 0;
|
||||||
|
return { paddingTop: `${headerHeight + tabNavHeight}px` };
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -67,19 +88,49 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 运营端:与改前一致 */
|
||||||
.layout-content {
|
.layout-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--app-page-bg);
|
background-color: var(--app-page-bg);
|
||||||
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||||
margin-top: 0; // 顶部导航是 fixed,不需要 margin
|
margin-top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
:deep(.arco-layout-content) {
|
:deep(.arco-layout-content) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 客户门户:无顶栏;侧栏与右侧内容卡片同 inset / 圆角 */
|
||||||
|
.layout-content--customer {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
min-height: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
|
.layout-main-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-layout-content.layout-main),
|
||||||
|
.layout-main {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1812,7 +1812,21 @@ export default {
|
|||||||
'nav.balance.recharge': 'Recharge',
|
'nav.balance.recharge': 'Recharge',
|
||||||
'nav.balance.rechargeRecord': 'Recharge records',
|
'nav.balance.rechargeRecord': 'Recharge records',
|
||||||
'nav.balance.accountFlow': 'Account transactions',
|
'nav.balance.accountFlow': 'Account transactions',
|
||||||
|
'nav.customerRail.group.business': 'Tasks',
|
||||||
|
'nav.customerRail.group.core': 'Essentials',
|
||||||
|
'nav.customerRail.group.warehouse': 'Warehouse',
|
||||||
|
'nav.customerRail.group.more': 'More',
|
||||||
|
'nav.customerRail.group.account': 'Account',
|
||||||
|
'nav.printControl.menu': 'Print',
|
||||||
'nav.printControl.download': 'Download print control / one-click parcel plugin',
|
'nav.printControl.download': 'Download print control / one-click parcel plugin',
|
||||||
|
'nav.printControl.hint.statusOff.admin': 'Printer not connected — enable Print in the top bar',
|
||||||
|
'nav.printControl.hint.statusOff.customer': 'Printer not connected — open Print in the left menu and connect',
|
||||||
|
'nav.printControl.hint.modalOff.admin': 'Not connected — enable Print in the top bar first',
|
||||||
|
'nav.printControl.hint.modalOff.customer': 'Not connected — open Print in the left menu and connect first',
|
||||||
|
'nav.printControl.hint.dispatchError.admin': 'Enable Print in the top bar and connect the local print app first',
|
||||||
|
'nav.printControl.hint.dispatchError.customer': 'Open Print in the left menu and connect the local print app first',
|
||||||
|
'nav.printControl.hint.pluginDownload.admin': 'Download it from the top-right icon',
|
||||||
|
'nav.printControl.hint.pluginDownload.customer': 'Download it from Print in the left menu',
|
||||||
|
|
||||||
// Order - My Orders
|
// Order - My Orders
|
||||||
'order.myOrders.tabs.all': 'All',
|
'order.myOrders.tabs.all': 'All',
|
||||||
|
|||||||
@ -1567,7 +1567,21 @@ export default {
|
|||||||
'nav.balance.recharge': '去充值',
|
'nav.balance.recharge': '去充值',
|
||||||
'nav.balance.rechargeRecord': '充值记录',
|
'nav.balance.rechargeRecord': '充值记录',
|
||||||
'nav.balance.accountFlow': '资金流水',
|
'nav.balance.accountFlow': '资金流水',
|
||||||
|
'nav.customerRail.group.business': '办事',
|
||||||
|
'nav.customerRail.group.core': '常用',
|
||||||
|
'nav.customerRail.group.warehouse': '仓储',
|
||||||
|
'nav.customerRail.group.more': '更多',
|
||||||
|
'nav.customerRail.group.account': '资产与账户',
|
||||||
|
'nav.printControl.menu': '打印',
|
||||||
'nav.printControl.download': '下载打印控件/一键添加包裹插件',
|
'nav.printControl.download': '下载打印控件/一键添加包裹插件',
|
||||||
|
'nav.printControl.hint.statusOff.admin': '未连接打印,请打开页面顶部「打印」开关',
|
||||||
|
'nav.printControl.hint.statusOff.customer': '未连接打印,请打开左侧菜单「打印」并连接',
|
||||||
|
'nav.printControl.hint.modalOff.admin': '未连接,请先打开页面顶部「打印」开关',
|
||||||
|
'nav.printControl.hint.modalOff.customer': '未连接,请先打开左侧菜单「打印」并连接',
|
||||||
|
'nav.printControl.hint.dispatchError.admin': '请先打开页面顶部「打印」开关并连接本机打印程序',
|
||||||
|
'nav.printControl.hint.dispatchError.customer': '请先打开左侧菜单「打印」并连接本机打印程序',
|
||||||
|
'nav.printControl.hint.pluginDownload.admin': '去右上角下载插件',
|
||||||
|
'nav.printControl.hint.pluginDownload.customer': '去左侧「打印」下载插件',
|
||||||
|
|
||||||
// 订单 - 批量任务
|
// 订单 - 批量任务
|
||||||
'order.batchJobs.filter.jobId': '任务 ID',
|
'order.batchJobs.filter.jobId': '任务 ID',
|
||||||
|
|||||||
334
src/utils/customer-portal-nav.ts
Normal file
334
src/utils/customer-portal-nav.ts
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
import type { OmsLoginType } from '@/api/rbac'
|
||||||
|
import type { MenuTreeDto } from '@/api/rbac'
|
||||||
|
import { canAccessCustomerPortalRoute, isAdminPortalOnlyPath, isCustomerPortalOnlyPath } from '@/utils/login-portal'
|
||||||
|
|
||||||
|
/** 客户顶栏高频入口(路径越靠前优先级越高) */
|
||||||
|
export const CUSTOMER_TOP_NAV_PRIORITY_PATHS = [
|
||||||
|
'/home/workplace',
|
||||||
|
'/home',
|
||||||
|
'/order/priceCheck',
|
||||||
|
'/order/placeOrder',
|
||||||
|
'/order/myOrders',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const CUSTOMER_SIDE_RAIL_COLLAPSED_WIDTH = 64
|
||||||
|
export const CUSTOMER_SIDE_RAIL_EXPANDED_WIDTH = 200
|
||||||
|
export const CUSTOMER_SIDE_RAIL_STORAGE_KEY = 'oms.customerSideRail.collapsed'
|
||||||
|
|
||||||
|
export type CustomerNavItem = {
|
||||||
|
id?: string | number
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
route?: string
|
||||||
|
type?: number
|
||||||
|
meta?: {
|
||||||
|
title?: string
|
||||||
|
icon?: string
|
||||||
|
order?: number
|
||||||
|
isHide?: boolean
|
||||||
|
hideInMenu?: boolean
|
||||||
|
customerPortalOnly?: boolean
|
||||||
|
adminPortalOnly?: boolean
|
||||||
|
}
|
||||||
|
/** 原一级模块名,仅侧栏分组/调试用 */
|
||||||
|
parentName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧栏任务分组(客户心智):
|
||||||
|
* - core:查价 / 下单 / 订单(最高频)
|
||||||
|
* - warehouse:仓储相关
|
||||||
|
* - more:其余办事入口
|
||||||
|
* - account:资产与账户(可收进用户下拉)
|
||||||
|
*/
|
||||||
|
export type CustomerNavGroupId = 'core' | 'warehouse' | 'more' | 'account'
|
||||||
|
|
||||||
|
export type CustomerNavGroup = {
|
||||||
|
id: CustomerNavGroupId
|
||||||
|
labelKey: string
|
||||||
|
labelFallback: string
|
||||||
|
items: CustomerNavItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 办事主路径:客户最常点的四个入口 */
|
||||||
|
const CUSTOMER_CORE_NAV_PATHS = [
|
||||||
|
'/home/workplace',
|
||||||
|
'/home',
|
||||||
|
'/order/priceCheck',
|
||||||
|
'/order/placeOrder',
|
||||||
|
'/order/myOrders',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function getCustomerNavPath(item: { path?: string; route?: string } | null | undefined): string {
|
||||||
|
return String(item?.path || item?.route || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收进用户下拉的辅助页(非高频办事入口) */
|
||||||
|
const USER_MENU_EXTRA_PATH_PREFIXES = [
|
||||||
|
'/api/openApi',
|
||||||
|
'/system/announcement',
|
||||||
|
'/system/warehouseAddress',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 余额下拉已承接的资金入口;勿再塞进用户下拉,避免重复。
|
||||||
|
* 对应:去充值 / 充值记录 / 资金流水
|
||||||
|
*/
|
||||||
|
const BALANCE_MENU_PATH_PREFIXES = [
|
||||||
|
'/user/recharge',
|
||||||
|
'/finance/rechargeRecord',
|
||||||
|
'/finance/accountFlow',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function matchesPathPrefix(path: string, prefix: string): boolean {
|
||||||
|
return path === prefix || path.startsWith(`${prefix}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否由余额下拉独占(用户菜单应排除) */
|
||||||
|
export function isCustomerBalanceMenuPath(path: string): boolean {
|
||||||
|
const p = path.trim()
|
||||||
|
if (!p) return false
|
||||||
|
return BALANCE_MENU_PATH_PREFIXES.some((prefix) => matchesPathPrefix(p, prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCustomerNavGroupId(path: string): CustomerNavGroupId {
|
||||||
|
const p = path.trim() || '/'
|
||||||
|
if (p === '/user' || p.startsWith('/user/')) return 'account'
|
||||||
|
if (p === '/finance' || p.startsWith('/finance/')) return 'account'
|
||||||
|
if (p === '/marketing' || p.startsWith('/marketing/')) return 'account'
|
||||||
|
if (USER_MENU_EXTRA_PATH_PREFIXES.some((prefix) => matchesPathPrefix(p, prefix))) {
|
||||||
|
return 'account'
|
||||||
|
}
|
||||||
|
if (p === '/warehouse' || p.startsWith('/warehouse/')) return 'warehouse'
|
||||||
|
if (CUSTOMER_CORE_NAV_PATHS.some((core) => p === core || p.startsWith(`${core}/`))) {
|
||||||
|
return 'core'
|
||||||
|
}
|
||||||
|
return 'more'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否账户类路由(含资金页,用于底栏高亮) */
|
||||||
|
export function isCustomerAccountNavPath(path: string): boolean {
|
||||||
|
return resolveCustomerNavGroupId(path) === 'account'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function partitionCustomerNavMenus(items: CustomerNavItem[]): {
|
||||||
|
business: CustomerNavItem[]
|
||||||
|
account: CustomerNavItem[]
|
||||||
|
} {
|
||||||
|
const business: CustomerNavItem[] = []
|
||||||
|
const account: CustomerNavItem[] = []
|
||||||
|
for (const item of items) {
|
||||||
|
const g = resolveCustomerNavGroupId(getCustomerNavPath(item))
|
||||||
|
if (g === 'account') account.push(item)
|
||||||
|
else business.push(item)
|
||||||
|
}
|
||||||
|
return { business, account }
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUSINESS_GROUP_META: Record<
|
||||||
|
Exclude<CustomerNavGroupId, 'account'>,
|
||||||
|
{ labelKey: string; labelFallback: string }
|
||||||
|
> = {
|
||||||
|
core: {
|
||||||
|
labelKey: 'nav.customerRail.group.core',
|
||||||
|
labelFallback: '常用',
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
labelKey: 'nav.customerRail.group.warehouse',
|
||||||
|
labelFallback: '仓储',
|
||||||
|
},
|
||||||
|
more: {
|
||||||
|
labelKey: 'nav.customerRail.group.more',
|
||||||
|
labelFallback: '更多',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 侧栏主列表分组。
|
||||||
|
* foldAccountIntoUserMenu=true 时,「资产与账户」不进侧栏,改由用户下拉承接。
|
||||||
|
*/
|
||||||
|
export function groupCustomerNavMenus(
|
||||||
|
items: CustomerNavItem[],
|
||||||
|
options?: { foldAccountIntoUserMenu?: boolean },
|
||||||
|
): CustomerNavGroup[] {
|
||||||
|
const buckets: Record<CustomerNavGroupId, CustomerNavItem[]> = {
|
||||||
|
core: [],
|
||||||
|
warehouse: [],
|
||||||
|
more: [],
|
||||||
|
account: [],
|
||||||
|
}
|
||||||
|
for (const item of items) {
|
||||||
|
buckets[resolveCustomerNavGroupId(getCustomerNavPath(item))].push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: CustomerNavGroup[] = []
|
||||||
|
;(['core', 'warehouse', 'more'] as const).forEach((id) => {
|
||||||
|
if (!buckets[id].length) return
|
||||||
|
const meta = BUSINESS_GROUP_META[id]
|
||||||
|
groups.push({
|
||||||
|
id,
|
||||||
|
labelKey: meta.labelKey,
|
||||||
|
labelFallback: meta.labelFallback,
|
||||||
|
items: buckets[id],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!options?.foldAccountIntoUserMenu && buckets.account.length) {
|
||||||
|
groups.push({
|
||||||
|
id: 'account',
|
||||||
|
labelKey: 'nav.customerRail.group.account',
|
||||||
|
labelFallback: '资产与账户',
|
||||||
|
items: buckets.account,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 客户侧栏默认展开:图标轨对首次使用不友好 */
|
||||||
|
export function readCustomerSideRailCollapsed(defaultCollapsed = false): boolean {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CUSTOMER_SIDE_RAIL_STORAGE_KEY)
|
||||||
|
if (raw === null) return defaultCollapsed
|
||||||
|
return raw === '1' || raw === 'true'
|
||||||
|
} catch {
|
||||||
|
return defaultCollapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeCustomerSideRailCollapsed(collapsed: boolean) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(CUSTOMER_SIDE_RAIL_STORAGE_KEY, collapsed ? '1' : '0')
|
||||||
|
} catch {
|
||||||
|
// ignore quota / private mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navPriority(path: string): number {
|
||||||
|
if (!path) return 9999
|
||||||
|
const idx = CUSTOMER_TOP_NAV_PRIORITY_PATHS.findIndex(
|
||||||
|
(p) => path === p || path.startsWith(`${p}/`),
|
||||||
|
)
|
||||||
|
return idx >= 0 ? idx : 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCustomerTopNavPriorityPath(path: string): boolean {
|
||||||
|
return navPriority(path) < 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleNavNode(
|
||||||
|
item: {
|
||||||
|
type?: number
|
||||||
|
name?: string
|
||||||
|
path?: string
|
||||||
|
route?: string
|
||||||
|
meta?: CustomerNavItem['meta']
|
||||||
|
isHidden?: boolean
|
||||||
|
},
|
||||||
|
loginType: OmsLoginType,
|
||||||
|
): boolean {
|
||||||
|
if (item.type === 2) return false
|
||||||
|
if (item.meta?.isHide || item.meta?.hideInMenu || item.isHidden) return false
|
||||||
|
const path = getCustomerNavPath(item)
|
||||||
|
if (
|
||||||
|
!canAccessCustomerPortalRoute(loginType, {
|
||||||
|
path,
|
||||||
|
customerPortalOnly: item.meta?.customerPortalOnly,
|
||||||
|
adminPortalOnly: item.meta?.adminPortalOnly,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNavItem(menu: MenuTreeDto, parentName?: string): CustomerNavItem {
|
||||||
|
const route = String(menu.route || '')
|
||||||
|
const name = String(menu.name || '').trim() || route
|
||||||
|
return {
|
||||||
|
id: menu.id,
|
||||||
|
name,
|
||||||
|
path: route,
|
||||||
|
route,
|
||||||
|
type: menu.type as number | undefined,
|
||||||
|
parentName,
|
||||||
|
meta: {
|
||||||
|
title: name,
|
||||||
|
icon: menu.icon || undefined,
|
||||||
|
order: menu.order,
|
||||||
|
isHide: menu.isHidden || false,
|
||||||
|
customerPortalOnly: isCustomerPortalOnlyPath(route),
|
||||||
|
adminPortalOnly: isAdminPortalOnlyPath(route),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将客户菜单树压平为可点击二级页(含无子级的一级,如首页)。
|
||||||
|
* 不改 RBAC 结构,仅改变客户门户展示。
|
||||||
|
*/
|
||||||
|
export function buildCustomerFlatNavMenus(
|
||||||
|
menuTree: MenuTreeDto[],
|
||||||
|
loginType: OmsLoginType,
|
||||||
|
): CustomerNavItem[] {
|
||||||
|
const result: CustomerNavItem[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
const push = (item: CustomerNavItem) => {
|
||||||
|
const path = getCustomerNavPath(item)
|
||||||
|
if (!path || seen.has(path)) return
|
||||||
|
if (!isVisibleNavNode(item, loginType)) return
|
||||||
|
seen.add(path)
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const menu of menuTree || []) {
|
||||||
|
if (!isVisibleNavNode(menu, loginType)) continue
|
||||||
|
const children = (menu.children || []).filter((c) => isVisibleNavNode(c, loginType))
|
||||||
|
if (children.length > 0) {
|
||||||
|
for (const child of children) {
|
||||||
|
push(toNavItem(child, menu.name))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 无二级的一级(首页等)直接作为入口
|
||||||
|
push(toNavItem(menu))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sort((a, b) => {
|
||||||
|
const pa = navPriority(getCustomerNavPath(a))
|
||||||
|
const pb = navPriority(getCustomerNavPath(b))
|
||||||
|
if (pa !== pb) return pa - pb
|
||||||
|
const oa = a.meta?.order ?? 0
|
||||||
|
const ob = b.meta?.order ?? 0
|
||||||
|
if (oa !== ob) return oa - ob
|
||||||
|
return getCustomerNavPath(a).localeCompare(getCustomerNavPath(b))
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 顶栏只展示高频入口;其余进侧栏全量索引 */
|
||||||
|
export function pickCustomerTopNavMenus(flatMenus: CustomerNavItem[]): CustomerNavItem[] {
|
||||||
|
return flatMenus.filter((item) => isCustomerTopNavPriorityPath(getCustomerNavPath(item)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最长路径匹配,避免短前缀误高亮 */
|
||||||
|
export function matchCustomerActiveNavPath(
|
||||||
|
currentPath: string,
|
||||||
|
items: Array<{ path?: string; route?: string }>,
|
||||||
|
): string {
|
||||||
|
let best = ''
|
||||||
|
let bestLen = -1
|
||||||
|
for (const item of items) {
|
||||||
|
const path = getCustomerNavPath(item)
|
||||||
|
if (!path) continue
|
||||||
|
if (
|
||||||
|
(currentPath === path || currentPath.startsWith(`${path}/`)) &&
|
||||||
|
path.length > bestLen
|
||||||
|
) {
|
||||||
|
bestLen = path.length
|
||||||
|
best = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
hiprintSendHtmlPrint,
|
hiprintSendHtmlPrint,
|
||||||
} from '@/utils/hiprint/hiprintElectronSocket';
|
} from '@/utils/hiprint/hiprintElectronSocket';
|
||||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
|
||||||
|
import { tPrintControlHint } from '@/utils/hiprint/printControlHints';
|
||||||
import {
|
import {
|
||||||
buildHiprintBlobPrintExtras,
|
buildHiprintBlobPrintExtras,
|
||||||
readHiprintPrintFormFromStorage,
|
readHiprintPrintFormFromStorage,
|
||||||
@ -43,7 +44,7 @@ async function sendBytesToHiprint(rawUrl: string, bytes: Uint8Array) {
|
|||||||
export async function dispatchPrintUrl(rawUrl: string) {
|
export async function dispatchPrintUrl(rawUrl: string) {
|
||||||
const hiprintState = useHiprintClientState();
|
const hiprintState = useHiprintClientState();
|
||||||
if (!hiprintState.enabled || !hiprintState.connected) {
|
if (!hiprintState.enabled || !hiprintState.connected) {
|
||||||
throw new Error('请先打开页面顶部「打印」开关并连接本机打印程序');
|
throw new Error(tPrintControlHint('dispatchError'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = String(rawUrl || '').trim();
|
const url = String(rawUrl || '').trim();
|
||||||
|
|||||||
14
src/utils/hiprint/printControlHints.ts
Normal file
14
src/utils/hiprint/printControlHints.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import i18n from '@/locale'
|
||||||
|
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||||
|
|
||||||
|
/** 打印入口位置提示:客户侧栏 vs 运营顶栏文案不同 */
|
||||||
|
export type PrintControlHintKind =
|
||||||
|
| 'statusOff'
|
||||||
|
| 'modalOff'
|
||||||
|
| 'dispatchError'
|
||||||
|
| 'pluginDownload'
|
||||||
|
|
||||||
|
export function tPrintControlHint(kind: PrintControlHintKind): string {
|
||||||
|
const portal = useRbacUserStore().isCustomerPortal() ? 'customer' : 'admin'
|
||||||
|
return String(i18n.global.t(`nav.printControl.hint.${kind}.${portal}`))
|
||||||
|
}
|
||||||
@ -33,7 +33,7 @@
|
|||||||
{{
|
{{
|
||||||
printerReady
|
printerReady
|
||||||
? $t('operation.printLabel.printerStatus.ready')
|
? $t('operation.printLabel.printerStatus.ready')
|
||||||
: $t('operation.printLabel.printerStatus.off')
|
: printerStatusOffText
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<a-checkbox v-model="keepScanFocus" class="pl-keep-focus">
|
<a-checkbox v-model="keepScanFocus" class="pl-keep-focus">
|
||||||
@ -212,6 +212,7 @@ import { useOperationScanFocus } from '@/composables/useOperationScanFocus'
|
|||||||
import PrintPreviewPane from '@/components/PrintTemplate/PrintPreviewPane.vue'
|
import PrintPreviewPane from '@/components/PrintTemplate/PrintPreviewPane.vue'
|
||||||
import { postOrderPrint } from '@/api/order'
|
import { postOrderPrint } from '@/api/order'
|
||||||
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'
|
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState'
|
||||||
|
import { tPrintControlHint } from '@/utils/hiprint/printControlHints'
|
||||||
import { dispatchPrintUrl } from '@/utils/hiprint/dispatchPrintUrl'
|
import { dispatchPrintUrl } from '@/utils/hiprint/dispatchPrintUrl'
|
||||||
import {
|
import {
|
||||||
getOrPrefetchPrintPreview,
|
getOrPrefetchPrintPreview,
|
||||||
@ -234,6 +235,7 @@ const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
|||||||
const scanInputRef = ref<{ focus?: () => void } | null>(null)
|
const scanInputRef = ref<{ focus?: () => void } | null>(null)
|
||||||
const { keepScanFocus, focusScanInput, maybeRefocusScan } = useOperationScanFocus('printLabel', scanInputRef)
|
const { keepScanFocus, focusScanInput, maybeRefocusScan } = useOperationScanFocus('printLabel', scanInputRef)
|
||||||
const hiprintState = useHiprintClientState()
|
const hiprintState = useHiprintClientState()
|
||||||
|
const printerStatusOffText = computed(() => tPrintControlHint('statusOff'))
|
||||||
|
|
||||||
defineOptions({ name: 'PrintLabel' })
|
defineOptions({ name: 'PrintLabel' })
|
||||||
|
|
||||||
|
|||||||
@ -2092,10 +2092,10 @@
|
|||||||
class="list-empty-hint"
|
class="list-empty-hint"
|
||||||
>
|
>
|
||||||
<span>{{ t('order.myOrders.empty.hint') }}</span>
|
<span>{{ t('order.myOrders.empty.hint') }}</span>
|
||||||
<a-button type="text" @click="router.push('/order/placeOrder')">
|
<a-button class="app-btn-primary-soft" @click="router.push('/order/placeOrder')">
|
||||||
{{ t('order.myOrders.empty.goPlaceOrder') }}
|
{{ t('order.myOrders.empty.goPlaceOrder') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="text" @click="router.push('/order/priceCheck')">
|
<a-button @click="router.push('/order/priceCheck')">
|
||||||
{{ t('order.myOrders.empty.goPriceCheck') }}
|
{{ t('order.myOrders.empty.goPriceCheck') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@ -2514,7 +2514,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
margin: 0 16px 12px;
|
margin: 0 16px 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustom
|
|||||||
import { setRapidPackageHostCookie, setRapidPackageWarehouseCookie, setRapidPackageCustomerCookie } from '@/utils/rapidPackagePluginHost'
|
import { setRapidPackageHostCookie, setRapidPackageWarehouseCookie, setRapidPackageCustomerCookie } from '@/utils/rapidPackagePluginHost'
|
||||||
import { useCurrentWarehouseStore } from '@/store'
|
import { useCurrentWarehouseStore } from '@/store'
|
||||||
import { PRINT_CONTROL_DOWNLOAD_URL } from '@/constants/printControl'
|
import { PRINT_CONTROL_DOWNLOAD_URL } from '@/constants/printControl'
|
||||||
|
import { tPrintControlHint } from '@/utils/hiprint/printControlHints'
|
||||||
|
|
||||||
defineOptions({ name: 'WarehouseParcel' })
|
defineOptions({ name: 'WarehouseParcel' })
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ interface ParcelItemRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const pluginHintDownloadText = computed(() => tPrintControlHint('pluginDownload'))
|
||||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||||
const {
|
const {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
@ -671,7 +673,7 @@ onMounted(async () => {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<icon-download />
|
<icon-download />
|
||||||
{{ t('warehouse.parcel.pluginHintDownload') }}
|
{{ pluginHintDownloadText }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user