feat: enhance redis and production IA
This commit is contained in:
1 parent
644981eef4
commit
58bbc394e9
76 files changed
+1702
-721
No files matched your search
@@ -113,17 +113,24 @@ const routePermissionMap: Record<string, UserRole[]> = {
|
||||
dashboard: ['admin', 'dba', 'developer', 'auditor'],
|
||||
instances: ['admin', 'dba'],
|
||||
'instance-groups': ['admin'],
|
||||
'data-dictionary': ['admin', 'dba', 'developer'],
|
||||
tickets: ['admin', 'dba', 'developer'],
|
||||
queries: ['admin', 'dba', 'developer'],
|
||||
roles: ['admin'],
|
||||
users: ['admin'],
|
||||
'audit-rules': ['admin', 'dba', 'auditor'],
|
||||
'audit-logs': ['admin', 'dba', 'auditor'],
|
||||
workflow: ['admin', 'dba', 'developer', 'auditor'],
|
||||
'auto-task': ['admin'],
|
||||
notifications: ['admin'],
|
||||
announcement: ['admin'],
|
||||
settings: ['admin'],
|
||||
};
|
||||
|
||||
const permissionAliasMap: Record<string, string[]> = {
|
||||
instances: ['datasource'],
|
||||
settings: ['config-platform'],
|
||||
};
|
||||
|
||||
export const actionPermissionMap: Record<string, UserRole[]> = {
|
||||
'ticket.create': ['admin', 'dba', 'developer'],
|
||||
'ticket.approve': ['admin', 'dba', 'developer'],
|
||||
@@ -314,17 +321,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
return cache.menu.get(menuId)!;
|
||||
}
|
||||
|
||||
const allowedRoles = routePermissionMap[menuId];
|
||||
const menuItem = menuItems.find(item => item.id === menuId || item.permission === menuId);
|
||||
const allowedRoles = menuItem?.roles || routePermissionMap[menuId];
|
||||
if (!allowedRoles) {
|
||||
cache.menu.set(menuId, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = allowedRoles.includes(user.role);
|
||||
const permissionKeys = [
|
||||
menuId,
|
||||
menuItem?.permission,
|
||||
...((permissionAliasMap[menuId] || [])),
|
||||
...((menuItem?.permission && permissionAliasMap[menuItem.permission]) || []),
|
||||
].filter(Boolean) as string[];
|
||||
const customPermission = permissionKeys.find(key => user.customPermissions && key in user.customPermissions);
|
||||
const result = allowedRoles.includes(user.role)
|
||||
&& (customPermission ? Boolean(user.customPermissions?.[customPermission]) : true);
|
||||
cache.menu.set(menuId, result);
|
||||
cache.timestamp = Date.now();
|
||||
return result;
|
||||
}, [user.role, isLoggedIn, isCacheValid]);
|
||||
}, [user.role, user.customPermissions, isLoggedIn, isCacheValid]);
|
||||
|
||||
/** 操作权限检查(带缓存,支持自定义权限覆盖) */
|
||||
const hasActionPermission = useCallback((action: PermissionAction | string): boolean => {
|
||||
@@ -446,8 +462,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}, [user, clearPermissionCache, emitPermissionChange]);
|
||||
|
||||
const visibleMenuItems = useMemo(() => {
|
||||
return menuItems.filter(item => item.roles.includes(user.role));
|
||||
}, [user.role]);
|
||||
return menuItems.filter(item => item.roles.includes(user.role) && hasPermission(item.id));
|
||||
}, [user.role, hasPermission]);
|
||||
|
||||
/** 检查是否拥有指定角色或以上级别权限 */
|
||||
const hasRoleLevel = useCallback((minRole: UserRole): boolean => {
|
||||
|
||||
@@ -35,7 +35,9 @@ export function useLogin(): LoginState {
|
||||
const { data: prefs } = await api.get<{ theme?: string; login_style?: string }>('/user/preferences');
|
||||
if (prefs?.theme) {
|
||||
const t = prefs.theme;
|
||||
if (t === 'dark' || t === 'light') {
|
||||
if (t === 'system') {
|
||||
setTheme('system');
|
||||
} else if (t === 'dark' || t === 'light') {
|
||||
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
|
||||
} else if (THEME_OPTIONS.some(o => o.id === t)) {
|
||||
setTheme(t as Theme);
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
|
||||
|
||||
export type Theme = 'ocean-deep' | 'light-professional' | 'midnight-purple' | 'pine-green' | 'orange-vitality';
|
||||
export type Theme = 'system' | 'light-professional' | 'ocean-deep' | 'pine-green';
|
||||
|
||||
export interface ThemeOption {
|
||||
id: Theme;
|
||||
name: string;
|
||||
description: string;
|
||||
isDark: boolean;
|
||||
isDark: boolean | 'system';
|
||||
primaryColor: string;
|
||||
}
|
||||
|
||||
export const THEME_OPTIONS: ThemeOption[] = [
|
||||
{ id: 'ocean-deep', name: '深海蓝', description: '专业稳重的深蓝色调', isDark: true, primaryColor: '#3b82f6' },
|
||||
{ id: 'light-professional', name: '明亮白', description: '清爽干净的浅色主题', isDark: false, primaryColor: '#2563eb' },
|
||||
{ id: 'midnight-purple', name: '午夜紫', description: '神秘优雅的紫色调', isDark: true, primaryColor: '#a855f7' },
|
||||
{ id: 'pine-green', name: '松林绿', description: '自然清新的绿色主题', isDark: true, primaryColor: '#22c55e' },
|
||||
{ id: 'orange-vitality', name: '活力橙', description: '温暖活力的橙色主题', isDark: true, primaryColor: '#f97316' },
|
||||
{ id: 'system', name: '跟随系统', description: '自动匹配系统深色或灰白浅色', isDark: 'system', primaryColor: '#2563eb' },
|
||||
{ id: 'light-professional', name: '灰白', description: '克制清爽的生产浅色主题', isDark: false, primaryColor: '#2563eb' },
|
||||
{ id: 'ocean-deep', name: '深色', description: '低亮度、高对比的专业暗色主题', isDark: true, primaryColor: '#3b82f6' },
|
||||
{ id: 'pine-green', name: '墨绿', description: '偏运维场景的深色备用主题', isDark: true, primaryColor: '#22c55e' },
|
||||
];
|
||||
|
||||
const THEME_ORDER: Theme[] = THEME_OPTIONS.map(o => o.id);
|
||||
const DEFAULT_THEME: Theme = 'ocean-deep';
|
||||
const DEFAULT_THEME: Theme = 'system';
|
||||
|
||||
// Backward compatibility: map old theme names to new ones
|
||||
function migrateLegacyTheme(value: string | null): Theme | null {
|
||||
if (value === 'dark') return 'ocean-deep';
|
||||
if (value === 'light') return 'light-professional';
|
||||
if (value === 'midnight-purple' || value === 'orange-vitality') return 'ocean-deep';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -51,8 +51,15 @@ function getInitialTheme(): Theme {
|
||||
return DEFAULT_THEME;
|
||||
}
|
||||
|
||||
function resolveSystemTheme(): Exclude<Theme, 'system'> {
|
||||
if (typeof window === 'undefined') return 'ocean-deep';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'ocean-deep' : 'light-professional';
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
const resolvedTheme = theme === 'system' ? resolveSystemTheme() : theme;
|
||||
document.documentElement.setAttribute('data-theme', resolvedTheme);
|
||||
document.documentElement.setAttribute('data-theme-choice', theme);
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
@@ -60,6 +67,12 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
if (theme !== 'system') return;
|
||||
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = () => applyTheme('system');
|
||||
media.addEventListener('change', handleChange);
|
||||
return () => media.removeEventListener('change', handleChange);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
|
||||
Reference in new issue
Block a user