vue3搭建一个后台管理系统

我爱海鲸 2026-02-03 15:24:45 暂无标签

简介Vite 5 、Vue 3 、TypeScript 、Element Plus 、Vue Router 4 、Pinia 、Axios

Element Plus(最主流、上手最简单)

  • 官网:https://element-plus.org/
  • 依赖:element-plus@^2.5.0+@element-plus/icons-vue(配套图标库,后台系统必备)

1、创建项目

npm create vite@latest

核心业务依赖(生产环境,pnpm add 直接安装 latest)

pnpm add vue@latest vue-router@latest pinia@latest axios@latest element-plus@latest

 辅助业务依赖(生产环境,配套工具也用 latest)

pnpm add @element-plus/icons-vue@latest pinia-plugin-persistedstate@latest

pnpm add -D sass-embedded

开发环境依赖(仅开发 / 构建使用,同样指定 latest)

pnpm add typescript@latest vue-tsc@latest @vitejs/plugin-vue@latest @types/node@latest -D

清除包:

pnpm store prune

安装pnpm:

npm install -g pnpm

安装rimraf

pnpm add rimraf@latest -D

2、运行相关命令:

"scripts": {
  "dev": "vite --host 0.0.0.0",
  "build": "vue-tsc && vite build",
  "preview": "vite preview",
  "preview:port": "vite preview --port 8080",
  "type-check": "vue-tsc --noEmit",
  "clean:dist": "rimraf dist 2>nul",
  "clean:packages": "rimraf node_modules 2>nul && rimraf pnpm-lock.yaml 2>nul",
  "clean:all": "rimraf dist node_modules pnpm-lock.yaml 2>nul",
  "reinstall": "pnpm run clean:packages && pnpm install"
}

3、vite.config.ts:

import { defineConfig, ConfigEnv, UserConfig } from 'vite' // 导入vite的类型定义
import vue from '@vitejs/plugin-vue'
import path from 'path'

// 生成唯一时间戳(精确到毫秒,确保每次打包都不同)
const buildTimestamp = new Date().getTime()

// https://vitejs.dev/config/
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
  return {
    plugins: [vue()],
    base: './', // 保持相对路径,无需关心CDN地址
    resolve: {
      alias: {
        // 明确指定路径别名的类型,避免TS类型提示警告
        '@': path.resolve(__dirname, 'src')
      }
    },
    build: {
      assetsDir: 'assets',
      outDir: 'dist',
      assetsInlineLimit: 4096,
      // 禁用打包缓存,确保每次打包重新计算哈希/时间戳
      cache: false,
      rollupOptions: {
        output: {
          // 方案1:时间戳 + hash(推荐,双重保证唯一性)
          assetFileNames: `assets/[name].${buildTimestamp}.[hash:8].[ext]`,
          chunkFileNames: `assets/[name].${buildTimestamp}.[hash:8].js`,
          entryFileNames: `assets/[name].${buildTimestamp}.[hash:8].js`,
          
          // 方案2:仅时间戳(极简,绝对唯一,可选)
          // assetFileNames: `assets/[name].${buildTimestamp}.[ext]`,
          // chunkFileNames: `assets/[name].${buildTimestamp}.js`,
          // entryFileNames: `assets/[name].${buildTimestamp}.js`
        }
      },
      sourcemap: false
    },
    server: {
      allowedHosts: ['www.test.com'],
      proxy: {
        '/api': {
          target: 'http://localhost:8967',
          changeOrigin: true,
          rewrite: (path) => path.replace('/api', '/api') // 修复代理路径
        }
      }
    }
  }
})

4、tsconfig.app.json:

{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "types": ["vite/client"],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

5、在src目录下建立router文件夹 在router文件夹下创建router.js:

import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'

// 路由懒加载
const Login = () => import('@/views/Login.vue')
const Home = () => import('@/views/Home.vue')

// 创建 Router 实例(基础配置,后续可扩展)
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/login',
      name: 'login',
      component: Login,
      meta: {
        title: '登录',
        requiresAuth: false // 登录页不需要认证
      }
    },
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: {
        title: '首页',
        requiresAuth: true // 需要登录
      }
    },
    {
      path: '/:pathMatch(.*)*',
      redirect: '/'
    }
  ]
})

// 路由守卫
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next) => {
  const userStore = useUserStore()
  const isLoggedIn = userStore.checkLogin()

  // 设置页面标题
  if (to.meta.title) {
    document.title = `${to.meta.title} - CMS 系统`
  }

  // 检查路由是否需要登录
  if (to.meta.requiresAuth) {
    if (!isLoggedIn) {
      // 未登录,跳转到登录页,并保存当前路径用于登录后跳转
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    // 如果已登录,访问登录页时重定向到首页
    if (to.path === '/login' && isLoggedIn) {
      next('/')
    } else {
      next()
    }
  }
})

export default router

types.d.ts:

import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    title?: string
    requiresAuth?: boolean
  }
}

6、在src目录下创建stores/user.ts

pina:

import { defineStore } from 'pinia'
import { ref } from 'vue'

