fix: 优化作业拍照上传
This commit is contained in:
parent
a6fde98a35
commit
5ec9b711aa
@ -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">
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user