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.

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