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

feat: migrate TabBar to 5-tab (首页/身体/心智/行动/我的), move shop→action path

User 4 месяцев назад
Родитель
Сommit
5554e9fcfa

+ 1 - 1
cfc-frontend/App.vue

@@ -18,7 +18,7 @@ export default {
       // 如果当前不在允许的未登录页面,则跳转发现页
       const pages = getCurrentPages()
       const currentRoute = pages.length > 0 ? pages[pages.length - 1].route : ''
-      const allowedRoutes = ['pages/discover/index', 'pages/index/index', 'pages/body/index', 'pages/mind/index', 'pages/shop/index']
+      const allowedRoutes = ['pages/discover/index', 'pages/index/index', 'pages/body/index', 'pages/mind/index', 'pages/action/index']
       if (!allowedRoutes.includes(currentRoute)) {
         uni.reLaunch({ url: '/pages/discover/index' })
       }

+ 7 - 3
cfc-frontend/components/bottom-nav.vue

@@ -37,15 +37,19 @@ export default {
       if (tab === this.current) return
 
       if (tab === 'index') {
-        // index 是 TabBar 页面,已登录用 switchTab,未登录用 navigateTo
+        // 首页是 TabBar 页面
         if (uni.getStorageSync('token')) {
           uni.switchTab({ url: '/pages/index/index' })
         } else {
           uni.navigateTo({ url: '/pages/index/index' })
         }
       } else if (tab === 'action') {
-        // shop 已从 TabBar 移除,统一用 navigateTo
-        uni.navigateTo({ url: '/pages/shop/index' })
+        // 行动是 TabBar 页面
+        if (uni.getStorageSync('token')) {
+          uni.switchTab({ url: '/pages/action/index' })
+        } else {
+          uni.navigateTo({ url: '/pages/action/index' })
+        }
       } else if (tab === 'mind') {
         // 心智文章页(非 TabBar)
         uni.navigateTo({ url: '/pages/mind/articles' })

+ 2 - 2
cfc-frontend/custom-tab-bar/index.vue

@@ -47,7 +47,7 @@ export default {
         { text: '首页', pagePath: '/pages/index/index', icon: '/static/tab-home.png', selectedIcon: '/static/tab-home-active.png' },
         { text: '身体', pagePath: '/pages/body/index', icon: '/static/tab-body.png', selectedIcon: '/static/tab-body-active.png' },
         { text: '心智', pagePath: '/pages/mind/index', icon: '/static/tab-mind.png', selectedIcon: '/static/tab-mind-active.png' },
-        { text: '行动', pagePath: '/pages/shop/index', icon: '/static/tab-action.png', selectedIcon: '/static/tab-action-active.png' },
+        { text: '行动', pagePath: '/pages/action/index', icon: '/static/tab-action.png', selectedIcon: '/static/tab-action-active.png' },
         { text: '我的', pagePath: '/pages/profile/profile', icon: '/static/tab-profile.png', selectedIcon: '/static/tab-profile-active.png' }
       ],
       teacherTabs: [
@@ -92,7 +92,7 @@ export default {
     switchTab(item, index) {
       this.currentIndex = index
       // 原生TabBar页使用switchTab,非Tab页使用redirectTo(比reLaunch轻量)
-      const nativeTabPages = ['/pages/index/index', '/pages/body/index', '/pages/mind/index', '/pages/shop/index', '/pages/profile/profile']
+      const nativeTabPages = ['/pages/index/index', '/pages/body/index', '/pages/mind/index', '/pages/action/index', '/pages/profile/profile']
       if (nativeTabPages.includes(item.pagePath)) {
         uni.switchTab({ url: item.pagePath })
       } else {

+ 24 - 8
cfc-frontend/pages.json

@@ -63,6 +63,12 @@
         "navigationBarTitleText": "文章详情"
       }
     },
+    {
+      "path": "pages/action/index",
+      "style": {
+        "navigationBarTitleText": "行动"
+      }
+    },
     {
       "path": "pages/profile/profile",
       "style": {
@@ -345,10 +351,6 @@
     {
       "root": "pages/shop",
       "pages": [
-        {
-          "path": "index",
-          "style": { "navigationBarTitleText": "行动" }
-        },
         {
           "path": "order-list/order-list",
           "style": { "navigationBarTitleText": "我的订单" }
@@ -452,19 +454,33 @@
     "list": [
       {
         "pagePath": "pages/index/index",
-        "text": "首页"
+        "text": "首页",
+        "iconPath": "static/tab-home.png",
+        "selectedIconPath": "static/tab-home-active.png"
       },
       {
         "pagePath": "pages/body/index",
-        "text": "身体"
+        "text": "身体",
+        "iconPath": "static/tab-body.png",
+        "selectedIconPath": "static/tab-body-active.png"
       },
       {
         "pagePath": "pages/mind/index",
-        "text": "心智"
+        "text": "心智",
+        "iconPath": "static/tab-mind.png",
+        "selectedIconPath": "static/tab-mind-active.png"
+      },
+      {
+        "pagePath": "pages/action/index",
+        "text": "行动",
+        "iconPath": "static/tab-action.png",
+        "selectedIconPath": "static/tab-action-active.png"
       },
       {
         "pagePath": "pages/profile/profile",
-        "text": "我的"
+        "text": "我的",
+        "iconPath": "static/tab-profile.png",
+        "selectedIconPath": "static/tab-profile-active.png"
       }
     ]
   }

+ 409 - 0
cfc-frontend/pages/action/index.vue

@@ -0,0 +1,409 @@
+<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">¥{{ item.price }}</text>
+              <text v-if="item.memberPrice" class="member-price">会员¥{{ 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>
+
+    <bottom-nav v-if="!isLoggedIn" current="action" />
+  </view>
+</template>
+
+<script>
+import { goodsCategory, productList } from '@/utils/api.js'
+import BottomNav from '../../components/bottom-nav.vue'
+
+export default {
+  components: { BottomNav },
+  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) {
+      const token = uni.getStorageSync('token')
+      if (token) {
+        uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
+      } else {
+        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/action/index') })
+      }
+    }
+  }
+}
+</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>

+ 1 - 1
cfc-frontend/pages/discover/index.vue

@@ -172,7 +172,7 @@ export default {
       this.loadProducts()
     },
     goShop() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/action/index' })
     },
     goDetail(id) {
       const token = uni.getStorageSync('token')

+ 1 - 9
cfc-frontend/pages/index/index.vue

@@ -196,17 +196,9 @@ export default {
 
       return currentRole
     },
-    syncTabBarText(role) {
-      const secondText = role === 'teacher' ? '家庭管理' : '任务'
-      const thirdText = role === 'teacher' ? '消息' : '心愿单'
-
-      uni.setTabBarItem({ index: 1, text: secondText })
-      uni.setTabBarItem({ index: 2, text: thirdText })
-    },
     syncRoleAndTabBar() {
       this.currentRole = this.normalizeCurrentRole()
       uni.setStorageSync('currentRole', this.currentRole)
-      this.syncTabBarText(this.currentRole)
     },
     // ===== 发现页 / 未登录态方法 =====
     loadProducts() {
@@ -229,7 +221,7 @@ export default {
       this.loadProducts()
     },
     goShop() {
-      uni.navigateTo({ url: '/pages/shop/index' })
+      uni.navigateTo({ url: '/pages/action/index' })
     },
     goDetail(id) {
       uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })

+ 1 - 1
cfc-frontend/pages/shop/index.vue

@@ -205,7 +205,7 @@ export default {
       if (token) {
         uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
       } else {
-        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/shop/index') })
+        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/action/index') })
       }
     }
   }