feat(device): 新增设备已安装应用管理功能
为设备详情页添加应用信息标签页,支持查看已安装应用列表、打开/清除/卸载应用,以及上传应用图标等操作。
This commit is contained in:
@@ -452,7 +452,75 @@
|
||||
<!-- 扩展标签页(应用信息 / 策略信息 / 合规状态 / 操作日志 / 定位信息) -->
|
||||
<el-tab-pane label="应用信息" name="appInfo">
|
||||
<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>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -486,6 +554,7 @@
|
||||
<script setup lang="ts">
|
||||
import DeviceAPI from "@/api/system/device";
|
||||
import type {
|
||||
ApkInstallInfo,
|
||||
DeviceBasicInfo,
|
||||
DeviceHardwareInfo,
|
||||
DeviceNetworkInfo,
|
||||
@@ -527,6 +596,9 @@ watch(
|
||||
() => props.serialno,
|
||||
(newSerialno) => {
|
||||
if (newSerialno) {
|
||||
// 切换设备时重置应用信息懒加载状态
|
||||
apkLoaded.value = false;
|
||||
apkList.value = [];
|
||||
loadAll(newSerialno);
|
||||
}
|
||||
},
|
||||
@@ -570,8 +642,14 @@ const loading = reactive({
|
||||
network: false,
|
||||
security: false,
|
||||
other: false,
|
||||
apk: false,
|
||||
});
|
||||
|
||||
/** 设备已安装应用列表 */
|
||||
const apkList = ref<ApkInstallInfo[]>([]);
|
||||
/** 应用信息是否已加载(懒加载,仅首次进入 tab 时拉取) */
|
||||
const apkLoaded = ref(false);
|
||||
|
||||
/** 空值统一显示占位符 */
|
||||
function formatText(value?: string | number | null): string {
|
||||
return value === undefined || value === null || value === "" ? "-" : String(value);
|
||||
@@ -583,6 +661,28 @@ function formatBool(value?: number | null): string {
|
||||
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) {
|
||||
Object.keys(reactiveObj as object).forEach(
|
||||
(key) => delete (reactiveObj as Record<string, unknown>)[key]
|
||||
@@ -692,6 +792,106 @@ async function loadAll(serialno?: string) {
|
||||
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 });
|
||||
|
||||
/** 操作按钮事件 */
|
||||
@@ -704,9 +904,11 @@ const handleEdit = () => {
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadAll(props.serialno).then(() => {
|
||||
ElMessage.success("刷新数据成功");
|
||||
});
|
||||
loadAll(props.serialno);
|
||||
if (apkLoaded.value && props.serialno) {
|
||||
loadApkInfo(props.serialno);
|
||||
}
|
||||
ElMessage.success("刷新数据成功");
|
||||
};
|
||||
|
||||
const handleRemoteLock = () => {
|
||||
@@ -931,6 +1133,32 @@ const handleMoreActions = (command: string) => {
|
||||
|
||||
.tab-content {
|
||||
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