Merge pull request '20261027' (#13) from test into master
All checks were successful
Build and Deploy Front / deploy (push) Successful in 2m16s

Reviewed-on: #13
This commit is contained in:
zzh 2026-10-27 17:52:02 +08:00
commit 3d7a19f4a7
36 changed files with 1631 additions and 298 deletions

View File

@ -1,17 +1,18 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { getWarehouseList } from '@/api/system/warehouse'
import { useCurrentWarehouseStore } from '@/store'
import WarehouseAddressPeekModal from '@/components/customer-side-rail/WarehouseAddressPeekModal.vue'
const props = withDefaults(
defineProps<{
variant?: 'navbar' | 'login' | 'sider'
/** 下拉位置;侧栏沉底用 rt */
/** 下拉位置;侧栏沉底用 rt(login 下拉保留) */
dropdownPosition?: 'br' | 'bl' | 'tr' | 'tl' | 'rt' | 'rb'
/** 侧栏收起:窄宽展示缩略文案(无图标) */
compact?: boolean
/** 下拉触发;侧栏默认 click,顶栏 hover */
/** 下拉触发;侧栏默认 click,顶栏 hover(已改为弹窗,仅 login 相关遗留) */
trigger?: 'hover' | 'click'
/** 侧栏展开:显示「发货仓库」语义前缀 */
showCaption?: boolean
@ -29,6 +30,7 @@ const props = withDefaults(
const { t } = useI18n()
const warehouseStore = useCurrentWarehouseStore()
const peekVisible = ref(false)
const displayName = computed(
() =>
@ -45,24 +47,13 @@ const labelText = computed(() => {
return name
})
const useDropdown = computed(() => props.variant === 'navbar' || props.variant === 'sider')
const dropdownTrigger = computed(() => {
if (props.trigger) return props.trigger
return props.variant === 'sider' ? 'click' : 'hover'
})
/** 顶栏 / 侧栏:点击打开仓库地址卡片弹窗(与客户侧栏一致) */
const usePeekModal = computed(() => props.variant === 'navbar' || props.variant === 'sider')
const showSiderCaption = computed(
() => props.showCaption && props.variant === 'sider' && !props.compact,
)
const contentClass = computed(() => props.contentClass || undefined)
const popupTranslate = computed<[number, number] | undefined>(() => {
const pos = props.dropdownPosition
return pos === 'tl' || pos === 'tr' ? [0, -6] : undefined
})
async function loadServiceDataList() {
try {
const { items } = await getWarehouseList({
@ -82,8 +73,8 @@ async function loadServiceDataList() {
}
}
function onSelectWarehouse(id: number) {
warehouseStore.setWarehouseId(id)
function openPeek() {
peekVisible.value = true
}
onMounted(() => {
@ -99,37 +90,19 @@ onMounted(() => {
{ 'is-compact': compact },
]"
>
<a-dropdown
v-if="useDropdown"
:trigger="dropdownTrigger"
:position="dropdownPosition"
:content-class="contentClass"
:popup-translate="popupTranslate"
:popup-max-height="320"
<button
v-if="usePeekModal"
type="button"
class="contact-service__label"
:title="displayName"
:aria-label="`${t('nav.contactService.shipWarehouse')}: ${displayName}`"
@click="openPeek"
>
<button
type="button"
class="contact-service__label"
:title="displayName"
:aria-label="`${t('nav.contactService.shipWarehouse')}: ${displayName}`"
>
<span v-if="showSiderCaption" class="contact-service__caption">
{{ t('nav.contactService.shipWarehouse') }}
</span>
<span class="contact-service__text">{{ labelText }}</span>
</button>
<template #content>
<a-doption
v-for="opt in warehouseStore.options"
:key="String(opt.value)"
:value="opt.value"
:selected="opt.value === warehouseStore.warehouseId"
@click="onSelectWarehouse(opt.value)"
>
{{ opt.label }}
</a-doption>
</template>
</a-dropdown>
<span v-if="showSiderCaption" class="contact-service__caption">
{{ t('nav.contactService.shipWarehouse') }}
</span>
<span class="contact-service__text">{{ labelText }}</span>
</button>
<a-select
v-else
@ -147,6 +120,11 @@ onMounted(() => {
{{ opt.label }}
</a-option>
</a-select>
<WarehouseAddressPeekModal
v-if="usePeekModal"
v-model:visible="peekVisible"
/>
</div>
</template>

View File

@ -1,7 +1,8 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { ARCO_ICON_NAMES, filterArcoIconNames } from '@/utils/arco-icon-list';
import { filterArcoIconNames } from '@/utils/arco-icon-list';
import { MENU_ICON_CATEGORIES } from '@/utils/menu-icon-catalog';
import { resolveArcoIcon, toPascalIconName } from '@/utils/menu-icon';
defineOptions({ name: 'MenuIconPicker' });
@ -28,60 +29,33 @@
const pickerVisible = ref(false);
const keyword = ref('');
const draftIcon = ref('');
const activeCategory = ref(MENU_ICON_CATEGORIES[0]?.key || 'common');
const normalizedValue = computed(() => toPascalIconName(props.modelValue || ''));
const previewIcon = computed(() => resolveArcoIcon(normalizedValue.value));
const filteredIcons = computed(() => {
const list = filterArcoIconNames(keyword.value)
if (keyword.value.trim()) return list
const commonSet = new Set(commonIcons)
return list.filter((name) => !commonSet.has(name))
const categoryTabs = computed(() =>
MENU_ICON_CATEGORIES.map((cat) => ({
key: cat.key,
label: t(`system.menu.iconPicker.category.${cat.key}`),
count: cat.icons.length,
})),
);
const categoryIcons = computed(() => {
const cat = MENU_ICON_CATEGORIES.find((item) => item.key === activeCategory.value);
return cat?.icons || [];
});
const commonIcons = [
'IconHome',
'IconMenu',
'IconApps',
'IconBook',
'IconFile',
'IconFolder',
'IconList',
'IconOrderedList',
'IconSettings',
'IconUser',
'IconUserGroup',
'IconSearch',
'IconPlus',
'IconEdit',
'IconDelete',
'IconRefresh',
'IconLink',
'IconSend',
'IconDownload',
'IconUpload',
'IconCloud',
'IconComputer',
'IconStorage',
'IconTag',
'IconLocation',
'IconBarChart',
'IconSchedule',
'IconSafe',
'IconLock',
'IconPrinter',
'IconBug',
'IconCode',
'IconExperiment',
'IconThunderbolt',
'IconRobot',
'IconDashboard',
].filter((name) => ARCO_ICON_NAMES.includes(name));
const searchIcons = computed(() => filterArcoIconNames(keyword.value));
const isSearching = computed(() => Boolean(keyword.value.trim()));
watch(pickerVisible, (visible) => {
if (visible) {
draftIcon.value = normalizedValue.value;
keyword.value = '';
activeCategory.value = MENU_ICON_CATEGORIES[0]?.key || 'common';
}
});
@ -133,7 +107,7 @@
v-model:visible="pickerVisible"
:title="t('system.menu.iconPicker.title')"
:footer="false"
width="720px"
width="840px"
unmount-on-close
class="menu-icon-picker-modal"
>
@ -150,33 +124,74 @@
</div>
</div>
<div v-if="!keyword.trim()" class="picker-section">
<div class="picker-section-title">{{ t('system.menu.iconPicker.common') }}</div>
<div class="icon-grid">
<template v-if="!isSearching">
<div class="picker-category-tabs">
<button
v-for="name in commonIcons"
:key="`common-${name}`"
v-for="tab in categoryTabs"
:key="tab.key"
type="button"
class="icon-item"
:class="{ active: draftIcon === name }"
:title="name"
@click="selectIcon(name)"
class="category-tab"
:class="{ active: activeCategory === tab.key }"
@click="activeCategory = tab.key"
>
<component :is="resolveArcoIcon(name)" />
<span class="icon-name">{{ name.replace(/^Icon/, '') }}</span>
<span>{{ tab.label }}</span>
<em>{{ tab.count }}</em>
</button>
</div>
</div>
<div class="picker-section picker-section-scroll">
<div class="picker-section">
<div class="picker-section-title">
{{ t(`system.menu.iconPicker.category.${activeCategory}`) }}
<span class="picker-count">({{ categoryIcons.length }})</span>
</div>
<div class="icon-grid">
<button
v-for="name in categoryIcons"
:key="`cat-${name}`"
type="button"
class="icon-item"
:class="{ active: draftIcon === name }"
:title="name"
@click="selectIcon(name)"
>
<component :is="resolveArcoIcon(name)" />
<span class="icon-name">{{ name.replace(/^Icon/, '') }}</span>
</button>
</div>
</div>
<div class="picker-section picker-section-scroll">
<div class="picker-section-title">
{{ t('system.menu.iconPicker.all') }}
<span class="picker-count">({{ searchIcons.length }})</span>
<span class="picker-hint">{{ t('system.menu.iconPicker.allHint') }}</span>
</div>
<div class="icon-grid">
<button
v-for="name in searchIcons"
:key="`all-${name}`"
type="button"
class="icon-item"
:class="{ active: draftIcon === name }"
:title="name"
@click="selectIcon(name)"
>
<component :is="resolveArcoIcon(name)" />
<span class="icon-name">{{ name.replace(/^Icon/, '') }}</span>
</button>
</div>
</div>
</template>
<div v-else class="picker-section picker-section-scroll">
<div class="picker-section-title">
{{ keyword.trim() ? t('system.menu.iconPicker.searchResult') : t('system.menu.iconPicker.all') }}
<span class="picker-count">({{ filteredIcons.length }})</span>
{{ t('system.menu.iconPicker.searchResult') }}
<span class="picker-count">({{ searchIcons.length }})</span>
</div>
<div class="icon-grid">
<button
v-for="name in filteredIcons"
:key="name"
v-for="name in searchIcons"
:key="`search-${name}`"
type="button"
class="icon-item"
:class="{ active: draftIcon === name }"
@ -188,7 +203,7 @@
</button>
</div>
<a-empty
v-if="filteredIcons.length === 0"
v-if="searchIcons.length === 0"
:description="t('system.menu.iconPicker.empty')"
/>
</div>
@ -236,7 +251,7 @@
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
margin-bottom: 12px;
}
.picker-current {
@ -254,6 +269,48 @@
}
}
.picker-category-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.category-tab {
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background: #fff;
color: var(--color-text-2);
cursor: pointer;
transition: all 0.15s ease;
em {
font-style: normal;
font-size: 12px;
color: var(--color-text-3);
}
&:hover {
border-color: var(--app-primary-color);
color: var(--app-primary-color);
}
&.active {
border-color: var(--app-primary-color);
background: var(--app-primary-color-light, #eef7f8);
color: var(--app-primary-color);
em {
color: var(--app-primary-color);
}
}
}
.picker-section {
& + .picker-section {
margin-top: 16px;
@ -279,9 +336,16 @@
color: var(--color-text-3);
}
.picker-hint {
margin-left: 8px;
font-weight: 400;
font-size: 12px;
color: var(--color-text-4);
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
gap: 8px;
}

View File

@ -716,14 +716,25 @@ const onMenuClick = async (event: { key: string }) => {
padding: 0 !important;
margin: 0 !important;
}
/* 固定宽度面板:禁止横向滚动条 */
.arco-scrollbar-container {
overflow-x: hidden !important;
}
.arco-scrollbar-track-direction-horizontal {
display: none !important;
}
}
.csr-user-panel {
/* 与客户侧栏展开宽对齐 */
width: 200px;
max-width: 200px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow-x: hidden;
background: #fff;
color: var(--color-text-1);
@ -840,7 +851,8 @@ const onMenuClick = async (event: { key: string }) => {
&__body {
padding: 6px;
max-height: min(52vh, 360px);
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}
&__footer {

View File

@ -1,14 +1,13 @@
/**
* 订单状态(与 oms0414-back OrderStatusCodes、设计文档 §10.3.1 一致)
*
* 标签色系:复用 utils/statusTag 信息强弱分档。
* 标签色系:按生命周期语义分色;渲染仍走 utils/statusTag。
*/
import {
STATUS_TAG_TONES,
buildBooleanTagHtml,
buildStatusTagHtml,
getStatusTagStyle,
type StatusTagColors,
type StatusTagTone,
} from '@/utils/statusTag'
@ -46,10 +45,6 @@ export function isOrderDeletable(status: unknown): boolean {
return ORDER_DELETABLE_STATUSES.includes(code as OrderStatusCode)
}
function tone(t: StatusTagTone): StatusTagColors {
return STATUS_TAG_TONES[t]
}
export interface OrderStatusMeta {
value: OrderStatusCode
/** i18n key:tabs.* 与 status.* 共用同一套文案 */
@ -87,62 +82,106 @@ export const ORDER_STATUS_NAME_TO_CODE: Record<string, OrderStatusCode> = {
已认领: 'claimed',
}
/**
* 订单状态标签色:按生命周期语义分色(软底标签),便于列表一眼区分。
* 待办暖色 / 在途品牌色 / 成功绿 / 失败红 / 问题橙 / 理赔紫。
*/
export const ORDER_STATUS_LIST: OrderStatusMeta[] = [
{ value: 'draft', labelKey: 'order.myOrders.status.draft', ...tone('neutral') },
{ value: 'predeclared', labelKey: 'order.myOrders.status.predeclared', ...tone('brand') },
{
value: 'draft',
labelKey: 'order.myOrders.status.draft',
tagColor: '#6b7280',
tagBgColor: '#f3f4f6',
tagBorder: '#e5e7eb',
},
{
value: 'predeclared',
labelKey: 'order.myOrders.status.predeclared',
tagColor: '#3d6a8a',
tagBgColor: '#eef4f8',
tagBorder: '#d0dee8',
},
{
value: 'pendingAllocation',
labelKey: 'order.myOrders.status.pendingAllocation',
...tone('wait'),
tagColor: '#8a6d3b',
tagBgColor: '#f7f2e8',
tagBorder: '#e8dcc6',
rowBg: '#faf8f5',
showTabCount: true,
},
{
value: 'pendingPayment',
labelKey: 'order.myOrders.status.pendingPayment',
tagColor: '#f53f3f',
tagBgColor: '#ffece8',
tagBorder: '#fbaca3',
rowBg: '#ffece8',
tagColor: '#ea580c',
tagBgColor: '#fff4e5',
tagBorder: '#fb923c',
rowBg: '#fff7ed',
showTabCount: true,
},
{
value: 'pendingShip',
labelKey: 'order.myOrders.status.pendingShip',
...tone('wait'),
tagColor: '#0f766e',
tagBgColor: '#f0fdfa',
tagBorder: '#99f6e4',
showTabCount: true,
},
{
value: 'pendingOnline',
labelKey: 'order.myOrders.status.pendingOnline',
...tone('brand'),
tagColor: '#1d4ed8',
tagBgColor: '#eff6ff',
tagBorder: '#bfdbfe',
showTabCount: true,
},
{
value: 'shipping',
labelKey: 'order.myOrders.status.shipping',
...tone('brand'),
tagColor: '#054f74',
tagBgColor: '#eef3f6',
tagBorder: '#b6cdd9',
showTabCount: true,
},
{
value: 'deliverySuccess',
labelKey: 'order.myOrders.status.deliverySuccess',
...tone('ok'),
tagColor: '#15803d',
tagBgColor: '#f0fdf4',
tagBorder: '#bbf7d0',
showTabCount: true,
},
{
value: 'deliveryFailed',
labelKey: 'order.myOrders.status.deliveryFailed',
...tone('alert'),
tagColor: '#b91c1c',
tagBgColor: '#fef2f2',
tagBorder: '#fecaca',
showTabCount: true,
},
{ value: 'deleted', labelKey: 'order.myOrders.status.deleted', ...tone('mute') },
{
value: 'deleted',
labelKey: 'order.myOrders.status.deleted',
tagColor: '#9ca3af',
tagBgColor: '#f9fafb',
tagBorder: '#e5e7eb',
},
{
value: 'problematic',
labelKey: 'order.myOrders.status.problematic',
...tone('alert'),
tagColor: '#c2410c',
tagBgColor: '#fff7ed',
tagBorder: '#fdba74',
rowBg: '#fff7ed',
showTabCount: true,
},
{ value: 'claimed', labelKey: 'order.myOrders.status.claimed', ...tone('neutral') },
{
value: 'claimed',
labelKey: 'order.myOrders.status.claimed',
tagColor: '#6d28d9',
tagBgColor: '#f5f3ff',
tagBorder: '#ddd6fe',
},
]
/** 已出库/在途及之后(不可拦截) */

View File

@ -358,6 +358,14 @@ const CORE_GROUPS: PageButtonGroup[] = [
{ code: 'Query', name: '查询' },
{ code: 'View', name: '查看监控' },
]),
B('Home.EventBusAdmin', [
{ code: 'Query', name: '查询' },
{ code: 'View', name: '查看 OMS EventBus' },
]),
B('Home.EventBusTrack', [
{ code: 'Query', name: '查询' },
{ code: 'View', name: '查看轨迹 EventBus' },
]),
// ── API ──
B('Api.UpstreamInterface', [

View File

@ -7,6 +7,7 @@ import localeH5AdminCenter from '@/views/h5/admin-center/locale/en-US';
import localeWorkplace from '@/views/home/workplace/locale/en-US';
import localeMonitor from '@/views/home/monitor/locale/en-US';
import localeEventBus from '@/views/home/eventbus/locale/en-US';
import localeSettings from './en-US/settings';
import marketingEn from './marketing/en-US';
@ -3442,7 +3443,7 @@ export default {
'system.menu.message.syncLevel3Skipped': 'Nothing to sync, {skipped} existing button(s) skipped',
'system.menu.message.syncLevel3Failed': 'Failed to sync level-3 buttons',
'system.menu.message.syncModuleSuccess':
'Synced: {level2Created} level-2 created, {level2Skipped} skipped; level-3 {level3Created} created, {level3Updated} updated, {level3Deleted} deleted, {level3Skipped} skipped',
'Synced: {level2Created} level-2 created, {level2Updated} updated, {level2Skipped} skipped; level-3 {level3Created} created, {level3Updated} updated, {level3Deleted} deleted, {level3Skipped} skipped',
'system.menu.message.syncModuleSkipped':
'Nothing to sync, skipped {level2Skipped} level-2 menu(s) and {level3Skipped} level-3 button(s)',
'system.menu.message.syncModulePartialFailed': 'Some pages failed to sync level-3 buttons: {details}',
@ -3646,12 +3647,21 @@ export default {
'system.menu.iconPicker.choose': 'Choose icon',
'system.menu.iconPicker.title': 'Choose menu icon',
'system.menu.iconPicker.placeholder': 'Enter or pick an icon, e.g. IconBook',
'system.menu.iconPicker.searchPlaceholder': 'Search icon name, e.g. Book, User, File',
'system.menu.iconPicker.searchPlaceholder': 'Search icon name, e.g. Book, User, File, Dashboard',
'system.menu.iconPicker.current': 'Selected',
'system.menu.iconPicker.common': 'Common',
'system.menu.iconPicker.all': 'All icons',
'system.menu.iconPicker.allHint': 'Or search by name',
'system.menu.iconPicker.searchResult': 'Search results',
'system.menu.iconPicker.empty': 'No matching icons',
'system.menu.iconPicker.category.common': 'Common',
'system.menu.iconPicker.category.nav': 'Navigation',
'system.menu.iconPicker.category.user': 'Users',
'system.menu.iconPicker.category.order': 'Orders / Warehouse',
'system.menu.iconPicker.category.finance': 'Finance / Marketing',
'system.menu.iconPicker.category.track': 'Tracking / Logistics',
'system.menu.iconPicker.category.system': 'System / Dev',
'system.menu.iconPicker.category.status': 'Status',
// System - Role Management (Permission Assignment)
'system.role.permission.expandAll': 'Expand All',
@ -6811,6 +6821,7 @@ export default {
...localeWorkplace,
...localeMonitor,
...localeEventBus,
...marketingEn,
...kbEn,
};

View File

@ -7,6 +7,7 @@ import localeH5AdminCenter from '@/views/h5/admin-center/locale/zh-CN';
import localeWorkplace from '@/views/home/workplace/locale/zh-CN';
import localeMonitor from '@/views/home/monitor/locale/zh-CN';
import localeEventBus from '@/views/home/eventbus/locale/zh-CN';
import localeSettings from './zh-CN/settings';
import marketingZh from './marketing/zh-CN';
@ -3300,7 +3301,7 @@ export default {
'system.menu.message.syncLevel3Skipped': '无需同步,已跳过 {skipped} 个已存在按钮',
'system.menu.message.syncLevel3Failed': '同步三级按钮失败',
'system.menu.message.syncModuleSuccess':
'同步完成:二级菜单新增 {level2Created} 个、跳过 {level2Skipped} 个;三级按钮新增 {level3Created} 个、修改 {level3Updated} 个、删除 {level3Deleted} 个、跳过 {level3Skipped} 个',
'同步完成:二级菜单新增 {level2Created} 个、更新 {level2Updated} 个、跳过 {level2Skipped} 个;三级按钮新增 {level3Created} 个、修改 {level3Updated} 个、删除 {level3Deleted} 个、跳过 {level3Skipped} 个',
'system.menu.message.syncModuleSkipped':
'无需同步,已跳过 {level2Skipped} 个二级菜单、{level3Skipped} 个三级按钮',
'system.menu.message.syncModulePartialFailed': '部分页面三级按钮同步失败:{details}',
@ -3503,12 +3504,21 @@ export default {
'system.menu.iconPicker.choose': '选择图标',
'system.menu.iconPicker.title': '选择菜单图标',
'system.menu.iconPicker.placeholder': '请输入或选择图标,如 IconBook',
'system.menu.iconPicker.searchPlaceholder': '搜索图标名称,如 Book、User、File',
'system.menu.iconPicker.searchPlaceholder': '搜索图标名称,如 Book、User、File、Dashboard',
'system.menu.iconPicker.current': '当前选中',
'system.menu.iconPicker.common': '常用图标',
'system.menu.iconPicker.all': '全部图标',
'system.menu.iconPicker.allHint': '也可直接搜索名称',
'system.menu.iconPicker.searchResult': '搜索结果',
'system.menu.iconPicker.empty': '未找到匹配图标',
'system.menu.iconPicker.category.common': '常用',
'system.menu.iconPicker.category.nav': '导航布局',
'system.menu.iconPicker.category.user': '用户组织',
'system.menu.iconPicker.category.order': '订单仓储',
'system.menu.iconPicker.category.finance': '财务营销',
'system.menu.iconPicker.category.track': '轨迹物流',
'system.menu.iconPicker.category.system': '系统开发',
'system.menu.iconPicker.category.status': '状态提示',
// 系统 - 角色管理(权限分配相关)
'system.role.permission.adminAllHint': 'Admin 角色默认拥有全部菜单与按钮权限,无需分配',
@ -6820,6 +6830,7 @@ export default {
...localeWorkplace,
...localeMonitor,
...localeEventBus,
...marketingZh,
...kbZh,
};

View File

@ -32,6 +32,38 @@ const DASHBOARD: AppRouteRecordRaw = {
permission: 'Home.Monitor',
},
},
{
path: 'adminEventBus',
name: 'AdminEventBus',
component: () => import('@/views/home/eventbus/index.vue'),
meta: {
locale: 'menu.home.adminEventBus',
requiresAuth: true,
roles: ['*'],
adminPortalOnly: true,
menuPermission: 'Home.EventBusAdmin',
icon: 'icon-mind-mapping',
order: 10,
ignoreCache: true,
frameSrc: 'https://api.test.ideship.com/admin/eventbus',
},
},
{
path: 'trackEventBus',
name: 'TrackEventBus',
component: () => import('@/views/home/eventbus/index.vue'),
meta: {
locale: 'menu.home.trackEventBus',
requiresAuth: true,
roles: ['*'],
adminPortalOnly: true,
menuPermission: 'Home.EventBusTrack',
icon: 'icon-relation',
order: 11,
ignoreCache: true,
frameSrc: 'https://api.test.ideship.com/track/eventbus',
},
},
],
};

View File

@ -18,5 +18,7 @@ declare module 'vue-router' {
order?: number; // Sort routing menu items. If set key, the higher the value, the more forward it is
noAffix?: boolean; // if set true, the tag will not affix in the tab-bar
ignoreCache?: boolean; // if set true, the page will not be cached
/** 外链嵌入页地址(iframe src),如 EventBus Dashboard */
frameSrc?: string;
}
}

View File

@ -95,10 +95,17 @@ const ORDER_NUMBER_FIELD_MAP: Record<string, FieldMapEntry> = {
const ORDER_EXTRA_FIELD_MAP: Record<string, FieldMapEntry> = {
orderStatus: { field: 'orderStatus', options: { multiAsIn: true } },
previousOrderStatus: { field: 'previousOrderStatus', options: { multiAsIn: true } },
handleStatus: { field: 'handleStatus', options: { multiAsIn: true } },
fulfillmentType: { field: 'fulfillmentType', options: { multiAsIn: true } },
stockBindStatus: { field: 'stockBindStatus', options: { multiAsIn: true } },
fulfillProgress: { field: 'fulfillProgress', options: { multiAsIn: true } },
shippingMethod: 'productCode',
serviceChannel: 'channelCode',
isPaid: { field: 'isPaid', options: { multiAsIn: true } },
isIntercept: { field: 'isIntercept', options: { multiAsIn: true } },
isRemote: { field: 'isRemote', options: { multiAsIn: true } },
isCod: { field: 'isCod', options: { multiAsIn: true } },
receivableAmount: { field: 'payableAmount', options: { asNumber: true } },
payableAmount: { field: 'payableAmount', options: { asNumber: true } },
predeclareFailReason: 'predeclareFailReason',

View File

@ -0,0 +1,475 @@
import { ARCO_ICON_NAMES } from '@/utils/arco-icon-list'
export interface MenuIconCategory {
/** i18n key: system.menu.iconPicker.category.{key} */
key: string
icons: string[]
}
/**
* 菜单图标选择器分类(尽量多放业务可用的 Arco Icon;不存在的会自动过滤)。
*/
const RAW_CATEGORIES: MenuIconCategory[] = [
{
key: 'common',
icons: [
'IconHome',
'IconDashboard',
'IconApps',
'IconMenu',
'IconSettings',
'IconList',
'IconOrderedList',
'IconUnorderedList',
'IconFile',
'IconFolder',
'IconBook',
'IconBookmark',
'IconSearch',
'IconFilter',
'IconSort',
'IconPlus',
'IconPlusCircle',
'IconEdit',
'IconDelete',
'IconSave',
'IconCopy',
'IconPaste',
'IconRefresh',
'IconSync',
'IconLink',
'IconLaunch',
'IconShareInternal',
'IconShareExternal',
'IconEye',
'IconEyeInvisible',
'IconMore',
'IconMoreVertical',
'IconStar',
'IconHeart',
'IconFire',
'IconBulb',
'IconThunderbolt',
'IconGift',
'IconTrophy',
'IconPushpin',
'IconTag',
'IconTags',
'IconCalendar',
'IconSchedule',
'IconClockCircle',
'IconHistory',
'IconNotification',
'IconMessage',
'IconUser',
'IconUserGroup',
'IconSafe',
'IconLock',
'IconComputer',
'IconStorage',
'IconCloud',
'IconSend',
'IconDownload',
'IconUpload',
'IconPrinter',
'IconScan',
'IconQrcode',
'IconLocation',
'IconCompass',
'IconPublic',
'IconBarChart',
'IconMindMapping',
'IconRelation',
'IconRobot',
'IconTool',
'IconCode',
'IconBug',
'IconExperiment',
],
},
{
key: 'nav',
icons: [
'IconDashboard',
'IconHome',
'IconApps',
'IconMenu',
'IconMenuFold',
'IconMenuUnfold',
'IconNav',
'IconLayout',
'IconLayers',
'IconCommon',
'IconMindMapping',
'IconRelation',
'IconBranch',
'IconSubscribe',
'IconSubscribeAdd',
'IconSubscribed',
'IconPushpin',
'IconBookmark',
'IconCompass',
'IconLocation',
'IconPublic',
'IconTranslate',
'IconLanguage',
'IconMoon',
'IconSun',
'IconSkin',
'IconExpand',
'IconShrink',
'IconFullscreen',
'IconFullscreenExit',
'IconToTop',
'IconToBottom',
'IconToLeft',
'IconToRight',
'IconUp',
'IconDown',
'IconLeft',
'IconRight',
'IconArrowUp',
'IconArrowDown',
'IconArrowLeft',
'IconArrowRight',
'IconArrowRise',
'IconArrowFall',
'IconDoubleUp',
'IconDoubleDown',
'IconDoubleLeft',
'IconDoubleRight',
'IconCaretUp',
'IconCaretDown',
'IconCaretLeft',
'IconCaretRight',
'IconSwap',
'IconLoop',
'IconDragArrow',
'IconDragDot',
'IconDragDotVertical',
],
},
{
key: 'user',
icons: [
'IconUser',
'IconUserGroup',
'IconUserAdd',
'IconIdcard',
'IconMan',
'IconWoman',
'IconSafe',
'IconLock',
'IconUnlock',
'IconEmail',
'IconAt',
'IconPhone',
'IconMobile',
'IconMessage',
'IconMessageBanned',
'IconNotification',
'IconNotificationClose',
'IconCustomerService',
'IconVoice',
'IconRobot',
'IconRobotAdd',
'IconTrophy',
'IconGift',
'IconStamp',
'IconHeart',
'IconStar',
'IconThumbUp',
'IconThumbDown',
'IconReply',
'IconShareInternal',
'IconShareExternal',
'IconShareAlt',
'IconShake',
'IconEar',
'IconDice',
],
},
{
key: 'order',
icons: [
'IconFile',
'IconDriveFile',
'IconFileImage',
'IconFilePdf',
'IconFileAudio',
'IconFileVideo',
'IconArchive',
'IconFolder',
'IconFolderAdd',
'IconFolderDelete',
'IconStorage',
'IconTags',
'IconTag',
'IconQrcode',
'IconScan',
'IconPrinter',
'IconImport',
'IconExport',
'IconSend',
'IconInteraction',
'IconSwap',
'IconSelectAll',
'IconCheckSquare',
'IconCheck',
'IconCalendar',
'IconCalendarClock',
'IconSchedule',
'IconClockCircle',
'IconHistory',
'IconAttachment',
'IconCopy',
'IconPaste',
'IconSave',
'IconScissor',
'IconFilter',
'IconFindReplace',
'IconSort',
'IconSortAscending',
'IconSortDescending',
'IconList',
'IconOrderedList',
'IconUnorderedList',
'IconEmpty',
'IconOriginalSize',
'IconZoomIn',
'IconZoomOut',
'IconRotateLeft',
'IconRotateRight',
'IconImage',
'IconImageClose',
'IconCamera',
'IconVideoCamera',
'IconMusic',
'IconRecord',
'IconRecordStop',
],
},
{
key: 'finance',
icons: [
'IconStorage',
'IconGift',
'IconTrophy',
'IconStamp',
'IconStar',
'IconHeart',
'IconFire',
'IconBulb',
'IconThunderbolt',
'IconBarChart',
'IconDashboard',
'IconWechatpay',
'IconAlipayCircle',
'IconSwap',
'IconList',
'IconFile',
'IconSafe',
'IconLock',
'IconUnlock',
'IconIdcard',
'IconCalendar',
'IconSchedule',
'IconHistory',
'IconTag',
'IconTags',
'IconBookmark',
'IconPushpin',
'IconSubscribe',
'IconSubscribed',
'IconThumbUp',
'IconThumbDown',
'IconCopyright',
],
},
{
key: 'track',
icons: [
'IconComputer',
'IconLiveBroadcast',
'IconSend',
'IconCompass',
'IconLocation',
'IconPublic',
'IconBookmark',
'IconPushpin',
'IconCheckCircle',
'IconExclamationCircle',
'IconCloseCircle',
'IconInfoCircle',
'IconQuestionCircle',
'IconMessage',
'IconNotification',
'IconDownload',
'IconUpload',
'IconCloud',
'IconCloudDownload',
'IconSync',
'IconRefresh',
'IconLoop',
'IconHistory',
'IconSchedule',
'IconCalendarClock',
'IconClockCircle',
'IconThunderbolt',
'IconMindMapping',
'IconRelation',
'IconBranch',
'IconInteraction',
'IconLaunch',
'IconShareExternal',
'IconWifi',
'IconMobile',
'IconPhone',
'IconScan',
'IconQrcode',
'IconPrinter',
'IconImport',
'IconExport',
'IconLoading',
'IconEmpty',
],
},
{
key: 'system',
icons: [
'IconSettings',
'IconTool',
'IconCode',
'IconCodeBlock',
'IconCodeSquare',
'IconCodeSandbox',
'IconCodepen',
'IconBug',
'IconExperiment',
'IconCommand',
'IconDesktop',
'IconComputer',
'IconCloud',
'IconCloudDownload',
'IconApps',
'IconCommon',
'IconStorage',
'IconDriveFile',
'IconWifi',
'IconPoweroff',
'IconPen',
'IconBrush',
'IconEraser',
'IconPalette',
'IconBgColors',
'IconFontColors',
'IconHighlight',
'IconCamera',
'IconVideoCamera',
'IconImage',
'IconMusic',
'IconPlayArrow',
'IconPlayCircle',
'IconPause',
'IconPauseCircle',
'IconStop',
'IconSkipNext',
'IconSkipPrevious',
'IconForward',
'IconBackward',
'IconMute',
'IconSound',
'IconRobot',
'IconRobotAdd',
'IconFormula',
'IconQuote',
'IconUndo',
'IconRedo',
'IconMosaic',
'IconScissor',
'IconBold',
'IconItalic',
'IconUnderline',
'IconStrikethrough',
'IconAlignLeft',
'IconAlignCenter',
'IconAlignRight',
'IconLineHeight',
'IconH1',
'IconH2',
'IconH3',
],
},
{
key: 'status',
icons: [
'IconCheck',
'IconCheckCircle',
'IconCheckSquare',
'IconClose',
'IconCloseCircle',
'IconExclamation',
'IconExclamationCircle',
'IconInfo',
'IconInfoCircle',
'IconQuestion',
'IconQuestionCircle',
'IconPlus',
'IconPlusCircle',
'IconMinus',
'IconMinusCircle',
'IconClockCircle',
'IconStop',
'IconPause',
'IconPauseCircle',
'IconPlayArrow',
'IconPlayCircle',
'IconLoading',
'IconSync',
'IconRefresh',
'IconLoop',
'IconObliqueLine',
'IconEmpty',
'IconEye',
'IconEyeInvisible',
'IconLock',
'IconUnlock',
'IconSafe',
'IconFire',
'IconThunderbolt',
'IconBulb',
'IconStar',
'IconHeart',
'IconThumbUp',
'IconThumbDown',
'IconUpCircle',
'IconDownCircle',
'IconLeftCircle',
'IconRightCircle',
'IconArrowRise',
'IconArrowFall',
],
},
]
function keepExisting(icons: string[]): string[] {
const available = new Set(ARCO_ICON_NAMES)
const seen = new Set<string>()
const result: string[] = []
for (const name of icons) {
if (!available.has(name) || seen.has(name)) continue
seen.add(name)
result.push(name)
}
return result
}
export const MENU_ICON_CATEGORIES: MenuIconCategory[] = RAW_CATEGORIES.map((cat) => ({
key: cat.key,
icons: keepExisting(cat.icons),
})).filter((cat) => cat.icons.length > 0)
/** 扁平常用图标(用于兼容旧逻辑) */
export const MENU_COMMON_ICONS: string[] = keepExisting(
MENU_ICON_CATEGORIES.flatMap((cat) => cat.icons),
)

View File

@ -18,6 +18,10 @@ export const TOP_MENU_DEFAULT_ICONS: Record<string, string> = {
/** 子菜单默认图标(按 locale key) */
export const SUB_MENU_DEFAULT_ICONS: Record<string, string> = {
'menu.home': 'IconHome',
'menu.home.workplace': 'IconDashboard',
'menu.home.monitor': 'IconLiveBroadcast',
'menu.home.adminEventBus': 'IconMindMapping',
'menu.home.trackEventBus': 'IconRelation',
'menu.user.customer': 'IconUserGroup',
'menu.user.profile': 'IconUser',
'menu.user.recharge': 'IconGift',
@ -45,7 +49,7 @@ export const SUB_MENU_DEFAULT_ICONS: Record<string, string> = {
'menu.track.trackOrder': 'IconList',
'menu.track.warningOrder': 'IconExclamationCircle',
'menu.track.trackState': 'IconCheckCircle',
'menu.track.trackFlag': 'IconFlag',
'menu.track.trackFlag': 'IconBookmark',
'menu.track.trackContent': 'IconMessage',
'menu.track.trackPull': 'IconDownload',
'menu.track.trackPush': 'IconUpload',

View File

@ -1,5 +1,5 @@
/**
* 列表状态标签:按信息强弱分档(低饱和墨色),对齐 OMS 主色 #007979。
* 列表状态标签:软底圆角(无硬描边),对齐 OMS 主色。
*
* 强弱(弱 → 强):
* - mute 弱信息:否 / 关闭 / 删除 / 无效
@ -61,6 +61,13 @@ export function getStatusTagTone(tone: StatusTagTone): StatusTagColors {
export type StatusTagVariant = 'solid' | 'ghost'
function resolveTagColors(
toneOrColors: StatusTagTone | StatusTagColors | null | undefined,
): StatusTagColors {
if (!toneOrColors) return STATUS_TAG_TONES.neutral
return typeof toneOrColors === 'string' ? STATUS_TAG_TONES[toneOrColors] : toneOrColors
}
function escapeAttr(text: string): string {
return String(text)
.replace(/&/g, '&amp;')
@ -69,52 +76,55 @@ function escapeAttr(text: string): string {
.replace(/"/g, '&quot;')
}
/** Vue / a-tag 内联样式 */
/** Vue / a-tag 内联样式(软底 + 左侧色条,无硬框) */
export function getStatusTagStyle(
toneOrColors: StatusTagTone | StatusTagColors | null | undefined,
variant: StatusTagVariant = 'solid',
): Record<string, string> {
const colors =
!toneOrColors
? STATUS_TAG_TONES.neutral
: typeof toneOrColors === 'string'
? STATUS_TAG_TONES[toneOrColors]
: toneOrColors
const colors = resolveTagColors(toneOrColors)
if (variant === 'ghost') {
return {
color: colors.tagColor,
background: 'transparent',
border: `1px solid ${colors.tagBorder}`,
borderRadius: '3px',
borderRadius: '6px',
fontWeight: '500',
fontSize: '12px',
lineHeight: '20px',
padding: '0 8px',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
}
}
return {
color: colors.tagColor,
background: colors.tagBgColor,
border: `1px solid ${colors.tagBorder}`,
borderRadius: '3px',
border: 'none',
borderRadius: '6px',
boxShadow: `inset 3px 0 0 ${colors.tagColor}`,
fontWeight: '500',
fontSize: '12px',
lineHeight: '22px',
padding: '0 10px 0 12px',
display: 'inline-flex',
alignItems: 'center',
letterSpacing: '0.02em',
}
}
/** Handsontable 单元格 HTML */
/** Handsontable 单元格 HTML:软底圆角标签(无色点) */
export function buildStatusTagHtml(
text: string,
toneOrColors: StatusTagTone | StatusTagColors | null | undefined = 'neutral',
variant: StatusTagVariant = 'solid',
): string {
const colors =
!toneOrColors
? STATUS_TAG_TONES.neutral
: typeof toneOrColors === 'string'
? STATUS_TAG_TONES[toneOrColors]
: toneOrColors
const colors = resolveTagColors(toneOrColors)
const safe = escapeAttr(text)
if (variant === 'ghost') {
return `<span style="display:inline-flex;align-items:center;height:20px;padding:0 6px;border-radius:3px;font-size:11px;font-weight:500;line-height:18px;color:${colors.tagColor};background:transparent;border:1px solid ${colors.tagBorder};white-space:nowrap;">${safe}</span>`
return `<span style="display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:6px;font-size:11px;font-weight:500;line-height:18px;color:${colors.tagColor};background:transparent;border:1px solid ${colors.tagBorder};white-space:nowrap;">${safe}</span>`
}
return `<span style="display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:3px;font-size:12px;font-weight:500;line-height:20px;letter-spacing:0.02em;color:${colors.tagColor};background:${colors.tagBgColor};border:1px solid ${colors.tagBorder};white-space:nowrap;">${safe}</span>`
return `<span style="display:inline-flex;align-items:center;height:22px;padding:0 10px;border-radius:6px;font-size:12px;font-weight:500;line-height:20px;letter-spacing:0.02em;color:${colors.tagColor};background:${colors.tagBgColor};border:none;white-space:nowrap;">${safe}</span>`
}
/** 布尔「是」专用高亮绿(比状态档 ok 更醒目) */

View File

@ -41,6 +41,8 @@ export interface SyncLevel3Options {
export interface SyncMenuTreeResult {
mode: 'module' | 'page';
level2Created: number;
/** 已有二级菜单补齐图标/权限码等字段 */
level2Updated: number;
level2Skipped: number;
level3Created: number;
level3Updated: number;
@ -187,39 +189,53 @@ async function reloadMenuNode(menuId: string): Promise<MenuTreeDto | null> {
async function ensurePageMenu(
parentMenu: MenuTreeDto,
pageDef: RoutePageMenuDef,
): Promise<{ menu: MenuTreeDto; created: boolean }> {
): Promise<{ menu: MenuTreeDto; created: boolean; updated: boolean }> {
const nextIcon = pageDef.icon?.trim() || undefined;
const existing = findPageChildMenu(parentMenu, pageDef.route, pageDef.permissionCode);
if (existing?.id) {
// 已有二级菜单:补齐或纠正 permissionCode(Xxx.Query → Xxx),便于同步「查询」等三级按钮
// 已有二级菜单:补齐或纠正 permissionCode(Xxx.Query → Xxx);图标仅补空,不覆盖人工更换
const existingCode = existing.permissionCode?.trim() || '';
const nextCode = pageDef.permissionCode?.trim() || '';
const shouldPatchCode =
!!nextCode &&
(!existingCode ||
existingCode === nextCode ||
normalizeResourcePermissionCode(existingCode) === nextCode);
if (shouldPatchCode && existingCode !== nextCode) {
existingCode !== nextCode &&
(!existingCode || normalizeResourcePermissionCode(existingCode) === nextCode);
const existingIcon = existing.icon?.trim() || '';
const shouldPatchIcon = !existingIcon && !!nextIcon;
if (shouldPatchCode || shouldPatchIcon) {
const permissionCode = shouldPatchCode ? nextCode : existing.permissionCode;
const icon = existingIcon || nextIcon;
try {
await MenuService.putRbacMenuUpdate(existing.id, {
name: existing.name || pageDef.name,
permissionCode: nextCode,
icon: existing.icon || pageDef.icon,
permissionCode,
icon,
route: existing.route || pageDef.route,
component: existing.component || pageDef.component,
order: existing.order ?? pageDef.order,
isHidden: existing.isHidden ?? pageDef.isHidden,
isCached: existing.isCached ?? false,
description: existing.description || `同步二级菜单权限码: ${nextCode}`,
description:
existing.description ||
(shouldPatchIcon
? `同步二级菜单图标: ${icon}`
: `同步二级菜单权限码: ${permissionCode}`),
});
return {
menu: { ...existing, permissionCode: nextCode },
menu: {
...existing,
permissionCode: permissionCode || existing.permissionCode,
icon: icon || existing.icon,
},
created: false,
updated: true,
};
} catch {
// 更新失败时仍返回原节点,后续三级同步可能因无权限码跳过
}
}
return { menu: existing, created: false };
return { menu: existing, created: false, updated: false };
}
const createdMenu = await MenuService.postRbacMenuCreate({
@ -227,7 +243,7 @@ async function ensurePageMenu(
name: pageDef.name,
type: 1,
permissionCode: pageDef.permissionCode,
icon: pageDef.icon,
icon: nextIcon,
route: pageDef.route,
component: pageDef.component,
order: pageDef.order,
@ -243,7 +259,7 @@ async function ensurePageMenu(
name: pageDef.name,
type: 1,
permissionCode: pageDef.permissionCode,
icon: pageDef.icon,
icon: nextIcon,
route: pageDef.route,
component: pageDef.component,
order: pageDef.order,
@ -251,6 +267,7 @@ async function ensurePageMenu(
children: [],
},
created: true,
updated: false,
};
}
@ -428,6 +445,7 @@ export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResul
return {
mode: 'page',
level2Created: 0,
level2Updated: 0,
level2Skipped: 0,
level3Created: result.created,
level3Updated: result.updated,
@ -448,6 +466,7 @@ export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResul
}
let level2Created = 0;
let level2Updated = 0;
let level2Skipped = 0;
let level3Created = 0;
let level3Updated = 0;
@ -460,10 +479,12 @@ export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResul
for (const pageDef of pageDefs) {
let pageMenu: MenuTreeDto;
let created = false;
let updated = false;
try {
const ensured = await ensurePageMenu(currentParent, pageDef);
pageMenu = ensured.menu;
created = ensured.created;
updated = ensured.updated;
} catch (error: unknown) {
level3FailedPages.push(`${pageDef.name}: ${readSyncError(error)}`);
continue;
@ -474,6 +495,12 @@ export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResul
if (refreshedParent) {
currentParent = refreshedParent;
}
} else if (updated) {
level2Updated += 1;
const refreshedParent = await reloadMenuNode(menu.id);
if (refreshedParent) {
currentParent = refreshedParent;
}
} else {
level2Skipped += 1;
}
@ -502,6 +529,7 @@ export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResul
return {
mode: 'module',
level2Created,
level2Updated,
level2Skipped,
level3Created,
level3Updated,

View File

@ -3,6 +3,11 @@ import { appRoutes } from '@/router/routes';
import type { AppRouteRecordRaw } from '@/router/routes/types';
import type { MenuTreeDto } from '@/api/rbac';
import { normalizeResourcePermissionCode } from '@/constants/pageButtonCatalog';
import {
PATH_TO_LOCALE_KEY,
SUB_MENU_DEFAULT_ICONS,
toPascalIconName,
} from '@/utils/menu-icon';
export interface RoutePageMenuDef {
name: string;
@ -52,6 +57,30 @@ export function resolveRouteMenuTitle(route: AppRouteRecordRaw): string {
return String(route.name || route.path);
}
/**
* 解析二级菜单同步图标:路由 meta.icon → locale 默认表;统一存 PascalCase(如 IconBook)。
*/
export function resolveRouteMenuIcon(
route: AppRouteRecordRaw,
routePath: string,
): string | undefined {
const fromMeta = (route.meta?.icon as string | undefined)?.trim();
if (fromMeta) {
const normalized = toPascalIconName(fromMeta);
return normalized || undefined;
}
const localeKey =
PATH_TO_LOCALE_KEY[normalizeRoutePath(routePath)] ||
(route.meta?.locale as string | undefined)?.trim();
if (!localeKey) return undefined;
const fromDefault = SUB_MENU_DEFAULT_ICONS[localeKey];
if (!fromDefault) return undefined;
const normalized = toPascalIconName(fromDefault);
return normalized || undefined;
}
function toRouteRecord(route: AppRouteRecordRaw): AppRouteRecordRaw {
return route;
}
@ -105,7 +134,7 @@ export function getPageRoutesFromModule(moduleRoute: AppRouteRecordRaw): RoutePa
permissionCode: rawPermission
? normalizeResourcePermissionCode(rawPermission) || rawPermission
: undefined,
icon: (child.meta.icon as string | undefined)?.trim() || undefined,
icon: resolveRouteMenuIcon(child, routePath),
order: (child.meta.order as number | undefined) ?? 0,
isHidden: Boolean(child.meta.hideInMenu),
});

View File

@ -8,6 +8,7 @@ export default {
'h5Admin.login.title': 'Staff login',
'h5Admin.login.brandName': 'Ops Admin',
'h5Admin.login.brandHint': 'Sign in with your staff account',
'h5Admin.login.backToSite': 'Back to website',
'h5Admin.login.accountPlaceholder': 'Account',
'h5Admin.login.passwordPlaceholder': 'Password',
'h5Admin.login.submit': 'Sign in',

View File

@ -8,6 +8,7 @@ export default {
'h5Admin.login.title': '员工登录',
'h5Admin.login.brandName': '运营管理',
'h5Admin.login.brandHint': '使用员工账号登录,处理待办与运单',
'h5Admin.login.backToSite': '返回官网',
'h5Admin.login.accountPlaceholder': '账号',
'h5Admin.login.passwordPlaceholder': '密码',
'h5Admin.login.submit': '登录',

View File

@ -9,10 +9,15 @@
<main class="h5-login-main">
<section class="h5-login-card">
<header class="h5-login-card__brand">
<div class="h5-login-card__brand-row">
<button
type="button"
class="h5-login-card__brand-row"
:aria-label="t('h5Admin.login.backToSite')"
@click="goOfficialSite"
>
<img :src="logoImg" alt="" class="h5-login-card__logo" />
<h2 class="h5-login-card__title">{{ t('h5Admin.login.brandName') }}</h2>
</div>
</button>
<p class="h5-login-card__hint">{{ t('h5Admin.login.brandHint') }}</p>
</header>
@ -78,6 +83,10 @@ const rbacUserStore = useRbacUserStore();
const form = reactive({ account: '', password: '' });
const loading = ref(false);
function goOfficialSite() {
router.push('/');
}
const errorBox = ref('');
function resolveRedirectPath() {
@ -226,6 +235,12 @@ onMounted(() => {
align-items: center;
justify-content: center;
gap: 10px;
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.h5-login-card__logo {

View File

@ -315,6 +315,7 @@ export default {
'h5.login.brand': 'IDE Customer Center',
'h5.login.brandName': 'IDE',
'h5.login.brandHint': 'Welcome to your IDE portal',
'h5.login.backToSite': 'Back to website',
'h5.login.tab.wechat': 'WeChat',
'h5.login.tab.password': 'Account',
'h5.login.switchPassword': 'Sign in with password',

View File

@ -314,6 +314,7 @@ export default {
'h5.login.brand': 'IDE客户中心',
'h5.login.brandName': 'IDE',
'h5.login.brandHint': '欢迎登录 IDE 客户门户',
'h5.login.backToSite': '返回官网',
'h5.login.tab.wechat': '微信登录',
'h5.login.tab.password': '账号登录',
'h5.login.switchPassword': '账号密码登录',

View File

@ -24,10 +24,15 @@
<main class="h5-login-main">
<section class="h5-login-card">
<header class="h5-login-card__brand">
<div class="h5-login-card__brand-row">
<button
type="button"
class="h5-login-card__brand-row"
:aria-label="t('h5.login.backToSite')"
@click="goOfficialSite"
>
<img :src="logoImg" alt="" class="h5-login-card__logo" />
<h2 class="h5-login-card__title">{{ t('h5.login.brandName') }}</h2>
</div>
</button>
<p class="h5-login-card__hint">{{ t('h5.login.brandHint') }}</p>
</header>
@ -175,6 +180,10 @@ function goBack() {
router.replace('/h5/orders');
}
function goOfficialSite() {
router.push('/');
}
function goRegister() {
if (!isWeChatBrowser()) {
Message.warning(t('h5.login.openInWechat'));
@ -463,6 +472,12 @@ onMounted(async () => {
align-items: center;
justify-content: center;
gap: 10px;
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.h5-login-card__logo {

View File

@ -0,0 +1,130 @@
<template>
<div class="eventbus-embed-container">
<div class="table-toolbar">
<div class="toolbar-left">
<span class="page-title">{{ pageTitle }}</span>
</div>
<div class="toolbar-right">
<a-button class="app-btn-search" @click="reloadFrame">
{{ t('home.eventbus.reload') }}
</a-button>
<a-button type="primary" class="app-btn-primary-soft" @click="openExternal">
{{ t('home.eventbus.openNew') }}
</a-button>
</div>
</div>
<div class="frame-wrap">
<iframe
v-if="frameSrc"
:key="frameKey"
class="eventbus-frame"
:src="frameSrc"
:title="pageTitle"
referrerpolicy="no-referrer-when-downgrade"
/>
<div v-else class="frame-empty">{{ t('home.eventbus.missingUrl') }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useMenuDisplay } from '@/composables/useMenuDisplay';
const { t } = useI18n();
const route = useRoute();
const { currentTitle } = useMenuDisplay();
const frameKey = ref(0);
const frameSrc = computed(() => {
const fromMeta = String(route.meta.frameSrc || '').trim();
if (fromMeta) return fromMeta;
return String(route.query.url || '').trim();
});
const pageTitle = computed(() => {
const fromMenu = currentTitle.value?.trim();
if (fromMenu) return fromMenu;
const localeKey = String(route.meta.locale || '').trim();
return localeKey ? t(localeKey) : t('home.eventbus.title');
});
function reloadFrame() {
frameKey.value += 1;
}
function openExternal() {
if (!frameSrc.value) return;
window.open(frameSrc.value, '_blank', 'noopener,noreferrer');
}
</script>
<style scoped lang="less">
/* 与 appList 等全高页一致:用视口高度,避免父级高度链未就绪时 iframe 塌成 0(开 F12 重排才显示) */
.eventbus-embed-container {
display: flex;
flex-direction: column;
height: calc(100vh - 86px);
max-height: calc(100vh - 86px);
min-height: 0;
padding: 16px;
box-sizing: border-box;
background: #f0f3f4;
overflow: hidden;
width: 100%;
}
.table-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
padding: 12px 16px;
background: #fff;
border-radius: 4px;
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.page-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-1);
}
.frame-wrap {
flex: 1;
min-height: 0;
position: relative;
background: #fff;
border-radius: 4px;
overflow: hidden;
}
.eventbus-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
display: block;
background: #fff;
}
.frame-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-3);
}
</style>

View File

@ -0,0 +1,8 @@
export default {
'menu.home.adminEventBus': 'OMS EventBus',
'menu.home.trackEventBus': 'Track EventBus',
'home.eventbus.title': 'EventBus Dashboard',
'home.eventbus.reload': 'Reload',
'home.eventbus.openNew': 'Open in new tab',
'home.eventbus.missingUrl': 'Embed URL is not configured',
};

View File

@ -0,0 +1,8 @@
export default {
'menu.home.adminEventBus': 'OMS EventBus',
'menu.home.trackEventBus': '轨迹 EventBus',
'home.eventbus.title': 'EventBus Dashboard',
'home.eventbus.reload': '刷新',
'home.eventbus.openNew': '新窗口打开',
'home.eventbus.missingUrl': '未配置嵌入地址',
};

View File

@ -587,7 +587,7 @@ onBeforeUnmount(() => {
display: block;
opacity: 1;
transform-origin: 50% 50%;
transform: translate3d(100px, 96px, 0) scale(4.1);
transform: translate3d(100px, 96px, 0) scale(2.73);
pointer-events: none;
user-select: none;
}

View File

@ -761,8 +761,16 @@
'clientNo',
'trackNo',
'orderStatus',
'previousOrderStatus',
'handleStatus',
'fulfillmentType',
'stockBindStatus',
'fulfillProgress',
'shippingMethod',
'isPaid',
'isIntercept',
'isRemote',
'isCod',
'receivableAmount',
'predeclareFailReason',
'payTime',
@ -1621,10 +1629,30 @@
const listColumnCatalog = computed(() => filterMyOrdersColumns(MY_ORDERS_COLUMN_CATALOG));
const hotColumns = computed(() => {
const boolOptions = [
{ label: t('order.myOrders.drawer.yes'), value: '1' },
{ label: t('order.myOrders.drawer.no'), value: '0' },
];
const statusOptions = ORDER_STATUS_LIST.map((meta) => ({
label: t(meta.labelKey),
value: meta.value,
}));
const handleStatusOptions = ORDER_HANDLE_STATUS_LIST.filter((m) => m.value !== 'all').map((meta) => ({
label: t(meta.labelKey),
value: meta.value,
}));
const fulfillmentTypeOptions = [
{ label: t('order.waybillManage.filter.fulfillmentDirect'), value: 'direct' },
{ label: t('order.waybillManage.filter.fulfillmentWarehouse'), value: 'warehouse' },
];
const stockBindStatusOptions = ['none', 'bound', 'deducted', 'released'].map((code) => ({
label: t(`order.myOrders.stockBind.status.${code}`),
value: code,
}));
const fulfillProgressOptions = FULFILL_PROGRESS_TABS.filter((m) => m.value !== 'all').map((meta) => ({
label: t(meta.labelKey),
value: meta.value,
}));
const numberKeys = new Set([
'waybillNo',
'customerOrderNo',
@ -1633,6 +1661,11 @@
'clientNo',
'trackNo',
]);
const enumFilter = (options: Array<{ label: string; value: string }>) => ({
filterType: 'enum' as const,
headerMode: 'icon' as const,
filterOptions: options,
});
const base = withServerFilterColumns(
buildHotColumns(listColumnCatalog.value, activeColumnKeys.value, t, te),
ORDER_SERVER_FILTER_KEYS,
@ -1648,22 +1681,23 @@
|| col.key === 'receiveTime' || col.key === 'signTime') {
return { filterType: 'date' as const, headerMode: 'icon' as const };
}
if (col.key === 'orderStatus') {
return {
filterType: 'enum' as const,
headerMode: 'icon' as const,
filterOptions: statusOptions,
};
if (col.key === 'orderStatus' || col.key === 'previousOrderStatus') {
return enumFilter(statusOptions);
}
if (col.key === 'isPaid') {
return {
filterType: 'enum' as const,
headerMode: 'icon' as const,
filterOptions: [
{ label: '已支付', value: '1' },
{ label: '已支付', value: '1' },
],
};
if (col.key === 'handleStatus') {
return enumFilter(handleStatusOptions);
}
if (col.key === 'fulfillmentType') {
return enumFilter(fulfillmentTypeOptions);
}
if (col.key === 'stockBindStatus') {
return enumFilter(stockBindStatusOptions);
}
if (col.key === 'fulfillProgress') {
return enumFilter(fulfillProgressOptions);
}
if (['isPaid', 'isIntercept', 'isRemote', 'isCod'].includes(col.key)) {
return enumFilter(boolOptions);
}
if (col.key === 'receivableAmount') {
return { filterType: 'number' as const, headerMode: 'icon' as const };

View File

@ -82,6 +82,7 @@
import { useHiprintClientState } from '@/utils/hiprint/hiprintClientState';
import {
ORDER_STATUS_TABS,
ORDER_STATUS_LIST,
ORDER_STATUS_ALL,
resolveOrderStatusCode,
isOrderDeletable,
@ -101,6 +102,7 @@
getOrderHandleStatusMeta,
type OrderHandleStatusCode,
} from '@/constants/orderHandleStatus';
import { FULFILL_PROGRESS_TABS } from '@/constants/fulfillProgress';
import { hasPermission } from '@/utils/rbac-permission';
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer';
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer';
@ -709,10 +711,17 @@
'clientNo',
'trackNo',
'orderStatus',
'previousOrderStatus',
'handleStatus',
'fulfillmentType',
'stockBindStatus',
'fulfillProgress',
'shippingMethod',
'serviceChannel',
'isPaid',
'isIntercept',
'isRemote',
'isCod',
'receivableAmount',
'predeclareFailReason',
'allocationPendingReason',
@ -1864,10 +1873,30 @@
const activeColumnKeys = computed(() => getActiveScheme(columnConfigState.value).keys);
const hotColumns = computed(() => {
const boolOptions = [
{ label: t('order.myOrders.drawer.yes'), value: '1' },
{ label: t('order.myOrders.drawer.no'), value: '0' },
];
const statusOptions = ORDER_STATUS_LIST.map((meta) => ({
label: t(meta.labelKey),
value: meta.value,
}));
const handleStatusOptions = ORDER_HANDLE_STATUS_LIST.filter((m) => m.value !== 'all').map((meta) => ({
label: t(meta.labelKey),
value: meta.value,
}));
const fulfillmentTypeOptions = [
{ label: t('order.waybillManage.filter.fulfillmentDirect'), value: 'direct' },
{ label: t('order.waybillManage.filter.fulfillmentWarehouse'), value: 'warehouse' },
];
const stockBindStatusOptions = ['none', 'bound', 'deducted', 'released'].map((code) => ({
label: t(`order.myOrders.stockBind.status.${code}`),
value: code,
}));
const fulfillProgressOptions = FULFILL_PROGRESS_TABS.filter((m) => m.value !== 'all').map((meta) => ({
label: t(meta.labelKey),
value: meta.value,
}));
const numberKeys = new Set([
'waybillNo',
'customerOrderNo',
@ -1877,6 +1906,11 @@
'clientNo',
'trackNo',
]);
const enumFilter = (options: Array<{ label: string; value: string }>) => ({
filterType: 'enum' as const,
headerMode: 'icon' as const,
filterOptions: options,
});
return withSortableColumns(
withServerFilterColumns(
buildHotColumns(columnCatalog.value, activeColumnKeys.value, t, te),
@ -1901,12 +1935,23 @@
if (col.key === 'receivableAmount') {
return { filterType: 'number' as const, headerMode: 'icon' as const };
}
if (col.key === 'orderStatus' || col.key === 'previousOrderStatus') {
return enumFilter(statusOptions);
}
if (col.key === 'handleStatus') {
return enumFilter(handleStatusOptions);
}
if (col.key === 'fulfillmentType') {
return {
filterType: 'enum' as const,
headerMode: 'icon' as const,
filterOptions: fulfillmentTypeOptions,
};
return enumFilter(fulfillmentTypeOptions);
}
if (col.key === 'stockBindStatus') {
return enumFilter(stockBindStatusOptions);
}
if (col.key === 'fulfillProgress') {
return enumFilter(fulfillProgressOptions);
}
if (['isPaid', 'isIntercept', 'isRemote', 'isCod'].includes(col.key)) {
return enumFilter(boolOptions);
}
return {};
},

View File

@ -61,6 +61,7 @@
</span>
<TextType
v-if="titleAnimate"
:key="shortTitle ? 'title-short' : 'title-full'"
as="span"
class-name="ai-welcome-title__type"
:text="shortTitle ? copy.titleRestShort : copy.titleRest"
@ -337,8 +338,13 @@
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 titleAnimate = ref(true);
// 首屏即按视口决定短标题,避免挂载后再切导致 TextType 卡住只剩「IDE.」
const shortTitle = ref(
typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches,
);
const titleAnimate = ref(
typeof window === 'undefined' || !window.matchMedia('(prefers-reduced-motion: reduce)').matches,
);
const draft = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),

View File

@ -14,6 +14,7 @@
:disabled="busy"
:value="modelValue"
@input="onInput"
@focus="onFocus"
@keydown="onKeydown"
></textarea>
<button type="submit" class="ai-composer-send" :class="{ 'is-empty': !modelValue.trim() }" :disabled="busy" :aria-label="sendLabel">
@ -56,6 +57,15 @@
resize();
}
function onFocus() {
const el = inputRef.value;
if (!el) return;
// iOS 键盘顶起后,把输入框滚进可视区
window.setTimeout(() => {
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}, 80);
}
function onKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();

View File

@ -9,8 +9,22 @@
</span>
</a>
<i class="mobile-nav-toggle mobile-nav-show si si-list" aria-hidden="true" @click="$emit('open-nav')"></i>
<i class="mobile-nav-toggle mobile-nav-hide d-none si si-x" aria-hidden="true" @click="$emit('close-nav')"></i>
<button
type="button"
class="mobile-nav-toggle mobile-nav-show"
:aria-label="isZh ? '打开菜单' : 'Open menu'"
@click="$emit('open-nav')"
>
<i class="si si-list" aria-hidden="true"></i>
</button>
<button
type="button"
class="mobile-nav-toggle mobile-nav-hide d-none"
:aria-label="isZh ? '关闭菜单' : 'Close menu'"
@click="$emit('close-nav')"
>
<i class="si si-x" aria-hidden="true"></i>
</button>
<nav id="navbar" class="navbar">
<ul>
<li ref="dockRef" class="site-nav-dock">

View File

@ -129,11 +129,11 @@
<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__titles">
<h4 class="ai-route-card__name">{{ item.productName }}</h4>
<span
v-if="item.productGroupName || item.productGroup"
class="ai-route-card__group"
>{{ item.productGroupName || item.productGroup }}</span>
<h4 class="ai-route-card__name">{{ item.productName }}</h4>
</div>
<p v-if="formatRouteTimeliness(item)" class="ai-route-card__eta">
{{ formatRouteTimeliness(item) }} {{ copy.days }}
@ -145,21 +145,9 @@
<span v-if="item.tradeTerms === 'DDP'"> · {{ copy.ddpHint }}</span>
<span v-else-if="item.tradeTerms === 'DDU'"> · {{ copy.dduHint }}</span>
</p>
<p
v-if="item.productDescription"
class="ai-route-card__desc"
:class="{ 'is-expanded': expanded[String(item.id)] }"
>
<p v-if="item.productDescription" class="ai-route-card__desc">
{{ item.productDescription }}
</p>
<button
v-if="item.productDescription.length > 72"
type="button"
class="ai-route-card__more"
@click="toggleExpand(item.id)"
>
{{ expanded[String(item.id)] ? copy.collapse : copy.expand }}
</button>
<div class="ai-route-card__actions">
<a class="ai-route-card__reg" :href="officialLoginUrl(destLabel(destFilter, lang) || undefined)">
{{ copy.register }}
@ -237,7 +225,6 @@
const search = ref('');
const currentPage = ref(1);
const bodyRef = ref<HTMLElement | null>(null);
const expanded = ref<Record<string, boolean>>({});
const groupChips = computed(() => {
const groups = (catalog.value?.groups || []).map((g) => ({ code: g.code, name: g.name || g.code }));
@ -324,11 +311,6 @@
resetPage();
}
function toggleExpand(id: string | number) {
const key = String(id);
expanded.value = { ...expanded.value, [key]: !expanded.value[key] };
}
function askItem(item: RouteItem) {
emit('ask', { text: buildRouteAskText(item, destFilter.value, lang.value), intentId: 'quote' });
}

View File

@ -123,7 +123,13 @@
watch(
() => props.text,
() => {
// text 变化时必须主动重启;若此时 displayedText 已是空串,依赖状态 watch 不会再触发
resetAnimation();
if (!isVisible.value) return;
clearTimeoutIfNeeded();
timeout = setTimeout(() => {
executeTypingAnimation();
}, props.initialDelay);
}
);

View File

@ -208,16 +208,24 @@
ensureOg('og:type', 'website');
}
function onMobileNavBackdrop(e: MouseEvent) {
const navEl = document.getElementById('navbar');
if (!navEl || e.target !== navEl) return;
closeMobileNav();
}
function openMobileNav() {
document.body.classList.add('mobile-nav-active');
document.querySelectorAll('.mobile-nav-show').forEach((n) => n.classList.add('d-none'));
document.querySelectorAll('.mobile-nav-hide').forEach((n) => n.classList.remove('d-none'));
document.getElementById('navbar')?.addEventListener('click', onMobileNavBackdrop);
}
function closeMobileNav() {
document.body.classList.remove('mobile-nav-active');
document.querySelectorAll('.mobile-nav-show').forEach((n) => n.classList.remove('d-none'));
document.querySelectorAll('.mobile-nav-hide').forEach((n) => n.classList.add('d-none'));
document.getElementById('navbar')?.removeEventListener('click', onMobileNavBackdrop);
}
function syncHash(id: string) {
@ -324,6 +332,10 @@
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (document.body.classList.contains('mobile-nav-active')) {
closeMobileNav();
return;
}
closePanel();
historyOpen.value = false;
wecomOpen.value = false;
@ -336,6 +348,14 @@
document.documentElement.style.setProperty('--ai-header-h', `${height}px`);
}
function syncVisualViewport() {
const vv = window.visualViewport;
const height = Math.round(vv?.height ?? window.innerHeight);
const offsetTop = Math.round(vv?.offsetTop ?? 0);
document.documentElement.style.setProperty('--ai-vv-height', `${height}px`);
document.documentElement.style.setProperty('--ai-vv-offset-top', `${offsetTop}px`);
}
watch(panel, (id) => {
document.body.classList.toggle('site-panel-routes', id === 'routes');
document.body.classList.toggle('site-panel-services', id === 'services');
@ -349,7 +369,11 @@
document.body.classList.add('command-home');
window.addEventListener('keydown', onKeydown);
window.addEventListener('resize', syncHeaderOffset);
window.addEventListener('resize', syncVisualViewport);
window.visualViewport?.addEventListener('resize', syncVisualViewport);
window.visualViewport?.addEventListener('scroll', syncVisualViewport);
syncHeaderOffset();
syncVisualViewport();
const hash = (window.location.hash || '').replace('#', '');
if (hash === '9610' || hash === 'services') openPanel('services');
else if (NAV_PANELS.includes(hash as PanelId)) openPanel(hash as Exclude<PanelId, null>);
@ -359,6 +383,9 @@
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeydown);
window.removeEventListener('resize', syncHeaderOffset);
window.removeEventListener('resize', syncVisualViewport);
window.visualViewport?.removeEventListener('resize', syncVisualViewport);
window.visualViewport?.removeEventListener('scroll', syncVisualViewport);
document.body.classList.remove('site-panel-routes', 'site-panel-services');
document.documentElement.classList.remove('command-home', 'app-boot-site');
closeMobileNav();

View File

@ -52,7 +52,7 @@
--ai-glass: var(--ai-surface);
--ai-content-max: 1200px;
--ai-guide-panel-w: 760px;
--ai-routes-panel-w: 1080px;
--ai-routes-panel-w: 1280px;
--ai-acrylic-bg: rgba(255, 255, 255, 0.32);
--ai-acrylic-bg-primary: color-mix(in srgb, var(--ai-gold-soft) 42%, rgba(255, 255, 255, 0.38));
/* Soft fills ? cool tint aligned with GradientWaves */
@ -169,6 +169,27 @@ body.command-home #navbar .site-nav-dock a {
}
@media (max-width: 1279px) {
/* 手机/平板抽屉:纵向列表,浅色底保证对比度 */
body.command-home #navbar,
body.command-home #navbar.navbar {
width: min(100%, 380px);
max-width: 100%;
}
body.command-home #navbar > ul {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
padding: calc(56px + env(safe-area-inset-top, 0px)) 18px
calc(24px + env(safe-area-inset-bottom, 0px));
background: #f4f7fa !important;
background-image: linear-gradient(180deg, #f8fafc 0%, #eef3f7 100%) !important;
border-top-left-radius: 24px;
border-bottom-left-radius: 24px;
box-shadow: -12px 0 40px rgba(15, 23, 42, 0.18);
}
body.command-home .site-nav-dock {
display: contents;
}
@ -176,22 +197,112 @@ body.command-home #navbar .site-nav-dock a {
body.command-home .site-nav-dock__glass,
body.command-home .site-nav-dock__slider,
body.command-home .site-nav-tools__glass {
display: none;
display: none !important;
}
body.command-home #navbar .site-nav-dock__item {
body.command-home #navbar > ul {
gap: 8px;
}
body.command-home #navbar .site-nav-dock__item,
body.command-home #navbar .site-nav-dock a,
body.command-home #navbar .site-nav-dock a:focus {
display: flex !important;
flex-direction: row;
gap: 10px;
align-items: center;
justify-content: flex-start;
gap: 12px;
width: 100%;
min-height: 52px;
padding: 14px 16px !important;
border-radius: 14px;
color: var(--ai-text) !important;
font-size: 1.05rem;
font-weight: 700;
background: rgba(255, 255, 255, 0.72);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
-webkit-tap-highlight-color: transparent;
}
body.command-home #navbar .site-nav-dock a:hover,
body.command-home #navbar .site-nav-dock a.active,
body.command-home #navbar .site-nav-dock a.active:focus,
body.command-home #navbar .site-nav-dock a.is-active,
body.command-home #navbar .site-nav-dock a.is-active:focus {
color: var(--ai-action) !important;
background: #fff;
}
body.command-home #navbar .site-nav-dock__icon {
width: 20px;
height: 20px;
width: 22px;
height: 22px;
flex-shrink: 0;
color: inherit;
}
body.command-home #navbar .site-nav-dock__label {
font-size: 1rem;
font-weight: 700;
font-size: inherit;
font-weight: inherit;
}
body.command-home #navbar > ul > li.site-nav-tools {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid rgba(18, 24, 32, 0.08);
}
body.command-home #navbar .site-nav-tools .site-nav-login,
body.command-home #navbar .site-nav-tools .site-nav-login:focus,
body.command-home #navbar .site-nav-tools .site-nav-login:hover,
body.command-home #navbar .site-nav-tools .site-nav-login:visited,
body.command-home #navbar .site-nav-tools .language-toggle-link,
body.command-home #navbar .site-nav-tools .language-toggle-link:focus,
body.command-home #navbar .site-nav-tools .language-toggle-link:hover,
body.command-home #navbar .site-nav-tools .language-toggle-link.active,
body.command-home #navbar .site-nav-tools .language-toggle-link.active:focus {
display: inline-flex !important;
justify-content: center;
width: 100%;
min-height: 46px;
padding: 0 16px !important;
border-radius: 14px;
background: #fff !important;
color: var(--ai-action) !important;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
body.command-home .mobile-nav-show,
body.command-home .mobile-nav-hide {
color: var(--ai-action);
}
body.command-home .mobile-nav-show,
body.command-home .mobile-nav-hide {
width: 44px;
height: 44px;
}
body.command-home .mobile-nav-hide {
color: var(--ai-text);
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.12);
}
body.command-home.mobile-nav-active .mobile-nav-hide {
color: var(--ai-text);
}
/* 点遮罩关闭:伪元素承接点击,落到 #navbar 本身 */
body.command-home.mobile-nav-active #navbar {
cursor: pointer;
}
body.command-home.mobile-nav-active #navbar > ul {
cursor: default;
}
}
@ -676,10 +787,11 @@ body.command-home #navbar a.language-toggle-link:hover .language-toggle-icon {
body.command-home #ai-advisor-shell {
display: block;
position: fixed;
top: 0;
top: var(--ai-vv-offset-top, 0px);
left: 0;
right: 0;
bottom: 0;
bottom: auto;
height: var(--ai-vv-height, 100%);
z-index: 10;
overflow: hidden;
}
@ -1987,7 +2099,7 @@ body.command-home .ai-stage-map--eu {
flex-direction: column;
gap: 8px;
min-width: 0;
min-height: 0;
height: 180px;
padding: 14px 16px;
border-radius: 16px;
border: none;
@ -2009,7 +2121,7 @@ body.command-home .ai-stage-map--eu {
flex-direction: row;
flex-wrap: wrap;
align-items: baseline;
gap: 0;
gap: 6px 8px;
}
.ai-route-card__group {
@ -2021,13 +2133,6 @@ body.command-home .ai-stage-map--eu {
color: var(--ai-muted);
}
.ai-route-card__group::after {
content: '?';
margin: 0 6px;
font-weight: 500;
color: color-mix(in srgb, var(--ai-muted) 55%, transparent);
}
.ai-route-card__name {
margin: 0;
min-width: 0;
@ -2086,32 +2191,14 @@ body.command-home .ai-stage-map--eu {
}
.ai-route-card__desc {
flex: 1;
min-height: 0;
margin: 0;
font-size: 0.8rem;
line-height: 1.45;
color: var(--ai-text-3);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ai-route-card__desc.is-expanded {
display: block;
-webkit-line-clamp: unset;
overflow: visible;
}
.ai-route-card__more {
align-self: flex-start;
margin: 0;
padding: 0;
border: 0;
background: none;
color: var(--ai-action);
font-size: 0.76rem;
font-weight: 650;
cursor: pointer;
overflow-y: auto;
scrollbar-width: thin;
}
.ai-route-card__actions {
@ -4310,8 +4397,19 @@ body.command-home #navbar .site-nav-avatar:hover {
}
@media (max-width: 640px) {
:root {
--ai-header-h: 64px;
}
body.command-home #header {
padding-top: env(safe-area-inset-top, 0px);
}
.ai-advisor-layout {
padding: calc(var(--ai-header-h) + 6px) 14px 0;
padding: calc(var(--ai-header-h) + env(safe-area-inset-top, 0px) + 4px)
max(14px, env(safe-area-inset-left, 0px))
0
max(14px, env(safe-area-inset-right, 0px));
}
.ai-routes-panel {
@ -4364,27 +4462,136 @@ body.command-home #navbar .site-nav-avatar:hover {
}
.ai-welcome__stack {
margin-top: 4px;
margin-top: 2px;
}
.ai-welcome-stats--compact {
width: 100%;
justify-content: space-between;
gap: 10px;
}
.ai-welcome-stats--compact .ai-welcome-stats__item {
flex: 1 1 0;
min-width: 0;
}
.ai-welcome-stats--compact .ai-welcome-stats__item:nth-child(n + 3) {
display: none;
}
.ai-welcome-stats__value {
font-size: clamp(1.35rem, 6vw, 1.7rem);
}
.ai-welcome-stats__label {
font-size: 0.72rem;
line-height: 1.3;
}
.ai-intent-grid--primary {
grid-template-columns: 1fr;
gap: 10px;
}
.ai-intent-grid--primary .ai-intent-door:last-child {
grid-column: auto;
}
.ai-partner-marquee {
min-width: 0;
/* 权益:横向滑动,首屏露出首卡 + 右缘提示 */
.ai-intent-grid--growth {
display: flex;
gap: 10px;
overflow-x: auto;
overflow-y: hidden;
padding: 0 2px 8px;
scroll-snap-type: x proximity;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
touch-action: pan-x;
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 28px), transparent 100%);
-webkit-mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 28px), transparent 100%);
}
.ai-partner-marquee__viewport {
flex: 0 1 auto;
.ai-intent-grid--growth::-webkit-scrollbar {
display: none;
}
.ai-intent-grid--growth .ai-intent-door {
flex: 0 0 min(72vw, 240px);
width: auto;
scroll-snap-align: start;
}
.ai-welcome__command .ai-intent-door,
.ai-welcome__command .ai-intent-door.is-primary {
min-height: 52px;
padding: 9px 12px 9px 10px;
gap: 10px;
}
.ai-intent-grid--primary {
gap: 8px;
}
.ai-welcome__command .ai-intent-door__icon {
width: 36px;
height: 36px;
}
.ai-welcome__command .ai-intent-door__title,
.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__title {
font-size: 0.98rem;
}
.ai-welcome__command .ai-intent-door__hint,
.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__hint {
font-size: 0.85rem;
line-height: 1.35;
-webkit-line-clamp: 3;
line-clamp: 3;
}
.ai-partner-marquee {
min-width: 0;
gap: 6px;
}
.ai-partner-marquee__viewport,
.ai-partner-marquee__viewport.is-static {
flex: 1 1 auto;
width: 100%;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
touch-action: pan-x;
mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 18px), transparent);
-webkit-mask-image: linear-gradient(90deg, transparent, #000 18px, #000 calc(100% - 18px), transparent);
}
.ai-partner-marquee__viewport::-webkit-scrollbar,
.ai-partner-marquee__viewport.is-static::-webkit-scrollbar {
display: none;
}
.ai-partner-marquee__viewport.is-static .ai-partner-marquee__track {
justify-content: flex-start;
width: max-content;
}
.ai-partner-marquee__viewport.is-static .ai-partner-marquee__strip {
flex-wrap: nowrap;
justify-content: flex-start;
gap: 8px;
}
.ai-trust-bar {
--ai-trust-bar-h: 76px;
gap: 8px 14px;
padding-top: 8px;
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
}
.ai-trust-icp {
@ -4392,33 +4599,125 @@ body.command-home #navbar .site-nav-avatar:hover {
font-size: 0.66rem;
}
/* 首页可滚:底部淡出提示还有内容(对话页不套 mask) */
.ai-app-main:has(.ai-welcome) {
-webkit-mask-image: linear-gradient(
to bottom,
#000 0%,
#000 calc(100% - 22px),
transparent 100%
);
mask-image: linear-gradient(
to bottom,
#000 0%,
#000 calc(100% - 22px),
transparent 100%
);
}
.ai-welcome__meta {
margin-top: 10px;
padding-top: 10px;
}
.ai-welcome__deck {
padding: 14px 8px 12px;
padding: 8px 4px 6px;
}
.ai-welcome__command {
padding: 0 0 4px;
gap: 14px;
padding: 0 0 calc(12px + env(safe-area-inset-bottom, 0px));
}
.ai-intent-section {
gap: 10px;
}
.ai-intent-group__label {
margin-bottom: 6px;
font-size: 0.8rem;
letter-spacing: 0.04em;
color: var(--ai-text-2);
}
.ai-welcome__command .ai-intent-door__hint,
.ai-welcome__command .ai-intent-door.is-primary .ai-intent-door__hint {
-webkit-line-clamp: 2;
line-clamp: 2;
}
.ai-welcome-title {
font-size: clamp(1.25rem, 5vw, 1.75rem);
white-space: normal;
font-size: clamp(1.45rem, 6.2vw, 1.85rem);
line-height: 1.28;
}
.ai-welcome-sub {
font-size: 0.84rem;
white-space: nowrap;
.ai-welcome-compose-panel {
padding: 12px 12px 10px;
border-radius: 22px;
}
.ai-welcome-compose-panel .ai-welcome-cta-link {
position: static;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 44px;
margin: 0 0 10px;
padding: 0 14px;
border-radius: 999px;
background: color-mix(in srgb, var(--ai-action) 12%, #fff);
color: var(--ai-action);
font-size: 0.95rem;
font-weight: 700;
text-decoration: none;
}
.ai-welcome-compose-panel .ai-welcome-cta-link:hover,
.ai-welcome-compose-panel .ai-welcome-cta-link:focus {
background: color-mix(in srgb, var(--ai-action) 18%, #fff);
text-decoration: none;
}
.ai-welcome-compose-panel .ai-welcome-compose__lead {
padding-right: 0;
padding-top: 0;
margin-bottom: 10px;
font-size: 0.92rem;
line-height: 1.45;
}
.ai-home-dests {
display: flex;
flex-wrap: nowrap;
gap: 8px;
margin-bottom: 10px;
overflow-x: auto;
max-width: 94vw;
-webkit-overflow-scrolling: touch;
padding-bottom: 2px;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x;
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 20px), transparent 100%);
-webkit-mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 20px), transparent 100%);
}
.ai-welcome-sub::-webkit-scrollbar {
.ai-home-dests::-webkit-scrollbar {
display: none;
}
.ai-trust-bar {
gap: 8px 14px;
.ai-home-dest {
flex: 0 0 auto;
min-height: 40px;
padding: 0 14px;
font-size: 0.88rem;
}
.ai-welcome-sub {
font-size: 0.88rem;
white-space: normal;
overflow: visible;
max-width: 100%;
}
.ai-new-chat-tip {
@ -4428,20 +4727,25 @@ body.command-home #navbar .site-nav-avatar:hover {
border-radius: 14px;
}
.ai-composer {
min-height: 76px;
padding: 12px 12px 12px 18px;
border-radius: 26px;
.ai-composer,
.ai-composer-slot--welcome .ai-composer {
min-height: 56px;
padding: 10px 10px 10px 16px;
border-radius: 22px;
}
.ai-composer-input {
min-height: 48px;
font-size: 1rem;
min-height: 44px;
font-size: 16px; /* iOS 避免自动放大 */
}
.ai-composer-send {
width: 46px;
height: 46px;
width: 44px;
height: 44px;
}
.ai-composer-dock {
padding-bottom: max(10px, env(safe-area-inset-bottom, 0px));
}
}

View File

@ -184,24 +184,37 @@ body.command-home {
color: #fff;
}
.mobile-nav-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
padding: 0;
border: none;
border-radius: 12px;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.mobile-nav-toggle .si {
font-size: 26px;
line-height: 1;
}
.mobile-nav-show {
z-index: 9999;
margin-right: 10px;
margin-right: 2px;
color: #fff;
font-size: 28px;
line-height: 0;
cursor: pointer;
}
.mobile-nav-hide {
position: fixed;
top: 20px;
right: 20px;
top: max(16px, env(safe-area-inset-top, 0px));
right: max(14px, env(safe-area-inset-right, 0px));
z-index: 9999;
color: #fff;
font-size: 32px;
line-height: 0;
cursor: pointer;
}
.mobile-nav-active {

View File

@ -474,6 +474,7 @@ function handleSyncMenu(record: MenuTreeDto) {
const result = await syncMenuTree(record)
const hasChanges =
result.level2Created > 0 ||
result.level2Updated > 0 ||
result.level3Created > 0 ||
result.level3Updated > 0 ||
result.level3Deleted > 0
@ -483,6 +484,7 @@ function handleSyncMenu(record: MenuTreeDto) {
Message.success(
t('system.menu.message.syncModuleSuccess', {
level2Created: result.level2Created,
level2Updated: result.level2Updated,
level2Skipped: result.level2Skipped,
level3Created: result.level3Created,
level3Updated: result.level3Updated,