first commit

This commit is contained in:
2026-09-16 15:04:08 +08:00
commit 43d095ea0f
117 changed files with 31982 additions and 0 deletions
+590
View File
@@ -0,0 +1,590 @@
<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>