| src/main/java/com/zy/asrs/controller/TvDeviceController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/entity/TvDevice.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/mapper/TvDeviceMapper.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/TvDeviceService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/impl/TvDeviceServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/mapper/TvDeviceMapper.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/index.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/tvDevice/tvDevice.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/asrs/controller/TvDeviceController.java
New file @@ -0,0 +1,333 @@ package com.zy.asrs.controller; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.baomidou.mybatisplus.plugins.Page; import com.core.annotations.ManagerAuth; import com.core.common.Cools; import com.core.common.R; import com.core.controller.AbstractBaseController; import com.zy.asrs.entity.ApkBuildTask; import com.zy.asrs.entity.TvDevice; import com.zy.asrs.service.ApkBuildTaskService; import com.zy.asrs.service.TvDeviceService; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; /** * 电视机设备Controller */ @RestController public class TvDeviceController extends AbstractBaseController { @Autowired private TvDeviceService tvDeviceService; @Autowired private ApkBuildTaskService apkBuildTaskService; @Value("${apk-build.apk-download-path}") private String apkDownloadPath; /** * 查询单个设备 */ @RequestMapping(value = "/tvDevice/{id}/auth") @ManagerAuth public R get(@PathVariable("id") Long id) { return R.ok(tvDeviceService.selectById(id)); } /** * 分页查询设备列表 */ @RequestMapping(value = "/tvDevice/list/auth") @ManagerAuth public R list(@RequestParam(defaultValue = "1") Integer curr, @RequestParam(defaultValue = "10") Integer limit, @RequestParam Map<String, Object> param) { excludeTrash(param); EntityWrapper<TvDevice> wrapper = new EntityWrapper<>(); for (Map.Entry<String, Object> entry : param.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value == null || "".equals(value.toString())) { continue; } if ("status".equals(key)) { wrapper.eq(key, value); } else { wrapper.like(key, String.valueOf(value)); } } wrapper.orderBy("id", false); return R.ok(tvDeviceService.selectPage(new Page<>(curr, limit), wrapper)); } /** * 获取所有设备(不分页) */ @RequestMapping(value = "/tvDevice/all/auth") @ManagerAuth public R listAll() { EntityWrapper<TvDevice> wrapper = new EntityWrapper<>(); wrapper.orderBy("name", true); return R.ok(tvDeviceService.selectList(wrapper)); } /** * 获取所有在线设备 */ @RequestMapping(value = "/tvDevice/online/auth") @ManagerAuth public R listOnline() { return R.ok(tvDeviceService.getOnlineDevices()); } /** * 新增设备 */ @RequestMapping(value = "/tvDevice/add/auth", method = RequestMethod.POST) @ManagerAuth public R add(@RequestBody TvDevice device) { if (Cools.isEmpty(device.getName())) { return R.error("设备名称不能为空"); } if (Cools.isEmpty(device.getIp())) { return R.error("设备IP不能为空"); } // 检查IP是否已存在 EntityWrapper<TvDevice> wrapper = new EntityWrapper<>(); wrapper.eq("ip", device.getIp()); if (tvDeviceService.selectCount(wrapper) > 0) { return R.error("该IP已存在"); } if (device.getPort() == null) { device.setPort(5555); } device.setStatus((short) 0); device.setCreateTime(new Date()); device.setUpdateTime(new Date()); tvDeviceService.insert(device); return R.ok(device); } /** * 更新设备 */ @RequestMapping(value = "/tvDevice/update/auth", method = RequestMethod.POST) @ManagerAuth public R update(@RequestBody TvDevice device) { if (device.getId() == null) { return R.error("设备ID不能为空"); } // 检查IP是否已被其他设备使用 if (!Cools.isEmpty(device.getIp())) { EntityWrapper<TvDevice> wrapper = new EntityWrapper<>(); wrapper.eq("ip", device.getIp()).ne("id", device.getId()); if (tvDeviceService.selectCount(wrapper) > 0) { return R.error("该IP已被其他设备使用"); } } device.setUpdateTime(new Date()); tvDeviceService.updateById(device); return R.ok(); } /** * 删除设备 */ @RequestMapping(value = "/tvDevice/delete/auth", method = RequestMethod.POST) @ManagerAuth public R delete(Long[] ids) { if (ids == null || ids.length == 0) { return R.error(); } tvDeviceService.deleteBatchIds(Arrays.asList(ids)); return R.ok(); } /** * 测试连接 */ @RequestMapping(value = "/tvDevice/testConnection/{id}/auth", method = RequestMethod.POST) @ManagerAuth public R testConnection(@PathVariable("id") Long id) { try { String result = tvDeviceService.testConnection(id); Map<String, Object> data = new HashMap<>(); data.put("result", result); data.put("device", tvDeviceService.selectById(id)); return R.ok(data); } catch (Exception e) { return R.error("连接失败: " + e.getMessage()); } } /** * 刷新所有设备状态 */ @RequestMapping(value = "/tvDevice/refreshAll/auth", method = RequestMethod.POST) @ManagerAuth public R refreshAllStatus() { try { int count = tvDeviceService.refreshAllStatus(); Map<String, Object> data = new HashMap<>(); data.put("refreshedCount", count); return R.ok(data); } catch (Exception e) { return R.error("刷新失败: " + e.getMessage()); } } /** * 安装打包任务的APK到设备 */ @RequestMapping(value = "/tvDevice/installFromTask/auth", method = RequestMethod.POST) @ManagerAuth public R installFromTask(@RequestBody JSONObject param) { try { Long taskId = param.getLong("taskId"); List<Long> deviceIds = param.getJSONArray("deviceIds").toJavaList(Long.class); if (taskId == null) { return R.error("请选择打包任务"); } if (deviceIds == null || deviceIds.isEmpty()) { return R.error("请选择安装设备"); } ApkBuildTask task = apkBuildTaskService.selectById(taskId); if (task == null) { return R.error("打包任务不存在"); } if (task.getApkPath() == null) { return R.error("APK文件未下载,请先下载"); } List<String> results = tvDeviceService.batchInstallApk(deviceIds, task.getApkPath()); return R.ok(results); } catch (Exception e) { return R.error("安装失败: " + e.getMessage()); } } /** * 上传APK并安装到设备 */ @RequestMapping(value = "/tvDevice/uploadAndInstall/auth", method = RequestMethod.POST) @ManagerAuth public R uploadAndInstall(@RequestParam("file") MultipartFile file, @RequestParam("deviceIds") String deviceIdsStr) { try { if (file.isEmpty()) { return R.error("请选择APK文件"); } String originalFilename = file.getOriginalFilename(); if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".apk")) { return R.error("请上传APK文件"); } // 解析设备ID List<Long> deviceIds = new ArrayList<>(); for (String idStr : deviceIdsStr.split(",")) { if (!idStr.trim().isEmpty()) { deviceIds.add(Long.parseLong(idStr.trim())); } } if (deviceIds.isEmpty()) { return R.error("请选择安装设备"); } // 保存文件 Path uploadDir = Paths.get(apkDownloadPath, "uploads"); if (!Files.exists(uploadDir)) { Files.createDirectories(uploadDir); } String fileName = System.currentTimeMillis() + "_" + originalFilename; Path filePath = uploadDir.resolve(fileName); file.transferTo(filePath.toFile()); // 安装到设备 List<String> results = tvDeviceService.batchInstallApk(deviceIds, filePath.toString()); Map<String, Object> data = new HashMap<>(); data.put("results", results); data.put("apkPath", filePath.toString()); return R.ok(data); } catch (Exception e) { return R.error("安装失败: " + e.getMessage()); } } /** * 启动设备上的应用 */ @RequestMapping(value = "/tvDevice/launchApp/{id}/auth", method = RequestMethod.POST) @ManagerAuth public R launchApp(@PathVariable("id") Long id, @RequestBody(required = false) JSONObject param) { try { String packageName = param != null ? param.getString("packageName") : null; String result = tvDeviceService.launchApp(id, packageName); Map<String, Object> data = new HashMap<>(); data.put("result", result); data.put("device", tvDeviceService.selectById(id)); return R.ok(data); } catch (Exception e) { return R.error("启动失败: " + e.getMessage()); } } /** * 批量启动设备上的应用 */ @RequestMapping(value = "/tvDevice/batchLaunchApp/auth", method = RequestMethod.POST) @ManagerAuth public R batchLaunchApp(@RequestBody JSONObject param) { try { List<Long> deviceIds = param.getJSONArray("deviceIds").toJavaList(Long.class); String packageName = param.getString("packageName"); if (deviceIds == null || deviceIds.isEmpty()) { return R.error("请选择设备"); } List<String> results = tvDeviceService.batchLaunchApp(deviceIds, packageName); return R.ok(results); } catch (Exception e) { return R.error("启动失败: " + e.getMessage()); } } /** * 获取设备屏幕截图 */ @RequestMapping(value = "/tvDevice/screenshot/{id}/auth", method = RequestMethod.GET) @ManagerAuth public R captureScreen(@PathVariable("id") Long id) { try { String base64Image = tvDeviceService.captureScreen(id); Map<String, Object> data = new HashMap<>(); data.put("image", base64Image); data.put("device", tvDeviceService.selectById(id)); return R.ok(data); } catch (Exception e) { return R.error("截图失败: " + e.getMessage()); } } } src/main/java/com/zy/asrs/entity/TvDevice.java
New file @@ -0,0 +1,163 @@ package com.zy.asrs.entity; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType; import java.io.Serializable; import java.util.Date; /** * 电视机设备实体类 */ @TableName("tv_device") public class TvDevice implements Serializable { private static final long serialVersionUID = 1L; /** * 主键ID */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 设备名称 */ private String name; /** * 设备IP地址 */ private String ip; /** * ADB端口(默认5555) */ private Integer port; /** * 设备状态:0-离线,1-在线 */ private Short status; /** * 备注 */ private String remark; /** * 最后连接时间 */ @TableField("last_connect_time") private Date lastConnectTime; /** * 创建时间 */ @TableField("create_time") private Date createTime; /** * 更新时间 */ @TableField("update_time") private Date updateTime; public TvDevice() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public Integer getPort() { return port; } public void setPort(Integer port) { this.port = port; } public Short getStatus() { return status; } public void setStatus(Short status) { this.status = status; } public String getStatus$() { if (null == this.status) { return null; } switch (this.status) { case 0: return "离线"; case 1: return "在线"; default: return String.valueOf(this.status); } } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public Date getLastConnectTime() { return lastConnectTime; } public void setLastConnectTime(Date lastConnectTime) { this.lastConnectTime = lastConnectTime; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Date getUpdateTime() { return updateTime; } public void setUpdateTime(Date updateTime) { this.updateTime = updateTime; } /** * 获取完整的ADB地址 */ public String getAdbAddress() { return this.ip + ":" + (this.port != null ? this.port : 5555); } } src/main/java/com/zy/asrs/mapper/TvDeviceMapper.java
New file @@ -0,0 +1,12 @@ package com.zy.asrs.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.asrs.entity.TvDevice; import org.apache.ibatis.annotations.Mapper; /** * 电视机设备Mapper接口 */ @Mapper public interface TvDeviceMapper extends BaseMapper<TvDevice> { } src/main/java/com/zy/asrs/service/TvDeviceService.java
New file @@ -0,0 +1,78 @@ package com.zy.asrs.service; import com.baomidou.mybatisplus.service.IService; import com.zy.asrs.entity.TvDevice; import java.util.List; /** * 电视机设备Service接口 */ public interface TvDeviceService extends IService<TvDevice> { /** * 测试ADB连接 * * @param id 设备ID * @return 连接结果 */ String testConnection(Long id) throws Exception; /** * 批量测试连接状态 * * @return 更新的设备数量 */ int refreshAllStatus(); /** * 安装APK到指定设备 * * @param deviceId 设备ID * @param apkPath APK文件路径 * @return 安装结果 */ String installApk(Long deviceId, String apkPath) throws Exception; /** * 批量安装APK到多台设备 * * @param deviceIds 设备ID列表 * @param apkPath APK文件路径 * @return 安装结果列表 */ List<String> batchInstallApk(List<Long> deviceIds, String apkPath); /** * 获取所有在线设备 * * @return 在线设备列表 */ List<TvDevice> getOnlineDevices(); /** * 启动设备上的应用 * * @param deviceId 设备ID * @param packageName 应用包名(可为null使用默认包名) * @return 启动结果 */ String launchApp(Long deviceId, String packageName) throws Exception; /** * 批量启动设备上的应用 * * @param deviceIds 设备ID列表 * @param packageName 应用包名(可为null使用默认包名) * @return 启动结果列表 */ List<String> batchLaunchApp(List<Long> deviceIds, String packageName); /** * 获取设备屏幕截图 * * @param deviceId 设备ID * @return Base64编码的PNG图片 */ String captureScreen(Long deviceId) throws Exception; } src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java
@@ -48,6 +48,16 @@ @Override public ApkBuildTask triggerBuild(String buildType, String repoAlias, String branch) throws Exception { // 检查是否有正在进行中的任务(状态0=等待中,1=打包中) List<ApkBuildTask> pendingTasks = this.baseMapper.selectPendingTasks(); if (!pendingTasks.isEmpty()) { ApkBuildTask runningTask = pendingTasks.get(0); throw new RuntimeException("已有打包任务正在进行中(ID: " + runningTask.getId() + ", 项目: " + (runningTask.getProjectName() != null ? runningTask.getProjectName() : runningTask.getRepoAlias()) + "),请等待完成后再创建新任务"); } // 构建请求JSON JSONObject requestBody = new JSONObject(); requestBody.put("build_type", buildType); src/main/java/com/zy/asrs/service/impl/TvDeviceServiceImpl.java
New file @@ -0,0 +1,369 @@ package com.zy.asrs.service.impl; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.asrs.entity.TvDevice; import com.zy.asrs.mapper.TvDeviceMapper; import com.zy.asrs.service.TvDeviceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Date; import java.util.List; /** * 电视机设备Service实现类 */ @Service public class TvDeviceServiceImpl extends ServiceImpl<TvDeviceMapper, TvDevice> implements TvDeviceService { private static final Logger log = LoggerFactory.getLogger(TvDeviceServiceImpl.class); @Value("${adb.path}") private String adbPath; @Value("${adb.default-package:com.zy.monitor}") private String defaultPackage; @Override public String testConnection(Long id) throws Exception { TvDevice device = this.selectById(id); if (device == null) { throw new RuntimeException("设备不存在"); } StringBuilder result = new StringBuilder(); String adbAddress = device.getAdbAddress(); try { // 断开已有连接 executeAdbCommand("disconnect", adbAddress); Thread.sleep(500); // 尝试连接 String connectResult = executeAdbCommand("connect", adbAddress); result.append("连接: ").append(connectResult).append("\n"); // 检查连接状态 boolean connected = connectResult.contains("connected") && !connectResult.contains("failed"); if (connected) { // 获取设备信息 String devicesResult = executeAdbCommand("devices"); result.append("设备列表: ").append(devicesResult); // 更新设备状态 device.setStatus((short) 1); device.setLastConnectTime(new Date()); } else { device.setStatus((short) 0); } device.setUpdateTime(new Date()); this.updateById(device); return result.toString(); } catch (Exception e) { device.setStatus((short) 0); device.setUpdateTime(new Date()); this.updateById(device); throw e; } } @Override public int refreshAllStatus() { List<TvDevice> devices = this.selectList(null); int count = 0; for (TvDevice device : devices) { try { testConnection(device.getId()); count++; } catch (Exception e) { log.error("测试设备 {} 连接失败: {}", device.getName(), e.getMessage()); } } return count; } @Override public String installApk(Long deviceId, String apkPath) throws Exception { TvDevice device = this.selectById(deviceId); if (device == null) { throw new RuntimeException("设备不存在"); } File apkFile = new File(apkPath); if (!apkFile.exists()) { throw new RuntimeException("APK文件不存在: " + apkPath); } StringBuilder result = new StringBuilder(); String adbAddress = device.getAdbAddress(); // 先连接设备 String connectResult = executeAdbCommand("connect", adbAddress); result.append("连接: ").append(connectResult).append("\n"); if (connectResult.contains("failed")) { device.setStatus((short) 0); device.setUpdateTime(new Date()); this.updateById(device); throw new RuntimeException("连接设备失败: " + connectResult); } // 等待连接稳定 Thread.sleep(1000); // 安装APK String installResult = executeAdbCommand("-s", adbAddress, "install", "-r", apkPath); result.append("安装: ").append(installResult); // 更新设备状态 device.setStatus((short) 1); device.setLastConnectTime(new Date()); device.setUpdateTime(new Date()); this.updateById(device); log.info("设备 {} 安装APK结果: {}", device.getName(), installResult); return result.toString(); } @Override public List<String> batchInstallApk(List<Long> deviceIds, String apkPath) { List<String> results = new ArrayList<>(); for (Long deviceId : deviceIds) { TvDevice device = this.selectById(deviceId); String deviceName = device != null ? device.getName() : "ID:" + deviceId; try { String result = installApk(deviceId, apkPath); results.add(deviceName + ": 安装成功\n" + result); } catch (Exception e) { results.add(deviceName + ": 安装失败 - " + e.getMessage()); } } return results; } @Override public List<TvDevice> getOnlineDevices() { return this.selectList(new EntityWrapper<TvDevice>().eq("status", 1)); } @Override public String launchApp(Long deviceId, String packageName) throws Exception { TvDevice device = this.selectById(deviceId); if (device == null) { throw new RuntimeException("设备不存在"); } // 使用默认包名如果未指定 String pkg = (packageName != null && !packageName.trim().isEmpty()) ? packageName.trim() : defaultPackage; StringBuilder result = new StringBuilder(); String adbAddress = device.getAdbAddress(); // 先连接设备 String connectResult = executeAdbCommand("connect", adbAddress); result.append("连接: ").append(connectResult).append("\n"); if (connectResult.contains("failed")) { device.setStatus((short) 0); device.setUpdateTime(new Date()); this.updateById(device); throw new RuntimeException("连接设备失败: " + connectResult); } // 等待连接稳定 Thread.sleep(500); // 方法1: 使用cmd package resolve-activity获取启动Activity(Android 7.0+) String launchResult = null; boolean launched = false; try { // 尝试使用dumpsys获取启动Activity String dumpResult = executeAdbCommand("-s", adbAddress, "shell", "cmd", "package", "resolve-activity", "--brief", pkg); if (dumpResult != null && dumpResult.contains("/")) { // 解析出Activity名称 String[] lines = dumpResult.split("\n"); for (String line : lines) { line = line.trim(); if (line.contains("/") && !line.startsWith("priority") && !line.startsWith("#")) { // 找到类似 com.zy.app/.MainActivity 的格式 launchResult = executeAdbCommand("-s", adbAddress, "shell", "am", "start", "-n", line); launched = true; break; } } } } catch (Exception e) { log.warn("cmd package方式失败,尝试备用方式: {}", e.getMessage()); } // 方法2: 如果方法1失败,使用am start -a android.intent.action.MAIN if (!launched) { launchResult = executeAdbCommand("-s", adbAddress, "shell", "am", "start", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER", "-n", pkg + "/.MainActivity"); // 如果还是失败,尝试常见的Activity名称 if (launchResult != null && launchResult.contains("Error")) { // 尝试 .ui.MainActivity launchResult = executeAdbCommand("-s", adbAddress, "shell", "am", "start", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER", "--package", pkg); } } result.append("启动: ").append(launchResult); // 检查是否启动成功 if (launchResult != null && (launchResult.contains("Starting:") || launchResult.contains("cmp="))) { log.info("设备 {} 启动应用 {} 成功", device.getName(), pkg); } else { log.warn("设备 {} 启动应用 {} 可能失败: {}", device.getName(), pkg, launchResult); } // 更新设备状态 device.setStatus((short) 1); device.setLastConnectTime(new Date()); device.setUpdateTime(new Date()); this.updateById(device); return result.toString(); } @Override public List<String> batchLaunchApp(List<Long> deviceIds, String packageName) { List<String> results = new ArrayList<>(); for (Long deviceId : deviceIds) { TvDevice device = this.selectById(deviceId); String deviceName = device != null ? device.getName() : "ID:" + deviceId; try { String result = launchApp(deviceId, packageName); results.add(deviceName + ": 启动成功\n" + result); } catch (Exception e) { results.add(deviceName + ": 启动失败 - " + e.getMessage()); } } return results; } @Override public String captureScreen(Long deviceId) throws Exception { TvDevice device = this.selectById(deviceId); if (device == null) { throw new RuntimeException("设备不存在"); } String adbAddress = device.getAdbAddress(); // 先确保连接 String connectResult = executeAdbCommand("connect", adbAddress); if (connectResult.contains("failed")) { device.setStatus((short) 0); device.setUpdateTime(new Date()); this.updateById(device); throw new RuntimeException("连接设备失败: " + connectResult); } // 使用exec-out直接获取PNG二进制数据 List<String> command = new ArrayList<>(); command.add(adbPath); command.add("-s"); command.add(adbAddress); command.add("exec-out"); command.add("screencap"); command.add("-p"); log.info("执行截图命令: {}", String.join(" ", command)); ProcessBuilder pb = new ProcessBuilder(command); Process process = pb.start(); // 读取二进制PNG数据 ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream is = process.getInputStream(); byte[] buffer = new byte[8192]; int len; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } int exitCode = process.waitFor(); byte[] pngData = baos.toByteArray(); if (exitCode != 0 || pngData.length < 100) { // 读取错误信息 BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); StringBuilder errorMsg = new StringBuilder(); String line; while ((line = errorReader.readLine()) != null) { errorMsg.append(line); } log.error("截图失败,退出码: {}, 数据长度: {}, 错误: {}", exitCode, pngData.length, errorMsg); throw new RuntimeException("截图失败: " + errorMsg); } // 更新设备状态 device.setStatus((short) 1); device.setLastConnectTime(new Date()); device.setUpdateTime(new Date()); this.updateById(device); // 转换为Base64 String base64Image = Base64.getEncoder().encodeToString(pngData); log.info("设备 {} 截图成功,图片大小: {} bytes", device.getName(), pngData.length); return base64Image; } /** * 执行ADB命令 */ private String executeAdbCommand(String... args) throws Exception { List<String> command = new ArrayList<>(); command.add(adbPath); command.addAll(Arrays.asList(args)); log.info("执行ADB命令: {}", String.join(" ", command)); ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(true); Process process = pb.start(); StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } int exitCode = process.waitFor(); String result = output.toString().trim(); if (exitCode != 0 && !result.contains("Success") && !result.contains("connected")) { log.warn("ADB命令执行返回码: {}, 输出: {}", exitCode, result); } return result; } } src/main/resources/application.yml
@@ -27,6 +27,10 @@ port: 6379 database: 0 # password: 123456 servlet: multipart: max-file-size: 100MB max-request-size: 100MB task: scheduling: pool: @@ -67,10 +71,12 @@ download-server-url: http://127.0.0.1:8088 # API认证Token x-token: uzCHJUQSb7tl2ZTjB0wI3XViqMgxRhvYL9EP # APK下载保存路径 apk-download-path: /stock/out/apk/ # APK下载保存路径(必须是绝对路径) apk-download-path: D:/apk_files/ # ADB配置 adb: # ADB可执行文件路径,如果在系统PATH中可直接填写adb path: D:\platform-tools\adb.exe path: D:\platform-tools\adb.exe # 默认APK包名(用于启动应用) default-package: com.zy src/main/resources/mapper/TvDeviceMapper.xml
New file @@ -0,0 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zy.asrs.mapper.TvDeviceMapper"> </mapper> src/main/webapp/views/index.html
@@ -105,13 +105,6 @@ <!-- <li class="layui-nav-item" lay-unselect>--> <!-- <a ew-event="note" title="便签"><i class="layui-icon layui-icon-note"></i></a>--> <!-- </li>--> <li class="layui-nav-item" lay-unselect id="fakeShow" style="display: none;user-select: none;margin-right: 10px;"> <div style="color: red;" id="fakeShowText">仿真模拟运行中</div> </li> <li class="layui-nav-item" lay-unselect id="licenseShow" style="display: none;user-select: none;"> <div style="color: red;">临时许可证有效期:<span id="licenseDays">29</span>天</div> </li> <li class="layui-nav-item layui-hide-xs" lay-unselect> <a ew-event="fullScreen" title="全屏"><i class="layui-icon layui-icon-screen-full"></i></a> </li> @@ -152,17 +145,6 @@ <!--初始化加载层--> <div class="layuimini-loader"> <div class="layuimini-loader-inner"></div> </div> <!-- 弹窗内容 --> <div class="popup" id="popup"> <div class="popup-content"> <h2 style="font-size: 28px;margin-bottom: 10px;">许可证即将过期</h2> <div id="popup-text" style="font-size: 28px;color: red"></div> <button style="background-color: #007bff;color: #fff;border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;font-size: 16px;" onclick="hidePopup()">关闭</button> </div> </div> <!-- 右下角SVG动画 --> @@ -213,30 +195,6 @@ loadSystemVersion(); }); // 显示弹窗 function showPopup(res) { document.getElementById('popup').style.display = 'block'; // 获取弹出窗口内容的容器元素 var popupText = document.getElementById('popup-text'); // 假设后台返回的字符串为 responseString if (res !== "") { // 获取当前日期 const currentDate = new Date(); // 创建新日期对象并添加天数 const newDate = new Date(); newDate.setDate(currentDate.getDate() + res + 1); // 将字符串设置为弹窗内容的文本 popupText.textContent = "许可证将于" + new Intl.DateTimeFormat('zh-CN').format(newDate) + "过期,剩余有效期:" + res + "天!"; } else { document.getElementById('popup').style.display = 'none'; } } // 隐藏弹窗 function hidePopup() { document.getElementById('popup').style.display = 'none'; } layui.config({ base: baseUrl + "/static/layui/lay/modules/" }).extend({ @@ -257,80 +215,6 @@ } } let fakeRunning = false let fakeStatusInterval = null function checkFakeStatus() { $.ajax({ url: baseUrl + "/openapi/getFakeSystemRunStatus", headers: { 'token': localStorage.getItem('token') }, method: 'GET', success: function (res) { if (res.code === 200) { if (res.data.isFake) { $("#fakeShow").show() let running = res.data.running if (running) { $("#fakeShowText").text("仿真模拟运行中") } else { $("#fakeShowText").text("仿真模拟未运行") } fakeRunning = running if (!fakeStatusInterval) { fakeStatusInterval = setInterval(checkFakeStatus, 1000); } } else { $("#fakeShow").hide() if (fakeStatusInterval) { clearInterval(fakeStatusInterval); fakeStatusInterval = null; } } } else { top.location.href = baseUrl + "/login"; } } }); } checkFakeStatus(); $("#fakeShow").on("click", function () { if (fakeRunning) { layer.confirm('确定要停止仿真模拟吗?', function (index) { layer.close(index); $.ajax({ url: baseUrl + "/openapi/stopFakeSystem", headers: { 'token': localStorage.getItem('token') }, method: 'POST', success: function (res) { if (res.code === 200) { layer.msg("仿真模拟已停止", { icon: 1 }); $("#fakeShowText").text("仿真模拟未运行") } else { layer.msg(res.msg, { icon: 2 }); } } }); }); } else { layer.confirm('确定要启动仿真模拟吗?', function (index) { layer.close(index); $.ajax({ url: baseUrl + "/openapi/startFakeSystem", headers: { 'token': localStorage.getItem('token') }, method: 'POST', success: function (res) { if (res.code === 200) { layer.msg("仿真模拟已启动", { icon: 1 }); $("#fakeShowText").text("仿真模拟运行中") } else { layer.msg(res.msg, { icon: 2 }); } } }); }); } }); $.ajax({ url: baseUrl + "/menu/auth", headers: { 'token': localStorage.getItem('token') }, @@ -349,31 +233,6 @@ top.location.href = baseUrl + "/login"; } else { layer.msg(res.msg, { icon: 2 }); } } }); $.ajax({ url: baseUrl + "/license/getLicenseDays", headers: { 'token': localStorage.getItem('token') }, method: 'POST', success: function (res) { if (res.code == 200) { let days = res.data if (days <= 30) { $("#licenseShow").show() $("#licenseDays").html(days) } if (days <= 15) { showPopup(days) } if (days < 0) { top.location.href = baseUrl + "/login"; } } else { top.location.href = baseUrl + "/login"; } } }); src/main/webapp/views/tvDevice/tvDevice.html
New file @@ -0,0 +1,903 @@ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>电视机设备管理</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <link rel="stylesheet" href="../../static/vue/element/element.css"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif; background: #f5f7fa; padding: 15px; } .app-container { background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 20px; } .header-section { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 10px; } .search-section { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } .action-buttons { display: flex; gap: 10px; } .table-section { margin-top: 15px; } .pagination-section { margin-top: 20px; display: flex; justify-content: flex-end; } .dialog-footer { text-align: right; } .form-tip { font-size: 12px; color: #909399; margin-top: 5px; } .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } .status-online { background: #67c23a; } .status-offline { background: #909399; } .install-section { background: #f5f7fa; border-radius: 8px; padding: 20px; margin-top: 20px; } .install-section h3 { margin-bottom: 15px; font-size: 16px; color: #303133; } .install-row { display: flex; gap: 15px; align-items: flex-start; flex-wrap: wrap; } .install-col { flex: 1; min-width: 300px; } .upload-demo { width: 100%; } .install-result { margin-top: 15px; padding: 10px; background: #f0f0f0; border-radius: 4px; white-space: pre-wrap; font-family: monospace; max-height: 200px; overflow-y: auto; } </style> </head> <body> <div id="app"> <div class="app-container" v-loading="tableLoading"> <!-- 头部区域 --> <div class="header-section"> <!-- 搜索区域 --> <div class="search-section"> <el-select v-model="searchForm.status" placeholder="全部状态" clearable size="small" style="width: 120px;"> <el-option label="在线" value="1"></el-option> <el-option label="离线" value="0"></el-option> </el-select> <el-input v-model="searchForm.name" placeholder="设备名称" size="small" style="width: 150px;" clearable></el-input> <el-input v-model="searchForm.ip" placeholder="IP地址" size="small" style="width: 150px;" clearable></el-input> <el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button> <el-button size="small" icon="el-icon-refresh-left" @click="handleReset">重置</el-button> </div> <!-- 操作按钮 --> <div class="action-buttons"> <el-button type="primary" size="small" icon="el-icon-plus" @click="showAddDialog">新增设备</el-button> <el-button type="success" size="small" icon="el-icon-refresh" @click="refreshAllDevices" :loading="refreshing">刷新状态</el-button> <el-button type="danger" size="small" icon="el-icon-delete" @click="handleBatchDelete" :disabled="selectedRows.length === 0">删除</el-button> </div> </div> <!-- 表格区域 --> <div class="table-section"> <el-table :data="tableData" border stripe @selection-change="handleSelectionChange" style="width: 100%;" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"> <el-table-column type="selection" width="50" align="center"></el-table-column> <el-table-column prop="id" label="ID" width="60" align="center"></el-table-column> <el-table-column prop="name" label="设备名称" min-width="150"></el-table-column> <el-table-column prop="ip" label="IP地址" width="140" align="center"></el-table-column> <el-table-column prop="port" label="端口" width="80" align="center"></el-table-column> <el-table-column prop="status" label="状态" width="100" align="center"> <template slot-scope="scope"> <span> <span class="status-dot" :class="scope.row.status === 1 ? 'status-online' : 'status-offline'"></span> {{ scope.row.status === 1 ? '在线' : '离线' }} </span> </template> </el-table-column> <el-table-column prop="lastConnectTime" label="最后连接时间" width="160" align="center"> <template slot-scope="scope"> {{ formatDate(scope.row.lastConnectTime) || '-' }} </template> </el-table-column> <el-table-column prop="remark" label="备注" min-width="100" show-overflow-tooltip></el-table-column> <el-table-column label="操作" width="300" align="center" fixed="right"> <template slot-scope="scope"> <el-button type="text" size="small" @click="showEditDialog(scope.row)">编辑</el-button> <el-button type="text" size="small" @click="testConnection(scope.row)" :loading="scope.row.testing">测试连接</el-button> <el-button type="text" size="small" style="color: #67c23a;" @click="launchApp(scope.row)" :loading="scope.row.launching">启动</el-button> <el-button type="text" size="small" style="color: #409eff;" @click="captureScreen(scope.row)" :loading="scope.row.capturing">截图</el-button> <el-button type="text" size="small" style="color: #f56c6c;" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> </div> <!-- 分页 --> <div class="pagination-section"> <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size="pageSize" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange" @current-change="handlePageChange"> </el-pagination> </div> <!-- 安装APK区域 --> <div class="install-section"> <h3><i class="el-icon-download" style="margin-right: 8px;"></i>安装APK到设备</h3> <div class="install-row"> <!-- 从打包任务安装 --> <div class="install-col"> <el-card shadow="hover"> <div slot="header"> <span>从打包任务安装</span> </div> <el-form label-position="top" size="small"> <el-form-item label="选择打包任务"> <el-select v-model="installForm.taskId" placeholder="请选择已完成的打包任务" style="width: 100%;" filterable> <el-option v-for="task in completedTasks" :key="task.id" :label="task.projectName || task.taskId" :value="task.id" :disabled="!task.apkPath"> <span>{{ task.projectName || task.repoAlias }}</span> <span style="float: right; color: #909399; font-size: 12px;"> {{ task.apkPath ? '已下载' : '未下载' }} </span> </el-option> </el-select> </el-form-item> <el-form-item label="选择目标设备"> <el-select v-model="installForm.deviceIds" multiple placeholder="请选择设备" style="width: 100%;"> <el-option v-for="device in allDevices" :key="device.id" :label="device.name + ' (' + device.ip + ')'" :value="device.id"> </el-option> </el-select> </el-form-item> <el-button type="primary" size="small" @click="installFromTask" :loading="installing" :disabled="!installForm.taskId || installForm.deviceIds.length === 0"> 安装到设备 </el-button> </el-form> </el-card> </div> <!-- 上传APK安装 --> <div class="install-col"> <el-card shadow="hover"> <div slot="header"> <span>上传APK安装</span> </div> <el-form label-position="top" size="small"> <el-form-item label="上传APK文件"> <el-upload class="upload-demo" :action="uploadUrl" :headers="uploadHeaders" :data="uploadData" :before-upload="beforeUpload" :on-success="onUploadSuccess" :on-error="onUploadError" :show-file-list="false" accept=".apk" :disabled="uploadForm.deviceIds.length === 0"> <el-button size="small" type="primary" :disabled="uploadForm.deviceIds.length === 0"> <i class="el-icon-upload"></i> 选择APK文件并安装 </el-button> </el-upload> <div class="form-tip">支持 .apk 格式文件</div> </el-form-item> <el-form-item label="选择目标设备"> <el-select v-model="uploadForm.deviceIds" multiple placeholder="请选择设备" style="width: 100%;"> <el-option v-for="device in allDevices" :key="device.id" :label="device.name + ' (' + device.ip + ')'" :value="device.id"> </el-option> </el-select> </el-form-item> </el-form> </el-card> </div> </div> <!-- 安装结果 --> <div class="install-result" v-if="installResult"> <strong>操作结果:</strong><br> {{ installResult }} </div> </div> <!-- 启动应用区域 --> <div class="install-section"> <h3><i class="el-icon-video-play" style="margin-right: 8px;"></i>启动设备应用</h3> <el-form :inline="true" size="small"> <el-form-item label="应用包名"> <el-input v-model="launchForm.packageName" placeholder="留空使用默认包名" style="width: 280px;"></el-input> </el-form-item> <el-form-item label="选择设备"> <el-select v-model="launchForm.deviceIds" multiple placeholder="请选择设备" style="width: 350px;"> <el-option v-for="device in allDevices" :key="device.id" :label="device.name + ' (' + device.ip + ')'" :value="device.id"> </el-option> </el-select> </el-form-item> <el-form-item> <el-button type="success" @click="batchLaunchApp" :loading="launching" :disabled="launchForm.deviceIds.length === 0"> <i class="el-icon-video-play"></i> 批量启动应用 </el-button> </el-form-item> </el-form> <div class="form-tip">默认包名在 application.yml 中配置(adb.default-package)</div> </div> </div> <!-- 新增/编辑设备对话框 --> <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="450px" :close-on-click-modal="false"> <el-form :model="deviceForm" :rules="deviceRules" ref="deviceFormRef" label-width="100px"> <el-form-item label="设备名称" prop="name"> <el-input v-model="deviceForm.name" placeholder="请输入设备名称"></el-input> </el-form-item> <el-form-item label="IP地址" prop="ip"> <el-input v-model="deviceForm.ip" placeholder="请输入IP地址,如 192.168.1.100"></el-input> </el-form-item> <el-form-item label="ADB端口" prop="port"> <el-input-number v-model="deviceForm.port" :min="1" :max="65535" style="width: 100%;"></el-input-number> <div class="form-tip">默认端口为 5555</div> </el-form-item> <el-form-item label="备注"> <el-input type="textarea" v-model="deviceForm.remark" placeholder="可选,输入备注信息"></el-input> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitDevice" :loading="submitting">确定</el-button> </div> </el-dialog> <!-- 截图预览对话框 --> <el-dialog :title="'设备截图 - ' + (screenshotDevice ? screenshotDevice.name : '')" :visible.sync="screenshotDialogVisible" width="800px" :close-on-click-modal="false" @close="stopAutoRefresh"> <div v-loading="screenshotLoading" style="text-align: center; min-height: 300px;"> <img v-if="screenshotImage" :src="'data:image/png;base64,' + screenshotImage" style="max-width: 100%; max-height: 500px; border: 1px solid #eee; border-radius: 4px;" /> <div v-else style="padding: 100px; color: #909399;">正在获取截图...</div> </div> <div slot="footer" class="dialog-footer"> <el-checkbox v-model="autoRefreshScreenshot" @change="toggleAutoRefresh">自动刷新</el-checkbox> <span style="margin-left: 10px; color: #909399; font-size: 12px;" v-if="autoRefreshScreenshot">每2秒刷新一次</span> <el-button @click="screenshotDialogVisible = false" style="margin-left: 20px;">关闭</el-button> <el-button type="primary" @click="refreshScreenshot" :loading="screenshotLoading">刷新截图</el-button> </div> </el-dialog> </div> <script src="../../static/vue/js/vue.min.js"></script> <script src="../../static/vue/element/element.js"></script> <script src="../../static/js/jquery/jquery-3.3.1.min.js"></script> <script src="../../static/js/common.js"></script> <script> new Vue({ el: '#app', data: { // 表格数据 tableData: [], tableLoading: false, total: 0, currentPage: 1, pageSize: 10, selectedRows: [], refreshing: false, // 搜索表单 searchForm: { status: '', name: '', ip: '' }, // 设备对话框 dialogVisible: false, dialogTitle: '新增设备', submitting: false, isEdit: false, deviceForm: { id: null, name: '', ip: '', port: 5555, remark: '' }, deviceRules: { name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], ip: [ { required: true, message: '请输入IP地址', trigger: 'blur' }, { pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: 'IP地址格式不正确', trigger: 'blur' } ], port: [{ required: true, message: '请输入端口', trigger: 'blur' }] }, // 安装相关 allDevices: [], completedTasks: [], installing: false, installResult: '', installForm: { taskId: '', deviceIds: [] }, uploadForm: { deviceIds: [] }, // 启动应用相关 launching: false, launchForm: { packageName: '', deviceIds: [] }, // 截图相关 screenshotDialogVisible: false, screenshotDevice: null, screenshotImage: '', screenshotLoading: false, autoRefreshScreenshot: false, screenshotTimer: null }, computed: { uploadUrl() { return baseUrl + '/tvDevice/uploadAndInstall/auth'; }, uploadHeaders() { return { 'token': localStorage.getItem('token') }; }, uploadData() { return { deviceIds: this.uploadForm.deviceIds.join(',') }; } }, created() { this.loadData(); this.loadAllDevices(); this.loadCompletedTasks(); }, methods: { // 获取请求头 getHeaders() { return { 'token': localStorage.getItem('token') }; }, // 加载数据 loadData() { this.tableLoading = true; const params = { curr: this.currentPage, limit: this.pageSize, ...this.searchForm }; $.ajax({ url: baseUrl + '/tvDevice/list/auth', headers: this.getHeaders(), data: params, success: (res) => { this.tableLoading = false; if (res.code === 200) { this.tableData = res.data.records || []; this.total = res.data.total || 0; } else if (res.code === 403) { top.location.href = baseUrl + '/'; } else { this.$message.error(res.msg || '加载失败'); } }, error: () => { this.tableLoading = false; this.$message.error('请求失败'); } }); }, // 加载所有设备 loadAllDevices() { $.ajax({ url: baseUrl + '/tvDevice/all/auth', headers: this.getHeaders(), success: (res) => { if (res.code === 200) { this.allDevices = res.data || []; } } }); }, // 加载已完成的打包任务 loadCompletedTasks() { $.ajax({ url: baseUrl + '/apkBuildTask/list/auth', headers: this.getHeaders(), data: { status: 2, limit: 100 }, success: (res) => { if (res.code === 200) { this.completedTasks = res.data.records || []; } } }); }, // 刷新所有设备状态 refreshAllDevices() { this.refreshing = true; $.ajax({ url: baseUrl + '/tvDevice/refreshAll/auth', headers: this.getHeaders(), method: 'POST', success: (res) => { this.refreshing = false; if (res.code === 200) { this.$message.success(`刷新完成,检测了 ${res.data.refreshedCount} 台设备`); this.loadData(); this.loadAllDevices(); } else { this.$message.error(res.msg || '刷新失败'); } }, error: () => { this.refreshing = false; this.$message.error('请求失败'); } }); }, // 测试连接 testConnection(row) { this.$set(row, 'testing', true); $.ajax({ url: baseUrl + '/tvDevice/testConnection/' + row.id + '/auth', headers: this.getHeaders(), method: 'POST', success: (res) => { this.$set(row, 'testing', false); if (res.code === 200) { // 更新行数据 Object.assign(row, res.data.device); this.$message.success('连接成功'); this.$alert(res.data.result, '连接结果'); } else { this.$message.error(res.msg || '连接失败'); } }, error: () => { this.$set(row, 'testing', false); this.$message.error('请求失败'); } }); }, // 截图 captureScreen(row) { this.screenshotDevice = row; this.screenshotImage = ''; this.autoRefreshScreenshot = false; this.screenshotDialogVisible = true; this.refreshScreenshot(); }, // 刷新截图 refreshScreenshot() { if (!this.screenshotDevice) return; this.screenshotLoading = true; $.ajax({ url: baseUrl + '/tvDevice/screenshot/' + this.screenshotDevice.id + '/auth', headers: this.getHeaders(), method: 'GET', success: (res) => { this.screenshotLoading = false; if (res.code === 200) { this.screenshotImage = res.data.image; // 更新设备状态 if (res.data.device) { Object.assign(this.screenshotDevice, res.data.device); } } else { this.$message.error(res.msg || '截图失败'); } }, error: () => { this.screenshotLoading = false; this.$message.error('请求失败'); } }); }, // 切换自动刷新 toggleAutoRefresh(enabled) { if (enabled) { this.screenshotTimer = setInterval(() => { if (!this.screenshotLoading) { this.refreshScreenshot(); } }, 2000); } else { this.stopAutoRefresh(); } }, // 停止自动刷新 stopAutoRefresh() { if (this.screenshotTimer) { clearInterval(this.screenshotTimer); this.screenshotTimer = null; } this.autoRefreshScreenshot = false; }, // 单个设备启动应用 launchApp(row) { this.$set(row, 'launching', true); $.ajax({ url: baseUrl + '/tvDevice/launchApp/' + row.id + '/auth', headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ packageName: this.launchForm.packageName }), success: (res) => { this.$set(row, 'launching', false); if (res.code === 200) { Object.assign(row, res.data.device); this.$message.success('启动成功'); this.installResult = res.data.result; } else { this.$message.error(res.msg || '启动失败'); } }, error: () => { this.$set(row, 'launching', false); this.$message.error('请求失败'); } }); }, // 批量启动应用 batchLaunchApp() { if (this.launchForm.deviceIds.length === 0) { this.$message.warning('请选择设备'); return; } this.launching = true; this.installResult = ''; $.ajax({ url: baseUrl + '/tvDevice/batchLaunchApp/auth', headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ deviceIds: this.launchForm.deviceIds, packageName: this.launchForm.packageName }), success: (res) => { this.launching = false; if (res.code === 200) { this.installResult = res.data.join('\n'); this.$message.success('启动完成'); this.loadData(); } else { this.$message.error(res.msg || '启动失败'); } }, error: () => { this.launching = false; this.$message.error('请求失败'); } }); }, // 搜索 handleSearch() { this.currentPage = 1; this.loadData(); }, // 重置 handleReset() { this.searchForm = { status: '', name: '', ip: '' }; this.currentPage = 1; this.loadData(); }, // 分页 handlePageChange(page) { this.currentPage = page; this.loadData(); }, handleSizeChange(size) { this.pageSize = size; this.currentPage = 1; this.loadData(); }, // 选择行 handleSelectionChange(selection) { this.selectedRows = selection; }, // 显示新增对话框 showAddDialog() { this.dialogTitle = '新增设备'; this.isEdit = false; this.deviceForm = { id: null, name: '', ip: '', port: 5555, remark: '' }; this.dialogVisible = true; this.$nextTick(() => { this.$refs.deviceFormRef && this.$refs.deviceFormRef.clearValidate(); }); }, // 显示编辑对话框 showEditDialog(row) { this.dialogTitle = '编辑设备'; this.isEdit = true; this.deviceForm = { ...row }; this.dialogVisible = true; this.$nextTick(() => { this.$refs.deviceFormRef && this.$refs.deviceFormRef.clearValidate(); }); }, // 提交设备 submitDevice() { this.$refs.deviceFormRef.validate((valid) => { if (!valid) return; this.submitting = true; const url = this.isEdit ? '/tvDevice/update/auth' : '/tvDevice/add/auth'; $.ajax({ url: baseUrl + url, headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify(this.deviceForm), success: (res) => { this.submitting = false; if (res.code === 200) { this.$message.success(this.isEdit ? '更新成功' : '添加成功'); this.dialogVisible = false; this.loadData(); this.loadAllDevices(); } else { this.$message.error(res.msg || '操作失败'); } }, error: () => { this.submitting = false; this.$message.error('请求失败'); } }); }); }, // 删除单个 handleDelete(row) { this.$confirm(`确定删除设备 "${row.name}" 吗?`, '提示', { type: 'warning' }).then(() => { $.ajax({ url: baseUrl + '/tvDevice/delete/auth', headers: this.getHeaders(), method: 'POST', data: { ids: [row.id] }, traditional: true, success: (res) => { if (res.code === 200) { this.$message.success('删除成功'); this.loadData(); this.loadAllDevices(); } else { this.$message.error(res.msg || '删除失败'); } } }); }).catch(() => { }); }, // 批量删除 handleBatchDelete() { if (this.selectedRows.length === 0) return; this.$confirm(`确定删除选中的 ${this.selectedRows.length} 台设备吗?`, '提示', { type: 'warning' }).then(() => { const ids = this.selectedRows.map(row => row.id); $.ajax({ url: baseUrl + '/tvDevice/delete/auth', headers: this.getHeaders(), method: 'POST', data: { ids: ids }, traditional: true, success: (res) => { if (res.code === 200) { this.$message.success('删除成功'); this.loadData(); this.loadAllDevices(); } else { this.$message.error(res.msg || '删除失败'); } } }); }).catch(() => { }); }, // 从打包任务安装 installFromTask() { if (!this.installForm.taskId || this.installForm.deviceIds.length === 0) { this.$message.warning('请选择打包任务和目标设备'); return; } this.installing = true; this.installResult = ''; $.ajax({ url: baseUrl + '/tvDevice/installFromTask/auth', headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ taskId: this.installForm.taskId, deviceIds: this.installForm.deviceIds }), success: (res) => { this.installing = false; if (res.code === 200) { this.installResult = res.data.join('\n'); this.$message.success('安装完成'); this.loadData(); } else { this.$message.error(res.msg || '安装失败'); } }, error: () => { this.installing = false; this.$message.error('请求失败'); } }); }, // 上传前检查 beforeUpload(file) { if (this.uploadForm.deviceIds.length === 0) { this.$message.warning('请先选择目标设备'); return false; } const isApk = file.name.toLowerCase().endsWith('.apk'); if (!isApk) { this.$message.error('只能上传APK文件'); return false; } this.installResult = ''; this.$message.info('正在上传并安装...'); return true; }, // 上传成功 onUploadSuccess(res, file) { if (res.code === 200) { this.installResult = res.data.results.join('\n'); this.$message.success('安装完成'); this.loadData(); } else { this.$message.error(res.msg || '安装失败'); } }, // 上传失败 onUploadError(err, file) { this.$message.error('上传失败'); }, // 格式化日期 formatDate(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } } }); </script> </body> </html>