Vue3 实战:从 0 搭建企业级后台管理系统(Router+Pinia+Axios+Element Plus 全整合)

前言

后台管理系统是前端开发中最常见的业务场景之一,也是 Vue 生态工具整合应用的典型案例。很多新手在学习 Vue3 时,往往只会单独使用某个工具(比如只写路由、只做状态管理),但到了实际项目中,如何把 Vue Router、Pinia、Axios、Element Plus 这些核心工具串联起来,实现一个完整的后台系统,却常常无从下手。

本文将从实战落地角度,手把手教你从 0 搭建一个具备完整功能的后台管理系统核心模块:涵盖多页面路由配置与导航Pinia 用户状态持久化Axios 企业级请求封装Element Plus 登录页与首页开发。内容覆盖基础配置、核心逻辑、踩坑解决方案,既适合 Vue3 新手入门实战,也能为中后台项目开发提供可落地的参考方案。

一、项目初始化与技术栈选型

1.1 核心技术栈说明

本次实战项目采用 Vue3 生态最主流、最稳定的技术组合,适配企业级开发标准:

技术工具 核心作用 选择理由
Vue3 + Vite 基础框架 / 构建工具 Vue3 组合式 API 开发效率高,Vite 打包速度比 Webpack 快 3-5 倍
Vue Router 4 路由管理 Vue3 官方配套路由,支持组合式 API、路由守卫
Pinia 2 状态管理 替代 Vuex,更轻量、支持 TS、无需嵌套模块
Axios 网络请求 支持拦截器、取消请求、跨域处理,后台系统请求必备
Element Plus UI 组件库 企业级后台组件丰富,适配 Vue3,文档友好
TypeScript 类型校验 提升代码可维护性,减少生产环境 bug

1.2 环境准备

确保本地已安装以下环境:

  • Node.js ≥ 14.18.0(推荐 16+/18+)
  • npm/pnpm/yarn(推荐 pnpm,速度更快、体积更小)
  • VS Code(安装 Vetur/Vue Official 扩展)

1.3 项目创建与依赖安装

步骤 1:创建 Vue3+TS+Vite 项目
bash 复制代码
# 创建项目(命名为vue-admin-demo)
pnpm create vite vue-admin-demo --template vue-ts

# 进入项目目录
cd vue-admin-demo

# 安装基础依赖
pnpm install
步骤 2:安装核心依赖
bash 复制代码
# 路由
pnpm add vue-router@4

# 状态管理
pnpm add pinia pinia-plugin-persistedstate

# 网络请求
pnpm add axios

# UI组件库
pnpm add element-plus
# Element Plus按需引入插件
pnpm add unplugin-vue-components unplugin-auto-import -D

1.4 项目目录结构梳理

初始化后整理项目结构(符合后台系统开发规范):

bash 复制代码
vue-admin-demo/
├── src/
│   ├── api/          # Axios封装与接口请求
│   ├── assets/       # 静态资源(图片、样式)
│   ├── components/   # 通用组件
│   ├── layouts/      # 布局组件(后台首页布局)
│   ├── pinia/        # Pinia状态管理
│   ├── router/       # 路由配置
│   ├── styles/       # 全局样式
│   ├── utils/        # 工具函数
│   ├── views/        # 页面视图(登录、首页、列表、详情)
│   ├── App.vue       # 根组件
│   ├── main.ts       # 入口文件
│   └── vite-env.d.ts # TS类型声明
├── .env.development  # 开发环境变量
├── vite.config.ts    # Vite配置
└── package.json      # 依赖配置

二、Vue Router 多页面应用开发

2.1 路由核心逻辑梳理

后台系统的路由核心需求:

  • 区分无需登录可访问(登录页)和需登录访问(首页、列表页、详情页)
  • 支持页面间导航(导航菜单、编程式跳转)
  • 详情页支持参数传递(如商品 ID、用户 ID)
  • 全局路由守卫校验登录状态

用 Mermaid 可视化路由结构:

2.2 路由配置实现

步骤 1:创建路由配置文件(src/router/index.ts)
javascript 复制代码
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/pinia/user'; // 后续Pinia会实现

