springboot+vue前后端分离项目-项目搭建18-Echarts数据图表统计(折线图、柱状图、饼图)
一、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); } }
效果:
以上仅供参考,如有疑问,留言联系