export interface UserInfo {
  username: string
  token: string
}

export const useUserStore = defineStore(
  'user',
  () => {
    // 用户信息
    const userInfo = ref<UserInfo | null>(null)
    const isLoggedIn = ref(false)

    // 登录
    const login = async (username: string, password: string) => {
      // TODO: 这里应该调用实际的登录 API
      // 模拟登录请求
      return new Promise<boolean>((resolve) => {
        setTimeout(() => {
          if (username && password) {
            userInfo.value = {
              username,
              token: `token_${Date.now()}`
            }
            isLoggedIn.value = true
            resolve(true)
          } else {
            resolve(false)
          }
        }, 500)
      })
    }

    // 登出
    const logout = () => {
      userInfo.value = null
      isLoggedIn.value = false
    }

    // 检查登录状态
    const checkLogin = () => {
      // 从持久化存储中恢复登录状态
      return isLoggedIn.value && userInfo.value !== null
    }

    return {
      userInfo,
      isLoggedIn,
      login,
      logout,
      checkLogin
    }
  },
  {
    persist: true // 启用持久化存储
  }
)

7、创建src/utils/request.ts文件

安装相关依赖:

npm install axios nprogress qs
npm install -D @types/axios @types/nprogress @types/qs typescript

pnpm add axios nprogress qs
pnpm add -D @types/axios @types/nprogress @types/qs typescript

添加md5加密依赖
pnpm add crypto-js

封装:

// 导入所需依赖
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import qs from 'qs';

// 移除小圆圈加载进度
NProgress.configure({ showSpinner: false });

// 定义接口:扩展AxiosRequestConfig,支持自定义是否显示进度条(可选)
interface CustomAxiosRequestConfig extends AxiosRequestConfig {
  showLoading?: boolean; // 自定义字段:是否显示NProgress进度条,默认true
}

// 内部使用的配置类型
type InternalCustomConfig = AxiosRequestConfig & {
  showLoading?: boolean;
}

// 定义接口:统一响应数据格式(根据你的后端实际返回格式调整,这里是通用模板)
interface ApiResponse<T = any> {
  code: string | number; // 后端返回的状态码(可能是字符串或数字)
  data: T; // 后端返回的业务数据
  message?: string; // 后端返回的提示信息(文档中使用 message)
  msg?: string; // 兼容旧格式的提示信息
  timestamp?: number; // 时间戳(可选)
}

// 创建axios实例
const request: AxiosInstance = axios.create({
  baseURL: '', // 请求的基础url(完整地址:http://localhost:8888/admin)
  timeout: 3000, // 超时时间
  headers: {
    'Content-Type': 'application/json;charset=utf-8' // 默认请求头
  }
});

// ===================== 请求拦截器 =====================
request.interceptors.request.use(
  (config) => {
    const customConfig = config as InternalCustomConfig;
    
    // 控制是否显示进度条,默认显示(如果config没有指定showLoading,就为true)
    const shouldShowLoading = customConfig.showLoading ?? true;
    if (shouldShowLoading) {
      NProgress.start();
    }

    // 只针对get请求进行参数序列化(处理数组格式参数,arrayFormat: 'repeat' 格式如:ids=1&ids=2)
    if (config.method?.toLowerCase() === 'get' && config.params) {
      config.paramsSerializer = {
        serialize: (params) => qs.stringify(params, { arrayFormat: 'repeat' })
      };
    }

    // 添加token到请求头(从localStorage获取,兼容null/undefined情况)
    const token = localStorage.getItem('token');
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    return config;
  },
  (error: AxiosError) => {
    // 请求拦截器错误处理(如配置错误)
    NProgress.done(); // 无论如何,错误时关闭进度条
    console.error('请求拦截器错误:', error);
    return Promise.reject(error);
  }
);

// ===================== 响应拦截器 =====================
request.interceptors.response.use(
  (response: AxiosResponse<ApiResponse>) => {
    // 响应成功,关闭进度条
    NProgress.done();

    // 提取后端返回的响应数据(直接返回核心业务数据,简化上层使用)
    const res = response.data;

    // 可在这里统一处理业务状态码(根据你的后端约定调整)
    // 支持字符串和数字格式的状态码
    const code = String(res.code);
    if (code !== '200') { // 假设200是成功状态码
      const errorMsg = res.message || res.msg || '请求失败';
      console.warn('业务请求失败:', errorMsg);
      // 这里可以添加全局提示(如Element Plus的ElMessage)
      // ElMessage.error(errorMsg);
      return Promise.reject(res);
    }

    return res.data; // 最终返回:后端的业务数据(res.data)
  },
  (error: AxiosError) => {
    // 响应失败,关闭进度条
    NProgress.done();

    // 统一处理HTTP状态码错误
    const errorMsg = handleHttpError(error);
    console.error('响应拦截器错误:', errorMsg);
    // 这里可以添加全局提示(如Element Plus的ElMessage)
    // ElMessage.error(errorMsg);

    // 返回错误信息,方便上层捕获处理
    return Promise.reject({
      code: error.response?.status || 500,
      msg: errorMsg
    });
  }
);

