Files
crawler-plugin/admin-frontend-vue/src/pages/records/RecordsDigitalHumanVersionPage.vue
T

281 lines
12 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.
<script setup lang="ts">
/** 数字人版本管理页(module 13 task 261 对齐 admin panel-digital-human-version;本轮补齐分页/三态/上传进度/下载语义)。 */
import { formatDateTime } from '@/utils/datetime'
import { computed, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
deleteDigitalHumanVersion,
fetchDigitalHumanDownloadUrl,
fetchDigitalHumanVersions,
releaseDigitalHumanVersion,
setLatestDigitalHumanVersion,
uploadDigitalHumanVersion,
} from './digitalhuman-api.ts'
import type { DigitalHumanVersion } from './digitalhuman-dto.ts'
import {
digitalHumanChangelogShort,
digitalHumanDeleteConfirmText,
digitalHumanVersionFileSizeText,
} from './digitalhuman-dto.ts'
import { validateDigitalHumanUpload } from './digitalhuman-upload-model.ts'
import { canReleaseDigitalHumanVersion, releaseDigitalHumanConfirmText } from './digitalhuman-release.ts'
import { canSetLatestDigitalHumanVersion, setLatestDigitalHumanConfirmText } from './digitalhuman-latest.ts'
import { digitalHumanStatusDisplay } from './digitalhuman-status-display.ts'
const loading = ref(false)
const items = ref<DigitalHumanVersion[]>([])
/** 分页(对齐 admin.js:6311 pageSize=20 + 数字页码 + 跳转;Java 列表接口不分页,前端分页呈现)。 */
const pageSize = 20
const page = ref(1)
const pagedItems = computed(() => {
const start = (page.value - 1) * pageSize
return items.value.slice(start, start + pageSize)
})
const uploadVisible = ref(false)
const uploading = ref(false)
const uploadProgress = ref<number | null>(null)
const form = ref({ version: '', file: null as { name?: string; size?: number } | null, changelog: '', minClientVersion: '' })
const confirming = ref<string | null>(null)
const downloadingVersion = ref<string | null>(null)
async function load() {
loading.value = true
try {
items.value = (await fetchDigitalHumanVersions()).items
// 数据收缩后页码越界回钳。
const last = Math.max(Math.ceil(items.value.length / pageSize), 1)
if (page.value > last) page.value = last
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '版本列表加载失败')
} finally {
loading.value = false
}
}
function onFileChange(file: File) {
form.value.file = file
}
async function submitUpload() {
const { valid, errors } = validateDigitalHumanUpload({ ...form.value, file: form.value.file })
if (!valid) {
ElMessage.warning(errors.version || errors.file || '请完善上传信息')
return
}
uploading.value = true
uploadProgress.value = 0
const version = (form.value.version || '').trim()
try {
await uploadDigitalHumanVersion(
{ version, file: form.value.file, changelog: form.value.changelog, minClientVersion: form.value.minClientVersion },
form.value.file?.name,
(percent) => {
uploadProgress.value = percent
},
)
ElMessage.success(`上传成功!版本:${version}(状态:草稿,请在列表中点击“发布”按钮)`)
uploadVisible.value = false
uploadProgress.value = null
form.value = { version: '', file: null, changelog: '', minClientVersion: '' }
load()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '上传失败')
} finally {
uploading.value = false
}
}
async function release(row: DigitalHumanVersion) {
const text = releaseDigitalHumanConfirmText(row.version)
if (!text) return
confirming.value = row.version
try {
await ElMessageBox.confirm(text, '发布版本', { type: 'warning', confirmButtonText: '发布', cancelButtonText: '取消' })
await releaseDigitalHumanVersion(row.version)
ElMessage.success('已发布')
load()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '发布失败')
} finally {
confirming.value = null
}
}
async function setLatest(row: DigitalHumanVersion) {
const text = setLatestDigitalHumanConfirmText(row.version)
if (!text) return
confirming.value = row.version
try {
await ElMessageBox.confirm(text, '设为最新', { type: 'warning', confirmButtonText: '设为最新', cancelButtonText: '取消' })
await setLatestDigitalHumanVersion(row.version)
ElMessage.success('已设为最新')
load()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '操作失败')
} finally {
confirming.value = null
}
}
/** 下载(对齐 admin.js downloadDigitalHumanVersionRELEASED 恒显示,点击取链接并触发下载)。 */
async function download(row: DigitalHumanVersion) {
if (!row.version) return
downloadingVersion.value = row.version
try {
const url = await fetchDigitalHumanDownloadUrl(row.version)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `ShuFuDigitalHuman-${row.version}.zip`
anchor.target = '_blank'
anchor.rel = 'noopener'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '下载失败')
} finally {
downloadingVersion.value = null
}
}
async function remove(row: DigitalHumanVersion) {
const text = digitalHumanDeleteConfirmText(row.version)
if (!text) return
confirming.value = row.version
try {
await ElMessageBox.confirm(text, '删除版本', {
type: 'warning',
confirmButtonText: '删除',
cancelButtonText: '取消',
})
await deleteDigitalHumanVersion(row.version)
ElMessage.success('删除成功')
load()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(error instanceof Error ? error.message : '删除失败')
} finally {
confirming.value = null
}
}
function canDelete(row: DigitalHumanVersion): boolean {
return row.isLatest !== true
}
function statusOf(row: DigitalHumanVersion): { text: string; tag: 'info' | 'success' | 'danger' } {
return digitalHumanStatusDisplay(row.status)
}
onMounted(load)
</script>
<template>
<div class="page-stack">
<div class="page-heading">
<div>
<h2>数字人版本管理</h2>
<p>管理数字人客户端的草稿/已发布/已废弃版本</p>
</div>
<div class="actions">
<el-button type="primary" @click="uploadVisible = true">上传草稿状态</el-button>
</div>
</div>
<el-card shadow="never">
<el-table v-loading="loading" :data="pagedItems" stripe border empty-text="暂无数字人版本">
<el-table-column prop="version" label="版本号" min-width="150" />
<el-table-column label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="statusOf(row as DigitalHumanVersion).tag" size="small">
{{ statusOf(row as DigitalHumanVersion).text }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="最新" min-width="60" align="center">
<template #default="{ row }">
<span v-if="(row as DigitalHumanVersion).isLatest" class="star" title="最新版本"></span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="100">
<template #default="{ row }">{{ digitalHumanVersionFileSizeText((row as DigitalHumanVersion).fileSize) }}</template>
</el-table-column>
<el-table-column label="MD5" min-width="130">
<template #default="{ row }">
<span :title="(row as DigitalHumanVersion).md5 || ''">{{ (row as DigitalHumanVersion).md5 ? `${((row as DigitalHumanVersion).md5 || '').substring(0, 12)}...` : '—' }}</span>
</template>
</el-table-column>
<el-table-column label="更新说明" min-width="180">
<template #default="{ row }">
<span :title="(row as DigitalHumanVersion).changelog || ''">{{ digitalHumanChangelogShort((row as DigitalHumanVersion).changelog) }}</span>
</template>
</el-table-column>
<el-table-column label="发布时间" min-width="170">
<template #default="{ row }">{{ formatDateTime((row as DigitalHumanVersion).releasedAt) }}</template>
</el-table-column>
<el-table-column label="操作" min-width="240" fixed="right">
<template #default="{ row }">
<el-button v-if="canReleaseDigitalHumanVersion((row as DigitalHumanVersion).status)" text type="warning" size="small" :loading="confirming === (row as DigitalHumanVersion).version" @click="release(row as DigitalHumanVersion)">发布</el-button>
<el-button v-if="!canReleaseDigitalHumanVersion((row as DigitalHumanVersion).status) && canSetLatestDigitalHumanVersion((row as DigitalHumanVersion).status) && !(row as DigitalHumanVersion).isLatest" text type="primary" size="small" :loading="confirming === (row as DigitalHumanVersion).version" @click="setLatest(row as DigitalHumanVersion)">设为最新</el-button>
<el-button v-if="canSetLatestDigitalHumanVersion((row as DigitalHumanVersion).status)" text type="success" size="small" :loading="downloadingVersion === (row as DigitalHumanVersion).version" @click="download(row as DigitalHumanVersion)">下载</el-button>
<el-button v-if="canDelete(row as DigitalHumanVersion)" text type="danger" size="small" :loading="confirming === (row as DigitalHumanVersion).version" @click="remove(row as DigitalHumanVersion)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<span> {{ items.length.toLocaleString() }} </span>
<el-pagination
background
layout="prev, pager, next, jumper"
:total="items.length"
:page-size="pageSize"
:current-page="page"
@current-change="(p: number) => { page = p }"
/>
</div>
</el-card>
<el-dialog v-model="uploadVisible" title="上传数字人版本" width="560px">
<p class="upload-desc">上传数字人程序 ZIP 包,系统会自动计算 MD5 并存储到 OSS。上传后状态为草稿,需要手动发布。</p>
<el-form label-width="110px">
<el-form-item label="版本号" required>
<el-input v-model="form.version" placeholder=" 1.0.2" />
</el-form-item>
<el-form-item label="压缩包" required>
<el-upload :auto-upload="false" :limit="1" accept=".zip" :on-change="(file: any) => onFileChange(file.raw)" :on-remove="() => (form.file = null)">
<el-button>选择文件</el-button>
</el-upload>
<div v-if="form.file" class="dim">{{ form.file.name }}</div>
</el-form-item>
<el-form-item label="最低客户端">
<el-input v-model="form.minClientVersion" placeholder="可选客户端最低版本要求" />
</el-form-item>
<el-form-item label="更新说明">
<el-input v-model="form.changelog" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
<el-alert v-if="uploading && uploadProgress != null" :title="`正在上传:${uploadProgress}%`" type="info" :closable="false" show-icon />
</el-form>
<template #footer>
<el-button @click="uploadVisible = false">取消</el-button>
<el-button type="primary" :loading="uploading" @click="submitUpload">上传草稿状态</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.page-heading { display: flex; justify-content: space-between; align-items: flex-start; }
.actions { display: flex; align-items: center; gap: 10px; }
.dim { color: var(--el-text-color-secondary); font-size: 12px; }
.star { color: #e6a23c; font-weight: 700; }
.upload-desc { margin: 0 0 12px; color: var(--el-text-color-secondary); font-size: 12.5px; line-height: 1.6; }
.table-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 14px; }
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
</style>