shish 1 dzień temu
rodzic
commit
d34bae801b
1 zmienionych plików z 320 dodań i 6 usunięć
  1. 320 6
      ghsApp/src/admin/billing/result.vue

+ 320 - 6
ghsApp/src/admin/billing/result.vue

@@ -1,5 +1,9 @@
+<!--
+  开单/报损成功页
+  用途:开单成功后的操作入口(分享、打印、回首页);挂账开单额外生成带小程序码的凭证海报,方便商家转发给客户核对欠款。
+  谁用:affirm / toPay 挂账或线下/余额提交后跳转。
+-->
 <template>
-
 <appResult :title="option.title">
   <view v-if="detailInfo.hasNoticeCustom && detailInfo.hasNoticeCustom == 1" style="margin-bottom:30upx;font-weight:bold;color:#049E2C;font-size:36upx;">【客户已收到新订单提醒】</view>
   <!-- #ifdef MP-WEIXIN -->
@@ -13,13 +17,39 @@
   </button>
   <!-- #endif -->
   <button v-if="option.title != '报损成功'" class="admin-button-com big" style="background-color: green;border-color: green;color:white;" @click="printOrder()">无价打印</button>
-  	<button v-if="option.title != '报损成功'" class="admin-button-com big" @click="gotoOrderDetail()">订单详情</button>
+  <button v-if="option.title != '报损成功'" class="admin-button-com big" @click="gotoOrderDetail()">订单详情</button>
 	<button class="admin-button-com big" @click="goBackHome">返回首页</button>
 
+  <!-- 仅挂账:凭证海报(含小程序码),客户长按可查看余额/待结 -->
+  <view v-if="showDebtPoster" class="debt-poster-block">
+    <view v-if="posterLoading" class="status-text">凭证生成中...</view>
+    <view v-else-if="posterFail" class="fail-box">
+      <text class="status-text fail-text">凭证生成失败,请重试</text>
+      <button class="admin-button-com big blue action-btn" @click="generatePoster">重新生成凭证</button>
+    </view>
+    <view v-else-if="posterUrl" class="poster-preview">
+      <image
+        class="poster-img"
+        :src="posterUrl"
+        mode="widthFix"
+        show-menu-by-longpress
+      />
+      <text class="poster-tip">长按图片可转发给客户核对挂账</text>
+    </view>
+    <button
+      v-if="posterUrl"
+      class="admin-button-com big blue action-btn"
+      @click="saveToAlbum"
+    >保存到相册</button>
+  </view>
+
+  <!-- 无挂账海报时才出广告,避免与海报抢版面 -->
   <!-- #ifdef MP-WEIXIN -->
-  <view style="width:688upx;height:20upx;background-color: #ece5e5;"></view>
+  <block v-if="!showDebtPoster">
+    <view style="width:688upx;height:20upx;background-color: #ece5e5;"></view>
     <ad-custom unit-id="adunit-42a99bd9e25e3626" style="width:688upx;"></ad-custom>
-  <view style="width:688upx;height:20upx;background-color: #ece5e5;"></view>
+    <view style="width:688upx;height:20upx;background-color: #ece5e5;"></view>
+  </block>
   <!-- #endif -->
 
   <modal-module :show="givePopShow" @click="confirmGive" :maskClosable="true" title="数量" padding="30rpx 30rpx" >
@@ -34,14 +64,23 @@
   </template>
   </modal-module>
 
+  <!-- 离屏 canvas:绘制挂账凭证海报 -->
+  <canvas
+    canvas-id="debtPosterCanvas"
+    class="poster-canvas"
+    :style="{ width: canvasW + 'px', height: canvasH + 'px' }"
+  />
 </appResult>
 
 </template>
 <script>
+import dayjs from 'dayjs'
 import { mapGetters } from "vuex"
 import appResult from "@/components/app-result"
 import { getDetail,cloudPrintOrder} from "@/api/order"
 import { getWeixinId } from "@/api/invite"
+import { getGatheringMiniCode } from '@/api/custom'
+import { getUserDet } from '@/api/member'
 import { IMGHOST } from '@/config'
 import { partItem } from "@/api/part"
 import { changeHalf } from "@/api/item"
