Просмотр исходного кода

零售端-新增购物车功能

ouyang 2 недель назад
Родитель
Сommit
6d281dc320

+ 6 - 1
mallApp/src/components/app-wrapper-empty.vue

@@ -2,7 +2,7 @@
 	<div class="app-empty">
 		<div class="app-empty-center">
 			<div class="app_empty_img">
-				<image :src="`${constant.hostUrl}/image/common/no_data_hua.png`" mode="widthFix"></image>
+				<image :src="emptyImgUrl" mode="widthFix"></image>
 			</div>
 			<div class="app-empty-title">
 				<span class="empty_noData">{{ title }}</span>
@@ -48,6 +48,11 @@ export default {
 		}
 	},
 	computed: {
+		/** 空状态图:优先 imgUrl,避免 hostUrl 未配置时路径变成 undefined/... */
+		emptyImgUrl() {
+			const imgHost = (this.constant && this.constant.imgUrl) || (this.constant && this.constant.hostUrl) || ''
+			return `${imgHost}/image/common/no_data_hua.png`
+		}
 	},
 	data() {
 		return {}

+ 27 - 5
mallApp/src/components/shop-tab-bar/index.vue

@@ -1,7 +1,7 @@
 <!--
   店铺底部导航栏(通用组件)
   用途:店铺首页/分类/购物车/订单四个入口,供店铺相关页面复用
-  说明:首页、购物车跳转暂未实现,仅样式与选中态
+  说明:首页跳转暂未实现;购物车跳转 pages/home/cart
 -->
 <template>
   <view class="shop-tab-bar">
@@ -20,7 +20,13 @@
           <text class="tab-badge-text">{{ cartCount > 99 ? '99+' : cartCount }}</text>
         </view>
       </view>
-      <text class="tab-label" :class="{ 'tab-label-active': current === item.key }">{{ item.label }}</text>
+      <text
+        class="tab-label"
+        :class="{
+          'tab-label-active': current === item.key,
+          'tab-label-active--cart': item.key === 'cart' && current === item.key
+        }"
+      >{{ item.label }}</text>
     </view>
   </view>
 </template>
