|
|
@@ -18,6 +18,13 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <view class="level-counts" v-if="!loading && levelCounts.length > 0">
|
|
|
+ <view class="level-count-chip" v-for="lc in levelCounts" :key="lc.level">
|
|
|
+ <text class="level-count-num">L{{ lc.level }}</text>
|
|
|
+ <text class="level-count-people">{{ lc.count }}人</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 层级切换 tabs -->
|
|
|
<view class="level-tabs">
|
|
|
<view class="level-tab" :class="{ active: currentMaxLevel === 1 }" @click="switchLevel(1)">1级</view>
|
|
|
@@ -27,18 +34,25 @@
|
|
|
<view class="level-tab" :class="{ active: currentMaxLevel === 5 }" @click="switchLevel(5)">5级</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 树形区域(从下往上生长:column-reverse,L1 在底部) -->
|
|
|
+ <!-- 树形区域:扁平渲染,depth 控制缩进 -->
|
|
|
<view class="tree-wrapper">
|
|
|
- <view class="tree-container">
|
|
|
- <TreeNode
|
|
|
- v-for="root in level1Roots"
|
|
|
- :key="root.userId"
|
|
|
- :node="root"
|
|
|
- :level="1"
|
|
|
- :maxLevel="currentMaxLevel"
|
|
|
- />
|
|
|
+ <view class="tree-container" v-if="displayNodes.length > 0">
|
|
|
+ <view v-for="item in displayNodes" :key="item.userId" class="tree-node"
|
|
|
+ :style="{ paddingLeft: (item.depth * 32) + 'rpx' }">
|
|
|
+ <view class="node-main" @click="item.hasChildren && toggleExpand(item.userId)">
|
|
|
+ <image v-if="item.avatar" class="node-avatar" :src="item.avatar" mode="aspectFill" />
|
|
|
+ <text v-else class="node-avatar-text">{{ item.nickname ? item.nickname.charAt(0) : '?' }}</text>
|
|
|
+ <text class="node-name">{{ item.nickname }}</text>
|
|
|
+ <text class="node-tag" v-if="item.level > 1">L{{ item.level }}</text>
|
|
|
+ <view class="node-stats">
|
|
|
+ <text class="node-stat consumption">消费 ¥{{ fenToYuan(item.memberConsumption) }}</text>
|
|
|
+ <text class="node-stat my-cf">得CF ¥{{ fenToYuan(item.myCfEarnedFromMember) }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="node-arrow" v-if="item.hasChildren">{{ expandedMap[item.userId] ? '▲' : '▼' }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="empty-state" v-if="!loading && level1Roots.length === 0">
|
|
|
+ <view class="empty-state" v-if="!loading && displayNodes.length === 0">
|
|
|
<text>暂无团队成员</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
@@ -53,51 +67,57 @@
|
|
|
import { getTeamTree, getTeamConsumption } from '@/utils/api.js'
|
|
|
|
|
|
export default {
|
|
|
- components: {
|
|
|
- TreeNode: {
|
|
|
- name: 'TreeNode',
|
|
|
- props: { node: Object, level: Number, maxLevel: Number },
|
|
|
- data() {
|
|
|
- return { expanded: false }
|
|
|
- },
|
|
|
- methods: {
|
|
|
- toggle() {
|
|
|
- this.expanded = !this.expanded
|
|
|
- }
|
|
|
- },
|
|
|
- template: `
|
|
|
- <view class="tree-node" :style="{ paddingLeft: (level > 1 ? (level - 1) * 24 : 0) + 'rpx' }">
|
|
|
- <view class="node-main" @click="toggle">
|
|
|
- <image v-if="node.avatar" class="node-avatar" :src="node.avatar" mode="aspectFill" />
|
|
|
- <text v-else class="node-avatar-text">{{ node.nickname ? node.nickname.charAt(0) : '?' }}</text>
|
|
|
- <text class="node-name">{{ node.nickname }}</text>
|
|
|
- <text class="node-tag" v-if="level > 1">L{{ level }}</text>
|
|
|
- <view class="node-stats">
|
|
|
- <text class="node-stat consumption">消费 ¥{{ (node.memberConsumption / 100).toFixed(2) }}</text>
|
|
|
- <text class="node-stat my-cf">得CF ¥{{ (node.myCfEarnedFromMember / 100).toFixed(2) }}</text>
|
|
|
- </view>
|
|
|
- <text class="node-arrow">{{ expanded ? '▲' : '▼' }}</text>
|
|
|
- </view>
|
|
|
- <view v-if="expanded && node.children && node.children.length > 0" class="children">
|
|
|
- <TreeNode
|
|
|
- v-for="child in node.children"
|
|
|
- :key="child.userId"
|
|
|
- :node="child"
|
|
|
- :level="level + 1"
|
|
|
- :maxLevel="maxLevel"
|
|
|
- />
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- `
|
|
|
- }
|
|
|
- },
|
|
|
data() {
|
|
|
return {
|
|
|
nodes: [],
|
|
|
- level1Roots: [],
|
|
|
+ levelCounts: [],
|
|
|
consumption: { totalConsumption: 0, totalMembers: 0, maxLevel: 3 },
|
|
|
currentMaxLevel: 3,
|
|
|
- loading: false
|
|
|
+ loading: false,
|
|
|
+ expandedMap: {}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ displayNodes() {
|
|
|
+ var self = this
|
|
|
+ if (this.nodes.length === 0) return []
|
|
|
+ var nodeMap = {}
|
|
|
+ var i, n, roots = [], result = []
|
|
|
+ for (i = 0; i < this.nodes.length; i++) {
|
|
|
+ n = this.nodes[i]
|
|
|
+ nodeMap[n.userId] = {
|
|
|
+ userId: n.userId, nickname: n.nickname, avatar: n.avatar,
|
|
|
+ memberConsumption: n.memberConsumption, myCfEarnedFromMember: n.myCfEarnedFromMember,
|
|
|
+ level: n.level, _children: []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (i = 0; i < this.nodes.length; i++) {
|
|
|
+ n = this.nodes[i]
|
|
|
+ if (n.parentId && nodeMap[n.parentId] && nodeMap[n.userId]) {
|
|
|
+ nodeMap[n.parentId]._children.push(nodeMap[n.userId])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (i = 0; i < this.nodes.length; i++) {
|
|
|
+ if (this.nodes[i].level === 1 && nodeMap[this.nodes[i].userId]) {
|
|
|
+ roots.push(nodeMap[this.nodes[i].userId])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ function flatten(list, depth) {
|
|
|
+ for (var j = 0; j < list.length; j++) {
|
|
|
+ var node = list[j]
|
|
|
+ var hasChildren = node._children.length > 0
|
|
|
+ result.push({
|
|
|
+ userId: node.userId, nickname: node.nickname, avatar: node.avatar,
|
|
|
+ memberConsumption: node.memberConsumption, myCfEarnedFromMember: node.myCfEarnedFromMember,
|
|
|
+ level: node.level, depth: depth, hasChildren: hasChildren, childCount: node._children.length
|
|
|
+ })
|
|
|
+ if (hasChildren && self.expandedMap[node.userId]) {
|
|
|
+ flatten(node._children, depth + 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ flatten(roots, 0)
|
|
|
+ return result
|
|
|
}
|
|
|
},
|
|
|
onLoad() {
|
|
|
@@ -120,45 +140,36 @@ export default {
|
|
|
var consumptionRes = await getTeamConsumption(self.currentMaxLevel)
|
|
|
if (treeRes && treeRes.data && treeRes.data.nodes) {
|
|
|
self.nodes = treeRes.data.nodes
|
|
|
- self.buildLevel1Roots()
|
|
|
}
|
|
|
if (consumptionRes && consumptionRes.data) {
|
|
|
self.consumption = consumptionRes.data
|
|
|
self.consumption.maxLevel = self.consumption.maxLevel || self.currentMaxLevel
|
|
|
}
|
|
|
+ self.buildLevelCounts()
|
|
|
} catch (e) {
|
|
|
console.error('加载团队树失败', e)
|
|
|
}
|
|
|
this.loading = false
|
|
|
},
|
|
|
+ buildLevelCounts() {
|
|
|
+ var max = this.consumption.maxLevel || this.currentMaxLevel || 3
|
|
|
+ var counts = []
|
|
|
+ var i
|
|
|
+ for (i = 1; i <= max; i++) counts.push({ level: i, count: 0 })
|
|
|
+ this.nodes.forEach(function(n) {
|
|
|
+ if (n && n.level && n.level >= 1 && n.level <= max) {
|
|
|
+ counts[n.level - 1].count++
|
|
|
+ }
|
|
|
+ })
|
|
|
+ this.levelCounts = counts
|
|
|
+ },
|
|
|
switchLevel(level) {
|
|
|
if (this.currentMaxLevel === level) return
|
|
|
this.currentMaxLevel = level
|
|
|
this.loadData()
|
|
|
},
|
|
|
- buildLevel1Roots() {
|
|
|
- var self = this
|
|
|
- // 将扁平 nodes 构建成以 L1 为根的子树
|
|
|
- var nodeMap = {}
|
|
|
- var l1Ids = []
|
|
|
- self.nodes.forEach(function(n) {
|
|
|
- nodeMap[n.userId] = { userId: n.userId, children: [] }
|
|
|
- if (n.level === 1) l1Ids.push(n.userId)
|
|
|
- })
|
|
|
- self.nodes.forEach(function(n) {
|
|
|
- if (!nodeMap[n.userId]) return
|
|
|
- nodeMap[n.userId] = Object.assign({}, nodeMap[n.userId], n)
|
|
|
- if (n.parentId && nodeMap[n.parentId]) {
|
|
|
- nodeMap[n.parentId].children.push(nodeMap[n.userId])
|
|
|
- }
|
|
|
- })
|
|
|
- // 按 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] }))
|
|
|
- self.level1Roots = l1Ids.map(function(id) { return nodeMap[id] })
|
|
|
+ toggleExpand(userId) {
|
|
|
+ this.$set(this.expandedMap, userId, !this.expandedMap[userId])
|
|
|
},
|
|
|
fenToYuan(fen) {
|
|
|
if (fen === null || fen === undefined) return '0.00'
|
|
|
@@ -213,11 +224,21 @@ export default {
|
|
|
.node-stat.consumption { color: #F97316; }
|
|
|
.node-stat.my-cf { color: #0EA5E9; }
|
|
|
.node-arrow { font-size: 20rpx; color: #94A3B8; flex-shrink: 0; }
|
|
|
-.children { margin-top: 12rpx; padding-left: 32rpx; border-left: 1rpx dashed #FED7AA; }
|
|
|
.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;
|
|
|
}
|
|
|
+.level-counts {
|
|
|
+ display: flex; padding: 0 30rpx; margin-bottom: 16rpx; gap: 12rpx;
|
|
|
+ flex-wrap: wrap; align-items: center;
|
|
|
+}
|
|
|
+.level-count-chip {
|
|
|
+ flex-shrink: 0; display: flex; align-items: center; gap: 6rpx;
|
|
|
+ background: #fff; border-radius: 16rpx; padding: 10rpx 20rpx;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
|
|
+}
|
|
|
+.level-count-num { font-size: 24rpx; font-weight: 600; color: #F97316; }
|
|
|
+.level-count-people { font-size: 24rpx; color: #64748B; }
|
|
|
</style>
|