springboot+vue前后端分离项目-项目搭建18-Echarts数据图表统计(折线图、柱状图、饼图)

xiexieyc / 2024-08-18 / 原文

一、Echarts官网导入,并编写静态统计页面

Echarts官网地址: https://echarts.apache.org/examples/zh/index.html

整体思路,参照官网先做个静态页面,然后后台请求数据,赋值到对应的属性

1. 在vue项目目录下执行 npm i echarts -S 

导入成功后能看到echarts包

 2. 先参照官网的代码编写静态统计表页面vue/src/views/JingtaiCharts.vue

<template>
  <div style="width: 100%; margin: 20px 20px">
    <el-row :gutter="10">
      <el-col :span="12">
        <el-card>
          <div style="height: 500px" id="line"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div style="height: 500px" id="bar"></div>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="10" style="margin: 20px 0">
      <el-col :span="12">
        <el-card>
          <div style="height: 500px" id="pie"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import * as echarts from 'echarts';

const option = {
  title: {
    text: '订单金额统计曲线图',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    left: 'left'
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: '金额',
      data: [1150, 1230, 1224, 1218, 1135, 1147, 1260],
      type: 'line',
      smooth: true
    },
    {
      name: '库存',
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line',
      smooth: true
    }
  ]
};

const option1 = {
  title: {
    text: '订单金额统计柱状图',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    left: 'left'
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: '金额',
      data: [1150, 1230, 1224, 1218, 1135, 1147, 1260],
      type: 'bar',
      smooth: true
    },
    {
      name: '销量',
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'bar',
      smooth: true
    }
  ]
};

