Files
ReelShortWeb/src/views/systemManagement/AppVersionView.vue
T

520 lines
16 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.
<!-- 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>