@@ -63,7 +69,15 @@ export default {
       if (item.key === this.current) {
         return
       }
-      const account = this.account || ''
+      const account = this.account || uni.getStorageSync('account') || ''
+      const hdId = uni.getStorageSync('hdId') || ''
+      const shopQuery = {}
+      if (account) {
+        shopQuery.account = account
+      }
+      if (hdId) {
+        shopQuery.hdId = hdId
+      }
       if (item.key === 'home') {
         // 店铺首页跳转功能暂未实现
         return
@@ -71,12 +85,15 @@ export default {
       if (item.key === 'category') {
         this.$util.pageTo({
           url: '/pages/home/shop-category',
-          query: { account }
+          query: shopQuery
         })
         return
       }
       if (item.key === 'cart') {
-        // 购物车跳转功能暂未实现
+        this.$util.pageTo({
+          url: '/pages/home/cart',
+          query: shopQuery
+        })
         return
       }
       if (item.key === 'order') {
@@ -157,4 +174,9 @@ export default {
   color: #ff4757;
   font-weight: 600;
 }
+
+.tab-label-active--cart {
+  color: #ff4757;
+  font-weight: 600;
+}
 </style>

+ 14 - 6
mallApp/src/constant/index.js

@@ -1,15 +1,22 @@
-import { ProjectName } from '@/config'
+import { ProjectName, APIHOST } from '@/config'
+
+/** 小程序 ext 未注入时的默认图片域名(与 ext.json imgHost 一致) */
+const DEFAULT_IMG_HOST = 'http://img.theflorist.cn'
 
 // #ifdef MP-WEIXIN
 const extConfig = wx.getExtConfigSync ? wx.getExtConfigSync() : {}
 // #endif
+// #ifndef MP-WEIXIN
+const extConfig = {}
+// #endif
+
 // 常量索引
 const envVal = {
 	// #ifdef H5
 	imgUrl: `${window.location.protocol}//img.${getFirstHost()}.com`,
 	// #endif
 	// #ifdef MP-WEIXIN
-	imgUrl: extConfig.imgHost,
+	imgUrl: extConfig.imgHost || DEFAULT_IMG_HOST,
 	// #endif
 	env: process.env.NODE_ENV,
 	// 本地连接地址
@@ -26,11 +33,12 @@ const envVal = {
 
 // #ifdef H5
 envVal.formal = process.env.NODE_ENV == 'development' ? 'http://shop.theflorist.cn' : `${window.location.protocol}//${window.location.host}`
-envVal.hostUrl = process.env.NODE_ENV == 'development' ? `http://api.shop.theflorist.cn` : `${window.location.protocol}//api.${window.location.host}`
+envVal.hostUrl = process.env.NODE_ENV == 'development' ? APIHOST : `${window.location.protocol}//api.${window.location.host}`
 // #endif
 // #ifdef MP-WEIXIN
-envVal.formal = process.env.NODE_ENV == 'development' ? extConfig.apiHost : extConfig.apiHost
-envVal.hostUrl = process.env.NODE_ENV == 'development' ? extConfig.apiHost : extConfig.apiHost
+// 开发者工具未加载 ext 配置时,回退 config.js 中的 APIHOST
+envVal.formal = extConfig.apiHost || APIHOST
+envVal.hostUrl = extConfig.apiHost || APIHOST
 // #endif
 
 function getFirstHost() {
@@ -41,7 +49,7 @@ function getFirstHost() {
 	host = host.length == 3 ? host[1] : host[0]
 	// #endif
 	// #ifndef H5
-	host = extConfig.apiHost
+	host = extConfig.apiHost || APIHOST
 	// #endif
 	return process.env.NODE_ENV == 'development' ? 'huaml' : host
 }

+ 1 - 0
mallApp/src/pages.json

@@ -9,6 +9,7 @@
         { "path": "pages/home/index", "style": { "navigationBarTitleText": "首页" } },
         { "path": "pages/home/category", "style": { "navigationBarTitleText": "花束" } },
         { "path": "pages/home/shop-category", "style": { "navigationBarTitleText": "分类" } },
+        { "path": "pages/home/cart", "style": { "navigationBarTitleText": "购物车", "navigationBarTextStyle": "black" } },
         { "path": "pages/home/shop-notice-detail", "style": { "navigationBarTitleText": "公告详情" } },
         { "path": "pages/home/pic-text-detail", "style": { "navigationBarTitleText": "图文详情" } },
         { "path": "pages/home/mall", "style": { "navigationBarTitleText": "相册" } },

+ 1165 - 0
mallApp/src/pages/home/cart.vue

@@ -0,0 +1,1165 @@
+<!--
+  店铺购物车页
+  用途:展示花材购物车(Vuex cg 选型),支持勾选、改数量、删除、去结算
+  路由:pages/home/cart?account=xxx&hdId=xxx
+-->
+<template>
+  <view class="cart-page">
+    <scroll-view scroll-y class="cart-scroll" :show-scrollbar="false">
+      <!-- 店铺信息 -->
+      <view class="store-card" v-if="displayCartList.length">
+        <view class="store-card-top">
+          <view class="store-main" @tap="goCategory">
+            <view class="store-icon-wrap">
+              <image v-if="shopImg" class="store-icon" :src="shopImg" mode="aspectFill" />
+              <sprite-icon v-else name="icon-home" :size="36" />
+            </view>
+            <text class="store-name">{{ shopName || '店铺' }}</text>
+            <text class="store-arrow">›</text>
+          </view>
+          <view class="store-promo" v-if="checkedDiscountAmount > 0" @tap="toggleDiscountDetail">
+            <text class="store-promo-text">满减优惠,已减{{ formatPrice(checkedDiscountAmount) }}元</text>
+            <text class="store-promo-arrow">›</text>
+          </view>
+        </view>
+        <text class="store-tip">本店商品由{{ shopName || '本店' }}配送</text>
+      </view>
+
+      <!-- 商品列表 -->
+      <view class="cart-list" v-if="displayCartList.length">
+        <view
+          class="cart-item"
+          v-for="(item, index) in displayCartList"
+          :key="index"
+        >
+          <view class="cart-item-check" :data-index="index" @tap="onToggleCheck">
+            <text
+              class="iconfont check-icon"
+              :class="isItemChecked(item) ? 'iconxuanzhong check-icon--active' : 'iconweixuanzhong'"
+            ></text>
+          </view>
+          <image
+            class="cart-item-cover"
+            :src="item.cover"
+            mode="aspectFill"
+            :data-index="index"
+            @tap="onGoItemDetail"
+          />
+          <view class="cart-item-body">
+            <view class="cart-item-top">
+              <view class="cart-item-title-wrap">
+                <text class="item-tag item-tag--hot" v-if="Number(item.discountPrice) > 0">热销</text>
+                <text class="cart-item-name">{{ item.itemName || item.name }}</text>
+              </view>
+              <view class="cart-item-del" :data-index="index" @tap.stop="onRemoveItem">
+                <sprite-icon name="cart-delete" :size="28" />
+              </view>
+            </view>
+            <text class="cart-item-spec">{{ getItemSpec(item) }}</text>
+            <view class="cart-item-bottom">
+              <view class="cart-item-price-wrap">
+                <text class="cart-item-price">¥{{ formatPrice(getCartItemUnitPrice(item)) }}</text>
+                <text
+                  v-if="cartPriceShowStrike(item)"
+                  class="cart-item-price-old"
+                >¥{{ formatPrice(getCartItemOriginalUnitPrice(item)) }}</text>
+              </view>
+              <view class="stepper" v-if="!isOutOfStock(item)">
+                <view class="stepper-btn stepper-btn--minus" :data-index="index" @tap.stop="onMinusItem">
+                  <text class="stepper-symbol">-</text>
+                </view>
+                <text class="stepper-num" :data-index="index" @tap.stop="onOpenCustomNum">{{ getItemQtyText(item) }}</text>
+                <view class="stepper-btn stepper-btn--plus" :data-index="index" @tap.stop="onPlusItem">
+                  <text class="stepper-symbol">+</text>
+                </view>
+              </view>
+              <text v-else class="out-stock">缺货</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 空购物车 -->
+      <view class="cart-empty" v-else>
+        <app-wrapper-empty title="购物车空空如也" :is-empty="true" />
+        <view class="cart-empty-btn" @tap="goCategory">去逛逛</view>
+      </view>
+
+      <view class="cart-scroll-placeholder"></view>
+    </scroll-view>
+
+    <!-- 底部结算栏 -->
+    <view class="cart-footer" v-if="displayCartList.length">
+      <view class="coupon-bar" @tap="toggleDiscountDetail">
+        <view class="coupon-bar-left">
+          <text class="coupon-badge">券</text>
+          <text class="coupon-label">优惠券</text>
+          <text class="coupon-desc" v-if="checkedDiscountAmount > 0">满减已减{{ formatPrice(checkedDiscountAmount) }}元</text>
+        </view>
+        <text class="coupon-bar-right" v-if="checkedDiscountAmount > 0">已优惠 ¥{{ formatPrice(checkedDiscountAmount) }} ›</text>
+        <text class="coupon-bar-right coupon-bar-right--muted" v-else>暂无可用 ›</text>
+      </view>
+      <view class="checkout-bar">
+        <view class="checkout-left" @tap="toggleSelectAll">
+          <text
+            class="iconfont check-icon"
+            :class="isAllChecked ? 'iconxuanzhong check-icon--active' : 'iconweixuanzhong'"
+          ></text>
+          <text class="checkout-all-text">全选</text>
+        </view>
+        <view class="checkout-center">
+          <view class="checkout-total-row">
+            <text class="checkout-total-label">合计:</text>
+            <text class="checkout-total-price">¥{{ formatPrice(checkedTotalPrice) }}</text>
+          </view>
+          <view class="checkout-discount-row" v-if="checkedDiscountAmount > 0" @tap="toggleDiscountDetail">
+            <text class="checkout-discount-text">已优惠 ¥{{ formatPrice(checkedDiscountAmount) }}</text>
+            <text class="checkout-discount-link">优惠明细 {{ showDiscountDetail ? '∧' : '∨' }}</text>
+          </view>
+        </view>
+        <view
+          class="checkout-btn"
+          :class="{ 'checkout-btn--disabled': !checkedCheckoutList.length }"
+          @tap="goCheckout"
+        >
+          去结算({{ checkedKindCount }})
+        </view>
+      </view>
+    </view>
+
+    <!-- 自定义扎数 -->
+    <modal-module
+      :show="isModel"
+      @cancel="modalCancel"
+      @click="affirm"
+      :title="customModalTitle"
+      color="#333"
+      :size="32"
+      padding="30upx 30upx"
+    >
+      <template slot="customContent">
+        <view class="select-cmd_bx" v-if="customData">
+          <view class="kc">&nbsp;</view>
+          <view class="num_bx">
+            <input v-model="customData.bigCount" :focus="isFocus" type="number" placeholder="扎数" />
+          </view>
+        </view>
+      </template>
+    </modal-module>
+
+    <shop-tab-bar current="cart" :account="shopAccount" :cart-count="cartBadgeCount" />
+  </view>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import ShopTabBar from '@/components/shop-tab-bar/index.vue'
+import ModalModule from '@/components/item/plugin/modal'
+import productMins from '@/mixins/cgProduct'
+import { getInfo } from '@/api/shop'
+import { getLimitBuyInfo } from '@/api/order'
+import { share } from '@/mixins'
+
+export default {
+  name: 'shopCart',
+  components: {
+    AppWrapperEmpty,
+    ShopTabBar,
+    ModalModule
+  },
+  mixins: [productMins, share],
+  data() {
+    return {
+      pageType: 'cg',
+      autoLoad: false,
+      isGhsProduct: true,
+      /** 购物车页不校验库存字段,避免缓存商品缺 bigNum 导致加减无效 */
+      offVerifyRepertory: true,
+      hdId: 0,
+      shopAccount: '',
+      shopName: '',
+      shopImg: '',
+      /** 勾选状态:key 为 id_classId */
+      checkedMap: {},
+      showDiscountDetail: false,
+      isModel: false,
+      isFocus: false,
+      customData: {
+        bigCount: 0,
+        smallCount: 0
+      },
+      limitBuyWarnList: []
+    }
+  },
+  computed: {
+    ...mapGetters({ loginInfo: 'getLoginInfo' }),
+    /** 过滤无效项,避免 selectList 含空项导致 bigCount 报错 */
+    displayCartList() {
+      return this.sanitizeCartList(this.selectList)
+    },
+    cartBadgeCount() {
+      let count = 0
+      this.displayCartList.forEach((item) => {
+        count += Number(item.bigCount) || 0
+        count += Number(item.smallCount) || 0
+      })
+      return count
+    },
+    /** 已勾选且数量有效的商品 */
+    checkedCheckoutList() {
+      if (!this.displayCartList.length) {
+        return []
+      }
+      return this.displayCartList.filter((item) => {
+        if (!this.isItemChecked(item)) {
+          return false
+        }
+        if (this.isOutOfStock(item)) {
+          return false
+        }
+        return this.hasValidQty(item)
+      })
+    },
+    /** 勾选商品合计金额 */
+    checkedTotalPrice() {
+      let price = 0
+      this.checkedCheckoutList.forEach((item) => {
+        price += this.getSelectItemTotalPrice(item)
+      })
+      return Math.round(price * 100) / 100
+    },
+    /** 勾选商品满减节省 */
+    checkedDiscountAmount() {
+      let discount = 0
+      this.checkedCheckoutList.forEach((item) => {
+        discount += this.getReachDiscountAmount(item)
+      })
+      return Math.round(discount * 100) / 100
+    },
+    /** 去结算括号内数量:按扎数+支数统计 */
+    checkedKindCount() {
+      let count = 0
+      this.checkedCheckoutList.forEach((item) => {
+        count += Number(item.bigCount) || 0
+        count += Number(item.smallCount) || 0
+      })
+      return count
+    },
+    /** 可勾选的有效商品(非缺货) */
+    selectableList() {
+      return this.displayCartList.filter((item) => !this.isOutOfStock(item))
+    },
+    isAllChecked() {
+      const list = this.selectableList
+      if (!list.length) {
+        return false
+      }
+      return list.every((item) => this.isItemChecked(item))
+    },
+    /** 弹窗标题,避免 customData 为空时模板解析异常 */
+    customModalTitle() {
+      return (this.customData && this.customData.itemName) || ''
+    }
+  },
+  onShow() {
+    this.applyShopFromCache()
+    this.cleanupCartList()
+    this.restoreItemCartFromStorage()
+    this.cleanupCartList()
+    this.syncCheckedMap()
+  },
+  methods: {
+    /** 清洗购物车数组,去掉空项并规范数量字段 */
+    sanitizeCartList(list) {
+      if (!Array.isArray(list)) {
+        return []
+      }
+      return list
+        .filter((item) => item && item.id !== undefined && item.id !== null)
+        .map((item) => ({
+          ...item,
+          bigCount: Number(item.bigCount) || 0,
+          smallCount: Number(item.smallCount) || 0
+        }))
+    },
+    /** 同步清洗后的列表到 Vuex */
+    cleanupCartList() {
+      const cleaned = this.sanitizeCartList(this.selectList)
+      const rawLen = Array.isArray(this.selectList) ? this.selectList.length : 0
+      if (cleaned.length !== rawLen) {
+        this.setSelectInfoByType({ type: this.pageType, info: cleaned })
+      }
+      return cleaned
+    },
+    /** 小程序事件包装时取出原始商品对象 */
+    normalizeCartItem(item) {
+      if (item && item.$orig) {
+        return item.$orig
+      }
+      return item
+    },
+    init() {
+      this.hdId = this.option.hdId
+        ? Number(this.option.hdId)
+        : (Number(uni.getStorageSync('hdId')) || 0)
+      this.shopAccount = this.option.account
+        ? String(this.option.account)
+        : (uni.getStorageSync('account') || '')
+      if (this.shopAccount) {
+        uni.setStorageSync('account', this.shopAccount)
+      }
+      if (this.hdId) {
+        uni.setStorageSync('hdId', this.hdId)
+      }
+      this.applyShopFromCache()
+      this.restoreItemCartFromStorage()
+      this.syncCheckedMap()
+      this.loadShopInfo()
+    },
+    /** 从本地缓存恢复店铺名称,接口失败时仍能展示 */
+    applyShopFromCache() {
+      const info = uni.getStorageSync('currentShop')
+      if (!info || typeof info !== 'object') {
+        return
+      }
+      const sjName = info.merchantName || ''
+      const subName = info.shopName || '首店'
+      this.shopName = subName === '首店' ? sjName : `${sjName} ${subName}`.trim()
+      this.shopImg = info.avatar || ''
+    },
+    /** 从本地缓存恢复花材购物车 */
+    restoreItemCartFromStorage() {
+      const account = this.shopAccount || (this.option && this.option.account) || uni.getStorageSync('account')
+      if (!account) {
+        this.resetSelectInfoByType(this.pageType)
+        return
+      }
+      const storageKey = 'selectList' + this.pageType + '_hd_' + account
+      const cached = uni.getStorageSync(storageKey)
+      if (!this.$util.isEmpty(cached)) {
+        this.setSelectInfoByType({
+          type: this.pageType,
+          info: this.sanitizeCartList(cached)
+        })
+      } else {
+        this.cleanupCartList()
+      }
+    },
+    loadShopInfo() {
+      return getInfo({ hdId: this.hdId || uni.getStorageSync('hdId') || 0 }).then((res) => {
+        if (res.code !== 1 || !res.data) {
+          return
+        }
+        if (res.data.hd && Number(res.data.hd.id) > 0) {
+          if (Number(this.hdId) === 0) {
+            this.hdId = res.data.hd.id
+          }
+          uni.setStorageSync('hdId', res.data.hd.id)
+        }
+        if (res.data.info) {
+          uni.setStorageSync('currentShop', res.data.info)
+          const info = res.data.info
+          const sjName = info.merchantName || ''
+          const subName = info.shopName || '首店'
+          this.shopName = subName === '首店' ? sjName : `${sjName} ${subName}`
+          this.shopImg = info.avatar || ''
+        }
+      }).catch(() => {})
+    },
+    getItemKey(item) {
+      if (!item || item.id === undefined || item.id === null) {
+        return ''
+      }
+      return `${item.id}_${item.classId}`
+    },
+    syncCheckedMap() {
+      const map = { ...this.checkedMap }
+      this.displayCartList.forEach((item) => {
+        const key = this.getItemKey(item)
+        if (!key) {
+          return
+        }
+        if (map[key] === undefined) {
+          map[key] = !this.isOutOfStock(item)
+        }
+      })
+      Object.keys(map).forEach((key) => {
+        const exists = (this.selectList || []).some((item) => this.getItemKey(item) === key)
+        if (!exists) {
+          delete map[key]
+        }
+      })
+      this.checkedMap = map
+    },
+    isItemChecked(item) {
+      if (!item) {
+        return false
+      }
+      return !!this.checkedMap[this.getItemKey(item)]
+    },
+    /** 小程序 v-for 内通过 data-index 取当前行商品 */
+    getCartItemByEvent(e) {
+      const index = Number(e.currentTarget.dataset.index)
+      if (isNaN(index) || index < 0) {
+        return null
+      }
+      return this.displayCartList[index] || null
+    },
+    onToggleCheck(e) {
+      const item = this.getCartItemByEvent(e)
+      if (item) {
+        this.toggleCheck(item)
+      }
+    },
+    onGoItemDetail(e) {
+      const item = this.getCartItemByEvent(e)
+      if (item) {
+        this.goItemDetail(item)
+      }
+    },
+    onRemoveItem(e) {
+      const item = this.getCartItemByEvent(e)
+      if (item) {
+        this.removeItem(item)
+      }
+    },
+    onPlusItem(e) {
+      const item = this.getCartItemByEvent(e)
+      if (item) {
+        this.plusItem(item)
+      }
+    },
+    onMinusItem(e) {
+      const item = this.getCartItemByEvent(e)
+      if (item) {
+        this.minusItem(item)
+      }
+    },
+    onOpenCustomNum(e) {
+      const item = this.getCartItemByEvent(e)
+      if (item) {
+        this.openCustomNum(item)
+      }
+    },
+    toggleCheck(item) {
+      if (this.isOutOfStock(item)) {
+        return
+      }
+      const key = this.getItemKey(item)
+      this.$set(this.checkedMap, key, !this.checkedMap[key])
+    },
+    toggleSelectAll() {
+      const next = !this.isAllChecked
+      const map = { ...this.checkedMap }
+      this.selectableList.forEach((item) => {
+        map[this.getItemKey(item)] = next
+      })
+      this.checkedMap = map
+    },
+    isOutOfStock(item) {
+      return Number(item.stock) <= 0 || (Number(item.bigNum) <= 0 && Number(item.smallNum) <= 0)
+    },
+    hasValidQty(item) {
+      return /(^[1-9]\d*$)/.test(item.bigCount) || /(^[1-9]\d*$)/.test(item.smallCount)
+    },
+    getItemSpec(item) {
+      if (item.itemRemark) {
+        return item.itemRemark
+      }
+      const ratio = Number(item.ratio)
+      if (item.ratioType == 0 && ratio > 0) {
+        return `${ratio}${item.smallUnit || '支'}/${item.bigUnit || '扎'}`
+      }
+      return `若干${item.smallUnit || '支'}/${item.bigUnit || '扎'}`
+    },
+    getItemQtyText(item) {
+      const big = Number(item.bigCount) || 0
+      const small = Number(item.smallCount) || 0
+      if (big > 0 && small > 0) {
+        return `${big}/${small}`
+      }
+      if (big > 0) {
+        return String(big)
+      }
+      if (small > 0) {
+        return String(small)
+      }
+      return '0'
+    },
+    formatPrice(val) {
+      const num = parseFloat(val)
+      return isNaN(num) ? '0' : num
+    },
+    /** 写入本地购物车缓存 */
+    persistCartToStorage() {
+      const account = this.shopAccount || (this.option && this.option.account) || uni.getStorageSync('account')
+      if (!account) {
+        return
+      }
+      uni.setStorageSync('selectList' + this.pageType + '_hd_' + account, this.sanitizeCartList(this.selectList))
+    },
+    /** 在 selectList 中定位当前行 */
+    findCartRowIndex(list, item) {
+      if (!item || item.id === undefined || item.id === null) {
+        return -1
+      }
+      return list.findIndex(
+        (el) => el && String(el.id) === String(item.id) && String(el.classId) === String(item.classId)
+      )
+    },
+    /** 更新购物车数量并同步 Vuex */
+    updateCartQty(item, bigCount, smallCount) {
+      item = this.normalizeCartItem(item)
+      const list = this.sanitizeCartList(this.$util.copyObject(this.selectList || []))
+      const idx = this.findCartRowIndex(list, item)
+      if (idx === -1) {
+        return
+      }
+      const nextBig = Number(bigCount) || 0
+      const nextSmall = Number(smallCount) || 0
+      if (nextBig <= 0 && nextSmall <= 0) {
+        list.splice(idx, 1)
+      } else {
+        list[idx] = {
+          ...list[idx],
+          bigCount: nextBig,
+          smallCount: nextSmall
+        }
+      }
+      this.setSelectInfoByType({ type: this.pageType, info: this.sanitizeCartList(list) })
+      this.persistCartToStorage()
+      this.syncCheckedMap()
+    },
+    plusItem(item) {
+      item = this.normalizeCartItem(item)
+      if (!item) {
+        return
+      }
+      const big = Number(item.bigCount) || 0
+      const small = Number(item.smallCount) || 0
+      this.updateCartQty(item, big + 1, small)
+      if (this.$util.hitRemind) {
+        this.$util.hitRemind()
+      }
+    },
+    minusItem(item) {
+      item = this.normalizeCartItem(item)
+      if (!item) {
+        return
+      }
+      const big = Number(item.bigCount) || 0
+      const small = Number(item.smallCount) || 0
+      if (big > 0) {
+        this.updateCartQty(item, big - 1, small)
+      } else if (small > 0) {
+        this.updateCartQty(item, big, small - 1)
+      }
+      if (this.$util.hitRemind) {
+        this.$util.hitRemind()
+      }
+    },
+    removeItem(item) {
+      uni.showModal({
+        title: '提示',
+        content: '确定删除该商品吗?',
+        success: (res) => {
+          if (res.confirm) {
+            this.delAllEvent(item)
+            this.persistCartToStorage()
+            this.syncCheckedMap()
+          }
+        }
+      })
+    },
+    openCustomNum(item) {
+      this.isModel = true
+      this.customData = { ...item }
+      this.$nextTick(() => {
+        this.customData.smallCount = this.customData.smallCount == 0 ? null : this.customData.smallCount
+        this.customData.bigCount = this.customData.bigCount == 0 ? null : this.customData.bigCount
+        this.isFocus = true
+      })
+    },
+    modalCancel() {
+      this.isModel = false
+      this.customData = {}
+    },
+    affirm(val) {
+      this.isFocus = false
+      this.$nextTick(() => {
+        if (val.index === 0) {
+          if (!(this.customData.bigCount > 0 || this.customData.smallCount > 0)) {
+            this.delAllEvent(this.customData)
+          }
+          this.modalCancel()
+          this.syncCheckedMap()
+          return false
+        }
+        if (!/(^[1-9]\d*$)/.test(this.customData.bigCount) && !/(^[1-9]\d*$)/.test(this.customData.smallCount)) {
+          this.delAllEvent(this.customData)
+          this.modalCancel()
+          this.syncCheckedMap()
+          return
+        }
+        const { bigCount, smallCount, bigNum, smallNum, ratio } = this.customData
+        const ratioNum = Number(ratio)
+        if (Number(bigCount) * ratioNum + Number(smallCount) > Number(bigNum) * ratioNum + Number(smallNum)) {
+          uni.showToast({ title: '库存还剩' + bigNum, icon: 'none' })
+        } else {
+          this.updateCartQty(this.customData, Number(bigCount) || 0, Number(smallCount) || 0)
+          this.modalCancel()
+        }
+      })
+    },
+    toggleDiscountDetail() {
+      this.showDiscountDetail = !this.showDiscountDetail
+    },
+    goCategory() {
+      this.$util.pageTo({
+        url: '/pages/home/shop-category',
+        query: { account: this.shopAccount, hdId: this.hdId }
+      })
+    },
+    goItemDetail(item) {
+      this.$util.pageTo({
+        url: '/pages/item/detail',
+        query: {
+          id: item.id,
+          account: this.shopAccount,
+          hdId: this.hdId
+        }
+      })
+    },
+    /** 去结算:仅提交已勾选商品 */
+    goCheckout() {
+      const list = this.checkedCheckoutList
+      if (!list.length) {
+        this.$msg('请选择要结算的商品')
+        return
+      }
+      const ghsId = Number(this.shopAccount) || Number(uni.getStorageSync('account')) || 0
+      if (!ghsId) {
+        this.$msg('缺少店铺信息')
+        return
+      }
+      const account = this.shopAccount || 0
+      const newList = list
+        .filter((item) => Number(item.limitBuy) > 0)
+        .map((item) => ({ id: item.id, bigCount: item.bigCount }))
+      const navigateAffirm = () => {
+        this.limitBuyWarnList = []
+        this.setSelectInfoByType({ type: this.pageType, info: list })
+        this.setLimitBuyInfoByType({ type: this.pageType, info: [] })
+        const realHdId = this.hdId || uni.getStorageSync('hdId') || 0
+        this.$util.pageTo({
+          url: '/pages/billing/affirmGhs',
+          query: { account, hdId: realHdId },
+          type: 2
+        })
+      }
+      if (newList.length > 0) {
+        getLimitBuyInfo({ ghsId, account, hdId: this.hdId, list: newList }).then((res) => {
+          const limitBuyInfo = Array.isArray(res.data) ? res.data : []
+          const warnList = limitBuyInfo.filter((item) => item.specialPrice === false && item.reachLimitBuyNum === true)
+          if (warnList.length > 0) {
+            this.limitBuyWarnList = warnList
+            this.$msg('有花材超出限购')
+            return
+          }
+          this.setLimitBuyInfoByType({ type: this.pageType, info: limitBuyInfo })
+          this.setSelectInfoByType({ type: this.pageType, info: list })
+          const realHdId = this.hdId || uni.getStorageSync('hdId') || 0
+          this.$util.pageTo({
+            url: '/pages/billing/affirmGhs',
+            query: { account, hdId: realHdId },
+            type: 2
+          })
+        })
+      } else {
+        navigateAffirm()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.cart-page {
+  min-height: 100vh;
+  background: $backColor;
+  box-sizing: border-box;
+  padding-bottom: calc(300upx + env(safe-area-inset-bottom));
+}
+
+.cart-scroll {
+  height: 100vh;
+  box-sizing: border-box;
+}
+
+.store-card {
+  margin: 16upx 24upx 0;
+  padding: 24upx;
+  background: #fff;
+  border-radius: 16upx;
+  box-sizing: border-box;
+}
+
+.store-card-top {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.store-main {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.store-icon-wrap {
+  width: 48upx;
+  height: 48upx;
+  border-radius: 24upx;
+  overflow: hidden;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.store-icon {
+  width: 48upx;
+  height: 48upx;
+}
+
+.store-name {
+  margin-left: 12upx;
+  font-size: 30upx;
+  font-weight: 600;
+  color: $fontColorMain;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.store-arrow {
+  margin-left: 8upx;
+  font-size: 32upx;
+  color: #ccc;
+  flex-shrink: 0;
+}
+
+.store-promo {
+  flex-shrink: 0;
+  margin-left: 12upx;
+  padding: 8upx 16upx;
+  background: $backPinkColor;
+  border-radius: 8upx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.store-promo-text {
+  font-size: 22upx;
+  color: $fontPinkColor;
+  white-space: nowrap;
+}
+
+.store-promo-arrow {
+  margin-left: 4upx;
+  font-size: 24upx;
+  color: $fontPinkColor;
+}
+
+.store-tip {
+  display: block;
+  margin-top: 12upx;
+  font-size: 22upx;
+  color: $greenColor;
+}
+
+.cart-list {
+  margin-top: 16upx;
+}
+
+.cart-item {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  margin: 0 24upx 16upx;
+  padding: 24upx;
+  background: #fff;
+  border-radius: 16upx;
+  box-sizing: border-box;
+}
+
+.cart-item-check {
+  padding-top: 48upx;
+  margin-right: 16upx;
+  flex-shrink: 0;
+}
+
+.check-icon {
+  font-size: 40upx;
+  color: #ddd;
+}
+
+.check-icon--active {
+  color: $greenColor;
+}
+
+.cart-item-cover {
+  width: 160upx;
+  height: 160upx;
+  border-radius: 12upx;
+  flex-shrink: 0;
+  background: #f0f0f0;
+}
+
+.cart-item-body {
+  flex: 1;
+  min-width: 0;
+  margin-left: 20upx;
+}
+
+.cart-item-top {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+}
+
+.cart-item-title-wrap {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.item-tag {
+  margin-right: 8upx;
+  padding: 0 8upx;
+  font-size: 20upx;
+  line-height: 1.4;
+  border-radius: 4upx;
+  flex-shrink: 0;
+}
+
+.item-tag--hot {
+  color: $fontPinkColor;
+  border: 1upx solid $fontPinkColor;
+}
+
+.cart-item-name {
+  flex: 1;
+  min-width: 0;
+  font-size: 30upx;
+  font-weight: 600;
+  color: $fontColorMain;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.cart-item-del {
+  margin-left: 12upx;
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.cart-item-spec {
+  display: block;
+  margin-top: 8upx;
+  font-size: 24upx;
+  color: $fontColor3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cart-item-bottom {
+  margin-top: 16upx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.cart-item-price-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: baseline;
+}
+
+.cart-item-price {
+  font-size: 32upx;
+  font-weight: 600;
+  color: $fontPinkColor;
+}
+
+.cart-item-price-old {
+  margin-left: 8upx;
+  font-size: 24upx;
+  color: $fontColor3;
+  text-decoration: line-through;
+}
+
+.stepper {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.stepper-btn {
+  width: 48upx;
+  height: 48upx;
+  border-radius: 8upx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-sizing: border-box;
+}
+
+.stepper-btn--minus {
+  border: 2upx solid $greenColor;
+  background: #fff;
+}
+
+.stepper-btn--plus {
+  background: $greenColor;
+}
+
+.stepper-symbol {
+  font-size: 32upx;
+  line-height: 1;
+  color: $greenColor;
+}
+
+.stepper-btn--plus .stepper-symbol {
+  color: #fff;
+}
+
+.stepper-num {
+  min-width: 64upx;
+  height: 48upx;
+  line-height: 48upx;
+  margin: 0 12upx;
+  padding: 0 8upx;
+  text-align: center;
+  font-size: 28upx;
+  color: $fontColorMain;
+  background: #f5f5f5;
+  border-radius: 8upx;
+}
+
+.out-stock {
+  font-size: 26upx;
+  color: $fontColor3;
+  padding: 8upx 20upx;
+  background: #f5f5f5;
+  border-radius: 8upx;
+}
+
+.cart-empty {
+  padding-top: 120upx;
+}
+
+.cart-empty-btn {
+  width: 280upx;
+  height: 72upx;
+  line-height: 72upx;
+  margin: 32upx auto 0;
+  text-align: center;
+  font-size: 28upx;
+  color: #fff;
+  background: $greenColor;
+  border-radius: 36upx;
+}
+
+.cart-scroll-placeholder {
+  height: 40upx;
+}
+
+.cart-footer {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: calc(110upx + env(safe-area-inset-bottom)); 
+  z-index: 190;
+  background: #fff;
+  box-shadow: 0 -4upx 16upx rgba(0, 0, 0, 0.06);
+}
+
+.coupon-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16upx 24upx;
+  background: $backPinkColor;
+  box-sizing: border-box;
+}
+
+.coupon-bar-left {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.coupon-badge {
+  width: 36upx;
+  height: 36upx;
+  line-height: 36upx;
+  text-align: center;
+  font-size: 22upx;
+  color: #fff;
+  background: #ff8c3a;
+  border-radius: 6upx;
+  flex-shrink: 0;
+}
+
+.coupon-label {
+  margin-left: 12upx;
+  font-size: 26upx;
+  color: $fontColorMain;
+  flex-shrink: 0;
+}
+
+.coupon-desc {
+  margin-left: 12upx;
+  font-size: 24upx;
+  color: $fontPinkColor;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.coupon-bar-right {
+  margin-left: 12upx;
+  font-size: 24upx;
+  color: $fontPinkColor;
+  flex-shrink: 0;
+}
+
+.coupon-bar-right--muted {
+  color: $fontColor3;
+}
+
+.checkout-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 16upx 24upx;
+  box-sizing: border-box;
+}
+
+.checkout-left {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.checkout-all-text {
+  margin-left: 8upx;
+  font-size: 26upx;
+  color: $fontColorMain;
+}
+
+.checkout-center {
+  flex: 1;
+  min-width: 0;
+  margin-left: 16upx;
+}
+
+.checkout-total-row {
+  display: flex;
+  flex-direction: row;
+  align-items: baseline;
+}
+
+.checkout-total-label {
+  font-size: 26upx;
+  color: $fontColorMain;
+}
+
+.checkout-total-price {
+  font-size: 36upx;
+  font-weight: 600;
+  color: $fontPinkColor;
+}
+
+.checkout-discount-row {
+  margin-top: 4upx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.checkout-discount-text {
+  font-size: 22upx;
+  color: $fontColor3;
+}
+
+.checkout-discount-link {
+  margin-left: 8upx;
+  font-size: 22upx;
+  color: $fontPinkColor;
+}
+
+.checkout-btn {
+  flex-shrink: 0;
+  min-width: 200upx;
+  height: 72upx;
+  line-height: 72upx;
+  padding: 0 28upx;
+  margin-left: 12upx;
+  text-align: center;
+  font-size: 28upx;
+  color: #fff;
+  background: $greenColor;
+  border-radius: 36upx;
+}
+
+.checkout-btn--disabled {
+  opacity: 0.5;
+}
+
+.select-cmd_bx .kc {
+  color: #999;
+  font-size: 28upx;
+  margin-bottom: 60upx;
+  text-align: center;
+  margin-top: 10upx;
+}
+
+.select-cmd_bx .num_bx {
+  display: flex;
+  align-items: center;
+  margin-bottom: 80upx;
+}
+
+.select-cmd_bx .num_bx input {
+  width: 480upx;
+  height: 80upx;
+  border: 1upx solid #ddd;
+  border-radius: 4upx;
+  text-align: center;
+  margin-left: 50upx;
+  font-size: 40upx;
+}
+</style>

+ 8 - 4
mallApp/src/pages/home/shop-category.vue

@@ -482,8 +482,12 @@ export default {
   },
   methods: {
     init() {
-      this.hdId = this.option.hdId ? Number(this.option.hdId) : 0
-      this.shopAccount = this.option.account ? String(this.option.account) : ''
+      this.hdId = this.option.hdId
+        ? Number(this.option.hdId)
+        : (Number(uni.getStorageSync('hdId')) || 0)
+      this.shopAccount = this.option.account
+        ? String(this.option.account)
+        : (uni.getStorageSync('account') || '')
       // 与 item 页一致:先写入 account / hdId,供 item/index 等接口使用
       if (this.shopAccount) {
         uni.setStorageSync('account', this.shopAccount)
@@ -1191,7 +1195,6 @@ export default {
   font-size: 24upx;
   color: #333;
   text-align: left;
-  border-top: 1upx solid rgba(255, 71, 87, 0.08);
 }
 
 .item-cate-sub text {
@@ -1239,6 +1242,7 @@ export default {
   padding: 16upx 8upx 16upx 14upx;
   background: #fff;
   border-top: 1upx solid #f5f5f5;
+  border-right: 1upx solid #f5f5f5;
 }
 
 .bouquet-cate-icon {
@@ -1463,7 +1467,7 @@ export default {
 /* 整页全宽 FooterCart:只改 bottom,保留组件默认 702upx 居中样式 */
 .shop-category-footer-cart {
   ::v-deep .cart-ft_bx {
-    bottom: calc(180upx + env(safe-area-inset-bottom));
+    bottom: calc(130upx + env(safe-area-inset-bottom));
   }
 }
 

+ 2 - 1
mallApp/src/utils/mainIndexSprite.js

@@ -16,7 +16,7 @@ const DEFAULT_SHEET_KEY = "main";
  */
 export const SPRITE_SHEETS = {
   main: {
-    path: "/hhb/sprite_icon/main-index.webp?time=2607101747",
+    path: "/hhb/sprite_icon/main-index.webp?time=26071610",
     width: 500,
     height: 500,
     icons: {
@@ -58,6 +58,7 @@ export const SPRITE_SHEETS = {
 	  "icon-cart-active": { x: 293, y: 245, w: 50, h: 50 },
 	  "icon-order": { x: 366, y: 245, w: 50, h: 50 },
 	  "icon-order-active": { x: 418, y: 245 , w: 50, h: 50 },
+	  "cart-delete": { x: 7, y: 298 , w: 46, h: 52 },//购物车-删除
     }
   },
   /** 进货/供货商专用雪碧图,坐标待设计稿测量后补全 */