Signature.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <template>
  2. <view
  3. class="signature-container"
  4. :style="containerStyle"
  5. ref="containerRef"
  6. >
  7. <canvas
  8. class="signature-canvas"
  9. disable-scroll
  10. :canvas-id="id"
  11. :style="canvasStyle"
  12. @touchstart="handleTouchStart"
  13. @touchmove="handleTouchMove"
  14. @touchend="handleTouchEnd"
  15. @touchcancel="handleTouchEnd"
  16. ></canvas>
  17. </view>
  18. </template>
  19. <script setup lang="ts">
  20. import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount } from 'vue';
  21. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  22. import { RandomUtils } from '@imengyu/imengyu-utils';
  23. const id = `signatureCanvas${RandomUtils.genNonDuplicateID(10)}`;
  24. // 定义绘图相关类型
  25. interface Point { x: number; y: number; }
  26. interface Line { points: Point[]; color: string; width: number; }
  27. export interface SignatureProps {
  28. backgroundColor?: string;
  29. lineColor?: string;
  30. lineWidth?: number;
  31. round?: boolean;
  32. border?: boolean;
  33. }
  34. export interface SignatureInstance {
  35. /**
  36. * 清空签名
  37. */
  38. clear: () => void;
  39. /**
  40. * 导出签名为图片
  41. */
  42. export: () => Promise<string>;
  43. }
  44. const props = withDefaults(defineProps<SignatureProps>(), {
  45. backgroundColor: () => propGetThemeVar('SignatureBackgroundColor', 'white'),
  46. lineColor: () => propGetThemeVar('SignatureLineColor', 'black'),
  47. lineWidth: () => propGetThemeVar('SignatureLineWidth', 3),
  48. round: () => propGetThemeVar('SignatureRound', true),
  49. border: () => propGetThemeVar('SignatureBorder', true),
  50. borderWidth: () => propGetThemeVar('SignatureBorderWidth', 2),
  51. borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.cell'),
  52. });
  53. const theme = useTheme();
  54. const instance = getCurrentInstance();
  55. const containerStyle = computed(() => ({
  56. backgroundColor: theme.resolveThemeColor(props.backgroundColor),
  57. borderRadius: props.round ? theme.resolveThemeSize('SignatureBorderRadius', 12) : '0',
  58. border: props.border ?
  59. `${theme.resolveThemeSize(props.borderWidth)}px solid ${theme.resolveThemeColor(props.borderColor)}`
  60. : 'none'
  61. }));
  62. const canvasStyle = computed(() => ({
  63. width: `${canvasWidth.value}px`,
  64. height: `${canvasHeight.value}px`
  65. }));
  66. // 组件状态
  67. const canvasContext: Ref<UniApp.CanvasContext | null> = ref(null);
  68. const canvasWidth = ref(0);
  69. const canvasHeight = ref(0);
  70. const lines = ref<Line[]>([]);
  71. const currentLine = ref<Line | null>(null);
  72. const containerRect = ref<UniApp.NodeInfo>();
  73. let timer = 0;
  74. let isDrawing = false;
  75. let isDirty = true;
  76. async function initCanvas() {
  77. // 获取容器尺寸信息
  78. containerRect.value = await new Promise<UniApp.NodeInfo>((resolve) => {
  79. uni.createSelectorQuery()
  80. .in(instance)
  81. .select('.signature-container')
  82. .boundingClientRect(resolve as any)
  83. .exec();
  84. });
  85. if (containerRect.value) {
  86. canvasWidth.value = containerRect.value.width!;
  87. canvasHeight.value = containerRect.value.height!;
  88. }
  89. // 创建Canvas上下文
  90. canvasContext.value = uni.createCanvasContext(id, instance);
  91. timer = setInterval(render, 50) as any;
  92. }
  93. function render() {
  94. if (!canvasContext.value)
  95. return;
  96. if (!isDirty)
  97. return;
  98. isDirty = false;
  99. drawBackground();
  100. lines.value.forEach(drawLine);
  101. if (currentLine.value)
  102. drawLine(currentLine.value);
  103. canvasContext.value.draw();
  104. }
  105. function handleTouchStart(e: any) {
  106. if (!canvasContext.value) return;
  107. isDrawing = true;
  108. const { x, y } = e.touches[0];
  109. // 创建新线条
  110. currentLine.value = {
  111. points: [{ x, y }],
  112. color: props.lineColor || 'black',
  113. width: props.lineWidth || 3
  114. };
  115. }
  116. function handleTouchMove(e: any) {
  117. if (!isDrawing || !currentLine.value || !canvasContext.value)
  118. return;
  119. const { x, y } = e.touches[0];
  120. currentLine.value.points.push({ x, y });
  121. isDirty = true;
  122. }
  123. function handleTouchEnd() {
  124. if (!isDrawing || !currentLine.value)
  125. return;
  126. isDrawing = false;
  127. lines.value.push(currentLine.value);
  128. currentLine.value = null;
  129. }
  130. function drawBackground() {
  131. if (!canvasContext.value || !canvasWidth.value || !canvasHeight.value) return;
  132. const ctx = canvasContext.value;
  133. ctx.save();
  134. ctx.fillStyle = props.backgroundColor || 'white';
  135. ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
  136. ctx.restore();
  137. }
  138. function drawLine(line: Line) {
  139. if (!canvasContext.value || line.points.length < 2)
  140. return;
  141. const ctx = canvasContext.value;
  142. ctx.strokeStyle = line.color;
  143. ctx.lineWidth = line.width;
  144. ctx.lineCap = 'round';
  145. ctx.lineJoin = 'round';
  146. ctx.beginPath();
  147. const firstPoint = line.points[0];
  148. ctx.moveTo(firstPoint.x, firstPoint.y);
  149. for (let i = 1; i < line.points.length; i++) {
  150. ctx.lineTo(line.points[i].x, line.points[i].y);
  151. }
  152. ctx.stroke();
  153. }
  154. function clear() {
  155. lines.value = [];
  156. currentLine.value = null;
  157. isDirty = true;
  158. }
  159. async function exportImage(): Promise<string> {
  160. if (!canvasWidth.value || !canvasHeight.value) {
  161. throw new Error('Canvas is not initialized');
  162. }
  163. return new Promise((resolve, reject) => {
  164. uni.canvasToTempFilePath({
  165. canvasId: id,
  166. width: canvasWidth.value,
  167. height: canvasHeight.value,
  168. destWidth: canvasWidth.value * 2,
  169. destHeight: canvasHeight.value * 2,
  170. quality: 1,
  171. success: (res) => resolve(res.tempFilePath),
  172. fail: (err) => reject(err)
  173. }, instance);
  174. });
  175. }
  176. onMounted(initCanvas);
  177. onBeforeUnmount(() => {
  178. clearInterval(timer);
  179. });
  180. defineExpose<SignatureInstance>({
  181. clear,
  182. export: exportImage
  183. });
  184. </script>
  185. <style lang="scss">
  186. .signature-container {
  187. width: 100%;
  188. height: 200px;
  189. position: relative;
  190. }
  191. .signature-canvas {
  192. width: 100%;
  193. height: 100%;
  194. }
  195. </style>