|
@@ -0,0 +1,412 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="page-container">
|
|
|
|
|
+ <!-- 搜索区域 -->
|
|
|
|
|
+ <el-card class="search-card">
|
|
|
|
|
+ <el-form :model="query" inline>
|
|
|
|
|
+ <el-form-item label="平台">
|
|
|
|
|
+ <el-select v-model="query.platform" placeholder="全部" clearable style="width:120px">
|
|
|
|
|
+ <el-option label="Android" :value="1" />
|
|
|
|
|
+ <el-option label="iOS" :value="2" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="状态">
|
|
|
|
|
+ <el-select v-model="query.status" placeholder="全部" clearable style="width:110px">
|
|
|
|
|
+ <el-option label="启用" :value="1" />
|
|
|
|
|
+ <el-option label="禁用" :value="0" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="版本号">
|
|
|
|
|
+ <el-input v-model="query.versionCode" placeholder="请输入版本号" clearable style="width:140px" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
|
|
|
|
+ <el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 操作按钮区 -->
|
|
|
|
|
+ <el-card class="action-card">
|
|
|
|
|
+ <div class="action-bar">
|
|
|
|
|
+ <el-button type="primary" :icon="Plus" @click="handleAdd">新增版本</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 表格区域 -->
|
|
|
|
|
+ <el-card class="table-card">
|
|
|
|
|
+ <el-table :data="tableData" v-loading="loading" border stripe>
|
|
|
|
|
+ <el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
|
|
+ <el-table-column prop="versionCode" label="版本号" width="100" align="center" />
|
|
|
|
|
+ <el-table-column prop="versionNumber" label="版本序号" width="100" align="center" />
|
|
|
|
|
+ <el-table-column label="平台" width="100" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="row.platform === 1 ? 'success' : 'primary'" size="small">
|
|
|
|
|
+ {{ row.platform === 1 ? 'Android' : 'iOS' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="更新类型" width="100" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-tag :type="row.updateType === 1 ? 'danger' : 'warning'" size="small">
|
|
|
|
|
+ {{ row.updateType === 1 ? '强制更新' : '推荐更新' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="downloadUrl" label="下载地址" min-width="200" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="fileSize" label="包大小" width="90" align="center">
|
|
|
|
|
+ <template #default="{ row }">{{ row.fileSize || '-' }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="状态" width="80" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-switch
|
|
|
|
|
+ :model-value="row.status === 1"
|
|
|
|
|
+ @change="handleToggleStatus(row)"
|
|
|
|
|
+ inline-prompt
|
|
|
|
|
+ active-text="启"
|
|
|
|
|
+ inactive-text="禁"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="publishTime" label="发布时间" width="170">
|
|
|
|
|
+ <template #default="{ row }">{{ formatDateTime(row.publishTime) }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="操作" width="130" align="center" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button size="small" type="warning" link @click="handleEdit(row)">编辑</el-button>
|
|
|
|
|
+ <el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <el-pagination
|
|
|
|
|
+ v-model:current-page="pageNum"
|
|
|
|
|
+ v-model:page-size="pageSize"
|
|
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
|
|
+ :total="total"
|
|
|
|
|
+ :hide-on-single-page="false"
|
|
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
+ background
|
|
|
|
|
+ class="pagination"
|
|
|
|
|
+ @size-change="loadData"
|
|
|
|
|
+ @current-change="loadData"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 新增/编辑弹窗 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="dialogVisible"
|
|
|
|
|
+ :title="dialogMode === 'add' ? '新增版本' : '编辑版本'"
|
|
|
|
|
+ width="580px"
|
|
|
|
|
+ destroy-on-close
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form :model="formData" :rules="formRules" ref="formRef" label-width="100px">
|
|
|
|
|
+ <el-form-item label="平台" prop="platform">
|
|
|
|
|
+ <el-select v-model="formData.platform" placeholder="请选择平台" style="width:100%">
|
|
|
|
|
+ <el-option label="Android" :value="1" />
|
|
|
|
|
+ <el-option label="iOS" :value="2" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="版本号" prop="versionCode">
|
|
|
|
|
+ <el-input v-model="formData.versionCode" placeholder="如:1.0.1" maxlength="20" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="版本序号">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="formData.versionNumber"
|
|
|
|
|
+ :min="1"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ placeholder="选填,不填则自动计算"
|
|
|
|
|
+ style="width:100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="form-tip">非必填,留空时根据版本号自动计算(如 1.0.1 = 10001)</div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="更新类型" prop="updateType">
|
|
|
|
|
+ <el-select v-model="formData.updateType" placeholder="请选择更新类型" style="width:100%">
|
|
|
|
|
+ <el-option label="强制更新" :value="1" />
|
|
|
|
|
+ <el-option label="推荐更新" :value="2" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="下载地址" prop="downloadUrl">
|
|
|
|
|
+ <!-- Android: 上传APK -->
|
|
|
|
|
+ <div v-if="formData.platform === 1" style="width:100%">
|
|
|
|
|
+ <el-upload
|
|
|
|
|
+ :show-file-list="false"
|
|
|
|
|
+ :http-request="handleApkUpload"
|
|
|
|
|
+ accept=".apk,.wgt"
|
|
|
|
|
+ :disabled="apkUploading"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-button type="primary" :loading="apkUploading">
|
|
|
|
|
+ {{ apkUploading ? '上传中...' : '上传APK安装包' }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="formData.downloadUrl"
|
|
|
|
|
+ placeholder="上传APK后自动填入,或手动输入下载地址"
|
|
|
|
|
+ style="margin-top: 8px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- iOS: 手动输入App Store链接 -->
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-else
|
|
|
|
|
+ v-model="formData.downloadUrl"
|
|
|
|
|
+ placeholder="请输入App Store链接"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="包大小" prop="fileSize">
|
|
|
|
|
+ <el-input v-model="formData.fileSize" placeholder="如:25.6MB(Android上传后自动填入)" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="更新内容" prop="updateContent">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="formData.updateContent"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="4"
|
|
|
|
|
+ placeholder="请输入更新内容描述"
|
|
|
|
|
+ maxlength="1000"
|
|
|
|
|
+ show-word-limit
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="状态">
|
|
|
|
|
+ <el-switch
|
|
|
|
|
+ v-model="formData.status"
|
|
|
|
|
+ :active-value="1"
|
|
|
|
|
+ :inactive-value="0"
|
|
|
|
|
+ active-text="启用"
|
|
|
|
|
+ inactive-text="禁用"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">保存</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, reactive, onMounted } from 'vue'
|
|
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
+import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
|
|
|
|
+import { formatDateTime } from '@/utils/date'
|
|
|
|
|
+import { parsePageData } from '@/utils/pagination'
|
|
|
|
|
+import {
|
|
|
|
|
+ getVersionPage,
|
|
|
|
|
+ addVersion,
|
|
|
|
|
+ updateVersion,
|
|
|
|
|
+ deleteVersion,
|
|
|
|
|
+ toggleVersionStatus,
|
|
|
|
|
+ uploadApk
|
|
|
|
|
+} from '@/api/version'
|
|
|
|
|
+
|
|
|
|
|
+// ======================== 列表数据 ========================
|
|
|
|
|
+const tableData = ref([])
|
|
|
|
|
+const total = ref(0)
|
|
|
|
|
+const loading = ref(false)
|
|
|
|
|
+const pageNum = ref(1)
|
|
|
|
|
+const pageSize = ref(10)
|
|
|
|
|
+
|
|
|
|
|
+const query = reactive({
|
|
|
|
|
+ platform: null,
|
|
|
|
|
+ status: null,
|
|
|
|
|
+ versionCode: ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+async function loadData() {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getVersionPage({
|
|
|
|
|
+ ...query,
|
|
|
|
|
+ pageNum: pageNum.value,
|
|
|
|
|
+ pageSize: pageSize.value
|
|
|
|
|
+ })
|
|
|
|
|
+ const { records, total: pageTotal } = parsePageData(res.data)
|
|
|
|
|
+ tableData.value = records
|
|
|
|
|
+ total.value = pageTotal
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleSearch() {
|
|
|
|
|
+ pageNum.value = 1
|
|
|
|
|
+ loadData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleReset() {
|
|
|
|
|
+ Object.assign(query, { platform: null, status: null, versionCode: '' })
|
|
|
|
|
+ pageNum.value = 1
|
|
|
|
|
+ loadData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ======================== 新增/编辑 ========================
|
|
|
|
|
+const dialogVisible = ref(false)
|
|
|
|
|
+const dialogMode = ref('add')
|
|
|
|
|
+const submitLoading = ref(false)
|
|
|
|
|
+const formRef = ref(null)
|
|
|
|
|
+const apkUploading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+const formData = reactive({
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ platform: 1,
|
|
|
|
|
+ versionCode: '',
|
|
|
|
|
+ versionNumber: null,
|
|
|
|
|
+ updateType: 2,
|
|
|
|
|
+ downloadUrl: '',
|
|
|
|
|
+ updateContent: '',
|
|
|
|
|
+ fileSize: '',
|
|
|
|
|
+ status: 1
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const formRules = {
|
|
|
|
|
+ platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
|
|
|
|
+ versionCode: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
|
|
|
|
|
+
|
|
|
|
|
+ updateType: [{ required: true, message: '请选择更新类型', trigger: 'change' }],
|
|
|
|
|
+ downloadUrl: [{ required: true, message: '请输入或上传下载地址', trigger: 'blur' }]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleAdd() {
|
|
|
|
|
+ dialogMode.value = 'add'
|
|
|
|
|
+ Object.assign(formData, {
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ platform: 1,
|
|
|
|
|
+ versionCode: '',
|
|
|
|
|
+ versionNumber: null,
|
|
|
|
|
+ updateType: 2,
|
|
|
|
|
+ downloadUrl: '',
|
|
|
|
|
+ updateContent: '',
|
|
|
|
|
+ fileSize: '',
|
|
|
|
|
+ status: 1
|
|
|
|
|
+ })
|
|
|
|
|
+ dialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleEdit(row) {
|
|
|
|
|
+ dialogMode.value = 'edit'
|
|
|
|
|
+ Object.assign(formData, {
|
|
|
|
|
+ id: row.id,
|
|
|
|
|
+ platform: row.platform,
|
|
|
|
|
+ versionCode: row.versionCode,
|
|
|
|
|
+ versionNumber: row.versionNumber,
|
|
|
|
|
+ updateType: row.updateType,
|
|
|
|
|
+ downloadUrl: row.downloadUrl,
|
|
|
|
|
+ updateContent: row.updateContent || '',
|
|
|
|
|
+ fileSize: row.fileSize || '',
|
|
|
|
|
+ status: row.status
|
|
|
|
|
+ })
|
|
|
|
|
+ dialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitForm() {
|
|
|
|
|
+ await formRef.value.validate()
|
|
|
|
|
+ submitLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = { ...formData }
|
|
|
|
|
+ if (dialogMode.value === 'add') {
|
|
|
|
|
+ await addVersion(payload)
|
|
|
|
|
+ ElMessage.success('新增成功')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await updateVersion(payload)
|
|
|
|
|
+ ElMessage.success('修改成功')
|
|
|
|
|
+ }
|
|
|
|
|
+ dialogVisible.value = false
|
|
|
|
|
+ loadData()
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ======================== APK上传 ========================
|
|
|
|
|
+
|
|
|
|
|
+async function handleApkUpload({ file }) {
|
|
|
|
|
+ apkUploading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await uploadApk(file)
|
|
|
|
|
+ if (res?.data?.url) {
|
|
|
|
|
+ formData.downloadUrl = res.data.url
|
|
|
|
|
+ // 自动计算文件大小
|
|
|
|
|
+ formData.fileSize = formatFileSize(file.size)
|
|
|
|
|
+ ElMessage.success('APK上传成功')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // 拦截器已提示
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ apkUploading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 格式化文件大小
|
|
|
|
|
+ */
|
|
|
|
|
+function formatFileSize(bytes) {
|
|
|
|
|
+ if (!bytes || bytes === 0) return ''
|
|
|
|
|
+ if (bytes < 1024) return bytes + 'B'
|
|
|
|
|
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
|
|
|
|
|
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + 'MB'
|
|
|
|
|
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ======================== 状态切换 ========================
|
|
|
|
|
+
|
|
|
|
|
+async function handleToggleStatus(row) {
|
|
|
|
|
+ await toggleVersionStatus(row.id)
|
|
|
|
|
+ ElMessage.success('状态切换成功')
|
|
|
|
|
+ loadData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ======================== 删除 ========================
|
|
|
|
|
+
|
|
|
|
|
+async function handleDelete(row) {
|
|
|
|
|
+ await ElMessageBox.confirm(
|
|
|
|
|
+ `确认删除版本「${row.versionCode}」(${row.platform === 1 ? 'Android' : 'iOS'})吗?`,
|
|
|
|
|
+ '删除确认',
|
|
|
|
|
+ { type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' }
|
|
|
|
|
+ )
|
|
|
|
|
+ await deleteVersion(row.id)
|
|
|
|
|
+ ElMessage.success('删除成功')
|
|
|
|
|
+ loadData()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(loadData)
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+.page-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-card {
|
|
|
|
|
+ :deep(.el-card__body) {
|
|
|
|
|
+ padding: 18px 20px 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-card {
|
|
|
|
|
+ :deep(.el-card__body) {
|
|
|
|
|
+ padding: 14px 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .action-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-card {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pagination {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-tip {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|