Browse Source

冲销单

shish 20 hours ago
parent
commit
c8f235755f

File diff suppressed because it is too large
+ 94 - 796
ghsApp/src/admin/billing/affirmForward.vue


+ 269 - 0
ghsApp/src/admin/billing/forwardResult.vue

@@ -0,0 +1,269 @@
+<template>
+  <appResult title="冲销成功">
+    <view class="forward-info">
+      <text class="forward-amount">已返充 ¥{{ actPrice.toFixed(2) }} 到 {{ customName || '客户' }} 的余额</text>
+      <text class="forward-balance">最新余额 ¥{{ Number(customBalance).toFixed(2) }}</text>
+    </view>
+    <button class="admin-button-com big blue" @click="showPoster">查看/分享退款凭证</button>
+    <button class="admin-button-com big" style="background-color: green;border-color: green;color:white;" @click="gotoOrderDetail">订单详情</button>
+    <button class="admin-button-com big" @click="goBackHome">返回首页</button>
+
+    <!-- 离屏画布,用于绘制退款凭证海报,绘制完成后导出为图片再展示 -->
+    <canvas canvas-id="forwardPosterCanvas" style="width: 360px; height: 560px; position: fixed; left: -9999px; top: -9999px; z-index: -1;"></canvas>
+    <item-poster-popup ref="itemPosterPopup" :poster-url="posterUrl" @close="closePoster" @save="savePosterImg" />
+  </appResult>
+</template>
+
+<script>
+/**
+ * 冲销单退款凭证页(ssh 冲销单功能)
+ * 用途:affirmForward.vue 提交冲销单成功后,如果客户选择"返充到余额",跳转到本页,
+ * 用 Canvas 绘制一张可长按转发/保存的退款凭证海报(客户名、退款金额、最新余额、冲销单号 + 收款二维码),
+ * 二维码复用 CustomController::actionGetGatheringCode,扫码后跳转 hdApp 的 admin/ghs/pay 页面查看余额明细。
+ * 如果没有返充余额,则走通用的 /admin/billing/result 成功页,不显示本页。
+ */
+import appResult from '@/components/app-result';
+import ItemPosterPopup from '@/components/item-poster-popup.vue';
+import { getGatheringCode } from '@/api/custom';
+import dayjs from 'dayjs';
+
+export default {
+  name: 'forwardResult',
+  components: {
+    appResult,
+    ItemPosterPopup
+  },
+  data() {
+    return {
+      orderId: 0,
+      orderSn: '',
+      actPrice: 0,
+      customId: 0,
+      customName: '',
+      customBalance: 0,
+      posterUrl: ''
+    };
+  },
+  onLoad(option) {
+    this.option = option;
+    this.orderId = option.orderId || 0;
+    this.orderSn = option.orderSn || '';
+    this.actPrice = option.actPrice ? parseFloat(option.actPrice) : 0;
+    this.customId = option.customId || 0;
+    this.customName = option.customName || '';
+    this.customBalance = option.customBalance !== undefined && option.customBalance !== '' ? parseFloat(option.customBalance) : 0;
+    this.generatePoster();
+  },
+  methods: {
+    /** 拉取收款二维码图片并绘制海报,绘制完成后自动弹出预览 */
+    generatePoster() {
+      if (!this.customId) {
+        return;
+      }
+      let that = this;
+      uni.showLoading({ mask: true, title: '生成凭证中' });
+      getGatheringCode({ id: this.customId })
+        .then((res) => {
+          if (res.code == 1 && res.data && res.data.imgUrl) {
+            uni.downloadFile({
+              url: res.data.imgUrl,
+              success: (downloadRes) => {
+                if (downloadRes.statusCode === 200) {
+                  that.drawPoster(downloadRes.tempFilePath);
+                } else {
+                  uni.hideLoading();
+                  uni.showToast({ title: '二维码下载失败', icon: 'none' });
+                }
+              },
+              fail: () => {
+                uni.hideLoading();
+                uni.showToast({ title: '二维码下载失败', icon: 'none' });
+              }
+            });
+          } else {
+            uni.hideLoading();
+          }
+        })
+        .catch(() => {
+          uni.hideLoading();
+        });
+    },
+    /**
+     * 用 Canvas 绘制退款凭证海报:顶部蓝色banner + 客户名,中部大号退款金额与最新余额,
+     * 底部收款二维码,用于客户长按识别查看资金明细
+     */
+    drawPoster(qrPath) {
+      let that = this;
+      const ctx = uni.createCanvasContext('forwardPosterCanvas', this);
+      const width = 360;
+      const height = 560;
+      const radius = 20;
+
+      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);
+
+      //顶部蓝色banner
+      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.actPrice.toFixed(2), width / 2, 205);
+      ctx.setFontSize(22);
+      ctx.setFillStyle('#999999');
+      ctx.fillText('已返充到余额', width / 2, 235);
+
+      //分隔线
+      ctx.setStrokeStyle('#eeeeee');
+      ctx.beginPath();
+      ctx.moveTo(30, 258);
+      ctx.lineTo(width - 30, 258);
+      ctx.stroke();
+
+      //信息行:最新余额
+      ctx.setFontSize(24);
+      ctx.setFillStyle('#333333');
+      ctx.setTextAlign('left');
+      ctx.fillText('最新余额', 30, 298);
+      ctx.setTextAlign('right');
+      ctx.setFillStyle('#3385ff');
+      ctx.fillText('¥' + Number(this.customBalance).toFixed(2), width - 30, 298);
+
+      //信息行:冲销单号
+      ctx.setFontSize(24);
+      ctx.setFillStyle('#333333');
+      ctx.setTextAlign('left');
+      ctx.fillText('冲销单号', 30, 333);
+      ctx.setTextAlign('right');
+      ctx.setFontSize(20);
+      ctx.setFillStyle('#666666');
+      ctx.fillText(this.orderSn, width - 30, 333);
+
+      //信息行:日期
+      ctx.setFontSize(24);
+      ctx.setFillStyle('#333333');
+      ctx.setTextAlign('left');
+      ctx.fillText('日期', 30, 368);
+      ctx.setTextAlign('right');
+      ctx.setFontSize(20);
+      ctx.setFillStyle('#666666');
+      ctx.fillText(dayjs().format('YYYY-MM-DD HH:mm'), width - 30, 368);
+
+      //二维码
+      const qrSize = 150;
+      const qrX = (width - qrSize) / 2;
+      const qrY = 390;
+      ctx.drawImage(qrPath, qrX, qrY, qrSize, qrSize);
+
+      ctx.setFontSize(20);
+      ctx.setFillStyle('#666666');
+      ctx.setTextAlign('center');
+      ctx.fillText('长按识别,查看余额明细', width / 2, qrY + qrSize + 30);
+
+      ctx.restore();
+
+      ctx.draw(false, () => {
+        //延迟确保画布已完全渲染,避免部分平台导出空白图片
+        setTimeout(() => {
+          uni.canvasToTempFilePath(
+            {
+              canvasId: 'forwardPosterCanvas',
+              fileType: 'jpg',
+              quality: 1,
+              success: (res) => {
+                uni.hideLoading();
+                that.posterUrl = res.tempFilePath;
+                that.$refs.itemPosterPopup.open();
+              },
+              fail: () => {
+                uni.hideLoading();
+                uni.showToast({ title: '生成图片失败', icon: 'none' });
+              }
+            },
+            that
+          );
+        }, 300);
+      });
+    },
+    showPoster() {
+      if (this.posterUrl) {
+        this.$refs.itemPosterPopup.open();
+      } else {
+        this.generatePoster();
+      }
+    },
+    closePoster() {
+      this.$refs.itemPosterPopup.close();
+    },
+    savePosterImg() {
+      let that = this;
+      uni.showLoading({ title: '正在保存...', mask: true });
+      uni.saveImageToPhotosAlbum({
+        filePath: this.posterUrl,
+        success() {
+          uni.hideLoading();
+          that.$refs.itemPosterPopup.close();
+          uni.showToast({ title: '保存成功', icon: 'success' });
+        },
+        fail() {
+          uni.hideLoading();
+          uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' });
+        }
+      });
+    },
+    gotoOrderDetail() {
+      this.$util.pageTo({ url: '/pagesOrder/detail', type: 2, query: { id: this.orderId } });
+    },
+    goBackHome() {
+      this.$util.pageTo({ url: '/admin/home/workbench', type: 4, query: { tabIndex: 0 } });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.admin-button-com {
+  margin-bottom: 30upx;
+  width: 100%;
+}
+.forward-info {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 25upx 0 40upx;
+  .forward-amount {
+    font-size: 30upx;
+    color: #333;
+    font-weight: bold;
+    text-align: center;
+  }
+  .forward-balance {
+    margin-top: 12upx;
+    font-size: 26upx;
+    color: #3385ff;
+  }
+}
+</style>

+ 7 - 1
ghsApp/src/admin/billing/index2.vue

@@ -465,7 +465,13 @@ export default {
 			if(forward == 0){
 				this.$util.pageTo({ url: '/admin/billing/affirm', type:2, query: {customId:this.option.customId,customName:customName,orderId:orderId,book:book}})
 			}else{
-				this.$util.pageTo({ url: '/admin/billing/affirmForward?customId='+customId+'&customName='+customName+'&orderId='+orderId, type:2})
+				//冲销单场景:relateOrderId/payWay/payWayName 由"售后转冲销单"入口(refund.vue)透传而来,
+				//用于关联原单做累计冲销上限校验,以及在确认页锁定退款方式为原单支付渠道
+				let url = '/admin/billing/affirmForward?customId='+customId+'&customName='+customName+'&orderId='+orderId
+				if (Number(this.option.relateOrderId) > 0) {
+					url += '&relateOrderId='+this.option.relateOrderId+'&payWay='+this.option.payWay+'&payWayName='+encodeURIComponent(this.option.payWayName || '')
+				}
+				this.$util.pageTo({ url: url, type:2})
 			}
 		}
 	}

