Преглед на файлове

跑腿运费设置和计算优化

ouyang преди 23 часа
родител
ревизия
7f0202904d
променени са 3 файла, в които са добавени 561 реда и са изтрити 133 реда
  1. 250 66
      hdApp/src/admin/home/psMethod.vue
  2. 1 0
      hdApp/src/api/ps-method/index.js
  3. 310 67
      mallApp/src/pages/billing/affirmMix.vue

+ 250 - 66
hdApp/src/admin/home/psMethod.vue

@@ -1,7 +1,10 @@
 <!--
   花店配送方式设置页
   用途:hdApp 门店设置-配送方式,读写 xhPsMethod / xhPsExplain / xhPsReduceRule
-  说明:与供货商 xhSh* 表分离,结算页等仍走 /sh-method/get-all-method
+  跑腿 style=2 规则摘要:
+  - 含花束且某花束包运费 → hsFreeKm/hsPerKmPrice;含花束且都不包运费 → 跑腿或自定义
+  - 不含花束:符合 xhPsReduceRule 免跑腿;不符合则跑腿或自定义
+  - calcType=0 时跑腿计算异常,最终用自定义计费兜底
 -->
 <template>  <view class="ps-method-page app-content">
     <!-- 顶部 Tab 切换 -->
@@ -26,33 +29,90 @@
           <input type="text" v-model="form.name" maxlength="4" @focus="form.name = ''" class="tui-input" placeholder="请填写名称,最多4个字" />
         </tui-list-cell>
 
-        <!-- 状态 -->
-        <tui-list-cell class="line-cell" :hover="false">
-          <view class="tui-title">状态</view>
-          <view class="btn-group">
-            <button
-              class="admin-button-com middle"
-              :class="[form.status == 1 ? 'blue' : 'default']"
-              @click="form.status = 1"
-            >
-              启用
-            </button>
-            <button
-              class="admin-button-com middle"
-              :class="[form.status == 0 ? 'blue' : 'default']"
-              @click="form.status = 0"
-              style="margin-left: 20upx;"
-            >
-              禁用
-            </button>
-          </view>
-        </tui-list-cell>
+        <!-- 跑腿 Tab:名称 → 算运费方式 → 绑定 → 状态 → 位置 -->
+        <block v-if="form.style === 2">
+          <tui-list-cell class="line-cell" :hover="false">
+            <view class="tui-title">算运费方式</view>
+            <view class="btn-group">
+              <button
+                class="admin-button-com middle"
+                :class="[form.calcType == 0 ? 'blue' : 'default']"
+                @click="changeCalcType(0)"
+              >
+                用跑腿
+              </button>
+              <button
+                class="admin-button-com middle"
+                :class="[form.calcType == 1 ? 'blue' : 'default']"
+                @click="changeCalcType(1)"
+                style="margin-left: 20upx;"
+              >
+                用自定义
+              </button>
+            </view>
+          </tui-list-cell>
 
-        <!-- 位置 -->
-        <tui-list-cell class="line-cell" :hover="false">
-          <view class="tui-title">位置</view>
-          <input type="number" v-model="form.sort" @focus="form.sort = ''" class="tui-input" placeholder="请填写顺序" />
-        </tui-list-cell>
+          <tui-list-cell v-if="form.calcType == 0" class="line-cell delivery-bind-cell" :hover="false">
+            <view class="tui-title">跑腿商户绑定</view>
+            <text class="delivery-bind-status">{{ deliveryBindText }}</text>
+            <button class="admin-button-com middle blue delivery-bind-btn" @click="goDeliveryManage">绑定设置</button>
+          </tui-list-cell>
+
+          <tui-list-cell class="line-cell" :hover="false">
+            <view class="tui-title">状态</view>
+            <view class="btn-group">
+              <button
+                class="admin-button-com middle"
+                :class="[form.status == 1 ? 'blue' : 'default']"
+                @click="changeStatus(1)"
+              >
+                启用
+              </button>
+              <button
+                class="admin-button-com middle"
+                :class="[form.status == 0 ? 'blue' : 'default']"
+                @click="changeStatus(0)"
+                style="margin-left: 20upx;"
+              >
+                禁用
+              </button>
+            </view>
+          </tui-list-cell>
+
+          <tui-list-cell class="line-cell" :hover="false">
+            <view class="tui-title">位置</view>
+            <input type="number" v-model="form.sort" @focus="form.sort = ''" class="tui-input" placeholder="请填写顺序" />
+          </tui-list-cell>
+        </block>
+
+        <!-- 非跑腿 Tab:名称 → 状态 → 位置 -->
+        <block v-else>
+          <tui-list-cell class="line-cell" :hover="false">
+            <view class="tui-title">状态</view>
+            <view class="btn-group">
+              <button
+                class="admin-button-com middle"
+                :class="[form.status == 1 ? 'blue' : 'default']"
+                @click="changeStatus(1)"
+              >
+                启用
+              </button>
+              <button
+                class="admin-button-com middle"
+                :class="[form.status == 0 ? 'blue' : 'default']"
+                @click="changeStatus(0)"
+                style="margin-left: 20upx;"
+              >
+                禁用
+              </button>
+            </view>
+          </tui-list-cell>
+
+          <tui-list-cell class="line-cell" :hover="false">
+            <view class="tui-title">位置</view>
+            <input type="number" v-model="form.sort" @focus="form.sort = ''" class="tui-input" placeholder="请填写顺序" />
+          </tui-list-cell>
+        </block>
 
         <!-- 送货 / 快递 特有字段 -->
         <block v-if="form.style === 0 || form.style === 4">
@@ -98,19 +158,35 @@
           </tui-list-cell>
         </block>
 
