【UniApp领域驱动开发】从0到1: 搞定小程序购物车优惠最大折扣计算逻辑(01)-前言

0 阅读21分钟

你有没有过这样的崩溃时刻?

周一刚上班,产品经理就甩过来一屏新需求:外卖小程序的购物车要加新的优惠券玩法、会员权益要升级、还要自动给用户算最大优惠。你点开那个几千行的index.vue文件,看着缠成一团的 UI 渲染、加购逻辑、价格计算、规则判断,头皮发麻 —— 改一个小需求,就要扫遍全文件的逻辑,生怕碰坏哪里,上线就出金额 bug,最后被拉去祭天。

如果你也被外卖 / 电商小程序的购物车代码折磨过,这系列文章,就是为你量身定做的。

一、背景

1. 业务需求的 无限战争

在外卖、电商类小程序的开发史上,购物车模块永远是业务需求的「兵家必争之地」,也是研发的「噩梦发源地」。

最开始,购物车的需求简单到离谱:把商品加进去,算个总价,你半天就能写完上线。但随着市场卷起来,产品经理的脑洞就像开了闸的洪水,一波接一波:

  • 商品维度的复杂化:从单纯的「选商品」,变成了「选规格」—— 辣度、甜度、份量、套餐组合,一个商品能拆出几十种 SKU;
  • 优惠券体系的「军备竞赛」:从基础的满减券、无门槛券,一路卷到满折券(满 100 打 8 折)、特价券(指定商品 1 元购)、组合商品券(买 A+B 立减 20),甚至还有第二件半价、第 N 件 0 元;
  • 付费会员体系的「修罗场」:每月 N 次专属折扣、部分商品除外、最高减 N 元;会员日固定免单、满 0 元赠指定餐品;还有专属活动,每月 1 次 0 元吃单品;
  • 积分与 互斥 规则的「迷宫」:积分可抵现,但部分优惠券不能用;会员权益和优惠券互斥;会员权益之间也互斥(不能同时享 9 折和免配送费)。

就在你好不容易把这些规则堆完,产品经理端着咖啡过来,轻飘飘甩下一句终极需求:**「用户加购完商品,系统自动给他算出当前能薅的最大羊毛,直接选好最优解,别让用户自己费劲挑。」**那一刻,你看着.vue 文件里几千行的 if-else,血压瞬间拉满。

2. 快速迭代埋下的 技术债务地雷

太多外卖小程序的购物车代码,都是典型的「快速迭代产物」:为了赶上线,先堆完功能再说,结果需求越堆越多,代码逐渐散发「坏味道」,最后变成谁都不敢碰的屎山。

  • UI 与业务逻辑强 耦合 ,牵一发而动全身

    • 一个 .vue 文件干了所有事:左边分类联动、右边商品列表、加购弹窗、规格选择、优惠券弹窗、购物车结算,全塞在这一个文件里。
    • 想改一下弹窗动画?可能不小心碰坏了价格计算的逻辑,线上出了金额 bug;
    • 想把 “优惠计算” 逻辑复用到另一个页面?发现它和 datamethods 缠在一起,剪不断理还乱。最后只能复制粘贴一份,从此一份逻辑要维护两个地方,改一处漏一处。
  • 业务规则硬编码,加需求如 「扫雷」

    • 代码里全是if (coupon.type === 'discount')if (selectedRights.includes(1))这样的千层饼判断,加一种新的优惠券类型,就得在几百行的 if-else 里再堆一层。
    • 互斥规则全是硬写死的:「选了优惠券就清空会员权益」,哪天产品说要改规则,你得把整个文件翻个底朝天;
    • 魔法数字满天飞:status: 1是可用,2是待生效,3是已过期,4是已核销,除了写代码的你,没人知道这些数字代表啥,过俩月连你自己都忘了。
  • 数据状态管理混乱,同步全靠 「手动补丁」

  购物车的状态管理,全靠手动打补丁,出 bug 全靠运气:

  • 用户删了购物车里的商品,已选的优惠券要不要重置?有时候忘写,就出现了「购物车空了还能用满减券」的线上 bug;
  • 加购、删购、清空购物车,每个操作都要手动调用一遍价格重算,漏一个地方就出问题;
  • 购物车数据、优惠券列表、会员权益,所有状态全裸露在外面,谁都能改,出了 bug 根本找不到是哪一步改坏的。

二、 大纲

本系列就带你打一场 「购物车屎山代码翻身仗」,从泥沼到殿堂,一步步拆解重构全流程,手把手带你写出高内聚、低耦合、随便加需求都不崩的购物车代码。

1. 扒一扒「快餐式代码」的底裤

  • 拿出真实业务里的反面教材,拆解最核心的商品总价、优惠金额计算逻辑;
  • 画清逻辑流向图,精准标出哪里是硬编码、哪里是耦合点、哪里藏着线上 bug 的雷。

2. 看看需求迭代是怎么把代码堆成屎山的

  • 模拟真实业务场景:产品要新增一种「第二件半价券」;
  • 错误示范现场:在原有的 if-else 大山里继续堆代码,看看逻辑是怎么一步步变得不可维护、测试场景是怎么指数级增长的。

3. 破局!先把 UI 和业务逻辑拆干净

  • 视图层(View)瘦身:只负责渲染和事件转发,点击「加购」只发事件,不处理任何计算逻辑;
  • 逻辑层抽离:把useCartuseCoupon拆成独立的 Composition API,先实现最基础的解耦。

4. 核心关:DDD 领域划分 + 设计模式落地,重构核心架构

  • DDD 限界上下文划分:把业务拆成三个独立的领域,各管各的事,绝不乱掺和

    • 购物车领域(Cart Context):购物车聚合根、购物车商品实体(含 SKU 规格),只负责商品的加购、删除、生命周期管理;
    • 折扣领域(Discount Context):折扣策略抽象接口、优惠券实现、会员权益实现,只负责单条优惠的规则与计算;
    • 折扣营销领域(Package Context):可用折扣列表工厂、最优折扣计算器,负责规则引擎、互斥判断、最大优惠计算。
  • 设计模式精准落地:每个模式都解决一个具体痛点

    • 策略模式:定义统一的折扣计算接口,每个优惠券 / 权益都是一个独立的策略类,彻底干掉 if-else;
    • 工厂模式:根据优惠类型动态创建对应的策略对象,消灭 switch-case,加新类型不用改核心代码;
    • 组合模式:处理「满赠 + 满减」的叠加场景(业务允许的情况下);
    • 规格模式:把硬编码的使用门槛、商品范围、互斥规则,变成可组合、可复用的积木式判断。

5. 验证关:新需求来了?加个类就搞定

  • 实战演示:产品又要加「第二件半价」的新玩法;
  • 正确示范:只需要新建一个SecondHalfPriceDiscount类继承基类,注册到工厂中,原有核心计算逻辑一行都不用改,完美实现需求。

6. 复盘关:看看重构完的代码到底香在哪

  • 硬核对比:重构前后的代码行数、类结构、测试覆盖度;
  • 全面总结:重构后代码在可维护性、可读性、可扩展性上的质的提升;
  • 避坑指南:前端落地 DDD 最容易踩的坑,以及对应的解决方案。

三、你能获得什么?(系列最终目标)

