feat: 添加部门管理功能、主题切换和多语言支持

refactor(dashboard): 重构用户管理页面和路由结构

feat(server): 实现部门管理API和RBAC增强功能

docs: 更新用户手册和管理员指南文档

style: 统一图标使用和组件命名规范

test: 添加部门服务和数据隔离测试用例

chore: 更新依赖和配置文件
This commit is contained in:
2026-03-28 22:52:12 +08:00
parent 22308fe042
commit d327706087
87 changed files with 21372 additions and 4806 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
FC,
} from 'react';
type Theme = 'light' | 'dark' | 'auto';
interface ThemeContextType {
theme: Theme;
resolvedTheme: 'light' | 'dark';
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: FC<ThemeProviderProps> = ({ children }) => {
const [theme, setThemeState] = useState<Theme>(() => {
const saved = localStorage.getItem('theme') as Theme;
return saved || 'auto';
});
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
if (theme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return theme;
});
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
if (theme === 'auto') {
setResolvedTheme(e.matches ? 'dark' : 'light');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
useEffect(() => {
const applyTheme = (isDark: boolean) => {
const root = document.documentElement;
if (isDark) {
root.classList.add('dark');
root.style.setProperty('--text-primary', '#e5e7eb');
root.style.setProperty('--text-secondary', '#d1d5db');
root.style.setProperty('--text-tertiary', '#9ca3af');
root.style.setProperty('--background-light', '#1f2937');
root.style.setProperty('--background-white', '#111827');
root.style.setProperty('--background-gray', '#374151');
root.style.setProperty('--background-dark', '#0f172a');
root.style.setProperty('--border-color', '#374151');
root.style.setProperty('--border-hover', '#4b5563');
} else {
root.classList.remove('dark');
root.style.setProperty('--text-primary', '#262626');
root.style.setProperty('--text-secondary', '#595959');
root.style.setProperty('--text-tertiary', '#8c8c8c');
root.style.setProperty('--background-light', '#f5f5f5');
root.style.setProperty('--background-white', '#ffffff');
root.style.setProperty('--background-gray', '#f0f0f0');
root.style.setProperty('--background-dark', '#1f2937');
root.style.setProperty('--border-color', '#e8e8e8');
root.style.setProperty('--border-hover', '#d9d9d9');
}
};
const isDark = theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
setResolvedTheme(isDark ? 'dark' : 'light');
applyTheme(isDark);
}, [theme]);
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem('theme', newTheme);
};
const toggleTheme = () => {
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};

View File

@@ -2,6 +2,14 @@ import { createContext, useContext, useState, useEffect, ReactNode, FC } from 'r
export type UserRole = 'ADMIN' | 'MANAGER' | 'OPERATOR' | 'FINANCE' | 'SOURCING' | 'LOGISTICS' | 'ANALYST';
export interface Department {
id: string;
name: string;
parentId: string | null;
path: string;
depth: number;
}
export interface User {
id: string;
name: string;
@@ -9,6 +17,13 @@ export interface User {
role: UserRole;
avatar?: string;
permissions: string[];
departments?: string[];
shops?: Array<{
id: string;
name: string;
platform: string;
departmentId: string;
}>;
subscription?: {
plan: 'free' | 'basic' | 'pro' | 'enterprise';
expiresAt?: string;