// ===================== 辅助函数:处理HTTP状态码错误 =====================
function handleHttpError(error: AxiosError): string {
  if (error.response) {
    // 服务器返回了响应,但状态码不在2xx范围内
    const status = error.response.status;
    switch (status) {
      case 401:
        // 未授权/令牌过期,可在这里跳转到登录页
        // router.push('/login');
        return '登录状态过期,请重新登录';
      case 403:
        return '没有权限访问该资源';
      case 404:
        return '请求地址不存在';
      case 500:
        return '服务器内部错误';
      default:
        return `请求失败,HTTP状态码:${status}`;
    }
  } else if (error.request) {
    // 发送了请求,但没有收到服务器响应
    return '网络异常,请检查网络连接';
  } else {
    // 请求配置过程中发生的错误
    return `请求配置错误:${error.message}`;
  }
}

// ===================== 封装通用请求方法(可选,简化上层调用) =====================
/**
 * GET请求
 * @param url 请求地址
 * @param params 请求参数
 * @param config 额外配置(如是否显示进度条)
 * @returns Promise<T>
 */
export const get = <T = any>(
  url: string,
  params?: Record<string, any>,
  config?: CustomAxiosRequestConfig
): Promise<T> => {
  return request.get<T>(url, {
    params,
    ...config
  }) as Promise<T>;
};

/**
 * POST请求
 * @param url 请求地址
 * @param data 请求体数据
 * @param config 额外配置(如是否显示进度条)
 * @returns Promise<T>
 */
export const post = <T = any>(
  url: string,
  data?: Record<string, any>,
  config?: CustomAxiosRequestConfig
): Promise<T> => {
  return request.post<T>(url, data, config) as Promise<T>;
};

/**
 * PUT请求
 * @param url 请求地址
 * @param data 请求体数据
 * @param config 额外配置(如是否显示进度条)
 * @returns Promise<T>
 */
export const put = <T = any>(
  url: string,
  data?: Record<string, any>,
  config?: CustomAxiosRequestConfig
): Promise<T> => {
  return request.put<T>(url, data, config) as Promise<T>;
};

/**
 * DELETE请求
 * @param url 请求地址
 * @param params 请求参数
 * @param config 额外配置(如是否显示进度条)
 * @returns Promise<T>
 */
export const del = <T = any>(
  url: string,
  params?: Record<string, any>,
  config?: CustomAxiosRequestConfig
): Promise<T> => {
  return request.delete<T>(url, {
    params,
    ...config
  }) as Promise<T>;
};

// 导出axios实例(保留原始能力,方便特殊场景使用)和封装的请求方法
export default request;
export type { ApiResponse, CustomAxiosRequestConfig };

8、相关的请求api:

src/
├── types/          # 全局类型定义目录
│   ├── api.ts      # 接口请求/响应相关类型(如统一响应体、公共请求参数)
│   ├── user.ts     # 用户模块相关类型(如用户信息、用户入参)
│   ├── goods.ts    # 商品模块相关类型
│   └── index.ts    # 类型导出入口(可选,方便统一导入)
├── api/            # 接口请求封装
├── utils/          # 工具类(如你之前的 request.ts)
└── views/          # 页面组件

types/api.ts:

/**
 * 统一响应体类型(根据后端实际返回格式定义)
 */
export interface ApiResponse<T = any> {
  code: string | number // 状态码,文档中是字符串,但可能是数字
  message: string // 提示信息
  data: T // 业务数据
  timestamp?: number // 时间戳(可选)
}

/**
 * 分页请求参数
 */
export interface PageParams {
  pageNum?: number // 页码
  pageSize?: number // 每页数量
}

/**
 * 分页响应数据
 */
export interface PageResult<T = any> {
  list: T[] // 数据列表
  total: number // 总记录数
  pageNum: number // 当前页码
  pageSize: number // 每页数量
}

types/index.ts:

/**
 * 类型定义统一导出入口
 */

// API 相关类型
export * from './api'

// 用户相关类型
export * from './user'

types/user.ts:

/**
 * 用户信息类型
 */
export interface UserInfo {
  userId: number // 用户ID
  username: string // 登录账号
  nickname: string // 用户昵称
  status: number // 用户状态:1=启用,0=禁用
  token: string // JWT Token
}

/**
 * 登录请求参数
 */
export interface LoginParams {
  username: string // 登录账号
  password: string // 登录密码(MD5加密后的)
  captchaKey?: string // 验证码key(可选)
  captchaCode?: string // 验证码(可选)
}

/**
 * 验证码响应数据
 */
export interface CaptchaData {
  captchaKey: string // 验证码key,登录时需要传递此值
  imageBase64: string // Base64编码的验证码图片
}

utils/crypto.ts:

import CryptoJS from 'crypto-js'

/**
 * MD5 加密
 * @param text 待加密的文本
 * @returns MD5 加密后的字符串(小写)
 */
export function md5(text: string): string {
  return CryptoJS.MD5(text).toString()
}

 

 

 

你好:我的2025