// 定义路由规则
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/home', // 默认跳转到首页
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login/index.vue'), // 懒加载
    meta: {
      requiresAuth: false, // 无需登录
      title: '登录页',
    },
  },
  {
    path: '/home',
    name: 'Home',
    component: () => import('@/views/Home/index.vue'),
    meta: {
      requiresAuth: true, // 需要登录
      title: '后台首页',
    },
  },
  {
    path: '/list',
    name: 'List',
    component: () => import('@/views/List/index.vue'),
    meta: {
      requiresAuth: true,
      title: '数据列表页',
    },
  },
  {
    path: '/detail/:id', // 动态参数id
    name: 'Detail',
    component: () => import('@/views/Detail/index.vue'),
    meta: {
      requiresAuth: true,
      title: '详情页',
    },
  },
  // 404页面
  {
    path: '/:pathMatch(.*)*',
    redirect: '/home',
  },
];

// 创建路由实例
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
});

// 全局路由守卫:校验登录状态
router.beforeEach((to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title as string || 'Vue后台管理系统';
  
  const userStore = useUserStore();
  // 判断当前路由是否需要登录
  if (to.meta.requiresAuth) {
    if (userStore.token) {
      // 已登录,正常访问
      next();
    } else {
      // 未登录,跳转到登录页
      next({ path: '/login' });
    }
  } else {
    // 无需登录,直接访问
    next();
  }
});

export default router;
步骤 2:在入口文件注册路由(src/main.ts)
javascript 复制代码
import { createApp } from 'vue';
import App from './App.vue';
// 引入路由
import router from './router';
// 引入Pinia(后续配置)
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
// 引入Element Plus(后续配置)
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

// 创建Pinia实例
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); // 持久化插件

const app = createApp(App);
// 注册插件
app.use(router);
app.use(pinia);
app.use(ElementPlus);

app.mount('#app');
步骤 3:修改根组件(src/App.vue)
html 复制代码
<template>
  <!-- 路由出口:所有页面都会渲染到这里 -->
  <router-view />
</template>

<script setup lang="ts">
// 根组件仅作为路由容器
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: "Microsoft YaHei", sans-serif;
}
</style>

2.3 路由导航与参数传递

1. 声明式导航(导航菜单 / 按钮)

在首页(Home)添加导航按钮,跳转到列表页 / 详情页:

html 复制代码
<template>
  <div class="home">
    <h2>后台首页</h2>
    <!-- 跳转到列表页 -->
    <el-button type="primary" @click="$router.push('/list')">
      前往数据列表页
    </el-button>
    <!-- 跳转到详情页(传递参数id=1) -->
    <el-button type="success" @click="$router.push('/detail/1')">
      查看ID=1的详情
    </el-button>
    <!-- 编程式导航的另一种写法 -->
    <el-button type="warning" @click="toDetail(2)">
      查看ID=2的详情
    </el-button>
  </div>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router';

const router = useRouter();
// 编程式导航传递参数
const toDetail = (id: number) => {
  router.push({
    path: `/detail/${id}`,
    // 也可以用query传递参数:
    // query: { id: id }
  });
};
</script>
2. 详情页接收参数(src/views/Detail/index.vue)
html 复制代码
<template>
  <div class="detail">
    <h2>详情页</h2>
    <p>当前详情ID:{{ id }}</p>
    <!-- 如果用query传递,取值:$route.query.id -->
    <el-button @click="$router.go(-1)">返回上一页</el-button>
  </div>
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router';

const route = useRoute();
// 获取params参数(注意:需添加TS类型断言)
const id = route.params.id as string;
</script>

三、Pinia 用户状态管理(登录状态 + 持久化)

3.1 Pinia 核心优势(对比 Vuex)

3.2 定义用户 Store(src/pinia/user.ts)

javascript 复制代码
import { defineStore } from 'pinia';

// 定义用户信息类型
interface UserInfo {
  id: number;
  username: string;
  avatar: string;
  roles: string[];
}

// 定义Store状态类型
interface UserState {
  token: string; // 登录令牌
  userInfo: UserInfo | null; // 用户信息
  isLogin: boolean; // 是否登录
}

