Skip to content

登录业务接口替换替换为真实项目接口与品牌管理模块 Eleme(Day2 1/2) ​

本页对应 day2,共 5 段视频,约 1 小时 34 分。

学习目标 ​

  • 能够基于二次封装的 axios,在用户 API 模块中定义登录接口,正确设置 POST 路径、username/password 请求体,并完成请求参数与响应数据的 TypeScript 类型定义。
  • 能够配置 Vite 代理解决跨域,将登录页调用替换为真实登录接口,并验证登录成功后 token 同时写入 Pinia 与本地存储。
  • 能够替换获取用户信息接口,在请求拦截器中携带 token,并在路由守卫触发时获取、存储和展示真实用户信息。
  • 能够替换退出登录接口,在退出登录或 token 过期时发起请求,并清理 Pinia 与本地存储中的登录状态。
  • 能够使用 Element Plus 的 Card、Button、Icon 等组件完成品牌管理页面静态搭建,包括卡片容器、操作按钮及图标展示。
  • 能够查阅 Swagger 接口文档,按请求方法、路径、字段大小写和参数位置完成接口联调与常见错误排查。

知识点 ​

本节目标:把模板登录换成真实项目接口 ​

本节要完成的是登录业务接口替换:把模板里写死的登录逻辑,换成真实项目接口。真实服务地址是 http://sph-h5-api.atguigu.cn,在线 API 文档在 http://39.98.123.211:8510/swagger-ui.html 和 http://39.98.123.211:8170/swagger-ui.html。转写稿里的“swaker”应理解为 Swagger,“as”是 axios,“peer”是 pinia,“eleant plus”是 Element Plus。

为什么需要:模板只演示流程,不接公司真实数据,登录、用户信息、退出登录都必须替换。
怎么想:先看 Swagger 确认请求方法、路径、参数和返回字段,再按模块写 API。
怎么做:登录、获取用户信息、退出登录三个接口都要换;登录成功拿 token,后续请求带 token。
易错点:不要直接抄代码,不要猜路径和字段大小写;username、password 这类字段必须按文档写。

按模块写 API:user/index.ts 与 request 二次封装 ​

为什么需要:前台项目常用一个 index.js 管所有接口,后台项目接口多,混在一起难找。
怎么想:按业务分模块,用户相关放 api/user/index.ts,权限、商品等以后各自建文件夹。
怎么做:从二次封装的 request 引入请求方法;可用枚举统一保存接口地址;写 reqUserLogin 等函数并导出。转写稿中登录是 POST,参数对象里放账号密码;返回数据里取 token。
易错点:字段大小写错一个字母后台就解不出;函数要导出,仓库里才能引入;类型可先跑通业务后补,但字段名不能错。

配置代理跨域:vite.config.ts 的 server.proxy ​

为什么需要:浏览器直接向真实服务器发请求会跨域,开发环境要用 Vite 代理转发。
怎么想:项目基础路径用 /app-dev,代理把 /app-dev 转发到真实 target,再用 rewrite 去掉前缀。
怎么做:在 vite.config.ts 的 server.proxy 中配置,笔记代码如下,必须原样保留:

ts
server: {
  proxy: {
    '/app-dev': {
      target: 'http://sph-h5-api.atguigu.cn',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/app-dev/, ''),
    },
  },
}

易错点:配置放错层级,不要写到 CSS 里;target 是真实服务器;配置文件变化后必须重启,转写稿演示用 npm run dev 重启;不配代理时请求路径会不对,控制台能看到请求失败。

登录成功保存 token 与获取用户信息 ​

为什么需要:token 是登录成功的唯一标识,购物车、个人中心、权限菜单都靠它识别“是谁”。
怎么想:登录成功后把 result.token 存两份:pinia 仓库一份、本地存储一份;路由守卫发现没有用户信息就发获取用户信息请求,拿到后再放行。
怎么做:在 user 仓库保存 token;写 reqUserInfo,用 GET 请求;请求拦截器从仓库取 token 放到请求头;拿到 name、avatar、roles、buttons、routes 后存仓库。转写稿调试时一开始拿不到用户信息,原因是请求头没带 token,补上后成功;不同账号返回的信息不同。
易错点:非组件区域取小仓库前要先引入大仓库,否则报错;token 字段大小写要准;刷新后本地存储还在,但仓库会清,所以还要重新获取用户信息。

