修正:恢复 src/frontend 其他文件,仅删除 study 目录

develop
hnu202326010131 3 months ago
parent d9e8e6b578
commit 9629af961f

@ -0,0 +1,190 @@
# 集群管理系统前端项目
基于 Hadoop 的集群管理系统前端界面,提供集群状态监控、日志查询、故障诊断和自动修复功能。
## 项目结构
```
src/fronted/
├── index.html # 主页面文件(重构后)
├── 原型图.html # 原始原型文件(已优化)
├── assets/ # 静态资源目录
│ ├── images/ # 图片资源
│ ├── fonts/ # 字体文件
│ └── icons/ # 图标文件
├── components/ # 可复用组件
│ ├── common/ # 通用组件
│ ├── layout/ # 布局组件
│ └── ui/ # UI 组件
├── styles/ # 样式文件
│ ├── base/ # 基础样式
│ │ ├── reset.css # CSS 重置
│ │ └── variables.css # CSS 变量
│ ├── components/ # 组件样式
│ │ ├── header.css # 头部样式
│ │ ├── sidebar.css # 侧边栏样式
│ │ ├── dashboard.css # 仪表板样式
│ │ └── buttons.css # 按钮样式
│ ├── layouts/ # 布局样式
│ │ ├── main.css # 主布局
│ │ └── responsive.css # 响应式布局
│ ├── utils/ # 工具样式
│ │ └── utilities.css # 原子类
│ └── main.css # 主样式文件
├── utils/ # 工具函数
│ ├── charts.js # 图表管理
│ ├── navigation.js # 导航管理
│ └── responsive.js # 响应式交互
└── views/ # 页面级组件
```
## 功能特性
### 🎯 核心功能
- **集群状态监控**: 实时显示节点状态、CPU、内存使用情况
- **日志查询**: 支持多条件筛选的日志查询功能
- **故障诊断**: 自动检测和诊断系统故障
- **自动修复**: 提供故障修复建议和自动修复功能
### 🎨 设计规范
- **HTML5 语义化**: 使用 `<header>`, `<nav>`, `<main>`, `<aside>`, `<section>`, `<article>` 等语义化标签
- **BEM 命名规范**: 所有 CSS 类名遵循 BEM (Block Element Modifier) 命名规范
- **CSS 模块化**: 样式按功能模块拆分,便于维护和复用
- **响应式设计**: 支持桌面端、平板端和移动端适配
### 📱 响应式特性
- **移动端优先**: 采用移动端优先的响应式设计策略
- **断点设计**:
- 移动端: ≤ 768px
- 平板端: 769px - 1024px
- 桌面端: ≥ 1025px
- **触摸手势**: 支持滑动手势操作侧边栏
- **自适应布局**: 图表、表格、卡片等组件自动适配不同屏幕尺寸
### ♿ 无障碍访问
- **ARIA 标签**: 完整的 ARIA 属性支持
- **键盘导航**: 支持 Tab 键和 ESC 键操作
- **屏幕阅读器**: 兼容主流屏幕阅读器
- **焦点管理**: 清晰的焦点指示和管理
## 技术栈
- **HTML5**: 语义化标签和现代 Web 标准
- **CSS3**: Flexbox、Grid、CSS Variables、媒体查询
- **JavaScript ES6+**: 模块化、类、箭头函数等现代语法
- **ECharts**: 数据可视化图表库
- **Font Awesome**: 图标库
- **Tailwind CSS**: 原子化 CSS 框架(临时保留)
## 开发规范
### CSS 规范
- **选择器特异性**: 不超过 3 级嵌套
- **BEM 命名**: 严格遵循 BEM 命名规范
- **CSS 变量**: 使用 CSS 自定义属性管理设计系统
- **模块化**: 按功能拆分 CSS 文件
### JavaScript 规范
- **ES6+ 语法**: 使用现代 JavaScript 语法
- **模块化**: 功能按模块拆分
- **类设计**: 使用 ES6 类组织代码
- **事件管理**: 统一的事件绑定和解绑
### HTML 规范
- **语义化**: 使用合适的 HTML5 语义化标签
- **无障碍**: 完整的 ARIA 属性和无障碍支持
- **SEO 友好**: 合理的 meta 标签和结构化数据
## 快速开始
### 1. 启动开发服务器
```bash
cd /home/devbox/project/src/fronted
python3 -m http.server 8080
```
### 2. 访问应用
打开浏览器访问: `http://localhost:8080/index.html`
### 3. 功能测试
- **导航切换**: 点击顶部导航项切换不同页面
- **响应式测试**: 调整浏览器窗口大小测试响应式布局
- **移动端测试**: 使用浏览器开发者工具模拟移动设备
- **无障碍测试**: 使用 Tab 键测试键盘导航
## 浏览器兼容性
- **现代浏览器**: Chrome 60+, Firefox 60+, Safari 12+, Edge 79+
- **移动浏览器**: iOS Safari 12+, Chrome Mobile 60+
- **特性支持**: CSS Grid, Flexbox, CSS Variables, ES6+
## 性能优化
- **CSS 优化**: 模块化加载,减少重复样式
- **JavaScript 优化**: 按需加载,事件委托
- **图片优化**: 使用 SVG 矢量图标
- **缓存策略**: 合理的资源缓存设置
## 维护说明
### 添加新页面
1. 在 `views/` 目录创建页面组件
2. 在 `styles/components/` 添加对应样式
3. 在 `utils/navigation.js` 中添加路由逻辑
### 添加新组件
1. 在 `components/` 对应目录创建组件
2. 在 `styles/components/` 添加组件样式
3. 遵循 BEM 命名规范
### 样式修改
1. 优先修改 CSS 变量 (`styles/base/variables.css`)
2. 组件样式修改对应的组件 CSS 文件
3. 确保响应式适配正常
## 部署说明
### 生产环境部署
1. 移除 Tailwind CSS CDN 引用
2. 压缩 CSS 和 JavaScript 文件
3. 配置适当的 HTTP 缓存头
4. 启用 Gzip 压缩
### 静态资源优化
- 图片压缩和格式优化
- CSS 和 JavaScript 文件合并压缩
- 字体文件子集化
- CDN 资源配置
## 更新日志
### v2.0.0 (2025-10-31)
- ✨ 完全重构项目结构
- ✨ 实现 HTML5 语义化标签
- ✨ 采用 BEM 命名规范
- ✨ 模块化 CSS 架构
- ✨ 响应式布局支持
- ✨ 无障碍访问优化
- ✨ 现代 JavaScript 重构
### v1.0.0 (2025-10-30)
- 🎉 初始版本发布
- 📊 基础仪表板功能
- 🔍 日志查询功能
- 🔧 故障诊断功能
- 🛠️ 自动修复功能
## 贡献指南
1. Fork 项目
2. 创建功能分支
3. 提交代码变更
4. 推送到分支
5. 创建 Pull Request
## 许可证
MIT License - 详见 LICENSE 文件

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because it is too large Load Diff