很多人觉得 DDD、设计模式是后端的玄学、是面试的考点,在前端小程序里根本落不了地。这个系列就用最真实的外卖购物车场景,带你把这些理论变成实打实的代码能力。

  跟着这个系列走完全程,你将解锁这些硬核能力:

  • ✅ 把面向对象设计(OOD)  从面试考点变成实战武器,用封装、继承、多态干掉烂代码;
  • ✅ 吃透DDD ****领域驱动设计的前端轻量化落地,再也不用觉得 DDD 是后端专属的复杂概念;
  • ✅ 把设计模式玩明白,策略模式、工厂模式信手拈来,写出符合SOLID 原则的优雅代码;
  • ✅ 搭出一套「产品随便提需求,我只需要加类」的防弹架构,再也不用为了迭代改代码改到崩溃。

如果你也受够了改不完的 if-else、修不完的线上 bug、迭代一次就重构一次的购物车代码,那就系好安全带,跟我一起开启这场代码的涅槃之旅!

附件

UI交互

image.png

uniapp代码

<template>
  <view class="page-container">

    <!-- 1. 顶部:店招与公告 -->
    <view class="top-header">
      <view class="store-info">
        <text class="store-name">炸串(青岛店)</text>
        <text class="store-desc">月售1200+ 炸串界的扛把子</text>
      </view>
      <!-- 公告滚动 -->
      <view class="notice-bar">
        <text class="notice-icon">📢</text>
        <swiper class="notice-swiper" circular vertical :autoplay="true" :interval="3000">
          <swiper-item v-for="(item, i) in notices" :key="i">
            <text class="notice-text">{{ item }}</text>
          </swiper-item>
        </swiper>
      </view>
    </view>

    <!-- 2. 主体:左右联动列表 -->
    <view class="main-content">
      <!-- 左侧:分类 -->
      <scroll-view
          scroll-y
          class="left-nav"
          :scroll-into-view="leftScrollId"
          scroll-with-animation
      >
        <view
            v-for="(cat, index) in menuData"
            :key="index"
            :id="'nav-' + index"
            class="nav-item"
            :class="{ active: currentIndex === index }"
            @click="clickLeftNav(index)"
        >
          <!-- 分类角标(如热销、新品) -->
          <text v-if="cat.tag" class="cat-tag">{{ cat.tag }}</text>
          <text class="cat-name">{{ cat.name }}</text>
          <view v-if="currentIndex === index" class="active-bar"></view>
        </view>
      </scroll-view>

      <!-- 右侧:商品列表 -->
      <scroll-view
          scroll-y
          class="right-goods"
          :scroll-into-view="goodsScrollId"
          scroll-with-animation
          @scroll="onRightScroll"
      >
        <view
            v-for="(cat, cIndex) in menuData"
            :key="cIndex"
            :id="'goods-' + cIndex"
            class="category-box"
        >
          <!-- 分类标题 -->
          <view class="category-title">{{ cat.name }}</view>

          <!-- 商品列表 -->
          <view
              v-for="(item, gIndex) in cat.items"
              :key="item.id"
              class="goods-item"
          >
            <image :src="item.image" mode="aspectFill" class="goods-img"></image>
            <view class="goods-info">
              <view class="goods-name">
                <text>{{ item.name }}</text>
                <text v-if="item.hot" class="hot-tag">爆款</text>
                <!-- 新增:特价标签 -->
                <text v-if="getSpecialPrice(item) !== null" class="special-tag">特价</text>
              </view>
              <text class="goods-desc">{{ item.desc }}</text>
              <view class="goods-bottom">
                <!-- 新增:显示原价+特价 -->
                <view class="price-group">
                  <text class="price">¥{{ getFinalGoodsPrice(item).toFixed(2) }}</text>
                  <text v-if="getSpecialPrice(item) !== null" class="original-price">¥{{ item.price.toFixed(2) }}</text>
                </view>
                <text class="sales">月销{{ item.sales }}</text>
                <!-- 加购按钮 -->
                <view class="cart-btn-group">
                  <text
                      v-if="getCartCount(item.id) > 0"
                      class="icon-btn minus"
                      @click="updateCart(item, -1)"
                  >-</text>
                  <text v-if="getCartCount(item.id) > 0" class="cart-num">{{ getCartCount(item.id) }}</text>
                  <text
                      class="icon-btn plus"
                      :class="{ bounce: item.isAdding }"
                      @click="openSpecPopup(item)"
                  >+</text>
                </view>
              </view>
            </view>
          </view>
        </view>
        <!-- 底部占位 -->
        <view style="height: 120rpx;"></view>
      </scroll-view>
    </view>

    <!-- 3. 底部:购物车+结算栏(新增权益使用次数提示) -->
    <view class="bottom-bar">
      <view class="cart-icon-wrapper" @click="showCart = true">
        <view class="icon-cart">
          <!-- 购物车图标 -->
          <text style="font-size: 40rpx;">🛒</text>
          <text v-if="totalCount > 0" class="badge">{{ totalCount > 99 ? '99+' : totalCount }}</text>
        </view>
        <view class="cart-total">
          <view class="price-row">
            <text class="total-label">合计</text>
            <text class="total-price">¥{{ payPrice.toFixed(2) }}</text>
            <text v-if="discountAmount > 0" class="discount-tip">已优惠¥{{ discountAmount.toFixed(2) }}</text>
          </view>
          <view class="delivery-row">
            <text class="delivery-tip">配送费 ¥{{ freightFee.toFixed(2) }}</text>
            <text class="coupon-entry" @click.stop="openCouponPopup">
              {{ selectedCoupon ? '已选1张券' : '优惠券' }}
            </text>
          </view>
          <!-- 新增:权益使用提示 -->
          <view v-if="selectedRights.length > 0" class="right-tip">
            <text v-if="selectedRights.includes(1)">本月8折券已用{{ monthlyDiscountCount }}/4次</text>
            <text v-if="selectedRights.includes(2)">周四免费淀粉肠已锁定</text>
          </view>
        </view>
      </view>
      <button class="submit-btn" :class="{ disabled: totalCount === 0 }">
        {{ totalCount === 0 ? '去选购' : `¥${payPrice.toFixed(2)} 去结算` }}
      </button>
    </view>

    <!-- 4. 购物车弹窗(新增规格显示、组合商品标识) -->
    <view v-if="showCart" class="cart-popup-mask" @click="showCart = false">
      <view class="cart-popup" @click.stop>
        <view class="popup-header">
          <text class="popup-title">购物车</text>
          <text class="clear-btn" @click="clearCart">清空</text>
        </view>
        <scroll-view scroll-y class="popup-list">
          <view v-for="item in cartList" :key="item.cartKey" class="popup-item">
            <view class="item-info">
              <text class="item-name">{{ item.name }}</text>
              <text v-if="item.specDesc" class="item-spec">{{ item.specDesc }}</text>
              <text v-if="item.isCombo" class="combo-tag">组合商品</text>
            </view>
            <view class="item-control">
              <text class="item-price">¥{{ (item.price * item.count).toFixed(2) }}</text>
              <view class="btn-group">
                <text class="icon-btn minus" @click="updateCart(item, -1)">-</text>
                <text class="num">{{ item.count }}</text>
                <text class="icon-btn plus" @click="updateCart(item, 1)">+</text>
              </view>
            </view>
          </view>
          <view v-if="cartList.length === 0" class="empty-cart">
            <text>购物车还是空的,快去选购吧~</text>
          </view>
        </scroll-view>
      </view>
    </view>

    <!-- 5. 商品规格选择弹窗(新增核心功能) -->
    <view v-if="specPopupVisible" class="spec-popup-mask" @click="closeSpecPopup">
      <view class="spec-popup" @click.stop>
        <!-- 商品头部信息 -->
        <view class="spec-header">
          <image :src="currentSpecGoods.image" class="spec-goods-img"></image>
          <view class="spec-goods-info">
            <text class="spec-goods-name">{{ currentSpecGoods.name }}</text>
            <text class="spec-goods-price">¥{{ getFinalGoodsPrice(currentSpecGoods).toFixed(2) }}</text>
          </view>
          <text class="close-spec" @click="closeSpecPopup">×</text>
        </view>

        <!-- 规格选项列表 -->
        <scroll-view scroll-y="true" class="spec-content">
          <view
              v-for="(spec, sIndex) in currentSpecGoods.specs"
              :key="sIndex"
              class="spec-group"
          >
            <text class="spec-title">{{ spec.name }}</text>
            <view class="spec-options">
              <text
                  v-for="(option, oIndex) in spec.options"
                  :key="oIndex"
                  class="spec-option"
                  :class="{ active: selectedSpecs[spec.name] === option }"
                  @click="selectSpec(spec.name, option)"
              >{{ option }}</text>
            </view>
          </view>
        </scroll-view>

        <!-- 底部数量选择+确认按钮 -->
        <view class="spec-footer">
          <view class="num-control">
            <text>购买数量</text>
            <view class="btn-group">
              <text class="icon-btn minus" :class="{ disabled: specBuyNum <= 1 }" @click="specBuyNum > 1 && specBuyNum--">-</text>
              <text class="num">{{ specBuyNum }}</text>
              <text class="icon-btn plus" :class="{ disabled: specBuyNum >= 99 }" @click="specBuyNum < 99 && specBuyNum++">+</text>
            </view>
          </view>
          <button
              class="confirm-spec-btn"
              :class="{ disabled: !isSpecSelected }"
              @click="confirmAddCart"
          >
            加入购物车
          </button>
        </view>
      </view>
    </view>

    <!-- 6. 优惠券&权益选择弹窗(新增核心功能) -->
    <view v-if="couponPopupVisible" class="coupon-popup-mask" @click="closeCouponPopup">
      <view class="coupon-popup" @click.stop>
        <view class="popup-header">
          <text class="popup-title">优惠选择</text>
          <text class="close-btn" @click="closeCouponPopup">×</text>
        </view>

        <!-- tab切换:优惠券 / 会员权益 -->
        <view class="coupon-tab">
          <text
              class="tab-item"
              :class="{ active: currentTab === 'coupon' }"
              @click="currentTab = 'coupon'"
          >优惠券</text>
          <text
              class="tab-item"
              :class="{ active: currentTab === 'right' }"
              @click="currentTab = 'right'"
          >会员权益</text>
        </view>

        <!-- 优惠券列表(新增4种类型) -->
        <scroll-view scroll-y class="coupon-content" v-if="currentTab === 'coupon'">
          <view class="coupon-title">可用优惠券</view>
          <view
              v-for="coupon in availableCoupon"
              :key="coupon.id"
              class="coupon-item"
              :class="{ selected: selectedCoupon?.id === coupon.id, combo: coupon.type === 'combo' }"
              @click="selectCoupon(coupon)"
          >
            <view class="coupon-left">
              <text class="coupon-amount">
                {{ getCouponAmountText(coupon) }}
              </text>
              <text class="coupon-name">{{ coupon.name }}</text>
              <text class="coupon-limit">{{ coupon.limit }}</text>
            </view>
            <view class="coupon-right">
              <text v-if="selectedCoupon?.id === coupon.id" class="selected-tag">已选中</text>
              <text v-else class="select-btn">立即使用</text>
            </view>
          </view>

          <view class="coupon-title">不可用优惠券</view>
          <view
              v-for="coupon in unavailableCoupon"
              :key="coupon.id"
              class="coupon-item disabled"
              :class="{ combo: coupon.type === 'combo' }"
          >
            <view class="coupon-left">
              <text class="coupon-amount">
                {{ getCouponAmountText(coupon) }}
              </text>
              <text class="coupon-name">{{ coupon.name }}</text>
              <text class="coupon-limit">{{ coupon.limit }}</text>
            </view>
            <view class="coupon-right">
              <text class="disabled-btn">不可用</text>
            </view>
          </view>

          <view v-if="couponList.length === 0" class="empty-coupon">
            <text>暂无可用优惠券</text>
          </view>
        </scroll-view>

        <!-- 会员权益列表(新增2种复杂权益) -->
        <scroll-view scroll-y class="coupon-content" v-if="currentTab === 'right'">
          <view class="right-item" v-for="right in memberRights" :key="right.id">
            <view class="right-info">
              <text class="right-name">{{ right.name }}</text>
              <text class="right-desc">{{ right.desc }}</text>
              <!-- 新增:权益使用状态/限制提示 -->
              <text v-if="right.id === 1" class="right-status">
                本月已用{{ monthlyDiscountCount }}/4次
                <text v-if="monthlyDiscountCount >= 4" class="right-status-red">(已用完)</text>
              </text>
              <text v-if="right.id === 2" class="right-status">
                {{ isThursday ? '今天可用!' : '每周四可用' }}
              </text>
            </view>
            <switch
                :checked="selectedRights.includes(right.id)"
                @change="toggleRight(right.id, $event)"
                color="#e6212a"
                :disabled="!isRightAvailable(right.id)"
            />
          </view>
        </scroll-view>

        <!-- 底部确认按钮 -->
        <view class="coupon-footer">
          <button class="confirm-coupon-btn" @click="confirmCoupon">确认使用</button>
        </view>
      </view>
    </view>

  </view>