退出登录接口替换 ​

为什么需要:退出不仅要清本地数据,还要通知服务器“这个 token 失效了”。
怎么想:退出登录是 POST,token 由请求拦截器统一带,不需要手动传参;接口没有返回数据,类型可按无返回处理。
怎么做:在 API 中写 reqUserLogout 并导出;在 user 仓库的退出/reset 逻辑里调用它,再清 pinia 和本地存储。
易错点:不要先清 token 再发请求,否则请求头没 token;无返回数据不用强行解构;退出登录可能在 token 过期或路由守卫中触发。

补 TS 类型与调试顺序 ​

为什么需要:参数和返回值有类型,后续写业务才有提示,减少字段写错。
怎么想:登录参数 LoginParams 用 username/password: string;登录返回 LoginResponse 含 token;用户信息返回含 name/avatar/roles/buttons/routes,后三个是字符串数组。
怎么做:类型可先放 API 文件或单独类型文件并导出;接口函数第二个泛型约束返回数据。转写稿建议先把业务跑通,最后统一补类型。
易错点:数组不要写成字符串;类型名和字段名按文档;调试时先看 Swagger,再看 Network 请求路径、代理是否生效、请求头有没有 token、Application 里本地存储是否写入。

转写稿后半段进入品牌管理静态搭建,不属于本节笔记“登录接口替换”的范围,本文不展开。

先搞清楚品牌管理模块要做什么 ​

品牌管理挂在商品管理模块下面。前台项目详情页里见过的那些品牌——小米、华为——就是这里维护的数据;当时前台的数据是写死的,还会出现“今天写一批、明天换一批”的情况,根子就在于后台没有真正的管理入口。所以这个模块的业务一句话概括:把已有的品牌数据用表格加分页展示出来,再提供新增、修改、删除,另外补一个导出 Excel。

后台管理系统有三样东西是必用的:按钮、表格、分页器。所以这一节先搭静态,顺序就是卡片容器 → 表格 → 列 → 分页。

外层容器为什么用 el-card ​

为什么需要:成品效果里外层有一个框,鼠标移上去还有一层淡淡的阴影。如果自己拿 div 加 border、加 box-shadow 也能做,但没必要。

怎么想:Element Plus 里负责“把信息聚合在容器中展示”的组件就是卡片 el-card,它由 header(头部,可选)和 body(身体)组成。所有 Element Plus 组件都以 el- 开头,是全局组件,不需要单独引入。

怎么做:先写 <el-card>,再给它加 shadow 属性。

html
<el-card shadow="hover">
  <template #header>品牌管理</template>
  <!-- 内容区 -->
</el-card>

易错点:第一次写完发现“怎么没有那个阴影效果”。这时要去查文档,而不是猜——组件页最底下是 API 表格,shadow 的说明是“设置阴影显示时机,默认 always”。改成 hover 才是“只有鼠标移上去才出现阴影”。另外头部那条分隔线是贯穿整块的,用 #header 插槽放内容即可;#header 是 Vue 的插槽简写,写法要记牢。

表格:el-table 的 border ​

为什么需要:品牌数据是多行多列的,用表格展示最直观;不加边框时列与列之间没有竖线,数据一多容易看串行。

怎么做:<el-table :data="品牌列表" border>。border 是布尔属性,写上就有纵向边框。

易错点:border 不要写成 :border="true" 之外的奇怪形式;更常见的错是忘了给 :data 绑定数据源,结果表头有了、一行数据都没有。

列:el-table-column 的 label、width、align ​

为什么需要:表格有多少列、每列叫什么、多宽、文字怎么对齐,都要在列组件上描述。

怎么做:

html
<el-table-column label="品牌名称" width="180" align="center" prop="tmName" />
  • label:顶部标题,也就是人看到的那一行表头文字;
  • width:列宽度;
  • align="center":该列文字居中。

易错点:label 是“显示名”,不是数据字段名;真正决定取哪条数据的是 prop,两者写混了就会出现“表头对、内容空”的情况。

分页:el-pagination 的 layout ​

为什么需要:品牌数据不止一页,分页器要同时显示总条数、每页条数选择、上一页、页码、下一页、跳转等元素。

