mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-11-04 21:01:38 +08:00 
			
		
		
		
	feat: 完善仪表盘访问趋势区块内容
This commit is contained in:
		@@ -19,8 +19,11 @@ package top.charles7c.cnadmin.monitor.mapper;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
import org.apache.ibatis.annotations.Param;
 | 
			
		||||
 | 
			
		||||
import top.charles7c.cnadmin.common.base.BaseMapper;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.entity.LogDO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
 | 
			
		||||
 | 
			
		||||
@@ -39,9 +42,19 @@ public interface LogMapper extends BaseMapper<LogDO> {
 | 
			
		||||
     */
 | 
			
		||||
    DashboardTotalVO selectDashboardTotal();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询仪表盘访问趋势信息
 | 
			
		||||
     *
 | 
			
		||||
     * @param days
 | 
			
		||||
     *            日期数
 | 
			
		||||
     *
 | 
			
		||||
     * @return 仪表盘访问趋势信息
 | 
			
		||||
     */
 | 
			
		||||
    List<DashboardAccessTrendVO> selectListDashboardAccessTrend(@Param("days") Integer days);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询仪表盘热门模块列表
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * @return 仪表盘热门模块列表
 | 
			
		||||
     */
 | 
			
		||||
    List<DashboardPopularModuleVO> selectListDashboardPopularModule();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package top.charles7c.cnadmin.monitor.model.vo;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import io.swagger.v3.oas.annotations.media.Schema;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 仪表盘-访问趋势信息
 | 
			
		||||
 *
 | 
			
		||||
 * @author Charles7c
 | 
			
		||||
 * @since 2023/9/9 20:20
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
@Schema(description = "仪表盘-访问趋势信息")
 | 
			
		||||
public class DashboardAccessTrendVO implements Serializable {
 | 
			
		||||
 | 
			
		||||
    private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 日期
 | 
			
		||||
     */
 | 
			
		||||
    @Schema(description = "日期", example = "2023-08-08")
 | 
			
		||||
    private String date;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 浏览量(PV)
 | 
			
		||||
     */
 | 
			
		||||
    @Schema(description = "浏览量(PV)", example = "1000")
 | 
			
		||||
    private Long pvCount;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * IP 数
 | 
			
		||||
     */
 | 
			
		||||
    @Schema(description = "IP 数", example = "500")
 | 
			
		||||
    private Long ipCount;
 | 
			
		||||
}
 | 
			
		||||
@@ -18,6 +18,7 @@ package top.charles7c.cnadmin.monitor.service;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardGeoDistributionVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
 | 
			
		||||
@@ -38,16 +39,25 @@ public interface DashboardService {
 | 
			
		||||
     */
 | 
			
		||||
    DashboardTotalVO getTotal();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询访问趋势信息
 | 
			
		||||
     *
 | 
			
		||||
     * @param days
 | 
			
		||||
     *            日期数
 | 
			
		||||
     * @return 访问趋势信息
 | 
			
		||||
     */
 | 
			
		||||
    List<DashboardAccessTrendVO> listAccessTrend(Integer days);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询热门模块列表
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * @return 热门模块列表
 | 
			
		||||
     */
 | 
			
		||||
    List<DashboardPopularModuleVO> listPopularModule();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询访客地域分布信息
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * @return 访客地域分布信息
 | 
			
		||||
     */
 | 
			
		||||
    DashboardGeoDistributionVO getGeoDistribution();
 | 
			
		||||
 
 | 
			
		||||
@@ -83,9 +83,16 @@ public interface LogService {
 | 
			
		||||
     */
 | 
			
		||||
    DashboardTotalVO getDashboardTotal();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询仪表盘访问趋势信息
 | 
			
		||||
     *
 | 
			
		||||
     * @return 仪表盘访问趋势信息
 | 
			
		||||
     */
 | 
			
		||||
    List<DashboardAccessTrendVO> listDashboardAccessTrend(Integer days);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询仪表盘热门模块列表
 | 
			
		||||
     * 
 | 
			
		||||
     *
 | 
			
		||||
     * @return 仪表盘热门模块列表
 | 
			
		||||
     */
 | 
			
		||||
    List<DashboardPopularModuleVO> listDashboardPopularModule();
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ import org.springframework.stereotype.Service;
 | 
			
		||||
import cn.hutool.core.convert.Convert;
 | 
			
		||||
import cn.hutool.core.util.NumberUtil;
 | 
			
		||||
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardGeoDistributionVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
 | 
			
		||||
@@ -63,6 +64,11 @@ public class DashboardServiceImpl implements DashboardService {
 | 
			
		||||
        return totalVO;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<DashboardAccessTrendVO> listAccessTrend(Integer days) {
 | 
			
		||||
        return logService.listDashboardAccessTrend(days);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<DashboardPopularModuleVO> listPopularModule() {
 | 
			
		||||
        List<DashboardPopularModuleVO> popularModuleList = logService.listDashboardPopularModule();
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,11 @@ public class LogServiceImpl implements LogService {
 | 
			
		||||
        return logMapper.selectDashboardTotal();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<DashboardAccessTrendVO> listDashboardAccessTrend(Integer days) {
 | 
			
		||||
        return logMapper.selectListDashboardAccessTrend(days);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<DashboardPopularModuleVO> listDashboardPopularModule() {
 | 
			
		||||
        return logMapper.selectListDashboardPopularModule();
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,18 @@
 | 
			
		||||
            (SELECT COUNT(*) FROM `sys_log` WHERE DATE(`create_time`) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)) AS yesterdayPvCount
 | 
			
		||||
    </select>
 | 
			
		||||
 | 
			
		||||
    <select id="selectListDashboardAccessTrend"
 | 
			
		||||
            resultType="top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO">
 | 
			
		||||
        SELECT
 | 
			
		||||
            DATE(`create_time`) AS date,
 | 
			
		||||
            COUNT(*) AS pvCount,
 | 
			
		||||
            COUNT(DISTINCT `client_ip`) AS ipCount
 | 
			
		||||
        FROM `sys_log`
 | 
			
		||||
        GROUP BY DATE(`create_time`)
 | 
			
		||||
        ORDER BY DATE(`create_time`) DESC
 | 
			
		||||
        LIMIT #{days}
 | 
			
		||||
    </select>
 | 
			
		||||
 | 
			
		||||
    <select id="selectListDashboardPopularModule"
 | 
			
		||||
            resultType="top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO">
 | 
			
		||||
        SELECT
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
 | 
			
		||||
 | 
			
		||||
const BASE_URL = '/dashboard';
 | 
			
		||||
 | 
			
		||||
@@ -10,6 +9,12 @@ export interface DashboardTotalRecord {
 | 
			
		||||
  newPvFromYesterday: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DashboardAccessTrendRecord {
 | 
			
		||||
  date: string;
 | 
			
		||||
  pvCount: number;
 | 
			
		||||
  ipCount: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DashboardPopularModuleRecord {
 | 
			
		||||
  module: string;
 | 
			
		||||
  pvCount: number;
 | 
			
		||||
@@ -31,6 +36,12 @@ export function getTotal() {
 | 
			
		||||
  return axios.get<DashboardTotalRecord>(`${BASE_URL}/total`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function listAccessTrend(days: number) {
 | 
			
		||||
  return axios.get<DashboardAccessTrendRecord[]>(
 | 
			
		||||
    `${BASE_URL}/access/trend/${days}`
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function listPopularModule() {
 | 
			
		||||
  return axios.get<DashboardPopularModuleRecord[]>(
 | 
			
		||||
    `${BASE_URL}/popular/module`
 | 
			
		||||
@@ -46,12 +57,3 @@ export function getGeoDistribution() {
 | 
			
		||||
export function listAnnouncement() {
 | 
			
		||||
  return axios.get<DashboardAnnouncementRecord[]>(`${BASE_URL}/announcement`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ContentDataRecord {
 | 
			
		||||
  x: string;
 | 
			
		||||
  y: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function queryContentData() {
 | 
			
		||||
  return axios.get<ContentDataRecord[]>('/api/content-data');
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,216 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a-spin :loading="loading" style="width: 100%">
 | 
			
		||||
    <a-card
 | 
			
		||||
      class="general-card"
 | 
			
		||||
      :header-style="{ paddingBottom: 0 }"
 | 
			
		||||
      :body-style="{
 | 
			
		||||
        paddingTop: '20px',
 | 
			
		||||
      }"
 | 
			
		||||
      :title="$t('workplace.accessTrend')"
 | 
			
		||||
    >
 | 
			
		||||
      <template #extra>
 | 
			
		||||
        <a-radio-group
 | 
			
		||||
          v-model:model-value="dateRange"
 | 
			
		||||
          type="button"
 | 
			
		||||
          @change="handleDateRangeChange as any"
 | 
			
		||||
        >
 | 
			
		||||
          <a-radio :value="7">
 | 
			
		||||
            {{ $t('workplace.accessTrend.dateRange7') }}
 | 
			
		||||
          </a-radio>
 | 
			
		||||
          <a-radio :value="30">
 | 
			
		||||
            {{ $t('workplace.accessTrend.dateRange30') }}
 | 
			
		||||
          </a-radio>
 | 
			
		||||
        </a-radio-group>
 | 
			
		||||
      </template>
 | 
			
		||||
      <Chart height="289px" :option="chartOption" />
 | 
			
		||||
    </a-card>
 | 
			
		||||
  </a-spin>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
  import { ref } from 'vue';
 | 
			
		||||
  import useLoading from '@/hooks/loading';
 | 
			
		||||
  import {
 | 
			
		||||
    DashboardAccessTrendRecord,
 | 
			
		||||
    listAccessTrend,
 | 
			
		||||
  } from '@/api/common/dashboard';
 | 
			
		||||
  import useChartOption from '@/hooks/chart-option';
 | 
			
		||||
  import { ToolTipFormatterParams } from '@/types/echarts';
 | 
			
		||||
 | 
			
		||||
  const tooltipItemsHtmlString = (items: ToolTipFormatterParams[]) => {
 | 
			
		||||
    return items
 | 
			
		||||
      .map(
 | 
			
		||||
        (el) => `<div class="content-panel">
 | 
			
		||||
        <p>
 | 
			
		||||
          <span style="background-color: ${el.color}" class="tooltip-item-icon"></span>
 | 
			
		||||
          <span>${el.seriesName}</span>
 | 
			
		||||
        </p>
 | 
			
		||||
        <span class="tooltip-value">
 | 
			
		||||
        ${el.value}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>`
 | 
			
		||||
      )
 | 
			
		||||
      .join('');
 | 
			
		||||
  };
 | 
			
		||||
  const { loading, setLoading } = useLoading(true);
 | 
			
		||||
  const dateRange = ref(30);
 | 
			
		||||
  const xAxis = ref<string[]>([]);
 | 
			
		||||
  const pvStatisticsData = ref<number[]>([]);
 | 
			
		||||
  const ipStatisticsData = ref<number[]>([]);
 | 
			
		||||
  const { chartOption } = useChartOption((isDark) => {
 | 
			
		||||
    return {
 | 
			
		||||
      grid: {
 | 
			
		||||
        left: '30',
 | 
			
		||||
        right: '0',
 | 
			
		||||
        top: '10',
 | 
			
		||||
        bottom: '50',
 | 
			
		||||
      },
 | 
			
		||||
      legend: {
 | 
			
		||||
        bottom: -3,
 | 
			
		||||
        icon: 'circle',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#4E5969',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: {
 | 
			
		||||
        type: 'category',
 | 
			
		||||
        offset: 2,
 | 
			
		||||
        data: xAxis.value,
 | 
			
		||||
        boundaryGap: false,
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          color: '#4E5969',
 | 
			
		||||
          formatter(value: number, idx: number) {
 | 
			
		||||
            if (idx === 0) return '';
 | 
			
		||||
            if (idx === xAxis.value.length - 1) return '';
 | 
			
		||||
            return `${value}`;
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        axisLine: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        axisTick: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          show: true,
 | 
			
		||||
          interval: (idx: number) => {
 | 
			
		||||
            if (idx === 0) return false;
 | 
			
		||||
            return idx !== xAxis.value.length - 1;
 | 
			
		||||
          },
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: isDark ? '#3F3F3F' : '#E5E8EF',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          show: true,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: '#23ADFF',
 | 
			
		||||
            width: 2,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: {
 | 
			
		||||
        type: 'value',
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          formatter(value: any, idx: number) {
 | 
			
		||||
            if (idx === 0) return value;
 | 
			
		||||
            return `${value}`;
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        axisLine: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            type: 'dashed',
 | 
			
		||||
            color: isDark ? '#3F3F3F' : '#E5E8EF',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: true,
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        formatter(params) {
 | 
			
		||||
          const [firstElement] = params as ToolTipFormatterParams[];
 | 
			
		||||
          return `<div>
 | 
			
		||||
            <p class="tooltip-title">${firstElement.axisValueLabel}</p>
 | 
			
		||||
            ${tooltipItemsHtmlString(params as ToolTipFormatterParams[])}
 | 
			
		||||
          </div>`;
 | 
			
		||||
        },
 | 
			
		||||
        className: 'echarts-tooltip-diy',
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          name: '浏览量(PV)',
 | 
			
		||||
          data: pvStatisticsData.value,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          color: isDark ? '#3D72F6' : '#246EFF',
 | 
			
		||||
          symbol: 'circle',
 | 
			
		||||
          symbolSize: 10,
 | 
			
		||||
          emphasis: {
 | 
			
		||||
            focus: 'series',
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              borderWidth: 2,
 | 
			
		||||
              borderColor: '#E0E3FF',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'IP数',
 | 
			
		||||
          data: ipStatisticsData.value,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          color: isDark ? '#A079DC' : '#00B2FF',
 | 
			
		||||
          symbol: 'circle',
 | 
			
		||||
          symbolSize: 10,
 | 
			
		||||
          emphasis: {
 | 
			
		||||
            focus: 'series',
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              borderWidth: 2,
 | 
			
		||||
              borderColor: '#E2F2FF',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 查询趋势图信息
 | 
			
		||||
   *
 | 
			
		||||
   * @param days 日期数
 | 
			
		||||
   */
 | 
			
		||||
  const getList = async (days: number) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      xAxis.value = [];
 | 
			
		||||
      pvStatisticsData.value = [];
 | 
			
		||||
      ipStatisticsData.value = [];
 | 
			
		||||
      const { data: chartData } = await listAccessTrend(days);
 | 
			
		||||
      chartData.forEach((el: DashboardAccessTrendRecord) => {
 | 
			
		||||
        xAxis.value.unshift(el.date);
 | 
			
		||||
        pvStatisticsData.value.unshift(el.pvCount);
 | 
			
		||||
        ipStatisticsData.value.unshift(el.ipCount);
 | 
			
		||||
      });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // you can report use errorHandler or other
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 切换日期范围
 | 
			
		||||
   *
 | 
			
		||||
   * @param days 日期数
 | 
			
		||||
   */
 | 
			
		||||
  const handleDateRangeChange = (days: number) => {
 | 
			
		||||
    getList(days);
 | 
			
		||||
  };
 | 
			
		||||
  getList(30);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="less"></style>
 | 
			
		||||
@@ -1,200 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a-spin :loading="loading" style="width: 100%">
 | 
			
		||||
    <a-card
 | 
			
		||||
      class="general-card"
 | 
			
		||||
      :header-style="{ paddingBottom: 0 }"
 | 
			
		||||
      :body-style="{
 | 
			
		||||
        paddingTop: '20px',
 | 
			
		||||
      }"
 | 
			
		||||
      :title="$t('workplace.contentData')"
 | 
			
		||||
    >
 | 
			
		||||
      <template #extra>
 | 
			
		||||
        <a-link>{{ $t('workplace.viewMore') }}</a-link>
 | 
			
		||||
      </template>
 | 
			
		||||
      <Chart height="289px" :option="chartOption" />
 | 
			
		||||
    </a-card>
 | 
			
		||||
  </a-spin>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
  import { ref } from 'vue';
 | 
			
		||||
  import { graphic } from 'echarts';
 | 
			
		||||
  import useLoading from '@/hooks/loading';
 | 
			
		||||
  import { queryContentData, ContentDataRecord } from '@/api/common/dashboard';
 | 
			
		||||
  import useChartOption from '@/hooks/chart-option';
 | 
			
		||||
  import { ToolTipFormatterParams } from '@/types/echarts';
 | 
			
		||||
  import { AnyObject } from '@/types/global';
 | 
			
		||||
 | 
			
		||||
  function graphicFactory(side: AnyObject) {
 | 
			
		||||
    return {
 | 
			
		||||
      type: 'text',
 | 
			
		||||
      bottom: '8',
 | 
			
		||||
      ...side,
 | 
			
		||||
      style: {
 | 
			
		||||
        text: '',
 | 
			
		||||
        textAlign: 'center',
 | 
			
		||||
        fill: '#4E5969',
 | 
			
		||||
        fontSize: 12,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  const { loading, setLoading } = useLoading(true);
 | 
			
		||||
  const xAxis = ref<string[]>([]);
 | 
			
		||||
  const chartsData = ref<number[]>([]);
 | 
			
		||||
  const graphicElements = ref([
 | 
			
		||||
    graphicFactory({ left: '2.6%' }),
 | 
			
		||||
    graphicFactory({ right: 0 }),
 | 
			
		||||
  ]);
 | 
			
		||||
  const { chartOption } = useChartOption(() => {
 | 
			
		||||
    return {
 | 
			
		||||
      grid: {
 | 
			
		||||
        left: '2.6%',
 | 
			
		||||
        right: '0',
 | 
			
		||||
        top: '10',
 | 
			
		||||
        bottom: '30',
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: {
 | 
			
		||||
        type: 'category',
 | 
			
		||||
        offset: 2,
 | 
			
		||||
        data: xAxis.value,
 | 
			
		||||
        boundaryGap: false,
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          color: '#4E5969',
 | 
			
		||||
          formatter(value: number, idx: number) {
 | 
			
		||||
            if (idx === 0) return '';
 | 
			
		||||
            if (idx === xAxis.value.length - 1) return '';
 | 
			
		||||
            return `${value}`;
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        axisLine: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        axisTick: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          show: true,
 | 
			
		||||
          interval: (idx: number) => {
 | 
			
		||||
            if (idx === 0) return false;
 | 
			
		||||
            if (idx === xAxis.value.length - 1) return false;
 | 
			
		||||
            return true;
 | 
			
		||||
          },
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: '#E5E8EF',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          show: true,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            color: '#23ADFF',
 | 
			
		||||
            width: 2,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      yAxis: {
 | 
			
		||||
        type: 'value',
 | 
			
		||||
        axisLine: {
 | 
			
		||||
          show: false,
 | 
			
		||||
        },
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          formatter(value: any, idx: number) {
 | 
			
		||||
            if (idx === 0) return value;
 | 
			
		||||
            return `${value}k`;
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        splitLine: {
 | 
			
		||||
          show: true,
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            type: 'dashed',
 | 
			
		||||
            color: '#E5E8EF',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        formatter(params) {
 | 
			
		||||
          const [firstElement] = params as ToolTipFormatterParams[];
 | 
			
		||||
          return `<div>
 | 
			
		||||
            <p class="tooltip-title">${firstElement.axisValueLabel}</p>
 | 
			
		||||
            <div class="content-panel"><span>总内容量</span><span class="tooltip-value">${(
 | 
			
		||||
              Number(firstElement.value) * 10000
 | 
			
		||||
            ).toLocaleString()}</span></div>
 | 
			
		||||
          </div>`;
 | 
			
		||||
        },
 | 
			
		||||
        className: 'echarts-tooltip-diy',
 | 
			
		||||
      },
 | 
			
		||||
      graphic: {
 | 
			
		||||
        elements: graphicElements.value,
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          data: chartsData.value,
 | 
			
		||||
          type: 'line',
 | 
			
		||||
          smooth: true,
 | 
			
		||||
          // symbol: 'circle',
 | 
			
		||||
          symbolSize: 12,
 | 
			
		||||
          emphasis: {
 | 
			
		||||
            focus: 'series',
 | 
			
		||||
            itemStyle: {
 | 
			
		||||
              borderWidth: 2,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          lineStyle: {
 | 
			
		||||
            width: 3,
 | 
			
		||||
            color: new graphic.LinearGradient(0, 0, 1, 0, [
 | 
			
		||||
              {
 | 
			
		||||
                offset: 0,
 | 
			
		||||
                color: 'rgba(30, 231, 255, 1)',
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                offset: 0.5,
 | 
			
		||||
                color: 'rgba(36, 154, 255, 1)',
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                offset: 1,
 | 
			
		||||
                color: 'rgba(111, 66, 251, 1)',
 | 
			
		||||
              },
 | 
			
		||||
            ]),
 | 
			
		||||
          },
 | 
			
		||||
          showSymbol: false,
 | 
			
		||||
          areaStyle: {
 | 
			
		||||
            opacity: 0.8,
 | 
			
		||||
            color: new graphic.LinearGradient(0, 0, 0, 1, [
 | 
			
		||||
              {
 | 
			
		||||
                offset: 0,
 | 
			
		||||
                color: 'rgba(17, 126, 255, 0.16)',
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                offset: 1,
 | 
			
		||||
                color: 'rgba(17, 128, 255, 0)',
 | 
			
		||||
              },
 | 
			
		||||
            ]),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
  const fetchData = async () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const { data: chartData } = await queryContentData();
 | 
			
		||||
      chartData.forEach((el: ContentDataRecord, idx: number) => {
 | 
			
		||||
        xAxis.value.push(el.x);
 | 
			
		||||
        chartsData.value.push(el.y);
 | 
			
		||||
        if (idx === 0) {
 | 
			
		||||
          graphicElements.value[0].style.text = el.x;
 | 
			
		||||
        }
 | 
			
		||||
        if (idx === chartData.length - 1) {
 | 
			
		||||
          graphicElements.value[1].style.text = el.x;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // you can report use errorHandler or other
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  fetchData();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="less"></style>
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
    :title="$t('workplace.docs')"
 | 
			
		||||
    :header-style="{ paddingBottom: 0 }"
 | 
			
		||||
    :body-style="{ paddingTop: '10px', paddingBottom: '10px' }"
 | 
			
		||||
    style="height: 198px"
 | 
			
		||||
    style="height: 200px"
 | 
			
		||||
  >
 | 
			
		||||
    <template #extra>
 | 
			
		||||
      <a-link href="https://doc.charles7c.top" target="_blank" rel="noopener">{{
 | 
			
		||||
@@ -70,10 +70,11 @@
 | 
			
		||||
  </a-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts"></script>
 | 
			
		||||
 | 
			
		||||
<style lang="less" scoped>
 | 
			
		||||
  .arco-card-body .arco-link {
 | 
			
		||||
    margin: 10px 0;
 | 
			
		||||
    color: rgb(var(--gray-8));
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
<script setup lang="ts"></script>
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
      class="general-card"
 | 
			
		||||
      :header-style="{ paddingBottom: '0' }"
 | 
			
		||||
      :body-style="{
 | 
			
		||||
        padding: '0 20px',
 | 
			
		||||
        padding: '0 20px 15px 20px',
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      <template #title>
 | 
			
		||||
@@ -58,6 +58,9 @@
 | 
			
		||||
        itemStyle: {
 | 
			
		||||
          borderWidth: 0,
 | 
			
		||||
        },
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: '#4E5969',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: true,
 | 
			
		||||
@@ -66,15 +69,15 @@
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'pie',
 | 
			
		||||
          radius: '70%',
 | 
			
		||||
          radius: '65%',
 | 
			
		||||
          label: {
 | 
			
		||||
            formatter: '{d}%',
 | 
			
		||||
            fontSize: 14,
 | 
			
		||||
            color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969',
 | 
			
		||||
          },
 | 
			
		||||
          itemStyle: {
 | 
			
		||||
            borderColor: isDark ? '#232324' : '#fff',
 | 
			
		||||
            borderWidth: 1,
 | 
			
		||||
            borderColor: '#D9F6FF',
 | 
			
		||||
          },
 | 
			
		||||
          data: statisticsData.value.locationIpStatistics,
 | 
			
		||||
        },
 | 
			
		||||
@@ -85,6 +88,6 @@
 | 
			
		||||
 | 
			
		||||
<style scoped lang="less">
 | 
			
		||||
  .general-card {
 | 
			
		||||
    min-height: 566px;
 | 
			
		||||
    min-height: 568px;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
  import Banner from './components/banner.vue';
 | 
			
		||||
  import DataPanel from './components/data-panel.vue';
 | 
			
		||||
  import ContentChart from './components/content-chart.vue';
 | 
			
		||||
  import ContentChart from './components/access-trend.vue';
 | 
			
		||||
  import PopularModule from './components/popular-module.vue';
 | 
			
		||||
  import CategoriesPercent from './components/geo-distribution.vue';
 | 
			
		||||
  import RecentlyVisited from './components/recently-visited.vue';
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,9 @@ export default {
 | 
			
		||||
  'workplace.allProject': 'All',
 | 
			
		||||
  'workplace.loadMore': 'More',
 | 
			
		||||
  'workplace.viewMore': 'More',
 | 
			
		||||
  'workplace.contentData': 'Content Data',
 | 
			
		||||
  'workplace.accessTrend': 'Access Trend',
 | 
			
		||||
  'workplace.accessTrend.dateRange7': 'Last 7 Days',
 | 
			
		||||
  'workplace.accessTrend.dateRange30': 'Last 30 Days',
 | 
			
		||||
  'workplace.popularModule': 'Popular Module(Top10)',
 | 
			
		||||
  'workplace.geoDistribution': 'Geo Distribution(Top10)',
 | 
			
		||||
  'workplace.unit.pecs': 'pecs',
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,9 @@ export default {
 | 
			
		||||
  'workplace.allProject': '所有项目',
 | 
			
		||||
  'workplace.loadMore': '加载更多',
 | 
			
		||||
  'workplace.viewMore': '查看更多',
 | 
			
		||||
  'workplace.contentData': '内容数据',
 | 
			
		||||
  'workplace.accessTrend': '访问趋势',
 | 
			
		||||
  'workplace.accessTrend.dateRange7': '近7天',
 | 
			
		||||
  'workplace.accessTrend.dateRange30': '近30天',
 | 
			
		||||
  'workplace.popularModule': '热门模块(Top10)',
 | 
			
		||||
  'workplace.geoDistribution': '访客地域分布(Top10)',
 | 
			
		||||
  'workplace.unit.pecs': '个',
 | 
			
		||||
 
 | 
			
		||||
@@ -21,15 +21,20 @@ import java.util.List;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
 | 
			
		||||
import io.swagger.v3.oas.annotations.Operation;
 | 
			
		||||
import io.swagger.v3.oas.annotations.Parameter;
 | 
			
		||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
 | 
			
		||||
import io.swagger.v3.oas.annotations.tags.Tag;
 | 
			
		||||
 | 
			
		||||
import org.springframework.validation.annotation.Validated;
 | 
			
		||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.PathVariable;
 | 
			
		||||
import org.springframework.web.bind.annotation.RequestMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController;
 | 
			
		||||
 | 
			
		||||
import top.charles7c.cnadmin.common.model.vo.R;
 | 
			
		||||
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.annotation.Log;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardGeoDistributionVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
 | 
			
		||||
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
 | 
			
		||||
@@ -58,6 +63,14 @@ public class DashboardController {
 | 
			
		||||
        return R.ok(dashboardService.getTotal());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Operation(summary = "查询访问趋势信息", description = "查询访问趋势信息")
 | 
			
		||||
    @Parameter(name = "days", description = "日期数", example = "30", in = ParameterIn.PATH)
 | 
			
		||||
    @GetMapping("/access/trend/{days}")
 | 
			
		||||
    public R<List<DashboardAccessTrendVO>> listAccessTrend(@PathVariable Integer days) {
 | 
			
		||||
        ValidationUtils.throwIf(7 != days && 30 != days, "仅支持查询近 7/30 天访问趋势信息");
 | 
			
		||||
        return R.ok(dashboardService.listAccessTrend(days));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Operation(summary = "查询热门模块列表", description = "查询热门模块列表")
 | 
			
		||||
    @GetMapping("/popular/module")
 | 
			
		||||
    public R<List<DashboardPopularModuleVO>> listPopularModule() {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user