</template>

<script setup>
import { ref, computed, onMounted, nextTick } from 'vue';

// --- 1. 基础模拟数据(新增组合商品、淀粉肠、复杂优惠券/权益) ---
const notices = ref([
  "新人专享:满30减5元!",
  "招牌炸年糕,外酥里嫩超好吃!",
  "每周四会员免费吃淀粉肠!",
  "每月4次8折券,最高减10元!"
]);

// 商品数据(新增组合商品、淀粉肠、特价商品标识)
const menuData = ref([
  {
    name: '必点招牌',
    tag: ' HOT ',
    items: [
      {
        id: 1,
        name: '招牌五花肉',
        desc: '肥而不腻 外焦里嫩',
        price: 12,
        sales: 999,
        hot: true,
        image: 'https://via.placeholder.com/200x200/ffcccc/ffffff?text=Pork',
        specs: [
          { name: '辣度', options: ['不辣', '微辣', '中辣', '特辣', '爆辣'], default: '微辣' }
        ]
      },
      {
        id: 2,
        name: '喜姐掌中宝',
        desc: '嘎嘣脆 鸡肉味',
        price: 15,
        sales: 856,
        hot: true,
        image: 'https://via.placeholder.com/200x200/ccffcc/333333?text=Cartilage',
        specs: [
          { name: '辣度', options: ['不辣', '微辣', '中辣', '特辣'], default: '微辣' },
          { name: '份量', options: ['5串', '10串', '20串'], default: '5串' }
        ]
      },
      {
        id: 3,
        name: '秘制炸年糕',
        desc: '甜辣酱绝配',
        price: 6,
        sales: 1200,
        image: 'https://via.placeholder.com/200x200/ccccff/333333?text=RiceCake',
        specs: [
          { name: '口味', options: ['甜辣酱', '番茄酱', '原味'], default: '甜辣酱' }
        ]
      },
      // 新增:淀粉肠(用于周四免费权益)
      {
        id: 4,
        name: '经典淀粉肠',
        desc: '童年的味道',
        price: 3,
        sales: 2000,
        hot: true,
        image: 'https://via.placeholder.com/200x200/ffffcc/333333?text=StarchSausage',
        specs: [
          { name: '辣度', options: ['不辣', '微辣', '中辣'], default: '微辣' }
        ]
      },
    ]
  },
  {
    name: '经典炸串',
    items: [
      {
        id: 5,
        name: '牛肉小串',
        desc: '鲜嫩多汁',
        price: 18,
        sales: 456,
        image: 'https://via.placeholder.com/200x200',
        specs: [
          { name: '辣度', options: ['不辣', '微辣', '中辣', '特辣'], default: '微辣' }
        ]
      },
      {
        id: 6,
        name: '羊肉串',
        desc: '正宗羊肉',
        price: 20,
        sales: 321,
        image: 'https://via.placeholder.com/200x200',
        specs: [
          { name: '辣度', options: ['不辣', '微辣', '中辣', '爆辣'], default: '微辣' }
        ]
      },
      {
        id: 7,
        name: '鸡肉串',
        desc: '低脂健康',
        price: 10,
        sales: 654,
        image: 'https://via.placeholder.com/200x200',
        specs: [] // 无规格商品,直接加购
      },
      {
        id: 8,
        name: '炸香肠',
        desc: '台式风味',
        price: 8,
        sales: 789,
        image: 'https://via.placeholder.com/200x200',
        specs: []
      },
    ]
  },
  {
    name: '特价专区',
    tag: ' 特价 ',
    items: [
      // 新增:特价商品
      {
        id: 9,
        name: '炸茄子',
        desc: '软嫩入味',
        price: 8,
        specialPrice: 4.9, // 新增:特价字段
        sales: 234,
        image: 'https://via.placeholder.com/200x200',
        specs: []
      },
      {
        id: 10,
        name: '炸金针菇',
        desc: '酥脆可口',
        price: 10,
        specialPrice: 6.6,
        sales: 567,
        image: 'https://via.placeholder.com/200x200',
        specs: []
      },
    ]
  },
  {
    name: '组合套餐',
    tag: ' 套餐 ',
    items: [
      // 新增:组合商品
      {
        id: 11,
        name: '单人豪华套餐',
        desc: '五花肉+年糕+淀粉肠+可乐',
        price: 25,
        comboPrice: 19.9, // 新增:组合价字段
        isCombo: true, // 新增:组合标识
        comboItemIds: [1, 3, 4], // 新增:组合包含的商品ID
        sales: 1500,
        hot: true,
        image: 'https://via.placeholder.com/200x200/ffccff/333333?text=Combo',
        specs: [
          { name: '五花肉辣度', options: ['不辣', '微辣', '中辣'], default: '微辣' },
          { name: '年糕口味', options: ['甜辣酱', '番茄酱'], default: '甜辣酱' },
          { name: '淀粉肠辣度', options: ['不辣', '微辣'], default: '微辣' }
        ]
      },
    ]
  }
]);

