Explorar o código

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

iwt hai 1 mes
pai
achega
1ec5e0ed7e

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminPurchaseFieldController.java

@@ -42,7 +42,7 @@ public class AdminPurchaseFieldController {
     @PostMapping("/save")
     public Result<Void> save(@RequestBody Map<String, Object> params) {
         Long productId = Long.parseLong(params.get("productId").toString());
-        List<ProductPurchaseField> fields = convertFieldList(params.get("fields"));
+List<ProductPurchaseField> fields = convertFieldList(params.get("fields"));
         return purchaseFieldService.save(productId, fields);
     }
 

+ 3 - 27
cfc-frontend/components/ArticleBookshelf.vue

@@ -1,20 +1,5 @@
 <template>
   <view class="bs-section">
-    <!-- 书册头部 -->
-    <view class="bs-header">
-      <view class="bs-header-left">
-        <text class="bs-book-icon">&#x1F4DA;</text>
-        <view class="bs-header-text">
-          <text class="bs-title">推荐阅读</text>
-          <text class="bs-subtitle">Knowledge · 知识</text>
-        </view>
-      </view>
-      <view class="bs-header-right" @click="$emit('moreArticles')">
-        <text class="bs-more-text">书库</text>
-        <text class="bs-more-arrow">&#x203A;</text>
-      </view>
-    </view>
-
     <!-- 无数据 -->
     <view v-if="(!articles || articles.length === 0) && !loading" class="bs-empty">
       <text class="bs-empty-text">书册整理中,敬请期待...</text>
@@ -42,24 +27,20 @@
         @scroll="onScroll"
       >
         <view class="bs-books-inner">
-          <!-- 左内边距起始 -->
           <view class="bs-book-spacer"></view>
 
-          <!-- 书籍列表 -->
           <view
             v-for="(article, idx) in displayArticles"
             :key="idx"
             class="bs-book-wrap"
             @click="$emit('articleClick', article)"
           >
-            <!-- 竖版封面主体 -->
             <view
               class="bs-book-cover"
               :style="{
                 background: getBookColor(article, idx)
               }"
             >
-              <!-- 封面图 -->
               <image
                 v-if="article.coverImage"
                 class="bs-cover-img"
@@ -67,7 +48,6 @@
                 mode="aspectFill"
               />
 
-              <!-- 无封面图时:渐变底 + 书名 -->
               <view v-else class="bs-cover-text">
                 <text
                   class="bs-cover-title"
@@ -75,39 +55,35 @@
                 >{{ article.title }}</text>
               </view>
 
-              <!-- 有封面图时:底部叠加文章标题 -->
               <view v-if="article.coverImage" class="bs-cover-title-bottom">
                 <text class="bs-cover-title-overlay">{{ article.title }}</text>
               </view>
 
-              <!-- 右侧书页切边(3D效果) -->
               <view class="bs-cover-edge"></view>
             </view>
 
-            <!-- 封面底部阴影(书架效果) -->
             <view class="bs-book-shadow"></view>
           </view>
 
-          <!-- 右内边距结尾 -->
           <view class="bs-book-spacer"></view>
         </view>
       </scroll-view>
 
-      <!-- 书架底板装饰 -->
       <view class="bs-shelf-bottom">
         <view class="bs-shelf-plank"></view>
         <view class="bs-shelf-bracket bs-shelf-bracket-left"></view>
         <view class="bs-shelf-bracket bs-shelf-bracket-right"></view>
       </view>
 
-      <!-- 翻页指示器 -->
       <view class="bs-page-dots" v-if="displayArticles.length > 3">
         <view
           v-for="n in dotCount"
           :key="n"
           class="bs-dot"
           :class="{ 'bs-dot-active': isActiveDot(n) }"
-        ></view>
+        >
+          <text class="bs-dot-text">{{ (n - 1) * 3 + 1 }}-{{ Math.min(n * 3, displayArticles.length) }}</text>
+        </view>
       </view>
     </view>
   </view>

+ 16 - 37
cfc-frontend/components/DimensionProducts.vue

@@ -1,14 +1,10 @@
 <template>
-  <view class="section">
-    <view class="section-header">
-      <text class="section-title">🛍️ {{ title }}</text>
-      <text class="section-more" v-if="showMore" @click="$emit('moreProducts')">更多 ›</text>
-    </view>
+  <view>
     <view v-if="hasFamily">
       <view v-if="!loading && displayProducts.length === 0" class="empty-state">
         <text class="empty-tip">{{ emptyText }}</text>
       </view>
