feat(设备详情): 新增设备截图轮播与截图管理功能

在设备详情页面新增截图轮播展示区域,支持截图列表加载、全屏预览、下发截图指令、单张删除及一键清空功能。
新增对应的后端接口(获取截图列表、删除截图、清空截图)及DeviceScreenshot类型定义。
This commit is contained in:
TongTongStudio
2026-08-06 01:42:30 +08:00
parent 899574ba3e
commit af54b42293
3 changed files with 379 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ import type {
DeviceSecurityInfo,
ApkInstallInfo,
DeviceLocationInfo,
DeviceScreenshot,
} from "./types";
import type { ExcelResult, PageResult, OptionItem } from "@/api/common";
@@ -379,6 +380,46 @@ const DeviceAPI = {
});
},
/**
* 获取设备截图列表(按上传时间倒序)
* 对应后端 GET /{sn}/screenshots (listScreenshots)
*
* @param sn 设备SN号
*/
getScreenshotList(sn: string) {
return request<any, DeviceScreenshot[]>({
url: `${DEVICE_BASE_URL}/${sn}/screenshots`,
method: "get",
});
},
/**
* 删除单张设备截图
* 对应后端 DELETE /{sn}/screenshots/{id} (deleteScreenshot)
*
* @param sn 设备SN号
* @param id 截图ID
*/
deleteScreenshot(sn: string, id: number) {
return request<any, boolean>({
url: `${DEVICE_BASE_URL}/${sn}/screenshots/${id}`,
method: "delete",
});
},
/**
* 清空设备全部截图
* 对应后端 DELETE /{sn}/screenshots (clearScreenshots)
*
* @param sn 设备SN号
*/
clearScreenshots(sn: string) {
return request<any, number>({
url: `${DEVICE_BASE_URL}/${sn}/screenshots`,
method: "delete",
});
},
/** 打开应用 */
launchApp(sn: string, packageName: string) {
return request({

View File

@@ -401,3 +401,19 @@ export interface DeviceLocationInfo {
/** 上次定位成功时间 */
lastSuccessfulTime?: string;
}
/** 设备截图信息 */
export interface DeviceScreenshot {
/** 截图ID */
id?: number;
/** 设备序列号 */
sn?: string;
/** 文件名称 */
fileName?: string;
/** 文件大小(字节) */
fileSize?: number;
/** 可访问的图片URL */
url?: string;
/** 上传时间 */
uploadTime?: string;
}

View File

@@ -56,6 +56,88 @@
</div>
</div>
<!-- 设备截图轮播 -->
<div v-loading="screenshotLoading" class="device-screenshots">
<div class="screenshot-title">
<span>设备截图</span>
<el-tag v-if="screenshots.length" size="small" type="info">
{{ screenshots.length }}
</el-tag>
<el-button
class="refresh-btn"
size="small"
:loading="screenshotLoading"
@click="refreshScreenshots"
>
刷新
</el-button>
<el-button
class="capture-btn"
size="small"
type="primary"
:loading="captureLoading"
@click="captureScreenshot"
>
截图
</el-button>
<el-button
class="clear-btn"
size="small"
type="danger"
plain
:disabled="screenshots.length === 0"
:loading="clearLoading"
@click="clearScreenshotsFn"
>
清空截图
</el-button>
</div>
<el-empty
v-if="!screenshotLoading && screenshots.length === 0"
:image-size="80"
description="暂无截图"
/>
<el-carousel
v-else
class="screenshot-carousel"
height="360px"
indicator-position="outside"
arrow="always"
:autoplay="false"
>
<el-carousel-item v-for="(item, index) in screenshots" :key="item.id">
<div class="screenshot-item">
<el-image
:src="resolveIconUrl(item.url)"
fit="contain"
class="screenshot-image"
@click="openPreview(index)"
>
<template #error>
<div class="screenshot-error">图片加载失败</div>
</template>
</el-image>
<div v-if="item.uploadTime" class="screenshot-meta">
上传时间{{ item.uploadTime }}
</div>
<span class="delete-btn" title="删除该截图" @click.stop="deleteScreenshotItem(item)">
<el-icon v-if="deletingId !== item.id" class="delete-icon"><Delete /></el-icon>
<el-icon v-else class="delete-icon is-loading"><Loading /></el-icon>
</span>
</div>
</el-carousel-item>
</el-carousel>
<!-- 全屏预览单张截图 -->
<el-image-viewer
v-if="previewVisible"
:url-list="screenshots.map((s) => resolveIconUrl(s.url) as string)"
:initial-index="previewIndex"
teleported
@close="previewVisible = false"
/>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button @click="handleReturn">
@@ -668,6 +750,7 @@ import type {
DeviceOtherInfo,
DeviceSecurityInfo,
DeviceLocationInfo,
DeviceScreenshot,
} from "@/api/system/device/types";
import { nextTick, reactive, ref, watch } from "vue";
import {
@@ -677,6 +760,7 @@ import {
Lock,
Warning,
Delete,
Loading,
RefreshRight,
ArrowDown,
VideoPlay,
@@ -740,6 +824,22 @@ const deviceData = reactive({
/** 设备图片 */
const deviceImage = ref("./src/assets/images/iphone-17-pro.webp");
/** 设备截图列表 */
const screenshots = ref<DeviceScreenshot[]>([]);
const screenshotLoading = ref(false);
/** 当前设备SN用于刷新截图 */
const currentSn = ref<string>("");
/** 全屏预览可见性 */
const previewVisible = ref(false);
/** 全屏预览初始索引 */
const previewIndex = ref(0);
/** 截图指令请求中 */
const captureLoading = ref(false);
/** 清空截图请求中 */
const clearLoading = ref(false);
/** 正在删除的截图ID用于单张删除按钮 loading */
const deletingId = ref<number | null>(null);
/** 分面板数据 */
const basicInfo = reactive<Partial<DeviceBasicInfo>>({});
const hardwareInfo = reactive<Partial<DeviceHardwareInfo>>({});
@@ -1026,6 +1126,103 @@ async function loadAll(serialno?: string) {
loadSecurityInfo(serialno);
loadOtherInfo(serialno);
loadLocationInfo(serialno);
loadScreenshots(serialno);
}
/** 设备截图列表 */
async function loadScreenshots(serialno: string) {
if (!serialno) return;
currentSn.value = serialno;
screenshotLoading.value = true;
try {
const data = await DeviceAPI.getScreenshotList(serialno);
screenshots.value = data || [];
} catch (e) {
console.error("加载设备截图失败", e);
screenshots.value = [];
} finally {
screenshotLoading.value = false;
}
}
/** 刷新设备截图列表 */
function refreshScreenshots() {
loadScreenshots(currentSn.value);
}
/** 点击单张截图,全屏预览(保留当前轮播顺序) */
function openPreview(index: number) {
previewIndex.value = index;
previewVisible.value = true;
}
/** 请求设备截图,下发指令后延迟 5 秒自动刷新一次列表 */
async function captureScreenshot() {
if (!currentSn.value) return;
captureLoading.value = true;
try {
await DeviceAPI.deviceScreenshot(currentSn.value);
ElMessage.success("已下发截图指令5 秒后自动刷新");
} catch (e) {
console.error("下发截图指令失败", e);
ElMessage.error("下发截图指令失败");
} finally {
captureLoading.value = false;
}
// 延迟 5 秒自动刷新一次截图列表
setTimeout(() => {
loadScreenshots(currentSn.value);
}, 5000);
}
/** 删除单张截图 */
async function deleteScreenshotItem(item: DeviceScreenshot) {
if (!item.id || !currentSn.value) return;
try {
await ElMessageBox.confirm("确认删除该截图", "提示", {
type: "warning",
confirmButtonText: "删除",
cancelButtonText: "取消",
});
} catch {
return; // 用户取消
}
deletingId.value = item.id;
try {
await DeviceAPI.deleteScreenshot(currentSn.value, item.id);
ElMessage.success("删除成功");
await loadScreenshots(currentSn.value);
} catch (e) {
console.error("删除截图失败", e);
ElMessage.error("删除截图失败");
} finally {
deletingId.value = null;
}
}
/** 清空设备全部截图 */
async function clearScreenshotsFn() {
if (!currentSn.value || screenshots.value.length === 0) return;
try {
await ElMessageBox.confirm("确认清空该设备的全部截图此操作不可恢复", "提示", {
type: "warning",
confirmButtonText: "清空",
cancelButtonText: "取消",
});
} catch {
return; // 用户取消
}
clearLoading.value = true;
try {
const count = await DeviceAPI.clearScreenshots(currentSn.value);
ElMessage.success(`已清空 ${count} 张截图`);
await loadScreenshots(currentSn.value);
} catch (e) {
console.error("清空截图失败", e);
ElMessage.error("清空截图失败");
} finally {
clearLoading.value = false;
}
}
/** 应用信息 */
@@ -1213,6 +1410,10 @@ const handleMoreActions = (command: string) => {
background: #f5f7fa;
.device-header {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: stretch;
padding: 20px;
margin-bottom: 20px;
background: #fff;
@@ -1221,8 +1422,9 @@ const handleMoreActions = (command: string) => {
.device-info {
display: flex;
flex: 1;
gap: 20px;
margin-bottom: 20px;
min-width: 360px;
.device-image {
width: 120px;
@@ -1250,7 +1452,7 @@ const handleMoreActions = (command: string) => {
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 12px;
.info-item {
@@ -1274,6 +1476,7 @@ const handleMoreActions = (command: string) => {
.action-buttons {
display: flex;
flex-basis: 100%;
flex-wrap: wrap;
gap: 12px;
align-items: center;
@@ -1301,6 +1504,123 @@ const handleMoreActions = (command: string) => {
}
}
}
// 设备截图轮播(与 device-info 左右排列)
.device-screenshots {
display: flex;
flex: 0 0 480px;
flex-direction: column;
min-width: 320px;
max-width: 480px;
padding-left: 24px;
border-left: 1px solid #ebeef5;
.screenshot-title {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 12px;
font-size: 15px;
font-weight: 600;
color: #303133;
.refresh-btn {
margin-left: auto;
}
}
.screenshot-carousel {
flex: 1;
overflow: hidden;
background: #f5f7fa;
border-radius: 8px;
}
.screenshot-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.screenshot-image {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 320px;
}
.screenshot-meta {
padding: 8px 0;
font-size: 12px;
color: #909399;
}
.screenshot-error {
font-size: 14px;
color: #f56c6c;
}
// 单张截图右下角删除按钮(正圆形,原生 span 不受 EP 样式干扰)
.delete-btn {
position: absolute;
right: 12px;
bottom: 12px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
background-color: rgba(245, 108, 108, 0.75);
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
transition: background-color 0.2s;
.delete-icon {
width: 14px;
height: 14px;
color: #fff;
}
.delete-icon.is-loading {
animation: rotate 1s linear infinite;
}
&:hover {
background-color: rgba(245, 108, 108, 0.9);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
// 窄屏时上下排列,截图区改为顶部分隔样式
@media (max-width: 900px) {
.device-info {
flex-basis: 100%;
}
.device-screenshots {
flex: 1 1 100%;
max-width: 100%;
padding-top: 16px;
padding-left: 0;
border-top: 1px solid #ebeef5;
border-left: none;
}
}
}
.device-sn-detail {