Selaa lähdekoodia

feat(mall): 优化商品搜索与筛选体验

- 支持首页关键词直达商品列表,并改为确认后触发搜索\n- 完善筛选清空与状态提示,修复微信端多 ID 路由截断和勾选高亮
shizhongqi 2 päivää sitten
vanhempi
commit
e43ba8d1a1

+ 8 - 4
mallApp/src/components/home/bannerSwiper.vue

@@ -29,6 +29,8 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import { parseFilterIds, encodeFilterValue, stashGoodsListFilter } from '@/utils/goodsListFilter'
+
 export default {
 export default {
   name: 'homeBannerSwiper',
   name: 'homeBannerSwiper',
   props: {
   props: {
@@ -74,7 +76,7 @@ export default {
     onBannerTap(index) {
     onBannerTap(index) {
       const item = (this.data.list || [])[index]
       const item = (this.data.list || [])[index]
       if (!item) return
       if (!item) return
-      const ids = String(item.value || '').split(',').filter(Boolean)
+      const ids = parseFilterIds(item.value)
       if (!ids.length) return
       if (!ids.length) return
       const type = Number(item.type)
       const type = Number(item.type)
       const account = this.account
       const account = this.account
@@ -89,13 +91,15 @@ export default {
         if (ids.length === 1) {
         if (ids.length === 1) {
           this.pageTo({ url: `/pages/goods/detail?id=${ids[0]}&account=${account}&hdId=${hdId}` })
           this.pageTo({ url: `/pages/goods/detail?id=${ids[0]}&account=${account}&hdId=${hdId}` })
         } else {
         } else {
-          this.pageTo({ url: `/pages/goods/list?account=${account}&hdId=${hdId}&filterType=2&filterValue=${ids.join(',')}` })
+          stashGoodsListFilter(2, ids)
+          this.pageTo({ url: `/pages/goods/list?account=${account}&hdId=${hdId}&filterType=2&filterValue=${encodeFilterValue(ids)}` })
         }
         }
         return
         return
       }
       }
-      // 分类/场景:跳转商品列表页,带上分类ID/场景ID作为初始筛选条件(与首页金刚区导航一致)
+      // 分类/场景:跳转商品列表页;多 ID 用竖线 + 缓存,避免微信截断逗号导致筛选不高亮
       if (type === 3 || type === 4) {
       if (type === 3 || type === 4) {
-        this.pageTo({ url: `/pages/goods/list?account=${account}&hdId=${hdId}&filterType=${type}&filterValue=${ids.join(',')}` })
+        stashGoodsListFilter(type, ids)
+        this.pageTo({ url: `/pages/goods/list?account=${account}&hdId=${hdId}&filterType=${type}&filterValue=${encodeFilterValue(ids)}` })
       }
       }
     },
     },
     /**
     /**

+ 8 - 5
mallApp/src/components/home/navGrid.vue

@@ -16,6 +16,8 @@
 </template>
 </template>
 
 
 <script>
 <script>
+import { parseFilterIds, encodeFilterValue, stashGoodsListFilter } from '@/utils/goodsListFilter'
+
 /** 预设图标 key → emoji,与 hdApp 后台 navGrid 编辑页 preset 列表一致 */
 /** 预设图标 key → emoji,与 hdApp 后台 navGrid 编辑页 preset 列表一致 */
 const PRESET_EMOJI_MAP = {
 const PRESET_EMOJI_MAP = {
   'preset:1': '🌸',
   'preset:1': '🌸',
@@ -57,17 +59,18 @@ export default {
     },
     },
     /**
     /**
      * 点击金刚区项:跳转商品列表,并按关联类型带上初始筛选
      * 点击金刚区项:跳转商品列表,并按关联类型带上初始筛选
-     * type=3 分类 → filterType=3&filterValue=分类ID串
-     * type=4 场景 → filterType=4&filterValue=场景ID串
+     * type=3 分类 → filterType=3;type=4 场景 → filterType=4
+     * 多 ID 不用逗号拼 URL(微信会截断),改用竖线,并写入缓存供列表页勾选高亮
      */
      */
     onTap(item) {
     onTap(item) {
       const account = this.account || ''
       const account = this.account || ''
       const hdId = this.hdId || ''
       const hdId = this.hdId || ''
       let url = `/pages/goods/list?account=${account}&hdId=${hdId}`
       let url = `/pages/goods/list?account=${account}&hdId=${hdId}`
       const type = parseInt(item && item.type, 10)
       const type = parseInt(item && item.type, 10)
-      const value = item && item.value ? String(item.value) : ''
-      if ((type === 3 || type === 4) && value) {
-        url += `&filterType=${type}&filterValue=${encodeURIComponent(value)}`
+      const ids = parseFilterIds(item && item.value)
+      if ((type === 3 || type === 4) && ids.length) {
+        stashGoodsListFilter(type, ids)
+        url += `&filterType=${type}&filterValue=${encodeFilterValue(ids)}`
       }
       }
       this.pageTo({ url })
       this.pageTo({ url })
     }
     }

+ 48 - 9
mallApp/src/components/home/topNav.vue

@@ -1,18 +1,29 @@
 <!--
 <!--
   店铺首页-顶部导航与搜索
   店铺首页-顶部导航与搜索
-  按商家在 hdApp 后台的配置展示:搜索框(按钮色/文字色可配)+ 消息中心入口;
+  按商家在 hdApp 后台的配置展示:可输入搜索框(按钮色/文字色可配)+ 消息中心入口;
+  输入关键词后点搜索/键盘确认,跳转商品列表并带上 searchText。
   门店名称由页面级 customNavBar 展示(xhShop.merchantName)。
   门店名称由页面级 customNavBar 展示(xhShop.merchantName)。
   用途:mallApp 顾客端店铺首页顶部区域
   用途:mallApp 顾客端店铺首页顶部区域
 -->
 -->
 <template>
 <template>
   <view v-if="data && data.enabled == 1" class="home-top-nav">
   <view v-if="data && data.enabled == 1" class="home-top-nav">
-    <view class="search-row" @click="goSearch">
+    <view class="search-row">
       <view class="search-input">
       <view class="search-input">
         <view class="search-icon"><zui-svg-icon icon="general-search" :width="24" :height="24" color="#999"></zui-svg-icon></view>
         <view class="search-icon"><zui-svg-icon icon="general-search" :width="24" :height="24" color="#999"></zui-svg-icon></view>
-        <text class="search-ph">{{ data.placeholder || '搜索鲜花、花束、绿植等' }}</text>
+        <input
+          class="search-field"
+          type="text"
+          confirm-type="search"
+          :value="searchText"
+          :placeholder="data.placeholder || '搜索鲜花、花束、绿植等'"
+          placeholder-class="search-ph"
+          @input="onSearchInput"
+          @confirm="goSearch"
+        />
         <view
         <view
           class="search-btn"
           class="search-btn"
           :style="{ background: data.searchBtnColor || '#FF6B88', color: data.searchFontColor || '#FFFFFF' }"
           :style="{ background: data.searchBtnColor || '#FF6B88', color: data.searchFontColor || '#FFFFFF' }"
+          @click="goSearch"
         >搜索</view>
         >搜索</view>
       </view>
       </view>
       <view class="message-icon" @click.stop="goMessageCenter"><zui-svg-icon icon="general-message" :width="24" :height="24" color="#999"></zui-svg-icon></view>
       <view class="message-icon" @click.stop="goMessageCenter"><zui-svg-icon icon="general-message" :width="24" :height="24" color="#999"></zui-svg-icon></view>
@@ -37,10 +48,35 @@ export default {
       default: ''
       default: ''
     }
     }
   },
   },
+  data() {
+    return {
+      /** 首页搜索框当前输入,跳转列表页时作为 searchText 带出 */
+      searchText: ''
+    }
+  },
   methods: {
   methods: {
-    /** 搜索入口:暂无独立搜索结果页,先跳店铺分类浏览页 */
+    /**
+     * 同步输入框内容
+     * 不用 v-model:避免小程序端 input 事件与双向绑定叠在一起时取值不稳定
+     */
+    onSearchInput(e) {
+      this.searchText = (e && e.detail && e.detail.value != null) ? e.detail.value : ''
+    },
+    /**
+     * 首页搜索:有关键词才跳商品列表,列表页回填搜索框并按关键词拉商品
+     * 空内容提示用户输入,避免空搜跳转后看不出与分类入口的区别
+     */
     goSearch() {
     goSearch() {
-      this.pageTo({ url: `/pages/home/category?account=${this.account}&hdId=${this.hdId}` })
+      const keyword = (this.searchText || '').trim()
+      if (!keyword) {
+        this.$msg('请输入搜索内容')
+        return
+      }
+      const account = this.account || ''
+      const hdId = this.hdId || ''
+      this.pageTo({
+        url: `/pages/goods/list?account=${account}&hdId=${hdId}&searchText=${encodeURIComponent(keyword)}`
+      })
     },
     },
     /**
     /**
      * 消息中心入口:跳转 消息列表(pages/message/index)
      * 消息中心入口:跳转 消息列表(pages/message/index)
@@ -80,13 +116,16 @@ export default {
   justify-content: center;
   justify-content: center;
   line-height: 1;
   line-height: 1;
 }
 }
-.search-ph {
+.search-field {
   flex: 1;
   flex: 1;
+  min-width: 0;
+  height: 100%;
+  font-size: 26upx;
+  color: #333;
+}
+.search-ph {
   font-size: 26upx;
   font-size: 26upx;
   color: #999;
   color: #999;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
 }
 }
 .search-btn {
 .search-btn {
   flex-shrink: 0;
   flex-shrink: 0;

+ 147 - 47
mallApp/src/pages/goods/list.vue

@@ -1,27 +1,33 @@
 <!--
 <!--
   商品列表页
   商品列表页
-  从店铺首页金刚区进入;支持搜索、分页触底加载、筛选弹窗(配送占位/价格/分类/场景)
+  从店铺首页金刚区进入,或从首页搜索框带 searchText 进入(回填搜索框并按关键词搜索);
+  支持搜索、分页触底加载、筛选弹窗(配送占位/价格/分类/场景)
   按钮配色与店铺首页统一:#FF4D6D / linear-gradient(135deg, #FF8FA3, #FF4D6D)
   按钮配色与店铺首页统一:#FF4D6D / linear-gradient(135deg, #FF8FA3, #FF4D6D)
   右侧购物车:单规格直接加购,多规格跳转详情选规格(与首页 goodsSection 一致)
   右侧购物车:单规格直接加购,多规格跳转详情选规格(与首页 goodsSection 一致)
   左下角悬浮购物车:展示当前店铺购物车数量,点击跳转 pages/home/cart
   左下角悬浮购物车:展示当前店铺购物车数量,点击跳转 pages/home/cart
 -->
 -->
 <template>
 <template>
   <view class="goods-list-page">
   <view class="goods-list-page">
-    <!-- 搜索栏 -->
+    <!-- 搜索栏:搜索按钮在输入框内,仅点击搜索/键盘确认才请求;筛选仍在框外 -->
     <view class="search-bar">
     <view class="search-bar">
       <view class="search-input-wrap">
       <view class="search-input-wrap">
-        <app-search-module
-          v-model="searchText"
+        <input
+          class="search-field"
+          type="text"
+          confirm-type="search"
+          :value="searchText"
           placeholder="请输入搜索商品名称"
           placeholder="请输入搜索商品名称"
-          :height="66"
-          :fontSize="28"
-          backColor="#fff"
-          @input="onSearchInput"
-          @search="doSearch"
+          placeholder-class="search-ph"
+          @input="onKeywordInput"
+          @confirm="doSearch"
         />
         />
+        <view class="search-btn" @click="doSearch">搜索</view>
+      </view>
+      <view class="filter-btn" @click="openFilter">
+        筛选
+        <!-- 弹窗内有已生效的选中项时显示圆点,提醒当前列表处于筛选中 -->
+        <view v-if="hasActiveFilter" class="filter-dot"></view>
       </view>
       </view>
-      <view class="search-btn" @click="doSearch">搜索</view>
-      <view class="filter-btn" @click="openFilter">筛选</view>
     </view>
     </view>
 
 
     <!-- 商品列表 -->
     <!-- 商品列表 -->
@@ -160,6 +166,7 @@
           <view class="filter-scroll-space"></view>
           <view class="filter-scroll-space"></view>
         </scroll-view>
         </scroll-view>
         <view class="filter-footer">
         <view class="filter-footer">
+          <view class="clear-btn" @click="clearFilterDraft">清空</view>
           <view class="confirm-btn" @click="confirmFilter">确认</view>
           <view class="confirm-btn" @click="confirmFilter">确认</view>
         </view>
         </view>
       </view>
       </view>
@@ -168,12 +175,12 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import AppSearchModule from '@/components/item/module/app-search'
 import AppWrapperEmpty from '@/components/app-wrapper-empty'
 import AppWrapperEmpty from '@/components/app-wrapper-empty'
 import BottomPopup from '@/components/plugin/bottom-popup'
 import BottomPopup from '@/components/plugin/bottom-popup'
 import { getClass, getList, getUseCaseList } from '@/api/category'
 import { getClass, getList, getUseCaseList } from '@/api/category'
 import { list } from '@/mixins'
 import { list } from '@/mixins'
 import productMins from '@/mixins/cgProduct'
 import productMins from '@/mixins/cgProduct'
+import { parseFilterIds, takeGoodsListFilter, isIdInList } from '@/utils/goodsListFilter'
 
 
 const PRICE_PRESETS = [
 const PRICE_PRESETS = [
   { key: 'lt50', label: '50元以下', min: '', max: '50' },
   { key: 'lt50', label: '50元以下', min: '', max: '50' },
@@ -202,7 +209,6 @@ const emptyDraft = () => ({
 export default {
 export default {
   name: 'goodsList',
   name: 'goodsList',
   components: {
   components: {
-    AppSearchModule,
     AppWrapperEmpty,
     AppWrapperEmpty,
     BottomPopup
     BottomPopup
   },
   },
@@ -213,13 +219,13 @@ export default {
       pageType: 'cg',
       pageType: 'cg',
       autoLoad: false,
       autoLoad: false,
       searchText: '',
       searchText: '',
-      searchTimer: null,
       filterShow: false,
       filterShow: false,
       categoryOptions: [],
       categoryOptions: [],
       useCaseOptions: [],
       useCaseOptions: [],
       deliveryOptions: DELIVERY_OPTIONS,
       deliveryOptions: DELIVERY_OPTIONS,
       pricePresets: PRICE_PRESETS,
       pricePresets: PRICE_PRESETS,
       // 已生效的筛选条件;goodsIds 仅由路由 filterType=2 带入,不在筛选弹窗中编辑
       // 已生效的筛选条件;goodsIds 仅由路由 filterType=2 带入,不在筛选弹窗中编辑
+      routeFilterApplied: false,
       filters: {
       filters: {
         categoryIds: [],
         categoryIds: [],
         useCaseIds: [],
         useCaseIds: [],
@@ -244,6 +250,29 @@ export default {
     /** 角标文案:超过 99 显示 99+ */
     /** 角标文案:超过 99 显示 99+ */
     cartBadgeText() {
     cartBadgeText() {
       return this.cartBadgeCount > 99 ? '99+' : String(this.cartBadgeCount)
       return this.cartBadgeCount > 99 ? '99+' : String(this.cartBadgeCount)
+    },
+    /**
+     * 筛选按钮是否显示圆点
+     * 只看弹窗里能勾选的项(配送/价格/分类/场景);goodsIds 是轮播带入、面板里看不到,不算
+     */
+    hasActiveFilter() {
+      const f = this.filters || {}
+      if (f.delivery) {
+        return true
+      }
+      if (f.minPrice !== '' && f.minPrice != null) {
+        return true
+      }
+      if (f.maxPrice !== '' && f.maxPrice != null) {
+        return true
+      }
+      if (f.categoryIds && f.categoryIds.length) {
+        return true
+      }
+      if (f.useCaseIds && f.useCaseIds.length) {
+        return true
+      }
+      return false
     }
     }
   },
   },
   /** 从详情/购物车返回时刷新角标,避免仍显示旧数量 */
   /** 从详情/购物车返回时刷新角标,避免仍显示旧数量 */
@@ -282,13 +311,30 @@ export default {
       })
       })
     },
     },
     /**
     /**
-     * 解析路由带入的 filterType / filterValue
-     * type=2 商品ID列表、type=3 分类、type=4 场景(首页轮播/金刚区跳转)
+     * 解析路由带入的筛选与搜索关键词
+     * searchText:首页搜索框跳转带入,回填搜索框并参与首次列表请求
+     * filterType=2 商品ID列表、type=3 分类、type=4 场景(首页轮播/金刚区跳转)
+     * 多 ID 优先用跳转前写入的缓存:微信可能截断 URL 里的逗号,导致筛选分类无法高亮
      */
      */
     applyRouteFilter() {
     applyRouteFilter() {
-      const type = parseInt((this.option && this.option.filterType) || 0, 10)
-      const value = (this.option && this.option.filterValue) ? String(this.option.filterValue) : ''
-      const ids = value.split(',').map((s) => s.trim()).filter(Boolean)
+      // 首页搜索跳转:回填关键词,init 随后 loadGoods 会带上 searchText
+      const keyword = (this.option && this.option.searchText) ? String(this.option.searchText) : ''
+      if (keyword) {
+        this.searchText = keyword
+      }
+      // 登录回调可能二次 init,避免缓存已被消费后用残缺 URL 把多分类冲掉
+      if (this.routeFilterApplied) {
+        return
+      }
+      this.routeFilterApplied = true
+      let type = parseInt((this.option && this.option.filterType) || 0, 10)
+      let ids = parseFilterIds((this.option && this.option.filterValue) || '')
+      const cached = takeGoodsListFilter()
+      // 搜索进入不套用分类缓存,避免误带到搜索结果;缓存 ID 更多时覆盖被截断的路由
+      if (cached && cached.ids.length && !keyword && (!ids.length || cached.ids.length >= ids.length)) {
+        type = cached.filterType || type
+        ids = cached.ids
+      }
       if (type === 2 && ids.length) {
       if (type === 2 && ids.length) {
         this.filters.goodsIds = ids
         this.filters.goodsIds = ids
       } else if (type === 3 && ids.length) {
       } else if (type === 3 && ids.length) {
@@ -347,20 +393,17 @@ export default {
           this.list.finished = true
           this.list.finished = true
         })
         })
     },
     },
