普通视图

发现新文章,点击刷新页面。
昨天以前首页

从0开始搭建Vue3前端骨架(附登录接口案例)

作者 MoXC
2026年5月5日 11:53

从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 或点击下方链接 进入中文官网

Node.js 中文网

1777801221714

点击 下载安装

1777801333393

选择匹配的操作系统,点击下载

1777808298374

下载完成

1777808955660

1.1.2 下载其他版本的 Node.js (可以跳过该步骤,有版本需求可选择观看)

如果需要用到其他版本的 Node.js ,可以按下面的方式操作

点击 全部安装包

1777808566469

路径末尾处就是版本号,需要什么版本就在末尾输入对应的版本号,下面以 v22.10.0 为例

1777808643346

输入 v20.10.0 后,即可得到下面的页面

1777809106652

  • Windows 用户选择 .msi 安装包
  • macOS 用户选择 .pkg
  • Linux 用户使用包管理器或二进制文件。

windows 64位选择如下

1777809580369

下面提供另一种查找方法

去除路径末尾的版本号

1777808779699

回车

1777808805306

得到下面的界面

1777808842287

向下拖动(会很长),就能找到我们需要的版本了,点击该版本

1777808910668

进入下面的页面

1777809029725

  • Windows 用户选择 .msi 安装包
  • macOS 用户选择 .pkg
  • Linux 用户使用包管理器或二进制文件。

windows 64位选择如下

1777809580369

1.1.3 安装

直接双击即可,接下来 一路 next 即可

1777809720677

1777809750525

1777809771920

1777809819072

1777809890613

1777809903446

1777809915025

1777810006701

1.1.4 验证

win + r 进入运行界面,输入 cmd ,回车

1777810664459

输入 node -v 查看 node 的版本

node -v

1777810727613

输入 npm -v 查看 npm 的版本

npm -v

1777810784587

能看到版本就说明安装成功了 ^ v ^

1.1.5 配置全局路径 (可以跳过该步骤,推荐完成)

作用:存放 全局安装的模块(通过 npm install -g 安装的包)

Node 安装目录下新建 node_global 文件夹

node_global

1777811415447

进入 node_global 文件夹,并复制其路径

1777811541827

在此处输入如下指令,设置 npm 全局安装的模块 的存放位置

npm config set prefix

1777811771037

路径为我们之前复制的 node_global 文件夹的路径,将路径复制在后面,回车

1777811901195

1.1.6 配置缓存路径(可以跳过该步骤,推荐完成)

作用:存放 npm 下载包时的缓存文件 。当执行 npm install 时,npm 会先将下载的包缓存到这里,下次再安装相同版本时直接从缓存读取,提高安装速度

Node 安装目录下新建 node_cache 文件夹

1777812137791

进入 node_cache 文件夹,并复制其路径

1777812215494

在此处输入如下指令, 设置 npm 下载包时的临时缓存目录

npm config set cache

1777812422335

路径为我们之前复制的 node_cache 文件夹的路径,将路径复制在后面,回车

1777812511615

1.1.7 配置国内的镜像

(2026.5.3 该镜像还能使用)

npm config set registry https://registry.npmmirror.com

1777812673937

1.2 包管理器选择(npm / pnpm)

包管理器常用的有以下几种:

  • npm(Node Package Manager):Node.js 自带,无需安装,命令简洁,生态最广。
  • pnpm:快速的、省空间的替代品,通过硬链接共享依赖,适合大型项目或多项目开发。
  • yarn:历史选择,目前已较少用于新项目。
1.2.1 本教程的选择

我们使用 npm 作为包管理器,因为:

  1. 安装 Node.js 后即可使用,无需额外步骤。
  2. 绝大多数 Vue 3 官方文档和示例都采用 npm。
  3. 对于本项目的规模,npm 的性能完全够用。

如果你希望尝试 pnpm,可以后续自行替换命令(例如 pnpm install 代替 npm install),不影响项目运行。

二、创建 Vue3 项目(含 TypeScript)

2.1 创建项目

2.1.1 创建文件夹

新建前端文件夹

frontend

1777814012394

