| | |
| | | url?: string |
| | | } |
| | | |
| | | type DirectUploadUrlPayload = string | { |
| | | uploadUrl?: string |
| | | url?: string |
| | | fileUrl?: string |
| | | ossUrl?: string |
| | | } |
| | | |
| | | function logDirectUpload(step: string, payload?: unknown) { |
| | | console.log(`[direct-upload] ${step}`, payload) |
| | | } |
| | | |
| | | interface CustomDirectUploadRequestOptions extends UploadCustomRequestOptions { |
| | | /** 可选:外部指定 fileId,不传则公共方法内部自动生成 */ |
| | | fileId?: string |
| | | /** 可选:允许业务侧覆盖“获取上传地址”的实现 */ |
| | | getUploadUrl?: (params: { fileId: string, file: File }) => Promise<string | { uploadUrl?: string, url?: string, fileUrl?: string, ossUrl?: string }> |
| | | getUploadUrl?: (params: { fileId: string, file: File }) => Promise<Service.ResponseResult<DirectUploadUrlPayload> | DirectUploadUrlPayload> |
| | | } |
| | | |
| | | /** |
| | | * 统一兼容后端返回的直传地址格式,便于页面只关心上传行为本身。 |
| | | * 支持两类返回: |
| | | * 1. 直接返回上传地址字符串 / 对象 |
| | | * 2. 返回标准响应结构,上传地址放在 data 字段里 |
| | | */ |
| | | function normalizeDirectUploadUrlResult(raw: string | { uploadUrl?: string, url?: string, fileUrl?: string, ossUrl?: string } | null | undefined): DirectUploadUrlResult | null { |
| | | function normalizeDirectUploadUrlResult(raw: Service.ResponseResult<DirectUploadUrlPayload> | DirectUploadUrlPayload | null | undefined): DirectUploadUrlResult | null { |
| | | if (!raw) |
| | | return null |
| | | |
| | | if (typeof raw === 'string') { |
| | | const uploadUrl = raw.trim() |
| | | const payload = ( |
| | | typeof raw === 'object' |
| | | && raw !== null |
| | | && 'data' in raw |
| | | ? raw.data |
| | | : raw |
| | | ) |
| | | |
| | | if (typeof payload === 'string') { |
| | | const uploadUrl = payload.trim() |
| | | if (!uploadUrl) |
| | | return null |
| | | return { |
| | |
| | | } |
| | | } |
| | | |
| | | const uploadUrl = String(raw.uploadUrl || raw.url || raw.fileUrl || raw.ossUrl || '').trim() |
| | | const uploadUrl = String(payload?.uploadUrl || payload?.url || payload?.fileUrl || payload?.ossUrl || '').trim() |
| | | if (!uploadUrl) |
| | | return null |
| | | |
| | | const fileUrl = String(raw.url || raw.fileUrl || raw.ossUrl || '').trim() |
| | | const fileUrl = String(payload?.url || payload?.fileUrl || payload?.ossUrl || '').trim() |
| | | return { |
| | | uploadUrl, |
| | | url: fileUrl || uploadUrl.split('?')[0] || '', |
| | |
| | | uploadUrl, |
| | | file, |
| | | onProgress, |
| | | fileId, |
| | | }: { |
| | | uploadUrl: string |
| | | file: File |
| | | onProgress?: (e: { percent: number }) => void |
| | | fileId?: string |
| | | }) { |
| | | return new Promise<void>((resolve, reject) => { |
| | | const xhr = new XMLHttpRequest() |
| | | logDirectUpload('xhr open', { |
| | | fileId, |
| | | fileName: file.name, |
| | | method: 'PUT', |
| | | uploadUrl, |
| | | }) |
| | | xhr.open('PUT', uploadUrl, true) |
| | | |
| | | xhr.upload.onprogress = (event) => { |
| | | if (!event.lengthComputable) |
| | | return |
| | | const percent = Math.min(100, Math.round((event.loaded / event.total) * 100)) |
| | | logDirectUpload('upload progress', { |
| | | fileId, |
| | | fileName: file.name, |
| | | loaded: event.loaded, |
| | | total: event.total, |
| | | percent, |
| | | }) |
| | | onProgress?.({ |
| | | percent: Math.min(100, Math.round((event.loaded / event.total) * 100)), |
| | | percent, |
| | | }) |
| | | } |
| | | |
| | | xhr.onload = () => { |
| | | logDirectUpload('xhr load', { |
| | | fileId, |
| | | fileName: file.name, |
| | | status: xhr.status, |
| | | responseText: xhr.responseText, |
| | | }) |
| | | if (xhr.status >= 200 && xhr.status < 300) { |
| | | onProgress?.({ percent: 100 }) |
| | | resolve() |
| | |
| | | } |
| | | |
| | | xhr.onerror = () => { |
| | | logDirectUpload('xhr error', { |
| | | fileId, |
| | | fileName: file.name, |
| | | status: xhr.status, |
| | | }) |
| | | reject(new Error('Network error')) |
| | | } |
| | | |
| | | xhr.onabort = () => { |
| | | logDirectUpload('xhr abort', { |
| | | fileId, |
| | | fileName: file.name, |
| | | }) |
| | | reject(new Error('Upload aborted')) |
| | | } |
| | | |
| | | logDirectUpload('xhr send', { |
| | | fileId, |
| | | fileName: file.name, |
| | | size: file.size, |
| | | type: file.type, |
| | | }) |
| | | xhr.send(file) |
| | | }) |
| | | } |
| | |
| | | }: CustomDirectUploadRequestOptions) { |
| | | try { |
| | | const rawFile = file.file |
| | | logDirectUpload('custom request start', { |
| | | fileName: file.name, |
| | | hasRawFile: Boolean(rawFile), |
| | | source: getUploadUrl ? 'custom getUploadUrl' : 'default getUploadOSSUrl', |
| | | }) |
| | | if (!rawFile) { |
| | | logDirectUpload('custom request missing raw file', { |
| | | fileName: file.name, |
| | | file, |
| | | }) |
| | | file.status = 'error' |
| | | onError() |
| | | window.$message.error('未获取到上传文件') |
| | |
| | | |
| | | // 直传链路里 fileId 由前端先生成,后续保存业务表单时直接提交它 |
| | | const nextFileId = String(fileId || uuidv4()) |
| | | const uploadUrlResult = normalizeDirectUploadUrlResult( |
| | | await (getUploadUrl |
| | | logDirectUpload('generated file id', { |
| | | fileId: nextFileId, |
| | | fileName: rawFile.name, |
| | | size: rawFile.size, |
| | | type: rawFile.type, |
| | | }) |
| | | const rawUploadUrlResponse = await (getUploadUrl |
| | | ? getUploadUrl({ fileId: nextFileId, file: rawFile }) |
| | | : getUploadOSSUrl({ fileId: nextFileId })), |
| | | ) |
| | | : getUploadOSSUrl({ fileId: nextFileId })) |
| | | logDirectUpload('get upload url response', { |
| | | fileId: nextFileId, |
| | | fileName: rawFile.name, |
| | | response: rawUploadUrlResponse, |
| | | }) |
| | | |
| | | const uploadUrlResult = normalizeDirectUploadUrlResult(rawUploadUrlResponse) |
| | | logDirectUpload('normalized upload url', { |
| | | fileId: nextFileId, |
| | | uploadUrl: uploadUrlResult?.uploadUrl, |
| | | fileUrl: uploadUrlResult?.url, |
| | | }) |
| | | |
| | | if (!uploadUrlResult?.uploadUrl) { |
| | | file.status = 'error' |
| | |
| | | return undefined |
| | | } |
| | | |
| | | logDirectUpload('start upload', { |
| | | fileId: nextFileId, |
| | | fileName: rawFile.name, |
| | | uploadUrl: uploadUrlResult.uploadUrl, |
| | | }) |
| | | await uploadFileToSignedUrl({ |
| | | uploadUrl: uploadUrlResult.uploadUrl, |
| | | file: rawFile, |
| | | onProgress, |
| | | fileId: nextFileId, |
| | | }) |
| | | logDirectUpload('upload success', { |
| | | fileId: nextFileId, |
| | | fileName: rawFile.name, |
| | | fileUrl: uploadUrlResult.url, |
| | | }) |
| | | |
| | | // 回写 n-upload 所需状态,保持页面文件列表展示正常 |
| | | file.status = 'finished' |
| | | file.url = uploadUrlResult.url || '' |
| | | logDirectUpload('call onFinish', { |
| | | fileId: nextFileId, |
| | | fileName: rawFile.name, |
| | | }) |
| | | onFinish() |
| | | |
| | | return { |
| | |
| | | } |
| | | } |
| | | catch (err) { |
| | | logDirectUpload('upload failed', { |
| | | error: err, |
| | | fileName: file.name, |
| | | }) |
| | | file.status = 'error' |
| | | onError() |
| | | window.$message.error('文件上传出错') |
| | |
| | | // const { maxSize = 5, allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'] } = options |
| | | const { maxSize = 5, allowedTypes } = options |
| | | const file = data.file.file |
| | | logDirectUpload('before upload check start', { |
| | | fileName: data.file.name, |
| | | hasRawFile: Boolean(file), |
| | | size: file?.size, |
| | | type: file?.type, |
| | | maxSize, |
| | | allowedTypes, |
| | | }) |
| | | if (!file) |
| | | return false |
| | | |
| | | // 检查文件类型 |
| | | if (allowedTypes && allowedTypes?.length > 0 && !allowedTypes.includes(file.type)) { |
| | | logDirectUpload('before upload check failed: type', { |
| | | fileName: data.file.name, |
| | | fileType: file.type, |
| | | allowedTypes, |
| | | }) |
| | | window.$message.error(`只能上传 ${allowedTypes.join('/')} 格式的文件`) |
| | | return false |
| | | } |
| | |
| | | // 检查文件大小 |
| | | const isLtSize = file.size / 1024 / 1024 < maxSize |
| | | if (!isLtSize) { |
| | | logDirectUpload('before upload check failed: size', { |
| | | fileName: data.file.name, |
| | | size: file.size, |
| | | maxSize, |
| | | }) |
| | | window.$message.error(`文件大小不能超过 ${maxSize}MB`) |
| | | return false |
| | | } |
| | | |
| | | logDirectUpload('before upload check passed', { |
| | | fileName: data.file.name, |
| | | size: file.size, |
| | | type: file.type, |
| | | }) |
| | | return true |
| | | } |
| | |
| | | import { beforeUploadCheck, customDirectUploadRequest } from '@/utils/upload' |
| | | import { releaseAppVersion } 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) |
| | |
| | | |
| | | async function customRequestAndroid(options: any) { |
| | | 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?.() |
| | | } |
| | |
| | | // 安卓包改为前端直传:先拿上传地址,再将文件直接传到 OSS |
| | | const res = await customDirectUploadRequest(options 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', |
| | |
| | | }] |
| | | } |
| | | finally { |
| | | logAppVersionAdd('android upload finished', { |
| | | uploading: false, |
| | | fileList: formModel.value.androidFiles, |
| | | }) |
| | | androidUploading.value = false |
| | | } |
| | | } |
| | | |
| | | async function customRequestIos(options: any) { |
| | | 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?.() |
| | | } |
| | |
| | | // iOS 包改为前端直传:上传完成后将 fileId 存入文件项 id 字段 |
| | | const res = await customDirectUploadRequest(options 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', |
| | |
| | | }] |
| | | } |
| | | 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 |
| | |
| | | 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') |
| | |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import OperationLogModal from '@/components/custom/OperationLogModal.vue' |
| | | |
| | | function logAppVersionUpload(step: string, payload?: unknown) { |
| | | console.log(`[app-version/index] ${step}`, payload) |
| | | } |
| | | |
| | | const listData = ref<AppVersionItem[]>([]) |
| | | const loading = ref(false) |
| | | const showForm = ref(false) |
| | |
| | | |
| | | async function handleConfirm() { |
| | | await formRef.value?.validate(async (errors) => { |
| | | logAppVersionUpload('handle confirm validate result', { |
| | | errors, |
| | | androidUploading: androidUploading.value, |
| | | iosUploading: iosUploading.value, |
| | | version: formModel.value.version, |
| | | }) |
| | | if (errors) { |
| | | window.$message?.error('请完善必填信息') |
| | | return |
| | |
| | | updateDescription: formModel.value.updateNote, |
| | | forceUpdateFlag: formModel.value.isForceUpdate ? 1 : 0, |
| | | } |
| | | logAppVersionUpload('release request params', params) |
| | | |
| | | const { isSuccess } = await releaseAppVersion(params) |
| | | logAppVersionUpload('release response', { isSuccess, params }) |
| | | if (isSuccess) { |
| | | window.$message?.success('发布成功') |
| | | showForm.value = false |
| | |
| | | |
| | | async function customRequestAndroid(options: any) { |
| | | androidUploading.value = true |
| | | logAppVersionUpload('android upload triggered', { |
| | | fileName: options?.file?.name, |
| | | file: options?.file, |
| | | }) |
| | | const originalOnError = options.onError |
| | | options.onError = () => { |
| | | logAppVersionUpload('android upload onError callback') |
| | | formModel.value.androidFiles = [] |
| | | originalOnError?.() |
| | | } |
| | |
| | | // 安卓包改为前端直传:先换取上传地址,再直接上传到 OSS |
| | | const res = await customDirectUploadRequest(options as UploadCustomRequestOptions) |
| | | if (!res) { |
| | | logAppVersionUpload('android upload result empty') |
| | | formModel.value.androidFiles = [] |
| | | } |
| | | else { |
| | | logAppVersionUpload('android upload result', res) |
| | | formModel.value.androidFiles = [{ |
| | | id: String(res.id || ''), |
| | | name: res.name || 'android.apk', |
| | |
| | | } |
| | | } |
| | | finally { |
| | | logAppVersionUpload('android upload finished', { |
| | | uploading: false, |
| | | fileList: formModel.value.androidFiles, |
| | | }) |
| | | androidUploading.value = false |
| | | } |
| | | } |
| | | |
| | | async function customRequestIos(options: any) { |
| | | iosUploading.value = true |
| | | logAppVersionUpload('ios upload triggered', { |
| | | fileName: options?.file?.name, |
| | | file: options?.file, |
| | | }) |
| | | const originalOnError = options.onError |
| | | options.onError = () => { |
| | | logAppVersionUpload('ios upload onError callback') |
| | | formModel.value.iosFiles = [] |
| | | originalOnError?.() |
| | | } |
| | |
| | | // iOS 包改为前端直传:上传成功后将 fileId 存到文件项 id 字段 |
| | | const res = await customDirectUploadRequest(options as UploadCustomRequestOptions) |
| | | if (!res) { |
| | | logAppVersionUpload('ios upload result empty') |
| | | formModel.value.iosFiles = [] |
| | | } |
| | | else { |
| | | logAppVersionUpload('ios upload result', res) |
| | | formModel.value.iosFiles = [{ |
| | | id: String(res.id || ''), |
| | | name: res.name || 'ios.ipa', |
| | |
| | | } |
| | | } |
| | | finally { |
| | | logAppVersionUpload('ios upload finished', { |
| | | uploading: false, |
| | | fileList: formModel.value.iosFiles, |
| | | }) |
| | | iosUploading.value = false |
| | | } |
| | | } |