提交 e4d15776 authored 作者: 陈泽健's avatar 陈泽健

docs(用例管理): 新增元素定位方案对比分析报告

feat(设备模拟): 多类型主题上报支持 + 前端设备列表增强

- 新增元素定位方案对比分析文档(录制器/智能定位/手动编写)
- 设备模拟:central_simulator多类型主题上报、topic_templates扩展
- 设备模拟:前端DeviceList组件增强、新增deviceSim API和类型定义
- 更新HANDOFF_设备模拟交接文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 7c13eeed
此差异已折叠。
...@@ -69,12 +69,13 @@ async def get_topic_templates(device_type: str): ...@@ -69,12 +69,13 @@ async def get_topic_templates(device_type: str):
templates=[ templates=[
TopicTemplateResponse( TopicTemplateResponse(
key=t["key"], key=t["key"],
template=t["template"], template=t.get("template", t.get("template_publish", "")),
label=t["label"], label=t["label"],
params=t["params"], params=t["params"],
param_labels=t["param_labels"], param_labels=t["param_labels"],
param_defaults=t["param_defaults"], param_defaults=t["param_defaults"],
direction=t["direction"], direction=t.get("direction", "publish"),
device_type=t.get("device_type"),
) )
for t in templates for t in templates
], ],
...@@ -271,9 +272,12 @@ async def download_import_template( ...@@ -271,9 +272,12 @@ async def download_import_template(
ws.title = "设备导入" ws.title = "设备导入"
# 表头 # 表头
headers = ["环境配置名称", "设备名称", "设备ID"] headers = ["环境配置名称", "设备名称", "设备编号"]
if device_type == "door": if device_type == "door":
headers.append("授权码(app_token)") headers.append("授权码(app_token)")
elif device_type == "central":
headers.append("会议室编号")
headers.append("主题类型")
headers.extend(["自动重连", "启用定时上报", "上报间隔(秒)"]) headers.extend(["自动重连", "启用定时上报", "上报间隔(秒)"])
ws.append(headers) ws.append(headers)
...@@ -281,6 +285,9 @@ async def download_import_template( ...@@ -281,6 +285,9 @@ async def download_import_template(
example = ["测试环境", f"{device_type}_001", f"{device_type}_device_001"] example = ["测试环境", f"{device_type}_001", f"{device_type}_device_001"]
if device_type == "door": if device_type == "door":
example.append("AUTH-0001") example.append("AUTH-0001")
elif device_type == "central":
example.append("A101")
example.append("设备在线")
example.extend(["是", "是", "30"]) example.extend(["是", "是", "30"])
ws.append(example) ws.append(example)
...@@ -316,8 +323,8 @@ async def import_devices( ...@@ -316,8 +323,8 @@ async def import_devices(
""" """
批量导入模拟设备 批量导入模拟设备
上传 Excel 文件批量创建设备。Excel 列:环境配置名称、设备名称、设备ID 上传 Excel 文件批量创建设备。Excel 列:环境配置名称、设备名称、设备编号
授权码(可选)、自动重连(可选)、启用定时上报(可选)、上报间隔(可选)。 授权码(可选,门口屏)、会议室编号(可选,中控)、自动重连(可选)、启用定时上报(可选)、上报间隔(可选)。
device_type 由前端页面决定,不包含在 Excel 中。 device_type 由前端页面决定,不包含在 Excel 中。
""" """
# 校验文件类型 # 校验文件类型
...@@ -433,6 +440,28 @@ async def update_simulator( ...@@ -433,6 +440,28 @@ async def update_simulator(
return SimulatorResponse.model_validate(device) return SimulatorResponse.model_validate(device)
@router.patch("/devices/{device_id}/type")
async def switch_device_type(
device_id: str,
device_type_name: str = Query(..., description="设备类型标识(room_online/device_online/audio/video/control/network/power)"),
service: DeviceSimService = Depends(get_device_sim_service),
):
"""
切换中控设备上报类型
支持在运行时切换中控设备的上报主题类型。
如果设备正在运行,会先停止再重启以应用新配置。
"""
try:
result = await service.switch_device_type(device_id, device_type_name)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"切换主题类型失败: {e}")
raise HTTPException(status_code=500, detail=f"切换主题类型失败: {str(e)}")
@router.delete("/devices/{device_id}") @router.delete("/devices/{device_id}")
async def delete_simulator( async def delete_simulator(
device_id: str, device_id: str,
......
...@@ -255,6 +255,7 @@ class TopicTemplateResponse(BaseModel): ...@@ -255,6 +255,7 @@ class TopicTemplateResponse(BaseModel):
param_labels: Dict[str, str] = Field(default={}, description="参数名 -> 中文标签映射") param_labels: Dict[str, str] = Field(default={}, description="参数名 -> 中文标签映射")
param_defaults: Dict[str, str] = Field(default={}, description="参数名 -> 默认值映射") param_defaults: Dict[str, str] = Field(default={}, description="参数名 -> 默认值映射")
direction: str = Field(..., description="方向:publish(上报) / subscribe(订阅)") direction: str = Field(..., description="方向:publish(上报) / subscribe(订阅)")
device_type: Optional[str] = Field(default=None, description="中控设备类型标识")
model_config = ConfigDict( model_config = ConfigDict(
alias_generator=to_camel, alias_generator=to_camel,
......
...@@ -478,15 +478,31 @@ class DeviceSimService: ...@@ -478,15 +478,31 @@ class DeviceSimService:
COLUMN_MAP = { COLUMN_MAP = {
"环境配置名称": "env_config_name", "环境配置名称": "env_config_name",
"设备名称": "device_name", "设备名称": "device_name",
"设备ID": "device_id",
"设备编号": "device_id",
"设备id": "device_id", "设备id": "device_id",
"授权码": "app_token", "授权码": "app_token",
"授权码(app_token)": "app_token", "授权码(app_token)": "app_token",
"会议室编号": "room_id",
"主题类型": "central_device_type",
"设备类型": "central_device_type", # 兼容旧版
"自动重连": "auto_reconnect", "自动重连": "auto_reconnect",
"启用定时上报": "report_enabled", "启用定时上报": "report_enabled",
"上报间隔(秒)": "report_interval", "上报间隔(秒)": "report_interval",
"上报间隔": "report_interval", "上报间隔": "report_interval",
} }
# 中控设备类型中文 → 标识映射
CENTRAL_DEVICE_TYPES = {
"会议室在线": "room_online",
"设备在线": "device_online",
"音频系统": "audio",
"视频系统": "video",
"控制系统": "control",
"网络系统": "network",
"电源系统": "power",
}
# 解析 Excel # 解析 Excel
wb = load_workbook(BytesIO(file_bytes), read_only=True) wb = load_workbook(BytesIO(file_bytes), read_only=True)
ws = wb.active ws = wb.active
...@@ -576,11 +592,32 @@ class DeviceSimService: ...@@ -576,11 +592,32 @@ class DeviceSimService:
report_enabled = _parse_bool(row_data.get("report_enabled"), True) report_enabled = _parse_bool(row_data.get("report_enabled"), True)
report_interval = _parse_int(row_data.get("report_interval"), 30) report_interval = _parse_int(row_data.get("report_interval"), 30)
# 构建 topic_params(授权码) # 构建 topic_params
topic_params = {} topic_params = {}
# 门口屏:授权码
app_token = row_data.get("app_token") app_token = row_data.get("app_token")
if app_token: if app_token:
topic_params["app_token"] = str(app_token).strip() topic_params["app_token"] = str(app_token).strip()
# 中控:会议室编号(用于主题和消息体)
room_id = row_data.get("room_id")
if room_id:
topic_params["room_id"] = str(room_id).strip()
# 中控:设备编号(用于消息体 client_udid)
# 设备编号 = device_id,存入 topic_params.device_number
if device_type == "central":
topic_params["device_number"] = str(row_data["device_id"]).strip()
# 中控:主题类型
central_device_type = row_data.get("central_device_type")
if central_device_type:
device_type_key = CENTRAL_DEVICE_TYPES.get(str(central_device_type).strip())
if device_type_key:
topic_params["device_type"] = device_type_key
else:
# 无效的主题类型,使用默认值
topic_params["device_type"] = "device_online"
elif device_type == "central":
# 中控设备但没有指定类型,默认为设备在线
topic_params["device_type"] = "device_online"
# 构建创建数据 # 构建创建数据
try: try:
...@@ -886,6 +923,58 @@ class DeviceSimService: ...@@ -886,6 +923,58 @@ class DeviceSimService:
return sim.report(topic, payload) return sim.report(topic, payload)
async def switch_device_type(self, device_id: str, device_type_name: str) -> dict:
"""
切换中控设备上报类型
Args:
device_id: 设备 ID
device_type_name: 设备类型标识(room_online/device_online/audio/video/control/network/power)
Returns:
dict: 切换结果
"""
# 校验主题类型
valid_types = ["room_online", "device_online", "audio", "video", "control", "network", "power"]
if device_type_name not in valid_types:
raise ValueError(f"无效的主题类型: {device_type_name},支持的类型: {', '.join(valid_types)}")
simulator = await self.get_simulator(device_id)
if not simulator:
raise ValueError(f"模拟设备不存在: {device_id}")
if simulator.device_type != "central":
raise ValueError("仅中控设备支持切换类型")
# 检查是否正在运行
was_running = False
with _running_simulators_lock:
sim = _running_simulators.get(device_id)
if sim and sim.is_running():
was_running = True
# 如果正在运行,先停止
if was_running:
await self.stop_simulator(device_id)
# 更新 topic_params
topic_params = dict(simulator.topic_params or {})
topic_params["device_type"] = device_type_name
simulator.topic_params = topic_params
simulator.updated_at = datetime.now()
await self.db.flush()
# 如果之前在运行,重新启动
if was_running:
await self.start_simulator(device_id)
return {
"device_id": device_id,
"device_type": device_type_name,
"was_running": was_running,
"message": f"设备类型已切换为 {device_type_name}" + (",设备已重启" if was_running else "")
}
# ==================== 批量操作 ==================== # ==================== 批量操作 ====================
async def batch_start_simulators(self, device_ids: List[str]) -> dict: async def batch_start_simulators(self, device_ids: List[str]) -> dict:
......
...@@ -95,10 +95,13 @@ class BaseSimulator(ABC): ...@@ -95,10 +95,13 @@ class BaseSimulator(ABC):
如果设备类型有真实主题模板(门口屏/无纸化),则使用真实主题; 如果设备类型有真实主题模板(门口屏/无纸化),则使用真实主题;
否则标记为无真实主题,使用旧格式 fallback。 否则标记为无真实主题,使用旧格式 fallback。
对于中控设备,根据 topic_params.device_type 选择对应的主题模板。
""" """
from app.simulators.topic_templates import ( from app.simulators.topic_templates import (
has_real_topics, has_real_topics,
resolve_all_topics, resolve_all_topics,
resolve_topics_by_device_type,
) )
self._has_real_topics = has_real_topics(self.device_type) self._has_real_topics = has_real_topics(self.device_type)
...@@ -108,6 +111,12 @@ class BaseSimulator(ABC): ...@@ -108,6 +111,12 @@ class BaseSimulator(ABC):
if 'device_id' not in params_with_device_id: if 'device_id' not in params_with_device_id:
params_with_device_id['device_id'] = self.device_id params_with_device_id['device_id'] = self.device_id
# 中控设备:根据 device_type 筛选对应主题
if self.device_type == "central" and "device_type" in self.topic_params:
self._resolved_topics = resolve_topics_by_device_type(
self.device_type, params_with_device_id
)
else:
self._resolved_topics = resolve_all_topics(self.device_type, params_with_device_id) self._resolved_topics = resolve_all_topics(self.device_type, params_with_device_id)
logger.info(f"设备 {self.device_id} 解析主题: {self._resolved_topics}") logger.info(f"设备 {self.device_id} 解析主题: {self._resolved_topics}")
......
...@@ -2,17 +2,18 @@ ...@@ -2,17 +2,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
模块名称:central_simulator.py 模块名称:central_simulator.py
模块描述:中控设备模拟器,模拟灯光/窗帘/投影等设备控制 模块描述:中控设备模拟器,支持 7 种主题类型上报
作者:czj 作者:czj
创建日期:2026-07-29 创建日期:2026-07-29
最后修改:2026-07-29 最后修改:2026-08-06
""" """
import json import json
import logging import logging
import random import random
import time import time
import uuid
from typing import Optional from typing import Optional
from app.simulators.base_simulator import BaseSimulator from app.simulators.base_simulator import BaseSimulator
...@@ -21,6 +22,18 @@ from app.services.mqtt_manager import MqttManager ...@@ -21,6 +22,18 @@ from app.services.mqtt_manager import MqttManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 中控设备类型映射
CENTRAL_DEVICE_TYPES = {
"room_online": "会议室在线",
"device_online": "设备在线",
"audio": "音频系统",
"video": "视频系统",
"control": "控制系统",
"network": "网络系统",
"power": "电源系统",
}
class CentralSimulator(BaseSimulator): class CentralSimulator(BaseSimulator):
""" """
中控设备模拟器 中控设备模拟器
...@@ -127,25 +140,198 @@ class CentralSimulator(BaseSimulator): ...@@ -127,25 +140,198 @@ class CentralSimulator(BaseSimulator):
} }
def build_status_payload(self) -> dict: def build_status_payload(self) -> dict:
"""构建设备状态上报消息""" """
sub_status = {} 根据设备类型动态选择消息体构建方法
for dev_id, config in self._sub_devices.items():
sub_status[dev_id] = { 支持的设备类型:
"status": config.get("status", "off"), - room_online: 会议室在线
"brightness": config.get("brightness"), - device_online: 设备在线
"position": config.get("position"), - audio: 音频系统
"temperature": config.get("temperature"), - video: 视频系统
"volume": config.get("volume"), - control: 控制系统
- network: 网络系统
- power: 电源系统
默认为 device_online(设备在线)。
"""
device_type = self.topic_params.get("device_type", "device_online")
builder_method = getattr(self, f"_build_{device_type}_payload", self._build_device_online_payload)
return builder_method()
def _build_room_online_payload(self) -> dict:
"""
构建会议室在线消息体
消息格式:
{
"udid": "uuid",
"action": "online",
"value": 1
}
"""
return {
"udid": str(uuid.uuid4()),
"action": "online",
"value": 1
} }
def _build_device_online_payload(self) -> dict:
"""
构建设备在线消息体
消息格式:
{
"action": "_updatestatus",
"client_udid": "{会议室编号}",
"data": [
{"device_udid": "{设备编号}", "power": 1, "online": 1, "watt": 10000, "run": "在线"},
{"device_udid": "{设备编号}", "power": 1, "online": 1, "watt": 1000, "run": "在线"}
]
}
"""
room_id = self.topic_params.get("room_id", self.device_id)
device_number = self.topic_params.get("device_number", self.device_id)
return { return {
"device_id": self.device_id, "action": "_updatestatus",
"status": "online", "client_udid": room_id,
"current_scene": self._current_scene, "data": [
"scene_name": self._SCENES.get(self._current_scene, {}).get("name", ""), {"device_udid": device_number, "power": 1, "online": 1, "watt": random.randint(5000, 15000), "run": "在线"},
"sub_device_count": len(self._sub_devices), {"device_udid": device_number, "power": 1, "online": 1, "watt": random.randint(500, 1500), "run": "在线"}
"sub_devices": sub_status, ]
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S") }
def _build_audio_payload(self) -> dict:
"""
构建音频系统消息体
消息格式:
{
"action": "_updateaudio",
"client_udid": "{设备编号}",
"data": [...]
}
"""
device_number = self.topic_params.get("device_number", self.device_id)
return {
"action": "_updateaudio",
"client_udid": device_number,
"data": [
{
"address": 1,
"data_all": {"volume": random.randint(20, 50), "mute": 0},
"data_channel": [
{"lineType": 1, "num": 1, "status": 1, "volume": random.randint(40, 80), "mute": 0},
{"lineType": 1, "num": 2, "status": 1, "volume": random.randint(40, 80), "mute": 0}
],
"data_meter": [{"lineType": 1, "num": 1, "dbvalue": random.randint(50, 90)}]
}
]
}
def _build_video_payload(self) -> dict:
"""
构建视频系统消息体
消息格式:
{
"action": "_updatevideo",
"client_udid": "{设备编号}",
"data": [...]
}
"""
device_number = self.topic_params.get("device_number", self.device_id)
return {
"action": "_updatevideo",
"client_udid": device_number,
"data": [
{
"address": 1,
"data_channel": [
{"lineType": 1, "num": 1, "status": random.randint(0, 1)},
{"lineType": 1, "num": 2, "status": random.randint(0, 1)}
]
}
]
}
def _build_control_payload(self) -> dict:
"""
构建控制系统消息体
消息格式:
{
"action": "_updatecontrol",
"client_udid": "{设备编号}",
"data": [...]
}
"""
device_number = self.topic_params.get("device_number", self.device_id)
return {
"action": "_updatecontrol",
"client_udid": device_number,
"data": [
{
"address": 1,
"data_com": [{"lineType": 0, "num": 1, "status": 1, "run": "run success"}],
"data_ir": [{"lineType": 1, "num": 1, "status": 1, "run": "run success"}],
"data_io": [{"lineType": 2, "num": 1, "status": 1, "run": "run success"}],
"data_rel": [{"lineType": 3, "num": 1, "status": 1, "run": "run success"}],
"data_bus": [{"lineType": 4, "num": 1, "status": 1, "run": "run success"}],
"data_net": [{"lineType": 5, "num": 1, "status": 1, "run": "run success"}]
}
]
}
def _build_network_payload(self) -> dict:
"""
构建网络系统消息体
消息格式:
{
"action": "_updatenetwork",
"client_udid": "{设备编号}",
"data": [...]
}
"""
device_number = self.topic_params.get("device_number", self.device_id)
return {
"action": "_updatenetwork",
"client_udid": device_number,
"data": [
{
"address": 1,
"data_channel": [
{"innum": 1, "status": 1, "run": "Good network"},
{"innum": 2, "status": 1, "run": "Good network"}
]
}
]
}
def _build_power_payload(self) -> dict:
"""
构建电源系统消息体
消息格式:
{
"action": "_updatepower",
"client_udid": "{设备编号}",
"data": [...]
}
"""
device_number = self.topic_params.get("device_number", self.device_id)
return {
"action": "_updatepower",
"client_udid": device_number,
"data": [
{
"address": 1,
"data_channel": [
{"innum": 1, "status": 1, "power": 1, "level": round(random.uniform(1.0, 2.0), 1)},
{"innum": 2, "status": 1, "power": 1, "level": round(random.uniform(1.0, 2.0), 1)}
]
}
]
} }
def _on_command(self, topic: str, payload: dict) -> None: def _on_command(self, topic: str, payload: dict) -> None:
......
...@@ -176,7 +176,85 @@ DEVICE_TOPIC_TEMPLATES: Dict[str, List[dict]] = { ...@@ -176,7 +176,85 @@ DEVICE_TOPIC_TEMPLATES: Dict[str, List[dict]] = {
"direction": "subscribe", "direction": "subscribe",
}, },
], ],
"central": [], # 暂无真实主题,保留旧格式 "central": [
# ========== 会议室在线(发布主题不同)==========
{
"key": "room_online",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/online/{room_id}/",
"label": "会议室在线",
"device_type": "room_online",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
# ========== 设备在线 ==========
{
"key": "device_online",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/master/client/",
"label": "设备在线",
"device_type": "device_online",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
# ========== 音频系统 ==========
{
"key": "audio",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/master/client/",
"label": "音频系统",
"device_type": "audio",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
# ========== 视频系统 ==========
{
"key": "video",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/master/client/",
"label": "视频系统",
"device_type": "video",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
# ========== 控制系统 ==========
{
"key": "control",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/master/client/",
"label": "控制系统",
"device_type": "control",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
# ========== 网络系统 ==========
{
"key": "network",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/master/client/",
"label": "网络系统",
"device_type": "network",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
# ========== 电源系统 ==========
{
"key": "power",
"template_subscribe": "/maintain/room/master/{room_id}/",
"template_publish": "/maintain/room/master/client/",
"label": "电源系统",
"device_type": "power",
"params": ["room_id"],
"param_labels": {"room_id": "会议室编号"},
"param_defaults": {"room_id": "A101"},
},
],
"client": [], # 暂无真实主题,保留旧格式 "client": [], # 暂无真实主题,保留旧格式
} }
...@@ -242,12 +320,78 @@ def resolve_all_topics(device_type: str, topic_params: dict) -> Dict[str, dict]: ...@@ -242,12 +320,78 @@ def resolve_all_topics(device_type: str, topic_params: dict) -> Dict[str, dict]:
templates = get_topic_templates(device_type) templates = get_topic_templates(device_type)
resolved = {} resolved = {}
for tmpl in templates: for tmpl in templates:
# 兼容新旧两种模板结构
if "template" in tmpl:
# 旧格式:单个 template + direction
resolved[tmpl["key"]] = { resolved[tmpl["key"]] = {
"topic": resolve_topic(tmpl["template"], topic_params or {}), "topic": resolve_topic(tmpl["template"], topic_params or {}),
"direction": tmpl["direction"], "direction": tmpl["direction"],
"label": tmpl["label"], "label": tmpl["label"],
"template": tmpl["template"], "template": tmpl["template"],
} }
else:
# 新格式:template_subscribe + template_publish(中控设备)
# 订阅主题
if "template_subscribe" in tmpl:
resolved[tmpl["key"] + "_sub"] = {
"topic": resolve_topic(tmpl["template_subscribe"], topic_params or {}),
"direction": "subscribe",
"label": tmpl["label"],
"template": tmpl["template_subscribe"],
"device_type": tmpl.get("device_type"),
}
# 发布主题
if "template_publish" in tmpl:
resolved[tmpl["key"]] = {
"topic": resolve_topic(tmpl["template_publish"], topic_params or {}),
"direction": "publish",
"label": tmpl["label"],
"template": tmpl["template_publish"],
"device_type": tmpl.get("device_type"),
}
return resolved
def resolve_topics_by_device_type(device_type: str, topic_params: dict) -> Dict[str, dict]:
"""
根据中控设备的 device_type 筛选对应主题模板
用于中控设备,根据 topic_params.device_type 选择对应的主题模板。
Args:
device_type: 设备类型(应为 "central")
topic_params: 参数值映射,需包含 device_type 字段
Returns:
Dict[str, dict]: 解析后的主题映射
"""
templates = get_topic_templates(device_type)
central_device_type = topic_params.get("device_type", "device_online")
resolved = {}
for tmpl in templates:
if tmpl.get("device_type") == central_device_type:
# 找到匹配的模板
# 订阅主题
if "template_subscribe" in tmpl:
resolved[tmpl["key"] + "_sub"] = {
"topic": resolve_topic(tmpl["template_subscribe"], topic_params or {}),
"direction": "subscribe",
"label": tmpl["label"],
"template": tmpl["template_subscribe"],
"device_type": tmpl.get("device_type"),
}
# 发布主题
if "template_publish" in tmpl:
resolved[tmpl["key"]] = {
"topic": resolve_topic(tmpl["template_publish"], topic_params or {}),
"direction": "publish",
"label": tmpl["label"],
"template": tmpl["template_publish"],
"device_type": tmpl.get("device_type"),
}
break # 只取匹配的一种
return resolved return resolved
......
...@@ -106,6 +106,18 @@ export function stopSimulator(id: string): Promise<{ message: string; id: string ...@@ -106,6 +106,18 @@ export function stopSimulator(id: string): Promise<{ message: string; id: string
return request.post(`${BASE}/devices/${id}/stop`) return request.post(`${BASE}/devices/${id}/stop`)
} }
/** 切换中控设备类型 */
export function switchDeviceType(id: string, deviceTypeName: string): Promise<{
device_id: string
device_type: string
was_running: boolean
message: string
}> {
return request.patch(`${BASE}/devices/${id}/type`, null, {
params: { device_type_name: deviceTypeName }
})
}
/** 手动触发上报 */ /** 手动触发上报 */
export function manualReport(id: string, data: ManualReportRequest): Promise<{ message: string; success: boolean }> { export function manualReport(id: string, data: ManualReportRequest): Promise<{ message: string; success: boolean }> {
return request.post(`${BASE}/devices/${id}/report`, data) return request.post(`${BASE}/devices/${id}/report`, data)
......
...@@ -55,23 +55,83 @@ ...@@ -55,23 +55,83 @@
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<el-table-column type="selection" width="50" /> <el-table-column type="selection" width="50" />
<el-table-column prop="deviceName" label="设备名称" min-width="140" /> <el-table-column prop="deviceName" min-width="140">
<el-table-column prop="deviceId" label="设备 ID" min-width="160" /> <template #header>
<el-table-column label="状态" width="100"> <el-tooltip content="设备的显示名称,可自定义" placement="top">
<span>设备名称</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="deviceId" min-width="160">
<template #header>
<el-tooltip content="设备的唯一标识符,用于 MQTT 消息关联" placement="top">
<span>设备 ID</span>
</el-tooltip>
</template>
</el-table-column>
<!-- 中控主题类型显示 -->
<el-table-column v-if="deviceType === 'central'" min-width="110">
<template #header>
<el-tooltip content="中控设备上报的 MQTT 主题类型,支持 7 种切换" placement="top">
<span>主题类型</span>
</el-tooltip>
</template>
<template #default="{ row }">
<el-tag type="primary" size="small">
{{ CENTRAL_DEVICE_TYPES[row.topicParams?.device_type] || '设备在线' }}
</el-tag>
</template>
</el-table-column>
<el-table-column width="100">
<template #header>
<el-tooltip content="设备的运行状态:运行中/已停止/异常" placement="top">
<span>状态</span>
</el-tooltip>
</template>
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.status === 'running' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small"> <el-tag :type="row.status === 'running' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small">
{{ row.status === 'running' ? '运行中' : row.status === 'error' ? '异常' : '已停止' }} {{ row.status === 'running' ? '运行中' : row.status === 'error' ? '异常' : '已停止' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="totalReports" label="上报次数" width="90" align="center" /> <el-table-column prop="totalReports" width="100" align="center">
<el-table-column label="最后上报" width="170"> <template #header>
<el-tooltip content="设备累计发送 MQTT 消息的次数" placement="top">
<span>上报次数</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column width="170">
<template #header>
<el-tooltip content="设备最后一次发送 MQTT 消息的时间" placement="top">
<span>最后上报</span>
</el-tooltip>
</template>
<template #default="{ row }"> <template #default="{ row }">
{{ row.lastReportedAt ? formatTime(row.lastReportedAt) : '-' }} {{ row.lastReportedAt ? formatTime(row.lastReportedAt) : '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="300" fixed="right"> <el-table-column label="操作" width="300" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<!-- 中控设备:切换类型下拉框 -->
<el-dropdown v-if="deviceType === 'central'" trigger="click" @command="(cmd: string) => handleSwitchType(row, cmd)" style="margin-right: 8px">
<el-button link type="primary" size="small">
切换类型 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(label, key) in CENTRAL_DEVICE_TYPES"
:key="key"
:command="key"
:disabled="row.topicParams?.device_type === key"
>
{{ label }}
<el-icon v-if="row.topicParams?.device_type === key" style="margin-left: 4px"><Check /></el-icon>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button <el-button
:type="row.status === 'running' ? 'warning' : 'success'" :type="row.status === 'running' ? 'warning' : 'success'"
size="small" size="small"
...@@ -258,6 +318,15 @@ ...@@ -258,6 +318,15 @@
下载模板文件 下载模板文件
</el-link> </el-link>
</template> </template>
<template v-if="deviceType === 'central'" #default>
<div style="margin-top: 8px; font-size: 12px; color: #909399;">
<div><b>会议室编号</b>:用于 MQTT 主题订阅和消息上报</div>
<div><b>主题类型</b>:中控设备上报主题类型,支持 7 种</div>
<div style="margin-top: 4px; padding-left: 12px;">
会议室在线 / 设备在线 / 音频系统 / 视频系统 / 控制系统 / 网络系统 / 电源系统
</div>
</div>
</template>
</el-alert> </el-alert>
<el-upload <el-upload
...@@ -323,8 +392,8 @@ ...@@ -323,8 +392,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Upload } from '@element-plus/icons-vue' import { Plus, Upload, ArrowDown, Check } from '@element-plus/icons-vue'
import { import {
listSimulators, listSimulators,
createSimulator, createSimulator,
...@@ -340,9 +409,10 @@ import { ...@@ -340,9 +409,10 @@ import {
batchDeleteDevices, batchDeleteDevices,
importDevices, importDevices,
downloadImportTemplate, downloadImportTemplate,
switchDeviceType,
} from '@/api/deviceSim' } from '@/api/deviceSim'
import type { Simulator, SimulatorCreate, EnvConfig, DeviceType, TopicTemplate, DeviceImportResponse } from '@/types/device' import type { Simulator, SimulatorCreate, EnvConfig, DeviceType, TopicTemplate, DeviceImportResponse } from '@/types/device'
import { DEVICE_TYPE_LABELS } from '@/types/device' import { DEVICE_TYPE_LABELS, CENTRAL_DEVICE_TYPES } from '@/types/device'
const props = defineProps<{ const props = defineProps<{
deviceType: DeviceType deviceType: DeviceType
...@@ -612,6 +682,37 @@ async function handleDelete(row: Simulator) { ...@@ -612,6 +682,37 @@ async function handleDelete(row: Simulator) {
} }
} }
/** 切换中控设备类型 */
async function handleSwitchType(row: Simulator, typeKey: string) {
const currentType = row.topicParams?.device_type || 'device_online'
if (currentType === typeKey) return
const typeLabel = CENTRAL_DEVICE_TYPES[typeKey]
const isRunning = row.status === 'running'
try {
await ElMessageBox.confirm(
`确定将设备「${row.deviceName}」切换为「${typeLabel}」?${isRunning ? '设备正在运行,切换后将自动重启。' : ''}`,
'切换主题类型',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return // 用户取消
}
operatingId.value = row.id
try {
const res = await switchDeviceType(row.id, typeKey)
ElMessage.success(res.message)
await loadDevices()
emit('device-changed')
} catch (e: any) {
ElMessage.error('切换类型失败: ' + (e.message || ''))
} finally {
operatingId.value = ''
}
}
/** 打开编辑弹窗 */ /** 打开编辑弹窗 */
function openEditDialog(row: Simulator) { function openEditDialog(row: Simulator) {
editForm.value = { editForm.value = {
......
...@@ -24,6 +24,17 @@ export const DEVICE_TYPE_ICONS: Record<DeviceType, string> = { ...@@ -24,6 +24,17 @@ export const DEVICE_TYPE_ICONS: Record<DeviceType, string> = {
client: 'Computer', client: 'Computer',
} }
/** 中控设备类型映射 */
export const CENTRAL_DEVICE_TYPES: Record<string, string> = {
room_online: '会议室在线',
device_online: '设备在线',
audio: '音频系统',
video: '视频系统',
control: '控制系统',
network: '网络系统',
power: '电源系统',
}
/** 环境配置 */ /** 环境配置 */
export interface EnvConfig { export interface EnvConfig {
id: string id: string
...@@ -204,6 +215,7 @@ export interface TopicTemplate { ...@@ -204,6 +215,7 @@ export interface TopicTemplate {
paramLabels: Record<string, string> paramLabels: Record<string, string>
paramDefaults: Record<string, string> paramDefaults: Record<string, string>
direction: 'publish' | 'subscribe' direction: 'publish' | 'subscribe'
deviceType?: string
} }
/** 主题模板列表响应 */ /** 主题模板列表响应 */
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论