Signature.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <view
  3. :id="containerId"
  4. class="signature-container"
  5. :style="containerStyle"
  6. >
  7. <text v-if="placeholder" :style="placeholderStyle">{{ placeholder }}</text>
  8. <canvas
  9. class="signature-canvas"
  10. disable-scroll
  11. :id="id"
  12. :canvas-id="id"
  13. :style="canvasStyle"
  14. :width="canvasWidth"
  15. :height="canvasHeight"
  16. @touchstart.stop="handleTouchStart"
  17. @touchmove.stop="handleTouchMove"
  18. @touchend.stop="handleTouchEnd"
  19. @touchcancel.stop="handleTouchEnd"
  20. @mousedown.stop="handleMouseDown"
  21. @mousemove.stop="handleMouseMove"
  22. @mouseup.stop ="handleMouseUp"
  23. ></canvas>
  24. </view>
  25. </template>
  26. <script setup lang="ts">
  27. import { ref, onMounted, type Ref, computed, getCurrentInstance, onBeforeUnmount, nextTick } from 'vue';
  28. import { propGetThemeVar, useTheme } from '../theme/ThemeDefine';
  29. import { RandomUtils, waitTimeOut } from '@imengyu/imengyu-utils';
  30. const id = `signatureCanvas${RandomUtils.genNonDuplicateID(10)}`;
  31. const containerId = `signatureContainer${RandomUtils.genNonDuplicateID(10)}`;
  32. // 定义绘图相关类型
  33. interface Point { x: number; y: number; }
  34. interface Line { points: Point[]; color: string; width: number; }
  35. export interface SignatureProps {
  36. innerStyle?: object;
  37. placeholderStyle?: object;
  38. backgroundColor?: string;
  39. lineColor?: string;
  40. lineWidth?: number;
  41. round?: boolean;
  42. border?: boolean;
  43. borderStyle?: string;
  44. borderWidth?: number;
  45. borderColor?: string;
  46. placeholder?: string;
  47. }
  48. export interface SignatureInstance {
  49. /**
  50. * 清空签名
  51. */
  52. clear: () => void;
  53. /**
  54. * 设置签名图片
  55. */
  56. setImage: (image: string) => void;
  57. /**
  58. * 导出签名为图片
  59. */
  60. export: () => Promise<string>;
  61. }
  62. const props = withDefaults(defineProps<SignatureProps>(), {
  63. backgroundColor: () => propGetThemeVar('SignatureBackgroundColor', 'white'),
  64. lineColor: () => propGetThemeVar('SignatureLineColor', 'black'),
  65. lineWidth: () => propGetThemeVar('SignatureLineWidth', 3),
  66. round: () => propGetThemeVar('SignatureRound', true),
  67. border: () => propGetThemeVar('SignatureBorder', true),
  68. borderWidth: () => propGetThemeVar('SignatureBorderWidth', 3),
  69. borderStyle: () => propGetThemeVar('SignatureBorderStyle', 'dashed'),
  70. borderColor: () => propGetThemeVar('SignatureBorderColor', 'border.signature'),
  71. placeholder: () => propGetThemeVar('SignaturePlaceholder', '请在虚线框内签名'),
  72. });
  73. const theme = useTheme();
  74. const instance = getCurrentInstance();
  75. const containerStyle = computed(() => ({
  76. backgroundColor: theme.resolveThemeColor(props.backgroundColor),
  77. borderRadius: props.round ? theme.resolveThemeSize('SignatureBorderRadius', 12) : '0',
  78. border: props.border ?
  79. `${theme.resolveThemeSize(props.borderWidth)} ${props.borderStyle} ${theme.resolveThemeColor(props.borderColor)}`
  80. : 'none',
  81. ...props.innerStyle
  82. }));
  83. const placeholderStyle = computed(() => ({
  84. fontSize: theme.resolveThemeSize('SignaturePlaceholderFontSize', 26),
  85. color: theme.resolveThemeColor('SignaturePlaceholderColor', 'text.second'),
  86. ...props.placeholderStyle
  87. }));
  88. const canvasStyle = computed(() => ({
  89. width: `${canvasWidth.value}px`,
  90. height: `${canvasHeight.value}px`
  91. }));
  92. // 组件状态
  93. const canvasContext: Ref<UniApp.CanvasContext | null> = ref(null);
  94. const canvasWidth = ref(0);
  95. const canvasHeight = ref(0);
  96. const lines = ref<Line[]>([]);
  97. const currentLine = ref<Line | null>(null);
  98. const containerRect = ref<UniApp.NodeInfo>();
  99. let timer = 0;
  100. let isDrawing = false;
  101. let absPos = [0,0];
  102. let isDirty = true;
  103. let isCreating = false;
  104. async function initCanvas() {
  105. if (isCreating)
  106. return;
  107. isCreating = true;
  108. await waitTimeOut(100);
  109. // 获取容器尺寸信息
  110. containerRect.value = await new Promise<UniApp.NodeInfo>((resolve) => {
  111. uni.createSelectorQuery()
  112. .in(instance)
  113. .select(`#${containerId}`)
  114. .boundingClientRect(resolve as any)
  115. .exec();
  116. });
  117. if (containerRect.value) {
  118. canvasWidth.value = containerRect.value.width! - 2;
  119. canvasHeight.value = containerRect.value.height! - 2;
  120. }
  121. if (canvasWidth.value > 0 && canvasHeight.value > 0) {
  122. if (!canvasContext.value)
  123. // 创建Canvas上下文
  124. canvasContext.value = uni.createCanvasContext(id, instance);
  125. }
  126. isCreating = false;
  127. }
  128. function render() {
  129. if (canvasWidth.value <= 0 || canvasHeight.value <= 0) {
  130. initCanvas();
  131. return;
  132. }
  133. if (!canvasContext.value)
  134. return;
  135. if (!isDirty)
  136. return;
  137. isDirty = false;
  138. drawBackground();
  139. lines.value.forEach(drawLine);
  140. if (currentLine.value)
  141. drawLine(currentLine.value);
  142. canvasContext.value.draw();
  143. }
  144. function startDrag(x: number, y: number) {
  145. isDrawing = true;
  146. // 创建新线条
  147. currentLine.value = {
  148. points: [{ x, y }],
  149. color: props.lineColor || 'black',
  150. width: props.lineWidth || 3
  151. };
  152. }
  153. function doDrag(x: number, y: number) {
  154. if (!currentLine.value)
  155. return;
  156. currentLine.value.points.push({ x, y });
  157. isDirty = true;
  158. }
  159. function endDrag() {
  160. if (!isDrawing || !currentLine.value)
  161. return;
  162. isDrawing = false;
  163. lines.value.push(currentLine.value);
  164. currentLine.value = null;
  165. }
  166. function handleTouchStart(e: any) {
  167. if (!canvasContext.value) return;
  168. const { x, y } = e.touches[0];
  169. startDrag(x, y);
  170. }
  171. function handleTouchMove(e: any) {
  172. if (!isDrawing || !currentLine.value || !canvasContext.value)
  173. return;
  174. const { x, y } = e.touches[0];
  175. doDrag(x, y);
  176. }
  177. function handleTouchEnd() {
  178. endDrag();
  179. }
  180. function handleMouseDown(e: any) {
  181. if (!canvasContext.value)
  182. return;
  183. //H5鼠标事件需要减去canvas位置
  184. uni.createSelectorQuery()
  185. .in(instance)
  186. .select(`#${id}`)
  187. .boundingClientRect()
  188. .exec((res) => {
  189. if (res[0]) {
  190. const { clientX, clientY } = e.touches[0];
  191. absPos = [res[0].left, res[0].top];
  192. startDrag(clientX - res[0].left, clientY - res[0].top);
  193. }
  194. });
  195. }
  196. function handleMouseMove(e: any) {
  197. if (!isDrawing || !currentLine.value || !canvasContext.value)
  198. return;
  199. const { clientX, clientY } = e.touches[0];
  200. doDrag(clientX - absPos[0], clientY - absPos[1]);
  201. }
  202. function handleMouseUp() {
  203. endDrag();
  204. }
  205. function drawBackground() {
  206. if (!canvasContext.value || !canvasWidth.value || !canvasHeight.value) return;
  207. const ctx = canvasContext.value;
  208. ctx.save();
  209. ctx.fillStyle = props.backgroundColor || 'white';
  210. ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
  211. ctx.restore();
  212. }
  213. function drawLine(line: Line) {
  214. if (!canvasContext.value || line.points.length < 2)
  215. return;
  216. const ctx = canvasContext.value;
  217. ctx.strokeStyle = line.color;
  218. ctx.lineWidth = line.width;
  219. ctx.lineCap = 'round';
  220. ctx.lineJoin = 'round';
  221. ctx.beginPath();
  222. const firstPoint = line.points[0];
  223. ctx.moveTo(firstPoint.x, firstPoint.y);
  224. for (let i = 1; i < line.points.length; i++) {
  225. ctx.lineTo(line.points[i].x, line.points[i].y);
  226. }
  227. ctx.stroke();
  228. }
  229. function clear() {
  230. lines.value = [];
  231. currentLine.value = null;
  232. isDirty = true;
  233. }
  234. async function exportImage(): Promise<string> {
  235. if (!canvasWidth.value || !canvasHeight.value) {
  236. throw new Error('Canvas is not initialized');
  237. }
  238. return new Promise((resolve, reject) => {
  239. uni.canvasToTempFilePath({
  240. canvasId: id,
  241. width: canvasWidth.value,
  242. height: canvasHeight.value,
  243. destWidth: canvasWidth.value * 2,
  244. destHeight: canvasHeight.value * 2,
  245. quality: 1,
  246. success: (res) => resolve(res.tempFilePath),
  247. fail: (err) => reject(err)
  248. }, instance);
  249. });
  250. }
  251. function setImage(image: string) {
  252. if (!canvasContext.value) return;
  253. canvasContext.value.drawImage(image, 0, 0, canvasWidth.value, canvasHeight.value);
  254. isDirty = true;
  255. }
  256. onMounted(async () => {
  257. await nextTick();
  258. await initCanvas();
  259. timer = setInterval(render, 100) as any;
  260. });
  261. onBeforeUnmount(() => {
  262. clearInterval(timer);
  263. timer = 0;
  264. });
  265. defineExpose<SignatureInstance>({
  266. clear,
  267. export: exportImage,
  268. setImage
  269. });
  270. </script>
  271. <style lang="scss">
  272. .signature-container {
  273. width: 100%;
  274. height: 200px;
  275. position: relative;
  276. display: flex;
  277. flex-direction: column;
  278. align-items: center;
  279. }
  280. .signature-canvas {
  281. width: 100%;
  282. height: 100%;
  283. }
  284. </style>