-    onSearchInput() {
-      if (this.searchTimer) {
-        clearTimeout(this.searchTimer)
-        this.searchTimer = null
-      }
-      this.searchTimer = setTimeout(() => {
-        this.doSearch()
-      }, 600)
+    /**
+     * 只同步输入框文案,不发请求
+     * 避免边输入边搜;真正搜索由搜索按钮 / 键盘确认触发
+     */
+    onKeywordInput(e) {
+      this.searchText = (e && e.detail && e.detail.value != null) ? e.detail.value : ''
     },
     },
+    /**
+     * 点击搜索按钮或键盘确认后才重置分页并按当前关键词拉列表
+     */
     doSearch() {
     doSearch() {
-      if (this.searchTimer) {
-        clearTimeout(this.searchTimer)
-        this.searchTimer = null
-      }
       this.resetList()
       this.resetList()
       this.loadGoods()
       this.loadGoods()
     },
     },
@@ -371,8 +414,8 @@ export default {
         pricePreset: presetKey,
         pricePreset: presetKey,
         customMin: presetKey ? '' : (this.filters.minPrice === '' || this.filters.minPrice == null ? '' : String(this.filters.minPrice)),
         customMin: presetKey ? '' : (this.filters.minPrice === '' || this.filters.minPrice == null ? '' : String(this.filters.minPrice)),
         customMax: presetKey ? '' : (this.filters.maxPrice === '' || this.filters.maxPrice == null ? '' : String(this.filters.maxPrice)),
         customMax: presetKey ? '' : (this.filters.maxPrice === '' || this.filters.maxPrice == null ? '' : String(this.filters.maxPrice)),
-        categoryIds: [...this.filters.categoryIds],
-        useCaseIds: [...this.filters.useCaseIds]
+        categoryIds: this.filters.categoryIds.map(String),
+        useCaseIds: this.filters.useCaseIds.map(String)
       }
       }
       this.filterShow = true
       this.filterShow = true
     },
     },