怎么想:这些子元素不是一个个写出来的,而是由 layout 一个字符串统一控制——写哪些、按什么顺序排,全看它。

怎么做:

html
<el-pagination layout="total, sizes, prev, pager, next, jumper" />

易错点:layout 是逗号分隔的字符串,顺序就是页面上的显示顺序;想调整位置就调这个字符串的顺序,不要去改样式。

获取品牌数据并渲染 ​

为什么需要:静态搭完只有表头和空壳,必须把接口返回的真实数据填进去。

怎么想:沿用前面登录接口的做法——接口按模块分文件管理(用户、权限、商品各一个文件夹),品牌属于商品模块,所以请求函数写在商品模块的 API 文件里,再由页面调用。

怎么做:调用接口拿到列表,赋给 el-table 的 :data;接口返回的字段名要和列上的 prop 一一对应。

易错点:字段名大小写、拼写必须和文档一致,差一个字母后台就解析不出来,这和前面写登录接口时踩的是同一个坑。

分页展示数据的业务 ​

为什么需要:一页把所有品牌铺开,页面又长又难用。

怎么想:分页要有两个驱动变量——当前页码和每页条数;页码一变就重新取数据,总条数则来自接口返回,交给 total 显示。

怎么做:页码变化时重新发请求,用新结果覆盖列表数据,再刷新总条数。

易错点:只改了页码变量却没重新请求,或者请求了却没更新总条数,都会出现“页码跳了、数据没变”的假分页。

这一节最容易踩的坑 ​

第一,不看文档直接抄代码。组件属性、插槽、API 全在文档里,shadow 就是靠翻 API 表才找到的。第二,按钮的 type 要拼准:primary 是主题色,success 绿、info 灰、warning 黄、danger 红,拼错就不生效。第三,图标写成 :icon="Plus",冒号不能少——它绑的是动态值,图标名首字母大写,Plus、Edit、Download 这几个增删改查常用图标记住即可。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

1. 代理跨域配置 + 登录接口 API 模块(TS)

ts
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  // 代理跨域:写在 server 里,和 plugins、resolve 是平级的兄弟
  server: {
    proxy: {
      '/app-dev': {
        target: 'http://sph-h5-api.atguigu.cn',
        changeOrigin: true,
        // 真实接口路径里没有 /app-dev,必须重写掉
        rewrite: (path) => path.replace(/^\/app-dev/, '')
      }
    }
  }
})
ts
// src/api/user/index.ts
import request from '@/utils/request'
import type { LoginParams, LoginResponseData, UserInfoResponseData } from './type'

// 枚举统一管理当前模块的接口地址
enum API {
  LOGIN_URL = '/admin/acl/index/login',
  USERINFO_URL = '/admin/acl/index/info',
  LOGOUT_URL = '/admin/acl/index/logout'
}

// 登录接口:POST,请求体携带 username / password
export const reqLogin = (data: LoginParams) =>
  request.post<any, LoginResponseData>(API.LOGIN_URL, data)

// 获取用户信息:GET,token 由请求拦截器统一带上
export const reqUserInfo = () =>
  request.get<any, UserInfoResponseData>(API.USERINFO_URL)

// 退出登录:POST,无参数、无返回数据
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
ts
// src/api/user/type.ts
export interface LoginParams {
  username: string
  password: string
}

export interface LoginResponseData {
  token: string
}

export interface UserInfoResponseData {
  name: string
  avatar: string
  roles: string[]
  buttons: string[]
  routes: string[]
}

要点与易错点:

  • 代理的 key /app-dev 必须和 axios 的 baseURL 前缀完全一致;真实接口路径里并没有 /app-dev,所以 rewrite 不能漏。
  • server 必须写在 defineConfig 的第一层,和 plugins、resolve 平级,别塞进 css 或别的地方。
  • 配置文件改了必须重启 dev server,否则代理不生效,会直接报跨域。
  • changeOrigin: true 用于修改请求头里的 host,是跨域能通的关键。
  • 枚举只是统一管理地址(用对象存也行),好处是接口地址集中一处,方便替换。
  • request.post<any, LoginResponseData> 的第二个泛型参数约束的是服务器返回数据的类型,第一个是 axios 返回的 data 类型,别写反。
  • username、password 字段名必须和后台约定的大小写一字不差,写错后台解不出来。