-        <!-- 跑腿 特有字段 -->
+        <!-- 跑腿 特有字段(花束运费、满减、自定义计费等) -->
         <block v-if="form.style === 2">
-          <!-- 跑腿商户绑定:跳转跑腿管理页授权/绑定门店 -->
-          <tui-list-cell class="line-cell delivery-bind-cell" :hover="false">
-            <view class="tui-title">跑腿商户绑定</view>
-            <text class="delivery-bind-status">{{ deliveryBindText }}</text>
-            <button class="admin-button-com middle blue delivery-bind-btn" @click="goDeliveryManage">绑定设置</button>
-          </tui-list-cell>
+          <!-- 花束包配送运费(xhPsMethod.hsFreeKm / hsPerKmPrice) -->
+          <view class="rule-section-wrapper">
+            <view class="section-header no-margin">
+              <text class="section-title">花束运费</text>
+            </view>
+            <tui-list-cell class="line-cell inner-cell" :hover="false">
+              <view class="tui-title">包配送</view>
+              <view class="input-row">
+                <input type="digit" v-model="form.hsFreeKm" @focus="form.hsFreeKm = ''" class="tui-input-sm" placeholder="0" />
+                <text class="unit-text">公里内免费配送</text>
+              </view>
+            </tui-list-cell>
+            <tui-list-cell class="line-cell inner-cell" :hover="false">
+              <view class="tui-title">{{ form.hsFreeKm || 0 }}公里以上</view>
+              <view class="input-row">
+                <text class="unit-text">每公里加收</text>
+                <input type="digit" v-model="form.hsPerKmPrice" @focus="form.hsPerKmPrice = ''" class="tui-input-sm" placeholder="0.00" />
+                <text class="unit-text">元</text>
+              </view>
+            </tui-list-cell>
+            <view class="section-tip">含包运费花束时按以上规则计费;花束都不包运费时按跑腿或自定义收费</view>
+          </view>
 
-          <!-- 满减规则圈起来 -->
+          <!-- 花材满减规则:仅订单不含花束时生效 -->
           <view class="rule-section-wrapper">
             <view class="section-header no-margin">
-              <text class="section-title">满减规则</text>
+              <text class="section-title">花材满减规则</text>
               <button class="admin-button-com mini blue" @click="addReduceRule">添加</button>
             </view>
             <view class="rule-list" v-if="form.reduceRules && form.reduceRules.length > 0">
@@ -130,41 +206,48 @@
               </view>
             </view>
             <view class="no-data-tip" v-else>暂无满减规则</view>
+            <view class="section-tip">仅纯花材订单生效:符合则免跑腿费;不符合再按跑腿或自定义计费</view>
           </view>
 
           <view class="notice-text">
-            不符合满减规则时,将通过跑腿计算运费,跑腿无法使用时,将使用自定义计费
+            <text v-if="form.calcType == 0">用跑腿计费时,若跑腿计算异常(未绑定/报价失败等),最终改用下方自定义计费</text>
+            <text v-else>用自定义计费:不包运费花束或未满减花材,均按下方自定义规则计距算费</text>
           </view>
 
-          <!-- 自定义计费 -->
-          <view class="section-header">
-            <text class="section-title">自定义计费</text>
-          </view>
-          <tui-list-cell class="line-cell" :hover="false">
-            <view class="tui-title">起步价</view>
-            <view class="input-row">
-              <input type="digit" v-model="form.startKm" @focus="form.startKm = ''" class="tui-input-sm" placeholder="0" />
-              <text class="unit-text">km内</text>
-              <input type="digit" v-model="form.startPrice" @focus="form.startPrice = ''" class="tui-input-sm" placeholder="0.00" />
-              <text class="unit-text">元</text>
-            </view>
-          </tui-list-cell>
-          <tui-list-cell class="line-cell" :hover="false">
-            <view class="tui-title">超出起步</view>
-            <view class="input-row">
-              <text class="unit-text">每公里加</text>
-              <input type="digit" v-model="form.perKmPrice" @focus="form.perKmPrice = ''" class="tui-input-sm" placeholder="0.00" />
-              <text class="unit-text">元</text>
-            </view>
-          </tui-list-cell>
-          <tui-list-cell class="line-cell" :hover="false">
-            <view class="tui-title">临时涨价</view>
-            <view class="input-row">
-              <input type="digit" v-model="form.changeRate" @focus="form.changeRate = ''" class="tui-input-sm" placeholder="0" />
-              <text class="unit-text">%</text>
-              <text class="unit-text">填负值时为降价</text>
+          <!-- 自定义计费:calcType=1 主路径;calcType=0 时跑腿异常最终兜底 -->
+          <view class="rule-section-wrapper">
+            <view class="section-header no-margin">
+              <text class="section-title">自定义计费</text>
+              <text v-if="form.calcType == 1" class="section-badge">当前生效</text>
+              <text v-else class="section-badge">跑腿异常时兜底</text>
             </view>
