Files
ylst-h5/src/views/Design/components/Questions/SignQuestion.vue
Huangzhe 40519243b8 fix[design]: 修复签名异常
- 签名题目在特定情况下会出现边框溢出的问题, 现在将边框的宽度设定为 window.innerWidth * 0.8
2025-03-25 09:30:59 +08:00

492 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref, useTemplateRef, onUnmounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { showConfirmDialog } from 'vant';
// question 属性
const element = defineModel<question>('element', { default: {} });
// 属性框是否激活
const active = defineModel<boolean>('active', { default: false });
// 题目索引
const index = defineModel<number | string>('index', { default: 0 });
// 答案
const answer = defineModel<string>('answer', { default: '' });
// 错误信息
const errorMessage = defineModel<string>('errorMessage', { default: '' });
const isPreview = defineModel('isPreview', { default: false });
const signatureCanvas = useTemplateRef('signatureCanvas');
const canvasWidth = ref(window.innerWidth * 0.8);
const canvasHeight = computed(() => canvasWidth.value / 1);
const isEraser = ref(false);
let ctx: CanvasRenderingContext2D;
let isDrawing = false;
const undoStack = ref<ImageData[]>([]);
const currentStep = ref(-1);
// 保存当前状态
const saveState = () => {
if (!ctx || !signatureCanvas.value) return;
const imageData = ctx.getImageData(
0,
0,
signatureCanvas.value.width,
signatureCanvas.value.height
);
currentStep.value += 1;
// 移除当前步骤之后的所有状态(处理在撤销后又进行了新的绘制的情况)
undoStack.value.splice(currentStep.value);
undoStack.value.push(imageData);
};
// 设置画笔样式
const setPenStyle = () => {
if (!ctx) return;
if (isEraser.value) {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 20;
} else {
ctx.globalCompositeOperation = 'source-over';
ctx.lineWidth = 2;
}
ctx.strokeStyle = '#000';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
};
// 切换画笔/橡皮擦
const togglePen = () => {
isEraser.value = !isEraser.value;
setPenStyle();
};
const route = useRoute();
onMounted(() => {
// 设置页面刷新标记
sessionStorage.setItem('is_page_refresh', '1');
if (!signatureCanvas.value) return;
// 获取 canvas 上下文
ctx = signatureCanvas.value.getContext('2d')!;
if (!ctx) return;
setPenStyle();
// 保存初始空白状态
saveState();
// 触摸开始,开始绘制适用于移动设备
signatureCanvas.value?.addEventListener('touchstart', (e) => {
// if (!active.value) {
// return;
// }
// 防止页面滚动
e.preventDefault();
isDrawing = true;
const rect = signatureCanvas.value!.getBoundingClientRect();
const touch = e.touches[0];
ctx.beginPath();
ctx.moveTo(touch.clientX - rect.left, touch.clientY - rect.top);
});
signatureCanvas.value?.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!isDrawing) return;
const rect = signatureCanvas.value!.getBoundingClientRect();
const touch = e.touches[0];
ctx.lineTo(touch.clientX - rect.left, touch.clientY - rect.top);
ctx.stroke();
});
signatureCanvas.value?.addEventListener('touchend', () => {
if (isDrawing) {
saveState();
}
isDrawing = false;
});
signatureCanvas.value?.addEventListener('touchcancel', () => {
isDrawing = false;
});
// 鼠标事件处理
signatureCanvas.value?.addEventListener('mousedown', (e) => {
isDrawing = true;
const rect = signatureCanvas.value!.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
});
signatureCanvas.value?.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const rect = signatureCanvas.value!.getBoundingClientRect();
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
ctx.stroke();
});
signatureCanvas.value?.addEventListener('mouseup', () => {
if (isDrawing) {
saveState();
}
isDrawing = false;
});
signatureCanvas.value?.addEventListener('mouseleave', () => {
if (isDrawing) {
saveState();
}
isDrawing = false;
});
});
/**
* 清除画布
*/
const clearCanvas = () => {
if (!ctx || !signatureCanvas.value) return;
ctx.clearRect(0, 0, signatureCanvas.value.width, signatureCanvas.value.height);
saveState();
};
/**
* 保存画布
* @param type {'dataUrl' | 'blob'} 保存类型
* @return {string | Blob | undefined | null}
*/
const saveCanvas = (type: 'dataUrl' | 'blob'): string | Blob | undefined | null => {
let img: string | Blob | undefined | null = undefined;
if (!ctx || !signatureCanvas.value) return;
if (type === 'blob') {
signatureCanvas.value.toBlob(
(blob) => {
img = blob;
},
'image/png',
{ quality: 0.9 }
);
return img;
}
if (type === 'dataUrl') return signatureCanvas.value.toDataURL('image/png');
};
/**
* 撤销
*/
const undo = () => {
if (!ctx || !signatureCanvas.value || currentStep.value <= 0) return;
currentStep.value -= 1;
const imageData = undoStack.value[currentStep.value];
if (imageData) {
ctx.putImageData(imageData, 0, 0);
}
};
const emit = defineEmits(['update:element']);
const emitValue = () => {
emit('update:element', element.value);
};
let aIndex = 1;
/**
* 上传文件
*/
async function handleUploadImg() {
// const file = new File([saveCanvas('blob')!], 'sign.png', { type: 'image/png' });
// const res = await CommonApi.cosUpload(file);
// console.log(`sign upload url`, res);
// // 传递答案
// answer.value = res;
showConfirmDialog({
title: '提示',
message: '上传成功',
showCancelButton: false
}).then(() => {
aIndex++;
answer.value = 'this is test' + aIndex;
});
saveCanvasState();
}
/**
* 保存canvas 状态
* 当组件注销时需要保存状态存到本地indexDB ,然后等待下次组件加载时恢复
* 但在页面刷新时不保存
*/
function saveCanvasState() {
if (!ctx || !signatureCanvas.value) return;
const imageData = ctx.getImageData(0, 0, canvasWidth.value, canvasHeight.value);
undoStack.value.push(imageData);
currentStep.value = undoStack.value.length - 1;
// 检查是否是页面刷新或关闭
const isPageRefresh = !sessionStorage.getItem('is_page_refresh');
// 如果是页面刷新,则不保存状态
if (isPageRefresh) {
console.log('页面刷新,不保存画布状态');
// 清除之前保存的状态
if (window.indexedDB) {
try {
const request = window.indexedDB.open('canvasState', 1);
request.onsuccess = (e: Event) => {
const db = (e.target as IDBOpenDBRequest).result;
const tx = db.transaction('canvasState', 'readwrite');
const store = tx.objectStore('canvasState');
store.delete(element.value.id || 'default_id');
tx.oncomplete = () => {
db.close();
};
};
} catch (err) {
console.error('Error clearing canvas state:', err);
}
}
return;
}
// 没有 indexDB, 不保存
if (!window.indexedDB) return;
// 将 canvas 转换为 base64 字符串
const canvasDataUrl = signatureCanvas.value.toDataURL('image/png');
// 保存到 indexDB
const request = window.indexedDB.open('canvasState', 1);
// 当数据库首次创建或版本升级时创建对象存储
request.onupgradeneeded = (e: IDBVersionChangeEvent) => {
const db = (e.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('canvasState')) {
db.createObjectStore('canvasState');
}
};
request.onerror = (e: Event) => {
console.error('Failed to open indexDB', e);
};
request.onsuccess = (e: Event) => {
const db = (e.target as IDBOpenDBRequest).result;
try {
const tx = db.transaction('canvasState', 'readwrite');
const store = tx.objectStore('canvasState');
// 只存储 base64 字符串,而不是 ImageData 对象
store.put(
{ canvasDataUrl, currentStep: currentStep.value },
element.value.id || 'default_id'
);
tx.oncomplete = () => {
db.close();
};
} catch (err) {
console.error('Error saving canvas state:', err);
}
};
}
onUnmounted(() => {
// 当组件注销时,保存状态
saveCanvasState();
});
/**
* 恢复canvas 状态
* 当组件加载时需要从localStorage恢复状态
*/
function restoreCanvasState() {
// 没有 indexDB, 不恢复
if (!window.indexedDB) return;
if (!ctx || !signatureCanvas.value) return;
const request = window.indexedDB.open('canvasState', 1);
// 当数据库首次创建或版本升级时创建对象存储
request.onupgradeneeded = (e: IDBVersionChangeEvent) => {
const db = (e.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains('canvasState')) {
db.createObjectStore('canvasState');
}
};
request.onerror = (e: Event) => {
console.error('Failed to open indexDB', e);
};
request.onsuccess = (e: Event) => {
const db = (e.target as IDBOpenDBRequest).result;
try {
const tx = db.transaction('canvasState', 'readonly');
const store = tx.objectStore('canvasState');
const getRequest = store.get(element.value.id || 'default_id');
getRequest.onsuccess = (e: Event) => {
const savedState = (e.target as IDBRequest).result;
if (savedState && savedState.canvasDataUrl) {
// 从 base64 字符串恢复 canvas
const img = new Image();
img.onload = () => {
ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
ctx.drawImage(img, 0, 0);
// 恢复后获取当前 ImageData 并存入 undoStack
const imageData = ctx.getImageData(0, 0, canvasWidth.value, canvasHeight.value);
undoStack.value = [imageData];
currentStep.value = 0;
};
img.src = savedState.canvasDataUrl;
}
db.close();
};
getRequest.onerror = (e: Event) => {
console.error('Error retrieving canvas state:', e);
db.close();
};
} catch (err) {
console.error('Error restoring canvas state:', err);
}
};
}
onMounted(() => {
// 当组件加载时,恢复状态
restoreCanvasState();
});
/**
* 清除画布状态
*/
function clearCanvasState() {
if (window.indexedDB) {
try {
const request = window.indexedDB.open('canvasState', 1);
request.onsuccess = (e: Event) => {
const db = (e.target as IDBOpenDBRequest).result;
const tx = db.transaction('canvasState', 'readwrite');
const store = tx.objectStore('canvasState');
store.delete(element.value.id || 'default_id');
tx.oncomplete = () => {
db.close();
};
};
} catch (err) {
console.error('Error clearing canvas state:', err);
}
}
}
// 监听页面卸载事件,用于检测页面刷新
window.addEventListener('beforeunload', () => {
// 清除页面刷新标记
sessionStorage.removeItem('is_page_refresh');
// 页面刷新时清除画布状态
clearCanvasState();
});
</script>
<template>
<van-field
:label="element.stem"
:required="element.config.is_required === 1"
label-align="top"
:border="false"
readonly
>
<template #left-icon> {{ isPreview ? element.title : index + 1 }}. </template>
<template #label>
<contenteditable
v-model="element.stem"
className="contenteditable-label"
:active="active"
@blur="emitValue"
:errorMessage="errorMessage"
></contenteditable>
</template>
<template #input>
<div class="sign-question">
<canvas
ref="signatureCanvas"
:width="canvasWidth"
:height="canvasHeight"
style="border: 1px dashed #ccc; border-radius: 4px"
>
</canvas>
<div class="sign-text" :class="{ show: true }">
<span
class="icon mobilefont mobilefont-qingkong"
title="清空"
@click="clearCanvas"
></span>
<span class="icon mobilefont mobilefont-chexiao" @click="undo"></span>
<span
class="icon mobilefont"
:class="isEraser ? 'mobilefont-huabi' : 'mobilefont-rubber'"
@click="togglePen"
></span>
<span class="icon mobilefont mobilefont-shangchuan" @click="handleUploadImg"></span>
</div>
<div class="sign-tips">请在空白区域书写您的签名</div>
</div>
</template>
</van-field>
</template>
<style lang="scss" scoped>
.sign-question {
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
width: 100%;
padding: 10px 0;
// canvas {
// width: 100%;
// touch-action: none;
// }
.sign-text {
position: absolute;
bottom: 0;
left: 50%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
width: 100%;
color: #999;
font-size: 14px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
transform: translate(-50%, -50%);
&.show {
opacity: 0.8;
pointer-events: auto;
}
span {
padding: 5px 10px;
cursor: pointer;
&:hover {
color: #666;
}
}
}
.sign-tips {
position: absolute;
top: 50%;
left: 50%;
width: max-content;
color: #cdcdcd;
transform: translate(-50%, -50%);
}
}
</style>