소스 검색

feat(home): 支持首页模块展示数量和更多列表

- 后台首页配置支持设置商品展示数量并提交 displayCount

- 商城首页按 goodsTotal 控制更多入口,新增模块商品分页列表页
shizhongqi 2 주 전
부모
커밋
f7e3f977be

+ 46 - 3
hdApp/src/admin/homePageConfig/goodsSection.vue

@@ -126,6 +126,19 @@
           </view>
           </view>
         </view>
         </view>
       </view>
       </view>
+
+      <view class="field-row">
+        <text class="field-label">商品展示数量</text>
+        <input
+          v-model="form.displayCount"
+          class="count-input"
+          type="number"
+          maxlength="2"
+          placeholder="默认全部"
+          placeholder-class="phcolor"
+        />
+        <view class="field-hint">范围1-10,留空为全部;设置数量超出商品总数时按全部展示</view>
+      </view>
     </view>
     </view>
 
 
     <view class="bottom-bar">
     <view class="bottom-bar">
@@ -167,7 +180,8 @@ export default {
         type: 2,
         type: 2,
         value: '',
         value: '',
         sort: 1,
         sort: 1,
-        layoutCols: 3
+        layoutCols: 3,
+        displayCount: ''
       },
       },
       typeOptions: [
       typeOptions: [
         { label: '商品', value: 2 },
         { label: '商品', value: 2 },
@@ -244,7 +258,8 @@ export default {
           type: parseInt(d.type, 10) || 2,
           type: parseInt(d.type, 10) || 2,
           value: d.value || '',
           value: d.value || '',
           sort: parseInt(d.sort, 10) || 1,
           sort: parseInt(d.sort, 10) || 1,
-          layoutCols: [1, 2, 3].includes(parseInt(d.layoutCols, 10)) ? parseInt(d.layoutCols, 10) : 3
+          layoutCols: [1, 2, 3].includes(parseInt(d.layoutCols, 10)) ? parseInt(d.layoutCols, 10) : 3,
+          displayCount: d.displayCount ? String(d.displayCount) : ''
         }
         }
         if (this.form.type === 2 && this.form.value) {
         if (this.form.type === 2 && this.form.value) {
           const ids = this.parseIds(this.form.value)
           const ids = this.parseIds(this.form.value)
@@ -293,6 +308,17 @@ export default {
       else ids.push(key)
       else ids.push(key)
       this.form.value = ids.join(',')
       this.form.value = ids.join(',')
     },
     },
+    /** 解析并校验商品展示数量:空=全部(0),否则必须在 1-10 */
+    parseDisplayCount() {
+      const raw = String(this.form.displayCount == null ? '' : this.form.displayCount).trim()
+      if (raw === '') return 0
+      const n = parseInt(raw, 10)
+      if (isNaN(n) || n < 1 || n > 10) {
+        this.$msg('商品展示数量范围为1-10')
+        return null
+      }
+      return n
+    },
     saveFn() {
     saveFn() {
       if (this.saving) return
       if (this.saving) return
       if (!String(this.form.name || '').trim()) {
       if (!String(this.form.name || '').trim()) {
@@ -303,6 +329,8 @@ export default {
         this.$msg('请选择关联内容')
         this.$msg('请选择关联内容')
         return
         return
       }
       }
+      const displayCount = this.parseDisplayCount()
+      if (displayCount === null) return
       this.saving = true
       this.saving = true
       this.cfg
       this.cfg
         .saveApi({
         .saveApi({
@@ -311,7 +339,8 @@ export default {
           type: this.form.type,
           type: this.form.type,
           value: this.form.value,
           value: this.form.value,
           sort: this.form.sort,
           sort: this.form.sort,
-          layoutCols: this.form.layoutCols
+          layoutCols: this.form.layoutCols,
+          displayCount
         })
         })
         .then((res) => {
         .then((res) => {
           if (res.code === 1) {
           if (res.code === 1) {
@@ -478,6 +507,20 @@ export default {
     background: radial-gradient(circle, #09c567 0 45%, transparent 50%);
     background: radial-gradient(circle, #09c567 0 45%, transparent 50%);
   }
   }
 }
 }
+.count-input {
+  height: 72upx;
+  padding: 0 20upx;
+  font-size: 28upx;
+  color: #333;
+  border: 1upx solid #e5e5e5;
+  border-radius: 8upx;
+  box-sizing: border-box;
+}
+.field-hint {
+  margin-top: 12upx;
+  font-size: 22upx;
+  color: $fontColor3;
+}
 .bottom-bar {
 .bottom-bar {
   position: fixed;
   position: fixed;
   left: 0;
   left: 0;

+ 33 - 10
hdApp/src/admin/homePageConfig/groupBuy.vue

@@ -48,12 +48,18 @@
         </view>
         </view>
       </tui-list-cell>
       </tui-list-cell>
 
 
-      <tui-list-cell class="line-cell between" :hover="false">
-        <view class="tui-title">商品首页展开</view>
-        <view class="tui-operate switch-wrap">
-          <switch :checked="form.expandHome == 1" color="#09C567" @change="onExpandHomeChange" />
-        </view>
+      <tui-list-cell class="line-cell" :hover="false">
+        <view class="tui-title">商品展示数量</view>
+        <input
+          v-model="form.displayCount"
+          class="tui-input"
+          type="number"
+          maxlength="2"
+          placeholder="默认全部"
+          placeholder-class="phcolor"
+        />
       </tui-list-cell>
       </tui-list-cell>
+      <view class="field-hint">范围1-10,留空为全部;设置数量超出商品总数时按全部展示</view>
 
 
       <tui-list-cell class="line-cell time-cell" :hover="false">
       <tui-list-cell class="line-cell time-cell" :hover="false">
         <view class="tui-title required">开始时间</view>
         <view class="tui-title required">开始时间</view>
@@ -191,7 +197,7 @@ export default {
         title: '',
         title: '',
         subtitle: '',
         subtitle: '',
         showCountdown: 1,
         showCountdown: 1,
-        expandHome: 0,
+        displayCount: '',
         startTime: 0,
         startTime: 0,
         endTime: 0,
         endTime: 0,
         startTimeStr: '',
         startTimeStr: '',
@@ -255,7 +261,7 @@ export default {
         title: data.title || '',
         title: data.title || '',
         subtitle: data.subtitle || '',
         subtitle: data.subtitle || '',
         showCountdown: data.showCountdown == 1 ? 1 : 0,
         showCountdown: data.showCountdown == 1 ? 1 : 0,
-        expandHome: data.expandHome == 1 ? 1 : 0,
+        displayCount: data.displayCount ? String(data.displayCount) : '',
         startTime,
         startTime,
         endTime,
         endTime,
         startTimeStr: unixToDatetime(startTime),
         startTimeStr: unixToDatetime(startTime),
@@ -294,8 +300,16 @@ export default {
     onShowCountdownChange(e) {
     onShowCountdownChange(e) {
       this.form.showCountdown = e.detail.value ? 1 : 0
       this.form.showCountdown = e.detail.value ? 1 : 0
     },
     },
-    onExpandHomeChange(e) {
-      this.form.expandHome = e.detail.value ? 1 : 0
+    /** 解析并校验商品展示数量:空=全部(0),否则必须在 1-10 */
+    parseDisplayCount() {
+      const raw = String(this.form.displayCount == null ? '' : this.form.displayCount).trim()
+      if (raw === '') return 0
+      const n = parseInt(raw, 10)
+      if (isNaN(n) || n < 1 || n > 10) {
+        this.$msg('商品展示数量范围为1-10')
+        return null
+      }
+      return n
     },
     },
     /** 切换首页排列规则:1排1列/1排2列/1排3列 */
     /** 切换首页排列规则:1排1列/1排2列/1排3列 */
     onLayoutColsChange(e) {
     onLayoutColsChange(e) {
@@ -369,6 +383,8 @@ export default {
         this.$msg('活动说明不能超过500字')
         this.$msg('活动说明不能超过500字')
         return
         return
       }
       }
+      const displayCount = this.parseDisplayCount()
+      if (displayCount === null) return
       const goodsPayload = (this.form.goods || []).map((g) => ({
       const goodsPayload = (this.form.goods || []).map((g) => ({
         goodsId: g.goodsId,
         goodsId: g.goodsId,
         price: parseFloat(g.price) || 0,
         price: parseFloat(g.price) || 0,
@@ -389,7 +405,7 @@ export default {
         title,
         title,
         subtitle,
         subtitle,
         showCountdown: this.form.showCountdown,
         showCountdown: this.form.showCountdown,
-        expandHome: this.form.expandHome,
+        displayCount,
         startTime: this.form.startTime,
         startTime: this.form.startTime,
         endTime: this.form.endTime,
         endTime: this.form.endTime,
         desc,
         desc,
@@ -470,6 +486,13 @@ export default {
   align-items: flex-start;
   align-items: flex-start;
 }
 }
 
 
+.field-hint {
+  padding: 0 30upx 20upx;
+  font-size: 22upx;
+  color: $fontColor3;
+  background: #fff;
+}
+
 .desc-card {
 .desc-card {
   margin: 20upx 0 0;
   margin: 20upx 0 0;
   padding: 24upx 30upx;
   padding: 24upx 30upx;

+ 33 - 10
hdApp/src/admin/homePageConfig/seckill.vue

@@ -48,12 +48,18 @@
         </view>
         </view>
       </tui-list-cell>
       </tui-list-cell>
 
 
-      <tui-list-cell class="line-cell between" :hover="false">
-        <view class="tui-title">商品首页展开</view>
-        <view class="tui-operate switch-wrap">
-          <switch :checked="form.expandHome == 1" color="#09C567" @change="onExpandHomeChange" />
-        </view>
+      <tui-list-cell class="line-cell" :hover="false">
+        <view class="tui-title">商品展示数量</view>
+        <input
+          v-model="form.displayCount"
+          class="tui-input"
+          type="number"
+          maxlength="2"
+          placeholder="默认全部"
+          placeholder-class="phcolor"
+        />
       </tui-list-cell>
       </tui-list-cell>
+      <view class="field-hint">范围1-10,留空为全部;设置数量超出商品总数时按全部展示</view>
 
 
       <tui-list-cell class="line-cell time-cell" :hover="false">
       <tui-list-cell class="line-cell time-cell" :hover="false">
         <view class="tui-title required">开始时间</view>
         <view class="tui-title required">开始时间</view>
@@ -191,7 +197,7 @@ export default {
         title: '',
         title: '',
         subtitle: '',
         subtitle: '',
         showCountdown: 1,
         showCountdown: 1,
-        expandHome: 0,
+        displayCount: '',
         startTime: 0,
         startTime: 0,
         endTime: 0,
         endTime: 0,
         startTimeStr: '',
         startTimeStr: '',
@@ -256,7 +262,7 @@ export default {
         title: data.title || '',
         title: data.title || '',
         subtitle: data.subtitle || '',
         subtitle: data.subtitle || '',
         showCountdown: data.showCountdown == 1 ? 1 : 0,
         showCountdown: data.showCountdown == 1 ? 1 : 0,
-        expandHome: data.expandHome == 1 ? 1 : 0,
+        displayCount: data.displayCount ? String(data.displayCount) : '',
         startTime,
         startTime,
         endTime,
         endTime,
         startTimeStr: unixToDatetime(startTime),
         startTimeStr: unixToDatetime(startTime),
@@ -296,8 +302,16 @@ export default {
     onShowCountdownChange(e) {
     onShowCountdownChange(e) {
       this.form.showCountdown = e.detail.value ? 1 : 0
       this.form.showCountdown = e.detail.value ? 1 : 0
     },
     },
-    onExpandHomeChange(e) {
-      this.form.expandHome = e.detail.value ? 1 : 0
+    /** 解析并校验商品展示数量:空=全部(0),否则必须在 1-10 */
+    parseDisplayCount() {
+      const raw = String(this.form.displayCount == null ? '' : this.form.displayCount).trim()
+      if (raw === '') return 0
+      const n = parseInt(raw, 10)
+      if (isNaN(n) || n < 1 || n > 10) {
+        this.$msg('商品展示数量范围为1-10')
+        return null
+      }
+      return n
     },
     },
     /** 切换首页排列规则:1排1列/1排2列/1排3列 */
     /** 切换首页排列规则:1排1列/1排2列/1排3列 */
     onLayoutColsChange(e) {
     onLayoutColsChange(e) {
@@ -373,6 +387,8 @@ export default {
         this.$msg('活动说明不能超过500字')
         this.$msg('活动说明不能超过500字')
         return
         return
       }
       }
+      const displayCount = this.parseDisplayCount()
+      if (displayCount === null) return
       const goodsPayload = (this.form.goods || []).map((g) => ({
       const goodsPayload = (this.form.goods || []).map((g) => ({
         goodsId: g.goodsId,
         goodsId: g.goodsId,
         price: parseFloat(g.price) || 0,
         price: parseFloat(g.price) || 0,
@@ -389,7 +405,7 @@ export default {
         title,
         title,
         subtitle,
         subtitle,
         showCountdown: this.form.showCountdown,
         showCountdown: this.form.showCountdown,
-        expandHome: this.form.expandHome,
+        displayCount,
         startTime: this.form.startTime,
         startTime: this.form.startTime,
         endTime: this.form.endTime,
         endTime: this.form.endTime,
         desc,
         desc,
@@ -470,6 +486,13 @@ export default {
   align-items: flex-start;
   align-items: flex-start;
 }
 }
 
 
+.field-hint {
+  padding: 0 30upx 20upx;
+  font-size: 22upx;
+  color: $fontColor3;
+  background: #fff;
+}
+
 .desc-card {
 .desc-card {
   margin: 20upx 0 0;
   margin: 20upx 0 0;
   padding: 24upx 30upx;
   padding: 24upx 30upx;

+ 2 - 0
mallApp/src/api/home-page-config/index.js

@@ -13,3 +13,5 @@ export const getGroupBuy = data => https.get('/home-page-config/get-group-buy',
 export const getHot = data => https.get('/home-page-config/get-hot', data)
 export const getHot = data => https.get('/home-page-config/get-hot', data)
 export const getNew = data => https.get('/home-page-config/get-new', data)
 export const getNew = data => https.get('/home-page-config/get-new', data)
 export const getPullGoods = data => https.get('/home-page-config/get-pull-goods', data)
 export const getPullGoods = data => https.get('/home-page-config/get-pull-goods', data)
+/** 首页模块「更多」商品列表分页:moduleKey=seckill|groupBuy|hot|new|pullGoods */
+export const getSectionGoods = data => https.get('/home-page-config/get-section-goods', data)

+ 44 - 13
mallApp/src/components/home/activitySection.vue

@@ -6,22 +6,25 @@
 <template>
 <template>
   <view v-if="data && data.enabled == 1 && data.goods && data.goods.length" class="home-activity">
   <view v-if="data && data.enabled == 1 && data.goods && data.goods.length" class="home-activity">
     <view class="activity-head">
     <view class="activity-head">
-      <view class="activity-title-row">
-        <text class="activity-title">{{ data.title || (isGroupBuy ? '团购优惠' : '限时秒杀') }}</text>
-        <view v-if="data.showCountdown == 1 && data.status == 1" class="countdown">
-          <text class="countdown-unit">{{ countdown.h }}</text>
-          <text class="countdown-colon">:</text>
-          <text class="countdown-unit">{{ countdown.m }}</text>
-          <text class="countdown-colon">:</text>
-          <text class="countdown-unit">{{ countdown.s }}</text>
+      <view class="activity-head-row">
+        <view class="activity-title-row">
+          <text class="activity-title">{{ data.title || (isGroupBuy ? '团购优惠' : '限时秒杀') }}</text>
+          <view v-if="data.showCountdown == 1 && data.status == 1" class="countdown">
+            <text class="countdown-unit">{{ countdown.h }}</text>
+            <text class="countdown-colon">:</text>
+            <text class="countdown-unit">{{ countdown.m }}</text>
+            <text class="countdown-colon">:</text>
+            <text class="countdown-unit">{{ countdown.s }}</text>
+          </view>
         </view>
         </view>
+        <text v-if="showMore" class="section-more" @click="goMore">更多 ></text>
       </view>
       </view>
-      <text class="activity-more">{{ data.subtitle || (isGroupBuy ? '多人拼团更实惠' : '每日精选 限时特惠') }}</text>
+      <text class="activity-subtitle">{{ data.subtitle || (isGroupBuy ? '多人拼团更实惠' : '每日精选 限时特惠') }}</text>
     </view>
     </view>
 
 
     <!-- 1排1列:左图右文,底部价格与按钮同行 -->
     <!-- 1排1列:左图右文,底部价格与按钮同行 -->
     <view v-if="cols === 1" class="list-1">
     <view v-if="cols === 1" class="list-1">
-      <view v-for="g in data.goods" :key="g.goodsId" class="row-card" @click="goDetail(g)">
+      <view v-for="g in data.goods" :key="g.id" class="row-card" @click="goDetail(g)">
         <image class="row-cover" :src="g.coverUrl" mode="aspectFill" />
         <image class="row-cover" :src="g.coverUrl" mode="aspectFill" />
         <view class="row-content">
         <view class="row-content">
           <text class="row-name">{{ g.name }}</text>
           <text class="row-name">{{ g.name }}</text>
@@ -39,7 +42,7 @@
 
 
     <!-- 1排2列 / 1排3列:网格 -->
     <!-- 1排2列 / 1排3列:网格 -->
     <view v-else class="goods-grid" :class="'cols-' + cols">
     <view v-else class="goods-grid" :class="'cols-' + cols">
-      <view v-for="g in data.goods" :key="g.goodsId" class="grid-card" @click="goDetail(g)">
+      <view v-for="g in data.goods" :key="g.id" class="grid-card" @click="goDetail(g)">
         <view class="grid-cover-wrap">
         <view class="grid-cover-wrap">
           <image class="grid-cover" :src="g.coverUrl" mode="aspectFill" />
           <image class="grid-cover" :src="g.coverUrl" mode="aspectFill" />
           <view v-if="isGroupBuy" class="group-badge">{{ g.groupSize }}人团</view>
           <view v-if="isGroupBuy" class="group-badge">{{ g.groupSize }}人团</view>
@@ -93,6 +96,10 @@ export default {
     cols() {
     cols() {
       const c = parseInt(this.data && this.data.layoutCols, 10)
       const c = parseInt(this.data && this.data.layoutCols, 10)
       return [1, 2, 3].includes(c) ? c : 3
       return [1, 2, 3].includes(c) ? c : 3
+    },
+    /** 展示数量小于商品总数时显示「更多」 */
+    showMore() {
+      return Number(this.data && this.data.goodsTotal || 0) > ((this.data && this.data.goods) || []).length
     }
     }
   },
   },
   watch: {
   watch: {
@@ -113,7 +120,18 @@ export default {
       return n % 1 === 0 ? String(n) : n.toFixed(2)
       return n % 1 === 0 ? String(n) : n.toFixed(2)
     },
     },
     goDetail(g) {
     goDetail(g) {
-      this.pageTo({ url: `/pages/goods/detail?id=${g.goodsId}&account=${this.account}&hdId=${this.hdId}` })
+      // 防御性判断:接口返回的活动商品统一带 id 字段(后端 filterActivityGoods 已用
+      // goodsId 补齐 id),仍保留 goodsId 兜底;g 为空时直接返回避免报错。
+      if (!g) return
+      const id = g.id || g.goodsId
+      if (!id) return
+      this.pageTo({ url: `/pages/goods/detail?id=${id}&account=${this.account}&hdId=${this.hdId}` })
+    },
+    goMore() {
+      const title = this.data.title || (this.isGroupBuy ? '团购优惠' : '限时秒杀')
+      this.pageTo({
+        url: `/pages/goods/section-list?moduleKey=${this.type}&title=${encodeURIComponent(title)}&account=${this.account}&hdId=${this.hdId}`
+      })
     },
     },
     restartCountdown() {
     restartCountdown() {
       if (this._timer) clearInterval(this._timer)
       if (this._timer) clearInterval(this._timer)
@@ -148,9 +166,16 @@ export default {
 .activity-head {
 .activity-head {
   margin-bottom: 20upx;
   margin-bottom: 20upx;
 }
 }
+.activity-head-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
 .activity-title-row {
 .activity-title-row {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+  flex: 1;
+  min-width: 0;
 }
 }
 .activity-title {
 .activity-title {
   font-size: 32upx;
   font-size: 32upx;
@@ -177,7 +202,13 @@ export default {
   color: #FF4D6D;
   color: #FF4D6D;
   font-weight: 700;
   font-weight: 700;
 }
 }
-.activity-more {
+.section-more {
+  flex-shrink: 0;
+  margin-left: 16upx;
+  font-size: 22upx;
+  color: #999;
+}
+.activity-subtitle {
   display: block;
   display: block;
   margin-top: 8upx;
   margin-top: 8upx;
   font-size: 22upx;
   font-size: 22upx;

+ 16 - 3
mallApp/src/components/home/goodsSection.vue

@@ -6,7 +6,7 @@
   <view v-if="data && data.enabled == 1 && data.goods && data.goods.length" class="home-goods-section">
   <view v-if="data && data.enabled == 1 && data.goods && data.goods.length" class="home-goods-section">
     <view class="section-head">
     <view class="section-head">
       <text class="section-title">{{ data.name }}</text>
       <text class="section-title">{{ data.name }}</text>
-      <text class="section-more" @click="goMore">更多 ></text>
+      <text v-if="showMore" class="section-more" @click="goMore">更多 ></text>
     </view>
     </view>
 
 
     <!-- 1排1列:横向列表 -->
     <!-- 1排1列:横向列表 -->
@@ -52,6 +52,11 @@ export default {
       type: Object,
       type: Object,
       default: () => ({})
       default: () => ({})
     },
     },
+    /** hot | new | pullGoods */
+    moduleKey: {
+      type: String,
+      default: 'hot'
+    },
     account: {
     account: {
       type: [String, Number],
       type: [String, Number],
       default: ''
       default: ''
@@ -65,6 +70,10 @@ export default {
     cols() {
     cols() {
       const c = parseInt(this.data && this.data.layoutCols, 10)
       const c = parseInt(this.data && this.data.layoutCols, 10)
       return [1, 2, 3].includes(c) ? c : 3
       return [1, 2, 3].includes(c) ? c : 3
+    },
+    /** 展示数量小于商品总数时显示「更多」 */
+    showMore() {
+      return Number(this.data && this.data.goodsTotal || 0) > ((this.data && this.data.goods) || []).length
     }
     }
   },
   },
   methods: {
   methods: {
@@ -79,11 +88,15 @@ export default {
       return String(n)
       return String(n)
     },
     },
     goDetail(g) {
     goDetail(g) {
+      // 防御性判断:列表快速翻页/刷新时点击事件可能命中已失效的 item,避免报错崩溃
+      if (!g || !g.id) return
       this.pageTo({ url: `/pages/goods/detail?id=${g.id}&account=${this.account}&hdId=${this.hdId}` })
       this.pageTo({ url: `/pages/goods/detail?id=${g.id}&account=${this.account}&hdId=${this.hdId}` })
     },
     },
-    /** 「更多」暂先跳店铺分类浏览页,后续可按关联类型精确筛选 */
     goMore() {
     goMore() {
-      this.pageTo({ url: `/pages/home/category?account=${this.account}&hdId=${this.hdId}` })
+      const title = (this.data && this.data.name) || ''
+      this.pageTo({
+        url: `/pages/goods/section-list?moduleKey=${this.moduleKey}&title=${encodeURIComponent(title)}&account=${this.account}&hdId=${this.hdId}`
+      })
     }
     }
   }
   }
 }
 }

+ 2 - 1
mallApp/src/pages.json

@@ -75,7 +75,8 @@
             "root": "pages/goods",
             "root": "pages/goods",
             "pages": [
             "pages": [
                 { "path": "detail", "style": { "navigationBarTitleText": "详情" } },
                 { "path": "detail", "style": { "navigationBarTitleText": "详情" } },
-                { "path": "list", "style": { "navigationBarTitleText": "商品列表", "enablePullDownRefresh": true } }
+                { "path": "list", "style": { "navigationBarTitleText": "商品列表", "enablePullDownRefresh": true } },
+                { "path": "section-list", "style": { "navigationBarTitleText": "商品列表", "enablePullDownRefresh": true } }
             ]
             ]
         },
         },
         {
         {

+ 262 - 0
mallApp/src/pages/goods/section-list.vue

@@ -0,0 +1,262 @@
+<!--
+  首页模块「更多」商品列表页
+  从秒杀/团购/热门推荐/今日上新/下拉商品的「更多」进入;
+  样式参照 goods/list.vue,本次不包含搜索与筛选功能。
+  秒杀/团购保留活动价、原价划线与团购人数展示。
+-->
+<template>
+  <view class="goods-list-page">
+    <view v-if="!$util.isEmpty(list.data)" class="card-list">
+      <view
+        v-for="item in list.data"
+        :key="item.id"
+        class="goods-card"
+        @click="goDetail(item)"
+      >
+        <view class="cover-wrap">
+          <image class="cover" :src="item.coverUrl || item.smallCover || item.cover" mode="aspectFill" />
+          <view v-if="isGroupBuy && item.groupSize" class="badge">{{ item.groupSize }}人团</view>
+          <view v-else-if="isSeckill" class="badge">秒杀</view>
+        </view>
+        <view class="card-body">
+          <text class="goods-name">{{ item.name }}</text>
+          <view class="bottom-row">
+            <view class="price-wrap">
+              <text class="price">
+                <text class="price-symbol">¥</text>
+                {{ formatPrice(item.price) }}
+              </text>
+              <text v-if="isActivity && item.originPrice" class="origin-price">
+                ¥{{ formatPrice(item.originPrice) }}
+              </text>
+            </view>
+            <text v-if="!isActivity" class="sold">已售{{ formatSold(item.sold || item.totalSold) }}件</text>
+          </view>
+        </view>
+      </view>
+      <view v-if="list.finished" class="list-end">已经到底了</view>
+    </view>
+    <block v-else-if="!list.loading">
+      <app-wrapper-empty title="暂无商品" :is-empty="true" />
+    </block>
+  </view>
+</template>
+
+<script>
+import AppWrapperEmpty from '@/components/app-wrapper-empty'
+import { getSectionGoods } from '@/api/home-page-config'
+import { list } from '@/mixins'
+
+export default {
+  name: 'sectionGoodsList',
+  components: {
+    AppWrapperEmpty
+  },
+  mixins: [list],
+  data() {
+    return {
+      moduleKey: '',
+      pageTitle: '商品列表'
+    }
+  },
+  computed: {
+    isSeckill() {
+      return this.moduleKey === 'seckill'
+    },
+    isGroupBuy() {
+      return this.moduleKey === 'groupBuy'
+    },
+    isActivity() {
+      return this.isSeckill || this.isGroupBuy
+    },
+    account() {
+      return (this.option && this.option.account) || uni.getStorageSync('account') || ''
+    },
+    hdId() {
+      return (this.option && this.option.hdId) || uni.getStorageSync('hdId') || ''
+    }
+  },
+  methods: {
+    init() {
+      this.moduleKey = (this.option && this.option.moduleKey) || ''
+      let title = (this.option && this.option.title) || ''
+      try {
+        title = title ? decodeURIComponent(title) : ''
+      } catch (e) {
+        // 标题解码失败时保留原始标题,避免异常阻断列表加载
+      }
+      this.pageTitle = title || '商品列表'
+      uni.setNavigationBarTitle({ title: this.pageTitle })
+      this.loadGoods()
+    },
+    buildQuery() {
+      return {
+        moduleKey: this.moduleKey,
+        page: this.list.page,
+        pageSize: 10
+      }
+    },
+    loadGoods() {
+      this.list.loading = true
+      return getSectionGoods(this.buildQuery())
+        .then((res) => {
+          this.completes(res)
+          // 后端返回的 title 优先用于导航栏(路由未带 title 时兜底)
+          if (res.code === 1 && res.data && res.data.title && !((this.option && this.option.title))) {
+            this.pageTitle = res.data.title
+            uni.setNavigationBarTitle({ title: this.pageTitle })
+          }
+        })
+        .catch(() => {
+          this.list.loading = false
+          this.list.finished = true
+        })
+    },
+    formatPrice(val) {
+      const n = parseFloat(val)
+      if (isNaN(n)) return '0'
+      return n % 1 === 0 ? String(n) : n.toFixed(2)
+    },
+    formatSold(val) {
+      const n = parseInt(val, 10) || 0
+      if (n >= 10000) return (n / 10000).toFixed(1) + '万+'
+      return String(n)
+    },
+    goDetail(item) {
+      // 防御性判断:v-for 的 :key 曾用过 item.id || item.goodsId 这类表达式,
+      // 在非 H5 端(小程序/App)不受支持,会导致列表刷新/翻页时点击命中的
+      // item 与实际数据错位甚至取到 undefined,这里兜底避免报错崩溃。
+      if (!item) return
+      const id = item.id || item.goodsId
+      if (!id) return
+      this.pageTo({
+        url: `/pages/goods/detail?id=${id}&account=${this.account}&hdId=${this.hdId}`
+      })
+    }
+  },
+  async onPullDownRefresh() {
+    this.resetList()
+    await this.loadGoods()
+    uni.stopPullDownRefresh()
+  },
+  async onReachBottom() {
+    if (!this.list.finished) {
+      await this.loadGoods()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.goods-list-page {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding-bottom: 40upx;
+}
+
+.card-list {
+  padding: 20upx;
+}
+
+.goods-card {
+  display: flex;
+  background: #fff;
+  border-radius: 16upx;
+  padding: 20upx;
+  margin-bottom: 20upx;
+  box-shadow: 0 4upx 16upx rgba(0, 0, 0, 0.04);
+}
+
+.cover-wrap {
+  position: relative;
+  width: 200upx;
+  height: 200upx;
+  flex-shrink: 0;
+  border-radius: 12upx;
+  overflow: hidden;
+  background: #f5f5f5;
+}
+
+.cover {
+  width: 100%;
+  height: 100%;
+}
+
+.badge {
+  position: absolute;
+  left: 0;
+  top: 0;
+  padding: 4upx 12upx;
+  font-size: 20upx;
+  color: #fff;
+  border-radius: 0 0 12upx 0;
+  background: #FF4D6D;
+}
+
+.card-body {
+  flex: 1;
+  min-width: 0;
+  margin-left: 20upx;
+  display: flex;
+  flex-direction: column;
+}
+
+.goods-name {
+  font-size: 30upx;
+  font-weight: 700;
+  color: #333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 1.4;
+}
+
+.bottom-row {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  margin-top: auto;
+  padding-top: 12upx;
+}
+
+.price-wrap {
+  display: flex;
+  align-items: baseline;
+  flex: 1;
+  min-width: 0;
+  margin-right: 12upx;
+}
+
+.price {
+  font-size: 34upx;
+  font-weight: 700;
+  color: #FF4D6D;
+}
+
+.price-symbol {
+  font-size: 24upx;
+  margin-right: 2upx;
+}
+
+.origin-price {
+  margin-left: 12upx;
+  font-size: 22upx;
+  color: #bbb;
+  text-decoration: line-through;
+}
+
+.sold {
+  font-size: 22upx;
+  color: #999;
+}
+
+.list-end {
+  padding: 30upx 0 20upx;
+  text-align: center;
+  font-size: 24upx;
+  color: #bbb;
+}
+</style>

+ 3 - 0
mallApp/src/pages/home/index.vue

@@ -54,18 +54,21 @@
           <home-goods-section
           <home-goods-section
             v-else-if="mod.key === 'hot'"
             v-else-if="mod.key === 'hot'"
             :data="pageData.hot"
             :data="pageData.hot"
+            module-key="hot"
             :account="account"
             :account="account"
             :hd-id="hdId"
             :hd-id="hdId"
           />
           />
           <home-goods-section
           <home-goods-section
             v-else-if="mod.key === 'new'"
             v-else-if="mod.key === 'new'"
             :data="pageData.new"
             :data="pageData.new"
+            module-key="new"
             :account="account"
             :account="account"
             :hd-id="hdId"
             :hd-id="hdId"
           />
           />
           <home-goods-section
           <home-goods-section
             v-else-if="mod.key === 'pullGoods'"
             v-else-if="mod.key === 'pullGoods'"
             :data="pageData.pullGoods"
             :data="pageData.pullGoods"
+            module-key="pullGoods"
             :account="account"
             :account="account"
             :hd-id="hdId"
             :hd-id="hdId"
           />
           />