diff --git a/doc/process/weekly/week-5/members/zoujiaxuan-weekly-summary-5.md b/doc/process/weekly/week-5/members/zoujiaxuan-weekly-summary-5.md
new file mode 100644
index 0000000..8b5b250
--- /dev/null
+++ b/doc/process/weekly/week-5/members/zoujiaxuan-weekly-summary-5.md
@@ -0,0 +1,55 @@
+# 邹佳轩第五周个人工作总结(Week 5 Summary)
+
+**总结周期:** 2025-10-21 至 2025-10-27
+**总结人:** 邹佳轩
+**本周角色:** 团队会议记录员
+
+## 一、周目标与总体进展
+- 目标概述:集群稳定性测试、HDFS 基本操作熟练、MapReduce 示例实践、配置优化与文档补充。
+- 总体进展:按计划完成稳定性测试与 HDFS 操作练习,完成 WordCount 示例运行与日志分析,应用标准化配置模板并进行 1G 内存优化,第四周遗留文档基本补齐。
+
+## 二、关键工作与成果
+- 部署巩固与稳定性测试(周一-周二)
+ - 验证 5 台虚拟机与 Hadoop 各组件运行状态,修复部分服务启动问题。
+ - 进行 HDFS 稳定性测试:上传 1G 测试文件、验证 3 副本策略、检查各节点运行情况。
+ - 输出《集群稳定性测试报告》初稿并记录测试过程与截图。
+- HDFS 基本操作练习(周三)
+ - 熟练掌握 `hdfs dfs -ls/-mkdir/-put/-get` 等常用命令;完成权限管理与目录结构规范化。
+ - 覆盖上传/下载/浏览/查看/删除等操作场景,形成命令清单与注意事项。
+- MapReduce 应用实践(周四-周五)
+ - 准备 WordCount 输入数据与作业提交命令,成功运行示例程序并验证完整流程。
+ - 分析执行日志,理解 Map 与 Reduce 阶段关键过程与性能指标。
+- 配置标准化与内存优化(贯穿本周)
+ - 应用团队标准化配置模板;在 1G 内存环境下进行参数优化(如 `HADOOP_HEAPSIZE`/`YARN_HEAPSIZE` 调整至 512M)。
+ - 按需停用不使用组件,降低内存占用并优化服务启动顺序。
+- 文档补充与记录
+ - 完成第四周遗留截图与文档补充;完善操作记录与命令说明。
+
+## 三、问题与解决
+- DataNode 连接与主机解析问题:
+ - 通过核对 `/etc/hosts`、防火墙/SELinux 状态与 `hdfs-site.xml` 端口配置,解决若干节点通信异常。
+- 内存不足与服务稳定性:
+ - 采用堆内存限制与组件精简策略,监控 JVM 堆使用并优化启动顺序,稳定集群运行。
+- 作业监控与日志定位:
+ - 梳理作业日志查看路径与关键字段,形成问题定位流程。
+
+## 四、经验与收获
+- 标准化配置显著提升部署一致性与问题定位效率。
+- 在低内存环境下,合理的参数与组件取舍是稳定运行的关键。
+- 对 HDFS 常用命令的熟练掌握,直接提升日常运维与验证效率。
+- 通过 WordCount 示例,完成了对 MapReduce 执行路径与指标的初步理解。
+
+## 五、量化达成情况
+- 稳定性测试:主要用例通过率 100%,完成报告与截图记录。
+- HDFS 操作:覆盖 20+ 常用命令与场景练习,形成清单与注意事项。
+- MapReduce 应用:成功运行 WordCount 示例并完成日志分析。
+- 文档补充:第四周相关文档与截图基本补齐,完成度 95%+。
+
+## 六、下周计划与准备(MapReduce 原理)
+- 收集与整理官方与社区资料,明确核心逻辑与关键参数说明。
+- 结合本周实践,设计原理文档结构与流程示意图。
+- 规划验证实验与示例代码,完善监控与日志分析方法。
+
+
+
+
diff --git a/doc/process/weekly/week-6/members/zoujiaxuan-weekly-plan-6.md b/doc/process/weekly/week-6/members/zoujiaxuan-weekly-plan-6.md
new file mode 100644
index 0000000..0d09a7b
--- /dev/null
+++ b/doc/process/weekly/week-6/members/zoujiaxuan-weekly-plan-6.md
@@ -0,0 +1,92 @@
+# 第六周个人周计划(运维组)— 邹嘉轩(zoujiaxuan-weekly-plan-6)
+
+## 本周总体目标
+- 完成 Hadoop 集群的基础部署与可用性验收(至少 1 NameNode + 2 DataNode)。
+- 建立初始日志采集链路(Flume Agent → 后端接口/存储),验证端到端可达与延迟。
+- 搭建监控与告警(Prometheus/Grafana + JMX/NodeExporter),形成首版仪表盘与阈值规则。
+- 编写并演练关键故障的自动恢复脚本(服务重启、节点隔离、数据副本修复)。
+- 输出本周《系统运行评估报告》,并与项目组对齐迭代项。
+
+说明依据:
+- 会议纪要(meeting-minutes-6.md):本周启动 Hadoop 集群部署、推进技术选型和协作规范;运维组负责集群部署方案与落地。
+- 项目核心任务说明:强调故障检测与自动恢复闭环,周度评估维度包含“准确性≥85%”“时效性≤5 分钟”“有效性≤5%复发”。
+
+## 关键任务与交付
+
+1) 集群部署与验收(Hadoop 3.x)
+- 环境准备:主机名与 `/etc/hosts` 规划、NTP/时间同步、开放端口与防火墙策略、SSH 免密。
+- 组件安装:JDK、Hadoop;配置 `core-site.xml`、`hdfs-site.xml`、`yarn-site.xml`、`mapred-site.xml`。
+- 初始化与启动:格式化 HDFS(`hdfs namenode -format`)、启动 HDFS/YARN;新增 2 台 DataNode 加入集群。
+- 验收检查:
+ - 可用性:`hdfs dfsadmin -report`、`yarn node -list`、跑一个示例 MapReduce 任务验证。
+ - 健康度:DataNode 副本分布、磁盘水位、NN/DN 心跳稳定性。
+- 交付物:部署与拓扑文档、配置文件清单、验收截图/日志。
+
+2) 日志采集链路(Flume)
+- 采集范围:Hadoop/HDFS/YARN 组件日志、系统关键日志(如 `syslog`)。
+- 部署方式:在各节点部署 Flume Agent(会议纪要明确由运维组推进)。
+- 传输与落地:先对接后端接口/存储(MySQL/Redis),形成最小可用链路;记录端到端延迟。
+- 验收指标:
+ - 日志入库成功率≥95%、平均延迟≤60 秒。
+ - 后端接口无错误、存储表结构与索引准备就绪(与后端协作)。
+- 交付物:Flume 配置文件、数据流图、延迟与成功率统计。
+
+3) 故障检测与自动恢复基础能力
+- 告警规则:
+ - NN/DN 离线或心跳异常;Block 丢失/副本不足;磁盘使用率>85%;YARN 队列阻塞;Job 超时。
+- 恢复脚本:
+ - 服务级:NameNode/ResourceManager/NodeManager/HDFS 守护进程重启。
+ - 节点级:隔离故障节点、触发 `hdfs balancer`、副本修复校验(`hdfs fsck`)。
+- 演练场景:
+ - 场景 A:DataNode 短时离线 → 自动重启与告警关闭;
+ - 场景 B:Block 丢失 → 触发副本修复并校验成功。
+- 交付物:脚本与演练记录(操作步骤、结果截图、复盘结论)。
+
+4) 监控与告警(可观测性)
+- 指标采集:Node Exporter、JMX Exporter(HDFS/YARN 关键指标)。
+- 仪表盘:Grafana 初版大盘(集群总览、节点视图、组件视图、告警面板)。
+- 告警通道:企业微信/邮件(选择其一先打通),设定阈值与抑制策略。
+- 交付物:Grafana 大盘链接/截图、告警规则清单。
+
+5) 备份与回滚策略
+- 内容:FSImage、EditLogs 定期备份;Hadoop/Flume 配置版本化;重要数据快照策略。
+- 恢复演练:模拟 NameNode 配置误改的回滚恢复。
+- 交付物:备份/回滚 SOP、演练记录。
+
+6) 周度评估与改进建议
+- 指标口径(对齐项目核心任务说明):
+ - 准确性:故障诊断准确率,目标≥85%。
+ - 时效性:从日志产生到修复完成的平均耗时,目标≤5 分钟。
+ - 有效性:修复后故障复发率≤5%。
+- 数据来源:Flume→后端→MySQL `fault_record` 表(与后端确认字段口径)。
+- 报告输出:《系统运行评估报告》(含三维度指标、环比变化、异常案例)。
+- 迭代建议:针对低指标项提出具体优化方案(责任人/截止时间)。
+
+## 时间安排(第六周)
+- 周一:集群环境准备(主机名/时间同步/SSH 免密/端口与防火墙);部署 JDK/Hadoop。
+- 周二:配置核心文件、初始化 HDFS、启动 HDFS/YARN;完成最小集群可运行。
+- 周三:扩容 DataNode、健康验收(报告输出);对接后端存储准备。
+- 周四:部署 Flume Agent 与采集链路打通;采集范围与频率校准。
+- 周五:搭建监控(Prometheus/Grafana/JMX/NodeExporter);设定首版告警规则。
+- 周六:编写自动恢复脚本并演练两类故障场景;形成演练记录与改进清单。
+- 周日:生成《系统运行评估报告》与本周运维交付清单;与项目组评审并创建下周迭代任务。
+
+## 协作与依赖
+- 与后端:确认日志入库表结构与接口稳定性;提供数据口径与查询方式。
+- 与测试:联合设计故障注入与恢复验证用例;采集验证数据。
+- 与前端:仪表盘展示需求与告警面板原型对齐。
+- 变更管理:所有变更走申请与回退预案,避免生产风险。
+
+## 风险与应对
+- 技术风险:部署复杂度高 → 采用详细 SOP 与分阶段验收,邀请有经验同学指导。
+- 进度风险:周内任务多 → 每日站会复核完成度,优先保证“集群可用+日志链路可达”。
+- 协作风险:跨组接口不一致 → 建立数据口径清单与每两天一次对齐沟通。
+
+## 本周交付物清单
+- 集群部署与验收文档、配置文件清单、拓扑图。
+- Flume 配置与采集链路验证报告。
+- 监控仪表盘与告警规则清单。
+- 自动恢复脚本与演练记录。
+- 《系统运行评估报告》(含三维度指标与迭代建议)。
+
+
diff --git a/src/frontend/README.md b/src/frontend/README.md
new file mode 100644
index 0000000..a1a87be
--- /dev/null
+++ b/src/frontend/README.md
@@ -0,0 +1,599 @@
+# ErrorDetecting 前端项目
+
+## 项目简介
+
+ErrorDetecting 是一个分布式系统错误检测与监控平台的前端应用,提供直观的用户界面来监控系统状态、管理故障和分析日志。
+
+## 功能特性
+
+- 🎯 **概览仪表板** - 系统整体运行状态概览
+- 🖥️ **集群监控** - 实时监控集群节点状态和性能指标
+- 🚨 **故障管理** - 故障报告、处理和跟踪管理
+- 📊 **日志分析** - 日志查询、分析和可视化
+- 👤 **用户管理** - 用户权限和角色管理
+- ⚙️ **系统配置** - 系统参数和配置管理
+- 📱 **响应式设计** - 支持多种设备和屏幕尺寸
+
+## 技术栈
+
+- **HTML5** - 页面结构
+- **CSS3** - 样式设计,包含响应式布局
+- **JavaScript (ES6+)** - 交互逻辑和数据处理
+- **Chart.js** - 数据可视化图表
+- **Font Awesome** - 图标库
+
+## 项目结构
+
+```
+frontend/
+├── index.html # 主页面入口
+├── styles/ # 样式文件目录
+│ ├── main.css # 主样式文件
+│ ├── login.css # 登录页面样式
+│ ├── cluster-monitor.css # 集群监控页面样式
+│ ├── fault-manage.css # 故障管理页面样式
+│ ├── log-analysis.css # 日志分析页面样式
+│ ├── components.css # 通用组件样式
+│ └── responsive.css # 响应式设计样式
+├── js/ # JavaScript文件目录
+│ ├── app.js # 主应用逻辑
+│ ├── components.js # 通用组件库
+│ ├── charts.js # 图表组件库
+│ └── demo-data.js # 演示数据管理
+├── views/ # 页面视图目录
+├── components/ # 可复用组件
+├── api/ # API接口封装
+├── utils/ # 工具函数
+├── router/ # 路由配置
+└── README.md # 项目说明文档
+```
+
+## 快速开始
+
+### 1. 环境要求
+
+- 现代浏览器 (Chrome, Firefox, Safari, Edge)
+- Python 3.x (用于本地开发服务器)
+
+### 2. 启动项目
+
+```bash
+# 进入前端目录
+cd src/frontend
+
+# 启动本地HTTP服务器
+python -m http.server 8080
+
+# 或者使用Node.js
+npx http-server -p 8080
+```
+
+### 3. 访问应用
+
+打开浏览器访问: http://localhost:8080
+
+### 4. 默认登录信息
+
+- 用户名: admin
+- 密码: admin123
+
+## 页面功能说明
+
+### 概览仪表板
+- 显示系统整体运行状态
+- 在线节点数量、活跃告警、资源使用率等关键指标
+- 系统性能趋势图表
+
+### 集群监控
+- 实时监控所有节点状态
+- 节点性能指标 (CPU、内存、磁盘使用率)
+- 节点管理操作 (重启、停止、详情查看)
+- 资源使用趋势分析
+
+### 故障管理
+- 故障列表查看和筛选
+- 故障详情查看和处理
+- 故障统计和分析
+- 故障报告导出
+
+### 日志分析
+- 多条件日志查询
+- 日志级别和服务筛选
+- 日志趋势分析图表
+- 实时日志监控
+
+## 组件库
+
+### 通用组件
+- **Modal** - 模态框组件
+- **Loading** - 加载动画组件
+- **Notification** - 通知组件
+- **Confirm** - 确认对话框
+- **Alert** - 提示框
+- **Tooltip** - 工具提示
+- **Dropdown** - 下拉菜单
+
+### 图表组件
+- **LineChart** - 线性图表
+- **BarChart** - 柱状图表
+- **PieChart** - 饼图
+- **DoughnutChart** - 环形图
+- **RealTimeChart** - 实时图表
+
+## 响应式设计
+
+项目采用响应式设计,支持以下设备:
+
+- 📱 **移动设备** (< 480px)
+- 📱 **小屏平板** (480px - 767px)
+- 💻 **中等屏幕** (768px - 1023px)
+- 🖥️ **大屏幕** (1024px - 1199px)
+- 🖥️ **超大屏幕** (≥ 1200px)
+
+## 浏览器支持
+
+- Chrome ≥ 60
+- Firefox ≥ 55
+- Safari ≥ 12
+- Edge ≥ 79
+
+## 开发指南
+
+### 添加新页面
+
+1. 在 `views/` 目录下创建页面文件夹
+2. 创建对应的HTML和CSS文件
+3. 在 `app.js` 中添加路由配置
+4. 在侧边栏导航中添加菜单项
+
+### 使用组件
+
+```javascript
+// 显示通知
+Notification.show('操作成功', 'success');
+
+// 显示确认对话框
+const result = await Confirm.show('确定要删除吗?');
+
+// 创建图表
+const chart = ChartFactory.createLineChart('chartCanvas', data);
+```
+
+### 获取演示数据
+
+```javascript
+// 获取仪表板数据
+const dashboardData = demoData.getDashboardData();
+
+// 搜索故障
+const faults = demoData.searchFaults('CPU', { level: 'critical' });
+
+// 获取实时数据
+const realtimeData = demoData.getRealtimeData();
+```
+
+## 部署说明
+
+### 生产环境部署
+
+1. 将所有文件上传到Web服务器
+2. 配置Web服务器支持SPA路由
+3. 确保所有静态资源可正常访问
+4. 配置HTTPS (推荐)
+
+### Nginx配置示例
+
+```nginx
+server {
+ listen 80;
+ server_name your-domain.com;
+ root /path/to/frontend;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+}
+```
+
+## 性能优化
+
+- 使用CDN加载第三方库
+- 启用Gzip压缩
+- 图片懒加载
+- 代码分割和按需加载
+- 缓存策略优化
+
+## 故障排除
+
+### 常见问题
+
+1. **页面无法加载**
+ - 检查HTTP服务器是否正常启动
+ - 确认端口号是否被占用
+
+2. **图表不显示**
+ - 检查Chart.js库是否正确加载
+ - 确认canvas元素是否存在
+
+3. **样式异常**
+ - 检查CSS文件路径是否正确
+ - 确认浏览器是否支持CSS特性
+
+## 贡献指南
+
+1. Fork 项目
+2. 创建功能分支
+3. 提交更改
+4. 推送到分支
+5. 创建Pull Request
+
+## 许可证
+
+本项目采用 MIT 许可证。
+
+## 联系方式
+
+如有问题或建议,请联系开发团队。
+
+---
+
+## 原始开发文档
+
+以下是项目的原始开发文档和需求分析:
+
+### 项目背景
+
+在基于 Hadoop 的故障检测与自动恢复项目中,前端开发者需围绕"运维人员可视化操作入口"核心目标,从需求对齐到上线维护全流程参与,具体工作可按**项目阶段**拆解为以下详细内容,同时紧密贴合项目文档(前景与范围、用例、核心任务)中的前端相关需求:
+
+### 一、项目前期:需求对齐与技术准备(项目启动-基础搭建阶段)
+此阶段核心是明确"做什么"和"用什么做",避免后期需求偏差,对应文档中"Web 应用功能范围""用例场景"等内容。
+
+#### 1. 需求分析与边界确认
+需与产品、后端、运维人员同步,明确前端核心功能边界,重点对齐以下内容:
+- **功能范围确认**(参考《前景与范围文档》4.1 功能范围):
+ - 必做:登录、集群监控(节点状态/资源趋势)、故障管理(列表/详情/修复)、日志分析(查询/AI 提交);
+ - 不做:非 Hadoop 组件(如 Spark)的监控、跨集群管理、离线日志回溯(仅实时日志)。
+- **用例场景拆解**(参考《用例文档》核心用例):
+ - 梳理每个用例的前端交互细节,例如:
+ - 故障修复(UC-006):需按风险分级展示按钮(低风险"自动修复"、中风险"确认修复"、高风险"申请审批"),并实时展示执行日志;
+ - AI 日志分析(UC-008):需限制最多勾选 10 条日志,分析超时后提示"后台继续处理,结果将推送"。
+- **运维人员体验诉求**:
+ - 深色主题适配(运维场景多在夜间使用,减少视觉疲劳);
+ - 操作简洁(核心功能如"修复""刷新"按钮突出,避免多层级跳转);
+ - 异常提示明确(如接口失败时需区分"集群连接中断""Token 过期"等场景)。
+
+#### 2. 技术栈选型与版本确认
+基于《核心任务说明文档》任务 5 明确的技术栈,进一步确认版本兼容性与工具链:
+- **核心技术栈**(固定):
+ - 框架:Vue 3.3+(Composition API 风格,便于逻辑复用);
+ - 构建工具:Vite 4.0+(比 Webpack 快,适配 Vue 3,支持热更新);
+ - UI 组件库:Element Plus 2.3+(需支持表格、弹窗、表单,适配 Vue 3);
+ - 图表库:ECharts 5.4+(需支持折线图(CPU/磁盘趋势)、表格标注(异常节点标红));
+ - 网络请求:Axios 1.4+(处理接口拦截、Token 携带、跨域);
+- **补充工具**:
+ - 代码规范:ESLint(Vue 3 规则)+ Prettier(统一格式);
+ - 样式方案:CSS 变量(实现深色/浅色主题切换)+ SCSS(嵌套样式,提高可维护性);
+ - 路由:Vue Router 4.2+(需支持 history 模式,配合 Nginx 部署)。
+
+### 二、项目初始化:前端架构搭建(应用开发阶段前期)
+此阶段需搭建可扩展的项目架构,为后续开发提效,避免后期重构。
+
+#### 1. 项目初始化与配置
+- **创建项目**:
+ ```bash
+ npm create vite@latest hadoop-fault-front -- --template vue # 初始化 Vue 3 项目
+ cd hadoop-fault-front && npm install # 安装依赖
+ npm install element-plus echarts axios vue-router@4 # 安装核心依赖
+ ```
+- **核心配置(vite.config.js)**:
+ - 跨域代理(开发环境调用后端 FastAPI 接口需解决跨域):
+ ```javascript
+ export default defineConfig({
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000', // 后端开发环境地址
+ changeOrigin: true, // 允许跨域
+ rewrite: (path) => path.replace(/^\/api/, '') // 去掉请求路径中的 /api 前缀
+ }
+ }
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src') // 路径别名,@ 指向 src 目录
+ }
+ }
+ })
+ ```
+ - 环境变量:创建 `.env.development`(开发环境)和 `.env.production`(生产环境),配置 API 基础地址:
+ ```env
+ # .env.development
+ VITE_API_BASE_URL = '/api' # 开发环境(走代理)
+ # .env.production
+ VITE_API_BASE_URL = 'http://生产后端IP:8000' # 生产环境(直接调用后端)
+ ```
+
+### 2. 目录结构设计(按功能拆分,便于维护)
+```
+frontend/
+├── api/ # 接口请求封装(按模块拆分)
+│ ├── user.js # 登录、退出接口
+│ ├── cluster.js# 集群状态接口
+│ ├── fault.js # 故障管理接口
+│ └── log.js # 日志查询、AI 分析接口
+├── components/ # 公共组件(复用性强)
+│ ├── Layout/ # 页面布局(侧边栏+顶部栏)
+│ ├── Common/ # 通用组件(加载动画、错误弹窗、确认弹窗)
+│ └── Chart/ # 图表组件(CPU/磁盘趋势图、异常节点表格)
+├── views/ # 核心页面(按功能模块拆分)
+│ ├── Login/ # 登录页
+│ ├── ClusterMonitor/ # 集群监控页
+│ ├── FaultManage/ # 故障管理页(列表+详情)
+│ └── LogAnalysis/ # 日志分析页
+├── router/ # 路由配置
+│ └── index.js # 路由规则(含权限守卫)
+├── utils/ # 工具函数
+│ ├── request.js# Axios 实例封装(拦截器、错误处理)
+│ ├── format.js # 时间格式化、日志筛选等工具
+│ └── auth.js # Token 存储/获取/删除
+├── styles/ # 全局样式
+│ ├── main.scss # 全局样式入口(引入 Element Plus 主题、CSS 变量)
+│ └── dark.scss # 深色主题样式
+└── App.vue # 根组件(路由出口)
+```
+
+
+## 三、核心开发:公共组件与页面实现(应用开发阶段中期)
+此阶段需优先开发公共组件(复用),再实现核心页面,严格贴合用例场景与交互需求。
+
+### 1. 公共组件开发(优先完成,减少重复代码)
+#### (1)基础布局组件(src/components/Layout/Index.vue)
+- **功能**:包含侧边栏(导航菜单)、顶部栏(用户信息、刷新、退出),适配所有页面;
+- **关键实现**:
+ - 侧边栏菜单:按“集群监控→故障管理→日志分析”排序(运维高频操作优先),用 Element Plus 的 `ElMenu` 组件;
+ - 顶部栏:显示当前登录用户(从 localStorage 获取),“退出”按钮触发清除 Token 并跳转登录页;
+ - 深色主题切换:用 Element Plus 的 `ElSwitch` 控制 `document.documentElement.classList` 切换 `dark` 类,配合 CSS 变量生效。
+
+#### (2)通用工具组件
+- **加载组件(src/components/Common/Loading.vue)**:
+ - 场景:接口请求时显示(如集群状态加载、AI 分析中);
+ - 实现:用 Element Plus 的 `ElLoading` 或自定义全屏加载动画,支持传入“加载文案”(如“AI 分析中,约 30 秒”)。
+- **错误弹窗组件(src/components/Common/ErrorTip.vue)**:
+ - 场景:接口请求失败(如集群连接中断、Token 过期);
+ - 实现:封装 Element Plus 的 `ElMessage`,支持区分错误类型(网络错误→“请检查网络”;业务错误→后端返回的错误信息)。
+- **确认弹窗组件(src/components/Common/Confirm.vue)**:
+ - 场景:中风险故障修复、账号禁用等需确认的操作;
+ - 实现:封装 Element Plus 的 `ElMessageBox`,支持传入“标题”“内容”“确认按钮文案”,返回 Promise 便于后续处理。
+
+#### (3)图表组件(src/components/Chart/ResourceTrend.vue)
+- **功能**:展示节点 CPU/磁盘使用率趋势(用 ECharts 折线图);
+- **关键实现**:
+ - 数据适配:接收后端返回的“时间轴+使用率数组”,格式化为 ECharts 所需的 `xAxis.data` 和 `series.data`;
+ - 异常标注:当使用率超过阈值(如磁盘 85%)时,用 ECharts 的 `markLine` 画红色警戒线,并标注“告警阈值”;
+ - 懒加载:用 Vue 的 `v-intersect` 指令(需安装 `@vueuse/core`),滚动到图表区域才初始化,减少首屏加载时间。
+
+### 2. 核心页面实现(按用例优先级开发,高优先级先做)
+#### (1)登录页(src/views/Login/Index.vue,用例 UC-001)
+- **功能**:账号密码验证,对接后端 `/api/user/login` 接口;
+- **关键实现**:
+ - 表单验证:用 Element Plus 的 `ElForm` 做规则校验(账号不能为空、密码≥6 位且含特殊字符);
+ - Token 处理:登录成功后,将后端返回的 Token 存入 localStorage(如 `localStorage.setItem('token', res.data.token)`),并跳转首页(`router.push('/cluster-monitor')`);
+ - 异常处理:账号禁用→提示“账号已禁用,请联系管理员”;密码错误→提示“账号或密码错误”(后端返回对应的错误码,前端匹配处理)。
+
+#### (2)集群监控页(src/views/ClusterMonitor/Index.vue,用例 UC-002、UC-003)
+- **功能**:展示节点列表(状态、角色、资源使用率)、CPU/磁盘趋势图,定时 5 分钟刷新;
+- **关键实现**:
+ - 节点列表:用 Element Plus 的 `ElTable`,按“角色”分组(NameNode、DataNode),异常状态标注(离线→标红、磁盘>90%→标橙);
+ - 资源趋势图:复用 `ResourceTrend` 组件,支持切换“CPU/磁盘”指标,默认显示近 24 小时数据;
+ - 定时刷新:用 `setInterval` 每 5 分钟调用 `getClusterStatus` 接口,页面离开时用 `onUnmounted` 清除定时器(避免内存泄漏);
+ - 手动刷新:“刷新”按钮触发接口调用,加载期间显示 `Loading` 组件。
+
+#### (3)故障管理页(分列表和详情,用例 UC-004、UC-005、UC-006)
+##### ① 故障列表页(src/views/FaultManage/List.vue)
+- **功能**:按“未修复/已修复”筛选故障,展示故障 ID、类型、发生时间、风险等级;
+- **关键实现**:
+ - 筛选功能:用 Element Plus 的 `ElSelect` 实现状态筛选,筛选后重新调用 `/api/fault/list` 接口(携带筛选参数);
+ - 分页:用 Element Plus 的 `ElPagination`,支持切换“每页条数”(10/20/50),分页参数同步到接口请求;
+ - 跳转详情:点击表格行,携带故障 ID 跳转详情页(`router.push(/fault-detail/${faultId})`)。
+
+##### ② 故障详情页(src/views/FaultManage/Detail.vue)
+- **功能**:展示故障日志、诊断结果、修复脚本,按风险等级显示修复按钮,实时展示修复日志;
+- **关键实现**:
+ - 数据加载:页面初始化时,通过路由参数 `route.params.faultId` 调用 `/api/fault/detail` 接口,加载故障信息;
+ - 风险分级按钮:
+ - 低风险:直接显示“自动修复”按钮,点击调用 `/api/fault/execute` 接口;
+ - 中风险:显示“确认修复”按钮,点击触发 `Confirm` 组件,确认后执行修复;
+ - 高风险:显示“申请审批”按钮,点击后提示“已发送审批请求,等待管理员确认”(后续通过 WebSocket 接收审批结果);
+ - 实时日志:修复过程中,用 `ElTag` 或 `ElText` 实时渲染后端返回的执行日志(接口返回 `stream` 流或通过 WebSocket 推送);
+ - 状态同步:修复完成后,自动更新故障状态(“未修复”→“已修复”),并提示“修复成功”,提供“返回列表”按钮。
+
+#### (4)日志分析页(src/views/LogAnalysis/Index.vue,用例 UC-007、UC-008)
+- **功能**:按“时间范围/节点/日志级别”查询日志,支持勾选日志提交 AI 分析;
+- **关键实现**:
+ - 查询表单:用 Element Plus 的 `ElDatePicker`(时间范围)、`ElSelect`(节点/日志级别),“查询”按钮触发 `/api/log/query` 接口;
+ - 日志列表:用 `ElTable` 展示结构化日志(时间戳、级别、组件、内容),支持勾选(限制最多 10 条,超过提示“最多勾选 10 条日志”);
+ - AI 分析:
+ - 点击“提交 AI 分析”,携带勾选的日志 ID 调用 `/api/llm/diagnose` 接口;
+ - 分析中显示 `Loading` 组件,超时(>30 秒)提示“分析超时,已后台继续处理,结果将推送”;
+ - 分析成功后,弹窗展示诊断结果(故障类型、原因、修复脚本),提供“前往故障详情”按钮。
+
+### 3. 接口请求封装(src/utils/request.js + src/api/模块.js)
+- **第一步:Axios 实例封装(src/utils/request.js)**:
+ - 处理请求头:自动携带 Token(从 localStorage 获取);
+ - 响应拦截:处理 401(Token 过期→清除 Token 并跳转登录)、500(服务器错误→提示“服务器异常,请重试”);
+ ```javascript
+ import axios from 'axios';
+ import { ElMessage } from 'element-plus';
+ import router from '@/router';
+
+ const service = axios.create({
+ baseURL: import.meta.env.VITE_API_BASE_URL,
+ timeout: 30000 // AI 分析接口超时设为 30 秒
+ });
+
+ // 请求拦截器:加 Token
+ service.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => Promise.reject(error)
+ );
+
+ // 响应拦截器:处理错误
+ service.interceptors.response.use(
+ (response) => response.data, // 直接返回响应体
+ (error) => {
+ if (error.response?.status === 401) {
+ // Token 过期:清除数据并跳转登录
+ localStorage.removeItem('token');
+ router.push('/login');
+ ElMessage.error('登录已过期,请重新登录');
+ } else {
+ ElMessage.error(error.message || '请求失败,请重试');
+ }
+ return Promise.reject(error);
+ }
+ );
+
+ export default service;
+ ```
+- **第二步:按模块封装接口(如 src/api/fault.js)**:
+ ```javascript
+ import request from '@/utils/request';
+
+ // 获取故障列表
+ export const getFaultList = (params) => {
+ return request({
+ url: '/fault/list',
+ method: 'get',
+ params // 筛选参数(status: 'unfixed'/'fixed')
+ });
+ };
+
+ // 执行故障修复
+ export const executeFaultFix = (faultId) => {
+ return request({
+ url: `/fault/execute`,
+ method: 'post',
+ data: { faultId }
+ });
+ };
+ ```
+
+
+## 四、联调与优化:接口联调与性能优化(应用开发阶段后期)
+此阶段需与后端紧密配合,解决接口问题,并优化页面性能与体验。
+
+### 1. 接口联调(核心:对齐数据格式,解决交互问题)
+- **联调准备**:与后端确认每个接口的“请求参数格式”“响应数据结构”(如故障列表接口返回 `{ code: 200, data: { list: [], total: 100 } }`);
+- **关键场景联调**:
+ - 集群状态接口:确认返回的“节点状态”字段(如 `status: 'online'/'offline'`),确保表格标注正确;
+ - 故障修复接口:确认“执行日志”的返回方式(stream 流→前端用 `onDownloadProgress` 实时接收;WebSocket→建立连接监听推送);
+ - AI 分析接口:确认“超时处理”逻辑(后端是否支持异步推送,前端是否需要轮询查询结果);
+- **问题记录**:用文档记录联调中的问题(如“故障详情接口缺少‘修复脚本’字段”),同步后端修复,避免遗漏。
+
+### 2. 性能与体验优化(贴合任务 8 中的“前端优化”要求)
+- **首屏加载优化**:
+ - 路由懒加载:在 `src/router/index.js` 中用 `() => import('@/views/xxx')` 实现,减少首屏 JS 体积;
+ ```javascript
+ const routes = [
+ {
+ path: '/cluster-monitor',
+ name: 'ClusterMonitor',
+ component: () => import('@/views/ClusterMonitor/Index.vue') // 懒加载
+ }
+ ];
+ ```
+ - 图表懒加载:非首屏图表(如日志分析页的趋势图)用 `v-intersect` 指令,滚动到可视区域才初始化;
+- **接口请求优化**:
+ - 缓存高频接口:如集群状态接口(5 分钟刷新),用 `localStorage` 缓存上次请求结果,避免重复调用;
+ - 合并重复请求:用 Axios 拦截器实现“同一接口未返回时,不重复发起请求”(如多次点击“刷新”按钮);
+- **兼容性优化**:
+ - 浏览器兼容:测试 Chrome 90+、Firefox 90+(项目要求),修复 CSS 兼容性问题(如 Flex 布局、CSS 变量);
+ - 响应式适配:用 Element Plus 的 `Layout` 组件和媒体查询,确保 1366px+ 屏幕正常显示(运维多使用台式机)。
+
+
+## 五、部署与维护:构建部署与上线后迭代(落地保障阶段)
+此阶段需配合运维完成部署,并处理上线后的问题与迭代需求。
+
+### 1. 项目构建与部署(贴合任务 9 中的“容器化部署”)
+#### (1)构建生产包
+- 执行 `npm run build` 生成 `dist` 目录(生产环境代码,已压缩混淆);
+- 检查 `dist` 目录结构(确保 `index.html` 正确引用 JS/CSS 文件)。
+
+#### (2)编写 Docker 相关文件(配合运维容器化部署)
+- **Dockerfile(前端镜像构建)**:
+ ```dockerfile
+ # 基础镜像:Nginx(轻量,适合静态资源部署)
+ FROM nginx:alpine
+
+ # 复制构建好的 dist 目录到 Nginx 的 html 目录
+ COPY dist /usr/share/nginx/html
+
+ # 复制 Nginx 配置文件(解决路由 history 模式和反向代理)
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+ # 暴露 80 端口(与 Nginx 配置一致)
+ EXPOSE 80
+
+ # 启动 Nginx(前台运行)
+ CMD ["nginx", "-g", "daemon off;"]
+ ```
+- **nginx.conf(Nginx 配置)**:
+ ```nginx
+ server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # 反向代理:将 /api 请求转发到后端服务(容器名+端口,由 docker-compose 管理)
+ location /api {
+ proxy_pass http://backend:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ # 解决 Vue Router history 模式的 404 问题(所有路由指向 index.html)
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+ }
+ ```
+- **配合运维**:将 `dist`、`Dockerfile`、`nginx.conf` 交给运维,由运维通过 `docker-compose` 与后端、MySQL、Redis 等服务一起部署。
+
+### 2. 上线后维护与迭代(贴合任务 10 中的“持续改进”)
+- **问题排查**:
+ - 生产环境 bug:通过浏览器“开发者工具”查看控制台错误(如接口 500、JS 报错),配合后端定位问题(如“日志分析页勾选超过 10 条未提示”→前端补充判断逻辑);
+ - 兼容性问题:收集运维反馈(如 Firefox 中图表不显示),修复 ECharts 初始化问题;
+- **需求迭代**:
+ - 低优先级需求:如“日志导出 Excel”(任务 6 中未提及,属于迭代需求),用 Element Plus 的 `XLSX` 库实现;
+ - 体验优化:如“故障列表页增加‘最近 24 小时’筛选”,根据运维反馈补充筛选条件;
+- **文档更新**:
+ - 维护前端开发文档,记录“接口变更历史”“新增功能说明”,方便后续迭代;
+ - 向运维提供“前端部署手册”,说明如何更新前端镜像、重启容器。
+
+
+## 六、关键注意事项(贯穿全流程)
+1. **安全问题**:
+ - Token 安全:避免用 `sessionStorage`(页面刷新丢失),用 `localStorage` 可配合简单加密(如 base64);
+ - XSS 防护:渲染日志内容时用 `v-text` 而非 `v-html`,避免注入恶意脚本;
+2. **可维护性**:
+ - 代码规范:严格遵守 ESLint 规则,提交代码前用 `npm run lint` 修复格式问题;
+ - 注释清晰:组件和接口函数需加注释(如 `// 执行故障修复:传入故障ID,返回修复结果`);
+3. **运维体验**:
+ - 操作反馈:所有用户操作(点击按钮、筛选)需有明确反馈(如按钮 loading、筛选后表格加载中);
+ - 异常兜底:接口返回空数据时(如无故障记录),显示“暂无数据”而非空白页面,提升运维信心。
+
+
+通过以上全流程工作,前端开发者可完成“从需求到上线”的闭环,最终交付一个符合运维需求、体验流畅、稳定可靠的可视化操作系统,助力 Hadoop 集群故障的自动化处理。
\ No newline at end of file
diff --git a/src/frontend/components/Layout/Header.html b/src/frontend/components/Layout/Header.html
new file mode 100644
index 0000000..a9a855d
--- /dev/null
+++ b/src/frontend/components/Layout/Header.html
@@ -0,0 +1,226 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/components/Layout/Sidebar.html b/src/frontend/components/Layout/Sidebar.html
new file mode 100644
index 0000000..151f659
--- /dev/null
+++ b/src/frontend/components/Layout/Sidebar.html
@@ -0,0 +1,275 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/index.html b/src/frontend/index.html
new file mode 100644
index 0000000..023b503
--- /dev/null
+++ b/src/frontend/index.html
@@ -0,0 +1,339 @@
+
+
+
+
+
+ Hadoop故障检测与自动恢复系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 用户名
+ 邮箱
+ 角色
+ 状态
+ 最后登录
+ 操作
+
+
+
+
+ admin
+ admin@example.com
+ 管理员
+ 活跃
+ 2024-01-15 14:30:00
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
快速开始
+
+ 登录系统后,首先查看概览仪表板了解系统整体状态
+ 通过集群监控页面查看各节点的详细信息
+ 在故障管理页面处理系统告警和故障
+ 使用日志分析功能定位问题根因
+
+
+
+
+
常见问题
+
+
如何添加新的监控节点?
+
在集群监控页面点击"添加节点"按钮,填写节点信息即可。
+
+
+
+
告警通知如何配置?
+
在系统配置页面的通知配置部分设置邮件和短信通知方式。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/js/app.js b/src/frontend/js/app.js
new file mode 100644
index 0000000..3a30bc9
--- /dev/null
+++ b/src/frontend/js/app.js
@@ -0,0 +1,510 @@
+// 主应用程序
+class ErrorDetectingApp {
+ constructor() {
+ this.currentPage = 'login';
+ this.isLoggedIn = false;
+ this.sidebarCollapsed = false;
+ this.darkMode = false;
+ this.init();
+ }
+
+ // 初始化应用
+ init() {
+ this.loadStoredSettings();
+ this.bindEvents();
+ this.initializeRouter();
+ this.checkAuthStatus();
+ }
+
+ // 加载存储的设置
+ loadStoredSettings() {
+ const darkMode = localStorage.getItem('darkMode');
+ const sidebarCollapsed = localStorage.getItem('sidebarCollapsed');
+
+ if (darkMode === 'true') {
+ this.toggleDarkMode(true);
+ }
+
+ if (sidebarCollapsed === 'true') {
+ this.toggleSidebar(true);
+ }
+ }
+
+ // 绑定事件
+ bindEvents() {
+ // 侧边栏切换
+ const sidebarToggle = document.getElementById('sidebarToggle');
+ if (sidebarToggle) {
+ sidebarToggle.addEventListener('click', () => this.toggleSidebar());
+ }
+
+ // 主题切换
+ const themeToggle = document.getElementById('themeToggle');
+ if (themeToggle) {
+ themeToggle.addEventListener('click', () => this.toggleDarkMode());
+ }
+
+ // 导航菜单点击
+ document.addEventListener('click', (e) => {
+ if (e.target.matches('[data-page]')) {
+ e.preventDefault();
+ const page = e.target.getAttribute('data-page');
+ this.navigateTo(page);
+ }
+ });
+
+ // 登录表单提交
+ const loginForm = document.getElementById('loginForm');
+ if (loginForm) {
+ loginForm.addEventListener('submit', (e) => this.handleLogin(e));
+ }
+
+ // 退出登录
+ const logoutBtn = document.getElementById('logoutBtn');
+ if (logoutBtn) {
+ logoutBtn.addEventListener('click', () => this.handleLogout());
+ }
+
+ // 窗口大小变化
+ window.addEventListener('resize', () => this.handleResize());
+ }
+
+ // 初始化路由
+ initializeRouter() {
+ // 监听浏览器前进后退
+ window.addEventListener('popstate', (e) => {
+ if (e.state && e.state.page) {
+ this.showPage(e.state.page, false);
+ }
+ });
+
+ // 初始页面路由
+ const hash = window.location.hash.slice(1);
+ if (hash) {
+ this.navigateTo(hash, false);
+ } else {
+ this.navigateTo('login', false);
+ }
+ }
+
+ // 检查认证状态
+ checkAuthStatus() {
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ // 这里应该验证token的有效性
+ this.isLoggedIn = true;
+ if (this.currentPage === 'login') {
+ this.navigateTo('dashboard');
+ }
+ } else {
+ this.isLoggedIn = false;
+ if (this.currentPage !== 'login') {
+ this.navigateTo('login');
+ }
+ }
+ }
+
+ // 导航到指定页面
+ navigateTo(page, pushState = true) {
+ // 检查是否需要登录
+ if (!this.isLoggedIn && page !== 'login') {
+ this.navigateTo('login');
+ return;
+ }
+
+ this.showPage(page, pushState);
+ }
+
+ // 显示页面
+ showPage(page, pushState = true) {
+ // 隐藏所有页面
+ const pages = document.querySelectorAll('.page-view');
+ pages.forEach(p => p.style.display = 'none');
+
+ // 显示目标页面
+ const targetPage = document.getElementById(page + 'Page');
+ if (targetPage) {
+ targetPage.style.display = 'block';
+ this.currentPage = page;
+
+ // 更新导航状态
+ this.updateNavigation(page);
+
+ // 更新浏览器历史
+ if (pushState) {
+ const url = page === 'login' ? '/' : `/#${page}`;
+ history.pushState({ page }, '', url);
+ }
+
+ // 更新页面标题
+ this.updatePageTitle(page);
+
+ // 触发页面加载事件
+ this.onPageLoad(page);
+ }
+
+ // 控制布局显示
+ const appLayout = document.getElementById('appLayout');
+ const loginContainer = document.getElementById('loginContainer');
+
+ if (page === 'login') {
+ if (appLayout) appLayout.style.display = 'none';
+ if (loginContainer) loginContainer.style.display = 'flex';
+ } else {
+ if (appLayout) appLayout.style.display = 'flex';
+ if (loginContainer) loginContainer.style.display = 'none';
+ }
+ }
+
+ // 更新导航状态
+ updateNavigation(page) {
+ // 移除所有活动状态
+ const navItems = document.querySelectorAll('.nav-item');
+ navItems.forEach(item => item.classList.remove('active'));
+
+ // 添加当前页面的活动状态
+ const currentNavItem = document.querySelector(`[data-page="${page}"]`);
+ if (currentNavItem) {
+ currentNavItem.closest('.nav-item').classList.add('active');
+ }
+
+ // 更新面包屑
+ this.updateBreadcrumb(page);
+ }
+
+ // 更新面包屑导航
+ updateBreadcrumb(page) {
+ const breadcrumb = document.getElementById('breadcrumb');
+ if (!breadcrumb) return;
+
+ const pageNames = {
+ 'dashboard': '概览仪表板',
+ 'cluster-monitor': '集群监控',
+ 'fault-manage': '故障管理',
+ 'log-analysis': '日志分析',
+ 'system-config': '系统配置',
+ 'user-manage': '用户管理',
+ 'system-monitor': '系统监控',
+ 'help': '帮助中心'
+ };
+
+ const pageName = pageNames[page] || '未知页面';
+ breadcrumb.innerHTML = `
+
+
+ 首页
+
+ /
+ ${pageName}
+ `;
+ }
+
+ // 更新页面标题
+ updatePageTitle(page) {
+ const titles = {
+ 'login': '登录 - 错误检测系统',
+ 'dashboard': '概览仪表板 - 错误检测系统',
+ 'cluster-monitor': '集群监控 - 错误检测系统',
+ 'fault-manage': '故障管理 - 错误检测系统',
+ 'log-analysis': '日志分析 - 错误检测系统',
+ 'system-config': '系统配置 - 错误检测系统',
+ 'user-manage': '用户管理 - 错误检测系统',
+ 'system-monitor': '系统监控 - 错误检测系统',
+ 'help': '帮助中心 - 错误检测系统'
+ };
+
+ document.title = titles[page] || '错误检测系统';
+ }
+
+ // 页面加载事件
+ onPageLoad(page) {
+ switch (page) {
+ case 'cluster-monitor':
+ this.initClusterMonitor();
+ break;
+ case 'fault-manage':
+ this.initFaultManage();
+ break;
+ case 'log-analysis':
+ this.initLogAnalysis();
+ break;
+ case 'dashboard':
+ this.initDashboard();
+ break;
+ }
+ }
+
+ // 切换侧边栏
+ toggleSidebar(force = null) {
+ const sidebar = document.getElementById('sidebar');
+ const mainContent = document.getElementById('mainContent');
+
+ if (!sidebar || !mainContent) return;
+
+ if (force !== null) {
+ this.sidebarCollapsed = force;
+ } else {
+ this.sidebarCollapsed = !this.sidebarCollapsed;
+ }
+
+ if (this.sidebarCollapsed) {
+ sidebar.classList.add('collapsed');
+ mainContent.classList.add('sidebar-collapsed');
+ } else {
+ sidebar.classList.remove('collapsed');
+ mainContent.classList.remove('sidebar-collapsed');
+ }
+
+ localStorage.setItem('sidebarCollapsed', this.sidebarCollapsed);
+ }
+
+ // 切换深色模式
+ toggleDarkMode(force = null) {
+ if (force !== null) {
+ this.darkMode = force;
+ } else {
+ this.darkMode = !this.darkMode;
+ }
+
+ if (this.darkMode) {
+ document.body.classList.add('dark-theme');
+ } else {
+ document.body.classList.remove('dark-theme');
+ }
+
+ localStorage.setItem('darkMode', this.darkMode);
+
+ // 更新主题切换按钮图标
+ const themeToggle = document.getElementById('themeToggle');
+ if (themeToggle) {
+ const icon = themeToggle.querySelector('i');
+ if (icon) {
+ icon.className = this.darkMode ? 'icon-sun' : 'icon-moon';
+ }
+ }
+ }
+
+ // 处理登录
+ handleLogin(e) {
+ e.preventDefault();
+
+ const username = document.getElementById('username').value;
+ const password = document.getElementById('password').value;
+ const captcha = document.getElementById('captcha').value;
+
+ // 简单的登录验证(实际项目中应该调用API)
+ if (username && password && captcha) {
+ // 模拟登录成功
+ this.isLoggedIn = true;
+ localStorage.setItem('authToken', 'mock-token-' + Date.now());
+ localStorage.setItem('username', username);
+
+ // 显示登录成功消息
+ this.showNotification('登录成功!', 'success');
+
+ // 跳转到仪表板
+ setTimeout(() => {
+ this.navigateTo('dashboard');
+ }, 1000);
+ } else {
+ this.showNotification('请填写完整的登录信息', 'error');
+ }
+ }
+
+ // 处理退出登录
+ handleLogout() {
+ this.isLoggedIn = false;
+ localStorage.removeItem('authToken');
+ localStorage.removeItem('username');
+
+ this.showNotification('已退出登录', 'info');
+ this.navigateTo('login');
+ }
+
+ // 处理窗口大小变化
+ handleResize() {
+ const width = window.innerWidth;
+
+ // 在小屏幕上自动收起侧边栏
+ if (width < 768 && !this.sidebarCollapsed) {
+ this.toggleSidebar(true);
+ }
+ }
+
+ // 显示通知
+ showNotification(message, type = 'info', duration = 3000) {
+ // 创建通知元素
+ const notification = document.createElement('div');
+ notification.className = `notification notification-${type}`;
+ notification.innerHTML = `
+
+
+ ${message}
+
+
+
+
+ `;
+
+ // 添加到页面
+ let container = document.getElementById('notificationContainer');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'notificationContainer';
+ container.className = 'notification-container';
+ document.body.appendChild(container);
+ }
+
+ container.appendChild(notification);
+
+ // 自动移除
+ setTimeout(() => {
+ if (notification.parentElement) {
+ notification.remove();
+ }
+ }, duration);
+ }
+
+ // 初始化仪表板
+ initDashboard() {
+ // 仪表板页面已在HTML中定义,这里可以添加动态数据加载
+ console.log('仪表板页面初始化完成');
+ }
+
+ // 初始化集群监控
+ initClusterMonitor() {
+ const container = document.getElementById('clusterMonitorPage');
+ if (container && !container.hasChildNodes()) {
+ // 动态加载集群监控页面内容
+ this.loadPageContent('views/ClusterMonitor/ClusterMonitor.html', container);
+ }
+ }
+
+ // 初始化故障管理
+ initFaultManage() {
+ const container = document.getElementById('faultManagePage');
+ if (container && !container.hasChildNodes()) {
+ // 动态加载故障管理页面内容
+ this.loadPageContent('views/FaultManage/FaultManage.html', container);
+ }
+ }
+
+ // 初始化日志分析
+ initLogAnalysis() {
+ const container = document.getElementById('logAnalysisPage');
+ if (container && !container.hasChildNodes()) {
+ // 动态加载日志分析页面内容
+ this.loadPageContent('views/LogAnalysis/LogAnalysis.html', container);
+ }
+ }
+
+ // 动态加载页面内容的辅助方法
+ async loadPageContent(url, container) {
+ try {
+ const response = await fetch(url);
+ if (response.ok) {
+ const html = await response.text();
+ container.innerHTML = html;
+ } else {
+ console.warn(`无法加载页面内容: ${url}`);
+ container.innerHTML = '页面加载失败
';
+ }
+ } catch (error) {
+ console.error(`加载页面内容时出错: ${url}`, error);
+ container.innerHTML = '页面加载出错
';
+ }
+ }
+}
+
+// 工具函数
+const Utils = {
+ // 格式化时间
+ formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {
+ const date = new Date(timestamp);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+
+ return format
+ .replace('YYYY', year)
+ .replace('MM', month)
+ .replace('DD', day)
+ .replace('HH', hours)
+ .replace('mm', minutes)
+ .replace('ss', seconds);
+ },
+
+ // 格式化文件大小
+ formatFileSize(bytes) {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ },
+
+ // 防抖函数
+ debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ },
+
+ // 节流函数
+ throttle(func, limit) {
+ let inThrottle;
+ return function() {
+ const args = arguments;
+ const context = this;
+ if (!inThrottle) {
+ func.apply(context, args);
+ inThrottle = true;
+ setTimeout(() => inThrottle = false, limit);
+ }
+ };
+ },
+
+ // 生成随机ID
+ generateId(length = 8) {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
+ },
+
+ // 深拷贝
+ deepClone(obj) {
+ if (obj === null || typeof obj !== 'object') return obj;
+ if (obj instanceof Date) return new Date(obj.getTime());
+ if (obj instanceof Array) return obj.map(item => this.deepClone(item));
+ if (typeof obj === 'object') {
+ const clonedObj = {};
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ clonedObj[key] = this.deepClone(obj[key]);
+ }
+ }
+ return clonedObj;
+ }
+ }
+};
+
+// 页面加载完成后初始化应用
+document.addEventListener('DOMContentLoaded', () => {
+ window.app = new ErrorDetectingApp();
+});
+
+// 导出到全局
+window.ErrorDetectingApp = ErrorDetectingApp;
+window.Utils = Utils;
\ No newline at end of file
diff --git a/src/frontend/js/charts.js b/src/frontend/js/charts.js
new file mode 100644
index 0000000..60d7b10
--- /dev/null
+++ b/src/frontend/js/charts.js
@@ -0,0 +1,435 @@
+/**
+ * 图表组件库
+ * 基于Chart.js实现各种数据可视化图表
+ */
+
+// 图表基础配置
+const ChartConfig = {
+ // 默认颜色主题
+ colors: {
+ primary: '#3b82f6',
+ success: '#10b981',
+ warning: '#f59e0b',
+ danger: '#ef4444',
+ info: '#06b6d4',
+ secondary: '#6b7280'
+ },
+
+ // 图表默认配置
+ defaultOptions: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'top',
+ labels: {
+ usePointStyle: true,
+ padding: 20
+ }
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleColor: '#fff',
+ bodyColor: '#fff',
+ borderColor: '#374151',
+ borderWidth: 1,
+ cornerRadius: 8,
+ displayColors: true
+ }
+ },
+ scales: {
+ x: {
+ grid: {
+ color: 'rgba(156, 163, 175, 0.1)'
+ },
+ ticks: {
+ color: '#6b7280'
+ }
+ },
+ y: {
+ grid: {
+ color: 'rgba(156, 163, 175, 0.1)'
+ },
+ ticks: {
+ color: '#6b7280'
+ }
+ }
+ }
+ }
+};
+
+// 线性图表组件
+class LineChart {
+ constructor(canvasId, options = {}) {
+ this.canvas = document.getElementById(canvasId);
+ this.ctx = this.canvas.getContext('2d');
+ this.options = { ...ChartConfig.defaultOptions, ...options };
+ this.chart = null;
+ this.data = {
+ labels: [],
+ datasets: []
+ };
+ }
+
+ // 初始化图表
+ init(data, options = {}) {
+ this.data = data;
+ const config = {
+ type: 'line',
+ data: this.data,
+ options: { ...this.options, ...options }
+ };
+
+ if (this.chart) {
+ this.chart.destroy();
+ }
+
+ this.chart = new Chart(this.ctx, config);
+ return this;
+ }
+
+ // 更新数据
+ updateData(newData) {
+ if (this.chart) {
+ this.chart.data = newData;
+ this.chart.update();
+ }
+ return this;
+ }
+
+ // 添加数据点
+ addDataPoint(label, values) {
+ if (this.chart) {
+ this.chart.data.labels.push(label);
+ values.forEach((value, index) => {
+ if (this.chart.data.datasets[index]) {
+ this.chart.data.datasets[index].data.push(value);
+ }
+ });
+ this.chart.update();
+ }
+ return this;
+ }
+
+ // 销毁图表
+ destroy() {
+ if (this.chart) {
+ this.chart.destroy();
+ this.chart = null;
+ }
+ }
+}
+
+// 柱状图组件
+class BarChart {
+ constructor(canvasId, options = {}) {
+ this.canvas = document.getElementById(canvasId);
+ this.ctx = this.canvas.getContext('2d');
+ this.options = { ...ChartConfig.defaultOptions, ...options };
+ this.chart = null;
+ }
+
+ init(data, options = {}) {
+ const config = {
+ type: 'bar',
+ data: data,
+ options: { ...this.options, ...options }
+ };
+
+ if (this.chart) {
+ this.chart.destroy();
+ }
+
+ this.chart = new Chart(this.ctx, config);
+ return this;
+ }
+
+ updateData(newData) {
+ if (this.chart) {
+ this.chart.data = newData;
+ this.chart.update();
+ }
+ return this;
+ }
+
+ destroy() {
+ if (this.chart) {
+ this.chart.destroy();
+ this.chart = null;
+ }
+ }
+}
+
+// 饼图组件
+class PieChart {
+ constructor(canvasId, options = {}) {
+ this.canvas = document.getElementById(canvasId);
+ this.ctx = this.canvas.getContext('2d');
+ this.options = {
+ ...ChartConfig.defaultOptions,
+ ...options,
+ scales: undefined // 饼图不需要坐标轴
+ };
+ this.chart = null;
+ }
+
+ init(data, options = {}) {
+ const config = {
+ type: 'pie',
+ data: data,
+ options: { ...this.options, ...options }
+ };
+
+ if (this.chart) {
+ this.chart.destroy();
+ }
+
+ this.chart = new Chart(this.ctx, config);
+ return this;
+ }
+
+ updateData(newData) {
+ if (this.chart) {
+ this.chart.data = newData;
+ this.chart.update();
+ }
+ return this;
+ }
+
+ destroy() {
+ if (this.chart) {
+ this.chart.destroy();
+ this.chart = null;
+ }
+ }
+}
+
+// 环形图组件
+class DoughnutChart {
+ constructor(canvasId, options = {}) {
+ this.canvas = document.getElementById(canvasId);
+ this.ctx = this.canvas.getContext('2d');
+ this.options = {
+ ...ChartConfig.defaultOptions,
+ ...options,
+ scales: undefined // 环形图不需要坐标轴
+ };
+ this.chart = null;
+ }
+
+ init(data, options = {}) {
+ const config = {
+ type: 'doughnut',
+ data: data,
+ options: { ...this.options, ...options }
+ };
+
+ if (this.chart) {
+ this.chart.destroy();
+ }
+
+ this.chart = new Chart(this.ctx, config);
+ return this;
+ }
+
+ updateData(newData) {
+ if (this.chart) {
+ this.chart.data = newData;
+ this.chart.update();
+ }
+ return this;
+ }
+
+ destroy() {
+ if (this.chart) {
+ this.chart.destroy();
+ this.chart = null;
+ }
+ }
+}
+
+// 实时图表组件(用于监控数据)
+class RealTimeChart extends LineChart {
+ constructor(canvasId, options = {}) {
+ super(canvasId, options);
+ this.maxDataPoints = options.maxDataPoints || 50;
+ this.updateInterval = options.updateInterval || 5000;
+ this.isRunning = false;
+ this.intervalId = null;
+ }
+
+ // 开始实时更新
+ start(dataCallback) {
+ if (this.isRunning) return;
+
+ this.isRunning = true;
+ this.intervalId = setInterval(() => {
+ if (typeof dataCallback === 'function') {
+ const newData = dataCallback();
+ this.addRealTimeData(newData);
+ }
+ }, this.updateInterval);
+ }
+
+ // 停止实时更新
+ stop() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ this.isRunning = false;
+ }
+
+ // 添加实时数据
+ addRealTimeData(data) {
+ if (!this.chart) return;
+
+ const { label, values } = data;
+
+ // 添加新数据点
+ this.chart.data.labels.push(label);
+ values.forEach((value, index) => {
+ if (this.chart.data.datasets[index]) {
+ this.chart.data.datasets[index].data.push(value);
+ }
+ });
+
+ // 限制数据点数量
+ if (this.chart.data.labels.length > this.maxDataPoints) {
+ this.chart.data.labels.shift();
+ this.chart.data.datasets.forEach(dataset => {
+ dataset.data.shift();
+ });
+ }
+
+ this.chart.update('none'); // 无动画更新,提高性能
+ }
+
+ destroy() {
+ this.stop();
+ super.destroy();
+ }
+}
+
+// 图表工厂类
+class ChartFactory {
+ static createLineChart(canvasId, data, options = {}) {
+ const chart = new LineChart(canvasId, options);
+ return chart.init(data, options);
+ }
+
+ static createBarChart(canvasId, data, options = {}) {
+ const chart = new BarChart(canvasId, options);
+ return chart.init(data, options);
+ }
+
+ static createPieChart(canvasId, data, options = {}) {
+ const chart = new PieChart(canvasId, options);
+ return chart.init(data, options);
+ }
+
+ static createDoughnutChart(canvasId, data, options = {}) {
+ const chart = new DoughnutChart(canvasId, options);
+ return chart.init(data, options);
+ }
+
+ static createRealTimeChart(canvasId, data, options = {}) {
+ const chart = new RealTimeChart(canvasId, options);
+ return chart.init(data, options);
+ }
+}
+
+// 数据生成器(用于演示)
+class DataGenerator {
+ // 生成时间序列数据
+ static generateTimeSeriesData(hours = 24, datasets = 1) {
+ const labels = [];
+ const now = new Date();
+
+ for (let i = hours - 1; i >= 0; i--) {
+ const time = new Date(now.getTime() - i * 60 * 60 * 1000);
+ labels.push(time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }));
+ }
+
+ const datasetsArray = [];
+ const colors = [
+ ChartConfig.colors.primary,
+ ChartConfig.colors.success,
+ ChartConfig.colors.warning,
+ ChartConfig.colors.danger
+ ];
+
+ for (let i = 0; i < datasets; i++) {
+ datasetsArray.push({
+ label: `数据集 ${i + 1}`,
+ data: Array.from({ length: hours }, () => Math.floor(Math.random() * 100)),
+ borderColor: colors[i % colors.length],
+ backgroundColor: colors[i % colors.length] + '20',
+ fill: false,
+ tension: 0.4
+ });
+ }
+
+ return { labels, datasets: datasetsArray };
+ }
+
+ // 生成饼图数据
+ static generatePieData(categories) {
+ const colors = [
+ ChartConfig.colors.primary,
+ ChartConfig.colors.success,
+ ChartConfig.colors.warning,
+ ChartConfig.colors.danger,
+ ChartConfig.colors.info,
+ ChartConfig.colors.secondary
+ ];
+
+ return {
+ labels: categories,
+ datasets: [{
+ data: categories.map(() => Math.floor(Math.random() * 100) + 10),
+ backgroundColor: colors.slice(0, categories.length),
+ borderWidth: 2,
+ borderColor: '#fff'
+ }]
+ };
+ }
+
+ // 生成柱状图数据
+ static generateBarData(categories, datasets = 1) {
+ const colors = [
+ ChartConfig.colors.primary,
+ ChartConfig.colors.success,
+ ChartConfig.colors.warning,
+ ChartConfig.colors.danger
+ ];
+
+ const datasetsArray = [];
+ for (let i = 0; i < datasets; i++) {
+ datasetsArray.push({
+ label: `数据集 ${i + 1}`,
+ data: categories.map(() => Math.floor(Math.random() * 100)),
+ backgroundColor: colors[i % colors.length],
+ borderColor: colors[i % colors.length],
+ borderWidth: 1
+ });
+ }
+
+ return {
+ labels: categories,
+ datasets: datasetsArray
+ };
+ }
+}
+
+// 导出到全局
+window.ChartConfig = ChartConfig;
+window.LineChart = LineChart;
+window.BarChart = BarChart;
+window.PieChart = PieChart;
+window.DoughnutChart = DoughnutChart;
+window.RealTimeChart = RealTimeChart;
+window.ChartFactory = ChartFactory;
+window.DataGenerator = DataGenerator;
+
+console.log('图表组件库已加载');
\ No newline at end of file
diff --git a/src/frontend/js/components.js b/src/frontend/js/components.js
new file mode 100644
index 0000000..e5a0959
--- /dev/null
+++ b/src/frontend/js/components.js
@@ -0,0 +1,690 @@
+// 通用组件库
+
+// 模态框组件
+class Modal {
+ constructor(options = {}) {
+ this.options = {
+ title: '提示',
+ content: '',
+ width: '500px',
+ height: 'auto',
+ closable: true,
+ maskClosable: true,
+ showFooter: true,
+ confirmText: '确定',
+ cancelText: '取消',
+ onConfirm: null,
+ onCancel: null,
+ onClose: null,
+ ...options
+ };
+
+ this.element = null;
+ this.isVisible = false;
+ this.create();
+ }
+
+ create() {
+ this.element = document.createElement('div');
+ this.element.className = 'modal-overlay';
+ this.element.innerHTML = `
+
+
+
+ ${this.options.content}
+
+ ${this.options.showFooter ? `
+
+ ` : ''}
+
+ `;
+
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ // 关闭按钮
+ const closeBtn = this.element.querySelector('.modal-close');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => this.close());
+ }
+
+ // 遮罩点击关闭
+ if (this.options.maskClosable) {
+ this.element.addEventListener('click', (e) => {
+ if (e.target === this.element) {
+ this.close();
+ }
+ });
+ }
+
+ // 确定按钮
+ const confirmBtn = this.element.querySelector('.modal-confirm');
+ if (confirmBtn) {
+ confirmBtn.addEventListener('click', () => {
+ if (this.options.onConfirm) {
+ const result = this.options.onConfirm();
+ if (result !== false) {
+ this.close();
+ }
+ } else {
+ this.close();
+ }
+ });
+ }
+
+ // 取消按钮
+ const cancelBtn = this.element.querySelector('.modal-cancel');
+ if (cancelBtn) {
+ cancelBtn.addEventListener('click', () => {
+ if (this.options.onCancel) {
+ this.options.onCancel();
+ }
+ this.close();
+ });
+ }
+
+ // ESC键关闭
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && this.isVisible) {
+ this.close();
+ }
+ });
+ }
+
+ show() {
+ if (!this.isVisible) {
+ document.body.appendChild(this.element);
+ this.isVisible = true;
+
+ // 添加动画
+ setTimeout(() => {
+ this.element.classList.add('show');
+ }, 10);
+ }
+ return this;
+ }
+
+ close() {
+ if (this.isVisible) {
+ this.element.classList.remove('show');
+
+ setTimeout(() => {
+ if (this.element.parentElement) {
+ document.body.removeChild(this.element);
+ }
+ this.isVisible = false;
+
+ if (this.options.onClose) {
+ this.options.onClose();
+ }
+ }, 300);
+ }
+ return this;
+ }
+
+ setContent(content) {
+ const body = this.element.querySelector('.modal-body');
+ if (body) {
+ body.innerHTML = content;
+ }
+ return this;
+ }
+
+ setTitle(title) {
+ const titleElement = this.element.querySelector('.modal-title');
+ if (titleElement) {
+ titleElement.textContent = title;
+ }
+ return this;
+ }
+}
+
+// 加载组件
+class Loading {
+ constructor(container = document.body, options = {}) {
+ this.container = container;
+ this.options = {
+ text: '加载中...',
+ size: 'medium',
+ overlay: true,
+ ...options
+ };
+
+ this.element = null;
+ this.isVisible = false;
+ this.create();
+ }
+
+ create() {
+ this.element = document.createElement('div');
+ this.element.className = `loading-container ${this.options.size}`;
+
+ if (this.options.overlay) {
+ this.element.classList.add('loading-overlay');
+ }
+
+ this.element.innerHTML = `
+
+
+ ${this.options.text ? `
${this.options.text}
` : ''}
+
+ `;
+ }
+
+ show() {
+ if (!this.isVisible) {
+ this.container.appendChild(this.element);
+ this.isVisible = true;
+
+ setTimeout(() => {
+ this.element.classList.add('show');
+ }, 10);
+ }
+ return this;
+ }
+
+ hide() {
+ if (this.isVisible) {
+ this.element.classList.remove('show');
+
+ setTimeout(() => {
+ if (this.element.parentElement) {
+ this.container.removeChild(this.element);
+ }
+ this.isVisible = false;
+ }, 300);
+ }
+ return this;
+ }
+
+ setText(text) {
+ const textElement = this.element.querySelector('.loading-text');
+ if (textElement) {
+ textElement.textContent = text;
+ }
+ return this;
+ }
+}
+
+// 通知组件
+class Notification {
+ static container = null;
+ static notifications = [];
+
+ static init() {
+ if (!this.container) {
+ this.container = document.createElement('div');
+ this.container.className = 'notification-container';
+ document.body.appendChild(this.container);
+ }
+ }
+
+ static show(message, type = 'info', options = {}) {
+ this.init();
+
+ const config = {
+ duration: 3000,
+ closable: true,
+ ...options
+ };
+
+ const notification = document.createElement('div');
+ notification.className = `notification notification-${type}`;
+
+ const id = Utils.generateId();
+ notification.setAttribute('data-id', id);
+
+ notification.innerHTML = `
+
+
+ ${message}
+ ${config.closable ? ' ' : ''}
+
+ `;
+
+ // 绑定关闭事件
+ if (config.closable) {
+ const closeBtn = notification.querySelector('.notification-close');
+ closeBtn.addEventListener('click', () => {
+ this.remove(id);
+ });
+ }
+
+ this.container.appendChild(notification);
+ this.notifications.push({ id, element: notification });
+
+ // 添加显示动画
+ setTimeout(() => {
+ notification.classList.add('show');
+ }, 10);
+
+ // 自动移除
+ if (config.duration > 0) {
+ setTimeout(() => {
+ this.remove(id);
+ }, config.duration);
+ }
+
+ return id;
+ }
+
+ static remove(id) {
+ const index = this.notifications.findIndex(n => n.id === id);
+ if (index !== -1) {
+ const notification = this.notifications[index];
+ notification.element.classList.remove('show');
+
+ setTimeout(() => {
+ if (notification.element.parentElement) {
+ this.container.removeChild(notification.element);
+ }
+ this.notifications.splice(index, 1);
+ }, 300);
+ }
+ }
+
+ static getIcon(type) {
+ const icons = {
+ success: 'check',
+ error: 'x',
+ warning: 'warning',
+ info: 'info'
+ };
+ return icons[type] || 'info';
+ }
+
+ static success(message, options) {
+ return this.show(message, 'success', options);
+ }
+
+ static error(message, options) {
+ return this.show(message, 'error', options);
+ }
+
+ static warning(message, options) {
+ return this.show(message, 'warning', options);
+ }
+
+ static info(message, options) {
+ return this.show(message, 'info', options);
+ }
+}
+
+// 确认对话框
+class Confirm {
+ static show(message, options = {}) {
+ return new Promise((resolve) => {
+ const config = {
+ title: '确认',
+ confirmText: '确定',
+ cancelText: '取消',
+ type: 'warning',
+ ...options
+ };
+
+ const modal = new Modal({
+ title: config.title,
+ content: `
+
+ `,
+ confirmText: config.confirmText,
+ cancelText: config.cancelText,
+ onConfirm: () => {
+ resolve(true);
+ },
+ onCancel: () => {
+ resolve(false);
+ },
+ onClose: () => {
+ resolve(false);
+ }
+ });
+
+ modal.show();
+ });
+ }
+}
+
+// 提示框
+class Alert {
+ static show(message, type = 'info', options = {}) {
+ return new Promise((resolve) => {
+ const config = {
+ title: '提示',
+ confirmText: '确定',
+ ...options
+ };
+
+ const modal = new Modal({
+ title: config.title,
+ content: `
+
+ `,
+ showFooter: true,
+ confirmText: config.confirmText,
+ cancelText: '',
+ onConfirm: () => {
+ resolve(true);
+ },
+ onClose: () => {
+ resolve(true);
+ }
+ });
+
+ // 隐藏取消按钮
+ modal.show();
+ const cancelBtn = modal.element.querySelector('.modal-cancel');
+ if (cancelBtn) {
+ cancelBtn.style.display = 'none';
+ }
+ });
+ }
+
+ static success(message, options) {
+ return this.show(message, 'success', options);
+ }
+
+ static error(message, options) {
+ return this.show(message, 'error', options);
+ }
+
+ static warning(message, options) {
+ return this.show(message, 'warning', options);
+ }
+
+ static info(message, options) {
+ return this.show(message, 'info', options);
+ }
+}
+
+// 工具提示组件
+class Tooltip {
+ constructor(element, options = {}) {
+ this.element = element;
+ this.options = {
+ content: '',
+ placement: 'top',
+ trigger: 'hover',
+ delay: 100,
+ ...options
+ };
+
+ this.tooltip = null;
+ this.isVisible = false;
+ this.init();
+ }
+
+ init() {
+ this.create();
+ this.bindEvents();
+ }
+
+ create() {
+ this.tooltip = document.createElement('div');
+ this.tooltip.className = `tooltip tooltip-${this.options.placement}`;
+ this.tooltip.innerHTML = `
+ ${this.options.content}
+
+ `;
+ document.body.appendChild(this.tooltip);
+ }
+
+ bindEvents() {
+ if (this.options.trigger === 'hover') {
+ this.element.addEventListener('mouseenter', () => {
+ this.showDelay();
+ });
+
+ this.element.addEventListener('mouseleave', () => {
+ this.hide();
+ });
+ } else if (this.options.trigger === 'click') {
+ this.element.addEventListener('click', () => {
+ this.toggle();
+ });
+ }
+ }
+
+ showDelay() {
+ setTimeout(() => {
+ this.show();
+ }, this.options.delay);
+ }
+
+ show() {
+ if (!this.isVisible) {
+ this.updatePosition();
+ this.tooltip.classList.add('show');
+ this.isVisible = true;
+ }
+ }
+
+ hide() {
+ if (this.isVisible) {
+ this.tooltip.classList.remove('show');
+ this.isVisible = false;
+ }
+ }
+
+ toggle() {
+ if (this.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ updatePosition() {
+ const rect = this.element.getBoundingClientRect();
+ const tooltipRect = this.tooltip.getBoundingClientRect();
+
+ let top, left;
+
+ switch (this.options.placement) {
+ case 'top':
+ top = rect.top - tooltipRect.height - 8;
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
+ break;
+ case 'bottom':
+ top = rect.bottom + 8;
+ left = rect.left + (rect.width - tooltipRect.width) / 2;
+ break;
+ case 'left':
+ top = rect.top + (rect.height - tooltipRect.height) / 2;
+ left = rect.left - tooltipRect.width - 8;
+ break;
+ case 'right':
+ top = rect.top + (rect.height - tooltipRect.height) / 2;
+ left = rect.right + 8;
+ break;
+ }
+
+ this.tooltip.style.top = `${top}px`;
+ this.tooltip.style.left = `${left}px`;
+ }
+
+ setContent(content) {
+ const contentElement = this.tooltip.querySelector('.tooltip-content');
+ if (contentElement) {
+ contentElement.innerHTML = content;
+ }
+ }
+
+ destroy() {
+ if (this.tooltip && this.tooltip.parentElement) {
+ document.body.removeChild(this.tooltip);
+ }
+ }
+}
+
+// 下拉菜单组件
+class Dropdown {
+ constructor(trigger, options = {}) {
+ this.trigger = trigger;
+ this.options = {
+ items: [],
+ placement: 'bottom-start',
+ trigger: 'click',
+ ...options
+ };
+
+ this.dropdown = null;
+ this.isVisible = false;
+ this.init();
+ }
+
+ init() {
+ this.create();
+ this.bindEvents();
+ }
+
+ create() {
+ this.dropdown = document.createElement('div');
+ this.dropdown.className = 'dropdown-menu';
+
+ const itemsHtml = this.options.items.map(item => {
+ if (item.divider) {
+ return '
';
+ }
+
+ return `
+
+ ${item.icon ? ` ` : ''}
+ ${item.text}
+
+ `;
+ }).join('');
+
+ this.dropdown.innerHTML = itemsHtml;
+ document.body.appendChild(this.dropdown);
+ }
+
+ bindEvents() {
+ // 触发器事件
+ if (this.options.trigger === 'click') {
+ this.trigger.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.toggle();
+ });
+ } else if (this.options.trigger === 'hover') {
+ this.trigger.addEventListener('mouseenter', () => {
+ this.show();
+ });
+
+ this.trigger.addEventListener('mouseleave', () => {
+ setTimeout(() => {
+ if (!this.dropdown.matches(':hover')) {
+ this.hide();
+ }
+ }, 100);
+ });
+
+ this.dropdown.addEventListener('mouseleave', () => {
+ this.hide();
+ });
+ }
+
+ // 点击菜单项
+ this.dropdown.addEventListener('click', (e) => {
+ const item = e.target.closest('.dropdown-item');
+ if (item && !item.classList.contains('disabled')) {
+ const value = item.getAttribute('data-value');
+ if (this.options.onSelect) {
+ this.options.onSelect(value, item);
+ }
+ this.hide();
+ }
+ });
+
+ // 点击外部关闭
+ document.addEventListener('click', (e) => {
+ if (!this.trigger.contains(e.target) && !this.dropdown.contains(e.target)) {
+ this.hide();
+ }
+ });
+ }
+
+ show() {
+ if (!this.isVisible) {
+ this.updatePosition();
+ this.dropdown.classList.add('show');
+ this.isVisible = true;
+ }
+ }
+
+ hide() {
+ if (this.isVisible) {
+ this.dropdown.classList.remove('show');
+ this.isVisible = false;
+ }
+ }
+
+ toggle() {
+ if (this.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ updatePosition() {
+ const rect = this.trigger.getBoundingClientRect();
+
+ let top, left;
+
+ switch (this.options.placement) {
+ case 'bottom-start':
+ top = rect.bottom + 4;
+ left = rect.left;
+ break;
+ case 'bottom-end':
+ top = rect.bottom + 4;
+ left = rect.right - this.dropdown.offsetWidth;
+ break;
+ case 'top-start':
+ top = rect.top - this.dropdown.offsetHeight - 4;
+ left = rect.left;
+ break;
+ case 'top-end':
+ top = rect.top - this.dropdown.offsetHeight - 4;
+ left = rect.right - this.dropdown.offsetWidth;
+ break;
+ }
+
+ this.dropdown.style.top = `${top}px`;
+ this.dropdown.style.left = `${left}px`;
+ }
+
+ destroy() {
+ if (this.dropdown && this.dropdown.parentElement) {
+ document.body.removeChild(this.dropdown);
+ }
+ }
+}
+
+// 导出组件
+window.Modal = Modal;
+window.Loading = Loading;
+window.Notification = Notification;
+window.Confirm = Confirm;
+window.Alert = Alert;
+window.Tooltip = Tooltip;
+window.Dropdown = Dropdown;
\ No newline at end of file
diff --git a/src/frontend/js/demo-data.js b/src/frontend/js/demo-data.js
new file mode 100644
index 0000000..26a20a0
--- /dev/null
+++ b/src/frontend/js/demo-data.js
@@ -0,0 +1,380 @@
+/**
+ * 演示数据文件
+ * 为各个页面提供模拟数据以展示功能
+ */
+
+// 演示数据管理器
+class DemoDataManager {
+ constructor() {
+ this.initData();
+ }
+
+ // 初始化所有演示数据
+ initData() {
+ this.dashboardData = this.generateDashboardData();
+ this.clusterData = this.generateClusterData();
+ this.faultData = this.generateFaultData();
+ this.logData = this.generateLogData();
+ this.chartData = this.generateChartData();
+ }
+
+ // 生成仪表板数据
+ generateDashboardData() {
+ return {
+ stats: {
+ onlineNodes: 12,
+ activeAlerts: 3,
+ avgCpuUsage: 68,
+ avgMemoryUsage: 45
+ },
+ recentAlerts: [
+ {
+ id: 1,
+ title: 'CPU使用率过高',
+ node: 'node-01',
+ level: 'warning',
+ time: '2024-01-15 14:30:00'
+ },
+ {
+ id: 2,
+ title: '磁盘空间不足',
+ node: 'node-03',
+ level: 'error',
+ time: '2024-01-15 14:25:00'
+ },
+ {
+ id: 3,
+ title: '网络连接异常',
+ node: 'node-07',
+ level: 'warning',
+ time: '2024-01-15 14:20:00'
+ }
+ ]
+ };
+ }
+
+ // 生成集群数据
+ generateClusterData() {
+ const nodes = [];
+ const statuses = ['online', 'warning', 'offline'];
+ const nodeTypes = ['master', 'worker', 'storage'];
+
+ for (let i = 1; i <= 12; i++) {
+ const nodeId = `node-${i.toString().padStart(2, '0')}`;
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
+
+ nodes.push({
+ id: nodeId,
+ name: `节点 ${i}`,
+ ip: `192.168.1.${100 + i}`,
+ type: nodeTypes[Math.floor(Math.random() * nodeTypes.length)],
+ status: status,
+ cpu: Math.floor(Math.random() * 100),
+ memory: Math.floor(Math.random() * 100),
+ disk: Math.floor(Math.random() * 100),
+ network: Math.floor(Math.random() * 1000),
+ uptime: Math.floor(Math.random() * 30) + 1,
+ lastUpdate: new Date(Date.now() - Math.random() * 300000).toLocaleString('zh-CN')
+ });
+ }
+
+ return {
+ overview: {
+ total: nodes.length,
+ online: nodes.filter(n => n.status === 'online').length,
+ warning: nodes.filter(n => n.status === 'warning').length,
+ offline: nodes.filter(n => n.status === 'offline').length,
+ avgCpu: Math.floor(nodes.reduce((sum, n) => sum + n.cpu, 0) / nodes.length),
+ cpuTrend: '+2.3%'
+ },
+ nodes: nodes
+ };
+ }
+
+ // 生成故障数据
+ generateFaultData() {
+ const faults = [];
+ const levels = ['critical', 'high', 'medium', 'low'];
+ const types = ['hardware', 'software', 'network', 'security'];
+ const statuses = ['open', 'in_progress', 'resolved', 'closed'];
+ const assignees = ['张三', '李四', '王五', '赵六', '钱七'];
+
+ for (let i = 1; i <= 50; i++) {
+ const faultId = `F${Date.now().toString().slice(-6)}${i.toString().padStart(2, '0')}`;
+ const level = levels[Math.floor(Math.random() * levels.length)];
+ const type = types[Math.floor(Math.random() * types.length)];
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
+
+ faults.push({
+ id: faultId,
+ title: this.generateFaultTitle(type),
+ description: this.generateFaultDescription(type),
+ level: level,
+ type: type,
+ status: status,
+ assignee: assignees[Math.floor(Math.random() * assignees.length)],
+ reporter: assignees[Math.floor(Math.random() * assignees.length)],
+ node: `node-${Math.floor(Math.random() * 12) + 1}`,
+ createdAt: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toLocaleString('zh-CN'),
+ updatedAt: new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000).toLocaleString('zh-CN'),
+ resolvedAt: status === 'resolved' ? new Date(Date.now() - Math.random() * 12 * 60 * 60 * 1000).toLocaleString('zh-CN') : null
+ });
+ }
+
+ return {
+ overview: {
+ total: faults.length,
+ critical: faults.filter(f => f.level === 'critical').length,
+ high: faults.filter(f => f.level === 'high').length,
+ medium: faults.filter(f => f.level === 'medium').length,
+ resolved: faults.filter(f => f.status === 'resolved').length
+ },
+ faults: faults
+ };
+ }
+
+ // 生成日志数据
+ generateLogData() {
+ const logs = [];
+ const levels = ['ERROR', 'WARN', 'INFO', 'DEBUG'];
+ const services = ['api-gateway', 'user-service', 'order-service', 'payment-service', 'notification-service'];
+ const hosts = ['host-01', 'host-02', 'host-03', 'host-04', 'host-05'];
+
+ for (let i = 1; i <= 200; i++) {
+ const level = levels[Math.floor(Math.random() * levels.length)];
+ const service = services[Math.floor(Math.random() * services.length)];
+ const host = hosts[Math.floor(Math.random() * hosts.length)];
+
+ logs.push({
+ id: i,
+ timestamp: new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000).toISOString(),
+ level: level,
+ service: service,
+ host: host,
+ message: this.generateLogMessage(level, service),
+ details: this.generateLogDetails(level, service),
+ userId: Math.random() > 0.7 ? `user_${Math.floor(Math.random() * 1000)}` : null,
+ requestId: `req_${Math.random().toString(36).substr(2, 9)}`,
+ ip: `192.168.1.${Math.floor(Math.random() * 255)}`
+ });
+ }
+
+ return {
+ overview: {
+ total: logs.length,
+ error: logs.filter(l => l.level === 'ERROR').length,
+ warn: logs.filter(l => l.level === 'WARN').length,
+ info: logs.filter(l => l.level === 'INFO').length,
+ availability: '99.8%'
+ },
+ logs: logs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
+ };
+ }
+
+ // 生成图表数据
+ generateChartData() {
+ return {
+ // 性能趋势数据
+ performanceTrend: DataGenerator.generateTimeSeriesData(24, 3),
+
+ // CPU使用率数据
+ cpuUsage: DataGenerator.generateTimeSeriesData(12, 1),
+
+ // 内存使用率数据
+ memoryUsage: DataGenerator.generateTimeSeriesData(12, 1),
+
+ // 故障分布数据
+ faultDistribution: DataGenerator.generatePieData(['硬件故障', '软件故障', '网络故障', '安全故障']),
+
+ // 日志级别分布
+ logLevelDistribution: DataGenerator.generatePieData(['ERROR', 'WARN', 'INFO', 'DEBUG']),
+
+ // 服务日志分布
+ serviceLogDistribution: DataGenerator.generateBarData(['api-gateway', 'user-service', 'order-service', 'payment-service'])
+ };
+ }
+
+ // 生成故障标题
+ generateFaultTitle(type) {
+ const titles = {
+ hardware: ['CPU温度过高', '内存故障', '硬盘损坏', '网卡故障', '电源异常'],
+ software: ['应用程序崩溃', '服务无响应', '数据库连接失败', '配置错误', '版本冲突'],
+ network: ['网络连接中断', '带宽不足', 'DNS解析失败', '路由异常', '防火墙阻断'],
+ security: ['异常登录尝试', '权限提升攻击', '恶意文件检测', 'SQL注入尝试', '暴力破解']
+ };
+
+ const typeTitle = titles[type] || titles.software;
+ return typeTitle[Math.floor(Math.random() * typeTitle.length)];
+ }
+
+ // 生成故障描述
+ generateFaultDescription(type) {
+ const descriptions = {
+ hardware: '硬件设备出现异常,可能影响系统正常运行',
+ software: '软件服务出现问题,需要及时处理以避免业务中断',
+ network: '网络连接出现异常,可能影响服务间通信',
+ security: '检测到安全威胁,需要立即采取防护措施'
+ };
+
+ return descriptions[type] || descriptions.software;
+ }
+
+ // 生成日志消息
+ generateLogMessage(level, service) {
+ const messages = {
+ ERROR: [
+ 'Database connection failed',
+ 'Authentication failed for user',
+ 'Service timeout occurred',
+ 'Invalid request format',
+ 'Internal server error'
+ ],
+ WARN: [
+ 'High memory usage detected',
+ 'Slow query performance',
+ 'Rate limit approaching',
+ 'Cache miss ratio high',
+ 'Deprecated API usage'
+ ],
+ INFO: [
+ 'User login successful',
+ 'Request processed successfully',
+ 'Service started',
+ 'Configuration updated',
+ 'Backup completed'
+ ],
+ DEBUG: [
+ 'Processing request',
+ 'Cache hit',
+ 'Validation passed',
+ 'Query executed',
+ 'Response sent'
+ ]
+ };
+
+ const levelMessages = messages[level] || messages.INFO;
+ return `[${service}] ${levelMessages[Math.floor(Math.random() * levelMessages.length)]}`;
+ }
+
+ // 生成日志详情
+ generateLogDetails(level, service) {
+ return `详细信息: ${service} 服务在处理请求时出现 ${level} 级别的事件。请查看相关日志以获取更多信息。`;
+ }
+
+ // 获取仪表板数据
+ getDashboardData() {
+ return this.dashboardData;
+ }
+
+ // 获取集群数据
+ getClusterData() {
+ return this.clusterData;
+ }
+
+ // 获取故障数据
+ getFaultData() {
+ return this.faultData;
+ }
+
+ // 获取日志数据
+ getLogData() {
+ return this.logData;
+ }
+
+ // 获取图表数据
+ getChartData() {
+ return this.chartData;
+ }
+
+ // 模拟实时数据更新
+ getRealtimeData() {
+ const now = new Date();
+ return {
+ timestamp: now.toLocaleTimeString('zh-CN'),
+ cpu: Math.floor(Math.random() * 100),
+ memory: Math.floor(Math.random() * 100),
+ network: Math.floor(Math.random() * 1000),
+ activeConnections: Math.floor(Math.random() * 500)
+ };
+ }
+
+ // 搜索故障
+ searchFaults(query, filters = {}) {
+ let results = [...this.faultData.faults];
+
+ // 文本搜索
+ if (query) {
+ results = results.filter(fault =>
+ fault.title.toLowerCase().includes(query.toLowerCase()) ||
+ fault.description.toLowerCase().includes(query.toLowerCase()) ||
+ fault.id.toLowerCase().includes(query.toLowerCase())
+ );
+ }
+
+ // 级别筛选
+ if (filters.level) {
+ results = results.filter(fault => fault.level === filters.level);
+ }
+
+ // 状态筛选
+ if (filters.status) {
+ results = results.filter(fault => fault.status === filters.status);
+ }
+
+ // 类型筛选
+ if (filters.type) {
+ results = results.filter(fault => fault.type === filters.type);
+ }
+
+ return results;
+ }
+
+ // 搜索日志
+ searchLogs(query, filters = {}) {
+ let results = [...this.logData.logs];
+
+ // 文本搜索
+ if (query) {
+ results = results.filter(log =>
+ log.message.toLowerCase().includes(query.toLowerCase()) ||
+ log.service.toLowerCase().includes(query.toLowerCase())
+ );
+ }
+
+ // 级别筛选
+ if (filters.level) {
+ results = results.filter(log => log.level === filters.level);
+ }
+
+ // 服务筛选
+ if (filters.service) {
+ results = results.filter(log => log.service === filters.service);
+ }
+
+ // 主机筛选
+ if (filters.host) {
+ results = results.filter(log => log.host === filters.host);
+ }
+
+ // 时间范围筛选
+ if (filters.startTime && filters.endTime) {
+ const start = new Date(filters.startTime);
+ const end = new Date(filters.endTime);
+ results = results.filter(log => {
+ const logTime = new Date(log.timestamp);
+ return logTime >= start && logTime <= end;
+ });
+ }
+
+ return results;
+ }
+}
+
+// 创建全局实例
+const demoData = new DemoDataManager();
+
+// 导出到全局
+window.DemoDataManager = DemoDataManager;
+window.demoData = demoData;
+
+console.log('演示数据管理器已加载');
\ No newline at end of file
diff --git a/src/frontend/styles/cluster-monitor.css b/src/frontend/styles/cluster-monitor.css
new file mode 100644
index 0000000..6cd5e2f
--- /dev/null
+++ b/src/frontend/styles/cluster-monitor.css
@@ -0,0 +1,634 @@
+/* 集群监控页面样式 */
+.cluster-monitor-page {
+ padding: 20px;
+ background: var(--bg-color);
+ min-height: 100vh;
+}
+
+/* 页面头部 */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.header-content .page-title {
+ font-size: 28px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0 0 8px 0;
+}
+
+.header-content .page-desc {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin: 0;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+}
+
+/* 集群概览统计 */
+.cluster-overview {
+ margin-bottom: 24px;
+}
+
+.overview-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 16px;
+}
+
+.overview-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ transition: all 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.card-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+}
+
+.card-icon.online {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--success-color);
+}
+
+.card-icon.warning {
+ background: rgba(251, 191, 36, 0.1);
+ color: var(--warning-color);
+}
+
+.card-icon.danger {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+.card-icon.info {
+ background: rgba(59, 130, 246, 0.1);
+ color: var(--primary-color);
+}
+
+.card-content {
+ flex: 1;
+}
+
+.card-value {
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.card-label {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.card-trend {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+}
+
+.card-trend.up {
+ color: var(--success-color);
+}
+
+.card-trend.down {
+ color: var(--danger-color);
+}
+
+/* 主要内容区域 */
+.cluster-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 24px;
+ margin-bottom: 24px;
+}
+
+/* 节点列表 */
+.cluster-nodes {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.section-header {
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.section-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.section-actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.filter-group select {
+ min-width: 120px;
+}
+
+.search-group {
+ position: relative;
+}
+
+.search-group input {
+ padding-right: 36px;
+ min-width: 200px;
+}
+
+.search-group .icon-search {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-secondary);
+ pointer-events: none;
+}
+
+.nodes-container {
+ max-height: 600px;
+ overflow-y: auto;
+}
+
+/* 节点卡片 */
+.node-card {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+ transition: background-color 0.2s ease;
+}
+
+.node-card:hover {
+ background: var(--hover-bg);
+}
+
+.node-card:last-child {
+ border-bottom: none;
+}
+
+.node-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.node-info .node-name {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.node-info .node-ip {
+ font-size: 14px;
+ color: var(--text-secondary);
+ font-family: 'Courier New', monospace;
+}
+
+.status-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.status-badge.online {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--success-color);
+}
+
+.status-badge.warning {
+ background: rgba(251, 191, 36, 0.1);
+ color: var(--warning-color);
+}
+
+.status-badge.offline {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+/* 节点指标 */
+.node-metrics {
+ margin-bottom: 12px;
+}
+
+.metric-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.metric-item:last-child {
+ margin-bottom: 0;
+}
+
+.metric-label {
+ width: 40px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.metric-value {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.progress-bar {
+ flex: 1;
+ height: 6px;
+ background: var(--border-color);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--success-color);
+ border-radius: 3px;
+ transition: width 0.3s ease;
+}
+
+.progress-fill.warning {
+ background: var(--warning-color);
+}
+
+.progress-fill.danger {
+ background: var(--danger-color);
+}
+
+.metric-text {
+ font-size: 12px;
+ color: var(--text-secondary);
+ min-width: 32px;
+ text-align: right;
+}
+
+/* 节点操作 */
+.node-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.btn-icon {
+ width: 28px;
+ height: 28px;
+ border: 1px solid var(--border-color);
+ background: transparent;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn-icon:hover {
+ background: var(--hover-bg);
+ border-color: var(--primary-color);
+}
+
+/* 监控图表 */
+.cluster-charts {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.chart-section {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.chart-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.chart-title {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.chart-controls select {
+ min-width: 120px;
+}
+
+.chart-legend {
+ display: flex;
+ gap: 16px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+.legend-color.cpu {
+ background: #3b82f6;
+}
+
+.legend-color.memory {
+ background: #10b981;
+}
+
+.legend-color.disk {
+ background: #f59e0b;
+}
+
+.traffic-stats {
+ display: flex;
+ gap: 20px;
+}
+
+.stat-item {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.stat-label {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.stat-value {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.chart-container {
+ padding: 20px;
+ height: 300px;
+}
+
+.chart-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-color);
+ border: 2px dashed var(--border-color);
+ border-radius: 4px;
+}
+
+.chart-loading {
+ text-align: center;
+ color: var(--text-secondary);
+}
+
+.chart-loading p {
+ margin: 8px 0 0 0;
+ font-size: 14px;
+}
+
+/* 实时告警面板 */
+.alert-panel {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.alert-header {
+ padding: 16px 20px;
+ background: rgba(239, 68, 68, 0.05);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.alert-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.alert-count {
+ background: var(--danger-color);
+ color: white;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.alert-toggle {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+}
+
+.alert-toggle:hover {
+ background: var(--hover-bg);
+}
+
+.alert-content {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.alert-item {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.alert-item:last-child {
+ border-bottom: none;
+}
+
+.alert-item.high {
+ border-left: 4px solid var(--danger-color);
+}
+
+.alert-item.medium {
+ border-left: 4px solid var(--warning-color);
+}
+
+.alert-item.low {
+ border-left: 4px solid var(--info-color);
+}
+
+.alert-icon {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+}
+
+.alert-info {
+ flex: 1;
+}
+
+.alert-message {
+ font-size: 14px;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.alert-time {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.alert-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.btn-small {
+ padding: 4px 12px;
+ font-size: 12px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn-small.btn-primary {
+ background: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+}
+
+.btn-small.btn-primary:hover {
+ background: var(--primary-hover);
+}
+
+.btn-small.btn-secondary {
+ background: transparent;
+ color: var(--text-secondary);
+}
+
+.btn-small.btn-secondary:hover {
+ background: var(--hover-bg);
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+ .cluster-content {
+ grid-template-columns: 1fr;
+ }
+
+ .overview-cards {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .cluster-monitor-page {
+ padding: 16px;
+ }
+
+ .page-header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .header-actions {
+ justify-content: flex-end;
+ }
+
+ .overview-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .section-header {
+ flex-direction: column;
+ gap: 12px;
+ align-items: stretch;
+ }
+
+ .section-actions {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .search-group input {
+ min-width: auto;
+ width: 100%;
+ }
+
+ .chart-header {
+ flex-direction: column;
+ gap: 12px;
+ align-items: stretch;
+ }
+
+ .chart-legend {
+ justify-content: center;
+ }
+
+ .traffic-stats {
+ justify-content: center;
+ }
+}
\ No newline at end of file
diff --git a/src/frontend/styles/components.css b/src/frontend/styles/components.css
new file mode 100644
index 0000000..35d13cf
--- /dev/null
+++ b/src/frontend/styles/components.css
@@ -0,0 +1,519 @@
+/* 通用组件样式 */
+
+/* 模态框样式 */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.modal-overlay.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modal-container {
+ background: var(--card-bg);
+ border-radius: 8px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ max-width: 90vw;
+ max-height: 90vh;
+ overflow: hidden;
+ transform: scale(0.9) translateY(-20px);
+ transition: transform 0.3s ease;
+}
+
+.modal-overlay.show .modal-container {
+ transform: scale(1) translateY(0);
+}
+
+.modal-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-secondary);
+}
+
+.modal-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.modal-close {
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: none;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: var(--text-secondary);
+ transition: all 0.2s ease;
+}
+
+.modal-close:hover {
+ background: var(--bg-color);
+ color: var(--text-primary);
+}
+
+.modal-body {
+ padding: 24px;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.modal-footer {
+ padding: 16px 24px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ background: var(--bg-secondary);
+}
+
+/* 确认对话框样式 */
+.confirm-content {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 20px 0;
+}
+
+.confirm-icon {
+ font-size: 32px;
+ color: #f59e0b;
+}
+
+.confirm-message {
+ font-size: 16px;
+ color: var(--text-primary);
+ line-height: 1.5;
+}
+
+/* 提示框样式 */
+.alert-content {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 20px 0;
+}
+
+.alert-icon {
+ font-size: 32px;
+}
+
+.alert-icon.icon-success {
+ color: #10b981;
+}
+
+.alert-icon.icon-error {
+ color: #ef4444;
+}
+
+.alert-icon.icon-warning {
+ color: #f59e0b;
+}
+
+.alert-icon.icon-info {
+ color: #3b82f6;
+}
+
+.alert-message {
+ font-size: 16px;
+ color: var(--text-primary);
+ line-height: 1.5;
+}
+
+/* 加载组件样式 */
+.loading-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.loading-container.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.8);
+ z-index: 100;
+}
+
+.dark-theme .loading-overlay {
+ background: rgba(0, 0, 0, 0.8);
+}
+
+.loading-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.loading-spinner {
+ position: relative;
+ width: 40px;
+ height: 40px;
+}
+
+.loading-container.small .loading-spinner {
+ width: 24px;
+ height: 24px;
+}
+
+.loading-container.large .loading-spinner {
+ width: 60px;
+ height: 60px;
+}
+
+.spinner-ring {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 3px solid transparent;
+ border-top-color: var(--primary-color);
+ border-radius: 50%;
+ animation: spin 1.2s linear infinite;
+}
+
+.spinner-ring:nth-child(2) {
+ animation-delay: -0.3s;
+ border-top-color: rgba(var(--primary-color-rgb), 0.7);
+}
+
+.spinner-ring:nth-child(3) {
+ animation-delay: -0.6s;
+ border-top-color: rgba(var(--primary-color-rgb), 0.4);
+}
+
+.spinner-ring:nth-child(4) {
+ animation-delay: -0.9s;
+ border-top-color: rgba(var(--primary-color-rgb), 0.2);
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.loading-text {
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.loading-container.small .loading-text {
+ font-size: 12px;
+}
+
+.loading-container.large .loading-text {
+ font-size: 16px;
+}
+
+/* 通知组件样式 */
+.notification-container {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 2000;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-width: 400px;
+}
+
+.notification {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ transform: translateX(100%);
+ opacity: 0;
+ transition: all 0.3s ease;
+}
+
+.notification.show {
+ transform: translateX(0);
+ opacity: 1;
+}
+
+.notification-success {
+ border-left: 4px solid #10b981;
+}
+
+.notification-error {
+ border-left: 4px solid #ef4444;
+}
+
+.notification-warning {
+ border-left: 4px solid #f59e0b;
+}
+
+.notification-info {
+ border-left: 4px solid #3b82f6;
+}
+
+.notification-content {
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.notification-icon {
+ font-size: 18px;
+ flex-shrink: 0;
+}
+
+.notification-success .notification-icon {
+ color: #10b981;
+}
+
+.notification-error .notification-icon {
+ color: #ef4444;
+}
+
+.notification-warning .notification-icon {
+ color: #f59e0b;
+}
+
+.notification-info .notification-icon {
+ color: #3b82f6;
+}
+
+.notification-message {
+ flex: 1;
+ font-size: 14px;
+ color: var(--text-primary);
+ line-height: 1.4;
+}
+
+.notification-close {
+ width: 24px;
+ height: 24px;
+ border: none;
+ background: none;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: var(--text-secondary);
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.notification-close:hover {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+/* 工具提示样式 */
+.tooltip {
+ position: absolute;
+ z-index: 1500;
+ background: rgba(0, 0, 0, 0.9);
+ color: white;
+ border-radius: 4px;
+ font-size: 12px;
+ padding: 8px 12px;
+ max-width: 200px;
+ word-wrap: break-word;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.2s ease;
+ pointer-events: none;
+}
+
+.tooltip.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.tooltip-content {
+ position: relative;
+ z-index: 1;
+}
+
+.tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border: 4px solid transparent;
+}
+
+.tooltip-top .tooltip-arrow {
+ bottom: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ border-top-color: rgba(0, 0, 0, 0.9);
+}
+
+.tooltip-bottom .tooltip-arrow {
+ top: -8px;
+ left: 50%;
+ transform: translateX(-50%);
+ border-bottom-color: rgba(0, 0, 0, 0.9);
+}
+
+.tooltip-left .tooltip-arrow {
+ right: -8px;
+ top: 50%;
+ transform: translateY(-50%);
+ border-left-color: rgba(0, 0, 0, 0.9);
+}
+
+.tooltip-right .tooltip-arrow {
+ left: -8px;
+ top: 50%;
+ transform: translateY(-50%);
+ border-right-color: rgba(0, 0, 0, 0.9);
+}
+
+/* 下拉菜单样式 */
+.dropdown-menu {
+ position: absolute;
+ z-index: 1200;
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ min-width: 160px;
+ max-width: 300px;
+ padding: 4px 0;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-8px);
+ transition: all 0.2s ease;
+}
+
+.dropdown-menu.show {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.dropdown-item {
+ padding: 8px 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ color: var(--text-primary);
+ transition: background-color 0.2s ease;
+}
+
+.dropdown-item:hover:not(.disabled) {
+ background: var(--bg-secondary);
+}
+
+.dropdown-item.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.dropdown-icon {
+ font-size: 16px;
+ color: var(--text-secondary);
+}
+
+.dropdown-text {
+ flex: 1;
+}
+
+.dropdown-divider {
+ height: 1px;
+ background: var(--border-color);
+ margin: 4px 0;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .modal-container {
+ width: 95vw !important;
+ max-height: 95vh;
+ }
+
+ .modal-header,
+ .modal-body,
+ .modal-footer {
+ padding: 16px;
+ }
+
+ .notification-container {
+ left: 20px;
+ right: 20px;
+ max-width: none;
+ }
+
+ .notification {
+ transform: translateY(-100%);
+ }
+
+ .notification.show {
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 480px) {
+ .modal-header,
+ .modal-body,
+ .modal-footer {
+ padding: 12px;
+ }
+
+ .modal-footer {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .modal-footer .btn {
+ width: 100%;
+ }
+
+ .notification-container {
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ }
+
+ .confirm-content,
+ .alert-content {
+ flex-direction: column;
+ text-align: center;
+ gap: 12px;
+ }
+}
\ No newline at end of file
diff --git a/src/frontend/styles/dark.css b/src/frontend/styles/dark.css
new file mode 100644
index 0000000..8be3700
--- /dev/null
+++ b/src/frontend/styles/dark.css
@@ -0,0 +1,133 @@
+/* 深色主题样式 */
+.dark {
+ /* 主色调 */
+ --primary-color: #409EFF;
+ --primary-light: #79BBFF;
+ --primary-dark: #337ECC;
+
+ /* 背景色 - 深色主题 */
+ --bg-color: #1A1A1A;
+ --bg-secondary: #2D2D2D;
+ --bg-tertiary: #3A3A3A;
+
+ /* 文字颜色 - 深色主题 */
+ --text-primary: #E5E5E5;
+ --text-regular: #CCCCCC;
+ --text-secondary: #999999;
+ --text-placeholder: #666666;
+
+ /* 边框颜色 - 深色主题 */
+ --border-color: #404040;
+ --border-light: #4A4A4A;
+ --border-lighter: #555555;
+
+ /* 状态颜色 - 深色主题适配 */
+ --success-color: #67C23A;
+ --warning-color: #E6A23C;
+ --danger-color: #F56C6C;
+ --info-color: #909399;
+
+ /* 阴影 - 深色主题 */
+ --box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
+ --box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2), 0 0 6px rgba(0, 0, 0, 0.1);
+}
+
+/* 深色主题下的特殊样式调整 */
+.dark body {
+ background-color: var(--bg-color);
+ color: var(--text-primary);
+}
+
+/* 深色主题下的表格行悬停效果 */
+.dark .data-table tr:hover {
+ background-color: var(--bg-tertiary);
+}
+
+/* 深色主题下的状态标签 */
+.dark .status-tag.online {
+ background-color: rgba(103, 194, 58, 0.2);
+ color: #81C784;
+ border: 1px solid rgba(103, 194, 58, 0.3);
+}
+
+.dark .status-tag.offline {
+ background-color: rgba(245, 108, 108, 0.2);
+ color: #EF5350;
+ border: 1px solid rgba(245, 108, 108, 0.3);
+}
+
+.dark .status-tag.warning {
+ background-color: rgba(230, 162, 60, 0.2);
+ color: #FFB74D;
+ border: 1px solid rgba(230, 162, 60, 0.3);
+}
+
+/* 深色主题下的表单控件 */
+.dark .form-control {
+ background-color: var(--bg-secondary);
+ border-color: var(--border-color);
+ color: var(--text-primary);
+}
+
+.dark .form-control:focus {
+ background-color: var(--bg-secondary);
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.3);
+}
+
+/* 深色主题下的按钮样式调整 */
+.dark .btn-secondary {
+ background-color: var(--bg-secondary);
+ color: var(--text-regular);
+ border-color: var(--border-color);
+}
+
+.dark .btn-secondary:hover {
+ background-color: var(--bg-tertiary);
+ border-color: var(--border-light);
+}
+
+/* 深色主题下的加载动画 */
+.dark .loading-spinner {
+ border-color: var(--border-color);
+ border-top-color: var(--primary-color);
+}
+
+/* 深色主题下的空状态 */
+.dark .empty-state {
+ color: var(--text-secondary);
+}
+
+.dark .empty-state-desc {
+ color: var(--text-placeholder);
+}
+
+/* 深色主题切换动画 */
+* {
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
+}
+
+/* 深色主题下的滚动条样式 */
+.dark ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+.dark ::-webkit-scrollbar-track {
+ background-color: var(--bg-secondary);
+}
+
+.dark ::-webkit-scrollbar-thumb {
+ background-color: var(--border-color);
+ border-radius: 4px;
+}
+
+.dark ::-webkit-scrollbar-thumb:hover {
+ background-color: var(--border-light);
+}
+
+/* 深色主题下的选中文本 */
+.dark ::selection {
+ background-color: rgba(64, 158, 255, 0.3);
+ color: var(--text-primary);
+}
\ No newline at end of file
diff --git a/src/frontend/styles/fault-manage.css b/src/frontend/styles/fault-manage.css
new file mode 100644
index 0000000..f29a32b
--- /dev/null
+++ b/src/frontend/styles/fault-manage.css
@@ -0,0 +1,735 @@
+/* 故障管理页面样式 */
+.fault-manage-page {
+ padding: 20px;
+ background: var(--bg-color);
+ min-height: 100vh;
+ position: relative;
+}
+
+/* 页面头部 */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.header-content .page-title {
+ font-size: 28px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0 0 8px 0;
+}
+
+.header-content .page-desc {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin: 0;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+}
+
+/* 故障统计概览 */
+.fault-overview {
+ margin-bottom: 24px;
+}
+
+.overview-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 16px;
+}
+
+.overview-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ transition: all 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.overview-card.critical .card-icon {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+.overview-card.high .card-icon {
+ background: rgba(251, 191, 36, 0.1);
+ color: var(--warning-color);
+}
+
+.overview-card.medium .card-icon {
+ background: rgba(59, 130, 246, 0.1);
+ color: var(--primary-color);
+}
+
+.overview-card.resolved .card-icon {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--success-color);
+}
+
+.card-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+}
+
+.card-content {
+ flex: 1;
+}
+
+.card-value {
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.card-label {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.card-trend {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+}
+
+.card-trend.up {
+ color: var(--success-color);
+}
+
+.card-trend.down {
+ color: var(--danger-color);
+}
+
+/* 故障筛选 */
+.fault-filters {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 24px;
+}
+
+.filter-section {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ align-items: end;
+}
+
+.filter-group {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.filter-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.search-group {
+ grid-column: span 2;
+}
+
+.search-input {
+ position: relative;
+}
+
+.search-input input {
+ padding-right: 36px;
+}
+
+.search-input .icon-search {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-secondary);
+ pointer-events: none;
+}
+
+.filter-actions {
+ display: flex;
+ gap: 8px;
+}
+
+/* 故障列表容器 */
+.fault-list-container {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.list-header {
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.list-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.list-title h2 {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.fault-count {
+ font-size: 14px;
+ color: var(--text-secondary);
+ background: var(--bg-color);
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+.list-controls {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.view-toggle {
+ display: flex;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.view-btn {
+ padding: 8px 12px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.view-btn.active {
+ background: var(--primary-color);
+ color: white;
+}
+
+.view-btn:not(.active):hover {
+ background: var(--hover-bg);
+}
+
+.sort-group select {
+ min-width: 150px;
+}
+
+/* 故障表格 */
+.fault-table-view {
+ overflow-x: auto;
+}
+
+.fault-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.fault-table th {
+ background: var(--bg-color);
+ padding: 12px 16px;
+ text-align: left;
+ font-weight: 500;
+ color: var(--text-primary);
+ border-bottom: 1px solid var(--border-color);
+ white-space: nowrap;
+}
+
+.fault-table th.sortable {
+ cursor: pointer;
+ user-select: none;
+}
+
+.fault-table th.sortable:hover {
+ background: var(--hover-bg);
+}
+
+.fault-table th span {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.fault-table td {
+ padding: 16px;
+ border-bottom: 1px solid var(--border-color);
+ vertical-align: top;
+}
+
+.fault-row:hover {
+ background: var(--hover-bg);
+}
+
+/* 故障行样式 */
+.fault-row.critical {
+ border-left: 4px solid var(--danger-color);
+}
+
+.fault-row.high {
+ border-left: 4px solid var(--warning-color);
+}
+
+.fault-row.resolved {
+ opacity: 0.7;
+}
+
+/* 故障ID */
+.fault-id .id-text {
+ font-family: 'Courier New', monospace;
+ font-size: 14px;
+ color: var(--primary-color);
+ font-weight: 500;
+}
+
+/* 故障标题 */
+.fault-title {
+ max-width: 300px;
+}
+
+.title-content .title-text {
+ font-size: 14px;
+ color: var(--text-primary);
+ font-weight: 500;
+ display: block;
+ margin-bottom: 4px;
+ line-height: 1.4;
+}
+
+.title-meta .affected-systems {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+/* 严重程度标签 */
+.severity-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.severity-badge.critical {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+.severity-badge.high {
+ background: rgba(251, 191, 36, 0.1);
+ color: var(--warning-color);
+}
+
+.severity-badge.medium {
+ background: rgba(59, 130, 246, 0.1);
+ color: var(--primary-color);
+}
+
+.severity-badge.low {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--success-color);
+}
+
+/* 故障类型标签 */
+.type-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ background: var(--bg-color);
+ color: var(--text-secondary);
+}
+
+.type-badge.hardware {
+ background: rgba(168, 85, 247, 0.1);
+ color: #8b5cf6;
+}
+
+.type-badge.software {
+ background: rgba(59, 130, 246, 0.1);
+ color: var(--primary-color);
+}
+
+.type-badge.network {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--success-color);
+}
+
+.type-badge.performance {
+ background: rgba(251, 191, 36, 0.1);
+ color: var(--warning-color);
+}
+
+.type-badge.security {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+/* 状态标签 */
+.status-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.status-badge.open {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--danger-color);
+}
+
+.status-badge.in-progress {
+ background: rgba(251, 191, 36, 0.1);
+ color: var(--warning-color);
+}
+
+.status-badge.resolved {
+ background: rgba(34, 197, 94, 0.1);
+ color: var(--success-color);
+}
+
+.status-badge.closed {
+ background: var(--bg-color);
+ color: var(--text-secondary);
+}
+
+/* 负责人信息 */
+.assignee-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.assignee-info .avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: var(--primary-color);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.assignee-info .name {
+ font-size: 14px;
+ color: var(--text-primary);
+}
+
+/* 时间信息 */
+.time-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.time-info .date {
+ font-size: 14px;
+ color: var(--text-primary);
+}
+
+.time-info .time {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+/* 故障操作 */
+.fault-actions {
+ display: flex;
+ gap: 4px;
+}
+
+.action-btn {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--border-color);
+ background: transparent;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.action-btn:hover {
+ background: var(--hover-bg);
+ border-color: var(--primary-color);
+}
+
+/* 分页控件 */
+.pagination-container {
+ padding: 16px 20px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.pagination-info {
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.pagination-controls {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.page-btn {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--border-color);
+ background: transparent;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 14px;
+}
+
+.page-btn:hover:not(:disabled) {
+ background: var(--hover-bg);
+}
+
+.page-btn.active {
+ background: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+}
+
+.page-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.page-dots {
+ padding: 0 8px;
+ color: var(--text-secondary);
+}
+
+.page-size-selector select {
+ min-width: 100px;
+}
+
+/* 故障详情侧边栏 */
+.fault-detail-sidebar {
+ position: fixed;
+ top: 0;
+ right: -500px;
+ width: 500px;
+ height: 100vh;
+ background: var(--card-bg);
+ border-left: 1px solid var(--border-color);
+ z-index: 1000;
+ transition: right 0.3s ease;
+ overflow-y: auto;
+}
+
+.fault-detail-sidebar.active {
+ right: 0;
+}
+
+.sidebar-header {
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-color);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.sidebar-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.close-btn {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--border-color);
+ background: transparent;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.close-btn:hover {
+ background: var(--hover-bg);
+}
+
+.sidebar-content {
+ padding: 20px;
+}
+
+.fault-detail-loading {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--text-secondary);
+}
+
+.fault-detail-loading p {
+ margin: 12px 0 0 0;
+ font-size: 14px;
+}
+
+/* 遮罩层 */
+.sidebar-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 999;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.sidebar-overlay.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+ .filter-section {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .search-group {
+ grid-column: span 3;
+ }
+
+ .fault-detail-sidebar {
+ width: 400px;
+ right: -400px;
+ }
+}
+
+@media (max-width: 768px) {
+ .fault-manage-page {
+ padding: 16px;
+ }
+
+ .page-header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .header-actions {
+ justify-content: flex-end;
+ }
+
+ .overview-cards {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .filter-section {
+ grid-template-columns: 1fr;
+ }
+
+ .search-group {
+ grid-column: span 1;
+ }
+
+ .list-header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .list-controls {
+ justify-content: space-between;
+ }
+
+ .fault-table {
+ font-size: 14px;
+ }
+
+ .fault-table th,
+ .fault-table td {
+ padding: 8px 12px;
+ }
+
+ .fault-title {
+ max-width: 200px;
+ }
+
+ .pagination-container {
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .fault-detail-sidebar {
+ width: 100%;
+ right: -100%;
+ }
+}
+
+@media (max-width: 480px) {
+ .overview-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .fault-table-view {
+ font-size: 12px;
+ }
+
+ .fault-actions {
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .action-btn {
+ width: 28px;
+ height: 28px;
+ }
+}
\ No newline at end of file
diff --git a/src/frontend/styles/layout.css b/src/frontend/styles/layout.css
new file mode 100644
index 0000000..b84f1d8
--- /dev/null
+++ b/src/frontend/styles/layout.css
@@ -0,0 +1,735 @@
+/* 布局组件样式 */
+
+/* ==================== 顶部导航栏样式 ==================== */
+.app-header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 60px;
+ background-color: var(--bg-color);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 20px;
+ z-index: 1000;
+ box-shadow: var(--box-shadow-light);
+}
+
+/* 头部左侧 */
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.menu-toggle {
+ display: none;
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: 6px;
+ color: var(--text-regular);
+ transition: all 0.2s ease;
+}
+
+.menu-toggle:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.system-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.system-title h1 {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.system-title .version {
+ font-size: 12px;
+ color: var(--text-secondary);
+ background-color: var(--bg-secondary);
+ padding: 2px 6px;
+ border-radius: 4px;
+}
+
+/* 头部中间 - 面包屑 */
+.header-center {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.breadcrumb {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+}
+
+.breadcrumb-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: color 0.2s ease;
+}
+
+.breadcrumb-item:hover {
+ color: var(--primary-color);
+}
+
+.breadcrumb-item.active {
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.breadcrumb-separator {
+ color: var(--text-placeholder);
+ font-size: 12px;
+}
+
+/* 头部右侧 */
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+/* 系统状态指示器 */
+.system-status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--success-color);
+ animation: pulse 2s infinite;
+}
+
+.status-dot.offline {
+ background-color: var(--danger-color);
+}
+
+.status-dot.warning {
+ background-color: var(--warning-color);
+}
+
+@keyframes pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.5; }
+ 100% { opacity: 1; }
+}
+
+/* 通知中心 */
+.notification-center {
+ position: relative;
+}
+
+.notification-btn {
+ position: relative;
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: 6px;
+ color: var(--text-regular);
+ transition: all 0.2s ease;
+}
+
+.notification-btn:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.notification-badge {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background-color: var(--danger-color);
+ color: white;
+ font-size: 10px;
+ padding: 2px 5px;
+ border-radius: 8px;
+ min-width: 16px;
+ text-align: center;
+ line-height: 1.2;
+}
+
+.notification-dropdown {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ width: 320px;
+ background-color: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ box-shadow: var(--box-shadow);
+ display: none;
+ z-index: 1001;
+ margin-top: 8px;
+}
+
+.notification-dropdown.show {
+ display: block;
+}
+
+.notification-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.notification-header h4 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.mark-all-read {
+ background: none;
+ border: none;
+ color: var(--primary-color);
+ font-size: 12px;
+ cursor: pointer;
+ padding: 4px 8px;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+}
+
+.mark-all-read:hover {
+ background-color: var(--bg-secondary);
+}
+
+.notification-list {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.notification-item {
+ display: flex;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-lighter);
+ transition: background-color 0.2s ease;
+}
+
+.notification-item:hover {
+ background-color: var(--bg-secondary);
+}
+
+.notification-item.unread {
+ background-color: rgba(64, 158, 255, 0.05);
+}
+
+.notification-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 12px;
+ flex-shrink: 0;
+}
+
+.notification-icon.warning {
+ background-color: rgba(230, 162, 60, 0.2);
+ color: var(--warning-color);
+}
+
+.notification-icon.danger {
+ background-color: rgba(245, 108, 108, 0.2);
+ color: var(--danger-color);
+}
+
+.notification-icon.success {
+ background-color: rgba(103, 194, 58, 0.2);
+ color: var(--success-color);
+}
+
+.notification-content {
+ flex: 1;
+}
+
+.notification-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.notification-desc {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-bottom: 4px;
+ line-height: 1.4;
+}
+
+.notification-time {
+ font-size: 11px;
+ color: var(--text-placeholder);
+}
+
+.notification-footer {
+ padding: 12px 16px;
+ text-align: center;
+ border-top: 1px solid var(--border-color);
+}
+
+.view-all {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.view-all:hover {
+ text-decoration: underline;
+}
+
+/* 主题切换 */
+.theme-toggle {
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: 6px;
+ color: var(--text-regular);
+ transition: all 0.2s ease;
+}
+
+.theme-toggle:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+/* 用户信息 */
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.user-avatar img {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.user-dropdown {
+ position: relative;
+}
+
+.user-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 6px 12px;
+ border-radius: 6px;
+ color: var(--text-primary);
+ transition: background-color 0.2s ease;
+}
+
+.user-btn:hover {
+ background-color: var(--bg-secondary);
+}
+
+.user-name {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.user-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ width: 240px;
+ background-color: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ box-shadow: var(--box-shadow);
+ display: none;
+ z-index: 1001;
+ margin-top: 8px;
+}
+
+.user-menu.show {
+ display: block;
+}
+
+.user-menu-header {
+ padding: 16px;
+ border-bottom: 1px solid var(--border-color);
+ text-align: center;
+}
+
+.user-avatar-large img {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ object-fit: cover;
+ margin-bottom: 8px;
+}
+
+.user-name-large {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.user-role {
+ font-size: 12px;
+ color: var(--text-secondary);
+ margin-bottom: 2px;
+}
+
+.user-email {
+ font-size: 11px;
+ color: var(--text-placeholder);
+}
+
+.user-menu-body {
+ padding: 8px 0;
+}
+
+.user-menu-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 16px;
+ color: var(--text-regular);
+ text-decoration: none;
+ font-size: 14px;
+ transition: all 0.2s ease;
+}
+
+.user-menu-item:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.user-menu-item.logout {
+ color: var(--danger-color);
+}
+
+.user-menu-item.logout:hover {
+ background-color: rgba(245, 108, 108, 0.1);
+}
+
+.user-menu-divider {
+ height: 1px;
+ background-color: var(--border-color);
+ margin: 8px 0;
+}
+
+/* ==================== 侧边栏样式 ==================== */
+.app-sidebar {
+ position: fixed;
+ top: 60px;
+ left: 0;
+ width: 240px;
+ height: calc(100vh - 60px);
+ background-color: var(--bg-color);
+ border-right: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ z-index: 999;
+ transition: width 0.3s ease;
+}
+
+/* 侧边栏头部 */
+.sidebar-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.sidebar-logo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.sidebar-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.sidebar-collapse {
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: none;
+ cursor: pointer;
+ border-radius: 4px;
+ color: var(--text-secondary);
+ transition: all 0.2s ease;
+}
+
+.sidebar-collapse:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+/* 导航菜单 */
+.sidebar-nav {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 0;
+}
+
+.nav-menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.nav-item {
+ margin-bottom: 2px;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 20px;
+ color: var(--text-regular);
+ text-decoration: none;
+ font-size: 14px;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.nav-link:hover {
+ background-color: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.nav-link.active {
+ background-color: rgba(64, 158, 255, 0.1);
+ color: var(--primary-color);
+ border-right: 3px solid var(--primary-color);
+}
+
+.nav-icon {
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.nav-text {
+ flex: 1;
+}
+
+.nav-badge {
+ background-color: var(--success-color);
+ color: white;
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 8px;
+ font-weight: 500;
+}
+
+.nav-count {
+ background-color: var(--bg-secondary);
+ color: var(--text-secondary);
+ font-size: 11px;
+ padding: 2px 6px;
+ border-radius: 8px;
+ min-width: 18px;
+ text-align: center;
+}
+
+.nav-alert {
+ background-color: var(--danger-color);
+ color: white;
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 8px;
+ min-width: 16px;
+ text-align: center;
+ animation: pulse 2s infinite;
+}
+
+/* 子菜单 */
+.nav-submenu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-secondary);
+ display: none;
+}
+
+.nav-item:hover .nav-submenu,
+.nav-item.active .nav-submenu {
+ display: block;
+}
+
+.nav-subitem {
+ margin: 0;
+}
+
+.nav-sublink {
+ display: flex;
+ align-items: center;
+ padding: 8px 20px 8px 52px;
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 13px;
+ transition: all 0.2s ease;
+}
+
+.nav-sublink:hover {
+ background-color: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.nav-sublink.active {
+ color: var(--primary-color);
+ background-color: rgba(64, 158, 255, 0.05);
+}
+
+/* 分割线 */
+.nav-divider {
+ height: 1px;
+ background-color: var(--border-color);
+ margin: 8px 20px;
+}
+
+/* 侧边栏底部 */
+.sidebar-footer {
+ padding: 16px 20px;
+ border-top: 1px solid var(--border-color);
+}
+
+.system-info {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.system-version {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 12px;
+}
+
+.version-label {
+ color: var(--text-secondary);
+}
+
+.version-number {
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.system-status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: var(--text-secondary);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .menu-toggle {
+ display: flex;
+ }
+
+ .app-sidebar {
+ transform: translateX(-100%);
+ transition: transform 0.3s ease;
+ }
+
+ .app-sidebar.show {
+ transform: translateX(0);
+ }
+
+ .header-center {
+ display: none;
+ }
+
+ .system-title h1 {
+ font-size: 18px;
+ }
+}
+
+/* 收起状态 */
+.app-sidebar.collapsed {
+ width: 60px;
+}
+
+.app-sidebar.collapsed .sidebar-title,
+.app-sidebar.collapsed .nav-text,
+.app-sidebar.collapsed .nav-badge,
+.app-sidebar.collapsed .nav-count,
+.app-sidebar.collapsed .nav-alert {
+ display: none;
+}
+
+.app-sidebar.collapsed .nav-submenu {
+ display: none !important;
+}
+
+.app-sidebar.collapsed .nav-link {
+ justify-content: center;
+ padding: 12px;
+}
+
+.app-sidebar.collapsed .sidebar-header {
+ padding: 16px 12px;
+ justify-content: center;
+}
+
+.app-sidebar.collapsed .sidebar-collapse {
+ display: none;
+}
+
+.app-sidebar.collapsed .sidebar-footer {
+ padding: 16px 12px;
+}
+
+.app-sidebar.collapsed .system-info {
+ align-items: center;
+}
+
+.app-sidebar.collapsed .system-version,
+.app-sidebar.collapsed .system-status {
+ display: none;
+}
\ No newline at end of file
diff --git a/src/frontend/styles/log-analysis.css b/src/frontend/styles/log-analysis.css
new file mode 100644
index 0000000..781c983
--- /dev/null
+++ b/src/frontend/styles/log-analysis.css
@@ -0,0 +1,759 @@
+/* 日志分析页面样式 */
+.log-analysis-page {
+ padding: 20px;
+ background: var(--bg-color);
+ min-height: 100vh;
+}
+
+/* 页面头部 */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 24px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.header-content h1 {
+ margin: 0 0 8px 0;
+ color: var(--text-primary);
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.header-content p {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+}
+
+/* 日志统计概览 */
+.log-overview {
+ margin-bottom: 32px;
+}
+
+.overview-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 20px;
+}
+
+.overview-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ transition: all 0.3s ease;
+}
+
+.overview-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.overview-card.total {
+ border-left: 4px solid #3b82f6;
+}
+
+.overview-card.error {
+ border-left: 4px solid #ef4444;
+}
+
+.overview-card.warning {
+ border-left: 4px solid #f59e0b;
+}
+
+.overview-card.info {
+ border-left: 4px solid #10b981;
+}
+
+.card-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+}
+
+.total .card-icon {
+ background: rgba(59, 130, 246, 0.1);
+}
+
+.error .card-icon {
+ background: rgba(239, 68, 68, 0.1);
+}
+
+.warning .card-icon {
+ background: rgba(245, 158, 11, 0.1);
+}
+
+.info .card-icon {
+ background: rgba(16, 185, 129, 0.1);
+}
+
+.card-content {
+ flex: 1;
+}
+
+.card-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.card-label {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.card-trend {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.card-trend.up {
+ color: #10b981;
+}
+
+.card-trend.down {
+ color: #ef4444;
+}
+
+/* 日志查询区域 */
+.log-query-section {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 24px;
+ margin-bottom: 32px;
+}
+
+.query-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.section-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.query-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.query-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.query-row {
+ display: flex;
+ gap: 20px;
+ align-items: flex-end;
+}
+
+.query-group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.query-group.full-width {
+ flex: 1 1 100%;
+}
+
+.query-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.time-range-selector {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.absolute-time {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.time-separator {
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.level-selector {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.checkbox-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.checkbox-item input[type="checkbox"] {
+ margin: 0;
+}
+
+.level-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+
+.level-badge.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: #ef4444;
+ border: 1px solid rgba(239, 68, 68, 0.2);
+}
+
+.level-badge.warning {
+ background: rgba(245, 158, 11, 0.1);
+ color: #f59e0b;
+ border: 1px solid rgba(245, 158, 11, 0.2);
+}
+
+.level-badge.info {
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+ border: 1px solid rgba(59, 130, 246, 0.2);
+}
+
+.level-badge.debug {
+ background: rgba(107, 114, 128, 0.1);
+ color: #6b7280;
+ border: 1px solid rgba(107, 114, 128, 0.2);
+}
+
+.search-input-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.search-options {
+ display: flex;
+ gap: 16px;
+}
+
+.advanced-query {
+ border-top: 1px solid var(--border-color);
+ padding-top: 20px;
+ margin-top: 20px;
+}
+
+.query-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 20px;
+ border-top: 1px solid var(--border-color);
+}
+
+.query-controls .btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+/* 日志分析结果区域 */
+.log-results-section {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+/* 日志图表 */
+.log-charts {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 20px;
+}
+
+.chart-container {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.chart-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.chart-title {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.chart-legend {
+ display: flex;
+ gap: 16px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+.legend-color.error {
+ background: #ef4444;
+}
+
+.legend-color.warning {
+ background: #f59e0b;
+}
+
+.legend-color.info {
+ background: #3b82f6;
+}
+
+.chart-content {
+ height: 300px;
+}
+
+.chart-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-secondary);
+ border-radius: 4px;
+ border: 2px dashed var(--border-color);
+}
+
+.chart-loading {
+ text-align: center;
+ color: var(--text-secondary);
+}
+
+.chart-loading p {
+ margin: 8px 0 0 0;
+ font-size: 14px;
+}
+
+/* 日志列表 */
+.log-list-container {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.list-header {
+ padding: 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-secondary);
+}
+
+.list-info {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.list-title {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.log-count {
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.search-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+}
+
+.status-text {
+ color: #10b981;
+ font-weight: 500;
+}
+
+.search-time {
+ color: var(--text-secondary);
+}
+
+.list-controls {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.display-options {
+ display: flex;
+ gap: 16px;
+}
+
+.view-controls {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+/* 日志条目 */
+.log-list {
+ max-height: 600px;
+ overflow-y: auto;
+}
+
+.log-entry {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ transition: background-color 0.2s ease;
+}
+
+.log-entry:hover {
+ background: var(--bg-secondary);
+}
+
+.log-entry:last-child {
+ border-bottom: none;
+}
+
+.log-entry.error {
+ border-left: 3px solid #ef4444;
+}
+
+.log-entry.warning {
+ border-left: 3px solid #f59e0b;
+}
+
+.log-entry.info {
+ border-left: 3px solid #3b82f6;
+}
+
+.log-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 12px;
+}
+
+.log-timestamp {
+ color: var(--text-secondary);
+ font-family: 'Courier New', monospace;
+ min-width: 180px;
+}
+
+.log-level {
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-weight: 500;
+ text-transform: uppercase;
+ min-width: 50px;
+ text-align: center;
+}
+
+.log-level.error {
+ background: rgba(239, 68, 68, 0.1);
+ color: #ef4444;
+}
+
+.log-level.warning {
+ background: rgba(245, 158, 11, 0.1);
+ color: #f59e0b;
+}
+
+.log-level.info {
+ background: rgba(59, 130, 246, 0.1);
+ color: #3b82f6;
+}
+
+.log-source {
+ color: var(--text-primary);
+ font-weight: 500;
+ min-width: 100px;
+}
+
+.log-host {
+ color: var(--text-secondary);
+ min-width: 120px;
+}
+
+.log-content {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-left: 12px;
+}
+
+.log-message {
+ color: var(--text-primary);
+ font-size: 14px;
+ line-height: 1.5;
+ font-family: 'Courier New', monospace;
+}
+
+.log-details {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.log-detail-item {
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.log-detail-item strong {
+ color: var(--text-primary);
+}
+
+.log-actions {
+ display: flex;
+ gap: 8px;
+ margin-left: auto;
+ align-self: flex-start;
+}
+
+.action-btn {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--border-color);
+ background: var(--card-bg);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ color: var(--text-secondary);
+}
+
+.action-btn:hover {
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ border-color: var(--primary-color);
+}
+
+/* 分页控件 */
+.pagination-container {
+ padding: 16px 20px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-secondary);
+}
+
+.pagination-info {
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.pagination-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.page-btn {
+ width: 32px;
+ height: 32px;
+ border: 1px solid var(--border-color);
+ background: var(--card-bg);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 14px;
+ color: var(--text-primary);
+}
+
+.page-btn:hover:not(:disabled) {
+ background: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+}
+
+.page-btn.active {
+ background: var(--primary-color);
+ color: white;
+ border-color: var(--primary-color);
+}
+
+.page-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.page-dots {
+ padding: 0 8px;
+ color: var(--text-secondary);
+}
+
+.jump-to-page {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: var(--text-secondary);
+}
+
+.jump-to-page input {
+ width: 60px;
+ text-align: center;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+ .log-charts {
+ grid-template-columns: 1fr;
+ }
+
+ .chart-content {
+ height: 250px;
+ }
+}
+
+@media (max-width: 768px) {
+ .log-analysis-page {
+ padding: 16px;
+ }
+
+ .page-header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .overview-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .query-row {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .list-header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .list-info {
+ flex-direction: column;
+ gap: 8px;
+ align-items: flex-start;
+ }
+
+ .log-header {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .log-details {
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .pagination-container {
+ flex-direction: column;
+ gap: 12px;
+ }
+}
+
+@media (max-width: 480px) {
+ .header-actions {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .time-range-selector {
+ flex-direction: column;
+ gap: 8px;
+ align-items: stretch;
+ }
+
+ .level-selector {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .search-options {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .query-controls {
+ flex-direction: column;
+ gap: 12px;
+ align-items: stretch;
+ }
+
+ .display-options {
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .view-controls {
+ flex-direction: column;
+ gap: 8px;
+ align-items: stretch;
+ }
+}
\ No newline at end of file
diff --git a/src/frontend/styles/login.css b/src/frontend/styles/login.css
new file mode 100644
index 0000000..cc434e3
--- /dev/null
+++ b/src/frontend/styles/login.css
@@ -0,0 +1,639 @@
+/* 登录页面样式 */
+
+.login-page {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+/* 背景装饰 */
+.login-background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+.bg-pattern {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image:
+ radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
+ background-size: 400px 400px;
+ animation: patternMove 20s linear infinite;
+}
+
+@keyframes patternMove {
+ 0% { transform: translate(0, 0); }
+ 100% { transform: translate(-400px, -400px); }
+}
+
+.bg-particles {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+/* 登录容器 */
+.login-container {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ width: 90%;
+ max-width: 1000px;
+ height: 600px;
+ background-color: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ backdrop-filter: blur(10px);
+}
+
+/* 左侧系统介绍 */
+.login-intro {
+ flex: 1;
+ background: linear-gradient(135deg, #409EFF 0%, #36CFC9 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ color: white;
+ position: relative;
+}
+
+.login-intro::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: url('data:image/svg+xml, ');
+ opacity: 0.3;
+}
+
+.intro-content {
+ position: relative;
+ z-index: 1;
+ text-align: center;
+}
+
+.system-logo {
+ margin-bottom: 24px;
+ animation: logoFloat 3s ease-in-out infinite;
+}
+
+@keyframes logoFloat {
+ 0%, 100% { transform: translateY(0px); }
+ 50% { transform: translateY(-10px); }
+}
+
+.system-name {
+ font-size: 32px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.system-desc {
+ font-size: 16px;
+ line-height: 1.6;
+ margin-bottom: 40px;
+ opacity: 0.9;
+}
+
+.feature-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ text-align: left;
+}
+
+.feature-item {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px;
+ background-color: rgba(255, 255, 255, 0.1);
+ border-radius: 12px;
+ backdrop-filter: blur(5px);
+ transition: transform 0.2s ease;
+}
+
+.feature-item:hover {
+ transform: translateX(5px);
+}
+
+.feature-icon {
+ font-size: 24px;
+ width: 48px;
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+}
+
+.feature-text h4 {
+ font-size: 16px;
+ font-weight: 600;
+ margin: 0 0 4px 0;
+}
+
+.feature-text p {
+ font-size: 14px;
+ margin: 0;
+ opacity: 0.8;
+}
+
+/* 右侧登录表单 */
+.login-form-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ background-color: white;
+}
+
+.login-form-wrapper {
+ width: 100%;
+ max-width: 360px;
+}
+
+.login-header {
+ text-align: center;
+ margin-bottom: 32px;
+}
+
+.login-header h2 {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0 0 8px 0;
+}
+
+.login-header p {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin: 0;
+}
+
+/* 表单样式 */
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.form-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.input-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.input-icon {
+ position: absolute;
+ left: 12px;
+ z-index: 1;
+ color: var(--text-placeholder);
+}
+
+.form-control {
+ width: 100%;
+ height: 44px;
+ padding: 0 44px 0 44px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-size: 14px;
+ color: var(--text-primary);
+ background-color: var(--bg-color);
+ transition: all 0.2s ease;
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
+}
+
+.form-control::placeholder {
+ color: var(--text-placeholder);
+}
+
+.password-toggle {
+ position: absolute;
+ right: 12px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-placeholder);
+ padding: 4px;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+}
+
+.password-toggle:hover {
+ color: var(--text-secondary);
+ background-color: var(--bg-secondary);
+}
+
+/* 验证码样式 */
+.captcha-wrapper {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.captcha-input {
+ flex: 1;
+}
+
+.captcha-image {
+ position: relative;
+ width: 100px;
+ height: 40px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ overflow: hidden;
+ background-color: #f5f5f5;
+}
+
+#captchaCanvas {
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+}
+
+.captcha-refresh {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 20px;
+ height: 20px;
+ background-color: rgba(0, 0, 0, 0.5);
+ border: none;
+ border-radius: 50%;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ transition: all 0.2s ease;
+}
+
+.captcha-refresh:hover {
+ background-color: rgba(0, 0, 0, 0.7);
+}
+
+/* 表单选项 */
+.form-options {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ margin: 8px 0;
+}
+
+.checkbox-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ color: var(--text-regular);
+}
+
+.checkbox-wrapper input[type="checkbox"] {
+ display: none;
+}
+
+.checkbox-custom {
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border-color);
+ border-radius: 3px;
+ position: relative;
+ transition: all 0.2s ease;
+}
+
+.checkbox-wrapper input[type="checkbox"]:checked + .checkbox-custom {
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.checkbox-wrapper input[type="checkbox"]:checked + .checkbox-custom::after {
+ content: '✓';
+ position: absolute;
+ top: -2px;
+ left: 2px;
+ color: white;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.forgot-password {
+ font-size: 14px;
+ color: var(--primary-color);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.forgot-password:hover {
+ color: var(--primary-dark);
+ text-decoration: underline;
+}
+
+/* 登录按钮 */
+.login-btn {
+ position: relative;
+ width: 100%;
+ height: 48px;
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
+ border: none;
+ border-radius: 8px;
+ color: white;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ overflow: hidden;
+ margin-top: 8px;
+}
+
+.login-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(64, 158, 255, 0.3);
+}
+
+.login-btn:active {
+ transform: translateY(0);
+}
+
+.login-btn.loading {
+ pointer-events: none;
+}
+
+.btn-text {
+ transition: opacity 0.2s ease;
+}
+
+.login-btn.loading .btn-text {
+ opacity: 0;
+}
+
+.btn-loading {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.login-btn.loading .btn-loading {
+ opacity: 1;
+}
+
+/* 分割线 */
+.login-divider {
+ position: relative;
+ text-align: center;
+ margin: 24px 0;
+ color: var(--text-placeholder);
+ font-size: 14px;
+}
+
+.login-divider::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background-color: var(--border-lighter);
+}
+
+.login-divider span {
+ background-color: white;
+ padding: 0 16px;
+ position: relative;
+ z-index: 1;
+}
+
+/* 社交登录 */
+.social-login {
+ display: flex;
+ gap: 12px;
+}
+
+.social-btn {
+ flex: 1;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background-color: white;
+ color: var(--text-regular);
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.social-btn:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+ background-color: rgba(64, 158, 255, 0.05);
+}
+
+/* 表单错误提示 */
+.form-error {
+ font-size: 12px;
+ color: var(--danger-color);
+ margin-top: 4px;
+ min-height: 16px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.form-error:empty {
+ display: none;
+}
+
+.form-error::before {
+ content: '⚠️';
+ font-size: 10px;
+}
+
+/* 登录页面底部 */
+.login-footer {
+ margin-top: 32px;
+ padding-top: 20px;
+ border-top: 1px solid var(--border-lighter);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 12px;
+ color: var(--text-placeholder);
+}
+
+.system-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.separator {
+ color: var(--border-color);
+}
+
+.help-links {
+ display: flex;
+ gap: 12px;
+}
+
+.help-link {
+ color: var(--text-placeholder);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.help-link:hover {
+ color: var(--primary-color);
+}
+
+/* 系统状态指示器 */
+.system-status-indicator {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ z-index: 3;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.status-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background-color: rgba(255, 255, 255, 0.9);
+ border-radius: 20px;
+ font-size: 12px;
+ color: var(--text-secondary);
+ backdrop-filter: blur(5px);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .login-container {
+ flex-direction: column;
+ width: 95%;
+ height: auto;
+ max-height: 90vh;
+ overflow-y: auto;
+ }
+
+ .login-intro {
+ padding: 30px 20px;
+ }
+
+ .system-name {
+ font-size: 24px;
+ }
+
+ .feature-list {
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 12px;
+ }
+
+ .feature-item {
+ flex: 1;
+ min-width: 140px;
+ padding: 12px;
+ }
+
+ .feature-icon {
+ width: 36px;
+ height: 36px;
+ font-size: 18px;
+ }
+
+ .login-form-container {
+ padding: 30px 20px;
+ }
+
+ .login-form-wrapper {
+ max-width: none;
+ }
+
+ .system-status-indicator {
+ position: relative;
+ bottom: auto;
+ right: auto;
+ margin-top: 20px;
+ align-self: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .login-container {
+ width: 100%;
+ height: 100vh;
+ border-radius: 0;
+ }
+
+ .captcha-wrapper {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .captcha-image {
+ width: 100%;
+ height: 50px;
+ }
+
+ .social-login {
+ flex-direction: column;
+ }
+
+ .login-footer {
+ flex-direction: column;
+ gap: 12px;
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/src/frontend/styles/main.css b/src/frontend/styles/main.css
new file mode 100644
index 0000000..750319a
--- /dev/null
+++ b/src/frontend/styles/main.css
@@ -0,0 +1,529 @@
+/* 全局样式重置 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+/* CSS 变量定义 - 浅色主题 */
+:root {
+ /* 主色调 */
+ --primary-color: #409EFF;
+ --primary-light: #79BBFF;
+ --primary-dark: #337ECC;
+
+ /* 背景色 */
+ --bg-color: #FFFFFF;
+ --bg-secondary: #F5F7FA;
+ --bg-tertiary: #EBEEF5;
+
+ /* 文字颜色 */
+ --text-primary: #303133;
+ --text-regular: #606266;
+ --text-secondary: #909399;
+ --text-placeholder: #C0C4CC;
+
+ /* 边框颜色 */
+ --border-color: #DCDFE6;
+ --border-light: #E4E7ED;
+ --border-lighter: #EBEEF5;
+
+ /* 状态颜色 */
+ --success-color: #67C23A;
+ --warning-color: #E6A23C;
+ --danger-color: #F56C6C;
+ --info-color: #909399;
+
+ /* 阴影 */
+ --box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ --box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
+
+ /* 布局尺寸 */
+ --header-height: 60px;
+ --sidebar-width: 240px;
+ --sidebar-collapsed-width: 64px;
+}
+
+/* 基础样式 */
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-primary);
+ background-color: var(--bg-color);
+ overflow-x: hidden;
+}
+
+/* 应用根容器 */
+#app {
+ width: 100%;
+ height: 100vh;
+ position: relative;
+}
+
+/* 页面容器 */
+.page-container {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 主布局 */
+.main-layout {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* 顶部导航栏 */
+.header {
+ height: var(--header-height);
+ background-color: var(--bg-color);
+ border-bottom: 1px solid var(--border-color);
+ box-shadow: var(--box-shadow-light);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 20px;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+}
+
+.header-logo {
+ font-size: 18px;
+ font-weight: bold;
+ color: var(--primary-color);
+ margin-right: 20px;
+}
+
+.header-title {
+ font-size: 16px;
+ color: var(--text-primary);
+}
+
+.header-right {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.header-user {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text-regular);
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+/* 侧边栏 */
+.sidebar {
+ width: var(--sidebar-width);
+ height: calc(100vh - var(--header-height));
+ background-color: var(--bg-color);
+ border-right: 1px solid var(--border-color);
+ position: fixed;
+ top: var(--header-height);
+ left: 0;
+ z-index: 999;
+ overflow-y: auto;
+ transition: width 0.3s ease;
+}
+
+.sidebar.collapsed {
+ width: var(--sidebar-collapsed-width);
+}
+
+.sidebar-menu {
+ padding: 20px 0;
+}
+
+.menu-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 20px;
+ color: var(--text-regular);
+ text-decoration: none;
+ transition: all 0.3s ease;
+ cursor: pointer;
+}
+
+.menu-item:hover {
+ background-color: var(--bg-secondary);
+ color: var(--primary-color);
+}
+
+.menu-item.active {
+ background-color: var(--primary-color);
+ color: #FFFFFF;
+}
+
+.menu-item-icon {
+ width: 20px;
+ height: 20px;
+ margin-right: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.menu-item-text {
+ flex: 1;
+ transition: opacity 0.3s ease;
+}
+
+.sidebar.collapsed .menu-item-text {
+ opacity: 0;
+ width: 0;
+ overflow: hidden;
+}
+
+/* 主内容区域 */
+.main-content {
+ flex: 1;
+ margin-top: var(--header-height);
+ margin-left: var(--sidebar-width);
+ padding: 20px;
+ background-color: var(--bg-secondary);
+ overflow-y: auto;
+ transition: margin-left 0.3s ease;
+}
+
+.sidebar.collapsed + .main-content {
+ margin-left: var(--sidebar-collapsed-width);
+}
+
+/* 页面视图 */
+.page-view {
+ background-color: var(--bg-color);
+ border-radius: 8px;
+ box-shadow: var(--box-shadow);
+ padding: 24px;
+ margin-bottom: 20px;
+}
+
+.page-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border-lighter);
+}
+
+.page-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.page-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+/* 卡片样式 */
+.card {
+ background-color: var(--bg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ box-shadow: var(--box-shadow-light);
+ overflow: hidden;
+}
+
+.card-header {
+ padding: 16px 20px;
+ background-color: var(--bg-tertiary);
+ border-bottom: 1px solid var(--border-color);
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.card-body {
+ padding: 20px;
+}
+
+/* 表格样式 */
+.table-container {
+ overflow-x: auto;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+ background-color: var(--bg-color);
+}
+
+.data-table th,
+.data-table td {
+ padding: 12px 16px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-lighter);
+}
+
+.data-table th {
+ background-color: var(--bg-tertiary);
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.data-table tr:hover {
+ background-color: var(--bg-secondary);
+}
+
+/* 状态标签 */
+.status-tag {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.status-tag.online {
+ background-color: #F0F9FF;
+ color: var(--success-color);
+ border: 1px solid #B3E5FC;
+}
+
+.status-tag.offline {
+ background-color: #FFF5F5;
+ color: var(--danger-color);
+ border: 1px solid #FFCDD2;
+}
+
+.status-tag.warning {
+ background-color: #FFFBF0;
+ color: var(--warning-color);
+ border: 1px solid #FFE0B2;
+}
+
+/* 按钮样式 */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 500;
+ text-decoration: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ outline: none;
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: #FFFFFF;
+ border-color: var(--primary-color);
+}
+
+.btn-primary:hover {
+ background-color: var(--primary-light);
+ border-color: var(--primary-light);
+}
+
+.btn-success {
+ background-color: var(--success-color);
+ color: #FFFFFF;
+ border-color: var(--success-color);
+}
+
+.btn-warning {
+ background-color: var(--warning-color);
+ color: #FFFFFF;
+ border-color: var(--warning-color);
+}
+
+.btn-danger {
+ background-color: var(--danger-color);
+ color: #FFFFFF;
+ border-color: var(--danger-color);
+}
+
+.btn-secondary {
+ background-color: var(--bg-color);
+ color: var(--text-regular);
+ border-color: var(--border-color);
+}
+
+.btn-secondary:hover {
+ background-color: var(--bg-secondary);
+ border-color: var(--border-light);
+}
+
+/* 表单样式 */
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.form-control {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 14px;
+ color: var(--text-primary);
+ background-color: var(--bg-color);
+ transition: border-color 0.3s ease;
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+/* 图表容器 */
+.chart-container {
+ width: 100%;
+ height: 400px;
+ margin: 20px 0;
+}
+
+/* 加载动画 */
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ color: var(--text-secondary);
+}
+
+.loading-spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--border-color);
+ border-top: 2px solid var(--primary-color);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-right: 8px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* 空状态 */
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: var(--text-secondary);
+}
+
+.empty-state-icon {
+ font-size: 48px;
+ margin-bottom: 16px;
+ opacity: 0.5;
+}
+
+.empty-state-text {
+ font-size: 16px;
+ margin-bottom: 8px;
+}
+
+.empty-state-desc {
+ font-size: 14px;
+ color: var(--text-placeholder);
+}
+
+/* 响应式设计 */
+@media (max-width: 1366px) {
+ .main-content {
+ padding: 16px;
+ }
+
+ .page-view {
+ padding: 20px;
+ }
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ transform: translateX(-100%);
+ transition: transform 0.3s ease;
+ }
+
+ .sidebar.mobile-open {
+ transform: translateX(0);
+ }
+
+ .main-content {
+ margin-left: 0;
+ padding: 12px;
+ }
+
+ .header {
+ padding: 0 16px;
+ }
+
+ .page-view {
+ padding: 16px;
+ }
+
+ .page-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .page-actions {
+ width: 100%;
+ justify-content: flex-end;
+ }
+}
+
+/* 工具类 */
+.text-center { text-align: center; }
+.text-left { text-align: left; }
+.text-right { text-align: right; }
+
+.mb-0 { margin-bottom: 0; }
+.mb-1 { margin-bottom: 8px; }
+.mb-2 { margin-bottom: 16px; }
+.mb-3 { margin-bottom: 24px; }
+.mb-4 { margin-bottom: 32px; }
+
+.mt-0 { margin-top: 0; }
+.mt-1 { margin-top: 8px; }
+.mt-2 { margin-top: 16px; }
+.mt-3 { margin-top: 24px; }
+.mt-4 { margin-top: 32px; }
+
+.d-flex { display: flex; }
+.d-block { display: block; }
+.d-none { display: none; }
+
+.flex-1 { flex: 1; }
+.align-center { align-items: center; }
+.justify-center { justify-content: center; }
+.justify-between { justify-content: space-between; }
+
+.w-100 { width: 100%; }
+.h-100 { height: 100%; }
\ No newline at end of file
diff --git a/src/frontend/styles/responsive.css b/src/frontend/styles/responsive.css
new file mode 100644
index 0000000..9e6655a
--- /dev/null
+++ b/src/frontend/styles/responsive.css
@@ -0,0 +1,486 @@
+/**
+ * 响应式设计样式
+ * 确保在不同屏幕尺寸下的良好显示效果
+ */
+
+/* 基础响应式断点 */
+:root {
+ --breakpoint-xs: 480px;
+ --breakpoint-sm: 768px;
+ --breakpoint-md: 1024px;
+ --breakpoint-lg: 1200px;
+ --breakpoint-xl: 1440px;
+}
+
+/* 通用响应式工具类 */
+.container {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 -10px;
+}
+
+.col {
+ flex: 1;
+ padding: 0 10px;
+}
+
+/* 隐藏/显示工具类 */
+.hidden-xs { display: block; }
+.hidden-sm { display: block; }
+.hidden-md { display: block; }
+.hidden-lg { display: block; }
+
+.visible-xs { display: none; }
+.visible-sm { display: none; }
+.visible-md { display: none; }
+.visible-lg { display: none; }
+
+/* 超小屏幕 (手机) */
+@media (max-width: 479px) {
+ .hidden-xs { display: none !important; }
+ .visible-xs { display: block !important; }
+
+ /* 容器调整 */
+ .container {
+ padding: 0 15px;
+ }
+
+ /* 应用布局调整 */
+ .app-layout {
+ flex-direction: column;
+ }
+
+ .sidebar-container {
+ position: fixed;
+ top: 0;
+ left: -280px;
+ width: 280px;
+ height: 100vh;
+ z-index: 1000;
+ transition: left 0.3s ease;
+ }
+
+ .sidebar-container.active {
+ left: 0;
+ }
+
+ .main-content {
+ margin-left: 0;
+ padding: 10px;
+ }
+
+ /* 头部调整 */
+ .header-container {
+ padding: 10px 15px;
+ }
+
+ .page-header {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: stretch;
+ }
+
+ .header-actions .btn {
+ flex: 1;
+ margin: 0 5px;
+ }
+
+ /* 卡片布局调整 */
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: 1fr;
+ gap: 15px;
+ }
+
+ .dashboard-card,
+ .stat-card {
+ padding: 15px;
+ }
+
+ /* 表格响应式 */
+ .data-table {
+ font-size: 12px;
+ }
+
+ .data-table th,
+ .data-table td {
+ padding: 8px 4px;
+ }
+
+ /* 隐藏不重要的列 */
+ .data-table .hidden-xs {
+ display: none;
+ }
+
+ /* 表单调整 */
+ .form-row {
+ flex-direction: column;
+ }
+
+ .form-group {
+ margin-bottom: 15px;
+ }
+
+ /* 按钮调整 */
+ .btn-group {
+ flex-direction: column;
+ }
+
+ .btn-group .btn {
+ margin: 2px 0;
+ }
+
+ /* 模态框调整 */
+ .modal-content {
+ width: 95%;
+ margin: 20px auto;
+ max-height: 90vh;
+ }
+
+ /* 图表容器调整 */
+ .chart-container {
+ height: 250px;
+ }
+
+ /* 节点卡片调整 */
+ .node-card {
+ margin-bottom: 15px;
+ }
+
+ .node-metrics {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+
+/* 小屏幕 (平板竖屏) */
+@media (min-width: 480px) and (max-width: 767px) {
+ .hidden-sm { display: none !important; }
+ .visible-sm { display: block !important; }
+
+ /* 容器调整 */
+ .container {
+ padding: 0 20px;
+ }
+
+ /* 侧边栏调整 */
+ .sidebar-container {
+ width: 240px;
+ }
+
+ .main-content {
+ margin-left: 240px;
+ padding: 15px;
+ }
+
+ /* 卡片布局调整 */
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+ }
+
+ /* 表格调整 */
+ .data-table {
+ font-size: 13px;
+ }
+
+ /* 图表容器调整 */
+ .chart-container {
+ height: 300px;
+ }
+
+ /* 节点指标调整 */
+ .node-metrics {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+/* 中等屏幕 (平板横屏/小笔记本) */
+@media (min-width: 768px) and (max-width: 1023px) {
+ .hidden-md { display: none !important; }
+ .visible-md { display: block !important; }
+
+ /* 卡片布局调整 */
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 25px;
+ }
+
+ /* 图表容器调整 */
+ .chart-container {
+ height: 350px;
+ }
+
+ /* 表单布局调整 */
+ .form-row {
+ display: flex;
+ gap: 20px;
+ }
+
+ .form-row .form-group {
+ flex: 1;
+ }
+}
+
+/* 大屏幕 (桌面) */
+@media (min-width: 1024px) and (max-width: 1199px) {
+ .hidden-lg { display: none !important; }
+ .visible-lg { display: block !important; }
+
+ /* 卡片布局调整 */
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ /* 图表容器调整 */
+ .chart-container {
+ height: 400px;
+ }
+}
+
+/* 超大屏幕 (大桌面) */
+@media (min-width: 1200px) {
+ /* 保持默认布局 */
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .chart-container {
+ height: 450px;
+ }
+}
+
+/* 移动设备横屏优化 */
+@media (max-width: 767px) and (orientation: landscape) {
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .chart-container {
+ height: 200px;
+ }
+
+ .modal-content {
+ max-height: 80vh;
+ }
+}
+
+/* 打印样式 */
+@media print {
+ .sidebar-container,
+ .header-actions,
+ .action-btn,
+ .btn {
+ display: none !important;
+ }
+
+ .main-content {
+ margin-left: 0;
+ padding: 0;
+ }
+
+ .page-header {
+ border-bottom: 2px solid #000;
+ margin-bottom: 20px;
+ }
+
+ .dashboard-cards,
+ .stats-overview {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+ }
+
+ .chart-container {
+ height: 300px;
+ border: 1px solid #ccc;
+ }
+
+ .data-table {
+ border-collapse: collapse;
+ }
+
+ .data-table th,
+ .data-table td {
+ border: 1px solid #000;
+ padding: 8px;
+ }
+}
+
+/* 高对比度模式支持 */
+@media (prefers-contrast: high) {
+ .dashboard-card,
+ .stat-card,
+ .node-card {
+ border: 2px solid #000;
+ }
+
+ .btn {
+ border: 2px solid #000;
+ }
+
+ .form-control {
+ border: 2px solid #000;
+ }
+}
+
+/* 减少动画模式支持 */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* 深色模式响应式调整 */
+@media (prefers-color-scheme: dark) {
+ .chart-container {
+ background-color: #1f2937;
+ border-color: #374151;
+ }
+
+ .data-table {
+ background-color: #1f2937;
+ color: #f9fafb;
+ }
+
+ .data-table th {
+ background-color: #374151;
+ }
+
+ .data-table tr:nth-child(even) {
+ background-color: #374151;
+ }
+}
+
+/* 触摸设备优化 */
+@media (hover: none) and (pointer: coarse) {
+ .btn,
+ .action-btn {
+ min-height: 44px;
+ min-width: 44px;
+ }
+
+ .form-control {
+ min-height: 44px;
+ }
+
+ .data-table td {
+ padding: 12px 8px;
+ }
+
+ /* 增大点击区域 */
+ .sidebar-nav a {
+ padding: 15px 20px;
+ }
+
+ .dropdown-item {
+ padding: 12px 16px;
+ }
+}
+
+/* 特定组件的响应式调整 */
+
+/* 登录页面响应式 */
+@media (max-width: 767px) {
+ .login-container {
+ padding: 20px;
+ }
+
+ .login-form {
+ padding: 30px 20px;
+ }
+
+ .login-header h1 {
+ font-size: 24px;
+ }
+}
+
+/* 故障管理页面响应式 */
+@media (max-width: 767px) {
+ .fault-filters {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .filter-group {
+ width: 100%;
+ }
+
+ .fault-table-container {
+ overflow-x: auto;
+ }
+
+ .fault-table {
+ min-width: 600px;
+ }
+}
+
+/* 日志分析页面响应式 */
+@media (max-width: 767px) {
+ .log-query-form {
+ grid-template-columns: 1fr;
+ gap: 15px;
+ }
+
+ .log-charts {
+ grid-template-columns: 1fr;
+ }
+
+ .log-entry {
+ padding: 15px;
+ }
+
+ .log-content {
+ font-size: 12px;
+ }
+}
+
+/* 集群监控页面响应式 */
+@media (max-width: 767px) {
+ .cluster-overview {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .node-list {
+ grid-template-columns: 1fr;
+ }
+
+ .monitoring-charts {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* 滚动条优化 */
+@media (max-width: 767px) {
+ /* 隐藏滚动条但保持功能 */
+ .data-table-container::-webkit-scrollbar {
+ height: 4px;
+ }
+
+ .data-table-container::-webkit-scrollbar-track {
+ background: #f1f5f9;
+ }
+
+ .data-table-container::-webkit-scrollbar-thumb {
+ background: #cbd5e1;
+ border-radius: 2px;
+ }
+}
\ No newline at end of file
diff --git a/src/frontend/views/ClusterMonitor/ClusterMonitor.html b/src/frontend/views/ClusterMonitor/ClusterMonitor.html
new file mode 100644
index 0000000..8971125
--- /dev/null
+++ b/src/frontend/views/ClusterMonitor/ClusterMonitor.html
@@ -0,0 +1,490 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
68%
+
平均CPU使用率
+
+
+ +5%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
DB-Server-01 内存使用率超过90%
+
2分钟前
+
+
+ 处理
+ 忽略
+
+
+
+
+
+
+
+
Web-Server-01 CPU使用率持续偏高
+
5分钟前
+
+
+ 处理
+ 忽略
+
+
+
+
+
+
+
+
Cache-Server-01 连接超时
+
10分钟前
+
+
+ 处理
+ 忽略
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/views/FaultManage/FaultManage.html b/src/frontend/views/FaultManage/FaultManage.html
new file mode 100644
index 0000000..f98d346
--- /dev/null
+++ b/src/frontend/views/FaultManage/FaultManage.html
@@ -0,0 +1,502 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
156
+
已解决故障
+
+
+ +15
+
+
+
+
+
+
+
+
+
+
+ 故障级别
+
+ 全部级别
+ 严重
+ 高级
+ 中级
+ 低级
+
+
+
+
+ 故障状态
+
+ 全部状态
+ 待处理
+ 处理中
+ 已解决
+ 已关闭
+
+
+
+
+ 故障类型
+
+ 全部类型
+ 硬件故障
+ 软件故障
+ 网络故障
+ 性能问题
+ 安全问题
+
+
+
+
+ 时间范围
+
+ 今天
+ 本周
+ 本月
+ 本季度
+ 本年
+
+
+
+
+
+
+
+
+ 重置
+
+
+
+ 应用筛选
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 故障ID
+
+
+
+ 故障标题
+
+
+
+ 严重程度
+
+
+
+ 故障类型
+
+
+
+ 状态
+
+
+
+ 负责人
+
+
+
+ 创建时间
+
+
+ 操作
+
+
+
+
+
+ FLT-2024-001
+
+
+
+
数据库连接池耗尽导致服务不可用
+
+ 影响系统: 用户服务, 订单服务
+
+
+
+
+ 严重
+
+
+ 软件故障
+
+
+ 处理中
+
+
+
+
+
+
+ 2024-01-15
+ 14:30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FLT-2024-002
+
+
+
+
Web服务器CPU使用率持续超过90%
+
+ 影响系统: Web前端
+
+
+
+
+ 高级
+
+
+ 性能问题
+
+
+ 待处理
+
+
+
+
+
+
+ 2024-01-15
+ 16:45
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FLT-2024-003
+
+
+
+
网络交换机端口故障导致部分节点离线
+
+ 影响系统: 集群节点3-5
+
+
+
+
+ 中级
+
+
+ 网络故障
+
+
+ 已解决
+
+
+
+
+
+
+ 2024-01-14
+ 09:20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/views/LogAnalysis/LogAnalysis.html b/src/frontend/views/LogAnalysis/LogAnalysis.html
new file mode 100644
index 0000000..7aca5b6
--- /dev/null
+++ b/src/frontend/views/LogAnalysis/LogAnalysis.html
@@ -0,0 +1,541 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1,234,567
+
总日志条数
+
+
+ +12.5%
+
+
+
+
+
+
+
+
+
+
1,245
+
错误日志
+
+
+ +8
+
+
+
+
+
+
+
+
+
+
3,456
+
警告日志
+
+
+ -15
+
+
+
+
+
+
+
+
+
+
98.7%
+
系统可用性
+
+
+ +0.2%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Database connection failed: Connection timeout after 30 seconds
+
+
+
+ Request ID: req-12345-abcde
+
+
+ User ID: user-67890
+
+
+ IP: 192.168.1.100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ High memory usage detected: 85% of available memory in use
+
+
+
+ Memory Usage: 6.8GB / 8GB
+
+
+ Process: order-processor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User authentication successful
+
+
+
+ User ID: user-12345
+
+
+ Session ID: sess-abcde-12345
+
+
+ IP: 192.168.1.105
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/frontend/views/Login/Login.html b/src/frontend/views/Login/Login.html
new file mode 100644
index 0000000..4c95b56
--- /dev/null
+++ b/src/frontend/views/Login/Login.html
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+
+
+
+
+
错误检测系统
+
智能化运维监控平台,实时检测系统异常,保障业务稳定运行
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file