问题: src/i18n/index.ts 静态导入 zh-CN.json (200KB compact) + en-US.json (292KB compact),两者共 486KB 全部内联到主 index chunk,致主 chunk 696KB 超过 Vite 600KB 警告阈值。 修复: - src/i18n/index.ts: 移除 en-US.json 静态导入,改为 ensureLocaleLoaded() 动态导入 + i18n.addResourceBundle() 按需加载 - zh-CN (默认/回退语言) 保持静态内联,无行为变化 - en-US 仅在用户切换英文时按需 fetch (290KB 独立 chunk) - 已选英文的用户刷新页面时异步加载 en-US (短暂中文闪烁,可接受) - src/store/index.ts: setLanguage 调用 ensureLocaleLoaded 确保目标语言包 加载完成后再切换 结果: - 主 index chunk: 696KB → 423KB (-39%, -273KB) - en-US chunk: 290KB 独立懒加载 (仅英文用户首次切换时 fetch) - Vite 600KB chunk 警告消除 - tsc -b=0, npm build✓ (18.18s), ESLint=0/43/any=0, go build=0 - 17/17 集成测试通过, Docker healthy
262 lines
6.8 KiB
TypeScript
262 lines
6.8 KiB
TypeScript
import { create } from "zustand";
|
||
import { persist } from "zustand/middleware";
|
||
import type { User } from "@/types";
|
||
import { removeItem, STORAGE_KEYS } from "@/utils/storage";
|
||
import i18n from "@/i18n";
|
||
|
||
// 主题状态
|
||
interface ThemeState {
|
||
sidebarCollapsed: boolean;
|
||
toggleSidebar: () => void;
|
||
}
|
||
|
||
export const useThemeStore = create<ThemeState>((set) => ({
|
||
sidebarCollapsed: false,
|
||
toggleSidebar: () =>
|
||
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
||
}));
|
||
|
||
// 语言状态(i18n — U3 国际化)
|
||
type AppLanguage = "zh-CN" | "en-US";
|
||
|
||
interface LanguageState {
|
||
language: AppLanguage;
|
||
setLanguage: (lang: AppLanguage) => void;
|
||
}
|
||
|
||
export const useLanguageStore = create<LanguageState>()(
|
||
persist(
|
||
(set) => ({
|
||
language: "zh-CN" as AppLanguage,
|
||
setLanguage: (lang) => {
|
||
// Ensure the target locale bundle is loaded (en-US is lazy) before switching.
|
||
import("@/i18n")
|
||
.then(async ({ default: i18n, ensureLocaleLoaded }) => {
|
||
await ensureLocaleLoaded(lang);
|
||
i18n.changeLanguage(lang);
|
||
})
|
||
.catch(() => {
|
||
/* i18n not yet loaded */
|
||
});
|
||
set({ language: lang });
|
||
},
|
||
}),
|
||
{
|
||
name: "language-storage",
|
||
},
|
||
),
|
||
);
|
||
|
||
// 会话安全策略:用于 RDP/SSH/POD 等远程会话的水印、剪贴板和文件管控。 // Logan.Li
|
||
interface SessionSecurityState {
|
||
watermarkEnabled: boolean;
|
||
recordEnabled: boolean;
|
||
clipboardEnabled: boolean;
|
||
fileUploadEnabled: boolean;
|
||
fileDownloadEnabled: boolean;
|
||
commandPolicyEnabled: boolean;
|
||
approvalRequired: boolean;
|
||
setWatermarkEnabled: (enabled: boolean) => void;
|
||
setPolicy: (
|
||
policy: Partial<
|
||
Omit<SessionSecurityState, "setWatermarkEnabled" | "setPolicy">
|
||
>,
|
||
) => void;
|
||
}
|
||
|
||
export const useSessionSecurityStore = create<SessionSecurityState>()(
|
||
persist(
|
||
(set) => ({
|
||
watermarkEnabled: true,
|
||
recordEnabled: true,
|
||
clipboardEnabled: true,
|
||
fileUploadEnabled: true,
|
||
fileDownloadEnabled: true,
|
||
commandPolicyEnabled: false,
|
||
approvalRequired: false,
|
||
setWatermarkEnabled: (watermarkEnabled) => set({ watermarkEnabled }),
|
||
setPolicy: (policy) => set(policy),
|
||
}),
|
||
{
|
||
name: "session-security-storage",
|
||
},
|
||
),
|
||
);
|
||
|
||
export const useSessionWatermarkStore = useSessionSecurityStore;
|
||
|
||
// 用户状态
|
||
interface UserState {
|
||
user: User | null;
|
||
token: string | null;
|
||
isAuthenticated: boolean;
|
||
theme?: string;
|
||
/** 当前用户权限码列表(RBAC 按钮级控制);admin 为 ["*"];null 表示未加载 */
|
||
permissions: string[] | null;
|
||
setUser: (user: User | null) => void;
|
||
setToken: (token: string | null) => void;
|
||
setPermissions: (permissions: string[] | null) => void;
|
||
login: (user: User, token: string, permissions?: string[]) => void;
|
||
logout: () => void;
|
||
}
|
||
|
||
export const useUserStore = create<UserState>()(
|
||
persist(
|
||
(set) => ({
|
||
user: null as User | null,
|
||
token: null as string | null,
|
||
isAuthenticated: false,
|
||
theme: "ocean-deep",
|
||
permissions: null as string[] | null,
|
||
setUser: (user) => set({ user }),
|
||
setToken: (token) => set({ token }),
|
||
setPermissions: (permissions) => set({ permissions }),
|
||
login: (user, token, permissions) =>
|
||
set({
|
||
user,
|
||
token,
|
||
isAuthenticated: true,
|
||
permissions: permissions ?? null,
|
||
}),
|
||
logout: () => {
|
||
removeItem(STORAGE_KEYS.token);
|
||
set({
|
||
user: null as User | null,
|
||
token: null as string | null,
|
||
isAuthenticated: false,
|
||
permissions: null,
|
||
});
|
||
},
|
||
}),
|
||
{
|
||
name: "user-storage",
|
||
},
|
||
),
|
||
);
|
||
|
||
// 菜单状态
|
||
interface MenuState {
|
||
activeMenu: string;
|
||
openMenus: string[];
|
||
setActiveMenu: (menu: string) => void;
|
||
toggleMenu: (menu: string) => void;
|
||
}
|
||
|
||
export const useMenuStore = create<MenuState>((set) => ({
|
||
activeMenu: "dashboard",
|
||
openMenus: [],
|
||
setActiveMenu: (menu) => set({ activeMenu: menu }),
|
||
toggleMenu: (menu) =>
|
||
set((state) => ({
|
||
openMenus: state.openMenus.includes(menu)
|
||
? state.openMenus.filter((m) => m !== menu)
|
||
: [...state.openMenus, menu],
|
||
})),
|
||
}));
|
||
|
||
// 标签页状态
|
||
interface TabItem {
|
||
id: string;
|
||
title: string;
|
||
path: string;
|
||
closable?: boolean;
|
||
}
|
||
|
||
interface TabState {
|
||
tabs: TabItem[];
|
||
activeTab: string;
|
||
addTab: (tab: TabItem) => void;
|
||
removeTab: (tabId: string) => void;
|
||
setActiveTab: (tabId: string) => void;
|
||
}
|
||
|
||
export const useTabStore = create<TabState>((set) => ({
|
||
tabs: [
|
||
{
|
||
id: "dashboard",
|
||
title: i18n.t("monitor.dashboard.custom.typeGauge"),
|
||
path: "/",
|
||
closable: false,
|
||
},
|
||
],
|
||
activeTab: "dashboard",
|
||
addTab: (tab) =>
|
||
set((state) => {
|
||
const exists = state.tabs.find((t) => t.id === tab.id);
|
||
if (exists) {
|
||
return { activeTab: tab.id };
|
||
}
|
||
return { tabs: [...state.tabs, tab], activeTab: tab.id };
|
||
}),
|
||
removeTab: (tabId) =>
|
||
set((state) => {
|
||
const newTabs = state.tabs.filter((t) => t.id !== tabId);
|
||
if (state.activeTab === tabId && newTabs.length > 0) {
|
||
return { tabs: newTabs, activeTab: newTabs[newTabs.length - 1].id };
|
||
}
|
||
return { tabs: newTabs };
|
||
}),
|
||
setActiveTab: (tabId) => set({ activeTab: tabId }),
|
||
}));
|
||
|
||
// 通知状态
|
||
interface Notification {
|
||
id: string;
|
||
type: "info" | "success" | "warning" | "error";
|
||
title: string;
|
||
message: string;
|
||
timestamp: number;
|
||
read: boolean;
|
||
}
|
||
|
||
interface NotificationState {
|
||
notifications: Notification[];
|
||
unreadCount: number;
|
||
addNotification: (
|
||
notification: Omit<Notification, "id" | "timestamp" | "read">,
|
||
) => void;
|
||
markAsRead: (id: string) => void;
|
||
markAllAsRead: () => void;
|
||
removeNotification: (id: string) => void;
|
||
}
|
||
|
||
export const useNotificationStore = create<NotificationState>((set) => ({
|
||
notifications: [],
|
||
unreadCount: 0,
|
||
addNotification: (notification) =>
|
||
set((state) => ({
|
||
notifications: [
|
||
{
|
||
...notification,
|
||
id: Date.now().toString(),
|
||
timestamp: Date.now(),
|
||
read: false,
|
||
},
|
||
...state.notifications,
|
||
].slice(0, 100),
|
||
unreadCount: state.unreadCount + 1,
|
||
})),
|
||
markAsRead: (id) =>
|
||
set((state) => {
|
||
const notification = state.notifications.find((n) => n.id === id);
|
||
if (notification && !notification.read) {
|
||
return {
|
||
notifications: state.notifications.map((n) =>
|
||
n.id === id ? { ...n, read: true } : n,
|
||
),
|
||
unreadCount: Math.max(0, state.unreadCount - 1),
|
||
};
|
||
}
|
||
return state;
|
||
}),
|
||
markAllAsRead: () =>
|
||
set((state) => ({
|
||
notifications: state.notifications.map((n) => ({ ...n, read: true })),
|
||
unreadCount: 0,
|
||
})),
|
||
removeNotification: (id) =>
|
||
set((state) => ({
|
||
notifications: state.notifications.filter((n) => n.id !== id),
|
||
})),
|
||
}));
|