// 定义Store
export const useUserStore = defineStore('user', {
  // 状态初始化
  state: (): UserState => ({
    token: '',
    userInfo: null,
    isLogin: false,
  }),
  // 计算属性(类似Vuex的getters)
  getters: {
    // 获取用户角色(简化写法)
    userRoles: (state) => state.userInfo?.roles || [],
  },
  // 方法(同步/异步,类似Vuex的mutations+actions)
  actions: {
    // 登录:存储token和用户信息
    login(token: string, userInfo: UserInfo) {
      this.token = token;
      this.userInfo = userInfo;
      this.isLogin = true;
    },
    // 退出登录:清空状态
    logout() {
      this.token = '';
      this.userInfo = null;
      this.isLogin = false;
      // 退出后跳转到登录页
      window.location.href = '/login';
    },
    // 更新用户信息
    updateUserInfo(userInfo: Partial<UserInfo>) {
      if (this.userInfo) {
        this.userInfo = { ...this.userInfo, ...userInfo };
      }
    },
  },
  // 持久化配置(关键!)
  persist: {
    enabled: true, // 开启持久化
    strategies: [
      {
        key: 'vue_admin_user', // 本地存储的key
        storage: localStorage, // 存储方式:localStorage/sessionStorage
        // 指定需要持久化的字段(按需选择)
        paths: ['token', 'userInfo', 'isLogin'],
      },
    ],
  },
});

3.3 Pinia 实战使用(登录 / 退出)

1. 登录时存储状态(后续登录页会调用)
javascript 复制代码
import { useUserStore } from '@/pinia/user';

const userStore = useUserStore();
// 模拟登录接口返回的token和用户信息
const mockToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const mockUserInfo = {
  id: 1,
  username: 'admin',
  avatar: 'https://avatars.githubusercontent.com/u/123456',
  roles: ['admin'],
};
// 调用Store的login方法
userStore.login(mockToken, mockUserInfo);
2. 退出登录(首页导航栏使用)
html 复制代码
<template>
  <el-button type="danger" @click="handleLogout">退出登录</el-button>
</template>

<script setup lang="ts">
import { useUserStore } from '@/pinia/user';

const userStore = useUserStore();
// 退出登录
const handleLogout = () => {
  userStore.logout();
};
</script>

四、Axios 封装(后台系统专属版)

4.1 封装核心思路

后台系统的 Axios 封装需满足:

  1. 统一基础 URL(区分开发 / 生产环境)
  2. 请求拦截器:自动添加 token、设置请求头
  3. 响应拦截器:统一错误处理、token 过期刷新
  4. 加载状态提示(Element Plus Loading)
  5. 封装通用请求方法(get/post/put/delete)

4.2 完整封装实现(src/api/request.ts)

javascript 复制代码
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus';
import { useUserStore } from '@/pinia/user';

// 定义接口返回数据类型(适配后台接口规范)
interface ResponseData<T = any> {
  code: number; // 业务状态码(200成功,401token过期,500服务器错误)
  msg: string; // 提示信息
  data: T; // 业务数据
}

// 创建Axios实例
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量中的接口地址
  timeout: 10000, // 超时时间
  headers: {
    'Content-Type': 'application/json;charset=utf-8',
  },
});

// 加载状态实例
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null;