@@ -65,7 +104,14 @@ export default {
 			giveTargetId:0,
 			givePopShow:false,
 			giveNum:1,
-      currentEnv:'production'
+      currentEnv:'production',
+      // 挂账凭证海报
+      posterUrl: '',
+      posterLoading: false,
+      posterFail: false,
+      customBalance: 0,
+      canvasW: 360,
+      canvasH: 640
     };
   },
   onLoad() {
@@ -74,6 +120,20 @@ export default {
   },
 	computed: {
 		...mapGetters({ loginInfo: "getLoginInfo",dictInfo:"getDictionariesInfo" }),
+    // 仅挂账开单展示凭证海报(payWay=3 或 debt=1)
+    showDebtPoster() {
+      if (this.option.title === '报损成功') return false
+      const payWay = Number(this.detailInfo.payWay)
+      const debt = Number(this.detailInfo.debt)
+      return payWay === 3 || debt === 1
+    },
+    posterAmount() {
+      const n = parseFloat(this.detailInfo.actPrice)
+      return isNaN(n) ? 0 : n
+    },
+    posterOrderSn() {
+      return this.detailInfo.orderSn || this.option.orderSn || ''
+    }
 	},
 	onShareAppMessage(res) {
 		const purchaseId = this.detailInfo.purchaseId || 0
@@ -88,6 +148,12 @@ export default {
 		}
 	},
   methods: {
+    /** 金额展示:最多两位小数 */
+    formatMoney(val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) return '0'
+      return String(parseFloat(n.toFixed(2)))
+    },
     giveItemFn(item){
       this.givePopShow = true
       this.giveNum = 1
@@ -174,7 +240,206 @@ export default {
         }
         // 分享标题日期用发货日,与订单列表 buildOrderShareTitle 一致
         this.title = buildOrderShareTitle(this.detailInfo)
+        // 挂账开单:生成带小程序码的凭证海报,供转发给客户
+        if (this.showDebtPoster && this.detailInfo.customId) {
+          this.generatePoster()
+        }
+      }
+    },
+    /**
+     * 拉取小程序码与客户最新余额,绘制挂账凭证海报
+     */
+    generatePoster() {
+      const that = this
+      const customId = that.detailInfo.customId
+      if (!customId) {
+        that.posterFail = true
+        return
+      }
+      that.posterLoading = true
+      that.posterFail = false
+      that.posterUrl = ''
+
+      Promise.all([
+        getGatheringMiniCode({ id: customId }),
+        getUserDet({ id: customId })
+      ])
+        .then(([codeRes, userRes]) => {
+          if (!codeRes || codeRes.code != 1) {
+            throw new Error((codeRes && codeRes.msg) || '获取小程序码失败')
+          }
+          if (!userRes || userRes.code != 1) {
+            throw new Error((userRes && userRes.msg) || '获取客户信息失败')
+          }
+          if (userRes.data && userRes.data.balance !== undefined && userRes.data.balance !== null) {
+            that.customBalance = parseFloat(userRes.data.balance) || 0
+          }
+          const imgUrl = (codeRes.data && (codeRes.data.imgUrl || codeRes.data.url)) || ''
+          if (!imgUrl) {
+            throw new Error('未获取到小程序码链接')
+          }
+          return that.downloadImage(imgUrl)
+        })
+        .then((qrPath) => {
+          return that.drawPoster(qrPath)
+        })
+        .catch(() => {
+          that.posterLoading = false
+          that.posterFail = true
+        })
+    },
+    downloadImage(url) {
+      return new Promise((resolve, reject) => {
+        uni.downloadFile({
+          url,
+          success(res) {
+            if (res.statusCode === 200 && res.tempFilePath) {
+              resolve(res.tempFilePath)
+            } else {
+              reject(new Error('download fail'))
+            }
+          },
+          fail() {
+            reject(new Error('download fail'))
+          }
+        })
+      })
+    },
+    // 绘制挂账凭证:金额、待结余额、单号、日期 + 小程序码
+    drawPoster(qrPath) {
+      const that = this
+      const ctx = uni.createCanvasContext('debtPosterCanvas', this)
+      const w = that.canvasW
+      const h = that.canvasH
+      const headerH = 126
+      const padX = 24
+
+      ctx.setFillStyle('#ffffff')
+      ctx.fillRect(0, 0, w, h)
+      ctx.setFillStyle('#3385ff')
+      ctx.fillRect(0, 0, w, headerH)
+
+      ctx.setFillStyle('#ffffff')
+      ctx.setTextAlign('center')
+      ctx.setFontSize(26)
+      ctx.fillText('挂账凭证', w / 2, 46)
+      ctx.setFontSize(22)
+      const nameText = that.customName || '客户'
+      ctx.fillText(nameText.length > 14 ? nameText.substr(0, 14) + '…' : nameText, w / 2, 86)
+
+      ctx.setFillStyle('#e02020')
+      ctx.setFontSize(44)
+      ctx.fillText('¥' + that.formatMoney(that.posterAmount), w / 2, 180)
+
+      ctx.setFillStyle('#333333')
+      ctx.setFontSize(22)
+      ctx.fillText('本单已挂账', w / 2, 208)
+
+      let rowY = 252
+      const labelX = padX
+      const valueX = w - padX
+      const rowGap = 36
+      const bal = Number(that.customBalance)
+      // 负余额展示为待结金额(正数),正余额直接展示
+      const balLabel = bal < 0 ? '当前待结' : '最新余额'
+      const balValue = bal < 0 ? ('¥' + that.formatMoney(Math.abs(bal))) : ('¥' + that.formatMoney(bal))
+      const balColor = bal < 0 ? '#e02020' : '#3385ff'
+
+      ctx.setTextAlign('left')
+      that.drawInfoRow(ctx, balLabel, balValue, labelX, valueX, rowY, false, balColor)
+      rowY += rowGap
+
+      ctx.setFillStyle('#666666')
+      ctx.setFontSize(20)
+      ctx.setTextAlign('left')
+      ctx.fillText('订单号', labelX, rowY)
+      const snText = that.posterOrderSn || '-'
+      const snMaxW = valueX - (labelX + 88)
+      ctx.setFillStyle('#333333')
+      const snSize = that.fitSnFontSize(ctx, snText, snMaxW, 16)
+      ctx.setFontSize(snSize)
+      ctx.setTextAlign('right')
+      ctx.fillText(snText, valueX, rowY)
+      rowY += rowGap
+
+      const dateStr = dayjs().format('YYYY-MM-DD HH:mm')
+      that.drawInfoRow(ctx, '日期', dateStr, labelX, valueX, rowY, true)
+      rowY += rowGap + 16
+
+      const qrSize = 180
+      const qrX = (w - qrSize) / 2
+      const qrY = rowY
+      ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize)
+
+      ctx.setFillStyle('#999999')
+      ctx.setFontSize(18)
+      ctx.setTextAlign('center')
+      ctx.fillText('长按识别查看余额明细', w / 2, qrY + qrSize + 36)
+
+      return new Promise((resolve, reject) => {
+        ctx.draw(false, () => {
+          setTimeout(() => {
+            uni.canvasToTempFilePath(
+              {
+                canvasId: 'debtPosterCanvas',
+                fileType: 'jpg',
+                quality: 1,
+                success(res) {
+                  that.posterUrl = res.tempFilePath
+                  that.posterLoading = false
+                  that.posterFail = false
+                  resolve()
+                },
+                fail() {
+                  that.posterLoading = false
+                  that.posterFail = true
+                  reject(new Error('export fail'))
+                }
+              },
+              that
+            )
+          }, 300)
+        })
+      })
+    },
+    drawInfoRow(ctx, label, value, labelX, valueX, y, valueSmall, valueColor) {
+      ctx.setFillStyle('#666666')
+      ctx.setFontSize(20)
+      ctx.setTextAlign('left')
+      ctx.fillText(label, labelX, y)
+      ctx.setFillStyle(valueColor || '#333333')
+      ctx.setFontSize(valueSmall ? 16 : 20)
+      ctx.setTextAlign('right')
+      ctx.fillText(value, valueX, y)
+    },
+    fitSnFontSize(ctx, text, maxWidth, startSize) {
+      let size = startSize || 16
+      const minSize = 10
+      ctx.setFontSize(size)
+      while (size > minSize && ctx.measureText(text).width > maxWidth) {
+        size -= 1
+        ctx.setFontSize(size)
+      }
+      return size
+    },
+    saveToAlbum() {
+      const that = this
+      if (!that.posterUrl) {
+        that.$msg && that.$msg('请等待凭证生成完成')
+        return
       }
+      uni.showLoading({ title: '正在保存...', mask: true })
+      uni.saveImageToPhotosAlbum({
+        filePath: that.posterUrl,
+        success() {
+          uni.hideLoading()
+          uni.showToast({ title: '保存成功', icon: 'success' })
+        },
+        fail() {
+          uni.hideLoading()
+          uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' })
+        }
+      })
     },
     //发货
     gotoOrderSend() {
@@ -205,7 +470,56 @@ export default {
 
 <style lang="scss" scoped>
 .admin-button-com {
-  margin-bottom:30upx;
+  margin-bottom:10upx;
   width: 100%;
 }
+.debt-poster-block {
+  width: 100%;
+  margin-top: 20upx;
+  margin-bottom: 20upx;
+}
+.status-text {
+  margin-bottom: 30upx;
+  font-size: 28upx;
+  color: #666666;
+  text-align: center;
+}
+.fail-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+}
+.fail-text {
+  color: #e02020;
+}
+.poster-preview {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 100%;
+  margin-bottom: 30upx;
+}
+.poster-img {
+  width: 450upx;
+  max-width: 100%;
+  border-radius: 12upx;
+  box-shadow: 0 8upx 24upx rgba(0, 0, 0, 0.12);
+}
+.poster-tip {
+  margin-top: 20upx;
+  font-size: 24upx;
+  color: #999999;
+  text-align: center;
+}
+.action-btn {
+  margin-bottom: 30upx;
+  width: 100%;
+}
+.poster-canvas {
+  position: fixed;
+  left: -9999px;
+  top: -9999px;
+  z-index: -1;
+}
 </style>