mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-30 07:46:13 +00:00
发布v2.0.0分支
This commit is contained in:
@@ -1,139 +0,0 @@
|
||||
import { defineStore, createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
import { getCurrentUserInfo } from "@/api/system/user";
|
||||
import { getInitConfig } from "@/api/system/config";
|
||||
import { getNoticeListAvailable } from '@/api/system/notice'
|
||||
import { getInitDict } from '@/api/system/dict'
|
||||
|
||||
// pinia-plugin-persistedstate 持久化存储官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/
|
||||
const store = createPinia()
|
||||
|
||||
store.use(piniaPluginPersistedstate)
|
||||
|
||||
|
||||
export const useNoticeStore = defineStore("notice", {
|
||||
state: () => ({
|
||||
noticeList: [], // 存储系统配置
|
||||
total: 0, // 存储系统配置
|
||||
isNoticeLoaded: false, // 标记配置是否已加载
|
||||
}),
|
||||
actions: {
|
||||
async getNotice() {
|
||||
const response = await getNoticeListAvailable();
|
||||
const { status_code, data } = response.data;
|
||||
if (status_code === 200) {
|
||||
this.noticeList = data.items;
|
||||
this.total = data.total;
|
||||
this.isNoticeLoaded = true;
|
||||
}
|
||||
},
|
||||
clearUserInfo() {
|
||||
this.noticeList = [];
|
||||
this.total = 0;
|
||||
this.isNoticeLoaded = false;
|
||||
},
|
||||
},
|
||||
persist: true
|
||||
});
|
||||
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
basicInfo: {},
|
||||
routeList: [],
|
||||
hasGetRoute: false,
|
||||
}),
|
||||
actions: {
|
||||
async getUserInfo() {
|
||||
const response = await getCurrentUserInfo();
|
||||
const result = response.data;
|
||||
const routers = result.data.menus;
|
||||
delete result.data.menus;
|
||||
this.setRoute(routers);
|
||||
this.basicInfo = { ...this.basicInfo, ...result.data };
|
||||
},
|
||||
setRoute(routers) {
|
||||
this.routeList = routers;
|
||||
this.hasGetRoute = true;
|
||||
},
|
||||
setAvatar(avatar) {
|
||||
this.basicInfo = { ...this.basicInfo, avatar };
|
||||
},
|
||||
clearUserInfo() {
|
||||
this.basicInfo = {};
|
||||
this.routeList = [];
|
||||
this.hasGetRoute = false;
|
||||
},
|
||||
},
|
||||
persist: true // 当前 store 启用了持久化
|
||||
});
|
||||
|
||||
export const useConfigStore = defineStore("config", {
|
||||
state: () => ({
|
||||
configData: {}, // 存储系统配置
|
||||
isConfigLoaded: false, // 标记配置是否已加载
|
||||
}),
|
||||
actions: {
|
||||
async getConfig() {
|
||||
const response = await getInitConfig();
|
||||
const { status_code, data } = response.data;
|
||||
if (status_code === 200) {
|
||||
data.forEach((item) => {
|
||||
this.configData[item.config_key] = item.config_value;
|
||||
})
|
||||
this.isConfigLoaded = true;
|
||||
}
|
||||
},
|
||||
getConfigValue(key) {
|
||||
return this.configData[key] || null;
|
||||
},
|
||||
},
|
||||
persist: true
|
||||
});
|
||||
|
||||
export const useDictStore = defineStore('dict', {
|
||||
state: () => ({
|
||||
dictObj: {},
|
||||
isLoaded: false,
|
||||
}),
|
||||
getters: {
|
||||
getDictObjData() {
|
||||
return this.dictObj
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 批量获取字典数据
|
||||
async getDict(types) {
|
||||
try {
|
||||
for (const type of types) {
|
||||
const response = await getInitDict(type)
|
||||
const result = response.data;
|
||||
|
||||
if (result.status_code === 200) {
|
||||
// 格式化数据
|
||||
this.dictObj[type] = result.data;
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取字典数据失败", error);
|
||||
}
|
||||
},
|
||||
|
||||
getDictLabel(datas, value) {
|
||||
const result = datas.find((item) => item.dict_value === value);
|
||||
const dict_data = {
|
||||
id: result.id,
|
||||
dict_value: result.dict_value,
|
||||
dict_label: result.dict_label,
|
||||
dict_type: result.dict_type,
|
||||
css_class: result.css_class,
|
||||
list_class: result.list_class,
|
||||
is_default: result.is_default,
|
||||
}
|
||||
return dict_data;
|
||||
}
|
||||
},
|
||||
persist: true
|
||||
})
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
store.use(piniaPluginPersistedstate)
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export * from "./modules/app.store";
|
||||
export * from "./modules/permission.store";
|
||||
export * from "./modules/settings.store";
|
||||
export * from "./modules/tags-view.store";
|
||||
export * from "./modules/user.store";
|
||||
export * from "./modules/dict.store";
|
||||
export * from "./modules/lock.store";
|
||||
export * from "./modules/config.store";
|
||||
export * from "./modules/notice.store";
|
||||
export { store };
|
||||
@@ -0,0 +1,107 @@
|
||||
import { defaultSettings } from "@/settings";
|
||||
|
||||
// 导入 Element Plus 中英文语言包
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
import en from "element-plus/es/locale/lang/en";
|
||||
import { store } from "@/store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import { SidebarStatus } from "@/enums/settings/layout.enum";
|
||||
|
||||
export const useAppStore = defineStore("app", () => {
|
||||
// 设备类型
|
||||
const device = useStorage("device", DeviceEnum.DESKTOP);
|
||||
// 布局大小
|
||||
const size = useStorage("size", defaultSettings.size);
|
||||
// 语言
|
||||
const language = useStorage("language", defaultSettings.language);
|
||||
// 侧边栏状态
|
||||
const sidebarStatus = useStorage("sidebarStatus", SidebarStatus.CLOSED);
|
||||
const sidebar = reactive({
|
||||
opened: sidebarStatus.value === SidebarStatus.OPENED,
|
||||
withoutAnimation: false,
|
||||
});
|
||||
|
||||
// 顶部菜单激活路径
|
||||
const activeTopMenuPath = useStorage("activeTopMenuPath", "");
|
||||
|
||||
/**
|
||||
* 根据语言标识读取对应的语言包
|
||||
*/
|
||||
const locale = computed(() => {
|
||||
if (language?.value == "en") {
|
||||
return en;
|
||||
} else {
|
||||
return zhCn;
|
||||
}
|
||||
});
|
||||
|
||||
// 切换侧边栏
|
||||
function toggleSidebar() {
|
||||
sidebar.opened = !sidebar.opened;
|
||||
sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED;
|
||||
}
|
||||
|
||||
// 关闭侧边栏
|
||||
function closeSideBar() {
|
||||
sidebar.opened = false;
|
||||
sidebarStatus.value = SidebarStatus.CLOSED;
|
||||
}
|
||||
|
||||
// 打开侧边栏
|
||||
function openSideBar() {
|
||||
sidebar.opened = true;
|
||||
sidebarStatus.value = SidebarStatus.OPENED;
|
||||
}
|
||||
|
||||
// 切换设备
|
||||
function toggleDevice(val: string) {
|
||||
device.value = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改变布局大小
|
||||
*
|
||||
* @param val 布局大小 default | small | large
|
||||
*/
|
||||
function changeSize(val: string) {
|
||||
size.value = val;
|
||||
}
|
||||
/**
|
||||
* 切换语言
|
||||
*
|
||||
* @param val
|
||||
*/
|
||||
function changeLanguage(val: string) {
|
||||
language.value = val;
|
||||
}
|
||||
/**
|
||||
* 混合模式顶部切换
|
||||
*/
|
||||
function activeTopMenu(val: string) {
|
||||
activeTopMenuPath.value = val;
|
||||
}
|
||||
return {
|
||||
device,
|
||||
sidebar,
|
||||
language,
|
||||
locale,
|
||||
size,
|
||||
activeTopMenu,
|
||||
toggleDevice,
|
||||
changeSize,
|
||||
changeLanguage,
|
||||
toggleSidebar,
|
||||
closeSideBar,
|
||||
openSideBar,
|
||||
activeTopMenuPath,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
|
||||
* 官方文档解释了如何在组件外部使用 Pinia Store:
|
||||
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
|
||||
*/
|
||||
export function useAppStoreHook() {
|
||||
return useAppStore(store);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { store } from "@/store";
|
||||
import ConfigAPI, { ConfigTable } from "@/api/system/config";
|
||||
|
||||
interface ConfigState {
|
||||
sys_git_code: ConfigTable;
|
||||
sys_help_doc: ConfigTable;
|
||||
sys_keep_record: ConfigTable;
|
||||
sys_login_background: ConfigTable;
|
||||
sys_web_clause: ConfigTable;
|
||||
sys_web_copyright: ConfigTable;
|
||||
sys_web_description: ConfigTable;
|
||||
sys_web_favicon: ConfigTable;
|
||||
sys_web_logo: ConfigTable;
|
||||
sys_web_privacy: ConfigTable;
|
||||
sys_web_title: ConfigTable;
|
||||
sys_web_version: ConfigTable;
|
||||
}
|
||||
|
||||
export const useConfigStore = defineStore("config", {
|
||||
state: () => ({
|
||||
configData: {} as ConfigState, // 存储系统配置
|
||||
isConfigLoaded: false, // 标记配置是否已加载
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async getConfig() {
|
||||
const response = await ConfigAPI.getInitConfig();
|
||||
response.data.data.forEach((item: ConfigTable) => {
|
||||
// 确保所有配置项都正确映射到 configData
|
||||
if (item.config_value !== undefined) {
|
||||
this.configData[item.config_key as keyof ConfigState] = item;
|
||||
}
|
||||
});
|
||||
this.isConfigLoaded = true;
|
||||
}
|
||||
},
|
||||
persist: true
|
||||
});
|
||||
|
||||
export function useConfigStoreHook() {
|
||||
return useConfigStore(store);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { store } from "@/store";
|
||||
import DictAPI, { DictDataTable } from "@/api/system/dict";
|
||||
|
||||
export const useDictStore = defineStore("dict", {
|
||||
state: () => ({
|
||||
dictData: {} as Record<string, DictDataTable[]>,
|
||||
isLoaded: false,
|
||||
}),
|
||||
getters: {
|
||||
getDictData(): Record<string, DictDataTable[]> {
|
||||
return this.dictData;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 批量获取字典数据
|
||||
async getDict(types: string[]) {
|
||||
try {
|
||||
for (const type of types) {
|
||||
const response = await DictAPI.getInitDict(type);
|
||||
this.dictData[type] = response.data.data as unknown as DictDataTable[];
|
||||
this.isLoaded = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取字典数据失败", error);
|
||||
}
|
||||
},
|
||||
|
||||
getDictLabel(type: string, value: string) {
|
||||
const result = this.dictData[type].find((item) => item.dict_value === value);
|
||||
if (!result) {
|
||||
return value;
|
||||
}
|
||||
const dict_data = {
|
||||
id: result.id,
|
||||
dict_value: result.dict_value,
|
||||
dict_label: result.dict_label,
|
||||
dict_type: result.dict_type,
|
||||
css_class: result.css_class,
|
||||
list_class: result.list_class,
|
||||
is_default: result.is_default,
|
||||
};
|
||||
return dict_data;
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
});
|
||||
|
||||
export function useDictStoreHook() {
|
||||
return useDictStore(store);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '../index'
|
||||
|
||||
interface lockInfo {
|
||||
isLock?: boolean
|
||||
password?: string
|
||||
}
|
||||
|
||||
interface LockState {
|
||||
lockInfo: lockInfo
|
||||
}
|
||||
|
||||
export const useLockStore = defineStore('lock', {
|
||||
state: (): LockState => {
|
||||
return {
|
||||
lockInfo: {
|
||||
// isLock: false, // 是否锁定屏幕
|
||||
// password: '' // 锁屏密码
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getLockInfo(): lockInfo {
|
||||
return this.lockInfo
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setLockInfo(lockInfo: lockInfo) {
|
||||
this.lockInfo = lockInfo
|
||||
},
|
||||
resetLockInfo() {
|
||||
this.lockInfo = {}
|
||||
},
|
||||
unLock(password: string) {
|
||||
if (this.lockInfo?.password === password) {
|
||||
this.resetLockInfo()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
persist: true
|
||||
})
|
||||
|
||||
export const useLockStoreWithOut = () => {
|
||||
return useLockStore(store)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { store } from "@/store";
|
||||
import NoticeAPI, { NoticeTable } from "@/api/system/notice";
|
||||
|
||||
|
||||
export const useNoticeStore = defineStore("notice", {
|
||||
state: () => ({
|
||||
noticeList: [] as NoticeTable[], // 存储系统配置
|
||||
total: 0, // 存储系统配置
|
||||
isNoticeLoaded: false, // 标记配置是否已加载
|
||||
}),
|
||||
actions: {
|
||||
async getNotice() {
|
||||
const response = await NoticeAPI.getNoticeListAvailable();
|
||||
this.noticeList = response.data.data.items;
|
||||
this.total = response.data.data.total;
|
||||
this.isNoticeLoaded = true;
|
||||
},
|
||||
clearUserInfo() {
|
||||
this.noticeList = [];
|
||||
this.total = 0;
|
||||
this.isNoticeLoaded = false;
|
||||
},
|
||||
},
|
||||
persist: true
|
||||
});
|
||||
|
||||
export function useNoticeStoreHook() {
|
||||
return useNoticeStore(store);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import router, { constantRoutes } from "@/router";
|
||||
import { store, useUserStore } from "@/store";
|
||||
import { listToTree } from "@/utils/common";
|
||||
import { MenuTable } from "@/api/system/menu";
|
||||
|
||||
const modules = import.meta.glob("../../views/**/**.vue");
|
||||
const Layout = () => import("@/layouts/index.vue");
|
||||
|
||||
|
||||
export interface Meta {
|
||||
/** 【目录】只有一个子路由是否始终显示 */
|
||||
alwaysShow?: boolean;
|
||||
/** 是否隐藏(true-是 false-否) */
|
||||
hidden?: boolean;
|
||||
/** ICON */
|
||||
icon?: string;
|
||||
/** 【菜单】是否开启页面缓存 */
|
||||
keepAlive?: boolean;
|
||||
/** 路由title */
|
||||
title?: string;
|
||||
/** 排序 */
|
||||
order?: number;
|
||||
/** 参数 */
|
||||
params?: string;
|
||||
/** 是否固定路由 */
|
||||
affix?: boolean;
|
||||
}
|
||||
|
||||
// 修改 component 类型,使其能接受动态导入函数
|
||||
export interface RouteVO {
|
||||
/** 子路由列表 */
|
||||
children: RouteVO[];
|
||||
/** 组件路径或组件导入函数 */
|
||||
component?: string | (() => Promise<Component>);
|
||||
/** 路由属性 */
|
||||
meta?: Meta;
|
||||
/** 路由名称 */
|
||||
name?: string;
|
||||
/** 路由路径 */
|
||||
path?: string;
|
||||
/** 跳转链接 */
|
||||
redirect?: string;
|
||||
}
|
||||
|
||||
export const generator = (routers: MenuTable[]): RouteVO[] => {
|
||||
return routers.map((item) => {
|
||||
const currentRouter: RouteVO = {
|
||||
children: [],
|
||||
path: item.route_path,
|
||||
name: item.route_name,
|
||||
component: item.component_path,
|
||||
redirect: item.redirect,
|
||||
meta: {
|
||||
title: item.title,
|
||||
icon: item.icon || undefined,
|
||||
keepAlive: item.keep_alive,
|
||||
hidden: item.hidden,
|
||||
order: item.order,
|
||||
alwaysShow: item.always_show,
|
||||
params: item.params,
|
||||
affix: item.affix,
|
||||
},
|
||||
};
|
||||
if (item.children && item.children.length > 0) {
|
||||
currentRouter.children = item.children ? generator(item.children) : [];
|
||||
}
|
||||
return currentRouter;
|
||||
});
|
||||
};
|
||||
|
||||
// router.beforeEach(async (to) => {
|
||||
// const accessToken = Auth.getAccessToken();
|
||||
|
||||
// const userStore = useUserStore();
|
||||
|
||||
// if (!accessToken && to.name !== "Login") {
|
||||
// return { name: "Login" };
|
||||
// } else if (accessToken && to.name === "Login") {
|
||||
// return { name: "Index" };
|
||||
// } else if (accessToken && !userStore.hasGetRoute) {
|
||||
// await userStore.getUserInfo();
|
||||
// const routersTree = listToTree(userStore.routeList);
|
||||
// const routerMap = generator(routersTree);
|
||||
// constantRoutes[0].children = [...(constantRoutes[0].children || []), ...routerMap].filter(Boolean) as RouteRecordRaw[];
|
||||
// router.addRoute(constantRoutes[0]);
|
||||
// return to.fullPath;
|
||||
// }
|
||||
// });
|
||||
|
||||
export const usePermissionStore = defineStore("permission", () => {
|
||||
// 存储所有路由,包括静态路由和动态路由
|
||||
const routes = ref<RouteRecordRaw[]>([]);
|
||||
// 混合模式左侧菜单路由
|
||||
const sideMenuRoutes = ref<RouteRecordRaw[]>([]);
|
||||
// 路由是否加载完成
|
||||
const routesLoaded = ref(false);
|
||||
|
||||
/**
|
||||
* 获取后台动态路由数据,解析并注册到全局路由
|
||||
*
|
||||
* @returns Promise<RouteRecordRaw[]> 解析后的动态路由列表
|
||||
*/
|
||||
async function generateRoutes() {
|
||||
try {
|
||||
const userStore = useUserStore();
|
||||
// 确保获取用户信息和路由列表
|
||||
if (!userStore.hasGetRoute) {
|
||||
await userStore.getUserInfo();
|
||||
}
|
||||
|
||||
const routersTree = listToTree(userStore.routeList);
|
||||
const routerMap = generator(routersTree);
|
||||
|
||||
// 从用户绑定的路由列表获取数据
|
||||
const dynamicRoutes = parseDynamicRoutes(routerMap);
|
||||
|
||||
// 重置路由,移除旧的动态路由
|
||||
resetRouter();
|
||||
|
||||
// 重新添加所有路由
|
||||
constantRoutes.forEach(route => {
|
||||
router.addRoute(route);
|
||||
});
|
||||
|
||||
routes.value = [...constantRoutes, ...dynamicRoutes];
|
||||
|
||||
routesLoaded.value = true;
|
||||
|
||||
console.log("✅ 动态路由:", (dynamicRoutes));
|
||||
return dynamicRoutes;
|
||||
} catch (error) {
|
||||
console.error("❌ 生成路由失败:", error);
|
||||
|
||||
// 即使失败也要设置状态,避免无限重试
|
||||
routesLoaded.value = false;
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父菜单路径设置侧边菜单
|
||||
*
|
||||
* @param parentPath 父菜单的路径,用于查找对应的菜单项
|
||||
*/
|
||||
const updateSideMenu = (parentPath: string) => {
|
||||
const matchedItem = routes.value.find((item) => item.path === parentPath);
|
||||
if (matchedItem && matchedItem.children) {
|
||||
sideMenuRoutes.value = matchedItem.children;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置路由
|
||||
*/
|
||||
const resetRouter = () => {
|
||||
// 创建常量路由名称集合,用于O(1)时间复杂度的查找
|
||||
const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean));
|
||||
|
||||
// 从 router 实例中移除动态路由
|
||||
routes.value.forEach((route) => {
|
||||
if (route.name && !constantRouteNames.has(route.name)) {
|
||||
router.removeRoute(route.name);
|
||||
}
|
||||
});
|
||||
|
||||
// 重置为仅包含常量路由
|
||||
routes.value = [...constantRoutes];
|
||||
sideMenuRoutes.value = [];
|
||||
routesLoaded.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
routes,
|
||||
sideMenuRoutes,
|
||||
routesLoaded,
|
||||
generateRoutes,
|
||||
updateSideMenu,
|
||||
resetRouter,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 解析后端返回的路由数据并转换为 Vue Router 兼容的路由配置
|
||||
*
|
||||
* @param rawRoutes 后端返回的原始路由数据
|
||||
* @returns 解析后的路由集合
|
||||
*/
|
||||
const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
|
||||
const parsedRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
rawRoutes.forEach((route) => {
|
||||
const normalizedRoute = { ...route } as RouteRecordRaw;
|
||||
// http://localhost:5180/#/dashboard/workplace
|
||||
// http://localhost:5180/#/dashboard/workplace
|
||||
|
||||
// 处理组件路径
|
||||
normalizedRoute.component =
|
||||
!normalizedRoute.component
|
||||
? Layout
|
||||
: modules[`../../views/${normalizedRoute.component}.vue`] ||
|
||||
modules["../../views/error/404.vue"];
|
||||
|
||||
// 递归解析子路由
|
||||
if (normalizedRoute.children) {
|
||||
normalizedRoute.children = parseDynamicRoutes(route.children);
|
||||
}
|
||||
|
||||
parsedRoutes.push(normalizedRoute);
|
||||
});
|
||||
|
||||
return parsedRoutes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出此hook函数用于在非组件环境(如其他store、工具函数等)中获取权限store实例
|
||||
*
|
||||
* 在组件中可直接使用usePermissionStore(),但在组件外部需要传入store实例
|
||||
* 此函数简化了这个过程,避免每次都手动传入store参数
|
||||
*/
|
||||
export function usePermissionStoreHook() {
|
||||
return usePermissionStore(store);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { defaultSettings } from "@/settings";
|
||||
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
|
||||
import type { LayoutMode } from "@/enums/settings/layout.enum";
|
||||
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
|
||||
import { SETTINGS_KEYS } from "@/constants";
|
||||
|
||||
// 🎯 设置项类型定义
|
||||
interface SettingsState {
|
||||
// 界面显示设置
|
||||
settingsVisible: boolean;
|
||||
showTagsView: boolean;
|
||||
showAppLogo: boolean;
|
||||
showWatermark: boolean;
|
||||
|
||||
// 布局设置
|
||||
layout: LayoutMode;
|
||||
sidebarColorScheme: string;
|
||||
|
||||
// 主题设置
|
||||
theme: ThemeMode;
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
// 🎯 可变更的设置项类型
|
||||
type MutableSetting = Exclude<keyof SettingsState, "settingsVisible">;
|
||||
type SettingValue<K extends MutableSetting> = SettingsState[K];
|
||||
|
||||
export const useSettingsStore = defineStore("setting", () => {
|
||||
// 🎯 基础设置 - 非持久化
|
||||
const settingsVisible = ref<boolean>(false);
|
||||
|
||||
// 🎯 持久化设置 - 使用分组常量
|
||||
const showTagsView = useStorage<boolean>(
|
||||
SETTINGS_KEYS.SHOW_TAGS_VIEW,
|
||||
defaultSettings.showTagsView
|
||||
);
|
||||
|
||||
const showAppLogo = useStorage<boolean>(SETTINGS_KEYS.SHOW_APP_LOGO, defaultSettings.showAppLogo);
|
||||
|
||||
const showWatermark = useStorage<boolean>(
|
||||
SETTINGS_KEYS.SHOW_WATERMARK,
|
||||
defaultSettings.showWatermark
|
||||
);
|
||||
|
||||
const sidebarColorScheme = useStorage<string>(
|
||||
SETTINGS_KEYS.SIDEBAR_COLOR_SCHEME,
|
||||
defaultSettings.sidebarColorScheme
|
||||
);
|
||||
|
||||
const layout = useStorage<LayoutMode>(SETTINGS_KEYS.LAYOUT, defaultSettings.layout as LayoutMode);
|
||||
|
||||
const themeColor = useStorage<string>(SETTINGS_KEYS.THEME_COLOR, defaultSettings.themeColor);
|
||||
|
||||
const theme = useStorage<ThemeMode>(SETTINGS_KEYS.THEME, defaultSettings.theme);
|
||||
|
||||
// 🎯 设置项映射
|
||||
const settingsMap = {
|
||||
showTagsView,
|
||||
showAppLogo,
|
||||
showWatermark,
|
||||
sidebarColorScheme,
|
||||
layout,
|
||||
} as const;
|
||||
|
||||
// 🎯 监听器 - 主题变化
|
||||
watch(
|
||||
[theme, themeColor],
|
||||
([newTheme, newThemeColor]) => {
|
||||
toggleDarkMode(newTheme === ThemeMode.DARK);
|
||||
const colors = generateThemeColors(newThemeColor, newTheme);
|
||||
applyTheme(colors);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 🎯 监听器 - 侧边栏配色方案变化
|
||||
watch(
|
||||
[sidebarColorScheme],
|
||||
([newSidebarColorScheme]) => {
|
||||
toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 🎯 统一的设置更新方法 - 类型安全
|
||||
function updateSetting<K extends keyof typeof settingsMap>(key: K, value: SettingValue<K>): void {
|
||||
const setting = settingsMap[key];
|
||||
if (setting) {
|
||||
(setting as Ref<any>).value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 主题相关的专用更新方法
|
||||
function updateTheme(newTheme: ThemeMode): void {
|
||||
theme.value = newTheme;
|
||||
}
|
||||
|
||||
function updateThemeColor(newColor: string): void {
|
||||
themeColor.value = newColor;
|
||||
}
|
||||
|
||||
function updateSidebarColorScheme(newScheme: string): void {
|
||||
sidebarColorScheme.value = newScheme;
|
||||
}
|
||||
|
||||
function updateLayout(newLayout: LayoutMode): void {
|
||||
layout.value = newLayout;
|
||||
}
|
||||
|
||||
// 🎯 设置面板显示控制
|
||||
function toggleSettingsPanel(): void {
|
||||
settingsVisible.value = !settingsVisible.value;
|
||||
}
|
||||
|
||||
function showSettingsPanel(): void {
|
||||
settingsVisible.value = true;
|
||||
}
|
||||
|
||||
function hideSettingsPanel(): void {
|
||||
settingsVisible.value = false;
|
||||
}
|
||||
|
||||
// 🎯 批量重置设置
|
||||
function resetSettings(): void {
|
||||
showTagsView.value = defaultSettings.showTagsView;
|
||||
showAppLogo.value = defaultSettings.showAppLogo;
|
||||
showWatermark.value = defaultSettings.showWatermark;
|
||||
sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
|
||||
layout.value = defaultSettings.layout as LayoutMode;
|
||||
themeColor.value = defaultSettings.themeColor;
|
||||
theme.value = defaultSettings.theme;
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
settingsVisible,
|
||||
showTagsView,
|
||||
showAppLogo,
|
||||
showWatermark,
|
||||
sidebarColorScheme,
|
||||
layout,
|
||||
themeColor,
|
||||
theme,
|
||||
|
||||
// 更新方法
|
||||
updateSetting,
|
||||
updateTheme,
|
||||
updateThemeColor,
|
||||
updateSidebarColorScheme,
|
||||
updateLayout,
|
||||
|
||||
// 面板控制
|
||||
toggleSettingsPanel,
|
||||
showSettingsPanel,
|
||||
hideSettingsPanel,
|
||||
|
||||
// 重置功能
|
||||
resetSettings,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
export const useTagsViewStore = defineStore("tagsView", () => {
|
||||
const visitedViews = ref<TagView[]>([]);
|
||||
const cachedViews = ref<string[]>([]);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
/**
|
||||
* 添加已访问视图到已访问视图列表中
|
||||
*/
|
||||
function addVisitedView(view: TagView) {
|
||||
// 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加
|
||||
if (view.path.startsWith("/redirect")) {
|
||||
return;
|
||||
}
|
||||
if (visitedViews.value.some((v) => v.name === view.name)) {
|
||||
return;
|
||||
}
|
||||
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
|
||||
if (view.affix) {
|
||||
visitedViews.value.unshift(view);
|
||||
} else {
|
||||
// 如果视图不是固定的,则在已访问的视图列表的末尾添加
|
||||
visitedViews.value.push(view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加缓存视图到缓存视图列表中
|
||||
*/
|
||||
function addCachedView(view: TagView) {
|
||||
const viewName = view.name;
|
||||
// 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
|
||||
if (cachedViews.value.includes(viewName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
|
||||
if (view.keepAlive) {
|
||||
cachedViews.value.push(viewName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已访问视图列表中删除指定的视图
|
||||
*/
|
||||
function delVisitedView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
for (const [i, v] of visitedViews.value.entries()) {
|
||||
// 找到与指定视图路径匹配的视图,在已访问视图列表中删除该视图
|
||||
if (v.path === view.path) {
|
||||
visitedViews.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delCachedView(view: TagView) {
|
||||
const viewName = view.name;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
if (index > -1) {
|
||||
cachedViews.value.splice(index, 1);
|
||||
}
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
function delOtherVisitedViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
visitedViews.value = visitedViews.value.filter((v) => {
|
||||
return v?.affix || v.path === view.path;
|
||||
});
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherCachedViews(view: TagView) {
|
||||
const viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
const index = cachedViews.value.indexOf(viewName);
|
||||
if (index > -1) {
|
||||
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||
} else {
|
||||
// if index = -1, there is no cached tags
|
||||
cachedViews.value = [];
|
||||
}
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisitedView(view: TagView) {
|
||||
for (let v of visitedViews.value) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addView(view: TagView) {
|
||||
addVisitedView(view);
|
||||
addCachedView(view);
|
||||
}
|
||||
|
||||
function delView(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delVisitedView(view);
|
||||
delCachedView(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delOtherViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
delOtherVisitedViews(view);
|
||||
delOtherCachedViews(view);
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delLeftViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
if (index >= currIndex || item?.affix) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheIndex = cachedViews.value.indexOf(item.name);
|
||||
if (cacheIndex > -1) {
|
||||
cachedViews.value.splice(cacheIndex, 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delRightViews(view: TagView) {
|
||||
return new Promise((resolve) => {
|
||||
const currIndex = visitedViews.value.findIndex((v) => v.path === view.path);
|
||||
if (currIndex === -1) {
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item, index) => {
|
||||
if (index <= currIndex || item?.affix) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
cachedViews.value = [];
|
||||
resolve({
|
||||
visitedViews: [...visitedViews.value],
|
||||
cachedViews: [...cachedViews.value],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAllVisitedViews() {
|
||||
return new Promise((resolve) => {
|
||||
const affixTags = visitedViews.value.filter((tag) => tag?.affix);
|
||||
visitedViews.value = affixTags;
|
||||
resolve([...visitedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
function delAllCachedViews() {
|
||||
return new Promise((resolve) => {
|
||||
cachedViews.value = [];
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前tagView
|
||||
*/
|
||||
function closeCurrentView() {
|
||||
const tags: TagView = {
|
||||
name: route.name as string,
|
||||
title: route.meta.title as string,
|
||||
path: route.path,
|
||||
fullPath: route.fullPath,
|
||||
affix: route.meta?.affix,
|
||||
keepAlive: route.meta?.keepAlive,
|
||||
query: route.query,
|
||||
};
|
||||
delView(tags).then((res: any) => {
|
||||
if (isActive(tags)) {
|
||||
toLastView(res.visitedViews, tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(tag: TagView) {
|
||||
return tag.path === route.path;
|
||||
}
|
||||
|
||||
function toLastView(visitedViews: TagView[], view?: TagView) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView && latestView.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view?.name === "Dashboard") {
|
||||
// to reload home page
|
||||
router.replace("/redirect" + view.fullPath);
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visitedViews,
|
||||
cachedViews,
|
||||
addVisitedView,
|
||||
addCachedView,
|
||||
delVisitedView,
|
||||
delCachedView,
|
||||
delOtherVisitedViews,
|
||||
delOtherCachedViews,
|
||||
updateVisitedView,
|
||||
addView,
|
||||
delView,
|
||||
delOtherViews,
|
||||
delLeftViews,
|
||||
delRightViews,
|
||||
delAllViews,
|
||||
delAllVisitedViews,
|
||||
delAllCachedViews,
|
||||
closeCurrentView,
|
||||
isActive,
|
||||
toLastView,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { store, useTagsViewStore, usePermissionStoreHook } from "@/store";
|
||||
|
||||
import AuthAPI, {type LoginFormData } from "@/api/system/auth";
|
||||
import UserAPI, {type UserInfo } from "@/api/system/user";
|
||||
import type { MenuTable } from "@/api/system/menu";
|
||||
|
||||
import { Auth } from "@/utils/auth";
|
||||
|
||||
export const useUserStore = defineStore("user", {
|
||||
state: () => ({
|
||||
basicInfo: {} as UserInfo,
|
||||
routeList: [] as MenuTable[],
|
||||
hasGetRoute: false,
|
||||
|
||||
// 记住我状态
|
||||
rememberMe: ref(Auth.getRememberMe()),
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getBasicInfo: (state) => state.basicInfo,
|
||||
getRouteList: (state) => state.routeList,
|
||||
getHasGetRoute: (state) => state.hasGetRoute,
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 获取用户信息
|
||||
async getUserInfo() {
|
||||
const response = await UserAPI.getCurrentUserInfo();
|
||||
const routers = response.data.data.menus;
|
||||
delete response.data.data.menus;
|
||||
this.setRoute(routers);
|
||||
this.basicInfo = { ...this.basicInfo, ...response.data.data };
|
||||
},
|
||||
|
||||
setRoute(routers: any) {
|
||||
this.routeList = routers;
|
||||
this.hasGetRoute = true;
|
||||
},
|
||||
setAvatar(avatar: string) {
|
||||
this.basicInfo = { ...this.basicInfo, avatar };
|
||||
},
|
||||
clearUserInfo() {
|
||||
this.basicInfo = {};
|
||||
this.routeList = [];
|
||||
this.hasGetRoute = false;
|
||||
},
|
||||
|
||||
// 登录
|
||||
login(LoginFormData: LoginFormData) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
AuthAPI.login(LoginFormData)
|
||||
.then((response) => {
|
||||
// 保存记住我状态和token
|
||||
this.rememberMe = LoginFormData.remember;
|
||||
Auth.setTokens(response.data.data.access_token, response.data.data.refresh_token, this.rememberMe);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
AuthAPI.logout({token: Auth.getAccessToken()})
|
||||
.then(() => {
|
||||
// 重置所有系统状态
|
||||
this.resetAllState();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 重置所有状态
|
||||
resetAllState() {
|
||||
// 重置用户状态
|
||||
Auth.clearAuth();
|
||||
// 重置路由
|
||||
usePermissionStoreHook().resetRouter();
|
||||
// 清除标签视图
|
||||
useTagsViewStore().delAllViews();
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
// 刷新token
|
||||
refreshToken() {
|
||||
const refreshToken = Auth.getRefreshToken();
|
||||
|
||||
if (!refreshToken) {
|
||||
return Promise.reject(new Error("没有有效的刷新令牌"));
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
AuthAPI.refreshToken({refresh_token: refreshToken})
|
||||
.then((data) => {
|
||||
// 更新令牌,保持当前记住我状态
|
||||
Auth.setTokens(data.access_token, data.refresh_token, Auth.getRememberMe());
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(" refreshToken 刷新失败", error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
persist: true
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 在组件外部使用UserStore的钩子函数
|
||||
* @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html
|
||||
*/
|
||||
export function useUserStoreHook() {
|
||||
return useUserStore(store);
|
||||
}
|
||||
Reference in New Issue
Block a user