| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- <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="channels" label="渠道">
- <a-select v-model="formModel.channels" multiple allow-clear placeholder="全部渠道"
- @change="search">
- <a-option v-for="item in channelOptions" :key="item.value" :value="item.value">
- {{ item.label }}
- </a-option>
- </a-select>
- </a-form-item>
- </a-col>
- <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 #channel="{ record }">
- <a-tag :color="channelColorMap[record.channel] || 'gray'">
- {{ channelLabelMap[record.channel] || record.channel }}
- </a-tag>
- </template>
- <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';
- import {
- zlongForwardChannelColorMap as channelColorMap,
- zlongForwardChannelLabelMap as channelLabelMap,
- zlongForwardChannelOptions as channelOptions,
- zlongForwardChannelValues,
- normalizeZlongForwardChannels,
- } from './zlongForwardChannels';
- 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({
- channels: [...zlongForwardChannelValues],
- 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 normalizeChannels = (channels?: string[]) => {
- const list = normalizeZlongForwardChannels(channels);
- return list.length > 0 ? list : [...zlongForwardChannelValues];
- };
- const buildChannelsValue = (channels?: string[]) => normalizeChannels(channels).join(',');
- const normalizeRecord = (item: XhsZlongForwardReportRecord): XhsZlongForwardReportRecord => ({
- ...item,
- channel: String(item.channel || '').toLowerCase(),
- total: toNumber(item.total),
- exception_count: toNumber(item.exception_count),
- });
- const sortedChartData = computed(() => {
- const grouped = new Map<string, XhsZlongForwardReportRecord>();
- chartData.value.forEach((item) => {
- const key = String(item.report_time || item.display_time || '');
- const current = grouped.get(key);
- if (current) {
- current.total += toNumber(item.total);
- current.exception_count += toNumber(item.exception_count);
- return;
- }
- grouped.set(key, {
- ...item,
- total: toNumber(item.total),
- exception_count: toNumber(item.exception_count),
- });
- });
- return Array.from(grouped.values()).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: 'channel',
- width: 110,
- },
- {
- 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,
- channels: buildChannelsValue(formModel.channels),
- 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>
|