+ 67 - 55
ghsApp/src/admin/home/components/OrderItem.vue

@@ -15,64 +15,68 @@
         </view>
       </view>
       <view class="order-list_info">
-        <view class="order-info_box">
-          <view>货号:</view>
-          <view>{{ item.sendNum }}</view>
-        </view>
-        <view class="order-info_box">
-          <view>创建:</view>
-          <view>
-            {{ item.addTime ? item.addTime.substr(5, 11) : '' }}
+        <!-- 冲销单:货号/创建/单号/方式/开单等明细都隐藏,中间用醒目红字提示,数量与价格行照常展示 -->
+        <template v-if="item.forward != 1">
+          <view class="order-info_box">
+            <view>货号:</view>
+            <view>{{ item.sendNum }}</view>
           </view>
-          <view
-            v-if="item.addTime && item.payTime && item.payTime != '0000-00-00 00:00:00' && item.addTime.substr(5, 5) != item.payTime.substr(5, 5)"
-            class="bill-date"
-          >
-            账单日期 {{ item.payTime ? item.payTime.substr(5, 11) : '' }}
+          <view class="order-info_box">
+            <view>创建:</view>
+            <view>
+              {{ item.addTime ? item.addTime.substr(5, 11) : '' }}
+            </view>
+            <view
+              v-if="item.addTime && item.payTime && item.payTime != '0000-00-00 00:00:00' && item.addTime.substr(5, 5) != item.payTime.substr(5, 5)"
+              class="bill-date"
+            >
+              账单日期 {{ item.payTime ? item.payTime.substr(5, 11) : '' }}
+            </view>
           </view>
