feat(设备详情): 优化应用列表展示和操作交互
- 新增停止应用功能,操作列改为图标按钮并优化样式 - 应用列表增加排序功能,新增更新时间列 - 修复应用图标加载路径问题,补全代理前缀 - 上传图标接口支持可选应用名称参数 - 调整定位信息标签页位置
This commit is contained in:
@@ -392,14 +392,31 @@ const DeviceAPI = {
|
||||
});
|
||||
},
|
||||
|
||||
/** 上传应用图标(全局图标库) */
|
||||
uploadAppIcon(packageName: string, file: File) {
|
||||
/** 停止应用 */
|
||||
stopApp(sn: string, packageName: string) {
|
||||
return request({
|
||||
url: `${DEVICE_BASE_URL}/${sn}/app/stop`,
|
||||
method: "post",
|
||||
data: { packageName },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 上传应用图标(全局图标库)
|
||||
*
|
||||
* @param packageName 应用包名
|
||||
* @param file 图标文件(jpg/jpeg/png/webp)
|
||||
* @param appName 应用名称,可选;不传则由服务端沿用已有名称
|
||||
*/
|
||||
uploadAppIcon(packageName: string, file: File, appName?: string) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return request<any, string>({
|
||||
url: `/api/v1/app-icons/${packageName}/upload`,
|
||||
method: "post",
|
||||
data: formData,
|
||||
// appName 服务端是 @RequestParam,走 query 而非 FormData 才能稳定绑定
|
||||
params: appName ? { appName } : undefined,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
|
||||
@@ -468,10 +468,10 @@
|
||||
<el-table-column label="图标" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
:src="row.iconUrl"
|
||||
:src="resolveIconUrl(row.iconUrl)"
|
||||
fit="cover"
|
||||
class="apk-icon"
|
||||
:preview-src-list="row.iconUrl ? [row.iconUrl] : []"
|
||||
:preview-src-list="row.iconUrl ? [resolveIconUrl(row.iconUrl)] : []"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
@@ -484,43 +484,88 @@
|
||||
prop="appName"
|
||||
label="应用名称"
|
||||
min-width="160"
|
||||
sortable
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="packageName"
|
||||
label="包名"
|
||||
min-width="220"
|
||||
sortable
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="versionName" label="版本" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="110">
|
||||
<el-table-column prop="versionName" label="版本" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="110" sortable sort-by="apkSize">
|
||||
<template #default="{ row }">{{ formatSize(row.apkSize) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="系统应用" width="90" align="center">
|
||||
<el-table-column
|
||||
label="系统应用"
|
||||
width="120"
|
||||
align="center"
|
||||
sortable
|
||||
sort-by="systemApp"
|
||||
>
|
||||
<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">
|
||||
<el-table-column
|
||||
label="安装时间"
|
||||
width="170"
|
||||
sortable
|
||||
:sort-method="
|
||||
(a: ApkInstallInfo, b: ApkInstallInfo) =>
|
||||
dateValue(a.installTime) - dateValue(b.installTime)
|
||||
"
|
||||
>
|
||||
<template #default="{ row }">{{ formatDate(row.installTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<el-table-column
|
||||
label="更新时间"
|
||||
width="170"
|
||||
sortable
|
||||
:sort-method="
|
||||
(a: ApkInstallInfo, b: ApkInstallInfo) =>
|
||||
dateValue(a.lastUpdateTime) - dateValue(b.lastUpdateTime)
|
||||
"
|
||||
>
|
||||
<template #default="{ row }">{{ formatDate(row.lastUpdateTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" 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>
|
||||
<div class="apk-actions">
|
||||
<el-tooltip content="打开" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:icon="VideoPlay"
|
||||
@click="handleLaunchApp(row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="停止" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
:icon="SwitchButton"
|
||||
@click="handleStopApp(row)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="清除数据" placement="top">
|
||||
<el-button link type="warning" :icon="Brush" @click="handleClearAppData(row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="卸载" placement="top">
|
||||
<el-button link type="danger" :icon="Delete" @click="handleUninstallApp(row)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</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>
|
||||
<el-tab-pane label="定位信息" name="locationInfo">
|
||||
<div class="tab-content">
|
||||
<el-empty description="定位信息加载中..." />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -541,12 +586,6 @@
|
||||
<el-empty description="操作日志加载中..." />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="定位信息" name="locationInfo">
|
||||
<div class="tab-content">
|
||||
<el-empty description="定位信息加载中..." />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -571,6 +610,9 @@ import {
|
||||
Delete,
|
||||
RefreshRight,
|
||||
ArrowDown,
|
||||
VideoPlay,
|
||||
Brush,
|
||||
SwitchButton,
|
||||
} from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
@@ -650,6 +692,22 @@ const apkList = ref<ApkInstallInfo[]>([]);
|
||||
/** 应用信息是否已加载(懒加载,仅首次进入 tab 时拉取) */
|
||||
const apkLoaded = ref(false);
|
||||
|
||||
/**
|
||||
* 拼接应用图标完整访问地址
|
||||
*
|
||||
* 后端返回的是相对路径(如 /static/app_icon/{md5}.png)。
|
||||
* img/el-image 的 src 是浏览器原生请求,不经过 axios,拿不到 baseURL,
|
||||
* 因此必须手动补上代理前缀,否则请求会打到前端 dev server 上导致 404。
|
||||
*/
|
||||
function resolveIconUrl(url?: string | null): string {
|
||||
if (!url) return "";
|
||||
// 已是绝对地址(http/https)或 base64,直接返回
|
||||
if (/^(https?:)?\/\//.test(url) || url.startsWith("data:")) return url;
|
||||
const baseApi = import.meta.env.VITE_APP_BASE_API ?? "";
|
||||
// 避免 baseApi 结尾与 url 开头的斜杠重复
|
||||
return `${baseApi.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
}
|
||||
|
||||
/** 空值统一显示占位符 */
|
||||
function formatText(value?: string | number | null): string {
|
||||
return value === undefined || value === null || value === "" ? "-" : String(value);
|
||||
@@ -683,6 +741,14 @@ function formatDate(value?: Date | string | null): string {
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
/** 将时间转为可用于排序的时间戳,无效值视为 0(排在最前) */
|
||||
function dateValue(value?: Date | string | null): number {
|
||||
if (value === undefined || value === null || value === "") return 0;
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
const time = date.getTime();
|
||||
return Number.isNaN(time) ? 0 : time;
|
||||
}
|
||||
|
||||
function assignTo<T extends object>(reactiveObj: T, data?: object) {
|
||||
Object.keys(reactiveObj as object).forEach(
|
||||
(key) => delete (reactiveObj as Record<string, unknown>)[key]
|
||||
@@ -866,28 +932,17 @@ const handleUninstallApp = (row: ApkInstallInfo) => {
|
||||
.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)
|
||||
/** 停止应用 */
|
||||
const handleStopApp = (row: ApkInstallInfo) => {
|
||||
ElMessageBox.confirm(`确定要停止运行应用「${row.appName || row.packageName}」吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success("图标上传成功");
|
||||
loadApkInfo(props.serialno);
|
||||
DeviceAPI.stopApp(props.serialno, row.packageName ?? "").then(() => {
|
||||
ElMessage.success("停止应用指令已发送");
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
@@ -1140,6 +1195,24 @@ const handleMoreActions = (command: string) => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.apk-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
.el-button {
|
||||
padding: 4px;
|
||||
margin-left: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.el-button .el-icon,
|
||||
.el-button svg {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.apk-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
Reference in New Issue
Block a user