优化
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type PdaApp = 'domestic' | 'overseas'
|
||||
export type PdaPackageType = 'apk' | 'wgt'
|
||||
export interface PdaRelease {
|
||||
id: string
|
||||
app: PdaApp
|
||||
appid: string
|
||||
packageType: PdaPackageType
|
||||
versionName: string
|
||||
versionCode: number
|
||||
minAppVersionCode: number
|
||||
mandatory: boolean
|
||||
releaseNotes: string
|
||||
status: 'DRAFT' | 'PUBLISHED' | 'WITHDRAWN'
|
||||
current: boolean
|
||||
sha256: string
|
||||
sizeBytes: number
|
||||
downloadUrl: string | null
|
||||
createdAt: string
|
||||
publishedAt: string | null
|
||||
}
|
||||
|
||||
const basePath = '/platform/v1/files/pda-releases'
|
||||
|
||||
export async function listPdaReleases(app: PdaApp) {
|
||||
const response = await http.get<ApiEnvelope<PdaRelease[]>, ApiEnvelope<PdaRelease[]>>(`${basePath}/${app}`)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function uploadPdaRelease(app: PdaApp, data: {
|
||||
packageType: PdaPackageType
|
||||
versionName: string
|
||||
versionCode: number
|
||||
minAppVersionCode: number
|
||||
mandatory: boolean
|
||||
releaseNotes: string
|
||||
file: File
|
||||
}) {
|
||||
const form = new FormData()
|
||||
form.append('packageType', data.packageType)
|
||||
form.append('versionName', data.versionName)
|
||||
form.append('versionCode', String(data.versionCode))
|
||||
form.append('minAppVersionCode', String(data.minAppVersionCode))
|
||||
form.append('mandatory', String(data.mandatory))
|
||||
form.append('releaseNotes', data.releaseNotes)
|
||||
form.append('file', data.file)
|
||||
const response = await http.post<ApiEnvelope<PdaRelease>, ApiEnvelope<PdaRelease>>(`${basePath}/${app}`, form, {
|
||||
timeout: 10 * 60 * 1000,
|
||||
})
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function publishPdaRelease(id: string) {
|
||||
const response = await http.post<ApiEnvelope<PdaRelease>, ApiEnvelope<PdaRelease>>(
|
||||
`${basePath}/${encodeURIComponent(id)}/publish`, {},
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function withdrawPdaRelease(id: string) {
|
||||
const response = await http.post<ApiEnvelope<PdaRelease>, ApiEnvelope<PdaRelease>>(
|
||||
`${basePath}/${encodeURIComponent(id)}/withdraw`, {},
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Download, Plus, RefreshCw } from '@lucide/vue'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { getApiErrorMessage } from '@/shared/api/http'
|
||||
import {
|
||||
listPdaReleases, publishPdaRelease, uploadPdaRelease, withdrawPdaRelease,
|
||||
type PdaApp, type PdaPackageType, type PdaRelease,
|
||||
} from '../api/pdaReleases'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const canManage = computed(() => access.can('platform:pda-release:manage'))
|
||||
const app = ref<PdaApp>('domestic')
|
||||
const rows = ref<PdaRelease[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const actionId = ref('')
|
||||
const uploadOpen = ref(false)
|
||||
const uploading = ref(false)
|
||||
const form = reactive({ packageType: 'wgt' as PdaPackageType, versionName: '', versionCode: 1,
|
||||
minAppVersionCode: 1, mandatory: false, releaseNotes: '', file: null as File | null })
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 215, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'app', label: '应用', width: 110, slotName: 'app' },
|
||||
{ key: 'packageType', label: '包类型', width: 95, slotName: 'packageType' },
|
||||
{ key: 'versionName', prop: 'versionName', label: '版本', width: 115 },
|
||||
{ key: 'versionCode', prop: 'versionCode', label: 'APK版本码', width: 105 },
|
||||
{ key: 'minAppVersionCode', prop: 'minAppVersionCode', label: '最低APK版本码', width: 135 },
|
||||
{ key: 'status', label: '状态', width: 115, slotName: 'status' },
|
||||
{ key: 'mandatory', label: '强制更新', width: 100, slotName: 'mandatory' },
|
||||
{ key: 'releaseNotes', prop: 'releaseNotes', label: '更新说明', minWidth: 190, showOverflowTooltip: true },
|
||||
{ key: 'sizeBytes', label: '大小', width: 100, slotName: 'sizeBytes' },
|
||||
{ key: 'createdAt', label: '上传时间', width: 170, slotName: 'createdAt' },
|
||||
{ key: 'publishedAt', label: '发布时间', width: 170, slotName: 'publishedAt' },
|
||||
]
|
||||
|
||||
const currentApk = computed(() => rows.value.find((row) => row.current && row.packageType === 'apk'))
|
||||
const currentWgt = computed(() => rows.value.find((row) => row.current && row.packageType === 'wgt'))
|
||||
function asRelease(row: unknown) { return row as PdaRelease }
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const requestedApp = app.value
|
||||
try {
|
||||
const result = await listPdaReleases(requestedApp)
|
||||
if (app.value === requestedApp) rows.value = result
|
||||
} catch (cause) {
|
||||
if (app.value === requestedApp) error.value = getApiErrorMessage(cause, '版本列表加载失败')
|
||||
} finally {
|
||||
if (app.value === requestedApp) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(app, () => { rows.value = []; void load() })
|
||||
onMounted(() => { void load() })
|
||||
|
||||
function openUpload() {
|
||||
Object.assign(form, { packageType: 'wgt', versionName: '', versionCode: currentApk.value?.versionCode ?? 1,
|
||||
minAppVersionCode: currentApk.value?.versionCode ?? 1, mandatory: false, releaseNotes: '', file: null })
|
||||
uploadOpen.value = true
|
||||
}
|
||||
|
||||
function selectFile(event: Event) {
|
||||
form.file = (event.target as HTMLInputElement).files?.[0] ?? null
|
||||
}
|
||||
|
||||
async function upload() {
|
||||
if (!form.file || !/^\d+(\.\d+)+$/.test(form.versionName)
|
||||
|| form.versionCode < 1 || form.minAppVersionCode < 1
|
||||
|| (form.packageType === 'apk' && form.versionCode !== form.minAppVersionCode)) {
|
||||
ElMessage.warning('请填写有效版本、最低APK版本码并选择对应安装包')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
await uploadPdaRelease(app.value, { ...form, file: form.file })
|
||||
uploadOpen.value = false
|
||||
ElMessage.success('版本草稿已上传,请核对后发布')
|
||||
await load()
|
||||
} catch (cause) {
|
||||
ElMessage.error(getApiErrorMessage(cause, '更新包上传失败'))
|
||||
} finally { uploading.value = false }
|
||||
}
|
||||
|
||||
async function changeStatus(row: PdaRelease, action: 'publish' | 'withdraw') {
|
||||
const label = action === 'publish' ? '发布' : '停发'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${label}${row.app === 'domestic' ? '国内' : '海外'}PDA ${row.packageType.toUpperCase()} ${row.versionName}?${action === 'publish' ? '发布后设备将收到更新。' : '停发后设备不再获取这个版本。'}`,
|
||||
`${label}PDA版本`, { type: 'warning', confirmButtonText: `确认${label}`, cancelButtonText: '取消', customClass: 'admin-density-confirm' })
|
||||
} catch { return }
|
||||
actionId.value = row.id
|
||||
try {
|
||||
if (action === 'publish') await publishPdaRelease(row.id)
|
||||
else await withdrawPdaRelease(row.id)
|
||||
ElMessage.success(`版本已${label}`)
|
||||
await load()
|
||||
} catch (cause) {
|
||||
ElMessage.error(getApiErrorMessage(cause, `${label}失败,请刷新后重试`))
|
||||
} finally { actionId.value = '' }
|
||||
}
|
||||
|
||||
function download(row: PdaRelease) {
|
||||
if (!row.downloadUrl) return
|
||||
const link = document.createElement('a')
|
||||
link.href = row.downloadUrl
|
||||
link.rel = 'noopener noreferrer'
|
||||
link.download = `${row.app}-pda-${row.versionName}.${row.packageType}`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage class="admin-density pda-releases-page" spacing="compact">
|
||||
<ListPageLayout aria-label="PDA版本">
|
||||
<template #toolbar>
|
||||
<div class="pda-release-toolbar">
|
||||
<div>
|
||||
<h1>PDA版本</h1>
|
||||
<el-radio-group v-model="app" aria-label="选择PDA应用">
|
||||
<el-radio-button value="domestic">国内 PDA</el-radio-button>
|
||||
<el-radio-button value="overseas">海外 PDA</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="pda-release-commands">
|
||||
<el-button :loading="loading" @click="load"><RefreshCw :size="16" />刷新</el-button>
|
||||
<el-button v-if="canManage" type="primary" @click="openUpload"><Plus :size="16" />上传版本</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #metrics>
|
||||
<div class="pda-release-current">
|
||||
<span>当前 APK:{{ currentApk ? `${currentApk.versionName} (${currentApk.versionCode})` : '尚未发布' }}</span>
|
||||
<span>当前 WGT:{{ currentWgt ? `${currentWgt.versionName} · 最低 APK ${currentWgt.minAppVersionCode}` : '尚未发布' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #state>
|
||||
<el-alert v-if="error" type="error" :title="error" show-icon :closable="false" />
|
||||
</template>
|
||||
<DataTable :data="rows" :columns="columns" :loading="loading" row-key="id" border stripe empty-text="暂无版本记录">
|
||||
<template #actions="{ row }">
|
||||
<div class="pda-release-actions">
|
||||
<el-button v-if="row.packageType === 'apk' && row.downloadUrl" link @click="download(asRelease(row))"><Download :size="15" />下载 APK</el-button>
|
||||
<el-button v-if="canManage && row.status === 'DRAFT'" link :loading="actionId === row.id" @click="changeStatus(asRelease(row), 'publish')">发布</el-button>
|
||||
<el-button v-if="canManage && row.current" link type="danger" :loading="actionId === row.id" @click="changeStatus(asRelease(row), 'withdraw')">停发</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #app="{ row }">{{ row.app === 'domestic' ? '国内' : '海外' }}</template>
|
||||
<template #packageType="{ row }">{{ row.packageType.toUpperCase() }}</template>
|
||||
<template #status="{ row }"><el-tag :type="row.current ? 'success' : row.status === 'DRAFT' ? 'info' : 'warning'" size="small">{{ row.current ? '当前发布' : row.status === 'DRAFT' ? '草稿' : row.status === 'WITHDRAWN' ? '已停发' : '历史发布' }}</el-tag></template>
|
||||
<template #mandatory="{ row }">{{ row.mandatory ? '是' : '否' }}</template>
|
||||
<template #sizeBytes="{ row }">{{ (row.sizeBytes / 1024 / 1024).toFixed(1) }} MB</template>
|
||||
<template #createdAt="{ row }">{{ row.createdAt?.replace('T', ' ').slice(0, 19) || '—' }}</template>
|
||||
<template #publishedAt="{ row }">{{ row.publishedAt?.replace('T', ' ').slice(0, 19) || '—' }}</template>
|
||||
</DataTable>
|
||||
</ListPageLayout>
|
||||
|
||||
<AppDialog v-model="uploadOpen" class="admin-density-dialog" title="上传PDA版本" width="min(640px, calc(100vw - 28px))" confirm-text="保存草稿" :confirm-loading="uploading" @confirm="upload">
|
||||
<el-form label-position="top" class="pda-release-form">
|
||||
<el-form-item label="包类型">
|
||||
<el-radio-group v-model="form.packageType"><el-radio-button value="apk">APK 整包</el-radio-button><el-radio-button value="wgt">WGT 热更新</el-radio-button></el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="pda-release-fields">
|
||||
<el-form-item label="版本号"><el-input v-model.trim="form.versionName" placeholder="例如 0.0.4" /></el-form-item>
|
||||
<el-form-item label="APK版本码"><el-input-number v-model="form.versionCode" :min="1" :controls="false" /></el-form-item>
|
||||
<el-form-item label="最低兼容APK版本码"><el-input-number v-model="form.minAppVersionCode" :min="1" :controls="false" /></el-form-item>
|
||||
<el-form-item label="强制更新"><el-switch v-model="form.mandatory" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="更新说明"><el-input v-model="form.releaseNotes" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item>
|
||||
<el-form-item :label="`选择 ${form.packageType.toUpperCase()} 文件(最大200MB)`"><input :key="form.packageType" type="file" :accept="`.${form.packageType}`" @change="selectFile" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pda-release-toolbar { display: flex; align-items: end; justify-content: space-between; gap: 16px; }
|
||||
.pda-release-toolbar h1 { margin: 0 0 10px; font-size: var(--admin-font-title, 16px); }
|
||||
.pda-release-commands, .pda-release-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.pda-release-commands :deep(.el-button), .pda-release-actions :deep(.el-button) { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.pda-release-current { display: flex; gap: 24px; padding: 10px 0; border-bottom: 1px solid var(--cc-border); }
|
||||
.pda-release-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 18px; }
|
||||
.pda-release-form :deep(.el-input-number) { width: 100%; }
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const pdaReleaseRoutes = [
|
||||
{
|
||||
path: 'system/pda-releases',
|
||||
name: 'pda-releases',
|
||||
component: () => import('@/modules/pda-releases/pages/PdaReleasesView.vue'),
|
||||
meta: { title: 'PDA版本', description: '国内与海外PDA安装包下载和热更新发布', permission: 'platform:pda-release:list' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
Reference in New Issue
Block a user