SpringBoot+Vue+Echarts实现可视化图表的渲染
先看效果:
1.首页
<template><div class="dashboard-container"><!-- 侧边栏 --><!-- 主内容区 --><div class="main-content"><!-- 数据卡片 --><div class="data-cards"><div class="card"><div class="card-header"><span class="title">总订单数</span><span class="iconfont icon-order"></span></div><div class="card-body"><div class="value">{{ orderCount }}</div><div class="trend"><span class="up">↑ 12.5%</span><span class="text">较上月</span></div></div></div><div class="card"><div class="card-header"><span class="title">商品总数</span><span class="iconfont icon-product"></span></div><div class="card-body"><div class="value">{{ productCount }}</div><div class="trend"><span class="up">↑ 8.3%</span><span class="text">较上月</span></div></div></div><div class="card"><div class="card-header"><span class="title">销售总额</span><span class="iconfont icon-money"></span></div><div class="card-body"><div class="value">¥{{ salesAmount }}</div><div class="trend"><span class="up">↑ 18.7%</span><span class="text">较上月</span></div></div></div><div class="card"><div class="card-header"><span class="title">用户总数</span><span class="iconfont icon-user"></span></div><div class="card-body"><div class="value">{{ userCount }}</div><div class="trend"><span class="up">↑ 23.1%</span><span class="text">较上月</span></div></div></div></div><!-- 图表区域 --><div class="charts"><div class="chart-container"><div class="chart-header"><h3>商品销售数量分布</h3>
<!-- <div class="time-filter"><button class="filter-btn active">本周</button><button class="filter-btn">本月</button><button class="filter-btn">全年</button></div>--></div><div class="chart-content" ref="salesChart"></div></div><div class="chart-container"><div class="chart-header"><h3>销售趋势分析</h3>
<!-- <div class="time-filter"><button class="filter-btn active">本周</button><button class="filter-btn">本月</button><button class="filter-btn">全年</button></div>--></div><div class="chart-content" ref="trendChart"></div></div></div><!-- 最近订单表格 -->
<!-- <div class="recent-orders"><div class="table-header"><h3>最近订单</h3><button class="view-all">查看全部</button></div><div class="table-container"><table><thead><tr><th>订单编号</th><th>用户</th><th>商品</th><th>金额</th><th>状态</th><th>操作</th></tr></thead><tbody><tr v-for="(order, index) in recentOrders" :key="index"><td>{{ order.orderNo }}</td><td>{{ order.username }}</td><td>{{ order.productName }}</td><td>¥{{ order.amount.toFixed(2) }}</td><td><span class="status" :class="order.statusClass">{{ order.status }}</span></td><td><button class="action-btn">详情</button></td></tr></tbody></table></div></div>--></div></div>
</template><script>
// import echarts from 'echarts';
import * as echarts from 'echarts';export default {data() {return {// 统计数据orderCount: 1284,productCount: 356,salesAmount: 89456.25,userCount: 5214,// 图表数据salesData: [{name: '电子产品', value: 286},{name: '服装鞋帽', value: 412},{name: '食品饮料', value: 325},{name: '家居用品', value: 189},{name: '美妆个护', value: 247},{name: '图书音像', value: 124}],// 最近订单数据recentOrders: [{orderNo: 'ORD-20250516-001',username: '张三',productName: 'iPhone 15 Pro',amount: 8999.00,status: '已完成',statusClass: 'completed'},{orderNo: 'ORD-20250516-002',username: '李四',productName: '连衣裙',amount: 329.00,status: '已完成',statusClass: 'completed'},{orderNo: 'ORD-20250516-003',username: '王五',productName: '无线耳机',amount: 1299.00,status: '处理中',statusClass: 'processing'},{orderNo: 'ORD-20250516-004',username: '赵六',productName: '咖啡礼盒',amount: 298.00,status: '已取消',statusClass: 'cancelled'},{orderNo: 'ORD-20250516-005',username: '孙七',productName: '智能手表',amount: 1899.00,status: '待付款',statusClass: 'pending'}],// 销售趋势数据trendData: {dates: ['5/10', '5/11', '5/12', '5/13', '5/14', '5/15', '5/16'],sales: [12540, 13860, 11250, 14680, 15920, 16850, 17240]}};},mounted() {this.fetchDashboardData();// setTimeout(() => {// this.initSalesChart();// this.initTrendChart();// }, 0);this.initSalesChart();this.initTrendChart();// 监听窗口大小变化,重新绘制图表window.addEventListener('resize', this.resizeCharts);},beforeDestroy() {// 移除事件监听window.removeEventListener('resize', this.resizeCharts);},methods: {// orderCount: 1284,// productCount: 356,// salesAmount: 89456.25,// userCount: 5214,fetchDashboardData() {var that=this;this.$http.post('/order/welcome').then(response => {console.log(response.data)// 调用API获取数据this.orderCount = response.data.orderCount;this.productCount = response.data.productCount;this.salesAmount=response.data.salesAmount;this.userCount=response.data.userCount})const data = {sql: `SELECTDATE (o.ceatetime) AS name,sum(od.amount) AS valFROM\`order\` oJOINorderdetail odON o.id = od.orderidJOINproduct p ON od.productid = p.idWHEREo.ceatetime BETWEEN '2025-05-01'AND CURDATE()AND o.state = '已完成'GROUP BYDATE (o.ceatetime)ORDER BYname ASC;`}this.$http.post('/order/selectAction',data).then(response => {var data=response.data.data;var namesArray = []; //名称var valsArray =[]; //数值var beanArray =[]; //数值for (var i = 0; i < data.length; i++) {var obj = data[i];namesArray.push(obj.name);valsArray.push(obj.val);beanArray.push({"name": obj.name, "value": obj.val});}this.trendData.dates=namesArray;this.trendData.sales=valsArray;// this.initTrendChart();})const data02 = {sql: `SELECT t.name AS name,COALESCE(SUM(od.count), 0) AS valFROM types tLEFT JOINproduct p ON t.id = p.mark2LEFT JOINorderdetail od ON p.id = od.productidLEFT JOIN\`order\` o ON od.orderid = o.id AND o.state = '已完成'GROUP BY t.id, t.name;`}this.$http.post('/order/selectAction',data02).then(response => {var data=response.data.data;var namesArray = []; //名称var valsArray =[]; //数值var beanArray =[]; //数值for (var i = 0; i < data.length; i++) {var obj = data[i];namesArray.push(obj.name);valsArray.push(obj.val);beanArray.push({"name": obj.name, "value": obj.val});}this.salesData=beanArray;// 初始化图表// this.initSalesChart();})},// 初始化商品销售数量分布图表initSalesChart() {const chartDom = this.$refs.salesChart;const myChart = echarts.init(chartDom);// 图表配置const option = {tooltip: {trigger: 'axis',axisPointer: {type: 'shadow'}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: {type: 'category',data: this.salesData.map(item => item.name),axisTick: {alignWithLabel: true}},yAxis: {type: 'value'},series: [{name: '销售数量',type: 'bar',barWidth: '60%',data: this.salesData.map(item => item.value),itemStyle: {color: '#007bff'}}]};// 使用配置项显示图表myChart.setOption(option);this.salesChart = myChart;},// 初始化销售趋势图表initTrendChart() {const chartDom = this.$refs.trendChart;const myChart = echarts.init(chartDom);// 图表配置const option = {tooltip: {trigger: 'axis'},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: {type: 'category',boundaryGap: false,data: this.trendData.dates},yAxis: {type: 'value'},series: [{name: '销售额',type: 'line',data: this.trendData.sales,itemStyle: {color: '#28a745'},areaStyle: {color: {type: 'linear',x: 0,y: 0,x2: 0,y2: 1,colorStops: [{offset: 0, color: 'rgba(40, 167, 69, 0.3)'},{offset: 1, color: 'rgba(40, 167, 69, 0)'}]}}}]};// 使用配置项显示图表myChart.setOption(option);this.trendChart = myChart;},// 图表自适应窗口大小resizeCharts() {if (this.salesChart) {this.salesChart.resize();}if (this.trendChart) {this.trendChart.resize();}}}
};
</script><style scoped>
.dashboard-container {display: flex;height: 100vh;overflow: hidden;
}.header {display: flex;justify-content: space-between;align-items: center;height: 60px;background-color: #2c3e50;color: white;padding: 0 20px;
}.logo {display: flex;align-items: center;font-size: 18px;font-weight: bold;
}.logo .iconfont {margin-right: 10px;
}.user-info {display: flex;align-items: center;
}.username {margin-right: 10px;
}.sidebar {width: 220px;background-color: #34495e;color: white;height: calc(100vh - 60px);overflow-y: auto;
}.menu-item {display: flex;align-items: center;padding: 15px 20px;cursor: pointer;transition: background-color 0.3s;
}.menu-item:hover,
.menu-item.active {background-color: #2c3e50;
}.menu-item .iconfont {margin-right: 15px;width: 20px;
}.main-content {flex: 1;padding: 20px;background-color: #f5f6fa;overflow-y: auto;
}.data-cards {display: grid;grid-template-columns: repeat(4, 1fr);gap: 20px;margin-bottom: 20px;
}.card {background-color: white;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);padding: 20px;
}.card-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 15px;
}.title {font-size: 16px;color: #777;
}.card-header .iconfont {font-size: 24px;color: #007bff;
}.card-body .value {font-size: 28px;font-weight: bold;margin-bottom: 10px;
}.trend {display: flex;align-items: center;font-size: 14px;
}.up {color: #28a745;margin-right: 5px;
}.down {color: #dc3545;margin-right: 5px;
}.charts {display: grid;grid-template-columns: 1fr 1fr;gap: 20px;margin-bottom: 20px;
}.chart-container {background-color: white;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);padding: 20px;
}.chart-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;
}.chart-header h3 {font-size: 18px;font-weight: 500;
}.time-filter {display: flex;
}.filter-btn {padding: 5px 15px;border: 1px solid #ddd;background-color: white;cursor: pointer;margin-left: -1px;
}.filter-btn.active {background-color: #007bff;color: white;border-color: #007bff;
}.filter-btn:first-child {border-radius: 4px 0 0 4px;
}.filter-btn:last-child {border-radius: 0 4px 4px 0;
}.chart-content {height: 300px;
}.recent-orders {background-color: white;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);padding: 20px;
}.table-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;
}.table-header h3 {font-size: 18px;font-weight: 500;
}.view-all {background-color: #007bff;color: white;border: none;padding: 8px 15px;border-radius: 4px;cursor: pointer;
}.table-container {overflow-x: auto;
}table {width: 100%;border-collapse: collapse;
}th,
td {padding: 12px 15px;text-align: left;border-bottom: 1px solid #eee;
}th {background-color: #f8f9fa;font-weight: 500;
}.status {padding: 3px 8px;border-radius: 4px;font-size: 12px;
}.status.pending {background-color: #fff3cd;color: #856404;
}.status.processing {background-color: #cce5ff;color: #004085;
}.status.completed {background-color: #d4edda;color: #155724;
}.status.cancelled {background-color: #f8d7da;color: #721c24;
}.action-btn {background-color: #007bff;color: white;border: none;padding: 5px 10px;border-radius: 4px;cursor: pointer;font-size: 14px;
}
</style>
2.控制层
//公共查询方法@RequestMapping("/selectAction")public Object selectAction(@RequestBody SqlRequest sql) {List<Map> mapList = orderMapper.selectAction(sql.getSql());System.out.println("sql = " + sql);Map map = new HashMap();map.put("data", mapList);return map;}//首页数据@RequestMapping("welcome")public Map<String, Object> welcome() {String sql="select count(*) nums from `order`";String sql1="select count(*) nums1 from product";String sql2="SELECT SUM(amount) AS nums2 FROM `order`";String sql3="select count(*) nums3 from user";Map<String, Object> map = new HashMap<String, Object>();List<Map> maps = orderMapper.selectAction(sql);List<Map> maps1 = productMapper.selectAction(sql1);List<Map> maps2 = orderMapper.selectAction(sql2);List<Map> maps3 = userMapper.selectAction(sql3);Object nums = maps.get(0).get("nums");map.put("orderCount",nums);map.put("productCount",maps1.get(0).get("nums1"));map.put("salesAmount",maps2.get(0).get("nums2"));map.put("userCount",maps3.get(0).get("nums3"));return map;}
3.Mapper
@Select(" ${sql} ")public List<Map> selectAction(@Param("sql") String sql);
4.前端直接传SQL,后台接收需要一个实体类封装:
class SqlRequest {private String sql; // 接收SQL语句// 可添加其他参数,如查询类型、分页信息等public String getSql() {return sql;}public void setSql(String sql) {this.sql = sql;}}
写法不是很规范,但是很多时候在别人的项目里去添加功能就很方便,不用去读原来的项目代码,直接在前端把SQL语句当做参数传到后端接口,和原来的项目隔离,实现功能。