第 13 章:对接后端真实接口(Axios 请求,替换 Mock 数据,完整业务闭环)
本章目标:
- 在若依 Vue3 项目中,封装接口请求函数,使用 axios 请求后端
- 改造看板页面,把页面里写死的模拟数据换成后端返回真实数据
- 增加全局 loading、请求异常捕获、请求失败空态兜底
- 增加日期筛选参数传给后端,实现后端数据筛选联动图表
- 处理接口返回数据格式转换,适配 ECharts 的 option 结构
第一步:先新建接口文件 src/api/chart.js(若依标准 api 目录)
// src/api/chart.js
import request from '@/utils/request'
/**
* 获取销售看板图表数据
* @param {Object} params
* @param {string} params.startDate 开始日期
* @param {string} params.endDate 结束日期
*/
export function getSaleChartData(params) {
return request({
url: '/api/chart/sale',
method: 'get',
params
})
}
说明:
@/utils/request是若依自带的 axios 封装,自带 token、响应拦截,直接复用。
第二步:改造看板页面,替换 mock,增加请求、异常处理
<template>
<div class="dashboard-page" style="padding:20px;">
<!-- 新增:全局日期筛选器,控制全部图表 -->
<div style="margin-bottom:20px;display:flex;align-items:center;gap:12px;">
<span>统计时间:</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
/>
<el-button type="primary" @click="loadAllChartData">刷新全部图表</el-button>
</div>
<el-row :gutter="20" style="margin-bottom:20px;">
<el-col :span="6">
<el-card shadow="hover">
<div style="font-size:14px;color:#909399;">今日销售额</div>
<div style="font-size:24px;font-weight:bold;color:#409EFF;margin-top:8px;">¥ {{statData.todaySale}}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div style="font-size:14px;color:#909399;">今日订单量</div>
<div style="font-size:24px;font-weight:bold;color:#67C23A;margin-top:8px;">{{statData.todayOrder}}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div style="font-size:14px;color:#909399;">新增用户</div>
<div style="font-size:24px;font-weight:bold;color:#E6A23C;margin-top:8px;">{{statData.newUser}}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div style="font-size:14px;color:#909399;">转化率</div>
<div style="font-size:24px;font-weight:bold;color:#F56C6C;margin-top:8px;">{{statData.rate}}%</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-bottom:20px;">
<el-col :span="12">
<el-card v-loading="loading">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>月度销售柱状图</span>
<el-button type="text" @click="exportBarImg">导出图片</el-button>
</div>
</template>
<BaseEchart
ref="barChartRef"
:option="barOption"
height="350px"
/>
</el-card>
</el-col>
<el-col :span="12">
<el-card v-loading="loading">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>产品销售占比饼图</span>
<el-button type="text" @click="exportPieImg">导出图片</el-button>
</div>
</template>
<BaseEchart
ref="pieChartRef"
:option="pieOption"
height="350px"
/>
</el-card>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-card v-loading="loading">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>销售额 + 同比增长率 组合图</span>
<div>
<el-switch v-model="comboAutoPlay" active-text="自动轮播" style="margin-right:15px;" />
<el-button type="text" @click="exportComboImg">导出图片</el-button>
</div>
</div>
</template>
<BaseEchart
ref="comboChartRef"
:option="comboOption"
:auto-play="comboAutoPlay"
height="380px"
/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import BaseEchart from '@/components/BaseEchart/index.vue'
import { ElMessage } from 'element-plus'
// 导入我们刚才写好的接口
import { getSaleChartData } from '@/api/chart'
const barChartRef = ref(null)
const pieChartRef = ref(null)
const comboChartRef = ref(null)
const comboAutoPlay = ref(true)
const loading = ref(false)
const dateRange = ref([])
// 统计卡片数据
const statData = ref({
todaySale: 0,
todayOrder: 0,
newUser: 0,
rate: 0
})
const barOption = ref({})
const pieOption = ref({})
const comboOption = ref({})
// 构建柱状图option
function buildBarOption(xData, saleData) {
return {
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: xData },
yAxis: { type: 'value', name: '万元' },
series: [{
type: 'bar',
data: saleData,
itemStyle: { color: '#409EFF', borderRadius: [4,4,0,0] }
}]
}
}
// 构建饼图option
function buildPieOption(pieData) {
return {
tooltip: { trigger: 'item', formatter: '{b}: {c}万元 ({d}%)' },
legend: { bottom: 10, left: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '45%'],
data: pieData,
itemStyle: { borderRadius: 6 }
}]
}
}
// 构建组合图option
function buildComboOption(xData, saleData, rateData) {
return {
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
legend: { data: ['销售额', '同比增长率'], bottom: 10 },
xAxis: [{ type: 'category', data: xData }],
yAxis: [
{ type: 'value', name: '销售额(万元)', min: 0 },
{ type: 'value', name: '增长率(%)', position: 'right', axisLabel: { formatter: '{value} %' } }
],
series: [
{
name: '销售额',
type: 'bar',
data: saleData,
itemStyle: { color: '#409EFF', borderRadius: [4,4,0,0] }
},
{
name: '同比增长率',
type: 'line',
yAxisIndex: 1,
smooth: true,
data: rateData,
itemStyle: { color: '#67C23A' }
}
]
}
}
// 加载全部图表数据(核心:请求后端接口)
const loadAllChartData = async () => {
loading.value = true
try {
const params = {}
// 组装时间参数传给后端
if(dateRange.value?.length === 2) {
params.startDate = dateRange.value[0]
params.endDate = dateRange.value[1]
}
// 请求后端接口
const res = await getSaleChartData(params)
const data = res.data
// 赋值顶部统计卡片
statData.value = {
todaySale: data.todaySale,
todayOrder: data.todayOrder,
newUser: data.newUser,
rate: data.rate
}
// 后端返回数据,转换为echarts可用配置
barOption.value = buildBarOption(data.bar.xData, data.bar.saleData)
pieOption.value = buildPieOption(data.pie.list)
comboOption.value = buildComboOption(data.combo.xData, data.combo.saleData, data.combo.rateData)
ElMessage.success('图表数据加载成功')
} catch (err) {
console.error('接口请求失败:', err)
ElMessage.error('获取图表数据失败,请检查后端接口!')
// 接口报错,清空图表,展示空态
barOption.value = {}
pieOption.value = {}
comboOption.value = {}
} finally {
loading.value = false
}
}
// 日期筛选变更
const handleDateChange = () => {
loadAllChartData()
}
// 导出通用方法
function exportImg(chartRef, name) {
const url = chartRef.value.getChartImageUrl()
if (!url) {
ElMessage.warning('图表未加载')
return
}
const a = document.createElement('a')
a.href = url
a.download = `${name}.png`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
ElMessage.success('导出成功')
}
const exportBarImg = () => exportImg(barChartRef, '月度销售柱状图')
const exportPieImg = () => exportImg(pieChartRef, '产品占比饼图')
const exportComboImg = () => exportImg(comboChartRef, '销售组合图')
onMounted(() => {
loadAllChartData()
})
</script>
后端约定返回数据格式(后端同学按这个格式返回 JSON)
{
"code":200,
"data":{
"todaySale":"128560",
"todayOrder":"1286",
"newUser":"86",
"rate":"3.28",
"bar":{
"xData":["1月","2月","3月","4月","5月","6月"],
"saleData":[32,45,38,52,48,60]
},
"pie":{
"list":[
{"name":"产品A","value":35},
{"name":"产品B","value":22},
{"name":"产品C","value":18},
{"name":"产品D","value":25}
]
},
"combo":{
"xData":["1月","2月","3月","4月","5月","6月"],
"saleData":[32,45,38,52,48,60],
"rateData":[12,18,15,22,19,28]
}
}
}
本章测试清单
- 新建
src/api/chart.js,粘贴接口代码 - 替换看板页面代码
- 本地调试(如果后端还没开发,可以先用 mock 模拟接口返回)
✅ 页面初始化自动请求接口,loading 遮罩显示
✅ 选择日期范围,自动携带参数请求后端,刷新全部图表
✅ 接口请求失败,弹出错误提示,图表清空兜底
✅ 顶部统计卡片数据由后端返回渲染
✅ 每个图表导出图片、轮播功能保持正常
✅ 窗口缩放自适应,路由切换销毁实例无内存泄漏
核心知识点
- 接口分层 :若依规范,所有 api 单独抽离到
src/api目录,页面只调用函数,不直接写 url - 参数透传:日期组件选中的时间,作为请求参数传给后端,后端做数据库筛选
- 异常捕获 try/catch:网络报错、后端 500、401 都会进入 catch,给用户友好提示
- 数据转换层:后端返回原始数据,前端做转换,组装成 ECharts 需要的 option 结构,解耦
- loading 统一控制:全局 loading 绑定 el-card,请求结束自动关闭,防止重复点击
