三步实现SpringBoot全局日志记录,整合Echarts实现数据大屏


🚀 注重版权,转载请注明原作者和原文链接

  • 小袁博客:https://boke.open-yuan.com/
  • 小袁博客后台:https://boke.open-yuan.com/back-manager/
  • 更多项目内容关注小红书🔍OpenYuan开袁 http://xhslink.com/I9zNaC
  • 有需求可以在小袁博客首页加我微信或者QQ

效果展示

image.png
image.png

MySQL

建表

CREATE TABLE `access_log` (
  `access_log_id` bigint NOT NULL AUTO_INCREMENT,
  `access_time` datetime NOT NULL COMMENT '访问时间',
  `access_ip` varchar(30) NOT NULL COMMENT '访问IP',
  `api_group` varchar(50) NOT NULL DEFAULT '默认' COMMENT '接口分组',
  `req_url` varchar(100) NOT NULL COMMENT '请求URL',
  `req_method` varchar(10) NOT NULL COMMENT '请求方式',
  `os` varchar(100) NULL DEFAULT NULL COMMENT '操作系统',
  `browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器',
  `lsp` varchar(15) NULL DEFAULT NULL COMMENT '运营商',
  `country` varchar(15) NULL DEFAULT NULL COMMENT '国家',
  `province` varchar(15) NULL DEFAULT NULL COMMENT '省',
  `city` varchar(15) NULL DEFAULT NULL COMMENT '城市',
  PRIMARY KEY (`access_log_id`)
) COMMENT='访问日志表';

后端

POJO实体

@Data
@TableName(value = "access_log")
public class AccessLog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 访问日志ID  
     */
    @TableId(type = IdType.AUTO)
    private Long accessLogId;

    /**
     * 访问时间  
     */
    private Date accessTime;

    /**
     * 访问IP  
     */
    private String accessIp;

    /**
     * 接口分组  
     */
    private String apiGroup;

    /**
     * 请求URL  
     */
    private String reqUrl;

    /**
     * 请求方式  
     */
    private String reqMethod;

    /**
     * 操作系统  
     */
    private String os;

    /**
     * 浏览器  
     */
    private String browser;

    /**
     * 运营商  
     */
    private String lsp;

    /**
     * 国家  
     */
    private String country;

    /**
     * 省  
     */
    private String province;

    /**
     * 城市  
     */
    private String city;
}

Mapper接口

@Repository
public interface AccessLogMapper extends BaseMapper<AccessLog> {
    
}

自定义注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 给接口分组
     */
    String apiGroup() default "默认";
}

案例:

@Log(apiGroup = "文章模块")
@RestController
@RequestMapping("/article")
public class ArticleController {
    ......
}

拦截器

ps:https://api.vvhan.com/api/getIpInfo?ip=[你的IP],这个网址是一个免费获取国家、省、市、运营商的地址
当然这种对IP地址的解析应该是放在定时任务中,每天晚上定时解析日志IP,如果解析IP的API挂了,接口会受到影响,我这里只是为了方便写在这里

@Slf4j
@Component
public class AccessLogInterceptor implements HandlerInterceptor {
    
    @Autowired
    private AccessLogMapper accessLogMapper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        try {
            // 获取客户端真是IP地址,这种网上很多现成代码
            String accessIp = NetUtil.getRemoteHost(request);
            // 获取User-Agent
            String requestUserAgent = request.getHeader("User-Agent");
            // 获取浏览器用户标识
            UserAgent userAgent = UserAgentUtil.parse(requestUserAgent);
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Log logAnnotation = handlerMethod.getMethod().getDeclaringClass().getAnnotation(Log.class);

            AccessLog accessLog = new AccessLog();
            accessLog.setAccessIp(accessIp);
            accessLog.setAccessTime(new Date());
            if (logAnnotation != null) {
                accessLog.setApiGroup(logAnnotation.apiGroup());
            }
            accessLog.setReqUrl(request.getRequestURI());
            accessLog.setReqMethod(request.getMethod());
            accessLog.setOs(userAgent.getOs().getName());
            accessLog.setBrowser(userAgent.getBrowser().getName());

            // 解析IP
            try {
                String ipParseStr = HttpUtil.get("https://api.vvhan.com/api/getIpInfo?ip=" + accessIp);
                JSONObject ipParseJson = JSONUtil.parseObj(ipParseStr);
                if (ipParseJson.getBool("success")) {
                    JSONObject infoJson = ipParseJson.getJSONObject("info");
                    accessLog.setLsp(infoJson.getStr("lsp"));
                    accessLog.setCountry(infoJson.getStr("country"));
                    accessLog.setProvince(infoJson.getStr("prov"));
                    accessLog.setCity(infoJson.getStr("city"));
                }
            } catch (Exception e) {
                accessLog.setLsp("未知");
                accessLog.setCountry("未知");
                accessLog.setProvince("未知");
                accessLog.setCity("未知");
            }
            accessLogMapper.insert(accessLog);
        } catch (Exception e) {
            log.error("", e);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

注册拦截器

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
    
    @Autowired
    private AccessLogInterceptor accessLogInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册全局日志拦截器
        registry.addInterceptor(accessLogInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/static/**")
                .excludePathPatterns("/error");

        // 其他拦截器......
    }
}

测试

到这里就完成一个简单的全局日志拦截器了,随便发几个请求测试一下,成功记录入库!
image.png

进阶——整合Echarts实现数据大屏

数据VO实体

浏览器访问占比情况VO
@Data
public class AccessBrowserGroupVo {
    
    private String browser;
    
    private Integer count;
}
运营商访问占比情况VO
@Data
public class AccessLspGroupVo {
    
    private String lsp;
    
    private Integer count;
}
各省份访问情况VO
@Data
public class AccessProvinceGroupVo {
    
    private String province;
    
    private Integer count;
}
每天访问情况VO
@Data
public class AccessTimeGroupVo {
    
    private String accessTime;
    
    private Integer count;
}

查询SQL

浏览器访问统计
<select id="countBrowserGroupAccess" resultType="com.xiaoyuan.common.vo.logs.AccessBrowserGroupVo">
  select browser, count(*) as count from access_log group by browser
</select>
运营商访问统计
<select id="countLspGroupAccess" resultType="com.xiaoyuan.common.vo.logs.AccessLspGroupVo">
  select lsp, count(*) as count from access_log group by lsp
</select>
各省份访问统计
<select id="countProvinceGroupAccess" resultType="com.xiaoyuan.common.vo.logs.AccessProvinceGroupVo">
  select province, count(*) as count from access_log group by province order by count desc limit 15
</select>
近15天内访问统计
<select id="countTimeGroupAccess" resultType="com.xiaoyuan.common.vo.logs.AccessTimeGroupVo">
    SELECT
        date_match.date_c as access_time,
        IFNULL( count, 0 ) as count
    FROM
        (
            SELECT
                DATE_FORMAT( @now := date_sub( @now, INTERVAL 1 DAY ), '%Y-%m-%d' ) AS date_c
            FROM
                ( SELECT @now := date_add( CURDATE(), INTERVAL 1 DAY ) FROM access_log LIMIT 15 ) date_match
            ORDER BY
                date_c
        ) date_match
            LEFT JOIN (
            SELECT
                DATE_FORMAT( access_time, '%Y-%m-%d' ) AS access_time,
                count(*) AS count
            FROM
                access_log
            WHERE
                    access_time >= (
                    SELECT
                        date_sub( curdate(), INTERVAL 15 DAY ))
            GROUP BY
                DATE_FORMAT( access_time, '%Y-%m-%d' )
        ) acc ON acc.access_time = date_match.date_c
</select>

后端业务

封装统一接口返回
/**
 * FileName:    R
 * Author:      小袁
 * Date:        2022/3/12 12:23
 * Description: 统一结果返回的类
 */
@Data
public class R<T> {

    private Boolean success;

    private Integer code;

    private String message;

    private T data;

    // 成功静态方法
    public static <T> R<T> success() {
        R<T> r = new R<>();
        r.setSuccess(true);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.setMessage(HttpStatusEnum.SUCCESS.getName());
        return r;
    }

    public static <T> R<T> success(String message) {
        R<T> r = new R<>();
        r.setSuccess(true);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.message(message);
        return r;
    }

    public static <T> R<T> success(T object) {
        R<T> r = new R<>();
        r.setData(object);
        r.setSuccess(true);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.setMessage(HttpStatusEnum.SUCCESS.getName());
        return r;
    }

    public static <T> R<T> success(String msg, T object) {
        R<T> r = new R<>();
        r.setData(object);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.setMessage(msg);
        return r;
    }

    // 失败静态方法
    public static <T> R<T> fail() {
        R<T> r = new R<>();
        r.setSuccess(false);
        r.setCode(HttpStatusEnum.FAIL.getCode());
        r.setMessage(HttpStatusEnum.FAIL.getName());
        return r;
    }

    public static <T> R<T> fail(String msg) {
        R<T> r = new R<>();
        r.setSuccess(false);
        r.setCode(HttpStatusEnum.FAIL.getCode());
        r.setMessage(msg);
        return r;
    }

    public static <T> R<T> fail(HttpStatusEnum httpStatusEnum) {
        R<T> r = new R<>();
        r.setSuccess(false);
        r.setCode(httpStatusEnum.getCode());
        r.setMessage(httpStatusEnum.getName());
        return r;
    }

    public R<T> message(String message){
        this.setMessage(message);
        return this;
    }

    public R<T> code(Integer code){
        this.setCode(code);
        return this;
    }

    public R<T> data(T data){
        this.setData(data);
        return this;
    }
}
封装客户端响应码
/**
 * FileName:    Code
 * Author:      小袁
 * Date:        2022/5/1 23:29
 * Description: 客户端响应状态码
 */
public enum HttpStatusEnum implements BaseCodeEnum {

    SUCCESS(200, "成功"),
    FAIL(20001, "失败"),
    INTERNAL_SERVER_ERROR(500, "服务器异常"),

    private final Integer code;
    private final String name;

    HttpStatusEnum(int code, String msg) {
        this.code = code;
        this.name = msg;
    }

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getName() {
        return this.name;
    }
}
Mapper接口
@Repository
public interface AccessLogMapper extends BaseMapper<AccessLog> {

    List<AccessLspGroupVo> countLspGroupAccess();

    List<AccessBrowserGroupVo> countBrowserGroupAccess();

    List<AccessProvinceGroupVo> countProvinceGroupAccess();

    List<AccessTimeGroupVo> countTimeGroupAccess();
}
Service接口
public interface AccessLogService extends IService<AccessLog> {

    List<AccessLspGroupVo> countLspGroupAccess();

    List<AccessBrowserGroupVo> countBrowserGroupAccess();

    List<AccessProvinceGroupVo> countProvinceGroupAccess();

    List<AccessTimeGroupVo> countTimeGroupAccess();
}
Service实现类
@Slf4j
@Service
public class AccessLogServiceImpl extends ServiceImpl<AccessLogMapper, AccessLog> implements AccessLogService {
    
    @Override
    public List<AccessLspGroupVo> countLspGroupAccess() {
        return this.baseMapper.countLspGroupAccess();
    }

    @Override
    public List<AccessBrowserGroupVo> countBrowserGroupAccess() {
        return this.baseMapper.countBrowserGroupAccess();
    }

    @Override
    public List<AccessProvinceGroupVo> countProvinceGroupAccess() {
        return this.baseMapper.countProvinceGroupAccess();
    }

    @Override
    public List<AccessTimeGroupVo> countTimeGroupAccess() {
        return this.baseMapper.countTimeGroupAccess();
    }
}
Controller接口
@RestController
@RequestMapping("/stat/access")
public class AccessStatController {
    
    @Autowired
    private AccessLogService accessLogService;

    /**
     * 查询15天内的访问次数情况-折线图
     */
    @GetMapping("/query_line_by_day")
    public R<List<AccessTimeGroupVo>> queryAccessLogByTimeGroup() {
        return R.success(accessLogService.countTimeGroupAccess());
    }

    /**
     * 查询省份访问占比-柱形图
     */
    @GetMapping("/query_col_by_province")
    public R<List<AccessProvinceGroupVo>> queryAccessLogByProvinceGroup() {
        return R.success(accessLogService.countProvinceGroupAccess());
    }

    /**
     * 查询运营商访问占比-饼图
     */
    @GetMapping("/query_pie_by_lsp")
    public R<List<AccessLspGroupVo>> queryAccessLogByLspGroup() {
        return R.success(accessLogService.countLspGroupAccess());
    }

    /**
     * 查询浏览器访问占比-饼图
     */
    @GetMapping("/query_pie_by_browser")
    public R<List<AccessBrowserGroupVo>> queryAccessLogByBrowserGroup() {
        return R.success(accessLogService.countBrowserGroupAccess());
    }
}

前端配置

安装axios、echarts
npm install axios
npm install echarts
封装request
import axios from 'axios'
import { Message, MessageBox,} from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
import router from '@/router'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api 的 base_url
  // timeout: 5000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
  config => {
    if (store.getters.token) {
     config.headers['token'] = getToken()
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// response 拦截器
service.interceptors.response.use(
  response => {
    /**
     * code为非200是抛错 可结合自己业务进行修改
     */
    const res = response.data

    const url = response.config.url

    if (res.code !== 200) {
      if (url.indexOf("/login") < 0 && res.code === 40005) {
        store.dispatch('FedLogOut').then(() => {
          router.push(`/login`)
        })
        Message({
          message: res.message,
          type: 'warning',
          duration: 2 * 1000,
        })

        return Promise.resolve(res)
      }else if (res.code >= 40000) {
        Message({
          message: res.message,
          type: 'error',
          duration: 3 * 1000
        })
        return Promise.resolve(res)
      }else {
        Message({
          message: res.message,
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(new Error(res.message || 'Error'))
      }
    } else {
      return res
    }
  },
  error => {
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service
定义API
import request from "../../utils/request";

export default {
  getAccessStatByTime() {
    return request({
      url: '/stat/access/query_line_by_day',
      method: 'get'
    })
  },

  getAccessStatByProvince() {
    return request({
      url: '/stat/access/query_col_by_province',
      method: 'get'
    })
  },

  getAccessStatByLsp() {
    return request({
      url: '/stat/access/query_pie_by_lsp',
      method: 'get'
    })
  },

  getAccessStatByBrowser() {
    return request({
      url: '/stat/access/query_pie_by_browser',
      method: 'get'
    })
  },
}
封装echarts
<template>
  <div :id="id" :class="className" :style="{ height: height, width: width }" />
</template>

<script>
import * as echarts from 'echarts'
export default {
  name: 'echart',
  props: {
    className: {
      type: String,
      default: 'chart'
    },
    id: {
      type: String,
      default: 'chart'
    },
    width: {
      type: String,
      default: '100%'
    },
    height: {
      type: String,
      default: '2.5rem'
    },
    options: {
      type: Object,
      default: ()=>({})
    }
  },
  data () {
    return {
      chart: null
    }
  },
  watch: {
    options: {
      handler (options) {
        // 设置true清空echart缓存
        this.chart.setOption(options, true)
      },
      deep: true
    }
  },
  mounted () {
    // echarts.registerTheme('tdTheme', tdTheme); // 覆盖默认主题
    this.initChart();
  },
  beforeDestroy () {
    this.chart.dispose()
    this.chart = null
  },
  methods: {
    initChart () {
      // 初始化echart
      this.chart = echarts.init(this.$el)
      this.chart.setOption(this.options, true)
    }
  }
}
</script>

<style>
body {
  margin: 0;
  padding: 0;
}
</style>
饼图-浏览器访问占比
<template>
  <div>
    <Echart :options="options"
      id="lspEcharts"
      height="300px"
      width="100%"/>
  </div>
</template>

<script>
  import statAccess from "@/api/stat/statAccess";
  import Echart from "@/components/Echart/index.vue";

  export default {
    components: {
      Echart
    },
    data() {
      return {
        options: {},
        lspData: []
      }
    },
    methods: {
      initData() {
        statAccess.getAccessStatByBrowser().then(res => {
          this.lspData = res.data.map(obj => {
            return {
              name: obj.browser,
              value: obj.count
            }
          })
          this.executeDraw()
        })
      },
      executeDraw() {
        this.options = {
          title: {
            text: '浏览器访问占比',
            left: 'center',
            textStyle: {
              color: '#FDF5E6'
            }
          },
          tooltip: {
            trigger: 'item'
          },
          legend: {
            orient: 'vertical',
            left: 'left',
            top: '20%',
            textStyle: {
              color: '#FDF5E6'
            }
          },
          series: [
            {
              name: '访问次数',
              type: 'pie',
              radius: '90%',
              top: '20%',
              data: this.lspData,
              label: {
                color: '#FDF5E6'
              },
              emphasis: {
                itemStyle: {
                  shadowBlur: 10,
                  shadowOffsetX: 0,
                  shadowColor: 'rgba(0, 0, 0, 0.5)'
                }
              }
            }
          ]
        }
      }
    },
    mounted() {
      this.initData()
    }
  }
</script>
饼图-运营商访问占比
<template>
  <div>
    <Echart :options="options"
            id="lspEcharts"
            height="300px"
            width="100%"/>
  </div>
</template>

<script>
import statAccess from "@/api/stat/statAccess";
import Echart from "@/components/Echart/index.vue";

export default {
  components: {
    Echart
  },
  data() {
    return {
      options: {},
      lspData: []
    }
  },
  methods: {
    initData() {
      statAccess.getAccessStatByLsp().then(res => {
        this.lspData = res.data.map(obj => {
          return {
            name: obj.lsp,
            value: obj.count
          }
        })
        this.executeDraw()
      })
    },
    executeDraw() {
      this.options = {
        title: {
          text: '运营商访问占比',
          left: 'center',
          textStyle: {
            color: '#FDF5E6'
          }
        },
        tooltip: {
          trigger: 'item'
        },
        legend: {
          orient: 'vertical',
          left: 'left',
          top: '20%',
          textStyle: {
            color: '#FDF5E6'
          }
        },
        series: [
          {
            name: '访问次数',
            type: 'pie',
            radius: '90%',
            top: '20%',
            data: this.lspData,
            label: {
              color: '#FDF5E6'
            },
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.5)'
              }
            }
          }
        ]
      }
    }
  },
  mounted() {
    this.initData()
  }
}
</script>
折线图-每天访问量情况
<template>
  <div>
    <Echart :options="options"
            id="timeEcharts"
            height="400px"
            width="100%"/>
  </div>
</template>

<script>
import statAccess from "@/api/stat/statAccess";
import Echart from "@/components/Echart/index.vue";

export default {
  components: {
    Echart
  },
  data() {
    return {
      options: {},
      xAxis: [],
      yAxis: []
    }
  },
  methods: {
    initData() {
      statAccess.getAccessStatByTime().then(res => {
        let x = []
        let y = []
        for (let i = 0; i < res.data.length; i++) {
          x.push(res.data[i].accessTime)
          y.push(res.data[i].count)
        }
        this.xAxis = x
        this.yAxis = y
        this.executeDraw()
      })
    },
    executeDraw() {
      this.options = {
        title: {
          show: true,
          text: '小袁博客15天内访问情况统计',
          left: 'center',
          textStyle: {
            color: '#FDF5E6'
          }
        },
        legend: {
          show: true,
          left: '1%',
          textStyle: {
            color: '#FDF5E6'
          }
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: { type: 'line' }
        },
        grid:{
          left:"1%",
          right:"1%",
          bottom:"1%",
          containLabel:true,
        },
        xAxis: {
          type: 'category',
          data: this.xAxis,
          boundaryGap: ['5%', '5%',],//坐标轴两边留白
          axisLabel: {
            color: '#FDF5E6'
          },
          axisLine: {//坐标轴
            lineStyle:{
              opacity: 0.01,//设置透明度就可以控制显示不显示
            },
          },
          splitLine: {//网格线
            lineStyle:{
              color: '#eeeeee',
            },
          },
          axisTick: {//刻度线
            show: false,//去掉刻度线
          },
        },
        yAxis: {
          type: 'value',
          name:'次         ',//是基于Y轴线对齐,用空格站位让坐标轴名称与刻度名称对齐
          axisLabel: {
            color: '#eee'
          },
          nameTextStyle: {
            color:'#444e65',
            align:'left',//文字水平对齐方式
            verticalAlign:'middle',//文字垂直对齐方式
          },
          axisTick: {//刻度线
            show: false,//去掉刻度线
          },
          axisLine: {//坐标轴线
            lineStyle:{
              opacity: 0,//透明度为0
            },
          },
          splitLine: {//网格线
            show: true,//网格线
            lineStyle:{
              color: 'rgba(211, 211, 211, 0.5)',
            },
          },
        },
        series: [
          {
            data: this.yAxis,
            smooth: true,
            type: 'line',
            name: '访问次数',
            itemStyle: {//折线拐点标志的样式。
              normal: {
                color: '#98F5FF',
              },
            },
          }
        ]
      }
    }
  },
  mounted() {
    this.initData()
  }
}
</script>
柱状图-各省份访问情况
<template>
  <div>
    <Echart :options="options"
            id="lspEcharts"
            height="300px"
            width="100%"/>
  </div>
</template>

<script>
import statAccess from "@/api/stat/statAccess";
import Echart from "@/components/Echart/index.vue";

export default {
  components: {
    Echart
  },
  data() {
    return {
      options: {},
      axis: [],
      series: []
    }
  },
  methods: {
    initData() {
      statAccess.getAccessStatByProvince().then(res => {
        let x = []
        let y = []
        for (let i = 0; i < res.data.length; i++) {
          x.push(res.data[i].province)
          y.push(res.data[i].count)
        }
        this.axis = x
        this.series = y
        this.executeDraw()
      })
    },
    executeDraw() {
      this.options = {
        tooltip: {},
        title: {
          text: '各城市访问情况占比',
          left: 'center',
          textStyle: {
            color: '#FDF5E6'
          }
        },
        grid:{
          left: "1%",
          right: "1%",
          bottom: "1%",
          containLabel: true,
        },
        legend: {
          show: true,
          left: '1%',
          textStyle: {
            color: '#FDF5E6'
          }
        },
        xAxis: {
          data: this.axis,
          axisLine: {
            show: false,
          },
          axisLabel: {
            color: '#FDF5E6'
          }
        },
        yAxis: {
          // 网格样式
          splitLine: {
            show: false,
          },
          axisLabel: {
            color: '#FDF5E6'
          }
        },
        series: [{
          name: '访问量',
          type: 'bar',
          data: this.series,
          barWidth: 15,
          itemStyle: {
            color: {
              type:'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [
                {
                  offset: 0,
                  color: 'rgba(255, 130, 71, 1)',
                },
                {
                  offset: 1,
                  color: 'rgba(255, 130, 71, 0.5)',
                },
              ],
              globaCoord: false,
            },
            barBorderRadius: [5, 5, 0, 0], // (顺时针左上,右上,右下,左下)
          },
        }],
      }
    }
  },
  mounted() {
    this.initData()
  }
}
</script>
首页

直接引入

<template>
  <div class="dashboard-container">
    <div class="dashboard-bg"></div>
    <div class="echart-div">
      <el-row :gutter="1" class="item">
      <el-col :span="8">
        <BrowserPie></BrowserPie>
      </el-col>
      <el-col :span="16">
        <TimeLine></TimeLine>
      </el-col>
      </el-row>
      <el-row style="margin-top: 35px" class="item">
        <el-col span="8">
          <LspPie></LspPie>
        </el-col>
        <el-col span="16">
          <ProvinceCol></ProvinceCol>
        </el-col>
      </el-row>
    </div>
  </div>
</template>

<script>
import LspPie from "./components/LspPie.vue";
import ProvinceCol from "./components/ProvinceCol.vue";
import BrowserPie from "./components/BrowserPie.vue";
import TimeLine from "./components/TimeLine.vue";

export default {
  components: {
    LspPie,
    ProvinceCol,
    BrowserPie,
    TimeLine
  },
  name: 'home',
  data() {
    return {
    }
  },
  mounted() {
  },
  methods: {
  }
}
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.dashboard {
  &-container {
    .echart-div {
      padding: 30px;
    }
  }
  &-text {
    font-size: 22px;
    line-height: 46px;
  }
  .personal {
    .box-card-header {
      position: relative;
      height: 220px;
      img {
        width: 100%;
        height: 100%;
        transition: all 0.2s linear;
        &:hover {
          transform: scale(1.1, 1.1);
          filter: contrast(130%);
        }
      }
    }
  }
}
.dashboard {
  &-bg {
    background-image: url('../../assets/img/home_bg.png');
    background-size: cover;
    background-repeat: no-repeat;
    background-attachment: fixed; /* 可选,固定背景图片 */
    background-position: center; /* 可选,设置背景图片位置 */
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    filter: blur(1px);
  }
}
</style>

结束

到这里整篇文章就结束了,我们重新捋一下整个流程

  • 全局过滤器拦截请求
  • 对请求信息进行解析入库
  • 定义API接口
  • 前端引入axios、echarts
  • 编写图形Vue组件
  • 前后端数据交互

🚀 注重版权,转载请注明原作者和原文链接

  • 小袁博客:https://boke.open-yuan.com/
  • 小袁博客后台:https://boke.open-yuan.com/back-manager/
  • 更多项目内容关注小红书🔍OpenYuan开袁 http://xhslink.com/I9zNaC
  • 有需求可以在小袁博客首页加我微信或者QQ

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/408635.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue 手势解锁功能

效果 实现 <script setup lang"ts"> const canvasRef ref<HTMLCanvasElement>() const ctx ref<CanvasRenderingContext2D | null>(null) const width px2px(600) const height px2px(700) const radius ref(px2px(50))const init () > …

【AI链接】 大模型语言模型网站链接

目录 GPT类1. chatgpt2. GROP3. Google AI Studio4. Moonshot AI (国内) 解读论文类&#xff1a;1. txyz 编程辅助插件&#xff1a;1. Fitten Code GPT类 1. chatgpt https://chat.openai.com/ 2. GROP https://groq.com/ 3. Google AI Studio https://aistudio.google…

计算机网络:思科实验【3-集线器与交换机的区别、交换机的自学习算法】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;Cisco Packet Tracer实验 本文对应的实验报告源文件请关注微信公众号程序员刘同学&#xff0c;回复思科获取下载链接。 实验目的实验环境实验内容集线器与交换机的区别交换机的自学习算法…

南京观海微电子----Verilog基础(一)——数据类型、运算符

1. 数据类型 1.1 常量 整数&#xff1a;整数可以用二进制b或B&#xff0c;八进制o或O&#xff0c;十进制d或D&#xff0c;十六进制h或H表示&#xff0c;例如&#xff0c;8’b00001111表示8位位宽的二进制整数&#xff0c;4’ha表示4位位宽的十六进制整数。 X和Z&#xff1a;X…

Github 2024-02-21 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-21统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目8非开发语言项目1TypeScript项目1 gpt4free 语言模型集合改进计划 创建周期&#xff1a;300 天开…

Excel的中高级用法

单元格格式&#xff0c;根据数值的正负分配不同的颜色和↑ ↓ 根据数值正负分配颜色 2-7 [蓝色]#,##0;[红色]-#,##0 分配颜色的基础上&#xff0c;根据正负加↑和↓ 2↑-7↓ 其实就是在上面颜色的代码基础上加个 向上的符号↑&#xff0c;或向下的符号↓ [蓝色]#,##0↑;[红色…

vivo 基于 StarRocks 构建实时大数据分析平台,为业务搭建数据桥梁

在大数据时代&#xff0c;数据分析和处理能力对于企业的决策和发展至关重要。 vivo 作为一家全球移动互联网智能终端公司&#xff0c;需要基于移动终端的制造、物流、销售等各个方面的数据进行分析以满足业务决策。 而随着公司数字化服务的演进&#xff0c;业务诉求和技术架构有…

动态规划课堂1-----斐波那契数列模型

目录 动态规划的概念&#xff1a; 动态规划的解法流程&#xff1a; 题目: 第 N 个泰波那契数 解法&#xff08;动态规划&#xff09; 代码&#xff1a; 优化&#xff1a; 题目&#xff1a;最小花费爬楼梯 解法&#xff08;动态规划&#xff09; 解法1&#xff1a; 解…

【QT 5 +Linux下软件生成+qt软件生成使用工具+学习他人文章+第一篇:使用linuxdeployqt软件生成】

【QT 5 Linux下软件生成qt软件生成使用工具学习他人文章第一篇&#xff1a;使用linuxdeployqt软件生成】 1、前言2、实验环境3、自我学习总结-本篇总结1、新手的疑问&#xff0c;做这件事的目的2、了解工具&#xff1a;linuxdeployqt工具3、解决相关使用过程中问题 4、参照文章…

5分钟轻松帮你EasyRecovery恢复女朋友照片

相信有不少男性电脑玩家都会将女朋友的照片存放在电脑硬盘之内&#xff0c;作为珍贵的收藏和回忆。但是在某些时候&#xff0c;如果我们错误地删除了这些照片&#xff0c;或者由于系统问题导致其中的照片丢失&#xff0c;那么我们怎么找回女朋友的照片&#xff1f;这个问题就足…

进程的学习

进程基本概念: 1.进程: 程序&#xff1a;存放在外存中的一段数据组成的文件 进程&#xff1a;是一个程序动态执行的过程,包括进程的创建、进程的调度、进程的消亡 2.进程相关命令: 1.top 动态查看当前系统中的所有进程信息&#xff08;根据CPU占用率排序&#xf…

微芒计划-简洁方便的效率待办管理工具【免费】

&#x1f632;微芒计划-简洁方便的效率待办管理工具【免费】 下载地址 &#x1f4dd;我的待办 快速添加待办任务&#xff0c;快速查看任务进度&#xff0c;摘要等。新增标签&#xff0c;分类&#xff0c;更好管理待办任务。 ☀️OKR目标管理 OKR让抽象的企业战略明确为上下对…

✅技术社区项目—Session/Cookie身份验证识别

session实现原理 SpringBoot提供了一套非常简单的session机制&#xff0c;那么它又是怎么工作的呢? 特别是它是怎么识别用户身份的呢? session又是存在什么地方的呢? 核心工作原理 借助cookie中的 JESSIONID 来作为用户身份标识&#xff0c;这个数据相同的&#xff0c;认…

车载电子电器架构 —— OEM基础技术概念开发流程

车载电子电器架构 —— 基础技术概念开发 我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗…

SpringMVC 学习(二)之第一个 SpringMVC 案例

目录 1 通过 Maven 创建一个 JavaWeb 工程 2 配置 web.xml 文件 3 创建 SpringMVC 配置文件 spring-mvc.xml 4 创建控制器 HelloController 5 创建视图 index.jsp 和 success.jsp 6 运行过程 7 参考文档 1 通过 Maven 创建一个 JavaWeb 工程 可以参考以下博文&#x…

吴恩达deeplearning.ai:Tensorflow训练一个神经网络

以下内容有任何不理解可以翻看我之前的博客哦&#xff1a;吴恩达deeplearning.ai 在之前的博客中。我们陆续学习了各个方面的有关深度学习的内容&#xff0c;今天可以从头开始训练一个神经网络了。 Tensorflow训练神经网络模型 我们使用之前用过的例子&#xff1a; 这个神经…

Python中的functools模块详解

大家好&#xff0c;我是海鸽。 函数被定义为一段代码&#xff0c;它接受参数&#xff0c;充当输入&#xff0c;执行涉及这些输入的一些处理&#xff0c;并根据处理返回一个值&#xff08;输出&#xff09;。当一个函数将另一个函数作为输入或返回另一个函数作为输出时&#xf…

JAVA算法和数据结构

一、Arrays类 1.1 Arrays基本使用 我们先认识一下Arrays是干什么用的&#xff0c;Arrays是操作数组的工具类&#xff0c;它可以很方便的对数组中的元素进行遍历、拷贝、排序等操作。 下面我们用代码来演示一下&#xff1a;遍历、拷贝、排序等操作。需要用到的方法如下 public…

26.HarmonyOS App(JAVA)列表对话框

列表对话框的单选模式&#xff1a; //单选模式 // listDialog.setSingleSelectItems(new String[]{"第1个选项","第2个选项"},1);//单选 // listDialog.setOnSingleSelectListener(new IDialog.ClickedListener() { // Override …

互联网加竞赛 机器视觉opencv答题卡识别系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 答题卡识别系统 - opencv python 图像识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分…