-        </view>
-        <view class="order-info_box">
-          <view>单号:</view>
-          <view>{{ item.orderSn }}</view>
-        </view>
-        <view class="order-info_box">
-          <view>方式:</view>
-          <view>
-            <text v-if="item.localOrder == 0">{{
-              item.sendType == 0
-                ? '送货上门'
-                : item.sendType == 1
-                ? '到店自取'
-                : item.sendType == 2
-                ? '跑腿送货'
-                : item.sendType == 3
-                ? item.wlName
-                : item.sendType == 4
-                ? '发快递'
-                : ''
-            }}</text>
-            <text v-else>{{
-              item.transType == 0
-                ? '德邦物流'
-                : item.transType == 1
-                ? '顺丰物流'
-                : item.transType == 2
-                ? '冷链物流'
-                : item.transType == 3
-                ? '航空物流'
-                : item.transType == 4
-                ? '同城配送'
-                : item.transType == 5
-                ? '到店自取'
-                : ''
-            }}</text>
+          <view class="order-info_box">
+            <view>单号:</view>
+            <view>{{ item.orderSn }}</view>
           </view>
-        </view>
-        <view class="order-info_box">
-          <view>开单:</view>
-          <view v-if="item.shopAdminId > 0">{{ item.shopAdminName }}</view>
-          <view v-else>{{ item.customShopAdminName }}</view>
-        </view>
+          <view class="order-info_box">
+            <view>方式:</view>
+            <view>
+              <text v-if="item.localOrder == 0">{{
+                item.sendType == 0
+                  ? '送货上门'
+                  : item.sendType == 1
+                  ? '到店自取'
+                  : item.sendType == 2
+                  ? '跑腿送货'
+                  : item.sendType == 3
+                  ? item.wlName
+                  : item.sendType == 4
+                  ? '发快递'
+                  : ''
+              }}</text>
+              <text v-else>{{
+                item.transType == 0
+                  ? '德邦物流'
+                  : item.transType == 1
+                  ? '顺丰物流'
+                  : item.transType == 2
+                  ? '冷链物流'
+                  : item.transType == 3
+                  ? '航空物流'
+                  : item.transType == 4
+                  ? '同城配送'
+                  : item.transType == 5
+                  ? '到店自取'
+                  : ''
+              }}</text>
+            </view>
+          </view>
+          <view class="order-info_box">
+            <view>开单:</view>
+            <view v-if="item.shopAdminId > 0">{{ item.shopAdminName }}</view>
+            <view v-else>{{ item.customShopAdminName }}</view>
+          </view>
+        </template>
+        <view class="forward-tag" v-else>冲销单</view>
         <view class="order-info_box">
           <view>数量:</view>
           <view v-if="item.smallNum > 0">{{ `${item.bigNum}/${item.smallNum}` }}</view>
@@ -1018,6 +1022,14 @@ export default {
   }
 }
 
