主题
登录业务接口替换替换为真实项目接口与品牌管理模块 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. 代理跨域配置 + 登录接口 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 useUserStorets
// 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 的代码提示插件,否则一上来全是补全提示容易懵。
常见错误
登录请求 404 / 路径不对
- 现象:浏览器请求地址仍是
/app-dev/...或直接打到前端本地路径。 - 原因:Vite 代理未配置、
rewrite未去掉/app-dev,或接口地址拼接错误。 - 修法:检查
vite.config.ts的server.proxy,确认target、changeOrigin、rewrite正确;修改配置后重启开发服务。
- 现象:浏览器请求地址仍是
登录接口参数名写错,后台收不到账号密码
- 现象:请求已发出但登录失败,或后台提示参数缺失。
- 原因:
username/password大小写、拼写与接口文档不一致。 - 修法:严格按 Swagger 文档字段名传参;联调前先用在线文档或接口工具验证一次。
获取用户信息失败,提示未带 token / 401
- 现象:登录成功,但获取用户信息拿不到数据,左侧菜单或用户信息异常。
- 原因:请求拦截器未从仓库取 token,或 token 未写入仓库/本地存储。
- 修法:登录成功后同时保存 token 到仓库和本地存储;确认请求拦截器统一携带 token;再检查获取用户信息接口是否走代理。
非组件区域使用小仓库报错或拿不到数据
- 现象:在
store或普通 TS 文件中直接调用用户小仓库报错/为空。 - 原因:非组件区域未先注入大仓库,或未调用
useUserStore()。 - 修法:在非组件 TS 区域先获取大仓库,再取用户小仓库;组件内按正常方式使用。
- 现象:在
配置文件改了但页面行为没变化
- 现象:代理、路径已改,刷新仍报错。
- 原因:Vite 配置文件变更后未重启开发服务。
- 修法:停止并重新执行开发启动命令;确认没有改错文件层级和配置项位置。
退出登录只清了本地数据,没有通知服务端
- 现象:本地 token 清了,但服务端会话/状态未同步。
- 原因:退出登录 action 中只删除仓库和本地存储,未调用退出接口。
- 修法:在退出逻辑中补充退出登录请求,再清理仓库和本地存储;请求头 token 交由拦截器统一处理。
练习
- 按接口文档完成登录接口替换:定义请求参数与返回值 TS 类型,登录成功后保存 token 到仓库和本地存储。
- 配置 Vite 代理跨域,要求真实请求去掉
/app-dev前缀;修改后重启并验证登录请求成功。 - 替换获取用户信息接口:在路由守卫触发时请求用户信息,确认请求头携带 token,并把昵称、头像等写入仓库。
- 替换退出登录接口:分别验证主动退出和 token 失效场景,确认先通知服务端,再清理仓库与本地存储。
- 使用 Element Plus 完成品牌管理静态页:卡片、表格、分页、添加/导出按钮和图标布局,要求结构清晰、按钮类型与图标使用正确。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 书写登录模块接口 | 23:58 |
| 2 | 完成真实的登录接口替换 | 14:13 |
| 3 | 完成真实的获取用户信息接口替换 | 29:45 |
| 4 | 完成真实的接口替换之退出登录 | 10:03 |
| 5 | 品牌管理静态搭建-1 | 16:55 |
小结
- 真实项目联调第一步不是写业务,而是先对照 Swagger 确认接口地址、请求方法、参数名和返回结构。
- 代理跨域配置属于工程配置,改完必须重启;路径重写要与真实接口前缀严格对应。
- 登录成功的核心产物是 token;token 的保存、携带、清理要形成完整闭环。
- 获取用户信息依赖 token,且通常在路由守卫中触发;拿不到用户信息时优先查请求头和代理。
- 非组件 TS 区域使用 Pinia 小仓库时,必须先注入大仓库,这是高频踩坑点。
- 退出登录不是单纯清本地数据,应先通知服务端,再清理前端状态。
- TS 类型可以后补,但接口参数和返回值最终要补全,避免长期
any造成联调隐患。 - Element Plus 静态搭建要先看文档再写,重点关注组件属性、插槽、按钮类型和图标用法。