This commit is contained in:
Ray.Hao
2025-12-27 10:47:21 +08:00
239 changed files with 16106 additions and 5516 deletions

View File

@@ -8,10 +8,11 @@ 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/app";
export * from "./modules/permission";
export * from "./modules/settings";
export * from "./modules/tags-view";
export * from "./modules/user";
export * from "./modules/dict";
export * from "./modules/tenant";
export { store };

View File

@@ -1,86 +1,54 @@
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";
import { DeviceEnum, SidebarStatus } from "@/enums";
import { STORAGE_KEYS } from "@/constants";
import { defaults } from "@/settings";
export const useAppStore = defineStore("app", () => {
// 设备类型
const device = useStorage(STORAGE_KEYS.DEVICE, DeviceEnum.DESKTOP);
// 布局大小
const size = useStorage(STORAGE_KEYS.SIZE, defaultSettings.size);
// 语言
const language = useStorage(STORAGE_KEYS.LANGUAGE, defaultSettings.language);
// 侧边栏状态
const size = useStorage(STORAGE_KEYS.SIZE, defaults.size);
const language = useStorage(STORAGE_KEYS.LANGUAGE, defaults.language);
const sidebarStatus = useStorage(STORAGE_KEYS.SIDEBAR_STATUS, SidebarStatus.CLOSED);
const sidebar = reactive({
opened: sidebarStatus.value === SidebarStatus.OPENED,
withoutAnimation: false,
});
// 顶部菜单激活路径
const activeTopMenuPath = useStorage(STORAGE_KEYS.ACTIVE_TOP_MENU_PATH, "");
/**
*
*/
const locale = computed(() => {
if (language?.value == "en") {
return en;
} else {
return zhCn;
}
});
const locale = computed(() => (language?.value === "en" ? en : 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,
@@ -98,11 +66,6 @@ export const useAppStore = defineStore("app", () => {
};
});
/**
* 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);
}

View File

@@ -1,5 +1,6 @@
import { store } from "@/store";
import DictAPI, { type DictItemOption } from "@/api/system/dict-api";
import DictAPI from "@/api/system/dict";
import type { DictItemOption } from "@/types/api";
import { STORAGE_KEYS } from "@/constants";
export const useDictStore = defineStore("dict", () => {

View File

@@ -3,7 +3,8 @@ import { constantRoutes } from "@/router";
import { store } from "@/store";
import router from "@/router";
import MenuAPI, { type RouteVO } from "@/api/system/menu-api";
import MenuAPI from "@/api/system/menu";
import { RouteVo } from "@/types";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("../../layouts/index.vue");
@@ -68,7 +69,7 @@ export const usePermissionStore = defineStore("permission", () => {
* Vue Router配置
* Layout层级嵌套
*/
const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => {
const transformRoutes = (routes: RouteVo[], isTopLevel: boolean = true): RouteRecordRaw[] => {
return routes.map((route) => {
const { component, children, ...args } = route;

View File

@@ -1,208 +0,0 @@
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 { STORAGE_KEYS } from "@/constants";
// 🎯 设置项类型定义
interface SettingsState {
// 界面显示设置
settingsVisible: boolean;
showTagsView: boolean;
showAppLogo: boolean;
showWatermark: boolean;
enableAiAssistant: 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>(
STORAGE_KEYS.SHOW_TAGS_VIEW,
defaultSettings.showTagsView
);
// 是否显示应用Logo
const showAppLogo = useStorage<boolean>(STORAGE_KEYS.SHOW_APP_LOGO, defaultSettings.showAppLogo);
// 是否显示水印
const showWatermark = useStorage<boolean>(
STORAGE_KEYS.SHOW_WATERMARK,
defaultSettings.showWatermark
);
// 是否启用 AI 助手
const enableAiAssistant = useStorage<boolean>(
STORAGE_KEYS.ENABLE_AI_ASSISTANT,
defaultSettings.enableAiAssistant
);
// 侧边栏配色方案
const sidebarColorScheme = useStorage<string>(
STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
defaultSettings.sidebarColorScheme
);
// 布局模式
const layout = useStorage<LayoutMode>(STORAGE_KEYS.LAYOUT, defaultSettings.layout as LayoutMode);
// 主题颜色
const themeColor = useStorage<string>(STORAGE_KEYS.THEME_COLOR, defaultSettings.themeColor);
// 主题模式(亮色/暗色)
const theme = useStorage<ThemeMode>(STORAGE_KEYS.THEME, defaultSettings.theme);
// 系统主题媒体查询(用于 ThemeMode.AUTO
const prefersDarkMedia =
typeof window !== "undefined" && typeof window.matchMedia === "function"
? window.matchMedia("(prefers-color-scheme: dark)")
: null;
function getSystemTheme(): ThemeMode {
return prefersDarkMedia && prefersDarkMedia.matches ? ThemeMode.DARK : ThemeMode.LIGHT;
}
function handleSystemThemeChange(e: MediaQueryListEvent | MediaQueryList) {
// 仅在用户选择自动(跟随系统)时响应系统主题变化
if (theme.value === ThemeMode.AUTO) {
const effectiveTheme = (e as MediaQueryList).matches ? ThemeMode.DARK : ThemeMode.LIGHT;
toggleDarkMode(effectiveTheme === ThemeMode.DARK);
const colors = generateThemeColors(themeColor.value, effectiveTheme);
applyTheme(colors);
}
}
if (prefersDarkMedia) {
// 兼容旧浏览器和新浏览器的监听方式
if (typeof prefersDarkMedia.addEventListener === "function") {
prefersDarkMedia.addEventListener("change", handleSystemThemeChange);
} else if (typeof (prefersDarkMedia as any).addListener === "function") {
(prefersDarkMedia as any).addListener(handleSystemThemeChange);
}
}
// 设置项映射,用于统一管理
const settingsMap = {
showTagsView,
showAppLogo,
showWatermark,
enableAiAssistant,
sidebarColorScheme,
layout,
} as const;
// 监听主题变化,自动应用样式
watch(
[theme, themeColor],
([newTheme, newThemeColor]: [ThemeMode, string]) => {
// 计算实际生效的主题:若为 AUTO 则使用系统当前偏好
const effectiveTheme: ThemeMode = newTheme === ThemeMode.AUTO ? getSystemTheme() : newTheme;
toggleDarkMode(effectiveTheme === ThemeMode.DARK);
const colors = generateThemeColors(newThemeColor, effectiveTheme);
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;
enableAiAssistant.value = defaultSettings.enableAiAssistant;
sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
layout.value = defaultSettings.layout as LayoutMode;
themeColor.value = defaultSettings.themeColor;
theme.value = defaultSettings.theme;
}
return {
// 状态
settingsVisible,
showTagsView,
showAppLogo,
showWatermark,
enableAiAssistant,
sidebarColorScheme,
layout,
themeColor,
theme,
// 更新方法
updateSetting,
updateTheme,
updateThemeColor,
updateSidebarColorScheme,
updateLayout,
// 面板控制
toggleSettingsPanel,
showSettingsPanel,
hideSettingsPanel,
// 重置功能
resetSettings,
};
});

View File

@@ -0,0 +1,93 @@
import { SidebarColor, ThemeMode } from "@/enums";
import type { LayoutMode } from "@/enums";
import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
import { STORAGE_KEYS } from "@/constants";
import { appConfig, defaults } from "@/settings";
export const useSettingsStore = defineStore("setting", () => {
// 界面显示
const settingsVisible = ref(false);
const showTagsView = useStorage(STORAGE_KEYS.SHOW_TAGS_VIEW, defaults.showTagsView);
const showAppLogo = useStorage(STORAGE_KEYS.SHOW_APP_LOGO, defaults.showAppLogo);
const showWatermark = useStorage(STORAGE_KEYS.SHOW_WATERMARK, defaults.showWatermark);
// 布局
const layout = useStorage<LayoutMode>(STORAGE_KEYS.LAYOUT, defaults.layout as LayoutMode);
const sidebarColorScheme = useStorage(
STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
defaults.sidebarColorScheme
);
// 主题
const theme = useStorage<ThemeMode>(STORAGE_KEYS.THEME, defaults.theme);
const themeColor = useStorage(STORAGE_KEYS.THEME_COLOR, defaults.themeColor);
// 特殊模式
const grayMode = useStorage(STORAGE_KEYS.GRAY_MODE, false);
const colorWeak = useStorage(STORAGE_KEYS.COLOR_WEAK, false);
// AI 助手:系统级 && 用户级
const userEnableAi = useStorage(STORAGE_KEYS.ENABLE_AI_ASSISTANT, false);
const enableAiAssistant = computed(() => appConfig.aiEnabled && userEnableAi.value);
// 主题变化监听
watch(
[theme, themeColor],
([t, c]: [ThemeMode, string]) => {
toggleDarkMode(t === ThemeMode.DARK);
applyTheme(generateThemeColors(c, t));
},
{ immediate: true }
);
watch(sidebarColorScheme, (v) => toggleSidebarColor(v === SidebarColor.CLASSIC_BLUE), {
immediate: true,
});
// 灰色模式监听
watch(
grayMode,
(v) => {
document.documentElement.style.filter = v ? "grayscale(100%)" : "";
},
{ immediate: true }
);
// 色弱模式监听
watch(
colorWeak,
(v) => {
document.documentElement.classList.toggle("color-weak", v);
},
{ immediate: true }
);
function resetSettings() {
showTagsView.value = defaults.showTagsView;
showAppLogo.value = defaults.showAppLogo;
showWatermark.value = defaults.showWatermark;
userEnableAi.value = false;
grayMode.value = false;
colorWeak.value = false;
sidebarColorScheme.value = defaults.sidebarColorScheme;
layout.value = defaults.layout as LayoutMode;
themeColor.value = defaults.themeColor;
theme.value = defaults.theme;
}
return {
settingsVisible,
showTagsView,
showAppLogo,
showWatermark,
enableAiAssistant,
userEnableAi,
grayMode,
colorWeak,
sidebarColorScheme,
layout,
themeColor,
theme,
resetSettings,
};
});

209
src/store/modules/tenant.ts Normal file
View File

@@ -0,0 +1,209 @@
import { store } from "@/store";
import TenantAPI from "@/api/system/tenant";
import type { TenantInfo } from "@/types/api";
import { STORAGE_KEYS } from "@/constants";
/**
* 租户 Store
*/
export const useTenantStore = defineStore("tenant", () => {
// 当前租户ID
const currentTenantId = ref<number | null>(null);
// 当前租户信息
const currentTenant = ref<TenantInfo | null>(null);
// 用户可访问的租户列表
const tenantList = ref<TenantInfo[]>([]);
/**
* 恢复租户信息
* 从 localStorage 恢复上次使用的租户
*/
function restoreTenant() {
const savedTenantId = localStorage.getItem(STORAGE_KEYS.TENANT_ID);
const savedTenantInfo = localStorage.getItem(STORAGE_KEYS.TENANT_INFO);
if (savedTenantId) {
currentTenantId.value = Number(savedTenantId);
}
if (savedTenantInfo) {
try {
currentTenant.value = JSON.parse(savedTenantInfo);
} catch (e) {
console.error("解析租户信息失败", e);
}
}
}
/**
* 获取用户租户列表
*/
function fetchTenantList() {
return new Promise<TenantInfo[]>((resolve, reject) => {
TenantAPI.getTenantList()
.then((data) => {
tenantList.value = data || [];
resolve(data || []);
})
.catch((error) => {
reject(error);
});
});
}
/**
* 加载租户
*
* 执行流程:
* 1. 获取用户可访问的租户列表
* 2. 尝试获取后端当前租户
* 3. 如果只有一个租户,自动选中
* 4. 否则等待用户手动选择
*
* @remarks
* 此方法由路由守卫调用,仅在启用多租户时执行
*/
async function loadTenant() {
restoreTenant();
// 1. 获取租户列表
await fetchTenantList();
// 2. 校验本地恢复的租户是否仍然可用(避免 tenantId 不在列表导致无默认选中)
if (
currentTenantId.value &&
tenantList.value.length > 0 &&
!tenantList.value.some((t) => t.id === currentTenantId.value)
) {
console.debug("[Tenant] 本地租户已不可用,清除并重新选择:", currentTenantId.value);
currentTenantId.value = null;
currentTenant.value = null;
localStorage.removeItem(STORAGE_KEYS.TENANT_ID);
localStorage.removeItem(STORAGE_KEYS.TENANT_INFO);
}
// 3. 如果已有租户列表,则保证一定有一个默认租户被选中
if (tenantList.value.length > 0) {
// 3.1 优先后端当前租户
if (!currentTenantId.value) {
try {
const currentTenantInfo = await TenantAPI.getCurrentTenant();
if (currentTenantInfo) {
setCurrentTenant(currentTenantInfo);
return;
}
} catch (error) {
console.debug("[Tenant] 获取当前租户失败,尝试本地/默认选择:", error);
}
}
// 3.2 本地已有 tenantId但 currentTenant 为空时,从列表补全 tenantInfo保持展示名称一致
if (currentTenantId.value && !currentTenant.value) {
const matched = tenantList.value.find((t) => t.id === currentTenantId.value);
if (matched) {
setCurrentTenant(matched);
return;
}
}
// 3.3 兜底:默认选中第一个(即使有多个租户,也保证 TenantSwitcher 有默认选中)
if (!currentTenantId.value) {
setCurrentTenant(tenantList.value[0]);
console.debug("[Tenant] 默认选中第一个租户:", tenantList.value[0].name);
}
}
}
/**
* 设置当前租户
*
* @param tenant 租户信息
*/
function setCurrentTenant(tenant: TenantInfo) {
currentTenantId.value = tenant.id;
currentTenant.value = tenant;
// 保存到 localStorage
localStorage.setItem(STORAGE_KEYS.TENANT_ID, String(tenant.id));
localStorage.setItem(STORAGE_KEYS.TENANT_INFO, JSON.stringify(tenant));
}
/**
* 切换租户
*
* @param tenantId 目标租户ID
*/
async function switchTenant(tenantId: number): Promise<void> {
try {
// 调用后端切换接口
const tenantInfo = await TenantAPI.switchTenant(tenantId);
// 后端返回切换后的租户信息
if (tenantInfo) {
setCurrentTenant(tenantInfo);
} else {
// 如果后端未返回,从租户列表中找到对应的租户信息
const tenant = tenantList.value.find((t) => t.id === tenantId);
if (tenant) {
setCurrentTenant(tenant);
} else {
// 如果列表中没有,重新获取租户信息
try {
const info = await TenantAPI.getCurrentTenant();
if (info) {
setCurrentTenant(info);
} else {
throw new Error("无法获取租户信息");
}
} catch (error) {
console.error("获取租户信息失败:", error);
throw new Error("切换租户后无法获取租户信息");
}
}
}
} catch (error) {
console.error("切换租户失败:", error);
throw error;
}
}
/**
* 清除租户信息
*/
function clearTenant() {
currentTenantId.value = null;
currentTenant.value = null;
tenantList.value = [];
localStorage.removeItem(STORAGE_KEYS.TENANT_ID);
localStorage.removeItem(STORAGE_KEYS.TENANT_INFO);
}
/**
* 设置租户列表
*/
function setTenantList(list: TenantInfo[]) {
tenantList.value = list || [];
}
// 恢复本地租户信息
restoreTenant();
return {
currentTenantId,
currentTenant,
tenantList,
loadTenant,
fetchTenantList,
setTenantList,
setCurrentTenant,
switchTenant,
clearTenant,
};
});
/**
* 在组件外部使用 TenantStore 的钩子函数
*/
export function useTenantStoreHook() {
return useTenantStore(store);
}

View File

@@ -1,13 +1,14 @@
import { store } from "@/store";
import AuthAPI, { type LoginFormData } from "@/api/auth-api";
import UserAPI, { type UserInfo } from "@/api/system/user-api";
import AuthAPI from "@/api/auth";
import UserAPI from "@/api/system/user";
import type { LoginRequest, UserInfo } from "@/types/api";
import { AuthStorage } from "@/utils/auth";
import { usePermissionStoreHook } from "@/store/modules/permission-store";
import { useDictStoreHook } from "@/store/modules/dict-store";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { useDictStoreHook } from "@/store/modules/dict";
import { useTagsViewStore } from "@/store";
import { cleanupWebSocket } from "@/plugins/websocket";
import { cleanupWebSocket } from "@/composables";
export const useUserStore = defineStore("user", () => {
// 用户信息
@@ -18,16 +19,16 @@ export const useUserStore = defineStore("user", () => {
/**
*
*
* @param {LoginFormData}
* @param {LoginRequest}
* @returns
*/
function login(LoginFormData: LoginFormData) {
function login(loginRequest: LoginRequest) {
return new Promise<void>((resolve, reject) => {
AuthAPI.login(LoginFormData)
AuthAPI.login(loginRequest)
.then((data) => {
const { accessToken, refreshToken } = data;
// 保存记住我状态和token
rememberMe.value = LoginFormData.rememberMe;
rememberMe.value = loginRequest.rememberMe ?? false;
AuthStorage.setTokens(accessToken, refreshToken, rememberMe.value);
resolve();
})