fix: 优化作业拍照上传

This commit is contained in:
shawko 2026-05-11 18:43:15 +08:00
parent a6fde98a35
commit 5ec9b711aa
3 changed files with 89 additions and 18 deletions

View File

@ -36,7 +36,7 @@
</template> </template>
<script setup lang="ts"> <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 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper' import { VueCropper } from 'vue-cropper'
@ -54,6 +54,8 @@ const imgLoading = ref(true)
// / // /
const MAX_IMG_DIMENSION = 1280 const MAX_IMG_DIMENSION = 1280
const IMAGE_LOAD_TIMEOUT = 10000
const CROP_CONFIRM_TIMEOUT = 8000
const option = reactive({ const option = reactive({
size: 0.85, size: 0.85,
@ -77,6 +79,20 @@ const option = reactive({
// props.img // props.img
let loadToken = 0 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> { function readFileAsDataURL(file: Blob | File): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -145,16 +161,29 @@ watch(
() => props.img, () => props.img,
async (val: string | Blob | File | undefined) => { async (val: string | Blob | File | undefined) => {
// //
clearImageLoadTimer()
imgUrl.value = '' imgUrl.value = ''
imgLoading.value = true imgLoading.value = true
if (!val) return if (!val) {
imgLoading.value = false
return
}
const myToken = ++loadToken const myToken = ++loadToken
try { try {
const url = await downscaleImage(val) const url = await downscaleImage(val)
// //
if (myToken !== loadToken) return if (myToken !== loadToken) return
if (!url) {
imgLoading.value = false
return
}
imgUrl.value = url imgUrl.value = url
imageLoadTimer = setTimeout(() => {
if (myToken !== loadToken) return
console.warn('cropper image load timeout')
imgLoading.value = false
}, IMAGE_LOAD_TIMEOUT)
} catch (e) { } catch (e) {
console.error('cropper image load error', e) console.error('cropper image load error', e)
if (myToken === loadToken) imgLoading.value = false if (myToken === loadToken) imgLoading.value = false
@ -165,6 +194,7 @@ watch(
function imgLoad(msg: string) { function imgLoad(msg: string) {
if (msg === 'success') { if (msg === 'success') {
clearImageLoadTimer()
imgLoading.value = false imgLoading.value = false
nextTick(() => { nextTick(() => {
if (cropperRef.value) { 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() { function rotateLeft() {
@ -189,18 +223,33 @@ function rotateRight() {
} }
function handleCancel() { function handleCancel() {
clearCropConfirmTimer()
loading.value = false
emit('update:visible', false) emit('update:visible', false)
emit('cancel') emit('cancel')
} }
function handleConfirm() { function handleConfirm() {
if (imgLoading.value || loading.value) return
if (!cropperRef.value?.getCropBlob) return
loading.value = true loading.value = true
cropperRef.value.getCropBlob((data: Blob) => { cropConfirmTimer = setTimeout(() => {
console.warn('cropper confirm timeout')
loading.value = false loading.value = false
}, CROP_CONFIRM_TIMEOUT)
cropperRef.value.getCropBlob((data: Blob) => {
clearCropConfirmTimer()
loading.value = false
if (!data) return
emit('confirm', data) emit('confirm', data)
emit('update:visible', false) emit('update:visible', false)
}) })
} }
onBeforeUnmount(() => {
clearImageLoadTimer()
clearCropConfirmTimer()
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -23,8 +23,8 @@ const OSS_URL = import.meta.env.VITE_OSS_URL
const { token } = userStore() const { token } = userStore()
function onUpload(options: any) { function onUpload(options: any) {
return typeof uploadFile === 'function' return typeof props.uploadFile === 'function'
? uploadFile({ ? props.uploadFile({
...options, ...options,
isAndroid: 0, isAndroid: 0,
}) })

View File

@ -64,6 +64,18 @@ interface WorkQuestion extends IQuestion {
} }
const paperList = ref<WorkQuestion[]>([]) const paperList = ref<WorkQuestion[]>([])
const paperInfo = ref<any>({}) 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[] { function normalizePaperList(list: unknown, markUnLook = false): WorkQuestion[] {
if (!Array.isArray(list)) return [] if (!Array.isArray(list)) return []
@ -224,13 +236,18 @@ async function uploadFile(options: anyObj) {
const currentQuestion = paperList.value[activeIdx.value] as anyObj | undefined const currentQuestion = paperList.value[activeIdx.value] as anyObj | undefined
if (!currentQuestion) return if (!currentQuestion) return
const currentTitleId = getQuestionKey(currentQuestion) const currentTitleId = getQuestionKey(currentQuestion)
if (currentQuestion.uploadLoading) {
XMessage.error('上传中,请稍后~')
return
}
setUploadLoadingByTitleId(currentTitleId, true) setUploadLoadingByTitleId(currentTitleId, true)
try { try {
let path = '' let path = ''
if (options.isAndroid) { if (options.isAndroid) {
if (options.submitAnswerPicOriginal) { if (options.submitAnswerPicOriginal) {
const res = await $XXL.uploadFile({ const res = await withUploadTimeout(
$XXL.uploadFile({
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`, url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
header: { header: {
Authorization: token, Authorization: token,
@ -238,20 +255,21 @@ async function uploadFile(options: anyObj) {
}, },
filePath: options.file, filePath: options.file,
fileKey: 'file', fileKey: 'file',
}) }),
path = res.url )
path = extractUploadPath(res)
} }
} else { } else {
const formData = new FormData() const formData = new FormData()
formData.append('file', options.file) 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 = extractUploadPath(res)
} }
path = String(path || '') path = String(path || '')
.replace(/[`]/g, '') .replace(/[`]/g, '')
.replace(/http:\/\//gi, 'https://') .replace(/http:\/\//gi, 'https://')
.trim() .trim()
if (!options.isAndroid && !path) { if ((options.submitAnswerPicOriginal || !options.isAndroid) && !path) {
hud.error('上传成功但未获取到图片地址,请重试') hud.error('上传成功但未获取到图片地址,请重试')
return return
} }
@ -291,7 +309,11 @@ async function uploadFile(options: anyObj) {
} }
} catch (error) { } catch (error) {
console.error('uploadFile error', error) console.error('uploadFile error', error)
hud.error('网络异常,请检查网络~') hud.error(
error instanceof Error && error.message === 'upload timeout'
? '上传超时,请重试~'
: '网络异常,请检查网络~',
)
} finally { } finally {
const finalIdx = paperList.value.findIndex(i => const finalIdx = paperList.value.findIndex(i =>
isSameQuestionKey(getQuestionKey(i as anyObj), currentTitleId), isSameQuestionKey(getQuestionKey(i as anyObj), currentTitleId),