| src/main/java/com/zy/asrs/controller/ApkBuildTaskController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/entity/ApkBuildTask.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/mapper/ApkBuildTaskMapper.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/ApkBuildTaskService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/mapper/ApkBuildTaskMapper.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/sdk/platform-tools-latest-windows.zip | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/apkBuild/apkBuild.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/asrs/controller/ApkBuildTaskController.java
New file @@ -0,0 +1,223 @@ 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.DateUtils; import com.core.common.R; import com.core.controller.AbstractBaseController; import com.zy.asrs.entity.ApkBuildTask; import com.zy.asrs.service.ApkBuildTaskService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * APK打包任务Controller */ @RestController public class ApkBuildTaskController extends AbstractBaseController { @Autowired private ApkBuildTaskService apkBuildTaskService; /** * 查询单个任务 */ @RequestMapping(value = "/apkBuildTask/{id}/auth") @ManagerAuth public R get(@PathVariable("id") Long id) { return R.ok(apkBuildTaskService.selectById(id)); } /** * 分页查询任务列表 */ @RequestMapping(value = "/apkBuildTask/list/auth") @ManagerAuth public R list(@RequestParam(defaultValue = "1") Integer curr, @RequestParam(defaultValue = "10") Integer limit, @RequestParam(required = false) String orderByField, @RequestParam(required = false) String orderByType, @RequestParam Map<String, Object> param) { excludeTrash(param); EntityWrapper<ApkBuildTask> wrapper = new EntityWrapper<>(); convert(param, wrapper); if (!Cools.isEmpty(orderByField)) { wrapper.orderBy(orderByField, "asc".equalsIgnoreCase(orderByType)); } else { wrapper.orderBy("id", false); } return R.ok(apkBuildTaskService.selectPage(new Page<>(curr, limit), wrapper)); } /** * 搜索参数转换 */ private void convert(Map<String, Object> map, EntityWrapper<ApkBuildTask> wrapper) { for (Map.Entry<String, Object> entry : map.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value == null || "".equals(value.toString())) { continue; } if (key.endsWith(">")) { wrapper.ge(Cools.deleteChar(key), DateUtils.convert(String.valueOf(value))); } else if (key.endsWith("<")) { wrapper.le(Cools.deleteChar(key), DateUtils.convert(String.valueOf(value))); } else if ("status".equals(key)) { wrapper.eq(key, value); } else { wrapper.like(key, String.valueOf(value)); } } } /** * 触发打包任务 */ @RequestMapping(value = "/apkBuildTask/build/auth", method = RequestMethod.POST) @ManagerAuth public R triggerBuild(@RequestBody JSONObject param) { try { String buildType = param.getString("buildType"); String repoAlias = param.getString("repoAlias"); String branch = param.getString("branch"); if (Cools.isEmpty(buildType)) { buildType = "release"; } if (Cools.isEmpty(repoAlias)) { return R.error("仓库别名不能为空"); } if (Cools.isEmpty(branch)) { branch = "master"; } ApkBuildTask task = apkBuildTaskService.triggerBuild(buildType, repoAlias, branch); return R.ok(task); } catch (Exception e) { return R.error("打包任务创建失败: " + e.getMessage()); } } /** * 刷新指定任务状态 */ @RequestMapping(value = "/apkBuildTask/refresh/{id}/auth", method = RequestMethod.POST) @ManagerAuth public R refreshStatus(@PathVariable("id") Long id) { try { ApkBuildTask task = apkBuildTaskService.refreshStatus(id); return R.ok(task); } catch (Exception e) { return R.error("刷新状态失败: " + e.getMessage()); } } /** * 刷新所有进行中的任务状态 */ @RequestMapping(value = "/apkBuildTask/refreshAll/auth", method = RequestMethod.POST) @ManagerAuth public R refreshAllPendingTasks() { try { int count = apkBuildTaskService.refreshAllPendingTasks(); // 返回所有任务列表 EntityWrapper<ApkBuildTask> wrapper = new EntityWrapper<>(); wrapper.orderBy("id", false); List<ApkBuildTask> tasks = apkBuildTaskService.selectList(wrapper); Map<String, Object> result = new HashMap<>(); result.put("tasks", tasks); result.put("refreshedCount", count); return R.ok(result); } catch (Exception e) { return R.error("刷新状态失败: " + e.getMessage()); } } /** * 下载APK到服务器 */ @RequestMapping(value = "/apkBuildTask/download/{id}/auth", method = RequestMethod.POST) @ManagerAuth public R downloadApk(@PathVariable("id") Long id) { try { String localPath = apkBuildTaskService.downloadApk(id); Map<String, Object> result = new HashMap<>(); result.put("apkPath", localPath); return R.ok(result); } catch (Exception e) { return R.error("下载APK失败: " + e.getMessage()); } } /** * 通过ADB安装APK到指定设备 */ @RequestMapping(value = "/apkBuildTask/install/{id}/auth", method = RequestMethod.POST) @ManagerAuth public R installApk(@PathVariable("id") Long id, @RequestBody JSONObject param) { try { String deviceIp = param.getString("deviceIp"); if (Cools.isEmpty(deviceIp)) { return R.error("设备IP不能为空"); } String installResult = apkBuildTaskService.installApk(id, deviceIp); Map<String, Object> result = new HashMap<>(); result.put("result", installResult); return R.ok(result); } catch (Exception e) { return R.error("安装APK失败: " + e.getMessage()); } } /** * 批量安装APK到多台设备 */ @RequestMapping(value = "/apkBuildTask/installBatch/{id}/auth", method = RequestMethod.POST) @ManagerAuth public R installApkBatch(@PathVariable("id") Long id, @RequestBody JSONObject param) { try { List<String> deviceIps = param.getJSONArray("deviceIps").toJavaList(String.class); if (deviceIps == null || deviceIps.isEmpty()) { return R.error("设备IP列表不能为空"); } List<String> results = apkBuildTaskService.installApkToMultipleDevices(id, deviceIps); return R.ok(results); } catch (Exception e) { return R.error("批量安装APK失败: " + e.getMessage()); } } /** * 删除任务记录 */ @RequestMapping(value = "/apkBuildTask/delete/auth", method = RequestMethod.POST) @ManagerAuth public R delete(Integer[] ids) { if (ids == null || ids.length == 0) { return R.error(); } apkBuildTaskService.deleteBatchIds(Arrays.asList(ids)); return R.ok(); } /** * 查询进行中的任务 */ @RequestMapping(value = "/apkBuildTask/pending/auth") @ManagerAuth public R getPendingTasks() { List<ApkBuildTask> tasks = apkBuildTaskService.getPendingTasks(); return R.ok(tasks); } } src/main/java/com/zy/asrs/entity/ApkBuildTask.java
New file @@ -0,0 +1,248 @@ 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; /** * APK打包任务实体类 */ @TableName("apk_build_task") public class ApkBuildTask implements Serializable { private static final long serialVersionUID = 1L; /** * 主键ID */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 远程打包任务ID */ @TableField("task_id") private String taskId; /** * 打包类型(release/debug) */ @TableField("build_type") private String buildType; /** * 仓库别名 */ @TableField("repo_alias") private String repoAlias; /** * 分支名称 */ private String branch; /** * 状态:0-等待中,1-打包中,2-成功,3-失败 */ private Short status; /** * 本地APK文件路径 */ @TableField("apk_path") private String apkPath; /** * 远程APK文件路径 */ @TableField("artifact_path") private String artifactPath; /** * 项目名称 */ @TableField("project_name") private String projectName; /** * 错误信息 */ private String error; /** * 元数据JSON */ private String meta; /** * 队列大小 */ @TableField("queue_size") private Integer queueSize; /** * 创建时间 */ @TableField("created_at") private Date createdAt; /** * 开始时间 */ @TableField("started_at") private Date startedAt; /** * 完成时间 */ @TableField("finished_at") private Date finishedAt; public ApkBuildTask() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTaskId() { return taskId; } public void setTaskId(String taskId) { this.taskId = taskId; } public String getBuildType() { return buildType; } public void setBuildType(String buildType) { this.buildType = buildType; } public String getRepoAlias() { return repoAlias; } public void setRepoAlias(String repoAlias) { this.repoAlias = repoAlias; } public String getBranch() { return branch; } public void setBranch(String branch) { this.branch = branch; } 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 "打包中"; case 2: return "成功"; case 3: return "失败"; default: return String.valueOf(this.status); } } public String getApkPath() { return apkPath; } public void setApkPath(String apkPath) { this.apkPath = apkPath; } public String getArtifactPath() { return artifactPath; } public void setArtifactPath(String artifactPath) { this.artifactPath = artifactPath; } public String getProjectName() { return projectName; } public void setProjectName(String projectName) { this.projectName = projectName; } public String getError() { return error; } public void setError(String error) { this.error = error; } public String getMeta() { return meta; } public void setMeta(String meta) { this.meta = meta; } public Integer getQueueSize() { return queueSize; } public void setQueueSize(Integer queueSize) { this.queueSize = queueSize; } public Date getCreatedAt() { return createdAt; } public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } public Date getStartedAt() { return startedAt; } public void setStartedAt(Date startedAt) { this.startedAt = startedAt; } public Date getFinishedAt() { return finishedAt; } public void setFinishedAt(Date finishedAt) { this.finishedAt = finishedAt; } } src/main/java/com/zy/asrs/mapper/ApkBuildTaskMapper.java
New file @@ -0,0 +1,19 @@ package com.zy.asrs.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.asrs.entity.ApkBuildTask; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * APK打包任务Mapper接口 */ @Mapper public interface ApkBuildTaskMapper extends BaseMapper<ApkBuildTask> { /** * 查询所有进行中的任务(等待中或打包中) */ List<ApkBuildTask> selectPendingTasks(); } src/main/java/com/zy/asrs/service/ApkBuildTaskService.java
New file @@ -0,0 +1,70 @@ package com.zy.asrs.service; import com.baomidou.mybatisplus.service.IService; import com.zy.asrs.entity.ApkBuildTask; import java.util.List; /** * APK打包任务Service接口 */ public interface ApkBuildTaskService extends IService<ApkBuildTask> { /** * 触发打包任务 * * @param buildType 打包类型(release/debug) * @param repoAlias 仓库别名 * @param branch 分支名称 * @return 创建的任务实体 */ ApkBuildTask triggerBuild(String buildType, String repoAlias, String branch) throws Exception; /** * 刷新指定任务的状态 * * @param id 任务ID * @return 更新后的任务实体 */ ApkBuildTask refreshStatus(Long id) throws Exception; /** * 刷新所有进行中的任务状态 * * @return 更新的任务数量 */ int refreshAllPendingTasks(); /** * 下载APK文件到本地服务器 * * @param id 任务ID * @return 本地APK文件路径 */ String downloadApk(Long id) throws Exception; /** * 通过ADB安装APK到指定设备 * * @param id 任务ID * @param deviceIp 设备IP地址 * @return 安装结果信息 */ String installApk(Long id, String deviceIp) throws Exception; /** * 批量安装APK到多台设备 * * @param id 任务ID * @param deviceIps 设备IP列表 * @return 安装结果列表 */ List<String> installApkToMultipleDevices(Long id, List<String> deviceIps); /** * 查询所有进行中的任务 * * @return 进行中的任务列表 */ List<ApkBuildTask> getPendingTasks(); } src/main/java/com/zy/asrs/service/impl/ApkBuildTaskServiceImpl.java
New file @@ -0,0 +1,359 @@ package com.zy.asrs.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.asrs.entity.ApkBuildTask; import com.zy.asrs.mapper.ApkBuildTaskMapper; import com.zy.asrs.service.ApkBuildTaskService; import com.zy.common.utils.HttpHandler; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.TimeUnit; /** * APK打包任务Service实现类 */ @Service public class ApkBuildTaskServiceImpl extends ServiceImpl<ApkBuildTaskMapper, ApkBuildTask> implements ApkBuildTaskService { private static final Logger log = LoggerFactory.getLogger(ApkBuildTaskServiceImpl.class); @Value("${apk-build.build-server-url}") private String buildServerUrl; @Value("${apk-build.download-server-url}") private String downloadServerUrl; @Value("${apk-build.x-token}") private String xToken; @Value("${apk-build.apk-download-path}") private String apkDownloadPath; @Value("${adb.path}") private String adbPath; @Override public ApkBuildTask triggerBuild(String buildType, String repoAlias, String branch) throws Exception { // 构建请求JSON JSONObject requestBody = new JSONObject(); requestBody.put("build_type", buildType); requestBody.put("repo_alias", repoAlias); requestBody.put("branch", branch); // 发送打包请求 Map<String, Object> headers = new HashMap<>(); headers.put("X-Token", xToken); String response = new HttpHandler.Builder() .setUri(buildServerUrl) .setPath("/build") .setHeaders(headers) .setJson(requestBody.toJSONString()) .build() .doPost(); log.info("触发打包响应: {}", response); if (response == null) { throw new RuntimeException("打包服务无响应"); } JSONObject result = JSON.parseObject(response); String taskId = result.getString("task_id"); String status = result.getString("status"); Integer queueSize = result.getInteger("queue_size"); if (taskId == null || taskId.isEmpty()) { throw new RuntimeException("打包服务返回的task_id为空"); } // 创建本地任务记录 ApkBuildTask task = new ApkBuildTask(); task.setTaskId(taskId); task.setBuildType(buildType); task.setRepoAlias(repoAlias); task.setBranch(branch); task.setStatus(convertStatus(status)); task.setQueueSize(queueSize); task.setCreatedAt(new Date()); this.insert(task); return task; } @Override public ApkBuildTask refreshStatus(Long id) throws Exception { ApkBuildTask task = this.selectById(id); if (task == null) { throw new RuntimeException("任务不存在"); } // 如果任务已完成或失败,无需刷新 if (task.getStatus() == 2 || task.getStatus() == 3) { return task; } // 查询远程状态 Map<String, Object> headers = new HashMap<>(); headers.put("X-Token", xToken); String response = new HttpHandler.Builder() .setUri(buildServerUrl) .setPath("/build/" + task.getTaskId()) .setHeaders(headers) .build() .doGet(); log.info("查询状态响应: {}", response); if (response == null) { throw new RuntimeException("查询状态服务无响应"); } JSONObject result = JSON.parseObject(response); String status = result.getString("status"); String artifactPath = result.getString("artifact_path"); String projectName = result.getString("project_name"); String error = result.getString("error"); String meta = result.getString("meta"); Long createdAtTimestamp = result.getLong("created_at"); Long startedAtTimestamp = result.getLong("started_at"); Long finishedAtTimestamp = result.getLong("finished_at"); // 更新任务信息 task.setStatus(convertStatus(status)); task.setArtifactPath(artifactPath); task.setProjectName(projectName); task.setError(error); task.setMeta(meta); if (createdAtTimestamp != null) { task.setCreatedAt(new Date((long) (createdAtTimestamp * 1000))); } if (startedAtTimestamp != null) { task.setStartedAt(new Date((long) (startedAtTimestamp * 1000))); } if (finishedAtTimestamp != null) { task.setFinishedAt(new Date((long) (finishedAtTimestamp * 1000))); } // 如果打包成功,自动下载APK if ("SUCCESS".equals(status) && artifactPath != null && task.getApkPath() == null) { try { String localPath = downloadApkInternal(task); task.setApkPath(localPath); } catch (Exception e) { log.error("自动下载APK失败: {}", e.getMessage()); } } this.updateById(task); return task; } @Override public int refreshAllPendingTasks() { List<ApkBuildTask> pendingTasks = this.baseMapper.selectPendingTasks(); int count = 0; for (ApkBuildTask task : pendingTasks) { try { refreshStatus(task.getId()); count++; } catch (Exception e) { log.error("刷新任务 {} 状态失败: {}", task.getId(), e.getMessage()); } } return count; } @Override public String downloadApk(Long id) throws Exception { ApkBuildTask task = this.selectById(id); if (task == null) { throw new RuntimeException("任务不存在"); } if (task.getStatus() != 2) { throw new RuntimeException("任务尚未完成,无法下载"); } if (task.getApkPath() != null && new File(task.getApkPath()).exists()) { return task.getApkPath(); } String localPath = downloadApkInternal(task); task.setApkPath(localPath); this.updateById(task); return localPath; } /** * 内部下载APK方法 */ private String downloadApkInternal(ApkBuildTask task) throws Exception { if (task.getTaskId() == null) { throw new RuntimeException("任务ID为空"); } String downloadUrl = downloadServerUrl + "/build/" + task.getTaskId() + "/download"; log.info("开始下载APK: {}", downloadUrl); // 创建下载目录 Path downloadDir = Paths.get(apkDownloadPath); if (!Files.exists(downloadDir)) { Files.createDirectories(downloadDir); } // 生成本地文件名 String fileName = task.getProjectName() != null ? task.getProjectName() + ".apk" : task.getTaskId() + ".apk"; Path localFile = downloadDir.resolve(fileName); // 下载文件 OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(300, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url(downloadUrl) .addHeader("X-Token", xToken) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("下载失败,HTTP状态码: " + response.code()); } try (InputStream is = response.body().byteStream(); OutputStream os = new FileOutputStream(localFile.toFile())) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } } } log.info("APK下载完成: {}", localFile.toString()); return localFile.toString(); } @Override public String installApk(Long id, String deviceIp) throws Exception { ApkBuildTask task = this.selectById(id); if (task == null) { throw new RuntimeException("任务不存在"); } if (task.getApkPath() == null || !new File(task.getApkPath()).exists()) { throw new RuntimeException("APK文件不存在,请先下载"); } StringBuilder result = new StringBuilder(); // 连接设备 String connectResult = executeAdbCommand("connect", deviceIp + ":5555"); result.append("连接设备: ").append(connectResult).append("\n"); // 等待设备连接稳定 Thread.sleep(1000); // 安装APK String installResult = executeAdbCommand("-s", deviceIp + ":5555", "install", "-r", task.getApkPath()); result.append("安装结果: ").append(installResult); log.info("ADB安装结果: {}", result.toString()); return result.toString(); } @Override public List<String> installApkToMultipleDevices(Long id, List<String> deviceIps) { List<String> results = new ArrayList<>(); for (String deviceIp : deviceIps) { try { String result = installApk(id, deviceIp); results.add(deviceIp + ": " + result); } catch (Exception e) { results.add(deviceIp + ": 安装失败 - " + e.getMessage()); } } return results; } @Override public List<ApkBuildTask> getPendingTasks() { return this.baseMapper.selectPendingTasks(); } /** * 执行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")) { log.warn("ADB命令执行返回码: {}, 输出: {}", exitCode, result); } return result; } /** * 转换状态字符串为数字 */ private Short convertStatus(String status) { if (status == null) { return 0; } switch (status.toUpperCase()) { case "PENDING": return 0; case "BUILDING": case "RUNNING": return 1; case "SUCCESS": return 2; case "FAILED": case "ERROR": return 3; default: return 0; } } } src/main/resources/application.yml
@@ -57,4 +57,20 @@ pwd: xltys1995 swagger: enable: false enable: false # APK打包服务配置 apk-build: # 打包和查询状态的服务器地址 build-server-url: http://127.0.0.1:8088 # 下载APK的服务器地址 download-server-url: http://127.0.0.1:8088 # API认证Token x-token: uzCHJUQSb7tl2ZTjB0wI3XViqMgxRhvYL9EP # APK下载保存路径 apk-download-path: /stock/out/apk/ # ADB配置 adb: # ADB可执行文件路径,如果在系统PATH中可直接填写adb path: D:\platform-tools\adb.exe src/main/resources/mapper/ApkBuildTaskMapper.xml
New file @@ -0,0 +1,10 @@ <?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.ApkBuildTaskMapper"> <!-- 查询所有进行中的任务(等待中或打包中) --> <select id="selectPendingTasks" resultType="com.zy.asrs.entity.ApkBuildTask"> SELECT * FROM apk_build_task WHERE status IN (0, 1) ORDER BY created_at DESC </select> </mapper> src/main/resources/sdk/platform-tools-latest-windows.zipBinary files differ
src/main/webapp/views/apkBuild/apkBuild.html
New file @@ -0,0 +1,762 @@ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>APK打包管理</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; } .auto-refresh-indicator { display: flex; align-items: center; font-size: 12px; color: #909399; margin-left: 10px; } .auto-refresh-indicator.active { color: #67c23a; } .auto-refresh-indicator .dot { width: 8px; height: 8px; border-radius: 50%; background: #909399; margin-right: 6px; } .auto-refresh-indicator.active .dot { background: #67c23a; animation: pulse 1.5s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.8); } } .status-tag { font-weight: 500; } .building-animation { animation: blink 1s infinite; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } .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; } .install-textarea { width: 100%; min-height: 120px; } .detail-row { margin-bottom: 15px; } .detail-label { font-weight: 500; color: #606266; margin-bottom: 5px; } .detail-value { color: #303133; word-break: break-all; } .error-text { color: #f56c6c; } </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="0"></el-option> <el-option label="打包中" value="1"></el-option> <el-option label="成功" value="2"></el-option> <el-option label="失败" value="3"></el-option> </el-select> <el-input v-model="searchForm.repo_alias" placeholder="仓库别名" size="small" style="width: 150px;" clearable></el-input> <el-input v-model="searchForm.branch" placeholder="分支名称" size="small" style="width: 120px;" 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="showBuildDialog">新建打包</el-button> <el-button type="success" size="small" icon="el-icon-refresh" @click="refreshAllTasks" :loading="refreshing">刷新状态</el-button> <el-button type="danger" size="small" icon="el-icon-delete" @click="handleBatchDelete" :disabled="selectedRows.length === 0">删除</el-button> <div class="auto-refresh-indicator" :class="{ active: autoRefreshEnabled }"> <span class="dot"></span> <span>{{ autoRefreshEnabled ? '自动刷新中' : '自动刷新已关闭' }}</span> </div> </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="taskId" label="任务ID" min-width="200" show-overflow-tooltip></el-table-column> <el-table-column prop="repoAlias" label="仓库别名" width="120" align="center"></el-table-column> <el-table-column prop="branch" label="分支" width="100" align="center"></el-table-column> <el-table-column prop="buildType" label="类型" width="80" align="center"> <template slot-scope="scope"> <el-tag size="mini" :type="scope.row.buildType === 'release' ? 'success' : 'warning'"> {{ scope.row.buildType }} </el-tag> </template> </el-table-column> <el-table-column prop="status" label="状态" width="100" align="center"> <template slot-scope="scope"> <el-tag size="small" :type="getStatusType(scope.row.status)" :class="{ 'building-animation': scope.row.status === 1 }"> {{ getStatusText(scope.row.status) }} </el-tag> </template> </el-table-column> <el-table-column prop="projectName" label="项目名称" min-width="150" show-overflow-tooltip></el-table-column> <el-table-column prop="createdAt" label="创建时间" width="160" align="center"> <template slot-scope="scope"> {{ formatDate(scope.row.createdAt) }} </template> </el-table-column> <el-table-column label="操作" width="180" align="center" fixed="right"> <template slot-scope="scope"> <el-button type="text" size="small" @click="showDetail(scope.row)">详情</el-button> <el-button type="text" size="small" v-if="scope.row.status === 2 && !scope.row.apkPath" @click="downloadApk(scope.row)" :loading="scope.row.downloading">下载</el-button> <el-button type="text" size="small" style="color: #67c23a;" v-if="scope.row.status === 2 && scope.row.apkPath" @click="showInstallDialog(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> </div> <!-- 新建打包对话框 --> <el-dialog title="新建打包任务" :visible.sync="buildDialogVisible" width="450px" :close-on-click-modal="false"> <el-form :model="buildForm" :rules="buildRules" ref="buildFormRef" label-width="100px"> <el-form-item label="打包类型" prop="buildType"> <el-radio-group v-model="buildForm.buildType"> <el-radio label="release">Release</el-radio> <el-radio label="debug">Debug</el-radio> </el-radio-group> </el-form-item> <el-form-item label="仓库别名" prop="repoAlias"> <el-input v-model="buildForm.repoAlias" placeholder="请输入仓库别名"></el-input> </el-form-item> <el-form-item label="分支名称" prop="branch"> <el-input v-model="buildForm.branch" placeholder="请输入分支名称"></el-input> <div class="form-tip">默认为 master 分支</div> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="buildDialogVisible = false">取消</el-button> <el-button type="primary" @click="submitBuild" :loading="buildSubmitting">提交打包</el-button> </div> </el-dialog> <!-- 任务详情对话框 --> <el-dialog title="任务详情" :visible.sync="detailDialogVisible" width="550px"> <div v-if="currentDetail"> <el-row :gutter="20"> <el-col :span="12"> <div class="detail-row"> <div class="detail-label">任务ID</div> <div class="detail-value">{{ currentDetail.taskId }}</div> </div> </el-col> <el-col :span="12"> <div class="detail-row"> <div class="detail-label">状态</div> <div class="detail-value"> <el-tag size="small" :type="getStatusType(currentDetail.status)"> {{ getStatusText(currentDetail.status) }} </el-tag> </div> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="detail-row"> <div class="detail-label">仓库别名</div> <div class="detail-value">{{ currentDetail.repoAlias }}</div> </div> </el-col> <el-col :span="12"> <div class="detail-row"> <div class="detail-label">分支</div> <div class="detail-value">{{ currentDetail.branch }}</div> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="detail-row"> <div class="detail-label">打包类型</div> <div class="detail-value">{{ currentDetail.buildType }}</div> </div> </el-col> <el-col :span="12"> <div class="detail-row"> <div class="detail-label">项目名称</div> <div class="detail-value">{{ currentDetail.projectName || '-' }}</div> </div> </el-col> </el-row> <div class="detail-row"> <div class="detail-label">APK路径</div> <div class="detail-value">{{ currentDetail.apkPath || '未下载' }}</div> </div> <el-row :gutter="20"> <el-col :span="8"> <div class="detail-row"> <div class="detail-label">创建时间</div> <div class="detail-value">{{ formatDate(currentDetail.createdAt) }}</div> </div> </el-col> <el-col :span="8"> <div class="detail-row"> <div class="detail-label">开始时间</div> <div class="detail-value">{{ formatDate(currentDetail.startedAt) || '-' }}</div> </div> </el-col> <el-col :span="8"> <div class="detail-row"> <div class="detail-label">完成时间</div> <div class="detail-value">{{ formatDate(currentDetail.finishedAt) || '-' }}</div> </div> </el-col> </el-row> <div class="detail-row" v-if="currentDetail.error"> <div class="detail-label">错误信息</div> <div class="detail-value error-text">{{ currentDetail.error }}</div> </div> </div> <div slot="footer" class="dialog-footer"> <el-button @click="detailDialogVisible = false">关闭</el-button> </div> </el-dialog> <!-- 安装对话框 --> <el-dialog title="安装APK到设备" :visible.sync="installDialogVisible" width="450px" :close-on-click-modal="false"> <el-form label-width="80px"> <el-form-item label="设备IP"> <el-input type="textarea" v-model="installIps" class="install-textarea" placeholder="请输入设备IP地址,多个设备请换行分隔 例如: 192.168.1.100 192.168.1.101"> </el-input> <div class="form-tip">支持批量安装,每行一个IP地址</div> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="installDialogVisible = false">取消</el-button> <el-button type="primary" @click="submitInstall" :loading="installing">确认安装</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: [], // 搜索表单 searchForm: { status: '', repo_alias: '', branch: '' }, // 自动刷新 autoRefreshEnabled: false, autoRefreshTimer: null, refreshing: false, // 新建打包对话框 buildDialogVisible: false, buildSubmitting: false, buildForm: { buildType: 'release', repoAlias: 'zy-monitor', branch: 'master' }, buildRules: { repoAlias: [{ required: true, message: '请输入仓库别名', trigger: 'blur' }] }, // 详情对话框 detailDialogVisible: false, currentDetail: null, // 安装对话框 installDialogVisible: false, installIps: '', installing: false, currentInstallTask: null }, created() { this.loadData(); }, beforeDestroy() { this.stopAutoRefresh(); }, methods: { // 获取请求头 getHeaders() { return { 'token': localStorage.getItem('token') }; }, // 加载数据 loadData() { this.tableLoading = true; const params = { curr: this.currentPage, limit: this.pageSize, ...this.searchForm }; $.ajax({ url: baseUrl + '/apkBuildTask/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; this.checkAutoRefresh(); } else if (res.code === 403) { top.location.href = baseUrl + '/'; } else { this.$message.error(res.msg || '加载失败'); } }, error: () => { this.tableLoading = false; this.$message.error('请求失败'); } }); }, // 检查是否需要自动刷新 checkAutoRefresh() { const hasPending = this.tableData.some(item => item.status === 0 || item.status === 1); if (hasPending && !this.autoRefreshEnabled) { this.startAutoRefresh(); } else if (!hasPending && this.autoRefreshEnabled) { this.stopAutoRefresh(); } }, // 开启自动刷新 startAutoRefresh() { if (this.autoRefreshTimer) return; this.autoRefreshEnabled = true; this.autoRefreshTimer = setInterval(() => { this.silentRefresh(); }, 5000); }, // 停止自动刷新 stopAutoRefresh() { if (this.autoRefreshTimer) { clearInterval(this.autoRefreshTimer); this.autoRefreshTimer = null; } this.autoRefreshEnabled = false; }, // 静默刷新 silentRefresh() { $.ajax({ url: baseUrl + '/apkBuildTask/refreshAll/auth', headers: this.getHeaders(), method: 'POST', success: (res) => { if (res.code === 200) { this.loadData(); } } }); }, // 手动刷新所有任务 refreshAllTasks() { this.refreshing = true; $.ajax({ url: baseUrl + '/apkBuildTask/refreshAll/auth', headers: this.getHeaders(), method: 'POST', success: (res) => { this.refreshing = false; if (res.code === 200) { this.$message.success(`刷新完成,更新了 ${res.data.refreshedCount} 个任务`); this.loadData(); } else if (res.code === 403) { top.location.href = baseUrl + '/'; } else { this.$message.error(res.msg || '刷新失败'); } }, error: () => { this.refreshing = false; this.$message.error('请求失败'); } }); }, // 搜索 handleSearch() { this.currentPage = 1; this.loadData(); }, // 重置 handleReset() { this.searchForm = { status: '', repo_alias: '', branch: '' }; 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; }, // 批量删除 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 + '/apkBuildTask/delete/auth', headers: this.getHeaders(), method: 'POST', data: { ids: ids }, traditional: true, success: (res) => { if (res.code === 200) { this.$message.success('删除成功'); this.loadData(); } else { this.$message.error(res.msg || '删除失败'); } } }); }).catch(() => { }); }, // 显示新建打包对话框 showBuildDialog() { this.buildForm = { buildType: 'release', repoAlias: 'zy-monitor', branch: 'master' }; this.buildDialogVisible = true; }, // 提交打包 submitBuild() { this.$refs.buildFormRef.validate((valid) => { if (!valid) return; this.buildSubmitting = true; $.ajax({ url: baseUrl + '/apkBuildTask/build/auth', headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify(this.buildForm), success: (res) => { this.buildSubmitting = false; if (res.code === 200) { this.$message.success('打包任务已创建'); this.buildDialogVisible = false; this.loadData(); } else if (res.code === 403) { top.location.href = baseUrl + '/'; } else { this.$message.error(res.msg || '创建失败'); } }, error: () => { this.buildSubmitting = false; this.$message.error('请求失败'); } }); }); }, // 显示详情 showDetail(row) { this.currentDetail = row; this.detailDialogVisible = true; }, // 下载APK downloadApk(row) { this.$set(row, 'downloading', true); $.ajax({ url: baseUrl + '/apkBuildTask/download/' + row.id + '/auth', headers: this.getHeaders(), method: 'POST', success: (res) => { this.$set(row, 'downloading', false); if (res.code === 200) { this.$message.success('APK下载完成: ' + res.data.apkPath); this.loadData(); } else { this.$message.error(res.msg || '下载失败'); } }, error: () => { this.$set(row, 'downloading', false); this.$message.error('请求失败'); } }); }, // 显示安装对话框 showInstallDialog(row) { this.currentInstallTask = row; this.installIps = ''; this.installDialogVisible = true; }, // 提交安装 submitInstall() { if (!this.installIps.trim()) { this.$message.warning('请输入设备IP地址'); return; } const ips = this.installIps.split('\n') .map(ip => ip.trim()) .filter(ip => ip !== ''); if (ips.length === 0) { this.$message.warning('请输入有效的设备IP地址'); return; } this.installing = true; const taskId = this.currentInstallTask.id; if (ips.length === 1) { // 单设备安装 $.ajax({ url: baseUrl + '/apkBuildTask/install/' + taskId + '/auth', headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ deviceIp: ips[0] }), success: (res) => { this.installing = false; this.installDialogVisible = false; if (res.code === 200) { this.$alert(res.data.result, '安装结果', { type: 'success' }); } else { this.$message.error(res.msg || '安装失败'); } }, error: () => { this.installing = false; this.$message.error('请求失败'); } }); } else { // 批量安装 $.ajax({ url: baseUrl + '/apkBuildTask/installBatch/' + taskId + '/auth', headers: this.getHeaders(), method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ deviceIps: ips }), success: (res) => { this.installing = false; this.installDialogVisible = false; if (res.code === 200) { this.$alert(res.data.join('\n'), '批量安装结果', { type: 'success' }); } else { this.$message.error(res.msg || '安装失败'); } }, error: () => { this.installing = false; this.$message.error('请求失败'); } }); } }, // 获取状态类型 getStatusType(status) { const types = { 0: 'info', 1: '', 2: 'success', 3: 'danger' }; return types[status] || 'info'; }, // 获取状态文本 getStatusText(status) { const texts = { 0: '等待中', 1: '打包中', 2: '成功', 3: '失败' }; return texts[status] || '未知'; }, // 格式化日期 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>