-          </tui-list-cell>
+            <tui-list-cell class="line-cell inner-cell" :hover="false">
+              <view class="tui-title">起步价</view>
+              <view class="input-row">
+                <input type="digit" v-model="form.startKm" @focus="form.startKm = ''" class="tui-input-sm" placeholder="0" />
+                <text class="unit-text">km内</text>
+                <input type="digit" v-model="form.startPrice" @focus="form.startPrice = ''" class="tui-input-sm" placeholder="0.00" />
+                <text class="unit-text">元</text>
+              </view>
+            </tui-list-cell>
+            <tui-list-cell class="line-cell inner-cell" :hover="false">
+              <view class="tui-title">超出起步</view>
+              <view class="input-row">
+                <text class="unit-text">每公里加</text>
+                <input type="digit" v-model="form.perKmPrice" @focus="form.perKmPrice = ''" class="tui-input-sm" placeholder="0.00" />
+                <text class="unit-text">元</text>
+              </view>
+            </tui-list-cell>
+            <tui-list-cell class="line-cell inner-cell" :hover="false">
+              <view class="tui-title">临时涨价</view>
+              <view class="input-row">
+                <input type="digit" v-model="form.changeRate" @focus="form.changeRate = ''" class="tui-input-sm" placeholder="0" />
+                <text class="unit-text">%</text>
+                <text class="unit-text">填负值时为降价</text>
+              </view>
+            </tui-list-cell>
+            <view v-if="form.calcType == 0" class="section-tip">建议填写完整,否则跑腿异常时将无法算出运费</view>
+          </view>
         </block>
 
         <!-- 物流 特有字段 -->
@@ -265,6 +348,7 @@
 /**
  * 花店配送方式设置
  * 路由:admin/home/psMethod
+ * 跑腿保存:无论用跑腿还是用自定义,都必须完整填写自定义计费;启用还需绑定跑腿或自定义完整
  */
 import TuiListCell from '@/components/plugin/list-cell'
 import { getPsMethodConfig, savePsMethodConfig, getPsMethodSorts } from '@/api/ps-method'
