**kubectl WebShell 修复:** - 引入 github.com/creack/pty 库实现真正的伪终端 - 替换 bash --noediting + TERM=dumb 方案为 PTY-backed bash - 支持 xterm-256color、窗口自适应、readline 交互 - 修复键盘输入无响应问题 **侧边栏优化:** - 独立菜单项高度从 py-1.5 改为 h-8,与父菜单对齐 - 独立菜单图标从 h-3 w-3 改为 h-3.5 w-3.5,统一 14px **构建优化:** - run.sh docker up 增加 docker image prune -f 清理悬空镜像 - 每次部署自动清理 builder cache + dangling images **认证修复(历史):** - Token 存储:Zustand + localStorage 双写,http.ts 双源 fallback - YamlViewer/DeploymentDetail token 来源统一为 Zustand store
162 lines
4.3 KiB
TypeScript
162 lines
4.3 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import type { User } from '@/types';
|
|
|
|
// 主题状态
|
|
interface ThemeState {
|
|
sidebarCollapsed: boolean;
|
|
toggleSidebar: () => void;
|
|
}
|
|
|
|
export const useThemeStore = create<ThemeState>((set) => ({
|
|
sidebarCollapsed: false,
|
|
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
|
|
}));
|
|
|
|
// 用户状态
|
|
interface UserState {
|
|
user: User | null;
|
|
token: string | null;
|
|
isAuthenticated: boolean;
|
|
theme?: string;
|
|
setUser: (user: User | null) => void;
|
|
setToken: (token: string | null) => void;
|
|
login: (user: User, token: string) => void;
|
|
logout: () => void;
|
|
}
|
|
|
|
export const useUserStore = create<UserState>()(
|
|
persist(
|
|
(set) => ({
|
|
user: null,
|
|
token: null,
|
|
isAuthenticated: false,
|
|
theme: 'ocean-deep',
|
|
setUser: (user) => set({ user }),
|
|
setToken: (token) => set({ token }),
|
|
login: (user, token) => set({ user, token, isAuthenticated: true }),
|
|
logout: () => { localStorage.removeItem('token'); set({ user: null, token: null, isAuthenticated: false }); },
|
|
}),
|
|
{
|
|
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: '仪表盘', 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),
|
|
})),
|
|
}));
|