Browse Source

在收入统计和团队统计接口中新增团队ID参数,优化仪表板视图,增强管理员和团队角色的数据展示功能,提升用户体验。

wuyi 3 tháng trước cách đây
mục cha
commit
3fc64c264a
2 tập tin đã thay đổi với 459 bổ sung21 xóa
  1. 9 2
      src/services/api.js
  2. 450 19
      src/views/DashboardView.vue

+ 9 - 2
src/services/api.js

@@ -194,10 +194,11 @@ export const hardDeleteIncome = async (id) => {
 }
 
 // 获取收入统计数据
-export const getIncomeStatistics = async (startDate, endDate) => {
+export const getIncomeStatistics = async (startDate, endDate, teamId) => {
   const params = {}
   if (startDate) params.startDate = startDate
   if (endDate) params.endDate = endDate
+  if (teamId) params.teamId = teamId
 
   const response = await api.get('/income/statistics/summary', { params })
   return response.data
@@ -313,11 +314,17 @@ export const resetAllTodayRevenue = async () => {
 }
 
 // 获取团队统计数据
-export const getTeamStatistics = async () => {
+export const getAllTeamStatistics = async () => {
   const response = await api.get('/teams/statistics/summary')
   return response.data
 }
 
+export const getTeamStatistics = async () => {
+  const response = await api.get('/teams/statistics/team')
+  return response.data
+}
+
+
 // 获取所有团队
 export const getAllTeams = async () => {
   const response = await api.get('/teams/all')

+ 450 - 19
src/views/DashboardView.vue

@@ -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>