From 798bf82740e0bc631c00a7f37ad02161a106c9a8 Mon Sep 17 00:00:00 2001 From: lijing1 Date: Fri, 27 Mar 2026 11:58:17 +0800 Subject: [PATCH] merge --- src/api/login/index.ts | 2 +- src/components/UserSelector/DeptList.vue | 10 +- src/i18n/pages/route/en.ts | 1 + src/i18n/pages/route/zh-cn.ts | 1 + src/layout/index.vue | 27 +- src/router/modules.ts | 6 + src/spdm/function.mjs | 10 + src/spdm/moduleMap.mjs | 8 + src/stores/userInfo.ts | 52 ++- src/utils/eventBinder.ts | 34 ++ src/utils/slidingTimeout.ts | 360 ++++++++++++++++++ src/utils/storage.ts | 6 +- src/views/admin/log/line-chart.vue | 2 +- src/views/admin/system/user/constant.ts | 2 + src/views/admin/system/user/deptTree.vue | 78 ++-- src/views/admin/system/user/editForm.vue | 11 +- src/views/admin/system/user/index.vue | 2 +- src/views/admin/system/user/staffTable.vue | 21 +- src/views/home/widgets/components/TopNav.vue | 7 + .../home/widgets/components/audit-log.vue | 14 +- .../home/widgets/components/calendar.vue | 28 +- .../home/widgets/components/favorite-flow.vue | 6 + .../home/widgets/components/favorite-menu.vue | 6 + src/views/home/widgets/components/news.vue | 12 +- .../home/widgets/components/sys-log-line.vue | 6 + src/views/home/widgets/components/sys-log.vue | 6 + 26 files changed, 639 insertions(+), 79 deletions(-) create mode 100644 src/utils/eventBinder.ts create mode 100644 src/utils/slidingTimeout.ts create mode 100644 src/views/admin/system/user/constant.ts diff --git a/src/api/login/index.ts b/src/api/login/index.ts index 636d5c2..078662a 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -192,7 +192,7 @@ interface LogoutParams { token: string; redirect_uri: string; } -export const logout = (params: LogoutParams) => { +export const logout = (params?: LogoutParams) => { return request({ // url: '/auth/token/logout', url: '/auth-service/v1/logout', diff --git a/src/components/UserSelector/DeptList.vue b/src/components/UserSelector/DeptList.vue index ee502d4..b275e16 100644 --- a/src/components/UserSelector/DeptList.vue +++ b/src/components/UserSelector/DeptList.vue @@ -97,12 +97,16 @@ const list = computed(() => { return [...formatChildDepartments.value, ...formatUserList.value]; }); +const cacheList = computed(() => { + return list.value.filter((item) => (!disabledIdList.includes(item?.id) && item.type === SourcePickerTypeEnum.dept ? !props.deptDisabled : true)); +}); + const selectAllChecked = computed(() => { - return list.value.filter((item) => !disabledIdList.includes(item?.id)).every((item) => item.checked); + return cacheList.value.every((item) => item.checked); }); const indeterminate = computed(() => { - const checkedCount = list.value.filter((item) => !disabledIdList.includes(item?.id) && item.checked).length; - return checkedCount > 0 && checkedCount < list.value.filter((item) => !disabledIdList.includes(item?.id)).length; + const checkedCount = cacheList.value.filter((item) => item.checked).length; + return checkedCount > 0 && checkedCount < cacheList.value.length; }); onMounted(() => { diff --git a/src/i18n/pages/route/en.ts b/src/i18n/pages/route/en.ts index b60d615..b914021 100644 --- a/src/i18n/pages/route/en.ts +++ b/src/i18n/pages/route/en.ts @@ -258,6 +258,7 @@ export default { simulationProcessDetail: 'Simulation Process Detail', spdmSystemApplication: 'Application Center', spdmCompetenceCenterCondition: 'Condition Map Library', + spdmCompetenceCenterConditionDetail: 'Condition Map Library Detail', spdmCompetenceCenterStandardScene: 'Standard Scene Library', spdmCompetenceCenterKnowledge: 'Knowledge Library', spdmCompetenceCenterParameter: 'Parameter Library', diff --git a/src/i18n/pages/route/zh-cn.ts b/src/i18n/pages/route/zh-cn.ts index 175eee2..8e8a99e 100644 --- a/src/i18n/pages/route/zh-cn.ts +++ b/src/i18n/pages/route/zh-cn.ts @@ -155,6 +155,7 @@ export default { simulationProcessDetail: '仿真流程详情', spdmSystemApplication: '应用中心', spdmCompetenceCenterCondition: '仿真地图库', + spdmCompetenceCenterConditionDetail: '仿真地图库详情', spdmCompetenceCenterStandardScene: '标准场景库', spdmCompetenceCenterKnowledge: '仿真标准库', spdmCompetenceCenterParameter: '仿真参数库', diff --git a/src/layout/index.vue b/src/layout/index.vue index 61c2a47..9c12410 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -11,8 +11,33 @@ import mittBus from '/@/utils/mitt'; import {useTenantInfo} from '/@/stores/tenant'; import {useRoutesList} from '/@/stores/routesList'; import LayoutDefault from '/@/layout/main/defaults.vue'; -import {useUserInfo} from '/@/stores/userInfo'; +import {logout, useUserInfo} from '/@/stores/userInfo'; import {useMsg} from '/@/stores/msg'; +import { slidingTimeout } from '/@/utils/slidingTimeout'; + +defineOptions({ + beforeRouteEnter(_to, _from, next) { + /** + * 处理超时登出逻辑 + */ + const handleTimeoutLogout = async () => { + console.log('超时登出'); + try { + await logout(); + slidingTimeout.reset(); + } catch (error) { + console.error('超时登出失败:', error); + } + }; + slidingTimeout.start(); + if (!slidingTimeout.checkInactiveTimeout()) { + next(); + slidingTimeout.on('timeout', handleTimeoutLogout); + } else { + next('/login'); + } + }, +}); // 定义变量内容 const route = useRoute(); diff --git a/src/router/modules.ts b/src/router/modules.ts index 11635be..8908ef4 100644 --- a/src/router/modules.ts +++ b/src/router/modules.ts @@ -143,6 +143,12 @@ export default [ component: () => import('/@/spdm/views/index.vue'), meta: {"icon":"fa fa-map","code":"spdm_CompetenceCenterCondition_view"}, }, + { + path: '/spdm/competenceCenter/condition/detail', + name: 'moduleRoutes.spdmCompetenceCenterConditionDetail', + component: () => import('/@/spdm/views/index.vue'), + meta: {"icon":"fa fa-map","code":"spdm_CompetenceCenterCondition_view"}, + }, { path: '/spdm/competenceCenter/standardScene', name: 'moduleRoutes.spdmCompetenceCenterStandardScene', diff --git a/src/spdm/function.mjs b/src/spdm/function.mjs index 93c1d68..bb344f7 100644 --- a/src/spdm/function.mjs +++ b/src/spdm/function.mjs @@ -377,6 +377,16 @@ export const appList = [ icon: 'fa fa-map', }, }, + { + name: 'spdmCompetenceCenterConditionDetail', + zhCn: '仿真地图库详情', + en: 'Condition Map Library Detail', + path: '/spdm/competenceCenter/condition/detail', + component: () => import('/@/spdm/views/index.vue'), + meta: { + icon: 'fa fa-map', + }, + }, ], }, { diff --git a/src/spdm/moduleMap.mjs b/src/spdm/moduleMap.mjs index f950cf7..6c0bd83 100644 --- a/src/spdm/moduleMap.mjs +++ b/src/spdm/moduleMap.mjs @@ -223,6 +223,14 @@ export const pageMap = { code: 'spdmCompetenceCenter_view', }, }, + spdmCompetenceCenterConditionDetail: { + path: '/spdm/competenceCenter/condition/detail', + name: 'moduleRoutes.spdmCompetenceCenterConditionDetail', + meta: { + icon: 'fa fa-map', + code: 'spdm_CompetenceCenterCondition_view', + }, + }, spdmCompetenceCenterStandardScene: { path: '/spdm/competenceCenter/standardScene', name: 'moduleRoutes.spdmCompetenceCenterStandardScene', diff --git a/src/stores/userInfo.ts b/src/stores/userInfo.ts index 3001131..52ceade 100644 --- a/src/stores/userInfo.ts +++ b/src/stores/userInfo.ts @@ -1,11 +1,20 @@ -import {defineStore} from 'pinia'; -import {REFRESH_TOKEN_KEY, Session, Token} from '/@/utils/storage'; -import {getUserInfo, login, loginByMobile, loginBySocial, refreshTokenApi, tenantAuthCheck, loginByAccount} from '/@/api/login/index'; -import {useMessage} from '/@/hooks/message'; -import {avatarFormat} from '/@/utils/commonFunction'; -import {ITenant} from '/@/api/admin/tenant'; import Cookies from 'js-cookie'; import { loginHeartbeat } from '/@/spdm/utils/index'; +import { defineStore } from 'pinia'; +import { REFRESH_TOKEN_KEY, Session, Token } from '/@/utils/storage'; +import { getUserInfo, login, loginByMobile, loginBySocial, logout as logoutApi, refreshTokenApi, tenantAuthCheck } from '/@/api/login/index'; +import { useMessage } from '/@/hooks/message'; +import { avatarFormat } from '/@/utils/commonFunction'; +import { ITenant } from '/@/api/admin/tenant'; +import { slidingTimeout } from '/@/utils/slidingTimeout'; + +export const logout = async () => { + await logoutApi(); + // 清除缓存/token等 + Session.clear(); + // 使用 reload 时,不需要调用 resetRoute() 重置路由 + window.location.reload(); +}; /** * @function useUserInfo @@ -56,13 +65,14 @@ export const useUserInfo = defineStore('userInfo', { return new Promise((resolve, reject) => { login(data) - .then(({data: res}) => { + .then(({ data: res }) => { // SPDM CODE - Cookies.set('cid_user_id', res.user_id) + Cookies.set('cid_user_id', res.user_id); Session.setTenant(res.tenant_id); // 存储token 信息 Token.set(res.access_token); Session.set(REFRESH_TOKEN_KEY, res.refresh_token); + slidingTimeout.updateTimeout(); resolve(res); }) .catch((err) => { @@ -85,25 +95,25 @@ export const useUserInfo = defineStore('userInfo', { login({ grant_type: 'remote', scope: 'server', - ...resp.data + ...resp.data, }) - .then(({data: res}) => { - Session.setTenant(res.tenant_id); - // 存储token 信息 - Token.set(res.access_token); - Session.set(REFRESH_TOKEN_KEY, res.refresh_token); - resolve(res); - }) - .catch((err) => { - useMessage().error(err?.msg || '系统异常请联系管理员'); - reject(err); - }); + .then(({ data: res }) => { + Session.setTenant(res.tenant_id); + // 存储token 信息 + Token.set(res.access_token); + Session.set(REFRESH_TOKEN_KEY, res.refresh_token); + resolve(res); + }) + .catch((err) => { + useMessage().error(err?.msg || '系统异常请联系管理员'); + reject(err); + }); }) .catch((err) => { const codeMsg: Record = { 'TENANT-0057': '校验同步用户账号密码失败(员工编号或密码不存在)!', 'TENANT-0058': '校验同步用户账号密码失败(员工停用,无法登录)!"', - } + }; useMessage().error(codeMsg[err?.errorCode] || '系统异常请联系管理员'); reject(err); }); diff --git a/src/utils/eventBinder.ts b/src/utils/eventBinder.ts new file mode 100644 index 0000000..d00eb17 --- /dev/null +++ b/src/utils/eventBinder.ts @@ -0,0 +1,34 @@ +/** + * 绑定操作事件 + * @param events 监听的事件列表 + * @param handler 事件处理函数 + */ +export const bindEvents = (events: Array, handler: () => void): void => { + events.forEach((event) => { + document.addEventListener(event, handler, { passive: true }); + }); + + // 页面可见性变化事件 + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + handler(); + } + }); +}; + +/** + * 解绑操作事件 + * @param events 监听的事件列表 + * @param handler 事件处理函数 + */ +export const unbindEvents = (events: Array, handler: () => void): void => { + events.forEach((event) => { + document.removeEventListener(event, handler); + }); + + document.removeEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + handler(); + } + }); +}; diff --git a/src/utils/slidingTimeout.ts b/src/utils/slidingTimeout.ts new file mode 100644 index 0000000..67e5cb7 --- /dev/null +++ b/src/utils/slidingTimeout.ts @@ -0,0 +1,360 @@ +import { bindEvents, unbindEvents } from '/@/utils/eventBinder'; +import { Local } from '/@/utils/storage'; +import { debounce, isFunction, isString } from 'lodash-es'; + +/** + * 滑动超时支持的事件类型 + */ +type SlidingTimeoutEvent = 'timeout' | 'activity'; + +/** + * 滑动超时配置项(移除onTimeout/onActivity,改为事件监听) + * @interface SlidingTimeoutOptions + * @property {string} [storageKey=EXPIRY_TIME_KEY] - 存储最后活动时间的本地存储key + * @property {number} [timeout=1000 * 60 * 60 * 8] - 默认8小时 超时时间(毫秒) + * @property {Array} [events=['mousemove', 'click', 'keydown', 'scroll', 'touchstart']] - 需要监听的用户操作事件列表 + * @property {number} [debounceDelay=500] - 事件防抖延迟时间(毫秒),避免频繁触发检查 + * @property {boolean} [checkOnStart=true] - 是否在调用start方法时立即检查超时状态 + */ +interface SlidingTimeoutOptions { + storageKey?: string; + timeout?: number; + events?: Array; + debounceDelay?: number; + checkOnStart?: boolean; +} + +/** + * 滑动超时监听类:监听用户操作事件,每次操作重置超时时间,超时后触发指定事件 + * 核心逻辑:用户无操作达到指定时间后触发'timeout'事件,有操作则触发'activity'事件 + * @example + * // 事件监听模式用法(核心修改点) + * const slidingTimeout = new SlidingTimeout({ + * timeout: 8 * 60 * 60 * 1000, + * }); + * + * // 绑定超时事件 + * slidingTimeout.on('timeout', () => { + * console.log('用户超时了'); + * await logout(); + * }); + * + * // 绑定用户活动事件 + * slidingTimeout.on('activity', () => { + * console.log('用户操作了'); + * }); + * + * // 启动监听 + * slidingTimeout.start(); + * + * @example + * // 高级用法:解绑事件/暂停恢复 + * // 绑定事件并保存回调引用 + * const activityHandler = () => console.log('用户有操作'); + * slidingTimeout.on('activity', activityHandler); + * + * // 解绑指定事件 + * slidingTimeout.off('activity', activityHandler); + * + * // 暂停监听 + * slidingTimeout.pause(); + * // 恢复监听 + * slidingTimeout.resume(); + * + * // 页面销毁时清理 + * onUnmounted(() => slidingTimeout.destroy()); + */ +class SlidingTimeout { + private static readonly DEFAULT_ACTIVITY_EVENTS: Array = ['mousemove', 'click', 'keydown', 'scroll', 'touchstart']; + private static readonly DEFAULT_STORAGE_KEY = 'EXPIRY_TIME_KEY'; + private static readonly EXPIRE_TIME = 8 * 60 * 60 * 1000; + + // 配置项 + private storageKey: string; + private timeout: number; + private events: Array; + private debounceDelay: number; + private checkOnStart: boolean; + + // 状态管理 + private isListenerActive = false; + private isPaused = false; + private isTimeoutTriggered = false; // 防止重复触发timeout事件 + + // 事件注册表:存储不同事件的回调函数列表 + private eventHandlers: Record void | Promise>> = { + timeout: [], + activity: [], + }; + + private debouncedCheckTimeout: ReturnType; + + constructor(options: SlidingTimeoutOptions) { + const { + storageKey = SlidingTimeout.DEFAULT_STORAGE_KEY, + timeout = SlidingTimeout.EXPIRE_TIME, + events = SlidingTimeout.DEFAULT_ACTIVITY_EVENTS, + debounceDelay = 500, + checkOnStart = true, + } = options; + + // 初始化配置 + this.storageKey = storageKey; + this.timeout = timeout; + this.events = events; + this.debounceDelay = debounceDelay; + this.checkOnStart = checkOnStart; + + // 初始化防抖函数 + this.debouncedCheckTimeout = debounce(this.executeTimeoutCheck.bind(this), this.debounceDelay, { leading: true, trailing: true, maxWait: 1000 }); + } + + /** + * 绑定事件监听 + * @param event 事件类型('timeout' | 'activity') + * @param handler 事件回调函数 + */ + public on(event: SlidingTimeoutEvent, handler: () => void | Promise): void { + if (!isString(event) || !['timeout', 'activity'].includes(event)) { + throw new TypeError(`事件类型必须是 'timeout' 或 'activity',当前传入:${event}`); + } + if (!isFunction(handler)) { + throw new TypeError('事件回调必须是一个函数'); + } + console.log(`SlidingTimeout: 绑定 ${event} 事件`); + this.eventHandlers[event].push(handler); + } + + /** + * 解绑事件监听 + * @param event 事件类型('timeout' | 'activity') + * @param handler 要解绑的回调函数(不传则解绑该事件的所有回调) + */ + public off(event: SlidingTimeoutEvent, handler?: () => void | Promise): void { + if (!isString(event) || !['timeout', 'activity'].includes(event)) { + throw new TypeError(`事件类型必须是 'timeout' 或 'activity',当前传入:${event}`); + } + + if (handler) { + // 解绑指定回调 + this.eventHandlers[event] = this.eventHandlers[event].filter((h) => h !== handler); + } else { + // 解绑该事件的所有回调 + this.eventHandlers[event] = []; + } + } + + /** + * 触发指定事件(内部使用) + * @param event 事件类型 + */ + private emit(event: SlidingTimeoutEvent): void { + const handlers = this.eventHandlers[event]; + if (!handlers.length) return; + // 依次执行所有回调,捕获单个回调的错误(不影响其他回调) + for (const handler of handlers) { + try { + handler(); + } catch (error) { + console.error(`SlidingTimeout: 执行 ${event} 事件回调失败`, error); + } + } + } + + /** + * 重置超时时间(清空本地存储的最后活动时间) + */ + public reset(): void { + try { + Local.remove(this.storageKey); + } catch (error) { + console.warn('滑动超时:清空最后操作时间失败', error); + } + } + + /** + * 销毁所有监听和计时器,重置状态 + */ + public destroy(): void { + console.log('SlidingTimeout: 销毁所有监听和计时器'); + // 取消防抖函数 + this.debouncedCheckTimeout.cancel(); + + // 解绑用户操作事件 + if (this.isListenerActive) { + try { + unbindEvents(this.events, this.debouncedCheckTimeout); + } catch (error) { + console.error('滑动超时:解绑事件监听失败', error); + } + this.isListenerActive = false; + } + + // 清空事件回调注册表 + this.eventHandlers = { timeout: [], activity: [] }; + + // 重置状态 + this.isPaused = false; + this.isTimeoutTriggered = false; + } + + /** + * 启动用户活动监听 + * 1. 先销毁已有监听 + * 2. 检查超时状态 + * 3. 初始化事件监听 + */ + public start(): void { + if (this.isListenerActive) { + this.destroy(); + } + + if (this.checkOnStart) { + this.executeTimeoutCheck(); + } + + this.initActivityListener(); + } + + /** + * 暂停监听 + */ + public pause(): void { + if (this.isPaused || this.isTimeoutTriggered) return; + + this.debouncedCheckTimeout.cancel(); + if (this.isListenerActive) { + try { + unbindEvents(this.events, this.debouncedCheckTimeout); + } catch (error) { + console.error('滑动超时:暂停解绑事件失败', error); + } + } + + this.isPaused = true; + } + + /** + * 恢复监听 + */ + public resume(): void { + if (!this.isPaused || this.isTimeoutTriggered) return; + + this.isPaused = false; + if (this.checkOnStart) { + this.executeTimeoutCheck(); + } + + this.initActivityListener(); + } + + /** + * 手动触发超时逻辑(立即执行timeout事件并销毁监听) + */ + public triggerTimeout(): void { + this.handleTimeout(); + } + + /** + * 更新最后活动时间为当前时间 + */ + public updateTimeout(): void { + try { + Local.set(this.storageKey, Date.now()); + } catch (error) { + console.warn('滑动超时:更新最后操作时间失败', error); + } + } + + private get lastActivityTime(): number | null { + try { + return Number(Local.get(this.storageKey)); + } catch (error) { + console.warn('滑动超时:获取最后操作时间失败', error); + return null; + } + } + + /** + * 检查是否超时未操作 + * @returns {boolean} 是否超时 + */ + public checkInactiveTimeout(): boolean { + if (this.isPaused || this.isTimeoutTriggered) return false; + try { + if (!this.lastActivityTime) return true; + const currentTime = Date.now(); + const timeElapsed = currentTime - this.lastActivityTime; + return timeElapsed > this.timeout; + } catch (error) { + console.error('滑动超时:检查超时状态失败', error); + return false; + } + } + + /** + * 处理用户活动逻辑 + */ + private handleActivity(): void { + if (this.isPaused || this.isTimeoutTriggered) return; + + this.updateTimeout(); + this.emit('activity'); // 触发activity事件 + } + + /** + * 处理超时逻辑(触发timeout事件) + */ + private handleTimeout(): void { + if (this.isTimeoutTriggered) return; // 防止重复触发 + + this.isTimeoutTriggered = true; + this.reset(); + this.emit('timeout'); // 触发timeout事件 + } + + /** + * 执行超时检查 + * @returns {void} 是否超时 + */ + private executeTimeoutCheck(): void { + const isTimeout = this.checkInactiveTimeout(); + if (isTimeout) { + this.handleTimeout(); + return; + } + this.handleActivity(); + } + + /** + * 初始化事件监听 + */ + private initActivityListener(): void { + if (this.isListenerActive || this.isPaused || this.isTimeoutTriggered) return; + + try { + bindEvents(this.events, this.debouncedCheckTimeout); + this.isListenerActive = true; + } catch (error) { + console.error('滑动超时:绑定事件监听失败', error); + } + } +} + +const EXPIRY_TIME_KEY = 'EXPIRY_TIME_KEY'; +const DEFAULT_TIMEOUT = 8 * 60 * 60 * 1000; +export const slidingTimeout = new SlidingTimeout({ + storageKey: EXPIRY_TIME_KEY, + timeout: DEFAULT_TIMEOUT, + checkOnStart: false, +}); + +// // 绑定timeout事件 +// slidingTimeout.on('timeout', () => { +// console.log('用户超时了'); +// }); + +// // 绑定activity事件 +// slidingTimeout.on('activity', () => { +// console.log('用户操作了'); +// }); diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 07f53f5..4b99a26 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -25,7 +25,11 @@ export const Local = { // 获取永久缓存 get(key: string) { let json = window.localStorage.getItem(Local.setKey(key)); - return JSON.parse(json); + try { + return JSON.parse(json); + } catch (error) { + return ''; + } }, // 移除永久缓存 remove(key: string) { diff --git a/src/views/admin/log/line-chart.vue b/src/views/admin/log/line-chart.vue index 5dbd9e0..dc50671 100644 --- a/src/views/admin/log/line-chart.vue +++ b/src/views/admin/log/line-chart.vue @@ -1,5 +1,5 @@ + + diff --git a/src/views/admin/system/user/editForm.vue b/src/views/admin/system/user/editForm.vue index f17f327..810580a 100644 --- a/src/views/admin/system/user/editForm.vue +++ b/src/views/admin/system/user/editForm.vue @@ -52,8 +52,9 @@