Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

240 řádky
5.0 KiB

  1. <template>
  2. <view class="sign-page">
  3. <view class="canvas-wrap">
  4. <view class="hint-text">
  5. <text>请在此处手写签名</text>
  6. </view>
  7. <canvas id="signCanvas" canvas-id="signCanvas" type="2d" class="sign-canvas" disable-scroll
  8. @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" />
  9. </view>
  10. <view class="bottom-btns">
  11. <view class="btn-clear" @click="handleClear">
  12. <text>清除</text>
  13. </view>
  14. <view class="btn-save" @click="handleSave">
  15. <text>保存</text>
  16. </view>
  17. </view>
  18. </view>
  19. </template>
  20. <script setup>
  21. import { ref, nextTick } from 'vue'
  22. import { onLoad } from '@dcloudio/uni-app'
  23. import { upload } from '@/utils/request.js'
  24. let ctx = null
  25. let canvasEl = null
  26. let canvasWidth = 0
  27. let canvasHeight = 0
  28. const hasDrawn = ref(false)
  29. const saving = ref(false)
  30. let lastX = 0
  31. let lastY = 0
  32. onLoad(async () => {
  33. await nextTick()
  34. setTimeout(() => {
  35. const query = uni.createSelectorQuery()
  36. query.select('#signCanvas').fields({ node: true, size: true }).exec((res) => {
  37. if (!res[0]) return
  38. canvasEl = res[0].node
  39. const dpr = uni.getSystemInfoSync().pixelRatio
  40. canvasWidth = res[0].width
  41. canvasHeight = res[0].height
  42. canvasEl.width = canvasWidth * dpr
  43. canvasEl.height = canvasHeight * dpr
  44. ctx = canvasEl.getContext('2d')
  45. ctx.scale(dpr, dpr)
  46. ctx.lineCap = 'round'
  47. ctx.lineJoin = 'round'
  48. ctx.lineWidth = 4
  49. ctx.strokeStyle = '#333'
  50. })
  51. }, 300)
  52. })
  53. const getPos = (e) => {
  54. const touch = e.touches[0]
  55. return { x: touch.x, y: touch.y }
  56. }
  57. const onTouchStart = (e) => {
  58. if (!ctx) return
  59. hasDrawn.value = true
  60. const { x, y } = getPos(e)
  61. lastX = x
  62. lastY = y
  63. ctx.beginPath()
  64. ctx.moveTo(x, y)
  65. }
  66. const onTouchMove = (e) => {
  67. if (!ctx) return
  68. const { x, y } = getPos(e)
  69. ctx.beginPath()
  70. ctx.moveTo(lastX, lastY)
  71. ctx.lineTo(x, y)
  72. ctx.stroke()
  73. lastX = x
  74. lastY = y
  75. }
  76. const onTouchEnd = () => {}
  77. const handleClear = () => {
  78. if (!ctx || !canvasEl) return
  79. ctx.clearRect(0, 0, canvasWidth, canvasHeight)
  80. hasDrawn.value = false
  81. }
  82. const handleSave = () => {
  83. if (!hasDrawn.value) {
  84. uni.showToast({ title: '请先签名', icon: 'none' })
  85. return
  86. }
  87. if (saving.value) return
  88. saving.value = true
  89. uni.showLoading({ title: '保存中...' })
  90. uni.canvasToTempFilePath({
  91. canvas: canvasEl,
  92. fileType: 'png',
  93. quality: 1,
  94. success: async (res) => {
  95. try {
  96. const dpr = uni.getSystemInfoSync().pixelRatio
  97. // 创建离屏 canvas 旋转签名(横屏→竖屏,逆时针90度)
  98. const offscreen = uni.createOffscreenCanvas({
  99. type: '2d',
  100. width: canvasHeight * dpr,
  101. height: canvasWidth * dpr
  102. })
  103. const offCtx = offscreen.getContext('2d')
  104. const img = offscreen.createImage()
  105. await new Promise((resolve, reject) => {
  106. img.onload = resolve
  107. img.onerror = reject
  108. img.src = res.tempFilePath
  109. })
  110. offCtx.translate(0, canvasWidth * dpr)
  111. offCtx.rotate(-Math.PI / 2)
  112. offCtx.drawImage(img, 0, 0)
  113. const rotatedRes = await new Promise((resolve, reject) => {
  114. uni.canvasToTempFilePath({
  115. canvas: offscreen,
  116. fileType: 'png',
  117. quality: 1,
  118. success: resolve,
  119. fail: reject
  120. })
  121. })
  122. // 上传签名图
  123. const uploadRes = await upload('/api/mp/upload', {
  124. filePath: rotatedRes.tempFilePath,
  125. name: 'file'
  126. })
  127. if (!uploadRes.data || !uploadRes.data.url) {
  128. throw { msg: '上传失败' }
  129. }
  130. uni.hideLoading()
  131. // 把签名图 URL 传回 sign 页面
  132. uni.$emit('signatureResult', { url: uploadRes.data.url })
  133. uni.navigateBack()
  134. } catch (err) {
  135. uni.hideLoading()
  136. uni.showToast({ title: err.msg || '上传失败,请重试', icon: 'none' })
  137. } finally {
  138. saving.value = false
  139. }
  140. },
  141. fail: () => {
  142. uni.hideLoading()
  143. uni.showToast({ title: '保存失败', icon: 'none' })
  144. saving.value = false
  145. }
  146. })
  147. }
  148. </script>
  149. <style lang="scss" scoped>
  150. .sign-page {
  151. display: flex;
  152. flex-direction: column;
  153. height: 100vh;
  154. background: #edf2fc;
  155. }
  156. .canvas-wrap {
  157. flex: 1;
  158. position: relative;
  159. overflow: hidden;
  160. .hint-text {
  161. position: absolute;
  162. top: 50%;
  163. left: 50%;
  164. transform: translate(-50%, -50%) rotate(90deg);
  165. pointer-events: none;
  166. white-space: nowrap;
  167. z-index: 0;
  168. text {
  169. font-size: 48rpx;
  170. color: #c0c8d8;
  171. font-weight: 300;
  172. letter-spacing: 16rpx;
  173. }
  174. }
  175. .sign-canvas {
  176. width: 100%;
  177. height: 100%;
  178. position: relative;
  179. z-index: 1;
  180. }
  181. }
  182. .bottom-btns {
  183. display: flex;
  184. height: calc(100rpx + env(safe-area-inset-bottom));
  185. flex-shrink: 0;
  186. padding-bottom: env(safe-area-inset-bottom);
  187. .btn-clear {
  188. flex: 1;
  189. display: flex;
  190. align-items: center;
  191. justify-content: center;
  192. background: #fff;
  193. text {
  194. writing-mode: vertical-rl;
  195. transform: rotate(90deg);
  196. font-size: 32rpx;
  197. color: #333;
  198. letter-spacing: 4rpx;
  199. }
  200. }
  201. .btn-save {
  202. flex: 2;
  203. display: flex;
  204. align-items: center;
  205. justify-content: center;
  206. background: #0e63e3;
  207. text {
  208. writing-mode: vertical-rl;
  209. transform: rotate(90deg);
  210. font-size: 32rpx;
  211. color: #fff;
  212. letter-spacing: 4rpx;
  213. }
  214. }
  215. }
  216. </style>