feat(device): 新增设备已安装应用管理功能
为设备详情页添加应用信息标签页,支持查看已安装应用列表、打开/清除/卸载应用,以及上传应用图标等操作。
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
|||||||
DeviceNetworkInfo,
|
DeviceNetworkInfo,
|
||||||
DeviceOtherInfo,
|
DeviceOtherInfo,
|
||||||
DeviceSecurityInfo,
|
DeviceSecurityInfo,
|
||||||
|
ApkInstallInfo,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { ExcelResult, PageResult, OptionItem } from "@/api/common";
|
import type { ExcelResult, PageResult, OptionItem } from "@/api/common";
|
||||||
@@ -355,6 +356,55 @@ const DeviceAPI = {
|
|||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 设备已安装应用列表 */
|
||||||
|
getApkList(serialno: string) {
|
||||||
|
return request<any, ApkInstallInfo[]>({
|
||||||
|
url: `${DEVICE_BASE_URL}/${serialno}/apk_list`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 打开应用 */
|
||||||
|
launchApp(sn: string, packageName: string) {
|
||||||
|
return request({
|
||||||
|
url: `${DEVICE_BASE_URL}/${sn}/app/launch`,
|
||||||
|
method: "post",
|
||||||
|
data: { packageName },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 清除应用数据 */
|
||||||
|
clearAppData(sn: string, packageName: string) {
|
||||||
|
return request({
|
||||||
|
url: `${DEVICE_BASE_URL}/${sn}/app/clear_data`,
|
||||||
|
method: "post",
|
||||||
|
data: { packageName },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 卸载应用 */
|
||||||
|
uninstallApp(sn: string, packageName: string) {
|
||||||
|
return request({
|
||||||
|
url: `${DEVICE_BASE_URL}/${sn}/app/uninstall`,
|
||||||
|
method: "post",
|
||||||
|
data: { packageName },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 上传应用图标(全局图标库) */
|
||||||
|
uploadAppIcon(packageName: string, file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
return request<any, string>({
|
||||||
|
url: `/api/v1/app-icons/${packageName}/upload`,
|
||||||
|
method: "post",
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeviceAPI;
|
export default DeviceAPI;
|
||||||
|
|||||||
@@ -329,3 +329,33 @@ export interface DeviceOtherInfo {
|
|||||||
freeRam?: string;
|
freeRam?: string;
|
||||||
totalRam?: string;
|
totalRam?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== 设备应用信息 =====================
|
||||||
|
|
||||||
|
/** 设备已安装应用信息 */
|
||||||
|
export interface ApkInstallInfo {
|
||||||
|
/** 包名 */
|
||||||
|
packageName?: string;
|
||||||
|
/** 应用名称 */
|
||||||
|
appName?: string;
|
||||||
|
/** 版本名称 */
|
||||||
|
versionName?: string;
|
||||||
|
/** 版本号 */
|
||||||
|
versionCode?: number;
|
||||||
|
/** 安装时间 */
|
||||||
|
installTime?: Date | string;
|
||||||
|
/** 最后更新时间 */
|
||||||
|
lastUpdateTime?: Date | string;
|
||||||
|
/** Apk文件大小 */
|
||||||
|
apkSize?: number;
|
||||||
|
/** App数据占用空间 */
|
||||||
|
dataSize?: number;
|
||||||
|
/** App缓存占用空间 */
|
||||||
|
cacheSize?: number;
|
||||||
|
/** apk MD5 */
|
||||||
|
md5?: string;
|
||||||
|
/** 是否为系统应用 */
|
||||||
|
systemApp?: boolean;
|
||||||
|
/** 应用图标访问地址 */
|
||||||
|
iconUrl?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -452,7 +452,75 @@
|
|||||||
<!-- 扩展标签页(应用信息 / 策略信息 / 合规状态 / 操作日志 / 定位信息) -->
|
<!-- 扩展标签页(应用信息 / 策略信息 / 合规状态 / 操作日志 / 定位信息) -->
|
||||||
<el-tab-pane label="应用信息" name="appInfo">
|
<el-tab-pane label="应用信息" name="appInfo">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<el-empty description="应用信息加载中..." />
|
<div class="apk-toolbar">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
:loading="loading.apk"
|
||||||
|
@click="loadApkInfo(props.serialno)"
|
||||||
|
>
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新应用列表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading.apk" :data="apkList" border stripe empty-text="暂无应用信息">
|
||||||
|
<el-table-column label="图标" width="70" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
:src="row.iconUrl"
|
||||||
|
fit="cover"
|
||||||
|
class="apk-icon"
|
||||||
|
:preview-src-list="row.iconUrl ? [row.iconUrl] : []"
|
||||||
|
preview-teleported
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="apk-icon-placeholder">无</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="appName"
|
||||||
|
label="应用名称"
|
||||||
|
min-width="160"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="packageName"
|
||||||
|
label="包名"
|
||||||
|
min-width="220"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column prop="versionName" label="版本" width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column label="大小" width="110">
|
||||||
|
<template #default="{ row }">{{ formatSize(row.apkSize) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="系统应用" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.systemApp" type="info" size="small">系统</el-tag>
|
||||||
|
<el-tag v-else type="success" size="small">普通</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="安装时间" width="170">
|
||||||
|
<template #default="{ row }">{{ formatDate(row.installTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="300" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="handleLaunchApp(row)">打开</el-button>
|
||||||
|
<el-button link type="warning" @click="handleClearAppData(row)">清除数据</el-button>
|
||||||
|
<el-button link type="info" @click="handleUploadIcon(row)">上传图标</el-button>
|
||||||
|
<el-button link type="danger" @click="handleUninstallApp(row)">卸载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<input
|
||||||
|
ref="iconFileInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
style="display: none"
|
||||||
|
@change="onIconFileChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
@@ -486,6 +554,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DeviceAPI from "@/api/system/device";
|
import DeviceAPI from "@/api/system/device";
|
||||||
import type {
|
import type {
|
||||||
|
ApkInstallInfo,
|
||||||
DeviceBasicInfo,
|
DeviceBasicInfo,
|
||||||
DeviceHardwareInfo,
|
DeviceHardwareInfo,
|
||||||
DeviceNetworkInfo,
|
DeviceNetworkInfo,
|
||||||
@@ -527,6 +596,9 @@ watch(
|
|||||||
() => props.serialno,
|
() => props.serialno,
|
||||||
(newSerialno) => {
|
(newSerialno) => {
|
||||||
if (newSerialno) {
|
if (newSerialno) {
|
||||||
|
// 切换设备时重置应用信息懒加载状态
|
||||||
|
apkLoaded.value = false;
|
||||||
|
apkList.value = [];
|
||||||
loadAll(newSerialno);
|
loadAll(newSerialno);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -570,8 +642,14 @@ const loading = reactive({
|
|||||||
network: false,
|
network: false,
|
||||||
security: false,
|
security: false,
|
||||||
other: false,
|
other: false,
|
||||||
|
apk: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 设备已安装应用列表 */
|
||||||
|
const apkList = ref<ApkInstallInfo[]>([]);
|
||||||
|
/** 应用信息是否已加载(懒加载,仅首次进入 tab 时拉取) */
|
||||||
|
const apkLoaded = ref(false);
|
||||||
|
|
||||||
/** 空值统一显示占位符 */
|
/** 空值统一显示占位符 */
|
||||||
function formatText(value?: string | number | null): string {
|
function formatText(value?: string | number | null): string {
|
||||||
return value === undefined || value === null || value === "" ? "-" : String(value);
|
return value === undefined || value === null || value === "" ? "-" : String(value);
|
||||||
@@ -583,6 +661,28 @@ function formatBool(value?: number | null): string {
|
|||||||
return value === 1 ? "是" : "否";
|
return value === 1 ? "是" : "否";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 字节数转可读大小 */
|
||||||
|
function formatSize(bytes?: number): string {
|
||||||
|
if (bytes === undefined || bytes === null || bytes === 0) return "-";
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
let size = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间格式化 */
|
||||||
|
function formatDate(value?: Date | string | null): string {
|
||||||
|
if (value === undefined || value === null || value === "") return "-";
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return "-";
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
function assignTo<T extends object>(reactiveObj: T, data?: object) {
|
function assignTo<T extends object>(reactiveObj: T, data?: object) {
|
||||||
Object.keys(reactiveObj as object).forEach(
|
Object.keys(reactiveObj as object).forEach(
|
||||||
(key) => delete (reactiveObj as Record<string, unknown>)[key]
|
(key) => delete (reactiveObj as Record<string, unknown>)[key]
|
||||||
@@ -692,6 +792,106 @@ async function loadAll(serialno?: string) {
|
|||||||
loadOtherInfo(serialno);
|
loadOtherInfo(serialno);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 应用信息 */
|
||||||
|
async function loadApkInfo(serialno: string) {
|
||||||
|
loading.apk = true;
|
||||||
|
try {
|
||||||
|
const data = await DeviceAPI.getApkList(serialno);
|
||||||
|
apkList.value = data || [];
|
||||||
|
apkLoaded.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载应用信息失败", e);
|
||||||
|
} finally {
|
||||||
|
loading.apk = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换标签页懒加载应用信息(仅首次进入时拉取) */
|
||||||
|
watch(activeTab, (tab) => {
|
||||||
|
if (tab === "appInfo" && props.serialno && !apkLoaded.value) {
|
||||||
|
loadApkInfo(props.serialno);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 打开应用 */
|
||||||
|
const handleLaunchApp = (row: ApkInstallInfo) => {
|
||||||
|
ElMessageBox.confirm(`确定要打开应用「${row.appName || row.packageName}」吗?`, "提示", {
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
DeviceAPI.launchApp(props.serialno, row.packageName ?? "").then(() => {
|
||||||
|
ElMessage.success("打开应用指令已发送");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 清除应用数据 */
|
||||||
|
const handleClearAppData = (row: ApkInstallInfo) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要清除应用「${row.appName || row.packageName}」的数据吗?此操作不可逆!`,
|
||||||
|
"警告",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
DeviceAPI.clearAppData(props.serialno, row.packageName ?? "").then(() => {
|
||||||
|
ElMessage.success("清除应用数据指令已发送");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 卸载应用 */
|
||||||
|
const handleUninstallApp = (row: ApkInstallInfo) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要卸载应用「${row.appName || row.packageName}」吗?此操作不可逆!`,
|
||||||
|
"警告",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "error",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
DeviceAPI.uninstallApp(props.serialno, row.packageName ?? "").then(() => {
|
||||||
|
ElMessage.success("卸载应用指令已发送");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 图标文件输入框 */
|
||||||
|
const iconFileInput = ref<HTMLInputElement>();
|
||||||
|
/** 当前准备上传图标的应用 */
|
||||||
|
let uploadIconTarget: ApkInstallInfo | null = null;
|
||||||
|
|
||||||
|
/** 上传图标:点击触发文件选择 */
|
||||||
|
const handleUploadIcon = (row: ApkInstallInfo) => {
|
||||||
|
uploadIconTarget = row;
|
||||||
|
iconFileInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 选择图标文件后上传 */
|
||||||
|
const onIconFileChange = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
input.value = "";
|
||||||
|
if (!file || !uploadIconTarget) return;
|
||||||
|
const row = uploadIconTarget;
|
||||||
|
DeviceAPI.uploadAppIcon(row.packageName ?? "", file)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success("图标上传成功");
|
||||||
|
loadApkInfo(props.serialno);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({ loadAll });
|
defineExpose({ loadAll });
|
||||||
|
|
||||||
/** 操作按钮事件 */
|
/** 操作按钮事件 */
|
||||||
@@ -704,9 +904,11 @@ const handleEdit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
loadAll(props.serialno).then(() => {
|
loadAll(props.serialno);
|
||||||
|
if (apkLoaded.value && props.serialno) {
|
||||||
|
loadApkInfo(props.serialno);
|
||||||
|
}
|
||||||
ElMessage.success("刷新数据成功");
|
ElMessage.success("刷新数据成功");
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoteLock = () => {
|
const handleRemoteLock = () => {
|
||||||
@@ -931,6 +1133,32 @@ const handleMoreActions = (command: string) => {
|
|||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
|
|
||||||
|
.apk-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apk-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apk-icon-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user