|
|
@@ -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>
|