2.1.2 idea 打开该文件夹

选择刚才创建的文件夹打开

1777814386663

界面如下

1777814503969

2.1.3 创建 Vue3 项目

打开终端

npm create vue@latest

这是 终端权限问题 ,解决方法请看 附录:常见问题(FAQ) -> 终端问题解决

1777815523989

第一次创建会有提示,输入 y 回车

1777818139368

输入项目名称,这个可以自己定义,回车

book-trading-frontend

1777818335395

这里选择 Yes

1777818395321

↑/↓ 箭头切换,空格 选中

1777818521674

这两个都不用选,直接回车

1777818563448

移动上下箭头,选择 Yes

1777818615090

效果,这个文件夹就是我们创建的 vue3项目(项目名称可能不同)

1777818658346

2.1.4 安装所有依赖

路径切换到我们创建的 vue3 项目

1777820552931

为 vue3 项目安装所有依赖(过程可能会有点久,我这里安装了5min)

npm i

1777821529413

2.1.5 启动项目(ctrl + c 停止项目)
npm run dev

1777821717401

点击链接,出现如下界面说明成功了 ^ v ^

1777821769327

2.2 项目结构解读

1777822516579

这里可以简单了解一下

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 目录

1777860273858

api 目录下创建 servicestypes 目录

  • services:存放API请求函数
  • types:存放TypeScript类型定义

1777861082772

3.2 assets

作用:存放需要经过构建工具处理的静态资源 ,例如图片、字体、全局样式文件等

src 目录下创建 assets 目录

1777864306324

3.3 layouts

作用:存放布局组件,例如整个后台管理系统的公共框架(侧边栏菜单、顶部导航栏、底部版权信息、内容区域等)

src 目录下创建 layouts 文件夹

1777862140320

3.4 stores

作用:存放 Pinia 定义的 store 文件 (类似于java中的全局变量、全局方法等)

counter.ts 是演示示例,可以保留也可以删除,这里选择删除示例

1777861217847

3.5 views

作用:存放具体的页面

src 目录下创建 views 目录

1777862253451

四、集成 Element Plus 与 Tailwind CSS

4.1 Element Plus

4.1.1 安装

安装指令:

npm i element-plus --save

1777862597481

安装成功(7min):

1777863055087

4.1.2 在 main.ts 中引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

1777863333394

接下来我们导入后,还要交给 app 使用(注意在挂载 mount 之前)

app.use(ElementPlus)

1777863488110

4.1.3 验证