@ -0,0 +1,60 @@
/* CSS Reset and Base Styles */
/* 基础重置样式 */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: #f9fafb;
color: #1e293b;
line-height: 1.6;
}
/* 移除默认样式 */
h1, h2, h3, h4, h5, h6 {
margin: 0;
font-weight: inherit;
}
ul, ol {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
button {
background: none;
border: none;
cursor: pointer;
font-family: inherit;
}
input, textarea, select {
font-family: inherit;
font-size: inherit;
}
img {
max-width: 100%;
height: auto;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

@ -0,0 +1,88 @@
/* CSS Custom Properties (Variables) */
/* CSS 自定义属性(变量)定义 */
:root {
/* 颜色系统 - Color System */
--color-primary: #3b82f6;
--color-primary-light: #60a5fa;
--color-primary-dark: #2563eb;
--color-secondary: #64748b;
--color-secondary-light: #94a3b8;
--color-secondary-dark: #475569;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #06b6d4;
/* 中性色 - Neutral Colors */
--color-white: #ffffff;
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
/* 字体系统 - Typography */
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-family-logo: "Pacifico", serif;
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* 间距系统 - Spacing */
--spacing-1: 0.25rem; /* 4px */
--spacing-2: 0.5rem; /* 8px */
--spacing-3: 0.75rem; /* 12px */
--spacing-4: 1rem; /* 16px */
--spacing-5: 1.25rem; /* 20px */
--spacing-6: 1.5rem; /* 24px */
--spacing-8: 2rem; /* 32px */
--spacing-10: 2.5rem; /* 40px */
--spacing-12: 3rem; /* 48px */
/* 边框圆角 - Border Radius */
--border-radius-none: 0px;
--border-radius-sm: 2px;
--border-radius-base: 4px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--border-radius-xl: 16px;
--border-radius-2xl: 20px;
--border-radius-full: 9999px;
/* 阴影系统 - Shadow System */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-base: 0 4px 12px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* 过渡动画 - Transitions */
--transition-fast: 150ms ease-in-out;
--transition-base: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* 断点 - Breakpoints */
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
}

@ -0,0 +1,299 @@
/* Button Component Styles */
/* 按钮组件样式 - 使用 BEM 命名规范 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-2);
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: 1.5;
border: 1px solid transparent;
border-radius: var(--border-radius-base);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
white-space: nowrap;
user-select: none;
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
/* 按钮尺寸变体 */
.btn--sm {
padding: var(--spacing-1) var(--spacing-3);
font-size: var(--font-size-xs);
}
.btn--md {
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
}
.btn--lg {
padding: var(--spacing-3) var(--spacing-6);
font-size: var(--font-size-base);
}
/* 主要按钮 */
.btn--primary {
background-color: var(--color-primary);
color: var(--color-white);
border-color: var(--color-primary);
}
.btn--primary:hover {
background-color: var(--color-primary-dark);
border-color: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn--primary:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
/* 次要按钮 */
.btn--secondary {
background-color: var(--color-gray-100);
color: var(--color-gray-700);
border-color: var(--color-gray-300);
}
.btn--secondary:hover {
background-color: var(--color-gray-200);
border-color: var(--color-gray-400);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn--secondary:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
/* 成功按钮 */
.btn--success {
background-color: var(--color-success);
color: var(--color-white);
border-color: var(--color-success);
}
.btn--success:hover {
background-color: #059669;
border-color: #059669;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
/* 警告按钮 */
.btn--warning {
background-color: var(--color-warning);
color: var(--color-white);
border-color: var(--color-warning);
}
.btn--warning:hover {
background-color: #d97706;
border-color: #d97706;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
/* 危险按钮 */
.btn--danger {
background-color: var(--color-error);
color: var(--color-white);
border-color: var(--color-error);
}
.btn--danger:hover {
background-color: #dc2626;
border-color: #dc2626;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
/* 轮廓按钮 */
.btn--outline {
background-color: transparent;
color: var(--color-primary);
border-color: var(--color-primary);
}
.btn--outline:hover {
background-color: var(--color-primary);
color: var(--color-white);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
/* 幽灵按钮 */
.btn--ghost {
background-color: transparent;
color: var(--color-gray-700);
border-color: transparent;
}
.btn--ghost:hover {
background-color: var(--color-gray-100);
color: var(--color-gray-900);
}
/* 链接样式按钮 */
.btn--link {
background-color: transparent;
color: var(--color-primary);
border-color: transparent;
padding: 0;
text-decoration: underline;
}
.btn--link:hover {
color: var(--color-primary-dark);
text-decoration: none;
}
/* 圆形按钮 */
.btn--rounded {
border-radius: var(--border-radius-full);
}
/* 方形按钮 */
.btn--square {
border-radius: 0;
}
/* 图标按钮 */
.btn--icon-only {
padding: var(--spacing-2);
width: 2.5rem;
height: 2.5rem;
}
.btn--icon-only.btn--sm {
width: 2rem;
height: 2rem;
padding: var(--spacing-1);
}
.btn--icon-only.btn--lg {
width: 3rem;
height: 3rem;
padding: var(--spacing-3);
}
/* 按钮图标 */
.btn__icon {
font-size: 1em;
line-height: 1;
}
.btn__icon--left {
margin-right: var(--spacing-2);
}
.btn__icon--right {
margin-left: var(--spacing-2);
}
/* 加载状态 */
.btn--loading {
position: relative;
color: transparent;
pointer-events: none;
}
.btn--loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 1rem;
height: 1rem;
margin: -0.5rem 0 0 -0.5rem;
border: 2px solid currentColor;
border-radius: var(--border-radius-full);
border-top-color: transparent;
animation: btn-spin 0.8s linear infinite;
}
@keyframes btn-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 按钮组 */
.btn-group {
display: inline-flex;
border-radius: var(--border-radius-base);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.btn-group .btn {
border-radius: 0;
border-right-width: 0;
}
.btn-group .btn:first-child {
border-top-left-radius: var(--border-radius-base);
border-bottom-left-radius: var(--border-radius-base);
}
.btn-group .btn:last-child {
border-top-right-radius: var(--border-radius-base);
border-bottom-right-radius: var(--border-radius-base);
border-right-width: 1px;
}
.btn-group .btn:not(:first-child):not(:last-child) {
border-radius: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.btn {
padding: var(--spacing-3) var(--spacing-4);
font-size: var(--font-size-base);
}
.btn--sm {
padding: var(--spacing-2) var(--spacing-3);
font-size: var(--font-size-sm);
}
.btn-group {
flex-direction: column;
}
.btn-group .btn {
border-right-width: 1px;
border-bottom-width: 0;
}
.btn-group .btn:first-child {
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
}
.btn-group .btn:last-child {
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
border-bottom-width: 1px;
}
}

@ -0,0 +1,263 @@
/* Dashboard Component Styles */
/* 仪表板组件样式 - 使用 BEM 命名规范 */
.dashboard {
padding: var(--spacing-6);
background-color: var(--color-gray-50);
min-height: 100vh;
}
/* 仪表板标题区域 */
.dashboard__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-6);
}
.dashboard__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-gray-900);
}
.dashboard__update-time {
font-size: var(--font-size-sm);
color: var(--color-gray-500);
}
/* 统计卡片网格 */
.dashboard__stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-6);
margin-bottom: var(--spacing-8);
}
/* 统计卡片样式 */
.dashboard__stat-card {
background-color: var(--color-white);
padding: var(--spacing-6);
border-radius: var(--border-radius-xl);
box-shadow: var(--shadow-base);
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.dashboard__stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.dashboard__stat-card-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.dashboard__stat-card-info {
flex: 1;
}
.dashboard__stat-card-label {
color: var(--color-gray-500);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-2);
}
.dashboard__stat-card-value {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-gray-900);
}
.dashboard__stat-card-value--success {
color: var(--color-success);
}
.dashboard__stat-card-value--warning {
color: var(--color-warning);
}
.dashboard__stat-card-value--error {
color: var(--color-error);
}
.dashboard__stat-card-icon {
width: 3rem;
height: 3rem;
border-radius: var(--border-radius-full);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
}
.dashboard__stat-card-icon--primary {
background-color: rgba(59, 130, 246, 0.1);
color: var(--color-primary);
}
.dashboard__stat-card-icon--success {
background-color: rgba(16, 185, 129, 0.1);
color: var(--color-success);
}
.dashboard__stat-card-icon--warning {
background-color: rgba(245, 158, 11, 0.1);
color: var(--color-warning);
}
.dashboard__stat-card-icon--error {
background-color: rgba(239, 68, 68, 0.1);
color: var(--color-error);
}
/* 图表网格 */
.dashboard__charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: var(--spacing-6);
margin-bottom: var(--spacing-8);
}
/* 图表卡片 */
.dashboard__chart-card {
background-color: var(--color-white);
padding: var(--spacing-6);
border-radius: var(--border-radius-xl);
box-shadow: var(--shadow-base);
}
.dashboard__chart-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-900);
margin-bottom: var(--spacing-4);
}
.dashboard__chart-container {
height: 300px;
width: 100%;
}
/* 表格容器 */
.dashboard__table-container {
background-color: var(--color-white);
border-radius: var(--border-radius-xl);
box-shadow: var(--shadow-base);
overflow: hidden;
}
.dashboard__table-header {
padding: var(--spacing-6) var(--spacing-6) var(--spacing-4);
border-bottom: 1px solid var(--color-gray-200);
}
.dashboard__table-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-900);
}
/* 表格样式 */
.dashboard__table {
width: 100%;
border-collapse: collapse;
}
.dashboard__table-head {
background-color: var(--color-gray-50);
}
.dashboard__table-th {
padding: var(--spacing-3) var(--spacing-6);
text-align: left;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--color-gray-500);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dashboard__table-td {
padding: var(--spacing-4) var(--spacing-6);
border-top: 1px solid var(--color-gray-200);
font-size: var(--font-size-sm);
color: var(--color-gray-900);
}
.dashboard__table-row:hover {
background-color: var(--color-gray-50);
}
/* 状态指示器 */
.dashboard__status-indicator {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.dashboard__status-dot {
width: 10px;
height: 10px;
border-radius: var(--border-radius-full);
}
.dashboard__status-dot--running {
background-color: var(--color-success);
}
.dashboard__status-dot--warning {
background-color: var(--color-warning);
}
.dashboard__status-dot--error {
background-color: var(--color-error);
}
.dashboard__status-text {
font-size: var(--font-size-sm);
color: var(--color-gray-700);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.dashboard__stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-4);
}
.dashboard__charts-grid {
grid-template-columns: 1fr;
gap: var(--spacing-4);
}
}
@media (max-width: 768px) {
.dashboard {
padding: var(--spacing-4);
}
.dashboard__header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-2);
}
.dashboard__stats-grid {
grid-template-columns: 1fr;
}
.dashboard__stat-card {
padding: var(--spacing-4);
}
.dashboard__chart-card {
padding: var(--spacing-4);
}
.dashboard__table-container {
overflow-x: auto;
}
}

