From eaabebbcebe755de7cd0164706be1bde31ceb1fd Mon Sep 17 00:00:00 2001 From: v6ole Date: Fri, 24 Apr 2026 08:53:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.8.x=20=E5=8A=9F=E8=83=BD=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E4=B8=8E=E6=95=B0=E6=8D=AE=E7=BB=B4=E6=8A=A4=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MAC地址格式统一为小写 xxxx-xxxx-xxxx,支持三种格式输入 - 更换设备MAC时自动清理OLT新发现列表中的冲突ONU记录 - 设备编辑新增场所类型字段(手动输入),区域改为下拉快速填充 - 编辑/更换设备时自动移除new_devices中同MAC的待入库记录 - 新增登录过期自动跳转(401拦截 + 过期提示) - 新增关于页面(/about),支持Markdown渲染,管理员可在设置中编辑内容 - 新增device_status_history每日自动清理任务(保留30天) - 更新README.md和about.md --- README.md | 330 +++++++++-------------------- about.md | 37 ++++ backend/app/api/v1/devices.py | 56 ++++- backend/app/api/v1/settings.py | 24 +++ backend/app/core/celery_app.py | 5 + backend/app/tasks/check_tasks.py | 16 ++ frontend/package-lock.json | 13 ++ frontend/package.json | 1 + frontend/src/components/Layout.vue | 10 +- frontend/src/router/index.js | 5 + frontend/src/utils/request.js | 14 ++ frontend/src/views/About.vue | 200 +++++++++++++++++ frontend/src/views/DeviceList.vue | 43 +++- frontend/src/views/Login.vue | 22 ++ frontend/src/views/Settings.vue | 83 ++++++++ 15 files changed, 616 insertions(+), 243 deletions(-) create mode 100644 about.md create mode 100644 frontend/src/views/About.vue diff --git a/README.md b/README.md index cf0622f..6b3683a 100644 --- a/README.md +++ b/README.md @@ -1,282 +1,148 @@ -# H3C ONU设备管理系统 +# H3C ONU 设备管理系统 -基于Python FastAPI + Vue 3的H3C OLT设备监控管理系统,用于监控和管理4000+ ONU设备的在线状态。 +基于 Python FastAPI + Vue 3 的 H3C OLT 设备监控管理系统,用于监控和管理大规模 ONU 设备的在线状态。 ## 功能特性 -### 核心功能 -- ✅ H3C OLT设备SSH连接和状态查询 -- ✅ ONU设备在线状态监控(4000+设备) -- ✅ Excel数据导入和批量管理 -- ✅ 定时自动状态检查(每30分钟) -- ✅ 手动刷新功能(5分钟冷却限制) -- ✅ 90天状态历史记录 +### 设备监控与管理 +- H3C OLT 设备 SSH 连接和状态查询 +- ONU 设备在线状态监控(支持 4000+ 设备) +- 定时自动状态检查(可配置间隔,最小 5 分钟) +- 手动刷新功能(5 分钟冷却限制) +- 设备信息编辑(区域、学校、楼宇、场所类型、备注) +- 设备 MAC 地址更换,保留完整变更历史 +- OLT 扫描新发现设备自动入库流程 + +### 数据管理 +- Excel 批量导入设备信息 +- 设备状态历史记录(保留 30 天) +- 每日状态快照(用于趋势图) +- 重复 MAC 地址检测 + +### 统计分析 +- 实时在线率统计 +- 7 天趋势折线图 +- 区域分布饼图 ### 权限管理 -- ✅ Casdoor统一认证集成 -- ✅ 基于RBAC的多级权限控制 -- ✅ 区域/学校级别的数据隔离 -- ✅ 设备信息变更审核流程 -- ✅ 用户角色和权限管理 +- Casdoor 统一认证(OIDC) +- 基于 RBAC 的多级权限控制(管理员 / 区域管理员 / 学校管理员 / 普通用户) +- 区域 / 学校级别的数据隔离 +- 角色权限精细配置 -### 界面功能 -- ✅ 响应式Web界面 -- ✅ 设备列表表格展示 -- ✅ 高级筛选和搜索 -- ✅ 实时统计图表 -- ✅ 设备详情和历史趋势 -- ✅ 数据导入和管理界面 +### 运维功能 +- 用户操作审计日志(保留 90 天) +- 业务下发(ONU 服务配置) +- 库存序列号管理 +- 关于页面(支持 Markdown,管理员可编辑) +- 登录过期自动跳转 ## 技术栈 -### 后端 -- **Python FastAPI** - 高性能异步Web框架 -- **PostgreSQL** - 关系型数据库 -- **Paramiko** - SSH连接库 -- **Celery + Redis** - 异步任务队列 -- **Pandas** - Excel数据处理 -- **SQLAlchemy** - ORM框架 -- **Casdoor SDK** - 统一认证 - -### 前端 -- **Vue 3 + Composition API** - 前端框架 -- **Element Plus** - UI组件库 -- **Axios** - HTTP客户端 -- **Vue Router** - 路由管理 -- **Pinia** - 状态管理 -- **ECharts** - 图表库 - -### 部署 -- **Docker + Docker Compose** - 容器化部署 -- **Nginx** - 反向代理 -- **外部PostgreSQL/Redis** - 数据库服务 +| 层级 | 技术 | +|------|------| +| 后端 | Python · FastAPI · SQLAlchemy · Celery · Redis · Paramiko · Pandas | +| 前端 | Vue 3 · Element Plus · ECharts · marked | +| 数据库 | PostgreSQL | +| 认证 | Casdoor(OIDC) | +| 部署 | Docker Compose · Nginx | ## 快速开始 ### 环境要求 - Docker 20.10+ - Docker Compose 2.0+ -- 外部PostgreSQL数据库 -- 外部Redis服务 -- Casdoor认证服务器 +- 外部 PostgreSQL 数据库 +- 外部 Redis 服务 +- Casdoor 认证服务器 -### 配置步骤 +### 启动步骤 -1. **克隆项目** +1. 克隆项目 ```bash git clone cd H3ConuMS2 ``` -2. **配置环境变量** +2. 配置环境变量 ```bash cp deploy/.env.example deploy/.env - # 编辑deploy/.env文件,配置数据库和Casdoor信息 + # 编辑 deploy/.env,填写数据库、Redis、Casdoor 等配置 ``` -3. **启动服务** +3. 启动服务 ```bash - cd deploy - docker-compose up -d + docker compose up -d ``` -4. **访问系统** - - 前端界面: http://localhost:8080 - - API文档: http://localhost:8000/docs - - 健康检查: http://localhost:8000/health +4. 运行数据库迁移 + ```bash + docker compose exec backend alembic upgrade head + ``` + +5. 访问系统 + - 前端界面:http://localhost:8080 + - API 文档:http://localhost:8000/docs ## 项目结构 ``` H3ConuMS2/ -├── backend/ # Python后端代码 +├── backend/ │ ├── app/ -│ │ ├── api/ # API路由 -│ │ ├── core/ # 核心配置 -│ │ ├── models/ # 数据模型 -│ │ ├── schemas/ # Pydantic模式 -│ │ ├── services/ # 业务逻辑 -│ │ ├── tasks/ # Celery任务 -│ │ └── utils/ # 工具函数 -│ ├── alembic/ # 数据库迁移 -│ ├── tests/ # 测试代码 -│ ├── requirements.txt # Python依赖 -│ └── Dockerfile # 后端Dockerfile -├── frontend/ # Vue前端代码 -│ ├── public/ # 静态资源 +│ │ ├── api/ # API 路由 +│ │ ├── core/ # 配置、数据库、Celery +│ │ ├── middleware/ # 权限、审计中间件 +│ │ ├── models/ # 数据模型 +│ │ ├── services/ # 业务逻辑 +│ │ └── tasks/ # Celery 定时任务 +│ ├── alembic/ # 数据库迁移 +│ └── Dockerfile +├── frontend/ │ ├── src/ -│ │ ├── api/ # API调用 -│ │ ├── assets/ # 资源文件 -│ │ ├── components/ # 组件 -│ │ ├── router/ # 路由配置 -│ │ ├── stores/ # 状态管理 -│ │ ├── views/ # 页面组件 -│ │ └── utils/ # 工具函数 -│ ├── package.json # 前端依赖 -│ └── Dockerfile # 前端Dockerfile -├── deploy/ # 部署配置 -│ ├── docker-compose.yml # Docker编排 -│ ├── nginx.conf # Nginx配置 -│ └── .env.example # 环境变量示例 -├── docs/ # 文档 -│ ├── api/ # API文档 -│ ├── deployment/ # 部署文档 -│ └── user-guide/ # 用户指南 -├── scripts/ # 脚本文件 -│ ├── init-db.sh # 数据库初始化 -│ ├── backup.sh # 数据备份 -│ └── monitor.sh # 系统监控 -└── 系统设计文档.md # 详细设计文档 +│ │ ├── api/ # API 调用 +│ │ ├── components/ # 公共组件 +│ │ ├── router/ # 路由配置 +│ │ ├── views/ # 页面组件 +│ │ └── utils/ # 工具函数 +│ └── Dockerfile +├── deploy/ # 部署配置(docker-compose、nginx、.env) +└── about.md # 系统介绍(关于页面内容) ``` -## 数据库初始化 +## 定时任务 -1. **创建数据库** - ```sql - CREATE DATABASE h3c_onu_ms; - CREATE USER h3c_user WITH PASSWORD 'your_password'; - GRANT ALL PRIVILEGES ON DATABASE h3c_onu_ms TO h3c_user; - ``` - -2. **运行迁移** - ```bash - cd backend - alembic upgrade head - ``` - -3. **初始化数据** - ```bash - python scripts/init_data.py - ``` +| 任务 | 执行时间 | 说明 | +|------|----------|------| +| 设备状态检查 | 每 5 分钟触发,内部节流 | 轮询所有 OLT 采集 ONU 状态 | +| 每日快照聚合 | 每天凌晨 1:00 | 生成趋势图数据 | +| 审计日志清理 | 每天凌晨 2:00 | 删除 90 天前的审计记录 | +| 状态历史清理 | 每天凌晨 3:00 | 删除 30 天前的状态历史 | ## 配置说明 -### Casdoor配置 -1. 在Casdoor中创建应用 -2. 配置回调地址: `http://your-domain/api/auth/callback` -3. 获取Client ID、Client Secret和Certificate -4. 在.env文件中配置Casdoor参数 +### Casdoor +1. 在 Casdoor 中创建应用 +2. 配置回调地址:`http://your-domain/api/auth/callback` +3. 在 `.env` 中填写 Client ID、Client Secret 和 Certificate -### OLT设备配置 -1. 通过系统界面添加OLT设备 -2. 配置SSH连接信息(IP、用户名、密码) -3. 设置插槽命令(如`display onu slot 1`) - -### 权限配置 -1. 在Casdoor中配置用户角色 -2. 在系统中配置角色权限映射 -3. 分配用户区域和学校权限 - -## 开发指南 - -### 后端开发 -```bash -cd backend -python -m venv venv -source venv/bin/activate # Linux/Mac -# venv\Scripts\activate # Windows -pip install -r requirements.txt -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -``` - -### 前端开发 -```bash -cd frontend -npm install -npm run dev -``` - -### 测试 -```bash -# 后端测试 -cd backend -pytest - -# 前端测试 -cd frontend -npm run test -``` - -## API文档 - -启动后端服务后,访问以下地址查看API文档: -- Swagger UI: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc - -## 监控和日志 - -### 健康检查 -``` -GET /health -``` - -### 指标监控 -``` -GET /metrics -``` - -### 日志查看 -```bash -# 查看后端日志 -docker-compose logs backend - -# 查看Celery日志 -docker-compose logs celery-worker - -# 查看Nginx日志 -docker-compose logs nginx -``` - -## 故障排除 - -### 常见问题 - -1. **SSH连接失败** - - 检查网络连通性 - - 验证SSH凭证 - - 检查防火墙设置 - -2. **数据库连接失败** - - 检查数据库服务状态 - - 验证连接字符串 - - 检查网络权限 - -3. **Casdoor登录失败** - - 检查Casdoor服务状态 - - 验证应用配置 - - 检查回调地址配置 - -### 日志级别 -- DEBUG: 开发环境详细日志 -- INFO: 生产环境常规日志 -- ERROR: 错误信息记录 - -## 贡献指南 - -1. Fork项目 -2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 创建Pull Request - -## 许可证 - -本项目采用MIT许可证 - 查看LICENSE文件了解详情。 - -## 联系方式 - -- 项目维护者: [Your Name] -- 问题反馈: [GitHub Issues] -- 文档更新: [GitHub Wiki] +### OLT 设备 +通过系统界面添加 OLT 设备,配置 SSH 连接信息(IP、用户名、密码)和插槽命令。 ## 更新日志 -### v1.0.0 (计划中) -- 基础设备监控功能 -- Casdoor认证集成 -- 权限管理系统 -- Docker化部署 +### v0.8.0 +- 新增审计日志功能 +- 新增关于页面(Markdown 编辑,管理员可配置) +- 新增登录过期自动跳转提示 +- 设备编辑支持场所类型字段,区域改为下拉快速填充 +- MAC 地址统一格式为 `xxxx-xxxx-xxxx`(小写) +- 更换设备 MAC 时自动清理 OLT 新发现列表中的冲突记录 +- 状态历史自动清理(保留 30 天) + +### v0.7.x 及以前 +- 基础设备监控、OLT 管理、权限系统、Excel 导入、库存管理等核心功能 --- -**重要提示**: 生产环境部署前,请务必修改默认密码和密钥,并配置适当的防火墙规则。 \ No newline at end of file +**注意**:生产环境部署前请修改默认密码和密钥,并配置适当的防火墙规则。 diff --git a/about.md b/about.md new file mode 100644 index 0000000..97f4241 --- /dev/null +++ b/about.md @@ -0,0 +1,37 @@ +# H3C ONU 管理系统 + +H3C ONU 管理系统(H3ConuMS)是一套面向校园网络运维团队的 ONU 设备集中管理平台,支持对接多台 H3C OLT 设备,实现大规模 ONU 的状态监控、信息管理与运维操作。 + +## 主要功能 + +**设备监控** +系统通过 SSH 定期轮询 OLT 设备,自动采集所有 ONU 的在线状态、光功率、距离、端口等信息,并以列表和卡片两种视图展示。支持手动触发刷新,状态变化实时可见。 + +**设备管理** +支持按区域、学校、楼宇、场所类型等维度对 ONU 设备进行分类管理。可通过 Excel 批量导入设备信息,也可在界面中逐条编辑。设备更换时记录完整的 MAC 地址变更历史。 + +**OLT 管理** +统一管理多台 OLT 设备的连接信息,支持端口级别的设备发现。OLT 扫描到的新设备会进入待入库列表,由运维人员补全信息后正式纳管。 + +**统计分析** +提供实时在线率统计、7 天趋势折线图、区域分布饼图等可视化报表,帮助运维团队快速掌握全网设备健康状况。 + +**权限管理** +基于 Casdoor 统一认证,支持管理员、区域管理员、学校管理员、普通用户等多级角色,数据访问范围按角色自动隔离。 + +**审计日志** +记录所有用户的写操作,包括设备编辑、更换、导入、权限变更等,保留 90 天供追溯查询。 + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 后端 | Python · FastAPI · SQLAlchemy · Celery · Redis | +| 前端 | Vue 3 · Element Plus · ECharts | +| 数据库 | PostgreSQL | +| 认证 | Casdoor(OIDC) | +| 部署 | Docker Compose · Nginx | + +## 版本 + +当前版本:v0.8.0 diff --git a/backend/app/api/v1/devices.py b/backend/app/api/v1/devices.py index 80595a0..59c610d 100644 --- a/backend/app/api/v1/devices.py +++ b/backend/app/api/v1/devices.py @@ -265,6 +265,7 @@ class DeviceUpdate(BaseModel): school_name: Optional[str] = None building: Optional[str] = None room_number: Optional[str] = None + place_type: Optional[str] = None notes: Optional[str] = None @@ -299,7 +300,17 @@ def update_device( device.school_name = body.school_name device.building = body.building or None device.room_number = body.room_number or None + device.place_type = body.place_type or None device.notes = body.notes or None + + # 若该设备 MAC 在 new_devices 待入库列表中,自动移除(已在设备列表中补全信息) + from app.models.device import NewDevice + dup_new = db.query(NewDevice).join( + ONUDevice, NewDevice.onu_device_id == ONUDevice.id + ).filter(ONUDevice.mac_address == device.mac_address).all() + for nd in dup_new: + db.delete(nd) + db.commit() return {"message": "更新成功"} @@ -317,16 +328,47 @@ def replace_device( if not device: raise HTTPException(status_code=404, detail="设备不存在") - new_mac = body.new_mac.upper().strip() - # 校验 MAC 格式(允许 XX:XX:XX:XX:XX:XX 或 XXXXXXXXXXXX) + new_mac_raw = body.new_mac.strip() + # 接受 xxxx-xxxx-xxxx、xx:xx:xx:xx:xx:xx、xxxxxxxxxxxx 三种格式,统一标准化为小写 xxxx-xxxx-xxxx import re - if not re.match(r'^([0-9A-F]{2}[:-]){5}[0-9A-F]{2}$|^[0-9A-F]{12}$', new_mac): - raise HTTPException(status_code=400, detail="MAC 地址格式不正确") + hex_only = re.sub(r'[:\-]', '', new_mac_raw).lower() + if not re.match(r'^[0-9a-f]{12}$', hex_only): + raise HTTPException(status_code=400, detail="MAC 地址格式不正确,支持 xxxx-xxxx-xxxx、xx:xx:xx:xx:xx:xx 或 xxxxxxxxxxxx") + new_mac = f"{hex_only[0:4]}-{hex_only[4:8]}-{hex_only[8:12]}" - # 检查新 MAC 是否已被其他设备使用 - existing = db.query(ONUDevice).filter( - ONUDevice.mac_address == new_mac, + # 同时匹配大小写和各种分隔符格式,兼容数据库旧数据 + hex_variants = [ + new_mac, + hex_only, + ':'.join(hex_only[i:i+2] for i in range(0, 12, 2)), + '-'.join(hex_only[i:i+2] for i in range(0, 12, 2)), + new_mac.upper(), + hex_only.upper(), + ':'.join(hex_only[i:i+2].upper() for i in range(0, 12, 2)), + '-'.join(hex_only[i:i+2].upper() for i in range(0, 12, 2)), + ] + + # 若新 MAC 在 new_devices 待入库列表中,先删除(更换后该记录已无意义) + from app.models.device import NewDevice + conflict_news = db.query(NewDevice).join( + ONUDevice, NewDevice.onu_device_id == ONUDevice.id + ).filter( + ONUDevice.mac_address.in_(hex_variants), ONUDevice.id != device_id + ).all() + conflict_new_onu_ids = {nd.onu_device_id for nd in conflict_news} + for nd in conflict_news: + db.delete(nd) + # 同时删除对应的空白 ONU 记录,避免设备列表出现重复 MAC + if conflict_new_onu_ids: + db.query(ONUDevice).filter(ONUDevice.id.in_(conflict_new_onu_ids)).delete(synchronize_session=False) + db.flush() + + # 检查新 MAC 是否已被其他 ONU 设备使用(排除刚刚从 new_devices 删除的临时 ONU) + existing = db.query(ONUDevice).filter( + ONUDevice.mac_address.in_(hex_variants), + ONUDevice.id != device_id, + ONUDevice.id.notin_(conflict_new_onu_ids) if conflict_new_onu_ids else True, ).first() if existing: raise HTTPException(status_code=400, detail="该 MAC 地址已被其他设备使用") diff --git a/backend/app/api/v1/settings.py b/backend/app/api/v1/settings.py index c4f11b2..01952a4 100644 --- a/backend/app/api/v1/settings.py +++ b/backend/app/api/v1/settings.py @@ -74,3 +74,27 @@ def update_check_interval( _get_redis().delete("check_all_devices:last_run") return {"key": "check_interval_seconds", "value": seconds} + + +@router.get("/about") +def get_about(db: Session = Depends(get_db)): + """获取关于页面内容(公开接口,无需登录)""" + setting = db.query(SystemSetting).filter_by(key='about_content').first() + return {"content": setting.value if setting else ""} + + +@router.put("/about") +def update_about( + body: dict, + db: Session = Depends(get_db), + _: dict = Depends(require_permission('*')), +): + """更新关于页面内容(仅管理员)""" + content = body.get("content", "") + setting = db.query(SystemSetting).filter_by(key='about_content').first() + if setting: + setting.value = content + else: + db.add(SystemSetting(key='about_content', value=content, description='关于页面内容(Markdown)')) + db.commit() + return {"key": "about_content", "value": content} diff --git a/backend/app/core/celery_app.py b/backend/app/core/celery_app.py index 771439b..7b47be8 100644 --- a/backend/app/core/celery_app.py +++ b/backend/app/core/celery_app.py @@ -34,5 +34,10 @@ celery_app.conf.update( 'schedule': crontab(hour=2, minute=0), # 每天凌晨 2:00 'options': {'queue': 'h3c_onu_ms'}, }, + 'cleanup-status-history': { + 'task': 'app.tasks.check_tasks.cleanup_status_history', + 'schedule': crontab(hour=3, minute=0), # 每天凌晨 3:00 + 'options': {'queue': 'h3c_onu_ms'}, + }, }, ) diff --git a/backend/app/tasks/check_tasks.py b/backend/app/tasks/check_tasks.py index e259a5b..ce005c9 100644 --- a/backend/app/tasks/check_tasks.py +++ b/backend/app/tasks/check_tasks.py @@ -174,3 +174,19 @@ def aggregate_daily_snapshot(): return {'success': False, 'error': str(e), 'traceback': traceback.format_exc()} finally: db.close() + + +@celery_app.task +def cleanup_status_history(): + """清理 30 天前的设备状态历史记录(每天凌晨执行)""" + from app.models.device import DeviceStatusHistory + db = SessionLocal() + try: + cutoff = datetime.utcnow() - timedelta(days=30) + deleted = db.query(DeviceStatusHistory).filter( + DeviceStatusHistory.checked_at < cutoff + ).delete(synchronize_session=False) + db.commit() + return {"deleted": deleted} + finally: + db.close() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 250f74e..cdff06a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.6.5", "echarts": "^5.6.0", "element-plus": "^2.5.0", + "marked": "^18.0.2", "pinia": "^2.1.7", "vue": "^3.4.0", "vue-router": "^4.2.5" @@ -1452,6 +1453,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.2", + "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.2.tgz", + "integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5f63723..df906f2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "axios": "^1.6.5", "echarts": "^5.6.0", "element-plus": "^2.5.0", + "marked": "^18.0.2", "pinia": "^2.1.7", "vue": "^3.4.0", "vue-router": "^4.2.5" diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index ef23052..4f82403 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -301,6 +301,14 @@ const allNavItems = [ ` }, + { + path: '/about', + label: '关于', + icon: ` + + + ` + }, ] const navItems = computed(() => @@ -314,7 +322,7 @@ const mobileNavItems = computed(() => navItems.value.slice(0, 3)) const extraNavItems = computed(() => navItems.value.slice(3).map((item, i) => ({ ...item, - desc: ['7天趋势与区域分布', '物料出入库台账', '管理系统用户', '配置角色权限', '定时检查等系统配置', '查看操作审计记录'][i] || '' + desc: ['7天趋势与区域分布', '物料出入库台账', '管理系统用户', '配置角色权限', '定时检查等系统配置', '查看操作审计记录', '系统信息与说明'][i] || '' })) ) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 7e68ba5..2da15be 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -68,6 +68,11 @@ const routes = [ component: () => import('../views/AuditLog.vue'), meta: { requiresRole: 'admin' } }, + { + path: '/about', + name: 'About', + component: () => import('../views/About.vue') + }, ] } ] diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js index 3821d64..70e2429 100644 --- a/frontend/src/utils/request.js +++ b/frontend/src/utils/request.js @@ -13,4 +13,18 @@ request.interceptors.request.use(config => { return config }) +request.interceptors.response.use( + res => res, + err => { + if (err.response?.status === 401) { + localStorage.removeItem('token') + // 避免在登录/回调页重复跳转 + if (!window.location.pathname.startsWith('/login') && !window.location.pathname.startsWith('/callback')) { + window.location.href = '/login?expired=1' + } + } + return Promise.reject(err) + } +) + export default request diff --git a/frontend/src/views/About.vue b/frontend/src/views/About.vue new file mode 100644 index 0000000..1f127e7 --- /dev/null +++ b/frontend/src/views/About.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/frontend/src/views/DeviceList.vue b/frontend/src/views/DeviceList.vue index 25b309b..9de7afe 100644 --- a/frontend/src/views/DeviceList.vue +++ b/frontend/src/views/DeviceList.vue @@ -361,7 +361,7 @@ - + @@ -385,10 +385,23 @@ - + + + + + + + + @@ -77,6 +106,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { ElMessage } from '../utils/message' import { getSettings, updateCheckInterval } from '../api/settings' +import request from '../utils/request' const intervalMinutes = ref(30) const currentSeconds = ref(1800) @@ -148,9 +178,32 @@ const save = async () => { onMounted(() => { load() + loadAbout() const timer = setInterval(load, 10000) onUnmounted(() => clearInterval(timer)) }) + +const aboutContent = ref('') +const aboutSaving = ref(false) + +const loadAbout = async () => { + try { + const { data } = await request.get('/settings/about') + aboutContent.value = data.content || '' + } catch {} +} + +const saveAbout = async () => { + aboutSaving.value = true + try { + await request.put('/settings/about', { content: aboutContent.value }) + ElMessage.success('关于页面内容已保存') + } catch (e) { + ElMessage.error(e?.response?.data?.detail || '保存失败') + } finally { + aboutSaving.value = false + } +}