|
|
@@ -1,45 +1,66 @@
|
|
|
<template>
|
|
|
<view class="signature-body">
|
|
|
- <!-- 左侧操作栏 -->
|
|
|
<view class="sidebar">
|
|
|
<view class="btns">
|
|
|
<view class="button" :class="{ 'is-disabled': generating }" @click="handleClear">
|
|
|
- 重签
|
|
|
+ <text class="button-label">重签</text>
|
|
|
</view>
|
|
|
<view class="button active" :class="{ 'is-disabled': generating }" @click="handleSubmit">
|
|
|
- {{ generating ? '生成中' : '完成签名' }}
|
|
|
+ <text class="button-label">{{ generating ? '生成中' : '完成签名' }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 签名画布 -->
|
|
|
<view class="canvas">
|
|
|
+ <!-- 导出画布留在视口内,由白色签名画布覆盖,避免原生画布在屏幕外不渲染。 -->
|
|
|
+ <canvas
|
|
|
+ id="signature-export-canvas"
|
|
|
+ canvas-id="signature-export-canvas"
|
|
|
+ class="export-canvas"
|
|
|
+ :type="canvasType"
|
|
|
+ :style="exportCanvasStyle"
|
|
|
+ @error="handleCanvasError"
|
|
|
+ />
|
|
|
<canvas
|
|
|
id="signature-canvas"
|
|
|
canvas-id="signature-canvas"
|
|
|
+ class="drawing-canvas"
|
|
|
+ :type="canvasType"
|
|
|
disable-scroll
|
|
|
@touchstart="onTouchStart"
|
|
|
@touchmove="onTouchMove"
|
|
|
@touchend="onTouchEnd"
|
|
|
+ @touchcancel="onTouchEnd"
|
|
|
+ @error="handleCanvasError"
|
|
|
/>
|
|
|
- <text class="signature-text">签 名 区</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- 隐藏旋转画布 -->
|
|
|
- <canvas
|
|
|
- class="hidden-canvas"
|
|
|
- id="signature-rotate-canvas"
|
|
|
- canvas-id="signature-rotate-canvas"
|
|
|
- :style="rotateCanvasStyle"
|
|
|
- />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, type CSSProperties, getCurrentInstance, nextTick, onMounted, ref } from 'vue'
|
|
|
+import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
|
+
|
|
|
+import {
|
|
|
+ createCanvas2DSurface,
|
|
|
+ createLegacySurface,
|
|
|
+ createSignatureTasks,
|
|
|
+ drawBackground,
|
|
|
+ drawSegment,
|
|
|
+ drawSignatureForExport,
|
|
|
+ getTouchPoint,
|
|
|
+ hasSignaturePixels,
|
|
|
+ type CanvasBounds,
|
|
|
+ type SignaturePoint,
|
|
|
+ type SignatureSurface,
|
|
|
+} from './signatureCanvas'
|
|
|
|
|
|
const CANVAS_ID = 'signature-canvas'
|
|
|
-const ROTATE_CANVAS_ID = 'signature-rotate-canvas'
|
|
|
+const EXPORT_CANVAS_ID = 'signature-export-canvas'
|
|
|
+const OUTPUT_WIDTH = 300
|
|
|
+let canvasType: '2d' | undefined
|
|
|
+// #ifdef MP-WEIXIN
|
|
|
+canvasType = '2d'
|
|
|
+// #endif
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
(e: 'submit', filePath: string): void
|
|
|
@@ -48,350 +69,296 @@ const emit = defineEmits<{
|
|
|
}>()
|
|
|
|
|
|
const instance = getCurrentInstance()
|
|
|
-
|
|
|
-const ctx = ref<UniApp.CanvasContext | null>(null)
|
|
|
-const canvasWidth = ref(0)
|
|
|
-const canvasHeight = ref(0)
|
|
|
-const rotateCanvasWidth = ref(1)
|
|
|
-const rotateCanvasHeight = ref(1)
|
|
|
-
|
|
|
-const drawing = ref(false)
|
|
|
-const lastPoint = ref<{ x: number; y: number } | null>(null)
|
|
|
-const hasSignature = ref(false)
|
|
|
+const tasks = createSignatureTasks()
|
|
|
const generating = ref(false)
|
|
|
-
|
|
|
-const rotateCanvasStyle = computed<CSSProperties>(() => ({
|
|
|
- width: `${rotateCanvasWidth.value}px`,
|
|
|
- height: `${rotateCanvasHeight.value}px`,
|
|
|
+const outputHeight = ref(150)
|
|
|
+const exportCanvasStyle = computed(() => ({
|
|
|
+ width: `${OUTPUT_WIDTH}px`,
|
|
|
+ height: `${outputHeight.value}px`,
|
|
|
}))
|
|
|
|
|
|
-onMounted(async () => {
|
|
|
- await nextTick()
|
|
|
-
|
|
|
- const windowInfo = uni.getWindowInfo()
|
|
|
- canvasWidth.value = windowInfo.windowWidth
|
|
|
- canvasHeight.value = windowInfo.windowHeight
|
|
|
-
|
|
|
- ctx.value = uni.createCanvasContext(CANVAS_ID, instance?.proxy as any)
|
|
|
-
|
|
|
- drawBackground()
|
|
|
-})
|
|
|
-
|
|
|
-const drawBackground = () => {
|
|
|
- if (!ctx.value) return
|
|
|
-
|
|
|
- ctx.value.setFillStyle('#ffffff')
|
|
|
- ctx.value.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
+// 原生 Canvas/Context 保持普通引用,不能交给 Vue 深层响应式代理。
|
|
|
+let surface: SignatureSurface | undefined
|
|
|
+let exportSurface: SignatureSurface | undefined
|
|
|
+let bounds: CanvasBounds | undefined
|
|
|
+let strokes: SignaturePoint[][] = []
|
|
|
+let currentStroke: SignaturePoint[] | undefined
|
|
|
+let initializing: Promise<void> | undefined
|
|
|
+let disposed = false
|
|
|
+let canvasRevision = 0
|
|
|
+
|
|
|
+type CanvasNodeInfo = UniApp.NodeInfo & { node?: WechatMiniprogram.Canvas }
|
|
|
+type TouchEvent = Parameters<typeof getTouchPoint>[0]
|
|
|
+
|
|
|
+const queryCanvas = (id: string) =>
|
|
|
+ tasks.run<CanvasNodeInfo>('读取画布尺寸', (resolve, reject) => {
|
|
|
+ const query = uni.createSelectorQuery()
|
|
|
+ if (instance?.proxy) query.in(instance.proxy)
|
|
|
+ query
|
|
|
+ .select(`#${id}`)
|
|
|
+ .fields({ node: canvasType === '2d', size: true, rect: true }, (result) => {
|
|
|
+ if (!result || Array.isArray(result) || !result.width || !result.height) {
|
|
|
+ reject(new Error('签名画布未就绪,请点击重签重试'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ resolve(result)
|
|
|
+ })
|
|
|
+ .exec()
|
|
|
+ })
|
|
|
|
|
|
- ctx.value.save()
|
|
|
- ctx.value.translate(canvasWidth.value / 2, canvasHeight.value / 2)
|
|
|
- ctx.value.rotate(Math.PI / 2)
|
|
|
- ctx.value.setFontSize(60)
|
|
|
- ctx.value.setFillStyle('rgba(0,0,0,0.06)')
|
|
|
- ctx.value.setTextAlign('center')
|
|
|
- ctx.value.fillText('签 名 区', 0, 0)
|
|
|
- ctx.value.restore()
|
|
|
+const initializeCanvas = async () => {
|
|
|
+ if (initializing) return initializing
|
|
|
+ const revision = ++canvasRevision
|
|
|
+ const isObsolete = () => disposed || revision !== canvasRevision
|
|
|
+ initializing = (async () => {
|
|
|
+ await nextTick()
|
|
|
+ if (isObsolete()) return
|
|
|
+ const info = await queryCanvas(CANVAS_ID)
|
|
|
+ if (isObsolete()) return
|
|
|
+ const measuredBounds = {
|
|
|
+ width: info.width!,
|
|
|
+ height: info.height!,
|
|
|
+ left: info.left ?? 0,
|
|
|
+ top: info.top ?? 0,
|
|
|
+ }
|
|
|
+ outputHeight.value = Math.max(1, Math.round((OUTPUT_WIDTH * info.width!) / info.height!))
|
|
|
+ await nextTick()
|
|
|
+ if (isObsolete()) return
|
|
|
+ const exportInfo = await queryCanvas(EXPORT_CANVAS_ID)
|
|
|
+ if (isObsolete()) return
|
|
|
+
|
|
|
+ let drawingSurface: SignatureSurface
|
|
|
+ let outputSurface: SignatureSurface
|
|
|
+ if (canvasType === '2d') {
|
|
|
+ if (!info.node || !exportInfo.node) {
|
|
|
+ throw new Error('当前微信无法初始化签名画布,请更新微信后重试')
|
|
|
+ }
|
|
|
+ const pixelRatio = uni.getWindowInfo().pixelRatio || 1
|
|
|
+ drawingSurface = createCanvas2DSurface(info.node, info.width!, info.height!, pixelRatio)
|
|
|
+ outputSurface = createCanvas2DSurface(exportInfo.node, OUTPUT_WIDTH, outputHeight.value, 1)
|
|
|
+ } else {
|
|
|
+ drawingSurface = createLegacySurface(CANVAS_ID, instance?.proxy, tasks)
|
|
|
+ outputSurface = createLegacySurface(EXPORT_CANVAS_ID, instance?.proxy, tasks)
|
|
|
+ }
|
|
|
|
|
|
- ctx.value.draw()
|
|
|
+ drawBackground(drawingSurface.context, measuredBounds.width, measuredBounds.height)
|
|
|
+ await drawingSurface.flush(false)
|
|
|
+ if (isObsolete()) return
|
|
|
+ bounds = measuredBounds
|
|
|
+ surface = drawingSurface
|
|
|
+ exportSurface = outputSurface
|
|
|
+ })()
|
|
|
+ try {
|
|
|
+ await initializing
|
|
|
+ } finally {
|
|
|
+ initializing = undefined
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-const onTouchStart = (e: any) => {
|
|
|
- const touch = (e as any).touches?.[0]
|
|
|
- if (!touch || !ctx.value || generating.value) return
|
|
|
-
|
|
|
- drawing.value = true
|
|
|
- lastPoint.value = { x: touch.x, y: touch.y }
|
|
|
+const reportError = (error: unknown) => {
|
|
|
+ if (disposed) return
|
|
|
+ console.error('[SignaturePad]', error)
|
|
|
+ emit('error', error)
|
|
|
}
|
|
|
|
|
|
-const onTouchMove = (e: any) => {
|
|
|
- if (!drawing.value || !ctx.value || !lastPoint.value || generating.value) return
|
|
|
-
|
|
|
- const touch = (e as any).touches?.[0]
|
|
|
- if (!touch) return
|
|
|
+const handleCanvasError = (event: { detail?: { errMsg?: string } }) => {
|
|
|
+ const error = new Error(event.detail?.errMsg || '签名画布异常,请点击重签重试')
|
|
|
+ canvasRevision++
|
|
|
+ tasks.cancel(error)
|
|
|
+ surface = undefined
|
|
|
+ exportSurface = undefined
|
|
|
+ currentStroke = undefined
|
|
|
+ if (!generating.value && !initializing) reportError(error)
|
|
|
+}
|
|
|
|
|
|
- const current = { x: touch.x, y: touch.y }
|
|
|
+onMounted(() => {
|
|
|
+ void initializeCanvas().catch(reportError)
|
|
|
+})
|
|
|
|
|
|
- ctx.value.beginPath()
|
|
|
- ctx.value.setLineCap('round')
|
|
|
- ctx.value.setStrokeStyle('#000000')
|
|
|
- ctx.value.setLineWidth(4)
|
|
|
- ctx.value.moveTo(lastPoint.value.x, lastPoint.value.y)
|
|
|
- ctx.value.lineTo(current.x, current.y)
|
|
|
- ctx.value.stroke()
|
|
|
- ctx.value.draw(true)
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ disposed = true
|
|
|
+ tasks.cancel(new Error('签名页面已关闭'))
|
|
|
+ if (generating.value) uni.hideLoading()
|
|
|
+ surface = undefined
|
|
|
+ exportSurface = undefined
|
|
|
+})
|
|
|
|
|
|
- hasSignature.value = true
|
|
|
- lastPoint.value = current
|
|
|
+const onTouchStart = (event: TouchEvent) => {
|
|
|
+ if (!surface || !bounds || generating.value) return
|
|
|
+ const point = getTouchPoint(event, bounds)
|
|
|
+ if (!point) return
|
|
|
+ currentStroke = [point]
|
|
|
+ strokes.push(currentStroke)
|
|
|
}
|
|
|
|
|
|
-const onTouchEnd = () => {
|
|
|
- drawing.value = false
|
|
|
- lastPoint.value = null
|
|
|
+const appendPoint = (event: TouchEvent) => {
|
|
|
+ if (!currentStroke || !surface || !bounds || generating.value) return
|
|
|
+ const point = getTouchPoint(event, bounds)
|
|
|
+ const previous = currentStroke[currentStroke.length - 1]
|
|
|
+ if (!point || (point.x === previous.x && point.y === previous.y)) return
|
|
|
+ currentStroke.push(point)
|
|
|
+ try {
|
|
|
+ drawSegment(surface.context, previous, point)
|
|
|
+ // 旧版端提交绘制命令;微信 Canvas 2D 已同步绘制,无需等待 draw 回调。
|
|
|
+ surface.flushStroke()
|
|
|
+ } catch (error) {
|
|
|
+ handleCanvasError({ detail: { errMsg: error instanceof Error ? error.message : undefined } })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-const handleClear = () => {
|
|
|
- if (!ctx.value || generating.value) return
|
|
|
-
|
|
|
- ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
|
|
|
- hasSignature.value = false
|
|
|
- drawBackground()
|
|
|
- emit('clear')
|
|
|
+const onTouchMove = (event: TouchEvent) => appendPoint(event)
|
|
|
+const onTouchEnd = (event: TouchEvent) => {
|
|
|
+ appendPoint(event)
|
|
|
+ currentStroke = undefined
|
|
|
}
|
|
|
|
|
|
-const finishGeneration = () => {
|
|
|
- generating.value = false
|
|
|
- uni.hideLoading()
|
|
|
+const handleClear = async () => {
|
|
|
+ if (generating.value || disposed) return
|
|
|
+ currentStroke = undefined
|
|
|
+ strokes = []
|
|
|
+ surface = undefined
|
|
|
+ exportSurface = undefined
|
|
|
+ try {
|
|
|
+ await initializeCanvas()
|
|
|
+ if (!disposed) emit('clear')
|
|
|
+ } catch (error) {
|
|
|
+ reportError(error)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-const handleSubmit = () => {
|
|
|
- if (generating.value) return
|
|
|
-
|
|
|
- if (!ctx.value) {
|
|
|
- emit('error', new Error('签名画布未就绪'))
|
|
|
+const handleSubmit = async () => {
|
|
|
+ if (generating.value || disposed) return
|
|
|
+ if (!surface || !exportSurface || !bounds) {
|
|
|
+ reportError(new Error('签名画布未就绪,请点击重签重试'))
|
|
|
return
|
|
|
}
|
|
|
-
|
|
|
- if (!hasSignature.value) {
|
|
|
- emit('error', new Error('请先完成签名'))
|
|
|
+ if (!strokes.some((stroke) => stroke.length > 1)) {
|
|
|
+ reportError(new Error('请先完成签名'))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ currentStroke = undefined
|
|
|
generating.value = true
|
|
|
uni.showLoading({ title: '生成签名中', mask: true })
|
|
|
-
|
|
|
- // 等待最后一笔真正写入视图层后再导出,避免快速提交时拿到空画布。
|
|
|
+ let filePath: string | undefined
|
|
|
+ let failure: unknown
|
|
|
+ const output = exportSurface
|
|
|
+ const revision = canvasRevision
|
|
|
+ const ensureCanvasActive = () => {
|
|
|
+ if (revision !== canvasRevision) throw new Error('签名画布已失效,请点击重签重试')
|
|
|
+ }
|
|
|
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
|
|
|
- )
|
|
|
+ // 从实际笔迹重绘横向签名,省去原图导出、图片加载及异步旋转链路。
|
|
|
+ drawSignatureForExport(output.context, bounds, strokes, OUTPUT_WIDTH, outputHeight.value)
|
|
|
+ await output.flush(false)
|
|
|
+ if (disposed) return
|
|
|
+ ensureCanvasActive()
|
|
|
+ const pixels = await output.readPixels(OUTPUT_WIDTH, outputHeight.value)
|
|
|
+ if (disposed) return
|
|
|
+ ensureCanvasActive()
|
|
|
+ if (!hasSignaturePixels(pixels)) throw new Error('签名生成异常,请点击重签重试')
|
|
|
+ filePath = await tasks.run<string>('导出签名图片', (resolve, reject) => {
|
|
|
+ output.exportFile(OUTPUT_WIDTH, outputHeight.value, resolve, reject)
|
|
|
})
|
|
|
+ ensureCanvasActive()
|
|
|
+ if (!filePath) throw new Error('签名文件为空,请重试')
|
|
|
} catch (error) {
|
|
|
- finishGeneration()
|
|
|
- emit('error', error)
|
|
|
+ failure = error
|
|
|
+ } finally {
|
|
|
+ if (!disposed) {
|
|
|
+ generating.value = false
|
|
|
+ uni.hideLoading()
|
|
|
+ }
|
|
|
}
|
|
|
+ if (disposed) return
|
|
|
+ // 先关闭本组件 loading,再交给父页面显示“保存中”。
|
|
|
+ if (failure) reportError(failure)
|
|
|
+ else if (filePath) emit('submit', filePath)
|
|
|
}
|
|
|
+</script>
|
|
|
|
|
|
-const waitForRotateCanvasReady = (width: number, height: number): Promise<void> => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const query = uni.createSelectorQuery()
|
|
|
- if (instance?.proxy) query.in(instance.proxy)
|
|
|
-
|
|
|
- query
|
|
|
- .select(`#${ROTATE_CANVAS_ID}`)
|
|
|
- .boundingClientRect((rect) => {
|
|
|
- if (
|
|
|
- !rect ||
|
|
|
- Array.isArray(rect) ||
|
|
|
- typeof rect.width !== 'number' ||
|
|
|
- typeof rect.height !== 'number' ||
|
|
|
- Math.abs(rect.width - width) > 1 ||
|
|
|
- Math.abs(rect.height - height) > 1
|
|
|
- ) {
|
|
|
- reject(new Error('签名画布尺寸未就绪'))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- resolve()
|
|
|
- })
|
|
|
- .exec()
|
|
|
- })
|
|
|
+<style scoped lang="scss">
|
|
|
+.signature-body {
|
|
|
+ display: flex;
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ffffff;
|
|
|
}
|
|
|
|
|
|
-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
|
|
|
- )
|
|
|
- })
|
|
|
+.sidebar {
|
|
|
+ width: 150rpx;
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: #e5f1fe;
|
|
|
}
|
|
|
|
|
|
-const rotateImage = (src: string) => {
|
|
|
- uni.getImageInfo({
|
|
|
- src,
|
|
|
- async success(info) {
|
|
|
- const outputWidth = 300
|
|
|
- const outputHeight = Math.max(1, Math.round((outputWidth * info.width) / info.height))
|
|
|
-
|
|
|
- try {
|
|
|
- rotateCanvasWidth.value = outputWidth
|
|
|
- rotateCanvasHeight.value = outputHeight
|
|
|
- await nextTick()
|
|
|
- await waitForRotateCanvasReady(outputWidth, outputHeight)
|
|
|
-
|
|
|
- const rotateCtx = uni.createCanvasContext(ROTATE_CANVAS_ID, instance?.proxy as any)
|
|
|
-
|
|
|
- rotateCtx.setFillStyle('#ffffff')
|
|
|
- rotateCtx.fillRect(0, 0, outputWidth, outputHeight)
|
|
|
- rotateCtx.translate(outputWidth / 2, outputHeight / 2)
|
|
|
- rotateCtx.rotate((270 * Math.PI) / 180)
|
|
|
- rotateCtx.drawImage(
|
|
|
- info.path || src,
|
|
|
- -outputHeight / 2,
|
|
|
- -outputWidth / 2,
|
|
|
- outputHeight,
|
|
|
- outputWidth
|
|
|
- )
|
|
|
-
|
|
|
- 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)
|
|
|
- },
|
|
|
- },
|
|
|
- instance?.proxy as any
|
|
|
- )
|
|
|
- } catch (error) {
|
|
|
- rotateCanvasWidth.value = 1
|
|
|
- rotateCanvasHeight.value = 1
|
|
|
- finishGeneration()
|
|
|
- emit('error', error)
|
|
|
- }
|
|
|
- })
|
|
|
- } catch (err) {
|
|
|
- rotateCanvasWidth.value = 1
|
|
|
- rotateCanvasHeight.value = 1
|
|
|
- finishGeneration()
|
|
|
- emit('error', err)
|
|
|
- }
|
|
|
- },
|
|
|
- fail(err) {
|
|
|
- finishGeneration()
|
|
|
- emit('error', err)
|
|
|
- },
|
|
|
- })
|
|
|
+.btns {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
}
|
|
|
-</script>
|
|
|
|
|
|
-<style scoped lang="scss">
|
|
|
-.signature-body {
|
|
|
+.button {
|
|
|
display: flex;
|
|
|
- width: 100%;
|
|
|
- height: 100vh;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 80rpx;
|
|
|
+ height: 250rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
background: #ffffff;
|
|
|
+ color: #007bff;
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
|
- .sidebar {
|
|
|
- width: 150rpx;
|
|
|
- background-color: #e5f1fe;
|
|
|
- flex-shrink: 0;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
-
|
|
|
- .btns {
|
|
|
- display: flex;
|
|
|
- gap: 100rpx;
|
|
|
- transform: rotate(90deg);
|
|
|
-
|
|
|
- .button {
|
|
|
- width: 250rpx;
|
|
|
- height: 80rpx;
|
|
|
- line-height: 80rpx;
|
|
|
- text-align: center;
|
|
|
- border-radius: 6rpx;
|
|
|
- background-color: #ffffff;
|
|
|
- color: #007bff;
|
|
|
- font-size: 28rpx;
|
|
|
- }
|
|
|
+ + .button {
|
|
|
+ margin-top: 100rpx;
|
|
|
+ }
|
|
|
|
|
|
- .button.is-disabled {
|
|
|
- opacity: 0.55;
|
|
|
- }
|
|
|
+ &.active {
|
|
|
+ background: #4b9ef2;
|
|
|
+ color: #ffffff;
|
|
|
+ }
|
|
|
|
|
|
- .button.active {
|
|
|
- background-color: #4b9ef2;
|
|
|
- color: #ffffff;
|
|
|
- }
|
|
|
- }
|
|
|
+ &.is-disabled {
|
|
|
+ opacity: 0.55;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .canvas {
|
|
|
- flex: 1;
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
+.button-label {
|
|
|
+ display: block;
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 250rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ line-height: 80rpx;
|
|
|
+ text-align: center;
|
|
|
+ white-space: nowrap;
|
|
|
+ transform: rotate(90deg);
|
|
|
+}
|
|
|
|
|
|
- canvas {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- display: block;
|
|
|
- }
|
|
|
+.canvas {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
|
|
|
- .signature-text {
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- transform: rotate(90deg);
|
|
|
- font-size: 80rpx;
|
|
|
- color: rgba(208, 216, 242, 0.3);
|
|
|
- pointer-events: none;
|
|
|
- user-select: none;
|
|
|
- }
|
|
|
- }
|
|
|
+.drawing-canvas {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ z-index: 1;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
|
|
|
- .hidden-canvas {
|
|
|
- position: fixed;
|
|
|
- left: -9999px;
|
|
|
- top: -9999px;
|
|
|
- pointer-events: none;
|
|
|
- }
|
|
|
+.export-canvas {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
|
</style>
|