<!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 style="margin-bottom: 15px;">
|
<el-card shadow="hover">
|
<div slot="header">
|
<span>手动异常信息</span>
|
</div>
|
<el-form label-position="top" size="small">
|
<el-form-item label="异常信息">
|
<el-input type="textarea" v-model="manualError" :rows="3"
|
placeholder="输入需要下发到设备的异常信息"></el-input>
|
</el-form-item>
|
<el-button type="primary" size="small" @click="saveManualError"
|
:loading="manualErrorSaving">保存</el-button>
|
<el-button size="small" @click="clearManualError"
|
:disabled="manualErrorSaving">清空</el-button>
|
</el-form>
|
</el-card>
|
</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="350" 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: #e6a23c;" @click="restartApp(scope.row)"
|
:loading="scope.row.restarting">重启</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="buildTaskLabel(task)" :value="task.id"
|
:disabled="!task.apkPath">
|
<span>{{ buildTaskLabel(task) }}</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-button type="warning" @click="batchRestartApp" :loading="launching"
|
:disabled="launchForm.deviceIds.length === 0">
|
<i class="el-icon-refresh"></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,
|
|
manualError: '',
|
manualErrorSaving: false
|
},
|
|
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();
|
this.loadManualError();
|
},
|
|
methods: {
|
// 获取请求头
|
getHeaders() {
|
return { 'token': localStorage.getItem('token') };
|
},
|
loadManualError() {
|
$.ajax({
|
url: baseUrl + '/openapi/manualError/auth',
|
headers: this.getHeaders(),
|
method: 'GET',
|
success: (res) => {
|
if (res.code === 200) {
|
this.manualError = (res.data && res.data.manualError) ? res.data.manualError : '';
|
} else if (res.code === 403) {
|
top.location.href = baseUrl + '/';
|
} else {
|
this.$message.error(res.msg || '加载失败');
|
}
|
},
|
error: () => {
|
this.$message.error('请求失败');
|
}
|
});
|
},
|
saveManualError() {
|
this.manualErrorSaving = true;
|
$.ajax({
|
url: baseUrl + '/openapi/manualError/auth',
|
headers: this.getHeaders(),
|
method: 'POST',
|
contentType: 'application/json;charset=UTF-8',
|
data: JSON.stringify({ manualError: this.manualError }),
|
success: (res) => {
|
this.manualErrorSaving = false;
|
if (res.code === 200) {
|
this.$message.success('保存成功');
|
} else if (res.code === 403) {
|
top.location.href = baseUrl + '/';
|
} else {
|
this.$message.error(res.msg || '保存失败');
|
}
|
},
|
error: () => {
|
this.manualErrorSaving = false;
|
this.$message.error('请求失败');
|
}
|
});
|
},
|
clearManualError() {
|
this.manualError = '';
|
this.saveManualError();
|
},
|
|
// 加载数据
|
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('请求失败');
|
}
|
});
|
},
|
|
// 单个设备重启应用
|
restartApp(row) {
|
this.$set(row, 'restarting', true);
|
$.ajax({
|
url: baseUrl + '/tvDevice/restartApp/' + 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, 'restarting', 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, 'restarting', false);
|
this.$message.error('请求失败');
|
}
|
});
|
},
|
|
// 批量重启应用
|
batchRestartApp() {
|
if (this.launchForm.deviceIds.length === 0) {
|
this.$message.warning('请选择设备');
|
return;
|
}
|
|
this.launching = true;
|
this.installResult = '';
|
|
$.ajax({
|
url: baseUrl + '/tvDevice/batchRestartApp/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('上传失败');
|
},
|
|
buildTaskLabel(task) {
|
const name = task.projectName || task.repoAlias || task.taskId || '';
|
const time = this.formatDate(task.createdAt) || '-';
|
const id = task.id != null ? task.id : '';
|
const status = task.apkPath ? '已下载' : '未下载';
|
return `${name} | ID: ${id} | ${time} | ${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>
|