@ -0,0 +1,283 @@
/* Header Component Styles */
/* 头部组件样式 - 使用 BEM 命名规范 */
.header {
background-color: var(--color-white);
box-shadow: var(--shadow-sm);
padding: var(--spacing-4) var(--spacing-6);
display: flex;
align-items: center;
justify-content: space-between;
position: relative; /* 供移动端展开的导航定位 */
}
/* 头部左侧区域 */
.header__left {
display: flex;
align-items: center;
gap: var(--spacing-10);
}
/* 移动端菜单按钮(默认隐藏,移动端显示) */
.header__mobile-menu-btn {
display: none;
width: 2rem;
height: 2rem;
border-radius: var(--border-radius-md);
background-color: var(--color-gray-200);
color: var(--color-gray-600);
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color var(--transition-fast);
}
.header__mobile-menu-btn:hover {
background-color: var(--color-gray-300);
}
/* Logo 样式 */
.header__logo {
font-size: var(--font-size-xl);
font-family: var(--font-family-logo);
color: var(--color-primary);
font-weight: var(--font-weight-normal);
}
/* 主导航容器 */
.header__nav {
display: flex;
gap: var(--spacing-6);
}
/* 导航项基础样式 */
.header__nav-item {
padding: var(--spacing-3) var(--spacing-3);
border-radius: var(--border-radius-md);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-gray-700);
transition: color var(--transition-fast);
cursor: pointer;
}
/* 导航项悬停和激活状态 */
.header__nav-item:hover,
.header__nav-item--active {
color: var(--color-primary);
}
/* 下拉菜单容器 */
.header__dropdown {
position: relative;
}
.header__dropdown-trigger {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.header__dropdown-icon {
font-size: var(--font-size-xs);
transition: transform var(--transition-fast);
}
.header__dropdown:hover .header__dropdown-icon {
transform: rotate(180deg);
}
/* 下拉菜单内容 */
.header__dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: var(--spacing-1);
width: 12rem;
background-color: var(--color-white);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-lg);
z-index: 10;
padding: var(--spacing-1) 0;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all var(--transition-fast);
}
.header__dropdown:hover .header__dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* 下拉菜单显隐状态(脚本控制) */
.header__dropdown-menu--show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.header__dropdown--active .header__dropdown-icon {
transform: rotate(180deg);
}
.header__dropdown-item {
display: block;
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
color: var(--color-gray-700);
transition: background-color var(--transition-fast);
}
.header__dropdown-item:hover {
background-color: var(--color-gray-100);
}
/* 头部右侧区域 */
.header__right {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
/* 搜索框容器 */
.header__search {
position: relative;
}
.header__search-input {
padding-left: var(--spacing-10);
padding-right: var(--spacing-4);
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-2);
font-size: var(--font-size-sm);
border: 1px solid var(--color-gray-300);
border-radius: var(--border-radius-md);
background-color: var(--color-white);
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.header__search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.header__search-icon {
position: absolute;
left: var(--spacing-3);
top: 50%;
transform: translateY(-50%);
color: var(--color-gray-400);
font-size: var(--font-size-sm);
}
/* 用户菜单 */
.header__user-menu {
position: relative;
}
.header__user-avatar {
width: 2rem;
height: 2rem;
border-radius: var(--border-radius-full);
background-color: var(--color-gray-200);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color var(--transition-fast);
}
.header__user-avatar:hover {
background-color: var(--color-gray-300);
}
.header__user-avatar-icon {
color: var(--color-gray-600);
font-size: var(--font-size-sm);
}
/* 用户下拉菜单 */
.header__user-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: var(--spacing-2);
width: 12rem;
background-color: var(--color-white);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-lg);
z-index: 20;
padding: var(--spacing-1) 0;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all var(--transition-fast);
}
.header__user-menu:hover .header__user-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* 用户菜单显隐状态(脚本控制) */
.header__user-dropdown--show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.header__user-dropdown-item {
display: block;
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
color: var(--color-gray-700);
transition: background-color var(--transition-fast);
}
.header__user-dropdown-item:hover {
background-color: var(--color-gray-100);
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
padding: var(--spacing-3) var(--spacing-4);
}
.header__left {
gap: var(--spacing-6);
}
/* 移动端显示菜单按钮 */
.header__mobile-menu-btn {
display: inline-flex;
}
/* 移动端导航默认隐藏,展开时作为下拉面板显示 */
.header__nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
display: none;
flex-direction: column;
gap: var(--spacing-2);
background-color: var(--color-white);
padding: var(--spacing-2) var(--spacing-4);
box-shadow: var(--shadow-lg);
z-index: 30;
border-radius: var(--border-radius-md);
}
.header__nav--mobile-open {
display: flex;
}
.header__search {
display: none;
}
}

@ -0,0 +1,125 @@
/* Sidebar Component Styles */
/* 侧边栏组件样式 - 使用 BEM 命名规范 */
.sidebar {
width: 16rem;
background-color: var(--color-white);
box-shadow: inset -1px 0 0 var(--color-gray-200);
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.sidebar--collapsed {
display: none;
}
/* 侧边栏标题区域 */
.sidebar__header {
padding: var(--spacing-4);
border-bottom: 1px solid var(--color-gray-200);
}
.sidebar__title {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-500);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* 侧边栏导航区域 */
.sidebar__nav {
padding: var(--spacing-4) 0;
flex: 1;
}
/* 侧边栏链接样式 */
.sidebar__link {
display: block;
padding: var(--spacing-3) var(--spacing-6);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-gray-700);
transition: all var(--transition-fast);
border-left: 3px solid transparent;
}
.sidebar__link:hover {
background-color: var(--color-gray-50);
color: var(--color-primary);
border-left-color: var(--color-primary);
}
.sidebar__link--active {
background-color: rgba(59, 130, 246, 0.1);
color: var(--color-primary);
border-left-color: var(--color-primary);
}
/* 侧边栏图标 */
.sidebar__icon {
margin-right: var(--spacing-3);
font-size: var(--font-size-sm);
width: 1rem;
text-align: center;
}
/* 侧边栏分组 */
.sidebar__group {
margin-bottom: var(--spacing-6);
}
.sidebar__group-title {
padding: var(--spacing-2) var(--spacing-6);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-400);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* 侧边栏底部区域 */
.sidebar__footer {
padding: var(--spacing-4);
border-top: 1px solid var(--color-gray-200);
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: -16rem; /* 默认收起在屏幕外 */
height: 100vh;
z-index: 50;
transition: left var(--transition-base);
}
/* 兼容 responsive.js 使用的类名 */
.sidebar--open,
.sidebar--mobile-open {
left: 0;
}
/* 与脚本统一的遮罩层类名DOM: .sidebar-overlay */
.sidebar__overlay,
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 40;
opacity: 0;
visibility: hidden;
transition: all var(--transition-base);
}
.sidebar__overlay--visible,
.sidebar-overlay--show {
opacity: 1;
visibility: visible;
}
}

@ -0,0 +1,282 @@
/* Main Layout Styles */
/* 主布局样式 - 使用 BEM 命名规范 */
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--color-gray-50);
}
/* 主容器 */
.layout__container {
display: flex;
flex: 1;
overflow: hidden;
}
/* 主内容区域 */
.layout__main {
flex: 1;
overflow: auto;
background-color: var(--color-gray-50);
}
/* 内容包装器 */
.layout__content {
padding: var(--spacing-6);
max-width: 100%;
}
/* 页面标题区域 */
.layout__page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-6);
padding-bottom: var(--spacing-4);
border-bottom: 1px solid var(--color-gray-200);
}
.layout__page-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-gray-900);
margin: 0;
}
.layout__page-subtitle {
font-size: var(--font-size-sm);
color: var(--color-gray-500);
margin-top: var(--spacing-1);
}
.layout__page-actions {
display: flex;
gap: var(--spacing-3);
align-items: center;
}
/* 网格系统 */
.layout__grid {
display: grid;
gap: var(--spacing-6);
}
.layout__grid--1 {
grid-template-columns: 1fr;
}
.layout__grid--2 {
grid-template-columns: repeat(2, 1fr);
}
.layout__grid--3 {
grid-template-columns: repeat(3, 1fr);
}
.layout__grid--4 {
grid-template-columns: repeat(4, 1fr);
}
.layout__grid--auto {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
/* 卡片布局 */
.layout__card {
background-color: var(--color-white);
border-radius: var(--border-radius-xl);
box-shadow: var(--shadow-base);
overflow: hidden;
}
.layout__card-header {
padding: var(--spacing-6) var(--spacing-6) var(--spacing-4);
border-bottom: 1px solid var(--color-gray-200);
}
.layout__card-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-900);
margin: 0;
}
.layout__card-description {
font-size: var(--font-size-sm);
color: var(--color-gray-500);
margin-top: var(--spacing-1);
}
.layout__card-body {
padding: var(--spacing-6);
}
.layout__card-footer {
padding: var(--spacing-4) var(--spacing-6);
background-color: var(--color-gray-50);
border-top: 1px solid var(--color-gray-200);
}
/* 分隔符 */
.layout__divider {
height: 1px;
background-color: var(--color-gray-200);
margin: var(--spacing-6) 0;
}
.layout__divider--vertical {
width: 1px;
height: auto;
margin: 0 var(--spacing-6);
}
/* 间距工具类 */
.layout__section {
margin-bottom: var(--spacing-10);
}
.layout__section:last-child {
margin-bottom: 0;
}
/* 滚动容器 */
.layout__scroll-container {
overflow: auto;
max-height: 100%;
}
.layout__scroll-container--horizontal {
overflow-x: auto;
overflow-y: hidden;
}
.layout__scroll-container--vertical {
overflow-x: hidden;
overflow-y: auto;
}
/* 粘性定位 */
.layout__sticky-top {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--color-white);
}
.layout__sticky-bottom {
position: sticky;
bottom: 0;
z-index: 10;
background-color: var(--color-white);
}
/* 居中容器 */
.layout__centered {
display: flex;
align-items: center;
justify-content: center;
min-height: 50vh;
}
.layout__centered-content {
text-align: center;
max-width: 400px;
}
/* 响应式断点 */
@media (max-width: 1280px) {
.layout__grid--4 {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1024px) {
.layout__content {
padding: var(--spacing-4);
}
.layout__grid--3,
.layout__grid--4 {
grid-template-columns: repeat(2, 1fr);
}
.layout__page-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-4);
}
.layout__page-actions {
width: 100%;
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.layout__container {
flex-direction: column;
}
.layout__content {
padding: var(--spacing-3);
}
.layout__grid--2,
.layout__grid--3,
.layout__grid--4 {
grid-template-columns: 1fr;
}
.layout__grid {
gap: var(--spacing-4);
}
.layout__card-header,
.layout__card-body,
.layout__card-footer {
padding: var(--spacing-4);
}
.layout__page-title {
font-size: var(--font-size-xl);
}
.layout__section {
margin-bottom: var(--spacing-6);
}
}
@media (max-width: 640px) {
.layout__content {
padding: var(--spacing-2);
}
.layout__page-header {
margin-bottom: var(--spacing-4);
padding-bottom: var(--spacing-3);
}
.layout__card-header,
.layout__card-body,
.layout__card-footer {
padding: var(--spacing-3);
}
}
/* 打印样式 */
@media print {
.layout {
background-color: white;
}
.layout__card {
box-shadow: none;
border: 1px solid var(--color-gray-300);
}
.layout__page-actions {
display: none;
}
}

