yjwang 11 часов назад
Родитель
Сommit
6a4a2ae404
1 измененных файлов с 383 добавлено и 20 удалено
  1. 383 20
      src/views/system/pointsRule/index.vue

+ 383 - 20
src/views/system/pointsRule/index.vue

@@ -12,9 +12,16 @@
             <span class="header-sub">消费送积分体系规则管理</span>
             <span class="header-sub">消费送积分体系规则管理</span>
           </div>
           </div>
         </div>
         </div>
-        <el-tag :type="form.enabled === 1 ? 'success' : 'info'" size="small" effect="plain">
-          {{ form.enabled === 1 ? '已启用' : '已禁用' }}
-        </el-tag>
+        <div class="header-right">
+          <span v-if="meta.updateTime" class="header-meta">
+            <i class="el-icon-time"></i>
+            最后更新:<b>{{ meta.updateTime }}</b>
+            <span v-if="meta.updateBy"> · {{ meta.updateBy }}</span>
+          </span>
+          <el-tag :type="form.enabled === 1 ? 'success' : 'info'" size="small" effect="plain">
+            {{ form.enabled === 1 ? '已启用' : '已禁用' }}
+          </el-tag>
+        </div>
       </div>
       </div>
 
 
       <el-form ref="form" :model="form" :rules="rules" label-width="150px" size="small">
       <el-form ref="form" :model="form" :rules="rules" label-width="150px" size="small">
@@ -119,8 +126,16 @@
             <span class="section-icon deduct"><i class="el-icon-s-finance"></i></span>
             <span class="section-icon deduct"><i class="el-icon-s-finance"></i></span>
             <span class="section-label">积分抵扣配置</span>
             <span class="section-label">积分抵扣配置</span>
             <span class="section-desc">设置积分抵现规则</span>
             <span class="section-desc">设置积分抵现规则</span>
+            <el-switch
+              v-model="form.deductEnabled"
+              :active-value="1"
+              :inactive-value="0"
+              active-color="#13ce66"
+              inactive-color="#ff4949"
+              class="section-switch"
+            />
           </div>
           </div>
-          <div class="section-body">
+          <div class="section-body" :class="{ 'section-disabled': form.deductEnabled === 0 }">
             <el-form-item label="积分支付比例" prop="payRate">
             <el-form-item label="积分支付比例" prop="payRate">
               <el-input-number
               <el-input-number
                 v-model="form.payRate"
                 v-model="form.payRate"
@@ -136,17 +151,21 @@
               </span>
               </span>
             </el-form-item>
             </el-form-item>
 
 
-            <el-form-item label="最高抵扣积分" prop="maxDeduct">
+            <el-form-item label="最高抵扣比例" prop="maxDeductPercent">
               <el-input-number
               <el-input-number
-                v-model="form.maxDeduct"
+                v-model="form.maxDeductPercent"
+                :precision="0"
                 :min="0"
                 :min="0"
-                :max="999999"
-                :step="10"
+                :max="100"
+                :step="5"
                 class="input-number-sm"
                 class="input-number-sm"
               />
               />
-              <span class="tip-box tip-default">
-                <i class="el-icon-info"></i>
-                每笔订单最多可用 <b>{{ form.maxDeduct || '不限' }}</b> 积分抵扣{{ form.maxDeduct > 0 ? ',超出部分不可使用' : '' }}
+              <span class="unit-suffix">%</span>
+              <span class="tip-box tip-warning">
+                <i class="el-icon-warning-outline"></i>
+                {{ form.maxDeductPercent > 0
+                  ? '每笔订单积分抵扣金额不超过订单金额的 ' + form.maxDeductPercent + '%,防止 0 元购买'
+                  : '不限制抵扣比例,存在 0 元购买风险,建议设置为 50%~90%' }}
               </span>
               </span>
             </el-form-item>
             </el-form-item>
 
 
@@ -164,6 +183,23 @@
                 {{ form.minDeductAmount > 0 ? '订单满 ' + form.minDeductAmount + ' 元才可使用积分抵扣' : '不限制,任意金额均可使用积分抵扣' }}
                 {{ form.minDeductAmount > 0 ? '订单满 ' + form.minDeductAmount + ' 元才可使用积分抵扣' : '不限制,任意金额均可使用积分抵扣' }}
               </span>
               </span>
             </el-form-item>
             </el-form-item>