@@ -297,6 +381,9 @@ export default {
         minNum: 0,
         unMeet: 0,
         unMeetFee: 0.00,
+        calcType: 0,
+        hsFreeKm: 0.00,
+        hsPerKmPrice: 0.00,
         startKm: 0.00,
         startPrice: 0.00,
         perKmPrice: 0.00,
@@ -337,7 +424,10 @@ export default {
   },
   onShow() {
     if (Number(this.form.style) === 2) {
-      this.loadDeliveryBindStatus()
+      // 从跑腿管理返回时同步绑定状态与服务端配置(取消授权可能已自动禁用)
+      this.loadDeliveryBindStatus().then(() => {
+        this.loadConfig(2)
+      })
     }
   },
   methods: {
@@ -389,6 +479,64 @@ export default {
       this.$util.pageTo({ url: '/admin/delivery/deliveryManage' })
     },
 
+    /**
+     * 切换算运费方式(不在此处校验自定义规则,保存时再提示)
+     * @param {number} calcType - 0 用跑腿,1 用自定义
+     */
+    changeCalcType(calcType) {
+      this.form.calcType = Number(calcType)
+    },
+
+    /**
+     * 切换启用/禁用;跑腿 style=2 启用前需绑定跑腿或配置自定义计费
+     * @param {number} status - 0 禁用,1 启用
+     */
+    changeStatus(status) {
+      const next = Number(status)
+      if (next === 1 && Number(this.form.style) === 2) {
+        // 启用前必须有完整自定义(兜底);用跑腿时还需绑定,或仅依赖自定义也可启用
+        if (!this.hasValidCustomConfig()) {
+          this.$msg('请先完整填写自定义计费规则后再启用')
+          return
+        }
+        if (!this.canEnableRunner()) {
+          this.$msg('请先绑定跑腿商户或配置完整的自定义计费后再启用')
+          return
+        }
+      }
+      this.form.status = next
+    },
+
+    /** 自定义计费三项是否均已有效填写 */
+    hasValidCustomConfig() {
+      const startKm = Number(this.form.startKm) || 0
+      const startPrice = Number(this.form.startPrice) || 0
+      const perKmPrice = Number(this.form.perKmPrice) || 0
+      return startKm > 0 && startPrice > 0 && perKmPrice > 0
+    },
+
+    /** 跑腿是否可启用:已绑定跑腿 或 自定义计费配置完整 */
+    canEnableRunner() {
+      return this.deliveryMerchantBound || this.hasValidCustomConfig()
+    },
+
+    /**
+     * 跑腿保存前校验:无论用跑腿还是用自定义,都必须完整填写自定义计费(跑腿异常时兜底)
+     * @returns {string} 错误文案,空字符串表示通过
+     */
+    validateRunnerBeforeSave() {
+      if (Number(this.form.style) !== 2) {
+        return ''
+      }
+      if (!this.hasValidCustomConfig()) {
+        return '请完整填写自定义计费规则'
+      }
+      if (Number(this.form.status) === 1 && !this.canEnableRunner()) {
+        return '请先绑定跑腿商户或配置完整的自定义计费后再启用'
+      }
+      return ''
+    },
+
     /**
      * 加载当前配送方式配置
      * @param {number} style - 配送类型 (0-4)
@@ -407,6 +555,9 @@ export default {
               minNum: data.minNum != null ? Number(data.minNum) : 0,
               unMeet: data.unMeet != null ? Number(data.unMeet) : 0,
               unMeetFee: data.unMeetFee != null ? parseFloat(data.unMeetFee) : 0.00,
+              calcType: data.calcType != null ? Number(data.calcType) : 0,
+              hsFreeKm: data.hsFreeKm != null ? parseFloat(data.hsFreeKm) : 0.00,
+              hsPerKmPrice: data.hsPerKmPrice != null ? parseFloat(data.hsPerKmPrice) : 0.00,
               startKm: data.startKm != null ? parseFloat(data.startKm) : 0.00,
               startPrice: data.startPrice != null ? parseFloat(data.startPrice) : 0.00,
               perKmPrice: data.perKmPrice != null ? parseFloat(data.perKmPrice) : 0.00,
@@ -568,6 +719,13 @@ export default {
           return;
         }
       }
+
+      const runnerErr = this.validateRunnerBeforeSave()
+      if (runnerErr) {
+        this.$msg(runnerErr)
+        return
+      }
+
       savePsMethodConfig(this.form).then(res => {
           if (res.code == 1) {
             this.$msg("保存成功");
@@ -725,6 +883,32 @@ export default {
   color: #333333;
 }
 
+.section-badge {
+  font-size: 24upx;
+  color: #3385ff;
+  margin-left: 12upx;
+}
+
+.section-tip {
+  font-size: 26upx;
+  color: #999999;
+  padding: 10upx 0 0;
+}
+
+.inner-cell {
+  margin-bottom: 0 !important;
+  border-radius: 0 !important;
+}
+
+.inner-cell:first-of-type {
+  border-radius: 12upx 12upx 0 0 !important;
+}
+
+.inner-cell:last-of-type {
+  border-radius: 0 0 12upx 12upx !important;
+  margin-bottom: 20upx !important;
+}
+
 /* 满减规则卡片 */
 .rule-list {
   margin-bottom: 20upx;

+ 1 - 0
hdApp/src/api/ps-method/index.js

@@ -1,6 +1,7 @@
 /**
  * 花店配送方式配置 API(xhPsMethod / xhPsExplain / xhPsReduceRule)
  * 用途:花店应用-门店设置-配送方式设置页,与供货商 xhSh* 表分离
+ * 跑腿 style=2 含 calcType、hsFreeKm、hsPerKmPrice 及自定义计费字段
  */
 import https from '@/plugins/luch-request_0.0.7/request'
 

+ 310 - 67
mallApp/src/pages/billing/affirmMix.vue

@@ -210,14 +210,14 @@
 					<view class="time-tip" v-if="form.sendType == 1">请在所选时间段内到店自取,逾期订单将自动取消</view>
 				</view>
 
-				<!-- 跑腿配送:说明 + 平台报价(无区块标题) -->
+				<!-- 跑腿配送:说明 + 平台报价/自定义计费(无区块标题) -->
 				<view class="affirm-card affirm-card--flat" v-if="form.sendType == 2">
-					<view v-if="form.openIntraCity == 0">
-						<text class="warn-text">运费自理或到付(请联系客服)</text>
-					</view>
-					<view v-else-if="form.openIntraCity == 2">
+					<view v-if="form.openIntraCity == 2">
 						<text class="error-text">跑腿配送暂不可用,请选其它方式</text>
 					</view>
+					<view v-else-if="!runnerFeeAvailable">
+						<text class="warn-text">运费自理或到付(请联系客服)</text>
+					</view>
 					<block v-else>
 						<view v-if="unMeetWarningText" class="delivery-note-box delivery-note-box--runner">
 							<view class="delivery-explain-line">
@@ -246,7 +246,8 @@
 								</view>
 							</view>
 						</view>
-						<view v-if="form.openIntraCity == 1">
+						<!-- 仅 calcType=0 且走平台报价时展示跑腿平台卡片 -->
+						<view v-if="showPlatformQuotes">
 							<view v-if="deliveryQuotes.length > 0" class="delivery-quotes-container">
 								<view class="delivery-quotes-grid">
 									<view
@@ -271,6 +272,9 @@
 								<text v-else class="quotes-hint">请先选择收货地址,将自动计算配送运费</text>
 							</view>
 						</view>
+						<view v-else-if="freightSource === 'customFallback'" class="quotes-loading">
+							<text class="quotes-hint">跑腿报价异常,已按门店自定义规则计费</text>
+						</view>
 					</block>
 				</view>
 
@@ -281,7 +285,8 @@
 						<view class="fee-value">
 							<text v-if="deliveryQuotesLoading" class="fee-value--loading">正在计算距离...</text>
 							<template v-else>
-								<text v-if="showDistance > 100" class="accent-text">{{ (showDistance / 1000).toFixed(1) }}公里</text>
+								<!-- 与计费一致展示到 0.01 公里,避免 6.95 被显示成 7.0 造成运费核对偏差 -->
+								<text v-if="showDistance > 100" class="accent-text">{{ (showDistance / 1000).toFixed(2) }}公里</text>
 								<text v-else class="accent-text">{{ showDistance }} 米</text>
 								<text v-if="showDeliveryPolicy && form.sendType != 2" class="delivery-policy-link" @click.stop="showDeliveryPolicyModal">免费配送条件</text>
 							</template>
@@ -491,11 +496,13 @@
 /**
  * 混合结算页(花束+花材),立即购买
  * 花束与花材合并下单,支持跑腿/自取/快递等多种配送及红包抵扣
+ * 跑腿运费:包运费花束 / 不包运费花束 / 纯花材满减;calcType 跑腿或自定义,跑腿异常兜底自定义
  */
 import SpriteIcon from "@/components/sprite-icon/index.vue";
 import productMins from "@/mixins/cgProduct";
 import { createMixOrder, inform } from "@/api/order";
 import { allDeliveryQuotes } from "@/api/express/delivery";
+import { getUserDistance } from "@/api/express";
 import { currentInfo } from "@/api/user";
 import { getInfo } from "@/api/shop";
 import { getAddressList } from "@/api/user-address";
@@ -568,6 +575,8 @@ export default {
 			selectedDeliveryIndex: -1,
 			selectedDeliveryData: null,
 			isFreeDelivery: false,
+			/** 跑腿运费来源:platform / custom / customFallback / bouquetIncluded / reduceFree */
+			freightSource: "",
 			deliveryPolicyList: [],
 			validAddress: false,
 			hbData: [],
@@ -746,18 +755,75 @@ export default {
 			});
 			return bigLength;
 		},
+		/** 最低消费数量用:花材扎数 + 花束份数(1 份花束计 1) */
+		mixOrderBigNum() {
+			let bouquetNum = 0;
+			(this.list || []).forEach((item) => {
+				if (this.isBouquetItem(item)) {
+					bouquetNum += Number(item.bigCount) || 0;
+				}
+			});
+			return (Number(this.mixItemBigNum) || 0) + bouquetNum;
+		},
 		/** 未达最低消费提示(来自批量预览缓存,与下单 calcFee 同源) */
 		unMeetWarningText() {
 			return (this.unMeetFeePreview && this.unMeetFeePreview.tip) || "";
 		},
 		displaySendCost() {
-			if (this.form.sendType == 2) {
-				if ([0, 2].includes(this.form.openIntraCity)) {
-					return false;
+			if (Number(this.form.sendType) !== 2) {
+				return false;
+			}
+			if (Number(this.form.openIntraCity) === 2) {
+				return false;
+			}
+			return this.runnerFeeAvailable;
+		},
+		/** 当前跑腿方式配置(xhPsMethod style=2) */
+		runnerPsConfig() {
+			return (this.psMethodList || []).find((item) => Number(item.style) === 2) || null;
+		},
+		/** 自定义计费三项是否完整 */
+		hasValidRunnerCustomConfig() {
+			const m = this.runnerPsConfig;
+			if (!m) return false;
+			return Number(m.startKm) > 0 && Number(m.startPrice) > 0 && Number(m.perKmPrice) > 0;
+		},
+		/** 是否可自动计跑腿/自定义运费 */
+		runnerFeeAvailable() {
+			if (Number(this.form.openIntraCity) === 2) return false;
+			if (Number(this.form.openIntraCity) === 1) return true;
+			return this.hasValidRunnerCustomConfig;
+		},
+		/**
+		 * 跑腿运费模式
+		 * bouquetIncluded=含包运费花束;bouquetNotIncluded=花束均不包运费;flowerOnly=纯花材
+		 */
+		freightCalcMode() {
+			if (Number(this.form.sendType) !== 2) {
+				return "";
+			}
+			const list = this.list || [];
+			const bouquets = list.filter((item) => this.isBouquetItem(item));
+			if (bouquets.length > 0) {
+				if (bouquets.some((item) => Number(item.freightType) === 1)) {
+					return "bouquetIncluded";
 				}
-				return true;
+				return "bouquetNotIncluded";
 			}
-			return false;
+			return "flowerOnly";
+		},
+		runnerCalcType() {
+			return Number((this.runnerPsConfig && this.runnerPsConfig.calcType) || 0);
+		},
+		/** 是否展示跑腿平台报价卡片 */
+		showPlatformQuotes() {
+			if (!this.runnerFeeAvailable) return false;
+			if (this.freightCalcMode === "bouquetIncluded") return false;
+			if (this.runnerCalcType === 1) return false;
+			if (["custom", "customFallback", "bouquetIncluded", "reduceFree"].includes(this.freightSource)) {
+				return false;
+			}
+			return Number(this.form.openIntraCity) === 1;
 		},
 		showDeliveryPolicy() {
 			if (!this.shopInfo) return false;
@@ -858,15 +924,31 @@ export default {
 				return Number(a.id || 0) - Number(b.id || 0);
 			});
 		},
-		/** 跑腿配送说明主文案 */
+		/** 跑腿配送说明主文案(随运费模式变化) */
 		runnerDeliveryNoteText() {
+			const mode = this.freightCalcMode;
+			if (mode === "bouquetIncluded") {
+				return "订单含包运费花束,按花束包配送规则计费";
+			}
+			if (mode === "bouquetNotIncluded") {
+				return this.runnerCalcType === 1
+					? "花束不包运费,按门店自定义规则计费"
+					: "花束不包运费,按跑腿平台报价计费;报价异常时按自定义规则兜底";
+			}
 			if (this.runnerReduceRules.length > 0) {
-				return "配送运费按跑腿平台报价计算,满足下方满减规则可免跑腿费。具体运费将在填写地址后自动计算";
+				return this.runnerCalcType === 1
+					? "纯花材订单满足下方满减可免跑腿费,否则按自定义规则计费"
+					: "纯花材订单满足下方满减可免跑腿费,否则按跑腿报价;报价异常时按自定义规则兜底";
 			}
-			return "配送运费按跑腿平台报价计算,具体运费将在填写地址后自动计算";
+			return this.runnerCalcType === 1
+				? "配送运费按门店自定义规则计算"
+				: "配送运费按跑腿平台报价计算,异常时按自定义规则兜底";
 		},
-		/** 跑腿免费配送条件(页内直接展示,来自 xhPsReduceRule) */
+		/** 花材满减规则文案:仅纯花材订单展示 */
 		inlineDeliveryPolicyLines() {
+			if (this.freightCalcMode !== "flowerOnly") {
+				return [];
+			}
 			return this.runnerReduceRules
 				.map((rule) => this.formatReduceRuleLine(rule))
 				.filter(Boolean);
@@ -979,8 +1061,8 @@ export default {
 			this.form.showAddress = item.showAddress || "";
 			this.validAddress = !this.$util.isEmpty(item.lat) && !this.$util.isEmpty(item.long);
 
-			if (this.form.sendType == 2 && Number(this.form.openIntraCity) == 1) {
-				this.getDeliveryQuotes();
+			if (this.form.sendType == 2 && this.runnerFeeAvailable) {
+				this.refreshRunnerFreight();
 			}
 		},
 		/** 启用且按 sort 排序的配送方式列表 */
@@ -1009,13 +1091,18 @@ export default {
 			} else {
 				this.applyCurrentUnMeetFee();
 				this.ensureDefaultAddressIfNeeded();
+				// 配送方式配置加载完成后,若已是跑腿则按新规则重算运费
+				if (Number(this.form.sendType) === 2 && this.validAddress && this.runnerFeeAvailable) {
+					this.refreshRunnerFreight();
+				}
 			}
 		},
 		/** 批量预览接口入参(商品金额 + 花材扎数) */
 		getUnMeetFeeBatchParams() {
 			return {
 				itemTotalAmount: Number(this.mixGoodsPrice) || 0,
-				bigNum: Number(this.mixItemBigNum) || 0
+				// 附加费最低消费数量:花束份数一并计入,避免「还差 N 扎」少算已购花束
+				bigNum: Number(this.mixOrderBigNum) || 0
 			};
 		},
 		/** 写入各 style 附加费缓存并应用到当前购买方式 */
@@ -1106,9 +1193,8 @@ export default {
 					this.shopInfo = { ...this.shopInfo, ...intraShop };
 					this.form.openIntraCity = intraShop.openIntraCity;
 
-					if (Number(this.form.openIntraCity) == 1 && this.form.sendType == 2 && this.validAddress) {
-						this.getDeliveryQuotes();
-						this.checkFreeDelivery();
+					if (this.form.sendType == 2 && this.validAddress && this.runnerFeeAvailable) {
+						this.refreshRunnerFreight();
 					}
 					this.getHbData();
 				}
@@ -1350,40 +1436,35 @@ export default {
 			}
 			return `${freeKm}公里内免跑腿费`;
 		},
-		/** 跑腿模式下商品/距离变化后重算是否免跑腿费 */
+		/** 商品金额/扎数变化后:纯花材可能影响满减,需重算跑腿费 */
 		recheckRunnerFreeDelivery() {
-			if (Number(this.form.sendType) !== 2 || !this.selectedDeliveryData) {
+			if (Number(this.form.sendType) !== 2 || !this.validAddress || !this.runnerFeeAvailable) {
 				return;
 			}
-			this.checkFreeDelivery();
+			if (this.freightCalcMode === "flowerOnly") {
+				this.refreshRunnerFreight();
+			}
 		},
 		/**
-		 * 跑腿是否免配送费:按当前购买方式 xhPsReduceRule 判断
-		 * 规则:满 meetNum 扎且满 meetAmount 元,且在 freeKm 公里内
+		 * 纯花材是否命中 xhPsReduceRule 免跑腿(含花束订单不读满减)
 		 */
 		checkFreeDelivery() {
-			if (this.displaySendCost == false) {
+			if (Number(this.form.sendType) !== 2 || this.freightCalcMode !== "flowerOnly") {
 				this.isFreeDelivery = false;
 				return false;
 			}
-
-			if (Number(this.form.sendType) !== 2) {
+			if (!this.displaySendCost) {
 				this.isFreeDelivery = false;
 				return false;
 			}
-
 			const distanceInMeters = Number(this.showDistance) || 0;
 			const flowerCount = Number(this.mixItemBigNum) || 0;
 			const flowerPrice = Number(this.mixGoodsPrice) || 0;
-			const rules = this.runnerReduceRules;
-
-			for (const rule of rules) {
+			for (const rule of this.runnerReduceRules) {
 				const meetNum = Number(rule.meetNum) || 0;
 				const meetAmount = Number(rule.meetAmount) || 0;
 				const freeKm = Number(rule.freeKm) || 0;
-				if (freeKm <= 0) {
-					continue;
-				}
+				if (freeKm <= 0) continue;
 				const freeKmInMeters = freeKm * 1000;
 				const numOk = meetNum <= 0 || flowerCount >= meetNum;
 				const amountOk = meetAmount <= 0 || flowerPrice >= meetAmount;
@@ -1392,10 +1473,80 @@ export default {
 					return true;
 				}
 			}
-
 			this.isFreeDelivery = false;
 			return false;
 		},
+		/** 花束包运费:hsFreeKm 内 0,超出按 hsPerKmPrice 每公里(向上取整) */
+		calcBouquetFreight(distanceMeters) {
+			const m = this.runnerPsConfig || {};
+			const freeMeters = (Number(m.hsFreeKm) || 0) * 1000;
+			const perKm = Number(m.hsPerKmPrice) || 0;
+			const dist = Number(distanceMeters) || 0;
+			if (dist <= freeMeters) return 0;
+			const km = Math.ceil((dist - freeMeters) / 1000);
+			return Math.round(km * perKm * 100) / 100;
+		},
+		/**
+		 * 自定义计费:起步价 + 超出公里 * 每公里价,再乘临时涨降价
+		 * 超出部分不足 1 公里按 1 公里计(向上取整)
+		 * 例:5km 内 2 元、超出每公里 1 元、涨 3%、距离 7km → (2+2)*1.03=4.12
+		 */
+		calcCustomFreight(distanceKm) {
+			const m = this.runnerPsConfig || {};
+			const startKm = Number(m.startKm) || 0;
+			const startPrice = Number(m.startPrice) || 0;
+			const perKmPrice = Number(m.perKmPrice) || 0;
+			const changeRate = Number(m.changeRate) || 0;
+			if (startKm <= 0 || startPrice <= 0 || perKmPrice <= 0) {
+				return null;
+			}
+			// 与后端一致,公里保留两位再计费
+			const km = Math.round((Number(distanceKm) || 0) * 100) / 100;
+			if (km <= 0) return null;
+			let sendCost = startPrice;
+			if (km > startKm) {
+				const extra = Math.round((km - startKm) * 100) / 100;
+				// 不足 1 公里按 1 公里
+				const billableExtra = Math.ceil(extra - 1e-9);
+				sendCost += billableExtra * perKmPrice;
+			}
+			if (changeRate !== 0) {
+				sendCost = sendCost * (1 + changeRate / 100);
+			}
+			return Math.round(sendCost * 100) / 100;
+		},
+		/** 拉取门店到收货地址距离(米;勿用 showDistance,其为公里字符串) */
+		fetchDeliveryDistanceMeters() {
+			const lat = this.form.lat || this.userInfo.lat;
+			const lng = this.form.long || this.userInfo.long;
+			return getUserDistance({ lat, lng }).then((res) => {
+				if (res.code == 1 && res.data) {
+					const meters = Number(res.data.distance);
+					return Number.isFinite(meters) && meters > 0 ? meters : 0;
+				}
+				return 0;
+			}).catch(() => 0);
+		},
+		/** 应用自定义/兜底运费到表单 */
+		applyCustomFreightResult(distanceMeters, source) {
+			const distanceKm = (Number(distanceMeters) || 0) / 1000;
+			const fee = this.calcCustomFreight(distanceKm);
+			this.showDistance = distanceMeters;
+			this.deliveryQuotes = [];
+			this.selectedDeliveryIndex = -1;
+			this.selectedDeliveryData = null;
+			if (fee == null) {
+				this.form.sendCost = 0;
+				this.freightSource = "";
+				this.isFreeDelivery = false;
+				this.$msg("自定义计费规则未配置完整,无法计算运费");
+				return false;
+			}
+			this.form.sendCost = fee;
+			this.freightSource = source;
+			this.isFreeDelivery = fee === 0;
+			return true;
+		},
 		selTimeFn(e) {
 			this.form.reachDate = e.detail.value;
 		},
@@ -1487,9 +1638,8 @@ export default {
 				this.ensureDefaultAddressIfNeeded();
 			}
 			if (num == 2) {
-				if (Number(this.form.openIntraCity) == 1 && this.validAddress) {
-					this.getDeliveryQuotes();
-					this.checkFreeDelivery();
+				if (this.runnerFeeAvailable && this.validAddress) {
+					this.refreshRunnerFreight();
 				}
 			} else {
 				this.deliveryQuotes = [];
@@ -1498,6 +1648,8 @@ export default {
 				this.form.sendCost = 0;
 				this.deliveryQuotesLoading = false;
 				this.showDistance = 0;
+				this.freightSource = "";
+				this.isFreeDelivery = false;
 			}
 			this.applyCurrentUnMeetFee();
 		},
@@ -1521,25 +1673,97 @@ export default {
 				}
 			};
 		},
-		getDeliveryQuotes() {
+		/**
+		 * 按规则刷新跑腿运费:包运费花束 / 花材满减 / 跑腿报价 / 自定义与异常兜底
+		 */
+		refreshRunnerFreight() {
+			if (Number(this.form.sendType) !== 2) {
+				return;
+			}
+			if (!this.runnerFeeAvailable) {
+				return;
+			}
 			if (this.$util.isEmpty(this.userInfo.lat) || this.$util.isEmpty(this.userInfo.long)) {
 				this.form.sendCost = 0;
+				this.freightSource = "";
 				this.$msg("请填写地址哈");
 				return;
 			}
+			const mode = this.freightCalcMode;
+			this.deliveryQuotesLoading = true;
+			this.isFreeDelivery = false;
+			this.freightSource = "";
+
+			// A:含包运费花束 → 只计距 + 花束运费
+			if (mode === "bouquetIncluded") {
+				this.deliveryQuotes = [];
+				this.selectedDeliveryIndex = -1;
+				this.selectedDeliveryData = null;
+				this.fetchDeliveryDistanceMeters().then((meters) => {
+					this.showDistance = meters;
+					const fee = this.calcBouquetFreight(meters);
+					this.form.sendCost = fee;
+					this.freightSource = "bouquetIncluded";
+					this.isFreeDelivery = fee === 0;
+				}).finally(() => {
+					this.deliveryQuotesLoading = false;
+				});
+				return;
+			}
+
+			// 先取距离,再判满减 / 自定义 / 跑腿
+			this.fetchDeliveryDistanceMeters().then((meters) => {
+				this.showDistance = meters;
+				if (mode === "flowerOnly" && this.checkFreeDelivery()) {
+					this.form.sendCost = 0;
+					this.freightSource = "reduceFree";
+					this.deliveryQuotes = [];
+					this.selectedDeliveryIndex = -1;
+					this.selectedDeliveryData = null;
+					this.deliveryQuotesLoading = false;
+					return;
+				}
+				if (this.runnerCalcType === 1) {
+					this.applyCustomFreightResult(meters, "custom");
+					this.deliveryQuotesLoading = false;
+					return;
+				}
+				// calcType=0:先尝试平台报价,失败则自定义兜底
+				this.fetchPlatformQuotesWithFallback(meters);
+			}).catch(() => {
+				this.deliveryQuotesLoading = false;
+				if (this.hasValidRunnerCustomConfig) {
+					this.fetchDeliveryDistanceMeters().then((meters) => {
+						this.applyCustomFreightResult(meters, "customFallback");
+					});
+				} else {
+					this.$msg("获取距离失败");
+				}
+			});
+		},
+		/** 拉跑腿平台报价;失败或无可用平台时改用自定义兜底 */
+		fetchPlatformQuotesWithFallback(distanceMeters) {
 			const hasBouquet = (this.list || []).some((item) => this.isBouquetItem(item));
 			if (Number(this.allCount.weight) <= 0 && !hasBouquet) {
-				this.$msg("商品没有重量,无法使用跑腿");
-				return false;
+				this.deliveryQuotesLoading = false;
+				if (this.hasValidRunnerCustomConfig) {
+					this.applyCustomFreightResult(distanceMeters, "customFallback");
+				} else {
+					this.$msg("商品没有重量,无法使用跑腿");
+				}
+				return;
+			}
+			if (Number(this.form.openIntraCity) !== 1) {
+				this.deliveryQuotesLoading = false;
+				this.applyCustomFreightResult(distanceMeters, "customFallback");
+				return;
 			}
+
 			this.deliveryQuotes = [];
 			this.selectedDeliveryIndex = -1;
 			this.selectedDeliveryData = null;
-			this.showDistance = 0;
-			this.deliveryQuotesLoading = true;
-
 			const hasItem = (this.list || []).some((item) => !this.isBouquetItem(item));
-			const buyType = hasItem || (hasItem && hasBouquet) ? "huaCai" : hasBouquet ? "huaShu" : "huaCai";
+			const buyType = hasItem ? "huaCai" : hasBouquet ? "huaShu" : "huaCai";
 			const { account, hdId } = this.syncShopContext();
 			allDeliveryQuotes({
 				buyType,
@@ -1556,18 +1780,15 @@ export default {
 						.filter((item) => item.isAble !== false)
 						.map((item) => {
 							const price = item.price ? (item.price / 100).toFixed(1) : "0.0";
-
 							let displayName = item.name;
 							let bracketContent = "";
 							if (displayName && displayName.includes("(")) {
 								bracketContent = displayName.split("(")[1].replace(")", "");
 								displayName = displayName.split("(")[0].trim();
 							}
-
 							if (item.en_name == "didi" && bracketContent != "") {
 								item.type = bracketContent;
 							}
-
 							return {
 								...item,
 								displayName,
@@ -1576,25 +1797,25 @@ export default {
 								bracketContent
 							};
 						});
-
 					const firstAvailableIndex = this.deliveryQuotes.findIndex((item) => item.isAble);
 					if (firstAvailableIndex !== -1) {
 						this.selectDelivery(this.deliveryQuotes[firstAvailableIndex], firstAvailableIndex);
-					} else {
-						this.form.sendCost = 0;
-						this.showDistance = 0;
+						this.freightSource = "platform";
+						return;
 					}
-				} else {
-					this.showDistance = 0;
-					uni.showToast({ title: "没有跑腿可用", icon: "none" });
 				}
+				// 无可用平台 → 自定义兜底
+				this.applyCustomFreightResult(distanceMeters || this.showDistance, "customFallback");
 			}).catch(() => {
-				this.showDistance = 0;
-				uni.showToast({ title: "获取报价失败", icon: "none" });
+				this.applyCustomFreightResult(distanceMeters || this.showDistance, "customFallback");
 			}).finally(() => {
 				this.deliveryQuotesLoading = false;
 			});
 		},
+		/** 兼容旧调用名 */
+		getDeliveryQuotes() {
+			this.refreshRunnerFreight();
+		},
 		selectDelivery(item, index) {
 			if (!item.isAble) {
 				return;
@@ -1602,8 +1823,18 @@ export default {
 			this.selectedDeliveryIndex = index;
 			this.selectedDeliveryData = item;
 			this.form.sendCost = item.priceNumber;
-			this.showDistance = item.distance || 0;
-			this.checkFreeDelivery();
+			this.showDistance = item.distance || this.showDistance || 0;
+			this.freightSource = "platform";
+			// 平台报价场景下花材满减已在 refresh 前置处理;此处仅同步免运标记
+			if (this.freightCalcMode === "flowerOnly") {
+				this.checkFreeDelivery();
+				if (this.isFreeDelivery) {
+					this.form.sendCost = 0;
+					this.freightSource = "reduceFree";
+				}
+			} else {
+				this.isFreeDelivery = false;
+			}
 		},
 		confirmForm() {
 			const hdId = this.option.hdId ? this.option.hdId : uni.getStorageSync("hdId") || 0;
@@ -1693,10 +1924,14 @@ export default {
 			if (this.isFreeDelivery) {
 				params.sendCost = 0;
 			}
-			if (this.form.sendType == 2 && this.selectedDeliveryData) {
+			if (this.form.sendType == 2) {
+				params.freightSource = this.freightSource || "";
+				params.deliveryDistance = this.showDistance || 0;
+			}
+			if (this.form.sendType == 2 && this.freightSource === "platform" && this.selectedDeliveryData) {
 				params.deliveryPlatform = this.selectedDeliveryData.en_name;
 				params.deliveryPrice = this.selectedDeliveryData.priceNumber;
-				params.deliveryDistance = this.selectedDeliveryData.distance || 0;
+				params.deliveryDistance = this.selectedDeliveryData.distance || this.showDistance || 0;
 				params.deliveryBracketContent = this.selectedDeliveryData.bracketContent;
 			}
 			if (this.form.sendType == 2 || this.form.sendType == 0) {
@@ -1706,12 +1941,20 @@ export default {
 				}
 			}
 			if (this.form.sendType == 2) {
-				if (Number(this.form.sendCost) < 0) {
+				if (Number(this.form.openIntraCity) == 2 || !this.runnerFeeAvailable) {
+					this.$msg("跑腿不可用,请用其它方式");
+					return false;
+				}
+				if (this.freightSource === "platform" && !this.selectedDeliveryData) {
+					this.$msg("请选择跑腿");
+					return false;
+				}
+				if (!this.freightSource && Number(this.form.sendCost) < 0) {
 					this.$msg("没有获取到跑腿费");
 					return false;
 				}
-				if (Number(this.form.openIntraCity) == 2) {
-					this.$msg("跑腿不可用,请用其它方式");
+				if (!this.isFreeDelivery && !this.freightSource && Number(this.form.sendCost) === 0 && this.freightCalcMode !== "bouquetIncluded") {
+					this.$msg("没有获取到跑腿费");
 					return false;
 				}
 			}