+/* 冲销单中间提示:ssh 冲销单功能,取代货号/创建/单号/方式/开单等明细行 */
+.forward-tag {
+  text-align: center;
+  color: #ff2842;
+  font-size: 44upx;
+  font-weight: bold;
+  padding: 24upx 0;
+}
 .order-list_info {
   padding: 10upx 30upx 30upx;
   border-bottom: 1upx solid #eeeeee;

+ 8 - 0
ghsApp/src/api/order/index.js

@@ -297,3 +297,11 @@ export const modifyAddress = data => {
 export const getTree = data => https.get("/order-tree/get-tree", data);
 
 export const getMergeOrderList = data => https.get("/order/merge-order-list", data);
+
+//创建冲销单(金额为负的独立订单)ssh 冲销单功能。relateOrderId 可选,传了则做累计冲销上限校验+锁定支付方式
+export const createForwardOrder = (data, config = {}) => {
+	return https.post("/order/create-forward-order", data, {}, config);
+};
+
+//按原销售单查询关联的冲销记录,供售后记录页展示“冲销记录” ssh 冲销单功能
+export const getForwardListByOrder = data => https.get("/order/get-forward-list-by-order", data);

+ 1 - 0
ghsApp/src/pages.json

@@ -332,6 +332,7 @@
 				{ "path": "result", "style": { "navigationBarTitleText": "开单结果" } },
 				{ "path": "affirm", "style": { "navigationBarTitleText": "确认下单" } },
 				{ "path": "affirmForward", "style": { "navigationBarTitleText": "确认下单" } },
+				{ "path": "forwardResult", "style": { "navigationBarTitleText": "冲销成功" } },
 				{ "path": "customerAffirm", "style": { "navigationBarTitleText": "确认下单" } },
 				{ "path": "confirm", "style": { "navigationBarTitleText": "待确认" } },
 				{"path": "toPay","style": {"navigationBarTitleText": "等待付款"}},

+ 14 - 3
ghsApp/src/pagesOrder/detail.vue

@@ -233,10 +233,11 @@
 					<view>过期时间:</view>
 					<view>{{detailInfo.deadline?detailInfo.deadline.substr(5,11):''}}</view>
 				</view>
-				<view v-if="detailInfo.refundLog == 1" class="order-info_box" style="margin-top:40upx;">
+				<!-- 累计退款 = 常规售后退款(tkPrice) + 冲销单退款(forwardPrice),任一有记录都展示本行;ssh 冲销单功能 -->
+				<view v-if="detailInfo.refundLog == 1 || detailInfo.hasForward == 1" class="order-info_box" style="margin-top:40upx;">
 					<view>累计退款:</view>
-					<view style="color:red;font-weight:bold;font-size:30upx;">{{parseFloat(detailInfo.tkPrice)}}</view>
-					<view class="price" v-if="detailInfo.refundLog==1">
+					<view style="color:red;font-weight:bold;font-size:30upx;">{{parseFloat((Number(detailInfo.tkPrice)||0) + (Number(detailInfo.forwardPrice)||0))}}</view>
+					<view class="price">
 						<button @click="pageTo({url: '/pagesOrder/refundList',query: {orderSn: detailInfo.orderSn}})" style="color:red;border: 1upx solid red;" class="admin-button-com">售后记录</button>
 					</view>
 				</view>
@@ -393,6 +394,11 @@
 							-¥{{ parseFloat(detailInfo.tkPrice) }}
 						</div>
 					</tui-list-cell>
+					<!-- 冲销单退款:ssh 冲销单功能,与常规售后退款(tkPrice)分开展示 -->
+					<tui-list-cell v-if="detailInfo.hasForward == 1" class="line-cell" :hover="false">
+						<div class="tui-title">冲销金额</div>
+						<div class="detail-price_box" style="font-size: 28upx">-¥{{ parseFloat(detailInfo.forwardPrice || 0) }}</div>
+					</tui-list-cell>
 					<tui-list-cell v-if="detailInfo.orderReachDiscountPrice > 0" class="line-cell" :hover="false">
 						<div class="tui-title">整单优惠</div>
 						<div class="detail-price_box" style="font-size: 28upx">-¥{{ detailInfo.orderReachDiscountPrice?parseFloat(detailInfo.orderReachDiscountPrice):0 }}</div>
@@ -449,6 +455,11 @@
 							-¥{{ parseFloat(detailInfo.tkPrice) }}
 						</div>
 					</tui-list-cell>
+					<!-- 冲销单退款:ssh 冲销单功能,与常规售后退款(tkPrice)分开展示 -->
+					<tui-list-cell v-if="detailInfo.hasForward == 1" class="line-cell" :hover="false">
+						<div class="tui-title">冲销金额</div>
+						<div class="detail-price_box" style="font-size: 28upx">-¥{{ parseFloat(detailInfo.forwardPrice || 0) }}</div>
+					</tui-list-cell>
 					<tui-list-cell class="line-cell" :hover="false" 
 					v-if="detailInfo.status==3 || detailInfo.status== 4">
 						<div class="tui-title">实际金额</div>

+ 121 - 10
ghsApp/src/pagesOrder/refund.vue

@@ -10,6 +10,21 @@
 			<text class="warning-action">查看</text>
 		</view>
 
+		<!-- 该订单非当天单/欠款已结清,提交后走冲销单方式退款,需要选择商品才能生成 -->
+		<view class="warning-tip forward-tip" v-if="isForwardEligible && !forwardExhausted">
+			<view class="warning-icon">ℹ</view>
+			<view class="warning-content">
+				<text class="warning-text">该订单将以冲销单方式退款</text>
+				<text class="warning-amount">需选择退货商品,退款方式将锁定为原支付渠道</text>
+			</view>
+		</view>
+		<view class="warning-tip" v-if="isForwardEligible && forwardExhausted">
+			<view class="warning-icon">⚠</view>
+			<view class="warning-content">
+				<text class="warning-text">该订单已冲销完,无法再售后</text>
+			</view>
+		</view>
+
 		<!-- 退款方式选择卡片 -->
 		<view class="card-section">
 			<view class="section-title">
@@ -25,7 +40,8 @@
 				<button 
 					class="type-button" 
 					:class="refundType==2?'active':''" 
-					@tap="refundType=2">
+					:disabled="isForwardEligible"
+					@tap="selectOnlyRefund">
 					只退款
 				</button>
 			</view>
@@ -60,8 +76,8 @@
 			</view>
 		</view>
 
-		<!-- 其他可退卡片 -->
-		<view class="card-section" v-if="Number(orderInfo.packCost)>0||Number(orderInfo.sendCost)>0">
+		<!-- 其他可退卡片:冲销单场景不支持打包费/运费退款,只能按商品冲销 -->
+		<view class="card-section" v-if="!isForwardEligible && (Number(orderInfo.packCost)>0||Number(orderInfo.sendCost)>0)">
 			<view class="section-title">
 				<text class="title-text">其他可退</text>
 			</view>
@@ -109,8 +125,8 @@
 			</view>
 		</view>
 
-		<!-- 金额信息卡片 -->
-		<view class="card-section">
+		<!-- 金额信息卡片:冲销单场景金额由下一步实际选择的花材数量决定,这里不需要手工填写实际退款金额 -->
+		<view class="card-section" v-if="!isForwardEligible">
 			<view class="section-title">
 				<text class="title-text">金额信息</text>
 			</view>
@@ -151,8 +167,8 @@
 			</view>
 		</view>
 
-		<!-- 退款原因卡片 -->
-		<view class="card-section">
+		<!-- 退款原因卡片:冲销单场景暂不需要区分原因 -->
+		<view class="card-section" v-if="!isForwardEligible">
 			<view class="section-title">
 				<text class="title-text">退款原因</text>
 			</view>
@@ -186,8 +202,8 @@
 			</view>
 		</view>
 
-		<!-- 提示信息 -->
-		<view class="tip-section">
+		<!-- 提示信息:冲销单场景走单独的提示(见顶部forward-tip),这里不再重复展示 -->
+		<view class="tip-section" v-if="!isForwardEligible">
 			<text v-if="orderInfo.onlinePay == 2" class="tip-text online-tip">此单线上付款,提交后,钱会原路自动退回给客户</text>
 			<text v-else-if="orderInfo.payWay == 2" class="tip-text online-tip">此单余额付款,提交后,钱会原路自动退回到客户余额</text>
 			<block v-else>
@@ -199,7 +215,7 @@
 		<!-- 底部操作按钮 -->
 		<view class="bottom-actions">
 			<button class="action-btn cancel-btn" @tap="cancelRefund">取消</button>
-			<button class="action-btn confirm-btn" @tap="confirmRefund">确定</button>
+			<button class="action-btn confirm-btn" :disabled="forwardExhausted" @tap="confirmRefund">确定</button>
 		</view>
 
 	</view>
@@ -210,6 +226,7 @@ import { getDetail,refund } from "@/api/order/index";
 import { ORDER_STATUS } from "@/utils/declare";
 import { getUnClear } from "@/api/clear"
 import { changeHalf } from "@/api/item"
+import dayjs from 'dayjs'
 export default {
 	name: "orderDetail",
 	components: {
@@ -231,6 +248,27 @@ export default {
 	onShow() {
 	},
 	computed:{
+		/**
+		 * 是否走"售后转冲销单":原单非当天单,或者原单曾有欠款且现已结清完毕。
+		 * 满足任一条件时,售后不再走老的退款流程,改为生成负数冲销单(ssh 冲销单功能)
+		 */
+		isForwardEligible(){
+			if(this.$util.isEmpty(this.orderInfo) || this.$util.isEmpty(this.orderInfo.payTime)){
+				return false
+			}
+			const notToday = dayjs(this.orderInfo.payTime).format('YYYY-MM-DD') !== dayjs().format('YYYY-MM-DD')
+			const debtCleared = Number(this.orderInfo.debtPrice) > 0 && Number(this.orderInfo.remainDebtPrice) == 0
+			return notToday || debtCleared
+		},
+		/** 累计冲销金额已达原单实付金额,不能再对该订单发起售后/冲销 */
+		forwardExhausted(){
+			if(!this.isForwardEligible){
+				return false
+			}
+			const actPrice = Number(this.orderInfo.actPrice) || 0
+			const forwardPrice = Number(this.orderInfo.forwardPrice) || 0
+			return actPrice <= forwardPrice
+		},
 		refundPrice(){
 			let price = 0
 			if(this.product){
@@ -255,6 +293,12 @@ export default {
 		}
 	},
 	watch:{
+		//冲销单场景强制走"退货并退款"(需要商品明细才能生成冲销单)
+		isForwardEligible(val){
+			if(val){
+				this.refundType = 1
+			}
+		}
 	},
 	methods: {
 		addHalf(item){
@@ -273,7 +317,56 @@ export default {
 		cancelRefund(){
 			uni.navigateBack({delta: 1});
 		},
+		/** "只退款"在冲销单场景下不可选(冲销单必须按商品记账),提示改选"退货并退款" */
+		selectOnlyRefund(){
+			if(this.isForwardEligible){
+				this.$msg('该订单需选择退货商品,走冲销单方式退款')
+				return
+			}
+			this.refundType = 2
+		},
+		/** payWay数字编码转文字,用于冲销单确认页只读展示原单支付渠道 */
+		getPayWayName(payWay){
+			const map = {0:'微信',1:'支付宝',2:'余额',3:'挂账',4:'现金',5:'银行卡'}
+			return map[payWay] !== undefined ? map[payWay] : '--'
+		},
+		/**
+		 * 售后转冲销单:跳转到花材选择页(index2),携带 forward=1 和 relateOrderId,
+		 * 由 index2 -> affirmForward 走冲销单确认与提交,退款方式在 affirmForward 锁定为原单支付渠道。
+		 * ssh 冲销单功能
+		 */
+		goForwardOrder(){
+			let hasSelected = this.product.some(ele => Number(ele.refundCount) > 0)
+			if(!hasSelected){
+				uni.showToast({title:'请选择要退货的商品',icon:'none'})
+				return
+			}
+			const customId = this.orderInfo.customId || 0
+			const customName = this.orderInfo.customName || ''
+			const payWay = this.orderInfo.payWay
+			const payWayName = this.getPayWayName(payWay)
+			this.$util.pageTo({
+				url: '/admin/billing/index2',
+				type: 2,
+				query: {
+					customId: customId,
+					customName: customName,
+					forward: 1,
+					relateOrderId: this.option.id,
+					payWay: payWay,
+					payWayName: payWayName
+				}
+			})
+		},
 		confirmRefund(){
+			if(this.forwardExhausted){
+				uni.showToast({title:'该订单已冲销完,无法再售后',icon:'none'})
+				return
+			}
+			if(this.isForwardEligible){
+				this.goForwardOrder()
+				return
+			}
 			let hasError = false
 			let product = []
 			let that = this
@@ -447,6 +540,24 @@ export default {
 	}
 }
 
+// 冲销单提示:蓝色信息条,区别于红色警示的 warning-tip
+.forward-tip {
+	background: rgba(51, 133, 255, 0.08);
+	border: 1upx solid rgba(51, 133, 255, 0.3);
+
+	.warning-icon {
+		color: #3385ff;
+	}
+	.warning-text {
+		color: #3385ff;
+	}
+	.warning-amount {
+		color: #3385ff;
+		font-size: 26upx;
+		font-weight: 500;
+	}
+}
+
 // 卡片样式
 .card-section {
 	background: #fff;

+ 47 - 1
ghsApp/src/pagesOrder/refundList.vue

@@ -19,19 +19,36 @@
 			  </view>
 			</block>
 			<block v-else>
-				<app-wrapper-empty title="暂无数据" :is-empty="$util.isEmpty(list.data)" />
+				<app-wrapper-empty title="暂无售后记录" :is-empty="$util.isEmpty(list.data)" />
+			</block>
+
+			<!-- 冲销记录:ssh 冲销单功能,展示在售后记录下方,单独一块(关联原单的冲销单,不分页) -->
+			<block v-if="!$util.isEmpty(forwardList)">
+				<view class="forward-title">冲销记录</view>
+				<view class="flex-space list forward-item" v-for="(item, index) in forwardList" :key="'forward'+index" @click="goForwardDetail(item)">
+					<view class="label">
+						<text>日期:{{item.addTime?item.addTime.substr(5,11):''}}</text>
+						<text>单号:{{item.forwardOrderSn}}</text>
+						<text>操作:{{item.shopAdminName}}</text>
+						<text>库存:{{item.forwardStock == 0 ? '已退回' : '未退回'}}</text>
+					</view>
+					<view class="flex val">-¥{{parseFloat(item.forwardPrice)}} <view class="iconfont iconxiangyou"></view> </view>
+				</view>
 			</block>
   </view>
 </template>
 <script>
 import { list } from "@/mixins";
 import { refundList } from "@/api/refund";
+import { getForwardListByOrder } from "@/api/order";
 export default {
   name: "refundList",
   components: {},
   mixins: [list],
   data() {
     return {
+      //关联原单的冲销记录,ssh 冲销单功能
+      forwardList: []
     };
   },
   onPullDownRefresh() {
@@ -56,6 +73,7 @@ export default {
   onShow() {
   	this.resetList();
     this._list();
+    this._getForwardList();
   },
   methods: {
     init(){
@@ -68,6 +86,12 @@ export default {
       }
       this.$util.pageTo({url: '/pagesOrder/refundDetail',query: {id: item.id}})
     },
+    goForwardDetail(item){
+      if(!item.forwardOrderId){
+        return
+      }
+      this.$util.pageTo({url: '/pagesOrder/detail',query: {id: item.forwardOrderId}})
+    },
     _list() {
       return refundList({
         page: this.list.page,
@@ -79,6 +103,17 @@ export default {
         }
       });
     },
+    /** 拉取当前订单关联的冲销记录,ssh 冲销单功能 */
+    _getForwardList() {
+      if(this.$util.isEmpty(this.option.orderSn)){
+        return
+      }
+      getForwardListByOrder({orderSn: this.option.orderSn}).then((res) => {
+        if(res.code == 1){
+          this.forwardList = res.data.list || []
+        }
+      })
+    },
   },
 };
 </script>
@@ -103,5 +138,16 @@ export default {
 				}
 			}
 		}
+		.forward-title{
+			padding: 20upx 30upx 10upx;
+			font-size: 28upx;
+			font-weight: bold;
+			color: #999999;
+		}
+		.forward-item{
+			.val{
+				color: #ff2842;
+			}
+		}
 	}
 </style>

+ 8 - 0
hdApp/src/api/purchase/index.js

@@ -32,6 +32,14 @@ export const getDetail = data => {
 	return https.get("/purchase/detail", data);
 };
 
+/**
+ * 按采购单查询关联的冲销记录,ssh 冲销单功能
+ * @param {Object} data { id: 采购单id }
+ */
+export const getForwardListByOrder = data => {
+	return https.get("/purchase/get-forward-list-by-order", data);
+};
+
 /** *
  * 采购订单详情
  */

+ 27 - 16
hdApp/src/pagesPurchase/orderItem.vue

@@ -18,24 +18,28 @@
     <view class="item-main" >
       <view class="item-info">
 
-        <view class="info-row">
-          <text class="info-label"> 创建:</text>
-          <text class="info-value">{{ info.addTime ? info.addTime.substring(5, 16) : '' }}</text>
-        </view>
+        <!-- 冲销单:创建/确认/单号/采购等明细都隐藏,中间用醒目红字提示,数量行照常展示 -->
+        <template v-if="info.forward != 1">
+          <view class="info-row">
+            <text class="info-label"> 创建:</text>
+            <text class="info-value">{{ info.addTime ? info.addTime.substring(5, 16) : '' }}</text>
+          </view>
 
-        <view class="info-row" v-if="info.payTime!='0000-00-00 00:00:00'">
-          <text class="info-label"> 确认:</text>
-          <text class="info-value">{{ info.payTime ? info.payTime.substring(5, 16) : '' }}</text>
-        </view>
+          <view class="info-row" v-if="info.payTime!='0000-00-00 00:00:00'">
+            <text class="info-label"> 确认:</text>
+            <text class="info-value">{{ info.payTime ? info.payTime.substring(5, 16) : '' }}</text>
+          </view>
 
-        <view class="info-row">
-          <text class="info-label"> 单号:</text>
-          <text class="info-value">{{ info.orderSn }} </text>
-        </view>
-        <view class="info-row">
-          <text class="info-label"> 采购:</text>
-          <text class="info-value">{{ info.shopAdminName }} </text>
-        </view>
+          <view class="info-row">
+            <text class="info-label"> 单号:</text>
+            <text class="info-value">{{ info.orderSn }} </text>
+          </view>
+          <view class="info-row">
+            <text class="info-label"> 采购:</text>
+            <text class="info-value">{{ info.shopAdminName }} </text>
+          </view>
+        </template>
+        <view class="forward-tag" v-else>冲销单</view>
         <view class="info-row">
           <text class="info-label"> 数量:</text>
           <text v-if="info.smallNum > 0" class="info-value">{{ info.bigNum }}/{{info.smallNum}}</text>
@@ -170,6 +174,13 @@ export default {
         font-weight: 400;
         color: #333333;
       }
+      /* 冲销单中间提示:ssh 冲销单功能,取代创建/确认/单号/采购等明细行 */
+      .forward-tag {
+        margin-top: 10upx;
+        font-size: 36upx;
+        font-weight: bold;
+        color: #ff2842;
+      }
     }
     .item-price {
       display: flex;

+ 9 - 3
hdApp/src/pagesPurchase/purDetails.vue

@@ -122,10 +122,11 @@
           <view>{{ getSendStatusText(detailInfo.sendStatus) }}</view>
         </view>
 
-        <view v-if="detailInfo.refundLog && detailInfo.refundLog == 1" class="order-info_box">
+        <!-- 累计退款 = 常规售后退款(tkPrice) + 冲销单退款(forwardPrice),任一有记录都展示本行;ssh 冲销单功能 -->
+        <view v-if="(detailInfo.refundLog && detailInfo.refundLog == 1) || detailInfo.hasForward == 1" class="order-info_box">
           <view>累计退款:</view>
-          <view style="color:red;font-weight:bold;font-size:30upx;">¥{{detailInfo.tkPrice?parseFloat(detailInfo.tkPrice):0}}</view>
-            <view class="price" v-if="detailInfo.refundLog && detailInfo.refundLog == 1">
+          <view style="color:red;font-weight:bold;font-size:30upx;">¥{{parseFloat((Number(detailInfo.tkPrice)||0) + (Number(detailInfo.forwardPrice)||0))}}</view>
+            <view class="price">
               <button @click="pageTo({url: '/pagesPurchase/refundList?id='+detailInfo.id})" class="admin-button-com">售后记录</button>
             </view>
         </view>
@@ -250,6 +251,11 @@
             <div class="tui-title">退款金额</div>
             <div class="detail-price_box" style="font-size: 28upx;">-¥{{ detailInfo.tkPrice?parseFloat(detailInfo.tkPrice):0}}</div>
           </tui-list-cell>
+          <!-- 冲销单退款:ssh 冲销单功能,与常规售后退款(tkPrice)分开展示 -->
+          <tui-list-cell class="line-cell" :hover="false" v-if="detailInfo.hasForward == 1">
+            <div class="tui-title">冲销金额</div>
+            <div class="detail-price_box" style="font-size: 28upx;">-¥{{ parseFloat(detailInfo.forwardPrice || 0) }}</div>
+          </tui-list-cell>
           <tui-list-cell class="line-cell" :hover="false" v-if="detailInfo.orderReachDiscountPrice > 0">
             <div class="tui-title">整单优惠</div>
             <div class="detail-price_box" style="font-size: 28upx;">-¥{{ detailInfo.orderReachDiscountPrice?parseFloat(detailInfo.orderReachDiscountPrice):0}}</div>

+ 43 - 2
hdApp/src/pagesPurchase/refundList.vue

@@ -14,20 +14,32 @@
 
 			  </view>
 			</block>
-			<block v-else>
-				<app-wrapper-empty title="暂无数据" :is-empty="$util.isEmpty(list.data)" />
+			<!-- 冲销记录:ssh 冲销单功能,展示在售后记录下方,单独一块(关联原单的冲销单,不分页) -->
+			<block v-if="!$util.isEmpty(forwardList)">
+				<view class="forward-title">冲销记录</view>
+				<view class="flex-space list forward-item" v-for="(item, index) in forwardList" :key="'forward'+index" @click="goForwardDetail(item)">
+					<view class="label">
+						<text>日期:{{item.addTime?item.addTime.substr(5,11):''}}</text>
+						<text>单号:{{item.forwardOrderSn}}</text>
+						<text>库存:{{item.forwardStock == 0 ? '已退回' : '未退回'}}</text>
+					</view>
+					<view class="flex val">-{{parseFloat(item.forwardPrice)}} <view class="iconfont iconxiangyou"></view> </view>
+				</view>
 			</block>
   </view>
 </template>
 <script>
 import { list } from "@/mixins";
 import { cgRefundList } from "@/api/cg-refund";
+import { getForwardListByOrder } from "@/api/purchase";
 export default {
   name: "refundList",
   components: {},
   mixins: [list],
   data() {
     return {
+      //关联原采购单的冲销记录,ssh 冲销单功能
+      forwardList: []
     };
   },
   onPullDownRefresh() {
@@ -52,6 +64,7 @@ export default {
   onShow() {
   	this.resetList();
     this.getRefundList();
+    this.getForwardList();
   },
   methods: {
     init(){
@@ -64,6 +77,12 @@ export default {
       }
       this.$util.pageTo({url: '/pagesPurchase/refundDetail',query: {id: item.id}})
     },
+    goForwardDetail(item){
+      if(!item.forwardCgId){
+        return
+      }
+      this.$util.pageTo({url: '/pagesPurchase/purDetails',query: {id: item.forwardCgId}})
+    },
     getRefundList() {
       return cgRefundList({
         page: this.list.page,
@@ -75,6 +94,17 @@ export default {
         }
       });
     },
+    /** 拉取当前采购单关联的冲销记录,ssh 冲销单功能 */
+    getForwardList() {
+      if(this.$util.isEmpty(this.option.id)){
+        return
+      }
+      getForwardListByOrder({id: this.option.id}).then((res) => {
+        if(res.code == 1){
+          this.forwardList = res.data.list || []
+        }
+      })
+    },
   },
 };
 </script>
@@ -99,5 +129,16 @@ export default {
 				}
 			}
 		}
+		.forward-title{
+			padding: 20upx 30upx 10upx;
+			font-size: 28upx;
+			font-weight: bold;
+			color: #999999;
+		}
+		.forward-item{
+			.val{
+				color: #ff2842;
+			}
+		}
 	}
 </style>

Some files were not shown because too many files changed in this diff