从0开始搭建Vue3前端骨架(附登录接口案例)
从0开始搭建Vue3前端骨架(附登录接口案例)
本文将带你从零搭建一个企业级 Vue 3 后台管理骨架,包含登录、路由守卫、状态管理、Axios 封装等核心模块。
::⚠️: 如果修改了 npm 全局路径,升级 Node.js 前最好备份并清理这些目录,避免权限问题。
- 使用
🛠 技术栈
说明:以上版本号为本文撰写时(2026.05)项目所依赖的实际版本,后续升级请以
package.json为准。
| 技术 | 版本 | 用途 |
|---|---|---|
| Vue | ^3.5.33 | 前端框架 |
| TypeScript | ^6.0.3 | 类型安全 |
| Vite | ^8.0.10 | 构建工具 |
| Pinia | ^3.0.4 | 状态管理 |
| Vue Router | ^5.0.6 | 路由 |
| Element Plus | ^2.13.7 | UI 组件库 |
| Tailwind CSS | ^3.4.19 | 原子化 CSS |
| Axios | ^1.16.0 | HTTP 客户端 |
🛠 开发环境与工具
- IDE:IntelliJ IDEA(推荐) / VS Code
- Node.js:20.19.0 或更高版本(本教程使用 v24.15.0)
- 包管理器:npm(Node.js 自带)
一、环境准备
1.1 Node.js 安装与配置
1.1.1 下载 Node.js
在浏览器中输入 nodejs.cn 或点击下方链接 进入中文官网
![]()
点击 下载安装
![]()
选择匹配的操作系统,点击下载
![]()
下载完成
![]()
1.1.2 下载其他版本的 Node.js (可以跳过该步骤,有版本需求可选择观看)
如果需要用到其他版本的 Node.js ,可以按下面的方式操作
点击 全部安装包
![]()
路径末尾处就是版本号,需要什么版本就在末尾输入对应的版本号,下面以 v22.10.0 为例
![]()
输入 v20.10.0 后,即可得到下面的页面
![]()
- Windows 用户选择
.msi安装包 - macOS 用户选择
.pkg - Linux 用户使用包管理器或二进制文件。
windows 64位选择如下
![]()
下面提供另一种查找方法
去除路径末尾的版本号
![]()
回车
![]()
得到下面的界面
![]()
向下拖动(会很长),就能找到我们需要的版本了,点击该版本
![]()
进入下面的页面
![]()
- Windows 用户选择
.msi安装包 - macOS 用户选择
.pkg - Linux 用户使用包管理器或二进制文件。
windows 64位选择如下
![]()
1.1.3 安装
直接双击即可,接下来 一路 next 即可
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
1.1.4 验证
win + r 进入运行界面,输入 cmd ,回车
![]()
输入 node -v 查看 node 的版本
node -v
![]()
输入 npm -v 查看 npm 的版本
npm -v
![]()
能看到版本就说明安装成功了 ^ v ^
1.1.5 配置全局路径 (可以跳过该步骤,推荐完成)
作用:存放 全局安装的模块(通过 npm install -g 安装的包)
在 Node 安装目录下新建 node_global 文件夹
node_global
![]()
进入 node_global 文件夹,并复制其路径
![]()
在此处输入如下指令,设置 npm 全局安装的模块 的存放位置
npm config set prefix
![]()
路径为我们之前复制的 node_global 文件夹的路径,将路径复制在后面,回车
![]()
1.1.6 配置缓存路径(可以跳过该步骤,推荐完成)
作用:存放 npm 下载包时的缓存文件 。当执行 npm install 时,npm 会先将下载的包缓存到这里,下次再安装相同版本时直接从缓存读取,提高安装速度
在 Node 安装目录下新建 node_cache 文件夹
![]()
进入 node_cache 文件夹,并复制其路径
![]()
在此处输入如下指令, 设置 npm 下载包时的临时缓存目录
npm config set cache
![]()
路径为我们之前复制的 node_cache 文件夹的路径,将路径复制在后面,回车
![]()
1.1.7 配置国内的镜像
(2026.5.3 该镜像还能使用)
npm config set registry https://registry.npmmirror.com
![]()
1.2 包管理器选择(npm / pnpm)
包管理器常用的有以下几种:
- npm(Node Package Manager):Node.js 自带,无需安装,命令简洁,生态最广。
- pnpm:快速的、省空间的替代品,通过硬链接共享依赖,适合大型项目或多项目开发。
- yarn:历史选择,目前已较少用于新项目。
1.2.1 本教程的选择
我们使用 npm 作为包管理器,因为:
- 安装 Node.js 后即可使用,无需额外步骤。
- 绝大多数 Vue 3 官方文档和示例都采用 npm。
- 对于本项目的规模,npm 的性能完全够用。
如果你希望尝试 pnpm,可以后续自行替换命令(例如 pnpm install 代替 npm install),不影响项目运行。
二、创建 Vue3 项目(含 TypeScript)
2.1 创建项目
2.1.1 创建文件夹
新建前端文件夹
frontend
![]()
2.1.2 idea 打开该文件夹
选择刚才创建的文件夹打开
![]()
界面如下
![]()
2.1.3 创建 Vue3 项目
打开终端
npm create vue@latest
这是 终端权限问题 ,解决方法请看 附录:常见问题(FAQ) -> 终端问题解决
![]()
第一次创建会有提示,输入 y 回车
![]()
输入项目名称,这个可以自己定义,回车
book-trading-frontend
![]()
这里选择 Yes
![]()
↑/↓ 箭头切换,空格 选中
![]()
这两个都不用选,直接回车
![]()
移动上下箭头,选择 Yes
![]()
效果,这个文件夹就是我们创建的 vue3项目(项目名称可能不同)
![]()
2.1.4 安装所有依赖
路径切换到我们创建的 vue3 项目
![]()
为 vue3 项目安装所有依赖(过程可能会有点久,我这里安装了5min)
npm i
![]()
2.1.5 启动项目(ctrl + c 停止项目)
npm run dev
![]()
点击链接,出现如下界面说明成功了 ^ v ^
![]()
2.2 项目结构解读
![]()
这里可以简单了解一下
book-trading-frontend/
├── .vscode/ # VS Code 编辑器配置文件夹(推荐设置)
├── node_modules/ # 项目依赖包目录(自动生成,无需手动修改)
├── public/ # 公共静态资源(不会经过构建,直接复制到 dist)
│ └── favicon.ico # 浏览器标签页图标
├── src/ # 源代码目录(核心开发区域)
│ ├── router/
│ │ └── index.ts # 路由配置文件(已自动配置基础路由)
│ ├── stores/
│ │ └── counter.ts # Pinia 示例 store(演示计数功能,可删除)
│ ├── App.vue # 根组件(整个应用的容器)
│ └── main.ts # 应用入口文件(挂载 Vue、Router、Pinia)
├── .editorconfig # 编辑器统一风格配置(缩进、字符集等)
├── .gitattributes # Git 属性配置(如换行符处理)
├── .gitignore # Git 忽略文件列表(已预设 node_modules 等)
├── .oxlintrc.json # Oxlint 代码检查工具配置文件
├── .prettierrc.json # Prettier 代码格式化规则(如单引号、无分号)
├── env.d.ts # TypeScript 环境类型声明(如 Vite 的 import.meta.env)
├── eslint.config.ts # ESLint 配置文件(TypeScript 格式)
├── index.html # 宿主 HTML 文件,Vue 应用将挂载到其中的 <div id="app">
├── package.json # 项目依赖与脚本定义(如 dev、build、preview)
├── package-lock.json # 依赖锁定文件(保证安装版本一致)
├── README.md # 项目说明文件(可自行补充内容)
├── tsconfig.app.json # TypeScript 配置(针对 src 目录)
├── tsconfig.json # TypeScript 根配置文件(引用其他 tsconfig)
├── tsconfig.node.json # TypeScript 配置(针对 vite.config.ts 等 Node 环境文件)
└── vite.config.ts # Vite 构建配置文件(定义插件、别名、代理等)
三、整理目录结构与文件
3.1 api
作用:管理前端向后端发送的请求
在 src 目录下创建 api 目录
![]()
在 api 目录下创建 services 和 types 目录
- services:存放API请求函数
- types:存放TypeScript类型定义
![]()
3.2 assets
作用:存放需要经过构建工具处理的静态资源 ,例如图片、字体、全局样式文件等
在 src 目录下创建 assets 目录
![]()
3.3 layouts
作用:存放布局组件,例如整个后台管理系统的公共框架(侧边栏菜单、顶部导航栏、底部版权信息、内容区域等)
在 src 目录下创建 layouts 文件夹
![]()
3.4 stores
作用:存放 Pinia 定义的 store 文件 (类似于java中的全局变量、全局方法等)
counter.ts 是演示示例,可以保留也可以删除,这里选择删除示例
![]()
3.5 views
作用:存放具体的页面
在 src 目录下创建 views 目录
![]()
四、集成 Element Plus 与 Tailwind CSS
4.1 Element Plus
4.1.1 安装
安装指令:
npm i element-plus --save
![]()
安装成功(7min):
![]()
4.1.2 在 main.ts 中引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
![]()
接下来我们导入后,还要交给 app 使用(注意在挂载 mount 之前)
app.use(ElementPlus)
![]()
4.1.3 验证
在 App.vue 中创建一个按钮组件(因为是 element-plus 的组件,所以一般都是 el-xxx )
能看见提示说明成功了
![]()
我们可以随便写点
<el-button>Button</el-button>
![]()
启动项目
npm run dev
![]()
点开地址可以看到如下效果
![]()
4.1.4 Element Plus 官网
A Vue 3 UI Framework | Element Plus
官网上介绍了如何安装使用,这里可以简单看一下,和我们的安装步骤是一样的
右上角可以切换中文
![]()
点击指南
![]()
左侧找到快速开始
![]()
![]()
npm的安装指令也有
npm install element-plus --save
![]()
4.2 Tailwind CSS
4.2.1 安装
安装指令:
npm install -D tailwindcss
![]()
安装成功(3s):
![]()
初始化指令:
(此处出错的可以前往 附录:常见问题(FAQ) -> Tailwind CSS初始化错误)
npx tailwindcss init -p
打开我们初始化得到的 tailwind.config.js
![]()
为 content 添加如下内容
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html", // 扫描根目录的 HTML 文件
"./src/**/*.{vue,js,ts,jsx,tsx}", // 扫描 src 下所有 Vue/JS/TS 等文件
],
theme: {
extend: {},
},
plugins: [],
}
![]()
4.2.2 在 main.css 中引入
在 assets 目录下创建 main.css 用来存放全局样式
![]()
将 tailwind 引入
main.css
@tailwind base;
@tailwind components;
@tailwind utilities;
![]()
4.2.3 在 main.ts 中引入
import './assets/main.css'
![]()
4.2.4 验证
在 App.vue 中使用,添加 class 样式,可以看到有如下 tailwind css 提示
![]()
启动项目
npm run dev
可以看到如下效果:
![]()
4.2.5 Tailwind CSS 官网
安装 - TailwindCSS中文文档 | TailwindCSS中文网
这里有安装步骤,略有不同
![]()
往下滑可以查看这些样式,ctrl + k 可以搜索样式进行查看
![]()
例如:对 背景色 并不熟悉,可以通过下面的方法查看
-
ctrl + k 打开搜索

