fix: 优化作业拍照上传
This commit is contained in:
parent
a6fde98a35
commit
5ec9b711aa
@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, nextTick } from 'vue'
|
||||
import { ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
|
||||
@ -54,6 +54,8 @@ const imgLoading = ref(true)
|
||||
|
||||
// 平板/低端设备建议较小尺寸,减小处理与上传耗时
|
||||
const MAX_IMG_DIMENSION = 1280
|
||||
const IMAGE_LOAD_TIMEOUT = 10000
|
||||
const CROP_CONFIRM_TIMEOUT = 8000
|
||||
|
||||
const option = reactive({
|
||||
size: 0.85,
|
||||
@ -77,6 +79,20 @@ const option = reactive({
|
||||
|
||||
// 用于校验异步过程中 props.img 是否被替换,避免设置过时数据
|
||||
let loadToken = 0
|
||||
let imageLoadTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let cropConfirmTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function clearImageLoadTimer() {
|
||||
if (!imageLoadTimer) return
|
||||
clearTimeout(imageLoadTimer)
|
||||
imageLoadTimer = null
|
||||
}
|
||||
|
||||
function clearCropConfirmTimer() {
|
||||
if (!cropConfirmTimer) return
|
||||
clearTimeout(cropConfirmTimer)
|
||||
cropConfirmTimer = null
|
||||
}
|
||||
|
||||
function readFileAsDataURL(file: Blob | File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -145,16 +161,29 @@ watch(
|
||||
() => props.img,
|
||||
async (val: string | Blob | File | undefined) => {
|
||||
// 立即清空旧图,避免在异步处理新图时显示上一张图
|
||||
clearImageLoadTimer()
|
||||
imgUrl.value = ''
|
||||
imgLoading.value = true
|
||||
if (!val) return
|
||||
if (!val) {
|
||||
imgLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const myToken = ++loadToken
|
||||
try {
|
||||
const url = await downscaleImage(val)
|
||||
// 防止后来的图片切换被覆盖
|
||||
if (myToken !== loadToken) return
|
||||
if (!url) {
|
||||
imgLoading.value = false
|
||||
return
|
||||
}
|
||||
imgUrl.value = url
|
||||
imageLoadTimer = setTimeout(() => {
|
||||
if (myToken !== loadToken) return
|
||||
console.warn('cropper image load timeout')
|
||||
imgLoading.value = false
|
||||
}, IMAGE_LOAD_TIMEOUT)
|
||||
} catch (e) {
|
||||
console.error('cropper image load error', e)
|
||||
if (myToken === loadToken) imgLoading.value = false
|
||||
@ -165,6 +194,7 @@ watch(
|
||||
|
||||
function imgLoad(msg: string) {
|
||||
if (msg === 'success') {
|
||||
clearImageLoadTimer()
|
||||
imgLoading.value = false
|
||||
nextTick(() => {
|
||||
if (cropperRef.value) {
|
||||
@ -177,7 +207,11 @@ function imgLoad(msg: string) {
|
||||
})
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
clearImageLoadTimer()
|
||||
console.warn('cropper image load failed', msg)
|
||||
imgLoading.value = false
|
||||
}
|
||||
|
||||
function rotateLeft() {
|
||||
@ -189,18 +223,33 @@ function rotateRight() {
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
clearCropConfirmTimer()
|
||||
loading.value = false
|
||||
emit('update:visible', false)
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (imgLoading.value || loading.value) return
|
||||
if (!cropperRef.value?.getCropBlob) return
|
||||
loading.value = true
|
||||
cropperRef.value.getCropBlob((data: Blob) => {
|
||||
cropConfirmTimer = setTimeout(() => {
|
||||
console.warn('cropper confirm timeout')
|
||||
loading.value = false
|
||||
}, CROP_CONFIRM_TIMEOUT)
|
||||
cropperRef.value.getCropBlob((data: Blob) => {
|
||||
clearCropConfirmTimer()
|
||||
loading.value = false
|
||||
if (!data) return
|
||||
emit('confirm', data)
|
||||
emit('update:visible', false)
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearImageLoadTimer()
|
||||
clearCropConfirmTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -23,8 +23,8 @@ const OSS_URL = import.meta.env.VITE_OSS_URL
|
||||
const { token } = userStore()
|
||||
|
||||
function onUpload(options: any) {
|
||||
return typeof uploadFile === 'function'
|
||||
? uploadFile({
|
||||
return typeof props.uploadFile === 'function'
|
||||
? props.uploadFile({
|
||||
...options,
|
||||
isAndroid: 0,
|
||||
})
|
||||
|
||||
@ -64,6 +64,18 @@ interface WorkQuestion extends IQuestion {
|
||||
}
|
||||
const paperList = ref<WorkQuestion[]>([])
|
||||
const paperInfo = ref<any>({})
|
||||
const UPLOAD_TIMEOUT = 30000
|
||||
|
||||
function withUploadTimeout<T>(task: Promise<T>, timeout = UPLOAD_TIMEOUT) {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
const timeoutTask = new Promise<T>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new Error('upload timeout')), timeout)
|
||||
})
|
||||
return Promise.race<T>([task, timeoutTask]).finally(() => {
|
||||
if (!timer) return
|
||||
clearTimeout(timer)
|
||||
})
|
||||
}
|
||||
|
||||
function normalizePaperList(list: unknown, markUnLook = false): WorkQuestion[] {
|
||||
if (!Array.isArray(list)) return []
|
||||
@ -224,34 +236,40 @@ async function uploadFile(options: anyObj) {
|
||||
const currentQuestion = paperList.value[activeIdx.value] as anyObj | undefined
|
||||
if (!currentQuestion) return
|
||||
const currentTitleId = getQuestionKey(currentQuestion)
|
||||
if (currentQuestion.uploadLoading) {
|
||||
XMessage.error('上传中,请稍后~')
|
||||
return
|
||||
}
|
||||
setUploadLoadingByTitleId(currentTitleId, true)
|
||||
|
||||
try {
|
||||
let path = ''
|
||||
if (options.isAndroid) {
|
||||
if (options.submitAnswerPicOriginal) {
|
||||
const res = await $XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token,
|
||||
'X-Tenant-Id': sass.platform.tenantId,
|
||||
},
|
||||
filePath: options.file,
|
||||
fileKey: 'file',
|
||||
})
|
||||
path = res.url
|
||||
const res = await withUploadTimeout(
|
||||
$XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token,
|
||||
'X-Tenant-Id': sass.platform.tenantId,
|
||||
},
|
||||
filePath: options.file,
|
||||
fileKey: 'file',
|
||||
}),
|
||||
)
|
||||
path = extractUploadPath(res)
|
||||
}
|
||||
} else {
|
||||
const formData = new FormData()
|
||||
formData.append('file', options.file)
|
||||
const { data: res } = await globalApi.uploadFile(formData)
|
||||
const { data: res } = await withUploadTimeout<any>(globalApi.uploadFile(formData))
|
||||
path = extractUploadPath(res)
|
||||
}
|
||||
path = String(path || '')
|
||||
.replace(/[`]/g, '')
|
||||
.replace(/http:\/\//gi, 'https://')
|
||||
.trim()
|
||||
if (!options.isAndroid && !path) {
|
||||
if ((options.submitAnswerPicOriginal || !options.isAndroid) && !path) {
|
||||
hud.error('上传成功但未获取到图片地址,请重试')
|
||||
return
|
||||
}
|
||||
@ -291,7 +309,11 @@ async function uploadFile(options: anyObj) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('uploadFile error', error)
|
||||
hud.error('网络异常,请检查网络~')
|
||||
hud.error(
|
||||
error instanceof Error && error.message === 'upload timeout'
|
||||
? '上传超时,请重试~'
|
||||
: '网络异常,请检查网络~',
|
||||
)
|
||||
} finally {
|
||||
const finalIdx = paperList.value.findIndex(i =>
|
||||
isSameQuestionKey(getQuestionKey(i as anyObj), currentTitleId),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user