Files
aiops/frontend/src/store/index.ts
T
attacker 8e211b64af perf(frontend): U48 i18n locale 懒加载 — en-US JSON 动态导入拆分为独立 chunk
问题: 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
2026-07-17 01:49:25 +08:00

262 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
})),
}));