refactor: refacotr preference

This commit is contained in:
vben
2024-06-01 23:15:29 +08:00
parent f7b97e8a83
commit fed47f5e05
139 changed files with 2205 additions and 1450 deletions

View File

@@ -1,7 +0,0 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -1,51 +0,0 @@
{
"name": "@vben/stores",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/stores"
},
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"imports": {
"#*": "./src/*"
},
"exports": {
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@vben-core/toolkit": "workspace:*",
"@vben-core/typings": "workspace:*",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "3.4.27",
"vue-router": "^4.3.2"
}
}

View File

@@ -1,9 +0,0 @@
// TODO: https://github.com/vuejs/pinia/issues/2098
declare module 'pinia' {
export function acceptHMRUpdate(
initialUseStore: StoreDefinition | any,
hot: any,
): (newModule: any) => any;
}
export {};

View File

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

View File

@@ -1,24 +0,0 @@
import { createPinia, setActivePinia } from 'pinia';
import {
// beforeEach,
describe,
// expect,
it,
} from 'vitest';
// import { useAccessStore } from '../modules/access';
describe('useAccessStore', () => {
it('app Name with test', () => {
setActivePinia(createPinia());
// let referenceStore = usePreferenceStore();
// beforeEach(() => {
// referenceStore = usePreferenceStore();
// });
// expect(referenceStore.appName).toBe('vben-admin');
// referenceStore.setAppName('vbenAdmin');
// expect(referenceStore.getAppName).toBe('vbenAdmin');
});
});

View File

