25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

250 lines
5.2 KiB

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