| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- <template>
- <view
- :id="containerId"
- class="signature-container"
- :style="containerStyle"
- >
- <text v-if="placeholder" :style="placeholderStyle">{{ placeholder }}</text>
- <canvas
- class="signature-canvas"
- disable-scroll
- :id="id"
- :canvas-id="id"
- :style="canvasStyle"
- :width="canvasWidth"
- :height="canvasHeight"
- @touchstart.stop="handleTouchStart"
- @touchmove.stop="handleTouchMove"
- @touchend.stop="handleTouchEnd"
- @touchcancel.stop="handleTouchEnd"
- @mousedown.stop="handleMouseDown"
- @mousemove.stop="handleMouseMove"
- @mouseup.stop ="handleMouseUp"
- ></canvas>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount, nextTick } from 'vue';
- import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
- import { RandomUtils, waitTimeOut } from '@imengyu/imengyu-utils';
- const id = `signatureCanvas${RandomUtils.genNonDuplicateID(10)}`;
- const containerId = `signatureContainer${RandomUtils.genNonDuplicateID(10)}`;
- // 定义绘图相关类型
- interface Point { x: number; y: number; }
- interface Line { points: Point[]; color: string; width: number; }
- export interface SignatureProps {
- innerStyle?: object;
- placeholderStyle?: object;
- backgroundColor?: string;
- lineColor?: string;
- lineWidth?: number;
- round?: boolean;
- border?: boolean;
- borderStyle?: string;
- borderWidth?: number;
- borderColor?: string;
- placeholder?: string;
- }
- export interface SignatureInstance {
- /**
- * 清空签名
- */
- clear: () => void;
- /**
- * 设置签名图片
- */
- setImage: (image: string) => void;
- /**
- * 导出签名为图片
- */
- export: () => Promise<string>;
- }
- const props = withDefaults(defineProps<SignatureProps>(), {
- backgroundColor: () => propGetThemeVar('SignatureBackgroundColor', 'white'),
- lineColor: () => propGetThemeVar('SignatureLineColor', 'black'),
- lineWidth: () => propGetThemeVar('SignatureLineWidth', 3),
- round: () => propGetThemeVar('SignatureRound', true),
- border: () => propGetThemeVar('SignatureBorder', true),
- borderWidth: () => propGetThemeVar('SignatureBorderWidth', 3),
- borderStyle: () => propGetThemeVar('SignatureBorderStyle', 'dashed'),
- borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.signature'),
- placeholder: () => propGetThemeVar('SignaturePlaceholder', '请在虚线框内签名'),
- });
- const theme = useTheme();
- const instance = getCurrentInstance();
- const containerStyle = computed(() => ({
- backgroundColor: theme.resolveThemeColor(props.backgroundColor),
- borderRadius: props.round ? theme.resolveThemeSize('SignatureBorderRadius', 12) : '0',
- border: props.border ?
- `${theme.resolveThemeSize(props.borderWidth)} ${props.borderStyle} ${theme.resolveThemeColor(props.borderColor)}`
- : 'none',
- ...props.innerStyle
- }));
- const placeholderStyle = computed(() => ({
- fontSize: theme.resolveThemeSize('SignaturePlaceholderFontSize', 26),
- color: theme.resolveThemeColor('SignaturePlaceholderColor', 'text.second'),
- ...props.placeholderStyle
- }));
- const canvasStyle = computed(() => ({
- width: `${canvasWidth.value}px`,
- height: `${canvasHeight.value}px`
- }));
- // 组件状态
- const canvasContext: Ref<UniApp.CanvasContext | null> = ref(null);
- const canvasWidth = ref(0);
- const canvasHeight = ref(0);
- const lines = ref<Line[]>([]);
- const currentLine = ref<Line | null>(null);
- const containerRect = ref<UniApp.NodeInfo>();
- let timer = 0;
- let isDrawing = false;
- let absPos = [0,0];
- let isDirty = true;
- let isCreating = false;
- async function initCanvas() {
- if (isCreating)
- return;
- isCreating = true;
- await waitTimeOut(100);
- // 获取容器尺寸信息
- containerRect.value = await new Promise<UniApp.NodeInfo>((resolve) => {
- uni.createSelectorQuery()
- .in(instance)
- .select(`#${containerId}`)
- .boundingClientRect(resolve as any)
- .exec();
- });
- if (containerRect.value) {
- canvasWidth.value = containerRect.value.width! - 2;
- canvasHeight.value = containerRect.value.height! - 2;
- }
- if (canvasWidth.value > 0 && canvasHeight.value > 0) {
- if (!canvasContext.value)
- // 创建Canvas上下文
- canvasContext.value = uni.createCanvasContext(id, instance);
- }
- isCreating = false;
- }
- function render() {
- if (canvasWidth.value <= 0 || canvasHeight.value <= 0) {
- initCanvas();
- return;
- }
- if (!canvasContext.value)
- return;
- if (!isDirty)
- return;
- isDirty = false;
- drawBackground();
- lines.value.forEach(drawLine);
- if (currentLine.value)
- drawLine(currentLine.value);
- canvasContext.value.draw();
- }
- function startDrag(x: number, y: number) {
- isDrawing = true;
- // 创建新线条
- currentLine.value = {
- points: [{ x, y }],
- color: props.lineColor || 'black',
- width: props.lineWidth || 3
- };
- }
- function doDrag(x: number, y: number) {
- if (!currentLine.value)
- return;
- currentLine.value.points.push({ x, y });
- isDirty = true;
- }
- function endDrag() {
- if (!isDrawing || !currentLine.value)
- return;
- isDrawing = false;
- lines.value.push(currentLine.value);
- currentLine.value = null;
- }
- function handleTouchStart(e: any) {
- if (!canvasContext.value) return;
- const { x, y } = e.touches[0];
- startDrag(x, y);
- }
- function handleTouchMove(e: any) {
- if (!isDrawing || !currentLine.value || !canvasContext.value)
- return;
- const { x, y } = e.touches[0];
- doDrag(x, y);
- }
- function handleTouchEnd() {
- endDrag();
- }
- function handleMouseDown(e: any) {
- if (!canvasContext.value)
- return;
- //H5鼠标事件需要减去canvas位置
- uni.createSelectorQuery()
- .in(instance)
- .select(`#${id}`)
- .boundingClientRect()
- .exec((res) => {
- if (res[0]) {
- const { clientX, clientY } = e.touches[0];
- absPos = [res[0].left, res[0].top];
- startDrag(clientX - res[0].left, clientY - res[0].top);
- }
- });
- }
- function handleMouseMove(e: any) {
- if (!isDrawing || !currentLine.value || !canvasContext.value)
- return;
- const { clientX, clientY } = e.touches[0];
- doDrag(clientX - absPos[0], clientY - absPos[1]);
- }
- function handleMouseUp() {
- endDrag();
- }
- function drawBackground() {
- if (!canvasContext.value || !canvasWidth.value || !canvasHeight.value) return;
- const ctx = canvasContext.value;
- ctx.save();
- ctx.fillStyle = props.backgroundColor || 'white';
- ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
- ctx.restore();
- }
- function drawLine(line: Line) {
- if (!canvasContext.value || line.points.length < 2)
- return;
- const ctx = canvasContext.value;
- ctx.strokeStyle = line.color;
- ctx.lineWidth = line.width;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.beginPath();
- const firstPoint = line.points[0];
- ctx.moveTo(firstPoint.x, firstPoint.y);
- for (let i = 1; i < line.points.length; i++) {
- ctx.lineTo(line.points[i].x, line.points[i].y);
- }
- ctx.stroke();
- }
- function clear() {
- lines.value = [];
- currentLine.value = null;
- isDirty = true;
- }
- async function exportImage(): Promise<string> {
- if (!canvasWidth.value || !canvasHeight.value) {
- throw new Error('Canvas is not initialized');
- }
- return new Promise((resolve, reject) => {
- uni.canvasToTempFilePath({
- canvasId: id,
- width: canvasWidth.value,
- height: canvasHeight.value,
- destWidth: canvasWidth.value * 2,
- destHeight: canvasHeight.value * 2,
- quality: 1,
- success: (res) => resolve(res.tempFilePath),
- fail: (err) => reject(err)
- }, instance);
- });
- }
- function setImage(image: string) {
- if (!canvasContext.value) return;
- canvasContext.value.drawImage(image, 0, 0, canvasWidth.value, canvasHeight.value);
- isDirty = true;
- }
- onMounted(async () => {
- await nextTick();
- await initCanvas();
- timer = setInterval(render, 100) as any;
- });
- onBeforeUnmount(() => {
- clearInterval(timer);
- timer = 0;
- });
- defineExpose<SignatureInstance>({
- clear,
- export: exportImage,
- setImage
- });
- </script>
- <style lang="scss">
- .signature-container {
- width: 100%;
- height: 200px;
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .signature-canvas {
- width: 100%;
- height: 100%;
- }
- </style>
|