@@ -1,95 +0,0 @@
import type { MenuRecordRaw, UserInfo } from '@vben-core/typings';
import type { RouteRecordRaw } from 'vue-router';
import { acceptHMRUpdate, defineStore } from 'pinia';
type AccessToken = null | string;
interface AccessState {
/**
* 可访问的菜单列表
*/
accessMenus: MenuRecordRaw[];
/**
* 可访问的路由列表
*/
accessRoutes: RouteRecordRaw[];
/**
* 登录 accessToken
*/
accessToken: AccessToken;
/**
* 用户信息
*/
userInfo: UserInfo | null;
/**
* 用户角色
*/
userRoles: string[];
}
/**
* @zh_CN 访问权限相关
*/
const useAccessStore = defineStore('access', {
actions: {
setAccessMenus(menus: MenuRecordRaw[]) {
this.accessMenus = menus;
},
setAccessRoutes(routes: RouteRecordRaw[]) {
this.accessRoutes = routes;
},
setAccessToken(token: AccessToken) {
this.accessToken = token;
},
setUserInfo(userInfo: UserInfo) {
// 设置用户信息
this.userInfo = userInfo;
// 设置角色信息
const roles = userInfo?.roles ?? [];
const roleValues = roles.map((item) => item.value);
this.setUserRoles(roleValues);
},
setUserRoles(roles: string[]) {
this.userRoles = roles;
},
},
getters: {
getAccessMenus(): MenuRecordRaw[] {
return this.accessMenus;
},
getAccessRoutes(): RouteRecordRaw[] {
return this.accessRoutes;
},
getAccessToken(): AccessToken {
return this.accessToken;
},
getUserInfo(): UserInfo | null {
return this.userInfo;
},
getUserRoles(): string[] {
return this.userRoles;
},
},
persist: {
// 持久化
// TODO: accessToken 过期时间
paths: ['accessToken', 'userRoles', 'userInfo'],
},
state: (): AccessState => ({
accessMenus: [],
accessRoutes: [],
accessToken: null,
userInfo: null,
userRoles: [],
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useAccessStore, hot));
}
export { useAccessStore };

View File

@@ -1,2 +0,0 @@
export * from './access';
export * from './tabs';

View File

@@ -1,399 +0,0 @@
import { startProgress, stopProgress } from '@vben-core/toolkit';
import { TabItem } from '@vben-core/typings';
import type { RouteRecordNormalized, Router } from 'vue-router';
import { acceptHMRUpdate, defineStore } from 'pinia';
import { toRaw } from 'vue';
/**
* @zh_CN 克隆路由,防止路由被修改
* @param route
*/
function cloneTab(route: TabItem): TabItem {
if (!route) {
return route;
}
const { matched, ...opt } = route;
return {
...opt,
matched: (matched
? matched.map((item) => ({
meta: item.meta,
name: item.name,
path: item.path,
}))
: undefined) as RouteRecordNormalized[],
};
}
function routeToTab(route: RouteRecordNormalized) {
return {
meta: route.meta,
name: route.name,
path: route.path,
} as unknown as TabItem;
}
interface TabsState {
/**
* @zh_CN 固定的标签页列表
*/
affixTabs: RouteRecordNormalized[];
/**
* @zh_CN 当前打开的标签页列表缓存
*/
cacheTabs: Set<string>;
/**
* @zh_CN 需要排除缓存的标签页
*/
excludeCacheTabs: Set<string>;
/**
* @zh_CN 是否刷新
*/
renderRouteView?: boolean;
/**
* @zh_CN 当前打开的标签页列表
*/
tabs: TabItem[];
}
/**
* @zh_CN 访问权限相关
*/
const useTabsStore = defineStore('tabs', {
actions: {
/**
* Close tabs in bulk
*/
async _bulkCloseByPaths(paths: string[]) {
this.tabs = this.tabs.filter((item) => {
return !paths.includes(this.getTabPath(item));
});
this.updateCacheTab();
},
/**
* @zh_CN 关闭标签页
* @param tab
*/
_close(tab: TabItem) {
const { fullPath } = tab;
if (this._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) {
// TODO: 跳转首页
return;
}
const firstTab = this.getTabs[0];
await this._goToTab(firstTab, router);
},
/**
* @zh_CN 跳转到标签页
* @param tab
*/
async _goToTab(tab: TabItem, router: Router) {
const { params, path, query } = tab;
const toParams = {
params: params || {},
path,
query: query || {},
};
await router.replace(toParams);
},
/**
* @zh_CN 是否是固定标签页
* @param tab
*/
_isAffixTab(tab: TabItem) {
return tab?.meta?.affixTab ?? false;
},
/**
* @zh_CN 添加标签页
* @param routeTab
*/
addTab(routeTab: TabItem) {
const tab = cloneTab(routeTab);
const { fullPath, meta, params, query } = tab;
if (meta?.hideInTab) {
return;
}
const tabIndex = this.tabs.findIndex((tab) => {
return this.getTabPath(tab) === this.getTabPath(routeTab);
});
if (tabIndex === -1) {
this.tabs.push(tab);
} else {
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
const currentTab = toRaw(this.tabs)[tabIndex];
if (!currentTab) {
return;
}
currentTab.params = params || currentTab.params;
currentTab.query = query || currentTab.query;
currentTab.fullPath = fullPath || currentTab.fullPath;
this.tabs.splice(tabIndex, 1, currentTab);
}
this.updateCacheTab();
},
/**
* @zh_CN 关闭所有标签页
*/
async closeAllTabs(router: Router) {
this.tabs = this.tabs.filter((tab) => this._isAffixTab(tab));
await this._goToDefaultTab(router);
this.updateCacheTab();
},
/**
* @zh_CN 关闭左侧标签页
* @param tab
*/
async closeLeftTabs(tab: TabItem) {
const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab),
);
if (index > 0) {
const leftTabs = this.tabs.slice(0, index);
const paths: string[] = [];
for (const item of leftTabs) {
if (!this._isAffixTab(tab)) {
paths.push(this.getTabPath(item));
}
}
await this._bulkCloseByPaths(paths);
}
},
/**
* @zh_CN 关闭其他标签页
* @param tab
*/
async closeOtherTabs(tab: TabItem) {
const closePaths = this.tabs.map((item) => this.getTabPath(item));
const paths: string[] = [];
for (const path of closePaths) {
if (path !== tab.fullPath) {
const closeTab = this.tabs.find(
(item) => this.getTabPath(item) === path,
);
if (!closeTab) {
continue;
}
if (!this._isAffixTab(tab)) {
paths.push(this.getTabPath(closeTab));
}
}
}
await this._bulkCloseByPaths(paths);
},
/**
* @zh_CN 关闭右侧标签页
* @param tab
*/
async closeRightTabs(tab: TabItem) {
const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab),
);
if (index >= 0 && index < this.tabs.length - 1) {
const rightTabs = this.tabs.slice(index + 1, this.tabs.length);
const paths: string[] = [];
for (const item of rightTabs) {
if (!this._isAffixTab(tab)) {
paths.push(this.getTabPath(item));
}
}
await this._bulkCloseByPaths(paths);
}
},
/**
* @zh_CN 关闭标签页
* @param tab
* @param router
*/
async closeTab(tab: TabItem, router: Router) {
const { currentRoute } = router;
// 关闭不是激活选项卡
if (this.getTabPath(currentRoute.value) !== this.getTabPath(tab)) {
this._close(tab);
this.updateCacheTab();
return;
}
const index = this.getTabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(currentRoute.value),
);
const before = this.getTabs[index - 1];
const after = this.getTabs[index + 1];
// 下一个tab存在跳转到下一个
if (after) {
this._close(currentRoute.value);
await this._goToTab(after, router);
// 上一个tab存在跳转到上一个
} else if (before) {
this._close(currentRoute.value);
await this._goToTab(before, router);
} else {
console.error('关闭标签页失败,当前只剩一个标签页。');
}
},
/**
* @zh_CN 通过key关闭标签页
* @param key
*/
async closeTabByKey(key: string, router: Router) {
const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === key,
);
if (index === -1) {
return;
}
await this.closeTab(this.tabs[index], router);
},
getTabPath(tab: RouteRecordNormalized | TabItem) {
return decodeURIComponent((tab as TabItem).fullPath || tab.path);
},
/**
* @zh_CN 固定标签页
* @param tab
*/
async pushPinTab(tab: TabItem) {
const index = this.tabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab),
);
if (index !== -1) {
this.tabs[index].meta.affixTab = true;
}
// TODO: 这里应该把tab从tbs中移除
this.affixTabs.push(tab as unknown as RouteRecordNormalized);
},
/**
* 刷新标签页
*/
async refreshTab(router: Router) {
const { currentRoute } = router;
const { name } = currentRoute.value;
this.excludeCacheTabs.add(name as string);
this.renderRouteView = false;
startProgress();
await new Promise((resolve) => setTimeout(resolve, 200));
this.excludeCacheTabs.delete(name as string);
this.renderRouteView = true;
stopProgress();
},
/**
* 设置固定标签页
* @param tabs
*/
setAffixTabs(tabs: RouteRecordNormalized[]) {
for (const tab of tabs) {
this.addTab(routeToTab(tab));
}
this.affixTabs = tabs;
},
/**
* @zh_CN 取消固定标签页
* @param tab
*/
async unPushPinTab(tab: TabItem) {
const index = this.affixTabs.findIndex(
(item) => this.getTabPath(item) === this.getTabPath(tab),
);
if (index !== -1) {
this.affixTabs[index].meta.affixTab = false;
this.affixTabs.splice(index, 1);
this.addTab(tab);
}
},
/**
* 根据当前打开的选项卡更新缓存
*/
async updateCacheTab() {
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.cacheTabs = cacheMap;
},
},
getters: {
getCacheTabs(): string[] {
return [...this.cacheTabs];
},
getExcludeTabs(): string[] {
return [...this.excludeCacheTabs];
},
getTabs(): TabItem[] {
const tabs: TabItem[] = [];
const affixTabPaths = new Set<string>();
for (const tab of this.affixTabs) {
if (!tab.meta.hideInTab) {
tabs.push(routeToTab(tab));
affixTabPaths.add(tab.path);
}
}
for (const tab of this.tabs) {
if (!affixTabPaths.has(tab.path) && !tab.meta.hideInTab) {
tabs.push(tab);
}
}
return tabs;
},
},
persist: {
// 持久化
paths: [],
},
state: (): TabsState => ({
affixTabs: [],
cacheTabs: new Set(),
excludeCacheTabs: new Set(),
renderRouteView: true,
tabs: [],
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useTabsStore, hot));
}
export { useTabsStore };

View File

@@ -1,31 +0,0 @@
import type { App } from 'vue';
import { createPinia } from 'pinia';
interface SetupStoreOptions {
/**
* @zh_CN 应用名,由于 @vben/stores 是公用的后续可能有多个app为了防止多个app缓存冲突可在这里配置应用名
* 应用名将被用于持久化的前缀
*/
namespace: string;
}
/**
* @zh_CN 初始化pinia
* @param app vue app 实例
*/
async function setupStore(app: App, options: SetupStoreOptions) {
const { createPersistedState } = await import('pinia-plugin-persistedstate');
const pinia = createPinia();
const { namespace } = options;
pinia.use(
createPersistedState({
// key $appName-$store.id
key: (storeKey) => `__${namespace}-${storeKey}__`,
storage: localStorage,
}),
);
app.use(pinia);
}
export { setupStore };

View File

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