diff --git a/src/App.vue b/src/App.vue index eacffaf..ad5f05d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -152,6 +152,17 @@ // 监听ws消息,用于系统级通知 mittBus.on('ws:message', handleWsMessageNotification) + + // 初始化菜单徽标系统(全局初始化,避免重复) + import('@/composables/useMenuBadge').then(({ useMenuBadge }) => { + const { initBadgeSystem } = useMenuBadge() + initBadgeSystem() + }) + + // 加载菜单徽标测试工具(仅开发环境) + if (import.meta.env.DEV) { + import('@/utils/menuBadgeTest') + } }) // 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新) @@ -166,6 +177,12 @@ ) { initWebSocket(token) } + + // 重新初始化徽标系统(用户重新登录时) + import('@/composables/useMenuBadge').then(({ useMenuBadge }) => { + const { initBadgeSystem } = useMenuBadge() + initBadgeSystem() + }) } else { // 退出登录时,关闭并重置 const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient') diff --git a/src/api/admin/user.ts b/src/api/admin/user.ts new file mode 100644 index 0000000..a9f7fc2 --- /dev/null +++ b/src/api/admin/user.ts @@ -0,0 +1,32 @@ +import request from '@/utils/http' + +export class userConService { + //分组列表 + static getGroupList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/user/group/list', + params + }) + } + //添加分组 + static addGroup(params: any) { + return request.post({ + url: '/admin/user/group/store', + data: params + }) + } + //编辑分组 + static editGroup(params: any) { + return request.post({ + url: '/admin/user/group/update', + data: params + }) + } + //删除分组 + static deleteGroup(params: any) { + return request.post({ + url: '/admin/user/group/destroy', + data: params + }) + } +} \ No newline at end of file diff --git a/src/api/commonApi.ts b/src/api/commonApi.ts index 2c812fe..3870f3f 100644 --- a/src/api/commonApi.ts +++ b/src/api/commonApi.ts @@ -61,6 +61,13 @@ export class commonService { params }) } + //车型下拉列表 + static getModelsSelect(params: any) { + return request.get({ + url: '/crm/vehicle/sku/preset', + params + }) + } //港口下拉列表 static getPortSelect(params: any) { return request.get({ @@ -132,4 +139,17 @@ export class commonService { params }) } + //人员分组场景 + static getUserGroupScene() { + return request.get({ + url: '/common/preset/usergroup/scene' + }) + } + //人员分组 + static getUserGroupSelect(params: any) { + return request.get({ + url: '/common/preset/usergroup', + params + }) + } } diff --git a/src/api/crmApi.ts b/src/api/crmApi.ts index 43867a8..313343d 100644 --- a/src/api/crmApi.ts +++ b/src/api/crmApi.ts @@ -285,4 +285,39 @@ export class CrmService { params }) } + //备货单撤回 + static stockrecall(params: any) { + return request.post({ + url: '/crm/stock/order/revoke/approval', + params + }) + } + //备货单作废 + static stockinvalid(params: any) { + return request.post({ + url: '/crm/stock/order/nullify/approval', + params + }) + } + //备货单删除 + static stockdelete(params: any) { + return request.post({ + url: '/crm/stock/order/destroy', + params + }) + } + //审核详情 + static stockapprovaldetailborder(params: any) { + return request.get({ + url: '/crm/approval/records/border', + params + }) + } + //审批转交 + static stockapprovaltransfer(params: any) { + return request.post({ + url: '/crm/approval/records/transfer', + params + }) + } } diff --git a/src/api/crmpc/vehicleApi.ts b/src/api/crmpc/vehicleApi.ts index dc314b8..631b152 100644 --- a/src/api/crmpc/vehicleApi.ts +++ b/src/api/crmpc/vehicleApi.ts @@ -95,4 +95,127 @@ export class vehicleService { params }) } + // 港口管理 + static getPortList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/common/port/list', + params + }) + } + static portAdd(params: any) { + return request.post({ + url: '/common/port/store', + params + }) + } + static portDetail(params: any) { + return request.get({ + url: '/common/port/show', + params + }) + } + static portEdit(params: any) { + return request.post({ + url: '/common/port/update', + params + }) + } + static portDelete(params: any) { + return request.post({ + url: '/common/port/destroy', + params + }) + } + // 车辆品牌列表 + static getBrandList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/common/brand/list', + params + }) + } + // 车辆品牌新增/详情/编辑/删除 + static brandAdd(params: any) { + return request.post({ + url: '/common/brand/store', + params + }) + } + static brandDetail(params: any) { + return request.get({ + url: '/common/brand/show', + params + }) + } + static brandEdit(params: any) { + return request.post({ + url: '/common/brand/update', + params + }) + } + static brandDelete(params: any) { + return request.post({ + url: '/common/brand/destroy', + params + }) + } + + // 车辆系列列表 + static getSeriesList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/common/brand/series/list', + params + }) + } + // 车辆系列新增/详情/编辑/删除 + static seriesAdd(params: any) { + return request.post({ + url: '/common/brand/series/store', + params + }) + } + static seriesDetail(params: any) { + return request.get({ + url: '/common/brand/series/show', + params + }) + } + static seriesEdit(params: any) { + return request.post({ + url: '/common/brand/series/update', + params + }) + } + static seriesDelete(params: any) { + return request.post({ + url: '/common/brand/series/destroy', + params + }) + } + // 车系采集相关接口 + static collectVehicleSeries(params: any) { + return request.get({ + url: '/common/brand/crawlSeries', + params + }) + } + static saveVehicleSeries(params: any) { + return request.post({ + url: '/common/brand/crawlSeriesCreate', + params + }) + } + + // 车型采集相关接口 + static collectVehicleModels(params: any) { + return request.get({ + url: '/common/brand/series/crawlModels', + params + }) + } + static saveVehicleModels(params: any) { + return request.post({ + url: '/common/brand/series/crawlModelCreate', + params + }) + } } diff --git a/src/api/vehicleApi.ts b/src/api/vehicleApi.ts index 9853219..6e2a482 100644 --- a/src/api/vehicleApi.ts +++ b/src/api/vehicleApi.ts @@ -193,4 +193,32 @@ export class vehicleService { params }) } + + // 车系采集相关接口 + static collectVehicleSeries(params: any) { + return request.get({ + url: '/common/brand/crawlSeries', + params + }) + } + static saveVehicleSeries(params: any) { + return request.post({ + url: '/common/brand/crawlSeriesCreate', + params + }) + } + + // 车型采集相关接口 + static collectVehicleModels(params: any) { + return request.get({ + url: '/common/brand/series/crawlModels', + params + }) + } + static saveVehicleModels(params: any) { + return request.post({ + url: '/common/brand/series/crawlModelCreate', + params + }) + } } diff --git a/src/assets/styles/el-dark.scss b/src/assets/styles/el-dark.scss index 2a968a9..30fe9f4 100644 --- a/src/assets/styles/el-dark.scss +++ b/src/assets/styles/el-dark.scss @@ -6,7 +6,7 @@ // 'white': #ffffff, 'black': #000000, - 'success': ('base': #13deb9), + 'success': ('base': #00A43F), 'warning': ('base': #ffae1f), 'danger': ('base': #ff4d4f), 'error': ('base': #fa896b) diff --git a/src/assets/styles/el-light.scss b/src/assets/styles/el-light.scss index 11452ac..54feeff 100644 --- a/src/assets/styles/el-light.scss +++ b/src/assets/styles/el-light.scss @@ -8,7 +8,7 @@ // 'white': #ffffff, 'black': #000000, - 'success': ('base': #13deb9), + 'success': ('base': #00A43F), 'warning': ('base': #ffae1f), 'danger': ('base': #ff4d4f), 'error': ('base': #fa896b) diff --git a/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue b/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue index 74bfb76..d072fc3 100644 --- a/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue +++ b/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue @@ -32,7 +32,10 @@ v-html="item.meta.icon" > {{ formatMenuTitle(item.meta.title) }} -
+
+
+ {{ getMenuTextBadge(item.path) }} +
@@ -41,6 +44,7 @@ import { AppRouteRecord } from '@/types/router' import { handleMenuJump } from '@/utils/navigation' import { formatMenuTitle } from '@/router/utils/utils' + import { useMenuBadge } from '@/composables/useMenuBadge' const props = defineProps({ item: { @@ -60,6 +64,9 @@ const emit = defineEmits(['close']) + // 使用菜单徽标系统 + const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge() + // 计算当前项是否有子菜单 const hasChildren = computed(() => { return props.item.children && props.item.children.length > 0 diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss b/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss index 873753a..9de24ac 100644 --- a/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss @@ -112,16 +112,17 @@ top: 0; right: 12px; bottom: 0; - min-width: 20px; - height: 20px; - padding: 0 5px; + min-width: 16px; + height: 16px; + // padding: 0 5px; margin: auto; - font-size: 12px; - line-height: 20px; + font-size: 10px; + line-height: 16px; color: #fff; text-align: center; background: #fd4e4e; - border-radius: 5px; + // border-radius: 5px; + border-radius: 50%; } .header { diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue b/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue index 305ad62..fde7242 100644 --- a/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue @@ -5,7 +5,10 @@ @@ -40,6 +43,7 @@ import type { AppRouteRecord } from '@/types/router' import { formatMenuTitle } from '@/router/utils/utils' import { handleMenuJump } from '@/utils/navigation' + import { useMenuBadge } from '@/composables/useMenuBadge' // 类型定义 interface Props { @@ -66,6 +70,9 @@ (e: 'close'): void }>() + // 使用菜单徽标系统 + const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge() + // 计算属性 const filteredMenuItems = computed(() => filterRoutes(props.list)) diff --git a/src/composables/useMenuBadge.ts b/src/composables/useMenuBadge.ts new file mode 100644 index 0000000..5c26c4f --- /dev/null +++ b/src/composables/useMenuBadge.ts @@ -0,0 +1,264 @@ +import { computed } from 'vue' +import { useMenuBadgeStore } from '@/store/modules/menuBadge' +import { useMenuStore } from '@/store/modules/menu' +import mittBus from '@/utils/sys/mittBus' + +// 全局初始化状态 +let globalInitialized = false + +// 重置全局初始化状态(用于退出登录后重新初始化) +export const resetGlobalInitialized = () => { + globalInitialized = false + // console.log('徽标系统全局初始化状态已重置') +} + +/** + * 菜单徽标管理 Composable + * 提供菜单徽标的统一管理接口 + */ +export function useMenuBadge() { + const menuBadgeStore = useMenuBadgeStore() + const menuStore = useMenuStore() + + // 计算属性:获取菜单列表(带徽标状态) + const menuListWithBadges = computed(() => { + const menuList = menuStore.menuList + if (!menuList.length) return menuList + + // 同步徽标状态到菜单列表 + menuBadgeStore.syncBadgeWithMenuList(menuList) + return menuList + }) + + // 更新菜单徽标 + const updateMenuBadge = (path: string, count: number, type: 'dot' | 'text' = 'text') => { + menuBadgeStore.updateBadgeData(path, count, type) + } + + // 批量更新菜单徽标 + const batchUpdateMenuBadges = ( + updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }> + ) => { + menuBadgeStore.batchUpdateBadgeData(updates) + } + + // 清除菜单徽标 + const clearMenuBadge = (path: string) => { + menuBadgeStore.clearBadge(path) + } + + // 清除所有菜单徽标 + const clearAllMenuBadges = () => { + menuBadgeStore.clearAllBadges() + } + + // 获取菜单徽标数量 + const getMenuBadgeCount = (path: string) => { + return menuBadgeStore.getBadgeCount(path) + } + + // 检查菜单是否显示徽标 + const isMenuBadgeVisible = (path: string) => { + return menuBadgeStore.shouldShowBadge(path) + } + + // 获取菜单文本徽标 + const getMenuTextBadge = (path: string) => { + return menuBadgeStore.shouldShowTextBadge(path) + } + + // 处理WebSocket徽标更新消息 + const handleWebSocketBadgeUpdate = (data: any) => { + // console.log('收到WebSocket徽标更新消息:', data) + + try { + // 根据消息类型处理不同的徽标更新 + if (data.event === 'menu.badge.update') { + const { path, count, type } = data.data + if (path && typeof count === 'number') { + updateMenuBadge(path, count, type || 'text') + } + } else if (data.event === 'menu.badge.batch_update') { + const { updates } = data.data + if (Array.isArray(updates)) { + batchUpdateMenuBadges(updates) + } + } else if (data.event === 'menu.badge.clear') { + const { path } = data.data + if (path) { + clearMenuBadge(path) + } else { + clearAllMenuBadges() + } + } + } catch (error) { + console.error('处理WebSocket徽标更新消息时出错:', error, data) + } + } + + // 处理菜单徽标数据数组格式 + const handleMenuBadgeDataArray = (data: any) => { + // console.log('收到菜单徽标数据数组:', data) + + if (Array.isArray(data.data)) { + const updates = data.data + .filter((item: any) => item.url && typeof item.badge === 'number') + .map((item: any) => ({ + path: item.url, + count: item.badge, + type: 'text' as const + })) + + if (updates.length > 0) { + // console.log('批量更新菜单徽标:', updates) + batchUpdateMenuBadges(updates) + } + } + } + + // 处理通知相关的徽标更新 + const handleNotificationBadgeUpdate = (data: any) => { + // console.log('收到通知徽标更新消息:', data) + + // 根据通知类型更新对应菜单的徽标 + if (data.event === 'notify.receive') { + // 通知接收,更新通知菜单徽标 + const currentCount = getMenuBadgeCount('/notification') + updateMenuBadge('/notification', currentCount + 1, 'text') + } else if (data.event === 'notify.read') { + // 通知已读,减少通知菜单徽标 + const currentCount = getMenuBadgeCount('/notification') + updateMenuBadge('/notification', Math.max(0, currentCount - 1), 'text') + } else if (data.event === 'notify.clear') { + // 清除所有通知,清除通知菜单徽标 + clearMenuBadge('/notification') + } + } + + // 处理审批相关的徽标更新 + const handleApprovalBadgeUpdate = (data: any) => { + // console.log('收到审批徽标更新消息:', data) + + if (data.event === 'approval.new') { + // 新审批,更新审批菜单徽标 + const currentCount = getMenuBadgeCount('/admin/approval') + updateMenuBadge('/admin/approval', currentCount + 1, 'text') + } else if (data.event === 'approval.processed') { + // 审批已处理,减少审批菜单徽标 + const currentCount = getMenuBadgeCount('/admin/approval') + updateMenuBadge('/admin/approval', Math.max(0, currentCount - 1), 'text') + } + } + + // 处理库存相关的徽标更新 + const handleStockBadgeUpdate = (data: any) => { + // console.log('收到库存徽标更新消息:', data) + + if (data.event === 'stock.low') { + // 库存不足,更新库存菜单徽标 + const currentCount = getMenuBadgeCount('/admin/stock') + updateMenuBadge('/admin/stock', currentCount + 1, 'dot') + } else if (data.event === 'stock.normal') { + // 库存正常,减少库存菜单徽标 + const currentCount = getMenuBadgeCount('/admin/stock') + updateMenuBadge('/admin/stock', Math.max(0, currentCount - 1), 'dot') + } + } + + // 统一的WebSocket消息处理 + const handleWebSocketMessage = (data: any) => { + if (!data || !data.event) return + + // 处理菜单徽标数据数组格式(如 approval.todo.counts) + if (data.event.includes('.todo.counts') || data.event.includes('.badge.counts')) { + handleMenuBadgeDataArray(data) + } else if (data.event.includes('menu.badge')) { + handleWebSocketBadgeUpdate(data) + } else if (data.event.includes('notify')) { + handleNotificationBadgeUpdate(data) + } else if (data.event.includes('approval')) { + handleApprovalBadgeUpdate(data) + } else if (data.event.includes('stock')) { + handleStockBadgeUpdate(data) + } + } + + // 初始化徽标系统 + const initBadgeSystem = () => { + if (globalInitialized) { + // console.log('菜单徽标系统已经全局初始化,跳过重复初始化') + return + } + + // console.log('初始化菜单徽标系统') + globalInitialized = true + + // 标记页面已加载完成 + menuBadgeStore.setPageReady(true) + + // 先清理可能存在的旧监听器,避免重复注册 + mittBus.off('ws:message', handleWebSocketMessage) + mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate) + mittBus.off('approval-badge-update', handleApprovalBadgeUpdate) + mittBus.off('stock-badge-update', handleStockBadgeUpdate) + mittBus.off('notification-badge-update', handleNotificationBadgeUpdate) + mittBus.off('menu:updated') + + // 监听WebSocket消息 + mittBus.on('ws:message', handleWebSocketMessage) + + // 监听特定的徽标更新事件 + mittBus.on('menu-badge-update', handleWebSocketBadgeUpdate) + mittBus.on('approval-badge-update', handleApprovalBadgeUpdate) + mittBus.on('stock-badge-update', handleStockBadgeUpdate) + mittBus.on('notification-badge-update', handleNotificationBadgeUpdate) + + // 监听菜单数据变化,同步徽标状态 + mittBus.on('menu:updated', () => { + menuBadgeStore.syncBadgeWithMenuList(menuStore.menuList) + }) + } + + // 清理徽标系统 + const cleanupBadgeSystem = () => { + if (!globalInitialized) { + // console.log('菜单徽标系统未全局初始化,跳过清理') + return + } + + // console.log('清理菜单徽标系统') + globalInitialized = false + + // 移除事件监听 + mittBus.off('ws:message', handleWebSocketMessage) + mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate) + mittBus.off('approval-badge-update', handleApprovalBadgeUpdate) + mittBus.off('stock-badge-update', handleStockBadgeUpdate) + mittBus.off('notification-badge-update', handleNotificationBadgeUpdate) + mittBus.off('menu:updated') + // 重置页面加载状态 + menuBadgeStore.setPageReady(false) + } + + // 注意:不在组件中自动初始化,避免重复初始化 + // 初始化应该在App.vue中全局调用一次 + + return { + // 计算属性 + menuListWithBadges, + + // 方法 + updateMenuBadge, + batchUpdateMenuBadges, + clearMenuBadge, + clearAllMenuBadges, + getMenuBadgeCount, + isMenuBadgeVisible, + getMenuTextBadge, + + // 系统方法 + initBadgeSystem, + cleanupBadgeSystem, + handleWebSocketMessage + } +} diff --git a/src/composables/useStockingForm.ts b/src/composables/useStockingForm.ts index 7b89a5a..957f39d 100644 --- a/src/composables/useStockingForm.ts +++ b/src/composables/useStockingForm.ts @@ -407,7 +407,7 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) { // 金额包装方法 const wrapMoney = (amount: any, currency: string, remark: string) => ({ currency: currency || '', - amount: amount === undefined || amount === null ? '' : String(amount), + amount: amount === undefined || amount === null ? 0 : String(amount), rate: getCurrencyRate(currency), remark }) diff --git a/src/store/modules/menuBadge.ts b/src/store/modules/menuBadge.ts new file mode 100644 index 0000000..e943b95 --- /dev/null +++ b/src/store/modules/menuBadge.ts @@ -0,0 +1,303 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { AppRouteRecord } from '@/types/router' +import { useUserStore } from './user' + +// 菜单徽标数据类型 +export interface MenuBadgeData { + /** 菜单路径 */ + path: string + /** 徽标数量 */ + count: number + /** 徽标类型:dot(红点) | text(数字) */ + type: 'dot' | 'text' + /** 最后更新时间 */ + lastUpdated: number +} + +// 菜单徽标Store +export const useMenuBadgeStore = defineStore( + 'menuBadgeStore', + () => { + // 徽标数据映射 - 使用普通对象而不是Map,避免持久化问题 + const badgeData = ref>({}) + + // 页面是否已加载完成 + const isPageReady = ref(false) + + // 待处理的徽标更新队列(页面未加载完成时使用) + const pendingUpdates = ref>([]) + + // 获取当前用户ID + const getCurrentUserId = (): string => { + try { + const userStore = useUserStore() + return userStore.info?.userId?.toString() || 'anonymous' + } catch { + return 'anonymous' + } + } + + // 获取用户特定的存储key + const getUserStorageKey = (): string => { + const userId = getCurrentUserId() + return `menu-badge-${userId}` + } + + // 从localStorage加载用户特定的徽标数据 + const loadUserBadgeData = () => { + try { + const storageKey = getUserStorageKey() + const stored = localStorage.getItem(storageKey) + if (stored) { + const parsed = JSON.parse(stored) + badgeData.value = parsed.badgeData || {} + isPageReady.value = parsed.isPageReady || false + pendingUpdates.value = parsed.pendingUpdates || [] + } + } catch (error) { + console.warn('加载用户徽标数据失败:', error) + } + } + + // 保存用户特定的徽标数据到localStorage + const saveUserBadgeData = () => { + try { + const storageKey = getUserStorageKey() + const data = { + badgeData: badgeData.value, + isPageReady: isPageReady.value, + pendingUpdates: pendingUpdates.value + } + localStorage.setItem(storageKey, JSON.stringify(data)) + } catch (error) { + console.warn('保存用户徽标数据失败:', error) + } + } + + // 清理其他用户的徽标数据 + const cleanupOtherUserData = () => { + try { + const currentUserId = getCurrentUserId() + const keys = Object.keys(localStorage) + keys.forEach((key) => { + if (key.startsWith('menu-badge-') && !key.endsWith(`-${currentUserId}`)) { + localStorage.removeItem(key) + } + }) + } catch (error) { + console.warn('清理其他用户数据失败:', error) + } + } + + // 计算属性:获取所有徽标数据 + const allBadgeData = computed(() => { + return { ...badgeData.value } + }) + + // 获取指定菜单的徽标数据 + const getBadgeData = (path: string): MenuBadgeData | null => { + return badgeData.value[path] || null + } + + // 获取指定菜单的徽标数量 + const getBadgeCount = (path: string): number => { + const data = badgeData.value[path] + return data?.count || 0 + } + + // 检查菜单是否应该显示徽标 + const shouldShowBadge = (path: string): boolean => { + const data = badgeData.value[path] + return Boolean(data && data.count > 0) + } + + // 检查菜单是否应该显示文本徽标 + const shouldShowTextBadge = (path: string): string | null => { + const data = badgeData.value[path] + if (data && data.count > 0 && data.type === 'text') { + return data.count > 99 ? '99+' : data.count.toString() + } + return null + } + + // 设置页面加载状态 + const setPageReady = (ready: boolean) => { + isPageReady.value = ready + + // 页面加载完成时,加载用户特定的徽标数据 + if (ready) { + loadUserBadgeData() + // 处理待处理的更新 + if (pendingUpdates.value.length > 0) { + // console.log('页面加载完成,处理待处理的徽标更新:', pendingUpdates.value.length) + pendingUpdates.value.forEach((update) => { + updateBadgeData(update.path, update.count, update.type) + }) + pendingUpdates.value = [] + } + } + } + + // 更新徽标数据 + const updateBadgeData = (path: string, count: number, type: 'dot' | 'text' = 'text') => { + // 如果页面未加载完成,将更新加入队列 + if (!isPageReady.value) { + // console.log('页面未加载完成,将徽标更新加入队列:', { path, count, type }) + // 检查是否已存在相同路径的待处理更新,避免重复 + const existingIndex = pendingUpdates.value.findIndex((item) => item.path === path) + if (existingIndex >= 0) { + pendingUpdates.value[existingIndex] = { path, count, type } + } else { + pendingUpdates.value.push({ path, count, type }) + } + // 保存到用户特定存储 + saveUserBadgeData() + return + } + + // console.log('更新菜单徽标:', { path, count, type }) + + if (count <= 0) { + // 如果数量为0或负数,移除徽标 + delete badgeData.value[path] + } else { + // 更新或创建徽标数据 + badgeData.value[path] = { + path, + count, + type, + lastUpdated: Date.now() + } + } + + // 保存到用户特定存储 + saveUserBadgeData() + } + + // 批量更新徽标数据 + const batchUpdateBadgeData = ( + updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }> + ) => { + if (!isPageReady.value) { + // console.log('页面未加载完成,将批量徽标更新加入队列:', updates.length) + updates.forEach((update) => { + const existingIndex = pendingUpdates.value.findIndex((item) => item.path === update.path) + if (existingIndex >= 0) { + pendingUpdates.value[existingIndex] = { + path: update.path, + count: update.count, + type: update.type || 'text' + } + } else { + pendingUpdates.value.push({ + path: update.path, + count: update.count, + type: update.type || 'text' + }) + } + }) + // 保存到用户特定存储 + saveUserBadgeData() + return + } + + // console.log('批量更新菜单徽标:', updates.length) + updates.forEach((update) => { + updateBadgeData(update.path, update.count, update.type || 'text') + }) + // 批量更新后保存 + saveUserBadgeData() + } + + // 清除指定菜单的徽标 + const clearBadge = (path: string) => { + updateBadgeData(path, 0) + } + + // 清除所有徽标 + const clearAllBadges = () => { + badgeData.value = {} + pendingUpdates.value = [] + // 保存到用户特定存储 + saveUserBadgeData() + } + + // 从菜单列表同步徽标状态 + const syncBadgeWithMenuList = (menuList: AppRouteRecord[]) => { + if (!isPageReady.value) { + // console.log('页面未加载完成,跳过菜单徽标同步') + return + } + + const updateMenuBadge = (menus: AppRouteRecord[]) => { + menus.forEach((menu) => { + const badgeData = getBadgeData(menu.path) + if (badgeData) { + // 更新菜单的徽标状态 + menu.meta.showBadge = badgeData.type === 'dot' && badgeData.count > 0 + menu.meta.showTextBadge = + badgeData.type === 'text' && badgeData.count > 0 + ? badgeData.count > 99 + ? '99+' + : badgeData.count.toString() + : undefined + } else { + // 清除菜单的徽标状态 + menu.meta.showBadge = false + menu.meta.showTextBadge = undefined + } + + // 递归处理子菜单 + if (menu.children) { + updateMenuBadge(menu.children) + } + }) + } + + updateMenuBadge(menuList) + } + + // 获取待处理更新的数量 + const getPendingUpdatesCount = () => pendingUpdates.value.length + + // 获取待处理的更新列表 + const getPendingUpdates = () => [...pendingUpdates.value] + + return { + // 状态 + badgeData, + isPageReady, + pendingUpdates, + + // 计算属性 + allBadgeData, + + // 方法 + getBadgeData, + getBadgeCount, + shouldShowBadge, + shouldShowTextBadge, + setPageReady, + updateBadgeData, + batchUpdateBadgeData, + clearBadge, + clearAllBadges, + syncBadgeWithMenuList, + getPendingUpdatesCount, + getPendingUpdates, + + // 用户隔离相关方法 + loadUserBadgeData, + saveUserBadgeData, + cleanupOtherUserData, + getCurrentUserId + } + }, + { + persist: { + key: 'menu-badge', + storage: localStorage + } + } +) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index 9e53132..643b839 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -140,6 +140,20 @@ export const useUserStore = defineStore( } const logOut = () => { + // 清理徽标系统 + import('@/composables/useMenuBadge').then(({ useMenuBadge, resetGlobalInitialized }) => { + const { cleanupBadgeSystem, clearAllMenuBadges } = useMenuBadge() + clearAllMenuBadges() + cleanupBadgeSystem() + resetGlobalInitialized() + }) + + // 清理徽标store中的其他用户数据 + import('@/store/modules/menuBadge').then(({ useMenuBadgeStore }) => { + const menuBadgeStore = useMenuBadgeStore() + menuBadgeStore.cleanupOtherUserData() + }) + // 清空所有本地缓存 localStorage.clear() sessionStorage.clear() diff --git a/src/utils/menuBadgeTest.ts b/src/utils/menuBadgeTest.ts new file mode 100644 index 0000000..25daa20 --- /dev/null +++ b/src/utils/menuBadgeTest.ts @@ -0,0 +1,186 @@ +/** + * 菜单徽标管理工具 + * 用于管理菜单徽标功能 + */ + +import { useMenuBadgeStore } from '@/store/modules/menuBadge' + +/** + * 菜单徽标管理类 + */ +export class MenuBadgeTest { + private menuBadgeStore = useMenuBadgeStore() + + /** + * 直接更新徽标 + */ + updateBadge(path: string, count: number, type: 'dot' | 'text' = 'text') { + // console.log(`直接更新徽标: ${path} = ${count} (${type})`) + this.menuBadgeStore.updateBadgeData(path, count, type) + } + + /** + * 批量更新徽标 + */ + batchUpdateBadges(updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>) { + // console.log('批量更新徽标:', updates) + this.menuBadgeStore.batchUpdateBadgeData(updates) + } + + /** + * 清除徽标 + */ + clearBadge(path: string) { + // console.log(`清除徽标: ${path}`) + this.menuBadgeStore.clearBadge(path) + } + + /** + * 清除所有徽标 + */ + clearAllBadges() { + // console.log('清除所有徽标') + this.menuBadgeStore.clearAllBadges() + } + + /** + * 获取徽标状态 + */ + getBadgeStatus(path: string) { + const data = this.menuBadgeStore.getBadgeData(path) + // console.log(`徽标状态 ${path}:`, data) + return data + } + + /** + * 获取所有徽标状态 + */ + getAllBadgeStatus() { + const allData = this.menuBadgeStore.allBadgeData + // console.log('所有徽标状态:', allData) + return allData + } + + /** + * 获取待处理更新数量 + */ + getPendingUpdatesCount() { + const count = this.menuBadgeStore.getPendingUpdatesCount() + // console.log(`待处理更新数量: ${count}`) + return count + } + + /** + * 设置页面加载状态 + */ + setPageReady(ready: boolean) { + // console.log(`设置页面加载状态: ${ready}`) + this.menuBadgeStore.setPageReady(ready) + } + + /** + * 检查实际菜单路径 + */ + checkActualMenuPaths() { + // console.log('检查实际菜单路径...') + + try { + // 获取菜单store + import('@/store/modules/menu') + .then(({ useMenuStore }) => { + const menuStore = useMenuStore() + this.printMenuPaths(menuStore.menuList) + }) + .catch((error) => { + console.error('动态导入菜单store失败:', error) + }) + } catch (error) { + console.error('检查菜单路径失败:', error) + } + } + + private printMenuPaths(menuList: any[]) { + // console.log('当前菜单列表:', menuList) + + // 递归打印所有菜单路径 + const printMenuPaths = (menus: any[], level = 0) => { + menus.forEach((menu) => { + // const indent = ' '.repeat(level) + // console.log(`${indent}${menu.path} - ${menu.meta?.title}`) + if (menu.children && menu.children.length > 0) { + printMenuPaths(menu.children, level + 1) + } + }) + } + + if (menuList && menuList.length > 0) { + printMenuPaths(menuList) + } else { + console.log('菜单列表为空,可能还未加载完成') + } + } + + /** + * 运行基础功能测试 + */ + runBasicTest() { + // console.log('开始运行基础功能测试...') + + // 1. 测试页面未加载完成时的更新 + // console.log('\n=== 测试页面未加载完成时的更新 ===') + this.setPageReady(false) + this.updateBadge('/admin/approval', 3, 'text') + this.updateBadge('/admin/stock', 1, 'dot') + + // 2. 设置页面加载完成 + // console.log('\n=== 设置页面加载完成 ===') + this.setPageReady(true) + + // 3. 测试批量更新 + // console.log('\n=== 测试批量更新 ===') + this.batchUpdateBadges([ + { path: '/admin/approval', count: 3, type: 'text' }, + { path: '/admin/stock', count: 1, type: 'dot' } + ]) + + // 4. 显示最终状态 + // console.log('\n=== 最终状态 ===') + this.getAllBadgeStatus() + + // console.log('\n基础功能测试完成!') + } + + /** + * 检查WebSocket连接状态 + */ + checkWebSocketStatus() { + // console.log('检查WebSocket连接状态...') + + // 检查全局WebSocket状态函数 + if (typeof window !== 'undefined' && (window as any).getWebSocketStatus) { + const status = (window as any).getWebSocketStatus() + console.log('WebSocket状态:', status) + } else { + console.log('WebSocket状态函数不可用') + } + + // 检查菜单徽标系统初始化状态 + const pendingCount = this.menuBadgeStore.getPendingUpdatesCount() + console.log('待处理的徽标更新数量:', pendingCount) + + if (pendingCount > 0) { + console.log('待处理的更新:', this.menuBadgeStore.getPendingUpdates()) + } + } +} + +// 创建全局测试实例 +const menuBadgeTest = new MenuBadgeTest() + +// 将测试工具挂载到全局对象,方便在控制台使用 +if (typeof window !== 'undefined') { + // @ts-expect-error - 挂载到全局对象用于调试 + window.menuBadgeTest = menuBadgeTest +} + +export default menuBadgeTest diff --git a/src/utils/sys/wsClient.ts b/src/utils/sys/wsClient.ts index 18b3381..962a845 100644 --- a/src/utils/sys/wsClient.ts +++ b/src/utils/sys/wsClient.ts @@ -1,4 +1,4 @@ -import mittBus from './mittBus' +import mittBus from './mittBus' import { useUserStore } from '@/store/modules/user' import { ElNotification } from 'element-plus' @@ -94,10 +94,14 @@ const handleMessage = (event: MessageEvent) => { let data: any = event.data try { data = JSON.parse(event.data) - } catch { - // 忽略解析错误 + } catch (error) { + // console.warn('WebSocket消息解析失败:', error, '原始数据:', event.data) + // 对于无法解析的消息,仍然尝试处理原始数据 + data = { event: 'unknown', data: event.data } } console.log('WebSocket message:', data) + + // 处理通知消息 if (data.event == 'notify.receive') { ElNotification({ title: data.data.notice_data.message.title || '通知', @@ -106,22 +110,85 @@ const handleMessage = (event: MessageEvent) => { }) mittBus.emit('notification-data-changed', 1) } + + // 处理菜单徽标更新消息 + if (data.event && data.event.includes('menu.badge')) { + // console.log('收到菜单徽标更新消息:', data) + // 直接触发徽标更新事件 + mittBus.emit('menu-badge-update', data) + } + + // 处理审批相关消息 + if (data.event && data.event.includes('approval')) { + // console.log('收到审批相关消息:', data) + // 如果是审批待办数量消息,直接触发徽标更新 + if (data.event.includes('.todo.counts') || data.event.includes('.badge.counts')) { + mittBus.emit('menu-badge-update', data) + } else { + // 其他审批消息触发审批徽标更新事件 + mittBus.emit('approval-badge-update', data) + } + } + + // 处理库存相关消息 + if (data.event && data.event.includes('stock')) { + // console.log('收到库存相关消息:', data) + // 触发库存徽标更新事件 + mittBus.emit('stock-badge-update', data) + } + + // 处理通知相关消息 + if (data.event && data.event.includes('notify')) { + // console.log('收到通知相关消息:', data) + // 触发通知徽标更新事件 + mittBus.emit('notification-badge-update', data) + } + + // 处理其他可能的徽标相关消息 + if (data.event && (data.event.includes('badge') || data.event.includes('count'))) { + // console.log('收到其他徽标相关消息:', data) + // 尝试作为通用徽标更新处理 + mittBus.emit('menu-badge-update', data) + } + + // 发送原始消息到事件总线 mittBus.emit('ws:message', data) + + // 检查WebSocket连接状态,如果连接异常则记录 + if (socket && socket.readyState !== WebSocket.OPEN) { + // console.warn('WebSocket连接状态异常,当前状态:', socket.readyState, '消息可能丢失') + } } // 设置WebSocket事件处理器 const setupWebSocketHandlers = (ws: WebSocket) => { ws.onopen = () => { + console.log('WebSocket连接已建立') isInitialConnection = false isConnecting = false startHeartbeat() mittBus.emit('ws:open') sendPing() + // 连接建立成功后推送一个连接确认消息 + const connectMessage = { + event: 'system.connect', + data: { + timestamp: Date.now(), + message: 'WebSocket连接已建立' + }, + trace_id: generateTraceId() + } + try { + ws.send(JSON.stringify(connectMessage)) + } catch (error) { + console.error('发送连接确认消息失败:', error) + } } ws.onmessage = handleMessage ws.onerror = (err) => { + console.error('WebSocket连接错误:', err) mittBus.emit('ws:error', err) isConnecting = false try { @@ -134,6 +201,7 @@ const setupWebSocketHandlers = (ws: WebSocket) => { } ws.onclose = () => { + console.log('WebSocket连接已关闭') mittBus.emit('ws:close') isConnecting = false if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { @@ -187,32 +255,50 @@ const scheduleReconnect = () => { // 初始化WebSocket连接 export const initWebSocket = (token: string) => { try { - if (!token) return + if (!token) { + console.log('WebSocket初始化失败:没有token') + return + } const now = Date.now() // 简单防抖:在短时间内重复触发则跳过 if (now - lastInitAt < INIT_DEBOUNCE_MS) { + console.log('WebSocket初始化被防抖跳过') return } // 避免并发初始化:已在连接中或已连接且 token 未变化则跳过 - if (isConnecting) return - if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { + if (isConnecting) { + console.log('WebSocket正在连接中,跳过重复初始化') + return + } + if ( + socket && + (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) + ) { if (lastInitToken === token) { + console.log('WebSocket已连接且token未变化,跳过初始化') return } // token 变更:需要切换连接 - try { socket.close() } catch {} + console.log('WebSocket token变更,关闭旧连接') + try { + socket.close() + } catch { + // 忽略关闭错误 + } socket = null } cleanup() const url = `${WS_BASE}${token}` + console.log('正在初始化WebSocket连接:', url) isConnecting = true lastInitToken = token lastInitAt = now socket = new WebSocket(url) setupWebSocketHandlers(socket) } catch (e) { + console.error('WebSocket初始化失败:', e) mittBus.emit('ws:error', e) isConnecting = false scheduleReconnect() @@ -270,4 +356,4 @@ export const getWebSocketStatus = () => ({ isInitialConnection, isConnecting, lastInitToken -}) \ No newline at end of file +}) diff --git a/src/views/admin/approval/approval_add.vue b/src/views/admin/approval/approval_add.vue index 9c7fb32..a3c9597 100644 --- a/src/views/admin/approval/approval_add.vue +++ b/src/views/admin/approval/approval_add.vue @@ -425,7 +425,13 @@ // const businessUsers = ref([]) // 选项缓存,避免重复请求 - const optionsCache = reactive({ role: [], user: [], department: [] }) + const optionsCache = reactive({ + role: [], + user: [], + department: [], + grouplist: [], + group_members: [] + }) // 懒加载并缓存不同类型的选项 const ensureOptionsLoaded = async (group) => { @@ -449,6 +455,21 @@ optionsCache.user = response || [] } group.options = optionsCache.user + } else if (type === 'scenario_leader_of_initiator') { + if (!optionsCache.grouplist || optionsCache.grouplist.length === 0) { + const response = await commonService.getUserGroupScene() + optionsCache.grouplist = response || [] + } + group.options = optionsCache.grouplist + } else if (type === 'group_members') { + if (!optionsCache.group_members || optionsCache.group_members.length === 0) { + const response = await commonService.getUserGroupSelect() + response.forEach((group) => { + group.name = group.title + }) + optionsCache.group_members = response || [] + } + group.options = optionsCache.group_members } else { group.options = [] } diff --git a/src/views/admin/approval/approval_edit.vue b/src/views/admin/approval/approval_edit.vue index 57537e7..7d1263f 100644 --- a/src/views/admin/approval/approval_edit.vue +++ b/src/views/admin/approval/approval_edit.vue @@ -453,6 +453,21 @@ optionsCache.user = response || [] } group.options = optionsCache.user + } else if (type === 'scenario_leader_of_initiator') { + if (!optionsCache.grouplist || optionsCache.grouplist.length === 0) { + const response = await commonService.getUserGroupScene() + optionsCache.grouplist = response || [] + } + group.options = optionsCache.grouplist + } else if (type === 'group_members') { + if (!optionsCache.group_members || optionsCache.group_members.length === 0) { + const response = await commonService.getUserGroupSelect() + response.forEach((group) => { + group.name = group.title + }) + optionsCache.group_members = response || [] + } + group.options = optionsCache.group_members } else { group.options = [] } @@ -590,10 +605,21 @@ if (ap.type === 'initiator_leader') { group.users = [] } else if (ap.value) { - group.users = String(ap.value) + const values = String(ap.value) .split(',') - .map((v) => Number((v || '').trim())) - .filter((n) => !Number.isNaN(n)) + .map((v) => (v || '').trim()) + .filter(Boolean) + // 判断是否为纯数字ID + const isNumericIds = values.every( + (v) => !Number.isNaN(Number(v)) && Number.isInteger(Number(v)) + ) + if (isNumericIds) { + // 如果是数字ID,转换为数字数组 + group.users = values.map((v) => Number(v)) + } else { + // 如果是字符串名称,保持为字符串数组 + group.users = values + } } node.approverGroups.push(group) } diff --git a/src/views/admin/approval/stocking_auth.vue b/src/views/admin/approval/stocking_auth.vue index 97e6485..80d70c3 100644 --- a/src/views/admin/approval/stocking_auth.vue +++ b/src/views/admin/approval/stocking_auth.vue @@ -29,7 +29,13 @@ @current-change="handleCurrentChange" >