+
+            <el-form-item label="最低起用积分" prop="minUseIntegral">
+              <el-input-number
+                v-model="form.minUseIntegral"
+                :precision="0"
+                :min="0"
+                :max="999999"
+                :step="10"
+                class="input-number-sm"
+              />
+              <span class="tip-box tip-default">
+                <i class="el-icon-info"></i>
+                {{ form.minUseIntegral > 0
+                  ? '用户可用积分不足 ' + form.minUseIntegral + ' 时不可抵扣,防止零碎积分滥用'
+                  : '不限制,1 积分即可起用' }}
+              </span>
+            </el-form-item>
           </div>
           </div>
         </div>
         </div>
 
 
@@ -231,6 +267,64 @@
         </div>
         </div>
       </el-form>
       </el-form>
 
 
+      <!-- 5. 积分计算示例 -->
+      <div class="section-block section-preview">
+        <div class="section-header">
+          <span class="section-icon preview"><i class="el-icon-cpu"></i></span>
+          <span class="section-label">积分计算示例</span>
+          <span class="section-desc">实时模拟规则效果,计算逻辑与后端一致</span>
+        </div>
+        <div class="section-body preview-body">
+          <div class="preview-inputs">
+            <div class="preview-input-item">
+              <span class="preview-input-label">消费金额</span>
+              <el-input-number
+                v-model="preview.amount"
+                :precision="2"
+                :min="0"
+                :step="10"
+                size="small"
+                class="preview-input-num"
+              />
+              <span class="preview-unit">元</span>
+            </div>
+            <div class="preview-input-item">
+              <span class="preview-input-label">用户积分余额</span>
+              <el-input-number
+                v-model="preview.integral"
+                :min="0"
+                :step="100"
+                size="small"
+                class="preview-input-num"
+              />
+              <span class="preview-unit">积分</span>
+            </div>
+          </div>
+          <div class="preview-result">
+            <div class="result-card result-earn">
+              <div class="result-label"><i class="el-icon-coin"></i> 本次消费可获积分</div>
+              <div class="result-value">{{ previewResult.earnIntegral }}</div>
+              <div class="result-tip">消费 {{ previewAmountText }} 元 × {{ form.rate || 0 }}(四舍五入取整)</div>
+            </div>
+            <div class="result-arrow"><i class="el-icon-right"></i></div>
+            <div class="result-card result-deduct">
+              <div class="result-label"><i class="el-icon-s-finance"></i> 积分可抵扣金额</div>
+              <div class="result-value">¥{{ previewResult.deductAmount }}</div>
+              <div class="result-tip">{{ previewResult.deductTip }}</div>
+            </div>
+            <div class="result-arrow"><i class="el-icon-right"></i></div>
+            <div class="result-card result-pay">
+              <div class="result-label"><i class="el-icon-wallet"></i> 订单实付金额</div>
+              <div class="result-value">¥{{ previewResult.payAmount }}</div>
+              <div class="result-tip">消费金额 − 抵扣金额</div>
+            </div>
+          </div>
+          <div v-if="form.enabled === 0" class="preview-warn">
+            <i class="el-icon-warning"></i> 当前规则已禁用,实际下单时不会发放积分
+          </div>
+        </div>
+      </div>
+
       <!-- 底部操作 -->
       <!-- 底部操作 -->
       <div class="form-footer">
       <div class="form-footer">
         <el-button size="medium" @click="handleReset" icon="el-icon-refresh">重 置</el-button>
         <el-button size="medium" @click="handleReset" icon="el-icon-refresh">重 置</el-button>
@@ -276,6 +370,14 @@ export default {
       productLoading: false,
       productLoading: false,
       categoryOptions: [],
       categoryOptions: [],
       productOptions: [],
       productOptions: [],
+      meta: {
+        updateTime: '',
+        updateBy: ''
+      },
+      preview: {
+        amount: 100,
+        integral: 500
+      },
       form: {
       form: {
         rate: 1.00,
         rate: 1.00,
         payRate: 100,
         payRate: 100,
@@ -285,8 +387,10 @@ export default {
         refundDeduct: 1,
         refundDeduct: 1,
         dailyLimit: 0,
         dailyLimit: 0,
         monthlyLimit: 0,
         monthlyLimit: 0,
-        maxDeduct: 0,
+        maxDeductPercent: 50,
         minDeductAmount: 0,
         minDeductAmount: 0,
+        minUseIntegral: 0,
+        deductEnabled: 1,
         enabled: 1
         enabled: 1
       },
       },
       rules: {
       rules: {
@@ -315,18 +419,92 @@ export default {
         monthlyLimit: [
         monthlyLimit: [
           { required: true, message: '请输入每月积分上限', trigger: 'blur' }
           { required: true, message: '请输入每月积分上限', trigger: 'blur' }
         ],
         ],
-        maxDeduct: [
-          { required: true, message: '请输入最高抵扣积分', trigger: 'blur' }
+        maxDeductPercent: [
+          { required: true, message: '请输入最高抵扣比例', trigger: 'blur' },
+          { type: 'number', min: 0, max: 100, message: '比例必须在 0~100 之间', trigger: 'blur' }
         ],
         ],
         minDeductAmount: [
         minDeductAmount: [
           { required: true, message: '请输入满减门槛', trigger: 'blur' }
           { required: true, message: '请输入满减门槛', trigger: 'blur' }
         ],
         ],
+        minUseIntegral: [
+          { required: true, message: '请输入最低起用积分', trigger: 'blur' },
+          { type: 'number', min: 0, message: '起用积分数不能为负', trigger: 'blur' }
+        ],
         enabled: [
         enabled: [
           { required: true, message: '请选择启用状态', trigger: 'change' }
           { required: true, message: '请选择启用状态', trigger: 'change' }
         ]
         ]
       }
       }
     }
     }
   },
   },