// 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 开启加载提示
    loadingInstance = ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.5)',
    });

    // 添加token到请求头
    const userStore = useUserStore();
    if (userStore.token && config.headers) {
      config.headers.Authorization = `Bearer ${userStore.token}`;
    }

    return config;
  },
  (error) => {
    // 关闭加载提示
    if (loadingInstance) loadingInstance.close();
    ElMessage.error('请求发送失败:' + error.message);
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse<ResponseData>) => {
    // 关闭加载提示
    if (loadingInstance) loadingInstance.close();

    const { code, msg, data } = response.data;
    // 业务状态码处理
    if (code === 200) {
      return data; // 成功:直接返回业务数据
    } else {
      // 业务错误提示
      ElMessage.error(msg || '请求失败');
      return Promise.reject(new Error(msg || '请求失败'));
    }
  },
  (error) => {
    // 关闭加载提示
    if (loadingInstance) loadingInstance.close();

    // 网络错误/HTTP状态码处理
    let errorMsg = '请求失败';
    if (error.response) {
      // HTTP状态码处理
      switch (error.response.status) {
        case 401:
          // Token过期:提示并退出登录
          ElMessageBox.confirm(
            '登录状态已过期,请重新登录',
            '提示',
            {
              confirmButtonText: '重新登录',
              cancelButtonText: '取消',
              type: 'warning',
            }
          ).then(() => {
            const userStore = useUserStore();
            userStore.logout();
          });
          errorMsg = '登录状态已过期';
          break;
        case 403:
          errorMsg = '暂无权限访问该资源';
          break;
        case 404:
          errorMsg = '请求地址不存在';
          break;
        case 500:
          errorMsg = '服务器内部错误,请稍后重试';
          break;
        default:
          errorMsg = error.response.data?.msg || '请求失败';
      }
    } else if (error.message.includes('timeout')) {
      errorMsg = '请求超时,请检查网络';
    } else if (error.message.includes('Network Error')) {
      errorMsg = '网络异常,请检查网络连接';
    }

    ElMessage.error(errorMsg);
    return Promise.reject(new Error(errorMsg));
  }
);

// 封装通用请求方法
export const request = {
  // GET请求
  get<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
    return service.get(url, { params, ...config });
  },
  // POST请求
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return service.post(url, data, config);
  },
  // PUT请求
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return service.put(url, data, config);
  },
  // DELETE请求
  delete<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
    return service.delete(url, { params, ...config });
  },
};

export default service;

4.3 配置环境变量(.env.development)

bash 复制代码
# 开发环境接口地址(根据实际后端地址修改)
VITE_API_BASE_URL = 'http://localhost:3000/api'
# 项目基础路径
VITE_BASE_URL = '/'

4.4 封装业务接口(src/api/user.ts)

javascript 复制代码
import { request } from './request';

// 定义登录接口参数类型
export interface LoginParams {
  username: string;
  password: string;
}

// 定义用户信息返回类型
export interface LoginResponse {
  token: string;
  userInfo: {
    id: number;
    username: string;
    avatar: string;
    roles: string[];
  };
}

// 登录接口
export const loginApi = (params: LoginParams) => {
  return request.post<LoginResponse>('/user/login', params);
};

// 获取用户信息接口
export const getUserInfoApi = () => {
  return request.get<LoginResponse['userInfo']>('/user/info');
};

五、Element Plus 开发后台核心页面

5.1 Element Plus 按需引入配置(vite.config.ts)

为减少打包体积,配置按需引入:

javascript 复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// Element Plus按需引入插件
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    // 自动导入Element Plus API
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    // 自动导入Element Plus组件
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    // 配置@别名
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  // 开发服务器配置(跨域代理)
  server: {
    port: 3001, // 前端端口
    proxy: {
      // 代理接口请求,解决跨域
      '/api': {
        target: 'http://localhost:3000', // 后端接口地址
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});

5.2 登录页开发(src/views/Login/index.vue)

包含表单验证、登录逻辑、整合 Pinia 和 Axios:

javascript 复制代码
<template>
  <div class="login-container">
    <div class="login-card">
      <h2 class="login-title">后台管理系统登录</h2>
      <el-form
        ref="loginFormRef"
        :model="loginForm"
        :rules="loginRules"
        label-width="80px"
        class="login-form"
      >
        <el-form-item label="用户名" prop="username">
          <el-input
            v-model="loginForm.username"
            placeholder="请输入用户名"
            prefix-icon="el-icon-user"
          />
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            placeholder="请输入密码"
            prefix-icon="el-icon-lock"
          />
        </el-form-item>
        <el-form-item label="验证码" prop="code">
          <el-input
            v-model="loginForm.code"
            placeholder="请输入验证码"
            style="width: 60%; display: inline-block"
          />
          <div class="code-img">
            <!-- 验证码图片(模拟) -->
            <img src="https://picsum.photos/100/40" alt="验证码" @click="refreshCode" />
          </div>
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            class="login-btn"
            @click="handleLogin"
            :loading="loading"
          >
            登录
          </el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import type { FormInstance, FormRules } from 'element-plus';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/pinia/user';
import { loginApi, type LoginParams } from '@/api/user';

// 表单Ref
const loginFormRef = ref<FormInstance>();
// 加载状态
const loading = ref(false);
// 路由实例
const router = useRouter();
// 用户Store
const userStore = useUserStore();

// 登录表单数据
const loginForm = ref<LoginParams & { code: string }>({
  username: '',
  password: '',
  code: '',
});

// 表单验证规则
const loginRules = ref<FormRules>({
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '用户名长度为3-20位', trigger: 'blur' },
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' },
  ],
  code: [
    { required: true, message: '请输入验证码', trigger: 'blur' },
    { len: 4, message: '验证码长度为4位', trigger: 'blur' },
  ],
});