// 优惠券数据(新增4种类型:满减、满折、特价、组合商品券)
const couponList = ref([
  {
    id: 1,
    name: '新人无门槛券',
    type: 'normal', // 无门槛(保留原有)
    amount: 5,
    threshold: 0,
    limit: '全场通用,无门槛',
    status: 1
  },
  {
    id: 2,
    name: '满30减10券',
    type: 'full', // 满减(保留原有)
    amount: 10,
    threshold: 30,
    limit: '全场通用,满30可用',
    status: 1
  },
  {
    id: 3,
    name: '全场满50打8折券',
    type: 'full_discount', // 新增:满折
    discount: 8,
    maxDiscount: 15,
    threshold: 50,
    limit: '全场通用,满50打8折,最高减15元',
    status: 1
  },
  {
    id: 4,
    name: '淀粉肠+年糕特价券',
    type: 'special', // 新增:指定商品特价券
    specialItemIds: [3, 4], // 指定商品ID
    specialPrice: 5, // 组合特价
    threshold: 0,
    limit: '指定淀粉肠+年糕,组合价5元',
    status: 1
  },
  {
    id: 5,
    name: '单人豪华套餐立减5元券',
    type: 'combo', // 新增:组合商品券
    comboItemIds: [11], // 指定组合商品ID
    amount: 5,
    threshold: 0,
    limit: '指定单人豪华套餐,立减5元',
    status: 1
  },
  {
    id: 6,
    name: '满100减50券',
    type: 'full',
    amount: 50,
    threshold: 100,
    limit: '全场通用,满100可用',
    status: 0
  }
]);

// 会员权益数据(新增2种复杂权益)
const memberRights = ref([
  {
    id: 1,
    name: '每月4次8折券',
    desc: '全场商品享8折优惠,每次最高减10元,与优惠券互斥',
    discount: 0.8,
    maxDiscount: 10,
    type: 'monthly_discount',
    monthlyLimit: 4 // 新增:每月使用次数限制
  },
  {
    id: 2,
    name: '每周四免费吃淀粉肠',
    desc: '每周四可免费领取1份经典淀粉肠,与优惠券互斥',
    freeItemId: 4, // 新增:指定免费商品ID
    type: 'weekly_free',
    weeklyDay: 4 // 新增:每周几可用(0=周日,1=周一...4=周四)
  },
  {
    id: 3,
    name: '免配送费',
    desc: '订单免2元配送费',
    type: 'freight_free'
  }
]);

// --- 2. 模拟用户数据(用于复杂权益) ---
const monthlyDiscountCount = ref(2); // 本月已用8折券次数
const isThursday = computed(() => {
  // 模拟今天是周四(方便测试)
  return true;
  // 真实代码:
  // const today = new Date();
  // return today.getDay() === 4;
});

// --- 3. 基础状态定义 ---
const currentIndex = ref(0);
const leftScrollId = ref('');
const goodsScrollId = ref('');
const groupTops = ref([]);
const cartList = ref([]); // 购物车结构:{ cartKey, id, name, price, count, image, specDesc, specs, isCombo, comboItemIds }
const showCart = ref(false);
const freightFee = ref(2); // 基础配送费

// --- 4. 规格选择弹窗状态 ---
const specPopupVisible = ref(false);
const currentSpecGoods = ref({});
const selectedSpecs = ref({});
const specBuyNum = ref(1);

// --- 5. 优惠券&权益弹窗状态 ---
const couponPopupVisible = ref(false);
const currentTab = ref('coupon');
const selectedCoupon = ref(null);
const selectedRights = ref([]);

// --- 6. 核心计算属性
// 购物车总数量
const totalCount = computed(() => {
  return cartList.value.reduce((sum, item) => sum + item.count, 0);
});

// 商品原价总和(未优惠,不含特价/组合价)
const originalTotalPrice = computed(() => {
  return cartList.value.reduce((sum, item) => {
    // 组合商品用原价,特价商品用原价
    const basePrice = item.isCombo ? menuData.value.flatMap(c => c.items).find(i => i.id === item.id)?.price || item.price :
        item.specialPrice ? menuData.value.flatMap(c => c.items).find(i => i.id === item.id)?.price || item.price :
            item.price;
    return sum + basePrice * item.count;
  }, 0);
});

// 商品最终单价(含特价/组合价,不含优惠券/权益)
const getFinalGoodsPrice = (item) => {
  if (item.isCombo) return item.comboPrice || item.price;
  if (item.specialPrice) return item.specialPrice;
  return item.price;
};

// 获取商品特价(用于标签显示)
const getSpecialPrice = (item) => {
  return item.specialPrice || null;
};

// 商品最终总价(含特价/组合价,不含优惠券/权益)
const finalGoodsTotalPrice = computed(() => {
  return cartList.value.reduce((sum, item) => sum + getFinalGoodsPrice(item) * item.count, 0);
});

