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.

475 lines
13 KiB

  1. <template>
  2. <div class="user-activity-stats-container">
  3. <!-- 用户活跃度趋势图 -->
  4. <div class="content-card">
  5. <div class="card-header">
  6. <el-icon class="header-icon"><TrendCharts /></el-icon>
  7. <span class="header-title">用户活跃度趋势图</span>
  8. </div>
  9. <div class="card-filter-row">
  10. <div class="filter-left">
  11. <span class="filter-label">时间段查询</span>
  12. <el-date-picker
  13. v-model="chartDateRange"
  14. type="daterange"
  15. range-separator="至"
  16. start-placeholder="开始时间"
  17. end-placeholder="结束时间"
  18. size="default"
  19. @change="handleChartDateChange"
  20. />
  21. </div>
  22. <div class="filter-right">
  23. <el-button-group>
  24. <el-button :type="chartMode === 'day' ? 'danger' : 'default'" @click="handleModeChange('day')">每日</el-button>
  25. <el-button :type="chartMode === 'week' ? 'danger' : 'default'" @click="handleModeChange('week')">近七日</el-button>
  26. <el-button :type="chartMode === 'month' ? 'danger' : 'default'" @click="handleModeChange('month')">近三十日</el-button>
  27. </el-button-group>
  28. </div>
  29. </div>
  30. <div class="chart-container" ref="chartRef"></div>
  31. </div>
  32. <!-- DeepChart活跃用户明细 -->
  33. <div class="content-card">
  34. <div class="card-header">
  35. <el-icon class="header-icon"><DataLine /></el-icon>
  36. <span class="header-title">DeepChart活跃用户明细</span>
  37. </div>
  38. <div class="table-filter-row">
  39. <el-input v-model="tableAccount" placeholder="请输入账号" style="width: 150px" />
  40. <el-select v-model="tableRegion" placeholder="请选择所属地区" style="width: 150px">
  41. <el-option label="全部" value="all" />
  42. <el-option
  43. v-for="item in regionOptions"
  44. :key="item.value"
  45. :label="item.label"
  46. :value="item.value"
  47. />
  48. </el-select>
  49. <el-date-picker
  50. v-model="tableDateRange"
  51. type="daterange"
  52. range-separator="至"
  53. start-placeholder="开始时间"
  54. end-placeholder="结束时间"
  55. size="default"
  56. style="width: 240px"
  57. />
  58. <el-button type="primary" class="search-btn-small" @click="handleTableSearch">搜索</el-button>
  59. <el-button type="primary" class="reset-btn-small" @click="handleTableReset">重置</el-button>
  60. <el-button type="danger" class="export-btn-small">数据导出</el-button>
  61. <el-button type="danger" class="export-list-btn">查看导出列表</el-button>
  62. </div>
  63. <el-table :data="tableData" style="width: 100%" :header-cell-style="headerCellStyle">
  64. <el-table-column prop="index" label="序号" width="80" align="center" />
  65. <el-table-column prop="account" label="账号" min-width="120" />
  66. <el-table-column prop="name" label="姓名" min-width="120" />
  67. <el-table-column prop="region" label="地区" width="100" />
  68. <el-table-column prop="loginCount" label="登录次数" width="100" align="center" />
  69. <el-table-column prop="totalDuration" label="总停留时长" min-width="150" align="center" />
  70. <el-table-column prop="avgDuration" label="平均停留时长" min-width="150" align="center" />
  71. <el-table-column prop="deepMate" label="DeepMate" min-width="100" align="center" />
  72. <el-table-column prop="deepExplore" label="深度探索" min-width="100" align="center" />
  73. <el-table-column prop="marketInfo" label="行情" min-width="100" align="center" />
  74. <el-table-column prop="updateTime" label="更新日期" min-width="150" align="center" />
  75. </el-table>
  76. <div class="pagination-container">
  77. <div class="total-count">{{ total }}</div>
  78. <el-pagination
  79. background
  80. layout="sizes, prev, pager, next, jumper"
  81. :total="total"
  82. :page-sizes="[10, 20, 50, 100]"
  83. v-model:current-page="currentPage"
  84. v-model:page-size="pageSize"
  85. @current-change="handlePageChange"
  86. @size-change="handleSizeChange"
  87. />
  88. </div>
  89. </div>
  90. </div>
  91. </template>
  92. <script setup>
  93. import { ref, onMounted, nextTick, watch } from 'vue';
  94. import { useRoute, useRouter } from 'vue-router';
  95. import * as echarts from 'echarts';
  96. import { getUserDeepChartTrend, getDeepChartActiveUserList, getRegionsList } from '../../api/platformData';
  97. const route = useRoute();
  98. const router = useRouter();
  99. // 图表筛选
  100. const chartDateRange = ref('');
  101. const chartMode = ref(route.query.mode || 'day'); // day, week, month
  102. const chartRef = ref(null);
  103. let chartInstance = null;
  104. // 初始化 URL 参数
  105. const initQueryParams = () => {
  106. const { mode, startTime, endTime } = route.query;
  107. if (mode) {
  108. chartMode.value = mode;
  109. }
  110. if (startTime && endTime) {
  111. chartDateRange.value = [new Date(startTime), new Date(endTime)];
  112. }
  113. };
  114. initQueryParams();
  115. const updateUrlParams = () => {
  116. const query = { ...route.query };
  117. query.mode = chartMode.value;
  118. if (chartDateRange.value && chartDateRange.value.length === 2) {
  119. query.startTime = formatDate(chartDateRange.value[0]);
  120. query.endTime = formatDate(chartDateRange.value[1]);
  121. } else {
  122. delete query.startTime;
  123. delete query.endTime;
  124. }
  125. router.replace({ query });
  126. };
  127. const handleChartDateChange = () => {
  128. updateUrlParams();
  129. fetchChartData();
  130. };
  131. const handleModeChange = (mode) => {
  132. chartMode.value = mode;
  133. updateUrlParams();
  134. fetchChartData();
  135. };
  136. // 格式化日期
  137. const formatDate = (date) => {
  138. if (!date) return '';
  139. const d = new Date(date);
  140. const pad = (n) => n < 10 ? '0' + n : n;
  141. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  142. };
  143. const fetchChartData = async () => {
  144. const params = {
  145. mode: chartMode.value
  146. };
  147. if (chartDateRange.value && chartDateRange.value.length === 2) {
  148. params.startTime = formatDate(chartDateRange.value[0]);
  149. params.endTime = formatDate(chartDateRange.value[1]);
  150. }
  151. try {
  152. const res = await getUserDeepChartTrend(params);
  153. console.log("获取DeepChart趋势数据响应:", res);
  154. const data = res.data || res; // 兼容处理
  155. if (Array.isArray(data)) {
  156. updateChart(data);
  157. }
  158. } catch (e) {
  159. console.error('获取DeepChart趋势数据失败:', e);
  160. }
  161. };
  162. const updateChart = (data) => {
  163. if (!chartRef.value) return;
  164. if (!chartInstance) {
  165. chartInstance = echarts.init(chartRef.value);
  166. }
  167. const dates = data.map(item => item.time_point);
  168. const activeUsers = data.map(item => item.active_users);
  169. const useCounts = data.map(item => item.use_count);
  170. const option = {
  171. tooltip: {
  172. trigger: 'axis',
  173. axisPointer: { type: 'cross' }
  174. },
  175. legend: {
  176. data: ['DeepChart活跃人数', 'DeepChart使用次数'],
  177. top: 'top',
  178. left: 'center',
  179. itemWidth: 35,
  180. itemHeight: 18,
  181. textStyle: {
  182. fontSize: 14
  183. }
  184. },
  185. grid: {
  186. left: '3%',
  187. right: '4%',
  188. bottom: '3%',
  189. containLabel: true
  190. },
  191. xAxis: {
  192. type: 'category',
  193. boundaryGap: false,
  194. data: dates
  195. },
  196. yAxis: [
  197. {
  198. type: 'value',
  199. min: 0,
  200. position: 'left',
  201. axisLabel: { formatter: '{value}' }
  202. },
  203. {
  204. type: 'value',
  205. min: 0,
  206. position: 'right',
  207. axisLabel: { formatter: '{value}' }
  208. }
  209. ],
  210. series: [
  211. {
  212. name: 'DeepChart活跃人数',
  213. type: 'line',
  214. yAxisIndex: 0,
  215. data: activeUsers,
  216. itemStyle: { color: '#e74c3c' },
  217. symbol: 'circle',
  218. symbolSize: 8,
  219. lineStyle: { width: 3 }
  220. },
  221. {
  222. name: 'DeepChart使用次数',
  223. type: 'line',
  224. yAxisIndex: 1,
  225. data: useCounts,
  226. itemStyle: { color: '#2ecc71' },
  227. symbol: 'circle',
  228. symbolSize: 8,
  229. lineStyle: { width: 3 }
  230. }
  231. ]
  232. };
  233. chartInstance.setOption(option);
  234. };
  235. const tableAccount = ref('');
  236. const tableRegion = ref('');
  237. const tableDateRange = ref('');
  238. const currentPage = ref(1);
  239. const pageSize = ref(10);
  240. const total = ref(0);
  241. const regionOptions = ref([]);
  242. // 获取地区列表
  243. const fetchRegionOptions = async () => {
  244. try {
  245. const res = await getRegionsList();
  246. console.log("获取地区列表响应:", res);
  247. const data = res.data || res;
  248. if (data && data.list) {
  249. regionOptions.value = data.list.map(region => ({ label: region, value: region }));
  250. }
  251. } catch (e) {
  252. console.error('获取地区列表失败:', e);
  253. }
  254. };
  255. // 表格数据
  256. const tableData = ref([]);
  257. const handleTableSearch = () => {
  258. currentPage.value = 1;
  259. fetchTableData();
  260. };
  261. const handleTableReset = () => {
  262. tableAccount.value = '';
  263. tableRegion.value = '';
  264. tableDateRange.value = '';
  265. currentPage.value = 1;
  266. fetchTableData();
  267. };
  268. const handlePageChange = (page) => {
  269. currentPage.value = page;
  270. fetchTableData();
  271. };
  272. const handleSizeChange = (size) => {
  273. pageSize.value = size;
  274. currentPage.value = 1;
  275. fetchTableData();
  276. };
  277. const fetchTableData = async () => {
  278. const params = {
  279. page: currentPage.value,
  280. page_size: pageSize.value
  281. };
  282. if (tableAccount.value) params.jwcode = tableAccount.value;
  283. if (tableRegion.value && tableRegion.value !== 'all') params.region = tableRegion.value;
  284. if (tableDateRange.value && tableDateRange.value.length === 2) {
  285. params.startTime = formatDate(tableDateRange.value[0]);
  286. params.endTime = formatDate(tableDateRange.value[1]);
  287. }
  288. try {
  289. const res = await getDeepChartActiveUserList(params);
  290. console.log("获取DeepChart活跃用户明细响应:", res);
  291. const data = res.data || res; // 兼容处理
  292. if (data && data.list) {
  293. tableData.value = data.list.map((item, index) => ({
  294. index: (currentPage.value - 1) * pageSize.value + index + 1,
  295. account: item.jwcode,
  296. name: item.username,
  297. region: item.region,
  298. loginCount: item.login_count,
  299. totalDuration: formatDuration(item.stay_time),
  300. avgDuration: formatDuration(item.avg_stay_time),
  301. deepMate: item.deepmate_count,
  302. deepExplore: item.dive_seek_count,
  303. marketInfo: item.market_info_count, // 行情登录次数
  304. updateTime: item.update_time // 使用 update_time
  305. }));
  306. total.value = data.total || 0;
  307. } else {
  308. tableData.value = [];
  309. total.value = 0;
  310. }
  311. } catch (e) {
  312. console.error('获取DeepChart活跃用户明细失败:', e);
  313. }
  314. };
  315. // 格式化时长 (秒 -> 时分秒)
  316. const formatDuration = (seconds) => {
  317. if (!seconds) return '0秒';
  318. const h = Math.floor(seconds / 3600);
  319. const m = Math.floor((seconds % 3600) / 60);
  320. const s = seconds % 60;
  321. let str = '';
  322. if (h > 0) str += `${h}小时`;
  323. if (m > 0) str += `${m}分钟`;
  324. if (s > 0) str += `${s}`;
  325. return str || '0秒';
  326. };
  327. const headerCellStyle = {
  328. background: '#f5f7fa',
  329. color: '#333',
  330. fontWeight: 'bold'
  331. };
  332. const initChart = () => {
  333. if (chartRef.value) {
  334. // 初始化图表实例
  335. if (!chartInstance) {
  336. chartInstance = echarts.init(chartRef.value);
  337. }
  338. // 初始加载数据
  339. fetchChartData();
  340. }
  341. };
  342. onMounted(() => {
  343. nextTick(() => {
  344. initChart();
  345. fetchTableData();
  346. fetchRegionOptions();
  347. });
  348. });
  349. </script>
  350. <style scoped>
  351. .user-activity-stats-container {
  352. padding: 20px;
  353. background-color: #fee6e6;
  354. min-height: calc(100vh - 40px);
  355. }
  356. /* Content Cards */
  357. .content-card {
  358. background: #fff;
  359. border-radius: 8px;
  360. padding: 20px;
  361. margin-bottom: 20px;
  362. border: 1px solid #f0f0f0;
  363. }
  364. .card-header {
  365. display: flex;
  366. align-items: center;
  367. gap: 8px;
  368. margin-bottom: 20px;
  369. }
  370. .header-icon {
  371. color: #409eff;
  372. font-size: 20px;
  373. }
  374. .header-title {
  375. font-size: 18px;
  376. font-weight: bold;
  377. color: #333;
  378. }
  379. /* Chart Section */
  380. .card-filter-row {
  381. display: flex;
  382. justify-content: space-between;
  383. align-items: center;
  384. margin-bottom: 20px;
  385. background: #fafafa;
  386. padding: 10px;
  387. border-radius: 4px;
  388. }
  389. .filter-left {
  390. display: flex;
  391. align-items: center;
  392. gap: 10px;
  393. }
  394. .filter-label {
  395. font-size: 14px;
  396. font-weight: bold;
  397. }
  398. .chart-container {
  399. width: 100%;
  400. height: 400px;
  401. }
  402. /* Table Section */
  403. .table-filter-row {
  404. display: flex;
  405. align-items: center;
  406. gap: 10px;
  407. margin-bottom: 20px;
  408. }
  409. .search-btn-small { background-color: #409eff; width: 70px; }
  410. .reset-btn-small { background-color: #409eff; border-color: #409eff; width: 70px; }
  411. .export-btn-small { background-color: #ff7875; border-color: #ff7875; width: 90px; }
  412. .export-list-btn { background-color: #ff7875; border-color: #ff7875; width: 110px; }
  413. /* Pagination */
  414. .pagination-container {
  415. display: flex;
  416. justify-content: center;
  417. align-items: center;
  418. margin-top: 20px;
  419. }
  420. .total-count {
  421. font-size: 14px;
  422. color: #606266;
  423. margin-right: 20px;
  424. }
  425. </style>