feat(software-version): 安装包改浏览器直传 MinIO,支持批量删除
- 后端新增 /api/admin/version/presign、confirm、delete 端点;直传后服务端校验对象并写 web_config - OSS 层新增软件版本对象 presign PUT/大小校验/删除与 client 桶 URL 识别 - 后台前端上传改 presign→PUT→confirm 三段式直传,带进度条;成功提示精简为"发布成功" - 版本列表加勾选/表头全选、批量删除与单行删除;操作按钮样式对齐其他子菜单
This commit is contained in:
@@ -5,7 +5,7 @@ import { formatDateTime } from '@/utils/datetime'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import OldPagination from '@/components/OldPagination.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchSoftwareVersions, uploadSoftwareVersion } from './version-api.ts'
|
||||
import { fetchSoftwareVersions, uploadSoftwareVersion, deleteSoftwareVersions } from './version-api.ts'
|
||||
import type { SoftwareVersionItem } from './version-dto.ts'
|
||||
import { isNonEmptyVersion } from './version-dto.ts'
|
||||
|
||||
@@ -29,8 +29,53 @@ watch(filteredItems, () => {
|
||||
page.value = Math.min(page.value, Math.max(1, Math.ceil(filteredItems.value.length / pageSize.value)))
|
||||
})
|
||||
|
||||
// 勾选删除:选中跨页累计;表头复选只作用于当前页,避免误选整表。
|
||||
const selectedIds = ref<Set<number>>(new Set())
|
||||
const selectedCount = computed(() => selectedIds.value.size)
|
||||
function toggleSelect(id: number) {
|
||||
const next = new Set(selectedIds.value)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
selectedIds.value = next
|
||||
}
|
||||
function toggleSelectAllPage() {
|
||||
const pageRows = pagedVersions.value
|
||||
const allOn = pageRows.length > 0 && pageRows.every((row) => selectedIds.value.has(row.id))
|
||||
const next = new Set(selectedIds.value)
|
||||
if (allOn) pageRows.forEach((row) => next.delete(row.id))
|
||||
else pageRows.forEach((row) => next.add(row.id))
|
||||
selectedIds.value = next
|
||||
}
|
||||
async function removeSelected() {
|
||||
if (!selectedCount.value) return
|
||||
const rows = items.value.filter((row) => selectedIds.value.has(row.id))
|
||||
const sample = rows.slice(0, 3).map((row) => row.version).join('、')
|
||||
const summary = rows.length > 3 ? `${sample} 等 ${rows.length} 个` : sample
|
||||
if (!window.confirm(`确认删除选中的 ${rows.length} 个版本(${summary})?此操作不可恢复。`)) return
|
||||
try {
|
||||
await deleteSoftwareVersions(Array.from(selectedIds.value))
|
||||
ElMessage.success(`已删除 ${rows.length} 个版本`)
|
||||
selectedIds.value = new Set()
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
async function removeOne(row: SoftwareVersionItem) {
|
||||
if (!window.confirm(`确认删除版本 ${row.version}?此操作不可恢复。`)) return
|
||||
try {
|
||||
await deleteSoftwareVersions([row.id])
|
||||
ElMessage.success('删除成功')
|
||||
selectedIds.value = new Set([...selectedIds.value].filter((id) => id !== row.id))
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const uploadVisible = ref(false)
|
||||
const uploading = ref(false)
|
||||
const uploadPercent = ref(0)
|
||||
const newVersion = ref('')
|
||||
const pickedFile = ref<File | null>(null)
|
||||
/** 弹窗内成功/失败文案(对齐 admin.js msgVersion 留驻)。 */
|
||||
@@ -63,6 +108,7 @@ function openUpload() {
|
||||
async function submitUpload() {
|
||||
uploadMsg.value = ''
|
||||
uploadMsgOk.value = false
|
||||
uploadPercent.value = 0
|
||||
if (!isNonEmptyVersion(newVersion.value)) {
|
||||
uploadMsg.value = '请填写版本号'
|
||||
return
|
||||
@@ -75,16 +121,23 @@ async function submitUpload() {
|
||||
uploadMsg.value = '仅支持 .zip 格式'
|
||||
return
|
||||
}
|
||||
if (pickedFile.value.size > 512 * 1024 * 1024) {
|
||||
uploadMsg.value = '文件超过允许的大小限制'
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const created = await uploadSoftwareVersion(newVersion.value.trim(), pickedFile.value, pickedFile.value.name)
|
||||
const version = created?.version || newVersion.value.trim()
|
||||
const link = created?.fileUrl
|
||||
// 对齐 admin.js:6484-6488:弹窗保持打开,成功文案(含链接)留驻可复制,仅清空输入并刷新列表。
|
||||
uploadMsg.value = link ? `发布成功。版本:${version},链接:${link}` : `发布成功。版本:${version}`
|
||||
// 浏览器直传 MinIO:presign → PUT(进度条)→ confirm 落库。
|
||||
await uploadSoftwareVersion(newVersion.value.trim(), pickedFile.value, pickedFile.value.name, (p) => {
|
||||
uploadPercent.value = p
|
||||
uploadMsg.value = p >= 100 ? '上传完成,正在登记版本...' : `正在上传:${p}%`
|
||||
})
|
||||
// 成功仅提示“发布成功”,弹窗留驻,不再展示版本号与链接。
|
||||
uploadMsg.value = '发布成功'
|
||||
uploadMsgOk.value = true
|
||||
newVersion.value = ''
|
||||
pickedFile.value = null
|
||||
uploadPercent.value = 0
|
||||
load()
|
||||
} catch (error) {
|
||||
uploadMsg.value = error instanceof Error ? error.message : '上传失败'
|
||||
@@ -103,6 +156,7 @@ onMounted(load)
|
||||
<h3>版本列表</h3>
|
||||
<div class="version-head-tools">
|
||||
<input v-model="versionKeyword" type="text" placeholder="搜索版本号" class="version-keyword" />
|
||||
<button class="btn btn-danger" type="button" :disabled="!selectedCount" @click="removeSelected">删除选中{{ selectedCount ? `(${selectedCount})` : '' }}</button>
|
||||
<button class="btn" type="button" @click="openUpload">上传新版本</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,32 +165,39 @@ onMounted(load)
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 120px">版本号</th>
|
||||
<th style="width: 40px" class="th-select">
|
||||
<input type="checkbox" :checked="pagedVersions.length > 0 && pagedVersions.every((r) => selectedIds.has(r.id))" @change="toggleSelectAllPage" :disabled="!pagedVersions.length" />
|
||||
</th>
|
||||
<th style="width: 110px">版本号</th>
|
||||
<th>下载链接</th>
|
||||
<th style="width: 160px">创建时间</th>
|
||||
<th style="width: 120px">操作</th>
|
||||
<th style="width: 150px">创建时间</th>
|
||||
<th style="width: 150px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-if="filteredItems.length">
|
||||
<tr v-for="row in pagedVersions" :key="row.version + row.id">
|
||||
<tr v-for="row in pagedVersions" :key="row.version + row.id" :class="{ 'row-selected': selectedIds.has(row.id) }">
|
||||
<td class="td-select">
|
||||
<input type="checkbox" :checked="selectedIds.has(row.id)" @change="toggleSelect(row.id)" />
|
||||
</td>
|
||||
<td>{{ row.version }}</td>
|
||||
<td>
|
||||
<a v-if="row.fileUrl" class="link-cell" :href="row.fileUrl" target="_blank" rel="noopener" :title="row.fileUrl">{{ row.fileUrl }}</a>
|
||||
<span v-else class="dim">—</span>
|
||||
</td>
|
||||
<td>{{ formatDateTime(row.createdAt) }}</td>
|
||||
<td>
|
||||
<a v-if="row.fileUrl" class="btn btn-sm btn-secondary dl-btn" :href="row.fileUrl" download>下载</a>
|
||||
<td class="ops-cell">
|
||||
<a v-if="row.fileUrl" class="btn btn-sm dl-btn" :href="row.fileUrl" download>下载</a>
|
||||
<span v-else class="dim">—</span>
|
||||
<button class="btn btn-sm btn-danger" type="button" @click="removeOne(row)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-else-if="loading">
|
||||
<td colspan="4" class="empty-tip">加载中...</td>
|
||||
<td colspan="5" class="empty-tip">加载中...</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td colspan="4" class="empty-tip">{{ versionKeyword ? '暂无匹配版本' : '暂无版本记录' }}</td>
|
||||
<td colspan="5" class="empty-tip">{{ versionKeyword ? '暂无匹配版本' : '暂无版本记录' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -157,6 +218,7 @@ onMounted(load)
|
||||
<p class="zip-hint">仅支持 .zip 格式</p>
|
||||
<div v-if="pickedFile" class="dim">{{ pickedFile.name }}</div>
|
||||
</el-form-item>
|
||||
<el-progress v-if="uploading && uploadPercent > 0" :percentage="uploadPercent" :stroke-width="10" :status="uploadPercent >= 100 ? 'success' : undefined" class="upload-progress" />
|
||||
<el-alert v-if="uploadMsg" :title="uploadMsg" :type="uploadMsgOk ? 'success' : 'error'" :closable="false" show-icon class="upload-msg" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -276,7 +338,7 @@ h3 {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.version-table-scroll > table {
|
||||
min-width: 760px;
|
||||
min-width: 840px;
|
||||
}
|
||||
.table-scroll th,
|
||||
.table-scroll td {
|
||||
@@ -338,6 +400,36 @@ h3 {
|
||||
color: #5b6f83;
|
||||
font-size: 12px;
|
||||
}
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #c06d77, #b35f6a);
|
||||
border-color: #b35f6a;
|
||||
}
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #cb7c84, #b96570);
|
||||
}
|
||||
.ops-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.row-selected td {
|
||||
background: #fdf0ee;
|
||||
}
|
||||
.th-select,
|
||||
.td-select {
|
||||
text-align: center;
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
.th-select input,
|
||||
.td-select input {
|
||||
cursor: pointer;
|
||||
accent-color: #b33a2e;
|
||||
}
|
||||
.upload-progress {
|
||||
margin: 2px 0 10px;
|
||||
}
|
||||
.upload-msg {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user