first commiit

This commit is contained in:
2025-02-08 10:15:06 +08:00
parent 6815bd083b
commit 262bf41379
242 changed files with 19959 additions and 1 deletions

107
src/store/modules/app.ts Normal file
View File

@@ -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/DeviceEnum";
import { SidebarStatusEnum } from "@/enums/SidebarStatusEnum";
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", SidebarStatusEnum.CLOSED);
const sidebar = reactive({
opened: sidebarStatus.value === SidebarStatusEnum.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 ? SidebarStatusEnum.OPENED : SidebarStatusEnum.CLOSED;
}
// 关闭侧边栏
function closeSideBar() {
sidebar.opened = false;
sidebarStatus.value = SidebarStatusEnum.CLOSED;
}
// 打开侧边栏
function openSideBar() {
sidebar.opened = true;
sidebarStatus.value = SidebarStatusEnum.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);
}

41
src/store/modules/dict.ts Normal file
View File

@@ -0,0 +1,41 @@
import { store } from "@/store";
import DictionaryAPI, { type DictVO, type DictData } from "@/api/system/dict";
export const useDictStore = defineStore("dict", () => {
const dictionary = useStorage<Record<string, DictData[]>>("dictionary", {});
const setDictionary = (dict: DictVO) => {
dictionary.value[dict.dictCode] = dict.dictDataList;
};
const loadDictionaries = async () => {
const dictList = await DictionaryAPI.getList();
dictList.forEach(setDictionary);
};
const getDictionary = (dictCode: string): DictData[] => {
return dictionary.value[dictCode] || [];
};
const clearDictionaryCache = () => {
dictionary.value = {};
};
const updateDictionaryCache = async () => {
clearDictionaryCache(); // 先清除旧缓存
await loadDictionaries(); // 重新加载最新字典数据
};
return {
dictionary,
setDictionary,
loadDictionaries,
getDictionary,
clearDictionaryCache,
updateDictionaryCache,
};
});
export function useDictStoreHook() {
return useDictStore(store);
}

View File

@@ -0,0 +1,116 @@
import type { RouteRecordRaw } from "vue-router";
import { constantRoutes } from "@/router";
import { store } from "@/store";
import router from "@/router";
import MenuAPI, { type RouteVO } from "@/api/system/menu";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
export const usePermissionStore = defineStore("permission", () => {
// 储所有路由,包括静态路由和动态路由
const routes = ref<RouteRecordRaw[]>(constantRoutes);
console.log(router);
// 混合模式左侧菜单路由
const mixedLayoutLeftRoutes = ref<RouteRecordRaw[]>([]);
// 路由是否加载完成
const isRoutesLoaded = ref(false);
/**
* 获取后台动态路由数据,解析并注册到全局路由
*
* @returns Promise<RouteRecordRaw[]> 解析后的动态路由列表
*/
function generateRoutes() {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
MenuAPI.getRoutes()
.then((data) => {
const dynamicRoutes = parseDynamicRoutes(data);
routes.value = [...constantRoutes, ...dynamicRoutes];
isRoutesLoaded.value = true;
resolve(dynamicRoutes);
})
.catch((error) => {
reject(error);
});
});
}
/**
* 根据父菜单路径设置混合模式左侧菜单
*
* @param parentPath 父菜单的路径,用于查找对应的菜单项
*/
const setMixedLayoutLeftRoutes = (parentPath: string) => {
const matchedItem = routes.value.find((item) => item.path === parentPath);
if (matchedItem && matchedItem.children) {
mixedLayoutLeftRoutes.value = matchedItem.children;
}
};
/**
* 重置路由
*/
const resetRouter = () => {
// 清空本地存储的路由和菜单数据
routes.value = [];
mixedLayoutLeftRoutes.value = [];
// 从 Vue Router 中移除所有动态注册的路由
router.getRoutes().forEach((route) => {
if (route.name) {
router.removeRoute(route.name);
}
});
isRoutesLoaded.value = false;
};
return {
routes,
mixedLayoutLeftRoutes,
isRoutesLoaded,
generateRoutes,
setMixedLayoutLeftRoutes,
resetRouter,
};
});
/**
* 解析后端返回的路由数据并转换为 Vue Router 兼容的路由配置
*
* 1. 遍历 `rawRoutes` 并转换为 `RouteRecordRaw` 格式。
* 2. 若 `component` 为 `"Layout"`,则替换为 `Layout` 组件。
* 3. 若 `component` 为字符串路径,则动态加载对应的 Vue 组件,找不到则默认 `404.vue`。
* 4. 递归解析 `children`,确保子路由也被正确转换。
*
* @param rawRoutes 后端返回的原始路由数据
* @returns 解析后的路由配置数组
*/
const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
const parsedRoutes: RouteRecordRaw[] = [];
rawRoutes.forEach((route) => {
const normalizedRoute = { ...route } as RouteRecordRaw;
// 处理组件路径
normalizedRoute.component =
normalizedRoute.component?.toString() === "Layout"
? Layout
: modules[`../../views/${normalizedRoute.component}.vue`] ||
modules["../../views/error-page/404.vue"];
// 递归解析子路由
if (normalizedRoute.children) {
normalizedRoute.children = parseDynamicRoutes(route.children);
}
parsedRoutes.push(normalizedRoute);
});
return parsedRoutes;
};
/**
* 在组件外使用 Pinia store 实例 @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html
*/
export function usePermissionStoreHook() {
return usePermissionStore(store);
}

