Ver código fonte

feat(shop): 新增 shop/index 店铺首页 + 修复 article-edit.vue :style 规范
- 新增 cfc-frontend/pages/shop/index/ 店铺分包含入口页面
- article-edit.vue: 将 :style 复杂内联表达式改为 dimStyle() 方法

User 2 meses atrás
pai
commit
c5f518a74e

+ 1 - 1
cfc-frontend/pages/article-center/article-edit.vue

@@ -44,7 +44,7 @@
             v-for="dim in dimensionList"
             :key="dim.code"
             :class="['dim-chip', selectedDimensions.indexOf(dim.code) >= 0 ? 'active' : '']"
-            :style="selectedDimensions.indexOf(dim.code) >= 0 ? { background: dim.color, color: '#fff', borderColor: dim.color } : {}"
+            :style="selectedDimensions.indexOf(dim.code) >= 0 ? 'background:' + dim.color + ';color:#fff;border-color:' + dim.color : ''"
             @click="toggleDimension(dim.code)"
           >
             {{ dim.name }}

+ 401 - 0
cfc-frontend/pages/shop/index/index.vue

@@ -0,0 +1,401 @@
+<template>
+  <view class="container">
+    <!-- Level 1 分类:2列横排网格 -->
+    <view class="cat-level1">
+      <view
+        v-for="cat in categories"
+        :key="cat.id"
+        :class="['cat1-item', currentCat1 && currentCat1.id === cat.id ? 'active' : '']"
+        @click="onCat1Select(cat)"
+      >
+        <image v-if="cat.image" class="cat1-icon" :src="cat.image" mode="aspectFit" />
+        <view v-else class="cat1-icon-placeholder">
+          <text class="cat1-icon-text">{{ cat.name.substr(0, 1) }}</text>
+        </view>
+        <text class="cat1-name">{{ cat.name }}</text>
+      </view>
+    </view>
+
+    <!-- Level 2 子分类:横向滚动chips -->
+    <scroll-view
+      v-if="currentCat1 && currentCat1.children && currentCat1.children.length"
+      scroll-x
+      enable-flex
+      show-scrollbar="false"
+      class="cat-level2-scroll"
+    >
+      <view class="cat2-chips">
+        <view
+          :class="['cat2-chip', !currentCat2 ? 'active' : '']"
+          @click="onCat2Select(null)"
+        >全部</view>
+        <view
+          v-for="child in currentCat1.children"
+          :key="child.id"
+          :class="['cat2-chip', currentCat2 && currentCat2.id === child.id ? 'active' : '']"
+          @click="onCat2Select(child)"
+        >{{ child.name }}</view>
+      </view>
+    </scroll-view>
+
+    <!-- 商品列表 -->
+    <scroll-view
+      scroll-y
+      class="product-list"
+      @scrolltolower="onLoadMore"
+      refresher-enabled
+      :refresher-triggered="refreshing"
+      @refresherrefresh="onRefresh"
+    >
+      <view v-if="loading && products.length === 0" class="loading-wrap">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-else-if="products.length === 0" class="empty-wrap">
+        <text class="empty-icon">📦</text>
+        <text class="empty-text">暂无商品</text>
+      </view>
+      <view v-else class="grid">
+        <view
+          v-for="item in products"
+          :key="item.id"
+          class="product-card"
+          @click="goDetail(item.id)"
+        >
+          <image
+            class="cover"
+            :src="item.image || item.coverImage || '/static/default-product.png'"
+            mode="aspectFill"
+          />
+          <view class="card-body">
+            <text class="product-name">{{ item.name }}</text>
+            <view class="price-row">
+              <text class="price">{{ formatPriceWithSymbol(item.price) }}</text>
+              <text v-if="item.memberPrice" class="member-price">会员{{ formatPriceWithSymbol(item.memberPrice) }}</text>
+            </view>
+            <text class="vendor-name">{{ item.brandName || '平台官方' }}</text>
+          </view>
+        </view>
+      </view>
+      <view v-if="loadingMore" class="loading-more">
+        <text class="loading-text">加载中...</text>
+      </view>
+      <view v-if="noMore && products.length > 0" class="no-more">
+        <text class="no-more-text">— 没有更多了 —</text>
+      </view>
+      <view v-if="!isLoggedIn" class="login-hint-bar">
+        <text class="login-hint-text">🔒 登录查看全部商品和会员价</text>
+      </view>
+      <view class="bottom-spacer"></view>
+    </scroll-view>
+
+
+  </view>
+</template>
+
+<script>
+import { goodsCategory, productList } from '@/utils/api.js'
+export default {
+  data() {
+    return {
+      categories: [],
+      currentCat1: null,
+      currentCat2: null,
+      products: [],
+      page: 1,
+      size: 20,
+      loading: false,
+      loadingMore: false,
+      refreshing: false,
+      noMore: false,
+      isLoggedIn: false
+    }
+  },
+  onLoad() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+    this.loadCategories()
+    this.loadProducts()
+  },
+  onShow() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+  },
+  methods: {
+    loadCategories() {
+      goodsCategory('0').then(res => {
+        if (res.code === 200 && res.data) {
+          this.categories = res.data || []
+        }
+      }).catch(() => {})
+    },
+    onCat1Select(cat) {
+      if (this.currentCat1 && this.currentCat1.id === cat.id) return
+      this.currentCat1 = cat
+      this.currentCat2 = null
+      this.page = 1
+      this.products = []
+      this.noMore = false
+      this.loadProducts()
+    },
+    onCat2Select(cat) {
+      this.currentCat2 = cat
+      this.page = 1
+      this.products = []
+      this.noMore = false
+      this.loadProducts()
+    },
+    loadProducts() {
+      if (this.loading) return
+      this.loading = true
+      const params = { page: this.page, size: this.size }
+      if (this.currentCat2) {
+        params.categoryId = this.currentCat2.id
+      } else if (this.currentCat1) {
+        params.categoryId = this.currentCat1.id
+      }
+      productList(params).then(res => {
+        this.loading = false
+        this.refreshing = false
+        if (res.code === 200 && res.data) {
+          const records = res.data.records || []
+          if (this.page === 1) {
+            this.products = records
+          } else {
+            this.products = this.products.concat(records)
+          }
+          const total = res.data.total || 0
+          this.noMore = this.products.length >= total
+        }
+      }).catch(() => {
+        this.loading = false
+        this.refreshing = false
+      })
+    },
+    onRefresh() {
+      this.refreshing = true
+      this.page = 1
+      this.noMore = false
+      this.loadProducts()
+    },
+    onLoadMore() {
+      if (this.noMore || this.loadingMore) return
+      this.page++
+      this.loadingMore = true
+      const params = { page: this.page, size: this.size }
+      if (this.currentCat2) {
+        params.categoryId = this.currentCat2.id
+      } else if (this.currentCat1) {
+        params.categoryId = this.currentCat1.id
+      }
+      productList(params).then(res => {
+        this.loadingMore = false
+        if (res.code === 200 && res.data) {
+          const records = res.data.records || []
+          this.products = this.products.concat(records)
+          const total = res.data.total || 0
+          this.noMore = this.products.length >= total
+        }
+      }).catch(() => {
+        this.loadingMore = false
+      })
+    },
+    goDetail(id) {
+      uni.navigateTo({ url: '/pages/shop/detail/detail?id=' + id })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background: #f5f5f5;
+}
+/* ==================== 分类布局 ==================== */
+/* Level 1 分类:2列横排网格 */
+.cat-level1 {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 16rpx 16rpx 8rpx;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+}
+.cat1-item {
+  width: 50%;
+  box-sizing: border-box;
+  padding: 8rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  text-align: center;
+  border-radius: 12rpx;
+  margin-bottom: 8rpx;
+}
+.cat1-item.active {
+  background: rgba(249, 115, 22, 0.08);
+}
+.cat1-icon {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  margin-bottom: 6rpx;
+  background: #f5f5f5;
+}
+.cat1-icon-placeholder {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  margin-bottom: 6rpx;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.cat1-icon-text {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #fff;
+}
+.cat1-name {
+  font-size: 24rpx;
+  color: #333;
+  max-width: 140rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.cat1-item.active .cat1-name {
+  color: #F97316;
+  font-weight: bold;
+}
+
+/* Level 2 子分类:横向滚动chips */
+.cat-level2-scroll {
+  background: #fff;
+  white-space: nowrap;
+  padding: 12rpx 16rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.cat2-chips {
+  display: inline-flex;
+  gap: 12rpx;
+  padding: 0 4rpx;
+}
+.cat2-chip {
+  display: inline-block;
+  padding: 8rpx 24rpx;
+  font-size: 24rpx;
+  color: #666;
+  background: #f0f0f0;
+  border-radius: 30rpx;
+  flex-shrink: 0;
+}
+.cat2-chip.active {
+  background: #F97316;
+  color: #fff;
+}
+
+/* ==================== 商品列表 ==================== */
+.product-list {
+  flex: 1;
+  height: calc(100vh - 220rpx);
+}
+.login-hint-bar {
+  text-align: center;
+  padding: 20rpx;
+  background: #fef9e7;
+  border-top: 1rpx solid #f0e6c0;
+}
+.login-hint-text {
+  font-size: 24rpx;
+  color: #b8860b;
+}
+.bottom-spacer {
+  height: 120rpx;
+}
+.grid {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 20rpx;
+  justify-content: space-between;
+}
+.product-card {
+  width: 49%;
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+}
+.cover {
+  width: 100%;
+  height: 320rpx;
+  background: #f0f0f0;
+}
+.card-body {
+  padding: 16rpx;
+}
+.product-name {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  lines: 2;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  margin-bottom: 12rpx;
+}
+.price-row {
+  display: flex;
+  align-items: baseline;
+  margin-bottom: 8rpx;
+}
+.price {
+  font-size: 32rpx;
+  color: #F97316;
+  font-weight: bold;
+  margin-right: 12rpx;
+}
+.member-price {
+  font-size: 22rpx;
+  color: #999;
+}
+.vendor-name {
+  font-size: 22rpx;
+  color: #999;
+}
+.loading-wrap,
+.empty-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.loading-text {
+  font-size: 26rpx;
+  color: #999;
+}
+.empty-icon {
+  font-size: 100rpx;
+  margin-bottom: 30rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+.loading-more,
+.no-more {
+  text-align: center;
+  padding: 30rpx;
+}
+.loading-text {
+  font-size: 24rpx;
+  color: #999;
+}
+.no-more-text {
+  font-size: 24rpx;
+  color: #ccc;
+}
+</style>