diff --git a/package.json b/package.json index 2b9ca89..ef08e3d 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "echarts": "^5.5.1", "element-plus": "^2.8.0", "github-markdown-css": "^5.8.1", + "html2canvas": "^1.4.1", "js-cookie": "^3.0.5", "localforage": "^1.10.0", "marked": "^15.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd60d90..c89be49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: github-markdown-css: specifier: ^5.8.1 version: 5.8.1 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -1269,6 +1272,10 @@ packages: balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1457,6 +1464,9 @@ packages: resolution: {integrity: sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==} engines: {node: '>=12 || >=16'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -1993,6 +2003,10 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -3242,6 +3256,9 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3357,6 +3374,9 @@ packages: util@0.10.4: resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -4650,6 +4670,8 @@ snapshots: balanced-match@2.0.0: {} + base64-arraybuffer@1.0.2: {} + binary-extensions@2.3.0: {} boolbase@1.0.0: {} @@ -4850,6 +4872,10 @@ snapshots: css-functions-list@3.2.2: {} + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -5528,6 +5554,11 @@ snapshots: html-tags@3.3.1: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -6755,6 +6786,10 @@ snapshots: yallist: 4.0.0 optional: true + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -6885,6 +6920,10 @@ snapshots: dependencies: inherits: 2.0.3 + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@8.3.2: {} vite-plugin-cdn-import@1.0.1(rollup@4.21.0)(vite@5.4.1(@types/node@20.16.1)(sass@1.77.8)): diff --git a/src/router/index.ts b/src/router/index.ts index d3fb5bf..a7fb2be 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,206 +1,180 @@ // import "@/utils/sso"; -import Cookies from "js-cookie"; -import { getConfig } from "@/config"; -import NProgress from "@/utils/progress"; -import { buildHierarchyTree } from "@/utils/tree"; -import remainingRouter from "./modules/remaining"; -import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; -import { usePermissionStoreHook } from "@/store/modules/permission"; -import { isUrl, openLink, storageLocal, isAllEmpty } from "@pureadmin/utils"; +import Cookies from 'js-cookie'; +import { getConfig } from '@/config'; +import NProgress from '@/utils/progress'; +import { buildHierarchyTree } from '@/utils/tree'; +import remainingRouter from './modules/remaining'; +import { useMultiTagsStoreHook } from '@/store/modules/multiTags'; +import { usePermissionStoreHook } from '@/store/modules/permission'; +import { isUrl, openLink, storageLocal, isAllEmpty } from '@pureadmin/utils'; import { - ascending, - getTopMenu, - initRouter, - isOneOfArray, - getHistoryMode, - findRouteByPath, - handleAliveRoute, - formatTwoStageRoutes, - formatFlatteningRoutes -} from "./utils"; -import { - type Router, - createRouter, - type RouteRecordRaw, - type RouteComponent -} from "vue-router"; -import { - type DataInfo, - userKey, - removeToken, - multipleTabsKey -} from "@/utils/auth"; + ascending, + getTopMenu, + initRouter, + isOneOfArray, + getHistoryMode, + findRouteByPath, + handleAliveRoute, + formatTwoStageRoutes, + formatFlatteningRoutes, +} from './utils'; +import { type Router, createRouter, type RouteRecordRaw, type RouteComponent } from 'vue-router'; +import { type DataInfo, userKey, removeToken, multipleTabsKey } from '@/utils/auth'; /** 自动导入全部静态路由,无需再手动引入!匹配 src/router/modules 目录(任何嵌套级别)中具有 .ts 扩展名的所有文件,除了 remaining.ts 文件 * 如何匹配所有文件请看:https://github.com/mrmlnc/fast-glob#basic-syntax * 如何排除文件请看:https://cn.vitejs.dev/guide/features.html#negative-patterns */ -const modules: Record = import.meta.glob( - ["./modules/**/*.ts", "!./modules/**/remaining.ts"], - { - eager: true - } -); +const modules: Record = import.meta.glob(['./modules/**/*.ts', '!./modules/**/remaining.ts'], { + eager: true, +}); /** 原始静态路由(未做任何处理) */ const routes = []; -Object.keys(modules).forEach(key => { - routes.push(modules[key].default); +Object.keys(modules).forEach((key) => { + routes.push(modules[key].default); }); /** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */ export const constantRoutes: Array = formatTwoStageRoutes( - formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity)))) + formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity)))) ); /** 用于渲染菜单,保持原始层级 */ -export const constantMenus: Array = ascending( - routes.flat(Infinity) -).concat(...remainingRouter); +export const constantMenus: Array = ascending(routes.flat(Infinity)).concat(...remainingRouter); /** 不参与菜单的路由 */ -export const remainingPaths = Object.keys(remainingRouter).map(v => { - return remainingRouter[v].path; +export const remainingPaths = Object.keys(remainingRouter).map((v) => { + return remainingRouter[v].path; }); /** 创建路由实例 */ export const router: Router = createRouter({ - history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY), - routes: constantRoutes.concat(...(remainingRouter as any)), - strict: true, - scrollBehavior(to, from, savedPosition) { - return new Promise(resolve => { - if (savedPosition) { - return savedPosition; - } else { - if (from.meta.saveSrollTop) { - const top: number = - document.documentElement.scrollTop || document.body.scrollTop; - resolve({ left: 0, top }); - } - } - }); - } + history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY), + routes: constantRoutes.concat(...(remainingRouter as any)), + strict: true, + scrollBehavior(to, from, savedPosition) { + return new Promise((resolve) => { + if (savedPosition) { + return savedPosition; + } else { + if (from.meta.saveSrollTop) { + const top: number = document.documentElement.scrollTop || document.body.scrollTop; + resolve({ left: 0, top }); + } + } + }); + }, }); /** 重置路由 */ export function resetRouter() { - router.getRoutes().forEach(route => { - const { name, meta } = route; - if (name && router.hasRoute(name) && meta?.backstage) { - router.removeRoute(name); - router.options.routes = formatTwoStageRoutes( - formatFlatteningRoutes( - buildHierarchyTree(ascending(routes.flat(Infinity))) - ) - ); - } - }); - usePermissionStoreHook().clearAllCachePage(); + router.getRoutes().forEach((route) => { + const { name, meta } = route; + if (name && router.hasRoute(name) && meta?.backstage) { + router.removeRoute(name); + router.options.routes = formatTwoStageRoutes(formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))); + } + }); + usePermissionStoreHook().clearAllCachePage(); } /** 路由白名单 */ -const whiteList = ["/login"]; +const whiteList = ['/login', '/htmlToImg']; const { VITE_HIDE_HOME } = import.meta.env; router.beforeEach((to: ToRouteType, _from, next) => { - if (to.meta?.keepAlive) { - handleAliveRoute(to, "add"); - // 页面整体刷新和点击标签页刷新 - if (_from.name === undefined || _from.name === "Redirect") { - handleAliveRoute(to); + if (to.meta?.keepAlive) { + handleAliveRoute(to, 'add'); + // 页面整体刷新和点击标签页刷新 + if (_from.name === undefined || _from.name === 'Redirect') { + handleAliveRoute(to); + } } - } - const userInfo = storageLocal().getItem>(userKey); - NProgress.start(); - const externalLink = isUrl(to?.name as string); - if (!externalLink) { - to.matched.some(item => { - if (!item.meta.title) return ""; - const Title = getConfig().Title; - if (Title) document.title = `${item.meta.title} | ${Title}`; - else document.title = item.meta.title as string; - }); - } - /** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */ - function toCorrectRoute() { - whiteList.includes(to.fullPath) ? next(_from.fullPath) : next(); - } - if (Cookies.get(multipleTabsKey) && userInfo) { - // 无权限跳转403页面 - if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) { - next({ path: "/error/403" }); + const userInfo = storageLocal().getItem>(userKey); + NProgress.start(); + const externalLink = isUrl(to?.name as string); + if (!externalLink) { + to.matched.some((item) => { + if (!item.meta.title) return ''; + const Title = getConfig().Title; + if (Title) document.title = `${item.meta.title} | ${Title}`; + else document.title = item.meta.title as string; + }); } - // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面 - if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") { - next({ path: "/error/404" }); + /** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */ + function toCorrectRoute() { + whiteList.includes(to.fullPath) && to.fullPath === '/login' ? next(_from.fullPath) : next(); } - if (_from?.name) { - // name为超链接 - if (externalLink) { - openLink(to?.name as string); - NProgress.done(); - } else { - toCorrectRoute(); - } - } else { - // 刷新 - if ( - usePermissionStoreHook().wholeMenus.length === 0 && - to.path !== "/login" - ) { - initRouter().then((router: Router) => { - if (!useMultiTagsStoreHook().getMultiTagsCache) { - const { path } = to; - const route = findRouteByPath( - path, - router.options.routes[0].children - ); - getTopMenu(true); - // query、params模式路由传参数的标签页不在此处处理 - if (route && route.meta?.title) { - if (isAllEmpty(route.parentId) && route.meta?.backstage) { - // 此处为动态顶级路由(目录) - const { path, name, meta } = route.children[0]; - useMultiTagsStoreHook().handleTags("push", { - path, - name, - meta - }); - } else { - const { path, name, meta } = route; - useMultiTagsStoreHook().handleTags("push", { - path, - name, - meta + if (Cookies.get(multipleTabsKey) && userInfo) { + // 无权限跳转403页面 + if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) { + next({ path: '/error/403' }); + } + // 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面 + if (VITE_HIDE_HOME === 'true' && to.fullPath === '/welcome') { + next({ path: '/error/404' }); + } + if (_from?.name) { + // name为超链接 + if (externalLink) { + openLink(to?.name as string); + NProgress.done(); + } else { + toCorrectRoute(); + } + } else { + // 刷新 + if (usePermissionStoreHook().wholeMenus.length === 0 && to.path !== '/login') { + initRouter().then((router: Router) => { + if (!useMultiTagsStoreHook().getMultiTagsCache) { + const { path } = to; + const route = findRouteByPath(path, router.options.routes[0].children); + getTopMenu(true); + // query、params模式路由传参数的标签页不在此处处理 + if (route && route.meta?.title) { + if (isAllEmpty(route.parentId) && route.meta?.backstage) { + // 此处为动态顶级路由(目录) + const { path, name, meta } = route.children[0]; + useMultiTagsStoreHook().handleTags('push', { + path, + name, + meta, + }); + } else { + const { path, name, meta } = route; + useMultiTagsStoreHook().handleTags('push', { + path, + name, + meta, + }); + } + } + } + // 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次) + if (isAllEmpty(to.name)) router.push(to.fullPath); }); - } } - } - // 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次) - if (isAllEmpty(to.name)) router.push(to.fullPath); - }); - } - toCorrectRoute(); - } - } else { - if (to.path !== "/login") { - if (whiteList.indexOf(to.path) !== -1) { - next(); - } else { - removeToken(); - next({ path: "/login" }); - } + toCorrectRoute(); + } } else { - next(); + if (to.path !== '/login') { + if (whiteList.indexOf(to.path) !== -1) { + next(); + } else { + removeToken(); + next({ path: '/login' }); + } + } else { + next(); + } } - } }); router.afterEach(() => { - NProgress.done(); + NProgress.done(); }); export default router; diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index 1edc03e..4704b54 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -37,4 +37,14 @@ export default [ rank: 103, }, }, + { + path: '/htmlToImg', + name: 'HtmlToImg', + component: () => import('@/views/utils/htmlToImg.vue'), + meta: { + title: 'HTML转图片', + showLink: false, + rank: 104, + }, + }, ] satisfies Array; diff --git a/src/views/utils/htmlToImg.vue b/src/views/utils/htmlToImg.vue new file mode 100644 index 0000000..4dc43b7 --- /dev/null +++ b/src/views/utils/htmlToImg.vue @@ -0,0 +1,207 @@ + + + + +