diff --git a/.env.local b/.env.local deleted file mode 100644 index 0196a4f..0000000 --- a/.env.local +++ /dev/null @@ -1,6 +0,0 @@ -# 前端构建配置(已 gitignore,勿提交) -# 前端 MQTT 账号:dpm / 123456 -VITE_API_BASE=http://192.168.1.9:10085 -VITE_MQTT_URL=ws://192.168.1.3:8083/mqtt -VITE_MQTT_USERNAME=dpm -VITE_MQTT_PASSWORD=123456 diff --git a/.gitignore b/.gitignore index 7d6b6db..ab253af 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,15 @@ dist-ssr # 后端密钥 / 前端本地配置(含阿里云密钥、MQTT 凭据,不入库) backend/.env +.env.local + +# Windows 安装包产物(可随时由 source + tauri build 重新生成,不入库) +*.exe + +# 误放入的 @appica 前端组件文档(非项目源码) +backend/installation.md +backend/autocomplete.md +backend/icons.md # 后端运行时数据(媒体上传、持久化配置) backend/media/ diff --git a/AGENTS.md b/AGENTS.md index 7240b70..a80f90d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # 工作规则(永远遵守) +> 📍 工作区位置:`code/dpm/`(园区大屏系统,独立仓库)。整体目录规范见根 `CLAUDE.md`;进程管理铁律适用于本仓库全部前后端服务。 + ## 进程管理铁律 - **绝对禁止 AI 启动、重启、停止任何前后端服务进程**(后端 10085 / s2s 8765 / vite 1420 / Tauri 等)。 - 需要启动或重启时,**必须先通知用户**,由用户亲自操作。 diff --git a/README.md b/README.md index 29016bd..7902103 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,93 @@ # 大屏媒体轮播系统 (DPM) -**Digital Playback Media** — 基于 Tauri + React 的全屏媒体轮播控制系统,专为昆明市大学生创业园展播大屏设计。 +> 📍 工作区位置:`code/dpm/`(独立仓库)。系统/运维文档见 `design/dpm/`;权威后端说明见 `backend/README.md`;整体目录规范见根 `CLAUDE.md`。 -## 概述 +**Digital Playback Media** — 园区大屏展播控制系统,服务昆明市大学生创业园:数据大屏、数字孪生 3D、AI 助手、语音助手、媒体轮播、企业展示墙,浏览器后台可远程控制。 -DPM 是一套完整的数字标牌(Digital Signage)解决方案,包含两大界面: +## 架构总览 -- **大屏展示页** (`/screen`) — 全屏自动轮播,支持视频与图片,具备沉浸式空间视觉背景 -- **管理后台** (`/admin`) — 登录保护的管理面板,用于媒体上传、播放列表编排、播放控制与系统设置 +``` +┌──────────────┐ REST(全部数据/媒体/设置) ┌────────────────┐ +│ Tauri 大屏 │ ◄────────────────────────── │ FastAPI :10085 │ +│ (React 前端) │ └───────┬────────┘ +│ │ MQTT 订阅 opc/display/command │ MQTT 发布 +│ 账号: dpm │ ◄────────────────────────────────────│ 账号: dpmserver +└──────┬───────┘ MQTT Broker (EMQX) │ + │ 192.168.1.3:1883 │ + └───────────────────────────────────────────────┘ +``` -系统通过 **SSE(Server-Sent Events)** 实现管理端与展示端的实时状态同步,支持局域网内远程管理。 +- **数据 / 媒体 / 设置** → REST API(FastAPI,JSON 文件持久化) +- **页面与媒体控制** → MQTT(EMQX broker),按 `clientId` 定向下发;SSE `/api/events` 作回退 +- **桌面壳** → Tauri 2(Rust),仅负责窗口生命周期与 3 个命令(读运行配置、双屏、安装更新) +- **管理后台** → FastAPI + Jinja 渲染的 `/admin` 页面(非 React 路由) -## 技术栈 +> **权威说明以 `backend/README.md` 为准**,含全部接口、MQTT 主题、环境变量与 Docker 部署。 -| 层级 | 技术 | +## 目录 + +| 路径 | 说明 | |------|------| -| 前端框架 | React 19 + React Router 7 | -| 构建工具 | Vite 7 | -| 桌面框架 | Tauri 2 (Rust) | -| HTTP 服务 | Axum 0.8 (嵌入式 HTTP 服务器) | -| 数据持久化 | JSON 文件存储 | -| 实时通信 | Server-Sent Events (SSE) | -| 内网穿透 | nps 客户端嵌入式集成 | - -## 功能特性 - -### 媒体管理 -- **本地上传** — 支持 .mp4、.mkv、.avi、.jpg、.jpeg、.png 格式 -- **远程 URL** — 添加网络媒体资源链接 -- **媒体预览** — 图片与视频的模态框预览 -- **文件删除** — 从媒体库移除文件 - -### 播放控制 -- **播放/暂停** — 实时控制展示端的播放状态 -- **上一曲/下一曲** — 切换播放内容 -- **顺序/随机播放** — 两种播放模式 -- **图片时长可调** — 1~300 秒 -- **音量控制** — 实时同步调节 - -### 展示端体验 -- **沉浸式空间背景** — 深邃星空背景 + 透视网格 + 动态环境光晕 -- **自动播放** — 启动后自动加载播放列表 -- **视频/图片自适应** — 全屏覆盖显示 -- **声音管理** — 浏览器自动播放策略处理 -- **点击暂停/恢复** — 现场快速控制 - -### 系统功能 -- **全屏/窗口模式切换** — 自由切换展示形态 -- **最小化窗口** — 后台运行 -- **开机自启动** — 系统级自启(Tauri autostart 插件) -- **关闭 → 最小化** — 防止误关闭 -- **内网穿透** — 内置 nps 客户端,支持外网远程访问 - -### 管理后台 -- **登录保护** — 账号密码验证 -- **实时状态显示** — 当前播放内容、状态 -- **上传进度条** — 文件上传实时反馈 -- **响应式设计** — 桌面/移动端自适应 -- **Toast 通知** — 操作即时反馈 +| `backend/` | **后端**:FastAPI(uv 管理),`:10085`;`app/` 内含全部 REST、MQTT、模拟引擎、AI/语音/RAG、手势识别。 | +| `src/` | **前端**:React 19 + Vite + Tailwind + three.js + MQTT + Tauri API。路由 `/`(数据)、`/twin`(孪生)、`/ai`、`/screen`(媒体)、`/voice`、`/wall`。 | +| `src-tauri/` | Tauri 2 桌面壳(Rust)。 | +| `docs/` | 部署 / 更新 / 语音 / MQTT 手册 / ESP32。 | +| `public/` | 静态资源。 | +| `数据资料/` | 园区本地数据资料(不入库)。 | ## 快速开始 -### 环境要求 - -- **Rust** (edition 2021) -- **Node.js** >= 18 -- **Yarn** 或 npm -- **Tauri CLI** (`cargo install tauri-cli`) - -### 安装 +### 前端(vite) ```bash -# 安装前端依赖 -yarn install - -# 开发模式运行(同时启动 Vite 开发服务器和 Tauri 应用) -yarn tauri dev - -# 仅启动 Web 开发服务器(浏览器中预览) -yarn dev +yarn dev # Vite 开发服务器 :1420 +yarn build # 构建 dist/ ``` -### 构建生产版本 +### 后端(uv) ```bash -yarn tauri build +cd backend +uv sync +cp .env.example .env # 按需填 DashScope Key、MQTT 账号 +uv run python main.py # 监听 0.0.0.0:10085 ``` -构建产物位于 `src-tauri/target/release/` 目录。 +### Docker 部署(后端 + EMQX,推荐) -## 项目结构 +```bash +export MQTT_PUBLIC_HOST=192.168.1.50 # 展播端可访问的服务器局域网 IP +export DASHSCOPE_API_KEY=sk-xxx # 未配置时 AI 走本地规则引擎 +docker compose up -d --build +curl http://<服务器IP>:10085/api/health +``` + +端口:`10085`(API/后台/媒体)、`8765`(s2s 语音)、`1883/8083/18083`(EMQX)。数据卷 `dpm_media` / `dpm_knowledge` / `dpm_data`。 + +## 关键约定 + +- **进程管理**:见 `AGENTS.md` —— AI 禁止启动/重启/停止任何服务(后端 :10085 / s2s :8765 / vite :1420 / Tauri)。 +- **凭据**:`backend/.env` 与根 `.env.local`(`VITE_*` 前缀)含阿里云密钥与 MQTT 凭据,不入库。 +- **前端连接配置**:`.env.local` 中 `VITE_API_BASE` / `VITE_MQTT_URL` / `VITE_MQTT_USERNAME` / `VITE_MQTT_PASSWORD`;运行时优先级:exe 旁 `config.json` > 后端 `/api/config` > 内置默认 IP。 + +## 项目结构(源码) ``` dpm/ -├── index.html # HTML 入口 -├── package.json # 前端依赖配置 -├── vite.config.js # Vite 构建配置(API 代理到 :10801) -├── public/ -│ └── npc.zip # 内网穿透客户端(嵌入二进制) -├── src/ # 前端源代码 -│ ├── main.jsx # React 入口 -│ ├── App.jsx # 路由配置(/admin, /screen) -│ ├── pages/ -│ │ ├── Admin.jsx # 管理后台页面 -│ │ └── Screen.jsx # 大屏展示页面 -│ ├── utils/ -│ │ └── api.js # API 封装(REST + SSE) -│ └── styles/ -│ ├── tokens.css # CSS 基础重置 -│ ├── admin.css # 管理后台样式 -│ └── screen.css # 大屏展示样式 -└── src-tauri/ # Rust 后端源代码 - ├── Cargo.toml # Rust 依赖配置 - ├── tauri.conf.json # Tauri 应用配置 - ├── capabilities/ - │ └── default.json # Tauri 权限配置 - └── src/ - ├── main.rs # 程序入口 - ├── lib.rs # 模块导出 + 启动逻辑 - ├── server.rs # Axum HTTP 服务器(路由 + 处理器) - ├── models.rs # 数据模型(Settings, PlaybackState, etc.) - ├── storage.rs # JSON 文件持久化存储 - └── events.rs # SSE 事件广播管理器 +├── package.json / vite.config.js +├── src/ # 前端(React 19) +│ ├── main.jsx # 入口:读 Tauri 运行配置 + /api/config +│ ├── App.jsx # 路由(/ /twin /ai /screen /voice /wall) +│ ├── pages/ # 六大屏页面 +│ ├── components/ # Building3D、AiChatPanel、GlobalVision 等 +│ ├── voice/ # 实时语音栈(s2s-ws-client、worklets) +│ └── utils/ # mqtt、useMqttControl、updateCheck、statusBus +├── backend/ +│ ├── main.py # 启动入口(prepare_env → uvicorn) +│ └── app/ # config/routers/mqtt/sim_engine/storage/llm/... +└── src-tauri/ # Tauri 2(Rust) ``` -## API 参考 - -系统内置 HTTP 服务器监听 `0.0.0.0:10801`,提供以下 REST API: - -| 方法 | 路径 | 说明 | -|------|------|------| -| POST | `/api/login` | 管理员登录 | -| GET | `/api/settings` | 获取系统设置 | -| POST | `/api/settings` | 更新系统设置 | -| GET | `/api/playlist` | 获取播放列表(含音量/模式/时长) | -| POST | `/api/playlist/add` | 添加到播放列表 | -| POST | `/api/playlist/remove` | 从播放列表移除 | -| GET | `/media` | 获取所有媒体文件列表 | -| POST | `/upload` | 上传媒体文件(multipart) | -| POST | `/api/media/add-url` | 添加 URL 媒体 | -| POST | `/api/delete` | 删除媒体文件 | -| POST | `/api/control` | 播放控制(play/pause/next/prev) | -| GET | `/api/state` | 获取当前播放状态 | -| POST | `/api/state` | 更新播放状态 | -| GET | `/api/events` | SSE 实时事件流 | -| POST | `/api/display-command` | 显示命令(minimize 等) | -| GET | `/file/*` | 静态媒体文件服务 | - -### 默认登录凭据 - -- **账号**: `admin` -- **密码**: `**********` - -> 可通过编辑系统数据目录下的 `dpm/data.json` 修改。 - -### 数据存储位置 - -系统数据存储在操作系统标准数据目录下的 `dpm/` 文件夹中: - -- **macOS**: `~/Library/Application Support/dpm/data.json` -- **Linux**: `~/.local/share/dpm/data.json` -- **Windows**: `C:\Users\<用户>\AppData\Roaming\dpm/data.json` - -媒体文件存储在用户 `~/Downloads/Media/` 目录。 - -## 内网穿透 - -系统内置 [nps](https://github.com/ehang-io/nps) 客户端,自动从嵌入的 `public/npc.zip` 解压并启动,实现外网远程访问管理后台。 - -- 启动参数在 `server.rs` 中配置 -- *仅支持 Windows* -- 自动设置 Unix 可执行权限 -- 解压到系统数据目录 `dpm/npc/` - -## 开发说明 - -### 端口 - -| 端口 | 用途 | -|------|------| -| 10801 | 后端 HTTP API 服务器 | -| 1420 | Vite 前端开发服务器 | - -### Vite 开发代理 - -开发模式下,Vite 自动将 `/api`、`/file`、`/media`、`/upload` 路径代理到 `localhost:10801`,前端无需关心跨域问题。 - -### 构建 - -前端构建产物(`dist/`)通过 `RustEmbed` 编译到 Rust 二进制中,发布时只需分发单个可执行文件。 - ## 许可 -本项目为昆明市大学生创业园公益定制开发。 - ---- - -*云南派音人工智能科技提供技术支持* +昆明市大学生创业园公益定制开发,*云南派音人工智能科技提供技术支持*。 diff --git a/backend/README.md b/backend/README.md index 1092b36..d7ea6e5 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,5 +1,7 @@ # OPC 智能园区后端(FastAPI + MQTT) +> 📍 工作区位置:`code/dpm/backend/`(DPM 园区大屏后端,独立仓库)。权威说明以本文档为准;系统/运维文档见 `design/dpm/`,前端见 `code/dpm/src/`。 + 独立后端服务:**所有数据通过 REST API 提供**,**页面/媒体控制通过 MQTT 下发**,媒体资源由后端统一存储与返回。 ## 架构 diff --git a/backend/app/config.py b/backend/app/config.py index ff4f8e7..311d08c 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -39,6 +39,10 @@ class Settings: DATA_FILE = Path(_env("DPM_DATA_FILE", str(BASE_DIR / "data.json"))) DIST_DIR = Path(_env("DPM_DIST_DIR", str(BASE_DIR.parent / "dist"))) # 前端构建产物(可选托管) + # ---- 视频帧保存(摄像头识别帧,仅检测到人脸/手势时落盘,便于排查) ---- + SAVE_VISION_FRAMES = _env("DPM_SAVE_VISION_FRAMES", "1") == "1" + VISION_FRAME_SAVE_DIR = Path(_env("DPM_VISION_SAVE_DIR", str(MEDIA_DIR / "video"))) + # ---- MQTT(优先使用 .env 中的局域网 Broker 配置) ---- MQTT_ENABLED = _env("DPM_MQTT_ENABLED", "1") == "1" MQTT_HOST = _env("DPM_MQTT_HOST", _env("MQTT_BROKER_HOST", "192.168.1.9")) diff --git a/backend/app/main.py b/backend/app/main.py index 07a3246..486e9a4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -14,7 +14,7 @@ from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel @@ -171,17 +171,7 @@ async def update_check(body: UpdateCheckBody): # ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ---------- @app.get("/{path:path}", include_in_schema=False) -async def spa_fallback(path: str): - if not DIST.exists(): - return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404) - dist = DIST.resolve() - if not path: - target = dist / "index.html" - else: - target = (dist / path).resolve() - if target.is_file() and target.is_relative_to(dist): - return FileResponse(target) - index = dist / "index.html" - if index.exists(): - return FileResponse(index) - return JSONResponse({"detail": "Not Found"}, status_code=404) +async def spa_fallback(request: Request): + """默认导向管理后台:屏蔽通过后端地址直接访问大屏前端(含首页 / 及 /twin /ai /screen 等)。 + /api、/admin、/static、/file 等已在更早路由/挂载处理,不会走到这里。""" + return RedirectResponse(url="/admin", status_code=307) diff --git a/backend/app/vision_yolo.py b/backend/app/vision_yolo.py index 51cec0b..a33d0b0 100644 --- a/backend/app/vision_yolo.py +++ b/backend/app/vision_yolo.py @@ -19,8 +19,12 @@ from pathlib import Path from PIL import Image +from .config import settings + log = logging.getLogger("dpm.vision") +_frame_seq = 0 + MODEL_DIR = Path(__file__).resolve().parent.parent / "models" FACE_MODEL_PATH = MODEL_DIR / "yolov8n-face.pt" POSE_MODEL_PATH = MODEL_DIR / "yolov8n-pose.pt" @@ -188,6 +192,40 @@ def predict_jpeg(jpeg_bytes: bytes): } +def _should_save(data: dict) -> bool: + """仅当画面检测到人脸/手势时才值得落盘保存。""" + return ( + int(data.get("faces") or 0) > 0 + or bool(data.get("raised")) + or bool(data.get("fists")) + or bool(data.get("both_up")) + or bool(data.get("pointing")) + or bool(data.get("hands_close")) + ) + + +def maybe_save_frame(jpeg: bytes, data: dict): + """仅将命中(有人/有动作)的 JPEG 帧落盘到 MEDIA_DIR/video/<小时目录>/,便于排查识别链路。 + 目录按小时分割(YYYYMMDD_HH),不自动清理。 + 可通过 DPM_SAVE_VISION_FRAMES=0 关闭,或 DPM_VISION_SAVE_DIR 改目录。""" + global _frame_seq + if not settings.SAVE_VISION_FRAMES: + return + if not _should_save(data): + return + _frame_seq += 1 + ts = time.strftime("%Y%m%d_%H%M%S") + hour = time.strftime("%Y%m%d_%H") + try: + save_dir = settings.VISION_FRAME_SAVE_DIR / hour + save_dir.mkdir(parents=True, exist_ok=True) + path = save_dir / f"{ts}_{_frame_seq:05d}.jpg" + with open(path, "wb") as f: + f.write(jpeg) + except Exception as e: # noqa: BLE001 + log.warning("vision: 保存帧失败 %s", e) + + def predict_base64(b64: str): """入口:base64 JPEG → 检测结果 dict(含 ok 标记,失败时带 error)""" try: @@ -198,6 +236,7 @@ def predict_base64(b64: str): try: data = predict_jpeg(jpeg) data["ok"] = True + maybe_save_frame(jpeg, data) return data except Exception as e: # noqa: BLE001 log.error("vision: 推理失败 %s", e, exc_info=True) diff --git a/backend/static/admin.css b/backend/static/admin.css index 4188d79..d04281d 100644 --- a/backend/static/admin.css +++ b/backend/static/admin.css @@ -1265,3 +1265,128 @@ button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; tra font-size: 13px; color: #1d2c44; outline: none; } .mqtt-input:focus { border-color: #2f6bff; } + +/* ============ 弱化连接状态(沉底、紧凑、低调) ============ */ +.conn-card { opacity: 0.88; } +.conn-card .card-header { padding-bottom: 6px; } +.conn-card .conn-status { display: flex; flex-wrap: wrap; gap: 6px 22px; } +.conn-card .conn-row { min-width: 150px; } +.conn-card .conn-label { font-size: 11px; } + +/* ============ 大屏控制:mqtt-group 按操作优先级排序 ============ */ +.mqtt-card { display: flex; flex-direction: column; } +.mqtt-card .mqtt-group { order: 50; } /* 未显式设置 order 的组默认靠后 */ + +/* ============ 手机适配(响应式) ============ */ +@media (max-width: 900px) { + .main-grid { grid-template-columns: 1fr; padding: 16px; gap: 16px; } +} +@media (max-width: 600px) { + .main-grid { padding: 10px; gap: 12px; } + .topbar { flex-wrap: wrap; row-gap: 8px; } + .topbar-center { order: 3; width: 100%; justify-content: flex-start; } + .page-switch-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; } + .mqtt-row { flex-wrap: wrap; gap: 8px; } + .page-switch-btn { flex: 1 1 45%; } + .form-inline { flex-direction: column; } + .form-row { flex-wrap: wrap; } + .conn-card .conn-status { flex-direction: column; gap: 6px; } + .card { padding: 16px; } +} + +/* ============ 遥控器式快捷控制条 ============ */ +.remote-bar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px 18px; + max-width: 1400px; + margin: 16px auto 0; + padding: 12px 16px; + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + box-shadow: var(--shadow-card); +} +.remote-group { display: flex; gap: 6px; } +.remote-btn { + width: 42px; height: 42px; + display: inline-flex; align-items: center; justify-content: center; + font-size: 24px; line-height: 1; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + background: #fff; color: var(--text); + cursor: pointer; + transition: var(--transition); +} +.remote-btn:hover { background: var(--primary-subtle); border-color: var(--primary); color: var(--primary); } +.remote-target { + display: flex; align-items: center; gap: 8px; + padding: 5px 12px; + background: var(--primary-subtle); + border: 1px solid rgba(0,189,125,.2); + border-radius: var(--radius-md); +} +.remote-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); white-space: nowrap; } +.remote-count { font-size: 11px; color: var(--text-dim); white-space: nowrap; } +.remote-target select { + min-width: 160px; + padding: 6px 10px; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + background: #fff; color: var(--text); font-size: 13px; +} +.remote-pages { display: flex; gap: 6px; flex-wrap: wrap; } + +@media (max-width: 600px) { + .remote-bar { margin: 10px; padding: 10px; } + .remote-target { flex: 1 1 100%; } + .remote-target select { flex: 1; min-width: 0; } +} + +/* 遥控条:页面切换合并模块 + 分隔线 */ +.remote-nav { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; } +.remote-sep { width: 1px; height: 26px; background: var(--border-default); margin: 0 4px; } +@media (max-width: 600px) { + .remote-nav { width: 100%; } + .remote-sep { display: none; } +} + +/* ============ 顶部控制模块(选择屏幕 / 页面切换,独立上下排列) ============ */ +.control-stack { + max-width: 1400px; + margin: 16px auto 0; + padding: 0 32px; + display: flex; + flex-direction: column; + gap: 24px; +} +.control-card { margin: 0; } +@media (max-width: 600px) { + .control-stack { padding: 0 10px; } +} + +/* 页面切换:左右箭头独立一行,与下方页面按钮分隔开 */ +.page-nav-prev { + display: flex; + gap: 8px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-subtle); +} +.page-nav-links { padding-top: 2px; } +@media (max-width: 600px) { + .page-nav-prev { justify-content: center; } + .page-nav-links { justify-content: flex-start; } +} + +/* 卡片图标:继承主题主色(appica color -> currentColor) */ +.card-header .card-icon { color: var(--primary); } + +/* 页面按钮:当前激活页高亮 */ +.page-switch-btn.active { + background: var(--primary); + border-color: var(--primary); + color: #fff; + box-shadow: 0 2px 8px rgba(0,189,125,0.35); +} diff --git a/backend/static/admin.js b/backend/static/admin.js index 5bd9e9f..f863ca4 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -14,6 +14,20 @@ var screenId = ''; var screenRole = ''; + /* 状态识别:点击某页面后,把该页操作模块前置到页面控制下方,并高亮当前页面按钮 */ + function activatePage(page) { + if (!page) return; + var cards = document.querySelectorAll('.main-grid .card[data-page]'); + for (var i = 0; i < cards.length; i++) cards[i].style.order = ''; + var active = null; + for (var j = 0; j < cards.length; j++) { + if (cards[j].getAttribute('data-page') === page) { active = cards[j]; break; } + } + if (active) active.style.order = -10; // 前置到 main-grid 顶部(页面控制下方) + var navs = document.querySelectorAll('[data-nav]'); + for (var k = 0; k < navs.length; k++) navs[k].classList.toggle('active', navs[k].getAttribute('data-nav') === page); + } + function applyScreenTarget(sel) { sel = sel || document.getElementById('screenTarget'); if (!sel) return; @@ -179,6 +193,8 @@ try { cmd = JSON.parse(btn.getAttribute('data-mqtt')); } catch (err) { toast('指令 JSON 错误', 'error'); return; } // 屏幕终端精准控制:除「双屏开关」外,注入目标 client_id + role;全空=全局广播 if (cmd.action !== 'dual_screen') { cmd.screen_id = screenId; cmd.screen_role = screenRole; } + // 状态识别:切页命令 → 前置对应页面操作模块 + if (cmd.action === 'navigate' && cmd.params && cmd.params.page) activatePage(cmd.params.page); var tgtTxt = screenId ? (cmd.screen_role === 'main' ? '主屏' : cmd.screen_role === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10) : '全部'; @@ -212,6 +228,7 @@ // 页面切换(绑定目标屏幕:screen_id 精确某台;空=全部) if (btn.hasAttribute('data-nav')) { var page = btn.getAttribute('data-nav'); + activatePage(page); // 状态识别:前置该页操作模块 var tgtTxt = screenId ? (screenRole === 'main' ? '主屏' : screenRole === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10) : '全部'; diff --git a/backend/templates/admin.html b/backend/templates/admin.html index b06f61a..9fee5e0 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -1,158 +1,246 @@ + - - -管理后台 · 昆明市大学生创业园 - + + + 管理后台 · 昆明市大学生创业园 + + -{% if not logged_in %} - -
-
-