App.vue 中创建一个按钮组件(因为是 element-plus 的组件,所以一般都是 el-xxx

能看见提示说明成功了

1777863639979

我们可以随便写点

<el-button>Button</el-button>

1777863700040

启动项目

npm run dev

1777863795093

点开地址可以看到如下效果

1777863834630

4.1.4 Element Plus 官网

A Vue 3 UI Framework | Element Plus

官网上介绍了如何安装使用,这里可以简单看一下,和我们的安装步骤是一样的

右上角可以切换中文

1767149910079

点击指南

1767150016853

左侧找到快速开始

1767150047195

1767150116755

npm的安装指令也有

npm install element-plus --save

1767150189598

4.2 Tailwind CSS

4.2.1 安装

安装指令:

npm install -D tailwindcss

1777864404732

安装成功(3s):

1777864441133

初始化指令:

(此处出错的可以前往 附录:常见问题(FAQ) -> Tailwind CSS初始化错误

npx tailwindcss init -p

打开我们初始化得到的 tailwind.config.js

1777865184605

content 添加如下内容

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",         // 扫描根目录的 HTML 文件
    "./src/**/*.{vue,js,ts,jsx,tsx}", // 扫描 src 下所有 Vue/JS/TS 等文件
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

1777865269090

4.2.2 在 main.css 中引入

assets 目录下创建 main.css 用来存放全局样式

1777865396032

tailwind 引入

main.css

@tailwind base;
@tailwind components;
@tailwind utilities;

1777865463197

4.2.3 在 main.ts 中引入
import './assets/main.css'

1777865598632

4.2.4 验证

App.vue 中使用,添加 class 样式,可以看到有如下 tailwind css 提示

1777865814254

启动项目

npm run dev

可以看到如下效果:

1777865925463

4.2.5 Tailwind CSS 官网

安装 - TailwindCSS中文文档 | TailwindCSS中文网

这里有安装步骤,略有不同

1767156327306

往下滑可以查看这些样式,ctrl + k 可以搜索样式进行查看

1767156445008

例如:对 背景色 并不熟悉,可以通过下面的方法查看

  • ctrl + k 打开搜索

    1767156445008

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

    1767156445008

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

    1767156445008

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

    1767156445008

五、封装 Axios 请求模块(核心)

5.1 安装

安装指令:

npm i axios

1777866273711

安装成功(18s):

1777866293061

5.2 创建 request.ts 并配置拦截器

request.ts

1777866427488

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

只要 协议、域名、端口 有任何一项不同,就属于“跨域”,我们的前端和后端的端口不同,前端去请求后端就会产生跨域问题

Home | Vite中文网

我们需要将这一段配置写到 vite.config.ts

1767688199140

也就是这一段,target 我改成我们以后的后端端口,这样就能和后端连接了

server: {
    proxy: {
      // 统一代理所有以 /api 开头的请求
      '/api': {
        target: 'http://localhost:8080',  // 1. 目标:转发到后端服务器
        changeOrigin: true, // 2. 关键:改变请求头中的Origin为目标地址,防止某些后端校验失败
         // 3. 重写路径:用''空字符串代替`/api`,/是转移,^是以什么开头
        rewrite: (path) => path.replace(/^/api/, ''),
      }
    }
  }

1777867094288

整体代码如下

1777867125703

六、登录与布局页面设计

6.1 login.vue 登录页面

6.1.1 创建页面

views 目录下如下配置

1777872708153

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

1777873143266

修改 App.vue 让它能够通过 路由 来展示 login/index.vue 页面

  • <router-view></router-view> :展示路由中的页面
<script setup lang="ts"></script>

<template>
  <router-view></router-view>
</template>

<style scoped></style>

1777874634946

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,可以看到如下效果

1777875535264

6.2 Layout.vue 布局组件

6.2.1 创建页面

1777872335306

6.2.2 路由注册

路由注册,将 Layout.vue 页面交给 index.ts 管理

{ path: '/layout', component: () => import('@/layouts/Layout.vue')},

1777875381871

6.2.3 页面实现

前往 Element Plus 官网

Container 布局容器 | Element Plus

我们使用这种布局

1777876089909

<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>

1777881550656

修改 login/index.vue 中的 login 函数,使其跳转至 Layout.vue 页面

// 获得路由器实例
const router = useRouter()

// 登录
const login = () => {
  router.push('/layout')
}

1777875787061

启动项目 npm run dev 效果如下:

1777881581806

稍加改造

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>

启动项目,效果如下:

1777881640491

6.3 Layout.vue 的子页面

1777881734207

6.3.1 创建页面

views 目录下创建两个 vue 页面

1777881930945

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

1777882318174

6.4 实现菜单

我们要实现的效果是,点击 侧边栏菜单项 ,右边展示对应菜单项的页面

1777882580801

Element Plus 官网上找到我们需要的代码

Menu 菜单 | Element Plus

1777882758502

1777882813560

我将源码简化处理了,保留了两个菜单项

  • 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> ,也就是侧边区域

1777883068487

el-main 区域设置 <router-view></router-view> ,相当于将 “展台” 设置在 el-main 区域,页面就可以在站台上展示了。App.vue 中也设置了这个

<router-view></router-view>

1777883244852

效果如下:

1777883293429

6.5 404 页面

6.5.1 创建页面

1777884173882

6.5.2 路由注册
{ path: '/:pathMatch(.*)', component: () => import('@/views/NotFound/index.vue') },

1777884246497

6.5.3 页面实现

Result 结果 | Element Plus

1777884402867

我将官网的代码修改一下

<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>

将代码复制到此处

1777884779293

输入一个错误的路径,可以看到自定义的404页面

http://localhost:5173/layout/workbenchgew

1777884839089

代码解释

1777884727727

到这里,前端的骨架差不多就搭建完成了,如果想知道具体的用法,可以看看后续的代码 ^ 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

1777873689190

types 目录下创建 common.types.ts

code 必须,其他两个非必须

// API 通用响应类型
export interface ApiResponse<T> {
  code: number
  data?: T
  msg?: string
}
7.3.2 DTO 与 VO 数据格式

对标后端的 EmployeeLoginDTOEmployeeLoginVO

1777874139168

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>

页面效果如下:

1777886684174

7.4.2 表单校验实现

Form 表单 | Element Plus 1777886789732

获取表单实例

// 获取表单实例
const ruleFormRef = ref<FormInstance>()

1777888267538

实例获取对象为 表单中的数据

ref="ruleFormRef"

1777887886565

这一步的作用是拿到表单中数据,且 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'}
  ]
})