-
输入关键词 background color ,选择第一个进行查看

-
左侧的是 tailwind ,右侧的是其对应的 纯css 样式

-
直接搜索不懂的样式也是可以的

五、封装 Axios 请求模块(核心)
5.1 安装
安装指令:
npm i axios
![]()
安装成功(18s):
![]()
5.2 创建 request.ts 并配置拦截器
request.ts
![]()
request.ts
type:仅用作类型注解,不会在运行时代码中出现
import axios, { type AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
// 创建 axios 实例,配置基础 URL
const service = axios.create({
baseURL: '/api'
})
// 请求拦截器:拦截前端向后端发送的所有请求
service.interceptors.request.use(
(config) => {
// 后续登录功能实现后,可在此处添加 token
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器:拦截后端向前端返回的所有响应
service.interceptors.response.use(
(response: AxiosResponse) => {
return response.data
},
(error) => Promise.reject(error)
)
export default service
5.3 解决跨域问题
跨域问题描述:
我们的前端是http://localhost:5173,后端假设是http://localhost:8080。
只要 协议、域名、端口 有任何一项不同,就属于“跨域”,我们的前端和后端的端口不同,前端去请求后端就会产生跨域问题
我们需要将这一段配置写到 vite.config.ts 中
![]()
也就是这一段,target 我改成我们以后的后端端口,这样就能和后端连接了
server: {
proxy: {
// 统一代理所有以 /api 开头的请求
'/api': {
target: 'http://localhost:8080', // 1. 目标:转发到后端服务器
changeOrigin: true, // 2. 关键:改变请求头中的Origin为目标地址,防止某些后端校验失败
// 3. 重写路径:用''空字符串代替`/api`,/是转移,^是以什么开头
rewrite: (path) => path.replace(/^/api/, ''),
}
}
}
![]()
整体代码如下
![]()
六、登录与布局页面设计
6.1 login.vue 登录页面
6.1.1 创建页面
在 views 目录下如下配置
![]()
6.1.2 注册路由
路由注册,将 login/index.vue 页面交给 index.ts 管理
顺便将根路径 / 也 redirect (重定向)到 /login
同时改造一下 router
index.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', redirect: '/login'},
{ path: '/login', component: () => import('@/views/login/index.vue') },
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
![]()
修改 App.vue 让它能够通过 路由 来展示 login/index.vue 页面
-
<router-view></router-view>:展示路由中的页面
<script setup lang="ts"></script>
<template>
<router-view></router-view>
</template>
<style scoped></style>
![]()
6.1.3 页面实现
简单编写一下 index.vue
login/index.vue
<script setup lang="ts">
// 登录
const login = () => {
}
</script>
<template>
<el-button class="text-pink-500" @click="login">员工登录</el-button>
</template>
<style scoped>
</style>
启动项目 npm run dev,可以看到如下效果
![]()
6.2 Layout.vue 布局组件
6.2.1 创建页面
![]()
6.2.2 路由注册
路由注册,将 Layout.vue 页面交给 index.ts 管理
{ path: '/layout', component: () => import('@/layouts/Layout.vue')},
![]()
6.2.3 页面实现
前往 Element Plus 官网
我们使用这种布局
![]()
<template>
<div class="common-layout">
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
![]()
修改 login/index.vue 中的 login 函数,使其跳转至 Layout.vue 页面
// 获得路由器实例
const router = useRouter()
// 登录
const login = () => {
router.push('/layout')
}
![]()
启动项目 npm run dev 效果如下:
![]()
稍加改造
Layout.vue
<script setup lang="ts">
import {useRouter} from "vue-router";
import { SwitchButton } from '@element-plus/icons-vue'
const router = useRouter()
const logout = () => {
router.push('/login')
}
</script>
<template>
<div class="common-layout">
<el-container>
<el-header class="header">
<div class="brand">
<div class="h-left">
<div class="font-bold">员工管理</div>
</div>
<div class="h-right">
<a href="" @click="logout">
<el-icon><SwitchButton /></el-icon> 退出
</a>
</div>
</div>
</el-header>
<el-container>
<el-aside class="h-screen w-40 bg-pink-50">Aside</el-aside>
<el-main class="p-5">
hello
</el-main>
</el-container>
</el-container>
</div>
</template>
<style scoped>
.header {
@apply border-b border-gray-200
shadow-sm p-8
}
.brand {
@apply flex items-center justify-between
h-full
}
</style>
启动项目,效果如下:
![]()
6.3 Layout.vue 的子页面
![]()
6.3.1 创建页面
在 views 目录下创建两个 vue 页面
![]()
workbench/index.vue
<script setup lang="ts">
</script>
<template>
<p>控制台页面</p>
</template>
<style scoped>
</style>
data/index.vue
<script setup lang="ts">
</script>
<template>
<p>数据管理页面</p>
</template>
<style scoped>
</style>
6.3.2 配置路由
index.ts 如下:
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', redirect: '/login'},
{ path: '/login', component: () => import('@/views/login/index.vue') },
{
path: '/layout',
component: () => import('@/layouts/Layout.vue'),
redirect: '/layout/workbench',
children: [
{ path: 'workbench', component: () => import('@/views/workbench/index.vue') },
{ path: 'data', component: () => import('@/views/data/index.vue')},
]
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
![]()
6.4 实现菜单
我们要实现的效果是,点击 侧边栏 的 菜单项 ,右边展示对应菜单项的页面
![]()
在 Element Plus 官网上找到我们需要的代码
![]()
![]()
我将源码简化处理了,保留了两个菜单项
- router:使用路由,就可以进行页面的跳转了
- index:指定跳转的路径
<el-menu
class="h-screen"
router
>
<el-menu-item index="/layout/workbench">工作台</el-menu-item>
<el-menu-item index="/layout/data">数据统计</el-menu-item>
</el-menu>
将这段代码放到 <el-aside> ,也就是侧边区域
![]()
在 el-main 区域设置 <router-view></router-view> ,相当于将 “展台” 设置在 el-main 区域,页面就可以在站台上展示了。App.vue 中也设置了这个
<router-view></router-view>
![]()
效果如下:
![]()
6.5 404 页面
6.5.1 创建页面
![]()
6.5.2 路由注册
{ path: '/:pathMatch(.*)', component: () => import('@/views/NotFound/index.vue') },
![]()
6.5.3 页面实现
![]()
我将官网的代码修改一下
<script setup lang="ts">
import router from "@/router";
import { CircleCloseFilled } from '@element-plus/icons-vue'
</script>
<template>
<div class="flex justify-center items-center min-h-screen">
<el-result>
<template #icon>
<CircleCloseFilled style="width: 100px; height: 100px; color: red"/>
</template>
<template #title>
<div class="text-3xl text-gray-700">这个页面不存在</div>
</template>
<template #sub-title>
<div class="text-xl text-gray-600">请检查您输入的网址是否正确</div>
</template>
<template #extra>
<el-button
type="primary"
@click="router.push('/layout/workbench')"
:size="'large'"
style="width: 100px; height: 45px"
class="text-lg"
round
>
返回首页
</el-button>
</template>
</el-result>
</div>
</template>
<style scoped>
</style>
将代码复制到此处
![]()
输入一个错误的路径,可以看到自定义的404页面
http://localhost:5173/layout/workbenchgew
![]()
代码解释
![]()
到这里,前端的骨架差不多就搭建完成了,如果想知道具体的用法,可以看看后续的代码 ^ v ^
七、实现登录功能(前后端联调)
本章的后端接口及数据结构 仅为演示 ,实际开发请根据项目真实接口调整。
7.1 员工登录接口文档(仅供演示参考)
7.1.1 基本信息
请求路径:/admin/employee/login
请求方式:POST
接口描述:员工登录,验证用户名和密码,成功后返回 JWT 令牌及员工基本信息
7.1.2 请求参数
参数格式:application/json
参数说明:
| 参数名 | 类型 | 是否必须 | 备注 |
|---|---|---|---|
| username | string | 必须 | 用户名 |
| password | string | 必须 | 密码 |
请求示例:
{
"username": "admin",
"password": "123456"
}
7.1.3 响应数据
参数格式:application/json
参数说明:
| 参数名 | 类型 | 是否必须 | 备注 | |
|---|---|---|---|---|
| code | number | 必须 | 响应码,1 代表成功,0 代表失败 | |
| msg | string | 非必须 | 提示信息 | |
| data | object | 非必须 | 返回的数据 | |
| - id | number | 非必须 | 主键值 | |
| - userName | string | 非必须 | 用户名 | |
| - name | string | 非必须 | 姓名 | |
| - token | string | 非必须 | JWT 令牌 |
响应数据样例(成功):
json
{
"code": 1,
"data": {
"id": 1,
"userName": "admin",
"name": "管理员",
"token": "eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzc3NDQyNjU2fQ.8zKmwKV2L6tOLejQPGH49p1Kb8cYvwdmbZmRwQ2HfF4"
},
"msg": null
}
7.2 后端配置(仅供演示参考)
7.2.1 Result 后端统一返回类
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; // 编码:1成功,0和其它数字为失败
private String msg; // 错误信息
private T data; // 数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
7.2.2 EmployeeLoginDTO
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
}
7.2.3 EmployeeLoginVO
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {
@ApiModelProperty("主键值")
private Long id;
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("jwt令牌")
private String token;
}
7.2.4 EmployeeController
@RestController
@RequestMapping("/admin/employee")
public class EmployeeController {
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
// 略...
return Result.success(employeeLoginVO);
}
}
7.3 前端创建与后端一致的配置
7.3.1 ApiResponse 通用响应类型
对标后端的 Result
![]()
在 types 目录下创建 common.types.ts
code 必须,其他两个非必须
// API 通用响应类型
export interface ApiResponse<T> {
code: number
data?: T
msg?: string
}
7.3.2 DTO 与 VO 数据格式
对标后端的 EmployeeLoginDTO 和 EmployeeLoginVO
![]()
在 types 目录下创建 employee.types.ts
// 员工登录请求数据格式
export interface EmployeeLoginDTO {
username: string
password: string
}
// 员工登录返回的数据格式
export interface EmployeeLoginVO {
id: number
userName: string
name: string
token: string
}
7.4 login 页面实现
7.4.1 登录界面实现
这是一个简单的,只包含 用户名 和 密码 的表单
login/index.vue
<script setup lang="ts">
import { reactive } from 'vue'
import type {EmployeeLoginDTO} from "@/api/types/employee.types.ts";
const form = reactive<EmployeeLoginDTO>({
username: '',
password: ''
})
// 登录
const handleLogin = () => {
}
</script>
<template>
<div class="flex justify-center items-center min-h-screen">
<div class="login-card">
<h2>登录</h2>
<el-form>
<el-form-item>
<el-input
v-model="form.username"
placeholder="用户名"
clearable
/>
</el-form-item>
<el-form-item>
<el-input
v-model="form.password"
type="password"
placeholder="密码"
show-password
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style scoped>
.login-card {
@apply w-80 p-4 rounded-lg bg-white border;
}
.login-card h2 {
@apply text-center mb-6;
}
.el-form-item {
@apply mb-4;
}
.el-button {
@apply w-full;
}
</style>
页面效果如下:
![]()
7.4.2 表单校验实现
获取表单实例
// 获取表单实例
const ruleFormRef = ref<FormInstance>()
![]()
实例获取对象为 表单中的数据
ref="ruleFormRef"
![]()
这一步的作用是拿到表单中数据,且 ruleFormRef 中会包含一些特殊的方法,更加方便我们进行表单校验
表单验证内容
关于 username 数据的验证讲解
-
required: true:必填项 -
message: '请输入用户名':不符合要求(没有填写),会展示 “请输入用户名” -
trigger: 'blur':判断触发时机,这里是blur即失去聚焦
// 表单验证
const rules = reactive<FormRules<EmployeeLoginDTO>>({
username: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur'}
],
password: [
{required: true, message: '请输入密码', trigger: 'blur'},
{min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur'}
]
})
![]()
获取表单中的数据,也就是获取 username 和 password 两个输入框中的内容
数据类型为 EmployeeLoginDTO ,也就是我们要提交给后端的数据
![]()
将 from 和 rules 绑定给表单
:model="form"
:rules="rules"
![]()
from 中的元素需要通过 prop 绑定给表单项
![]()
效果如下:
![]()
7.5 登录逻辑
7.5.1 登录接口 api 实现
在 api/services 目录下创建 employee.ts ,用来存放员工相关的 api
![]()
employee.ts
// 员工登录
export const loginApi = () => {
}
接下来通过 接口文档 分析 参数 和 返回值
-
参数:
EmployeeLoginDTO类型的参数,即usernamepassword
-
返回值:
data中存储的是EmployeeLoginVO类型的返回值,即-
code -
dataidusernamenametoken
-
msg
-
请求方式为 Post,路径为 /admin/employee/login
因此,api 设计如下:
employee.ts
// 员工登录
import type {EmployeeLoginDTO, EmployeeLoginVO} from "@/api/types/employee.types.ts";
import type {ApiResponse} from "@/api/types/common.types.ts";
import request from "@/api/request.ts";
export const loginApi = (employeeLoginDTO: EmployeeLoginDTO): Promise<ApiResponse<EmployeeLoginVO>> => {
return request.post('/admin/employee/login', employeeLoginDTO)
}
7.5.2 登录页面调用 api
const router = useRouter()
// 登录
const handleLogin = () => {
// 没有填写值,直接返回
if (!ruleFormRef.value) return
// ruleFormRef 是表单实例,它里面有 validate 方法,可以验证表单
// 若验证通过,则返回 true,并赋值给valid;否则返回 false,并赋值给valid
ruleFormRef.value.validate(async (valid) => {
if (valid) { // valid 表示验证是否通过
// 调用登录接口,将 form 值传递给接口
const res = await loginApi(form)
// code 为 1 表示登录成功
if (res.code === 1 && res.data) {
ElMessage.success("登录成功!")
// 存储当前员工信息到浏览器中
localStorage.setItem('token', res.data.token)
// JSON.stringify (这里是将对象)转换成字符串
localStorage.setItem('loginUser', JSON.stringify(res.data))
router.push('/layout')
} else {
ElMessage.error(res.msg)
}
} else {
console.log('请按照表单规则填写')
}
})
}
![]()
登录效果如下(我有后端程序,因此能够成功得到反馈)
![]()
7.6 创建 Pinia Store 管理用户状态(stores/user.ts)
因为用户登录后的信息后期会经常用到,像 token 、 username 这些,因此,可以将这些方法存储在仓库,要用到的时候就可以随时调用
创建 user.ts
![]()
user.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { EmployeeLoginVO } from '@/api/types/employee.types'
// 创建用户仓库
export const useUserStore = defineStore('user', () => {
// 存储 token
const token = ref(localStorage.getItem('token') || '')
// 存储 用户信息
const userInfo = ref<EmployeeLoginVO | null>(
(() => {
const stored = localStorage.getItem('loginUser')
return stored ? JSON.parse(stored) : null
})() // () 作用为立即执行函数,返还值作为ref的初始值;如果没有它,ref得到的就是一个函数体
)
// 用户登录 存储 token 和 用户信息
function setLogin(data: EmployeeLoginVO) {
token.value = data.token
userInfo.value = data
localStorage.setItem('token', data.token)
localStorage.setItem('loginUser', JSON.stringify(data))
}
// 用户登出 删除 token 和 用户信息
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
localStorage.removeItem('loginUser')
}
// 返回用户仓库
return { token, userInfo, setLogin, logout }
})
修改 login/index.vue 中的登录存储逻辑
将存储功能改为 调用 user.ts 中的方法
删除当前选中的代码
![]()
// 拿到仓库实例
const userStore = useUserStore()
![]()
// 用户仓库保存登录信息
userStore.setLogin(res.data)
![]()
7.7 路由守卫
为了防止用户未登录就访问到内部页面,需要设置路由守卫,没有登录就跳转到登录界面
-
to:即将要进入的目标路由对象(包含路径、参数、查询参数等完整信息),也就是 哪个路径发出的请求 -
from:当前导航正要离开的路由对象,也就是 这个请求希望跳转的路径 -
next:能够决定当前请求如何执行-
next():放行,允许进入目标路由。 -
next('/login'):中断当前导航,重定向到指定路径。 -
next(false):中断导航,停留在当前页面。
-
// 路由守卫:检查是否登录
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 通过用户仓库的 token 判断,如果没有 token,则跳转到登录页面
if (to.path !== '/login' && !userStore.token) {
next('/login')
} else {
next()
}
})
![]()
7.8 完善请求拦截器:自动添加 Token
请求拦截器:所有向后端发送的请求都会经过请求拦截器
除了登录操作,其他任何操作都需要将 token 交给后端进行判断。我们通过 request.ts 中的 请求拦截器 ,统一设置 token ,就可以减少很多操作
后端 .yml 文件中明确提到—— 前端传递过来的令牌名称 为 token
![]()
(该图为后端 application.yml 中的配置,具体情况请参考自己的后端相关配置)
因此,请求头 中也要叫 token ,即
config.headers.token
// 从仓库中获取 token
const userStore = useUserStore()
if (userStore.token) {
config.headers.token = userStore.token
}
![]()
7.9 完善响应拦截器:统一处理业务状态码
响应拦截器:所有后端返回的信息都会经过响应拦截器
后端返回的数据固定为:
-
code:状态码。1为成功,0为失败 -
data:具体的数据 -
msg:错误信息
可以看到,code 为 0 就没必要进行后续的页面代码了
因此,我们可以在响应拦截器提前处理
const res = response.data
if (res.code !== 1) {
ElMessage.error(res.msg || '请求失败')
return Promise.reject(new Error(res.msg || 'Error'))
}
![]()
login/index.vue 中可以去除关于 code 的判断
![]()
7.10 退出登录逻辑
点击 退出 按钮,需要退回到 登录页面 ,并且 将用户信息清除
![]()
// 拿到仓库实例
const userStore = useUserStore()
// 退出登录逻辑
const logout = () => {
// 调用用户仓库的 logout 方法退出登录,会清除用户信息
userStore.logout()
router.push('/login')
}
![]()
附录:常见问题(FAQ)
终端权限问题
快速解决
-
关闭 idea
-
在桌面 右键 ,选择 以管理员身份运行

Tailwind CSS 初始化错误
![]()
# 先卸载
npm uninstall tailwindcss
# 重新安装(明确指定版本)
npm install -D tailwindcss@3 postcss autoprefixer
# 然后用相对路径执行初始化
.\node_modules.bin\tailwindcss init -p
2026-05-05 更新:修正了代码块语法高亮