@@ -402,8 +445,11 @@ export default {
         this.draft.pricePreset = ''
         this.draft.pricePreset = ''
       }
       }
     },
     },
+    /**
+     * 分类是否勾选:接口 id 可能是数字,路由解析后是字符串,统一转字符串再比
+     */
     isCategoryChecked(id) {
     isCategoryChecked(id) {
-      return this.draft.categoryIds.indexOf(String(id)) > -1
+      return isIdInList(this.draft.categoryIds, id)
     },
     },
     toggleCategory(id) {
     toggleCategory(id) {
       const key = String(id)
       const key = String(id)
@@ -414,8 +460,11 @@ export default {
         this.draft.categoryIds.push(key)
         this.draft.categoryIds.push(key)
       }
       }
     },
     },
+    /**
+     * 场景是否勾选:与分类相同,避免数字/字符串不一致导致不高亮
+     */
     isUseCaseChecked(id) {
     isUseCaseChecked(id) {
-      return this.draft.useCaseIds.indexOf(String(id)) > -1
+      return isIdInList(this.draft.useCaseIds, id)
     },
     },
     toggleUseCase(id) {
     toggleUseCase(id) {
       const key = String(id)
       const key = String(id)
@@ -426,6 +475,13 @@ export default {
         this.draft.useCaseIds.push(key)
         this.draft.useCaseIds.push(key)
       }
       }
     },
     },
