|
|
@@ -1,40 +1,471 @@
|
|
|
-<script setup>
|
|
|
-// 欢迎页面组件
|
|
|
-</script>
|
|
|
-
|
|
|
<template>
|
|
|
- <div class="welcome-container">
|
|
|
- <div class="welcome-content">
|
|
|
- <h1 class="welcome-title">欢迎使用管理系统</h1>
|
|
|
- <p class="welcome-subtitle">欢迎来到用户管理系统</p>
|
|
|
+ <div class="dashboard-container">
|
|
|
+ <!-- 团队统计卡片 -->
|
|
|
+ <div v-if="isAdmin || isTeam" class="stats-cards">
|
|
|
+ <!-- 管理员视图 -->
|
|
|
+ <div v-if="isAdmin" class="admin-stats">
|
|
|
+ <div class="card">
|
|
|
+ <h3>团队总数</h3>
|
|
|
+ <p class="stat-value">{{ teamStats?.totalTeams || 0 }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>总收入</h3>
|
|
|
+ <p class="stat-value">{{ formatAmount(teamStats?.totalRevenue) }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>今日收入</h3>
|
|
|
+ <p class="stat-value">{{ formatAmount(teamStats?.todayRevenue) }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 队长视图 -->
|
|
|
+ <div v-else-if="isTeam" class="team-stats">
|
|
|
+ <div class="card">
|
|
|
+ <h3>团队名称</h3>
|
|
|
+ <p class="stat-value">{{ teamStats?.name || '-' }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>总收入</h3>
|
|
|
+ <p class="stat-value">{{ formatAmount(teamStats?.totalRevenue) }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>今日收入</h3>
|
|
|
+ <p class="stat-value">{{ formatAmount(teamStats?.todayRevenue) }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>佣金率</h3>
|
|
|
+ <p class="stat-value">{{ formatRate(teamStats?.commissionRate) }}%</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 收入图表 -->
|
|
|
+ <div class="chart-container">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h2>最近7天收入统计</h2>
|
|
|
+ <div v-if="isAdmin" class="chart-team-selector">
|
|
|
+ <Select
|
|
|
+ v-model="selectedChartTeamId"
|
|
|
+ :options="teamOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择团队"
|
|
|
+ class="w-full"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-wrapper">
|
|
|
+ <canvas id="incomeChart" height="300"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 团队数据表格 (仅管理员可见) -->
|
|
|
+ <div v-if="isAdmin" class="teams-table-container">
|
|
|
+ <h2>团队数据一览</h2>
|
|
|
+ <DataTable :value="teams" stripedRows class="teams-table">
|
|
|
+ <Column field="id" header="ID" sortable></Column>
|
|
|
+ <Column field="name" header="团队名称" sortable></Column>
|
|
|
+ <Column field="totalRevenue" header="总收入" sortable>
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatAmount(slotProps.data.totalRevenue) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ <Column field="todayRevenue" header="今日收入" sortable>
|
|
|
+ <template #body="slotProps">
|
|
|
+ {{ formatAmount(slotProps.data.todayRevenue) }}
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+ </DataTable>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, computed, inject, watch } from 'vue'
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
+import { useTeamStore } from '@/stores/team'
|
|
|
+import { getAllTeamStatistics, getTeamStatistics, getIncomeStatistics } from '@/services/api'
|
|
|
+import Chart from 'chart.js/auto'
|
|
|
+import Select from 'primevue/select'
|
|
|
+import DataTable from 'primevue/datatable'
|
|
|
+import Column from 'primevue/column'
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+const teamStore = useTeamStore()
|
|
|
+const role = computed(() => userStore.userInfo?.role)
|
|
|
+
|
|
|
+const isAdmin = inject('isAdmin')
|
|
|
+const isTeam = inject('isTeam')
|
|
|
+const isPromoter = inject('isPromoter')
|
|
|
+
|
|
|
+// 数据
|
|
|
+const teamStats = ref(null)
|
|
|
+const selectedTeamId = ref(null)
|
|
|
+const incomeStats = ref(null)
|
|
|
+const loading = ref(true)
|
|
|
+const teams = ref([])
|
|
|
+const chartInstance = ref(null)
|
|
|
+const selectedChartTeamId = ref(null) // 用于图表的团队选择
|
|
|
+
|
|
|
+// 团队选项
|
|
|
+const teamOptions = computed(() => {
|
|
|
+ return [
|
|
|
+ { label: '所有团队', value: null },
|
|
|
+ ...teamStore.teams.map((team) => ({
|
|
|
+ label: team.name,
|
|
|
+ value: team.id
|
|
|
+ }))
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+// 获取选中团队的图表数据
|
|
|
+const selectedTeamChartData = computed(() => {
|
|
|
+ if (!incomeStats.value || !selectedChartTeamId.value) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const teamData = incomeStats.value.teams.find((team) => team.teamId === selectedChartTeamId.value)
|
|
|
+ return teamData || null
|
|
|
+})
|
|
|
+
|
|
|
+// 格式化金额,保留2位小数
|
|
|
+const formatAmount = (amount) => {
|
|
|
+ if (amount === undefined || amount === null) return '0.00'
|
|
|
+ return Number(amount).toFixed(2)
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化比率,保留2位小数
|
|
|
+const formatRate = (rate) => {
|
|
|
+ if (rate === undefined || rate === null) return '0.00'
|
|
|
+ return Number(rate).toFixed(2)
|
|
|
+}
|
|
|
+
|
|
|
+// 获取当前日期和7天前的日期
|
|
|
+const getDateRange = () => {
|
|
|
+ const endDate = new Date()
|
|
|
+ const startDate = new Date()
|
|
|
+ startDate.setDate(startDate.getDate() - 6) // 最近7天(包括今天)
|
|
|
+
|
|
|
+ return {
|
|
|
+ startDate: formatDate(startDate),
|
|
|
+ endDate: formatDate(endDate)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化日期为 YYYY-MM-DD
|
|
|
+const formatDate = (date) => {
|
|
|
+ const year = date.getFullYear()
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
+ const day = String(date.getDate()).padStart(2, '0')
|
|
|
+ return `${year}-${month}-${day}`
|
|
|
+}
|
|
|
+
|
|
|
+// 加载团队统计数据
|
|
|
+const loadTeamStats = async () => {
|
|
|
+ try {
|
|
|
+ loading.value = true
|
|
|
+
|
|
|
+ console.log('当前用户角色:', role.value)
|
|
|
+ console.log('用户信息:', userStore.userInfo)
|
|
|
+ console.log('角色判断:', { isAdmin, isTeam, isPromoter })
|
|
|
+
|
|
|
+ if (isAdmin.value) {
|
|
|
+ console.log('加载管理员团队统计数据')
|
|
|
+ const data = await getAllTeamStatistics()
|
|
|
+ console.log('管理员团队统计数据:', data)
|
|
|
+ teamStats.value = data
|
|
|
+ teams.value = data.allTeams || []
|
|
|
+ if (teams.value.length > 0 && !selectedTeamId.value) {
|
|
|
+ selectedTeamId.value = teams.value[0].id
|
|
|
+ }
|
|
|
+ } else if (isTeam.value) {
|
|
|
+ console.log('加载队长团队统计数据')
|
|
|
+ const data = await getTeamStatistics()
|
|
|
+ console.log('队长团队统计数据:', data)
|
|
|
+ teamStats.value = data
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载团队统计数据失败:', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载收入统计数据
|
|
|
+const loadIncomeStats = async () => {
|
|
|
+ try {
|
|
|
+ loading.value = true
|
|
|
+ const { startDate, endDate } = getDateRange()
|
|
|
+ let params = { startDate, endDate }
|
|
|
+
|
|
|
+ const data = await getIncomeStatistics(startDate, endDate)
|
|
|
+ incomeStats.value = data
|
|
|
+
|
|
|
+ // 渲染图表
|
|
|
+ renderChart()
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载收入统计数据失败:', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 渲染图表
|
|
|
+const renderChart = () => {
|
|
|
+ if (!incomeStats.value) return
|
|
|
+
|
|
|
+ const chartElement = document.getElementById('incomeChart')
|
|
|
+ if (!chartElement) return
|
|
|
+
|
|
|
+ // 如果已有图表实例,先销毁
|
|
|
+ if (chartInstance.value) {
|
|
|
+ chartInstance.value.destroy()
|
|
|
+ }
|
|
|
+
|
|
|
+ const ctx = chartElement.getContext('2d')
|
|
|
+
|
|
|
+ // 准备数据
|
|
|
+ const labels = incomeStats.value.dates
|
|
|
+ let data, tipData, commissionData
|
|
|
+
|
|
|
+ // 根据是否选择了特定团队来决定使用哪组数据
|
|
|
+ if (selectedTeamChartData.value) {
|
|
|
+ // 使用选中团队的数据
|
|
|
+ data = selectedTeamChartData.value.data
|
|
|
+ tipData = selectedTeamChartData.value.tip
|
|
|
+ commissionData = selectedTeamChartData.value.commission
|
|
|
+ } else {
|
|
|
+ // 使用总体数据
|
|
|
+ data = incomeStats.value.total
|
|
|
+ tipData = incomeStats.value.totalTip
|
|
|
+ commissionData = incomeStats.value.totalCommission
|
|
|
+ }
|
|
|
+
|
|
|
+ const datasets = [
|
|
|
+ {
|
|
|
+ label: '总收入',
|
|
|
+ data: data,
|
|
|
+ borderColor: '#2563eb',
|
|
|
+ backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '打赏收入',
|
|
|
+ data: tipData,
|
|
|
+ borderColor: '#10b981',
|
|
|
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '返佣收入',
|
|
|
+ data: commissionData,
|
|
|
+ borderColor: '#f59e0b',
|
|
|
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4
|
|
|
+ }
|
|
|
+ ]
|
|
|
+
|
|
|
+ // 创建图表
|
|
|
+ chartInstance.value = new Chart(ctx, {
|
|
|
+ type: 'line',
|
|
|
+ data: { labels, datasets },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top'
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ y: {
|
|
|
+ beginAtZero: true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 监听团队选择变化
|
|
|
+watch(selectedChartTeamId, () => {
|
|
|
+ renderChart()
|
|
|
+})
|
|
|
+
|
|
|
+// 组件挂载时加载数据
|
|
|
+onMounted(async () => {
|
|
|
+ if (isAdmin.value) {
|
|
|
+ await teamStore.loadTeams()
|
|
|
+ }
|
|
|
+ await loadTeamStats()
|
|
|
+ await loadIncomeStats()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
<style scoped>
|
|
|
-.welcome-container {
|
|
|
+.dashboard-container {
|
|
|
+ padding: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-cards {
|
|
|
display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- min-height: 60vh;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 1rem;
|
|
|
+ margin-bottom: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+.admin-stats {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 1rem;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.admin-stats .card {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 200px;
|
|
|
+ max-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.team-stats {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 1rem;
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
-.welcome-content {
|
|
|
+.team-stats .card {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 200px;
|
|
|
+ max-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.card {
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
text-align: center;
|
|
|
- padding: 2rem;
|
|
|
}
|
|
|
|
|
|
-.welcome-title {
|
|
|
- font-size: 3rem;
|
|
|
+.card h3 {
|
|
|
+ font-size: 1rem;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 0.75rem;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 1.75rem;
|
|
|
font-weight: bold;
|
|
|
color: #2563eb;
|
|
|
+ margin: 0;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+.team-selector {
|
|
|
+ grid-column: 1 / -1;
|
|
|
+ margin-top: 1rem;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.team-selector select {
|
|
|
+ padding: 0.5rem;
|
|
|
+ border-radius: 0.25rem;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-container {
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.chart-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
margin-bottom: 1rem;
|
|
|
}
|
|
|
|
|
|
-.welcome-subtitle {
|
|
|
+.chart-header h2 {
|
|
|
font-size: 1.25rem;
|
|
|
- color: #6b7280;
|
|
|
margin: 0;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-team-selector {
|
|
|
+ width: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+/* PrimeVue Select 组件样式 */
|
|
|
+:deep(.p-dropdown) {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.p-dropdown-label) {
|
|
|
+ padding: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.chart-wrapper {
|
|
|
+ height: 400px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.teams-table-container {
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 0.5rem;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-top: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.teams-table-container h2 {
|
|
|
+ font-size: 1.25rem;
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 1rem;
|
|
|
+ color: #1f2937;
|
|
|
+}
|
|
|
+
|
|
|
+.teams-table {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.p-datatable .p-datatable-thead > tr > th) {
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ color: #4b5563;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: 0.75rem 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.p-datatable .p-datatable-tbody > tr) {
|
|
|
+ transition: background-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.p-datatable .p-datatable-tbody > tr:nth-child(even)) {
|
|
|
+ background-color: #f9fafb;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.p-datatable .p-datatable-tbody > tr:hover) {
|
|
|
+ background-color: #f3f4f6;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.p-datatable .p-datatable-tbody > tr > td) {
|
|
|
+ padding: 0.75rem 1rem;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|