|
|
@@ -0,0 +1,512 @@
|
|
|
+<template>
|
|
|
+ <div class="container">
|
|
|
+ <Breadcrumb :items="['menu.settings', 'menu.settings.xhsZlongReport']" />
|
|
|
+
|
|
|
+ <a-row :gutter="16" class="summary-row">
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当前小时" :value="summary.hour" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当日累计" :value="summary.day" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当月累计" :value="summary.month" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当年累计" :value="summary.year" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-fire class="summary-icon summary-icon-request" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-row :gutter="16" class="summary-row">
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当前小时异常" :value="summary.hour_exception" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当日异常" :value="summary.day_exception" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当月异常" :value="summary.month_exception" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-card class="summary-card">
|
|
|
+ <a-statistic title="当年异常" :value="summary.year_exception" :value-from="0" animation show-group-separator>
|
|
|
+ <template #prefix><icon-exclamation-circle class="summary-icon summary-icon-exception" /></template>
|
|
|
+ </a-statistic>
|
|
|
+ </a-card>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+
|
|
|
+ <a-card class="general-card" :title="$t('menu.settings.xhsZlongReport')">
|
|
|
+ <a-row>
|
|
|
+ <a-col :flex="1">
|
|
|
+ <a-form :model="formModel" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
|
|
|
+ label-align="left">
|
|
|
+ <a-row :gutter="16">
|
|
|
+ <a-col :span="6">
|
|
|
+ <a-form-item field="bucket_type" label="统计粒度">
|
|
|
+ <a-select v-model="formModel.bucket_type" @change="search">
|
|
|
+ <a-option value="hour">小时</a-option>
|
|
|
+ <a-option value="day">日</a-option>
|
|
|
+ <a-option value="month">月</a-option>
|
|
|
+ <a-option value="year">年</a-option>
|
|
|
+ </a-select>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="10">
|
|
|
+ <a-form-item field="dateRange" label="时间范围">
|
|
|
+ <a-range-picker v-model="dateRange" value-format="YYYY-MM-DD" style="width: 100%"
|
|
|
+ allow-clear @change="search" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ </a-form>
|
|
|
+ </a-col>
|
|
|
+ <a-divider style="height: 38px" direction="vertical" />
|
|
|
+ <a-col :flex="'86px'" style="text-align: right">
|
|
|
+ <a-space direction="vertical" :size="18">
|
|
|
+ <a-button type="primary" @click="search">
|
|
|
+ <template #icon>
|
|
|
+ <icon-search />
|
|
|
+ </template>
|
|
|
+ 查询
|
|
|
+ </a-button>
|
|
|
+ </a-space>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+
|
|
|
+ <div class="trend-panel">
|
|
|
+ <div class="trend-header">
|
|
|
+ <span class="trend-title">趋势图</span>
|
|
|
+ <a-space :size="16">
|
|
|
+ <span class="trend-legend">
|
|
|
+ <i class="trend-dot trend-dot-request"></i>
|
|
|
+ 请求数
|
|
|
+ </span>
|
|
|
+ <span class="trend-legend">
|
|
|
+ <i class="trend-dot trend-dot-exception"></i>
|
|
|
+ 异常数
|
|
|
+ </span>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+ <Chart height="320px" :option="chartOption" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <a-row style="margin-bottom: 16px">
|
|
|
+ <a-col :span="12"></a-col>
|
|
|
+ <a-col :span="12" style="display: flex; align-items: center; justify-content: end;">
|
|
|
+ <a-tooltip :content="$t('searchTable.actions.refresh')">
|
|
|
+ <div class="action-icon" @click="refresh"><icon-refresh size="18" /></div>
|
|
|
+ </a-tooltip>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+
|
|
|
+ <a-table row-key="redis_key" :loading="loading" :pagination="pagination" :columns="columns"
|
|
|
+ :data="renderData" :bordered="{ headerCell: true }" size="small" @page-change="onPageChange"
|
|
|
+ @page-size-change="onPageSizeChange">
|
|
|
+ <template #bucket_type="{ record }">
|
|
|
+ <a-tag :color="bucketColorMap[record.bucket_type] || 'gray'">
|
|
|
+ {{ bucketLabelMap[record.bucket_type] || record.bucket_type }}
|
|
|
+ </a-tag>
|
|
|
+ </template>
|
|
|
+ <template #total="{ record }">
|
|
|
+ <a-tag color="arcoblue">{{ Number(record.total || 0).toLocaleString() }}</a-tag>
|
|
|
+ </template>
|
|
|
+ <template #exception_count="{ record }">
|
|
|
+ <a-tag color="orangered">{{ Number(record.exception_count || 0).toLocaleString() }}</a-tag>
|
|
|
+ </template>
|
|
|
+ </a-table>
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
|
|
+import useLoading from '@/hooks/loading';
|
|
|
+import useChartOption from '@/hooks/chart-option';
|
|
|
+import type { Pagination } from '@/types/global';
|
|
|
+import {
|
|
|
+ queryXhsZlongForwardReport,
|
|
|
+ type XhsZlongForwardReportParams,
|
|
|
+ type XhsZlongForwardReportRecord,
|
|
|
+ type XhsZlongForwardReportSummary,
|
|
|
+} from '@/api/taobao';
|
|
|
+
|
|
|
+const bucketLabelMap: Record<string, string> = {
|
|
|
+ hour: '小时',
|
|
|
+ day: '日',
|
|
|
+ month: '月',
|
|
|
+ year: '年',
|
|
|
+};
|
|
|
+const bucketColorMap: Record<string, string> = {
|
|
|
+ hour: 'blue',
|
|
|
+ day: 'green',
|
|
|
+ month: 'orange',
|
|
|
+ year: 'purple',
|
|
|
+};
|
|
|
+
|
|
|
+const { loading, setLoading } = useLoading(false);
|
|
|
+const renderData = ref<XhsZlongForwardReportRecord[]>([]);
|
|
|
+const chartData = ref<XhsZlongForwardReportRecord[]>([]);
|
|
|
+const summary = ref<XhsZlongForwardReportSummary>({
|
|
|
+ hour: 0,
|
|
|
+ day: 0,
|
|
|
+ month: 0,
|
|
|
+ year: 0,
|
|
|
+ hour_exception: 0,
|
|
|
+ day_exception: 0,
|
|
|
+ month_exception: 0,
|
|
|
+ year_exception: 0,
|
|
|
+});
|
|
|
+
|
|
|
+const formModel = reactive({
|
|
|
+ bucket_type: 'day',
|
|
|
+});
|
|
|
+
|
|
|
+const dateRange = ref<string[]>([
|
|
|
+ dayjs().subtract(30, 'day').format('YYYY-MM-DD'),
|
|
|
+ dayjs().format('YYYY-MM-DD'),
|
|
|
+]);
|
|
|
+
|
|
|
+const basePagination: Pagination = {
|
|
|
+ current: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ total: 0,
|
|
|
+ showPageSize: true,
|
|
|
+ showTotal: true,
|
|
|
+ pageSizeOptions: [20, 50, 100],
|
|
|
+};
|
|
|
+const pagination = reactive({
|
|
|
+ ...basePagination,
|
|
|
+});
|
|
|
+
|
|
|
+const toNumber = (value: unknown) => Number(value || 0);
|
|
|
+const normalizeRecord = (item: XhsZlongForwardReportRecord): XhsZlongForwardReportRecord => ({
|
|
|
+ ...item,
|
|
|
+ total: toNumber(item.total),
|
|
|
+ exception_count: toNumber(item.exception_count),
|
|
|
+});
|
|
|
+const sortedChartData = computed(() => {
|
|
|
+ return [...chartData.value].sort((left, right) => {
|
|
|
+ return new Date(left.report_time).getTime() - new Date(right.report_time).getTime();
|
|
|
+ });
|
|
|
+});
|
|
|
+const chartXAxis = computed(() => sortedChartData.value.map((item) => item.display_time));
|
|
|
+const chartTotalSeries = computed(() => sortedChartData.value.map((item) => item.total));
|
|
|
+const chartExceptionSeries = computed(() => sortedChartData.value.map((item) => item.exception_count));
|
|
|
+const chartLabelInterval = computed(() => {
|
|
|
+ const count = chartXAxis.value.length;
|
|
|
+ if (count <= 12) return 0;
|
|
|
+ return Math.ceil(count / 12) - 1;
|
|
|
+});
|
|
|
+const { chartOption } = useChartOption(() => ({
|
|
|
+ grid: {
|
|
|
+ left: 48,
|
|
|
+ right: 24,
|
|
|
+ top: 48,
|
|
|
+ bottom: 48,
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ className: 'echarts-tooltip-diy',
|
|
|
+ formatter(params: any) {
|
|
|
+ const lines = (params || []).map((item: any) => {
|
|
|
+ return `<div class="content-panel"><span>${item.seriesName}</span><span class="tooltip-value">${Number(item.value || 0).toLocaleString()}</span></div>`;
|
|
|
+ });
|
|
|
+ const axisLabel = params?.[0]?.axisValueLabel || '';
|
|
|
+ return `<div><p class="tooltip-title">${axisLabel}</p>${lines.join('')}</div>`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ show: false,
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: chartXAxis.value,
|
|
|
+ axisLabel: {
|
|
|
+ interval: chartLabelInterval.value,
|
|
|
+ rotate: chartXAxis.value.length > 10 ? 25 : 0,
|
|
|
+ color: '#4E5969',
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#E5E8EF',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLabel: {
|
|
|
+ color: '#4E5969',
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#E5E8EF',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '请求数',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ showSymbol: false,
|
|
|
+ symbolSize: 8,
|
|
|
+ data: chartTotalSeries.value,
|
|
|
+ lineStyle: {
|
|
|
+ width: 3,
|
|
|
+ color: '#165DFF',
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: '#165DFF',
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: 'rgba(22, 93, 255, 0.12)',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '异常数',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ showSymbol: false,
|
|
|
+ symbolSize: 8,
|
|
|
+ data: chartExceptionSeries.value,
|
|
|
+ lineStyle: {
|
|
|
+ width: 3,
|
|
|
+ color: '#F53F3F',
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: '#F53F3F',
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: 'rgba(245, 63, 63, 0.08)',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+}));
|
|
|
+
|
|
|
+const columns = computed<TableColumnData[]>(() => [
|
|
|
+ {
|
|
|
+ title: '统计粒度',
|
|
|
+ slotName: 'bucket_type',
|
|
|
+ width: 100,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '统计时间',
|
|
|
+ dataIndex: 'display_time',
|
|
|
+ width: 180,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '请求数',
|
|
|
+ slotName: 'total',
|
|
|
+ width: 120,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '异常数',
|
|
|
+ slotName: 'exception_count',
|
|
|
+ width: 120,
|
|
|
+ },
|
|
|
+]);
|
|
|
+
|
|
|
+const buildParams = (current = 1, pageSize = pagination.pageSize): XhsZlongForwardReportParams => {
|
|
|
+ return {
|
|
|
+ current,
|
|
|
+ pageSize,
|
|
|
+ bucket_type: formModel.bucket_type,
|
|
|
+ start: dateRange.value?.[0] || undefined,
|
|
|
+ end: dateRange.value?.[1] || undefined,
|
|
|
+ getTotal: true,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const buildChartParams = (): XhsZlongForwardReportParams => {
|
|
|
+ return {
|
|
|
+ ...buildParams(1, formModel.bucket_type === 'hour' ? 10000 : 2000),
|
|
|
+ getTotal: false,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const fetchTableData = async (params: XhsZlongForwardReportParams) => {
|
|
|
+ const res: any = await queryXhsZlongForwardReport(params);
|
|
|
+ if (!res?.data) return;
|
|
|
+
|
|
|
+ renderData.value = (res.data.list || []).map(normalizeRecord);
|
|
|
+ summary.value = {
|
|
|
+ hour: toNumber(res.data.summary?.hour),
|
|
|
+ day: toNumber(res.data.summary?.day),
|
|
|
+ month: toNumber(res.data.summary?.month),
|
|
|
+ year: toNumber(res.data.summary?.year),
|
|
|
+ hour_exception: toNumber(res.data.summary?.hour_exception),
|
|
|
+ day_exception: toNumber(res.data.summary?.day_exception),
|
|
|
+ month_exception: toNumber(res.data.summary?.month_exception),
|
|
|
+ year_exception: toNumber(res.data.summary?.year_exception),
|
|
|
+ };
|
|
|
+ pagination.current = res.data.page || 1;
|
|
|
+ pagination.pageSize = res.data.size || pagination.pageSize;
|
|
|
+ pagination.total = res.data.count || 0;
|
|
|
+};
|
|
|
+
|
|
|
+const fetchChartData = async () => {
|
|
|
+ const res: any = await queryXhsZlongForwardReport(buildChartParams());
|
|
|
+ if (!res?.data) {
|
|
|
+ chartData.value = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ chartData.value = (res.data.list || []).map(normalizeRecord);
|
|
|
+};
|
|
|
+
|
|
|
+const loadReport = async (current = 1, pageSize = pagination.pageSize) => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ await Promise.all([
|
|
|
+ fetchTableData(buildParams(current, pageSize)),
|
|
|
+ fetchChartData(),
|
|
|
+ ]);
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err);
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const search = () => {
|
|
|
+ loadReport(1, pagination.pageSize);
|
|
|
+};
|
|
|
+
|
|
|
+const refresh = () => {
|
|
|
+ loadReport(pagination.current, pagination.pageSize);
|
|
|
+};
|
|
|
+
|
|
|
+const onPageChange = async (current: number) => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ await fetchTableData(buildParams(current, pagination.pageSize));
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err);
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onPageSizeChange = (pageSize: number) => {
|
|
|
+ pagination.pageSize = pageSize;
|
|
|
+ loadReport(1, pageSize);
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ search();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+export default {
|
|
|
+ name: 'XhsZlongReport',
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.container {
|
|
|
+ padding: 0 20px 20px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-row {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-card {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-icon {
|
|
|
+ margin-right: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.summary-icon-request {
|
|
|
+ color: rgb(var(--arcoblue-6));
|
|
|
+}
|
|
|
+
|
|
|
+.summary-icon-exception {
|
|
|
+ color: rgb(var(--orangered-6));
|
|
|
+}
|
|
|
+
|
|
|
+.trend-panel {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 20px 20px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: linear-gradient(180deg, var(--color-fill-1), var(--color-bg-1));
|
|
|
+}
|
|
|
+
|
|
|
+.trend-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.trend-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--color-text-1);
|
|
|
+}
|
|
|
+
|
|
|
+.trend-legend {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: var(--color-text-2);
|
|
|
+}
|
|
|
+
|
|
|
+.trend-dot {
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+.trend-dot-request {
|
|
|
+ background: #165dff;
|
|
|
+}
|
|
|
+
|
|
|
+.trend-dot-exception {
|
|
|
+ background: #f53f3f;
|
|
|
+}
|
|
|
+
|
|
|
+.report-alert {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.general-card) {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+</style>
|