fix: 修复画板无法上传问题

This commit is contained in:
shawko 2026-05-12 15:26:30 +08:00
parent d066302c31
commit 4805c7ee35
6 changed files with 146 additions and 45 deletions

View File

@ -94,6 +94,17 @@ function handleSave() {
}
}
function dataUrlToBlob(dataUrl: string) {
const [header, body = ''] = dataUrl.split(',')
const mime = header.match(/data:(.*?);base64/)?.[1] || 'image/jpeg'
const binary = atob(body)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return new Blob([bytes], { type: mime })
}
function handleSaveAsync(): Promise<{ blob: Blob | null; data: string | null }> {
return new Promise(resolve => {
if (signaturePad.isEmpty() && !hasBackground) {
@ -102,7 +113,17 @@ function handleSaveAsync(): Promise<{ blob: Blob | null; data: string | null }>
}
const canvas = canvasRef.value as HTMLCanvasElement
const pointData = JSON.stringify(signaturePad.toData())
canvas.toBlob(blob => resolve({ blob, data: pointData }), 'image/jpeg', 0.85)
const resolveWithFallback = (blob: Blob | null) => {
resolve({
blob: blob || dataUrlToBlob(canvas.toDataURL('image/jpeg', 0.85)),
data: pointData,
})
}
if (typeof canvas.toBlob === 'function') {
canvas.toBlob(resolveWithFallback, 'image/jpeg', 0.85)
} else {
resolveWithFallback(null)
}
})
}
@ -174,7 +195,8 @@ onMounted(() => {
canvasRef.value.height = canvasPanelRef.value.clientHeight
signaturePad = new SignaturePad(canvasRef.value, {
backgroundColor: props.bg,
throttle: 0,
throttle: 16,
minDistance: 1,
})
fillCanvasBackground()
}
@ -203,9 +225,16 @@ defineExpose({
.signature-pad {
width: 100%;
height: 100%;
touch-action: none;
user-select: none;
.canvas-panel {
width: 100%;
height: 100%;
touch-action: none;
canvas {
display: block;
touch-action: none;
}
}
}
</style>

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { XMessage, debounce } from '@/hooks'
import { $XXL, isWebEvn } from '@/utils'
import CameraPreview from './CameraPreview.vue'
import ImageCropper from '@/components/ImageCropper/index.vue'
@ -63,9 +62,19 @@ async function save() {
return XMessage.error('内容为空,无法保存')
}
showCanvas.value = false
if (typeof props.uploadFile !== 'function') {
return XMessage.error('上传方法不存在,请重试')
}
props.uploadFile?.({ file: blob, submitAnswerPicOriginal: data })
try {
const uploadSuccess = await props.uploadFile({ file: blob, submitAnswerPicOriginal: data })
if (uploadSuccess !== false) {
showCanvas.value = false
}
} catch (e) {
console.error('uploadFile error', e)
XMessage.error('上传失败,请重试')
}
}
const showCropper = ref(false)
@ -78,6 +87,7 @@ function onUploadCamera(options: any) {
showCanvas.value = false
uploadQueue.value.push(options)
processQueue()
return Promise.resolve()
}
function processQueue() {
@ -126,25 +136,7 @@ async function handleStart() {
return XMessage.error('上传中,请稍后~')
}
if (isWebEvn) {
showCanvas.value = true
} else {
const paths = props.isAndroid ? props.submitAnswerPicOriginal || '' : ''
$XXL.showDoodle(
{
mode: 1,
paths,
},
({ filepath, paths }: { filepath: string; paths: string }) => {
props.uploadFile?.({
file: filepath,
submitAnswerPicOriginal: paths,
isAndroid: 1,
})
},
)
}
showCanvas.value = true
}
const uploadRef = ref()

View File

@ -233,11 +233,11 @@ function setUploadLoadingByTitleId(titleId: unknown, value: boolean) {
//
async function uploadFile(options: anyObj) {
const currentQuestion = paperList.value[activeIdx.value] as anyObj | undefined
if (!currentQuestion) return
if (!currentQuestion) return false
const currentTitleId = getQuestionKey(currentQuestion)
if (currentQuestion.uploadLoading) {
XMessage.error('上传中,请稍后~')
return
return false
}
setUploadLoadingByTitleId(currentTitleId, true)
@ -262,7 +262,7 @@ async function uploadFile(options: anyObj) {
.trim()
if ((options.submitAnswerPicOriginal || !options.isAndroid) && !path) {
hud.error('上传成功但未获取到图片地址,请重试')
return
return false
}
if (options.submitAnswerPicOriginal) {
submitQuestion({
@ -298,13 +298,16 @@ async function uploadFile(options: anyObj) {
await switchQuestion(activeIdx.value)
}
}
return true
} catch (error) {
console.error('uploadFile error', error)
const message = getUploadErrorMessage(error)
console.error('uploadFile error', message, error)
hud.error(
error instanceof Error && error.message === 'upload timeout'
? '上传超时,请重试~'
: '网络异常,请检查网络~',
)
return false
} finally {
const finalIdx = paperList.value.findIndex(i =>
isSameQuestionKey(getQuestionKey(i as anyObj), currentTitleId),
@ -444,6 +447,17 @@ function extractUploadPath(res: any) {
return res?.data?.filePath || res?.data?.url || res?.filePath || res?.url || res?.data?.path || ''
}
function getUploadErrorMessage(error: unknown) {
if (error instanceof Error) return error.message
if (typeof error === 'string' && error) return error
if (error === null || error === undefined) return '上传失败,未返回错误信息'
try {
return JSON.stringify(error)
} catch {
return '上传失败'
}
}
function findQuestionIndexById(answerId: unknown) {
return paperList.value.findIndex(i => isSameQuestionKey(getQuestionKey(i as anyObj), answerId))
}

View File

@ -348,11 +348,21 @@ export const globalStore = defineStore('global', () => {
}
// PageSpy
setTimeout(() => {
const { userInfo } = userStore()
window.$pageSpy = new window.PageSpy({
title: `${userInfo ? `${userInfo.userId}-${userInfo.account}` : '未登陆'}`,
project: getSN(),
})
try {
const PageSpy = window.PageSpy
if (typeof PageSpy !== 'function') {
console.warn('PageSpy is not ready')
return
}
const { userInfo } = userStore()
window.$pageSpy?.abort?.()
window.$pageSpy = new PageSpy({
title: `${userInfo ? `${userInfo.userId}-${userInfo.account}` : '未登陆'}`,
project: getSN(),
})
} catch (err) {
console.warn('PageSpy init failed', err)
}
}, 1000)
return null
} catch (err) {

View File

@ -125,7 +125,7 @@ const pack = (method: string, config: anyObj = {}, cb?: anyFuc): any => {
if (res.code === 200) {
resolve(res.data)
} else {
reject(res.message)
reject(new Error(res.message || `${method} failed: ${JSON.stringify(res)}`))
}
}
}

View File

@ -57,6 +57,16 @@ const normalizeCosUrl = (location: string, sts: CosStsData, key: string) => {
return /^https?:\/\//i.test(url) ? url.replace(/^http:\/\//i, 'https://') : `https://${url}`
}
const normalizeUploadError = (error: unknown, fallback = 'COS上传失败') => {
if (error instanceof Error) return error
if (typeof error === 'string' && error) return new Error(error)
try {
return new Error(JSON.stringify(error) || fallback)
} catch {
return new Error(fallback)
}
}
const normalizeStsTime = (value: number) => (value > 9999999999 ? Math.floor(value / 1000) : value)
const normalizeSts = (sts: CosStsData): CosStsData => ({
@ -103,6 +113,37 @@ const createCos = (fileType: CosFileType) =>
},
})
const uploadFileToCosByPost = async (
sts: CosStsData,
key: string,
file: File | Blob,
onProgress?: UploadProgress,
) => {
const fields = await createPostObjectFields(sts, key)
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', `https://${getCosHost(sts)}`)
xhr.upload.onprogress = event => {
if (event.lengthComputable) {
onProgress?.({ percent: Math.round((event.loaded / event.total) * 100) })
}
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve()
return
}
reject(new Error(`COS上传失败(${xhr.status})`))
}
xhr.onerror = () => reject(new Error('COS上传网络异常'))
xhr.ontimeout = () => reject(new Error('COS上传超时'))
const formData = new FormData()
Object.entries(fields).forEach(([name, value]) => formData.append(name, value))
formData.append('file', file)
xhr.send(formData)
})
}
export const uploadFileToCos = async (
file: File | Blob,
options: { fileName?: string; fileType?: CosFileType; onProgress?: UploadProgress } = {},
@ -110,17 +151,32 @@ export const uploadFileToCos = async (
const fileType = options.fileType || 2
const sts = await getCosSts(fileType)
const key = buildKey(sts, file, options.fileName)
const result = await createCos(fileType).putObject({
Bucket: sts.bucket,
Region: sts.region,
Key: key,
Body: file,
ContentType: file.type || undefined,
onProgress: progress => {
options.onProgress?.({ percent: Math.round((progress.percent || 0) * 100) })
},
})
const url = normalizeCosUrl(result.Location, sts, key)
let location = ''
try {
const result = await createCos(fileType).putObject({
Bucket: sts.bucket,
Region: sts.region,
Key: key,
Body: file,
ContentType: file.type || undefined,
onProgress: progress => {
options.onProgress?.({ percent: Math.round((progress.percent || 0) * 100) })
},
})
location = result?.Location || ''
if (!location) throw new Error('COS上传结果为空')
} catch (error) {
console.warn(
'COS putObject failed, fallback to postObject',
normalizeUploadError(error).message,
)
try {
await uploadFileToCosByPost(sts, key, file, options.onProgress)
} catch (postError) {
throw normalizeUploadError(postError)
}
}
const url = normalizeCosUrl(location, sts, key)
return {
bucket: sts.bucket,
filePath: url,