From 46a1479ccd7bd579be4bd9d16a601c19dbe1a15c Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 1 May 2025 18:50:57 +0800 Subject: [PATCH 01/14] chore: router vaild limit --- src/routes.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/routes.ts b/src/routes.ts index 17fa977..40151a9 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -53,6 +53,9 @@ const routerConfig: RouterOptions = { const router = createRouter(routerConfig); +// 上次路由的有效时间 +const VALID_DURATION = 24 * 60 * 60 * 1000; // 24小时,单位是毫秒 + router.beforeEach((to, from, next) => { // 进度条 if (typeof NProgress !== 'undefined') { @@ -60,8 +63,19 @@ router.beforeEach((to, from, next) => { } // 如果是根路径,且有上次访问的路径,则重定向到上次的路径 - if (to.path === config.routerPrefix + '/' && localStorage.getItem('lastPath')) { - next({ path: localStorage.getItem('lastPath') }); + if (to.path === config.routerPrefix + '/') { + const lastPath = localStorage.getItem('lastPath'); + const lastAccessTime = localStorage.getItem('lastPathAccessTime'); + + // 检查路径和时间戳是否存在,并且是否在有效期内 + if (lastPath && lastAccessTime && Date.now() - Number(lastAccessTime) < VALID_DURATION) { + next({ path: lastPath }); + } else { + // 如果路径无效或超时,清除存储的路径和时间戳 + localStorage.removeItem('lastPath'); + localStorage.removeItem('lastPathAccessTime'); + next(); + } } else { next(); } @@ -71,9 +85,10 @@ router.afterEach((to) => { if (typeof NProgress !== 'undefined') { NProgress.done(); } - // 保存当前路径 + // 保存当前路径和访问时间 if (to.path !== config.routerPrefix + '/') { localStorage.setItem('lastPath', to.path); + localStorage.setItem('lastPathAccessTime', Date.now().toString()); } }); From d48f858765d6cb0e7096db68f9a7fb645f043206 Mon Sep 17 00:00:00 2001 From: Wesley <985189328@qq.com> Date: Thu, 8 May 2025 00:12:55 +0800 Subject: [PATCH 02/14] feat: sso update --- src/config/index.ts | 19 +- src/hooks/common.ts | 1 - src/hooks/useBreadcrumb.tsx | 39 ++-- src/hooks/useParams.ts | 36 +++ src/pages/equipment/return.vue | 2 +- src/pages/index.vue | 349 ++++++++++++---------------- src/pages/manage/network-portal.vue | 215 +++++++++++++++++ src/pages/other/CHANGELOG.vue | 2 +- src/routes.ts | 44 +++- 9 files changed, 467 insertions(+), 240 deletions(-) create mode 100644 src/hooks/useParams.ts create mode 100644 src/pages/manage/network-portal.vue diff --git a/src/config/index.ts b/src/config/index.ts index c426827..c87719a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -92,6 +92,17 @@ const routerMap: RouteMaps = [ }, ], }, + { + label: '上网认证', + children: [ + { + key: 'network-portal', + label: '绑定列表', + icon: 'internet', + component: () => import('@pages/manage/network-portal.vue'), + }, + ], + }, { label: 'Management', children: [ @@ -295,10 +306,12 @@ export const routerPrefix = config.routerPrefix; export { routerMap, config, packageVersion }; export const VERSION = config.version; -export const VersionMode = config.versionMode; -export const SystemName = config.systemName; -export const SystemNameEn = config.systemNameEn; +export const versionMode = config.versionMode; +export const systemName = config.systemName; +export const systemNameEn = config.systemNameEn; export const pagePermissionVerify = config.pagePermissionVerify; export const menuPermissionVerify = config.menuPermissionVerify; export const useViewTransition = config.useViewTransition; export const allowHotUpdate = config.allowHotUpdate; +export const loginVerify = config.loginVerify; +export const menuUseCollapsed = config.menuUseCollapsed; diff --git a/src/hooks/common.ts b/src/hooks/common.ts index 3462fd2..60c81ee 100644 --- a/src/hooks/common.ts +++ b/src/hooks/common.ts @@ -74,7 +74,6 @@ export function getRoutePathObj( export function verifyPath(path: string, map = routerMap, deep = 0) { for (const item of map) { if (item?.key === path) { - console.warn(item); return true; } if (item?.children) { diff --git a/src/hooks/useBreadcrumb.tsx b/src/hooks/useBreadcrumb.tsx index d5c549f..771622e 100644 --- a/src/hooks/useBreadcrumb.tsx +++ b/src/hooks/useBreadcrumb.tsx @@ -1,5 +1,6 @@ import { defineComponent, ref, toRefs, watch } from 'vue'; import { routerMap } from '../config'; +import { Breadcrumb, BreadcrumbItem } from 'tdesign-vue-next'; import type { RouteMaps } from '@type/type'; export default defineComponent({ @@ -10,28 +11,22 @@ export default defineComponent({ }, }, setup(props) { - // const { value } = toRefs(props) const { value } = toRefs(props); + const componentValue = ref(value.value); watch( () => value.value, (newVal) => { - value.value = newVal; + componentValue.value = newVal; }, ); const isHidden = ref(false); - // 通过url判断当前页面 - // const getpath = () => { - // const path = window.location.hash.replace('#', '').replace(config.routerPrefix + '/', ''); - // return path; - // }; - - function getIteminMap( + const getItemInMap = ( map: RouteMaps, value: string, deep = 0, - ): { parent: string | null; current: string | null; fatherCrumb?: string | null } { + ): { parent: string | null; current: string | null; fatherCrumb?: string | null } => { for (const item of map) { if (item?.key === value) { isHidden.value = item?.hiddenBreadCrumb ?? false; @@ -42,7 +37,7 @@ export default defineComponent({ }; } if (item?.children) { - const result = getIteminMap(item.children, value, deep + 1); + const result = getItemInMap(item.children, value, deep + 1); if (result?.current) { return { parent: result?.fatherCrumb ?? result?.parent ?? item.label, @@ -52,33 +47,33 @@ export default defineComponent({ } } return { parent: null, current: null }; - } + }; - function renderCrumbItem(path: string, level = 0) { - const valuekey = getIteminMap(routerMap, path); + const renderCrumbItem = (path: string, level = 0) => { + const valueKey = getItemInMap(routerMap, path); if (level === 0) { return ( <> - 媒体部管理系统 - {renderCrumbItem(value.value, level + 1)} + 媒体部管理系统 + {renderCrumbItem(componentValue.value, level + 1)} ); } - if (!valuekey?.parent) { - return {valuekey?.current}; + if (!valueKey?.parent) { + return {valueKey?.current}; } else { return ( <> - {valuekey?.parent} - {valuekey?.current} + {valueKey?.parent} + {valueKey?.current} ); } - } + }; return () => (
- {renderCrumbItem(value.value)} + {renderCrumbItem(componentValue.value)}
); }, diff --git a/src/hooks/useParams.ts b/src/hooks/useParams.ts new file mode 100644 index 0000000..7467e4b --- /dev/null +++ b/src/hooks/useParams.ts @@ -0,0 +1,36 @@ +import { isArray } from 'lodash-es'; +import { LocationQuery } from 'vue-router'; + +export function getURLAllParams(location: Location) { + if (!location?.search) return {}; + const query = location?.search?.substring(1); + const vars = query.split('&').filter(Boolean); + const params: { [k: string]: string } = {}; + for (let i = 0; i < vars.length; i++) { + const pair = vars[i].split('=').filter(Boolean); + params[pair[0]] = pair[1]; + } + return params; +} + +export function getParams() { + return ( + routerQuery: LocationQuery, + location: Location, + key: string | string[], + ): { [key: string]: string | undefined } => { + const cachedURLQuery = getURLAllParams(location); + + if (isArray(key)) { + return key.reduce((acc, element) => { + acc[element] = routerQuery?.[element] ?? cachedURLQuery?.[element] ?? undefined; + return acc; + }, {}); + } + + const value = (routerQuery?.[key] as string) ?? cachedURLQuery?.[key]; + return { + [key]: value ?? undefined, + }; + }; +} diff --git a/src/pages/equipment/return.vue b/src/pages/equipment/return.vue index de59537..8c496a7 100644 --- a/src/pages/equipment/return.vue +++ b/src/pages/equipment/return.vue @@ -298,7 +298,7 @@ const getTime = () => { }; onMounted(() => { - var USER_INFO_STR = localStorage.getItem('user_info'); + var USER_INFO_STR = localStorage.getItem('userInfo'); try { let a = JSON.parse(USER_INFO_STR); if (a) { diff --git a/src/pages/index.vue b/src/pages/index.vue index d09c4a3..ad4a784 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -125,11 +125,11 @@ class="MainContent" :class="{ 'SideMenuShow-MainContent': SideMenu.show, - SideMenuUseCollapsed: config.menuUseCollapsed && !SideMenu.show, + SideMenuUseCollapsed: menuUseCollapsed && !SideMenu.show, }" :NoShowMenu="!TitleMenu.show" > - +
- -
@@ -172,32 +167,46 @@ import SideMenus from '../hooks/useMenu'; import BreadCrumb from '../hooks/useBreadcrumb'; import { computed, onBeforeMount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'; import { themeMode, toggleTheme } from '../utils/theme'; -import { config, routerMap, useViewTransition, allowHotUpdate, packageVersion } from '../config'; +import { + config, + routerMap, + useViewTransition, + allowHotUpdate, + packageVersion, + routerPrefix, + systemName, + loginVerify, + menuUseCollapsed, +} from '../config'; import { PoweroffIcon, ChatBubbleHelpIcon } from 'tdesign-icons-vue-next'; import { NotifyPlugin } from 'tdesign-vue-next'; import { getCurrentPage, verifyPath, getSSOURL, getLoginURL, getRoutePathObj, VerifyToken } from '../hooks/common'; import { useRequest } from '../hooks/useRequest'; import PageTooSmall from '../components/pages/PageSmall.vue'; -import router from '../routes'; +import { useRoute, useRouter } from 'vue-router'; import type { LocationQueryRaw } from 'vue-router'; +import { getParams, getURLAllParams } from '@hooks/useParams'; +import { curryRight, isEmpty, isObject } from 'lodash-es'; + +const router = useRouter(); +const route = useRoute(); watch( () => router.currentRoute.value.path, (val) => { - const v = val.replace(config.routerPrefix + '/', ''); - SideMenu.ComponentValue = v; + const v = val.replace(routerPrefix + '/', ''); SideMenu.value = v; }, ); +const getParamsData = getParams(); const componentPermissions = ref([]); const TitleMenu = reactive({ - text: config.systemName, + text: systemName, show: true, }); const SideMenu = reactive({ value: 'Content', - ComponentValue: 'Content', show: false, }); const MainContent = reactive({ @@ -205,13 +214,6 @@ const MainContent = reactive({ classOut: false, lastChoose: 'Content', ComponentValue: 'Content', - breadcrumb: { - show: true, - parent: '设备操作', - current: '借出', - changing1: false, - changing2: false, - }, AccountMenuOptions: [ // { content: "个人中心", value: 1, prefixIcon: }, { content: '遇到问题', value: 2, prefixIcon: }, @@ -221,7 +223,6 @@ const MainContent = reactive({ const login_info = reactive({ name: '-', code: '-', - class: '-', permissions: [], login_time: '', }); @@ -324,7 +325,6 @@ const getUserInfoByToken = (TOKEN) => { if (RES.errcode == 0) { login_info.code = RES.data.usercode; login_info.name = RES.data.name; - login_info.class = RES.data.class; login_info.login_time = RES.data.login_time; } }, @@ -445,17 +445,19 @@ const handleChangeComponent = (componentName: string, doNotToggleSideMenu: boole componentPermissions.value = getComponentPermissions(componentName); MainContent.lastChoose = componentName; SideMenu.value = componentName; - SideMenu.ComponentValue = componentName; // 若菜单不是Collapsed模式,则判断当前菜单是否为展开状态,若是则关闭 - if (config.menuUseCollapsed && SideMenu.show && !config.menuUseCollapsed) { + if (menuUseCollapsed && SideMenu.show && !menuUseCollapsed) { doNotToggleSideMenu ? null : ToggleSideMenu(); } // 应用动画 MainContent.classOut = true; setTimeout(() => { router.push({ - path: `${config.routerPrefix}/${componentName}`, - query: query as LocationQueryRaw, + path: `${routerPrefix}/${componentName}`, + query: { + ...route.query, + ...(query as LocationQueryRaw), + }, }); MainContent.ComponentValue = componentName; setTimeout(() => { @@ -546,193 +548,129 @@ const checkUpdate = () => { * @getUrlParam * @desc 获取参数 * @param id 参数名 + * @deprecated 已弃用,请使用 getParams() hooks */ -const getUrlParam = (variable) => { - // 先尝试从 hash 后的参数中获取 - const hashParts = window.location.hash.split('?'); - if (hashParts.length > 1) { - const hashParams = new URLSearchParams(hashParts[1]); - const value = hashParams.get(variable); - if (value) return value; - } - - // 如果在 hash 后没有找到,再尝试从 URL 参数中获取 - const urlParams = new URLSearchParams(window.location.search); - return urlParams.get(variable); -}; - -/** - * @applyUrlParam - * @desc 获取参数 - * @param new_param 参数名 - * @param value 参数值 - */ -// const applyUrlParam = (new_param, value, location = window.location) => { -// if (!new_param || !value) { -// return false; -// } -// var regUrl = location.search == '' ? false : true; -// var regParam = location.search.indexOf(new_param) == -1 ? true : false; -// var UrlH = location.origin + location.pathname; -// var UrlS = location.href; -// if (regUrl) { -// //有参数了,追加 -// if (regParam) { -// //没有参数,直接加 -// var newurl = UrlS + '&' + new_param + '=' + value; -// window.history.pushState(null, null, newurl); -// } else { -// //有参数,替换 -// var newurl = updateUrlParam(new_param, value); -// window.history.pushState(null, null, newurl); -// } -// } else { -// //没有参数,直接加 -// var newurl = UrlH + '?' + new_param + '=' + value; -// window.history.pushState(null, null, newurl); +// const getUrlParam = (variable) => { +// // 先尝试从 hash 后的参数中获取 +// const hashParts = window.location.hash.split('?'); +// if (hashParts.length > 1) { +// const hashParams = new URLSearchParams(hashParts[1]); +// const value = hashParams.get(variable); +// if (value) return value; // } -// }; -/** - * @updateUrlParam - * @desc 获取参数 - * @param key 参数名 - * @param value 参数值 - */ -// const updateUrlParam = (key, value) => { -// var uri = window.location.href; -// if (!value) { -// return uri; -// } -// var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i'); -// var separator = uri.indexOf('?') !== -1 ? '&' : '?'; -// if (uri.match(re)) { -// return uri.replace(re, '$1' + key + '=' + value + '$2'); -// } else { -// return uri + separator + key + '=' + value; -// } -// }; - -// const removeParam = (key, sourceURL) => { -// var rtn = sourceURL.split('?')[0], -// param, -// params_arr = [], -// queryString = sourceURL.indexOf('?') !== -1 ? sourceURL.split('?')[1] : ''; -// if (queryString !== '') { -// params_arr = queryString.split('&'); -// for (var i = params_arr.length - 1; i >= 0; i -= 1) { -// param = params_arr[i].split('=')[0]; -// if (param === key) { -// params_arr.splice(i, 1); -// } -// } -// rtn = rtn + '?' + params_arr.join('&'); -// } -// return rtn; +// // 如果在 hash 后没有找到,再尝试从 URL 参数中获取 +// const urlParams = new URLSearchParams(window.location.search); +// return urlParams.get(variable); // }; onBeforeMount(() => { console.info('System Start Running!'); document.body.style.overflow = 'hidden'; - const searchParams = new URLSearchParams(window.location.search); - const actionType = searchParams.get('actionType'); - - if (actionType == 'Login_Back') { - // 登录页面返回 - const TOKEN = searchParams.get('user_token'); - if (TOKEN) { - localStorage.setItem('token', TOKEN); - const USER_INFO = { - code: searchParams.get('user_code'), - name: searchParams.get('user_name'), - class: searchParams.get('user_class'), - login_time: searchParams.get('login_time'), - }; - login_info.code = searchParams.get('user_code'); - login_info.name = searchParams.get('user_name'); - login_info.class = searchParams.get('user_class'); - login_info.login_time = searchParams.get('login_time'); - localStorage.setItem('user_info', JSON.stringify(USER_INFO)); - - // 获取当前路由路径 - const hashParams = new URLSearchParams(); - hashParams.set('user_token', TOKEN); - - // 更新 URL,只修改 search 参数 - const newUrl = new URL(window.location.href); - newUrl.search = `?${hashParams.toString()}`; - window.history.replaceState(null, '', newUrl.toString()); + + router.isReady().then(() => { + const paramsKeys = ['actionType', 'user_token', 'user_code', 'user_name', 'login_time', 'path']; + const { + actionType, + user_token: TOKEN, + user_code, + user_name, + login_time, + path: param_path, + } = getParamsData(route.query, location, paramsKeys); + const actionType_LowerCase = actionType?.toLowerCase(); + if (actionType_LowerCase == 'login_back') { + // 登录页面返回 + if (TOKEN) { + localStorage.setItem('token', TOKEN); + const USER_INFO = { + code: user_code, + name: user_name, + login_time: login_time, + }; + localStorage.setItem('userInfo', decodeURIComponent(JSON.stringify(USER_INFO))); + + login_info.code = user_code; + login_info.name = user_name; + login_info.login_time = login_time; + } } - } - // 查询Token - const VERIFY_TOKEN = localStorage.getItem('token'); - if ((VERIFY_TOKEN == null || !VERIFY_TOKEN) && config.loginVerify == true) { - // 没有登录数据,遣返登录页面 - console.warn('未登录,跳转统一认证'); - setTimeout(() => { - location.href = getLoginURL(); - }, 1500); - } else if (config.loginVerify == true) { - // 验证登录 - setTimeout(async () => { - if (await VerifyToken()) { - // 设置userInfo - const userInfo = localStorage.getItem('user_info'); - if (userInfo) { - const { code, name, class: class_name, login_time } = JSON.parse(userInfo); - login_info.code = code; - login_info.name = name; - login_info.class = class_name; - login_info.login_time = login_time; - } - // pass - var param_path = getUrlParam('path'); - if (param_path) { - console.info('检测到 Path 参数,跳转至指定页面。'); - if (VerifyPath(param_path) === true) { - handleChangeComponent(param_path, true); + const currentURLParams = getURLAllParams(location); + // 有内容时,将url的seaarch参数转成router的参数 + if (isObject(currentURLParams) && !isEmpty(currentURLParams)) { + // 只保留token参数 + const newQuery = { + ...{ + user_token: currentURLParams?.user_token, + }, + ...route.query, + } as Record; + location.replace( + `/#/system/${localStorage.getItem('lastPath') ?? MainContent.lastChoose}?${new URLSearchParams( + newQuery, + ).toString()}`, + ); + } + // url没有Token,从内存查询Token,并验证是否有效 + const VERIFY_TOKEN = localStorage.getItem('token'); + if ((VERIFY_TOKEN == null || !VERIFY_TOKEN) && loginVerify == true) { + // 没有登录数据,遣返登录页面 + console.warn('未登录,跳转统一认证'); + // setTimeout(() => { + // location.href = getLoginURL(); + // }, 1500); + } else if (loginVerify == true) { + // 验证登录 + setTimeout(async () => { + if (await VerifyToken()) { + // 设置userInfo + const userInfo = localStorage.getItem('userInfo'); + if (userInfo) { + const { code, name, login_time } = JSON.parse(userInfo); + login_info.code = code; + login_info.name = name; + login_info.login_time = login_time; + } + // pass + if (param_path) { + console.info('检测到 Path 参数,跳转至指定页面。'); + if (VerifyPath(param_path) === true) { + handleChangeComponent(param_path, true); + } } + // 下方是为了修复刷新时跳到默认页面(首页)的问题 + handleChangeComponent(localStorage.getItem('lastPath') ?? MainContent.lastChoose, true); + localStorage.setItem('token', VERIFY_TOKEN); + NotifyPlugin('success', { + title: '温馨提示', + content: () => { + return ( +
+ 欢迎使用媒体部管理系统,祝您今日工作顺利! +
+ ); + }, + duration: 5000, + }); + getUserInfoByToken(VERIFY_TOKEN); + } else { + // location.href = getLoginURL(); } - // 下方是为了修复刷新时跳到默认页面(首页)的问题 - handleChangeComponent(localStorage.getItem('lastPath') ?? MainContent.lastChoose, true); - localStorage.setItem('token', VERIFY_TOKEN); - NotifyPlugin('success', { - title: '温馨提示', - content: () => { - return ( -
- 欢迎使用媒体部管理系统,祝您今日工作顺利! -
- ); - }, - duration: 5000, - }); - getUserInfoByToken(VERIFY_TOKEN); - } else { - location.href = getLoginURL(); - } - }); - } else { - NotifyPlugin('success', { - title: '温馨提示', - content: () => { - return ( -
- 当前已关闭登录检测,请确认当前非生产环境!! -
- ); - }, - duration: 5000, - }); - // handleChangeComponent(MainContent.lastChoose,true,false) - // var param_path = getUrlParam("path"); - // if (param_path) { - // console.log("检测到 Path 参数,跳转至指定页面。"); - // if (VerifyPath(param_path) === true) { - // handleChangeComponent(param_path, true); - // } - // } - } + }); + } else { + NotifyPlugin('success', { + title: '温馨提示', + content: () => { + return ( +
+ 当前已关闭登录检测,请确认当前非生产环境!! +
+ ); + }, + duration: 5000, + }); + } + }); // update checkUpdate(); // load message @@ -740,12 +678,12 @@ onBeforeMount(() => { LoadUserPermissions(); const currentPage = getCurrentPage(); if (currentPage) { - SideMenu.ComponentValue = currentPage; SideMenu.value = currentPage; } }); onMounted(() => { + const { __DEBUG_DONTCHECKLOGINSTATUS } = getParamsData(route.query, location, '__DEBUG_DONTCHECKLOGINSTATUS'); document.body.style.overflow = ''; // document.getElementById("loading").display = "none" theme.value = ThemeMode(); @@ -755,7 +693,8 @@ onMounted(() => { window.addEventListener('resize', (e) => { GetPageWidth(e); }); - if (getUrlParam('__DEBUG_DONTCHECKLOGINSTATUS') != 'yes') { + // DEBUG-前端不检测登录状态 + if (__DEBUG_DONTCHECKLOGINSTATUS != 'yes') { // 开始检测 startCheckToken(); } diff --git a/src/pages/manage/network-portal.vue b/src/pages/manage/network-portal.vue new file mode 100644 index 0000000..3b6d77b --- /dev/null +++ b/src/pages/manage/network-portal.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/pages/other/CHANGELOG.vue b/src/pages/other/CHANGELOG.vue index 971b9c4..31d854a 100644 --- a/src/pages/other/CHANGELOG.vue +++ b/src/pages/other/CHANGELOG.vue @@ -63,7 +63,7 @@ + + diff --git a/src/pages/index.vue b/src/pages/index.vue index e3a681f..bbb34fe 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -267,6 +267,17 @@ watch( }, ); +router.afterEach(() => { + // 应用动画 + setTimeout(() => { + MainContent.classOut = false; + MainContent.classIn = true; + setTimeout(() => { + MainContent.classIn = false; + }, 280); + }, 280); +}); + const viewAllMessage = () => { handleChangeComponent('MessageList', true); }; @@ -292,10 +303,6 @@ const getMessage = () => { }, error: function (err) { console.error(err); - NotifyPlugin.error({ - title: '获取消息内容失败', - content: err, - }); }, }); }; @@ -478,13 +485,6 @@ const handleChangeComponent = (componentName: string, doNotToggleSideMenu: boole }, }); MainContent.ComponentValue = componentName; - setTimeout(() => { - MainContent.classOut = false; - MainContent.classIn = true; - setTimeout(() => { - MainContent.classIn = false; - }, 280); - }, 280); }, 200); }; @@ -724,6 +724,16 @@ export default {