xhsZlongReport.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. <template>
  2. <div class="container">
  3. <Breadcrumb :items="['menu.settings', 'menu.settings.xhsZlongReport']" />
  4. <a-row :gutter="16" class="summary-row">
  5. <a-col :span="6">
  6. <a-card class="summary-card">
  7. <a-statistic title="当前小时" :value="summary.hour" :value-from="0" animation show-group-separator>
  8. <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
  9. </a-statistic>
  10. </a-card>
  11. </a-col>
  12. <a-col :span="6">
  13. <a-card class="summary-card">
  14. <a-statistic title="当日累计" :value="summary.day" :value-from="0" animation show-group-separator>
  15. <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
  16. </a-statistic>
  17. </a-card>
  18. </a-col>
  19. <a-col :span="6">
  20. <a-card class="summary-card">
  21. <a-statistic title="当月累计" :value="summary.month" :value-from="0" animation show-group-separator>
  22. <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
  23. </a-statistic>
  24. </a-card>
  25. </a-col>
  26. <a-col :span="6">
  27. <a-card class="summary-card">
  28. <a-statistic title="当年累计" :value="summary.year" :value-from="0" animation show-group-separator>
  29. <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
  30. </a-statistic>
  31. </a-card>
  32. </a-col>
  33. </a-row>
  34. <a-row :gutter="16" class="summary-row">
  35. <a-col :span="6">
  36. <a-card class="summary-card">
  37. <a-statistic title="当前小时异常" :value="summary.hour_exception" :value-from="0" animation show-group-separator>
  38. <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
  39. </a-statistic>
  40. </a-card>
  41. </a-col>
  42. <a-col :span="6">
  43. <a-card class="summary-card">
  44. <a-statistic title="当日异常" :value="summary.day_exception" :value-from="0" animation show-group-separator>
  45. <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
  46. </a-statistic>
  47. </a-card>
  48. </a-col>
  49. <a-col :span="6">
  50. <a-card class="summary-card">
  51. <a-statistic title="当月异常" :value="summary.month_exception" :value-from="0" animation show-group-separator>
  52. <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
  53. </a-statistic>
  54. </a-card>
  55. </a-col>
  56. <a-col :span="6">
  57. <a-card class="summary-card">
  58. <a-statistic title="当年异常" :value="summary.year_exception" :value-from="0" animation show-group-separator>
  59. <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
  60. </a-statistic>
  61. </a-card>
  62. </a-col>
  63. </a-row>
  64. <a-card class="general-card" :title="$t('menu.settings.xhsZlongReport')">
  65. <a-row>
  66. <a-col :flex="1">
  67. <a-form :model="formModel" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
  68. label-align="left">
  69. <a-row :gutter="16">
  70. <a-col :span="6">
  71. <a-form-item field="channels" label="渠道">
  72. <a-select v-model="formModel.channels" multiple allow-clear placeholder="全部渠道"
  73. @change="search">
  74. <a-option v-for="item in channelOptions" :key="item.value" :value="item.value">
  75. {{ item.label }}
  76. </a-option>
  77. </a-select>
  78. </a-form-item>
  79. </a-col>
  80. <a-col :span="6">
  81. <a-form-item field="bucket_type" label="统计粒度">
  82. <a-select v-model="formModel.bucket_type" @change="search">
  83. <a-option value="hour">小时</a-option>
  84. <a-option value="day">日</a-option>
  85. <a-option value="month">月</a-option>
  86. <a-option value="year">年</a-option>
  87. </a-select>
  88. </a-form-item>
  89. </a-col>
  90. <a-col :span="10">
  91. <a-form-item field="dateRange" label="时间范围">
  92. <a-range-picker v-model="dateRange" value-format="YYYY-MM-DD" style="width: 100%"
  93. allow-clear @change="search" />
  94. </a-form-item>
  95. </a-col>
  96. </a-row>
  97. </a-form>
  98. </a-col>
  99. <a-divider style="height: 38px" direction="vertical" />
  100. <a-col :flex="'86px'" style="text-align: right">
  101. <a-space direction="vertical" :size="18">
  102. <a-button type="primary" @click="search">
  103. <template #icon>
  104. <icon-search />
  105. </template>
  106. 查询
  107. </a-button>
  108. </a-space>
  109. </a-col>
  110. </a-row>
  111. <div class="trend-panel">
  112. <div class="trend-header">
  113. <span class="trend-title">趋势图</span>
  114. <a-space :size="16">
  115. <span class="trend-legend">
  116. <i class="trend-dot trend-dot-request"></i>
  117. 请求数
  118. </span>
  119. <span class="trend-legend">
  120. <i class="trend-dot trend-dot-exception"></i>
  121. 异常数
  122. </span>
  123. </a-space>
  124. </div>
  125. <Chart height="320px" :option="chartOption" />
  126. </div>
  127. <a-row style="margin-bottom: 16px">
  128. <a-col :span="12"></a-col>
  129. <a-col :span="12" style="display: flex; align-items: center; justify-content: end;">
  130. <a-tooltip :content="$t('searchTable.actions.refresh')">
  131. <div class="action-icon" @click="refresh"><icon-refresh size="18" /></div>
  132. </a-tooltip>
  133. </a-col>
  134. </a-row>
  135. <a-table row-key="redis_key" :loading="loading" :pagination="pagination" :columns="columns"
  136. :data="renderData" :bordered="{ headerCell: true }" size="small" @page-change="onPageChange"
  137. @page-size-change="onPageSizeChange">
  138. <template #channel="{ record }">
  139. <a-tag :color="channelColorMap[record.channel] || 'gray'">
  140. {{ channelLabelMap[record.channel] || record.channel }}
  141. </a-tag>
  142. </template>
  143. <template #bucket_type="{ record }">
  144. <a-tag :color="bucketColorMap[record.bucket_type] || 'gray'">
  145. {{ bucketLabelMap[record.bucket_type] || record.bucket_type }}
  146. </a-tag>
  147. </template>
  148. <template #total="{ record }">
  149. <a-tag color="arcoblue">{{ Number(record.total || 0).toLocaleString() }}</a-tag>
  150. </template>
  151. <template #exception_count="{ record }">
  152. <a-tag color="orangered">{{ Number(record.exception_count || 0).toLocaleString() }}</a-tag>
  153. </template>
  154. </a-table>
  155. </a-card>
  156. </div>
  157. </template>
  158. <script lang="ts" setup>
  159. import { computed, onMounted, reactive, ref } from 'vue';
  160. import dayjs from 'dayjs';
  161. import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
  162. import useLoading from '@/hooks/loading';
  163. import useChartOption from '@/hooks/chart-option';
  164. import type { Pagination } from '@/types/global';
  165. import {
  166. queryXhsZlongForwardReport,
  167. type XhsZlongForwardReportParams,
  168. type XhsZlongForwardReportRecord,
  169. type XhsZlongForwardReportSummary,
  170. } from '@/api/taobao';
  171. import {
  172. zlongForwardChannelColorMap as channelColorMap,
  173. zlongForwardChannelLabelMap as channelLabelMap,
  174. zlongForwardChannelOptions as channelOptions,
  175. zlongForwardChannelValues,
  176. normalizeZlongForwardChannels,
  177. } from './zlongForwardChannels';
  178. const bucketLabelMap: Record<string, string> = {
  179. hour: '小时',
  180. day: '日',
  181. month: '月',
  182. year: '年',
  183. };
  184. const bucketColorMap: Record<string, string> = {
  185. hour: 'blue',
  186. day: 'green',
  187. month: 'orange',
  188. year: 'purple',
  189. };
  190. const { loading, setLoading } = useLoading(false);
  191. const renderData = ref<XhsZlongForwardReportRecord[]>([]);
  192. const chartData = ref<XhsZlongForwardReportRecord[]>([]);
  193. const summary = ref<XhsZlongForwardReportSummary>({
  194. hour: 0,
  195. day: 0,
  196. month: 0,
  197. year: 0,
  198. hour_exception: 0,
  199. day_exception: 0,
  200. month_exception: 0,
  201. year_exception: 0,
  202. });
  203. const formModel = reactive({
  204. channels: [...zlongForwardChannelValues],
  205. bucket_type: 'day',
  206. });
  207. const dateRange = ref<string[]>([
  208. dayjs().subtract(30, 'day').format('YYYY-MM-DD'),
  209. dayjs().format('YYYY-MM-DD'),
  210. ]);
  211. const basePagination: Pagination = {
  212. current: 1,
  213. pageSize: 20,
  214. total: 0,
  215. showPageSize: true,
  216. showTotal: true,
  217. pageSizeOptions: [20, 50, 100],
  218. };
  219. const pagination = reactive({
  220. ...basePagination,
  221. });
  222. const toNumber = (value: unknown) => Number(value || 0);
  223. const normalizeChannels = (channels?: string[]) => {
  224. const list = normalizeZlongForwardChannels(channels);
  225. return list.length > 0 ? list : [...zlongForwardChannelValues];
  226. };
  227. const buildChannelsValue = (channels?: string[]) => normalizeChannels(channels).join(',');
  228. const normalizeRecord = (item: XhsZlongForwardReportRecord): XhsZlongForwardReportRecord => ({
  229. ...item,
  230. channel: String(item.channel || '').toLowerCase(),
  231. total: toNumber(item.total),
  232. exception_count: toNumber(item.exception_count),
  233. });
  234. const sortedChartData = computed(() => {
  235. const grouped = new Map<string, XhsZlongForwardReportRecord>();
  236. chartData.value.forEach((item) => {
  237. const key = String(item.report_time || item.display_time || '');
  238. const current = grouped.get(key);
  239. if (current) {
  240. current.total += toNumber(item.total);
  241. current.exception_count += toNumber(item.exception_count);
  242. return;
  243. }
  244. grouped.set(key, {
  245. ...item,
  246. total: toNumber(item.total),
  247. exception_count: toNumber(item.exception_count),
  248. });
  249. });
  250. return Array.from(grouped.values()).sort((left, right) => {
  251. return new Date(left.report_time).getTime() - new Date(right.report_time).getTime();
  252. });
  253. });
  254. const chartXAxis = computed(() => sortedChartData.value.map((item) => item.display_time));
  255. const chartTotalSeries = computed(() => sortedChartData.value.map((item) => item.total));
  256. const chartExceptionSeries = computed(() => sortedChartData.value.map((item) => item.exception_count));
  257. const chartLabelInterval = computed(() => {
  258. const count = chartXAxis.value.length;
  259. if (count <= 12) return 0;
  260. return Math.ceil(count / 12) - 1;
  261. });
  262. const { chartOption } = useChartOption(() => ({
  263. grid: {
  264. left: 48,
  265. right: 24,
  266. top: 48,
  267. bottom: 48,
  268. },
  269. tooltip: {
  270. trigger: 'axis',
  271. className: 'echarts-tooltip-diy',
  272. formatter(params: any) {
  273. const lines = (params || []).map((item: any) => {
  274. return `<div class="content-panel"><span>${item.seriesName}</span><span class="tooltip-value">${Number(item.value || 0).toLocaleString()}</span></div>`;
  275. });
  276. const axisLabel = params?.[0]?.axisValueLabel || '';
  277. return `<div><p class="tooltip-title">${axisLabel}</p>${lines.join('')}</div>`;
  278. },
  279. },
  280. legend: {
  281. show: false,
  282. },
  283. xAxis: {
  284. type: 'category',
  285. boundaryGap: false,
  286. data: chartXAxis.value,
  287. axisLabel: {
  288. interval: chartLabelInterval.value,
  289. rotate: chartXAxis.value.length > 10 ? 25 : 0,
  290. color: '#4E5969',
  291. },
  292. axisLine: {
  293. lineStyle: {
  294. color: '#E5E8EF',
  295. },
  296. },
  297. },
  298. yAxis: {
  299. type: 'value',
  300. axisLabel: {
  301. color: '#4E5969',
  302. },
  303. splitLine: {
  304. lineStyle: {
  305. type: 'dashed',
  306. color: '#E5E8EF',
  307. },
  308. },
  309. },
  310. series: [
  311. {
  312. name: '请求数',
  313. type: 'line',
  314. smooth: true,
  315. showSymbol: false,
  316. symbolSize: 8,
  317. data: chartTotalSeries.value,
  318. lineStyle: {
  319. width: 3,
  320. color: '#165DFF',
  321. },
  322. itemStyle: {
  323. color: '#165DFF',
  324. },
  325. areaStyle: {
  326. color: 'rgba(22, 93, 255, 0.12)',
  327. },
  328. },
  329. {
  330. name: '异常数',
  331. type: 'line',
  332. smooth: true,
  333. showSymbol: false,
  334. symbolSize: 8,
  335. data: chartExceptionSeries.value,
  336. lineStyle: {
  337. width: 3,
  338. color: '#F53F3F',
  339. },
  340. itemStyle: {
  341. color: '#F53F3F',
  342. },
  343. areaStyle: {
  344. color: 'rgba(245, 63, 63, 0.08)',
  345. },
  346. },
  347. ],
  348. }));
  349. const columns = computed<TableColumnData[]>(() => [
  350. {
  351. title: '渠道',
  352. slotName: 'channel',
  353. width: 110,
  354. },
  355. {
  356. title: '统计粒度',
  357. slotName: 'bucket_type',
  358. width: 100,
  359. },
  360. {
  361. title: '统计时间',
  362. dataIndex: 'display_time',
  363. width: 180,
  364. },
  365. {
  366. title: '请求数',
  367. slotName: 'total',
  368. width: 120,
  369. },
  370. {
  371. title: '异常数',
  372. slotName: 'exception_count',
  373. width: 120,
  374. },
  375. ]);
  376. const buildParams = (current = 1, pageSize = pagination.pageSize): XhsZlongForwardReportParams => {
  377. return {
  378. current,
  379. pageSize,
  380. channels: buildChannelsValue(formModel.channels),
  381. bucket_type: formModel.bucket_type,
  382. start: dateRange.value?.[0] || undefined,
  383. end: dateRange.value?.[1] || undefined,
  384. getTotal: true,
  385. };
  386. };
  387. const buildChartParams = (): XhsZlongForwardReportParams => {
  388. return {
  389. ...buildParams(1, formModel.bucket_type === 'hour' ? 10000 : 2000),
  390. getTotal: false,
  391. };
  392. };
  393. const fetchTableData = async (params: XhsZlongForwardReportParams) => {
  394. const res: any = await queryXhsZlongForwardReport(params);
  395. if (!res?.data) return;
  396. renderData.value = (res.data.list || []).map(normalizeRecord);
  397. summary.value = {
  398. hour: toNumber(res.data.summary?.hour),
  399. day: toNumber(res.data.summary?.day),
  400. month: toNumber(res.data.summary?.month),
  401. year: toNumber(res.data.summary?.year),
  402. hour_exception: toNumber(res.data.summary?.hour_exception),
  403. day_exception: toNumber(res.data.summary?.day_exception),
  404. month_exception: toNumber(res.data.summary?.month_exception),
  405. year_exception: toNumber(res.data.summary?.year_exception),
  406. };
  407. pagination.current = res.data.page || 1;
  408. pagination.pageSize = res.data.size || pagination.pageSize;
  409. pagination.total = res.data.count || 0;
  410. };
  411. const fetchChartData = async () => {
  412. const res: any = await queryXhsZlongForwardReport(buildChartParams());
  413. if (!res?.data) {
  414. chartData.value = [];
  415. return;
  416. }
  417. chartData.value = (res.data.list || []).map(normalizeRecord);
  418. };
  419. const loadReport = async (current = 1, pageSize = pagination.pageSize) => {
  420. setLoading(true);
  421. try {
  422. await Promise.all([
  423. fetchTableData(buildParams(current, pageSize)),
  424. fetchChartData(),
  425. ]);
  426. } catch (err) {
  427. console.error(err);
  428. } finally {
  429. setLoading(false);
  430. }
  431. };
  432. const search = () => {
  433. loadReport(1, pagination.pageSize);
  434. };
  435. const refresh = () => {
  436. loadReport(pagination.current, pagination.pageSize);
  437. };
  438. const onPageChange = async (current: number) => {
  439. setLoading(true);
  440. try {
  441. await fetchTableData(buildParams(current, pagination.pageSize));
  442. } catch (err) {
  443. console.error(err);
  444. } finally {
  445. setLoading(false);
  446. }
  447. };
  448. const onPageSizeChange = (pageSize: number) => {
  449. pagination.pageSize = pageSize;
  450. loadReport(1, pageSize);
  451. };
  452. onMounted(() => {
  453. search();
  454. });
  455. </script>
  456. <script lang="ts">
  457. export default {
  458. name: 'XhsZlongReport',
  459. };
  460. </script>
  461. <style scoped lang="less">
  462. .container {
  463. padding: 0 20px 20px 20px;
  464. }
  465. .summary-row {
  466. margin-bottom: 16px;
  467. }
  468. .summary-card {
  469. height: 100%;
  470. }
  471. .summary-icon {
  472. margin-right: 8px;
  473. font-size: 16px;
  474. }
  475. .summary-icon-request {
  476. color: rgb(var(--arcoblue-6));
  477. }
  478. .summary-icon-exception {
  479. color: rgb(var(--orangered-6));
  480. }
  481. .trend-panel {
  482. margin-bottom: 16px;
  483. padding: 20px 20px 8px;
  484. border-radius: 12px;
  485. background: linear-gradient(180deg, var(--color-fill-1), var(--color-bg-1));
  486. }
  487. .trend-header {
  488. display: flex;
  489. align-items: center;
  490. justify-content: space-between;
  491. margin-bottom: 12px;
  492. }
  493. .trend-title {
  494. font-size: 16px;
  495. font-weight: 600;
  496. color: var(--color-text-1);
  497. }
  498. .trend-legend {
  499. display: inline-flex;
  500. align-items: center;
  501. gap: 8px;
  502. color: var(--color-text-2);
  503. }
  504. .trend-dot {
  505. width: 10px;
  506. height: 10px;
  507. border-radius: 50%;
  508. }
  509. .trend-dot-request {
  510. background: #165dff;
  511. }
  512. .trend-dot-exception {
  513. background: #f53f3f;
  514. }
  515. .report-alert {
  516. margin-bottom: 16px;
  517. }
  518. :deep(.general-card) {
  519. height: 100%;
  520. }
  521. </style>