|
|
@@ -3,8 +3,12 @@
|
|
|
<!-- 左侧操作栏 -->
|
|
|
<view class="sidebar">
|
|
|
<view class="btns">
|
|
|
- <view class="button" @click="handleClear">重签</view>
|
|
|
- <view class="button active" @click="handleSubmit">完成签名</view>
|
|
|
+ <view class="button" :class="{ 'is-disabled': generating }" @click="handleClear">
|
|
|
+ 重签
|
|
|
+ </view>
|
|
|
+ <view class="button active" :class="{ 'is-disabled': generating }" @click="handleSubmit">
|
|
|
+ {{ generating ? '生成中' : '完成签名' }}
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -53,6 +57,8 @@ const rotateCanvasHeight = ref(1)
|
|
|
|
|
|
const drawing = ref(false)
|
|
|
const lastPoint = ref<{ x: number; y: number } | null>(null)
|
|
|
+const hasSignature = ref(false)
|
|
|
+const generating = ref(false)
|
|
|
|
|
|
const rotateCanvasStyle = computed<CSSProperties>(() => ({
|
|
|
width: `${rotateCanvasWidth.value}px`,
|
|
|
@@ -91,14 +97,14 @@ const drawBackground = () => {
|
|
|
|
|
|
const onTouchStart = (e: any) => {
|
|
|
const touch = (e as any).touches?.[0]
|
|
|
- if (!touch || !ctx.value) return
|
|
|
+ if (!touch || !ctx.value || generating.value) return
|
|
|
|
|
|
drawing.value = true
|
|
|
lastPoint.value = { x: touch.x, y: touch.y }
|
|
|
}
|
|
|
|
|
|
const onTouchMove = (e: any) => {
|
|
|
- if (!drawing.value || !ctx.value || !lastPoint.value) return
|
|
|
+ if (!drawing.value || !ctx.value || !lastPoint.value || generating.value) return
|
|
|
|
|
|
const touch = (e as any).touches?.[0]
|
|
|
if (!touch) return
|
|
|
@@ -114,6 +120,7 @@ const onTouchMove = (e: any) => {
|
|
|
ctx.value.stroke()
|
|
|
ctx.value.draw(true)
|
|
|
|
|
|
+ hasSignature.value = true
|
|
|
lastPoint.value = current
|
|
|
}
|
|
|
|
|
|
@@ -123,39 +130,58 @@ const onTouchEnd = () => {
|
|
|
}
|
|
|
|
|
|
const handleClear = () => {
|
|
|
- if (!ctx.value) return
|
|
|
+ if (!ctx.value || generating.value) return
|
|
|
|
|
|
ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
+ hasSignature.value = false
|
|
|
drawBackground()
|
|
|
emit('clear')
|
|
|
}
|
|
|
|
|
|
+const finishGeneration = () => {
|
|
|
+ generating.value = false
|
|
|
+ uni.hideLoading()
|
|
|
+}
|
|
|
+
|
|
|
const handleSubmit = () => {
|
|
|
+ if (generating.value) return
|
|
|
+
|
|
|
if (!ctx.value) {
|
|
|
emit('error', new Error('签名画布未就绪'))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- uni.showLoading({ title: '生成签名中' })
|
|
|
+ if (!hasSignature.value) {
|
|
|
+ emit('error', new Error('请先完成签名'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ generating.value = true
|
|
|
+ uni.showLoading({ title: '生成签名中', mask: true })
|
|
|
|
|
|
// 等待最后一笔真正写入视图层后再导出,避免快速提交时拿到空画布。
|
|
|
- ctx.value.draw(true, () => {
|
|
|
- uni.canvasToTempFilePath(
|
|
|
- {
|
|
|
- canvasId: CANVAS_ID,
|
|
|
- fileType: 'png',
|
|
|
- quality: 1,
|
|
|
- success(res) {
|
|
|
- rotateImage(res.tempFilePath)
|
|
|
- },
|
|
|
- fail(err) {
|
|
|
- uni.hideLoading()
|
|
|
- emit('error', err)
|
|
|
+ try {
|
|
|
+ ctx.value.draw(true, () => {
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
+ {
|
|
|
+ canvasId: CANVAS_ID,
|
|
|
+ fileType: 'png',
|
|
|
+ quality: 1,
|
|
|
+ success(res) {
|
|
|
+ rotateImage(res.tempFilePath)
|
|
|
+ },
|
|
|
+ fail(err) {
|
|
|
+ finishGeneration()
|
|
|
+ emit('error', err)
|
|
|
+ },
|
|
|
},
|
|
|
- },
|
|
|
- instance?.proxy as any
|
|
|
- )
|
|
|
- })
|
|
|
+ instance?.proxy as any
|
|
|
+ )
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ finishGeneration()
|
|
|
+ emit('error', error)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const waitForRotateCanvasReady = (width: number, height: number): Promise<void> => {
|
|
|
@@ -184,6 +210,39 @@ const waitForRotateCanvasReady = (width: number, height: number): Promise<void>
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+const validateRotatedSignature = (width: number, height: number): Promise<void> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.canvasGetImageData(
|
|
|
+ {
|
|
|
+ canvasId: ROTATE_CANVAS_ID,
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ success: (result) => {
|
|
|
+ const pixels = result.data ?? []
|
|
|
+
|
|
|
+ for (let index = 0; index < pixels.length; index += 4) {
|
|
|
+ const red = Number(pixels[index] ?? 255)
|
|
|
+ const green = Number(pixels[index + 1] ?? 255)
|
|
|
+ const blue = Number(pixels[index + 2] ?? 255)
|
|
|
+ const alpha = Number(pixels[index + 3] ?? 0)
|
|
|
+
|
|
|
+ if (alpha > 0 && red < 160 && green < 160 && blue < 160) {
|
|
|
+ resolve()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ reject(new Error('签名生成异常,请重新签名'))
|
|
|
+ },
|
|
|
+ fail: reject,
|
|
|
+ },
|
|
|
+ instance?.proxy as any
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
const rotateImage = (src: string) => {
|
|
|
uni.getImageInfo({
|
|
|
src,
|
|
|
@@ -211,41 +270,50 @@ const rotateImage = (src: string) => {
|
|
|
outputWidth
|
|
|
)
|
|
|
|
|
|
- rotateCtx.draw(false, () => {
|
|
|
- uni.canvasToTempFilePath(
|
|
|
- {
|
|
|
- canvasId: ROTATE_CANVAS_ID,
|
|
|
- width: outputWidth,
|
|
|
- height: outputHeight,
|
|
|
- destWidth: outputWidth,
|
|
|
- destHeight: outputHeight,
|
|
|
- fileType: 'png',
|
|
|
- quality: 1,
|
|
|
- success(res) {
|
|
|
- rotateCanvasWidth.value = 1
|
|
|
- rotateCanvasHeight.value = 1
|
|
|
- uni.hideLoading()
|
|
|
- emit('submit', res.tempFilePath)
|
|
|
+ rotateCtx.draw(false, async () => {
|
|
|
+ try {
|
|
|
+ await validateRotatedSignature(outputWidth, outputHeight)
|
|
|
+
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
+ {
|
|
|
+ canvasId: ROTATE_CANVAS_ID,
|
|
|
+ width: outputWidth,
|
|
|
+ height: outputHeight,
|
|
|
+ destWidth: outputWidth,
|
|
|
+ destHeight: outputHeight,
|
|
|
+ fileType: 'png',
|
|
|
+ quality: 1,
|
|
|
+ success(res) {
|
|
|
+ rotateCanvasWidth.value = 1
|
|
|
+ rotateCanvasHeight.value = 1
|
|
|
+ finishGeneration()
|
|
|
+ emit('submit', res.tempFilePath)
|
|
|
+ },
|
|
|
+ fail(err) {
|
|
|
+ rotateCanvasWidth.value = 1
|
|
|
+ rotateCanvasHeight.value = 1
|
|
|
+ finishGeneration()
|
|
|
+ emit('error', err)
|
|
|
+ },
|
|
|
},
|
|
|
- fail(err) {
|
|
|
- rotateCanvasWidth.value = 1
|
|
|
- rotateCanvasHeight.value = 1
|
|
|
- uni.hideLoading()
|
|
|
- emit('error', err)
|
|
|
- },
|
|
|
- },
|
|
|
- instance?.proxy as any
|
|
|
- )
|
|
|
+ instance?.proxy as any
|
|
|
+ )
|
|
|
+ } catch (error) {
|
|
|
+ rotateCanvasWidth.value = 1
|
|
|
+ rotateCanvasHeight.value = 1
|
|
|
+ finishGeneration()
|
|
|
+ emit('error', error)
|
|
|
+ }
|
|
|
})
|
|
|
} catch (err) {
|
|
|
rotateCanvasWidth.value = 1
|
|
|
rotateCanvasHeight.value = 1
|
|
|
- uni.hideLoading()
|
|
|
+ finishGeneration()
|
|
|
emit('error', err)
|
|
|
}
|
|
|
},
|
|
|
fail(err) {
|
|
|
- uni.hideLoading()
|
|
|
+ finishGeneration()
|
|
|
emit('error', err)
|
|
|
},
|
|
|
})
|
|
|
@@ -283,6 +351,10 @@ const rotateImage = (src: string) => {
|
|
|
font-size: 28rpx;
|
|
|
}
|
|
|
|
|
|
+ .button.is-disabled {
|
|
|
+ opacity: 0.55;
|
|
|
+ }
|
|
|
+
|
|
|
.button.active {
|
|
|
background-color: #4b9ef2;
|
|
|
color: #ffffff;
|