@ -0,0 +1,444 @@
/* Responsive Layout Styles */
/* 响应式布局样式 - 移动端适配和响应式设计 */
/* ==================== 移动端优先的响应式设计 ==================== */
/* 基础布局响应式调整 */
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.layout__container {
display: flex;
flex: 1;
min-height: 0; /* 防止 flex 子元素溢出 */
}
/* ==================== 头部响应式设计 ==================== */
/* 移动端头部调整 */
@media (max-width: 768px) {
.header {
padding: var(--spacing-3) var(--spacing-4);
flex-wrap: wrap;
gap: var(--spacing-3);
}
.header__left {
gap: var(--spacing-4);
flex: 1;
min-width: 0;
}
.header__logo {
font-size: var(--font-size-lg);
}
/* 移动端导航隐藏,使用汉堡菜单 */
.header__nav {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--color-white);
box-shadow: var(--shadow-lg);
flex-direction: column;
padding: var(--spacing-4);
z-index: 50;
}
.header__nav--mobile-open {
display: flex;
}
.header__nav-item {
padding: var(--spacing-3) var(--spacing-4);
border-radius: var(--border-radius-md);
width: 100%;
text-align: left;
}
/* 移动端汉堡菜单按钮 */
.header__mobile-menu-btn {
display: block;
background: none;
border: none;
font-size: var(--font-size-lg);
color: var(--color-gray-700);
cursor: pointer;
padding: var(--spacing-2);
}
.header__right {
gap: var(--spacing-2);
}
.header__search {
display: none; /* 移动端隐藏搜索框 */
}
}
/* 桌面端汉堡菜单隐藏 */
@media (min-width: 769px) {
.header__mobile-menu-btn {
display: none;
}
}
/* ==================== 侧边栏响应式设计 ==================== */
.sidebar {
width: 250px;
flex-shrink: 0;
transition: transform var(--transition-normal);
}
/* 移动端侧边栏 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 40;
background-color: var(--color-white);
transform: translateX(-100%);
box-shadow: var(--shadow-xl);
}
.sidebar--mobile-open {
transform: translateX(0);
}
/* 移动端遮罩层 */
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 30;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-normal), visibility var(--transition-normal);
}
.sidebar-overlay--show {
opacity: 1;
visibility: visible;
}
/* 调整主内容区域 */
.layout__main {
width: 100%;
margin-left: 0;
}
}
/* 平板端侧边栏 */
@media (min-width: 769px) and (max-width: 1024px) {
.sidebar {
width: 200px;
}
}
/* ==================== 主内容区域响应式设计 ==================== */
.layout__main {
flex: 1;
min-width: 0;
overflow-x: auto;
}
.layout__content {
padding: var(--spacing-6);
max-width: 100%;
}
/* 移动端主内容调整 */
@media (max-width: 768px) {
.layout__content {
padding: var(--spacing-4);
}
.layout__page-header {
flex-direction: column;
gap: var(--spacing-4);
align-items: stretch;
}
.layout__page-actions {
width: 100%;
}
.layout__page-actions .btn {
width: 100%;
justify-content: center;
}
}
/* ==================== 仪表板响应式设计 ==================== */
/* 统计卡片网格 */
.dashboard__stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-4);
}
/* 移动端统计卡片 */
@media (max-width: 640px) {
.dashboard__stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-3);
}
}
/* 超小屏幕单列显示 */
@media (max-width: 480px) {
.dashboard__stats-grid {
grid-template-columns: 1fr;
}
}
/* 图表网格响应式 */
.dashboard__charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: var(--spacing-6);
}
/* 移动端图表 */
@media (max-width: 768px) {
.dashboard__charts-grid {
grid-template-columns: 1fr;
gap: var(--spacing-4);
}
.dashboard__chart-container {
height: 250px; /* 移动端降低图表高度 */
}
}
/* 超小屏幕图表 */
@media (max-width: 480px) {
.dashboard__charts-grid {
grid-template-columns: 1fr;
}
.dashboard__chart-container {
height: 200px;
}
}
/* ==================== 表格响应式设计 ==================== */
/* 移动端表格滚动 */
@media (max-width: 768px) {
.dashboard__table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.dashboard__table {
min-width: 600px; /* 确保表格有最小宽度 */
}
.dashboard__table-th,
.dashboard__table-td {
padding: var(--spacing-2) var(--spacing-3);
font-size: var(--font-size-sm);
}
}
/* 超小屏幕表格优化 */
@media (max-width: 480px) {
.dashboard__table {
min-width: 500px;
}
.dashboard__table-th,
.dashboard__table-td {
padding: var(--spacing-2);
font-size: var(--font-size-xs);
}
/* 隐藏不重要的列 */
.dashboard__table-th:nth-child(6),
.dashboard__table-td:nth-child(6) {
display: none;
}
}
/* ==================== 卡片响应式设计 ==================== */
.layout__card {
margin-bottom: var(--spacing-6);
}
/* 移动端卡片 */
@media (max-width: 768px) {
.layout__card {
margin-bottom: var(--spacing-4);
border-radius: var(--border-radius-lg);
}
.layout__card-body {
padding: var(--spacing-4);
}
}
/* ==================== 网格系统响应式 ==================== */
.layout__grid {
display: grid;
gap: var(--spacing-4);
}
.layout__grid--2 {
grid-template-columns: repeat(2, 1fr);
}
.layout__grid--3 {
grid-template-columns: repeat(3, 1fr);
}
.layout__grid--4 {
grid-template-columns: repeat(4, 1fr);
}
/* 移动端网格调整 */
@media (max-width: 768px) {
.layout__grid--2,
.layout__grid--3,
.layout__grid--4 {
grid-template-columns: 1fr;
gap: var(--spacing-3);
}
}
/* 平板端网格调整 */
@media (min-width: 769px) and (max-width: 1024px) {
.layout__grid--3,
.layout__grid--4 {
grid-template-columns: repeat(2, 1fr);
}
}
/* ==================== 按钮响应式设计 ==================== */
/* 移动端按钮 */
@media (max-width: 768px) {
.btn {
padding: var(--spacing-3) var(--spacing-4);
font-size: var(--font-size-sm);
}
.btn--large {
padding: var(--spacing-4) var(--spacing-6);
font-size: var(--font-size-base);
}
.btn--small {
padding: var(--spacing-2) var(--spacing-3);
font-size: var(--font-size-xs);
}
}
/* ==================== 下拉菜单响应式设计 ==================== */
/* 移动端下拉菜单 */
@media (max-width: 768px) {
.header__dropdown-menu,
.header__user-dropdown {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
transform: none;
border-radius: var(--border-radius-lg) var(--border-radius-lg) 0 0;
max-height: 50vh;
overflow-y: auto;
}
}
/* ==================== 工具类响应式扩展 ==================== */
/* 移动端显示/隐藏 */
@media (max-width: 640px) {
.u-hidden-mobile {
display: none !important;
}
.u-block-mobile {
display: block !important;
}
.u-flex-mobile {
display: flex !important;
}
}
/* 平板端显示/隐藏 */
@media (min-width: 641px) and (max-width: 1024px) {
.u-hidden-tablet {
display: none !important;
}
.u-block-tablet {
display: block !important;
}
.u-flex-tablet {
display: flex !important;
}
}
/* 桌面端显示/隐藏 */
@media (min-width: 1025px) {
.u-hidden-desktop {
display: none !important;
}
.u-block-desktop {
display: block !important;
}
.u-flex-desktop {
display: flex !important;
}
}
/* ==================== 打印样式 ==================== */
@media print {
.header,
.sidebar,
.layout__page-actions {
display: none !important;
}
.layout__main {
width: 100% !important;
margin: 0 !important;
}
.layout__content {
padding: 0 !important;
}
.dashboard__chart-container {
height: 300px !important;
break-inside: avoid;
}
.layout__card {
break-inside: avoid;
margin-bottom: 1rem !important;
}
}