// 会员权益优惠金额(新增复杂逻辑)
const rightDiscountAmount = computed(() => {
  let discount = 0;
  // 每月4次8折券
  if (selectedRights.value.includes(1)) {
    if (monthlyDiscountCount.value < 4) {
      const baseDiscount = finalGoodsTotalPrice.value * (1 - memberRights.value[0].discount);
      discount += Math.min(baseDiscount, memberRights.value[0].maxDiscount);
    }
  }
  // 每周四免费吃淀粉肠
  if (selectedRights.value.includes(2)) {
    if (isThursday.value) {
      // 查找购物车中是否有淀粉肠
      const starchSausage = cartList.value.find(item => item.id === 4);
      if (starchSausage) {
        // 免费1份
        discount += getFinalGoodsPrice(starchSausage);
      }
    }
  }
  // 免配送费
  if (selectedRights.value.includes(3)) {
    discount += freightFee.value;
  }
  return discount;
});

// 优惠券优惠金额(新增4种类型硬编码)
const couponDiscountAmount = computed(() => {
  if (!selectedCoupon.value) return 0;
  const coupon = selectedCoupon.value;
  const basePrice = finalGoodsTotalPrice.value;
  const cartItemIds = cartList.value.map(item => item.id);

  // 无门槛券
  if (coupon.type === 'normal') {
    return Math.min(coupon.amount, basePrice);
  }
  // 满减券
  if (coupon.type === 'full') {
    if (basePrice >= coupon.threshold) {
      return Math.min(coupon.amount, basePrice);
    }
  }
  // 满折券
  if (coupon.type === 'full_discount') {
    if (basePrice >= coupon.threshold) {
      const discount = basePrice * (1 - coupon.discount / 10);
      return coupon.maxDiscount ? Math.min(discount, coupon.maxDiscount) : discount;
    }
  }
  // 指定商品特价券
  if (coupon.type === 'special') {
    // 检查购物车是否包含所有指定商品
    const hasAllSpecialItems = coupon.specialItemIds.every(id => cartItemIds.includes(id));
    if (hasAllSpecialItems) {
      // 计算指定商品的原价总和
      const specialItemsOriginalTotal = coupon.specialItemIds.reduce((sum, id) => {
        const item = cartList.value.find(i => i.id === id);
        const originalItem = menuData.value.flatMap(c => c.items).find(i => i.id === id);
        return sum + (originalItem?.price || item?.price || 0) * (item?.count || 1);
      }, 0);
      // 优惠金额 = 指定商品原价总和 - 组合特价
      return Math.max(0, specialItemsOriginalTotal - coupon.specialPrice);
    }
  }
  // 组合商品券
  if (coupon.type === 'combo') {
    // 检查购物车是否包含指定组合商品
    const hasComboItem = coupon.comboItemIds.some(id => cartItemIds.includes(id));
    if (hasComboItem) {
      // 优惠金额 = 立减金额 * 组合商品数量
      const comboItemCount = cartList.value.filter(item => coupon.comboItemIds.includes(item.id)).reduce((sum, item) => sum + item.count, 0);
      return Math.min(coupon.amount * comboItemCount, basePrice);
    }
  }
  return 0;
});

// 总优惠金额(互斥规则:取最大的)
const discountAmount = computed(() => {
  return Math.max(rightDiscountAmount.value, couponDiscountAmount.value);
});

// 最终实付金额
const payPrice = computed(() => {
  let pay = finalGoodsTotalPrice.value + freightFee.value - discountAmount.value;
  // 最低实付0元
  return pay < 0 ? 0 : pay;
});

// 可用优惠券列表(新增4种类型判断)
const availableCoupon = computed(() => {
  return couponList.value.filter(coupon => {
    if (coupon.status !== 1) return false;
    const basePrice = finalGoodsTotalPrice.value;
    const cartItemIds = cartList.value.map(item => item.id);

    // 无门槛/满减/满折:判断门槛
    if (['normal', 'full', 'full_discount'].includes(coupon.type)) {
      return basePrice >= coupon.threshold;
    }
    // 指定商品特价券:判断是否包含所有指定商品
    if (coupon.type === 'special') {
      return coupon.specialItemIds.every(id => cartItemIds.includes(id));
    }
    // 组合商品券:判断是否包含指定组合商品
    if (coupon.type === 'combo') {
      return coupon.comboItemIds.some(id => cartItemIds.includes(id));
    }
    return false;
  });
});

// 不可用优惠券列表
const unavailableCoupon = computed(() => {
  return couponList.value.filter(coupon => {
    if (coupon.status !== 1) return true;
    const basePrice = finalGoodsTotalPrice.value;
    const cartItemIds = cartList.value.map(item => item.id);

    if (['normal', 'full', 'full_discount'].includes(coupon.type)) {
      return basePrice < coupon.threshold;
    }
    if (coupon.type === 'special') {
      return !coupon.specialItemIds.every(id => cartItemIds.includes(id));
    }
    if (coupon.type === 'combo') {
      return !coupon.comboItemIds.some(id => cartItemIds.includes(id));
    }
    return true;
  });
});

// 规格是否全部选中
const isSpecSelected = computed(() => {
  if (!currentSpecGoods.value.specs || currentSpecGoods.value.specs.length === 0) return true;
  for (let spec of currentSpecGoods.value.specs) {
    if (!selectedSpecs.value[spec.name]) return false;
  }
  return true;
});

// 权益是否可用
const isRightAvailable = (rightId) => {
  if (rightId === 1) {
    return monthlyDiscountCount.value < 4;
  }
  if (rightId === 2) {
    return isThursday.value;
  }
  return true;
};

// 获取优惠券金额文本
const getCouponAmountText = (coupon) => {
  if (coupon.type === 'normal' || coupon.type === 'full' || coupon.type === 'combo') {
    return `¥${coupon.amount}`;
  }
  if (coupon.type === 'full_discount') {
    return `${coupon.discount}折`;
  }
  if (coupon.type === 'special') {
    return `¥${coupon.specialPrice}`;
  }
  return '';
};

// --- 7. 基础联动逻辑 ---
// 左侧点击 -> 右侧滚动
const clickLeftNav = (index) => {
  currentIndex.value = index;
  goodsScrollId.value = `goods-${index}`;
};

// 右侧滚动 -> 左侧高亮
const onRightScroll = (e) => {
  const scrollTop = e.detail.scrollTop;
  const scrollVal = scrollTop + 20;

  for (let i = 0; i < groupTops.value.length; i++) {
    let height1 = groupTops.value[i];
    let height2 = groupTops.value[i + 1];

    if (!height2) {
      if (scrollVal >= height1) {
        currentIndex.value = i;
        leftScrollId.value = `nav-${i}`;
      }
      continue;
    }

    if (scrollVal >= height1 && scrollVal < height2) {
      currentIndex.value = i;
      leftScrollId.value = `nav-${i}`;
      return;
    }
  }
};

// 初始化计算右侧分类高度
const initHeights = () => {
  nextTick(() => {
    const query = uni.createSelectorQuery().in(getCurrentPages()[getCurrentPages().length - 1]);
    query.selectAll('.category-box').boundingClientRect(data => {
      let tops = [];
      let top = 0;
      data.forEach(rect => {
        tops.push(top);
        top += rect.height;
      });
      groupTops.value = tops;
    }).exec();
  });
};

// --- 8. 商品规格选择核心逻辑 ---
// 打开规格弹窗
const openSpecPopup = (goods) => {
  if (!goods.specs || goods.specs.length === 0) {
    updateCart(goods, 1);
    return;
  }

  currentSpecGoods.value = goods;
  specBuyNum.value = 1;
  selectedSpecs.value = {};
  goods.specs.forEach(spec => {
    selectedSpecs.value[spec.name] = spec.default;
  });
  specPopupVisible.value = true;
};

