424 lines
13 KiB
Vue
424 lines
13 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<el-card shadow="never" class="mb-4">
|
||
<el-form ref="queryFormRef" :model="params" :inline="true" @submit.prevent>
|
||
<el-form-item label="应用包名" prop="packageName">
|
||
<el-input
|
||
v-model.trim="params.packageName"
|
||
placeholder="按包名筛选(留空显示全部)"
|
||
clearable
|
||
style="width: 320px"
|
||
@keyup.enter="handleQuery"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
|
||
<el-button :icon="RefreshLeft" @click="handleResetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-card shadow="never">
|
||
<!-- 工具栏 -->
|
||
<div class="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<span v-if="selectedRow" class="text-sm">
|
||
已选中:v{{ selectedRow.version }} ·
|
||
<span class="text-info">{{ selectedRow.packageName }}</span>
|
||
<el-tag
|
||
v-if="selectedRow.status === 1"
|
||
type="success"
|
||
effect="light"
|
||
size="small"
|
||
class="ml-1"
|
||
>
|
||
当前生效
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="selectedRow.locked === 1"
|
||
type="warning"
|
||
effect="light"
|
||
size="small"
|
||
class="ml-1"
|
||
>
|
||
已锁定
|
||
</el-tag>
|
||
</span>
|
||
<span v-else class="text-info text-xs">请先在下方列表选择一行,再进行锁定 / 解锁</span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<el-button
|
||
type="primary"
|
||
:icon="Lock"
|
||
:disabled="!selectedRow || selectedRow.status === 1"
|
||
:loading="lockLoading"
|
||
@click="handleToolbarLock"
|
||
>
|
||
锁定
|
||
</el-button>
|
||
<el-button
|
||
type="warning"
|
||
:icon="Unlock"
|
||
:disabled="!selectedRow || selectedRow.locked !== 1"
|
||
:loading="unlockLoading"
|
||
@click="handleToolbarUnlock"
|
||
>
|
||
解锁
|
||
</el-button>
|
||
<el-button type="success" :icon="Upload" @click="openUploadDialog">手动上传</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="list"
|
||
row-key="id"
|
||
border
|
||
stripe
|
||
highlight-current-row
|
||
@current-change="handleCurrentChange"
|
||
>
|
||
<el-table-column label="图标" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-image
|
||
:src="withBase(scope.row.iconUrl)"
|
||
fit="contain"
|
||
style="width: 48px; height: 48px; border-radius: 6px"
|
||
:preview-src-list="previewList"
|
||
:initial-index="previewIndex(scope.row)"
|
||
preview-teleported
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="应用包名"
|
||
prop="packageName"
|
||
min-width="180"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column label="应用名称" prop="appName" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="版本" prop="version" width="80" align="center" />
|
||
<el-table-column label="状态" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'" effect="light">
|
||
{{ scope.row.status === 1 ? "当前生效" : "历史版本" }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="锁定" width="90" align="center">
|
||
<template #default="scope">
|
||
<el-tag v-if="scope.row.locked === 1" type="warning" effect="light">已锁定</el-tag>
|
||
<span v-else class="text-info">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="来源" prop="source" width="100" align="center">
|
||
<template #default="scope">
|
||
<el-tag v-if="scope.row.source === 'admin'" effect="plain">管理端</el-tag>
|
||
<el-tag v-else-if="scope.row.source === 'device'" effect="plain" type="info">
|
||
设备端
|
||
</el-tag>
|
||
<span v-else>{{ scope.row.source }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="MD5" prop="iconMd5" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="更新时间" prop="updateTime" width="180" />
|
||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||
<template #default="scope">
|
||
<el-button
|
||
v-if="scope.row.status === 0"
|
||
type="primary"
|
||
link
|
||
:icon="Lock"
|
||
:loading="lockLoadingId === scope.row.id"
|
||
@click="handleLock(scope.row)"
|
||
>
|
||
锁定此版本
|
||
</el-button>
|
||
<el-button
|
||
v-else-if="scope.row.locked === 1"
|
||
type="warning"
|
||
link
|
||
:icon="Unlock"
|
||
:loading="lockLoadingId === scope.row.id"
|
||
@click="handleUnlock(scope.row)"
|
||
>
|
||
解锁
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
:icon="Delete"
|
||
:loading="deleteLoadingId === scope.row.id"
|
||
@click="handleDelete(scope.row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<pagination
|
||
v-if="total > 0"
|
||
v-model:total="total"
|
||
v-model:page="params.pageNum"
|
||
v-model:limit="params.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
@pagination="fetchData"
|
||
/>
|
||
<el-empty v-if="!loading && total === 0" description="暂无图标" />
|
||
|
||
<!-- 手动上传对话框 -->
|
||
<el-dialog
|
||
v-model="uploadDialogVisible"
|
||
title="手动上传图标"
|
||
width="480px"
|
||
append-to-body
|
||
@closed="resetUploadDialog"
|
||
>
|
||
<el-form :model="uploadForm" label-width="96px">
|
||
<el-form-item label="应用包名" required>
|
||
<el-input
|
||
v-model.trim="uploadForm.packageName"
|
||
placeholder="如 com.example.app"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="应用名称">
|
||
<el-input
|
||
v-model.trim="uploadForm.appName"
|
||
placeholder="可选,不填沿用已有"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="图标文件" required>
|
||
<el-upload
|
||
v-model:file-list="uploadFileList"
|
||
:auto-upload="false"
|
||
:limit="1"
|
||
:show-file-list="true"
|
||
accept=".jpg,.jpeg,.png,.webp,image/*"
|
||
:before-upload="validateUploadFile"
|
||
drag
|
||
class="w-full"
|
||
>
|
||
<div class="el-upload__text">
|
||
将图标拖到此处,或
|
||
<em>点击选择</em>
|
||
</div>
|
||
<template #tip>
|
||
<div class="text-info text-xs">仅支持 jpg/jpeg/png/webp,大小 ≤ 2MB</div>
|
||
</template>
|
||
</el-upload>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :icon="Upload" :loading="uploadLoading" @click="submitUpload">
|
||
确认上传
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, ref } from "vue";
|
||
import { Delete, Lock, RefreshLeft, Search, Unlock, Upload } from "@element-plus/icons-vue";
|
||
import {
|
||
ElMessage,
|
||
ElMessageBox,
|
||
type FormInstance,
|
||
type UploadRawFile,
|
||
type UploadUserFile,
|
||
} from "element-plus";
|
||
|
||
import { usePageTable } from "@/composables";
|
||
import AppIconAPI from "@/api/device/appIcon";
|
||
import type { AppIconQuery, AppIconVO } from "@/api/device/types";
|
||
import { withBase } from "@/utils/asset-url";
|
||
|
||
defineOptions({
|
||
name: "AppIcon",
|
||
});
|
||
|
||
const queryFormRef = ref<FormInstance>();
|
||
|
||
// 分页表格(打开即加载全部图标)
|
||
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
|
||
AppIconVO,
|
||
AppIconQuery
|
||
>({
|
||
initialParams: { pageNum: 1, pageSize: 10, packageName: "" },
|
||
request: AppIconAPI.getPage,
|
||
onBeforeReset: () => queryFormRef.value?.resetFields(),
|
||
});
|
||
|
||
// 当前选中行
|
||
const selectedRow = ref<AppIconVO | null>(null);
|
||
|
||
// 上传(手动上传对话框)
|
||
const uploadDialogVisible = ref(false);
|
||
const uploadLoading = ref(false);
|
||
const uploadForm = reactive({
|
||
packageName: "",
|
||
appName: "",
|
||
});
|
||
const uploadFileList = ref<UploadUserFile[]>([]);
|
||
|
||
// loading 状态
|
||
const lockLoading = ref(false);
|
||
const unlockLoading = ref(false);
|
||
const lockLoadingId = ref<number | null>(null);
|
||
const deleteLoadingId = ref<number | null>(null);
|
||
|
||
function handleCurrentChange(row: AppIconVO | null) {
|
||
selectedRow.value = row;
|
||
}
|
||
|
||
const previewList = computed(() => list.value.map((item) => withBase(item.iconUrl)));
|
||
const previewIndex = (row: AppIconVO) => {
|
||
const idx = list.value.findIndex((item) => item.id === row.id);
|
||
return idx < 0 ? 0 : idx;
|
||
};
|
||
|
||
function validateUploadFile(file: UploadRawFile) {
|
||
const allowExt = ["jpg", "jpeg", "png", "webp"];
|
||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||
const isImage = file.type.startsWith("image/") || allowExt.includes(ext);
|
||
if (!isImage) {
|
||
ElMessage.warning("仅支持 jpg/jpeg/png/webp 格式图标");
|
||
return false;
|
||
}
|
||
if (file.size > 2 * 1024 * 1024) {
|
||
ElMessage.warning("图标大小不能超过 2MB");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function openUploadDialog() {
|
||
uploadDialogVisible.value = true;
|
||
}
|
||
|
||
function resetUploadDialog() {
|
||
uploadForm.packageName = "";
|
||
uploadForm.appName = "";
|
||
uploadFileList.value = [];
|
||
}
|
||
|
||
async function submitUpload() {
|
||
const pkg = uploadForm.packageName.trim();
|
||
if (!pkg) {
|
||
ElMessage.warning("请填写应用包名");
|
||
return;
|
||
}
|
||
if (!/^[A-Za-z0-9._]+$/.test(pkg)) {
|
||
ElMessage.warning("应用包名仅支持字母、数字、. 和 _");
|
||
return;
|
||
}
|
||
const file = uploadFileList.value[0]?.raw as File | undefined;
|
||
if (!file) {
|
||
ElMessage.warning("请选择图标文件");
|
||
return;
|
||
}
|
||
uploadLoading.value = true;
|
||
try {
|
||
await AppIconAPI.upload(pkg, file, uploadForm.appName.trim() || undefined);
|
||
ElMessage.success("上传成功");
|
||
uploadDialogVisible.value = false;
|
||
await fetchData();
|
||
} finally {
|
||
uploadLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleLock(row: AppIconVO) {
|
||
if (!row.id || !row.packageName) return;
|
||
lockLoadingId.value = row.id;
|
||
try {
|
||
await AppIconAPI.lock(row.packageName, row.id);
|
||
ElMessage.success("已锁定该版本");
|
||
await fetchData();
|
||
} finally {
|
||
lockLoadingId.value = null;
|
||
}
|
||
}
|
||
|
||
async function handleUnlock(row: AppIconVO) {
|
||
if (!row.packageName) return;
|
||
lockLoadingId.value = row.id ?? null;
|
||
try {
|
||
await AppIconAPI.unlock(row.packageName);
|
||
ElMessage.success("已解锁");
|
||
await fetchData();
|
||
} finally {
|
||
lockLoadingId.value = null;
|
||
}
|
||
}
|
||
|
||
// 工具栏:锁定当前选中行
|
||
async function handleToolbarLock() {
|
||
const row = selectedRow.value;
|
||
if (!row || !row.id || !row.packageName) return;
|
||
lockLoading.value = true;
|
||
try {
|
||
await AppIconAPI.lock(row.packageName, row.id);
|
||
ElMessage.success("已锁定该版本");
|
||
await fetchData();
|
||
} finally {
|
||
lockLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 工具栏:解锁当前选中行(需为已锁定)
|
||
async function handleToolbarUnlock() {
|
||
const row = selectedRow.value;
|
||
if (!row || !row.packageName) return;
|
||
unlockLoading.value = true;
|
||
try {
|
||
await AppIconAPI.unlock(row.packageName);
|
||
ElMessage.success("已解锁");
|
||
await fetchData();
|
||
} finally {
|
||
unlockLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleDelete(row: AppIconVO) {
|
||
if (!row.id || !row.packageName) return;
|
||
const isActive = row.status === 1;
|
||
const tip = isActive
|
||
? "该图标为当前生效版本,删除后该应用将无生效图标,确认删除?"
|
||
: "确认删除该历史图标版本?";
|
||
try {
|
||
await ElMessageBox.confirm(tip, "删除图标", {
|
||
type: "warning",
|
||
confirmButtonText: "确定",
|
||
cancelButtonText: "取消",
|
||
});
|
||
} catch {
|
||
return;
|
||
}
|
||
|
||
deleteLoadingId.value = row.id;
|
||
try {
|
||
await AppIconAPI.delete(row.packageName, row.id);
|
||
ElMessage.success("删除成功");
|
||
await fetchData();
|
||
} finally {
|
||
deleteLoadingId.value = null;
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.text-info {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
</style>
|