更新最新代码

This commit is contained in:
luob
2025-12-24 23:48:38 +08:00
parent e728cf2c5e
commit 1fd17ef73a
1320 changed files with 83513 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
export * from './modules';
export * from './setup';
export { defineStore, storeToRefs } from 'pinia';

View File

@@ -0,0 +1,46 @@
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([]);
});
});

View File

@@ -0,0 +1,147 @@
import type { RouteRecordRaw } from 'vue-router';
import type { MenuRecordRaw } from '@vben-core/typings';
import { acceptHMRUpdate, defineStore } from 'pinia';
type AccessToken = null | string;
interface AccessState {
/**
* 权限码
*/
accessCodes: string[];
/**
* 可访问的菜单列表
*/
accessMenus: MenuRecordRaw[];
/**
* 可访问的路由列表
*/
accessRoutes: RouteRecordRaw[];
/**
* 登录 accessToken
*/
accessToken: AccessToken;
/**
* 是否已经检查过权限
*/
isAccessChecked: boolean;
/**
* 是否锁屏状态
*/
isLockScreen: boolean;
/**
* 锁屏密码
*/
lockScreenPassword?: string;
/**
* 登录是否过期
*/
loginExpired: boolean;
/**
* 登录 accessToken
*/
refreshToken: AccessToken;
/**
* 登录租户编号
*/
tenantId: null | number;
/**
* 访问租户编号
*/
visitTenantId: null | number;
}
/**
* @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);
},
lockScreen(password: string) {
this.isLockScreen = true;
this.lockScreenPassword = password;
},
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;
},
setTenantId(tenantId: null | number) {
this.tenantId = tenantId;
},
setVisitTenantId(visitTenantId: number) {
this.visitTenantId = visitTenantId;
},
unlockScreen() {
this.isLockScreen = false;
this.lockScreenPassword = undefined;
},
},
persist: {
// 持久化
pick: [
'accessToken',
'refreshToken',
'accessCodes',
'tenantId',
'visitTenantId',
'isLockScreen',
'lockScreenPassword',
],
},
state: (): AccessState => ({
accessCodes: [],
accessMenus: [],
accessRoutes: [],
accessToken: null,
isAccessChecked: false,
isLockScreen: false,
lockScreenPassword: undefined,
loginExpired: false,
refreshToken: null,
tenantId: null,
visitTenantId: null,
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useAccessStore, hot));
}

View File

@@ -0,0 +1,300 @@
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: {},
key: '/home',
name: 'Home',
path: '/home',
};
const addNewTab = store.addTab(tab);
expect(store.tabs.length).toBe(1);
expect(store.tabs[0]).toEqual(addNewTab);
});
it('adds a new tab if it does not exist', () => {
const store = useTabbarStore();
const newTab: any = {
fullPath: '/new',
meta: {},
name: 'New',
path: '/new',
};
const addNewTab = store.addTab(newTab);
expect(store.tabs).toContainEqual(addNewTab);
});
it('updates an existing tab instead of adding a new one', () => {
const store = useTabbarStore();
const initialTab: any = {
fullPath: '/existing',
meta: {
fullPathKey: false,
},
name: 'Existing',
path: '/existing',
query: {},
};
store.addTab(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.addTab({
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._bulkCloseByKeys(['/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',
};
const addTargetTab = store.addTab(targetTab);
await store.closeLeftTabs(addTargetTab);
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',
};
const addTargetTab = store.addTab(targetTab);
store.addTab({
fullPath: '/contact',
meta: {},
name: 'Contact',
path: '/contact',
} as any);
await store.closeOtherTabs(addTargetTab);
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',
};
const addTargetTab = 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(addTargetTab);
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);
});
});

View File

@@ -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([]);
});
});

View File

@@ -0,0 +1,60 @@
import { acceptHMRUpdate, defineStore } from 'pinia';
interface BasicUserInfo {
[key: string]: any;
/**
* 头像
*/
avatar: string;
/**
* 用户邮箱
*/
email?: string;
/**
* 用户昵称
*/
nickname: 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;
},
setUserRoles(roles: string[]) {
this.userRoles = roles;
},
},
state: (): AccessState => ({
userInfo: null,
userRoles: [],
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useUserStore, hot));
}

View File

@@ -0,0 +1,60 @@
import type { Pinia } from 'pinia';
import type { App } from 'vue';
import { createPinia } from 'pinia';
import SecureLS from 'secure-ls';
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;
const ls = new SecureLS({
encodingType: 'aes',
encryptionSecret: import.meta.env.VITE_APP_STORE_SECURE_KEY,
isCompression: true,
// @ts-ignore secure-ls does not have a type definition for this
metaKey: `${namespace}-secure-meta`,
});
pinia.use(
createPersistedState({
// key $appName-$store.id
key: (storeKey) => `${namespace}-${storeKey}`,
storage: import.meta.env.DEV
? localStorage
: {
getItem(key) {
return ls.get(key);
},
setItem(key, value) {
ls.set(key, value);
},
},
}),
);
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();
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src", "shim-pinia.d.ts"]
}