<script setup lang="tsx">
|
import { useMessage } from 'naive-ui'
|
import type { FormInst, FormRules, UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
|
import { onActivated, onBeforeUnmount, onDeactivated, ref } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { beforeUploadCheck, customDirectUploadRequest } from '@/utils/upload'
|
import { releaseAppVersion, stringifyAppVersionFileValue } from '@/service/api/appVersion'
|
|
function logAppVersionAdd(step: string, payload?: unknown) {
|
console.log(`[app-version/add] ${step}`, payload)
|
}
|
|
const router = useRouter()
|
const message = useMessage()
|
const formRef = ref<FormInst | null>(null)
|
const androidUploading = ref(false)
|
const iosUploading = ref(false)
|
|
const formModel = ref({
|
version: '',
|
androidFiles: [] as UploadFileInfo[],
|
iosFiles: [] as UploadFileInfo[],
|
updateNote: '',
|
isForceUpdate: false,
|
})
|
|
const rules: FormRules = {
|
version: [
|
{ required: true, message: '请输入版本号', trigger: ['blur', 'input'] },
|
],
|
androidFiles: [
|
{ required: true, type: 'array', message: '请上传安卓安装包', trigger: ['blur', 'change'] },
|
],
|
iosFiles: [
|
{ required: true, type: 'array', message: '请上传 iOS 安装包', trigger: ['blur', 'change'] },
|
],
|
updateNote: [
|
{ required: true, message: '请输入更新说明', trigger: ['blur', 'input'] },
|
],
|
}
|
|
function beforeUploadApk(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
|
return beforeUploadCheck(data, { maxSize: 500, allowedTypes: [] })
|
}
|
|
function beforeUploadIpa(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
|
return beforeUploadCheck(data, { maxSize: 500, allowedTypes: [] })
|
}
|
|
let androidAbortController: AbortController | null = null
|
let iosAbortController: AbortController | null = null
|
|
function resetFormAndUploads() {
|
if (androidAbortController) {
|
androidAbortController.abort()
|
androidAbortController = null
|
}
|
if (iosAbortController) {
|
iosAbortController.abort()
|
iosAbortController = null
|
}
|
androidUploading.value = false
|
iosUploading.value = false
|
formModel.value = {
|
version: '',
|
androidFiles: [],
|
iosFiles: [],
|
updateNote: '',
|
isForceUpdate: false,
|
}
|
formRef.value?.restoreValidation()
|
}
|
|
onActivated(() => {
|
resetFormAndUploads()
|
})
|
|
onDeactivated(() => {
|
resetFormAndUploads()
|
})
|
|
onBeforeUnmount(() => {
|
resetFormAndUploads()
|
})
|
|
async function customRequestAndroid(options: any) {
|
if (androidAbortController) {
|
androidAbortController.abort()
|
}
|
androidAbortController = new AbortController()
|
|
androidUploading.value = true
|
logAppVersionAdd('android upload triggered', {
|
fileName: options?.file?.name,
|
file: options?.file,
|
})
|
const originalOnError = options.onError
|
options.onError = () => {
|
logAppVersionAdd('android upload onError callback')
|
formModel.value.androidFiles = []
|
originalOnError?.()
|
}
|
try {
|
// 安卓包改为前端直传:先拿上传地址,再将文件直接传到 OSS
|
const res = await customDirectUploadRequest({
|
...options,
|
signal: androidAbortController.signal,
|
} as UploadCustomRequestOptions)
|
if (!res) {
|
logAppVersionAdd('android upload result empty')
|
formModel.value.androidFiles = []
|
return
|
}
|
logAppVersionAdd('android upload result', res)
|
formModel.value.androidFiles = [{
|
id: String(res.id || ''),
|
name: res.name || 'android.apk',
|
status: 'finished',
|
url: res.url || '',
|
}]
|
}
|
finally {
|
logAppVersionAdd('android upload finished', {
|
uploading: false,
|
fileList: formModel.value.androidFiles,
|
})
|
androidUploading.value = false
|
}
|
}
|
|
async function customRequestIos(options: any) {
|
if (iosAbortController) {
|
iosAbortController.abort()
|
}
|
iosAbortController = new AbortController()
|
|
iosUploading.value = true
|
logAppVersionAdd('ios upload triggered', {
|
fileName: options?.file?.name,
|
file: options?.file,
|
})
|
const originalOnError = options.onError
|
options.onError = () => {
|
logAppVersionAdd('ios upload onError callback')
|
formModel.value.iosFiles = []
|
originalOnError?.()
|
}
|
try {
|
// iOS 包改为前端直传:上传完成后将 fileId 存入文件项 id 字段
|
const res = await customDirectUploadRequest({
|
...options,
|
signal: iosAbortController.signal,
|
} as UploadCustomRequestOptions)
|
if (!res) {
|
logAppVersionAdd('ios upload result empty')
|
formModel.value.iosFiles = []
|
return
|
}
|
logAppVersionAdd('ios upload result', res)
|
formModel.value.iosFiles = [{
|
id: String(res.id || ''),
|
name: res.name || 'ios.ipa',
|
status: 'finished',
|
url: res.url || '',
|
}]
|
}
|
finally {
|
logAppVersionAdd('ios upload finished', {
|
uploading: false,
|
fileList: formModel.value.iosFiles,
|
})
|
iosUploading.value = false
|
}
|
}
|
|
async function handleConfirm() {
|
await formRef.value?.validate(async (errors) => {
|
logAppVersionAdd('handle confirm validate result', {
|
errors,
|
androidUploading: androidUploading.value,
|
iosUploading: iosUploading.value,
|
version: formModel.value.version,
|
})
|
if (errors) {
|
message.error('请完善必填信息')
|
return
|
}
|
|
if (androidUploading.value || iosUploading.value) {
|
message.error('请等待文件上传完成')
|
return
|
}
|
|
// 直传阶段拿到 fileId 后,保存时按后端要求提交包含 id、name、url 的 JSON 字符串,未使用的 url 传空字符串
|
const androidFile = stringifyAppVersionFileValue({
|
id: String(formModel.value.androidFiles[0]?.id || '').trim(),
|
name: String(formModel.value.androidFiles[0]?.name || '').trim(),
|
})
|
const iosFile = stringifyAppVersionFileValue({
|
id: String(formModel.value.iosFiles[0]?.id || '').trim(),
|
name: String(formModel.value.iosFiles[0]?.name || '').trim(),
|
})
|
|
if (!androidFile || !iosFile) {
|
message.error('请等待文件上传完成')
|
return
|
}
|
|
const params = {
|
version: formModel.value.version,
|
androidFile,
|
iosFile,
|
updateDescription: formModel.value.updateNote,
|
forceUpdateFlag: formModel.value.isForceUpdate ? 1 : 0,
|
}
|
logAppVersionAdd('release request params', params)
|
|
const { isSuccess } = await releaseAppVersion(params)
|
logAppVersionAdd('release response', { isSuccess, params })
|
if (isSuccess) {
|
message.success('发布成功')
|
window.sessionStorage.setItem('appVersionManageNeedRefresh', '1')
|
router.back()
|
}
|
})
|
}
|
|
function handleCancel() {
|
resetFormAndUploads()
|
router.back()
|
}
|
</script>
|
|
<template>
|
<div class="h-full flex flex-col bg-white rounded-md">
|
<div class="flex items-center px-6 pt-6 mb-6">
|
<div class="flex items-center relative pl-3">
|
<div class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-[#0068FF] rounded-sm" />
|
<span class="text-base font-bold text-[#1D2129] ml-2">新增APP版本号</span>
|
</div>
|
</div>
|
|
<div class="flex-1 overflow-y-auto px-6">
|
<n-form
|
ref="formRef"
|
:model="formModel"
|
:rules="rules"
|
label-placement="left"
|
inline
|
require-mark-placement="left"
|
:show-feedback="false"
|
class="max-w-4xl flex flex-col gap-16px"
|
>
|
<n-form-item label="版本号" path="version">
|
<n-input v-model:value="formModel.version" placeholder="请输入版本号" class="!w-[400px]" />
|
</n-form-item>
|
|
<n-form-item label="安卓文件" path="androidFiles">
|
<div class="flex flex-col gap-2">
|
<n-upload
|
v-model:file-list="formModel.androidFiles"
|
:max="1"
|
accept=".apk"
|
:on-before-upload="beforeUploadApk"
|
:custom-request="customRequestAndroid"
|
:show-file-list="false"
|
>
|
<n-button style="width: 100px" :loading="androidUploading" :disabled="androidUploading">
|
<template #icon>
|
<icon-park-outline-upload />
|
</template>
|
{{ formModel.androidFiles.length > 0 ? '重新上传' : '点击上传' }}
|
</n-button>
|
</n-upload>
|
|
<div v-if="formModel.androidFiles.length > 0" class="flex items-center bg-[#F5F6F8] px-3 py-2 rounded w-fit gap-3">
|
<svg-icons-android-filled class="text-[#00B42A] text-lg" />
|
<span class="text-sm text-[#1D2129] max-w-[200px] truncate" :title="formModel.androidFiles[0].name">{{ formModel.androidFiles[0].name }}</span>
|
<n-spin v-if="androidUploading" size="small" />
|
<icon-park-outline-close v-else class="text-gray-400 cursor-pointer hover:text-gray-600" @click="formModel.androidFiles = []" />
|
</div>
|
</div>
|
</n-form-item>
|
|
<n-form-item label="iOS文件" path="iosFiles">
|
<div class="flex flex-col gap-2">
|
<n-upload
|
v-model:file-list="formModel.iosFiles"
|
:max="1"
|
accept=".ipa"
|
:on-before-upload="beforeUploadIpa"
|
:custom-request="customRequestIos"
|
:show-file-list="false"
|
>
|
<n-button style="width: 100px" :loading="iosUploading" :disabled="iosUploading">
|
<template #icon>
|
<icon-park-outline-upload />
|
</template>
|
{{ formModel.iosFiles.length > 0 ? '重新上传' : '点击上传' }}
|
</n-button>
|
</n-upload>
|
|
<div v-if="formModel.iosFiles.length > 0" class="flex items-center bg-[#F5F6F8] px-3 py-2 rounded w-fit gap-3">
|
<ri-apple-fill class="text-black text-lg" />
|
<span class="text-sm text-[#1D2129] max-w-[200px] truncate" :title="formModel.iosFiles[0].name">{{ formModel.iosFiles[0].name }}</span>
|
<n-spin v-if="iosUploading" size="small" />
|
<icon-park-outline-close v-else class="text-gray-400 cursor-pointer hover:text-gray-600" @click="formModel.iosFiles = []" />
|
</div>
|
</div>
|
</n-form-item>
|
|
<n-form-item label="更新说明" path="updateNote" label-placement="top">
|
<n-input
|
v-model:value="formModel.updateNote"
|
type="textarea"
|
placeholder="请输入"
|
:autosize="{ minRows: 6, maxRows: 10 }"
|
show-count
|
style="width: 750px;"
|
:maxlength="200"
|
/>
|
</n-form-item>
|
|
<n-form-item label="强制更新" path="isForceUpdate" label-placement="left">
|
<n-switch v-model:value="formModel.isForceUpdate" />
|
</n-form-item>
|
</n-form>
|
</div>
|
|
<div class="flex justify-end gap-3 mt-auto px-6 py-4 border-t border-gray-100">
|
<n-button style="padding: 8px 29px;" @click="handleCancel">
|
取消
|
</n-button>
|
<n-button style="padding: 8px 29px;" type="primary" @click="handleConfirm">
|
保存
|
</n-button>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
</style>
|