2. 登录 / 用户信息 / 退出登录的 Pinia 仓库 + 请求拦截器带 token

ts
// src/store/modules/user.ts
import { defineStore } from 'pinia'
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user'
import type { LoginParams } from '@/api/user/type'
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'

const useUserStore = defineStore('User', {
  state: () => ({
    token: GET_TOKEN(), // 一刷新就从本地存储取回,避免直接丢失
    username: '',
    avatar: ''
  }),
  actions: {
    async userLogin(data: LoginParams) {
      const result = await reqLogin(data)
      if (result.code === 200) {
        // 登录成功:仓库存一份 + 本地存储持久化一份
        this.token = result.token
        SET_TOKEN(result.token)
        return 'ok'
      } else {
        return Promise.reject(new Error(result.message))
      }
    },
    async userInfo() {
      const result = await reqUserInfo()
      if (result.code === 200) {
        this.username = result.name
        this.avatar = result.avatar
        return 'ok'
      } else {
        return Promise.reject(new Error(result.message))
      }
    },
    async logout() {
      const result = await reqLogout()
      if (result.code === 200) {
        this.token = ''
        this.username = ''
        this.avatar = ''
        REMOVE_TOKEN()
        return 'ok'
      } else {
        return Promise.reject(new Error(result.message))
      }
    }
  }
})

export default useUserStore
ts
// src/utils/request.ts —— 在已封装好的实例上补请求拦截器
import useUserStore from '@/store/modules/user'

request.interceptors.request.use((config) => {
  // 非组件区域(TS 文件)拿小仓库,必须先引大仓库再调用得到实例
  const userStore = useUserStore()
  if (userStore.token) {
    config.headers.token = userStore.token
  }
  return config
})

要点与易错点:

  • 在非组件区域(store、utils 这类 TS 文件)里取小仓库,必须先 import useUserStore 再 const userStore = useUserStore();只有在组件里才能直接调。
  • 请求头带 token 一定要先判空,否则会带一个 undefined 上去,后台直接告诉你没带 token。
  • token 要存两处:pinia 里一份(供全局用),本地存储一份(刷新后还能恢复);GET_TOKEN() 写在 state 初始值里就是干这个的。
  • logout 只是通知服务器"我要退出",本身不需要返回值;但仓库数据和本地存储必须同时清空。
  • result.token 还是 result.data.token,取决于响应拦截器有没有提前解包 data,以你项目里实际的返回结构为准,别照抄。
  • 触发时机要记牢:userInfo 在全局路由守卫里调(有用户信息就放行,没有就先请求再放行);logout 在 token 过期或手动退出时调。

3. 品牌管理模块静态搭建(Vue3 + Element Plus)

vue
<template>
  <el-card shadow="hover">
    <!-- 顶部按钮区 -->
    <el-button type="primary" size="default" :icon="Plus">添加</el-button>
    <el-button type="primary" size="default" :icon="Download">导出</el-button>

    <!-- 表格:border 让竖方向也有边框 -->
    <el-table border style="margin: 16px 0">
      <el-table-column label="序号" width="80" align="center" />
      <el-table-column label="品牌名称" align="center" />
      <el-table-column label="品牌LOGO" align="center" />
      <el-table-column label="品牌地址" align="center" />
    </el-table>

    <!-- 分页:layout 决定子组件的位置布局 -->
    <el-pagination
      layout="prev, pager, next, jumper, ->, total"
      :total="18"
      v-model:current-page="1"
    />
  </el-card>
</template>

<script setup lang="ts">
// 图标要单独引入,模板里 :icon 是动态绑定
import { Plus, Download } from '@element-plus/icons-vue'
</script>

要点与易错点:

  • Element Plus 的组件都是 el- 前缀、全局注册,不用 import 就能用;但图标必须从 @element-plus/icons-vue 单独引入。
  • :icon="Plus" 的冒号不能少,冒号是动态绑定(传的是组件对象),不写冒号就变成传字符串 "Plus",图标不显示。
  • el-card 默认自带边框和鼠标移入效果,shadow="hover" 是阴影显示时机(默认是 always),不需要自己写盒子样式。
  • table 的 border 让表格竖着方向也有边框;column 的 label 是表头文字、width 是列宽、align="center" 是文字居中。
  • pagination 的 layout 决定子组件的位置和顺序,-> 是往右推的间隔符,total 是总条数。
  • 先老老实实手写一遍熟悉 API,再去装 Element Plus 的代码提示插件,否则一上来全是补全提示容易懵。