View File

@@ -0,0 +1,75 @@
import defaultSettings from "@/settings";
import { ThemeEnum } from "@/enums/ThemeEnum";
import { generateThemeColors, applyTheme, toggleDarkMode } from "@/utils/theme";
type SettingsValue = boolean | string;
export const useSettingsStore = defineStore("setting", () => {
// 基本设置
const settingsVisible = ref(false);
// 标签
const tagsView = useStorage<boolean>("tagsView", defaultSettings.tagsView);
// 侧边栏 Logo
const sidebarLogo = useStorage<boolean>("sidebarLogo", defaultSettings.sidebarLogo);
// 布局
const layout = useStorage<string>("layout", defaultSettings.layout);
// 水印
const watermarkEnabled = useStorage<boolean>(
"watermarkEnabled",
defaultSettings.watermarkEnabled
);
// 主题
const themeColor = useStorage<string>("themeColor", defaultSettings.themeColor);
const theme = useStorage<string>("theme", defaultSettings.theme);
// 监听主题变化
watch(
[theme, themeColor],
([newTheme, newThemeColor]) => {
toggleDarkMode(newTheme === ThemeEnum.DARK);
const colors = generateThemeColors(newThemeColor);
applyTheme(colors);
},
{ immediate: true }
);
// 设置映射
const settingsMap: Record<string, Ref<SettingsValue>> = {
tagsView,
sidebarLogo,
layout,
watermarkEnabled,
};
function changeSetting({ key, value }: { key: string; value: SettingsValue }) {
const setting = settingsMap[key];
if (setting) setting.value = value;
}
function changeTheme(val: string) {
theme.value = val;
}
function changeThemeColor(color: string) {
themeColor.value = color;
}
function changeLayout(val: string) {
layout.value = val;
}
return {
settingsVisible,
tagsView,
sidebarLogo,
layout,
themeColor,
theme,
watermarkEnabled,
changeSetting,
changeTheme,
changeThemeColor,
changeLayout,
};
});

View File

@@ -0,0 +1,254 @@
export const useTagsViewStore = defineStore("tagsView", () => {
const visitedViews = ref<TagView[]>([]);
const cachedViews = ref<string[]>([]);
const router = useRouter();
const route = useRoute();
/**
* 添加已访问视图到已访问视图列表中
*/
function addVisitedView(view: TagView) {
// 如果已经存在于已访问的视图列表中,则不再添加
if (visitedViews.value.some((v) => v.path === view.path)) {
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,
};
});

123
src/store/modules/user.ts Normal file
View File

@@ -0,0 +1,123 @@
import { store } from "@/store";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { useDictStoreHook } from "@/store/modules/dict";
import AuthAPI, { type LoginFormData } from "@/api/auth";
import UserAPI, { type UserInfo } from "@/api/system/user";
import { setToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
export const useUserStore = defineStore("user", () => {
const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo);
/**
* 登录
*
* @param {LoginFormData}
* @returns
*/
function login(LoginFormData: LoginFormData) {
return new Promise<void>((resolve, reject) => {
AuthAPI.login(LoginFormData)
.then((data) => {
const { tokenType, accessToken, refreshToken } = data;
setToken(tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
setRefreshToken(refreshToken);
resolve();
})
.catch((error) => {
reject(error);
});
});
}
/**
* 获取用户信息
*
* @returns {UserInfo} 用户信息
*/
function getUserInfo() {
return new Promise<UserInfo>((resolve, reject) => {
UserAPI.getInfo()
.then((data) => {
if (!data) {
reject("Verification failed, please Login again.");
return;
}
Object.assign(userInfo.value, { ...data });
resolve(data);
})
.catch((error) => {
reject(error);
});
});
}
/**
* 登出
*/
function logout() {
return new Promise<void>((resolve, reject) => {
AuthAPI.logout()
.then(() => {
clearUserData();
resolve();
})
.catch((error) => {
reject(error);
});
});
}
/**
* 刷新 token
*/
function refreshToken() {
const refreshToken = getRefreshToken();
return new Promise<void>((resolve, reject) => {
AuthAPI.refreshToken(refreshToken)
.then((data) => {
const { tokenType, accessToken, refreshToken } = data;
setToken(tokenType + " " + accessToken);
setRefreshToken(refreshToken);
resolve();
})
.catch((error) => {
console.log(" refreshToken 刷新失败", error);
reject(error);
});
});
}
/**
* 清理用户数据
*
* @returns
*/
function clearUserData() {
return new Promise<void>((resolve) => {
clearToken();
usePermissionStoreHook().resetRouter();
useDictStoreHook().clearDictionaryCache();
resolve();
});
}
return {
userInfo,
getUserInfo,
login,
logout,
clearUserData,
refreshToken,
};
});
/**
* 用于在组件外部如在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 useUserStoreHook() {
return useUserStore(store);
}