@ -0,0 +1,281 @@
/* Main Stylesheet */
/* 主样式表 - 导入所有模块化样式 */
/* 基础样式 */
@import './base/reset.css';
@import './base/variables.css';
/* 布局样式 */
@import './layouts/main.css';
@import './layouts/responsive.css';
/* 组件样式 */
@import './components/header.css';
@import './components/sidebar.css';
@import './components/dashboard.css';
@import './components/buttons.css';
/* 工具类 */
@import './utils/utilities.css';
/* 全局样式补充 */
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-family-base);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 焦点样式 */
*:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
*:focus:not(:focus-visible) {
outline: none;
}
/* 选择文本样式 */
::selection {
background-color: rgba(59, 130, 246, 0.2);
color: var(--color-gray-900);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background-color: var(--color-gray-100);
}
::-webkit-scrollbar-thumb {
background-color: var(--color-gray-300);
border-radius: var(--border-radius-base);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-gray-400);
}
/* 打印样式 */
@media print {
* {
box-shadow: none !important;
text-shadow: none !important;
}
body {
background: white !important;
color: black !important;
}
.header,
.sidebar {
display: none !important;
}
.layout__main {
margin: 0 !important;
padding: 0 !important;
}
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
:root {
--color-gray-50: #ffffff;
--color-gray-100: #f0f0f0;
--color-gray-200: #e0e0e0;
--color-gray-300: #c0c0c0;
--color-gray-400: #808080;
--color-gray-500: #606060;
--color-gray-600: #404040;
--color-gray-700: #202020;
--color-gray-800: #101010;
--color-gray-900: #000000;
}
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* 诊断三栏布局优化 */
#diagnosis-split {
gap: 8px;
min-height: 620px;
}
#diag-left,
#diag-middle,
#diag-right {
border-color: var(--color-gray-200);
background-color: #ffffff;
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
#diag-divider-1,
#diag-divider-2 {
width: 6px;
background-color: var(--color-gray-200);
border-radius: 4px;
transition: background-color 0.2s ease;
}
#diag-divider-1:hover,
#diag-divider-2:hover {
background-color: var(--color-primary);
}
/* 树形与搜索优化 */
#diag-tree-search {
border-color: var(--color-gray-300);
}
#diag-tree-search:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
#diag-tree .btn[data-cluster] {
width: 100%;
text-align: left;
background-color: #eaf2ff;
border-color: #d6e8ff;
color: var(--color-gray-900);
}
#diag-tree .btn[data-cluster]:hover {
background-color: #d6e8ff;
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
border-color: #cfe0ff;
}
.cluster-toggle-icon {
margin-right: 6px;
color: var(--color-primary);
transition: color 0.2s ease, transform 0.2s ease;
cursor: pointer;
}
#diag-tree .diag-node-list {
border: 1px solid var(--color-gray-200);
border-radius: 8px;
background-color: #fff;
}
#diag-tree .diag-node-list li:not(:first-child) .diag-node-btn {
border-top: 1px solid var(--color-gray-200);
}
#diag-tree .diag-node-btn {
width: 100%;
text-align: left;
background-color: #ffffff;
border: none;
padding: 8px 10px;
cursor: grab;
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
#diag-tree .diag-node-btn:hover {
background-color: #f5fbff;
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
border-color: #cfe8ff;
}
#diag-tree .btn[data-cluster]:hover .cluster-toggle-icon {
color: var(--color-primary-dark);
transform: translateY(-0.5px);
}
/* 实时日志条目优化 */
#diag-live-logs .u-text-sm {
padding: 6px 8px;
border: 1px solid var(--color-gray-200);
border-radius: 6px;
background-color: var(--color-gray-50);
}
#diag-live-logs-list .diag-log-btn {
width: 100%;
text-align: left;
background-color: #ffffff;
border: 1px solid var(--color-gray-200);
border-radius: 6px;
padding: 8px 10px;
}
#diag-live-logs-list .diag-log-btn:hover {
background-color: var(--color-gray-50);
}
/* 日志预览文本优化 */
#diag-preview-content {
background-color: var(--color-gray-50);
border-left: 4px solid var(--color-gray-200);
min-height: 300px;
line-height: 1.5;
}
/* 聊天历史块优化 */
#chat-history .u-border {
background-color: var(--color-gray-50);
border-color: var(--color-gray-200);
}
#chat-input {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* 聊天输入内置发送按钮布局 */
.chat-input-wrap {
position: relative;
}
#chat-input {
padding-right: 96px;
}
.chat-send-btn {
position: absolute;
right: 8px;
bottom: 8px;
}
#diag-tree .diag-node-list--collapsed {
display: none;
}
.modal {
position: fixed;
inset: 0;
z-index: 50;
}
.modal__overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.35);
}
.modal__content {
position: relative;
max-width: 520px;
margin: 10% auto 0;
background: #fff;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
}

@ -0,0 +1,564 @@
/* Utility Classes */
/* 工具类样式 - 原子化 CSS */
/* 显示/隐藏 */
.u-hidden {
display: none !important;
}
.u-visible {
visibility: visible !important;
}
.u-invisible {
visibility: hidden !important;
}
.u-sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
/* 文本对齐 */
.u-text-left {
text-align: left !important;
}
.u-text-center {
text-align: center !important;
}
.u-text-right {
text-align: right !important;
}
.u-text-justify {
text-align: justify !important;
}
/* 文本颜色 */
.u-text-primary {
color: var(--color-primary) !important;
}
.u-text-secondary {
color: var(--color-secondary) !important;
}
.u-text-success {
color: var(--color-success) !important;
}
.u-text-warning {
color: var(--color-warning) !important;
}
.u-text-error {
color: var(--color-error) !important;
}
.u-text-muted {
color: var(--color-gray-500) !important;
}
.u-text-white {
color: var(--color-white) !important;
}
/* 字体大小 */
.u-text-xs {
font-size: var(--font-size-xs) !important;
}
.u-text-sm {
font-size: var(--font-size-sm) !important;
}
.u-text-base {
font-size: var(--font-size-base) !important;
}
.u-text-lg {
font-size: var(--font-size-lg) !important;
}
.u-text-xl {
font-size: var(--font-size-xl) !important;
}
.u-text-2xl {
font-size: var(--font-size-2xl) !important;
}
.u-text-3xl {
font-size: var(--font-size-3xl) !important;
}
/* 字体粗细 */
.u-font-normal {
font-weight: var(--font-weight-normal) !important;
}
.u-font-medium {
font-weight: var(--font-weight-medium) !important;
}
.u-font-semibold {
font-weight: var(--font-weight-semibold) !important;
}
.u-font-bold {
font-weight: var(--font-weight-bold) !important;
}
/* 背景颜色 */
.u-bg-primary {
background-color: var(--color-primary) !important;
}
.u-bg-secondary {
background-color: var(--color-secondary) !important;
}
.u-bg-success {
background-color: var(--color-success) !important;
}
.u-bg-warning {
background-color: var(--color-warning) !important;
}
.u-bg-error {
background-color: var(--color-error) !important;
}
.u-bg-white {
background-color: var(--color-white) !important;
}
.u-bg-gray-50 {
background-color: var(--color-gray-50) !important;
}
.u-bg-gray-100 {
background-color: var(--color-gray-100) !important;
}
.u-bg-transparent {
background-color: transparent !important;
}
/* 边距 - Margin */
.u-m-0 {
margin: 0 !important;
}
.u-m-1 {
margin: var(--spacing-1) !important;
}
.u-m-2 {
margin: var(--spacing-2) !important;
}
.u-m-3 {
margin: var(--spacing-3) !important;
}
.u-m-4 {
margin: var(--spacing-4) !important;
}
.u-m-6 {
margin: var(--spacing-6) !important;
}
.u-m-8 {
margin: var(--spacing-8) !important;
}
/* 垂直边距 */
.u-my-0 {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.u-my-2 {
margin-top: var(--spacing-2) !important;
margin-bottom: var(--spacing-2) !important;
}
.u-my-4 {
margin-top: var(--spacing-4) !important;
margin-bottom: var(--spacing-4) !important;
}
.u-my-6 {
margin-top: var(--spacing-6) !important;
margin-bottom: var(--spacing-6) !important;
}
/* 水平边距 */
.u-mx-auto {
margin-left: auto !important;
margin-right: auto !important;
}
.u-mx-2 {
margin-left: var(--spacing-2) !important;
margin-right: var(--spacing-2) !important;
}
.u-mx-4 {
margin-left: var(--spacing-4) !important;
margin-right: var(--spacing-4) !important;
}
/* 内边距 - Padding */
.u-p-0 {
padding: 0 !important;
}
.u-p-2 {
padding: var(--spacing-2) !important;
}
.u-p-4 {
padding: var(--spacing-4) !important;
}
.u-p-6 {
padding: var(--spacing-6) !important;
}
/* 垂直内边距 */
.u-py-2 {
padding-top: var(--spacing-2) !important;
padding-bottom: var(--spacing-2) !important;
}
.u-py-4 {
padding-top: var(--spacing-4) !important;
padding-bottom: var(--spacing-4) !important;
}
/* 水平内边距 */
.u-px-2 {
padding-left: var(--spacing-2) !important;
padding-right: var(--spacing-2) !important;
}
.u-px-4 {
padding-left: var(--spacing-4) !important;
padding-right: var(--spacing-4) !important;
}
.u-px-6 {
padding-left: var(--spacing-6) !important;
padding-right: var(--spacing-6) !important;
}
/* 弹性布局 */
.u-flex {
display: flex !important;
}
.u-inline-flex {
display: inline-flex !important;
}
.u-flex-col {
flex-direction: column !important;
}
.u-flex-row {
flex-direction: row !important;
}
.u-items-center {
align-items: center !important;
}
.u-items-start {
align-items: flex-start !important;
}
.u-items-end {
align-items: flex-end !important;
}
.u-justify-center {
justify-content: center !important;
}
.u-justify-between {
justify-content: space-between !important;
}
.u-justify-start {
justify-content: flex-start !important;
}
.u-justify-end {
justify-content: flex-end !important;
}
.u-flex-1 {
flex: 1 1 0% !important;
}
.u-flex-shrink-0 {
flex-shrink: 0 !important;
}
/* 网格布局 */
.u-grid {
display: grid !important;
}
.u-grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
}
.u-grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}
.u-grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}
.u-grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
}
.u-gap-2 {
gap: var(--spacing-2) !important;
}
.u-gap-4 {
gap: var(--spacing-4) !important;
}
.u-gap-6 {
gap: var(--spacing-6) !important;
}
/* 宽度 */
.u-w-full {
width: 100% !important;
}
.u-w-auto {
width: auto !important;
}
.u-w-fit {
width: fit-content !important;
}
/* 高度 */
.u-h-full {
height: 100% !important;
}
.u-h-auto {
height: auto !important;
}
.u-min-h-screen {
min-height: 100vh !important;
}
/* 边框 */
.u-border {
border: 1px solid var(--color-gray-200) !important;
}
.u-border-0 {
border: 0 !important;
}
.u-border-t {
border-top: 1px solid var(--color-gray-200) !important;
}
.u-border-b {
border-bottom: 1px solid var(--color-gray-200) !important;
}
.u-border-l {
border-left: 1px solid var(--color-gray-200) !important;
}
.u-border-r {
border-right: 1px solid var(--color-gray-200) !important;
}
/* 圆角 */
.u-rounded {
border-radius: var(--border-radius-base) !important;
}
.u-rounded-md {
border-radius: var(--border-radius-md) !important;
}
.u-rounded-lg {
border-radius: var(--border-radius-lg) !important;
}
.u-rounded-xl {
border-radius: var(--border-radius-xl) !important;
}
.u-rounded-full {
border-radius: var(--border-radius-full) !important;
}
/* 阴影 */
.u-shadow {
box-shadow: var(--shadow-base) !important;
}
.u-shadow-sm {
box-shadow: var(--shadow-sm) !important;
}
.u-shadow-md {
box-shadow: var(--shadow-md) !important;
}
.u-shadow-lg {
box-shadow: var(--shadow-lg) !important;
}
.u-shadow-none {
box-shadow: none !important;
}
/* 溢出 */
.u-overflow-hidden {
overflow: hidden !important;
}
.u-overflow-auto {
overflow: auto !important;
}
.u-overflow-x-auto {
overflow-x: auto !important;
}
.u-overflow-y-auto {
overflow-y: auto !important;
}
/* 定位 */
.u-relative {
position: relative !important;
}
.u-absolute {
position: absolute !important;
}
.u-fixed {
position: fixed !important;
}
.u-sticky {
position: sticky !important;
}
/* z-index */
.u-z-10 {
z-index: 10 !important;
}
.u-z-20 {
z-index: 20 !important;
}
.u-z-50 {
z-index: 50 !important;
}
/* 过渡动画 */
.u-transition {
transition: all var(--transition-base) !important;
}
.u-transition-fast {
transition: all var(--transition-fast) !important;
}
.u-transition-slow {
transition: all var(--transition-slow) !important;
}
/* 光标 */
.u-cursor-pointer {
cursor: pointer !important;
}
.u-cursor-not-allowed {
cursor: not-allowed !important;
}
/* 用户选择 */
.u-select-none {
user-select: none !important;
}
.u-select-all {
user-select: all !important;
}
/* 响应式工具类 */
@media (max-width: 768px) {
.u-md-hidden {
display: none !important;
}
.u-md-block {
display: block !important;
}
.u-md-flex {
display: flex !important;
}
.u-md-grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
}
.u-md-text-center {
text-align: center !important;
}
}
@media (max-width: 640px) {
.u-sm-hidden {
display: none !important;
}
.u-sm-block {
display: block !important;
}
.u-sm-text-sm {
font-size: var(--font-size-sm) !important;
}
.u-sm-p-2 {
padding: var(--spacing-2) !important;
}
}