const option2 = {
  title: {
    text: '订单金额比例饼图',
    subtext: '比例图',
    left: 'center'
  },
  tooltip: {
    trigger: 'item'
  },
  legend: {
    orient: 'vertical',
    left: 'left'
  },
  series: [
    {
      name: '图书种类',
      type: 'pie',
      radius: '50%',
      data: [
        { value: 1048, name: '故事书' },
        { value: 735, name: '童话书' },
        { value: 580, name: '科学百科' },
        { value: 484, name: '数学' },
        { value: 300, name: '哲学' }
      ],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ]
};
export default {
  name: "JingtaiCharts",
  mounted() {
    //曲线图
    let lineDom = document.getElementById('line');
    let lineDomChart = echarts.init(lineDom);
    lineDomChart.setOption(option);
    //柱状图
    let barDom = document.getElementById('bar');
    let barDomChart = echarts.init(barDom);
    barDomChart.setOption(option1);
    //饼图
    let pieDom = document.getElementById('pie');
    let pieDomChart = echarts.init(pieDom);
    pieDomChart.setOption(option2);
  }
}
</script>

<style scoped>

</style>

效果:

二、动态统计页面

1. 新增vue/src/views/Orders.vue,显示订单数据列表

<template>
  <div style="width: 100%; padding: 10px">
    <!--    功能区-->
    <div style="margin: 10px 0">
      <el-button type="primary" @click="add()">新增</el-button><!--管理员才有新增按钮-->
      <el-popconfirm title="确定删除吗" @confirm="deleteBatch">
        <template #reference>
          <el-button type="danger">批量删除</el-button>
        </template>
      </el-popconfirm>
      <el-button type="info" plain @click="exportData">批量导出</el-button>
      <el-upload ref="upload" :action="fileImportUrl" :on-success="fileImportSuccess"
                 style="display: inline-block; margin-left: 10px" :show-file-list="false">
        <el-button type="primary">批量导入</el-button>
      </el-upload>
    </div>
    <!--    搜索区-->
    <div style="margin: 10px 0">
      <el-input v-model="search" placeholder="请输入订单名称" style="width: 20%" clearable></el-input>
      <el-button type="primary" style="margin-left: 10px" @click="load">查询</el-button>
    </div>
    <el-table :data="tableData" border stripe style="width: 100%" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55"></el-table-column>
      <el-table-column prop="id" label="序号"
                       sortable
      />
      <el-table-column prop="no" label="订单编号" />
      <el-table-column prop="name" label="订单名称" />
      <el-table-column prop="money" label="订单金额" />
      <el-table-column prop="userName" label="用户" />
      <el-table-column prop="category" label="订单分类" />
      <el-table-column prop="date" label="创建时间" />
      <div class="demo-image__preview">
      </div>
      <el-table-column fixed="right" label="操作" min-width="120">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="handleEdit(scope.row)">
            编辑
          </el-button>
          <el-button link type="primary" size="small">查看</el-button>
          <el-popconfirm title="确认删除吗?" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button link type="primary" size="small">删除</el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <div style="margin: 10px 0">
      <el-pagination
          v-model:current-page="currentPage"
          v-model:page-size="pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
      />
    </div>
    <el-dialog v-model="dialogVisible" title="订单信息" width="30%">
      <el-form label-width="auto" :model="form" :rules="rules" style="width: 600px">
        <el-form-item label="订单名称">
          <el-input v-model="form.name" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="订单金额">
          <el-input v-model="form.money" style="width: 80%"></el-input>
        </el-form-item>
        <el-form-item label="订单分类">
          <el-select v-model="form.category" style="width: 80%">
            <el-option v-for="item in ['水果', '蔬菜', '零食', '饮料', '奶制品', '糕点']" :key="item" :value="item"></el-option>
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="save()">
            确 定
          </el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script>

import request from "@/utils/request";

export default {
  name: 'Orders',
  components: {

  },
  data() {
    return {
      user: {},
      form: {},
      dialogVisible: false,
      search: '',
      currentPage: 1,
      pageSize: 10,
      total: 0,
      tableData: [],
      rules: {
        name: [
          { required: true, message: '请输入订单名称', trigger: 'blur' }
        ],
        money: [
          { required: true, message: '请输入订单金额', trigger: 'blur' }
        ],
        category: [
          { required: true, message: '请输入订单分类', trigger: 'blur' }
        ],
      },
      fileImportUrl: "http://" + window.server.filesUploadUrl + ":9090/orders/import",
      ids: []
    }
  },
  created() {
    this.load()
    let userStr = localStorage.getItem("user") || {}
    this.user = JSON.parse(userStr)

    request.get("/user/" + this.user.id).then(res => {
      if (res.data === '0'){
        this.user = res.data
      }
    })
  },
  methods: {
    exportData(){  //批量导出
      if(!this.ids.length){  //没选择行时导出全部 或 根据我的搜索条件导出
        window.open('http://localhost:9090/orders/export?search=' + this.search)
      } else {
        let idStr = this.ids.join(',')  //  [1,2,3] => '1,2,3'
        window.open('http://localhost:9090/orders/export?ids=' + idStr)
      }
    },
    fileImportSuccess(res){  //  批量导入成功后提示
      console.log(res)
      if(res.code === '200'){
        this.load()
        this.$message({
          type: "success",
          message: "批量导入成功"
        })
      }else {
        this.$message({
          type: "error",
          message: "res.msg"
        })
      }
    },
    deleteBatch(){
      if(!this.ids.length){
        this.$message.warning("请选择数据!")
        return
      }
      request.post("/orders/deleteBatch", this.ids).then(res => {
        if(res.code === '200'){
          this.$message.success("批量删除成功")
          this.load()
        } else {
          this.$message.error(res.msg)
        }
      })
    },
    handleSelectionChange(val){
      this.ids = val.map(v => v.id)   //[{id,name},{id,name}] => [id,id]
    },
    load() {
      request.get("/orders", {
        params:{
          pageNum: this.currentPage,
          pageSize: this.pageSize,
          search: this.search
        }
      }).then(res=>{
        console.log(res)
        this.tableData = res.data.records
        this.total = res.data.total
      })
    },
    add(){
      this.dialogVisible = true
      this.form = {}
    },
    save(){
      if(this.form.id){  //更新
        request.put("/orders", this.form).then(res => {
          console.log(res)
          if(res.code === '200'){
            this.$message({
              type: "success",
              message: "更新成功"
            })
          }else {
            this.$message({
              type: "error",
              message: "res.msg"
            })
          }
          this.load()      //更新后刷新表格数据
          this.dialogVisible = false   //关闭弹窗
        })
      } else {           //新增
        request.post("/orders", this.form).then(res => {
          console.log(res)
          if(res.code === '200'){
            this.$message({
              type: "success",
              message: "新增成功"
            })
          }else {
            this.$message({
              type: "error",
              message: "res.msg"
            })
          }
          this.load()      //更新后刷新表格数据
          this.dialogVisible = false   //关闭弹窗
        })
      }
    },
    handleEdit(row) {
      this.form = JSON.parse(JSON.stringify(row))
      this.dialogVisible = true
      this.$nextTick(() => {
        this.$refs["upload"].clearFiles()  //清除历史文件列表
      })
    },
    handleDelete(id) {
      console.log(id)
      request.delete("/orders/" + id).then(res => {
        if(res.code === '200'){
          this.$message({
            type: "success",
            message: "删除成功"
          })
        }else {
          this.$message({
            type: "error",
            message: "res.msg"
          })
        }
        this.load()      //删除后刷新表格数据
      })
    },
    handleSizeChange() {     //改变当前每页个数触发
      this.load()
    },
    handleCurrentChange() {  //改变当前页码触发
      this.load()
    }
  }
}

</script>

2. 新增vue/src/views/Charts.vue页面,显示动态统计表

<template>
  <div style="width: 100%; margin: 20px 20px">
    <el-row :gutter="10">
      <el-col :span="12">
        <el-card>
          <div style="height: 500px" id="line"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div style="height: 500px" id="bar"></div>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="10" style="margin: 20px 0">
      <el-col :span="12">
        <el-card>
          <div style="height: 500px" id="pie"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import * as echarts from 'echarts';
import request from "@/utils/request";

const option = {
  title: {
    text: '订单金额统计曲线图',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    left: 'left'
  },
  xAxis: {
    type: 'category',
    data: []
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: '金额',
      data: [],
      type: 'line',
      smooth: true
    }
  ]
};