// 刷新验证码
const refreshCode = () => {
  // 模拟刷新验证码(修改图片URL)
  const codeImg = document.querySelector('.code-img img') as HTMLImageElement;
  codeImg.src = `https://picsum.photos/100/40?${Date.now()}`;
};

// 登录逻辑
const handleLogin = async () => {
  if (!loginFormRef.value) return;

  try {
    // 表单验证
    await loginFormRef.value.validate();
    loading.value = true;

    // 调用登录接口
    const res = await loginApi({
      username: loginForm.value.username,
      password: loginForm.value.password,
    });

    // 存储登录状态到Pinia
    userStore.login(res.token, res.userInfo);

    // 登录成功提示
    ElMessage.success('登录成功!');

    // 跳转到首页
    router.push('/home');
  } catch (error) {
    console.error('登录失败:', error);
    ElMessage.error('登录失败,请检查账号密码');
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped lang="scss">
.login-container {
  width: 100vw;
  height: 100vh;
  background: linear-gradient(120deg, #409eff, #67c23a);
  display: flex;
  justify-content: center;
  align-items: center;
}

.login-card {
  width: 400px;
  padding: 30px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}

.login-title {
  text-align: center;
  color: #409eff;
  margin-bottom: 20px;
  font-size: 20px;
}

.login-form {
  margin-top: 20px;
}

.code-img {
  display: inline-block;
  margin-left: 10px;
  img {
    cursor: pointer;
    border-radius: 4px;
  }
}

.login-btn {
  width: 100%;
  height: 40px;
  font-size: 16px;
}
</style>

5.3 后台首页开发(布局 + 导航菜单)

步骤 1:创建布局组件(src/layouts/MainLayout.vue)
html 复制代码
<template>
  <el-container style="height: 100vh;">
    <!-- 侧边栏 -->
    <el-aside width="200px" style="background-color: #2e3b4e;">
      <div class="logo">
        <img src="https://picsum.photos/40/40" alt="logo" />
        <span>后台管理系统</span>
      </div>
      <!-- 导航菜单 -->
      <el-menu
        default-active="1"
        class="el-menu-vertical-demo"
        background-color="#2e3b4e"
        text-color="#fff"
        active-text-color="#ffd04b"
        @select="handleMenuSelect"
      >
        <el-menu-item index="1">
          <el-icon><House /></el-icon>
          <template #title>首页</template>
        </el-menu-item>
        <el-menu-item index="2">
          <el-icon><List /></el-icon>
          <template #title>数据列表</template>
        </el-menu-item>
        <el-sub-menu index="3">
          <template #title>
            <el-icon><Setting /></el-icon>
            <span>系统设置</span>
          </template>
          <el-menu-item index="3-1">用户管理</el-menu-item>
          <el-menu-item index="3-2">角色管理</el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>

    <!-- 主内容区 -->
    <el-container>
      <!-- 顶部导航 -->
      <el-header style="text-align: right; font-size: 12px">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>查看</el-dropdown-item>
              <el-dropdown-item>新增</el-dropdown-item>
              <el-dropdown-item>删除</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
        <span>欢迎您,{{ userStore.userInfo?.username }}</span>
        <el-button type="text" @click="handleLogout" style="margin-left: 10px">退出</el-button>
      </el-header>

      <!-- 内容区域(路由出口) -->
      <el-main>
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useUserStore } from '@/pinia/user';
import { ElMessage } from 'element-plus';
// 引入Element Plus图标
import { House, List, Setting } from '@element-plus/icons-vue';

const router = useRouter();
const userStore = useUserStore();

// 菜单选择事件
const handleMenuSelect = (index: string) => {
  switch (index) {
    case '1':
      router.push('/home');
      break;
    case '2':
      router.push('/list');
      break;
    case '3-1':
      ElMessage.info('用户管理功能开发中...');
      break;
    case '3-2':
      ElMessage.info('角色管理功能开发中...');
      break;
  }
};

// 退出登录
const handleLogout = () => {
  userStore.logout();
};
</script>

<style scoped lang="scss">
.el-header {
  background-color: #fff;
  color: #333;
  line-height: 60px;
  border-bottom: 1px solid #e6e6e6;
}

.el-aside {
  color: #333;

  .logo {
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 16px;
    border-bottom: 1px solid #404854;
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      margin-right: 10px;
    }
  }
}

.el-main {
  background-color: #f5f7fa;
  color: #333;
  padding: 20px;
}

.el-menu-vertical-demo {
  border-right: none;
  height: calc(100vh - 60px);
}
</style>
步骤 2:修改首页组件(src/views/Home/index.vue)
html 复制代码
<template>
  <div class="home-content">
    <el-card title="后台概览" class="home-card">
      <el-row :gutter="20">
        <el-col :span="6">
          <div class="stat-card">
            <p class="stat-title">今日访问量</p>
            <p class="stat-value">1,234</p>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-card">
            <p class="stat-title">今日订单数</p>
            <p class="stat-value">567</p>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-card">
            <p class="stat-title">今日销售额</p>
            <p class="stat-value">¥89,012</p>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-card">
            <p class="stat-title">用户总数</p>
            <p class="stat-value">10,890</p>
          </div>
        </el-col>
      </el-row>
    </el-card>

    <!-- 快速操作 -->
    <el-card title="快速操作" class="home-card" style="margin-top: 20px;">
      <el-button type="primary" icon="el-icon-plus">新增数据</el-button>
      <el-button type="success" icon="el-icon-download">导出数据</el-button>
      <el-button type="warning" icon="el-icon-upload">导入数据</el-button>
      <el-button type="info" icon="el-icon-refresh">刷新数据</el-button>
    </el-card>
  </div>
</template>

<script setup lang="ts">
// 首页业务逻辑(可后续扩展)
</script>

<style scoped lang="scss">
.home-content {
  width: 100%;
}

.home-card {
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.stat-card {
  background: linear-gradient(120deg, #e53935, #e35d5b);
  padding: 20px;
  border-radius: 8px;
  color: #fff;
  text-align: center;
  .stat-title {
    font-size: 14px;
    margin-bottom: 10px;
    opacity: 0.8;
  }
  .stat-value {
    font-size: 24px;
    font-weight: bold;
  }
}

// 不同卡片不同渐变
.el-col:nth-child(2) .stat-card {
  background: linear-gradient(120deg, #43a047, #4caf50);
}
.el-col:nth-child(3) .stat-card {
  background: linear-gradient(120deg, #1e88e5, #2196f3);
}
.el-col:nth-child(4) .stat-card {
  background: linear-gradient(120deg, #fdd835, #ffc107);
}
</style>
步骤 3:修改路由配置,让首页使用布局组件

修改src/router/index.ts中的首页路由:

javascript 复制代码
{
  path: '/home',
  name: 'Home',
  // 布局组件作为父组件,首页内容作为子路由
  component: () => import('@/layouts/MainLayout.vue'),
  children: [
    {
      path: '', // 子路由默认路径
      component: () => import('@/views/Home/index.vue'),
    },
  ],
  meta: {
    requiresAuth: true,
    title: '后台首页',
  },
},
{
  path: '/list',
  name: 'List',
  component: () => import('@/layouts/MainLayout.vue'),
  children: [
    {
      path: '',
      component: () => import('@/views/List/index.vue'),
    },
  ],
  meta: {
    requiresAuth: true,
    title: '数据列表页',
  },
},

六、实战踩坑与解决方案

6.1 常见问题及解决

问题场景 原因分析 解决方案
路由跳转白屏 路由懒加载路径错误,或布局组件无路由出口 1. 检查 import 路径是否正确;2. 确保布局组件中有<router-view />
Pinia 持久化失效 未安装 pinia-plugin-persistedstate,或配置错误 1. 确认安装插件并注册;2. 检查 persist 配置中的 paths 是否包含需要持久化的字段
Axios 请求跨域 开发环境未配置代理,或生产环境后端未配置 CORS 1. Vite 配置 proxy 代理(见 vite.config.ts);2. 后端配置 Access-Control-Allow-Origin
Element Plus 图标不显示 未按需引入图标,或版本不兼容 1. 手动引入需要的图标(如 import {House} from '@element-plus/icons-vue');2. 确保 Element Plus 版本≥2.3.0
登录后刷新页面状态丢失 Pinia 未开启持久化,或 token 未存储到 localStorage 启用 pinia-plugin-persistedstate,配置 persist 存储到 localStorage

6.2 性能优化建议

  1. 路由懒加载:所有页面均使用() => import('@/views/xxx/index.vue')懒加载,减少首屏加载体积;
  2. Element Plus 按需引入:通过 unplugin-vue-components 插件自动按需引入组件,避免全局引入体积过大;
  3. Axios 取消重复请求:添加取消请求逻辑,避免同一接口多次请求导致数据错乱;
  4. 图片懒加载:首页统计卡片的图片使用v-lazy指令懒加载(需安装 vue3-lazy)。

七、总结与进阶建议

7.1 核心总结

  1. 路由管理:通过 Vue Router 实现多页面导航,结合路由守卫控制登录权限,参数传递支持 params/query 两种方式;
  2. 状态管理:Pinia 替代 Vuex,轻量且支持持久化,完美适配后台系统的用户状态管理;
  3. 请求封装:Axios 拦截器统一处理 token、错误、加载状态,封装通用请求方法提升开发效率;
  4. UI 开发:Element Plus 提供丰富的后台组件,结合布局组件快速搭建后台系统页面结构。

7.2 进阶建议

  1. 权限控制:基于 Pinia 的用户角色,实现路由 / 按钮级别的权限控制;
  2. 国际化:集成 vue-i18n 实现多语言切换,适配海外后台系统;
  3. 主题定制:基于 Element Plus 的主题变量,实现动态主题切换;
  4. 错误监控:集成 Sentry 捕获前端错误,提升系统稳定性;
  5. 打包优化:配置 Vite 的 build 选项,压缩代码、拆分 chunk,减少生产环境包体积。

最后

本文从 0 到 1 整合了 Vue3 生态的核心工具,实现了一个具备完整登录流程、多页面导航、状态管理、请求封装的后台管理系统核心模块。内容覆盖基础配置、核心逻辑、踩坑解决方案,既适合 Vue3 新手入门实战,也能为企业级后台项目开发提供参考。

如果对你有帮助,欢迎点赞 + 收藏 + 关注,后续会持续更新 Vue3 后台系统进阶内容(如权限控制、国际化、打包优化)。

如果有任何问题或不同见解,欢迎在评论区交流哦~

相关推荐
不能只会打代码2 小时前
基于Vue 3 + Spring Boot的物联网生鲜品储运系统设计与实现(源码附有详细的文档讲解)
java·前端·vue.js·spring boot·后端·物联网·github
A923A2 小时前
【Vue3大事件 | 项目笔记】第三天
前端·vue.js·笔记·vue·前端项目
Smoothcloud润云2 小时前
告别 Selenium:Playwright 现代 Web 自动化测试从入门到实战
前端·人工智能·selenium·测试工具·架构·自动化
前端小D2 小时前
ES6 中的 Promise
前端·javascript·es6·promise
光影少年2 小时前
React和Vue的区别?
前端·vue.js·react.js
遗憾随她而去.2 小时前
前端跨页面通信:8 种方案全解析(附实战案例)
前端
何中应2 小时前
<el-tag>标签使用
前端·vue.js·elementui
清汤饺子2 小时前
Cursor 独有的 12 个技巧:这些是 Claude Code 没有的
前端·后端·ai编程
白菜__2 小时前
阿里V2滑块小程序版本
javascript·爬虫·网络协议·小程序·node.js