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();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
return request<any, string>({
|
return request<any, string>({
|
||||||
url: `/api/v1/app-icons/${packageName}/upload`,
|
url: `/api/v1/app-icons/${packageName}/upload`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: formData,
|
||||||
|
// appName 服务端是 @RequestParam,走 query 而非 FormData 才能稳定绑定
|
||||||
|
params: appName ? { appName } : undefined,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -468,10 +468,10 @@
|
|||||||
<el-table-column label="图标" width="70" align="center">
|
<el-table-column label="图标" width="70" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-image
|
<el-image
|
||||||
:src="row.iconUrl"
|
:src="resolveIconUrl(row.iconUrl)"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
class="apk-icon"
|
class="apk-icon"
|
||||||
:preview-src-list="row.iconUrl ? [row.iconUrl] : []"
|
:preview-src-list="row.iconUrl ? [resolveIconUrl(row.iconUrl)] : []"
|
||||||
preview-teleported
|
preview-teleported
|
||||||
>
|
>
|
||||||
<template #error>
|
<template #error>
|
||||||
@@ -484,43 +484,88 @@
|
|||||||
prop="appName"
|
prop="appName"
|
||||||
label="应用名称"
|
label="应用名称"
|
||||||
min-width="160"
|
min-width="160"
|
||||||
|
sortable
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="packageName"
|
prop="packageName"
|
||||||
label="包名"
|
label="包名"
|
||||||
min-width="220"
|
min-width="220"
|
||||||
|
sortable
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column prop="versionName" label="版本" width="100" show-overflow-tooltip />
|
<el-table-column prop="versionName" label="版本" width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="大小" width="110">
|
<el-table-column label="大小" width="110" sortable sort-by="apkSize">
|
||||||
<template #default="{ row }">{{ formatSize(row.apkSize) }}</template>
|
<template #default="{ row }">{{ formatSize(row.apkSize) }}</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.systemApp" type="info" size="small">系统</el-tag>
|
<el-tag v-if="row.systemApp" type="info" size="small">系统</el-tag>
|
||||||
<el-tag v-else type="success" size="small">普通</el-tag>
|
<el-tag v-else type="success" size="small">普通</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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>
|
<template #default="{ row }">{{ formatDate(row.installTime) }}</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="handleLaunchApp(row)">打开</el-button>
|
<div class="apk-actions">
|
||||||
<el-button link type="warning" @click="handleClearAppData(row)">清除数据</el-button>
|
<el-tooltip content="打开" placement="top">
|
||||||
<el-button link type="info" @click="handleUploadIcon(row)">上传图标</el-button>
|
<el-button
|
||||||
<el-button link type="danger" @click="handleUninstallApp(row)">卸载</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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<input
|
</div>
|
||||||
ref="iconFileInput"
|
</el-tab-pane>
|
||||||
type="file"
|
<el-tab-pane label="定位信息" name="locationInfo">
|
||||||
accept="image/jpeg,image/png,image/webp"
|
<div class="tab-content">
|
||||||
style="display: none"
|
<el-empty description="定位信息加载中..." />
|
||||||
@change="onIconFileChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
@@ -541,12 +586,6 @@
|
|||||||
<el-empty description="操作日志加载中..." />
|
<el-empty description="操作日志加载中..." />
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="定位信息" name="locationInfo">
|
|
||||||
<div class="tab-content">
|
|
||||||
<el-empty description="定位信息加载中..." />
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -571,6 +610,9 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
RefreshRight,
|
RefreshRight,
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
|
VideoPlay,
|
||||||
|
Brush,
|
||||||
|
SwitchButton,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
|
||||||
@@ -650,6 +692,22 @@ const apkList = ref<ApkInstallInfo[]>([]);
|
|||||||
/** 应用信息是否已加载(懒加载,仅首次进入 tab 时拉取) */
|
/** 应用信息是否已加载(懒加载,仅首次进入 tab 时拉取) */
|
||||||
const apkLoaded = ref(false);
|
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 {
|
function formatText(value?: string | number | null): string {
|
||||||
return value === undefined || value === null || value === "" ? "-" : String(value);
|
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())}`;
|
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) {
|
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]
|
||||||
@@ -866,28 +932,17 @@ const handleUninstallApp = (row: ApkInstallInfo) => {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 图标文件输入框 */
|
/** 停止应用 */
|
||||||
const iconFileInput = ref<HTMLInputElement>();
|
const handleStopApp = (row: ApkInstallInfo) => {
|
||||||
/** 当前准备上传图标的应用 */
|
ElMessageBox.confirm(`确定要停止运行应用「${row.appName || row.packageName}」吗?`, "提示", {
|
||||||
let uploadIconTarget: ApkInstallInfo | null = null;
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
/** 上传图标:点击触发文件选择 */
|
type: "warning",
|
||||||
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(() => {
|
.then(() => {
|
||||||
ElMessage.success("图标上传成功");
|
DeviceAPI.stopApp(props.serialno, row.packageName ?? "").then(() => {
|
||||||
loadApkInfo(props.serialno);
|
ElMessage.success("停止应用指令已发送");
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -1140,6 +1195,24 @@ const handleMoreActions = (command: string) => {
|
|||||||
margin-bottom: 12px;
|
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 {
|
.apk-icon {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|||||||
Reference in New Issue
Block a user