DashboardView.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. <template>
  2. <div class="dashboard-container">
  3. <!-- 团队统计卡片 -->
  4. <div v-if="isAdmin || isTeam" class="stats-cards">
  5. <!-- 管理员视图 -->
  6. <div v-if="isAdmin" class="admin-stats">
  7. <div class="card">
  8. <h3>团队总数</h3>
  9. <p class="stat-value">{{ teamStats?.totalTeams || 0 }}</p>
  10. </div>
  11. <div class="card">
  12. <h3>总收入</h3>
  13. <p class="stat-value">{{ formatAmount(teamStats?.totalRevenue) }}</p>
  14. </div>
  15. <div class="card">
  16. <h3>今日收入</h3>
  17. <p class="stat-value">{{ formatAmount(teamStats?.todayRevenue) }}</p>
  18. </div>
  19. </div>
  20. <!-- 队长视图 -->
  21. <div v-else-if="isTeam" class="team-stats">
  22. <div class="card">
  23. <h3>团队名称</h3>
  24. <p class="stat-value">{{ teamStats?.name || '-' }}</p>
  25. </div>
  26. <div class="card">
  27. <h3>总收入</h3>
  28. <p class="stat-value">{{ formatAmount(teamStats?.totalRevenue) }}</p>
  29. </div>
  30. <div class="card">
  31. <h3>今日收入</h3>
  32. <p class="stat-value">{{ formatAmount(teamStats?.todayRevenue) }}</p>
  33. </div>
  34. <div class="card">
  35. <h3>佣金率</h3>
  36. <p class="stat-value">{{ formatRate(teamStats?.commissionRate) }}%</p>
  37. </div>
  38. </div>
  39. </div>
  40. <!-- 收入图表 -->
  41. <div class="chart-container">
  42. <div class="chart-header">
  43. <h2>最近7天收入统计</h2>
  44. <div v-if="isAdmin" class="chart-team-selector">
  45. <Select
  46. v-model="selectedChartTeamId"
  47. :options="teamOptions"
  48. optionLabel="label"
  49. optionValue="value"
  50. placeholder="选择团队"
  51. class="w-full"
  52. />
  53. </div>
  54. </div>
  55. <div class="chart-wrapper">
  56. <canvas id="incomeChart" height="300"></canvas>
  57. </div>
  58. </div>
  59. <!-- 团队数据表格 (仅管理员可见) -->
  60. <div v-if="isAdmin" class="teams-table-container">
  61. <h2>团队数据一览</h2>
  62. <DataTable :value="teams" stripedRows class="teams-table">
  63. <Column field="id" header="ID" sortable></Column>
  64. <Column field="name" header="团队名称" sortable></Column>
  65. <Column field="totalRevenue" header="总收入" sortable>
  66. <template #body="slotProps">
  67. {{ formatAmount(slotProps.data.totalRevenue) }}
  68. </template>
  69. </Column>
  70. <Column field="todayRevenue" header="今日收入" sortable>
  71. <template #body="slotProps">
  72. {{ formatAmount(slotProps.data.todayRevenue) }}
  73. </template>
  74. </Column>
  75. </DataTable>
  76. </div>
  77. </div>
  78. </template>
  79. <script setup>
  80. import { ref, onMounted, computed, inject, watch } from 'vue'
  81. import { useUserStore } from '@/stores/user'
  82. import { useTeamStore } from '@/stores/team'
  83. import { getAllTeamStatistics, getTeamStatistics, getIncomeStatistics } from '@/services/api'
  84. import Chart from 'chart.js/auto'
  85. import Select from 'primevue/select'
  86. import DataTable from 'primevue/datatable'
  87. import Column from 'primevue/column'
  88. const userStore = useUserStore()
  89. const teamStore = useTeamStore()
  90. const role = computed(() => userStore.userInfo?.role)
  91. const isAdmin = inject('isAdmin')
  92. const isTeam = inject('isTeam')
  93. const isPromoter = inject('isPromoter')
  94. // 数据
  95. const teamStats = ref(null)
  96. const selectedTeamId = ref(null)
  97. const incomeStats = ref(null)
  98. const loading = ref(true)
  99. const teams = ref([])
  100. const chartInstance = ref(null)
  101. const selectedChartTeamId = ref(null) // 用于图表的团队选择
  102. // 团队选项
  103. const teamOptions = computed(() => {
  104. return [
  105. { label: '所有团队', value: null },
  106. ...teamStore.teams.map((team) => ({
  107. label: team.name,
  108. value: team.id
  109. }))
  110. ]
  111. })
  112. // 获取选中团队的图表数据
  113. const selectedTeamChartData = computed(() => {
  114. if (!incomeStats.value || !selectedChartTeamId.value) {
  115. return null
  116. }
  117. const teamData = incomeStats.value.teams.find((team) => team.teamId === selectedChartTeamId.value)
  118. return teamData || null
  119. })
  120. // 格式化金额,保留2位小数
  121. const formatAmount = (amount) => {
  122. if (amount === undefined || amount === null) return '0.00'
  123. return Number(amount).toFixed(2)
  124. }
  125. // 格式化比率,保留2位小数
  126. const formatRate = (rate) => {
  127. if (rate === undefined || rate === null) return '0.00'
  128. return Number(rate).toFixed(2)
  129. }
  130. // 获取当前日期和7天前的日期
  131. const getDateRange = () => {
  132. const endDate = new Date()
  133. const startDate = new Date()
  134. startDate.setDate(startDate.getDate() - 6) // 最近7天(包括今天)
  135. return {
  136. startDate: formatDate(startDate),
  137. endDate: formatDate(endDate)
  138. }
  139. }
  140. // 格式化日期为 YYYY-MM-DD
  141. const formatDate = (date) => {
  142. const year = date.getFullYear()
  143. const month = String(date.getMonth() + 1).padStart(2, '0')
  144. const day = String(date.getDate()).padStart(2, '0')
  145. return `${year}-${month}-${day}`
  146. }
  147. // 加载团队统计数据
  148. const loadTeamStats = async () => {
  149. try {
  150. loading.value = true
  151. console.log('当前用户角色:', role.value)
  152. console.log('用户信息:', userStore.userInfo)
  153. console.log('角色判断:', { isAdmin, isTeam, isPromoter })
  154. if (isAdmin.value) {
  155. console.log('加载管理员团队统计数据')
  156. const data = await getAllTeamStatistics()
  157. console.log('管理员团队统计数据:', data)
  158. teamStats.value = data
  159. teams.value = data.allTeams || []
  160. if (teams.value.length > 0 && !selectedTeamId.value) {
  161. selectedTeamId.value = teams.value[0].id
  162. }
  163. } else if (isTeam.value) {
  164. console.log('加载队长团队统计数据')
  165. const data = await getTeamStatistics()
  166. console.log('队长团队统计数据:', data)
  167. teamStats.value = data
  168. }
  169. } catch (error) {
  170. console.error('加载团队统计数据失败:', error)
  171. } finally {
  172. loading.value = false
  173. }
  174. }
  175. // 加载收入统计数据
  176. const loadIncomeStats = async () => {
  177. try {
  178. loading.value = true
  179. const { startDate, endDate } = getDateRange()
  180. let params = { startDate, endDate }
  181. const data = await getIncomeStatistics(startDate, endDate)
  182. incomeStats.value = data
  183. // 渲染图表
  184. renderChart()
  185. } catch (error) {
  186. console.error('加载收入统计数据失败:', error)
  187. } finally {
  188. loading.value = false
  189. }
  190. }
  191. // 渲染图表
  192. const renderChart = () => {
  193. if (!incomeStats.value) return
  194. const chartElement = document.getElementById('incomeChart')
  195. if (!chartElement) return
  196. // 如果已有图表实例,先销毁
  197. if (chartInstance.value) {
  198. chartInstance.value.destroy()
  199. }
  200. const ctx = chartElement.getContext('2d')
  201. // 准备数据
  202. const labels = incomeStats.value.dates
  203. let data, tipData, commissionData
  204. // 根据是否选择了特定团队来决定使用哪组数据
  205. if (selectedTeamChartData.value) {
  206. // 使用选中团队的数据
  207. data = selectedTeamChartData.value.data
  208. tipData = selectedTeamChartData.value.tip
  209. commissionData = selectedTeamChartData.value.commission
  210. } else {
  211. // 使用总体数据
  212. data = incomeStats.value.total
  213. tipData = incomeStats.value.totalTip
  214. commissionData = incomeStats.value.totalCommission
  215. }
  216. const datasets = [
  217. {
  218. label: '总收入',
  219. data: data,
  220. borderColor: '#2563eb',
  221. backgroundColor: 'rgba(37, 99, 235, 0.1)',
  222. fill: true,
  223. tension: 0.4
  224. },
  225. {
  226. label: '打赏收入',
  227. data: tipData,
  228. borderColor: '#10b981',
  229. backgroundColor: 'rgba(16, 185, 129, 0.1)',
  230. fill: true,
  231. tension: 0.4
  232. },
  233. {
  234. label: '返佣收入',
  235. data: commissionData,
  236. borderColor: '#f59e0b',
  237. backgroundColor: 'rgba(245, 158, 11, 0.1)',
  238. fill: true,
  239. tension: 0.4
  240. }
  241. ]
  242. // 创建图表
  243. chartInstance.value = new Chart(ctx, {
  244. type: 'line',
  245. data: { labels, datasets },
  246. options: {
  247. responsive: true,
  248. maintainAspectRatio: false,
  249. plugins: {
  250. legend: {
  251. position: 'top'
  252. },
  253. tooltip: {
  254. mode: 'index',
  255. intersect: false
  256. }
  257. },
  258. scales: {
  259. y: {
  260. beginAtZero: true
  261. }
  262. }
  263. }
  264. })
  265. }
  266. // 监听团队选择变化
  267. watch(selectedChartTeamId, () => {
  268. renderChart()
  269. })
  270. // 组件挂载时加载数据
  271. onMounted(async () => {
  272. if (isAdmin.value) {
  273. await teamStore.loadTeams()
  274. }
  275. await loadTeamStats()
  276. await loadIncomeStats()
  277. })
  278. </script>
  279. <style scoped>
  280. .dashboard-container {
  281. padding: 1.5rem;
  282. }
  283. .stats-cards {
  284. display: flex;
  285. flex-wrap: wrap;
  286. gap: 1rem;
  287. margin-bottom: 2rem;
  288. }
  289. .admin-stats {
  290. display: flex;
  291. flex-wrap: wrap;
  292. gap: 1rem;
  293. width: 100%;
  294. }
  295. .admin-stats .card {
  296. flex: 1;
  297. min-width: 200px;
  298. max-width: 300px;
  299. }
  300. .team-stats {
  301. display: flex;
  302. flex-wrap: wrap;
  303. gap: 1rem;
  304. width: 100%;
  305. }
  306. .team-stats .card {
  307. flex: 1;
  308. min-width: 200px;
  309. max-width: 300px;
  310. }
  311. .card {
  312. background-color: white;
  313. border-radius: 0.5rem;
  314. padding: 1.5rem;
  315. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  316. display: flex;
  317. flex-direction: column;
  318. justify-content: center;
  319. align-items: center;
  320. text-align: center;
  321. }
  322. .card h3 {
  323. font-size: 1rem;
  324. color: #6b7280;
  325. margin-top: 0;
  326. margin-bottom: 0.75rem;
  327. white-space: nowrap;
  328. }
  329. .stat-value {
  330. font-size: 1.75rem;
  331. font-weight: bold;
  332. color: #2563eb;
  333. margin: 0;
  334. line-height: 1.2;
  335. }
  336. .team-selector {
  337. grid-column: 1 / -1;
  338. margin-top: 1rem;
  339. display: flex;
  340. align-items: center;
  341. gap: 0.5rem;
  342. }
  343. .team-selector select {
  344. padding: 0.5rem;
  345. border-radius: 0.25rem;
  346. border: 1px solid #d1d5db;
  347. }
  348. .chart-container {
  349. background-color: white;
  350. border-radius: 0.5rem;
  351. padding: 1.5rem;
  352. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  353. }
  354. .chart-header {
  355. display: flex;
  356. justify-content: space-between;
  357. align-items: center;
  358. margin-bottom: 1rem;
  359. }
  360. .chart-header h2 {
  361. font-size: 1.25rem;
  362. margin: 0;
  363. color: #1f2937;
  364. }
  365. .chart-team-selector {
  366. width: 200px;
  367. }
  368. /* PrimeVue Select 组件样式 */
  369. :deep(.p-dropdown) {
  370. width: 100%;
  371. }
  372. :deep(.p-dropdown-label) {
  373. padding: 0.5rem;
  374. }
  375. .chart-wrapper {
  376. height: 400px;
  377. position: relative;
  378. }
  379. .teams-table-container {
  380. background-color: white;
  381. border-radius: 0.5rem;
  382. padding: 1.5rem;
  383. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  384. margin-top: 1.5rem;
  385. }
  386. .teams-table-container h2 {
  387. font-size: 1.25rem;
  388. margin-top: 0;
  389. margin-bottom: 1rem;
  390. color: #1f2937;
  391. }
  392. .teams-table {
  393. width: 100%;
  394. }
  395. :deep(.p-datatable .p-datatable-thead > tr > th) {
  396. background-color: #f3f4f6;
  397. color: #4b5563;
  398. font-weight: 600;
  399. padding: 0.75rem 1rem;
  400. }
  401. :deep(.p-datatable .p-datatable-tbody > tr) {
  402. transition: background-color 0.2s;
  403. }
  404. :deep(.p-datatable .p-datatable-tbody > tr:nth-child(even)) {
  405. background-color: #f9fafb;
  406. }
  407. :deep(.p-datatable .p-datatable-tbody > tr:hover) {
  408. background-color: #f3f4f6;
  409. }
  410. :deep(.p-datatable .p-datatable-tbody > tr > td) {
  411. padding: 0.75rem 1rem;
  412. border-bottom: 1px solid #e5e7eb;
  413. }
  414. </style>