This commit is contained in:
rainv123
2025-09-19 09:33:47 +08:00
11 changed files with 2387 additions and 104 deletions
@@ -5,6 +5,10 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -12,32 +16,45 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
@Tag(name = "设备管理")
@AllArgsConstructor
@RestController
@RequestMapping("/device")
public class DeviceController {
private final DeviceService deviceService;
private final RedisUtils redisUtils;
private final SysParamsService sysParamsService;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
RestTemplate restTemplate, ObjectMapper objectMapper) {
this.deviceService = deviceService;
this.redisUtils = redisUtils;
this.sysParamsService = sysParamsService;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
@PostMapping("/bind/{agentId}/{deviceCode}")
@Operation(summary = "绑定设备")
@@ -75,6 +92,88 @@ public class DeviceController {
return new Result<List<DeviceEntity>>().ok(devices);
}
@PostMapping("/bind/{agentId}")
@Operation(summary = "转发POST请求到MQTT网关")
@RequiresPermissions("sys:role:normal")
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
try {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return new Result<>();
}
// 获取当前用户的设备列表
UserDetail user = SecurityUser.getUser();
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
// 构建deviceIds数组
java.util.List<String> deviceIds = new java.util.ArrayList<>();
for (DeviceEntity device : devices) {
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
// 替换冒号为下划线
groupId = groupId.replace(":", "_");
macAddress = macAddress.replace(":", "_");
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
deviceIds.add(mqttClientId);
}
// 构建完整的URL
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 生成Bearer令牌
String token = generateBearerToken();
if (token == null) {
return new Result<String>().error("令牌生成失败");
}
headers.set("Authorization", "Bearer " + token);
// 构建请求体JSON
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, headers);
// 发送POST请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
// 返回响应
return new Result<String>().ok(response.getBody());
} catch (Exception e) {
return new Result<String>().error("转发请求失败: " + e.getMessage());
}
}
private String generateBearerToken() {
try {
// 获取当前日期,格式为yyyy-MM-dd
String dateStr = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 获取MQTT签名密钥
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
return null;
}
// 将日期字符串与MQTT_SIGNATURE_KEY连接
String tokenContent = dateStr + signatureKey;
// 对连接后的字符串进行SHA256哈希计算
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
return token;
} catch (Exception e) {
return null;
}
}
@PostMapping("/unbind")
@Operation(summary = "解绑设备")
@RequiresPermissions("sys:role:normal")
@@ -109,4 +208,63 @@ public class DeviceController {
deviceService.manualAddDevice(user.getId(), dto);
return new Result<>();
}
@PostMapping("/commands/{deviceId}")
@Operation(summary = "发送设备指令")
@RequiresPermissions("sys:role:normal")
public Result<String> sendDeviceCommand(@PathVariable String deviceId, @RequestBody String command) {
try {
// 从系统参数中获取MQTT网关地址
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
return new Result<String>().error("MQTT网关地址未配置");
}
// 构建完整的URL
// 获取设备信息以构建mqttClientId
DeviceEntity deviceById = deviceService.selectById(deviceId);
if (!deviceById.getUserId().equals(SecurityUser.getUser().getId())) {
return new Result<String>().error("设备不存在");
}
String macAddress = deviceById != null ? deviceById.getMacAddress() : "unknown";
String groupId = deviceById != null ? deviceById.getBoard() : null;
if (groupId == null) {
groupId = "GID_default";
}
groupId = groupId.replace(":", "_");
macAddress = macAddress.replace(":", "_");
// 拼接为groupId@@@macAddress@@@deviceId格式
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
String url = "http://" + mqttGatewayUrl + "/api/commands/" + mqttClientId;
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
// 生成Bearer令牌
String dateStr = java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
if (StringUtils.isBlank(signatureKey)) {
return new Result<String>().error("MQTT签名密钥未配置");
}
String tokenContent = dateStr + signatureKey;
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
headers.set("Authorization", "Bearer " + token);
// 构建请求体
HttpEntity<String> requestEntity = new HttpEntity<>(command, headers);
// 发送POST请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
// 返回响应
return new Result<String>().ok(response.getBody());
} catch (Exception e) {
return new Result<String>().error("发送指令失败: " + e.getMessage());
}
}
}
@@ -189,7 +189,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
try {
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
: "GID_default";
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, clientId, groupId);
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, groupId);
if (mqtt != null) {
mqtt.setEndpoint(mqttUdpConfig);
response.setMqtt(mqtt);
@@ -480,11 +480,10 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
* 构建MQTT配置信息
*
* @param macAddress MAC地址
* @param clientId 客户端ID (UUID)
* @param groupId 分组ID
* @return MQTT配置对象
*/
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String clientId, String groupId)
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
throws Exception {
// 从环境变量或系统参数获取签名密钥
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
@@ -496,8 +495,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
// 构建客户端ID格式:groupId@@@macAddress@@@uuid
String groupIdSafeStr = groupId.replace(":", "_");
String deviceIdSafeStr = macAddress.replace(":", "_");
String clientIdSafeStr = clientId.replace(":", "_");
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, clientIdSafeStr);
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, deviceIdSafeStr);
// 构建用户数据(包含IP等信息)
Map<String, String> userData = new HashMap<>();
@@ -0,0 +1,3 @@
delete from `sys_params` where param_code = 'server.mqtt_manager_api';
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
VALUES (119, 'server.mqtt_manager_api', 'null', 'string', 1, 'MQTT网关管理API的地址');
@@ -345,6 +345,13 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509080922.sql
- changeSet:
id: 202509161610
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509161610.sql
- changeSet:
id: 202509161701
author: RanChen
@@ -85,4 +85,38 @@ export default {
});
}).send();
},
// 获取设备状态
getDeviceStatus(agentId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/bind/${agentId}`)
.method('POST')
.data({}) // 发送空对象作为请求体
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取设备状态失败:', err);
RequestService.reAjaxFun(() => {
this.getDeviceStatus(agentId, callback);
});
}).send();
},
// 发送设备指令
sendDeviceCommand(deviceId, mcpData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/device/commands/${deviceId}`)
.method('POST')
.data(mcpData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('发送设备指令失败:', err);
RequestService.reAjaxFun(() => {
this.sendDeviceCommand(deviceId, mcpData, callback);
});
}).send();
},
}
File diff suppressed because it is too large Load Diff
+135
View File
@@ -15,6 +15,137 @@ export default {
'header.paramManagement': 'Params Management',
'header.dictManagement': 'Dict Management',
// McpToolCallDialog component text
'mcpToolCall.title': 'Tool Call',
'mcpToolCall.execute': 'Execute',
'mcpToolCall.chooseFunction': '1、Choose Function',
'mcpToolCall.searchFunction': 'Search Function',
'mcpToolCall.noResults': 'No matching functions found',
'mcpToolCall.settings': '2、Parameter Settings',
'mcpToolCall.inputPlaceholder': 'Please enter {label}',
'mcpToolCall.valueRange': 'Value range: {min} - {max}',
'mcpToolCall.selectPlaceholder': 'Please select {label}',
'mcpToolCall.lightTheme': 'Light Theme',
'mcpToolCall.darkTheme': 'Dark Theme',
'mcpToolCall.pleaseSelect': 'Please select a function',
'mcpToolCall.cancel': 'Cancel',
'mcpToolCall.requiredField': 'Please enter {field}',
'mcpToolCall.minValue': 'Minimum value is {value}',
'mcpToolCall.maxValue': 'Maximum value is {value}',
'mcpToolCall.selectTool': 'Please select a tool to execute',
'mcpToolCall.executionResult': '3、Execution Result',
'mcpToolCall.copyResult': 'Copy Result',
'mcpToolCall.noResultYet': 'No result yet',
'mcpToolCall.loadingToolList': 'Loading tool list...',
// Tool names
'mcpToolCall.toolName.getDeviceStatus': 'View Device Status',
'mcpToolCall.toolName.setVolume': 'Set Volume',
'mcpToolCall.toolName.setBrightness': 'Set Brightness',
'mcpToolCall.toolName.setTheme': 'Set Theme',
'mcpToolCall.toolName.takePhoto': 'Take Photo & Recognize',
'mcpToolCall.toolName.getSystemInfo': 'System Info',
'mcpToolCall.toolName.reboot': 'Reboot Device',
'mcpToolCall.toolName.upgradeFirmware': 'Upgrade Firmware',
'mcpToolCall.toolName.getScreenInfo': 'Screen Info',
'mcpToolCall.toolName.snapshot': 'Screen Snapshot',
'mcpToolCall.toolName.previewImage': 'Preview Image',
'mcpToolCall.toolName.setDownloadUrl': 'Set Download URL',
// Tool categories
'mcpToolCall.category.audio': 'Audio',
'mcpToolCall.category.display': 'Display',
'mcpToolCall.category.camera': 'Camera',
'mcpToolCall.category.system': 'System',
'mcpToolCall.category.assets': 'Assets',
'mcpToolCall.category.deviceInfo': 'Device Info',
// Table categories and properties
'mcpToolCall.table.audioSpeaker': 'Audio Speaker',
'mcpToolCall.table.screen': 'Screen',
'mcpToolCall.table.network': 'Network',
'mcpToolCall.table.audioControl': 'Audio Control',
'mcpToolCall.table.screenControl': 'Screen Control',
'mcpToolCall.table.systemControl': 'System Control',
'mcpToolCall.table.screenInfo': 'Screen Info',
'mcpToolCall.table.hardwareInfo': 'Hardware Info',
'mcpToolCall.table.memoryInfo': 'Memory Info',
'mcpToolCall.table.applicationInfo': 'Application Info',
'mcpToolCall.table.networkInfo': 'Network Info',
'mcpToolCall.table.displayInfo': 'Display Info',
'mcpToolCall.table.deviceInfo': 'Device Info',
'mcpToolCall.table.systemInfo': 'System Info',
// Table column headers
'mcpToolCall.table.component': 'Component',
'mcpToolCall.table.property': 'Property',
'mcpToolCall.table.value': 'Value',
'mcpToolCall.prop.volume': 'Volume',
'mcpToolCall.prop.brightness': 'Brightness',
'mcpToolCall.prop.theme': 'Theme',
'mcpToolCall.prop.type': 'Type',
'mcpToolCall.prop.ssid': 'SSID',
'mcpToolCall.prop.signalStrength': 'Signal Strength',
'mcpToolCall.prop.operationResult': 'Operation Result',
'mcpToolCall.prop.width': 'Width',
'mcpToolCall.prop.height': 'Height',
'mcpToolCall.prop.screenType': 'Type',
'mcpToolCall.prop.chipModel': 'Chip Model',
'mcpToolCall.prop.cpuCores': 'CPU Cores',
'mcpToolCall.prop.chipVersion': 'Chip Version',
'mcpToolCall.prop.flashSize': 'Flash Size',
'mcpToolCall.prop.minFreeHeap': 'Minimum Free Heap',
'mcpToolCall.prop.applicationName': 'Application Name',
'mcpToolCall.prop.applicationVersion': 'Application Version',
'mcpToolCall.prop.compileTime': 'Compile Time',
'mcpToolCall.prop.idfVersion': 'IDF Version',
'mcpToolCall.prop.macAddress': 'MAC Address',
'mcpToolCall.prop.ipAddress': 'IP Address',
'mcpToolCall.prop.wifiName': 'WiFi Name',
'mcpToolCall.prop.wifiChannel': 'WiFi Channel',
'mcpToolCall.prop.screenSize': 'Screen Size',
'mcpToolCall.prop.deviceUuid': 'Device UUID',
'mcpToolCall.prop.systemLanguage': 'System Language',
'mcpToolCall.prop.currentOtaPartition': 'Current OTA Partition',
'mcpToolCall.prop.getResult': 'Get Result',
'mcpToolCall.prop.url': 'URL',
'mcpToolCall.prop.quality': 'Quality',
'mcpToolCall.prop.question': 'Question',
// Tool help texts
'mcpToolCall.help.getDeviceStatus': 'View the current running status of the device, including volume, screen, battery and other information.',
'mcpToolCall.help.setVolume': 'Adjust the volume of the device, please enter a value between 0-100.',
'mcpToolCall.help.setBrightness': 'Adjust the brightness of the device screen, please enter a value between 0-100.',
'mcpToolCall.help.setTheme': 'Switch the display theme of the device screen, you can choose light or dark mode.',
'mcpToolCall.help.takePhoto': 'Take photos with the device camera and perform recognition analysis, please enter the question you want to ask.',
'mcpToolCall.help.getSystemInfo': 'Get the system information of the device, including hardware specifications, software version, etc.',
'mcpToolCall.help.reboot': 'Reboot the device, the device will restart after execution.',
'mcpToolCall.help.upgradeFirmware': 'Download and upgrade the device firmware from the specified URL, the device will restart automatically after the upgrade.',
'mcpToolCall.help.getScreenInfo': 'Get detailed information about the screen, such as resolution, size and other parameters.',
'mcpToolCall.help.snapshot': 'Take a screenshot of the current screen and upload it to the specified URL.',
'mcpToolCall.help.previewImage': 'Preview images from the specified URL on the device screen.',
'mcpToolCall.help.setDownloadUrl': 'Set the download address for device resource files.',
// Other text
'mcpToolCall.text.strong': 'Strong',
'mcpToolCall.text.medium': 'Medium',
'mcpToolCall.text.weak': 'Weak',
'mcpToolCall.text.dark': 'Dark',
'mcpToolCall.text.light': 'Light',
'mcpToolCall.text.setSuccess': 'Setting successful',
'mcpToolCall.text.setFailed': 'Setting failed',
'mcpToolCall.text.brightnessSetSuccess': 'Brightness setting successful',
'mcpToolCall.text.brightnessSetFailed': 'Brightness setting failed',
'mcpToolCall.text.themeSetSuccess': 'Theme setting successful',
'mcpToolCall.text.themeSetFailed': 'Theme setting failed',
'mcpToolCall.text.rebootCommandSent': 'Reboot command sent',
'mcpToolCall.text.rebootFailed': 'Reboot failed',
'mcpToolCall.text.monochrome': 'Monochrome Screen',
'mcpToolCall.text.color': 'Color Screen',
'mcpToolCall.text.getSuccessParseFailed': 'Get successful, but parse failed',
'mcpToolCall.text.getFailed': 'Get failed',
'mcpToolCall.text.getSuccessFormatError': 'Get successful, but data format is abnormal',
// Dictionary data dialog related
'dictDataDialog.addDictData': 'Add Dictionary Data',
'dictDataDialog.dictLabel': 'Dictionary Label',
@@ -327,6 +458,7 @@ export default {
'device.bindWithCode': '6-digit Verification Code Binding',
'device.manualAdd': 'Manual Add',
'device.unbind': 'Unbind',
'device.toolCall': 'Tool Call',
'device.selectAtLeastOne': 'Please select at least one record',
'device.confirmBatchUnbind': 'Are you sure you want to unbind {count} selected devices?',
'device.batchUnbindSuccess': 'Successfully unbound {count} devices',
@@ -342,6 +474,9 @@ export default {
'device.autoUpdateDisabled': 'Auto update disabled',
'device.batchUnbindSuccess': 'Successfully unbound {count} devices',
'device.getFirmwareTypeFailed': 'Failed to fetch firmware type',
'device.deviceStatus': 'Status',
'device.online': 'Online',
'device.offline': 'Offline',
// Message tips
'message.success': 'Operation Successful',
+135
View File
@@ -15,6 +15,137 @@ export default {
'header.paramManagement': '参数管理',
'header.dictManagement': '字典管理',
// McpToolCallDialog组件文本
'mcpToolCall.title': '工具调用',
'mcpToolCall.execute': '执行',
'mcpToolCall.chooseFunction': '1、选择功能',
'mcpToolCall.searchFunction': '搜索功能',
'mcpToolCall.noResults': '未找到匹配的功能',
'mcpToolCall.settings': '2、参数设置',
'mcpToolCall.inputPlaceholder': '请输入{label}',
'mcpToolCall.valueRange': '取值范围:{min} - {max}',
'mcpToolCall.selectPlaceholder': '请选择{label}',
'mcpToolCall.lightTheme': '浅色主题',
'mcpToolCall.darkTheme': '深色主题',
'mcpToolCall.pleaseSelect': '请选择一个功能',
'mcpToolCall.cancel': '取消',
'mcpToolCall.requiredField': '请输入{field}',
'mcpToolCall.minValue': '最小值为{value}',
'mcpToolCall.maxValue': '最大值为{value}',
'mcpToolCall.selectTool': '请选择要执行的工具',
'mcpToolCall.executionResult': '3、执行结果',
'mcpToolCall.copyResult': '复制结果',
'mcpToolCall.noResultYet': '暂无执行结果',
'mcpToolCall.loadingToolList': '正在获取工具列表...',
// 工具名称
'mcpToolCall.toolName.getDeviceStatus': '查看设备状态',
'mcpToolCall.toolName.setVolume': '设置音量',
'mcpToolCall.toolName.setBrightness': '设置亮度',
'mcpToolCall.toolName.setTheme': '设置主题',
'mcpToolCall.toolName.takePhoto': '拍照识别',
'mcpToolCall.toolName.getSystemInfo': '系统信息',
'mcpToolCall.toolName.reboot': '重启设备',
'mcpToolCall.toolName.upgradeFirmware': '升级固件',
'mcpToolCall.toolName.getScreenInfo': '屏幕信息',
'mcpToolCall.toolName.snapshot': '屏幕截图',
'mcpToolCall.toolName.previewImage': '预览图片',
'mcpToolCall.toolName.setDownloadUrl': '设置下载地址',
// 工具分类
'mcpToolCall.category.audio': '音频',
'mcpToolCall.category.display': '显示',
'mcpToolCall.category.camera': '拍摄',
'mcpToolCall.category.system': '系统',
'mcpToolCall.category.assets': '资源',
'mcpToolCall.category.deviceInfo': '设备信息',
// 表格分类和属性
'mcpToolCall.table.audioSpeaker': '音频扬声器',
'mcpToolCall.table.screen': '屏幕',
'mcpToolCall.table.network': '网络',
'mcpToolCall.table.audioControl': '音频控制',
'mcpToolCall.table.screenControl': '屏幕控制',
'mcpToolCall.table.systemControl': '系统控制',
'mcpToolCall.table.screenInfo': '屏幕信息',
'mcpToolCall.table.hardwareInfo': '硬件信息',
'mcpToolCall.table.memoryInfo': '内存信息',
'mcpToolCall.table.applicationInfo': '应用信息',
'mcpToolCall.table.networkInfo': '网络信息',
'mcpToolCall.table.displayInfo': '显示信息',
'mcpToolCall.table.deviceInfo': '设备信息',
'mcpToolCall.table.systemInfo': '系统信息',
// 表格列标题
'mcpToolCall.table.component': '组件',
'mcpToolCall.table.property': '属性',
'mcpToolCall.table.value': '值',
'mcpToolCall.prop.volume': '音量',
'mcpToolCall.prop.brightness': '亮度',
'mcpToolCall.prop.theme': '主题',
'mcpToolCall.prop.type': '类型',
'mcpToolCall.prop.ssid': 'SSID',
'mcpToolCall.prop.signalStrength': '信号强度',
'mcpToolCall.prop.operationResult': '操作结果',
'mcpToolCall.prop.width': '宽度',
'mcpToolCall.prop.height': '高度',
'mcpToolCall.prop.screenType': '类型',
'mcpToolCall.prop.chipModel': '芯片型号',
'mcpToolCall.prop.cpuCores': 'CPU核心数',
'mcpToolCall.prop.chipVersion': '芯片版本',
'mcpToolCall.prop.flashSize': 'Flash大小',
'mcpToolCall.prop.minFreeHeap': '最小可用堆',
'mcpToolCall.prop.applicationName': '应用名称',
'mcpToolCall.prop.applicationVersion': '应用版本',
'mcpToolCall.prop.compileTime': '编译时间',
'mcpToolCall.prop.idfVersion': 'IDF版本',
'mcpToolCall.prop.macAddress': 'MAC地址',
'mcpToolCall.prop.ipAddress': 'IP地址',
'mcpToolCall.prop.wifiName': 'WiFi名称',
'mcpToolCall.prop.wifiChannel': 'WiFi信道',
'mcpToolCall.prop.screenSize': '屏幕尺寸',
'mcpToolCall.prop.deviceUuid': '设备UUID',
'mcpToolCall.prop.systemLanguage': '系统语言',
'mcpToolCall.prop.currentOtaPartition': '当前OTA分区',
'mcpToolCall.prop.getResult': '获取结果',
'mcpToolCall.prop.url': 'URL',
'mcpToolCall.prop.quality': '质量',
'mcpToolCall.prop.question': '问题',
// 工具帮助文本
'mcpToolCall.help.getDeviceStatus': '查看设备的当前运行状态,包括音量、屏幕、电池等信息。',
'mcpToolCall.help.setVolume': '调整设备的音量大小,请输入0-100之间的数值。',
'mcpToolCall.help.setBrightness': '调整设备屏幕的亮度,请输入0-100之间的数值。',
'mcpToolCall.help.setTheme': '切换设备屏幕的显示主题,可以选择浅色或深色模式。',
'mcpToolCall.help.takePhoto': '使用设备摄像头拍摄照片并进行识别分析,请输入要询问的问题。',
'mcpToolCall.help.getSystemInfo': '获取设备的系统信息,包括硬件规格、软件版本等。',
'mcpToolCall.help.reboot': '重启设备,执行后设备将重新启动。',
'mcpToolCall.help.upgradeFirmware': '从指定URL下载并升级设备固件,升级后设备会自动重启。',
'mcpToolCall.help.getScreenInfo': '获取屏幕的详细信息,如分辨率、尺寸等参数。',
'mcpToolCall.help.snapshot': '对当前屏幕进行截图并上传到指定URL。',
'mcpToolCall.help.previewImage': '在设备屏幕上预览指定URL的图片。',
'mcpToolCall.help.setDownloadUrl': '设置设备资源文件的下载地址。',
// 其他文本
'mcpToolCall.text.strong': '强',
'mcpToolCall.text.medium': '中',
'mcpToolCall.text.weak': '弱',
'mcpToolCall.text.dark': '深色',
'mcpToolCall.text.light': '浅色',
'mcpToolCall.text.setSuccess': '设置成功',
'mcpToolCall.text.setFailed': '设置失败',
'mcpToolCall.text.brightnessSetSuccess': '亮度设置成功',
'mcpToolCall.text.brightnessSetFailed': '亮度设置失败',
'mcpToolCall.text.themeSetSuccess': '主题设置成功',
'mcpToolCall.text.themeSetFailed': '主题设置失败',
'mcpToolCall.text.rebootCommandSent': '重启指令已发送',
'mcpToolCall.text.rebootFailed': '重启失败',
'mcpToolCall.text.monochrome': '单色屏',
'mcpToolCall.text.color': '彩色屏',
'mcpToolCall.text.getSuccessParseFailed': '获取成功,但解析失败',
'mcpToolCall.text.getFailed': '获取失败',
'mcpToolCall.text.getSuccessFormatError': '获取成功,但数据格式异常',
// 字典数据对话框相关
'dictDataDialog.addDictData': '新增字典数据',
'dictDataDialog.dictLabel': '字典标签',
@@ -328,6 +459,7 @@ export default {
'device.bindWithCode': '6位验证码绑定',
'device.manualAdd': '手动添加',
'device.unbind': '解绑',
'device.toolCall': '工具调用',
'device.selectAtLeastOne': '请至少选择一条记录',
'device.confirmBatchUnbind': '确认要解绑选中的 {count} 台设备吗?',
'device.batchUnbindSuccess': '成功解绑 {count} 台设备',
@@ -343,6 +475,9 @@ export default {
'device.autoUpdateDisabled': '已关闭自动升级',
'device.batchUnbindSuccess': '成功解绑 {count} 台设备',
'device.getFirmwareTypeFailed': '获取固件类型失败',
'device.deviceStatus': '状态',
'device.online': '在线',
'device.offline': '离线',
// 消息提示
'message.success': '操作成功',
+135
View File
@@ -15,6 +15,137 @@ export default {
'header.paramManagement': '參數管理',
'header.dictManagement': '字典管理',
// McpToolCallDialog组件文本
'mcpToolCall.title': '工具調用',
'mcpToolCall.execute': '執行',
'mcpToolCall.chooseFunction': '1、選擇功能',
'mcpToolCall.searchFunction': '搜索功能',
'mcpToolCall.noResults': '未找到匹配的功能',
'mcpToolCall.settings': '2、參數設置',
'mcpToolCall.inputPlaceholder': '請輸入{label}',
'mcpToolCall.valueRange': '取值範圍:{min} - {max}',
'mcpToolCall.selectPlaceholder': '請選擇{label}',
'mcpToolCall.lightTheme': '淺色主題',
'mcpToolCall.darkTheme': '深色主題',
'mcpToolCall.pleaseSelect': '請選擇一個功能',
'mcpToolCall.cancel': '取消',
'mcpToolCall.requiredField': '請輸入{field}',
'mcpToolCall.minValue': '最小值為{value}',
'mcpToolCall.maxValue': '最大值為{value}',
'mcpToolCall.selectTool': '請選擇要執行的工具',
'mcpToolCall.executionResult': '3、執行結果',
'mcpToolCall.copyResult': '複製結果',
'mcpToolCall.noResultYet': '暫無執行結果',
'mcpToolCall.loadingToolList': '正在獲取工具列表...',
// 工具名稱
'mcpToolCall.toolName.getDeviceStatus': '查看設備狀態',
'mcpToolCall.toolName.setVolume': '設置音量',
'mcpToolCall.toolName.setBrightness': '設置亮度',
'mcpToolCall.toolName.setTheme': '設置主題',
'mcpToolCall.toolName.takePhoto': '拍照識別',
'mcpToolCall.toolName.getSystemInfo': '系統資訊',
'mcpToolCall.toolName.reboot': '重啟設備',
'mcpToolCall.toolName.upgradeFirmware': '升級固件',
'mcpToolCall.toolName.getScreenInfo': '螢幕資訊',
'mcpToolCall.toolName.snapshot': '螢幕截圖',
'mcpToolCall.toolName.previewImage': '預覽圖片',
'mcpToolCall.toolName.setDownloadUrl': '設置下載地址',
// 工具分類
'mcpToolCall.category.audio': '音頻',
'mcpToolCall.category.display': '顯示',
'mcpToolCall.category.camera': '拍攝',
'mcpToolCall.category.system': '系統',
'mcpToolCall.category.assets': '資源',
'mcpToolCall.category.deviceInfo': '設備資訊',
// 表格分類和屬性
'mcpToolCall.table.audioSpeaker': '音頻揚聲器',
'mcpToolCall.table.screen': '螢幕',
'mcpToolCall.table.network': '網路',
'mcpToolCall.table.audioControl': '音頻控制',
'mcpToolCall.table.screenControl': '螢幕控制',
'mcpToolCall.table.systemControl': '系統控制',
'mcpToolCall.table.screenInfo': '螢幕資訊',
'mcpToolCall.table.hardwareInfo': '硬體資訊',
'mcpToolCall.table.memoryInfo': '記憶體資訊',
'mcpToolCall.table.applicationInfo': '應用資訊',
'mcpToolCall.table.networkInfo': '網路資訊',
'mcpToolCall.table.displayInfo': '顯示資訊',
'mcpToolCall.table.deviceInfo': '設備資訊',
'mcpToolCall.table.systemInfo': '系統資訊',
// 表格列標題
'mcpToolCall.table.component': '組件',
'mcpToolCall.table.property': '屬性',
'mcpToolCall.table.value': '值',
'mcpToolCall.prop.volume': '音量',
'mcpToolCall.prop.brightness': '亮度',
'mcpToolCall.prop.theme': '主題',
'mcpToolCall.prop.type': '類型',
'mcpToolCall.prop.ssid': 'SSID',
'mcpToolCall.prop.signalStrength': '信號強度',
'mcpToolCall.prop.operationResult': '操作結果',
'mcpToolCall.prop.width': '寬度',
'mcpToolCall.prop.height': '高度',
'mcpToolCall.prop.screenType': '類型',
'mcpToolCall.prop.chipModel': '晶片型號',
'mcpToolCall.prop.cpuCores': 'CPU核心數',
'mcpToolCall.prop.chipVersion': '晶片版本',
'mcpToolCall.prop.flashSize': 'Flash大小',
'mcpToolCall.prop.minFreeHeap': '最小可用堆',
'mcpToolCall.prop.applicationName': '應用名稱',
'mcpToolCall.prop.applicationVersion': '應用版本',
'mcpToolCall.prop.compileTime': '編譯時間',
'mcpToolCall.prop.idfVersion': 'IDF版本',
'mcpToolCall.prop.macAddress': 'MAC地址',
'mcpToolCall.prop.ipAddress': 'IP地址',
'mcpToolCall.prop.wifiName': 'WiFi名稱',
'mcpToolCall.prop.wifiChannel': 'WiFi信道',
'mcpToolCall.prop.screenSize': '螢幕尺寸',
'mcpToolCall.prop.deviceUuid': '設備UUID',
'mcpToolCall.prop.systemLanguage': '系統語言',
'mcpToolCall.prop.currentOtaPartition': '當前OTA分區',
'mcpToolCall.prop.getResult': '獲取結果',
'mcpToolCall.prop.url': 'URL',
'mcpToolCall.prop.quality': '品質',
'mcpToolCall.prop.question': '問題',
// 工具幫助文本
'mcpToolCall.help.getDeviceStatus': '查看設備的當前運行狀態,包括音量、螢幕、電池等資訊。',
'mcpToolCall.help.setVolume': '調整設備的音量大小,請輸入0-100之間的數值。',
'mcpToolCall.help.setBrightness': '調整設備螢幕的亮度,請輸入0-100之間的數值。',
'mcpToolCall.help.setTheme': '切換設備螢幕的顯示主題,可以選擇淺色或深色模式。',
'mcpToolCall.help.takePhoto': '使用設備攝像頭拍攝照片並進行識別分析,請輸入要詢問的問題。',
'mcpToolCall.help.getSystemInfo': '獲取設備的系統資訊,包括硬體規格、軟體版本等。',
'mcpToolCall.help.reboot': '重啟設備,執行後設備將重新啟動。',
'mcpToolCall.help.upgradeFirmware': '從指定URL下載並升級設備固件,升級後設備會自動重啟。',
'mcpToolCall.help.getScreenInfo': '獲取螢幕的詳細資訊,如解析度、尺寸等參數。',
'mcpToolCall.help.snapshot': '對當前螢幕進行截圖並上傳到指定URL。',
'mcpToolCall.help.previewImage': '在設備螢幕上預覽指定URL的圖片。',
'mcpToolCall.help.setDownloadUrl': '設置設備資源文件的下載地址。',
// 其他文本
'mcpToolCall.text.strong': '強',
'mcpToolCall.text.medium': '中',
'mcpToolCall.text.weak': '弱',
'mcpToolCall.text.dark': '深色',
'mcpToolCall.text.light': '淺色',
'mcpToolCall.text.setSuccess': '設置成功',
'mcpToolCall.text.setFailed': '設置失敗',
'mcpToolCall.text.brightnessSetSuccess': '亮度設置成功',
'mcpToolCall.text.brightnessSetFailed': '亮度設置失敗',
'mcpToolCall.text.themeSetSuccess': '主題設置成功',
'mcpToolCall.text.themeSetFailed': '主題設置失敗',
'mcpToolCall.text.rebootCommandSent': '重啟指令已發送',
'mcpToolCall.text.rebootFailed': '重啟失敗',
'mcpToolCall.text.monochrome': '單色屏',
'mcpToolCall.text.color': '彩色屏',
'mcpToolCall.text.getSuccessParseFailed': '獲取成功,但解析失敗',
'mcpToolCall.text.getFailed': '獲取失敗',
'mcpToolCall.text.getSuccessFormatError': '獲取成功,但數據格式異常',
// 字典數據對話框相關
'dictDataDialog.addDictData': '新增字典數據',
'dictDataDialog.dictLabel': '字典標籤',
@@ -327,6 +458,7 @@ export default {
'device.bindWithCode': '6位驗證碼綁定',
'device.manualAdd': '手動添加',
'device.unbind': '解綁',
'device.toolCall': '工具調用',
'device.selectAtLeastOne': '請至少選擇一條記錄',
'device.confirmBatchUnbind': '確認要解綁選中的 {count} 台設備嗎?',
'device.batchUnbindSuccess': '成功解綁 {count} 台設備',
@@ -342,6 +474,9 @@ export default {
'device.autoUpdateDisabled': '已關閉自動升級',
'device.batchUnbindSuccess': '成功解綁 {count} 台設備',
'device.getFirmwareTypeFailed': '獲取固件類型失敗',
'device.deviceStatus': '狀態',
'device.online': '在線',
'device.offline': '離線',
// 消息提示
'message.success': '操作成功',
+158 -83
View File
@@ -1,12 +1,12 @@
<template>
<div class="welcome">
<HeaderBar/>
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('device.management') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
@keyup.enter.native="handleSearch" clearable/>
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('device.search') }}</el-button>
</div>
</div>
@@ -16,9 +16,9 @@
<div class="content-area">
<el-card class="device-card" shadow="never">
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
:header-cell-class-name="headerCellClassName" v-loading="loading"
:element-loading-text="$t('deviceManagement.loading')"
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
:header-cell-class-name="headerCellClassName" v-loading="loading"
:element-loading-text="$t('deviceManagement.loading')" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
@@ -29,74 +29,83 @@
{{ getFirmwareTypeName(scope.row.model) }}
</template>
</el-table-column>
<el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion" align="center"></el-table-column>
<el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion"
align="center"></el-table-column>
<el-table-column :label="$t('device.macAddress')" prop="macAddress" align="center"></el-table-column>
<el-table-column :label="$t('device.bindTime')" prop="bindTime" align="center"></el-table-column>
<el-table-column :label="$t('device.lastConversation')" prop="lastConversation" align="center"></el-table-column>
<el-table-column :label="$t('device.lastConversation')" prop="lastConversation"
align="center"></el-table-column>
<el-table-column :label="$t('device.deviceStatus')" prop="deviceStatus" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.remark')" align="center">
<template #default="{ row }">
<el-input
v-show="row.isEdit"
v-model="row.remark"
size="mini"
maxlength="64"
show-word-limit
@blur="onRemarkBlur(row)"
@keyup.enter.native="onRemarkEnter(row)"
/>
<el-input v-show="row.isEdit" v-model="row.remark" size="mini" maxlength="64" show-word-limit
@blur="onRemarkBlur(row)" @keyup.enter.native="onRemarkEnter(row)" />
<span v-show="!row.isEdit" class="remark-view">
<i
class="el-icon-edit"
@click="row.isEdit = true"
style="cursor: pointer;"
></i>
<span @click="row.isEdit = true">
{{ row.remark || '-' }}
<i class="el-icon-edit" @click="row.isEdit = true" style="cursor: pointer;"></i>
<span @click="row.isEdit = true">
{{ row.remark || '-' }}
</span>
</span>
</span>
</template>
</el-table-column>
<el-table-column :label="$t('device.autoUpdate')" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
@change="handleOtaSwitchChange(scope.row)"></el-switch>
@change="handleOtaSwitchChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column :label="$t('device.operation')" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
{{ $t('device.unbind') }}
</el-button>
{{ $t('device.unbind') }}
</el-button>
<el-button v-if="scope.row.deviceStatus === 'online'" size="mini" type="text" @click="handleMcpToolCall(scope.row.device_id)">
{{ $t('device.toolCall') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
{{ $t('device.bindWithCode') }}
</el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
{{ $t('device.manualAdd') }}
</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{ $t('device.unbind') }}</el-button>
</div>
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
{{ $t('device.bindWithCode') }}
</el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
{{ $t('device.manualAdd') }}
</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{
$t('device.unbind')
}}</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{ $t('dictManagement.firstPage') }}</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{ $t('dictManagement.prevPage') }}</button>
<el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
$t('dictManagement.firstPage')
}}</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">{{
$t('dictManagement.prevPage')
}}</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{ $t('dictManagement.nextPage') }}</button>
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}</span>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
$t('dictManagement.nextPage') }}</button>
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length)
}}</span>
</div>
</div>
</el-card>
@@ -105,9 +114,10 @@
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
@refresh="fetchBindDevices(currentAgentId)"/>
@refresh="fetchBindDevices(currentAgentId)" />
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
@refresh="fetchBindDevices(currentAgentId)"/>
@refresh="fetchBindDevices(currentAgentId)" />
<McpToolCallDialog :visible.sync="mcpToolCallDialogVisible" :device-id="selectedDeviceId" />
</div>
</template>
@@ -115,19 +125,23 @@
<script>
import Api from '@/apis/api';
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
import McpToolCallDialog from "@/components/McpToolCallDialog.vue";
export default {
components: {
HeaderBar,
HeaderBar,
AddDeviceDialog,
ManualAddDeviceDialog
ManualAddDeviceDialog,
McpToolCallDialog
},
data() {
return {
addDeviceDialogVisible: false,
manualAddDeviceDialogVisible: false,
mcpToolCallDialogVisible: false,
selectedDeviceId: '',
searchKeyword: "",
activeSearchKeyword: "",
currentAgentId: this.$route.query.agentId || '',
@@ -145,8 +159,8 @@ export default {
const keyword = this.activeSearchKeyword.toLowerCase();
if (!keyword) return this.deviceList;
return this.deviceList.filter(device =>
(device.model && device.model.toLowerCase().includes(keyword)) ||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
(device.model && device.model.toLowerCase().includes(keyword)) ||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
);
},
@@ -160,8 +174,8 @@ export default {
},
// 计算当前页是否全选
isCurrentPageAllSelected() {
return this.paginatedDeviceList.length > 0 &&
this.paginatedDeviceList.every(device => device.selected);
return this.paginatedDeviceList.length > 0 &&
this.paginatedDeviceList.every(device => device.selected);
},
visiblePages() {
const pages = [];
@@ -236,7 +250,7 @@ export default {
batchUnbindDevices(deviceIds) {
const promises = deviceIds.map(id => {
return new Promise((resolve, reject) => {
Api.device.unbindDevice(id, ({data}) => {
Api.device.unbindDevice(id, ({ data }) => {
if (data.code === 0) {
resolve();
} else {
@@ -246,19 +260,19 @@ export default {
});
});
Promise.all(promises)
.then(() => {
this.$message.success({
message: this.$t('device.batchUnbindSuccess').replace('{count}', deviceIds.length),
showClose: true
});
this.fetchBindDevices(this.currentAgentId);
})
.catch(error => {
this.$message.error({
message: error || this.$t('device.batchUnbindError'),
showClose: true
});
.then(() => {
this.$message.success({
message: this.$t('device.batchUnbindSuccess').replace('{count}', deviceIds.length),
showClose: true
});
this.fetchBindDevices(this.currentAgentId);
})
.catch(error => {
this.$message.error({
message: error || this.$t('device.batchUnbindError'),
showClose: true
});
});
},
handleAddDevice() {
this.addDeviceDialogVisible = true;
@@ -266,6 +280,11 @@ export default {
handleManualAddDevice() {
this.manualAddDeviceDialogVisible = true;
},
handleMcpToolCall(deviceId) {
this.selectedDeviceId = deviceId;
this.mcpToolCallDialogVisible = true;
},
submitRemark(row) {
if (row._submitting) return;
@@ -308,18 +327,18 @@ export default {
cancelButtonText: this.$t('button.cancel'),
type: 'warning'
}).then(() => {
Api.device.unbindDevice(device_id, ({data}) => {
Api.device.unbindDevice(device_id, ({ data }) => {
if (data.code === 0) {
this.$message.success({
message: this.$t('device.unbindSuccess'),
showClose: true
});
this.$message.success({
message: this.$t('device.unbindSuccess'),
showClose: true
});
this.fetchBindDevices(this.$route.query.agentId);
} else {
this.$message.error({
message: data.msg || this.$t('device.unbindFailed'),
showClose: true
});
this.$message.error({
message: data.msg || this.$t('device.unbindFailed'),
showClose: true
});
}
});
});
@@ -339,7 +358,7 @@ export default {
fetchBindDevices(agentId) {
this.loading = true;
Api.device.getAgentBindDevices(agentId, ({data}) => {
Api.device.getAgentBindDevices(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
this.deviceList = data.data.map(device => {
@@ -356,18 +375,74 @@ export default {
_submitting: false,
otaSwitch: device.autoUpdate === 1,
rawBindTime: new Date(device.createDate).getTime(),
selected: false
selected: false,
// 初始设置为离线状态
deviceStatus: 'offline'
};
})
.sort((a, b) => a.rawBindTime - b.rawBindTime);
.sort((a, b) => a.rawBindTime - b.rawBindTime);
this.activeSearchKeyword = "";
this.searchKeyword = "";
// 获取设备列表后,立即获取设备状态
this.fetchDeviceStatus(agentId);
} else {
this.$message.error(data.msg || this.$t('device.getListFailed'));
}
});
},
headerCellClassName({columnIndex}) {
// 获取设备状态
fetchDeviceStatus(agentId) {
Api.device.getDeviceStatus(agentId, ({ data }) => {
if (data.code === 0) {
try {
// 解析后端返回的设备状态JSON
const statusData = JSON.parse(data.data);
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
if (statusData && typeof statusData === 'object') {
// 更新设备状态
this.updateDeviceStatusFromResponse(statusData);
}
} catch (error) {
// JSON解析失败,忽略状态更新
}
}
});
},
// 根据API响应更新设备状态
updateDeviceStatusFromResponse(deviceStatusMap) {
this.deviceList.forEach(device => {
// 构建设备的MQTT客户端ID
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
// 从状态映射中获取设备状态
if (deviceStatusMap[mqttClientId]) {
const statusInfo = deviceStatusMap[mqttClientId];
let isOnline = false;
if (statusInfo.isAlive === true) {
isOnline = true;
} else if (statusInfo.isAlive === false) {
isOnline = false;
} else if (statusInfo.isAlive === null && statusInfo.exists === true) {
isOnline = true;
} else {
isOnline = false;
}
device.deviceStatus = isOnline ? 'online' : 'offline';
} else {
// 如果没有找到对应的状态信息,默认为离线
device.deviceStatus = 'offline';
}
});
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
@@ -378,12 +453,12 @@ export default {
return firmwareType ? firmwareType.name : type
},
updateDeviceInfo(device_id, payload, callback) {
return Api.device.updateDeviceInfo(device_id, payload, ({data}) => {
return Api.device.updateDeviceInfo(device_id, payload, ({ data }) => {
callback(data.code === 0, data);
})
},
handleOtaSwitchChange(row) {
this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => {
this.updateDeviceInfo(row.device_id, { autoUpdate: row.otaSwitch ? 1 : 0 }, (result, { msg }) => {
if (result) {
this.$message.success(row.otaSwitch ? this.$t('device.autoUpdateEnabled') : this.$t('device.autoUpdateDisabled'));
return;
+11 -12
View File
@@ -16,8 +16,8 @@
<div class="content-area">
<el-card class="user-card" shadow="never">
<el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
:element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
:element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
@@ -36,13 +36,13 @@
<el-table-column :label="$t('modelConfig.action')" align="center" width="300px">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="resetPassword(scope.row)">{{ $t('user.resetPassword')
}}</el-button>
}}</el-button>
<el-button size="mini" type="text" v-if="scope.row.status === 1"
@click="handleChangeStatus(scope.row, 0)">{{ $t('user.disableAccount') }}</el-button>
<el-button size="mini" type="text" v-if="scope.row.status === 0"
@click="handleChangeStatus(scope.row, 1)">{{ $t('user.enableAccount') }}</el-button>
<el-button size="mini" type="text" @click="deleteUser(scope.row)">{{ $t('user.deleteUser')
}}</el-button>
}}</el-button>
</template>
</el-table-column>
</el-table>
@@ -54,19 +54,18 @@
</el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">{{
$t('user.enable')
}}</el-button>
}}</el-button>
<el-button size="mini" type="warning" @click="batchDisable"><i
class="el-icon-remove-outline rotated-icon"></i>{{
$t('user.disable') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">{{ $t('user.delete')
}}</el-button>
}}</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange"
:class="['page-size-select', { 'page-size-select-en': isEnglish }]">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('modelConfig.itemsPerPage', { items: item })"
:value="item">
:label="$t('modelConfig.itemsPerPage', { items: item })" :value="item">
</el-option>
</el-select>
@@ -82,7 +81,7 @@
</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('modelConfig.nextPage') }}
</button>
<span class="total-text">{{ $t('modelConfig.totalRecords', { count: total }) }}</span>
<span class="total-text">{{ $t('modelConfig.totalRecords', { total: total }) }}</span>
</div>
</div>
</el-card>
@@ -347,7 +346,7 @@ export default {
this.$message.warning(this.$t('user.selectUsersFirst'));
return;
}
const userIds = this.selection.map(item => item.userId);
this.$confirm(this.$t('user.confirmDeleteSelected', { count: this.selection.length }), this.$t('common.warning'), {
confirmButtonText: this.$t('common.confirm'),
@@ -380,10 +379,10 @@ export default {
this.$message.warning(this.$t('user.selectUsersFirst'));
return;
}
const actionText = status === 1 ? this.$t('user.enable') : this.$t('user.disable');
const userIds = this.selection.map(item => item.userId);
this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: this.selection.length }), this.$t('common.warning'), {
confirmButtonText: this.$t('common.confirm'),
cancelButtonText: this.$t('common.cancel'),