+  computed: {
+    previewAmountText() {
+      const v = Number(this.preview.amount) || 0
+      return v.toFixed(2)
+    },
+    previewResult() {
+      const amount = Number(this.preview.amount) || 0
+      const integral = Number(this.preview.integral) || 0
+      const rate = Number(this.form.rate) || 0
+      const payRate = Number(this.form.payRate) || 1
+      const maxDeductPercent = Number(this.form.maxDeductPercent) || 0
+      const minDeductAmount = Number(this.form.minDeductAmount) || 0
+      const minUseIntegral = Number(this.form.minUseIntegral) || 0
+      const deductEnabled = Number(this.form.deductEnabled)
+
+      // 发放积分:消费金额 × rate,四舍五入取整(与后端 setScale(0, HALF_UP) 一致)
+      const earnIntegral = Math.round(amount * rate)
+
+      // 积分抵扣功能关闭
+      if (deductEnabled === 0) {
+        return {
+          earnIntegral,
+          deductAmount: '0.00',
+          deductTip: '积分抵扣功能已关闭',
+          payAmount: amount.toFixed(2)
+        }
+      }
+
+      // 抵扣门槛校验:minDeductAmount=0 表示不限制
+      const canDeduct = minDeductAmount > 0 ? amount >= minDeductAmount : true
+      let usableIntegral = 0
+      let deductAmountVal = 0
+      let deductTip = ''
+
+      if (!canDeduct) {
+        deductTip = `未满 ${minDeductAmount.toFixed(2)} 元门槛,不可使用积分抵扣`
+      } else if (integral <= 0) {
+        deductTip = '用户无可用积分'
+      } else if (minUseIntegral > 0 && integral < minUseIntegral) {
+        // 最低起用积分校验:用户积分不足起用门槛,不可抵扣
+        deductTip = `用户积分 ${integral} 不足起用门槛 ${minUseIntegral},不可抵扣`
+      } else {
+        // 可用积分 = 用户全部积分(maxDeduct 已废弃,改用 maxDeductPercent 按比例限制)
+        usableIntegral = integral
+        // 抵扣金额 = 可用积分 / payRate,保留2位小数四舍五入(与后端 divide(payRate, 2, HALF_UP) 一致)
+        let deduct = usableIntegral / payRate
+        // 抵扣金额不超过订单金额(后端有此约束)
+        deduct = Math.min(deduct, amount)
+        // 最高抵扣比例限制:抵扣金额不超过订单金额的 maxDeductPercent%
+        if (maxDeductPercent > 0) {
+          const maxDeductAmount = amount * maxDeductPercent / 100
+          if (deduct > maxDeductAmount) {
+            deduct = maxDeductAmount
+            // 反推实际可用积分(向下取整,保证不超额)
+            usableIntegral = Math.floor(deduct * payRate)
+          }
+        }
+        deductAmountVal = deduct
+        const percentTip = maxDeductPercent > 0 ? `·比例上限${maxDeductPercent}%` : ''
+        deductTip = `可用 ${usableIntegral} 积分(用户${integral}${percentTip})÷ ${payRate}`
+      }
+
+      const deductAmount = deductAmountVal.toFixed(2)
+      // 实付金额 = 消费金额 - 抵扣金额,不低于 0
+      const payAmount = Math.max(amount - deductAmountVal, 0).toFixed(2)
+
+      return { earnIntegral, deductAmount, deductTip, payAmount }
+    }
+  },
   mounted() {
   mounted() {
     this.loadCategoryDict()
     this.loadCategoryDict()
     this.loadConfig()
     this.loadConfig()
@@ -352,15 +530,22 @@ export default {
             refundDeduct: config.refundDeduct ?? 1,
             refundDeduct: config.refundDeduct ?? 1,
             dailyLimit: config.dailyLimit ?? 0,
             dailyLimit: config.dailyLimit ?? 0,
             monthlyLimit: config.monthlyLimit ?? 0,
             monthlyLimit: config.monthlyLimit ?? 0,
-            maxDeduct: config.maxDeduct ?? 0,
+            maxDeductPercent: config.maxDeductPercent ?? 50,
             minDeductAmount: config.minDeductAmount ?? 0,
             minDeductAmount: config.minDeductAmount ?? 0,
+            minUseIntegral: config.minUseIntegral ?? 0,
+            deductEnabled: config.deductEnabled ?? 1,
             enabled: config.enabled ?? 1
             enabled: config.enabled ?? 1
           }
           }
+          // 保存配置元信息(来自 sys_config 表 BaseEntity 字段)
+          this.meta.updateTime = response.data.updateTime || ''
+          this.meta.updateBy = response.data.updateBy || ''
           if (config.excludeProductIds && config.excludeProductIds.length > 0) {
           if (config.excludeProductIds && config.excludeProductIds.length > 0) {
             this.loadSelectedProducts(config.excludeProductIds)
             this.loadSelectedProducts(config.excludeProductIds)
           }
           }
         } else {
         } else {
           this.configId = response.data ? response.data.configId : null
           this.configId = response.data ? response.data.configId : null
+          this.meta.updateTime = response.data ? (response.data.updateTime || '') : ''
+          this.meta.updateBy = response.data ? (response.data.updateBy || '') : ''
         }
         }
       }).catch(() => {
       }).catch(() => {
         this.$message.warning('配置加载失败,将使用默认值')
         this.$message.warning('配置加载失败,将使用默认值')
@@ -429,6 +614,8 @@ export default {
               this.configId = response.data ? response.data.configId : null
               this.configId = response.data ? response.data.configId : null
             }
             }
             this.$message.success('积分规则配置保存成功')
             this.$message.success('积分规则配置保存成功')
+            // 保存成功后重新加载以刷新更新时间/更新人
+            this.loadConfig()
           }
           }
           this.submitLoading = false
           this.submitLoading = false
         }).catch(() => {
         }).catch(() => {
@@ -452,8 +639,10 @@ export default {
           refundDeduct: 1,
           refundDeduct: 1,
           dailyLimit: 0,
           dailyLimit: 0,
           monthlyLimit: 0,
           monthlyLimit: 0,
-          maxDeduct: 0,
+          maxDeductPercent: 50,
           minDeductAmount: 0,
           minDeductAmount: 0,
+          minUseIntegral: 0,
+          deductEnabled: 1,
           enabled: 1
           enabled: 1
         }
         }
         this.categoryOptions = []
         this.categoryOptions = []
@@ -475,6 +664,9 @@ export default {
   padding: 0;
   padding: 0;
   border-bottom: none;
   border-bottom: none;
 }
 }