const option1 = {
  title: {
    text: '订单金额统计柱状图',
    left: 'center'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    left: 'left'
  },
  xAxis: {
    type: 'category',
    data: []
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: '金额',
      data: [],
      type: 'bar',
      smooth: true
    }
  ]
};

const option2 = {
  title: {
    text: '订单金额比例饼图',
    subtext: '比例图',
    left: 'center'
  },
  tooltip: {
    trigger: 'item'
  },
  legend: {
    orient: 'vertical',
    left: 'left'
  },
  series: [
    {
      name: '图书种类',
      type: 'pie',
      radius: '50%',
      label: {
        show: true,
        formatter(param) {
          return param.name + '(' + param.percent + '%)';
        }
      },
      data: [],
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }
  ]
};
export default {
  name: "Charts",
  mounted() {
    //曲线图
    let lineDom = document.getElementById('line');
    let lineDomChart = echarts.init(lineDom);
    //柱状图
    let barDom = document.getElementById('bar');
    let barDomChart = echarts.init(barDom);
    //饼图
    let pieDom = document.getElementById('pie');
    let pieDomChart = echarts.init(pieDom);

    request.get("/orders/charts").then(res => {
      //曲线图
      option.xAxis.data = res.data?.line?.map(v => v.date) || []
      option.series[0].data = res.data?.line?.map(v => v.value) || []
      lineDomChart.setOption(option);
      //柱状图
      option1.xAxis.data = res.data?.bar?.map(v => v.name) || []
      option1.series[0].data = res.data?.bar?.map(v => v.value) || []
      barDomChart.setOption(option1);
      //饼图
      option2.series[0].data = res.data?.bar
      pieDomChart.setOption(option2);
    })
  }
}
</script>

<style scoped>

</style>

侧边栏配置同步调整

3. 后端新建Orders表、entity、Mapper、Service、Controller

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Orders {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String no;
    private String name;
    private BigDecimal money;
    private Integer userid;
    private String category;
    private String date;

    @TableField(exist = false)
    private String userName;
}
package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.Orders;

public interface OrdersMapper extends BaseMapper<Orders> {

}
package com.example.demo.Service;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.Orders;
import com.example.demo.mapper.OrdersMapper;
import org.springframework.stereotype.Service;

@Service
public class OrdersService extends ServiceImpl<OrdersMapper, Orders> {
}
package com.example.demo.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Service.OrdersService;
import com.example.demo.common.AuthAccess;
import com.example.demo.common.HoneyLogs;
import com.example.demo.common.LogType;
import com.example.demo.common.Result;
import com.example.demo.entity.Orders;
import com.example.demo.entity.User;
import com.example.demo.entity.Orders;
import com.example.demo.mapper.OrdersMapper;
import com.example.demo.mapper.UserMapper;
import com.example.demo.mapper.OrdersMapper;
import com.example.demo.utils.TokenUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.weaver.ast.Or;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/orders")
public class OrdersController {

    //正常Mapper是在Service里引用,Controllerl里引用Service,本案例是为了方便调用,非正规操作
    @Autowired
    OrdersService ordersService;

    @Resource
    UserMapper userMapper;

    @HoneyLogs(operation = "订单", type = LogType.ADD)
    @PostMapping
    public Result<?> save(@RequestBody Orders orders){
        User currentUser = TokenUtils.getCurrentUser(); // 获取当前登录的用户信息
        orders.setUserid(currentUser.getId());
        orders.setNo(IdUtil.fastSimpleUUID());
        orders.setDate(DateUtil.today());  // 年月日的当前日期
        ordersService.save(orders);
        return Result.success();
    }

    @HoneyLogs(operation = "订单", type = LogType.UPDATE)
    @PutMapping
    public Result<?> update(@RequestBody Orders orders){
        ordersService.updateById(orders);
        return Result.success();
    }