// 关闭规格弹窗
const closeSpecPopup = () => {
  specPopupVisible.value = false;
  currentSpecGoods.value = {};
  selectedSpecs.value = {};
  specBuyNum.value = 1;
};

// 选择规格
const selectSpec = (specName, option) => {
  selectedSpecs.value[specName] = option;
};

// 确认加购
const confirmAddCart = () => {
  if (!isSpecSelected.value) return;

  const goods = currentSpecGoods.value;
  const specDesc = Object.values(selectedSpecs.value).join(' · ');
  const cartKey = `${goods.id}-${JSON.stringify(selectedSpecs.value)}`;

  const existedItem = cartList.value.find(item => item.cartKey === cartKey);
  if (existedItem) {
    existedItem.count += specBuyNum.value;
  } else {
    cartList.value.push({
      cartKey,
      id: goods.id,
      name: goods.name,
      price: getFinalGoodsPrice(goods),
      count: specBuyNum.value,
      image: goods.image,
      specDesc,
      specs: { ...selectedSpecs.value },
      isCombo: goods.isCombo,
      comboItemIds: goods.comboItemIds,
      specialPrice: goods.specialPrice
    });
  }

  goods.isAdding = true;
  setTimeout(() => goods.isAdding = false, 300);
  closeSpecPopup();
};

// --- 9. 购物车核心逻辑 ---
// 获取商品在购物车中的总数量(忽略规格)
const getCartCount = (id) => {
  return cartList.value.filter(item => item.id === id).reduce((sum, item) => sum + item.count, 0);
};

// 更新购物车数量(加减操作)
const updateCart = (item, num) => {
  if (item.cartKey) {
    const target = cartList.value.find(i => i.cartKey === item.cartKey);
    if (target) {
      target.count += num;
      if (target.count <= 0) {
        cartList.value = cartList.value.filter(i => i.cartKey !== item.cartKey);
        // 新增:删除商品后,检查优惠券/权益是否还满足条件
        checkAndClearInvalidDiscount();
      }
    }
    return;
  }

  const cartKey = `${item.id}-${JSON.stringify({})}`;
  const target = cartList.value.find(i => i.cartKey === cartKey);
  if (target) {
    target.count += num;
    if (target.count <= 0) {
      cartList.value = cartList.value.filter(i => i.cartKey !== cartKey);
      checkAndClearInvalidDiscount();
    }
  } else {
    if (num > 0) {
      cartList.value.push({
        cartKey,
        id: item.id,
        name: item.name,
        price: getFinalGoodsPrice(item),
        count: 1,
        image: item.image,
        specDesc: '',
        specs: {},
        isCombo: item.isCombo,
        comboItemIds: item.comboItemIds,
        specialPrice: item.specialPrice
      });
    }
  }

  if (num > 0) {
    item.isAdding = true;
    setTimeout(() => item.isAdding = false, 300);
  }
};

// 新增:检查并清除无效的优惠券/权益(又一堆硬编码)
const checkAndClearInvalidDiscount = () => {
  // 检查优惠券
  if (selectedCoupon.value) {
    const isAvailable = availableCoupon.value.some(c => c.id === selectedCoupon.value.id);
    if (!isAvailable) {
      selectedCoupon.value = null;
    }
  }
  // 检查每周四免费淀粉肠
  if (selectedRights.value.includes(2)) {
    const hasStarchSausage = cartList.value.some(item => item.id === 4);
    if (!hasStarchSausage) {
      selectedRights.value = selectedRights.value.filter(id => id !== 2);
    }
  }
};

// 清空购物车
const clearCart = () => {
  uni.showModal({
    title: '提示',
    content: '确定清空购物车吗?',
    success: (res) => {
      if (res.confirm) {
        cartList.value = [];
        showCart.value = false;
        selectedCoupon.value = null;
        selectedRights.value = [];
      }
    }
  });
};

// --- 10. 优惠券&权益核心逻辑 ---
// 打开优惠券弹窗
const openCouponPopup = () => {
  if (totalCount.value === 0) {
    uni.showToast({ title: '请先选购商品', icon: 'none' });
    return;
  }
  couponPopupVisible.value = true;
  currentTab.value = 'coupon';
};

// 关闭优惠券弹窗
const closeCouponPopup = () => {
  couponPopupVisible.value = false;
};

// 选择优惠券
const selectCoupon = (coupon) => {
  if (selectedCoupon.value?.id === coupon.id) {
    selectedCoupon.value = null;
  } else {
    selectedCoupon.value = coupon;
    selectedRights.value = [];
  }
};

// 切换会员权益
const toggleRight = (rightId, e) => {
  const isChecked = e.detail.value;
  if (isChecked) {
    selectedRights.value.push(rightId);
    selectedCoupon.value = null;
  } else {
    selectedRights.value = selectedRights.value.filter(id => id !== rightId);
  }
};

// 确认优惠选择
const confirmCoupon = () => {
  closeCouponPopup();
  uni.showToast({
    title: discountAmount.value > 0 ? `已优惠¥${discountAmount.toFixed(2)}` : '已选择优惠',
    icon: 'success'
  });
};

// --- 11. 生命周期 ---
onMounted(() => {
  setTimeout(() => {
    initHeights();
  }, 300);
});
</script>

<style lang="scss" scoped>
/* 全局变量:喜姐炸串品牌色 */
$primary-red: #e6212a;
$light-bg: #f7f8fa;
$text-main: #333;
$text-sub: #999;
$border-color: #eee;
$disabled-color: #ccc;
$special-yellow: #ff9900;
$combo-purple: #9933ff;

.page-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: #fff;
}

/* --- 顶部店招样式(原有兼容) --- */
.top-header {
  background-color: #fff;
  padding: 20rpx 30rpx;
  border-bottom: 1px solid $border-color;
  z-index: 10;

  .store-info {
    display: flex;
    flex-direction: column;
    margin-bottom: 15rpx;

    .store-name {
      font-size: 36rpx;
      font-weight: bold;
      color: $text-main;
    }
    .store-desc {
      font-size: 22rpx;
      color: $text-sub;
      margin-top: 5rpx;
    }
  }

  .notice-bar {
    display: flex;
    align-items: center;
    background-color: #fff8e8;
    padding: 10rpx 20rpx;
    border-radius: 8rpx;

    .notice-icon {
      font-size: 24rpx;
      margin-right: 10rpx;
    }
    .notice-swiper {
      height: 36rpx;
      flex: 1;

      text {
        font-size: 22rpx;
        color: #ff8c00;
        line-height: 36rpx;
      }
    }
  }
}