+.main-card >>> .el-card__body {
+  padding: 0;
+}
 
 
 /* ===== 卡片头部 ===== */
 /* ===== 卡片头部 ===== */
 .card-header {
 .card-header {
@@ -515,6 +707,22 @@ export default {
   font-size: 13px;
   font-size: 13px;
   color: #909399;
   color: #909399;
 }
 }
+.header-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.header-meta {
+  font-size: 12px;
+  color: #909399;
+}
+.header-meta i {
+  margin-right: 3px;
+}
+.header-meta b {
+  color: #606266;
+  font-weight: 500;
+}
 
 
 /* ===== 分区块 ===== */
 /* ===== 分区块 ===== */
 .section-block {
 .section-block {
@@ -527,6 +735,9 @@ export default {
 .section-block:hover {
 .section-block:hover {
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
   box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
 }
 }
+.section-block >>> .el-form {
+  padding: 0;
+}
 
 
 .section-header {
 .section-header {
   display: flex;
   display: flex;
@@ -547,10 +758,11 @@ export default {
   font-size: 14px;
   font-size: 14px;
   flex-shrink: 0;
   flex-shrink: 0;
 }
 }
-.section-icon.earn   { background: linear-gradient(135deg, #67C23A, #529b2e); }
-.section-icon.scope  { background: linear-gradient(135deg, #409EFF, #337ECC); }
-.section-icon.deduct { background: linear-gradient(135deg, #E6A23C, #cf9236); }
-.section-icon.rules  { background: linear-gradient(135deg, #9B59B6, #7d3c98); }
+.section-icon.earn    { background: linear-gradient(135deg, #67C23A, #529b2e); }
+.section-icon.scope   { background: linear-gradient(135deg, #409EFF, #337ECC); }
+.section-icon.deduct  { background: linear-gradient(135deg, #E6A23C, #cf9236); }
+.section-icon.rules   { background: linear-gradient(135deg, #9B59B6, #7d3c98); }
+.section-icon.preview { background: linear-gradient(135deg, #00B8A9, #00857a); }
 
 
 .section-label {
 .section-label {
   font-size: 14px;
   font-size: 14px;
@@ -562,6 +774,19 @@ export default {
   color: #909399;
   color: #909399;
   margin-left: 4px;
   margin-left: 4px;
 }
 }
+.section-switch {
+  margin-left: auto;
+}
+.section-disabled {
+  opacity: 0.5;
+  pointer-events: none;
+}
+.unit-suffix {
+  margin-left: 4px;
+  font-size: 14px;
+  color: #606266;
+  font-weight: 500;
+}
 
 
 .section-body {
 .section-body {
   padding: 16px 20px 4px;
   padding: 16px 20px 4px;
@@ -632,6 +857,115 @@ export default {
   border: 1px solid #e4e7ed;
   border: 1px solid #e4e7ed;
 }
 }
 
 
+/* ===== 积分计算示例 ===== */
+.section-preview .section-header {
+  background: linear-gradient(135deg, #f0fffe 0%, #e6faf8 100%);
+}
+.preview-body {
+  padding: 20px 20px 16px;
+}
+.preview-inputs {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 32px;
+  padding: 8px 0 20px;
+  border-bottom: 1px dashed #EBEEF5;
+  margin-bottom: 20px;
+}
+.preview-input-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.preview-input-label {
+  font-size: 13px;
+  color: #606266;
+  font-weight: 500;
+  white-space: nowrap;
+}
+.preview-input-num {
+  width: 160px;
+}
+.preview-unit {
+  font-size: 12px;
+  color: #909399;
+}
+.preview-result {
+  display: flex;
+  align-items: stretch;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+.result-card {
+  flex: 1;
+  min-width: 180px;
+  padding: 16px 18px;
+  border-radius: 8px;
+  border: 1px solid #EBEEF5;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+.result-card .result-label {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.result-card .result-value {
+  font-size: 26px;
+  font-weight: 700;
+  line-height: 1.2;
+}
+.result-card .result-tip {
+  font-size: 11px;
+  color: #C0C4CC;
+  line-height: 1.5;
+}
+.result-earn {
+  border-color: #e1f3d8;
+  background: linear-gradient(135deg, #f0f9eb 0%, #ffffff 100%);
+}
+.result-earn .result-value { color: #67C23A; }
+.result-earn .result-label i { color: #67C23A; }
+
+.result-deduct {
+  border-color: #faecd8;
+  background: linear-gradient(135deg, #fdf6ec 0%, #ffffff 100%);
+}
+.result-deduct .result-value { color: #E6A23C; }
+.result-deduct .result-label i { color: #E6A23C; }
+
+.result-pay {
+  border-color: #d9ecff;
+  background: linear-gradient(135deg, #ecf5ff 0%, #ffffff 100%);
+}
+.result-pay .result-value { color: #409EFF; }
+.result-pay .result-label i { color: #409EFF; }
+
+.result-arrow {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  color: #C0C4CC;
+  font-size: 18px;
+}
+.preview-warn {
+  margin-top: 14px;
+  padding: 8px 14px;
+  background: #fef0f0;
+  color: #F56C6C;
+  border: 1px solid #fde2e2;
+  border-radius: 4px;
+  font-size: 12px;
+}
+.preview-warn i {
+  margin-right: 4px;
+}
+
 /* ===== 底部操作栏 ===== */
 /* ===== 底部操作栏 ===== */
 .form-footer {
 .form-footer {
   display: flex;
   display: flex;
@@ -691,4 +1025,33 @@ export default {
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   white-space: nowrap;
   white-space: nowrap;
 }
 }
+
+/* ===== 响应式 ===== */
+@media (max-width: 768px) {
+  .section-block {
+    margin: 0 12px 16px;
+  }
+  .section-body {
+    padding: 16px 12px 4px;
+  }
+  .select-wide,
+  .select-wide-x {
+    width: 100%;
+  }
+  .preview-inputs {
+    flex-direction: column;
+    gap: 16px;
+  }
+  .preview-result {
+    flex-direction: column;
+  }
+  .result-arrow {
+    transform: rotate(90deg);
+    width: auto;
+    height: 20px;
+  }
+  .form-footer {
+    margin: 0 12px 8px;
+  }
+}
 </style>
 </style>