release: update 6.2.0

This commit is contained in:
xiaoxian521 2025-10-30 09:42:46 +08:00
parent 6106ebf2cc
commit f0ff132561
20 changed files with 1761 additions and 1656 deletions

2
.nvmrc
View File

@ -1 +1 @@
v22.17.1
v22.20.0

View File

@ -1,4 +1,5 @@
{
"tailwindCSS.experimental.configFile": "src/style/tailwind.css",
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {

View File

@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "6.1.0",
"version": "6.2.0",
"private": true,
"type": "module",
"scripts": {
@ -51,74 +51,74 @@
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.3.0",
"@pureadmin/utils": "^2.6.2",
"@vueuse/core": "^13.6.0",
"@vueuse/core": "^14.0.0",
"@vueuse/motion": "^3.0.3",
"animate.css": "^4.1.1",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.10.4",
"axios": "^1.12.2",
"dayjs": "^1.11.18",
"echarts": "^6.0.0",
"element-plus": "^2.11.5",
"js-cookie": "^3.0.5",
"localforage": "^1.10.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.3",
"pinyin-pro": "^3.26.0",
"pinyin-pro": "^3.27.0",
"qs": "^6.14.0",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.6",
"vue": "^3.5.18",
"vue-router": "^4.5.1",
"vue": "^3.5.22",
"vue-router": "^4.6.3",
"vue-tippy": "^6.7.1",
"vue-types": "^6.0.0"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@commitlint/types": "^19.8.1",
"@eslint/js": "^9.32.0",
"@faker-js/faker": "^9.9.0",
"@iconify/json": "^2.2.364",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@commitlint/types": "^20.0.0",
"@eslint/js": "^9.38.0",
"@faker-js/faker": "^10.1.0",
"@iconify/json": "^2.2.400",
"@iconify/vue": "4.2.0",
"@tailwindcss/vite": "^4.1.11",
"@tailwindcss/vite": "^4.1.16",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.19.9",
"@types/node": "^20.19.23",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.14.0",
"@types/sortablejs": "^1.15.8",
"@types/sortablejs": "^1.15.9",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vitejs/plugin-vue-jsx": "^5.1.1",
"boxen": "^8.0.1",
"code-inspector-plugin": "^1.0.3",
"cssnano": "^7.1.0",
"eslint": "^9.32.0",
"code-inspector-plugin": "^1.2.10",
"cssnano": "^7.1.1",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3",
"eslint-plugin-vue": "^10.3.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.5.1",
"gradient-string": "^3.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"lint-staged": "^16.2.6",
"postcss": "^8.5.6",
"postcss-html": "^1.8.0",
"postcss-load-config": "^6.0.1",
"postcss-scss": "^4.0.9",
"prettier": "^3.6.2",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^6.0.3",
"sass": "^1.89.2",
"stylelint": "^16.23.0",
"stylelint-config-recess-order": "^7.1.0",
"rollup-plugin-visualizer": "^6.0.5",
"sass": "^1.93.2",
"stylelint": "^16.25.0",
"stylelint-config-recess-order": "^7.4.0",
"stylelint-config-recommended-vue": "^1.6.1",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-prettier": "^5.0.3",
"svgo": "^4.0.0",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"unplugin-icons": "^22.2.0",
"vite": "^7.0.6",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"unplugin-icons": "^22.5.0",
"vite": "^7.1.12",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.2.0",
@ -126,10 +126,10 @@
"vite-plugin-router-warn": "^1.0.0",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.4"
"vue-tsc": "^3.1.2"
},
"engines": {
"node": "^20.19.0 || >=22.12.0",
"node": "^20.19.0 || >=22.13.0",
"pnpm": ">=9"
},
"pnpm": {

3083
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"Version": "6.1.0",
"Version": "6.2.0",
"Title": "PureAdmin",
"FixedHeader": true,
"HiddenSideBar": false,

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { emitter } from "@/utils/mitt";
import NProgress from "@/utils/progress";
import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag";
import { routerArrays } from "@/layout/types";
@ -204,12 +205,14 @@ function dynamicRouteTag(value: string): void {
/** 刷新路由 */
function onFresh() {
NProgress.start();
const { fullPath, query } = unref(route);
router.replace({
path: "/redirect" + fullPath,
query
});
handleAliveRoute(route as ToRouteType, "refresh");
NProgress.done();
}
function deleteDynamicTag(obj: any, current: any, tag?: string) {
@ -351,7 +354,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
break;
}
setTimeout(() => {
showMenuModel(route.fullPath, route.query);
showMenuModel(route.fullPath, route.query, route.params);
});
}
@ -386,15 +389,18 @@ function disabledMenus(value: boolean, fixedTag = false) {
function showMenuModel(
currentPath: string,
query: object = {},
params: object = {},
refresh = false
) {
const allRoute = multiTags.value;
const routeLength = multiTags.value.length;
let currentIndex = -1;
if (isAllEmpty(query)) {
currentIndex = allRoute.findIndex(v => v.path === currentPath);
} else {
if (!isAllEmpty(params)) {
currentIndex = allRoute.findIndex(v => isEqual(v.params, params));
} else if (!isAllEmpty(query)) {
currentIndex = allRoute.findIndex(v => isEqual(v.query, query));
} else {
currentIndex = allRoute.findIndex(v => v.path === currentPath);
}
function fixedTagDisabled() {
if (allRoute[currentIndex]?.meta?.fixedTag) {
@ -460,14 +466,14 @@ function openMenu(tag, e) {
} else if (route.path !== tag.path && route.name !== tag.name) {
//
tagsViews[0].show = false;
showMenuModel(tag.path, tag.query);
showMenuModel(tag.path, tag.query, tag.params);
} else if (multiTags.value.length === 2 && route.path !== tag.path) {
showMenus(true);
//
tagsViews[4].show = false;
} else if (route.path === tag.path) {
//
showMenuModel(tag.path, tag.query, true);
showMenuModel(tag.path, tag.query, tag.params);
} else {
showMenuModel(tag.path, tag.query, tag.params, true);
}
currentSelect.value = tag;

View File

@ -17,13 +17,22 @@ const loading = ref(true);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameRef = ref<HTMLElement | null>(null);
const fallbackTimer = ref<number | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
unref(currentRoute.meta)?.frameLoading === false && hideLoading();
function clearFallbackTimer() {
if (fallbackTimer.value !== null) {
clearTimeout(fallbackTimer.value);
fallbackTimer.value = null;
}
}
function hideLoading() {
loading.value = false;
clearFallbackTimer();
}
function init() {
@ -32,32 +41,42 @@ function init() {
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent("onload", () => {
hideLoading();
});
_frame.attachEvent("onload", hideLoading);
} else {
iframe.onload = () => {
hideLoading();
};
iframe.onload = hideLoading;
}
});
}
let isRedirect = false;
watch(
() => currentRoute.fullPath,
path => {
if (
currentRoute.name === "Redirect" &&
path.includes(props.frameInfo?.fullPath)
props.frameInfo?.fullPath &&
path.includes(props.frameInfo.fullPath)
) {
frameSrc.value = path; // redirect
isRedirect = true;
loading.value = true;
return;
}
//
if (props.frameInfo?.fullPath === path) {
frameSrc.value = props.frameInfo?.frameSrc;
if (props.frameInfo?.fullPath === path && isRedirect) {
loading.value = true;
clearFallbackTimer();
const url = new URL(props.frameInfo.frameSrc, window.location.origin);
const joinChar = url.search ? "&" : "?";
frameSrc.value = `${props.frameInfo.frameSrc}${joinChar}t=${Date.now()}`;
fallbackTimer.value = window.setTimeout(() => {
if (loading.value) {
hideLoading();
}
}, 1500);
isRedirect = false;
}
},
{ immediate: true }
);
onMounted(() => {

View File

@ -113,14 +113,21 @@ export function useTags() {
]);
function conditionHandle(item, previous, next) {
const currentName = route.name || "";
const itemName = item.name || "";
if (isBoolean(route?.meta?.showLink) && route?.meta?.showLink === false) {
if (Object.keys(route.query).length > 0) {
return isEqual(route.query, item.query) ? previous : next;
return currentName === itemName && isEqual(route.query, item.query)
? previous
: next;
} else {
return isEqual(route.params, item.params) ? previous : next;
return currentName === itemName && isEqual(route.params, item.params)
? previous
: next;
}
} else {
return route.path === item.path ? previous : next;
return currentName === itemName ? previous : next;
}
}

View File

@ -6,6 +6,7 @@ export const routerArrays: Array<RouteConfigs> =
? [
{
path: "/welcome",
name: "Welcome",
meta: {
title: "首页",
icon: "ep/home-filled"

View File

@ -93,6 +93,14 @@ export const router: Router = createRouter({
}
});
/** 记录已经加载的页面路径 */
const loadedPaths = new Set<string>();
/** 重置已加载页面记录 */
export function resetLoadedPaths() {
loadedPaths.clear();
}
/** 重置路由 */
export function resetRouter() {
router.clearRoutes();
@ -103,6 +111,7 @@ export function resetRouter() {
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
);
usePermissionStoreHook().clearAllCachePage();
resetLoadedPaths();
}
/** 路由白名单 */
@ -111,6 +120,12 @@ const whiteList = ["/login"];
const { VITE_HIDE_HOME } = import.meta.env;
router.beforeEach((to: ToRouteType, _from, next) => {
to.meta.loaded = loadedPaths.has(to.path);
if (!to.meta.loaded) {
NProgress.start();
}
if (to.meta?.keepAlive) {
handleAliveRoute(to, "add");
// 页面整体刷新和点击标签页刷新
@ -119,7 +134,6 @@ router.beforeEach((to: ToRouteType, _from, next) => {
}
}
const userInfo = storageLocal().getItem<DataInfo<number>>(userKey);
NProgress.start();
const externalLink = isUrl(to?.name as string);
if (!externalLink) {
to.matched.some(item => {
@ -204,7 +218,8 @@ router.beforeEach((to: ToRouteType, _from, next) => {
}
});
router.afterEach(() => {
router.afterEach(to => {
loadedPaths.add(to.path);
NProgress.done();
});

View File

@ -7,8 +7,27 @@ export default [
component: () => import("@/views/login/index.vue"),
meta: {
title: "登录",
showLink: false,
rank: 101
showLink: false
}
},
// 全屏403无权访问页面
{
path: "/access-denied",
name: "AccessDenied",
component: () => import("@/views/error/403.vue"),
meta: {
title: "403",
showLink: false
}
},
// 全屏500服务器出错页面
{
path: "/server-error",
name: "ServerError",
component: () => import("@/views/error/500.vue"),
meta: {
title: "500",
showLink: false
}
},
{
@ -16,8 +35,7 @@ export default [
component: Layout,
meta: {
title: "加载中...",
showLink: false,
rank: 102
showLink: false
},
children: [
{

View File

@ -139,12 +139,17 @@ function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
}
}
/** 动态路由注册完成后再添加全屏404页面不存在页面避免刷新动态路由页面时误跳转到404页面 */
function addPathMatch() {
if (!router.hasRoute("pathMatch")) {
router.addRoute({
path: "/:pathMatch(.*)",
name: "pathMatch",
redirect: "/error/404"
path: "/:pathMatch(.*)*",
name: "PageNotFound",
component: () => import("@/views/error/404.vue"),
meta: {
title: "404",
showLink: false
}
});
}
}

View File

@ -80,22 +80,15 @@ export const useMultiTagsStore = defineStore("pure-multiTags", {
if (isBoolean(tagVal?.meta?.showLink) && !tagVal?.meta?.showLink)
return;
const tagPath = tagVal.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {
return tag.path === tagPath;
return (
tag.path === tagPath &&
isEqual(tag?.query, tagVal?.query) &&
isEqual(tag?.params, tagVal?.params)
);
});
// 判断tag中的query键值是否相等
const tagQueryHasExits = this.multiTags.some(tag => {
return isEqual(tag?.query, tagVal?.query);
});
// 判断tag中的params键值是否相等
const tagParamsHasExits = this.multiTags.some(tag => {
return isEqual(tag?.params, tagVal?.params);
});
if (tagHasExits && tagQueryHasExits && tagParamsHasExits) return;
if (tagHasExits) return;
// 动态路由可打开的最大数量
const dynamicLevel = tagVal?.meta?.dynamicLevel ?? -1;

View File

@ -2,7 +2,6 @@ import { defineStore } from "pinia";
import {
type cacheType,
store,
debounce,
ascending,
getKeyList,
filterTree,
@ -33,21 +32,8 @@ export const usePermissionStore = defineStore("pure-permission", {
this.constantMenus.concat(routes) as any
);
},
cacheOperate({ mode, name }: cacheType) {
const delIndex = this.cachePageList.findIndex(v => v === name);
switch (mode) {
case "refresh":
this.cachePageList = this.cachePageList.filter(v => v !== name);
break;
case "add":
this.cachePageList.push(name);
break;
case "delete":
delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
break;
}
/** 监听缓存页面是否存在于标签页,不存在则删除 */
debounce(() => {
clearCache() {
let cacheLength = this.cachePageList.length;
const nameList = getKeyList(useMultiTagsStoreHook().multiTags, "name");
while (cacheLength > 0) {
@ -59,7 +45,22 @@ export const usePermissionStore = defineStore("pure-permission", {
);
cacheLength--;
}
})();
},
cacheOperate({ mode, name }: cacheType) {
const delIndex = this.cachePageList.findIndex(v => v === name);
switch (mode) {
case "refresh":
this.cachePageList = this.cachePageList.filter(v => v !== name);
this.clearCache();
break;
case "add":
this.cachePageList.push(name);
break;
case "delete":
delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
this.clearCache();
break;
}
},
/** 清空缓存页面 */
clearAllCachePage() {

View File

@ -172,10 +172,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
.is-active {
@ -270,10 +266,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
/* 子菜单中还有子菜单 */
@ -374,10 +366,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
.nest-menu .el-sub-menu > .el-sub-menu__title,
@ -530,10 +518,6 @@
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: var(--pure-theme-sub-menu-active-text) !important;
i {
color: var(--pure-theme-sub-menu-active-text) !important;
}
}
.is-active {

View File

@ -10,7 +10,6 @@ import type {
PureHttpRequestConfig
} from "./types.d";
import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
@ -61,8 +60,6 @@ class PureHttp {
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof config.beforeRequestCallback === "function") {
config.beforeRequestCallback(config);
@ -121,8 +118,6 @@ class PureHttp {
instance.interceptors.response.use(
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
// 优先判断post/get等方法是否传入回调否则执行初始化设置等回调
if (typeof $config.beforeResponseCallback === "function") {
$config.beforeResponseCallback(response);
@ -137,8 +132,6 @@ class PureHttp {
(error: PureHttpError) => {
const $error = error;
$error.isCancelRequest = Axios.isCancel($error);
// 关闭进度条动画
NProgress.done();
// 所有的响应异常 区分来源为取消请求/非取消请求
return Promise.reject($error);
}

View File

@ -10,9 +10,11 @@ const router = useRouter();
</script>
<template>
<div class="flex justify-center items-center h-[640px]">
<div
class="flex flex-col md:flex-row justify-center items-center min-h-full w-full p-4 md:p-0"
>
<noAccess />
<div class="ml-12">
<div class="mt-8 md:ml-12 md:mt-0 text-center md:text-left">
<p
v-motion
class="font-medium text-4xl mb-4! dark:text-white"
@ -32,7 +34,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4! text-gray-500"
class="text-xl mb-4! text-gray-500"
:initial="{
opacity: 0,
y: 100
@ -50,6 +52,7 @@ const router = useRouter();
<el-button
v-motion
type="primary"
class="block mx-auto md:inline-block md:mx-0"
:initial="{
opacity: 0,
y: 100
@ -68,3 +71,9 @@ const router = useRouter();
</div>
</div>
</template>
<style scoped>
.main-content {
margin: 0 !important;
}
</style>

View File

@ -10,9 +10,11 @@ const router = useRouter();
</script>
<template>
<div class="flex justify-center items-center h-[640px]">
<div
class="flex flex-col md:flex-row justify-center items-center min-h-full w-full p-4 md:p-0"
>
<noExist />
<div class="ml-12">
<div class="mt-8 md:ml-12 md:mt-0 text-center md:text-left">
<p
v-motion
class="font-medium text-4xl mb-4! dark:text-white"
@ -32,7 +34,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4! text-gray-500"
class="text-xl mb-4! text-gray-500"
:initial="{
opacity: 0,
y: 100
@ -50,6 +52,7 @@ const router = useRouter();
<el-button
v-motion
type="primary"
class="block mx-auto md:inline-block md:mx-0"
:initial="{
opacity: 0,
y: 100
@ -68,3 +71,9 @@ const router = useRouter();
</div>
</div>
</template>
<style scoped>
.main-content {
margin: 0 !important;
}
</style>

View File

@ -10,9 +10,11 @@ const router = useRouter();
</script>
<template>
<div class="flex justify-center items-center h-[640px]">
<div
class="flex flex-col md:flex-row justify-center items-center min-h-full w-full p-4 md:p-0"
>
<noServer />
<div class="ml-12">
<div class="mt-8 md:ml-12 md:mt-0 text-center md:text-left">
<p
v-motion
class="font-medium text-4xl mb-4! dark:text-white"
@ -32,7 +34,7 @@ const router = useRouter();
</p>
<p
v-motion
class="mb-4! text-gray-500"
class="text-xl mb-4! text-gray-500"
:initial="{
opacity: 0,
y: 100
@ -50,6 +52,7 @@ const router = useRouter();
<el-button
v-motion
type="primary"
class="block mx-auto md:inline-block md:mx-0"
:initial="{
opacity: 0,
y: 100
@ -68,3 +71,9 @@ const router = useRouter();
</div>
</div>
</template>
<style scoped>
.main-content {
margin: 0 !important;
}
</style>

2
types/router.d.ts vendored
View File

@ -56,6 +56,8 @@ declare global {
* `activePath``activePath``path`
*/
activePath?: string;
/** 当前页面是否已经加载过 */
loaded?: boolean;
}
/**