Files
vue3-element-admin/src/views/devices/app-icon/index.vue
T

424 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>