-      <view class="product-list" v-if="displayProducts.length > 0">
+      <view v-else class="product-list">
         <view class="product-card" v-for="prod in displayProducts" :key="prod.id" @click="onProductClick(prod)">
           <image class="product-img" :src="getImageUrl(prod.coverImage) || '/static/default-product.png'" mode="aspectFill" />
           <text class="product-name line-clamp-2">{{ prod.name }}</text>
@@ -73,7 +69,7 @@ export default {
       return this.$store.getters.hasFamily
     },
     displayProducts: function() {
-      return this.products && this.products.length > 0 ? this.products : this.selfProducts
+      return (this.products && this.products.length > 0 ? this.products : this.selfProducts).slice(0, 2)
     },
     emptyText: function() {
       // 有 familyId(已登录且选中孩子)但推荐接口无数据
@@ -103,7 +99,7 @@ export default {
       self.loading = true
       var params = {
         dimensionCode: self.dimensionCode,
-        limit: 6
+        limit: 4
       }
       getDimensionProducts(params).then(function(res) {
         self.loading = false
@@ -135,24 +131,6 @@ export default {
 </script>
 
 <style scoped>
-.section {
-  margin: 20rpx 20rpx;
-}
-.section-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
-}
-.section-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-.section-more {
-  font-size: 24rpx;
-  color: #999;
-}
 .product-list {
   display: flex;
   flex-wrap: wrap;
@@ -160,39 +138,40 @@ export default {
 .product-card {
   width: calc(50% - 10rpx);
   background: #fff;
-  border-radius: 16rpx;
+  border-radius: 14rpx;
   overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-  margin-right: 20rpx;
-  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.06);
+  margin-right: 14rpx;
+  margin-bottom: 14rpx;
 }
 .product-card:nth-child(2n) {
   margin-right: 0;
 }
 .product-img {
   width: 100%;
-  height: 240rpx;
+  height: 180rpx;
   background: #f0f0f0;
 }
 .product-name {
-  font-size: 26rpx;
+  font-size: 24rpx;
   color: #333;
-  padding: 12rpx 16rpx 0;
+  padding: 8rpx 14rpx 0;
+  line-height: 1.3;
 }
 .price-row {
   display: flex;
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
-  padding: 8rpx 16rpx 16rpx;
+  padding: 4rpx 14rpx 12rpx;
 }
 .product-price {
-  font-size: 28rpx;
+  font-size: 26rpx;
   color: #F97316;
   font-weight: bold;
 }
 .product-reason {
-  font-size: 20rpx;
+  font-size: 18rpx;
   color: #999;
   max-width: 60%;
   overflow: hidden;
@@ -204,7 +183,7 @@ export default {
   background: linear-gradient(135deg, #6366F1, #818CF8);
   padding: 4rpx 16rpx;
   border-radius: 20rpx;
-  margin: 0 16rpx 16rpx;
+  margin: 0 10rpx 10rpx;
 }
 .match-score {
   font-size: 22rpx;

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

@@ -260,7 +260,8 @@ export default {
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-top: -44rpx;
+  margin-top: -20rpx;
+  flex-shrink: 0;
   box-shadow: 0 4rpx 16rpx rgba(249,115,22,0.4);
 }
 

+ 184 - 53
cfc-frontend/pages/index-home/index.vue

@@ -89,32 +89,53 @@
       </view>
     </view>
 
-    <!-- 功能入口网格(3列,与健康/成长页一致) -->
+    <!-- 快捷功能入口(单排) -->
     <view class="section">
-      <view class="grid-3col">
-        <view class="grid-item" @click="navTo('/pages/tasks/tasks')">
-          <view class="grid-icon" style="background:rgba(99,102,241,0.15)"><text>📋</text></view>
-          <text class="grid-label">任务</text>
+      <view class="quick-bar">
+        <view class="quick-item" @click="navTo('/pages/tasks/tasks')">
+          <view class="quick-icon" style="background:rgba(99,102,241,0.15)"><text>📋</text></view>
+          <text class="quick-label">任务</text>
         </view>
-        <view class="grid-item" @click="navTo('/pages/action-detail/interaction-log')">
-          <view class="grid-icon" style="background:rgba(16,185,129,0.15)"><text>💬</text></view>
-          <text class="grid-label">记录互动</text>
+        <view class="quick-item" @click="navTo('/pages/action-detail/interaction-log')">
+          <view class="quick-icon" style="background:rgba(16,185,129,0.15)"><text>💬</text></view>
+          <text class="quick-label">互动</text>
         </view>
-        <view class="grid-item" @click="navTo('/pages/mind-detail/family-dashboard')">
-          <view class="grid-icon" style="background:rgba(139,92,246,0.15)"><text>🏠</text></view>
-          <text class="grid-label">家庭天盘</text>
+        <view class="quick-item" @click="navTo('/pages/mind-detail/family-dashboard')">
+          <view class="quick-icon" style="background:rgba(139,92,246,0.15)"><text>🏠</text></view>
+          <text class="quick-label">天盘</text>
         </view>
-        <view class="grid-item" @click="navTo('/pages/profile-extra/family-members')">
-          <view class="grid-icon" style="background:rgba(245,158,11,0.15)"><text>👨‍👩‍👧‍👦</text></view>
-          <text class="grid-label">成员管理</text>
+        <view class="quick-item" @click="navTo('/pages/profile-extra/family-members')">
+          <view class="quick-icon" style="background:rgba(245,158,11,0.15)"><text>👨‍👩‍👧‍👦</text></view>
+          <text class="quick-label">成员</text>
         </view>
-        <view class="grid-item" @click="navTo('/pages/discover/circles')">
-          <view class="grid-icon" style="background:rgba(255,107,157,0.15)"><text>🤝</text></view>
-          <text class="grid-label">圈子</text>
+        <view class="quick-item" @click="navTo('/pages/discover/circles')">
+          <view class="quick-icon" style="background:rgba(255,107,157,0.15)"><text>🤝</text></view>
+          <text class="quick-label">圈子</text>
         </view>
-        <view class="grid-item" @click="navTo('/pages/shop/index/index')">
-          <view class="grid-icon" style="background:rgba(249,115,22,0.15)"><text>🛒</text></view>
-          <text class="grid-label">商城</text>
+        <view class="quick-item" @click="navTo('/pages/shop/index/index')">
+          <view class="quick-icon" style="background:rgba(249,115,22,0.15)"><text>🛒</text></view>
+          <text class="quick-label">商城</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 我的任务(仅当有待完成时显示) -->
+    <view v-if="hasPendingTasks" class="section">
+      <view class="section-header">
+        <text class="section-title">📝 我的任务</text>
+        <text class="section-more" @click="navTo('/pages/tasks/tasks')">全部 ›</text>
+      </view>
+      <view class="task-quick-list">
+        <view
+          v-for="task in pendingTasks"
+          :key="task.id"
+          class="task-quick-item"
+        >
+          <view class="task-quick-info">
+            <text class="task-quick-title line-clamp-1">{{ task.title }}</text>
+            <text class="task-quick-meta">+{{ task.points }}分</text>
+          </view>
+          <button class="btn-quick-complete" @click="quickCompleteTask(task)">完成</button>
         </view>
       </view>
     </view>
@@ -138,19 +159,30 @@
     </view>
 
     <!-- 推荐文章 -->
-    <ArticleBookshelf
-      :articles="actionArticles"
-      :isLoggedIn="true"
-      @articleClick="goArticleDetail"
-      @moreArticles="goMoreArticles" />
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">📖 推荐阅读</text>
+        <text class="section-more" @click="goMoreArticles">更多 ›</text>
+      </view>
+      <ArticleBookshelf
+        :articles="actionArticles"
+        :isLoggedIn="true"
+        @articleClick="goArticleDetail" />
+    </view>
 
     <!-- 推荐商品 -->
-    <DimensionProducts
-      dimensionCode="action"
-      :products="dimensionProducts"
-      :isLoggedIn="true"
-      @productClick="goProductDetail"
-      @moreProducts="goMoreProducts" />
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">🛍️ 推荐商品</text>
+        <text class="section-more" @click="goMoreProducts">更多 ›</text>
+      </view>
+      <DimensionProducts
+        dimensionCode="action"
+        :products="dimensionProducts"
+        :isLoggedIn="true"
+        :showMore="false"
+        @productClick="goProductDetail" />
+    </view>
 
     <!-- 底部占位 -->
     <view class="bottom-spacer" style="height: 40rpx;"></view>
@@ -298,13 +330,11 @@
 import store from '../../store/index.js'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import PageBanner from '../../components/PageBanner.vue'
-import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getProductsByDomain, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge } from '../../utils/api.js'
-import store from '../../store/index.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getProductsByDomain, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi } from '../../utils/api.js'
 import config from '@/config.js'
 
 export default {
@@ -332,6 +362,7 @@ export default {
       splashOverlay: _isFirstLaunch,
       splashOverlayFading: false,
       _splashReady: false,
+      _watchPageReadyDone: false,
 
       typeList: [
         { label: '全部', value: '' },
@@ -367,6 +398,7 @@ export default {
       children: [],
       currentChildId: null,
       activeChallenges: [],
+      pendingTasks: [],
       loggedInInitDone: false,
 
       // 会员状态(沙盘下方会员入口使用)
@@ -399,6 +431,9 @@ export default {
         wealth: d.wealthScore || 0
       }
     },
+    hasPendingTasks: function() {
+      return (this.pendingTasks && this.pendingTasks.length > 0)
+    },
     actionArticles: function() {
       if (!this.articles || this.articles.length === 0) return []
       var result = []
@@ -500,6 +535,7 @@ export default {
           self.loadDimensionData()
         }
         self.childrenChecked = true
+        self.loadTodayTasks()
       }).catch(function(e) {
         console.log('获取孩子列表失败', e)
         self.childrenChecked = true
@@ -523,10 +559,10 @@ export default {
     },
     loadActionProducts: function() {
       var self = this
-      getProductsByDomain('action', 1, 4).then(function(res) {
+      getProductsByDomain('action', 1, 2).then(function(res) {
         if (res && res.data) {
           var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.dimensionProducts = list.slice(0, 4)
+          self.dimensionProducts = list.slice(0, 2)
         }
       }).catch(function() { self.dimensionProducts = [] })
     },
@@ -570,6 +606,35 @@ export default {
         self.activeChallenges = []
       })
     },
+    loadTodayTasks: function() {
+      var self = this
+      if (!self.currentChildId) {
+        self.pendingTasks = []
+        return
+      }
+      getTodayTasks(self.currentChildId).then(function(res) {
+        if (res.code === 200 && Array.isArray(res.data)) {
+          self.pendingTasks = res.data.filter(function(t) {
+            return t.status === 'pending'
+          })
+        }
+      }).catch(function() {
+        self.pendingTasks = []
+      })
+    },
+    quickCompleteTask: function(task) {
+      var self = this
+      completeTaskApi(task.id, self.currentChildId, '').then(function(res) {
+        if (res.code === 200) {
+          uni.showToast({ title: '获得 ' + (res.data && res.data.pointsEarned || 0) + ' 积分', icon: 'success' })
+          self.loadTodayTasks()
+        } else {
+          uni.showToast({ title: (res && res.message) || '完成失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '完成失败,请重试', icon: 'none' })
+      })
+    },
     onChallengeCheckin: function(payload) {
       var self = this
       updateChallengeProgress({ challengeId: payload.challengeId, delta: 1 }).then(function(res) {
@@ -699,6 +764,14 @@ export default {
       this.checkMemberStatus()
       this._watchPageReady()
     },
+    _watchPageReady() {
+      if (this._watchPageReadyDone) return
+      this._watchPageReadyDone = true
+      var that = this
+      that.$watch('_splashReady', function(newVal) {
+        if (newVal) that._tryHideSplash()
+      })
+    },
     isLoggedIn() {
       return !!uni.getStorageSync('token')
     },
@@ -1417,43 +1490,101 @@ export default {
 
 /* ---- 登录态内容布局 ---- */
 .logged-in-container {
-  padding-bottom: 40rpx;
+  padding-bottom: 120rpx;
 }
 
-/* ===== 功能入口网格(3列,对齐健康/成长页) ===== */
-.grid-3col {
+/* ===== 快捷功能入口(单排,6个横向排列) ===== */
+.quick-bar {
   display: flex;
-  flex-wrap: wrap;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
   background: #fff;
   border-radius: 20rpx;
-  padding: 8rpx 0;
+  padding: 20rpx 10rpx;
 }
 
-.grid-item {
-  width: 33.33%;
+.quick-item {
+  flex: 1;
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 20rpx 0;
+  gap: 6rpx;
 }
 
-.grid-icon {
-  width: 72rpx;
-  height: 72rpx;
-  border-radius: 18rpx;
+.quick-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 16rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-bottom: 8rpx;
 }
 
-.grid-icon text {
-  font-size: 32rpx;
+.quick-icon text {
+  font-size: 28rpx;
 }
 
-.grid-item .grid-label {
-  font-size: 24rpx;
+.quick-item .quick-label {
+  font-size: 22rpx;
   color: #666;
 }
 
+/* ===== 我的任务快捷列表 ===== */
+.task-quick-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+
+.task-quick-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 18rpx 24rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+
+.task-quick-info {
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+  flex: 1;
+}
+
+.task-quick-title {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  line-height: 1.4;
+}
+
+.task-quick-meta {
+  font-size: 22rpx;
+  color: #F97316;
+  margin-top: 4rpx;
+  font-weight: bold;
+}
+
+.btn-quick-complete {
+  margin-left: 16rpx;
+  padding: 8rpx 24rpx;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 24rpx;
+  font-weight: bold;
+  border-radius: 20rpx;
+  border: none;
+  line-height: 1;
+  height: 44rpx;
+  flex-shrink: 0;
+}
+
+.btn-quick-complete::after {
+  border: none;
+}
+
 </style>