/* --- 主体左右联动样式(新增特价标签、原价划线) --- */
.main-content {
  flex: 1;
  display: flex;
  overflow: hidden;

  .left-nav {
    width: 180rpx;
    height: 100%;
    background-color: $light-bg;

    .nav-item {
      height: 100rpx;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 26rpx;
      color: #666;
      position: relative;
      background-color: $light-bg;

      &.active {
        background-color: #fff;
        color: $primary-red;
        font-weight: bold;

        .active-bar {
          position: absolute;
          left: 0;
          top: 30rpx;
          bottom: 30rpx;
          width: 6rpx;
          background-color: $primary-red;
          border-radius: 0 4rpx 4rpx 0;
        }
      }

      .cat-tag {
        position: absolute;
        top: 10rpx;
        right: 10rpx;
        font-size: 16rpx;
        background-color: $primary-red;
        color: #fff;
        padding: 2rpx 6rpx;
        border-radius: 4rpx;
        transform: scale(0.8);
      }
    }
  }

  .right-goods {
    flex: 1;
    height: 100%;
    padding: 0 20rpx;
    background-color: #fff;

    .category-box {
      padding-bottom: 20rpx;

      .category-title {
        font-size: 28rpx;
        font-weight: bold;
        color: $text-main;
        padding: 20rpx 0;
        background-color: #fff;
        position: sticky;
        top: 0;
        z-index: 1;
      }

      .goods-item {
        display: flex;
        margin-bottom: 30rpx;
        position: relative;

        .goods-img {
          width: 140rpx;
          height: 140rpx;
          border-radius: 12rpx;
          background-color: #eee;
        }

        .goods-info {
          flex: 1;
          margin-left: 20rpx;
          display: flex;
          flex-direction: column;
          justify-content: space-between;

          .goods-name {
            font-size: 28rpx;
            color: $text-main;
            font-weight: 500;
            display: flex;
            align-items: center;
            flex-wrap: wrap;

            .hot-tag {
              font-size: 20rpx;
              color: $primary-red;
              border: 1px solid $primary-red;
              padding: 0 6rpx;
              border-radius: 4rpx;
              margin-left: 10rpx;
              transform: scale(0.9);
            }
            .special-tag {
              font-size: 20rpx;
              color: #fff;
              background-color: $special-yellow;
              padding: 0 6rpx;
              border-radius: 4rpx;
              margin-left: 10rpx;
              transform: scale(0.9);
            }
          }

          .goods-desc {
            font-size: 22rpx;
            color: $text-sub;
            margin-top: 5rpx;
          }

          .goods-bottom {
            display: flex;
            align-items: center;
            justify-content: space-between;

            .price-group {
              display: flex;
              align-items: baseline;
              .price {
                font-size: 32rpx;
                color: $primary-red;
                font-weight: bold;
              }
              .original-price {
                font-size: 22rpx;
                color: $text-sub;
                text-decoration: line-through;
                margin-left: 10rpx;
              }
            }
            .sales {
              font-size: 20rpx;
              color: $text-sub;
              margin-left: 10rpx;
              flex: 1;
            }

            .cart-btn-group {
              display: flex;
              align-items: center;

              .cart-num {
                font-size: 26rpx;
                margin: 0 10rpx;
                min-width: 30rpx;
                text-align: center;
              }

              .icon-btn {
                width: 44rpx;
                height: 44rpx;
                border-radius: 50%;
                display: flex;
                align-items: center;
                justify-content: center;
                font-size: 32rpx;
                font-weight: bold;
                line-height: 1;

                &.plus {
                  background-color: $primary-red;
                  color: #fff;
                  &.bounce {
                    animation: bounceIn 0.3s;
                  }
                }
                &.minus {
                  background-color: #fff;
                  color: $text-main;
                  border: 1px solid #ddd;
                }
                &.disabled {
                  background-color: $disabled-color;
                  color: #fff;
                }
              }
            }
          }
        }
      }
    }
  }
}

/* --- 底部结算栏(新增权益使用提示) --- */
.bottom-bar {
  height: 140rpx;
  background-color: #fff;
  display: flex;
  align-items: center;
  padding: 0 20rpx;
  box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
  position: relative;
  z-index: 20;

  .cart-icon-wrapper {
    flex: 1;
    display: flex;
    align-items: center;

    .icon-cart {
      position: relative;
      background-color: #333;
      width: 80rpx;
      height: 80rpx;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;

      .badge {
        position: absolute;
        top: -5rpx;
        right: -5rpx;
        background-color: $primary-red;
        color: #fff;
        font-size: 20rpx;
        padding: 2rpx 10rpx;
        border-radius: 20rpx;
        min-width: 30rpx;
        text-align: center;
        border: 2px solid #fff;
      }
    }

    .cart-total {
      margin-left: 20rpx;
      display: flex;
      flex-direction: column;
      flex: 1;

      .price-row {
        display: flex;
        align-items: baseline;
        flex-wrap: wrap;
        .total-label {
          font-size: 24rpx;
          color: $text-main;
        }
        .total-price {
          font-size: 36rpx;
          color: $primary-red;
          font-weight: bold;
          margin-left: 10rpx;
        }
        .discount-tip {
          font-size: 20rpx;
          color: $primary-red;
          margin-left: 10rpx;
        }
      }
      .delivery-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        width: 100%;
        margin-top: 5rpx;
        .delivery-tip {
          font-size: 20rpx;
          color: $text-sub;
        }
        .coupon-entry {
          font-size: 22rpx;
          color: $primary-red;
        }
      }
      .right-tip {
        display: flex;
        flex-direction: column;
        margin-top: 5rpx;
        text {
          font-size: 20rpx;
          color: $text-sub;
        }
      }
    }
  }

  .submit-btn {
    width: 220rpx;
    height: 70rpx;
    background-color: $primary-red;
    color: #fff;
    border: none;
    border-radius: 35rpx;
    font-size: 28rpx;
    font-weight: bold;
    line-height: 70rpx;
    padding: 0;
    margin: 0;

    &.disabled {
      background-color: $disabled-color;
    }
  }
}

/* --- 购物车弹窗(新增组合商品标识) --- */
.cart-popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0,0,0,0.5);
  z-index: 100;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;

  .cart-popup {
    background-color: #fff;
    max-height: 60vh;
    border-radius: 20rpx 20rpx 0 0;
    animation: slideUp 0.3s ease;

    .popup-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 30rpx;
      border-bottom: 1px solid $border-color;

      .popup-title {
        font-size: 30rpx;
        font-weight: bold;
        color: $text-main;
      }
      .clear-btn {
        font-size: 24rpx;
        color: $text-sub;
      }
      .close-btn {
        font-size: 40rpx;
        color: $text-sub;
        line-height: 1;
      }
    }

    .popup-list {
      max-height: 50vh;
      padding: 0 30rpx;
      box-sizing: border-box;
      .popup-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 30rpx 0;
        border-bottom: 1px solid #f5f5f5;

        .item-info {
          display: flex;
          flex-direction: column;
          flex: 1;
          align-items: flex-start;
          .item-name {
            font-size: 28rpx;
            color: $text-main;
            display: flex;
            align-items: center;
          }
          .item-spec {
            font-size: 22rpx;
            color: $text-sub;
            margin-top: 5rpx;
          }
          .combo-tag {
            font-size: 18rpx;
            color: #fff;
            background-color: $combo-purple;
            padding: 2rpx 8rpx;
            border-radius: 4rpx;
            margin-left: 10rpx;
            transform: scale(0.9);
          }
        }

        .item-control {
          display: flex;
          align-items: center;

          .item-price {
            font-size: 28rpx;
            color: $primary-red;
            font-weight: bold;
            margin-right: 30rpx;
          }

          .btn-group {
            display: flex;
            align-items: center;

            .icon-btn {
              width: 44rpx;
              height: 44rpx;
              border-radius: 50%;
              display: flex;
              align-items: center;
              justify-content: center;
              font-size: 32rpx;
              font-weight: bold;
              border: 1px solid #ddd;
              background-color: #fff;
            }
            .num {
              width: 50rpx;
              text-align: center;
              font-size: 26rpx;
            }
          }
        }
      }
      .empty-cart, .empty-coupon {
        padding: 100rpx 0;
        text-align: center;
        font-size: 26rpx;
        color: $text-sub;
      }
    }
  }
}

