第 13 章:对接后端真实接口(Axios 请求,替换 Mock 数据,完整业务闭环)

第 13 章:对接后端真实接口(Axios 请求,替换 Mock 数据,完整业务闭环)

本章目标:

  1. 在若依 Vue3 项目中,封装接口请求函数,使用 axios 请求后端
  2. 改造看板页面,把页面里写死的模拟数据换成后端返回真实数据
  3. 增加全局 loading、请求异常捕获、请求失败空态兜底
  4. 增加日期筛选参数传给后端,实现后端数据筛选联动图表
  5. 处理接口返回数据格式转换,适配 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]
    }
  }
}

本章测试清单

  1. 新建 src/api/chart.js,粘贴接口代码
  2. 替换看板页面代码
  3. 本地调试(如果后端还没开发,可以先用 mock 模拟接口返回)
    ✅ 页面初始化自动请求接口,loading 遮罩显示
    ✅ 选择日期范围,自动携带参数请求后端,刷新全部图表
    ✅ 接口请求失败,弹出错误提示,图表清空兜底
    ✅ 顶部统计卡片数据由后端返回渲染
    ✅ 每个图表导出图片、轮播功能保持正常
    ✅ 窗口缩放自适应,路由切换销毁实例无内存泄漏

核心知识点

  1. 接口分层 :若依规范,所有 api 单独抽离到src/api目录,页面只调用函数,不直接写 url
  2. 参数透传:日期组件选中的时间,作为请求参数传给后端,后端做数据库筛选
  3. 异常捕获 try/catch:网络报错、后端 500、401 都会进入 catch,给用户友好提示
  4. 数据转换层:后端返回原始数据,前端做转换,组装成 ECharts 需要的 option 结构,解耦
  5. loading 统一控制:全局 loading 绑定 el-card,请求结束自动关闭,防止重复点击
相关推荐
Aaswk2 小时前
从 HTML/CSS/JS 到 Vue + Axios 的一篇实战笔记
前端·css·vue.js·html
+VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue图书借阅管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
星光开发者2 小时前
基于Spring Boot的充电桩管理系统的设计与实现-计算机毕设【课程设计】57105
vue.js·spring boot·vscode·mysql·django·php·express
ℋᙚᵐⁱᒻᵉ鲸落3 小时前
ETypeError: nameRef.value?.focus is not a functio. 设置el-input自动聚焦时报错
前端·javascript·vue.js·elementui
+VX:Fegn089512 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·课程设计
老男孩游戏时光20 小时前
Vue3 Composition API 最佳实践,别再乱写 setup 了
vue.js
+VX:Fegn089521 小时前
计算机毕业设计|基于java+ vue共享单车信息系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
Bs_MoneyMagnet21 小时前
基于springboot+vue的非遗物质文化遗产系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
计算机毕设定制辅导-无忧学长1 天前
《基于SpringBoot的未成年人健康知识科普平台的设计与实现》
java·开发语言·vue.js·spring boot·未成年人健康知识科普平台