commit b8974dce59f8ae7069561cc183dfcf6536373c22 Author: root <13910913995@163.com> Date: Mon May 18 09:12:14 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7df8b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Python +.venv/ +backend/.venv/ +Python/ +**/__pycache__/ +*.py[cod] +*.pyd +*.so +*.dll +*.dylib +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +coverage.xml +htmlcov/ + +# Python packaging / caches +backend/.deps-cache/ +backend/.pip-cache/ +backend/.pip-cache-py38/ +backend/UNKNOWN.egg-info/ +backend/emcp_backend.egg-info/ +backend/.tmp_ws_check.py + +# Runtime data +backend/data/ + +# Environment files +.env +.env.* + +# Frontend +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# IDE / OS +.vscode/ +.idea/ +*.swp +Thumbs.db +.DS_Store + diff --git a/.trae/skills/python-fullstack-scaffold/SKILL.md b/.trae/skills/python-fullstack-scaffold/SKILL.md new file mode 100644 index 0000000..fc7e24e --- /dev/null +++ b/.trae/skills/python-fullstack-scaffold/SKILL.md @@ -0,0 +1,454 @@ +--- +name: "python-fullstack-scaffold" +description: "为 FastAPI + WebSocket + Web 前端系统生成前后端开发框架。用户需要搭建或重构 RK3568 电气量测控平台的 Python 全栈代码结构时调用。" +--- + +# Python Fullstack Scaffold + +## 适用场景 + +当用户出现以下意图时,调用此技能: + +- 要根据需求文档搭建 `FastAPI + WebSocket + Web前端` 的项目骨架 +- 要为 `RK3568 / Ubuntu 20.04 / Python 3.8` 平台设计前后端分层结构 +- 要从接口文档反推后端路由、数据模型、配置存储和前端页面模块 +- 要统一实时数据、设备状态、报警事件、参数配置的代码组织方式 +- 要为嵌入式 C 程序与 Python 服务之间的适配层预留稳定接口 + +如果用户只是修一个接口、改一个组件、补一个测试,不要调用此技能。 + +## 技能目标 + +基于“电气量测控平台系统需求分析与架构设计技术方案”,输出一套可直接落地的前后端开发框架,覆盖: + +- 后端目录结构 +- 前端目录结构 +- 核心领域模型 +- RESTful API 路由分层 +- WebSocket 推送框架 +- JSON/SQLite 数据存储组织 +- 嵌入式适配层抽象 +- 开发顺序、验收点与最小可运行骨架 + +## 固定技术约束 + +在生成方案或代码时,默认遵循以下约束,除非用户明确要求变更: + +- 平台:`RK3568`,系统为 `Ubuntu 20.04` +- 后端:`Python 3.8`、`FastAPI`、`Uvicorn` +- 前端:Web 前端,优先采用当前项目已有技术栈;若项目未定型,则使用轻量、易部署的前端组织方式 +- 通信:`HTTP RESTful API + WebSocket` +- 配置存储:`JSON` +- 报警存储:`SQLite` +- 实时数据:仅内存缓存,不落盘 +- 实时刷新周期:`0.5 秒` +- 指令下发响应目标:`1 秒内` +- 系统需支持 `7 x 24` 连续运行 + +## 总体架构原则 + +始终按下面的职责边界组织代码: + +1. 硬件层:`RK3568 + AI/AO + 开关量 + 网络/串口 + 显示屏` +2. 嵌入式 C 程序层:采集、控制执行、状态监测、报警生成 +3. Python 服务层:接口、业务编排、缓存、存储、推送 +4. Web 界面层:配置、状态、实时数据、报警、控制 + +禁止把以下职责混在一起: + +- 路由层里直接写文件读写和数据库细节 +- WebSocket 推送逻辑散落在多个业务模块中 +- 前端页面组件里直接硬编码接口路径和协议细节 +- C 程序适配逻辑与业务规则耦合 + +## 后端推荐目录 + +当用户要求搭建后端框架时,优先生成或对齐为如下结构: + +```text +backend/ + app/ + main.py + core/ + config.py + logging.py + security.py + response.py + exceptions.py + api/ + deps.py + router.py + routes/ + realtime.py + status.py + config_device.py + config_channel.py + config_alarm.py + config_system.py + alarms.py + control.py + schemas/ + common.py + realtime.py + status.py + device_config.py + channel_config.py + alarm_setting.py + system_config.py + alarm_event.py + control.py + services/ + realtime_service.py + status_service.py + config_service.py + alarm_service.py + control_service.py + sync_service.py + repositories/ + json_config_repo.py + alarm_repo.py + adapters/ + c_device_client.py + mock_device_client.py + protocol_models.py + ws/ + manager.py + realtime_publisher.py + alarm_publisher.py + db/ + sqlite.py + models.py + cache/ + memory_store.py + tasks/ + polling.py + utils/ + time_utils.py + backup.py + config/ + device.json + channel.json + setting.json + data/ + alarm.db + tests/ +``` + +## 前端推荐目录 + +当用户要求搭建前端框架时,优先生成或对齐为如下结构: + +```text +frontend/ + src/ + main.ts + App.vue + router/ + index.ts + api/ + http.ts + realtime.ts + status.ts + config.ts + alarm.ts + control.ts + websocket/ + realtime.ts + alarm.ts + reconnect.ts + stores/ + realtime.ts + status.ts + alarm.ts + config.ts + views/ + RealtimeView.vue + StatusView.vue + DeviceConfigView.vue + ChannelConfigView.vue + AlarmSettingView.vue + AlarmHistoryView.vue + ControlView.vue + SystemConfigView.vue + components/ + status/ + realtime/ + alarm/ + config/ + control/ + types/ + api.ts + realtime.ts + status.ts + config.ts + alarm.ts + utils/ + format.ts + validators.ts +``` + +如果仓库已有前端目录,则优先复用现有技术栈、构建工具和状态管理方式,不要无意义重建。 + +## 领域模型拆分 + +构建代码框架时,必须先把需求拆成以下核心模型: + +### 1. 实时数据模型 + +- `line_list` +- `switch` +- `ai_collect` + +其中 `line_list` 需要体现: + +- `line_no` +- 一次值 `pri_val` +- 二次值 `sec_val` + +### 2. 设备状态模型 + +- `self_check` +- `net1` +- `net2` +- `uart1` +- `uart2` + +### 3. 设备基础配置模型 + +- 操作密码 +- 硬件版本信息 +- 软件版本信息 +- 网口配置 +- 串口配置 + +### 4. 通道配置模型 + +- `ai_channel` +- `ao_channel` + +### 5. 定值与 AI 报警设置模型 + +- 线路告警阈值 +- 故障告警设置 +- AI 通道报警设置 + +### 6. 系统配置模型 + +- 对时设置 +- 亮度设置 +- 屏保时间 + +### 7. 报警事件模型 + +- `alarm_type` +- `time` +- `no` +- `type` +- `content` +- `level` + +### 8. 控制指令模型 + +- `ch` +- `action` + +## API 设计规则 + +生成后端接口时,统一遵守以下规范: + +- 所有 HTTP 接口统一响应结构:`code`、`msg`、`data` +- 路由层仅负责参数校验、调用服务、返回标准响应 +- `GET /api/real-time-data` 作为实时数据备用查询接口 +- `GET /api/device-status` 用于设备状态读取 +- `POST /api/config/device` 持久化设备基础配置并下发设备 +- `POST /api/config/channel` 持久化 AI/AO 通道配置并下发设备 +- `POST /api/config/line_alarm_setting` 持久化线路定值与故障告警设置 +- `POST /api/config/ai_alarm_setting` 持久化 AI 告警设置 +- `POST /api/config/system` 处理对时、亮度、屏保配置,并实时下发 +- `GET /api/alarm/list` 分页查询报警历史 +- `POST /api/control/switch` 下发开关量控制指令 + +如果用户要求生成代码,优先先搭出路由、Schema、Service、Repository 的闭环,再补设备适配与前端页面。 + +## WebSocket 设计规则 + +必须把 WebSocket 当作一等公民设计,不要退化为轮询替代品。 + +### 实时数据通道 + +- 地址:`/ws/real-time` +- 推送周期:`0.5 秒` +- 推送内容:`real_time` +- 数据来源:内存缓存中的最新采样结果 + +### 报警通道 + +- 地址:`/ws/alarm` +- 触发方式:报警事件产生即推送 +- 推送内容:单条报警事件或标准化事件消息 + +### WebSocket 实现要求 + +- 统一连接管理器 +- 支持多客户端广播 +- 支持断线清理 +- 前端必须具备自动重连 +- 推送数据结构必须和前端类型定义一致 + +## 存储设计规则 + +### JSON 配置 + +以下配置默认存放在 `config/`: + +- `device.json` +- `channel.json` +- `setting.json` + +要求: + +- 保存前先做 Schema 校验 +- 写入时支持原子替换 +- 修改后自动备份旧版本 +- 关键字段变更后调用设备下发接口 + +### SQLite 报警库 + +报警事件单独存放在 SQLite 中,至少包含: + +- `id` +- `alarm_type` +- `time` +- `no` +- `type` +- `content` +- `level` + +要求: + +- 写入逻辑集中在 Repository +- 查询支持分页 +- 前端历史页调用分页接口,不直接访问数据库 + +### 内存缓存 + +以下数据只保存在内存中: + +- 实时量测数据 +- 实时设备状态 +- 最近待推送报警缓冲 + +## 嵌入式适配层规则 + +当用户要落地与 C 程序交互的代码框架时,必须建立设备适配层,不要让业务层直接依赖具体协议细节。 + +至少抽象以下能力: + +- 读取实时数据 +- 读取设备状态 +- 读取报警事件 +- 下发基础配置 +- 下发通道配置 +- 下发系统设置 +- 下发开关控制 + +推荐做法: + +- 定义统一客户端接口,例如 `DeviceClient` +- 提供 `MockDeviceClient` 供联调和前端开发使用 +- 提供 `CDeviceClient` 作为真实嵌入式对接实现 +- 通过配置切换 mock / real 模式 + +## 安全与可靠性规则 + +必须纳入以下要求: + +- 密码不得明文长期存储,至少做哈希处理 +- 配置接口增加基本权限校验能力 +- WebSocket 支持断线重连 +- 配置写入支持备份 +- 服务异常场景需易于接入守护或自动重启机制 + +## 推荐开发顺序 + +当用户要求“开始搭框架”时,按以下顺序推进: + +1. 先梳理接口、数据模型、目录结构 +2. 再生成后端基础工程:配置、日志、异常、统一响应 +3. 再生成 Schema、Repository、Service、Router +4. 再搭建内存缓存、轮询任务、WebSocket 管理器 +5. 再补 SQLite 与 JSON 持久化 +6. 再抽象设备适配层和 mock 数据源 +7. 最后生成前端 API 封装、状态管理、页面骨架与 WebSocket 客户端 + +## 输出要求 + +调用本技能后,输出内容应尽量包含以下几项: + +### 如果用户要“方案” + +输出: + +- 建议目录结构 +- 模块职责划分 +- 核心数据模型清单 +- API / WebSocket 对应关系 +- 开发阶段拆分 + +### 如果用户要“直接写代码” + +输出并执行: + +- 先识别现有仓库中 `backend/`、`frontend/` 的当前结构 +- 尽量增量修改,不要推倒重来 +- 先落最小可运行骨架,再补细节 +- 优先保证接口契约、模块边界、可测试性 + +## 生成代码时的验收清单 + +生成或修改后,至少检查: + +- 后端是否存在统一应用入口 +- 路由、服务、仓储、适配层是否职责分离 +- 是否覆盖需求文档中的全部 API +- 是否存在 `/ws/real-time` 与 `/ws/alarm` +- 实时数据是否只走内存缓存 +- 报警历史是否落 SQLite +- 配置是否落 JSON +- 前端是否拆出页面、API、WebSocket、类型定义 +- 是否保留 mock 联调能力 + +## 常用提示模板 + +### 模板 1:搭建完整框架 + +```text +请基于当前仓库和需求文档,搭建 RK3568 电气量测控平台的前后端代码框架。 +后端使用 FastAPI,前端沿用仓库现有技术栈。 +要求包含目录结构、统一响应、配置读写、SQLite 告警存储、WebSocket 实时推送、设备适配层与 mock 数据源。 +``` + +### 模板 2:只搭后端骨架 + +```text +请按 python-fullstack-scaffold 技能,为该项目生成后端骨架。 +要求覆盖 realtime、status、config、alarm、control 五类能力,并预留嵌入式 C 程序适配层。 +``` + +### 模板 3:只搭前端骨架 + +```text +请按 python-fullstack-scaffold 技能,为该项目生成前端页面骨架和 API/WebSocket 接入层。 +要求覆盖实时数据、设备状态、基础配置、通道配置、报警历史和控制页面。 +``` + +## 执行提醒 + +在实际执行前,先确认以下信息: + +- 当前仓库是否已经存在 `backend/` 和 `frontend/` +- 前端当前使用的具体技术栈和构建工具 +- 后端是否已有可复用入口、配置系统和测试结构 +- 用户要的是“技能文件”、 “设计方案” 还是 “直接生成代码” + +若信息不足,先补充上下文;若仓库已有实现,优先做兼容式演进,而不是重建。 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..c01db09 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,16 @@ +# EMCP Backend + +## 启动 + +```bash +pip install -e .[dev] +uvicorn app.main:app --reload +``` + +## 说明 + +- 默认使用 `MockDeviceClient` 产生实时数据和模拟告警 +- 需要鉴权的接口使用请求头 `X-API-Token: admin123` +- WebSocket 通道: + - `/ws/real-time` + - `/ws/alarm` diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/adapters/device_client.py b/backend/app/adapters/device_client.py new file mode 100644 index 0000000..e524523 --- /dev/null +++ b/backend/app/adapters/device_client.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from datetime import datetime +from random import Random +from typing import Any, Dict, List + +from app.schemas.platform import ( + AlarmEvent, + ChannelConfigIn, + DeviceConfigIn, + DeviceStatus, + LineAlarmSettingIn, + LineData, + RealtimeData, + SystemConfigIn, + SwitchControlIn, + ValueGroup, +) + + +class DeviceClient(ABC): + @abstractmethod + def read_realtime_data(self) -> RealtimeData: + raise NotImplementedError + + @abstractmethod + def read_device_status(self) -> DeviceStatus: + raise NotImplementedError + + @abstractmethod + def read_alarm_events(self) -> List[AlarmEvent]: + raise NotImplementedError + + @abstractmethod + def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_ai_alarm_setting(self, payload: List[Dict[str, Any]]) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]: + raise NotImplementedError + + +class MockDeviceClient(DeviceClient): + def __init__(self) -> None: + self._random = Random(3568) + self._tick = 0 + + def _value_group(self, base_u: float, base_i: float, base_p: float) -> ValueGroup: + delta = self._tick % 5 + return ValueGroup( + Ua=base_u + delta, + Ub=base_u + 2 + delta, + Uc=base_u - 1 + delta, + Ia=base_i + 0.1 * delta, + Ib=base_i - 0.1 + 0.1 * delta, + Ic=base_i + 0.2 + 0.1 * delta, + Pa=base_p + delta, + Pb=base_p - 2 + delta, + Pc=base_p + 3 + delta, + Pt=base_p * 3 + delta, + Qa=12 + delta, + Qb=11 + delta, + Qc=13 + delta, + Qt=36 + delta, + Sa=base_p + 15 + delta, + Sb=base_p + 10 + delta, + Sc=base_p + 16 + delta, + St=base_p * 3 + 44 + delta, + PFa=0.98, + PFb=0.97, + PFc=0.99, + PFt=0.98, + Uab=base_u * 1.73, + Ubc=base_u * 1.72, + Uca=base_u * 1.73, + frq=50.0, + ) + + def read_realtime_data(self) -> RealtimeData: + self._tick += 1 + line_list = [ + LineData(line_no=1, pri_val=self._value_group(6000, 150, 820), sec_val=self._value_group(57.6, 1.2, 68)), + LineData(line_no=2, pri_val=self._value_group(5990, 148, 810), sec_val=self._value_group(57.2, 1.1, 66)), + LineData(line_no=3, pri_val=self._value_group(6010, 151, 830), sec_val=self._value_group(58.1, 1.3, 70)), + LineData(line_no=4, pri_val=self._value_group(6020, 149, 825), sec_val=self._value_group(58.4, 1.2, 69)), + ] + switch = {f"di{i}": int((i + self._tick) % 2 == 0) for i in range(1, 13)} + switch.update({f"do{i}": int((i + self._tick + 1) % 2 == 0) for i in range(1, 13)}) + ai_collect = {f"ai{i}": round(1 + self._random.random() * 5 + (self._tick % 3) * 0.1, 2) for i in range(1, 13)} + return RealtimeData(line_list=line_list, switch=switch, ai_collect=ai_collect) + + def read_device_status(self) -> DeviceStatus: + return DeviceStatus( + self_check="正常", + net1="正常", + net2="正常", + uart1="正常", + uart2="正常" if self._tick % 4 else "断开", + ) + + def read_alarm_events(self) -> List[AlarmEvent]: + if self._tick % 6 != 0: + return [] + return [ + AlarmEvent( + alarm_type="line_alarm", + time=datetime.now(), + no=str((self._tick // 6) % 4 + 1), + type="电压", + content="模拟告警:线路电压越限", + level="中", + ) + ] + + def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: + return {"send_status": "成功", "target": "device", "items": len(payload.net) + len(payload.uart)} + + def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]: + return { + "send_status": "成功", + "target": "channel", + "items": len(payload.ai_channel) + len(payload.ao_channel), + } + + def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]: + return {"send_status": "成功", "target": "line_alarm", "line_no": payload.line_no} + + def send_ai_alarm_setting(self, payload: List[Dict[str, Any]]) -> Dict[str, Any]: + return {"send_status": "成功", "target": "ai_alarm", "items": len(payload)} + + def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]: + return {"send_status": "成功", "target": "system", "brightness": payload.brightness} + + def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]: + action_text = "合" if payload.action == 1 else "分" + return {"control_status": f"执行成功: 开关{payload.ch}{action_text}"} + + +class CDeviceClient(MockDeviceClient): + """真实 C 程序接入前先复用 mock 行为,后续在此类中替换协议实现。""" diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/router.py b/backend/app/api/router.py new file mode 100644 index 0000000..e4d86de --- /dev/null +++ b/backend/app/api/router.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from app.api.routes import alarms, auth, config, control, realtime, status + + +api_router = APIRouter() +api_router.include_router(realtime.router) +api_router.include_router(status.router) +api_router.include_router(auth.router) +api_router.include_router(config.router) +api_router.include_router(alarms.router) +api_router.include_router(control.router) diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/api/routes/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/routes/alarms.py b/backend/app/api/routes/alarms.py new file mode 100644 index 0000000..d94452e --- /dev/null +++ b/backend/app/api/routes/alarms.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, Query + +from app.core.response import success_response +from app.services.platform_service import platform_service + + +router = APIRouter(prefix="/alarm", tags=["alarm"]) + + +@router.get("/list") +def list_alarms(page: int = Query(default=1, ge=1), size: int = Query(default=20, ge=1, le=100)) -> dict: + data = platform_service.list_alarms(page=page, size=size) + return success_response(data, msg="获取报警历史成功") diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py new file mode 100644 index 0000000..b98afa6 --- /dev/null +++ b/backend/app/api/routes/auth.py @@ -0,0 +1,16 @@ +from typing import Any, Dict + +from fastapi import APIRouter + +from app.core.response import success_response +from app.core.security import verify_access_password +from app.schemas.platform import PasswordVerifyIn + + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/verify-password") +def verify_password_api(payload: PasswordVerifyIn) -> Dict[str, Any]: + is_valid = verify_access_password(payload.password) + return success_response(is_valid, msg="密码校验完成") diff --git a/backend/app/api/routes/config.py b/backend/app/api/routes/config.py new file mode 100644 index 0000000..410b0c6 --- /dev/null +++ b/backend/app/api/routes/config.py @@ -0,0 +1,36 @@ +from typing import Any, Dict, List + +from fastapi import APIRouter, Depends + +from app.core.response import success_response +from app.core.security import verify_api_token +from app.schemas.platform import AiAlarmSettingIn, ChannelConfigIn, DeviceConfigIn, LineAlarmSettingIn, SystemConfigIn +from app.services.platform_service import platform_service + + +router = APIRouter(prefix="/config", tags=["config"]) + + +@router.post("/device", dependencies=[Depends(verify_api_token)]) +def save_device_config(payload: DeviceConfigIn) -> Dict[str, Any]: + return success_response(platform_service.save_device_config(payload)) + + +@router.post("/channel", dependencies=[Depends(verify_api_token)]) +def save_channel_config(payload: ChannelConfigIn) -> Dict[str, Any]: + return success_response(platform_service.save_channel_config(payload)) + + +@router.post("/line_alarm_setting", dependencies=[Depends(verify_api_token)]) +def save_line_alarm_setting(payload: LineAlarmSettingIn) -> Dict[str, Any]: + return success_response(platform_service.save_line_alarm_setting(payload)) + + +@router.post("/ai_alarm_setting", dependencies=[Depends(verify_api_token)]) +def save_ai_alarm_setting(payload: List[AiAlarmSettingIn]) -> Dict[str, Any]: + return success_response(platform_service.save_ai_alarm_setting(payload)) + + +@router.post("/system", dependencies=[Depends(verify_api_token)]) +def save_system_config(payload: SystemConfigIn) -> Dict[str, Any]: + return success_response(platform_service.save_system_config(payload)) diff --git a/backend/app/api/routes/control.py b/backend/app/api/routes/control.py new file mode 100644 index 0000000..2ef98bc --- /dev/null +++ b/backend/app/api/routes/control.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, Depends + +from app.core.response import success_response +from app.core.security import verify_api_token +from app.schemas.platform import SwitchControlIn +from app.services.platform_service import platform_service + + +router = APIRouter(prefix="/control", tags=["control"]) + + +@router.post("/switch", dependencies=[Depends(verify_api_token)]) +def switch_control(payload: SwitchControlIn) -> dict: + return success_response(platform_service.switch_control(payload)) diff --git a/backend/app/api/routes/realtime.py b/backend/app/api/routes/realtime.py new file mode 100644 index 0000000..f286152 --- /dev/null +++ b/backend/app/api/routes/realtime.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.core.response import success_response +from app.services.platform_service import platform_service + + +router = APIRouter(tags=["realtime"]) + + +@router.get("/real-time-data") +def get_realtime_data() -> dict: + data = platform_service.get_realtime_data() + return success_response(data.model_dump(), msg="获取实时数据成功") diff --git a/backend/app/api/routes/status.py b/backend/app/api/routes/status.py new file mode 100644 index 0000000..5de18c8 --- /dev/null +++ b/backend/app/api/routes/status.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.core.response import success_response +from app.services.platform_service import platform_service + + +router = APIRouter(tags=["status"]) + + +@router.get("/device-status") +def get_device_status() -> dict: + data = platform_service.get_device_status() + return success_response(data.model_dump(), msg="获取设备状态成功") diff --git a/backend/app/cache/memory_store.py b/backend/app/cache/memory_store.py new file mode 100644 index 0000000..33a290c --- /dev/null +++ b/backend/app/cache/memory_store.py @@ -0,0 +1,37 @@ +from collections import deque +from dataclasses import dataclass, field +from threading import Lock +from typing import Deque, Optional + +from app.schemas.platform import AlarmEvent, DeviceStatus, RealtimeData + + +@dataclass +class MemoryStore: + realtime_data: Optional[RealtimeData] = None + device_status: Optional[DeviceStatus] = None + recent_alarms: Deque[AlarmEvent] = field(default_factory=lambda: deque(maxlen=100)) + _lock: Lock = field(default_factory=Lock) + + def set_realtime(self, data: RealtimeData) -> None: + with self._lock: + self.realtime_data = data + + def get_realtime(self) -> Optional[RealtimeData]: + with self._lock: + return self.realtime_data + + def set_status(self, status: DeviceStatus) -> None: + with self._lock: + self.device_status = status + + def get_status(self) -> Optional[DeviceStatus]: + with self._lock: + return self.device_status + + def push_alarm(self, alarm: AlarmEvent) -> None: + with self._lock: + self.recent_alarms.appendleft(alarm) + + +memory_store = MemoryStore() diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..89d30ae --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,31 @@ +from pathlib import Path + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +BASE_DIR = Path(__file__).resolve().parents[2] + + +class Settings(BaseSettings): + app_name: str = "电气量测控平台" + app_version: str = "0.1.0" + api_prefix: str = "/api" + realtime_ws_path: str = "/ws/real-time" + alarm_ws_path: str = "/ws/alarm" + poll_interval_seconds: float = 0.5 + auth_password: str = "admin123" + use_mock_device: bool = True + config_dir: Path = BASE_DIR / "config" + data_dir: Path = BASE_DIR / "data" + alarm_db_path: Path = BASE_DIR / "data" / "alarm.db" + backup_dir: Path = BASE_DIR / "data" / "backups" + + model_config = SettingsConfigDict( + env_prefix="EMCP_", + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + +settings = Settings() diff --git a/backend/app/core/response.py b/backend/app/core/response.py new file mode 100644 index 0000000..6e2e484 --- /dev/null +++ b/backend/app/core/response.py @@ -0,0 +1,9 @@ +from typing import Any, Dict + + +def success_response(data: Any = None, msg: str = "操作成功", code: int = 200) -> Dict[str, Any]: + return { + "code": code, + "msg": msg, + "data": data if data is not None else {}, + } diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..0152dd2 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,27 @@ +import hashlib +import secrets +from typing import Optional + +from fastapi import Header, HTTPException, status + +from app.core.config import settings + + +def hash_password(password: str) -> str: + return hashlib.sha256(password.encode("utf-8")).hexdigest() + + +def verify_password(password: str, expected_hash: str) -> bool: + return hash_password(password) == expected_hash + + +def verify_access_password(password: str) -> bool: + return secrets.compare_digest(password, settings.auth_password) + + +def verify_api_token(x_api_token: Optional[str] = Header(default=None)) -> None: + if x_api_token != settings.auth_password: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的访问令牌", + ) diff --git a/backend/app/db/sqlite.py b/backend/app/db/sqlite.py new file mode 100644 index 0000000..faf0b25 --- /dev/null +++ b/backend/app/db/sqlite.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import sqlite3 +from pathlib import Path +from typing import Optional + +from app.core.config import settings + + +def get_connection(db_path: Optional[Path] = None) -> sqlite3.Connection: + path = db_path or settings.alarm_db_path + path.parent.mkdir(parents=True, exist_ok=True) + connection = sqlite3.connect(path) + connection.row_factory = sqlite3.Row + return connection + + +def init_db() -> None: + with get_connection() as connection: + connection.execute( + """ + CREATE TABLE IF NOT EXISTS alarm_event ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alarm_type TEXT NOT NULL, + time TEXT NOT NULL, + no TEXT, + type TEXT, + content TEXT NOT NULL, + level TEXT NOT NULL + ) + """ + ) + connection.commit() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..b108fff --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import asyncio +from contextlib import asynccontextmanager + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware + +from app.api.router import api_router +from app.core.config import settings +from app.db.sqlite import init_db +from app.tasks.polling import device_polling_loop, stop_task +from app.ws.manager import ws_manager + + +@asynccontextmanager +async def lifespan(_: FastAPI): + init_db() + poll_task = asyncio.create_task(device_polling_loop()) + try: + yield + finally: + await stop_task(poll_task) + + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router, prefix=settings.api_prefix) + + +@app.get("/") +def healthcheck() -> dict: + return {"status": "ok", "service": settings.app_name} + + +@app.websocket(settings.realtime_ws_path) +async def realtime_ws(websocket: WebSocket) -> None: + await ws_manager.connect("real-time", websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + ws_manager.disconnect("real-time", websocket) + + +@app.websocket(settings.alarm_ws_path) +async def alarm_ws(websocket: WebSocket) -> None: + await ws_manager.connect("alarm", websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + ws_manager.disconnect("alarm", websocket) diff --git a/backend/app/repositories/alarm_repo.py b/backend/app/repositories/alarm_repo.py new file mode 100644 index 0000000..4fca949 --- /dev/null +++ b/backend/app/repositories/alarm_repo.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import Any, Dict, List + +from app.db.sqlite import get_connection +from app.schemas.platform import AlarmEvent + + +class AlarmRepository: + def save_alarm(self, alarm: AlarmEvent) -> int: + with get_connection() as connection: + cursor = connection.execute( + """ + INSERT INTO alarm_event (alarm_type, time, no, type, content, level) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + alarm.alarm_type, + alarm.time.isoformat(sep=" ", timespec="seconds"), + alarm.no, + alarm.type, + alarm.content, + alarm.level, + ), + ) + connection.commit() + return int(cursor.lastrowid) + + def list_alarms(self, page: int = 1, size: int = 20) -> List[Dict[str, Any]]: + offset = (page - 1) * size + with get_connection() as connection: + cursor = connection.execute( + """ + SELECT id, alarm_type, time, no, type, content, level + FROM alarm_event + ORDER BY id DESC + LIMIT ? OFFSET ? + """, + (size, offset), + ) + return [dict(row) for row in cursor.fetchall()] diff --git a/backend/app/repositories/json_config_repo.py b/backend/app/repositories/json_config_repo.py new file mode 100644 index 0000000..ebf37aa --- /dev/null +++ b/backend/app/repositories/json_config_repo.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +from app.core.config import settings +from app.utils.backup import backup_file + + +class JsonConfigRepository: + def __init__(self, config_dir: Optional[Path] = None) -> None: + self.config_dir = config_dir or settings.config_dir + self.config_dir.mkdir(parents=True, exist_ok=True) + + def _write_json(self, filename: str, payload: Union[Dict[str, Any], List[Any]]) -> Path: + path = self.config_dir / filename + backup_file(path, settings.backup_dir) + temp_path = path.with_suffix(".tmp") + with temp_path.open("w", encoding="utf-8") as file: + json.dump(payload, file, ensure_ascii=False, indent=2) + temp_path.replace(path) + return path + + def write_device_config(self, payload: Dict[str, Any]) -> Path: + return self._write_json("device.json", payload) + + def write_channel_config(self, payload: Dict[str, Any]) -> Path: + return self._write_json("channel.json", payload) + + def write_setting_config(self, payload: Dict[str, Any]) -> Path: + return self._write_json("setting.json", payload) + + def read_json(self, filename: str) -> Union[Dict[str, Any], List[Any]]: + path = self.config_dir / filename + if not path.exists(): + return {} + with path.open("r", encoding="utf-8") as file: + return json.load(file) diff --git a/backend/app/schemas/common.py b/backend/app/schemas/common.py new file mode 100644 index 0000000..d6a3b22 --- /dev/null +++ b/backend/app/schemas/common.py @@ -0,0 +1,14 @@ +from typing import Any + +from pydantic import BaseModel, Field + + +class ApiResponse(BaseModel): + code: int = 200 + msg: str = "操作成功" + data: Any = Field(default_factory=dict) + + +class PaginationQuery(BaseModel): + page: int = Field(default=1, ge=1) + size: int = Field(default=20, ge=1, le=100) diff --git a/backend/app/schemas/platform.py b/backend/app/schemas/platform.py new file mode 100644 index 0000000..7cfb234 --- /dev/null +++ b/backend/app/schemas/platform.py @@ -0,0 +1,158 @@ +from datetime import datetime +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field + + +class ValueGroup(BaseModel): + Ua: float = 0.0 + Ub: float = 0.0 + Uc: float = 0.0 + Ia: float = 0.0 + Ib: float = 0.0 + Ic: float = 0.0 + Pa: float = 0.0 + Pb: float = 0.0 + Pc: float = 0.0 + Pt: float = 0.0 + Qa: float = 0.0 + Qb: float = 0.0 + Qc: float = 0.0 + Qt: float = 0.0 + Sa: float = 0.0 + Sb: float = 0.0 + Sc: float = 0.0 + St: float = 0.0 + PFa: float = 0.0 + PFb: float = 0.0 + PFc: float = 0.0 + PFt: float = 0.0 + Uab: float = 0.0 + Ubc: float = 0.0 + Uca: float = 0.0 + frq: float = 50.0 + + +class LineData(BaseModel): + line_no: int + pri_val: ValueGroup = Field(default_factory=ValueGroup) + sec_val: ValueGroup = Field(default_factory=ValueGroup) + + +class RealtimeData(BaseModel): + line_list: List[LineData] + switch: Dict[str, int] + ai_collect: Dict[str, float] + + +class RealtimeEnvelope(BaseModel): + type: str = "real_time" + data: RealtimeData + + +class DeviceStatus(BaseModel): + self_check: str = "正常" + net1: str = "正常" + net2: str = "正常" + uart1: str = "正常" + uart2: str = "断开" + + +class HardwareVersion(BaseModel): + board_version: str = "B001.001.001" + display_version: str = "S001.001.001" + other_version: str = "Y001.001.001" + + +class SoftwareVersion(BaseModel): + display_program: str = "001.001.001" + communication_program: str = "001.001.001" + measurement_program: str = "001.001.001" + + +class NetConfigItem(BaseModel): + nic: str + ip: str = "" + mask: str = "" + gateway: str = "" + protocol: str = "" + + +class UartConfigItem(BaseModel): + port: str + baud: int = 9600 + parity: str = "NONE" + data_bits: int = 8 + stop_bits: int = 1 + protocol: str = "" + + +class DeviceConfigIn(BaseModel): + password: str = "123456" + hardware_version: HardwareVersion = Field(default_factory=HardwareVersion) + software_version: SoftwareVersion = Field(default_factory=SoftwareVersion) + net: List[NetConfigItem] = Field(default_factory=list) + uart: List[UartConfigItem] = Field(default_factory=list) + + +class ChannelItem(BaseModel): + ch: int + singal_type: str + line_no: int + type: str + limit_low: float + limit_high: float + + +class ChannelConfigIn(BaseModel): + ai_channel: List[ChannelItem] = Field(default_factory=list) + ao_channel: List[ChannelItem] = Field(default_factory=list) + + +class AlarmRule(BaseModel): + category: str + limit: Optional[float] = None + delay: int = 0 + output_node: str = "" + enabled: bool = True + + +class LineAlarmSettingIn(BaseModel): + line_no: int + over_limit_alarm: List[AlarmRule] = Field(default_factory=list) + fault_alarm: List[AlarmRule] = Field(default_factory=list) + + +class AiAlarmSettingIn(BaseModel): + channel_no: int + singal_type: str + limit_low: float + limit_high: float + delay: int = 0 + output_node: str = "" + enabled: bool = True + + +class SystemConfigIn(BaseModel): + time_sync: str = "auto" + brightness: int = Field(default=80, ge=0, le=100) + screen_saver: int = Field(default=60, ge=0) + + +class PasswordVerifyIn(BaseModel): + password: str + + +class AlarmEvent(BaseModel): + id: Optional[int] = None + alarm_type: str + time: datetime + no: str = "" + type: str = "" + content: str + level: str = "一般" + + +class SwitchControlIn(BaseModel): + ch: int = Field(ge=1) + action: int = Field(ge=0, le=1) diff --git a/backend/app/services/platform_service.py b/backend/app/services/platform_service.py new file mode 100644 index 0000000..44d8d8c --- /dev/null +++ b/backend/app/services/platform_service.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from typing import Any, Dict, List + +from app.adapters.device_client import CDeviceClient, MockDeviceClient +from app.cache.memory_store import memory_store +from app.core.config import settings +from app.core.security import hash_password +from app.repositories.alarm_repo import AlarmRepository +from app.repositories.json_config_repo import JsonConfigRepository +from app.schemas.platform import ( + AiAlarmSettingIn, + ChannelConfigIn, + DeviceConfigIn, + DeviceStatus, + LineAlarmSettingIn, + RealtimeData, + SwitchControlIn, + SystemConfigIn, +) +from app.ws.manager import ws_manager + + +class PlatformService: + def __init__(self) -> None: + self.config_repo = JsonConfigRepository() + self.alarm_repo = AlarmRepository() + self.device_client = MockDeviceClient() if settings.use_mock_device else CDeviceClient() + + def get_realtime_data(self) -> RealtimeData: + cached = memory_store.get_realtime() + if cached is not None: + return cached + realtime = self.device_client.read_realtime_data() + memory_store.set_realtime(realtime) + return realtime + + def get_device_status(self) -> DeviceStatus: + cached = memory_store.get_status() + if cached is not None: + return cached + status = self.device_client.read_device_status() + memory_store.set_status(status) + return status + + def save_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: + data = payload.model_dump() + data["password"] = hash_password(payload.password) + path = self.config_repo.write_device_config(data) + device_result = self.device_client.send_device_config(payload) + return {"save_path": f"/config/{path.name}", **device_result} + + def save_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]: + path = self.config_repo.write_channel_config(payload.model_dump()) + device_result = self.device_client.send_channel_config(payload) + return {"save_path": f"/config/{path.name}", **device_result} + + def save_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]: + current = self.config_repo.read_json("setting.json") + if not isinstance(current, dict): + current = {} + current["line_alarm_setting"] = payload.model_dump() + path = self.config_repo.write_setting_config(current) + device_result = self.device_client.send_line_alarm_setting(payload) + return {"save_path": f"/config/{path.name}", **device_result} + + def save_ai_alarm_setting(self, payload: List[AiAlarmSettingIn]) -> Dict[str, Any]: + current = self.config_repo.read_json("setting.json") + if not isinstance(current, dict): + current = {} + current["ai_alarm_setting"] = [item.model_dump() for item in payload] + path = self.config_repo.write_setting_config(current) + device_result = self.device_client.send_ai_alarm_setting(current["ai_alarm_setting"]) + return {"save_path": f"/config/{path.name}", **device_result} + + def save_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]: + current = self.config_repo.read_json("setting.json") + if not isinstance(current, dict): + current = {} + current["system_config"] = payload.model_dump() + self.config_repo.write_setting_config(current) + return self.device_client.send_system_config(payload) + + def list_alarms(self, page: int, size: int) -> List[Dict[str, Any]]: + return self.alarm_repo.list_alarms(page=page, size=size) + + def switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]: + return self.device_client.send_switch_control(payload) + + async def poll_device_once(self) -> None: + realtime = self.device_client.read_realtime_data() + status = self.device_client.read_device_status() + alarms = self.device_client.read_alarm_events() + + memory_store.set_realtime(realtime) + memory_store.set_status(status) + await ws_manager.broadcast("real-time", {"type": "real_time", "data": realtime.model_dump()}) + + for alarm in alarms: + alarm.id = self.alarm_repo.save_alarm(alarm) + memory_store.push_alarm(alarm) + await ws_manager.broadcast("alarm", alarm.model_dump(mode="json")) + + +platform_service = PlatformService() diff --git a/backend/app/tasks/polling.py b/backend/app/tasks/polling.py new file mode 100644 index 0000000..4e79807 --- /dev/null +++ b/backend/app/tasks/polling.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import asyncio +import contextlib +from typing import Optional + +from app.core.config import settings +from app.services.platform_service import platform_service + + +async def device_polling_loop() -> None: + while True: + await platform_service.poll_device_once() + await asyncio.sleep(settings.poll_interval_seconds) + + +async def stop_task(task: Optional[asyncio.Task]) -> None: + if task is None: + return + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task diff --git a/backend/app/utils/backup.py b/backend/app/utils/backup.py new file mode 100644 index 0000000..d4a59a0 --- /dev/null +++ b/backend/app/utils/backup.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +import shutil +from datetime import datetime +from pathlib import Path + + +def backup_file(source: Path, backup_dir: Path) -> None: + if not source.exists(): + return + backup_dir.mkdir(parents=True, exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + target = backup_dir / f"{source.stem}_{timestamp}{source.suffix}" + shutil.copy2(source, target) diff --git a/backend/app/ws/manager.py b/backend/app/ws/manager.py new file mode 100644 index 0000000..a822d01 --- /dev/null +++ b/backend/app/ws/manager.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import json +from collections import defaultdict +from typing import Any, DefaultDict, Dict, List, Set + +from fastapi import WebSocket + + +class WebSocketManager: + def __init__(self) -> None: + self._connections: DefaultDict[str, Set[WebSocket]] = defaultdict(set) + + async def connect(self, channel: str, websocket: WebSocket) -> None: + await websocket.accept() + self._connections[channel].add(websocket) + + def disconnect(self, channel: str, websocket: WebSocket) -> None: + self._connections[channel].discard(websocket) + + async def broadcast(self, channel: str, payload: Dict[str, Any]) -> None: + disconnected: List[WebSocket] = [] + for websocket in self._connections[channel]: + try: + await websocket.send_text(json.dumps(payload, ensure_ascii=False, default=str)) + except Exception: + disconnected.append(websocket) + for websocket in disconnected: + self.disconnect(channel, websocket) + + +ws_manager = WebSocketManager() diff --git a/backend/config/channel.json b/backend/config/channel.json new file mode 100644 index 0000000..0c863fe --- /dev/null +++ b/backend/config/channel.json @@ -0,0 +1,22 @@ +{ + "ai_channel": [ + { + "ch": 1, + "singal_type": "4-20mA22222", + "line_no": 1, + "type": "UA", + "limit_low": 0.0, + "limit_high": 20.0 + } + ], + "ao_channel": [ + { + "ch": 1, + "singal_type": "1-5v", + "line_no": 2, + "type": "UA", + "limit_low": 0.0, + "limit_high": 20.0 + } + ] +} \ No newline at end of file diff --git a/backend/config/device.json b/backend/config/device.json new file mode 100644 index 0000000..c501daf --- /dev/null +++ b/backend/config/device.json @@ -0,0 +1,47 @@ +{ + "password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "hardware_version": { + "board_version": "B001.001.002", + "display_version": "S001.001.001", + "other_version": "Y001.001.001" + }, + "software_version": { + "display_program": "001.001.001", + "communication_program": "001.001.001", + "measurement_program": "001.001.001" + }, + "net": [ + { + "nic": "网卡一", + "ip": "192.168.1.10", + "mask": "255.255.255.0", + "gateway": "192.168.1.1", + "protocol": "Modbus TCP" + }, + { + "nic": "网卡二", + "ip": "192.168.1.56", + "mask": "255.255.255.255", + "gateway": "192.168.1.56", + "protocol": "Modbus TCP" + } + ], + "uart": [ + { + "port": "COM1", + "baud": 9600, + "parity": "NONE", + "data_bits": 8, + "stop_bits": 1, + "protocol": "" + }, + { + "port": "COM2", + "baud": 115200, + "parity": "NONE", + "data_bits": 8, + "stop_bits": 1, + "protocol": "Modbus RTU" + } + ] +} \ No newline at end of file diff --git a/backend/config/setting.json b/backend/config/setting.json new file mode 100644 index 0000000..e9c6f8b --- /dev/null +++ b/backend/config/setting.json @@ -0,0 +1,39 @@ +{ + "line_alarm_setting": { + "line_no": 1, + "over_limit_alarm": [ + { + "category": "??", + "limit": 180.0, + "delay": 180, + "output_node": "??1", + "enabled": true + } + ], + "fault_alarm": [ + { + "category": "PT??", + "limit": null, + "delay": 180, + "output_node": "??1", + "enabled": true + } + ] + }, + "ai_alarm_setting": [ + { + "channel_no": 1, + "singal_type": "4-20mA", + "limit_low": 0.0, + "limit_high": 20.0, + "delay": 180, + "output_node": "开出1", + "enabled": true + } + ], + "system_config": { + "time_sync": "manual", + "brightness": 83, + "screen_saver": 60 + } +} \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..dafa401 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "emcp-backend" +version = "0.1.0" +description = "RK3568 电气量测控平台后端骨架" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "fastapi>=0.115,<1.0", + "uvicorn[standard]>=0.30,<1.0", + "pydantic>=2.7,<3.0", + "pydantic-settings>=2.3,<3.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.2,<9.0", + "httpx>=0.27,<1.0", +] + +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/backend/setup.py b/backend/setup.py new file mode 100644 index 0000000..4c50bfd --- /dev/null +++ b/backend/setup.py @@ -0,0 +1,21 @@ +from setuptools import find_packages, setup + + +setup( + name="emcp-backend", + version="0.1.0", + description="RK3568 电气量测控平台后端骨架", + packages=find_packages(), + install_requires=[ + "fastapi>=0.115,<1.0", + "uvicorn[standard]>=0.30,<1.0", + "pydantic>=2.7,<3.0", + "pydantic-settings>=2.3,<3.0", + ], + extras_require={ + "dev": [ + "pytest>=8.2,<9.0", + "httpx>=0.27,<1.0", + ] + }, +) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..e8225cb --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,20 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +client = TestClient(app) + + +def test_healthcheck() -> None: + response = client.get("/") + assert response.status_code == 200 + assert response.json()["status"] == "ok" + + +def test_realtime_endpoint() -> None: + response = client.get("/api/real-time-data") + assert response.status_code == 200 + payload = response.json() + assert payload["code"] == 200 + assert "line_list" in payload["data"] diff --git a/document/gui.h b/document/gui.h new file mode 100644 index 0000000..ca9bc82 --- /dev/null +++ b/document/gui.h @@ -0,0 +1,207 @@ +#ifndef __GUI_H +#define __GUI_H + +#include "time.h" + +#define LINE_TOTAL_NUM 4 //· +#define ANALOG_CH_NUM 12 //AIAOģͨ + + +/**************************************************************С -> ****************************************************************************/ + +//ʵʱ +typedef struct +{ + //ѹ + float Ua; //Aѹ + float Ub; //Bѹ + float Uc; //Cѹ + + // + float Ia; //A + float Ib; //B + float Ic; //C + + //й + float Pa; + float Pb; + float Pc; + float Pt; //й + + //޹ + float Qa; + float Qb; + float Qc; + float Qt; //޹ + + //ڹ + float Sa; + float Sb; + float Sc; + float St; //ڹ + + // + float PFa; + float PFb; + float PFc; + float PFt; //ܹ + + //ߵѹ + float Uab; + float Ubc; + float Uca; + + //Ƶ + float frq; + +}measure_unit_t; + +//Bʱʱ +typedef union +{ + uint8_t b_code[6]; + + struct + { + uint8_t ascii_s_unit : 4; + uint8_t ascii_s_ten : 4; + uint8_t ascii_min_unit : 4; + uint8_t ascii_min_ten : 4; + uint8_t ascii_hour_unit : 4; + uint8_t ascii_hour_ten : 4; + uint8_t ascii_m_day_unit: 4; + uint8_t ascii_m_day_ten : 4; + uint8_t ascii_month_unit: 4; //4bit + uint8_t ascii_month_ten : 4; //4bit + uint8_t ascii_year_unit : 4; //4bit + uint8_t ascii_year_ten : 4; //4bit + }time; +}b_code_t; + + +//롢 +typedef struct +{ + uint16_t in; //ÿһλӦһͨ״̬ 1 = ϣ 0 = + uint16_t out; //ÿһλӦһͨ״̬ 1 = ϣ 0 = +}d_io_t; + +//ģ:AI AO +typedef struct +{ + float in[ANALOG_CH_NUM]; //ģ + float out[ANALOG_CH_NUM]; //ģ +}a_io_t; + +//¼ +typedef struct +{ + struct tm t; //¼ʱ + uint32_t ms; //¼ĺʱ + uint8_t evt; //ͣ0 = ¼ 1 = ¼ +}event_t; + +// +typedef struct +{ uint32_t stamp; //ʱ + int16_t ch[8]; //4·ʱ8ͨuaubuc, iaibicƵϵ + //AIɼͨʱ 8ͨӦ +}ch_data_t; + +// +typedef struct +{ + uint8_t line_index; //· 0 ~ 3 = 4·ѹݣ4 = AIɼ + ch_data_t dot[64]; //64һܲ +}waveform_data_t; + + +//С -> +typedef struct +{ + measure_unit_t measure_unit[LINE_TOTAL_NUM];//ܹ4· + + a_io_t d_io; //ģ롢 + + d_io_t a_io; //롢 + + b_code_t b_code; //Bʱ + + event_t event[2]; //¼־:event[0] = pt ߣ event[1] = CT + + waveform_data_t waveform; //ʵʱһܲ + +}gui_in_t; + + +/************************************************************** -> С****************************************************************************/ +//һ޸ģ·һ + + +//ڲ +typedef struct +{ + uint32_t baud_tate; // 9600 115200Ĭϣ + uint8_t parity; //Уλ 0 = У飬 1 = У飬 2 = żУ + uint8_t data; //λ 0 = 8λ + uint8_t stop; //ֹͣλ 0 = 1λ 1 = 2λ + uint8_t protocol; //ͨѶЭ 0 = Э飬1 = modbus +}com_param_t; + + +//ģֵͨ +typedef struct +{ + uint8_t sig_type;//ź 0 = 4~20maźţ 1 = 1~5Vź + uint8_t line_sub;//· 1 = ·1 2 = ·2 3 =·3 4 = ·4 0 = װ + uint8_t att; // : 0 = UA, 1 = UB, 2 = UC, 3 = UAB, 4 = UBC, 5 = UCA, 6 = P, 7 = Q, 8 = F + uint8_t u_limit; //ֵ : + uint8_t d_limit; //ֵ : +}analog_ch_param_t; + + +//·ֵ-> +typedef struct +{ + // uint8_t index; //·ţ 1 = ·1, 2 = ·2 3 = ·3, 4 = ·4 + uint8_t limit; //ֵ + uint8_t delay; //ʱ [1 ~ 10] + uint8_t out_num; //ڵ㣺 [1 = 1 2 = 2...] + uint8_t select; //Ͷˣ [0 = Ͷ 1 = Ͷ] +}line_param_t; + +typedef struct +{ + line_param_t type[7]; //7𣺵ѹ, ʣƵʣPTߣCT +}line_type_t; + + +//AI -> +typedef struct +{ + uint8_t u_limit; //ֵ : + uint8_t d_limit; //ֵ : + uint8_t delay; //ʱ [1 ~ 10] + uint8_t out_num; //ڵ㣺 [1 = 1 2 = 2...] + uint8_t select; //Ͷˣ [0 = Ͷ 1 = Ͷ] +}ai_alarm_param_t; + + +// -> С +//ģ +typedef struct +{ + struct tm t; //ʱ + + d_io_t a_io; //롢 + + com_param_t com_param[4]; //ڲ + line_type_t line_param[4]; //·ֵ: 4·ֵһ· + analog_ch_param_t a_in_param[12]; //12·AIģͨ + analog_ch_param_t a_out_param[12]; //12·AOģͨ + ai_alarm_param_t ai_alarm_param[12];//12·AIģ뱨 + +}gui_out_t; + + +#endif diff --git a/document/前后端启动说明.md b/document/前后端启动说明.md new file mode 100644 index 0000000..a167067 --- /dev/null +++ b/document/前后端启动说明.md @@ -0,0 +1,422 @@ +# 电气量测控平台前后端启动说明 + +## 1. 文档说明 + +本文档用于说明 `EMCP` 项目前后端的开发环境准备、依赖安装、启动步骤、访问地址以及常见问题处理方式。 + +项目目录: + +- 根目录:`d:\Trae_space\EMCP` +- 后端目录:`d:\Trae_space\EMCP\backend` +- 前端目录:`d:\Trae_space\EMCP\frontend` +- 文档目录:`d:\Trae_space\EMCP\document` + +--- + +## 2. 当前项目技术栈 + +### 2.1 后端 + +- 语言:`Python 3.8` +- 当前虚拟环境版本:`Python 3.8.10` +- 框架:`FastAPI` +- 服务启动:`Uvicorn` +- 配置存储:`JSON` +- 报警存储:`SQLite` +- 实时通信:`WebSocket` + +### 2.2 前端 + +- 框架:`Vue 3` +- 构建工具:`Vite` +- HTTP 请求:`Axios` + +--- + +## 3. 启动前准备 + +### 3.1 后端准备 + +后端目录已创建虚拟环境: + +- 虚拟环境路径:`d:\Trae_space\EMCP\backend\.venv` + +如虚拟环境不存在,可在 `backend` 目录重新创建: + +```powershell +cd d:\Trae_space\EMCP\backend +py -3.8 -m venv .venv +``` + +激活虚拟环境: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +``` + +### 3.2 前端准备 + +确保本机已安装: + +- `Node.js` +- `npm` + +进入前端目录后通过 `npm` 安装依赖。 + +--- + +## 4. 首次安装依赖 + +### 4.1 安装后端依赖 + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +pip install -e .[dev] +``` + +说明: + +- 当前项目已包含 `setup.py` 与 `pyproject.toml` +- 若 `pip install -e .[dev]` 因环境兼容问题失败,可先升级 `pip` 后重试: + +```powershell +python -m pip install --upgrade pip +pip install -e .[dev] +``` + +### 4.2 安装前端依赖 + +```powershell +cd d:\Trae_space\EMCP\frontend +npm install +``` + +--- + +## 5. 启动后端 + +### 5.1 启动命令 + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +.\.venv\Scripts\python.exe -m uvicorn app.main:app --reload +``` + +### 5.2 启动成功标志 + +终端出现类似信息表示启动成功: + +```text +Uvicorn running on http://127.0.0.1:8000 +``` + +### 5.3 后端访问地址 + +- 健康检查:`http://127.0.0.1:8000/` +- 接口文档:`http://127.0.0.1:8000/docs` +- OpenAPI:`http://127.0.0.1:8000/openapi.json` + +### 5.4 后端主要接口 + +- 实时数据:`GET /api/real-time-data` +- 设备状态:`GET /api/device-status` +- 设备配置:`POST /api/config/device` +- 通道配置:`POST /api/config/channel` +- 线路报警设置:`POST /api/config/line_alarm_setting` +- AI 报警设置:`POST /api/config/ai_alarm_setting` +- 系统设置:`POST /api/config/system` +- 报警历史:`GET /api/alarm/list` +- 控制指令:`POST /api/control/switch` + +### 5.5 WebSocket 地址 + +- 实时数据:`ws://127.0.0.1:8000/ws/real-time` +- 报警推送:`ws://127.0.0.1:8000/ws/alarm` + +### 5.6 后端 Debug 调试启动 + +开发阶段如果需要断点调试后端,推荐使用以下两种方式。 + +#### 方式一:在 IDE 中直接调试 `uvicorn` + +适用于 `Trae / VS Code / Cursor` 一类支持 `launch.json` 的编辑器。 + +可在工作区创建或补充调试配置文件: + +- `d:\Trae_space\EMCP\.vscode\launch.json` + +示例配置: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug FastAPI Backend", + "type": "python", + "request": "launch", + "module": "uvicorn", + "cwd": "d:\\Trae_space\\EMCP\\backend", + "args": [ + "app.main:app", + "--host", + "127.0.0.1", + "--port", + "8000", + "--reload" + ], + "jinja": true, + "justMyCode": true + } + ] +} +``` + +使用方式: + +1. 打开项目根目录 `d:\Trae_space\EMCP` +2. 选择 `Debug FastAPI Backend` +3. 启动调试 +4. 在 `backend/app/` 下设置断点 + +说明: + +- `cwd` 必须指向 `backend`,否则 `app.main:app` 可能导入失败 +- 调试时建议使用后端虚拟环境解释器:`d:\Trae_space\EMCP\backend\.venv\Scripts\python.exe` +- 如果 `--reload` 导致断点命中不稳定,可临时去掉 `--reload` + +#### 方式二:使用 `debugpy` 启动后再附加调试 + +先安装调试依赖: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +pip install debugpy +``` + +然后使用以下命令启动: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +python -m debugpy --listen 5678 -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload +``` + +启动后可在 IDE 中使用“附加到进程/Attach”方式连接: + +- 主机:`127.0.0.1` +- 端口:`5678` + +如果不需要热重载,也可以使用更稳定的命令: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +python -m debugpy --listen 5678 -m uvicorn app.main:app --host 127.0.0.1 --port 8000 +``` + +#### Debug 调试建议 + +- 需要稳定断点时,优先关闭 `--reload` +- 排查接口逻辑时,建议在以下位置打断点: + - `backend/app/api/routes/` + - `backend/app/services/platform_service.py` + - `backend/app/adapters/device_client.py` +- 如果要调试模拟采集链路,可重点查看: + - `backend/app/tasks/polling.py` + - `backend/app/cache/memory_store.py` + - `backend/app/ws/manager.py` + +--- + +## 6. 启动前端 + +### 6.1 启动命令 + +```powershell +cd d:\Trae_space\EMCP\frontend +npm run dev -- --host 127.0.0.1 --port 5173 +``` + +### 6.2 启动成功标志 + +终端出现类似地址表示启动成功: + +```text +Local: http://127.0.0.1:5173/ +``` + +### 6.3 前端访问地址 + +- 前端页面:`http://127.0.0.1:5173/` + +--- + +## 7. 推荐启动顺序 + +建议按以下顺序启动: + +1. 先启动后端 +2. 再启动前端 +3. 浏览器打开前端地址进行联调 + +原因: + +- 前端默认请求后端接口:`http://127.0.0.1:8000/api` +- 前端默认连接后端 WebSocket: + - `ws://127.0.0.1:8000/ws/real-time` + - `ws://127.0.0.1:8000/ws/alarm` + +前端接口配置位置: + +- `frontend/src/api/http.ts` + +默认请求头中已携带: + +- `X-API-Token: admin123` + +--- + +## 8. 模拟系统说明 + +当前项目后端默认启用模拟设备采集。 + +后端配置位置: + +- `backend/app/core/config.py` + +当前关键配置: + +```python +use_mock_device: bool = True +poll_interval_seconds: float = 0.5 +``` + +说明: + +- `use_mock_device = True` 表示使用模拟设备数据源 +- 后端会按 `0.5` 秒周期采集模拟数据 +- 前端可用于联调实时数据、设备状态、报警历史、控制指令、配置保存等功能 + +--- + +## 9. 当前已打通页面 + +当前前端页面均已可以进入,已完成联调的页面如下: + +- 实时数据 +- 设备状态 +- 设备配置 +- 通道配置 +- 报警设置 +- 报警历史 +- 控制指令 +- 系统设置 + +其中: + +- `设备状态` 支持手动刷新 +- `报警历史` 支持分页和刷新 +- `设备配置 / 通道配置 / 报警设置` 已接通真实后端提交接口 + +--- + +## 10. 常用开发命令 + +### 10.1 后端测试 + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +python -m pytest +``` + +### 10.2 前端构建 + +```powershell +cd d:\Trae_space\EMCP\frontend +npm run build +``` + +--- + +## 11. 常见问题 + +### 11.1 后端启动失败,提示类型注解错误 + +如果出现类似报错: + +```text +TypeError: 'type' object is not subscriptable +``` + +原因通常是 Python 版本不兼容。当前项目要求使用: + +- `Python 3.8` + +请确认虚拟环境版本是否正确。 + +### 11.2 前端页面打不开 + +请检查: + +- 前端服务是否已启动 +- 地址是否为 `http://127.0.0.1:5173/` +- `npm install` 是否已执行成功 + +### 11.3 前端有页面但接口不通 + +请检查: + +- 后端服务是否已启动 +- 后端地址是否为 `http://127.0.0.1:8000` +- `frontend/src/api/http.ts` 中 `baseURL` 是否正确 + +### 11.4 页面能打开但实时数据不更新 + +请检查: + +- 后端是否正常运行 +- 后端轮询任务是否已启动 +- WebSocket 是否可连 +- 若 WebSocket 未连通,前端当前会自动回退到 HTTP 轮询读取实时数据 + +### 11.5 保存接口返回 401 或 403 + +请检查请求头中是否包含: + +```text +X-API-Token: admin123 +``` + +--- + +## 12. 关闭服务 + +在各自终端中按: + +```text +Ctrl + C +``` + +即可停止前端或后端服务。 + +--- + +## 13. 建议 + +日常开发建议保持两个独立终端: + +- 终端 1:后端 `uvicorn` +- 终端 2:前端 `vite` + +如果需要后续交付或部署,可在本说明基础上继续补充: + +- 生产环境启动说明 +- Windows 开机自启说明 +- Ubuntu / RK3568 部署说明 +- Nginx 反向代理说明 diff --git a/document/在RK3568设备上系统部署及使用说明.md b/document/在RK3568设备上系统部署及使用说明.md new file mode 100644 index 0000000..024934b --- /dev/null +++ b/document/在RK3568设备上系统部署及使用说明.md @@ -0,0 +1,579 @@ +# RK3568 设备上系统部署及使用说明 + +## 1. 文档目的 + +本文档用于说明如何在 `RK3568 + Ubuntu 20.04` 设备上部署 `EMCP` 电气量测控平台的前后端系统,并完成以下目标: + +- 部署后端 `FastAPI + Uvicorn` 服务 +- 部署前端静态页面 +- 配置前后端开机自动启动 +- 登录桌面后自动打开浏览器并访问主页面 +- 给出现场快速设置和日常使用说明 + +--- + +## 2. 适用范围 + +适用对象: + +- 硬件平台:`RK3568` +- 操作系统:`Ubuntu 20.04` +- 部署模式:前后端均部署在同一台设备本机 + +当前工程实际情况: + +- 后端运行端口:`8000` +- 前端访问地址建议使用:`http://127.0.0.1:5173/` +- 前端默认请求后端地址:`http://127.0.0.1:8000/api` +- 前端默认请求头中携带:`X-API-Token: admin123` +- 默认安全校验密码:`admin123` +- 后端默认启用模拟采集:`use_mock_device = True` + +说明: + +- 当前前端代码默认调用本机 `127.0.0.1:8000` 的后端接口,因此推荐前后端部署在同一台 RK3568 设备上 +- 如果后续需要将前后端拆分到不同设备,需要同步调整前端接口配置 + +--- + +## 3. 部署建议 + +推荐目录规划如下: + +```text +/opt/emcp/ +├── backend/ +├── frontend/ +└── scripts/ +``` + +建议使用以下方式部署: + +- 后端:`systemd` 托管 `uvicorn` +- 前端:前端执行 `npm run build` 后,用轻量静态服务对 `dist/` 目录发布 +- 浏览器自启动:使用桌面自动启动项 `~/.config/autostart/*.desktop` + +--- + +## 4. 环境准备 + +### 4.1 安装系统依赖 + +在 RK3568 设备终端执行: + +```bash +sudo apt update +sudo apt install -y python3.8 python3.8-venv python3-pip curl git +sudo apt install -y nodejs npm +``` + +说明: + +- Ubuntu 20.04 默认可直接使用 `Python 3.8` +- 如果设备上的 `nodejs` 版本过低,建议升级到 `Node.js 18 LTS` 后再安装前端依赖 + +### 4.2 安装浏览器 + +如果设备带图形桌面,建议至少安装一个浏览器: + +```bash +sudo apt install -y chromium-browser +``` + +如果系统仓库没有 `chromium-browser`,也可以使用: + +```bash +sudo apt install -y firefox +``` + +### 4.3 拷贝项目代码 + +将工程目录拷贝到设备,例如: + +```bash +sudo mkdir -p /opt/emcp +sudo chown -R $USER:$USER /opt/emcp +``` + +然后将项目复制到: + +```text +/opt/emcp/backend +/opt/emcp/frontend +``` + +如果是通过 `git` 拉取,也可以在 `/opt/emcp` 下执行: + +```bash +git clone <你的仓库地址> /opt/emcp +``` + +如果项目目录本身已经包含 `backend` 和 `frontend`,则以实际目录结构为准。 + +--- + +## 5. 后端部署 + +### 5.1 创建虚拟环境 + +```bash +cd /opt/emcp/backend +python3.8 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +``` + +### 5.2 安装后端依赖 + +```bash +cd /opt/emcp/backend +source .venv/bin/activate +pip install -e . +``` + +如需测试或调试工具,再安装: + +```bash +pip install -e .[dev] +``` + +### 5.3 手动启动后端验证 + +```bash +cd /opt/emcp/backend +source .venv/bin/activate +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +验证方式: + +- 本机健康检查:`http://127.0.0.1:8000/` +- 接口文档:`http://127.0.0.1:8000/docs` + +如果能正常返回服务信息,说明后端部署成功。 + +--- + +## 6. 前端部署 + +### 6.1 安装前端依赖 + +```bash +cd /opt/emcp/frontend +npm install +``` + +### 6.2 构建前端 + +```bash +cd /opt/emcp/frontend +npm run build +``` + +构建完成后会生成: + +```text +/opt/emcp/frontend/dist +``` + +### 6.3 手动启动前端验证 + +推荐使用 Python 内置静态服务快速发布: + +```bash +cd /opt/emcp/frontend +python3 -m http.server 5173 --directory dist --bind 0.0.0.0 +``` + +验证地址: + +- 前端页面:`http://127.0.0.1:5173/` + +说明: + +- 当前前端默认调用本机后端 `http://127.0.0.1:8000/api` +- 因此前端页面只要在设备本机打开,即可直接访问后端服务 + +--- + +## 7. 首次联调检查 + +建议按以下顺序进行: + +1. 先启动后端 +2. 再启动前端静态服务 +3. 浏览器打开 `http://127.0.0.1:5173/` +4. 检查实时数据、设备状态、报警历史等页面是否能正常进入 + +当前项目已打通页面包括: + +- 实时数据 +- 设备状态 +- 设备配置 +- 通道配置 +- 报警设置 +- 报警历史 +- 控制指令 +- 系统设置 + +补充说明: + +- `设备状态` 页面支持刷新 +- `报警历史` 页面支持分页和刷新 +- 配置保存前会弹出安全校验密码输入框 +- 当前默认安全校验密码为 `admin123` + +--- + +## 8. 配置开机自动启动 + +推荐使用 `systemd` 管理前后端服务。 + +### 8.1 配置后端服务 + +创建文件: + +```text +/etc/systemd/system/emcp-backend.service +``` + +内容如下: + +```ini +[Unit] +Description=EMCP Backend Service +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/emcp/backend +ExecStart=/opt/emcp/backend/.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +说明: + +- `User=ubuntu` 需要替换为设备实际登录用户名 +- 如需切换真实采集设备,可结合 `.env` 或配置文件调整后端参数 + +### 8.2 配置前端服务 + +创建文件: + +```text +/etc/systemd/system/emcp-frontend.service +``` + +内容如下: + +```ini +[Unit] +Description=EMCP Frontend Static Service +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/emcp/frontend +ExecStart=/usr/bin/python3 -m http.server 5173 --directory /opt/emcp/frontend/dist --bind 0.0.0.0 +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +### 8.3 启用开机自启 + +执行: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable emcp-backend.service +sudo systemctl enable emcp-frontend.service +sudo systemctl start emcp-backend.service +sudo systemctl start emcp-frontend.service +``` + +查看状态: + +```bash +sudo systemctl status emcp-backend.service +sudo systemctl status emcp-frontend.service +``` + +查看日志: + +```bash +journalctl -u emcp-backend.service -f +journalctl -u emcp-frontend.service -f +``` + +--- + +## 9. 开机自动打开浏览器访问主页面 + +本功能依赖图形桌面环境。若设备只启动命令行、不进入桌面,则无法自动弹出浏览器。 + +### 9.1 前置条件 + +建议提前确认以下两项: + +- 设备已安装浏览器 +- 设备已启用桌面自动登录,或现场人员上电后会手动登录桌面 + +说明: + +- 只有在用户登录图形桌面后,自动启动项才会执行 +- 如果希望“上电后无需人工登录就自动打开页面”,需要同时开启系统自动登录 + +### 9.2 创建浏览器启动脚本 + +创建目录: + +```bash +mkdir -p /opt/emcp/scripts +``` + +创建脚本文件: + +```text +/opt/emcp/scripts/start-emcp-browser.sh +``` + +内容如下: + +```bash +#!/bin/bash +sleep 8 + +URL="http://127.0.0.1:5173/" + +if command -v chromium-browser >/dev/null 2>&1; then + chromium-browser --start-fullscreen "$URL" >/dev/null 2>&1 & +elif command -v firefox >/dev/null 2>&1; then + firefox "$URL" >/dev/null 2>&1 & +else + xdg-open "$URL" >/dev/null 2>&1 & +fi +``` + +赋予执行权限: + +```bash +chmod +x /opt/emcp/scripts/start-emcp-browser.sh +``` + +说明: + +- `sleep 8` 用于等待前后端服务启动完成 +- 如果设备性能较低,可改为 `sleep 10` 或 `sleep 15` +- 如需全屏显示,优先使用 `chromium-browser --start-fullscreen` + +### 9.3 配置桌面自动启动项 + +创建目录: + +```bash +mkdir -p ~/.config/autostart +``` + +创建文件: + +```text +~/.config/autostart/emcp-browser.desktop +``` + +内容如下: + +```ini +[Desktop Entry] +Type=Application +Name=EMCP Browser Autostart +Exec=/opt/emcp/scripts/start-emcp-browser.sh +X-GNOME-Autostart-enabled=true +Terminal=false +``` + +配置完成后,用户每次登录桌面时都会自动打开浏览器并访问: + +```text +http://127.0.0.1:5173/ +``` + +--- + +## 10. 快捷设置说明 + +这一节适合现场交付时快速操作。 + +### 10.1 一次性完成部署 + +按以下顺序操作即可: + +1. 拷贝项目到 `/opt/emcp` +2. 创建后端虚拟环境并安装依赖 +3. 前端执行 `npm install` 和 `npm run build` +4. 创建两个 `systemd` 服务 +5. 创建浏览器自动启动脚本 +6. 创建 `~/.config/autostart/emcp-browser.desktop` +7. 执行 `systemctl enable` 完成开机自启 + +### 10.2 最常用检查命令 + +检查后端状态: + +```bash +sudo systemctl status emcp-backend.service +``` + +检查前端状态: + +```bash +sudo systemctl status emcp-frontend.service +``` + +重启后端: + +```bash +sudo systemctl restart emcp-backend.service +``` + +重启前端: + +```bash +sudo systemctl restart emcp-frontend.service +``` + +重新打开浏览器主页: + +```bash +/opt/emcp/scripts/start-emcp-browser.sh +``` + +### 10.3 修改主页地址 + +如果前端端口改为其他端口,例如 `8080`,需要同步修改以下两处: + +- `/opt/emcp/scripts/start-emcp-browser.sh` 中的 `URL` +- 前端静态服务端口或访问地址 + +### 10.4 修改登录后自动打开行为 + +如果不希望每次登录都自动打开浏览器,可以删除或重命名: + +```text +~/.config/autostart/emcp-browser.desktop +``` + +--- + +## 11. 日常使用说明 + +### 11.1 启动后访问方式 + +当设备开机完成并进入桌面后,系统会自动: + +- 启动后端服务 +- 启动前端静态服务 +- 打开浏览器访问主页 + +默认主页地址: + +```text +http://127.0.0.1:5173/ +``` + +### 11.2 页面使用提示 + +- 实时数据页:查看模拟或采集到的实时数据 +- 设备状态页:支持手动刷新状态 +- 报警历史页:支持分页和刷新 +- 配置类页面:保存前需要输入安全校验密码 + +当前默认安全校验密码: + +```text +admin123 +``` + +建议交付后尽快修改为现场正式密码。 + +--- + +## 12. 常见问题 + +### 12.1 浏览器没有自动打开 + +请检查: + +- 是否进入了图形桌面 +- `~/.config/autostart/emcp-browser.desktop` 是否存在 +- `/opt/emcp/scripts/start-emcp-browser.sh` 是否有执行权限 +- 浏览器是否已安装 + +### 12.2 浏览器打开了,但页面访问失败 + +请检查: + +- 前端服务是否启动成功 +- `5173` 端口是否被其他程序占用 +- `/opt/emcp/frontend/dist` 是否已经构建生成 + +### 12.3 页面能打开,但接口不通 + +请检查: + +- 后端服务是否启动成功 +- `8000` 端口是否正常监听 +- 前端默认接口地址是否仍为 `http://127.0.0.1:8000/api` + +### 12.4 页面保存时报权限或校验失败 + +请检查: + +- 默认请求头 `X-API-Token` 是否被修改 +- 输入的安全校验密码是否正确 +- 当前默认密码是否仍为 `admin123` + +### 12.5 修改代码后页面没有变化 + +请检查是否重新执行了前端构建: + +```bash +cd /opt/emcp/frontend +npm run build +sudo systemctl restart emcp-frontend.service +``` + +后端代码修改后则需要重启后端服务: + +```bash +sudo systemctl restart emcp-backend.service +``` + +--- + +## 13. 推荐交付方式 + +建议现场交付时采用以下方式: + +- 设备开机自动进入桌面 +- 后端和前端均使用 `systemd` 自启动 +- 浏览器自动全屏打开主页 +- 保留一个终端窗口或 SSH 方式用于维护 + +如需进一步增强生产部署能力,后续可继续补充: + +- `Nginx` 托管前端静态资源 +- 反向代理后端接口 +- HTTPS 证书 +- 真实硬件采集驱动接入 +- 自动备份配置与报警数据库 diff --git a/document/在RK3568设备上系统部署及使用说明.pdf b/document/在RK3568设备上系统部署及使用说明.pdf new file mode 100644 index 0000000..b5b5199 Binary files /dev/null and b/document/在RK3568设备上系统部署及使用说明.pdf differ diff --git a/document/电气量测控平台-显示界面设计和数据接口说.docx b/document/电气量测控平台-显示界面设计和数据接口说.docx new file mode 100644 index 0000000..339d11c Binary files /dev/null and b/document/电气量测控平台-显示界面设计和数据接口说.docx differ diff --git a/document/电气量测控平台-显示界面设计和数据接口说.pdf b/document/电气量测控平台-显示界面设计和数据接口说.pdf new file mode 100644 index 0000000..349a528 Binary files /dev/null and b/document/电气量测控平台-显示界面设计和数据接口说.pdf differ diff --git a/document/电气量测控平台系统需求分析及技术框架设计.md b/document/电气量测控平台系统需求分析及技术框架设计.md new file mode 100644 index 0000000..a437fbb --- /dev/null +++ b/document/电气量测控平台系统需求分析及技术框架设计.md @@ -0,0 +1,689 @@ +# 电气量测控平台系统需求分析与架构设计技术方案 +**适配平台:RK3568 嵌入式平台(ubuntu 22.04系统)** +**技术栈:FastAPI + HTTP + WebSocket + Python + Web前端** + +## 一、文档概述 +### 1.1 文档目的 +本文档基于《电气量测控平台显示界面设计和数据接口说明》、`gui.h`头文件及项目功能描述,明确**RK3568嵌入式平台**上Python编写、Web界面展示的电气量测控配置软件的功能、性能、接口、数据存储需求,完成系统架构设计,为软件开发、测试及交付提供技术依据。 + +### 1.2 适用范围 +本文档适用于**RK3568嵌入式硬件**,以Python为开发语言、FastAPI为服务框架、Web为交互界面的电气量测控配置软件,涵盖参数配置、实时数据交互、状态监测、报警管理等全流程功能设计。 + +### 1.3 参考资料 +1. 《电气量测控平台显示界面设计和数据接口说明》 +2. `gui.h`嵌入式程序头文件 +3. 项目功能需求描述 + +## 二、系统需求分析 +### 2.1 运行环境需求 +- **硬件环境**:RK3568嵌入式开发板(支持Linux系统、文件系统、SQLite数据库) +- **软件环境**:Linux操作系统、Python 3.8、**FastAPI**、Uvicorn ASGI服务、SQLite数据库、JSON文件存储支持 +- **交互环境**:Web浏览器(支持WebSocket、动态数据渲染、按钮交互、表格/波形展示) + +### 2.2 功能需求 +#### 2.2.1 参数配置管理功能 +实现测控设备全维度参数配置,配置项支持**JSON文件本地存储**,关键参数通过接口下发至C语言嵌入式程序,具体配置项如下: +1. **基础设备配置** + - 设备软件版本信息展示与配置 + - 通讯参数设置:串口、网口参数配置 + - 操作密码设置:支持密码修改、本地JSON文件存储 +2. **装置通道配置** + - AI通道设置:模拟量输入配置、信号类型、所属线路、数据阈值 + - AO通道设置:模拟量输出配置 + - 定值设置:电压/电流/功率等超限限值、检测延时、报警输出配置 +3. **系统管理配置** + - 对时设置:参数实时下发嵌入式程序 + - 灯光设置:屏幕亮度、屏保时间,参数实时下发嵌入式程序 + +#### 2.2.2 实时数据动态显示功能 +1. **数据读取周期**:间隔**0.5秒**从C语言控制系统读取实时数据 +2. **数据类型**:模拟量、开关量、AI采集数据 +3. **推送方式**:**WebSocket主动推送**至Web界面,动态刷新展示 +4. **存储规则**:动态数据仅内存缓存,**不保存到本地** + +#### 2.2.3 设备状态实时监测功能 +从嵌入式系统动态读取设备状态,Web界面实时展示: +- 装置自检状态(正常/异常) +- 网络1状态、网络2状态 +- 串口1状态、串口2状态 + +#### 2.2.4 报警事件管理功能 +1. **报警数据采集**:实时获取嵌入式设备报警事件 +2. **数据存储**:报警事件保存至**SQLite数据库** +3. **展示方式**:**WebSocket实时推送**报警至Web界面,支持弹窗提醒+历史查询 + +### 2.3 数据交互需求 +#### 2.3.1 上行数据(嵌入式系统→Python服务) +- 实时数据:0.5秒/次,模拟量、开关量、AI采集数据 +- 状态数据:装置自检、网络、串口状态 +- 报警数据:实时报警事件数据 + +#### 2.3.2 下行数据(Python服务→嵌入式系统) +- 配置数据:设备参数、通道配置、定值、对时、灯光参数 +- 控制指令:开关量分/合控制信号 + +### 2.4 数据存储需求 +1. **JSON文件存储**:设备版本、通讯参数、密码、AI/AO配置、定值配置等**非实时参数** +2. **SQLite数据库**:报警事件数据(支持历史查询) +3. **动态数据**:仅内存缓存,不持久化存储 + +### 2.5 性能需求 +1. **数据刷新率**:实时数据读取与WebSocket推送间隔≤0.5秒 +2. **响应速度**:参数配置下发、开关控制指令响应时间≤1秒 +3. **稳定性**:7×24小时连续运行 +4. **兼容性**:适配RK3568硬件,兼容嵌入式C程序接口 + +### 2.6 通信服务需求 +1. **HTTP服务**:基于FastAPI提供**RESTful API**,用于参数配置、状态查询、报警查询、控制指令 +2. **WebSocket服务**:基于FastAPI提供双工通信,用于**实时数据、报警数据主动推送**至Web界面 + +## 三、系统架构设计 +### 3.1 总体架构 +系统采用**分层架构 + 前后端分离**设计,从下到上分为: +**硬件层 → 嵌入式C程序层 → Python FastAPI服务层 → Web界面层** + +``` +Web界面层(HTML/CSS/JS) + ↓↑(HTTP + WebSocket) +Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) + ↓↑(内部进程/核间通讯) +嵌入式C程序层(数据采集 + 控制执行) + ↓ +硬件层(RK3568 + 测控硬件 + 通讯模块) +``` + +### 3.2 分层详细设计 +#### 3.2.1 硬件层 +- 核心硬件:RK3568嵌入式开发板 +- 外设模块:AI/AO采集模块、开关量模块、网络/串口模块、显示屏 + +#### 3.2.2 嵌入式C程序层 +- 数据采集模块:20ms周期采集模拟量、开关量、AI数据 +- 控制执行模块:接收参数与指令,执行配置、对时、灯光、开关控制 +- 状态监测模块:自检、网口、串口状态实时上报 +- 报警生成模块:异常事件生成并上传 +- 通讯模块:与Python服务双向数据交互 + +#### 3.2.3 Python FastAPI服务层(核心) +采用**FastAPI + Uvicorn**搭建,提供**HTTP接口**和**WebSocket推送**双服务: +1. **RESTful API模块**:处理配置提交、状态查询、报警查询、控制指令 +2. **WebSocket推送模块**:0.5秒推送实时数据、实时报警数据到Web端 +3. **参数配置模块**:JSON文件读写、配置下发C程序 +4. **实时数据模块**:定时读取C程序数据,缓存并通过WebSocket推送 +5. **报警管理模块**:接收报警→存储SQLite→WebSocket推送前端 +6. **设备状态模块**:采集并缓存设备运行状态 + +#### 3.2.4 Web界面层 +- 菜单区:配置、状态、数据、报警、控制入口 +- 状态区:自检、网络、串口实时状态 +- 主显示区:实时数据表格、配置表单、控制按钮、报警列表 +- 报警区:WebSocket实时弹窗+滚动显示 + +### 3.3 数据流向设计 +1. **实时/报警数据流向** +嵌入式C程序 → Python → WebSocket主动推送 → Web界面(0.5秒刷新) +2. **参数配置流向** +Web界面 → HTTP POST → Python → JSON存储 + 下发C程序 +3. **状态/查询流向** +Web界面 → HTTP GET → Python → 返回数据展示 + +## 四、接口详细设计(RESTful + WebSocket) +### 4.1 RESTful API 接口详情(完整参数+返回值) +所有接口统一返回格式: +```json +{ + "code": 200, // 200=成功,400=参数错误,500=服务异常 + "msg": "操作成功", + "data": {} // 业务数据 +} +``` + +--- + +#### 1. GET /api/real-time-data +**作用**:获取最新一次实时数据(备用,主用WebSocket) +**参数**:无 +**返回data**: + +```json +{ + "code": 200, + "msg": "获取实时数据成功", + "data": { + "line_list": [ + { + "line_no": 1, + "pri_val": { + "Ua": 6000.0, + "Ub": 6002.5, + "Uc": 5998.3, + "Ia": 150.2, + "Ib": 148.9, + "Ic": 151.6, + "Pa": 820.5, + "Pb": 815.3, + "Pc": 825.7, + "Pt": 2461.5, + "Qa": 120.3, + "Qb": 118.6, + "Qc": 122.9, + "Qt": 361.8, + "Sa": 835.2, + "Sb": 829.7, + "Sc": 840.3, + "St": 2505.2, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 10400.1, + "Ubc": 10395.6, + "Uca": 10398.2, + "frq": 50.00 + }, + "sec_val": { + "Ua": 57.6, + "Ub": 57.7, + "Uc": 57.5, + "Ia": 1.2, + "Ib": 1.18, + "Ic": 1.22, + "Pa": 68.2, + "Pb": 67.8, + "Pc": 68.6, + "Pt": 204.6, + "Qa": 9.8, + "Qb": 9.6, + "Qc": 10.1, + "Qt": 29.5, + "Sa": 69.5, + "Sb": 68.9, + "Sc": 70.1, + "St": 208.5, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 100.0, + "Ubc": 99.8, + "Uca": 99.9, + "frq": 50.00 + } + }, + { + "line_no": 2, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 3, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 4, + "pri_val": {}, + "sec_val": {} + } + ], + "switch": { + "di1": 1, + "di2": 0, + "di3": 1, + "di4": 0, + "di5": 1, + "di6": 0, + "di7": 1, + "di8": 0, + "di9": 1, + "di10": 0, + "di11": 1, + "di12": 0, + "do1": 1, + "do2": 0, + "do3": 1, + "do4": 0, + "do5": 1, + "do6": 0, + "do7": 1, + "do8": 0, + "do9": 1, + "do10": 0, + "do11": 1, + "do12": 0 + }, + "ai_collect": { + "ai1": 4.52, + "ai2": 3.86, + "ai3": 5.21, + "ai4": 2.99, + "ai5": 6.10, + "ai6": 1.75, + "ai7": 3.22, + "ai8": 4.91, + "ai9": 2.18, + "ai10": 5.47, + "ai11": 3.06, + "ai12": 4.33 + } + } +} +``` + +--- + +#### 2. GET /api/device-status +**作用**:获取设备当前运行状态 +**参数**:无 +**返回data**: +```json +{ + "self_check": "正常", + "net1": "正常", + "net2": "正常", + "uart1": "正常", + "uart2": "断开" +} +``` + +--- + +#### 3. POST /api/config/device +**作用**:提交设备基础配置(版本、通讯、密码) +**请求body参数**: +```json +{ + "password": "123456", + "hardware_version": { + "board_version": "B001.001.001", + "display_version": "S001.001.001", + "other_version": "Y001.001.001" + }, + "software_version": { + "display_program": "001.001.001", + "communication_program": "001.001.001", + "measurement_program": "001.001.001" + }, + "net": [ + { + "nic": "网卡一", + "ip": "", + "mask": "", + "gateway": "", + "protocol": "" + }, + { + "nic": "网卡二", + "ip": "192.168.1.56", + "mask": "255.255.255.255", + "gateway": "192.168.1.56", + "protocol": "Modbus TCP" + } + ], + "uart": [ + { + "port": "COM1", + "baud": 0, + "parity": "", + "data_bits": 0, + "stop_bits": 0, + "protocol": "" + }, + { + "port": "COM2", + "baud": 115200, + "parity": "NONE", + "data_bits": 8, + "stop_bits": 1, + "protocol": "Modbus RTU" + } + ] +} +``` +**返回data**:`{"save_path":"/config/device.json","send_status":"成功"}` + +--- + +#### 4. POST /api/config/channel +**作用**:提交AI/AO通道配置(AI:12通道,AO:12通道) +**请求body参数**: + +```json +{ + "ai_channel": [{"ch":1,"singal_type":"4-20mA","line_no":1,"type":"UA","limit_low":0,"limit_high":20}], + "ao_channel": [{"ch":1,"singal_type":"1-5v","line_no":2,"type":"UA","limit_low":0,"limit_high":20}] +} +``` +**返回data**:`{"save_path":"/config/channel.json","send_status":"成功"}` + +--- + +#### 5. POST /api/config/line_alarm_setting +**作用**:提交定值报警阈值配置 +**请求body参数**: + +```json +{ + "line_no": 1, + "over_limit_alarm": [ + { + "category": "电压", + "limit": 180, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "category": "电流", + "limit": 180, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "category": "差流", + "limit": 180, + "delay": 180, + "output_node": "开出1", + "enabled": false + }, + { + "category": "功率", + "limit": 180, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "category": "频率", + "limit": 180, + "delay": 180, + "output_node": "开出1", + "enabled": false + } + ], + "fault_alarm": [ + { + "category": "PT断线", + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "category": "CT断线", + "delay": 180, + "output_node": "开出1", + "enabled": false + } + ] + } +``` +**返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}` + +--- + +#### 5. POST /api/config/ai_alarm_setting + +**作用**:提交AI报警设置 +**请求body参数**: + +```json +[ + { + "channel_no": 1, + "singal_type": "4-20mA", + "limit_low":0, + "limit_high":20, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "channel_no": 2, + "singal_type": "1-5v", + "limit_low":0, + "limit_high":20, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + ] +``` + +**返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}` + +#### 6. POST /api/config/system + +**作用**:提交系统对时、灯光配置 +**请求body参数**: +```json +{ + "time_sync": "auto", + "brightness": 80, + "screen_saver": 60 +} +``` +**返回data**:`{"send_status":"成功"}` + +--- + +#### 7. GET /api/alarm/list +**作用**:分页查询历史报警 +**参数**:`page=1&size=20` +**返回data**: +```json +[ +{ + "id": 1, + "alarm_type": "operate_alarm", + "time": "2025-01-01 12:00:00", + "no": "", + "type": "", + "content": "告警内容", + "level": "告警等级" +} +] +``` + +--- + +#### 8. POST /api/control/switch +**作用**:下发开关量控制指令 +**请求body参数**: +```json +{"ch":1,"action":1} // 1=合,0=分 +``` +**返回data**:`{"control_status":"执行成功"}` + +--- + +### 4.2 WebSocket 接口(主动推送) +#### 1. WebSocket 地址:`ws://ip:port/ws/real-time` +**推送内容**:实时数据(0.5秒/次) +```json +{ + "type": "real_time", + "data": { + "line_list": [ + { + "line_no": 1, + "pri_val": { + "Ua": 6000.0, + "Ub": 6002.5, + "Uc": 5998.3, + "Ia": 150.2, + "Ib": 148.9, + "Ic": 151.6, + "Pa": 820.5, + "Pb": 815.3, + "Pc": 825.7, + "Pt": 2461.5, + "Qa": 120.3, + "Qb": 118.6, + "Qc": 122.9, + "Qt": 361.8, + "Sa": 835.2, + "Sb": 829.7, + "Sc": 840.3, + "St": 2505.2, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 10400.1, + "Ubc": 10395.6, + "Uca": 10398.2, + "frq": 50.00 + }, + "sec_val": { + "Ua": 57.6, + "Ub": 57.7, + "Uc": 57.5, + "Ia": 1.2, + "Ib": 1.18, + "Ic": 1.22, + "Pa": 68.2, + "Pb": 67.8, + "Pc": 68.6, + "Pt": 204.6, + "Qa": 9.8, + "Qb": 9.6, + "Qc": 10.1, + "Qt": 29.5, + "Sa": 69.5, + "Sb": 68.9, + "Sc": 70.1, + "St": 208.5, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 100.0, + "Ubc": 99.8, + "Uca": 99.9, + "frq": 50.00 + } + }, + { + "line_no": 2, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 3, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 4, + "pri_val": {}, + "sec_val": {} + } + ], + "switch": { + "di1": 1, + "di2": 0, + "di3": 1, + "di4": 0, + "di5": 1, + "di6": 0, + "di7": 1, + "di8": 0, + "di9": 1, + "di10": 0, + "di11": 1, + "di12": 0, + "do1": 1, + "do2": 0, + "do3": 1, + "do4": 0, + "do5": 1, + "do6": 0, + "do7": 1, + "do8": 0, + "do9": 1, + "do10": 0, + "do11": 1, + "do12": 0 + }, + "ai_collect": { + "ai1": 4.52, + "ai2": 3.86, + "ai3": 5.21, + "ai4": 2.99, + "ai5": 6.10, + "ai6": 1.75, + "ai7": 3.22, + "ai8": 4.91, + "ai9": 2.18, + "ai10": 5.47, + "ai11": 3.06, + "ai12": 4.33 + } + } +} +``` + +#### 2. WebSocket 地址:`ws://ip:port/ws/alarm` +**推送内容**:实时报警事件(触发即推) +```json +{ + "alarm_type": "line_alarm", + "time": "2025-01-01 12:00:00", + "no": "1", + "type": "", + "content": "告警内容", + "level": "告警等级" +}, +{ + "alarm_type": "ai_alarm", + "time": "2025-01-01 12:00:00", + "no": "2", + "type": "", + "content": "告警内容", + "level": "告警等级" +}, +{ + "alarm_type": "operate_alarm", + "time": "2025-01-01 12:00:00", + "no": "3", + "type": "", + "content": "告警内容", + "level": "告警等级" +} +``` + +## 五、数据存储设计 +### 5.1 JSON配置文件 +- 路径:`/config/` +- 文件:`device.json`、`channel.json`、`setting.json` + +### 5.2 SQLite报警数据库 +表:`alarm_event` +| 字段 | 类型 | 说明 | +|------|------|------| +| id | INTEGER | 主键 | +| alarm_type | TEXT | 告警类型(line_alarm,ai_alarm,operate_alarm) | +| time | DATETIME | 事件发生时间 | +| no | TEXT | 线路号/通道号 | +| type | TEXT | 类型 | +| content | TEXT | 告警详情 | +| level | TEXT | 告警等级 | + +## 六、安全与可靠性设计 +1. **密码加密存储**,接口权限校验 +2. WebSocket断线自动重连 +3. 配置文件修改自动备份 +4. 服务异常自动重启 + +## 七、总结 +本方案基于**RK3568平台**,采用**FastAPI + HTTP + WebSocket**架构,满足电气量测控平台全部需求: +- RESTful API 负责配置、查询、控制 +- WebSocket 负责实时数据、报警数据主动推送 +- JSON 存储配置,SQLite 存储报警 +- 0.5秒实时刷新,界面无卡顿、数据不丢失 + +系统架构轻量化、高性能,完全适配嵌入式平台运行。 + +--- + diff --git a/document/电气量测控平台系统需求分析及技术框架设计.pdf b/document/电气量测控平台系统需求分析及技术框架设计.pdf new file mode 100644 index 0000000..261a02c Binary files /dev/null and b/document/电气量测控平台系统需求分析及技术框架设计.pdf differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9984d21 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 电气量测控平台 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..b782924 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1832 @@ +{ + "name": "emcp-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "emcp-frontend", + "version": "0.1.0", + "dependencies": { + "axios": "^1.7.2", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.0.5", + "vue-tsc": "^2.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..127956c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "emcp-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.2", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.0.5", + "vue-tsc": "^2.2.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..ff44e59 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,54 @@ + + + diff --git a/frontend/src/api/http.ts b/frontend/src/api/http.ts new file mode 100644 index 0000000..0784a2c --- /dev/null +++ b/frontend/src/api/http.ts @@ -0,0 +1,10 @@ +import axios from 'axios' + +export const http = axios.create({ + baseURL: 'http://127.0.0.1:8000/api', + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + 'X-API-Token': 'admin123', + }, +}) diff --git a/frontend/src/api/platform.ts b/frontend/src/api/platform.ts new file mode 100644 index 0000000..5cef58a --- /dev/null +++ b/frontend/src/api/platform.ts @@ -0,0 +1,71 @@ +import { http } from './http' +import type { + AiAlarmSettingItem, + AlarmEvent, + ApiResponse, + ChannelConfigPayload, + DeviceConfigPayload, + DeviceStatus, + LineAlarmSettingPayload, + RealtimeData, + SystemConfigPayload, +} from '../types/platform' + +export async function fetchRealtimeData(): Promise { + const response = await http.get>('/real-time-data') + return response.data.data +} + +export async function fetchDeviceStatus(): Promise { + const response = await http.get>('/device-status') + return response.data.data +} + +export async function fetchAlarmHistory(page = 1, size = 20): Promise { + const response = await http.get>(`/alarm/list?page=${page}&size=${size}`) + return response.data.data +} + +export async function verifyAccessPassword(password: string): Promise { + const response = await http.post>('/auth/verify-password', { password }) + return response.data.data +} + +export async function saveDeviceConfig(payload: DeviceConfigPayload): Promise> { + const response = await http.post>('/config/device', payload) + return response.data +} + +export async function saveChannelConfig( + payload: ChannelConfigPayload, +): Promise> { + const response = await http.post>('/config/channel', payload) + return response.data +} + +export async function saveLineAlarmSetting( + payload: LineAlarmSettingPayload, +): Promise> { + const response = await http.post>('/config/line_alarm_setting', payload) + return response.data +} + +export async function saveAiAlarmSetting( + payload: AiAlarmSettingItem[], +): Promise> { + const response = await http.post>( + '/config/ai_alarm_setting', + payload, + ) + return response.data +} + +export async function controlSwitch(ch: number, action: number): Promise> { + const response = await http.post>('/control/switch', { ch, action }) + return response.data +} + +export async function saveSystemConfig(payload: SystemConfigPayload): Promise> { + const response = await http.post>('/config/system', payload) + return response.data +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..fe5bae3 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './style.css' + +createApp(App).mount('#app') diff --git a/frontend/src/stores/platform.ts b/frontend/src/stores/platform.ts new file mode 100644 index 0000000..e2be739 --- /dev/null +++ b/frontend/src/stores/platform.ts @@ -0,0 +1,142 @@ +import { reactive } from 'vue' +import { fetchAlarmHistory, fetchDeviceStatus, fetchRealtimeData } from '../api/platform' +import { connectAlarm, connectRealtimeWithLifecycle } from '../websocket/client' +import type { AlarmEvent, DeviceStatus, RealtimeData } from '../types/platform' + +interface PlatformState { + realtime: RealtimeData | null + status: DeviceStatus | null + alarms: AlarmEvent[] + alarmPage: number + alarmPageSize: number + alarmHasNext: boolean + alarmLoading: boolean + realtimeConnected: boolean + realtimeSource: string + realtimeUpdatedAt: string + statusLoading: boolean + statusUpdatedAt: string + alarmUpdatedAt: string +} + +const state = reactive({ + realtime: null, + status: null, + alarms: [], + alarmPage: 1, + alarmPageSize: 20, + alarmHasNext: false, + alarmLoading: false, + realtimeConnected: false, + realtimeSource: 'init', + realtimeUpdatedAt: '', + statusLoading: false, + statusUpdatedAt: '', + alarmUpdatedAt: '', +}) + +let realtimeSocket: WebSocket | null = null +let alarmSocket: WebSocket | null = null +let realtimePollTimer: number | null = null + +function markRealtime(data: RealtimeData, source: string) { + state.realtime = data + state.realtimeSource = source + state.realtimeUpdatedAt = new Date().toLocaleTimeString() +} + +async function syncRealtimeByHttp(source = 'http-poll') { + const realtime = await fetchRealtimeData() + markRealtime(realtime, source) +} + +async function refreshStatus() { + state.statusLoading = true + try { + state.status = await fetchDeviceStatus() + state.statusUpdatedAt = new Date().toLocaleTimeString() + } finally { + state.statusLoading = false + } +} + +async function refreshAlarms(page = state.alarmPage) { + state.alarmLoading = true + try { + const alarms = await fetchAlarmHistory(page, state.alarmPageSize) + state.alarms = alarms + state.alarmPage = page + state.alarmHasNext = alarms.length === state.alarmPageSize + state.alarmUpdatedAt = new Date().toLocaleTimeString() + } finally { + state.alarmLoading = false + } +} + +async function nextAlarmPage() { + if (!state.alarmHasNext) { + return + } + await refreshAlarms(state.alarmPage + 1) +} + +async function prevAlarmPage() { + if (state.alarmPage <= 1) { + return + } + await refreshAlarms(state.alarmPage - 1) +} + +async function bootstrap() { + await syncRealtimeByHttp('http-init') + await refreshStatus() + await refreshAlarms(1) + + if (!realtimeSocket) { + realtimeSocket = connectRealtimeWithLifecycle( + (data) => { + state.realtimeConnected = true + markRealtime(data, 'websocket') + }, + { + onOpen: () => { + state.realtimeConnected = true + }, + onClose: () => { + state.realtimeConnected = false + }, + onError: () => { + state.realtimeConnected = false + }, + }, + ) + } + + if (!alarmSocket) { + alarmSocket = connectAlarm((alarm) => { + if (state.alarmPage === 1) { + state.alarms = [alarm, ...state.alarms].slice(0, state.alarmPageSize) + state.alarmUpdatedAt = new Date().toLocaleTimeString() + } + }) + } + + if (!realtimePollTimer) { + realtimePollTimer = window.setInterval(() => { + if (!state.realtimeConnected) { + void syncRealtimeByHttp('http-fallback') + } + }, 500) + } +} + +export function usePlatformStore() { + return { + state, + bootstrap, + refreshStatus, + refreshAlarms, + nextAlarmPage, + prevAlarmPage, + } +} diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..9c01e45 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,173 @@ +:root { + font-family: Arial, Helvetica, sans-serif; + color: #0f172a; + background: #f8fafc; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + +button, +input, +select { + font: inherit; +} + +.app-shell { + min-height: 100vh; + padding: 24px; +} + +.page-header { + margin-bottom: 16px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + gap: 16px; +} + +.panel { + background: #ffffff; + border: 1px solid #dbe3ef; + border-radius: 12px; + padding: 16px; + box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} + +.tabs button { + border: 1px solid #cbd5e1; + background: #ffffff; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; +} + +.tabs button.active { + background: #2563eb; + color: #ffffff; + border-color: #2563eb; +} + +.metrics { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; +} + +.metric { + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 10px 12px; + background: #f8fafc; +} + +.metric .label { + color: #475569; + font-size: 12px; +} + +.metric .value { + font-size: 18px; + font-weight: 700; + margin-top: 4px; +} + +.status-ok { + color: #15803d; +} + +.status-warn { + color: #b45309; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} + +.form-grid label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 14px; +} + +.form-grid input, +.form-grid select { + padding: 8px 10px; + border: 1px solid #cbd5e1; + border-radius: 8px; + background: #fff; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + border-bottom: 1px solid #e2e8f0; + padding: 8px 4px; + text-align: left; + font-size: 14px; + vertical-align: top; +} + +.table input, +.table select { + width: 100%; + padding: 6px 8px; + border: 1px solid #cbd5e1; + border-radius: 8px; + background: #fff; +} + +.actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.primary { + border: none; + background: #2563eb; + color: #fff; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; +} + +.primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.hint { + color: #475569; + font-size: 13px; + margin-top: 12px; +} diff --git a/frontend/src/types/platform.ts b/frontend/src/types/platform.ts new file mode 100644 index 0000000..4602320 --- /dev/null +++ b/frontend/src/types/platform.ts @@ -0,0 +1,127 @@ +export interface ValueGroup { + Ua: number + Ub: number + Uc: number + Ia: number + Ib: number + Ic: number + Pt: number + frq: number +} + +export interface LineData { + line_no: number + pri_val: ValueGroup + sec_val: ValueGroup +} + +export interface RealtimeData { + line_list: LineData[] + switch: Record + ai_collect: Record +} + +export interface DeviceStatus { + self_check: string + net1: string + net2: string + uart1: string + uart2: string +} + +export interface AlarmEvent { + id?: number + alarm_type: string + time: string + no: string + type: string + content: string + level: string +} + +export interface ApiResponse { + code: number + msg: string + data: T +} + +export interface HardwareVersion { + board_version: string + display_version: string + other_version: string +} + +export interface SoftwareVersion { + display_program: string + communication_program: string + measurement_program: string +} + +export interface NetConfigItem { + nic: string + ip: string + mask: string + gateway: string + protocol: string +} + +export interface UartConfigItem { + port: string + baud: number + parity: string + data_bits: number + stop_bits: number + protocol: string +} + +export interface DeviceConfigPayload { + password: string + hardware_version: HardwareVersion + software_version: SoftwareVersion + net: NetConfigItem[] + uart: UartConfigItem[] +} + +export interface ChannelItem { + ch: number + singal_type: string + line_no: number + type: string + limit_low: number + limit_high: number +} + +export interface ChannelConfigPayload { + ai_channel: ChannelItem[] + ao_channel: ChannelItem[] +} + +export interface AlarmRule { + category: string + limit?: number + delay: number + output_node: string + enabled: boolean +} + +export interface LineAlarmSettingPayload { + line_no: number + over_limit_alarm: AlarmRule[] + fault_alarm: AlarmRule[] +} + +export interface AiAlarmSettingItem { + channel_no: number + singal_type: string + limit_low: number + limit_high: number + delay: number + output_node: string + enabled: boolean +} + +export interface SystemConfigPayload { + time_sync: string + brightness: number + screen_saver: number +} diff --git a/frontend/src/utils/saveGuard.ts b/frontend/src/utils/saveGuard.ts new file mode 100644 index 0000000..bdf8bb6 --- /dev/null +++ b/frontend/src/utils/saveGuard.ts @@ -0,0 +1,20 @@ +import { verifyAccessPassword } from '../api/platform' + +export async function ensureSaveAuthorized(): Promise<{ ok: boolean; message: string }> { + const password = window.prompt('请输入安全校验密码') + + if (password === null) { + return { ok: false, message: '已取消安全校验' } + } + + if (!password.trim()) { + return { ok: false, message: '安全校验密码不能为空' } + } + + const verified = await verifyAccessPassword(password) + if (!verified) { + return { ok: false, message: '安全校验失败,密码错误' } + } + + return { ok: true, message: '安全校验通过' } +} diff --git a/frontend/src/views/AlarmHistoryView.vue b/frontend/src/views/AlarmHistoryView.vue new file mode 100644 index 0000000..b936793 --- /dev/null +++ b/frontend/src/views/AlarmHistoryView.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/views/AlarmSettingView.vue b/frontend/src/views/AlarmSettingView.vue new file mode 100644 index 0000000..a586794 --- /dev/null +++ b/frontend/src/views/AlarmSettingView.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/views/ChannelConfigView.vue b/frontend/src/views/ChannelConfigView.vue new file mode 100644 index 0000000..832abcd --- /dev/null +++ b/frontend/src/views/ChannelConfigView.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/views/ControlView.vue b/frontend/src/views/ControlView.vue new file mode 100644 index 0000000..811f2bb --- /dev/null +++ b/frontend/src/views/ControlView.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/views/DeviceConfigView.vue b/frontend/src/views/DeviceConfigView.vue new file mode 100644 index 0000000..017895d --- /dev/null +++ b/frontend/src/views/DeviceConfigView.vue @@ -0,0 +1,78 @@ + + + diff --git a/frontend/src/views/RealtimeView.vue b/frontend/src/views/RealtimeView.vue new file mode 100644 index 0000000..f54555f --- /dev/null +++ b/frontend/src/views/RealtimeView.vue @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/views/StatusView.vue b/frontend/src/views/StatusView.vue new file mode 100644 index 0000000..8fa3f38 --- /dev/null +++ b/frontend/src/views/StatusView.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/views/SystemConfigView.vue b/frontend/src/views/SystemConfigView.vue new file mode 100644 index 0000000..6785909 --- /dev/null +++ b/frontend/src/views/SystemConfigView.vue @@ -0,0 +1,38 @@ + + + diff --git a/frontend/src/websocket/client.ts b/frontend/src/websocket/client.ts new file mode 100644 index 0000000..2b38514 --- /dev/null +++ b/frontend/src/websocket/client.ts @@ -0,0 +1,37 @@ +import type { AlarmEvent, RealtimeData } from '../types/platform' + +function buildWsUrl(path: string): string { + return `ws://127.0.0.1:8000${path}` +} + +export function connectRealtime(onMessage: (data: RealtimeData) => void): WebSocket { + const socket = new WebSocket(buildWsUrl('/ws/real-time')) + socket.onmessage = (event) => { + const payload = JSON.parse(event.data) + onMessage(payload.data) + } + return socket +} + +export function connectRealtimeWithLifecycle( + onMessage: (data: RealtimeData) => void, + handlers: { + onOpen?: () => void + onClose?: () => void + onError?: () => void + }, +): WebSocket { + const socket = connectRealtime(onMessage) + socket.onopen = () => handlers.onOpen?.() + socket.onclose = () => handlers.onClose?.() + socket.onerror = () => handlers.onError?.() + return socket +} + +export function connectAlarm(onMessage: (data: AlarmEvent) => void): WebSocket { + const socket = new WebSocket(buildWsUrl('/ws/alarm')) + socket.onmessage = (event) => { + onMessage(JSON.parse(event.data)) + } + return socket +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..79c91b5 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ES2020", "DOM"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..6db2aa6 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 5173, + }, +})