|
|
@@ -1,73 +1,389 @@
|
|
|
<!--
|
|
|
- 冲销成功结果页(方案A)
|
|
|
- 用途:create 成功后跳转;fundType=2(返余额)时展示收款小程序码海报,其它仅简单成功提示。
|
|
|
+ 冲销成功 / 退款凭证页(方案A)
|
|
|
+ 用途:create 成功且 fundType=返余额时,本地 canvas 绘制「退款凭证」海报(含收款小程序码),
|
|
|
+ 海报图直接展示在本页(不弹框),可长按转发或保存;其它 fundType 仅展示简单成功信息。
|
|
|
-->
|
|
|
<template>
|
|
|
<appResult title="冲销成功">
|
|
|
- <view class="forward-info" v-if="Number(fundType) === 2">
|
|
|
- <text class="forward-info__amount">已返充 ¥{{ actPrice }} 到 {{ customName || '客户' }} 余额</text>
|
|
|
- <text class="forward-info__sn" v-if="forwardSn">冲销单号 {{ forwardSn }}</text>
|
|
|
- </view>
|
|
|
- <view class="forward-info" v-else>
|
|
|
- <text class="forward-info__amount">冲销成功 ¥{{ actPrice }}</text>
|
|
|
+ <!-- 返余额:金额/单号/余额已在海报图内展示,页面上不再重复 -->
|
|
|
+ <view class="forward-info" v-if="Number(fundType) !== 2">
|
|
|
+ <text class="forward-info__amount">冲销成功 ¥{{ formatMoney(actPrice) }}</text>
|
|
|
<text class="forward-info__sn" v-if="forwardSn">冲销单号 {{ forwardSn }}</text>
|
|
|
<text class="forward-info__tip">{{ fundTypeTip }}</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 返余额:展示小程序码海报,客户可扫码充值 -->
|
|
|
- <view class="poster-wrap" v-if="Number(fundType) === 2 && miniCodeUrl">
|
|
|
- <image class="poster-img" :src="miniCodeUrl" mode="widthFix" show-menu-by-longpress />
|
|
|
- <text class="poster-tip">长按图片可转发或保存,客户扫码向您充值</text>
|
|
|
+ <!-- 退款凭证海报:直接展示在页面上 -->
|
|
|
+ <view class="poster-wrap" v-if="Number(fundType) === 2">
|
|
|
+ <view v-if="drawing && !posterUrl" class="poster-loading">
|
|
|
+ <text>凭证生成中...</text>
|
|
|
+ </view>
|
|
|
+ <image
|
|
|
+ v-if="posterUrl"
|
|
|
+ class="poster-img"
|
|
|
+ :src="posterUrl"
|
|
|
+ mode="widthFix"
|
|
|
+ show-menu-by-longpress
|
|
|
+ />
|
|
|
+ <text v-if="posterUrl" class="poster-tip">长按图片转发或保存到相册</text>
|
|
|
</view>
|
|
|
|
|
|
- <button class="admin-button-com big blue" v-if="orderId" @click="goOrder">查看原单</button>
|
|
|
- <button class="admin-button-com big" @click="goHome">返回首页</button>
|
|
|
+ <!-- 底部操作按钮统一宽度,与海报视觉对齐 -->
|
|
|
+ <view class="btn-col">
|
|
|
+ <button
|
|
|
+ v-if="Number(fundType) === 2 && posterUrl"
|
|
|
+ class="admin-button-com big blue btn-item"
|
|
|
+ @click="savePosterImg"
|
|
|
+ >保存到相册</button>
|
|
|
+ <button
|
|
|
+ v-if="Number(fundType) === 2 && !posterUrl && !drawing"
|
|
|
+ class="admin-button-com big blue btn-item"
|
|
|
+ @click="generatePoster"
|
|
|
+ >重新生成凭证</button>
|
|
|
+ <button
|
|
|
+ v-if="orderId"
|
|
|
+ class="admin-button-com big blue btn-item"
|
|
|
+ @click="goOrder"
|
|
|
+ >查看原单</button>
|
|
|
+ <button class="admin-button-com big btn-item" @click="goHome">返回首页</button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <canvas
|
|
|
+ canvas-id="forwardPosterCanvas"
|
|
|
+ class="poster-canvas"
|
|
|
+ :style="{ width: canvasW + 'px', height: canvasH + 'px' }"
|
|
|
+ />
|
|
|
</appResult>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import appResult from '@/components/app-result'
|
|
|
import { getGatheringMiniCode } from '@/api/custom'
|
|
|
+import { getUserDet } from '@/api/member'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+// 冲销成功页缓存 key:绕过小程序 URL 长度限制,保证 customBalance 不丢
|
|
|
+const FORWARD_RESULT_CACHE_KEY = 'forwardResultPayload'
|
|
|
|
|
|
export default {
|
|
|
name: 'forwardResult',
|
|
|
- components: { appResult },
|
|
|
+ components: {
|
|
|
+ appResult
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
fundType: 0,
|
|
|
actPrice: 0,
|
|
|
customId: 0,
|
|
|
customName: '',
|
|
|
+ customBalance: null,
|
|
|
forwardSn: '',
|
|
|
forwardId: 0,
|
|
|
orderId: 0,
|
|
|
- miniCodeUrl: ''
|
|
|
+ posterUrl: '',
|
|
|
+ drawing: false,
|
|
|
+ canvasW: 360,
|
|
|
+ canvasH: 640
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
fundTypeTip() {
|
|
|
const map = { 1: '已发起原路退回', 2: '已返充余额', 3: '仅记账,未动资金' }
|
|
|
return map[Number(this.fundType)] || ''
|
|
|
+ },
|
|
|
+ hasBalance() {
|
|
|
+ return this.customBalance !== null && this.customBalance !== '' && !isNaN(Number(this.customBalance))
|
|
|
}
|
|
|
},
|
|
|
onLoad(option) {
|
|
|
this.fundType = Number(option.fundType || 0)
|
|
|
this.actPrice = parseFloat(option.amount || option.actPrice || 0)
|
|
|
this.customId = Number(option.customId || 0)
|
|
|
- this.customName = option.customName || ''
|
|
|
+ try {
|
|
|
+ this.customName = decodeURIComponent(option.customName || '')
|
|
|
+ } catch (e) {
|
|
|
+ this.customName = option.customName || ''
|
|
|
+ }
|
|
|
+ // 兼容 query 传数字/字符串;缺省为 null,海报里不画成 --
|
|
|
+ if (option.customBalance !== undefined && option.customBalance !== null && option.customBalance !== '') {
|
|
|
+ const bal = parseFloat(option.customBalance)
|
|
|
+ this.customBalance = isNaN(bal) ? null : bal
|
|
|
+ } else {
|
|
|
+ this.customBalance = null
|
|
|
+ }
|
|
|
this.forwardSn = option.forwardSn || ''
|
|
|
this.forwardId = Number(option.forwardId || 0)
|
|
|
this.orderId = Number(option.orderId || 0)
|
|
|
- // 返余额时拉取收款小程序码做海报
|
|
|
+ // 优先用本地缓存补全(create 成功页写入),避免 query 丢余额
|
|
|
+ this.applyCachedPayload()
|
|
|
if (this.fundType === 2 && this.customId > 0) {
|
|
|
- this.loadMiniCode()
|
|
|
+ this.generatePoster()
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
- loadMiniCode() {
|
|
|
- getGatheringMiniCode({ id: this.customId }).then(res => {
|
|
|
+ formatMoney(v) {
|
|
|
+ const n = Number(v)
|
|
|
+ if (isNaN(n)) {
|
|
|
+ return '0.00'
|
|
|
+ }
|
|
|
+ return n.toFixed(2)
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 读取冲销成功跳转前写入的缓存,补全余额/单号等关键字段
|
|
|
+ */
|
|
|
+ applyCachedPayload() {
|
|
|
+ try {
|
|
|
+ const cached = uni.getStorageSync(FORWARD_RESULT_CACHE_KEY)
|
|
|
+ if (!cached || typeof cached !== 'object') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 仅在同一次冲销(forwardId/customId 对得上)时采用缓存,避免串数据
|
|
|
+ const sameForward = !cached.forwardId || !this.forwardId || Number(cached.forwardId) === Number(this.forwardId)
|
|
|
+ const sameCustom = !cached.customId || !this.customId || Number(cached.customId) === Number(this.customId)
|
|
|
+ if (!sameForward || !sameCustom) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (!this.hasBalance) {
|
|
|
+ this.applyBalanceFromApi(cached.customBalance)
|
|
|
+ }
|
|
|
+ if (!this.customName && cached.customName) {
|
|
|
+ this.customName = cached.customName
|
|
|
+ }
|
|
|
+ if (!this.forwardSn && cached.forwardSn) {
|
|
|
+ this.forwardSn = cached.forwardSn
|
|
|
+ }
|
|
|
+ if (!this.customId && cached.customId) {
|
|
|
+ this.customId = Number(cached.customId)
|
|
|
+ }
|
|
|
+ if (!this.orderId && cached.orderId) {
|
|
|
+ this.orderId = Number(cached.orderId)
|
|
|
+ }
|
|
|
+ if (!this.actPrice && cached.amount != null) {
|
|
|
+ this.actPrice = parseFloat(cached.amount) || 0
|
|
|
+ }
|
|
|
+ if (!this.fundType && cached.fundType != null) {
|
|
|
+ this.fundType = Number(cached.fundType)
|
|
|
+ }
|
|
|
+ uni.removeStorageSync(FORWARD_RESULT_CACHE_KEY)
|
|
|
+ } catch (e) {
|
|
|
+ // 缓存读失败不影响主流程,后续走接口补余额
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 用接口/缓存返回的客户余额补全页面/海报展示
|
|
|
+ */
|
|
|
+ applyBalanceFromApi(raw) {
|
|
|
+ if (raw === undefined || raw === null || raw === '') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const bal = parseFloat(raw)
|
|
|
+ if (!isNaN(bal)) {
|
|
|
+ this.customBalance = bal
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 生成凭证:并行拉小程序码 + 客户详情余额,保证「最新余额」一定有数
|
|
|
+ */
|
|
|
+ generatePoster() {
|
|
|
+ if (!this.customId || this.drawing) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.drawing = true
|
|
|
+ this.posterUrl = ''
|
|
|
+ uni.showLoading({ mask: true, title: '生成凭证中' })
|
|
|
+ const that = this
|
|
|
+ const codeReq = getGatheringMiniCode({
|
|
|
+ id: this.customId,
|
|
|
+ env_version: this.$util.getMiniEnvVersion ? this.$util.getMiniEnvVersion() : 'release'
|
|
|
+ })
|
|
|
+ // 客户详情是余额权威来源;query/拉码任一丢参时仍能画出正确余额
|
|
|
+ const detailReq = getUserDet({ id: this.customId }).catch(() => null)
|
|
|
+ Promise.all([codeReq, detailReq]).then(([res, detRes]) => {
|
|
|
+ if (detRes && detRes.code == 1 && detRes.data) {
|
|
|
+ that.applyBalanceFromApi(detRes.data.balance)
|
|
|
+ }
|
|
|
if (res.code == 1 && res.data && res.data.imgUrl) {
|
|
|
- this.miniCodeUrl = res.data.imgUrl
|
|
|
+ that.applyBalanceFromApi(res.data.balance)
|
|
|
+ uni.downloadFile({
|
|
|
+ url: res.data.imgUrl,
|
|
|
+ success: downloadRes => {
|
|
|
+ if (downloadRes.statusCode === 200) {
|
|
|
+ that.drawPoster(downloadRes.tempFilePath)
|
|
|
+ } else {
|
|
|
+ that.drawing = false
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '小程序码下载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ that.drawing = false
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '小程序码下载失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ that.drawing = false
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: res.msg || '获取小程序码失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(() => {
|
|
|
+ that.drawing = false
|
|
|
+ uni.hideLoading()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 按可用宽度缩小单号字号,保证与「冲销单号」标签同一行且不重叠
|
|
|
+ */
|
|
|
+ fitSnFontSize(ctx, sn, maxWidth) {
|
|
|
+ let size = 16
|
|
|
+ while (size > 10) {
|
|
|
+ ctx.setFontSize(size)
|
|
|
+ const measured = ctx.measureText ? ctx.measureText(sn) : null
|
|
|
+ const w = measured && measured.width ? measured.width : sn.length * size * 0.62
|
|
|
+ if (w <= maxWidth) {
|
|
|
+ return size
|
|
|
+ }
|
|
|
+ size -= 1
|
|
|
+ }
|
|
|
+ return 10
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 绘制退款凭证海报。
|
|
|
+ * 冲销单号与标签同一行:单号用更小字号,避免换行难看,也避免盖住左侧文案。
|
|
|
+ */
|
|
|
+ drawPoster(qrPath) {
|
|
|
+ const that = this
|
|
|
+ const ctx = uni.createCanvasContext('forwardPosterCanvas', this)
|
|
|
+ const width = this.canvasW
|
|
|
+ const height = this.canvasH
|
|
|
+ const radius = 20
|
|
|
+ const padX = 30
|
|
|
+
|
|
|
+ ctx.save()
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.moveTo(radius, 0)
|
|
|
+ ctx.lineTo(width - radius, 0)
|
|
|
+ ctx.arcTo(width, 0, width, radius, radius)
|
|
|
+ ctx.lineTo(width, height - radius)
|
|
|
+ ctx.arcTo(width, height, width - radius, height, radius)
|
|
|
+ ctx.lineTo(radius, height)
|
|
|
+ ctx.arcTo(0, height, 0, height - radius, radius)
|
|
|
+ ctx.lineTo(0, radius)
|
|
|
+ ctx.arcTo(0, 0, radius, 0, radius)
|
|
|
+ ctx.closePath()
|
|
|
+ ctx.clip()
|
|
|
+
|
|
|
+ ctx.setFillStyle('#ffffff')
|
|
|
+ ctx.fillRect(0, 0, width, height)
|
|
|
+
|
|
|
+ ctx.setFillStyle('#3385ff')
|
|
|
+ ctx.fillRect(0, 0, width, 140)
|
|
|
+ ctx.setFontSize(28)
|
|
|
+ ctx.setFillStyle('#ffffff')
|
|
|
+ ctx.setTextAlign('center')
|
|
|
+ ctx.fillText('退款凭证', width / 2, 60)
|
|
|
+ ctx.setFontSize(22)
|
|
|
+ ctx.fillText(this.customName || '客户', width / 2, 100)
|
|
|
+
|
|
|
+ ctx.setFontSize(46)
|
|
|
+ ctx.setFillStyle('#ff2842')
|
|
|
+ ctx.setTextAlign('center')
|
|
|
+ ctx.fillText('¥' + this.formatMoney(this.actPrice), width / 2, 205)
|
|
|
+ ctx.setFontSize(22)
|
|
|
+ ctx.setFillStyle('#999999')
|
|
|
+ ctx.fillText('已返充到余额', width / 2, 235)
|
|
|
+
|
|
|
+ ctx.setStrokeStyle('#eeeeee')
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.moveTo(padX, 258)
|
|
|
+ ctx.lineTo(width - padX, 258)
|
|
|
+ ctx.stroke()
|
|
|
+
|
|
|
+ // 最新余额(有值才画金额,无值画 --)
|
|
|
+ const balanceText = this.hasBalance ? ('¥' + this.formatMoney(this.customBalance)) : '--'
|
|
|
+ ctx.setFontSize(24)
|
|
|
+ ctx.setFillStyle('#333333')
|
|
|
+ ctx.setTextAlign('left')
|
|
|
+ ctx.fillText('最新余额', padX, 298)
|
|
|
+ ctx.setTextAlign('right')
|
|
|
+ ctx.setFillStyle('#3385ff')
|
|
|
+ ctx.fillText(balanceText, width - padX, 298)
|
|
|
+
|
|
|
+ // 冲销单号同一行:左侧标签,右侧按宽度缩小字号,保证不换行、不重叠
|
|
|
+ const snLabel = '冲销单号'
|
|
|
+ const snText = this.forwardSn || '--'
|
|
|
+ const snLabelSize = 24
|
|
|
+ ctx.setFontSize(snLabelSize)
|
|
|
+ ctx.setFillStyle('#333333')
|
|
|
+ ctx.setTextAlign('left')
|
|
|
+ ctx.fillText(snLabel, padX, 333)
|
|
|
+ const snLabelW = (ctx.measureText ? ctx.measureText(snLabel).width : snLabelSize * 4) || snLabelSize * 4
|
|
|
+ const snMaxW = width - padX * 2 - snLabelW - 16
|
|
|
+ const snSize = this.fitSnFontSize(ctx, snText, snMaxW)
|
|
|
+ ctx.setTextAlign('right')
|
|
|
+ ctx.setFontSize(snSize)
|
|
|
+ ctx.setFillStyle('#666666')
|
|
|
+ ctx.fillText(snText, width - padX, 333)
|
|
|
+
|
|
|
+ ctx.setFontSize(24)
|
|
|
+ ctx.setFillStyle('#333333')
|
|
|
+ ctx.setTextAlign('left')
|
|
|
+ ctx.fillText('日期', padX, 368)
|
|
|
+ ctx.setTextAlign('right')
|
|
|
+ ctx.setFontSize(20)
|
|
|
+ ctx.setFillStyle('#666666')
|
|
|
+ ctx.fillText(dayjs().format('YYYY-MM-DD HH:mm'), width - padX, 368)
|
|
|
+
|
|
|
+ const qrSize = 150
|
|
|
+ const qrX = (width - qrSize) / 2
|
|
|
+ const qrY = 400
|
|
|
+ ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize)
|
|
|
+
|
|
|
+ ctx.setFontSize(20)
|
|
|
+ ctx.setFillStyle('#999999')
|
|
|
+ ctx.setTextAlign('center')
|
|
|
+ ctx.fillText('长按识别,查看余额明细', width / 2, qrY + qrSize + 36)
|
|
|
+
|
|
|
+ ctx.restore()
|
|
|
+
|
|
|
+ ctx.draw(false, () => {
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.canvasToTempFilePath(
|
|
|
+ {
|
|
|
+ canvasId: 'forwardPosterCanvas',
|
|
|
+ fileType: 'jpg',
|
|
|
+ quality: 1,
|
|
|
+ destWidth: width * 2,
|
|
|
+ destHeight: height * 2,
|
|
|
+ success: res => {
|
|
|
+ that.drawing = false
|
|
|
+ uni.hideLoading()
|
|
|
+ that.posterUrl = res.tempFilePath
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ that.drawing = false
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '生成图片失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ that
|
|
|
+ )
|
|
|
+ }, 300)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ savePosterImg() {
|
|
|
+ if (!this.posterUrl) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.showLoading({ title: '正在保存...', mask: true })
|
|
|
+ uni.saveImageToPhotosAlbum({
|
|
|
+ filePath: this.posterUrl,
|
|
|
+ success() {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '保存成功', icon: 'success' })
|
|
|
+ },
|
|
|
+ fail() {
|
|
|
+ uni.hideLoading()
|
|
|
+ uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' })
|
|
|
}
|
|
|
})
|
|
|
},
|
|
|
@@ -90,6 +406,7 @@ export default {
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
margin-bottom: 30upx;
|
|
|
+ width: 100%;
|
|
|
|
|
|
&__amount {
|
|
|
font-size: 32upx;
|
|
|
@@ -102,6 +419,14 @@ export default {
|
|
|
margin-top: 12upx;
|
|
|
font-size: 26upx;
|
|
|
color: #666;
|
|
|
+ word-break: break-all;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__balance {
|
|
|
+ margin-top: 12upx;
|
|
|
+ font-size: 26upx;
|
|
|
+ color: #3385ff;
|
|
|
}
|
|
|
|
|
|
&__tip {
|
|
|
@@ -115,23 +440,51 @@ export default {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 30upx;
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 10upx;
|
|
|
+}
|
|
|
+
|
|
|
+.poster-loading {
|
|
|
+ padding: 40upx 0;
|
|
|
+ font-size: 26upx;
|
|
|
+ color: #999;
|
|
|
}
|
|
|
|
|
|
.poster-img {
|
|
|
- width: 420upx;
|
|
|
+ width: 100%;
|
|
|
background: #fff;
|
|
|
- border-radius: 12upx;
|
|
|
+ border-radius: 16upx;
|
|
|
+ box-shadow: 0 8upx 24upx rgba(0, 0, 0, 0.08);
|
|
|
}
|
|
|
|
|
|
.poster-tip {
|
|
|
margin-top: 16upx;
|
|
|
- font-size: 24upx;
|
|
|
+ margin-bottom: 10upx;
|
|
|
+ font-size: 26upx;
|
|
|
color: #999;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
-.admin-button-com {
|
|
|
+/* 与海报同宽拉满,保证「保存/查看原单/返回首页」一样长 */
|
|
|
+.btn-col {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-item {
|
|
|
+ width: 100% !important;
|
|
|
+ display: block !important;
|
|
|
+ box-sizing: border-box;
|
|
|
margin-top: 20upx;
|
|
|
}
|
|
|
+
|
|
|
+.poster-canvas {
|
|
|
+ position: fixed;
|
|
|
+ left: -9999px;
|
|
|
+ top: -9999px;
|
|
|
+ z-index: -1;
|
|
|
+}
|
|
|
</style>
|