|
|
@@ -0,0 +1,264 @@
|
|
|
+<template>
|
|
|
+ <view class="container">
|
|
|
+ <!-- 顶部统计卡:团队总消费 / 人数 / 统计层级 -->
|
|
|
+ <view class="stats-card">
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-value orange">{{ totalConsumptionYuan }}</text>
|
|
|
+ <text class="stat-label">团队总消费</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-divider"></view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-value blue">{{ totalMembers }}</text>
|
|
|
+ <text class="stat-label">团队人数</text>
|
|
|
+ </view>
|
|
|
+ <view class="stat-divider"></view>
|
|
|
+ <view class="stat-item">
|
|
|
+ <text class="stat-value green">{{ currentMaxLevel }}级</text>
|
|
|
+ <text class="stat-label">统计层级</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 层级切换 tabs -->
|
|
|
+ <view class="level-tabs">
|
|
|
+ <view
|
|
|
+ class="level-tab"
|
|
|
+ :class="{ active: currentMaxLevel === lvl }"
|
|
|
+ @click="switchLevel(lvl)"
|
|
|
+ v-for="lvl in levels"
|
|
|
+ :key="lvl">
|
|
|
+ {{ lvl }}级
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 树形区域(从下往上生长:column-reverse,L1 在底部,分支向上延伸) -->
|
|
|
+ <view class="tree-wrapper">
|
|
|
+ <view class="tree-container" v-if="level1Roots.length > 0">
|
|
|
+ <TreeNode
|
|
|
+ v-for="root in level1Roots"
|
|
|
+ :key="root.userId"
|
|
|
+ :node="root"
|
|
|
+ :level="1"
|
|
|
+ :maxLevel="currentMaxLevel"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
+ <view class="empty-state" v-if="!loading && level1Roots.length === 0">
|
|
|
+ <text>暂无团队成员</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="loading-overlay" v-if="loading">
|
|
|
+ <text>加载中...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import TreeNode from '@/components/TreeNode.vue'
|
|
|
+import { getTeamTree, getTeamConsumption } from '@/utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: { TreeNode },
|
|
|
+ data: function() {
|
|
|
+ return {
|
|
|
+ nodes: [],
|
|
|
+ level1Roots: [],
|
|
|
+ consumption: { totalConsumption: 0, totalMembers: 0, maxLevel: 3 },
|
|
|
+ currentMaxLevel: 3,
|
|
|
+ levels: [1, 2, 3, 4, 5],
|
|
|
+ loading: false,
|
|
|
+ _loadFired: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ totalConsumptionYuan: function() {
|
|
|
+ var fen = this.consumption && this.consumption.totalConsumption != null ? this.consumption.totalConsumption : 0
|
|
|
+ return (fen / 100).toFixed(2)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad: function() {
|
|
|
+ this.loadData()
|
|
|
+ },
|
|
|
+ onShow: function() {
|
|
|
+ // 首次 onShow 跳过(onLoad 已加载);切 Tab 返回时刷新
|
|
|
+ if (this._loadFired) {
|
|
|
+ this.loadData()
|
|
|
+ } else {
|
|
|
+ this._loadFired = true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async loadData: function() {
|
|
|
+ this.loading = true
|
|
|
+ try {
|
|
|
+ var treeRes = await getTeamTree(this.currentMaxLevel)
|
|
|
+ var consumptionRes = await getTeamConsumption(this.currentMaxLevel)
|
|
|
+ if (treeRes && treeRes.data && Array.isArray(treeRes.data.nodes)) {
|
|
|
+ this.nodes = treeRes.data.nodes
|
|
|
+ this.buildLevel1Roots()
|
|
|
+ }
|
|
|
+ if (consumptionRes && consumptionRes.data) {
|
|
|
+ var d = consumptionRes.data
|
|
|
+ this.consumption = {
|
|
|
+ totalConsumption: d.totalConsumption || 0,
|
|
|
+ totalMembers: d.totalMembers || 0,
|
|
|
+ maxLevel: d.maxLevel != null ? d.maxLevel : this.currentMaxLevel
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载团队树失败', e)
|
|
|
+ }
|
|
|
+ this.loading = false
|
|
|
+ },
|
|
|
+ switchLevel: function(lvl) {
|
|
|
+ if (this.currentMaxLevel === lvl) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.currentMaxLevel = lvl
|
|
|
+ this.loadData()
|
|
|
+ },
|
|
|
+ buildLevel1Roots: function() {
|
|
|
+ var nodeMap = {}
|
|
|
+ var l1Ids = []
|
|
|
+ this.nodes.forEach(function(n) {
|
|
|
+ nodeMap[n.userId] = Object.assign({ children: [] }, n)
|
|
|
+ if (n.level === 1) {
|
|
|
+ l1Ids.push(n.userId)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ this.nodes.forEach(function(n) {
|
|
|
+ if (!nodeMap[n.userId]) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (n.parentId && nodeMap[n.parentId]) {
|
|
|
+ nodeMap[n.parentId].children.push(nodeMap[n.userId])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ var sortNodes = function(arr) {
|
|
|
+ arr.sort(function(a, b) {
|
|
|
+ return (a.userId || 0) - (b.userId || 0)
|
|
|
+ })
|
|
|
+ arr.forEach(function(c) {
|
|
|
+ sortNodes(c.children)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ sortNodes(
|
|
|
+ l1Ids.map(function(id) {
|
|
|
+ return nodeMap[id]
|
|
|
+ })
|
|
|
+ )
|
|
|
+ this.level1Roots = l1Ids.map(function(id) {
|
|
|
+ return nodeMap[id]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: $dim-wealth-tint;
|
|
|
+ padding-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-card {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 30rpx;
|
|
|
+ margin: 20rpx 30rpx 16rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(249, 115, 22, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item {
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: bold;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value.orange {
|
|
|
+ color: #F97316;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value.blue {
|
|
|
+ color: #0EA5E9;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value.green {
|
|
|
+ color: #10B981;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #94A3B8;
|
|
|
+ margin-top: 4rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-divider {
|
|
|
+ width: 1rpx;
|
|
|
+ height: 48rpx;
|
|
|
+ background: #FED7AA;
|
|
|
+}
|
|
|
+
|
|
|
+.level-tabs {
|
|
|
+ display: flex;
|
|
|
+ padding: 0 30rpx;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+ overflow-x: auto;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.level-tab {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #64748B;
|
|
|
+ background: #F1F5F9;
|
|
|
+}
|
|
|
+
|
|
|
+.level-tab.active {
|
|
|
+ background: #F97316;
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-wrapper {
|
|
|
+ padding: 0 30rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-container {
|
|
|
+ min-height: 300rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column-reverse;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 80rpx;
|
|
|
+ color: #94A3B8;
|
|
|
+ font-size: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(255, 255, 255, 0.85);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 999;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #94A3B8;
|
|
|
+}
|
|
|
+</style>
|