Browse Source

feat(T31): 我的团队 团队数据三列显示 + 客户归属真实总数

- 后端 supply-team/customers 响应改为 { list, total }:total 为体系/上级下客户归属真实总数
  (原前端以 size=50 分页结果长度当数量,客户超 50 个即失真)
- team-system.vue:删除与「层级关系-团队成员」重复的统计卡;
  团队数据改三列(calc(100%/3) + box-sizing:border-box,第 3n 张去右分隔线);
  「客户归属」改用后端返回的 total
- team-customers.vue:兼容 { list, total } 新响应形状(保留旧数组形状回退)
Agnes Agent 1 week ago
parent
commit
558b985c1d

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyTeamController.java

@@ -24,7 +24,7 @@ public class SupplyTeamController {
      * 查询上级供应商下的客户列表
      */
     @PostMapping("/customers")
-    public Result<List<Map<String, Object>>> listCustomers(
+    public Result<Map<String, Object>> listCustomers(
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId) {
         Long systemId = ParamUtils.getLong(params.get("systemId"));
@@ -39,8 +39,8 @@ public class SupplyTeamController {
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
 
-        List<Map<String, Object>> customers = supplyCustomerRelationService.listCustomers(systemId, parentId, page, size);
-        return Result.success(customers);
+        Map<String, Object> data = supplyCustomerRelationService.listCustomers(systemId, parentId, page, size);
+        return Result.success(data);
     }
 
     /**

+ 14 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/SupplyCustomerRelationService.java

@@ -33,21 +33,24 @@ public class SupplyCustomerRelationService {
 
     /**
      * 查询某上级供应商下的客户列表
-     * 返回:家庭ID、家庭名称、成员昵称、加入时间、归属来源
+     * 返回:{ list: [...], total: N } —— list 为分页后的客户(家庭ID、家庭名称、成员昵称、加入时间、归属来源),
+     *       total 为该体系/上级下的客户总数(团队数据「客户归属」的真实总数,T31)
      */
-    public List<Map<String, Object>> listCustomers(Long systemId, Long parentId, Integer page, Integer size) {
+    public Map<String, Object> listCustomers(Long systemId, Long parentId, Integer page, Integer size) {
         // parentId 为空时返回整个体系的客户列表(团队维度「团队客户」,T31);否则按指定上级过滤
         List<SupplyCustomerRelation> relations = parentId == null
                 ? supplyCustomerRelationMapper.listBySystem(systemId)
                 : supplyCustomerRelationMapper.listByParent(systemId, parentId);
-        
+
+        int total = relations.size();
         // 分页处理
         int fromIndex = (page - 1) * size;
-        int toIndex = Math.min(fromIndex + size, relations.size());
-        if (fromIndex >= relations.size()) {
-            return Collections.emptyList();
+        int toIndex = Math.min(fromIndex + size, total);
+        if (fromIndex >= total) {
+            relations = Collections.emptyList();
+        } else {
+            relations = relations.subList(fromIndex, toIndex);
         }
-        relations = relations.subList(fromIndex, toIndex);
 
         List<Map<String, Object>> result = new ArrayList<>();
         for (SupplyCustomerRelation relation : relations) {
@@ -92,7 +95,10 @@ public class SupplyCustomerRelationService {
 
             result.add(customer);
         }
-        return result;
+        Map<String, Object> wrapper = new HashMap<>();
+        wrapper.put("list", result);
+        wrapper.put("total", total);
+        return wrapper;
     }
 
     /**

+ 49 - 43
cfc-frontend/pages/vendor/team-customers/team-customers.vue

@@ -6,7 +6,7 @@
     </view>
 
     <view class="content-area">
-      <view v-if="isLoading" class="loading-indicator">
+      <view v-if="loading" class="loading-indicator">
         <image class="loading-avatar" src="/static/images/avatar_default.png" mode="aspectFill"></image>
         <text class="loading-text">加载中...</text>
       </view>
@@ -25,51 +25,50 @@
           </view>
         </view>
 
+        <!-- 空状态 -->
+        <view class="no-data" v-if="filteredCustomers.length === 0">
+          <view class="empty-state">
+            <image src="/static/images/icon_empty_state.png" class="empty-icon" mode="aspectFit"></image>
+            <text>暂无客户数据</text>
+          </view>
+        </view>
+
         <!-- 列表区域 -->
-        <view v-else>
-          <view class="no-data" v-if="filteredCustomers.length === 0">
-            <view class="empty-state">
-              <image src="/static/images/icon_empty_state.png" class="empty-icon" mode="aspectFit"></image>
-              <text>暂无客户数据</text>
-            </view>
+        <view class="customer-item" v-for="customer in filteredCustomers" :key="customer.id">
+          <view class="customer-avatar-wrap">
+            <image
+              v-if="customer.avatar"
+              class="customer-avatar-img"
+              :src="customer.avatar"
+              mode="aspectFill"></image>
+            <text v-else class="customer-avatar-icon">👤</text>
           </view>
 
-          <view class="customer-item" v-for="customer in filteredCustomers" :key="customer.id">
-            <view class="customer-avatar-wrap">
-              <image
-                v-if="customer.avatar"
-                class="customer-avatar-img"
-                :src="customer.avatar"
-                mode="aspectFill"></image>
-              <text v-else class="customer-avatar-icon">👤</text>
+          <view class="customer-info">
+            <view class="customer-name">{{ customer.familyName || customer.buyerNickname || '微信用户' }}</view>
+            <view class="customer-meta">
+              <text class="member-type">{{ customer.memberType === 'login' ? '登录用户' : '本地用户' }}</text>
+              <text class="join-time">{{ formatJoinTime(customer.createdAt) }}</text>
             </view>
+          </view>
 
-            <view class="customer-info">
-              <view class="customer-name">{{ customer.nickname || '微信用户' }}</view>
-              <view class="customer-meta">
-                <text class="member-type">{{ customer.memberType || '本地用户' }}</text>
-                <text class="join-time">{{ formatJoinTime(customer.joinTime) }}</text>
-              </view>
+          <view class="customer-right">
+            <view class="order-count">
+              <text>{{ customer.loginMemberCount || 0 }}</text>
+              <text>成员</text>
             </view>
 
-            <view class="customer-right">
-              <view class="order-count">
-                <text>{{ customer.orderCount || 0 }}</text>
-                <text>订单</text>
-              </view>
-
-              <view class="source-tag" :class="'tag-' + (customer.source || 'default')">
-                <text>{{ getSourceTagLabel(customer.source) }}</text>
-              </view>
+            <view class="source-tag" :class="'tag-' + (customer.source || 'default')">
+              <text>{{ getSourceTagLabel(customer.source) }}</text>
             </view>
           </view>
+        </view>
 
-          <!-- 加载更多 -->
-          <view class="load-more" v-if="hasMore && !loading">
-            <view @click="loadMore" class="load-more-btn">
-              <text>加载更多</text>
-              <image v-if="loadingMore" src="/static/images/arrow_down.png" class="spinner" mode="aspectFit"></image>
-            </view>
+        <!-- 加载更多 -->
+        <view class="load-more" v-if="hasMore && !loading">
+          <view @click="loadMore" class="load-more-btn">
+            <text>加载更多</text>
+            <image v-if="loadingMore" src="/static/images/arrow_down.png" class="spinner" mode="aspectFit"></image>
           </view>
         </view>
       </view>
@@ -79,15 +78,18 @@
 
 <script>
 import fontScaleMixin from '@/mixins/font-scale.js'
+import { getVendorTeamCustomers } from '@/utils/api.js'
+import { parseDate } from '@/utils/format.js'
 export default {
   mixins: [fontScaleMixin],
   data() {
     return {
       // 页面状态
-      status: 'all', // all | referred | order | manual
+      status: 'all', // all | referral | order | manual
       page: 1,
       hasMore: true,
       loading: false,
+      loadingMore: false,
       refreshing: false,
 
       // 数据
@@ -95,7 +97,7 @@ export default {
       filteredCustomers: [], // 筛选后的客户列表
       statusOptions: [       // 筛选选项
         { key: 'all', label: '全部' },
-        { key: 'referred', label: '推荐人' },
+        { key: 'referral', label: '推荐码' },
         { key: 'order', label: '下单' },
         { key: 'manual', label: '人工' }
       ],
@@ -135,7 +137,7 @@ export default {
 
       try {
         // 并发请求:获取客户列表 + 获取统计概览
-        var res = await this.$http.getTeamCustomers({
+        var res = await getVendorTeamCustomers({
           systemId: this.systemId,
           parentId: this.parentId,
           page: this.page,
@@ -143,8 +145,11 @@ export default {
         })
 
         if (res && res.data) {
-          this.customers = this.page === 1 ? res.data.records || [] : this.customers.concat(res.data.records || [])
-          this.hasMore = (res.data.records || []).length >= 20
+          // 后端返回 { list, total }(Map),兼容旧版数组形状
+          var data = res.data
+          var list = data instanceof Array ? data : (data.list || data.records || [])
+          this.customers = this.page === 1 ? list : this.customers.concat(list)
+          this.hasMore = (list || []).length >= 20
           this.page++
 
           // 计算筛选后的列表
@@ -190,7 +195,8 @@ export default {
     // 格式化加入时间
     formatJoinTime(timeStr) {
       if (!timeStr) return '-'
-      var d = this.parseDate(timeStr)
+      var d = parseDate(timeStr)
+      if (!d) return '-'
       return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
     },
 
@@ -201,7 +207,7 @@ export default {
         'order': '下单自动',
         'referral': '推荐码'
       }
-      return labels[source] || source
+      return labels[source] || source || '未标注'
     }
   }
 }

+ 45 - 31
cfc-frontend/pages/vendor/team-system/team-system.vue

@@ -1,8 +1,8 @@
 <template>
   <view class="page-team-system" :style="{ '--font-scale': fontScale }">
-    <!-- 团队切换器(T31 多团队管理;每行两项) -->
+    <!-- 团队切换器(T31 多团队管理;厂商卡片一行横排,超出可横向滚动) -->
     <view class="team-switcher-wrap" v-if="teamList.length > 0">
-      <view class="team-switcher">
+      <scroll-view class="team-switcher" scroll-x="true" :show-scrollbar="false">
         <view
           class="team-tab"
           v-for="team in teamList"
@@ -12,7 +12,7 @@
         >
           <text class="team-tab-text">{{ team.name || '未命名团队' }}</text>
         </view>
-      </view>
+      </scroll-view>
     </view>
 
     <!-- 头部:体系信息 -->
@@ -70,10 +70,6 @@
         <text class="section-title">团队数据</text>
       </view>
       <view class="stats-grid">
-        <view class="stat-card">
-          <text class="stat-num">{{ teamStats.memberCount || 0 }}</text>
-          <text class="stat-name">团队成员</text>
-        </view>
         <view class="stat-card">
           <text class="stat-num">{{ teamStats.customerCount || 0 }}</text>
           <text class="stat-name">客户归属</text>
@@ -161,13 +157,13 @@ export default {
       systemInfo: null,
       hierarchyInfo: null,
       teamStats: {
-        memberCount: 0,
         customerCount: 0,
         orderCount: 0,
         amount: 0
       },
       teamMembers: [],
-      teamCustomers: []
+      teamCustomers: [],
+      customerTotal: 0   // 「客户归属」真实总数(后端 total,避免分页截断)
     }
   },
 
@@ -241,7 +237,8 @@ export default {
           this.teamMembers = membersRes
         }
         if (customersRes) {
-          this.teamCustomers = customersRes
+          this.teamCustomers = customersRes.list || []
+          this.customerTotal = customersRes.total || 0
         }
 
         // 更新统计
@@ -285,7 +282,8 @@ export default {
           this.teamMembers = membersRes
         }
         if (customersRes) {
-          this.teamCustomers = customersRes
+          this.teamCustomers = customersRes.list || []
+          this.customerTotal = customersRes.total || 0
         }
         this.updateStats()
       } catch (error) {
@@ -323,7 +321,7 @@ export default {
       }
     },
 
-    // 获取团队客户
+    // 获取团队客户(返回 { list, total };兼容旧后端数组形状)
     async fetchTeamCustomers() {
       try {
         const res = await getVendorTeamCustomers({
@@ -332,16 +330,22 @@ export default {
           page: 1,
           size: 50
         })
-        return res.data || []
+        var data = res.data || {}
+        if (data instanceof Array) {
+          return { list: data, total: data.length }
+        }
+        return {
+          list: data.list || [],
+          total: data.total || 0
+        }
       } catch (e) {
-        return []
+        return { list: [], total: 0 }
       }
     },
 
-    // 更新统计数据
+    // 更新统计数据(「客户归属」用后端返回的真实总数,避免分页截断)
     updateStats() {
-      this.teamStats.memberCount = this.teamMembers.length
-      this.teamStats.customerCount = this.teamCustomers.length
+      this.teamStats.customerCount = this.customerTotal || 0
     },
 
     // 跳转管理页面
@@ -383,7 +387,7 @@ export default {
   box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
 }
 
-/* 团队切换器(T31 多团队管理;每行两项) */
+/* 团队切换器(T31 多团队管理;厂商卡片一行横排,超出可横向滚动) */
 .team-switcher-wrap {
   background-color: $app-surface;
   padding: 20rpx 0;
@@ -391,23 +395,26 @@ export default {
 }
 
 .team-switcher {
-  display: flex;
-  flex-wrap: wrap;
-  padding: 0 $uni-space-page;
+  width: 100%;
+  white-space: nowrap;
 }
 
-/* 每行两项:50% 宽 + 半间距,两列之间自动留 16rpx 空隙 */
 .team-tab {
-  display: flex;
+  display: inline-flex;
   align-items: center;
   justify-content: center;
-  width: calc(50% - 8rpx);
-  margin: 0 8rpx 16rpx;
-  padding: 16rpx 20rpx;
+  padding: 16rpx 28rpx;
+  margin-left: $uni-space-lg;
   border-radius: 999rpx;
   background-color: $uni-primary-pale;
-  box-sizing: border-box;
-  flex-shrink: 0;
+}
+
+.team-tab:first-child {
+  margin-left: $uni-space-page;
+}
+
+.team-tab:last-child {
+  margin-right: $uni-space-page;
 }
 
 .team-tab-active {
@@ -506,6 +513,7 @@ export default {
   background-color: $uni-primary-pale;
   border-radius: 16rpx;
   margin-bottom: 16rpx;
+  box-sizing: border-box;
 }
 
 .hierarchy-label {
@@ -565,6 +573,7 @@ export default {
   background-color: $facade-fill-blue-ghost;
   border-radius: 16rpx;
   text-align: center;
+  box-sizing: border-box;
 }
 
 .stat-value {
@@ -586,11 +595,16 @@ export default {
 }
 
 .stat-card {
-  width: 50%;
-  padding: 20rpx;
+  width: calc(100% / 3);
+  padding: 20rpx 12rpx;
   text-align: center;
+  box-sizing: border-box;
   border-right: 1rpx solid $brand-primary-light-2;
-  border-bottom: 1rpx solid $brand-primary-light-2;
+}
+
+/* 三列布局:每第 3 张(最后一列)不画右边线,避免右侧多余分隔线 */
+.stat-card:nth-child(3n) {
+  border-right: none;
 }
 
 .stat-num {