    @HoneyLogs(operation = "订单", type = LogType.DELETE)
    @DeleteMapping("/{id}")
    public Result<?> delete(@PathVariable Long id){
        ordersService.removeById(id);
        return Result.success();
    }

    @HoneyLogs(operation = "订单", type = LogType.BATCH_DELETE)
    @PostMapping("/deleteBatch") //  批量删除
    public Result<?> deleteBatch(@RequestBody List<Integer> ids){
        ordersService.removeBatchByIds(ids);
        return Result.success();
    }

    @GetMapping("/selectAll")
    public Result<?> selectAll(){
        List<Orders> ordersList = ordersService.list(new QueryWrapper<Orders>().orderByDesc("id"));
        return Result.success(ordersList);
    }
    @GetMapping
    public Result<?> findPage(@RequestParam(defaultValue = "1") Integer pageNum,
                              @RequestParam(defaultValue = "10") Integer pageSize,
                              @RequestParam(defaultValue = "") String search){
        QueryWrapper<Orders> queryWrapper = new QueryWrapper<Orders>().orderByDesc("id");
        queryWrapper.like(StrUtil.isNotBlank(search), "name", search);
        Page<Orders> ordersPage = ordersService.page(new Page<>(pageNum, pageSize), queryWrapper);
        List<Orders> records = ordersPage.getRecords();
        for (Orders orders :records) {
            Integer userid = orders.getUserid();
            User user = userMapper.selectById(userid);
            if (user != null) {
                orders.setUserName(user.getUsername());
            }
        }
        return Result.success(ordersPage);
    }

    //批量导出
    @AuthAccess
    @GetMapping("/export")
    public void exportData(@RequestParam(required = false) String search,
                           @RequestParam(required = false) String ids,  // 1,2,3,4
                           HttpServletResponse response) throws IOException {
        List<Orders> list;
        QueryWrapper<Orders> queryWrapper = new QueryWrapper<>();
        if (StrUtil.isNotBlank(ids)) {  //  第二种按选择的行导出
            List<Integer> idsArr = Arrays.stream(ids.split(",")).map(Integer::valueOf).collect(Collectors.toList());
            queryWrapper.in("id", idsArr);
        } else {  // 第一种全部导出或条件导出
            queryWrapper.like(StrUtil.isNotBlank(search),"name", search);
        }
        list = ordersService.list(queryWrapper);
        ExcelWriter writer = ExcelUtil.getWriter(true);
        writer.write(list, true);
        //设置响应文件类型,设置响应文件名称
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        response.setHeader("Content-Disposition","attachment;filename=" + URLEncoder.encode("订单表","utf-8") + ".xlsx");
        //导出数据写到响应的输出流里,关闭流
        ServletOutputStream outputStream = response.getOutputStream();
        writer.flush(outputStream, true);
        writer.close();
        outputStream.flush();
        outputStream.close();
    }

    /**
     * 批量导入
     * @param file 传入的excel文件
     * @return
     * @throws IOException
     */
    @AuthAccess
    @PostMapping("/import")
    public Result importData(MultipartFile file) throws IOException {
        ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
        List<Orders> ordersList = reader.readAll(Orders.class);
        //写入数据库
        for(Orders orders : ordersList){
            ordersService.save(orders);
        }
        return Result.success();
    }

    /**
     * 获取统计图数据
     * @return 动态数据
     */
    @AuthAccess
    @GetMapping("/charts")
    public Result charts() {
        // 包装折线图数据
        List<Orders> ordersList = ordersService.list();
        List<Dict> linelist = new ArrayList<>();
        Set<String> dates = ordersList.stream().map(Orders::getDate).collect(Collectors.toSet());
        ArrayList<String> dateList = CollUtil.newArrayList(dates);
        dateList.sort(Comparator.naturalOrder());
        for (String date : dateList) {
            // 统计date日期的所有金额总数
            BigDecimal sum = ordersList.stream().filter(orders -> orders.getDate().equals(date)).map(Orders::getMoney)
                    .reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            Dict line = Dict.create();
            line.set("date",date).set("value", sum);
            linelist.add(line);
        }
        // 包装柱状图、饼图数据
        List<Dict> barlist = new ArrayList<>();
        Set<String> categories = ordersList.stream().map(Orders::getCategory).collect(Collectors.toSet());
        for (String cate : categories) {
            // 统计cate的所有金额总数
            BigDecimal sum = ordersList.stream().filter(orders -> orders.getCategory().equals(cate)).map(Orders::getMoney)
                    .reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
            Dict bar = Dict.create();
            bar.set("name",cate).set("value", sum);
            barlist.add(bar);
        }
        // 包装所有数据
        Dict res = Dict.create().set("line",linelist).set("bar",barlist);
        return Result.success(res);
    }
}

效果: