提交 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):
templates=[
TopicTemplateResponse(
key=t["key"],
template=t["template"],
template=t.get("template", t.get("template_publish", "")),
label=t["label"],
params=t["params"],
param_labels=t["param_labels"],
param_defaults=t["param_defaults"],
direction=t["direction"],
direction=t.get("direction", "publish"),
device_type=t.get("device_type"),
)
for t in templates
],
......@@ -271,9 +272,12 @@ async def download_import_template(
ws.title = "设备导入"
# 表头
headers = ["环境配置名称", "设备名称", "设备ID"]
headers = ["环境配置名称", "设备名称", "设备编号"]
if device_type == "door":
headers.append("授权码(app_token)")
elif device_type == "central":
headers.append("会议室编号")
headers.append("主题类型")
headers.extend(["自动重连", "启用定时上报", "上报间隔(秒)"])
ws.append(headers)
......@@ -281,6 +285,9 @@ async def download_import_template(
example = ["测试环境", f"{device_type}_001", f"{device_type}_device_001"]
if device_type == "door":
example.append("AUTH-0001")
elif device_type == "central":
example.append("A101")
example.append("设备在线")
example.extend(["是", "是", "30"])
ws.append(example)
......@@ -316,8 +323,8 @@ async def import_devices(
"""
批量导入模拟设备
上传 Excel 文件批量创建设备。Excel 列:环境配置名称、设备名称、设备ID
授权码(可选)、自动重连(可选)、启用定时上报(可选)、上报间隔(可选)。
上传 Excel 文件批量创建设备。Excel 列:环境配置名称、设备名称、设备编号
授权码(可选,门口屏)、会议室编号(可选,中控)、自动重连(可选)、启用定时上报(可选)、上报间隔(可选)。
device_type 由前端页面决定,不包含在 Excel 中。
"""
# 校验文件类型
......@@ -433,6 +440,28 @@ async def update_simulator(
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}")
async def delete_simulator(
device_id: str,
......
......@@ -255,6 +255,7 @@ class TopicTemplateResponse(BaseModel):
param_labels: Dict[str, str] = Field(default={}, description="参数名 -> 中文标签映射")
param_defaults: Dict[str, str] = Field(default={}, description="参数名 -> 默认值映射")
direction: str = Field(..., description="方向:publish(上报) / subscribe(订阅)")
device_type: Optional[str] = Field(default=None, description="中控设备类型标识")
model_config = ConfigDict(
alias_generator=to_camel,
......
......@@ -478,15 +478,31 @@ class DeviceSimService:
COLUMN_MAP = {
"环境配置名称": "env_config_name",
"设备名称": "device_name",
"设备ID": "device_id",
"设备编号": "device_id",
"设备id": "device_id",
"授权码": "app_token",
"授权码(app_token)": "app_token",
"会议室编号": "room_id",
"主题类型": "central_device_type",
"设备类型": "central_device_type", # 兼容旧版
"自动重连": "auto_reconnect",
"启用定时上报": "report_enabled",
"上报间隔(秒)": "report_interval",
"上报间隔": "report_interval",
}
# 中控设备类型中文 → 标识映射
CENTRAL_DEVICE_TYPES = {
"会议室在线": "room_online",
"设备在线": "device_online",
"音频系统": "audio",
"视频系统": "video",
"控制系统": "control",
"网络系统": "network",
"电源系统": "power",
}
# 解析 Excel
wb = load_workbook(BytesIO(file_bytes), read_only=True)
ws = wb.active
......@@ -576,11 +592,32 @@ class DeviceSimService:
report_enabled = _parse_bool(row_data.get("report_enabled"), True)
report_interval = _parse_int(row_data.get("report_interval"), 30)
# 构建 topic_params(授权码)
# 构建 topic_params
topic_params = {}
# 门口屏:授权码
app_token = row_data.get("app_token")
if app_token:
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:
......@@ -886,6 +923,58 @@ class DeviceSimService:
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:
......
......@@ -95,10 +95,13 @@ class BaseSimulator(ABC):
如果设备类型有真实主题模板(门口屏/无纸化),则使用真实主题;
否则标记为无真实主题,使用旧格式 fallback。
对于中控设备,根据 topic_params.device_type 选择对应的主题模板。
"""
from app.simulators.topic_templates import (
has_real_topics,
resolve_all_topics,
resolve_topics_by_device_type,
)
self._has_real_topics = has_real_topics(self.device_type)
......@@ -108,7 +111,13 @@ class BaseSimulator(ABC):
if 'device_id' not in params_with_device_id:
params_with_device_id['device_id'] = self.device_id
self._resolved_topics = resolve_all_topics(self.device_type, params_with_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)
logger.info(f"设备 {self.device_id} 解析主题: {self._resolved_topics}")
def get_resolved_topic(self, template_key: str) -> Optional[str]:
......
......@@ -2,17 +2,18 @@
# -*- coding: utf-8 -*-
"""
模块名称:central_simulator.py
模块描述:中控设备模拟器,模拟灯光/窗帘/投影等设备控制
模块描述:中控设备模拟器,支持 7 种主题类型上报
作者:czj
创建日期:2026-07-29
最后修改:2026-07-29
最后修改:2026-08-06
"""
import json
import logging
import random
import time
import uuid
from typing import Optional
from app.simulators.base_simulator import BaseSimulator
......@@ -21,6 +22,18 @@ from app.services.mqtt_manager import MqttManager
logger = logging.getLogger(__name__)
# 中控设备类型映射
CENTRAL_DEVICE_TYPES = {
"room_online": "会议室在线",
"device_online": "设备在线",
"audio": "音频系统",
"video": "视频系统",
"control": "控制系统",
"network": "网络系统",
"power": "电源系统",
}
class CentralSimulator(BaseSimulator):
"""
中控设备模拟器
......@@ -127,25 +140,198 @@ class CentralSimulator(BaseSimulator):
}
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"),
"brightness": config.get("brightness"),
"position": config.get("position"),
"temperature": config.get("temperature"),
"volume": config.get("volume"),
}
"""
根据设备类型动态选择消息体构建方法
支持的设备类型:
- room_online: 会议室在线
- device_online: 设备在线
- audio: 音频系统
- video: 视频系统
- 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 {
"device_id": self.device_id,
"status": "online",
"current_scene": self._current_scene,
"scene_name": self._SCENES.get(self._current_scene, {}).get("name", ""),
"sub_device_count": len(self._sub_devices),
"sub_devices": sub_status,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
"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 {
"action": "_updatestatus",
"client_udid": room_id,
"data": [
{"device_udid": device_number, "power": 1, "online": 1, "watt": random.randint(5000, 15000), "run": "在线"},
{"device_udid": device_number, "power": 1, "online": 1, "watt": random.randint(500, 1500), "run": "在线"}
]
}
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:
......
......@@ -176,7 +176,85 @@ DEVICE_TOPIC_TEMPLATES: Dict[str, List[dict]] = {
"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": [], # 暂无真实主题,保留旧格式
}
......@@ -242,12 +320,78 @@ def resolve_all_topics(device_type: str, topic_params: dict) -> Dict[str, dict]:
templates = get_topic_templates(device_type)
resolved = {}
for tmpl in templates:
resolved[tmpl["key"]] = {
"topic": resolve_topic(tmpl["template"], topic_params or {}),
"direction": tmpl["direction"],
"label": tmpl["label"],
"template": tmpl["template"],
}
# 兼容新旧两种模板结构
if "template" in tmpl:
# 旧格式:单个 template + direction
resolved[tmpl["key"]] = {
"topic": resolve_topic(tmpl["template"], topic_params or {}),
"direction": tmpl["direction"],
"label": tmpl["label"],
"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
......
......@@ -106,6 +106,18 @@ export function stopSimulator(id: string): Promise<{ message: string; id: string
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 }> {
return request.post(`${BASE}/devices/${id}/report`, data)
......
......@@ -55,23 +55,83 @@
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="deviceName" label="设备名称" min-width="140" />
<el-table-column prop="deviceId" label="设备 ID" min-width="160" />
<el-table-column label="状态" width="100">
<el-table-column prop="deviceName" min-width="140">
<template #header>
<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 }">
<el-tag :type="row.status === 'running' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small">
{{ row.status === 'running' ? '运行中' : row.status === 'error' ? '异常' : '已停止' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="totalReports" label="上报次数" width="90" align="center" />
<el-table-column label="最后上报" width="170">
<el-table-column prop="totalReports" width="100" align="center">
<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 }">
{{ row.lastReportedAt ? formatTime(row.lastReportedAt) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<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
:type="row.status === 'running' ? 'warning' : 'success'"
size="small"
......@@ -258,6 +318,15 @@
下载模板文件
</el-link>
</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-upload
......@@ -323,8 +392,8 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Upload, ArrowDown, Check } from '@element-plus/icons-vue'
import {
listSimulators,
createSimulator,
......@@ -340,9 +409,10 @@ import {
batchDeleteDevices,
importDevices,
downloadImportTemplate,
switchDeviceType,
} from '@/api/deviceSim'
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<{
deviceType: DeviceType
......@@ -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) {
editForm.value = {
......
......@@ -24,6 +24,17 @@ export const DEVICE_TYPE_ICONS: Record<DeviceType, string> = {
client: 'Computer',
}
/** 中控设备类型映射 */
export const CENTRAL_DEVICE_TYPES: Record<string, string> = {
room_online: '会议室在线',
device_online: '设备在线',
audio: '音频系统',
video: '视频系统',
control: '控制系统',
network: '网络系统',
power: '电源系统',
}
/** 环境配置 */
export interface EnvConfig {
id: string
......@@ -204,6 +215,7 @@ export interface TopicTemplate {
paramLabels: Record<string, string>
paramDefaults: Record<string, string>
direction: 'publish' | 'subscribe'
deviceType?: string
}
/** 主题模板列表响应 */
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论