deepchart后台管理系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

583 lines
18 KiB

  1. <template>
  2. <div class="user-overview-container">
  3. <div class="tab-header">
  4. <div
  5. class="tab-item"
  6. :class="{ active: activeTab === 'overview' }"
  7. @click="activeTab = 'overview'"
  8. >
  9. 数据概览
  10. </div>
  11. <div
  12. class="tab-item"
  13. :class="{ active: activeTab === 'detail' }"
  14. @click="activeTab = 'detail'"
  15. >
  16. 数据明细
  17. </div>
  18. </div>
  19. <!-- 数据概览 -->
  20. <div v-show="activeTab === 'overview'" class="tab-content overview-content">
  21. <div class="stats-row">
  22. <!-- 用户总数 -->
  23. <div class="stat-card purple-gradient big-card">
  24. <div class="card-title">
  25. <el-icon><UserFilled /></el-icon>
  26. </div>
  27. <div class="card-value">154,838</div>
  28. <div class="card-tag up">
  29. 较昨日增加 5.22%
  30. </div>
  31. </div>
  32. <div class="right-stats-col">
  33. <!-- 会员总数 -->
  34. <div class="stat-card orange-gradient small-card">
  35. <div class="top-row">
  36. <div class="card-title">
  37. <el-icon><Trophy /></el-icon>
  38. </div>
  39. <div class="card-value-small">154,838</div>
  40. </div>
  41. <div class="card-tag-wrapper">
  42. <div class="card-tag up">较昨日增加 15.22%</div>
  43. </div>
  44. </div>
  45. <!-- 非会员总数 -->
  46. <div class="stat-card blue-gradient small-card">
  47. <div class="top-row">
  48. <div class="card-title">
  49. <el-icon><User /></el-icon>
  50. </div>
  51. <div class="card-value-small">154,838</div>
  52. </div>
  53. <div class="card-tag-wrapper">
  54. <div class="card-tag down">较昨日减少 1.22%</div>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. <!-- 用户构成比例 -->
  60. <div class="composition-section">
  61. <div class="section-header">
  62. <el-icon><PieChart /></el-icon>
  63. </div>
  64. <div class="charts-row">
  65. <div class="chart-wrapper">
  66. <div ref="chartMemberRef" class="chart-box"></div>
  67. <div class="legend-custom">
  68. <div class="legend-item"><span class="dot green"></span>会员用户</div>
  69. <div class="legend-item"><span class="dot red"></span>非会员用户</div>
  70. </div>
  71. </div>
  72. <div class="chart-wrapper">
  73. <div ref="chartNewOldRef" class="chart-box"></div>
  74. <div class="legend-custom">
  75. <div class="legend-item"><span class="dot green"></span>会员用户</div>
  76. <div class="legend-item"><span class="dot red"></span>新非网数量</div>
  77. <div class="legend-item"><span class="dot blue"></span>老非网数量</div>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. <!-- 数据明细 -->
  84. <div v-show="activeTab === 'detail'" class="tab-content detail-content">
  85. <!-- 搜索栏 -->
  86. <div class="search-bar">
  87. <div class="search-label">时间段查询</div>
  88. <el-date-picker
  89. v-model="dateRange"
  90. type="daterange"
  91. range-separator="至"
  92. start-placeholder="开始时间"
  93. end-placeholder="结束时间"
  94. size="default"
  95. />
  96. <el-button type="primary" class="search-btn">搜索</el-button>
  97. <el-button type="primary" class="reset-btn">重置</el-button>
  98. <el-button type="danger" class="export-btn">数据导出</el-button>
  99. </div>
  100. <!-- 表格1: 用户构成明细 -->
  101. <div class="detail-section">
  102. <div class="section-title"><el-icon><User /></el-icon> </div>
  103. <el-table :data="tableData1" style="width: 100%" :header-cell-style="headerCellStyle">
  104. <el-table-column prop="type" label="用户类型" width="180">
  105. <template #default="scope">
  106. <span :class="{
  107. 'text-red': scope.row.type === '用户总数',
  108. 'sub-item-text': ['新非网总数', '老非网总数'].includes(scope.row.type)
  109. }">{{ scope.row.type }}</span>
  110. </template>
  111. </el-table-column>
  112. <el-table-column prop="total" label="当前总数" />
  113. <el-table-column prop="dailyNew" label="较昨日新增">
  114. <template #default="scope">
  115. <span class="text-green">{{ scope.row.dailyNew }}</span>
  116. </template>
  117. </el-table-column>
  118. <el-table-column prop="weeklyNew" label="较上周新增">
  119. <template #default="scope">
  120. <span class="text-green">{{ scope.row.weeklyNew }}</span>
  121. </template>
  122. </el-table-column>
  123. <el-table-column prop="monthlyNew" label="较上月新增">
  124. <template #default="scope">
  125. <span class="text-green">{{ scope.row.monthlyNew }}</span>
  126. </template>
  127. </el-table-column>
  128. <el-table-column prop="periodNew" label="时间段新增" />
  129. </el-table>
  130. </div>
  131. <!-- 表格2: 新注册用户来源 -->
  132. <div class="detail-section">
  133. <div class="section-title"><el-icon><User /></el-icon> </div>
  134. <el-table :data="tableData2" style="width: 100%" :header-cell-style="headerCellStyle">
  135. <el-table-column prop="channel" label="来源渠道" />
  136. <el-table-column prop="dailyNew" label="今日新增" />
  137. <el-table-column prop="weeklyNew" label="本周新增" />
  138. <el-table-column prop="monthlyNew" label="本月新增" />
  139. <el-table-column prop="periodNew" label="时间段新增" />
  140. <el-table-column prop="percent" label="占比" />
  141. </el-table>
  142. </div>
  143. <!-- 表格3: 老用户来源 -->
  144. <div class="detail-section">
  145. <div class="section-title"><el-icon><User /></el-icon> </div>
  146. <el-table :data="tableData3" style="width: 100%" :header-cell-style="headerCellStyle">
  147. <el-table-column prop="channel" label="来源渠道" />
  148. <el-table-column prop="dailyNew" label="今日新增" />
  149. <el-table-column prop="weeklyNew" label="本周新增" />
  150. <el-table-column prop="monthlyNew" label="本月新增" />
  151. <el-table-column prop="periodNew" label="时间段新增" />
  152. <el-table-column prop="percent" label="占比" />
  153. </el-table>
  154. </div>
  155. <!-- 图表: 用户来源渠道分布 -->
  156. <div class="detail-section chart-section-bg">
  157. <div class="section-title"><el-icon><PieChart /></el-icon> </div>
  158. <div ref="chartBarRef" class="bar-chart-box"></div>
  159. </div>
  160. </div>
  161. </div>
  162. </template>
  163. <script setup>
  164. import { ref, onMounted, nextTick, watch } from 'vue';
  165. import { useRoute, useRouter } from 'vue-router';
  166. import * as echarts from 'echarts';
  167. const route = useRoute();
  168. const router = useRouter();
  169. const activeTab = ref(route.query.tab || 'overview');
  170. const dateRange = ref('');
  171. const chartMemberRef = ref(null);
  172. const chartNewOldRef = ref(null);
  173. const chartBarRef = ref(null);
  174. // 表格数据
  175. const tableData1 = [
  176. { type: '用户总数', total: '154,832', dailyNew: '+3.44', weeklyNew: '+21,379', monthlyNew: '+21,379', periodNew: '' },
  177. { type: '会员总数', total: '42,567', dailyNew: '+5.56', weeklyNew: '+2,379', monthlyNew: '+2,379', periodNew: '' },
  178. { type: '非会员总数', total: '112,265', dailyNew: '+9.32', weeklyNew: '+92,123', monthlyNew: '+92,123', periodNew: '' },
  179. { type: '新非网总数', total: '68,420', dailyNew: '+35.34', weeklyNew: '+12,689', monthlyNew: '+12,689', periodNew: '' },
  180. { type: '老非网总数', total: '68,420', dailyNew: '+23.45', weeklyNew: '+12,033', monthlyNew: '+12,033', periodNew: '' },
  181. ];
  182. const tableData2 = [
  183. { channel: 'App Store', dailyNew: '154,832', weeklyNew: '+3.44', monthlyNew: '+21,379', periodNew: '', percent: '38%' },
  184. { channel: 'Play Store', dailyNew: '42,567', weeklyNew: '+5.56', monthlyNew: '+2,379', periodNew: '', percent: '30%' },
  185. { channel: 'H5', dailyNew: '112,265', weeklyNew: '+9.32', monthlyNew: '+92,123', periodNew: '', percent: '17%' },
  186. { channel: 'APK', dailyNew: '68,420', weeklyNew: '+35.34', monthlyNew: '+12,689', periodNew: '', percent: '10%' },
  187. { channel: '总计', dailyNew: '68,420', weeklyNew: '+23.45', monthlyNew: '+12,033', periodNew: '', percent: '100%' },
  188. ];
  189. const tableData3 = [
  190. { channel: 'HC 注册过', dailyNew: '1,245', weeklyNew: '8,742', monthlyNew: '32,567', periodNew: '', percent: '38%' },
  191. { channel: 'HC 注册过', dailyNew: '987', weeklyNew: '6,912', monthlyNew: '25,432', periodNew: '', percent: '30%' },
  192. { channel: '海外 CRM', dailyNew: '543', weeklyNew: '3,801', monthlyNew: '14,567', periodNew: '', percent: '17%' },
  193. { channel: '其他', dailyNew: '321', weeklyNew: '2,247', monthlyNew: '8,654', periodNew: '', percent: '10%' },
  194. { channel: '总计', dailyNew: '3,096', weeklyNew: '21,702', monthlyNew: '81,220', periodNew: '', percent: '100%' },
  195. ];
  196. const headerCellStyle = {
  197. background: '#fff0f0',
  198. color: '#333',
  199. fontWeight: 'bold'
  200. };
  201. const initCharts = () => {
  202. if (activeTab.value === 'overview') {
  203. nextTick(() => {
  204. // Chart 1: 会员/非会员
  205. if (chartMemberRef.value) {
  206. const chart1 = echarts.init(chartMemberRef.value);
  207. chart1.setOption({
  208. tooltip: {
  209. trigger: 'item',
  210. formatter: '{b}: {c} ({d}%)'
  211. },
  212. color: ['#ff4d4f', '#52c41a'],
  213. series: [
  214. {
  215. type: 'pie',
  216. radius: ['50%', '70%'],
  217. avoidLabelOverlap: false,
  218. label: { show: false },
  219. data: [
  220. { value: 112265, name: '非会员用户' },
  221. { value: 42567, name: '会员用户' }
  222. ]
  223. }
  224. ]
  225. });
  226. }
  227. // Chart 2: 三色环形图
  228. if (chartNewOldRef.value) {
  229. const chart2 = echarts.init(chartNewOldRef.value);
  230. chart2.setOption({
  231. tooltip: {
  232. trigger: 'item',
  233. formatter: '{b}: {c} ({d}%)'
  234. },
  235. color: ['#ff4d4f', '#52c41a', '#409eff'], // 红 绿 蓝
  236. series: [
  237. {
  238. type: 'pie',
  239. radius: ['50%', '70%'],
  240. avoidLabelOverlap: false,
  241. label: { show: false },
  242. data: [
  243. { value: 300, name: '新非网数量' },
  244. { value: 500, name: '会员用户' },
  245. { value: 400, name: '老非网数量' }
  246. ]
  247. }
  248. ]
  249. });
  250. }
  251. });
  252. } else if (activeTab.value === 'detail') {
  253. nextTick(() => {
  254. // Chart 3: 柱状图
  255. if (chartBarRef.value) {
  256. const chart3 = echarts.init(chartBarRef.value);
  257. chart3.setOption({
  258. tooltip: {
  259. trigger: 'item',
  260. },
  261. legend: {
  262. data: ['新用户', '老用户'],
  263. top: 'top',
  264. left: 'center'
  265. },
  266. grid: {
  267. left: '3%',
  268. right: '4%',
  269. bottom: '3%',
  270. containLabel: true
  271. },
  272. xAxis: [
  273. {
  274. type: 'category',
  275. data: ['App Store', 'Play Store', 'H5', 'APK', 'CRM系统', '其他'],
  276. axisTick: { alignWithLabel: true }
  277. }
  278. ],
  279. yAxis: [
  280. { type: 'value' }
  281. ],
  282. series: [
  283. {
  284. name: '新用户',
  285. type: 'bar',
  286. barWidth: '20%',
  287. color: '#40a9ff',
  288. data: [580, 1150, 650, 780, 0, 0],
  289. stack: 'total'
  290. },
  291. {
  292. name: '老用户',
  293. type: 'bar',
  294. barWidth: '20%',
  295. color: '#9287e7',
  296. data: [0, 0, 0, 0, 1100, 1300],
  297. stack: 'total'
  298. }
  299. ]
  300. });
  301. }
  302. });
  303. }
  304. };
  305. watch(activeTab, (newVal) => {
  306. // 同步 tab 状态到 URL
  307. router.replace({ query: { ...route.query, tab: newVal } });
  308. initCharts();
  309. });
  310. onMounted(() => {
  311. initCharts();
  312. });
  313. </script>
  314. <style scoped>
  315. .user-overview-container {
  316. padding: 20px;
  317. background-color: #fee6e6;
  318. }
  319. /* Tabs */
  320. .tab-header {
  321. display: flex;
  322. margin-bottom: 20px;
  323. }
  324. .tab-item {
  325. padding: 6px 16px;
  326. margin-right: 10px;
  327. background-color: #fff;
  328. border: 1px solid #ffcccc;
  329. border-radius: 4px;
  330. cursor: pointer;
  331. color: #ff4d4f;
  332. font-size: 14px;
  333. }
  334. .tab-item.active {
  335. background-color: #ff4d4f;
  336. color: #fff;
  337. }
  338. /* Overview Tab */
  339. .stats-row {
  340. display: flex;
  341. gap: 20px;
  342. margin-bottom: 20px;
  343. }
  344. .big-card {
  345. flex: 1;
  346. height: 360px;
  347. border-radius: 12px;
  348. padding: 24px;
  349. display: flex;
  350. flex-direction: column;
  351. justify-content: flex-start; /* 从顶部开始布局 */
  352. color: #fff;
  353. position: relative; /* 确保绝对定位相对于卡片 */
  354. }
  355. .purple-gradient {
  356. background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%);
  357. }
  358. .orange-gradient {
  359. background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%);
  360. background: linear-gradient(to right, #ffafbd, #ffc3a0); /* approximate orange */
  361. background: linear-gradient(90deg, #ff8c6d 0%, #ffcba4 100%);
  362. }
  363. .blue-gradient {
  364. background: linear-gradient(135deg, #9BB7FC 0%, #66a6ff 100%);
  365. }
  366. .right-stats-col {
  367. flex: 2;
  368. display: flex;
  369. flex-direction: column;
  370. gap: 20px;
  371. }
  372. .small-card {
  373. flex: 1;
  374. border-radius: 12px;
  375. padding: 20px;
  376. display: flex;
  377. flex-direction: column;
  378. justify-content: flex-start; /* 改为从顶部开始布局 */
  379. color: #fff;
  380. position: relative; /* 确保绝对定位相对于卡片 */
  381. }
  382. .card-title {
  383. font-size: 24px; /* 字体放大 */
  384. font-weight: bold; /* 加粗 */
  385. display: flex;
  386. align-items: center;
  387. gap: 8px;
  388. margin-bottom: 20px; /* 增加底部间距 */
  389. width: 100%; /* 占满宽度 */
  390. }
  391. .card-value {
  392. font-size: 64px;
  393. font-weight: bold;
  394. margin: auto 0; /* 垂直居中剩余空间 */
  395. text-align: center;
  396. }
  397. .card-value-small {
  398. font-size: 48px;
  399. font-weight: bold;
  400. margin-left: auto;
  401. }
  402. .top-row {
  403. display: flex;
  404. align-items: center;
  405. justify-content: space-between;
  406. }
  407. .card-tag-wrapper {
  408. display: flex;
  409. justify-content: flex-end;
  410. margin-top: 10px;
  411. }
  412. .card-tag {
  413. background-color: #fff;
  414. padding: 4px 12px;
  415. border-radius: 4px;
  416. font-weight: bold;
  417. display: inline-block;
  418. margin: 0 auto; /* Center for big card */
  419. }
  420. .big-card .card-tag {
  421. font-size: 18px;
  422. }
  423. .card-tag.up {
  424. color: #52c41a;
  425. }
  426. .card-tag.down {
  427. color: #ff4d4f;
  428. }
  429. /* Composition Section */
  430. .composition-section {
  431. background: linear-gradient(135deg, #b8c6db 0%, #f5f7fa 100%);
  432. background-color: #b8c4f9; /* Fallback */
  433. background: linear-gradient(to right, #a18cd1, #c2e9fb);
  434. border-radius: 12px;
  435. padding: 20px;
  436. /* background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%); */
  437. background: #a3b1ff; /* approximate purple-ish */
  438. }
  439. .section-header {
  440. color: #fff;
  441. font-size: 20px;
  442. margin-bottom: 15px;
  443. display: flex;
  444. align-items: center;
  445. gap: 8px;
  446. }
  447. .charts-row {
  448. display: flex;
  449. gap: 20px;
  450. }
  451. .chart-wrapper {
  452. flex: 1;
  453. background: #e6e9f5;
  454. border-radius: 12px;
  455. border: 2px solid #fff;
  456. height: 220px;
  457. display: flex;
  458. align-items: center;
  459. padding: 20px;
  460. }
  461. .chart-box {
  462. flex: 1;
  463. height: 100%;
  464. }
  465. .legend-custom {
  466. display: flex;
  467. flex-direction: column;
  468. gap: 10px;
  469. min-width: 120px;
  470. }
  471. .legend-item {
  472. display: flex;
  473. align-items: center;
  474. font-size: 14px;
  475. color: #333;
  476. font-weight: bold;
  477. }
  478. .dot {
  479. width: 12px;
  480. height: 12px;
  481. border-radius: 50%;
  482. margin-right: 8px;
  483. }
  484. .dot.green { background-color: #52c41a; }
  485. .dot.red { background-color: #ff4d4f; }
  486. .dot.blue { background-color: #409eff; }
  487. /* Detail Tab */
  488. .search-bar {
  489. background: #fff;
  490. padding: 15px;
  491. border-radius: 8px;
  492. display: flex;
  493. align-items: center;
  494. gap: 10px;
  495. margin-bottom: 20px;
  496. border: 1px solid #f0f0f0;
  497. }
  498. .search-label {
  499. font-weight: bold;
  500. margin-right: 5px;
  501. }
  502. .search-btn, .reset-btn {
  503. width: 80px;
  504. }
  505. .search-btn {
  506. background-color: #409eff;
  507. }
  508. .reset-btn {
  509. background-color: #a0cfff;
  510. border-color: #a0cfff;
  511. }
  512. .export-btn {
  513. margin-left: auto;
  514. background-color: #ff7875;
  515. border-color: #ff7875;
  516. }
  517. .detail-section {
  518. background: #fff;
  519. border-radius: 8px;
  520. padding: 15px;
  521. margin-bottom: 20px;
  522. border: 1px solid #f0f0f0;
  523. }
  524. .section-title {
  525. color: #409eff;
  526. font-size: 16px;
  527. font-weight: bold;
  528. margin-bottom: 15px;
  529. display: flex;
  530. align-items: center;
  531. gap: 5px;
  532. }
  533. .text-red { color: #ff4d4f; font-weight: bold; }
  534. .text-green { color: #52c41a; font-weight: bold; }
  535. .sub-item-text {
  536. font-size: 12px;
  537. padding-left: 20px;
  538. color: #606266;
  539. display: inline-block;
  540. }
  541. .chart-section-bg {
  542. /* background: #fff; already set by detail-section */
  543. }
  544. .bar-chart-box {
  545. width: 100%;
  546. height: 350px;
  547. }
  548. </style>