feat(设备详情): 新增设备截图轮播与截图管理功能
在设备详情页面新增截图轮播展示区域,支持截图列表加载、全屏预览、下发截图指令、单张删除及一键清空功能。 新增对应的后端接口(获取截图列表、删除截图、清空截图)及DeviceScreenshot类型定义。
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user