常见错误 ​

  1. 登录请求 404 / 路径不对

    • 现象:浏览器请求地址仍是 /app-dev/... 或直接打到前端本地路径。
    • 原因:Vite 代理未配置、rewrite 未去掉 /app-dev,或接口地址拼接错误。
    • 修法:检查 vite.config.ts 的 server.proxy,确认 target、changeOrigin、rewrite 正确;修改配置后重启开发服务。
  2. 登录接口参数名写错,后台收不到账号密码

    • 现象:请求已发出但登录失败,或后台提示参数缺失。
    • 原因:username/password 大小写、拼写与接口文档不一致。
    • 修法:严格按 Swagger 文档字段名传参;联调前先用在线文档或接口工具验证一次。
  3. 获取用户信息失败,提示未带 token / 401

    • 现象:登录成功,但获取用户信息拿不到数据,左侧菜单或用户信息异常。
    • 原因:请求拦截器未从仓库取 token,或 token 未写入仓库/本地存储。
    • 修法:登录成功后同时保存 token 到仓库和本地存储;确认请求拦截器统一携带 token;再检查获取用户信息接口是否走代理。
  4. 非组件区域使用小仓库报错或拿不到数据

    • 现象:在 store 或普通 TS 文件中直接调用用户小仓库报错/为空。
    • 原因:非组件区域未先注入大仓库,或未调用 useUserStore()。
    • 修法:在非组件 TS 区域先获取大仓库,再取用户小仓库;组件内按正常方式使用。
  5. 配置文件改了但页面行为没变化

    • 现象:代理、路径已改,刷新仍报错。
    • 原因:Vite 配置文件变更后未重启开发服务。
    • 修法:停止并重新执行开发启动命令;确认没有改错文件层级和配置项位置。
  6. 退出登录只清了本地数据,没有通知服务端

    • 现象:本地 token 清了,但服务端会话/状态未同步。
    • 原因:退出登录 action 中只删除仓库和本地存储,未调用退出接口。
    • 修法:在退出逻辑中补充退出登录请求,再清理仓库和本地存储;请求头 token 交由拦截器统一处理。

练习 ​

  1. 按接口文档完成登录接口替换:定义请求参数与返回值 TS 类型,登录成功后保存 token 到仓库和本地存储。
  2. 配置 Vite 代理跨域,要求真实请求去掉 /app-dev 前缀;修改后重启并验证登录请求成功。
  3. 替换获取用户信息接口:在路由守卫触发时请求用户信息,确认请求头携带 token,并把昵称、头像等写入仓库。
  4. 替换退出登录接口:分别验证主动退出和 token 失效场景,确认先通知服务端,再清理仓库与本地存储。
  5. 使用 Element Plus 完成品牌管理静态页:卡片、表格、分页、添加/导出按钮和图标布局,要求结构清晰、按钮类型与图标使用正确。

原视频对照 ​

#视频时长
1书写登录模块接口23:58
2完成真实的登录接口替换14:13
3完成真实的获取用户信息接口替换29:45
4完成真实的接口替换之退出登录10:03
5品牌管理静态搭建-116:55

小结 ​

  1. 真实项目联调第一步不是写业务,而是先对照 Swagger 确认接口地址、请求方法、参数名和返回结构。
  2. 代理跨域配置属于工程配置,改完必须重启;路径重写要与真实接口前缀严格对应。
  3. 登录成功的核心产物是 token;token 的保存、携带、清理要形成完整闭环。
  4. 获取用户信息依赖 token,且通常在路由守卫中触发;拿不到用户信息时优先查请求头和代理。
  5. 非组件 TS 区域使用 Pinia 小仓库时,必须先注入大仓库,这是高频踩坑点。
  6. 退出登录不是单纯清本地数据,应先通知服务端,再清理前端状态。
  7. TS 类型可以后补,但接口参数和返回值最终要补全,避免长期 any 造成联调隐患。
  8. Element Plus 静态搭建要先看文档再写,重点关注组件属性、插槽、按钮类型和图标用法。