Files
quanxiyun-app/src/pages/b-manager-home/index.vue
T
2026-09-16 15:04:08 +08:00

591 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-container">
<!-- 自定义顶部导航栏 -->
<BTopBar
title="全犀云"
:showBell="true"
@bellClick="openSub('notifications')"
/>
<!-- 滚动区域使用 flex:1 + height:0 实现精准防遮挡布局 -->
<scroll-view
scroll-y
class="page-content"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<!-- Hero Card -->
<view class="hero-card">
<view class="hero-grid-bg"></view>
<view class="hero-row1">
<text class="hero-greeting">{{ greetingText }}{{ userName }}</text>
<view class="hero-level-chip" @click="openSub('b-sub-manager-level')">
<text class="chip-star"></text>
<text>{{ dashboard.level_name }}</text>
<text class="chip-arrow"></text>
</view>
</view>
<text class="hero-label">总收益积分</text>
<view class="hero-amount-row">
<text class="hero-amount">{{ dashboard.total_points ?? 0 }}</text>
<text class="hero-unit">累计已结算</text>
</view>
<view class="hero-rate">
<text class="rate-tag">积分说明</text>
<text class="rate-text">1 = 1积分</text>
</view>
<view class="hero-stats">
<view class="hero-stat">
<text class="hs-label">下属团长</text>
<text class="hs-val">{{ dashboard.leader_count || 0 }}<text class="hs-val-unit"> </text></text>
<text class="hs-sub">商务经理下属</text>
</view>
<view class="hero-stat">
<text class="hs-label">团长直接推荐C端</text>
<text class="hs-val">{{ dashboard.total_c_count || 0 }}<text class="hs-val-unit"> </text></text>
<text class="hs-sub">下属团长直接推荐</text>
</view>
<view class="hero-stat">
<text class="hs-label">本月积分</text>
<text class="hs-val">{{ dashboard.total_points ?? 0 }}</text>
<text class="hs-sub">累计激励金</text>
</view>
</view>
</view>
<!-- Notice Bar -->
<view class="notice-bar" @click="openSub('notifications')">
<view class="nb-icon">
<text class="nb-icon-emoji">📢</text>
</view>
<text class="nb-text">{{ noticeText }}</text>
<text class="nb-more">更多 </text>
</view>
<!-- 数据概览 -->
<view class="section">
<view class="section-header">
<text class="section-title">数据概览</text>
</view>
<view class="monitor-grid">
<view
v-for="(item, idx) in monitorList"
:key="idx"
class="mg-item"
:style="{ background: item.bg }"
>
<text class="mg-label">{{ item.title }}</text>
<text class="mg-val" :style="{ color: item.valColor || '#0F1226' }">
{{ item.value }}<text v-if="item.unit" class="mg-unit">{{ item.unit }}</text>
</text>
<text class="mg-trend" :style="{ color: item.trendColor || '#8B91A1' }">{{ item.trend }}</text>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="section">
<view class="section-header">
<text class="section-title">快捷操作</text>
</view>
<view class="quick-grid">
<view
v-for="(item, idx) in quickActions"
:key="idx"
class="quick-item"
@click="handleAction(item)"
>
<view class="qi-icon" :style="{ background: item.bg }">
<text class="qi-emoji">{{ item.emoji }}</text>
</view>
<text class="qi-label">{{ item.label }}</text>
</view>
</view>
</view>
<!-- 直属团长贡献榜 -->
<view class="section">
<view class="section-header">
<text class="section-title">直属团长贡献榜</text>
<text class="section-more" @click="switchTab('b-manager-team')">全部 </text>
</view>
<view class="card">
<view v-for="(item, idx) in leaderboard" :key="idx" class="lb-item">
<view class="lb-rank" :class="['gold', 'silver', 'bronze'][idx] || ''">{{ item.rank }}</view>
<text class="lb-name">{{ item.name }}</text>
<text class="lb-val">{{ item.value }}</text>
</view>
</view>
</view>
<!-- 最新动态 -->
<NewsFeed ref="newsFeedRef" role="manager" @item-click="handleNewsClick" />
<!-- 底部安全区与 TabBar 占位 -->
<view class="safe-bottom-spacer"></view>
</scroll-view>
<custom-tab-bar :current="0"></custom-tab-bar>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import BTopBar from '@/components/BTopBar.vue'
import CustomTabBar from '@/components/CustomTabBar.vue'
import NewsFeed from '@/components/NewsFeed.vue'
import { openSub, switchTab } from '@/composables/useNavigation'
import request from '@/utils/request'
import { formatThousands } from '@/utils/util'
const refreshing = ref(false)
const noticeText = ref('')
const fetchNotice = async () => {
try {
const res = await request.get('/c/notices', { limit: 1, showLoading: false })
const items = res.data?.items || res.data || []
noticeText.value = items[0]?.title || ''
} catch (e) { /* 忽略 */ }
}
const userName = computed(() => {
const info = uni.getStorageSync('userInfo')
return info?.nickname || '用户'
})
const greetingText = computed(() => {
const h = new Date().getHours()
if (h >= 6 && h < 12) return '早上好'
if (h >= 12 && h < 18) return '下午好'
return '晚上好'
})
const dashboard = ref({
total_points: 0, frozen_points: 0, total_amount: 0,
active_leader_count: 0, device_count: 0,
leader_count: 0, total_c_count: 0, level_name: 'VIP1'
})
const leaderboard = ref([])
const monitorList = ref([
{ title: '总积分', value: '0', trend: '累计激励金', bg: 'linear-gradient(135deg,#E6F4FF,#F0F7FF)', valColor: '#1677FF' },
{ title: '待生效积分', value: '0', trend: 'T+1生效', bg: 'linear-gradient(135deg,#FFF7E6,#FFFBF0)', valColor: '#FF8800' },
{ title: '活跃团长', value: '0', trend: '7日内活跃', bg: 'linear-gradient(135deg,#E6FAF5,#F0FBF8)' },
{ title: '活跃C端', value: '0', trend: '团长直接推荐', bg: 'linear-gradient(135deg,#F3E8FF,#FAF5FF)', valColor: '#722ED1' },
{ title: '设备总数', value: '0', unit: '台', trend: '已支付订单', bg: 'linear-gradient(135deg,#FFF1F0,#FFF5F5)' },
{ title: '总金额', value: '¥0', trend: 'C端消费总额', bg: 'linear-gradient(135deg,#E6F4FF,#F0F7FF)', valColor: '#1677FF' }
])
const quickActions = ref([
{ label: '分享中心', emoji: '❤️', bg: '#FFE4E6', type: 'sub', target: 'b-sub-manager-promo' },
{ label: '我的团队', emoji: '👥', bg: '#E6F4FF', type: 'tab', target: 'b-manager-team' },
{ label: '生成海报', emoji: '🖼️', bg: '#F3E8FF', type: 'sub', target: 'b-sub-manager-poster' },
{ label: '积分明细', emoji: '💰', bg: '#E6FAF5', type: 'tab', target: 'b-manager-revenue' }
])
const handleAction = (item) => {
if (item.type === 'sub') { openSub(item.target) }
else if (item.type === 'tab') { switchTab(item.target) }
}
let fetchingDashboard = false
const fetchDashboard = async () => {
if (fetchingDashboard) return
fetchingDashboard = true
try {
const res = await request.get('/c/manager/dashboard', { showLoading: false })
if (res.data) {
const d = res.data
dashboard.value = d
monitorList.value = [
{ title: '总积分', value: formatThousands(d.total_points || 0), trend: '累计激励金', bg: 'linear-gradient(135deg,#E6F4FF,#F0F7FF)', valColor: '#1677FF' },
{ title: '待生效积分', value: formatThousands(d.frozen_points || 0), trend: 'T+1生效', bg: 'linear-gradient(135deg,#FFF7E6,#FFFBF0)', valColor: '#FF8800' },
{ title: '活跃团长', value: String(d.active_leader_count || 0), trend: '/ ' + (d.leader_count || 0) + ' 下属', bg: 'linear-gradient(135deg,#E6FAF5,#F0FBF8)' },
{ title: '活跃C端', value: String(d.total_c_count || 0), trend: '团长直接推荐', bg: 'linear-gradient(135deg,#F3E8FF,#FAF5FF)', valColor: '#722ED1' },
{ title: '设备总数', value: String(d.device_count || 0), unit: '台', trend: '已支付订单', bg: 'linear-gradient(135deg,#FFF1F0,#FFF5F5)' },
{ title: '总金额', value: '¥' + formatThousands(d.total_amount || 0), trend: 'C端消费总额', bg: 'linear-gradient(135deg,#E6F4FF,#F0F7FF)', valColor: '#1677FF' }
]
leaderboard.value = d.leaderboard || []
}
} catch (e) { console.error('获取经理首页失败', e) }
finally { fetchingDashboard = false }
}
onMounted(() => {
Promise.all([fetchDashboard(), fetchNotice()])
})
const newsFeedRef = ref(null)
const onRefresh = () => {
refreshing.value = true
Promise.all([
fetchDashboard(),
fetchNotice(),
newsFeedRef.value?.fetchNews?.()
]).finally(() => { refreshing.value = false })
}
const handleNewsClick = (item) => { openSub('notice-detail', { id: item.id }) }
</script>
<style lang="scss" scoped>
/* 根节点采用 Flex 布局 */
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background: #F2F4F8;
}
/* 滚动区域填满剩余高度 */
.page-content {
flex: 1;
height: 0;
box-sizing: border-box;
}
.safe-bottom-spacer {
height: calc(env(safe-area-inset-bottom) + 160rpx);
}
/* Hero Card */
.hero-card {
background: linear-gradient(135deg, #0A1733 0%, #0D2B6E 35%, #1342B5 65%, #1B66E8 100%);
padding: 40rpx 32rpx 0;
color: #fff;
position: relative;
overflow: hidden;
border-radius: 0 0 48rpx 48rpx;
&::after {
content: '';
position: absolute;
top: -160rpx;
right: -120rpx;
width: 480rpx;
height: 480rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(64,150,255,.25), transparent 65%);
pointer-events: none;
}
&::before {
content: '';
position: absolute;
bottom: 80rpx;
left: -100rpx;
width: 320rpx;
height: 320rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,196,167,.18), transparent 65%);
pointer-events: none;
}
}
.hero-grid-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px);
background-size: 60rpx 60rpx;
mask-image: linear-gradient(180deg, transparent 0%, #000 30%, #000 70%, transparent 100%);
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 30%, #000 70%, transparent 100%);
pointer-events: none;
}
.hero-row1 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 8rpx;
position: relative;
z-index: 1;
}
.hero-greeting {
font-size: 24rpx;
opacity: .7;
letter-spacing: .6rpx;
}
.hero-level-chip {
font-size: 22rpx;
padding: 10rpx 20rpx;
border-radius: 28rpx;
background: linear-gradient(135deg, rgba(255,215,0,.95), rgba(255,165,0,.95));
color: #0F3460;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 8rpx;
box-shadow: 0 4rpx 16rpx rgba(255,165,0,.45);
white-space: nowrap;
flex-shrink: 0;
}
.chip-star { font-size: 22rpx; }
.chip-arrow { opacity: .7; }
.hero-label {
font-size: 24rpx;
opacity: .55;
margin-bottom: 12rpx;
position: relative;
z-index: 1;
}
.hero-amount-row {
position: relative;
z-index: 1;
}
.hero-amount {
font-size: 76rpx;
font-weight: 800;
letter-spacing: -2rpx;
line-height: 1;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
background: linear-gradient(180deg, #fff, #E6F4FF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-unit {
font-size: 26rpx;
opacity: .55;
font-weight: 400;
margin-left: 12rpx;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
}
.hero-rate {
font-size: 20rpx;
color: rgba(255,255,255,.7);
display: flex;
align-items: center;
gap: 6rpx;
margin-top: 4rpx;
position: relative;
z-index: 1;
}
.rate-tag {
background: rgba(255,255,255,.15);
padding: 2rpx 10rpx;
border-radius: 16rpx;
font-size: 18rpx;
}
.rate-text { opacity: .7; }
.hero-stats {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2rpx;
margin-top: 40rpx;
background: rgba(255,255,255,.1);
border-radius: 28rpx 28rpx 0 0;
overflow: hidden;
position: relative;
z-index: 1;
}
.hero-stat {
background: rgba(255,255,255,.07);
padding: 26rpx 20rpx;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
transition: background .2s;
position: relative;
&:active { background: rgba(255,255,255,.14); }
}
.hs-label { font-size: 22rpx; opacity: .65; display: block; }
.hs-val { font-size: 38rpx; font-weight: 700; font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif; display: block; margin-top: 4rpx; }
.hs-val-unit { font-size: 24rpx; font-weight: 400; }
.hs-sub { font-size: 20rpx; opacity: .55; display: block; margin-top: 2rpx; }
.hs-sub.up { color: #7FFFD4; opacity: 1; }
/* Notice Bar */
.notice-bar {
background: #fff;
margin: 24rpx 24rpx;
padding: 22rpx 28rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
font-size: 24rpx;
box-shadow: 0 2rpx 6rpx rgba(15,18,38,.04);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: linear-gradient(180deg, #FF8800, #FFB84D);
}
}
.nb-icon {
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, #FFF7E6, #FFE7BA);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(255,136,0,.15);
}
.nb-icon-emoji { font-size: 24rpx; }
.nb-text { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #5A6072; }
.nb-more { color: #BCC2CE; font-size: 22rpx; flex-shrink: 0; }
/* Section */
.section { padding: 0 24rpx; margin-top: 24rpx; }
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #0F1226;
display: flex;
align-items: center;
gap: 12rpx;
&::before {
content: '';
width: 6rpx;
height: 28rpx;
background: linear-gradient(180deg, #1677FF, #4096FF);
border-radius: 4rpx;
}
}
.section-more {
font-size: 24rpx;
color: #8B91A1;
display: flex;
align-items: center;
gap: 4rpx;
}
/* Monitor Grid */
.monitor-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16rpx;
}
.mg-item {
border-radius: 28rpx;
padding: 28rpx 16rpx;
text-align: center;
box-shadow: 0 2rpx 6rpx rgba(15,18,38,.04);
}
.mg-label { font-size: 20rpx; color: #8B91A1; display: block; }
.mg-val { font-size: 44rpx; font-weight: 700; font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif; display: block; margin: 8rpx 0; }
.mg-unit { font-size: 24rpx; font-weight: 400; color: #8B91A1; margin-left: 2rpx; }
.mg-trend { font-size: 18rpx; color: #8B91A1; display: block; }
/* Quick Grid */
.quick-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.quick-item {
background: #fff;
border-radius: 28rpx;
padding: 28rpx 8rpx 20rpx;
text-align: center;
box-shadow: 0 2rpx 6rpx rgba(15,18,38,.04);
transition: all .2s;
&:active { transform: scale(.95); box-shadow: 0 4rpx 16rpx rgba(15,18,38,.06); }
}
.qi-icon {
width: 84rpx;
height: 84rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 14rpx;
}
.qi-emoji { font-size: 40rpx; line-height: 1; }
.qi-label { font-size: 22rpx; color: #0F1226; font-weight: 500; }
/* Card */
.card {
background: #fff;
border-radius: 28rpx;
padding: 0 24rpx;
box-shadow: 0 2rpx 6rpx rgba(15,18,38,.04);
}
/* Leaderboard */
.lb-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 22rpx 0;
&+& { border-top: 1rpx solid #F3F5F9; }
}
.lb-rank {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #F3F5F9;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 24rpx;
flex-shrink: 0;
color: #5A6072;
font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif;
&.gold { background: linear-gradient(135deg, #FFD700, #FFA500); color: #fff; box-shadow: 0 4rpx 16rpx rgba(255,165,0,.4); }
&.silver { background: linear-gradient(135deg, #D8DCE5, #A8AEC0); color: #fff; box-shadow: 0 4rpx 16rpx rgba(168,174,192,.4); }
&.bronze { background: linear-gradient(135deg, #CD7F32, #A0522D); color: #fff; box-shadow: 0 4rpx 16rpx rgba(160,82,45,.4); }
}
.lb-name { flex: 1; font-size: 26rpx; font-weight: 500; color: #0F1226; }
.lb-val { font-weight: 700; color: #1677FF; font-size: 28rpx; font-family: 'DIN Alternate', 'Helvetica Neue', sans-serif; }
</style>