@ -0,0 +1,497 @@
/**
* 简易前端权限与认证管理
* - 统一登录与注册事件
* - 角色admin / operator / observer
* - 路由白名单与页面元素显示控制
*/
(function () {
class AuthManager {
constructor() {
this.user = null; // { username, role }
this.approvalQueue = []; // 注册审批队列(前端占位)
this.users = [];
this._confirmCb = null;
this.restore();
this.bindForms();
this.bootstrapApprovals();
this.bootstrapUsers();
this.initUserManagement();
this.bindConfirmModal();
}
// 状态持久化
restore() {
try {
const raw = localStorage.getItem('cm_user');
if (raw) this.user = JSON.parse(raw);
const rawUsers = localStorage.getItem('cm_users_list');
if (rawUsers) this.users = JSON.parse(rawUsers);
} catch (e) {}
}
persist() {
if (this.user) {
localStorage.setItem('cm_user', JSON.stringify(this.user));
} else {
localStorage.removeItem('cm_user');
}
if (Array.isArray(this.users)) {
localStorage.setItem('cm_users_list', JSON.stringify(this.users));
}
}
isAuthenticated() {
return !!(this.user && this.user.username && this.user.role);
}
getRole() {
return this.user?.role || null;
}
getDefaultPage() {
const role = this.getRole();
if (role === 'admin') return 'cluster-list';
if (role === 'operator') return 'cluster-list';
if (role === 'observer') return 'cluster-list';
return 'login';
}
// 路由访问控制
allowRoute(page) {
// 公共页
const publicPages = ['login', 'register'];
if (publicPages.includes(page)) return true;
const role = this.getRole();
if (!role) return false;
const basePages = [
'cluster-list', 'dashboard', 'logs', 'diagnosis',
'fault-center', 'exec-logs', 'flume-config', 'alert-config', 'llm-config',
'profile', 'account', 'audit-logs'
];
if (role === 'admin') {
return basePages.concat(['user-management', 'role-assignment', 'permission-policy']).includes(page);
}
if (role === 'operator') {
// 操作者可见基础监控与故障管理 + 个人主页/账号管理
const operatorPages = ['cluster-list', 'dashboard', 'logs', 'diagnosis', 'fault-center', 'exec-logs', 'profile', 'account'];
return operatorPages.includes(page);
}
if (role === 'observer' || role === 'user') {
const observerPages = ['cluster-list', 'dashboard', 'logs', 'fault-center', 'exec-logs', 'profile', 'account'];
return observerPages.includes(page);
}
return false;
}
// 登录与注册
login(username, password) {
// 演示账号
const demo = {
admin: { u: 'admin', p: 'admin123', role: 'admin' },
ops: { u: 'ops', p: 'ops123', role: 'operator' },
obs: { u: 'obs', p: 'obs123', role: 'observer' },
};
const matched = Object.values(demo).find(d => d.u === username && d.p === password);
if (matched) {
this.user = { username, role: matched.role };
this.persist();
return { ok: true, role: matched.role };
}
return { ok: false, message: '账号或密码错误' };
}
logout() {
this.user = null;
this.persist();
this.toggleAuthUI();
}
register({ username, password, confirm, contact, role }) {
if (!username || !password || !confirm || !contact) {
return { ok: false, message: '请填写所有必填字段' };
}
if (password !== confirm) {
return { ok: false, message: '两次密码不一致' };
}
// 进入审批队列(前端占位)
const item = { username, role: role || 'operator', contact, status: 'pending' };
this.approvalQueue.push(item);
this.renderApprovalQueue();
return { ok: true };
}
// 根据角色应用界面权限
applyRolePermissions() {
const role = this.getRole();
// 侧边栏与管理员专属元素
const sidebar = document.querySelector('.sidebar');
document.querySelectorAll('[data-role="admin-only"]').forEach(el => {
el.style.display = (role === 'admin') ? '' : 'none';
});
if (sidebar) {
sidebar.style.display = (role === 'admin') ? '' : 'none';
}
// 禁用编辑操作:
// - 观察者:仅在故障中心可编辑,其他页面禁用
// - 操作者:仅在允许页面启用编辑
const isObserver = role === 'observer';
const isOperator = role === 'operator';
const current = window.navigationManager?.getCurrentPage();
const operatorEditable = ['dashboard', 'diagnosis', 'fault-center', 'exec-logs'];
document.querySelectorAll('[data-requires-edit="true"]').forEach(el => {
if (isObserver && current !== 'fault-center') {
el.setAttribute('disabled', 'true');
el.title = '观察者在当前页面不可执行变更';
} else if (isOperator && !operatorEditable.includes(current)) {
el.setAttribute('disabled', 'true');
el.title = '操作者在当前页面不可执行变更';
} else {
el.removeAttribute('disabled');
el.removeAttribute('title');
}
});
// 观察者页面:隐藏仪表板中的节点操作按钮
const nodeOpButtons = document.querySelectorAll('[data-action="start-node"],[data-action="stop-node"],[data-action="delete-node"]');
nodeOpButtons.forEach(el => {
if (isObserver) {
el.style.display = 'none';
} else {
el.style.display = '';
}
});
// 日志筛选项可见性:根据角色显示不同筛选(演示)
const show = (id, visible) => { const el = document.getElementById(id); if (el) el.parentElement.style.display = visible ? '' : 'none'; };
if (role === 'admin') {
show('log-level', true); show('source-cluster', true); show('source-node', true);
show('op-type', true); show('user-id', true); show('time-range', true);
} else if (role === 'operator') {
show('log-level', true); show('source-cluster', true); show('source-node', true);
show('op-type', true); show('user-id', false); show('time-range', true);
} else if (role === 'observer' || role === 'user') {
// 去掉用户ID筛选项
show('log-level', true); show('source-cluster', true); show('source-node', true);
show('op-type', true); show('user-id', false); show('time-range', true);
}
// 观察者删除故障诊断页面入口(仅隐藏导航项)
const diagNav = document.querySelector('.header__nav a[data-page="diagnosis"]');
if (diagNav) {
diagNav.style.display = (role === 'observer') ? 'none' : '';
}
}
// 登录/注册页与用户菜单显示切换
toggleAuthUI() {
const isAuthed = this.isAuthenticated();
const userMenu = document.querySelector('.header__user-menu');
const headerNav = document.querySelector('.header__nav');
const headerSearch = document.querySelector('.header__search');
if (userMenu) userMenu.style.display = isAuthed ? '' : 'none';
if (headerNav) headerNav.style.display = isAuthed ? '' : 'none';
if (headerSearch) headerSearch.style.display = isAuthed ? '' : 'none';
}
// 绑定表单与按钮事件
bindForms() {
// 登录
const loginForm = document.getElementById('login-form');
const goRegister = document.getElementById('go-register');
if (loginForm) {
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const username = document.getElementById('login-account')?.value.trim();
const password = document.getElementById('login-password')?.value.trim();
const res = this.login(username, password);
const msgBox = this.ensureMsgBox('login');
if (res.ok) {
msgBox.textContent = `登录成功,角色:${res.role}`;
this.toggleAuthUI();
const target = this.getDefaultPage();
window.location.hash = `#${target}`;
window.navigationManager?.switchPage(target);
} else {
msgBox.textContent = res.message || '登录失败';
}
});
}
if (goRegister) {
goRegister.addEventListener('click', () => {
window.location.hash = '#register';
window.navigationManager?.switchPage('register');
});
}
// 注册
const regForm = document.getElementById('register-form');
if (regForm) {
regForm.addEventListener('submit', (e) => {
e.preventDefault();
const payload = {
username: document.getElementById('reg-username')?.value.trim(),
password: document.getElementById('reg-password')?.value.trim(),
confirm: document.getElementById('reg-password-confirm')?.value.trim(),
contact: document.getElementById('reg-contact')?.value.trim(),
role: document.getElementById('reg-role')?.value,
};
const res = this.register(payload);
const msgBox = this.ensureMsgBox('register');
if (res.ok) {
msgBox.textContent = '提交成功,已进入审批队列';
window.location.hash = '#login';
window.navigationManager?.switchPage('login');
} else {
msgBox.textContent = res.message || '提交失败';
}
});
}
}
renderProfile() {
const role = this.getRole();
const usernameMap = { admin: 'admin', operator: 'ops', observer: 'obs' };
const emailMap = { admin: 'admin@example.com', operator: 'ops@example.com', observer: 'obs@example.com' };
const roleNameMap = { admin: '管理员', operator: '操作员', observer: '观察员' };
const u = usernameMap[role] || '-';
const e = emailMap[role] || '-';
const r = roleNameMap[role] || '-';
const unEl = document.getElementById('profile-username');
const emEl = document.getElementById('profile-email');
const rlEl = document.getElementById('profile-role');
if (unEl) unEl.textContent = u;
if (emEl) emEl.textContent = e;
if (rlEl) rlEl.textContent = r;
}
ensureMsgBox(pageId) {
const section = document.getElementById(pageId);
if (!section) return { textContent: '' };
let box = section.querySelector('.auth-msg');
if (!box) {
box = document.createElement('div');
box.className = 'auth-msg u-mt-2 u-text-sm u-text-gray-700';
section.appendChild(box);
}
return box;
}
// 渲染审批队列(管理员页)
renderApprovalQueue() {
const tbody = document.getElementById('admin-approval-tbody');
if (!tbody) return;
tbody.innerHTML = '';
this.approvalQueue.forEach((item, idx) => {
const tr = document.createElement('tr');
tr.className = 'dashboard__table-row';
tr.innerHTML = `
<td class="dashboard__table-td">${item.username}</td>
<td class="dashboard__table-td">${item.role}</td>
<td class="dashboard__table-td">${item.contact}</td>
<td class="dashboard__table-td">
<button class="btn u-text-sm" data-action="approve" data-idx="${idx}" data-requires-edit="true">通过</button>
<button class="btn u-text-sm u-ml-2" data-action="reject" data-idx="${idx}" data-requires-edit="true">拒绝</button>
</td>`;
tbody.appendChild(tr);
});
// 绑定审批按钮事件(委托)
const table = document.getElementById('admin-approval-table');
if (table && !table.__binded) {
table.__binded = true;
table.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const action = btn.getAttribute('data-action');
const idx = parseInt(btn.getAttribute('data-idx'), 10);
const item = this.approvalQueue[idx];
if (!item) return;
if (action === 'approve') {
item.status = 'approved';
this.approvalQueue.splice(idx, 1);
this.renderApprovalQueue();
} else if (action === 'reject') {
item.status = 'rejected';
this.approvalQueue.splice(idx, 1);
this.renderApprovalQueue();
}
});
}
}
bootstrapApprovals() {
if (!Array.isArray(this.approvalQueue)) this.approvalQueue = [];
if (this.approvalQueue.length === 0) {
this.approvalQueue = [
{ username: 'new-ops', role: 'operator', contact: 'ops.candidate@example.com', status: 'pending' },
{ username: 'new-obs', role: 'observer', contact: 'obs.candidate@example.com', status: 'pending' },
{ username: 'new-admin', role: 'admin', contact: 'admin.candidate@example.com', status: 'pending' }
];
this.renderApprovalQueue();
}
}
bootstrapUsers() {
if (!Array.isArray(this.users) || this.users.length === 0) {
this.users = [
{ username: 'alice', email: 'alice@example.com', role: 'admin', status: 'enabled' },
{ username: 'bob', email: 'bob@example.com', role: 'observer', status: 'pending' }
];
this.persist();
}
}
renderUsersList() {
const table = document.getElementById('admin-user-table');
if (!table) return;
const tbody = table.querySelector('tbody');
if (!tbody) return;
tbody.innerHTML = '';
(this.users || []).forEach(u => {
const tr = document.createElement('tr');
tr.className = 'dashboard__table-row';
tr.innerHTML = `
<td class="dashboard__table-td">${u.username}</td>
<td class="dashboard__table-td">${u.email}</td>
<td class="dashboard__table-td">${this.roleName(u.role)}</td>
<td class="dashboard__table-td">${this.statusBadge(u.status)}</td>
<td class="dashboard__table-td">
<button class="btn u-text-sm" data-action="ban" data-username="${u.username}" data-requires-edit="true">封禁</button>
<button class="btn u-text-sm u-ml-2" data-action="unban" data-username="${u.username}" data-requires-edit="true">解禁</button>
<button class="btn u-text-sm u-ml-2" data-action="delete" data-username="${u.username}" data-requires-edit="true">删除</button>
</td>`;
tbody.appendChild(tr);
});
}
roleName(r) {
if (r === 'admin') return '管理员';
if (r === 'operator') return '操作员';
if (r === 'observer') return '观察员';
return r || '';
}
statusBadge(s) {
if (s === 'enabled') return '<span class="u-text-success">启用</span>';
if (s === 'pending') return '<span class="u-text-warning">待审核</span>';
if (s === 'disabled') return '<span class="u-text-error">禁用</span>';
return s || '';
}
initUserManagement() {
this.renderUsersList();
const addBtn = document.getElementById('admin-add-user');
const modal = document.getElementById('admin-add-user-modal');
const form = document.getElementById('admin-add-user-form');
const cancel = document.getElementById('admin-add-user-cancel');
const err = document.getElementById('admin-add-user-error');
const userTable = document.getElementById('admin-user-table');
if (modal && userTable) {
const userArticle = userTable.closest('article');
const parent = userArticle?.parentElement;
if (parent && userArticle) {
parent.insertBefore(modal, userArticle);
}
}
if (addBtn && modal) {
addBtn.addEventListener('click', () => {
modal.style.display = '';
});
}
if (cancel && modal) {
cancel.addEventListener('click', () => {
modal.style.display = 'none';
if (err) { err.style.display = 'none'; err.textContent = ''; }
form?.reset();
});
}
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
const username = document.getElementById('new-user-username')?.value.trim();
const email = document.getElementById('new-user-email')?.value.trim();
const role = document.getElementById('new-user-role')?.value;
const status = document.getElementById('new-user-status')?.value;
if (!username || !email || !role || !status) {
if (err) { err.style.display = ''; err.textContent = '请填写完整信息'; }
return;
}
if ((this.users || []).some(u => u.username === username)) {
if (err) { err.style.display = ''; err.textContent = '用户名已存在'; }
return;
}
this.users.push({ username, email, role, status });
this.persist();
this.renderUsersList();
modal.style.display = 'none';
if (err) { err.style.display = 'none'; err.textContent = ''; }
form.reset();
});
}
if (userTable && !userTable.__binded) {
userTable.__binded = true;
userTable.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const action = btn.getAttribute('data-action');
const username = btn.getAttribute('data-username');
const idx = (this.users || []).findIndex(u => u.username === username);
if (idx === -1) return;
if (action === 'ban') {
this.showConfirm(`确认封禁用户 ${username} ?`, () => {
this.users[idx].status = 'disabled';
this.persist();
this.renderUsersList();
});
} else if (action === 'unban') {
this.showConfirm(`确认解禁用户 ${username} ?`, () => {
this.users[idx].status = 'enabled';
this.persist();
this.renderUsersList();
});
} else if (action === 'delete') {
this.showConfirm(`确认删除用户 ${username} ?`, () => {
this.users.splice(idx, 1);
this.persist();
this.renderUsersList();
});
}
});
}
}
bindConfirmModal() {
const modal = document.getElementById('confirm-modal');
const msg = document.getElementById('confirm-modal-message');
const ok = document.getElementById('confirm-modal-ok');
const cancel = document.getElementById('confirm-modal-cancel');
if (!modal || !msg || !ok || !cancel) return;
cancel.addEventListener('click', () => {
modal.style.display = 'none';
this._confirmCb = null;
});
ok.addEventListener('click', () => {
const cb = this._confirmCb;
modal.style.display = 'none';
this._confirmCb = null;
if (typeof cb === 'function') cb();
});
}
showConfirm(message, onOk) {
const modal = document.getElementById('confirm-modal');
const msg = document.getElementById('confirm-modal-message');
if (!modal || !msg) return;
msg.textContent = message || '';
this._confirmCb = onOk;
modal.style.display = '';
}
}
window.authManager = new AuthManager();
})();