1777888401471

获取表单中的数据,也就是获取 usernamepassword 两个输入框中的内容

数据类型为 EmployeeLoginDTO ,也就是我们要提交给后端的数据

1777888902884

fromrules 绑定给表单

:model="form"
:rules="rules"

1777895007927

from 中的元素需要通过 prop 绑定给表单项

1777895091357

效果如下:

1777895110191

7.5 登录逻辑

7.5.1 登录接口 api 实现

api/services 目录下创建 employee.ts ,用来存放员工相关的 api

1777895237748

employee.ts

// 员工登录
export const loginApi = () => {

}

接下来通过 接口文档 分析 参数返回值

  • 参数EmployeeLoginDTO 类型的参数,即

    • username
    • password
  • 返回值data 中存储的是 EmployeeLoginVO 类型的返回值,即

    • code

    • data

      • id
      • username
      • name
      • token
    • 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('请按照表单规则填写')
    }
  })
}

1777896629925

登录效果如下(我有后端程序,因此能够成功得到反馈)

1777896542098

7.6 创建 Pinia Store 管理用户状态(stores/user.ts

因为用户登录后的信息后期会经常用到,像 tokenusername 这些,因此,可以将这些方法存储在仓库,要用到的时候就可以随时调用

创建 user.ts

1777897082225

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 中的方法

删除当前选中的代码

1777897950513

// 拿到仓库实例
const userStore = useUserStore()

1777898085056

// 用户仓库保存登录信息
userStore.setLogin(res.data)

1777898189506

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()
  }
})

1777898806379

7.8 完善请求拦截器:自动添加 Token

请求拦截器:所有向后端发送的请求都会经过请求拦截器

除了登录操作,其他任何操作都需要将 token 交给后端进行判断。我们通过 request.ts 中的 请求拦截器 ,统一设置 token ,就可以减少很多操作

后端 .yml 文件中明确提到—— 前端传递过来的令牌名称token

1777899306032

(该图为后端 application.yml 中的配置,具体情况请参考自己的后端相关配置)

因此,请求头 中也要叫 token ,即

config.headers.token

// 从仓库中获取 token
const userStore = useUserStore()
if (userStore.token) {
    config.headers.token = userStore.token
}

1777899431859

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'))
    }

1777899899165

login/index.vue 中可以去除关于 code 的判断

1777900029905

7.10 退出登录逻辑

点击 退出 按钮,需要退回到 登录页面 ,并且 将用户信息清除

1777900338844

// 拿到仓库实例
const userStore = useUserStore()

// 退出登录逻辑
const logout = () => {
  // 调用用户仓库的 logout 方法退出登录,会清除用户信息
  userStore.logout()
  router.push('/login')
}

1777900404020

附录:常见问题(FAQ)

终端权限问题

快速解决
  1. 关闭 idea

  2. 在桌面 右键 ,选择 以管理员身份运行

    1777815095720

Tailwind CSS 初始化错误

1777865086112

# 先卸载
npm uninstall tailwindcss
# 重新安装(明确指定版本)
npm install -D tailwindcss@3 postcss autoprefixer
# 然后用相对路径执行初始化
.\node_modules.bin\tailwindcss init -p

2026-05-05 更新:修正了代码块语法高亮

❌
❌