You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

280 rivejä
6.1 KiB

  1. <template>
  2. <scroll-view class="page" scroll-y :scroll-with-animation="false" :enable-flex="true"
  3. :scroll-enabled="!isSigning">
  4. <!-- 协议内容 -->
  5. <view class="section">
  6. <view class="doc-title">{{ docTitle }}</view>
  7. <rich-text class="doc-body" :nodes="docContent" />
  8. </view>
  9. <!-- 收入金额(仅income类型) -->
  10. <view class="section" v-if="signType === 'income'">
  11. <view class="form-label">请填写您的个人月可支配收入(元)</view>
  12. <view class="amount-wrap">
  13. <text class="amount-prefix">¥</text>
  14. <u-input v-model="incomeAmount" type="number" placeholder="请输入金额" border="none" />
  15. </view>
  16. </view>
  17. <!-- 签名区域 -->
  18. <view class="section">
  19. <view class="sign-label">请在下方签名确认</view>
  20. <view class="canvas-wrap">
  21. <canvas canvas-id="signCanvas" class="sign-canvas"
  22. @touchstart="onTouchStart" @touchmove.stop.prevent="onTouchMove" @touchend="onTouchEnd" />
  23. <text v-if="!hasSigned" class="canvas-placeholder">请在此处签名</text>
  24. </view>
  25. <view class="sign-actions">
  26. <view class="clear-btn" @tap="clearSign">清除重签</view>
  27. </view>
  28. </view>
  29. <!-- 底部按钮 -->
  30. <view class="btn-wrap">
  31. <u-button text="确认签署" :loading="submitting" @click="confirmSign" color="#0E63E3" size="large" />
  32. </view>
  33. </scroll-view>
  34. </template>
  35. <script setup>
  36. import { ref } from 'vue'
  37. import { onLoad, onReady } from '@dcloudio/uni-app'
  38. import { get, post, upload } from '@/utils/request.js'
  39. const signType = ref('')
  40. const docTitle = ref('')
  41. const docContent = ref('')
  42. const incomeAmount = ref('')
  43. const hasSigned = ref(false)
  44. const submitting = ref(false)
  45. const isSigning = ref(false)
  46. let ctx = null
  47. let points = []
  48. const titleMap = {
  49. income: '个人可支配收入声明',
  50. privacy: '个人信息处理同意书',
  51. promise: '声明与承诺'
  52. }
  53. onLoad((options) => {
  54. signType.value = options.type || 'privacy'
  55. // uni.setNavigationBarTitle({ title: titleMap[signType.value] || '签署协议' })
  56. loadContent()
  57. })
  58. onReady(() => {
  59. ctx = uni.createCanvasContext('signCanvas')
  60. ctx.setStrokeStyle('#333')
  61. ctx.setLineWidth(3)
  62. ctx.setLineCap('round')
  63. ctx.setLineJoin('round')
  64. })
  65. const loadContent = async () => {
  66. try {
  67. const key = 'sign_' + signType.value
  68. const res = await get('/api/content', { key })
  69. docTitle.value = res.data.title || ''
  70. docContent.value = res.data.content || ''
  71. } catch (e) {}
  72. }
  73. const onTouchStart = (e) => {
  74. isSigning.value = true
  75. const touch = e.touches[0]
  76. points = [{ x: touch.x, y: touch.y }]
  77. ctx.beginPath()
  78. ctx.moveTo(touch.x, touch.y)
  79. }
  80. const onTouchMove = (e) => {
  81. const touch = e.touches[0]
  82. points.push({ x: touch.x, y: touch.y })
  83. ctx.lineTo(touch.x, touch.y)
  84. ctx.stroke()
  85. ctx.draw(true)
  86. ctx.beginPath()
  87. ctx.moveTo(touch.x, touch.y)
  88. if (!hasSigned.value) hasSigned.value = true
  89. }
  90. const onTouchEnd = () => {
  91. points = []
  92. isSigning.value = false
  93. }
  94. const clearSign = () => {
  95. ctx.clearRect(0, 0, 9999, 9999)
  96. ctx.draw()
  97. hasSigned.value = false
  98. }
  99. const confirmSign = async () => {
  100. if (!hasSigned.value) {
  101. return uni.showToast({ title: '请先签名', icon: 'none' })
  102. }
  103. if (signType.value === 'income' && (!incomeAmount.value || Number(incomeAmount.value) <= 0)) {
  104. return uni.showToast({ title: '请填写有效的收入金额', icon: 'none' })
  105. }
  106. submitting.value = true
  107. try {
  108. // 1. 导出 canvas 为图片
  109. const tempPath = await new Promise((resolve, reject) => {
  110. uni.canvasToTempFilePath({
  111. canvasId: 'signCanvas',
  112. fileType: 'png',
  113. success: (res) => resolve(res.tempFilePath),
  114. fail: reject
  115. })
  116. })
  117. // 2. 上传签名图到 COS
  118. const uploadRes = await upload('/api/mp/upload', { filePath: tempPath, name: 'file' })
  119. if (!uploadRes.data || !uploadRes.data.url) {
  120. throw { msg: '签名图上传失败' }
  121. }
  122. const signImage = uploadRes.data.url
  123. // 3. 调后端合成接口
  124. const params = {
  125. type: signType.value,
  126. signImage,
  127. amount: signType.value === 'income' ? incomeAmount.value : undefined
  128. }
  129. const res = await post('/api/mp/sign', params)
  130. // 4. 通过事件把结果传回 myinfo 页面
  131. uni.$emit('signResult', {
  132. type: signType.value,
  133. url: res.data.url,
  134. amount: signType.value === 'income' ? incomeAmount.value : undefined
  135. })
  136. uni.showToast({ title: '签署成功', icon: 'success' })
  137. setTimeout(() => uni.navigateBack(), 1000)
  138. } catch (e) {
  139. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  140. } finally {
  141. submitting.value = false
  142. }
  143. }
  144. </script>
  145. <style lang="scss" scoped>
  146. .page {
  147. height: 100vh;
  148. background: #f4f4f5;
  149. padding: 24rpx;
  150. padding-bottom: 140rpx;
  151. }
  152. .section {
  153. background: #fff;
  154. margin-bottom: 24rpx;
  155. border-radius: 10rpx;
  156. padding: 32rpx;
  157. border: 1rpx solid #ebeef5;
  158. }
  159. .doc-title {
  160. font-size: 32rpx;
  161. font-weight: 600;
  162. text-align: center;
  163. margin-bottom: 32rpx;
  164. color: #222;
  165. }
  166. .doc-body {
  167. font-size: 28rpx;
  168. color: #555;
  169. line-height: 1.8;
  170. }
  171. .form-label {
  172. font-size: 28rpx;
  173. color: #333;
  174. font-weight: 600;
  175. margin-bottom: 16rpx;
  176. }
  177. .amount-wrap {
  178. display: flex;
  179. align-items: center;
  180. border: 1rpx solid #ddd;
  181. border-radius: 12rpx;
  182. overflow: hidden;
  183. gap: 12rpx;
  184. }
  185. .amount-prefix {
  186. padding: 20rpx 24rpx;
  187. font-size: 28rpx;
  188. color: #999;
  189. background: #f8f8f8;
  190. }
  191. .sign-label {
  192. font-size: 28rpx;
  193. color: #333;
  194. font-weight: 600;
  195. margin-bottom: 16rpx;
  196. }
  197. .canvas-wrap {
  198. position: relative;
  199. border: 1rpx solid #ddd;
  200. border-radius: 12rpx;
  201. overflow: hidden;
  202. margin-bottom: 16rpx;
  203. }
  204. .sign-canvas {
  205. width: 100%;
  206. height: 300rpx;
  207. background: #fff;
  208. }
  209. .canvas-placeholder {
  210. position: absolute;
  211. top: 50%;
  212. left: 50%;
  213. transform: translate(-50%, -50%);
  214. color: #ccc;
  215. font-size: 32rpx;
  216. pointer-events: none;
  217. }
  218. .sign-actions {
  219. display: flex;
  220. justify-content: flex-end;
  221. }
  222. .clear-btn {
  223. padding: 12rpx 32rpx;
  224. border: 1rpx solid #ccc;
  225. border-radius: 12rpx;
  226. font-size: 26rpx;
  227. color: #666;
  228. &:active {
  229. border-color: #0e63e3;
  230. color: #0e63e3;
  231. }
  232. }
  233. .btn-wrap {
  234. position: fixed;
  235. bottom: 0;
  236. left: 0;
  237. right: 0;
  238. background: #fff;
  239. padding: 20rpx 32rpx;
  240. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  241. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
  242. z-index: 100;
  243. }
  244. </style>