|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useState, useMemo, useEffect } from 'react';
|
|
|
|
|
|
|
+import React, { useState, useMemo, useEffect, useRef } from 'react';
|
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { Shield, Settings, Users, Cpu, MapPin, Layers, Bell, User, LogOut, ChevronDown, Activity, Radio, Lock, AlertCircle, CheckCircle, Clock, Menu, Building2, UserCog, BookOpen, Server, Globe, Gauge, Briefcase, MessageSquare, FileText, FolderTree, KeyRound, HardDrive, Database, Network, Workflow, ClipboardList, Wrench, Archive, Package, Box, Grid3x3, List, LayoutGrid, FolderOpen, FileCheck, FileEdit, FileSearch } from 'lucide-react';
|
|
import { Shield, Settings, Users, Cpu, MapPin, Layers, Bell, User, LogOut, ChevronDown, Activity, Radio, Lock, AlertCircle, CheckCircle, Clock, Menu, Building2, UserCog, BookOpen, Server, Globe, Gauge, Briefcase, MessageSquare, FileText, FolderTree, KeyRound, HardDrive, Database, Network, Workflow, ClipboardList, Wrench, Archive, Package, Box, Grid3x3, List, LayoutGrid, FolderOpen, FileCheck, FileEdit, FileSearch } from 'lucide-react';
|
|
@@ -653,25 +653,25 @@ export default function Dashboard() {
|
|
|
<div className="h-screen flex flex-col bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50 overflow-hidden">
|
|
<div className="h-screen flex flex-col bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50 overflow-hidden">
|
|
|
{/* 顶部导航栏 */}
|
|
{/* 顶部导航栏 */}
|
|
|
<nav className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 flex-shrink-0 z-50 shadow-sm">
|
|
<nav className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 flex-shrink-0 z-50 shadow-sm">
|
|
|
- <div className="px-6 py-4">
|
|
|
|
|
- <div className="flex items-center justify-between">
|
|
|
|
|
|
|
+ <div className="px-6 py-4 min-w-0">
|
|
|
|
|
+ <div className="flex items-center justify-between gap-4 min-w-0">
|
|
|
{/* Logo区域 */}
|
|
{/* Logo区域 */}
|
|
|
- <div className="flex items-center gap-8">
|
|
|
|
|
- <div className="flex items-center gap-3">
|
|
|
|
|
- <div className="relative">
|
|
|
|
|
|
|
+ <div className="flex items-center gap-4 lg:gap-8 flex-shrink-0 min-w-0">
|
|
|
|
|
+ <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
|
|
|
|
|
+ <div className="relative flex-shrink-0">
|
|
|
<div className="w-11 h-11 rounded-xl flex items-center justify-center shadow-lg shadow-blue-400/40" style={{ background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 100%)' }}>
|
|
<div className="w-11 h-11 rounded-xl flex items-center justify-center shadow-lg shadow-blue-400/40" style={{ background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 100%)' }}>
|
|
|
<Shield className="w-6 h-6 text-white" strokeWidth={2.5} />
|
|
<Shield className="w-6 h-6 text-white" strokeWidth={2.5} />
|
|
|
</div>
|
|
</div>
|
|
|
<div className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
|
|
<div className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div>
|
|
|
|
|
- <h1 className="text-lg text-gray-900">{env.appTitle}</h1>
|
|
|
|
|
- <p className="text-xs text-gray-500">{t('login.subtitle')}</p>
|
|
|
|
|
|
|
+ <div className="min-w-0">
|
|
|
|
|
+ <h1 className="text-lg text-gray-900 truncate">{env.appTitle}</h1>
|
|
|
|
|
+ <p className="text-xs text-gray-500 truncate">{t('login.subtitle')}</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* 主菜单 */}
|
|
{/* 主菜单 */}
|
|
|
- <div className="hidden lg:flex items-center gap-2 ml-4">
|
|
|
|
|
|
|
+ <div className="hidden lg:flex items-center gap-2 ml-2 lg:ml-4 flex-shrink min-w-0">
|
|
|
{filteredMainMenus.map((item) => {
|
|
{filteredMainMenus.map((item) => {
|
|
|
const Icon = item.icon;
|
|
const Icon = item.icon;
|
|
|
const isActive = activeMenu === item.key;
|
|
const isActive = activeMenu === item.key;
|
|
@@ -679,34 +679,79 @@ export default function Dashboard() {
|
|
|
// 只有系统配置显示下拉菜单,其他菜单的二级菜单在页面内用 tab 标签显示
|
|
// 只有系统配置显示下拉菜单,其他菜单的二级菜单在页面内用 tab 标签显示
|
|
|
if (item.key === 'systemConfig' && filteredSubMenuConfig[item.key] && filteredSubMenuConfig[item.key].length > 0) {
|
|
if (item.key === 'systemConfig' && filteredSubMenuConfig[item.key] && filteredSubMenuConfig[item.key].length > 0) {
|
|
|
const isDropdownOpen = showDropdownMenu === item.key;
|
|
const isDropdownOpen = showDropdownMenu === item.key;
|
|
|
|
|
+ const buttonRef = useRef<HTMLButtonElement>(null);
|
|
|
|
|
+ const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算下拉菜单位置
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (isDropdownOpen && buttonRef.current) {
|
|
|
|
|
+ const updatePosition = () => {
|
|
|
|
|
+ if (buttonRef.current) {
|
|
|
|
|
+ const rect = buttonRef.current.getBoundingClientRect();
|
|
|
|
|
+ setDropdownPosition({
|
|
|
|
|
+ top: rect.bottom + 8,
|
|
|
|
|
+ left: rect.left,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ updatePosition();
|
|
|
|
|
+ window.addEventListener('scroll', updatePosition, true);
|
|
|
|
|
+ window.addEventListener('resize', updatePosition);
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ window.removeEventListener('scroll', updatePosition, true);
|
|
|
|
|
+ window.removeEventListener('resize', updatePosition);
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setDropdownPosition(null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [isDropdownOpen]);
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <div
|
|
|
|
|
- key={item.key}
|
|
|
|
|
- className="relative"
|
|
|
|
|
- onMouseEnter={() => {
|
|
|
|
|
- if (dropdownTimer) clearTimeout(dropdownTimer);
|
|
|
|
|
- setShowDropdownMenu(item.key);
|
|
|
|
|
- }}
|
|
|
|
|
- onMouseLeave={() => {
|
|
|
|
|
- const timer = setTimeout(() => setShowDropdownMenu(null), 200);
|
|
|
|
|
- setDropdownTimer(timer);
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- <button
|
|
|
|
|
- className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
|
|
|
|
|
- isActive
|
|
|
|
|
- ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
|
|
|
|
|
- : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
|
|
|
|
|
- }`}
|
|
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={item.key}
|
|
|
|
|
+ className="relative"
|
|
|
|
|
+ onMouseEnter={() => {
|
|
|
|
|
+ if (dropdownTimer) clearTimeout(dropdownTimer);
|
|
|
|
|
+ setShowDropdownMenu(item.key);
|
|
|
|
|
+ }}
|
|
|
|
|
+ onMouseLeave={() => {
|
|
|
|
|
+ const timer = setTimeout(() => setShowDropdownMenu(null), 200);
|
|
|
|
|
+ setDropdownTimer(timer);
|
|
|
|
|
+ }}
|
|
|
>
|
|
>
|
|
|
- <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
|
|
|
|
|
- <span className={`text-sm ${isActive ? 'text-white' : ''}`}>{item.name || t(`nav.${item.key}`)}</span>
|
|
|
|
|
- <ChevronDown className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''} ${isActive ? 'text-white' : ''}`} />
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ ref={buttonRef}
|
|
|
|
|
+ className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
|
|
|
|
|
+ isActive
|
|
|
|
|
+ ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
|
|
|
|
|
+ : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
|
|
|
|
|
+ <span className={`text-sm ${isActive ? 'text-white' : ''}`}>{item.name || t(`nav.${item.key}`)}</span>
|
|
|
|
|
+ <ChevronDown className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''} ${isActive ? 'text-white' : ''}`} />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- {/* 下拉菜单 - 网格布局 */}
|
|
|
|
|
- {isDropdownOpen && (
|
|
|
|
|
- <div className="absolute top-full left-0 mt-2 w-[340px] bg-white rounded-xl shadow-xl border border-gray-200/50 p-3 animate-in fade-in slide-in-from-top-2 duration-200 z-50">
|
|
|
|
|
|
|
+ {/* 下拉菜单 - 使用 fixed 定位,避免被父容器裁剪 */}
|
|
|
|
|
+ {isDropdownOpen && dropdownPosition && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="fixed bg-white rounded-xl shadow-xl border border-gray-200/50 p-3 animate-in fade-in slide-in-from-top-2 duration-200 z-[100]"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ top: `${dropdownPosition.top}px`,
|
|
|
|
|
+ left: `${dropdownPosition.left}px`,
|
|
|
|
|
+ width: '340px',
|
|
|
|
|
+ }}
|
|
|
|
|
+ onMouseEnter={() => {
|
|
|
|
|
+ if (dropdownTimer) clearTimeout(dropdownTimer);
|
|
|
|
|
+ setShowDropdownMenu(item.key);
|
|
|
|
|
+ }}
|
|
|
|
|
+ onMouseLeave={() => {
|
|
|
|
|
+ const timer = setTimeout(() => setShowDropdownMenu(null), 200);
|
|
|
|
|
+ setDropdownTimer(timer);
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
{(filteredSubMenuConfig[item.key] || []).map((subItem) => {
|
|
{(filteredSubMenuConfig[item.key] || []).map((subItem) => {
|
|
|
const IconComponent = subItem.icon;
|
|
const IconComponent = subItem.icon;
|
|
@@ -742,7 +787,7 @@ export default function Dashboard() {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
- </div>
|
|
|
|
|
|
|
+ </>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -773,7 +818,7 @@ export default function Dashboard() {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* 右侧功能区 */}
|
|
{/* 右侧功能区 */}
|
|
|
- <div className="flex items-center gap-4">
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2 lg:gap-4 flex-shrink-0">
|
|
|
{/* 语言切换 */}
|
|
{/* 语言切换 */}
|
|
|
<button
|
|
<button
|
|
|
onClick={toggleLanguage}
|
|
onClick={toggleLanguage}
|
|
@@ -879,7 +924,7 @@ export default function Dashboard() {
|
|
|
filteredSubMenuConfig[activeMenu]?.length > 1 &&
|
|
filteredSubMenuConfig[activeMenu]?.length > 1 &&
|
|
|
activeMenu !== 'systemConfig' && (
|
|
activeMenu !== 'systemConfig' && (
|
|
|
<div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2 mb-6">
|
|
<div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2 mb-6">
|
|
|
- <div className="flex items-center gap-2 overflow-x-auto">
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2 overflow-x-auto min-w-0">
|
|
|
{filteredSubMenuConfig[activeMenu]?.map((item) => {
|
|
{filteredSubMenuConfig[activeMenu]?.map((item) => {
|
|
|
const isActive = activeSubMenu === item.key;
|
|
const isActive = activeSubMenu === item.key;
|
|
|
const menuKey = activeMenu === 'userManagement' ? 'userManagement' :
|
|
const menuKey = activeMenu === 'userManagement' ? 'userManagement' :
|
|
@@ -890,7 +935,7 @@ export default function Dashboard() {
|
|
|
<button
|
|
<button
|
|
|
key={item.key}
|
|
key={item.key}
|
|
|
onClick={() => setActiveSubMenu(item.key)}
|
|
onClick={() => setActiveSubMenu(item.key)}
|
|
|
- className={`px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
|
|
|
|
|
|
|
+ className={`px-3 lg:px-4 py-2 lg:py-2.5 rounded-xl text-xs lg:text-sm transition-all duration-200 whitespace-nowrap flex-shrink-0 ${
|
|
|
isActive
|
|
isActive
|
|
|
? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
|
|
? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
|
|
|
: 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
|
|
: 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
|