/* --- 规格选择弹窗样式(原有兼容) --- */
.spec-popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0,0,0,0.5);
  z-index: 200;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;

  .spec-popup {
    background-color: #fff;
    border-radius: 20rpx 20rpx 0 0;
    animation: slideUp 0.3s ease;
    max-height: 70vh;
    display: flex;
    flex-direction: column;

    .spec-header {
      display: flex;
      padding: 30rpx;
      position: relative;
      border-bottom: 1px solid $border-color;

      .spec-goods-img {
        width: 160rpx;
        height: 160rpx;
        border-radius: 12rpx;
        background-color: #eee;
      }
      .spec-goods-info {
        flex: 1;
        margin-left: 20rpx;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        .spec-goods-name {
          font-size: 30rpx;
          font-weight: bold;
          color: $text-main;
          margin-bottom: 10rpx;
        }
        .spec-goods-price {
          font-size: 32rpx;
          color: $primary-red;
          font-weight: bold;
        }
      }
      .close-spec {
        position: absolute;
        top: 20rpx;
        right: 30rpx;
        font-size: 40rpx;
        color: $text-sub;
        line-height: 1;
      }
    }

    .spec-content {
      flex: 1;
      padding: 0 30rpx;
      overflow-y: auto;

      .spec-group {
        padding: 30rpx 0;
        border-bottom: 1px solid #f5f5f5;
        .spec-title {
          font-size: 28rpx;
          font-weight: 500;
          color: $text-main;
          margin-bottom: 20rpx;
        }
        .spec-options {
          display: flex;
          flex-wrap: wrap;
          gap: 20rpx;
          .spec-option {
            min-width: 120rpx;
            padding: 10rpx 20rpx;
            border: 1px solid $border-color;
            border-radius: 8rpx;
            text-align: center;
            font-size: 26rpx;
            color: $text-main;
            background-color: #f9f9f9;

            &.active {
              border-color: $primary-red;
              background-color: #fff5f5;
              color: $primary-red;
              font-weight: 500;
            }
          }
        }
      }
    }

    .spec-footer {
      padding: 30rpx;
      border-top: 1px solid $border-color;
      display: flex;
      align-items: center;
      justify-content: space-between;

      .num-control {
        display: flex;
        align-items: center;
        font-size: 26rpx;
        color: $text-main;
        .btn-group {
          display: flex;
          align-items: center;
          margin-left: 20rpx;
          .icon-btn {
            width: 44rpx;
            height: 44rpx;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 32rpx;
            font-weight: bold;
            border: 1px solid #ddd;
            background-color: #fff;
            &.disabled {
              background-color: $disabled-color;
              color: #fff;
              border-color: $disabled-color;
            }
          }
          .num {
            width: 50rpx;
            text-align: center;
            font-size: 26rpx;
          }
        }
      }

      .confirm-spec-btn {
        width: 240rpx;
        height: 70rpx;
        background-color: $primary-red;
        color: #fff;
        border: none;
        border-radius: 35rpx;
        font-size: 28rpx;
        font-weight: bold;
        line-height: 70rpx;
        padding: 0;
        margin: 0;

        &.disabled {
          background-color: $disabled-color;
        }
      }
    }
  }
}

/* --- 优惠券&权益弹窗样式(新增组合券样式、权益状态提示) --- */
.coupon-popup-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0,0,0,0.5);
  z-index: 200;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;

  .coupon-popup {
    background-color: #fff;
    border-radius: 20rpx 20rpx 0 0;
    animation: slideUp 0.3s ease;
    max-height: 80vh;
    display: flex;
    flex-direction: column;

    .coupon-tab {
      display: flex;
      border-bottom: 1px solid $border-color;
      .tab-item {
        flex: 1;
        text-align: center;
        padding: 20rpx 0;
        font-size: 28rpx;
        color: $text-main;
        position: relative;
        &.active {
          color: $primary-red;
          font-weight: 500;
          &::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 60rpx;
            height: 4rpx;
            background-color: $primary-red;
            border-radius: 2rpx;
          }
        }
      }
    }

    .coupon-content {
      flex: 1;
      padding: 0 30rpx;
      overflow-y: auto;
      box-sizing: border-box;
      .coupon-title {
        font-size: 26rpx;
        color: $text-sub;
        padding: 20rpx 0 10rpx;
      }

      .coupon-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 20rpx;
        margin-bottom: 20rpx;
        background: linear-gradient(90deg, #fff5f5 0%, #fff 100%);
        border: 1px solid #ffe0e0;
        border-radius: 12rpx;
        position: relative;
        overflow: hidden;

        &::before, &::after {
          content: '';
          position: absolute;
          left: -10rpx;
          width: 20rpx;
          height: 20rpx;
          border-radius: 50%;
          background-color: #fff;
        }
        &::before {
          top: -10rpx;
        }
        &::after {
          bottom: -10rpx;
        }

        &.selected {
          border-color: $primary-red;
          background: linear-gradient(90deg, #ffe0e0 0%, #fff 100%);
        }
        &.disabled {
          opacity: 0.5;
          background: #f9f9f9;
          border-color: #eee;
        }
        &.combo {
          background: linear-gradient(90deg, #f5f0ff 0%, #fff 100%);
          border-color: #e0d0ff;
          &.selected {
            border-color: $combo-purple;
            background: linear-gradient(90deg, #e0d0ff 0%, #fff 100%);
          }
        }

        .coupon-left {
          flex: 1;
          .coupon-amount {
            font-size: 40rpx;
            font-weight: bold;
            color: $primary-red;
            .combo & {
              color: $combo-purple;
            }
          }
          .coupon-name {
            display: block;
            font-size: 28rpx;
            color: $text-main;
            margin: 5rpx 0;
          }
          .coupon-limit {
            font-size: 22rpx;
            color: $text-sub;
          }
        }
        .coupon-right {
          display: flex;
          flex-direction: column;
          align-items: center;
          .select-btn {
            width: 120rpx;
            height: 50rpx;
            line-height: 50rpx;
            text-align: center;
            background-color: $primary-red;
            color: #fff;
            border-radius: 25rpx;
            font-size: 24rpx;
            .combo & {
              background-color: $combo-purple;
            }
          }
          .selected-tag {
            font-size: 24rpx;
            color: $primary-red;
            font-weight: 500;
            .combo & {
              color: $combo-purple;
            }
          }
          .disabled-btn {
            width: 120rpx;
            height: 50rpx;
            line-height: 50rpx;
            text-align: center;
            background-color: $disabled-color;
            color: #fff;
            border-radius: 25rpx;
            font-size: 24rpx;
          }
        }
      }

      .right-item {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 30rpx 0;
        border-bottom: 1px solid $border-color;
        .right-info {
          flex: 1;
          .right-name {
            font-size: 28rpx;
            color: $text-main;
            font-weight: 500;
          }
          .right-desc {
            display: block;
            font-size: 24rpx;
            color: $text-sub;
            margin-top: 5rpx;
          }
          .right-status {
            display: block;
            font-size: 22rpx;
            color: $text-sub;
            margin-top: 5rpx;
            .right-status-red {
              color: $primary-red;
            }
          }
        }
      }
    }

    .coupon-footer {
      padding: 30rpx;
      border-top: 1px solid $border-color;
      .confirm-coupon-btn {
        width: 100%;
        height: 80rpx;
        background-color: $primary-red;
        color: #fff;
        border: none;
        border-radius: 40rpx;
        font-size: 30rpx;
        font-weight: bold;
        line-height: 80rpx;
        padding: 0;
        margin: 0;
      }
    }
  }
}

/* --- 动画定义 --- */
@keyframes bounceIn {
  0% { transform: scale(1); }
  50% { transform: scale(1.3); }
  100% { transform: scale(1); }
}

@keyframes slideUp {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}
</style>