mirror of
https://github.com/certd/certd.git
synced 2026-05-18 14:27:36 +08:00
🔱: [client] sync upgrade with 7 commits [trident-sync]
chore: Merge branch 'vben' # Conflicts: # package.json perf: antdv示例改成使用vben框架 chore: vben chore: vben chore: vben
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
export * from "./modules";
|
||||
export * from "./setup";
|
||||
export { defineStore, storeToRefs } from "pinia";
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { useAccessStore } from "./access";
|
||||
|
||||
describe("useAccessStore", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("updates accessMenus state", () => {
|
||||
const store = useAccessStore();
|
||||
expect(store.accessMenus).toEqual([]);
|
||||
store.setAccessMenus([{ name: "Dashboard", path: "/dashboard" }]);
|
||||
expect(store.accessMenus).toEqual([{ name: "Dashboard", path: "/dashboard" }]);
|
||||
});
|
||||
|
||||
it("updates accessToken state correctly", () => {
|
||||
const store = useAccessStore();
|
||||
expect(store.accessToken).toBeNull(); // 初始状态
|
||||
store.setAccessToken("abc123");
|
||||
expect(store.accessToken).toBe("abc123");
|
||||
});
|
||||
|
||||
it("returns the correct accessToken", () => {
|
||||
const store = useAccessStore();
|
||||
store.setAccessToken("xyz789");
|
||||
expect(store.accessToken).toBe("xyz789");
|
||||
});
|
||||
|
||||
// 测试设置空的访问菜单列表
|
||||
it("handles empty accessMenus correctly", () => {
|
||||
const store = useAccessStore();
|
||||
store.setAccessMenus([]);
|
||||
expect(store.accessMenus).toEqual([]);
|
||||
});
|
||||
|
||||
// 测试设置空的访问路由列表
|
||||
it("handles empty accessRoutes correctly", () => {
|
||||
const store = useAccessStore();
|
||||
store.setAccessRoutes([]);
|
||||
expect(store.accessRoutes).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
import type { MenuRecordRaw } from "../../typings";
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from "pinia";
|
||||
|
||||
type AccessToken = null | string;
|
||||
|
||||
interface AccessState {
|
||||
/**
|
||||
* 权限码
|
||||
*/
|
||||
accessCodes: string[];
|
||||
/**
|
||||
* 可访问的菜单列表
|
||||
*/
|
||||
accessMenus: MenuRecordRaw[];
|
||||
/**
|
||||
* 可访问的路由列表
|
||||
*/
|
||||
accessRoutes: RouteRecordRaw[];
|
||||
/**
|
||||
* 登录 accessToken
|
||||
*/
|
||||
accessToken: AccessToken;
|
||||
/**
|
||||
* 是否已经检查过权限
|
||||
*/
|
||||
isAccessChecked: boolean;
|
||||
/**
|
||||
* 登录是否过期
|
||||
*/
|
||||
loginExpired: boolean;
|
||||
/**
|
||||
* 登录 accessToken
|
||||
*/
|
||||
refreshToken: AccessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 访问权限相关
|
||||
*/
|
||||
export const useAccessStore = defineStore("core-access", {
|
||||
actions: {
|
||||
getMenuByPath(path: string) {
|
||||
function findMenu(menus: MenuRecordRaw[], path: string): MenuRecordRaw | undefined {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
if (menu.children) {
|
||||
const matched = findMenu(menu.children, path);
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return findMenu(this.accessMenus, path);
|
||||
},
|
||||
setAccessCodes(codes: string[]) {
|
||||
this.accessCodes = codes;
|
||||
},
|
||||
setAccessMenus(menus: MenuRecordRaw[]) {
|
||||
this.accessMenus = menus;
|
||||
},
|
||||
setAccessRoutes(routes: RouteRecordRaw[]) {
|
||||
this.accessRoutes = routes;
|
||||
},
|
||||
setAccessToken(token: AccessToken) {
|
||||
this.accessToken = token;
|
||||
},
|
||||
setIsAccessChecked(isAccessChecked: boolean) {
|
||||
this.isAccessChecked = isAccessChecked;
|
||||
},
|
||||
setLoginExpired(loginExpired: boolean) {
|
||||
this.loginExpired = loginExpired;
|
||||
},
|
||||
setRefreshToken(token: AccessToken) {
|
||||
this.refreshToken = token;
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
// 持久化
|
||||
pick: ["accessToken", "refreshToken", "accessCodes"]
|
||||
},
|
||||
state: (): AccessState => ({
|
||||
accessCodes: [],
|
||||
accessMenus: [],
|
||||
accessRoutes: [],
|
||||
accessToken: null,
|
||||
isAccessChecked: false,
|
||||
loginExpired: false,
|
||||
refreshToken: null
|
||||
})
|
||||
});
|
||||
|
||||
// 解决热更新问题
|
||||
const hot = import.meta.hot;
|
||||
if (hot) {
|
||||
hot.accept(acceptHMRUpdate(useAccessStore, hot));
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./access";
|
||||
export * from "./lock";
|
||||
export * from "./tabbar";
|
||||
export * from "./user";
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { useLockStore } from "./lock";
|
||||
|
||||
describe("useLockStore", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("should initialize with correct default state", () => {
|
||||
const store = useLockStore();
|
||||
expect(store.isLockScreen).toBe(false);
|
||||
expect(store.lockScreenPassword).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should lock screen with a password", () => {
|
||||
const store = useLockStore();
|
||||
store.lockScreen("1234");
|
||||
expect(store.isLockScreen).toBe(true);
|
||||
expect(store.lockScreenPassword).toBe("1234");
|
||||
});
|
||||
|
||||
it("should unlock screen and clear password", () => {
|
||||
const store = useLockStore();
|
||||
store.lockScreen("1234");
|
||||
store.unlockScreen();
|
||||
expect(store.isLockScreen).toBe(false);
|
||||
expect(store.lockScreenPassword).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
interface AppState {
|
||||
/**
|
||||
* 是否锁屏状态
|
||||
*/
|
||||
isLockScreen: boolean;
|
||||
/**
|
||||
* 锁屏密码
|
||||
*/
|
||||
lockScreenPassword?: string;
|
||||
}
|
||||
|
||||
export const useLockStore = defineStore("core-lock", {
|
||||
actions: {
|
||||
lockScreen(password: string) {
|
||||
this.isLockScreen = true;
|
||||
this.lockScreenPassword = password;
|
||||
},
|
||||
|
||||
unlockScreen() {
|
||||
this.isLockScreen = false;
|
||||
this.lockScreenPassword = undefined;
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
pick: ["isLockScreen", "lockScreenPassword"]
|
||||
},
|
||||
state: (): AppState => ({
|
||||
isLockScreen: false,
|
||||
lockScreenPassword: undefined
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useTabbarStore } from "./tabbar";
|
||||
|
||||
describe("useAccessStore", () => {
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: []
|
||||
});
|
||||
router.push = vi.fn();
|
||||
router.replace = vi.fn();
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("adds a new tab", () => {
|
||||
const store = useTabbarStore();
|
||||
const tab: any = {
|
||||
fullPath: "/home",
|
||||
meta: {},
|
||||
name: "Home",
|
||||
path: "/home"
|
||||
};
|
||||
store.addTab(tab);
|
||||
expect(store.tabs.length).toBe(1);
|
||||
expect(store.tabs[0]).toEqual(tab);
|
||||
});
|
||||
|
||||
it("adds a new tab if it does not exist", () => {
|
||||
const store = useTabbarStore();
|
||||
const newTab: any = {
|
||||
fullPath: "/new",
|
||||
meta: {},
|
||||
name: "New",
|
||||
path: "/new"
|
||||
};
|
||||
store.addTab(newTab);
|
||||
expect(store.tabs).toContainEqual(newTab);
|
||||
});
|
||||
|
||||
it("updates an existing tab instead of adding a new one", () => {
|
||||
const store = useTabbarStore();
|
||||
const initialTab: any = {
|
||||
fullPath: "/existing",
|
||||
meta: {},
|
||||
name: "Existing",
|
||||
path: "/existing",
|
||||
query: {}
|
||||
};
|
||||
store.tabs.push(initialTab);
|
||||
const updatedTab = { ...initialTab, query: { id: "1" } };
|
||||
store.addTab(updatedTab);
|
||||
expect(store.tabs.length).toBe(1);
|
||||
expect(store.tabs[0]?.query).toEqual({ id: "1" });
|
||||
});
|
||||
|
||||
it("closes all tabs", async () => {
|
||||
const store = useTabbarStore();
|
||||
store.tabs = [{ fullPath: "/home", meta: {}, name: "Home", path: "/home" }] as any;
|
||||
router.replace = vi.fn();
|
||||
|
||||
await store.closeAllTabs(router);
|
||||
|
||||
expect(store.tabs.length).toBe(1);
|
||||
});
|
||||
|
||||
it("closes a non-affix tab", () => {
|
||||
const store = useTabbarStore();
|
||||
const tab: any = {
|
||||
fullPath: "/closable",
|
||||
meta: {},
|
||||
name: "Closable",
|
||||
path: "/closable"
|
||||
};
|
||||
store.tabs.push(tab);
|
||||
store._close(tab);
|
||||
expect(store.tabs.length).toBe(0);
|
||||
});
|
||||
|
||||
it("does not close an affix tab", () => {
|
||||
const store = useTabbarStore();
|
||||
const affixTab: any = {
|
||||
fullPath: "/affix",
|
||||
meta: { affixTab: true },
|
||||
name: "Affix",
|
||||
path: "/affix"
|
||||
};
|
||||
store.tabs.push(affixTab);
|
||||
store._close(affixTab);
|
||||
expect(store.tabs.length).toBe(1); // Affix tab should not be closed
|
||||
});
|
||||
|
||||
it("returns all cache tabs", () => {
|
||||
const store = useTabbarStore();
|
||||
store.cachedTabs.add("Home");
|
||||
store.cachedTabs.add("About");
|
||||
expect(store.getCachedTabs).toEqual(["Home", "About"]);
|
||||
});
|
||||
|
||||
it("returns all tabs, including affix tabs", () => {
|
||||
const store = useTabbarStore();
|
||||
const normalTab: any = {
|
||||
fullPath: "/normal",
|
||||
meta: {},
|
||||
name: "Normal",
|
||||
path: "/normal"
|
||||
};
|
||||
const affixTab: any = {
|
||||
fullPath: "/affix",
|
||||
meta: { affixTab: true },
|
||||
name: "Affix",
|
||||
path: "/affix"
|
||||
};
|
||||
store.tabs.push(normalTab);
|
||||
store.affixTabs.push(affixTab);
|
||||
expect(store.getTabs).toContainEqual(normalTab);
|
||||
expect(store.affixTabs).toContainEqual(affixTab);
|
||||
});
|
||||
|
||||
it("navigates to a specific tab", async () => {
|
||||
const store = useTabbarStore();
|
||||
const tab: any = { meta: {}, name: "Dashboard", path: "/dashboard" };
|
||||
|
||||
await store._goToTab(tab, router);
|
||||
|
||||
expect(router.replace).toHaveBeenCalledWith({
|
||||
params: {},
|
||||
path: "/dashboard",
|
||||
query: {}
|
||||
});
|
||||
});
|
||||
|
||||
it("closes multiple tabs by paths", async () => {
|
||||
const store = useTabbarStore();
|
||||
store.addTab({
|
||||
fullPath: "/home",
|
||||
meta: {},
|
||||
name: "Home",
|
||||
path: "/home"
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: "/about",
|
||||
meta: {},
|
||||
name: "About",
|
||||
path: "/about"
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: "/contact",
|
||||
meta: {},
|
||||
name: "Contact",
|
||||
path: "/contact"
|
||||
} as any);
|
||||
|
||||
await store._bulkCloseByPaths(["/home", "/contact"]);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0]?.name).toBe("About");
|
||||
});
|
||||
|
||||
it("closes all tabs to the left of the specified tab", async () => {
|
||||
const store = useTabbarStore();
|
||||
store.addTab({
|
||||
fullPath: "/home",
|
||||
meta: {},
|
||||
name: "Home",
|
||||
path: "/home"
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: "/about",
|
||||
meta: {},
|
||||
name: "About",
|
||||
path: "/about"
|
||||
} as any);
|
||||
const targetTab: any = {
|
||||
fullPath: "/contact",
|
||||
meta: {},
|
||||
name: "Contact",
|
||||
path: "/contact"
|
||||
};
|
||||
store.addTab(targetTab);
|
||||
|
||||
await store.closeLeftTabs(targetTab);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0]?.name).toBe("Contact");
|
||||
});
|
||||
|
||||
it("closes all tabs except the specified tab", async () => {
|
||||
const store = useTabbarStore();
|
||||
store.addTab({
|
||||
fullPath: "/home",
|
||||
meta: {},
|
||||
name: "Home",
|
||||
path: "/home"
|
||||
} as any);
|
||||
const targetTab: any = {
|
||||
fullPath: "/about",
|
||||
meta: {},
|
||||
name: "About",
|
||||
path: "/about"
|
||||
};
|
||||
store.addTab(targetTab);
|
||||
store.addTab({
|
||||
fullPath: "/contact",
|
||||
meta: {},
|
||||
name: "Contact",
|
||||
path: "/contact"
|
||||
} as any);
|
||||
|
||||
await store.closeOtherTabs(targetTab);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0]?.name).toBe("About");
|
||||
});
|
||||
|
||||
it("closes all tabs to the right of the specified tab", async () => {
|
||||
const store = useTabbarStore();
|
||||
const targetTab: any = {
|
||||
fullPath: "/home",
|
||||
meta: {},
|
||||
name: "Home",
|
||||
path: "/home"
|
||||
};
|
||||
store.addTab(targetTab);
|
||||
store.addTab({
|
||||
fullPath: "/about",
|
||||
meta: {},
|
||||
name: "About",
|
||||
path: "/about"
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: "/contact",
|
||||
meta: {},
|
||||
name: "Contact",
|
||||
path: "/contact"
|
||||
} as any);
|
||||
|
||||
await store.closeRightTabs(targetTab);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0]?.name).toBe("Home");
|
||||
});
|
||||
|
||||
it("closes the tab with the specified key", async () => {
|
||||
const store = useTabbarStore();
|
||||
const keyToClose = "/about";
|
||||
store.addTab({
|
||||
fullPath: "/home",
|
||||
meta: {},
|
||||
name: "Home",
|
||||
path: "/home"
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: keyToClose,
|
||||
meta: {},
|
||||
name: "About",
|
||||
path: "/about"
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: "/contact",
|
||||
meta: {},
|
||||
name: "Contact",
|
||||
path: "/contact"
|
||||
} as any);
|
||||
|
||||
await store.closeTabByKey(keyToClose, router);
|
||||
|
||||
expect(store.tabs).toHaveLength(2);
|
||||
expect(store.tabs.find((tab) => tab.fullPath === keyToClose)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("refreshes the current tab", async () => {
|
||||
const store = useTabbarStore();
|
||||
const currentTab: any = {
|
||||
fullPath: "/dashboard",
|
||||
meta: { name: "Dashboard" },
|
||||
name: "Dashboard",
|
||||
path: "/dashboard"
|
||||
};
|
||||
router.currentRoute.value = currentTab;
|
||||
|
||||
await store.refresh(router);
|
||||
|
||||
expect(store.excludeCachedTabs.has("Dashboard")).toBe(false);
|
||||
expect(store.renderRouteView).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,533 @@
|
||||
import type { Router, RouteRecordNormalized } from "vue-router";
|
||||
|
||||
import type { TabDefinition } from "/@/vben/typings";
|
||||
|
||||
import { toRaw } from "vue";
|
||||
|
||||
import { preferences } from "/@/vben/preferences";
|
||||
import { openRouteInNewWindow, startProgress, stopProgress } from "/@/vben/shared/utils";
|
||||
|
||||
import { acceptHMRUpdate, defineStore } from "pinia";
|
||||
|
||||
interface TabbarState {
|
||||
/**
|
||||
* @zh_CN 当前打开的标签页列表缓存
|
||||
*/
|
||||
cachedTabs: Set<string>;
|
||||
/**
|
||||
* @zh_CN 拖拽结束的索引
|
||||
*/
|
||||
dragEndIndex: number;
|
||||
/**
|
||||
* @zh_CN 需要排除缓存的标签页
|
||||
*/
|
||||
excludeCachedTabs: Set<string>;
|
||||
/**
|
||||
* @zh_CN 是否刷新
|
||||
*/
|
||||
renderRouteView?: boolean;
|
||||
/**
|
||||
* @zh_CN 当前打开的标签页列表
|
||||
*/
|
||||
tabs: TabDefinition[];
|
||||
/**
|
||||
* @zh_CN 更新时间,用于一些更新场景,使用watch深度监听的话,会损耗性能
|
||||
*/
|
||||
updateTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 访问权限相关
|
||||
*/
|
||||
export const useTabbarStore = defineStore("core-tabbar", {
|
||||
actions: {
|
||||
/**
|
||||
* Close tabs in bulk
|
||||
*/
|
||||
async _bulkCloseByPaths(paths: string[]) {
|
||||
this.tabs = this.tabs.filter((item) => {
|
||||
return !paths.includes(getTabPath(item));
|
||||
});
|
||||
|
||||
this.updateCacheTabs();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭标签页
|
||||
* @param tab
|
||||
*/
|
||||
_close(tab: TabDefinition) {
|
||||
const { fullPath } = tab;
|
||||
if (isAffixTab(tab)) {
|
||||
return;
|
||||
}
|
||||
const index = this.tabs.findIndex((item) => item.fullPath === fullPath);
|
||||
index !== -1 && this.tabs.splice(index, 1);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 跳转到默认标签页
|
||||
*/
|
||||
async _goToDefaultTab(router: Router) {
|
||||
if (this.getTabs.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const firstTab = this.getTabs[0];
|
||||
if (firstTab) {
|
||||
await this._goToTab(firstTab, router);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @zh_CN 跳转到标签页
|
||||
* @param tab
|
||||
* @param router
|
||||
*/
|
||||
async _goToTab(tab: TabDefinition, router: Router) {
|
||||
const { params, path, query } = tab;
|
||||
const toParams = {
|
||||
params: params || {},
|
||||
path,
|
||||
query: query || {}
|
||||
};
|
||||
await router.replace(toParams);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 添加标签页
|
||||
* @param routeTab
|
||||
*/
|
||||
addTab(routeTab: TabDefinition) {
|
||||
const tab = cloneTab(routeTab);
|
||||
if (!isTabShown(tab)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = this.tabs.findIndex((tab) => {
|
||||
return getTabPath(tab) === getTabPath(routeTab);
|
||||
});
|
||||
|
||||
if (tabIndex === -1) {
|
||||
const maxCount = preferences.tabbar.maxCount;
|
||||
// 获取动态路由打开数,超过 0 即代表需要控制打开数
|
||||
const maxNumOfOpenTab = (routeTab?.meta?.maxNumOfOpenTab ?? -1) as number;
|
||||
// 如果动态路由层级大于 0 了,那么就要限制该路由的打开数限制了
|
||||
// 获取到已经打开的动态路由数, 判断是否大于某一个值
|
||||
if (maxNumOfOpenTab > 0 && this.tabs.filter((tab) => tab.name === routeTab.name).length >= maxNumOfOpenTab) {
|
||||
// 关闭第一个
|
||||
const index = this.tabs.findIndex((item) => item.name === routeTab.name);
|
||||
index !== -1 && this.tabs.splice(index, 1);
|
||||
} else if (maxCount > 0 && this.tabs.length >= maxCount) {
|
||||
// 关闭第一个
|
||||
const index = this.tabs.findIndex((item) => !Reflect.has(item.meta, "affixTab") || !item.meta.affixTab);
|
||||
index !== -1 && this.tabs.splice(index, 1);
|
||||
}
|
||||
this.tabs.push(tab);
|
||||
} else {
|
||||
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
|
||||
const currentTab = toRaw(this.tabs)[tabIndex];
|
||||
const mergedTab = {
|
||||
...currentTab,
|
||||
...tab,
|
||||
meta: { ...currentTab?.meta, ...tab.meta }
|
||||
};
|
||||
if (currentTab) {
|
||||
const curMeta = currentTab.meta;
|
||||
if (Reflect.has(curMeta, "affixTab")) {
|
||||
mergedTab.meta.affixTab = curMeta.affixTab;
|
||||
}
|
||||
if (Reflect.has(curMeta, "newTabTitle")) {
|
||||
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
|
||||
}
|
||||
}
|
||||
|
||||
this.tabs.splice(tabIndex, 1, mergedTab);
|
||||
}
|
||||
this.updateCacheTabs();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭所有标签页
|
||||
*/
|
||||
async closeAllTabs(router: Router) {
|
||||
const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
|
||||
await this._goToDefaultTab(router);
|
||||
this.updateCacheTabs();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭左侧标签页
|
||||
* @param tab
|
||||
*/
|
||||
async closeLeftTabs(tab: TabDefinition) {
|
||||
const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
|
||||
|
||||
if (index < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftTabs = this.tabs.slice(0, index);
|
||||
const paths: string[] = [];
|
||||
|
||||
for (const item of leftTabs) {
|
||||
if (!isAffixTab(item)) {
|
||||
paths.push(getTabPath(item));
|
||||
}
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭其他标签页
|
||||
* @param tab
|
||||
*/
|
||||
async closeOtherTabs(tab: TabDefinition) {
|
||||
const closePaths = this.tabs.map((item) => getTabPath(item));
|
||||
|
||||
const paths: string[] = [];
|
||||
|
||||
for (const path of closePaths) {
|
||||
if (path !== tab.fullPath) {
|
||||
const closeTab = this.tabs.find((item) => getTabPath(item) === path);
|
||||
if (!closeTab) {
|
||||
continue;
|
||||
}
|
||||
if (!isAffixTab(closeTab)) {
|
||||
paths.push(getTabPath(closeTab));
|
||||
}
|
||||
}
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
},
|
||||
/**
|
||||
* @zh_CN 关闭右侧标签页
|
||||
* @param tab
|
||||
*/
|
||||
async closeRightTabs(tab: TabDefinition) {
|
||||
const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
|
||||
|
||||
if (index !== -1 && index < this.tabs.length - 1) {
|
||||
const rightTabs = this.tabs.slice(index + 1);
|
||||
|
||||
const paths: string[] = [];
|
||||
for (const item of rightTabs) {
|
||||
if (!isAffixTab(item)) {
|
||||
paths.push(getTabPath(item));
|
||||
}
|
||||
}
|
||||
await this._bulkCloseByPaths(paths);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 关闭标签页
|
||||
* @param tab
|
||||
* @param router
|
||||
*/
|
||||
async closeTab(tab: TabDefinition, router: Router) {
|
||||
const { currentRoute } = router;
|
||||
|
||||
// 关闭不是激活选项卡
|
||||
if (getTabPath(currentRoute.value) !== getTabPath(tab)) {
|
||||
this._close(tab);
|
||||
this.updateCacheTabs();
|
||||
return;
|
||||
}
|
||||
const index = this.getTabs.findIndex((item) => getTabPath(item) === getTabPath(currentRoute.value));
|
||||
|
||||
const before = this.getTabs[index - 1];
|
||||
const after = this.getTabs[index + 1];
|
||||
|
||||
// 下一个tab存在,跳转到下一个
|
||||
if (after) {
|
||||
this._close(tab);
|
||||
await this._goToTab(after, router);
|
||||
// 上一个tab存在,跳转到上一个
|
||||
} else if (before) {
|
||||
this._close(tab);
|
||||
await this._goToTab(before, router);
|
||||
} else {
|
||||
console.error("Failed to close the tab; only one tab remains open.");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 通过key关闭标签页
|
||||
* @param key
|
||||
* @param router
|
||||
*/
|
||||
async closeTabByKey(key: string, router: Router) {
|
||||
const originKey = decodeURIComponent(key);
|
||||
const index = this.tabs.findIndex((item) => getTabPath(item) === originKey);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = this.tabs[index];
|
||||
if (tab) {
|
||||
await this.closeTab(tab, router);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据路径获取标签页
|
||||
* @param path
|
||||
*/
|
||||
getTabByPath(path: string) {
|
||||
return this.getTabs.find((item) => getTabPath(item) === path) as TabDefinition;
|
||||
},
|
||||
/**
|
||||
* @zh_CN 新窗口打开标签页
|
||||
* @param tab
|
||||
*/
|
||||
async openTabInNewWindow(tab: TabDefinition) {
|
||||
openRouteInNewWindow(tab.fullPath || tab.path);
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
async pinTab(tab: TabDefinition) {
|
||||
const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
|
||||
if (index !== -1) {
|
||||
const oldTab = this.tabs[index];
|
||||
tab.meta.affixTab = true;
|
||||
tab.meta.title = oldTab?.meta?.title as string;
|
||||
// this.addTab(tab);
|
||||
this.tabs.splice(index, 1, tab);
|
||||
}
|
||||
// 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
|
||||
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
// 获得固定tabs的index
|
||||
const newIndex = affixTabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
|
||||
// 交换位置重新排序
|
||||
await this.sortTabs(index, newIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新标签页
|
||||
*/
|
||||
async refresh(router: Router) {
|
||||
const { currentRoute } = router;
|
||||
const { name } = currentRoute.value;
|
||||
|
||||
this.excludeCachedTabs.add(name as string);
|
||||
this.renderRouteView = false;
|
||||
startProgress();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
this.excludeCachedTabs.delete(name as string);
|
||||
this.renderRouteView = true;
|
||||
stopProgress();
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 重置标签页标题
|
||||
*/
|
||||
async resetTabTitle(tab: TabDefinition) {
|
||||
if (tab?.meta?.newTabTitle) {
|
||||
return;
|
||||
}
|
||||
const findTab = this.tabs.find((item) => getTabPath(item) === getTabPath(tab));
|
||||
if (findTab) {
|
||||
findTab.meta.newTabTitle = undefined;
|
||||
await this.updateCacheTabs();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置固定标签页
|
||||
* @param tabs
|
||||
*/
|
||||
setAffixTabs(tabs: RouteRecordNormalized[]) {
|
||||
for (const tab of tabs) {
|
||||
tab.meta.affixTab = true;
|
||||
this.addTab(routeToTab(tab));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 设置标签页标题
|
||||
* @param tab
|
||||
* @param title
|
||||
*/
|
||||
async setTabTitle(tab: TabDefinition, title: string) {
|
||||
const findTab = this.tabs.find((item) => getTabPath(item) === getTabPath(tab));
|
||||
|
||||
if (findTab) {
|
||||
findTab.meta.newTabTitle = title;
|
||||
|
||||
await this.updateCacheTabs();
|
||||
}
|
||||
},
|
||||
|
||||
setUpdateTime() {
|
||||
this.updateTime = Date.now();
|
||||
},
|
||||
/**
|
||||
* @zh_CN 设置标签页顺序
|
||||
* @param oldIndex
|
||||
* @param newIndex
|
||||
*/
|
||||
async sortTabs(oldIndex: number, newIndex: number) {
|
||||
const currentTab = this.tabs[oldIndex];
|
||||
if (!currentTab) {
|
||||
return;
|
||||
}
|
||||
this.tabs.splice(oldIndex, 1);
|
||||
this.tabs.splice(newIndex, 0, currentTab);
|
||||
this.dragEndIndex = this.dragEndIndex + 1;
|
||||
},
|
||||
/**
|
||||
* @zh_CN 切换固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
async toggleTabPin(tab: TabDefinition) {
|
||||
const affixTab = tab?.meta?.affixTab ?? false;
|
||||
|
||||
await (affixTab ? this.unpinTab(tab) : this.pinTab(tab));
|
||||
},
|
||||
|
||||
/**
|
||||
* @zh_CN 取消固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
async unpinTab(tab: TabDefinition) {
|
||||
const index = this.tabs.findIndex((item) => getTabPath(item) === getTabPath(tab));
|
||||
|
||||
if (index !== -1) {
|
||||
const oldTab = this.tabs[index];
|
||||
tab.meta.affixTab = false;
|
||||
tab.meta.title = oldTab?.meta?.title as string;
|
||||
// this.addTab(tab);
|
||||
this.tabs.splice(index, 1, tab);
|
||||
}
|
||||
// 过滤固定tabs,后面更改affixTabOrder的值的话可能会有问题,目前行464排序affixTabs没有设置值
|
||||
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
// 获得固定tabs的index,使用固定tabs的下一个位置也就是活动tabs的第一个位置
|
||||
const newIndex = affixTabs.length;
|
||||
// 交换位置重新排序
|
||||
await this.sortTabs(index, newIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据当前打开的选项卡更新缓存
|
||||
*/
|
||||
async updateCacheTabs() {
|
||||
const cacheMap = new Set<string>();
|
||||
|
||||
for (const tab of this.tabs) {
|
||||
// 跳过不需要持久化的标签页
|
||||
const keepAlive = tab.meta?.keepAlive;
|
||||
if (!keepAlive) {
|
||||
continue;
|
||||
}
|
||||
(tab.matched || []).forEach((t, i) => {
|
||||
if (i > 0) {
|
||||
cacheMap.add(t.name as string);
|
||||
}
|
||||
});
|
||||
|
||||
const name = tab.name as string;
|
||||
cacheMap.add(name);
|
||||
}
|
||||
this.cachedTabs = cacheMap;
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
affixTabs(): TabDefinition[] {
|
||||
const affixTabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
|
||||
return affixTabs.sort((a, b) => {
|
||||
const orderA = (a.meta?.affixTabOrder ?? 0) as number;
|
||||
const orderB = (b.meta?.affixTabOrder ?? 0) as number;
|
||||
return orderA - orderB;
|
||||
});
|
||||
},
|
||||
getCachedTabs(): string[] {
|
||||
return [...this.cachedTabs];
|
||||
},
|
||||
getExcludeCachedTabs(): string[] {
|
||||
return [...this.excludeCachedTabs];
|
||||
},
|
||||
getTabs(): TabDefinition[] {
|
||||
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
|
||||
return [...this.affixTabs, ...normalTabs].filter(Boolean);
|
||||
}
|
||||
},
|
||||
persist: [
|
||||
// tabs不需要保存在localStorage
|
||||
{
|
||||
pick: ["tabs"],
|
||||
storage: sessionStorage
|
||||
}
|
||||
],
|
||||
state: (): TabbarState => ({
|
||||
cachedTabs: new Set(),
|
||||
dragEndIndex: 0,
|
||||
excludeCachedTabs: new Set(),
|
||||
renderRouteView: true,
|
||||
tabs: [],
|
||||
updateTime: Date.now()
|
||||
})
|
||||
});
|
||||
|
||||
// 解决热更新问题
|
||||
const hot = import.meta.hot;
|
||||
if (hot) {
|
||||
hot.accept(acceptHMRUpdate(useTabbarStore, hot));
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 克隆路由,防止路由被修改
|
||||
* @param route
|
||||
*/
|
||||
function cloneTab(route: TabDefinition): TabDefinition {
|
||||
if (!route) {
|
||||
return route;
|
||||
}
|
||||
const { matched, meta, ...opt } = route;
|
||||
return {
|
||||
...opt,
|
||||
matched: (matched
|
||||
? matched.map((item) => ({
|
||||
meta: item.meta,
|
||||
name: item.name,
|
||||
path: item.path
|
||||
}))
|
||||
: undefined) as RouteRecordNormalized[],
|
||||
meta: {
|
||||
...meta,
|
||||
newTabTitle: meta.newTabTitle
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 是否是固定标签页
|
||||
* @param tab
|
||||
*/
|
||||
function isAffixTab(tab: TabDefinition) {
|
||||
return tab?.meta?.affixTab ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 是否显示标签
|
||||
* @param tab
|
||||
*/
|
||||
function isTabShown(tab: TabDefinition) {
|
||||
const matched = tab?.matched ?? [];
|
||||
return !tab.meta.hideInTab && matched.every((item) => !item.meta.hideInTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 获取标签页路径
|
||||
* @param tab
|
||||
*/
|
||||
function getTabPath(tab: RouteRecordNormalized | TabDefinition) {
|
||||
return decodeURIComponent((tab as TabDefinition).fullPath || tab.path);
|
||||
}
|
||||
|
||||
function routeToTab(route: RouteRecordNormalized) {
|
||||
return {
|
||||
meta: route.meta,
|
||||
name: route.name,
|
||||
path: route.path
|
||||
} as TabDefinition;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { useUserStore } from "./user";
|
||||
|
||||
describe("useUserStore", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("returns correct userInfo", () => {
|
||||
const store = useUserStore();
|
||||
const userInfo: any = { name: "Jane Doe", roles: [{ value: "user" }] };
|
||||
store.setUserInfo(userInfo);
|
||||
expect(store.userInfo).toEqual(userInfo);
|
||||
});
|
||||
|
||||
// 测试重置用户信息时的行为
|
||||
it("clears userInfo and userRoles when setting null userInfo", () => {
|
||||
const store = useUserStore();
|
||||
store.setUserInfo({
|
||||
roles: [{ roleName: "User", value: "user" }]
|
||||
} as any);
|
||||
expect(store.userInfo).not.toBeNull();
|
||||
expect(store.userRoles.length).toBeGreaterThan(0);
|
||||
|
||||
store.setUserInfo(null as any);
|
||||
expect(store.userInfo).toBeNull();
|
||||
expect(store.userRoles).toEqual([]);
|
||||
});
|
||||
|
||||
// 测试在没有用户角色时返回空数组
|
||||
it("returns an empty array for userRoles if not set", () => {
|
||||
const store = useUserStore();
|
||||
expect(store.userRoles).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { acceptHMRUpdate, defineStore } from "pinia";
|
||||
|
||||
interface BasicUserInfo {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
avatar: string;
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
realName: string;
|
||||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
roles?: string[];
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface AccessState {
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
userInfo: BasicUserInfo | null;
|
||||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
userRoles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 用户信息相关
|
||||
*/
|
||||
export const useUserStore = defineStore("core-user", {
|
||||
actions: {
|
||||
setUserInfo(userInfo: BasicUserInfo | null) {
|
||||
// 设置用户信息
|
||||
this.userInfo = userInfo;
|
||||
// 设置角色信息
|
||||
const roles = userInfo?.roles ?? [];
|
||||
this.setUserRoles(roles);
|
||||
},
|
||||
setUserRoles(roles: string[]) {
|
||||
this.userRoles = roles;
|
||||
}
|
||||
},
|
||||
state: (): AccessState => ({
|
||||
userInfo: null,
|
||||
userRoles: []
|
||||
})
|
||||
});
|
||||
|
||||
// 解决热更新问题
|
||||
const hot = import.meta.hot;
|
||||
if (hot) {
|
||||
hot.accept(acceptHMRUpdate(useUserStore, hot));
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Pinia } from "pinia";
|
||||
|
||||
import type { App } from "vue";
|
||||
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
let pinia: Pinia;
|
||||
|
||||
export interface InitStoreOptions {
|
||||
/**
|
||||
* @zh_CN 应用名,由于 /@/vben/stores 是公用的,后续可能有多个app,为了防止多个app缓存冲突,可在这里配置应用名,应用名将被用于持久化的前缀
|
||||
*/
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 初始化pinia
|
||||
*/
|
||||
export async function initStores(app: App, options: InitStoreOptions) {
|
||||
const { createPersistedState } = await import("pinia-plugin-persistedstate");
|
||||
pinia = createPinia();
|
||||
const { namespace } = options;
|
||||
pinia.use(
|
||||
createPersistedState({
|
||||
// key $appName-$store.id
|
||||
key: (storeKey) => `${namespace}-${storeKey}`,
|
||||
storage: localStorage
|
||||
})
|
||||
);
|
||||
app.use(pinia);
|
||||
return pinia;
|
||||
}
|
||||
|
||||
export function resetAllStores() {
|
||||
if (!pinia) {
|
||||
console.error("Pinia is not installed");
|
||||
return;
|
||||
}
|
||||
const allStores = (pinia as any)._s;
|
||||
for (const [_key, store] of allStores) {
|
||||
store.$reset();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user