@ -0,0 +1,284 @@
/**
* 图表管理工具类
* 负责初始化和管理 ECharts 图表实例
*/
class ChartManager {
constructor() {
this.charts = new Map(); // 存储图表实例
this.init();
}
/**
* 初始化所有图表
*/
init() {
// 等待 DOM 加载完成后初始化图表
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.initAllCharts();
});
} else {
this.initAllCharts();
}
}
/**
* 初始化所有图表
*/
initAllCharts() {
this.initCpuChart();
this.initMemoryChart();
// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', () => {
this.resizeAllCharts();
});
}
/**
* 初始化 CPU 使用率趋势图
*/
initCpuChart() {
const chartElement = document.getElementById('cpuChart');
if (!chartElement) return;
const chart = echarts.init(chartElement);
// CPU 使用率图表配置
const option = {
title: {
text: 'CPU 使用率 (%)',
textStyle: {
fontSize: 14,
fontWeight: 'normal',
color: '#374151'
}
},
tooltip: {
trigger: 'axis',
formatter: function(params) {
const data = params[0];
return `${data.name}<br/>CPU 使用率: ${data.value}%`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
axisLine: {
lineStyle: {
color: '#E5E7EB'
}
},
axisLabel: {
color: '#6B7280'
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLine: {
lineStyle: {
color: '#E5E7EB'
}
},
axisLabel: {
color: '#6B7280',
formatter: '{value}%'
},
splitLine: {
lineStyle: {
color: '#F3F4F6'
}
}
},
series: [{
name: 'CPU 使用率',
type: 'line',
smooth: true,
data: [20, 35, 45, 60, 55, 40, 30],
lineStyle: {
color: '#3B82F6',
width: 2
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: 'rgba(59, 130, 246, 0.3)'
}, {
offset: 1,
color: 'rgba(59, 130, 246, 0.05)'
}]
}
},
itemStyle: {
color: '#3B82F6'
}
}]
};
chart.setOption(option);
this.charts.set('cpu', chart);
}
/**
* 初始化内存使用情况图
*/
initMemoryChart() {
const chartElement = document.getElementById('memoryChart');
if (!chartElement) return;
const chart = echarts.init(chartElement);
// 内存使用情况图表配置
const option = {
title: {
text: '内存使用情况',
textStyle: {
fontSize: 14,
fontWeight: 'normal',
color: '#374151'
}
},
tooltip: {
trigger: 'item',
formatter: function(params) {
return `${params.name}<br/>使用量: ${params.value} GB (${params.percent}%)`;
}
},
legend: {
orient: 'horizontal',
bottom: '0%',
textStyle: {
color: '#6B7280'
}
},
series: [{
name: '内存使用',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '45%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '16',
fontWeight: 'bold',
color: '#374151'
}
},
labelLine: {
show: false
},
data: [
{
value: 8.5,
name: '已使用',
itemStyle: {
color: '#EF4444'
}
},
{
value: 15.5,
name: '可用',
itemStyle: {
color: '#10B981'
}
}
]
}]
};
chart.setOption(option);
this.charts.set('memory', chart);
}
/**
* 调整所有图表大小
*/
resizeAllCharts() {
this.charts.forEach(chart => {
chart.resize();
});
}
/**
* 更新 CPU 图表数据
* @param {Array} data - 新的数据数组
*/
updateCpuChart(data) {
const chart = this.charts.get('cpu');
if (chart && data) {
chart.setOption({
series: [{
data: data
}]
});
}
}
/**
* 更新内存图表数据
* @param {Object} data - 内存使用数据 {used: number, available: number}
*/
updateMemoryChart(data) {
const chart = this.charts.get('memory');
if (chart && data) {
chart.setOption({
series: [{
data: [
{
value: data.used,
name: '已使用',
itemStyle: {
color: '#EF4444'
}
},
{
value: data.available,
name: '可用',
itemStyle: {
color: '#10B981'
}
}
]
}]
});
}
}
/**
* 销毁所有图表实例
*/
dispose() {
this.charts.forEach(chart => {
chart.dispose();
});
this.charts.clear();
}
}
// 创建全局图表管理器实例
window.chartManager = new ChartManager();