+    /**
+     * 清空弹窗草稿中的全部可选项(配送/价格/分类/场景)
+     * 只改草稿,不关弹窗、不立刻请求;点确认后才生效,避免误触直接打乱列表
+     */
+    clearFilterDraft() {
+      this.draft = emptyDraft()
+    },
     confirmFilter() {
     confirmFilter() {
       let minPrice = ''
       let minPrice = ''
       let maxPrice = ''
       let maxPrice = ''
@@ -517,12 +573,6 @@ export default {
     if (!this.list.finished) {
     if (!this.list.finished) {
       await this.loadGoods()
       await this.loadGoods()
     }
     }
-  },
-  beforeDestroy() {
-    if (this.searchTimer) {
-      clearTimeout(this.searchTimer)
-      this.searchTimer = null
-    }
   }
   }
 }
 }
 </script>
 </script>
@@ -586,25 +636,60 @@ export default {
 .search-input-wrap {
 .search-input-wrap {
   flex: 1;
   flex: 1;
   min-width: 0;
   min-width: 0;
+  height: 64upx;
+  padding: 4upx 4upx 4upx 24upx;
   border: 1upx solid #e5e5e5;
   border: 1upx solid #e5e5e5;
   border-radius: 32upx;
   border-radius: 32upx;
-  overflow: hidden;
+  background: #fff;
+  display: flex;
+  align-items: center;
+}
+
+.search-field {
+  flex: 1;
+  min-width: 0;
+  height: 100%;
+  font-size: 26upx;
+  color: #333;
+}
+
+.search-ph {
+  font-size: 26upx;
+  color: #999;
 }
 }
 
 
 .search-btn,
 .search-btn,
 .filter-btn {
 .filter-btn {
   flex-shrink: 0;
   flex-shrink: 0;
-  margin-left: 12upx;
   padding: 0 28upx;
   padding: 0 28upx;
-  height: 64upx;
-  line-height: 64upx;
+  height: 56upx;
+  line-height: 56upx;
   font-size: 26upx;
   font-size: 26upx;
   color: #fff;
   color: #fff;
   background: linear-gradient(135deg, #FF8FA3, #FF4D6D);
   background: linear-gradient(135deg, #FF8FA3, #FF4D6D);
-  border-radius: 32upx;
+  border-radius: 28upx;
   text-align: center;
   text-align: center;
 }
 }
 
 
+.filter-btn {
+  position: relative;
+  margin-left: 12upx;
+  height: 64upx;
+  line-height: 64upx;
+  border-radius: 32upx;
+}
+
+/* 有筛选条件时的白点:衬在粉色按钮上更醒目,无选中项时不渲染 */
+.filter-dot {
+  position: absolute;
+  top: 10upx;
+  right: 10upx;
+  width: 14upx;
+  height: 14upx;
+  border-radius: 50%;
+  background: #fff;
+}
+
 .card-list {
 .card-list {
   padding: 20upx;
   padding: 20upx;
 }
 }
@@ -860,19 +945,34 @@ export default {
 
 
 .filter-footer {
 .filter-footer {
   flex-shrink: 0;
   flex-shrink: 0;
+  display: flex;
+  align-items: center;
   padding: 20upx 30upx;
   padding: 20upx 30upx;
   padding-bottom: calc(20upx + env(safe-area-inset-bottom));
   padding-bottom: calc(20upx + env(safe-area-inset-bottom));
   background: #fff;
   background: #fff;
 }
 }
 
 
+.clear-btn,
 .confirm-btn {
 .confirm-btn {
+  flex: 1;
   height: 88upx;
   height: 88upx;
   line-height: 88upx;
   line-height: 88upx;
   text-align: center;
   text-align: center;
   font-size: 32upx;
   font-size: 32upx;
   font-weight: 600;
   font-weight: 600;
+  border-radius: 44upx;
+  box-sizing: border-box;
+}
+
+.clear-btn {
+  margin-right: 20upx;
+  color: #FF4D6D;
+  background: #fff;
+  border: 2upx solid #FF4D6D;
+}
+
+.confirm-btn {
   color: #fff;
   color: #fff;
   background: linear-gradient(135deg, #FF8FA3, #FF4D6D);
   background: linear-gradient(135deg, #FF8FA3, #FF4D6D);
-  border-radius: 44upx;
 }
 }
 </style>
 </style>

+ 101 - 0
mallApp/src/utils/goodsListFilter.js

@@ -0,0 +1,101 @@
+/**
+ * 商品列表页初始筛选(首页金刚区 / 轮播跳转共用)
+ * 顾客从店铺首页点分类/场景/多商品进入 list 页时,用这里统一解析和传递 ID。
+ * 解决:微信小程序会把 URL query 里的逗号当成参数分隔,多 ID 用逗号拼接会导致只收到第一个,筛选高亮丢失。
+ */
+
+export const GOODS_LIST_FILTER_STORAGE_KEY = 'mallGoodsListRouteFilter'
+
+/** 缓存有效期:仅覆盖「点击后立刻打开列表」这一跳,避免污染后续无筛选进入 */
+const FILTER_STASH_TTL_MS = 15000
+
+/**
+ * 把后台存的关联 value 解析成 ID 字符串数组
+ * 兼容逗号、竖线、中文逗号、URL 编码、JSON 数组;保证和筛选勾选用的 String(id) 一致
+ * @param {*} raw 路由 filterValue 或金刚区/轮播 item.value
+ * @returns {string[]}
+ */
+export function parseFilterIds(raw) {
+  if (Array.isArray(raw)) {
+    return raw.map((id) => String(id).trim()).filter(Boolean)
+  }
+  if (raw == null || raw === '') {
+    return []
+  }
+  let value = String(raw)
+  try {
+    value = decodeURIComponent(value)
+  } catch (e) {
+    // 已解码或含非法 % 时保持原串,继续按分隔符拆
+  }
+  value = value.replace(/%2C/gi, ',').replace(/%7C/gi, '|')
+  const trimmed = value.trim()
+  if (trimmed.charAt(0) === '[') {
+    try {
+      const arr = JSON.parse(trimmed)
+      if (Array.isArray(arr)) {
+        return arr.map((id) => String(id).trim()).filter(Boolean)
+      }
+    } catch (e) {
+      // 非 JSON 则走分隔符
+    }
+  }
+  return trimmed.split(/[,|,]/).map((s) => s.trim()).filter(Boolean)
+}
+
+/**
+ * 写入 URL 的 filterValue:用竖线连接后再 encode,避免微信把逗号当 query 分隔符
+ * @param {string[]} ids
+ * @returns {string}
+ */
+export function encodeFilterValue(ids) {
+  return encodeURIComponent((ids || []).join('|'))
+}
+
+/**
+ * 跳转列表前把完整 ID 写入缓存
+ * 路由参数被截断时,列表页仍能按这份数据勾选分类/场景
+ * @param {number} filterType 2 商品 / 3 分类 / 4 场景
+ * @param {string[]} ids
+ */
+export function stashGoodsListFilter(filterType, ids) {
+  uni.setStorageSync(GOODS_LIST_FILTER_STORAGE_KEY, {
+    filterType,
+    ids: (ids || []).map(String),
+    ts: Date.now()
+  })
+}
+
+/**
+ * 读取并清除跳转缓存;超时则丢弃,防止返回首页后再从搜索进入仍带上旧分类
+ * @returns {{ filterType: number, ids: string[] } | null}
+ */
+export function takeGoodsListFilter() {
+  const cached = uni.getStorageSync(GOODS_LIST_FILTER_STORAGE_KEY)
+  if (cached) {
+    uni.removeStorageSync(GOODS_LIST_FILTER_STORAGE_KEY)
+  }
+  if (!cached || !cached.ts || Date.now() - cached.ts > FILTER_STASH_TTL_MS) {
+    return null
+  }
+  const ids = Array.isArray(cached.ids) ? cached.ids.map(String).filter(Boolean) : []
+  if (!ids.length) {
+    return null
+  }
+  return {
+    filterType: parseInt(cached.filterType, 10) || 0,
+    ids
+  }
+}
+
+/**
+ * 判断筛选项是否已选中
+ * 分类接口 id 常为数字,路由解析后是字符串,必须转成同一类型再比
+ * @param {Array} list
+ * @param {string|number} id
+ * @returns {boolean}
+ */
+export function isIdInList(list, id) {
+  const key = String(id)
+  return (list || []).some((item) => String(item) === key)
+}