#
Junjie
2026-01-19 5c6fe2af0d00d448fae31d6c4e5bab3a63d54192
#
7个文件已添加
3个文件已修改
2026 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/TvDeviceController.java 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/TvDevice.java 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/TvDeviceMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/TvDeviceService.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/TvDeviceServiceImpl.java 369 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/TvDeviceMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/index.html 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/tvDevice/tvDevice.html 903 ●●●●● 补丁 | 查看 | 原始文档 | 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>