File diff suppressed because it is too large Load Diff

@ -0,0 +1,355 @@
/**
* 响应式交互管理工具类
* 负责移动端菜单侧边栏切换和响应式行为
*/
class ResponsiveManager {
constructor() {
this.isMobile = false;
this.isTablet = false;
this.sidebarOpen = false;
this.mobileMenuOpen = false;
this.init();
}
/**
* 初始化响应式功能
*/
init() {
// 等待 DOM 加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setup();
});
} else {
this.setup();
}
}
/**
* 设置响应式功能
*/
setup() {
this.createMobileElements();
this.bindEvents();
this.checkScreenSize();
// 监听窗口大小变化
window.addEventListener('resize', () => {
this.handleResize();
});
}
/**
* 创建移动端需要的元素
*/
createMobileElements() {
this.createMobileMenuButton();
this.createSidebarOverlay();
}
/**
* 创建移动端菜单按钮
*/
createMobileMenuButton() {
const headerLeft = document.querySelector('.header__left');
if (!headerLeft) return;
// 检查是否已存在
if (document.querySelector('.header__mobile-menu-btn')) return;
const mobileMenuBtn = document.createElement('button');
mobileMenuBtn.className = 'header__mobile-menu-btn';
mobileMenuBtn.innerHTML = '<i class="fas fa-bars" aria-hidden="true"></i>';
mobileMenuBtn.setAttribute('aria-label', '打开导航菜单');
mobileMenuBtn.setAttribute('aria-expanded', 'false');
// 插入到 logo 之后
const logo = headerLeft.querySelector('.header__logo');
if (logo && logo.nextSibling) {
headerLeft.insertBefore(mobileMenuBtn, logo.nextSibling);
} else {
headerLeft.appendChild(mobileMenuBtn);
}
}
/**
* 创建侧边栏遮罩层
*/
createSidebarOverlay() {
const container = document.querySelector('.layout__container');
if (!container) return;
// 检查是否已存在
if (document.querySelector('.sidebar-overlay')) return;
const overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
overlay.setAttribute('aria-hidden', 'true');
container.appendChild(overlay);
}
/**
* 绑定事件监听器
*/
bindEvents() {
// 移动端菜单按钮
const mobileMenuBtn = document.querySelector('.header__mobile-menu-btn');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', (e) => {
e.preventDefault();
this.toggleMobileMenu();
});
}
// 侧边栏遮罩层点击
const overlay = document.querySelector('.sidebar-overlay');
if (overlay) {
overlay.addEventListener('click', () => {
this.closeSidebar();
});
}
// ESC 键关闭菜单
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.closeMobileMenu();
this.closeSidebar();
}
});
// 导航项点击时关闭移动端菜单
const navItems = document.querySelectorAll('.header__nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
if (this.isMobile) {
this.closeMobileMenu();
}
});
});
}
/**
* 检查屏幕尺寸
*/
checkScreenSize() {
const width = window.innerWidth;
this.isMobile = width <= 768;
this.isTablet = width > 768 && width <= 1024;
// 根据屏幕尺寸调整布局
this.adjustLayout();
}
/**
* 处理窗口大小变化
*/
handleResize() {
const wasMobile = this.isMobile;
this.checkScreenSize();
// 如果从移动端切换到桌面端,关闭移动端菜单
if (wasMobile && !this.isMobile) {
this.closeMobileMenu();
this.closeSidebar();
}
// 通知图表管理器调整大小
if (window.chartManager) {
setTimeout(() => {
window.chartManager.resizeAllCharts();
}, 300);
}
}
/**
* 调整布局
*/
adjustLayout() {
const body = document.body;
// 添加屏幕尺寸类名
body.classList.remove('is-mobile', 'is-tablet', 'is-desktop');
if (this.isMobile) {
body.classList.add('is-mobile');
} else if (this.isTablet) {
body.classList.add('is-tablet');
} else {
body.classList.add('is-desktop');
}
}
/**
* 切换移动端菜单
*/
toggleMobileMenu() {
if (this.mobileMenuOpen) {
this.closeMobileMenu();
} else {
this.openMobileMenu();
}
}
/**
* 打开移动端菜单
*/
openMobileMenu() {
const nav = document.querySelector('.header__nav');
const btn = document.querySelector('.header__mobile-menu-btn');
if (nav && btn) {
nav.classList.add('header__nav--mobile-open');
btn.setAttribute('aria-expanded', 'true');
btn.innerHTML = '<i class="fas fa-times" aria-hidden="true"></i>';
this.mobileMenuOpen = true;
// 阻止背景滚动
document.body.style.overflow = 'hidden';
}
}
/**
* 关闭移动端菜单
*/
closeMobileMenu() {
const nav = document.querySelector('.header__nav');
const btn = document.querySelector('.header__mobile-menu-btn');
if (nav && btn) {
nav.classList.remove('header__nav--mobile-open');
btn.setAttribute('aria-expanded', 'false');
btn.innerHTML = '<i class="fas fa-bars" aria-hidden="true"></i>';
this.mobileMenuOpen = false;
// 恢复背景滚动
document.body.style.overflow = '';
}
}
/**
* 切换侧边栏
*/
toggleSidebar() {
if (this.sidebarOpen) {
this.closeSidebar();
} else {
this.openSidebar();
}
}
/**
* 打开侧边栏
*/
openSidebar() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar && overlay) {
sidebar.classList.add('sidebar--mobile-open');
overlay.classList.add('sidebar-overlay--show');
this.sidebarOpen = true;
// 阻止背景滚动
document.body.style.overflow = 'hidden';
}
}
/**
* 关闭侧边栏
*/
closeSidebar() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar && overlay) {
sidebar.classList.remove('sidebar--mobile-open');
overlay.classList.remove('sidebar-overlay--show');
this.sidebarOpen = false;
// 恢复背景滚动
document.body.style.overflow = '';
}
}
/**
* 获取当前屏幕类型
* @returns {string} 屏幕类型'mobile', 'tablet', 'desktop'
*/
getScreenType() {
if (this.isMobile) return 'mobile';
if (this.isTablet) return 'tablet';
return 'desktop';
}
/**
* 检查是否为移动端
* @returns {boolean}
*/
isMobileDevice() {
return this.isMobile;
}
/**
* 检查是否为平板端
* @returns {boolean}
*/
isTabletDevice() {
return this.isTablet;
}
/**
* 检查是否为桌面端
* @returns {boolean}
*/
isDesktopDevice() {
return !this.isMobile && !this.isTablet;
}
/**
* 添加触摸手势支持
*/
addTouchSupport() {
let startX = 0;
let startY = 0;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!this.isMobile) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const deltaX = endX - startX;
const deltaY = endY - startY;
// 水平滑动距离大于垂直滑动距离,且滑动距离足够
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
// 从左边缘向右滑动,打开侧边栏
if (startX < 50 && deltaX > 0 && !this.sidebarOpen) {
this.openSidebar();
}
// 向左滑动,关闭侧边栏
else if (deltaX < -50 && this.sidebarOpen) {
this.closeSidebar();
}
}
}, { passive: true });
}
}
// 创建全局响应式管理器实例
window.responsiveManager = new ResponsiveManager();
// 添加触摸手势支持
window.responsiveManager.addTouchSupport();
Loading…
Cancel
Save