feat: enhance redis and production IA

This commit is contained in:
Logan.Li committed 2026-07-07 10:39:33 +08:00
1 parent 644981eef4
commit 58bbc394e9
76 files changed
+1702 -721

No files matched your search

+22 -6
View File
@@ -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 => {
+3 -1
View File
@@ -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);
+22 -9
View File
@@ -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(() => {