第 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,请求结束自动关闭,防止重复点击
相关推荐
咖啡无伴侣1 小时前
1. 别再手动改代码格式了!Vue3 项目工程化规范搭建实战
vue.js·架构
知野小兔3 小时前
Vuex和Pinia状态管理
vue.js
VX_bysjlw9854 小时前
数码设备销售网站设计与实现39138-计算机毕设原创(免费领源码+带部署教程)
java·vue.js·spring boot·mysql·tomcat·mybatis·idea
whi9 小时前
一个小工具,解决了一个困扰 Vue 开发者多年的类型检查难题
vue.js·typescript
xcs1940521 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
雪芽蓝域zzs1 天前
第 5 节:Marker 点击弹出 InfoWindow 信息窗口
vue.js
大包子1 天前
structuredClone vs lodash.cloneDeep:核心差异与选型指南
javascript·vue.js
雪芽蓝域zzs1 天前
第 3 节:地图点击事件,点击获取经纬度
vue.js
WayneX1 天前
开源 Vue 3 组件库 Morya UI:把组件、文档、AI 工具链一起做进一个包
前端·vue.js·前端框架
hai_android1 天前
Chat 聊天模块功能总结
前端·javascript·vue.js