520 lines
16 KiB
Vue
520 lines
16 KiB
Vue
<!-- Author: fruit.com.cn -->
|
||
|
||
<script setup lang="ts">
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { computed, ref } from 'vue'
|
||
import PageHeading from '@/views/components/PageHeading.vue'
|
||
import { AppIcons } from '@/constants/icons'
|
||
import { dateCell } from '@/utils/format'
|
||
import {
|
||
batchCreateAppVersionsApi,
|
||
deleteAppVersionApi,
|
||
listAppVersionsApi,
|
||
updateAppVersionApi,
|
||
updateAppVersionStatusApi
|
||
} from '@/api/admin/appVersion'
|
||
import type { AppVersionItem, AppVersionSaveData } from '@/api/admin/appVersion'
|
||
|
||
const platformOptions = [
|
||
{ label: 'Android', value: 'ANDROID' },
|
||
{ label: 'iOS', value: 'IOS' }
|
||
]
|
||
const statusOptions = [
|
||
{ label: '已发布', value: 1 },
|
||
{ label: '已下架', value: 0 }
|
||
]
|
||
|
||
interface FilterState {
|
||
platform: string
|
||
status: number | undefined
|
||
keyword: string
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const list = ref<AppVersionItem[]>([])
|
||
const total = ref(0)
|
||
const pageIndex = ref(1)
|
||
const pageSize = ref(20)
|
||
const filter = ref<FilterState>({ platform: '', status: undefined, keyword: '' })
|
||
|
||
const dialogOpen = ref(false)
|
||
const formLoading = ref(false)
|
||
const editingId = ref<string | null>(null)
|
||
const formRef = ref<FormInstance>()
|
||
|
||
/** 新建时两个平台一起配(各自可启停),编辑时用单份。 */
|
||
interface PlatformBlock {
|
||
enabled: boolean
|
||
data: AppVersionSaveData
|
||
}
|
||
const emptyPlatform = (platform: 'ANDROID' | 'IOS'): AppVersionSaveData => ({
|
||
platform,
|
||
versionCode: 0,
|
||
versionName: '',
|
||
downloadUrl: '',
|
||
updateContent: '',
|
||
forceUpdate: 0,
|
||
minSupportedCode: 0
|
||
})
|
||
const androidBlock = ref<PlatformBlock>({ enabled: true, data: emptyPlatform('ANDROID') })
|
||
const iosBlock = ref<PlatformBlock>({ enabled: true, data: emptyPlatform('IOS') })
|
||
const editForm = ref<AppVersionSaveData>(emptyPlatform('ANDROID'))
|
||
|
||
const rules: FormRules = {
|
||
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||
versionCode: [
|
||
{ required: true, message: '请填写整数版本号', trigger: 'blur' },
|
||
{ type: 'number', min: 1, max: 999999, message: '范围 1~999999', trigger: 'blur' }
|
||
],
|
||
versionName: [
|
||
{ required: true, message: '请填写展示版本号', trigger: 'blur' },
|
||
{ max: 32, message: '最长 32 字符', trigger: 'blur' }
|
||
],
|
||
downloadUrl: [
|
||
{ required: true, message: '请填写下载地址', trigger: 'blur' },
|
||
{ max: 512, message: '最长 512 字符', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
async function fetchList() {
|
||
loading.value = true
|
||
try {
|
||
const res = await listAppVersionsApi({
|
||
pageIndex: pageIndex.value,
|
||
pageSize: pageSize.value,
|
||
platform: filter.value.platform || undefined,
|
||
status: filter.value.status,
|
||
keyword: filter.value.keyword.trim() || undefined
|
||
})
|
||
list.value = res.data?.list ?? []
|
||
total.value = res.data?.total ?? 0
|
||
} catch {
|
||
/* axios interceptor 已弹消息 */
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function openCreate() {
|
||
editingId.value = null
|
||
androidBlock.value = { enabled: true, data: emptyPlatform('ANDROID') }
|
||
iosBlock.value = { enabled: true, data: emptyPlatform('IOS') }
|
||
dialogOpen.value = true
|
||
}
|
||
|
||
function openEdit(row: AppVersionItem) {
|
||
editingId.value = row.id
|
||
editForm.value = {
|
||
platform: row.platform,
|
||
versionCode: row.versionCode,
|
||
versionName: row.versionName,
|
||
downloadUrl: row.downloadUrl,
|
||
updateContent: row.updateContent ?? '',
|
||
forceUpdate: row.forceUpdate === 1 ? 1 : 0,
|
||
minSupportedCode: row.minSupportedCode
|
||
}
|
||
dialogOpen.value = true
|
||
}
|
||
|
||
async function submit() {
|
||
formLoading.value = true
|
||
try {
|
||
if (editingId.value) {
|
||
await updateAppVersionApi(editingId.value, editForm.value)
|
||
ElMessage.success('已更新')
|
||
} else {
|
||
const toCreate: AppVersionSaveData[] = []
|
||
if (androidBlock.value.enabled) toCreate.push(androidBlock.value.data)
|
||
if (iosBlock.value.enabled) toCreate.push(iosBlock.value.data)
|
||
if (toCreate.length === 0) {
|
||
ElMessage.warning('至少启用一个平台')
|
||
return
|
||
}
|
||
await batchCreateAppVersionsApi(toCreate)
|
||
ElMessage.success(`已创建 ${toCreate.length} 条版本`)
|
||
}
|
||
dialogOpen.value = false
|
||
await fetchList()
|
||
} catch {
|
||
/* axios interceptor 已弹消息 */
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function toggleStatus(row: AppVersionItem) {
|
||
const next = row.status === 1 ? 0 : 1
|
||
await updateAppVersionStatusApi(row.id, next as 0 | 1)
|
||
ElMessage.success(next === 1 ? '已发布' : '已下架')
|
||
await fetchList()
|
||
}
|
||
|
||
async function remove(row: AppVersionItem) {
|
||
await ElMessageBox.confirm(
|
||
`确认删除 ${row.platformText} ${row.versionName}(${row.versionCode})?`,
|
||
'删除版本',
|
||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||
)
|
||
await deleteAppVersionApi(row.id)
|
||
ElMessage.success('已删除')
|
||
await fetchList()
|
||
}
|
||
|
||
const forceText = computed(() => (v: number) => (v === 1 ? '强制' : '可选升级'))
|
||
</script>
|
||
|
||
<template>
|
||
<PageHeading eyebrow="后台管理" title="版本管理" description="App 版本发布、上下架与删除。" />
|
||
|
||
<section class="toolbar">
|
||
<el-form :inline="true" :model="filter" @submit.prevent="fetchList">
|
||
<el-form-item label="平台">
|
||
<el-select v-model="filter.platform" clearable placeholder="全部" style="width: 120px">
|
||
<el-option
|
||
v-for="o in platformOptions"
|
||
:key="o.value"
|
||
:label="o.label"
|
||
:value="o.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="filter.status" clearable placeholder="全部" style="width: 120px">
|
||
<el-option v-for="o in statusOptions" :key="o.value" :label="o.label" :value="o.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="关键字">
|
||
<el-input
|
||
v-model="filter.keyword"
|
||
placeholder="匹配 versionName"
|
||
clearable
|
||
style="width: 180px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :icon="AppIcons.search" @click="fetchList">查询</el-button>
|
||
<el-button plain @click="openCreate" :icon="AppIcons.add">新建版本</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</section>
|
||
|
||
<el-table :data="list" v-loading="loading" border stripe>
|
||
<el-table-column prop="platformText" label="平台" width="100" />
|
||
<el-table-column prop="versionCode" label="版本码" width="90" />
|
||
<el-table-column prop="versionName" label="展示版本" width="120" />
|
||
<el-table-column prop="downloadUrl" label="下载地址" show-overflow-tooltip min-width="220">
|
||
<template #default="{ row }">
|
||
<el-link :href="row.downloadUrl" target="_blank" type="primary" :underline="false">
|
||
{{ row.downloadUrl }}
|
||
</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="forceUpdate" label="升级" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.forceUpdate === 1 ? 'danger' : 'info'" size="small">
|
||
{{ forceText(row.forceUpdate) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="minSupportedCode" label="兜底最低" width="100" />
|
||
<el-table-column prop="status" label="状态" width="90">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||
{{ row.statusText }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="releasedAt" label="发布时间" width="160" :formatter="dateCell" />
|
||
<el-table-column prop="createdAt" label="创建时间" width="160" :formatter="dateCell" />
|
||
<el-table-column label="操作" width="240" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||
<el-button size="small" link type="warning" @click="toggleStatus(row)">
|
||
{{ row.status === 1 ? '下架' : '发布' }}
|
||
</el-button>
|
||
<el-button size="small" link type="danger" @click="remove(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
class="pagination"
|
||
v-model:current-page="pageIndex"
|
||
v-model:page-size="pageSize"
|
||
:total="total"
|
||
layout="total, sizes, prev, pager, next"
|
||
:page-sizes="[10, 20, 50]"
|
||
@current-change="fetchList"
|
||
@size-change="fetchList"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="dialogOpen"
|
||
:title="editingId ? '编辑版本' : '新建版本'"
|
||
width="820px"
|
||
@close="() => formRef?.resetFields()"
|
||
>
|
||
<!-- 编辑模式:单平台 -->
|
||
<el-form
|
||
v-if="editingId"
|
||
ref="formRef"
|
||
:model="editForm"
|
||
:rules="rules"
|
||
label-width="120px"
|
||
v-loading="formLoading"
|
||
>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="整数版本" prop="versionCode">
|
||
<el-input-number
|
||
v-model="editForm.versionCode"
|
||
:min="1"
|
||
:max="999999"
|
||
controls-position="right"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="16">
|
||
<el-form-item label="展示版本" prop="versionName">
|
||
<el-input v-model="editForm.versionName" placeholder="如 1.2.0" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="下载地址" prop="downloadUrl">
|
||
<el-input v-model="editForm.downloadUrl" placeholder="Android apk / iOS App Store 链接" />
|
||
</el-form-item>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="强制升级">
|
||
<el-switch
|
||
v-model="editForm.forceUpdate"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
active-text="强制"
|
||
inactive-text="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="兜底最低版本">
|
||
<el-input-number
|
||
v-model="editForm.minSupportedCode"
|
||
:min="0"
|
||
controls-position="right"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="更新说明">
|
||
<el-input
|
||
v-model="editForm.updateContent"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="换行分隔多条更新内容"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 新建模式:Android + iOS 两块 -->
|
||
<div v-else class="dual-blocks" v-loading="formLoading">
|
||
<div class="platform-block" :class="{ disabled: !androidBlock.enabled }">
|
||
<header>
|
||
<span class="platform-title">Android</span>
|
||
<el-switch
|
||
v-model="androidBlock.enabled"
|
||
active-text="启用"
|
||
inactive-text="跳过"
|
||
inline-prompt
|
||
/>
|
||
</header>
|
||
<el-form
|
||
:model="androidBlock.data"
|
||
:rules="rules"
|
||
label-width="120px"
|
||
:disabled="!androidBlock.enabled"
|
||
>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="整数版本" prop="versionCode">
|
||
<el-input-number
|
||
v-model="androidBlock.data.versionCode"
|
||
:min="1"
|
||
:max="999999"
|
||
controls-position="right"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="16">
|
||
<el-form-item label="展示版本" prop="versionName">
|
||
<el-input v-model="androidBlock.data.versionName" placeholder="如 1.2.0" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="下载地址" prop="downloadUrl">
|
||
<el-input v-model="androidBlock.data.downloadUrl" placeholder="apk 下载链接" />
|
||
</el-form-item>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="强制升级">
|
||
<el-switch
|
||
v-model="androidBlock.data.forceUpdate"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
active-text="强制"
|
||
inactive-text="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="兜底最低">
|
||
<el-input-number
|
||
v-model="androidBlock.data.minSupportedCode"
|
||
:min="0"
|
||
controls-position="right"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="更新说明">
|
||
<el-input
|
||
v-model="androidBlock.data.updateContent"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="换行分隔多条"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="platform-block" :class="{ disabled: !iosBlock.enabled }">
|
||
<header>
|
||
<span class="platform-title">iOS</span>
|
||
<el-switch
|
||
v-model="iosBlock.enabled"
|
||
active-text="启用"
|
||
inactive-text="跳过"
|
||
inline-prompt
|
||
/>
|
||
</header>
|
||
<el-form
|
||
:model="iosBlock.data"
|
||
:rules="rules"
|
||
label-width="120px"
|
||
:disabled="!iosBlock.enabled"
|
||
>
|
||
<el-row :gutter="12">
|
||
<el-col :span="8">
|
||
<el-form-item label="整数版本" prop="versionCode">
|
||
<el-input-number
|
||
v-model="iosBlock.data.versionCode"
|
||
:min="1"
|
||
:max="999999"
|
||
controls-position="right"
|
||
class="w-full"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="16">
|
||
<el-form-item label="展示版本" prop="versionName">
|
||
<el-input v-model="iosBlock.data.versionName" placeholder="如 1.2.0" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="下载地址" prop="downloadUrl">
|
||
<el-input v-model="iosBlock.data.downloadUrl" placeholder="App Store 链接" />
|
||
</el-form-item>
|
||
<el-row :gutter="12">
|
||
<el-col :span="12">
|
||
<el-form-item label="强制升级">
|
||
<el-switch
|
||
v-model="iosBlock.data.forceUpdate"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
active-text="强制"
|
||
inactive-text="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="兜底最低">
|
||
<el-input-number
|
||
v-model="iosBlock.data.minSupportedCode"
|
||
:min="0"
|
||
controls-position="right"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="更新说明">
|
||
<el-input
|
||
v-model="iosBlock.data.updateContent"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="换行分隔多条"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<el-button @click="dialogOpen = false">取消</el-button>
|
||
<el-button type="primary" :loading="formLoading" @click="submit">
|
||
{{ editingId ? '保存' : '创建并发布' }}
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped lang="less">
|
||
.toolbar {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pagination {
|
||
margin-top: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.dual-blocks {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.platform-block {
|
||
padding: 14px 16px 4px;
|
||
background: var(--el-fill-color-blank);
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 8px;
|
||
|
||
header {
|
||
display: flex;
|
||
padding-bottom: 8px;
|
||
margin-bottom: 6px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.platform-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.disabled {
|
||
border-style: dashed;
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
</style>
|