vue3 复刻一下大佬的 经典 vue-admin-template (1)

65 阅读6分钟

(一)、准备 vue3 环境,本次复刻环境版本如下:

1、node .js  --- 20.18.1

2、 全局安装下 vite 

npm install -g vite

3、本次包管理工具都使用 pnpm ,和npm 类似,都是包管理工具  全局安装一下

npm install -g pnpm

(二)、初始化项目

提供了一个初始化的 vue + ts 的创建模板

pnpm create vite my-vue-app --template vue-ts

注意一哈:可能有些 ts 配置文件会报错(缺少这个配置:incremental)

配置文件名字:tsconfig.node.json

ts:

配置完成后保存下,如果还有报错提示,关编辑器重开;

noUncheckedSideEffectImports,如果提示这个报错了,需要升级下 ts, 命令:npm install -D typescript@latest,如果还不行就删除这个配置;

{
 "compilerOptions": {
   "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
   "incremental": true,
   "target": "ES2023",
   "lib": ["ES2023"],
   "module": "ESNext",
   "types": ["node"],
   "skipLibCheck": true,

   /* Bundler mode */
   "moduleResolution": "bundler",
   "allowImportingTsExtensions": true,
   "verbatimModuleSyntax": true,
   "moduleDetection": "force",
   "noEmit": true,

   /* Linting */
   "strict": true,
   "noUnusedLocals": true,
   "noUnusedParameters": true,
   "noFallthroughCasesInSwitch": true,
   "noUncheckedSideEffectImports": false
 },
 "include": ["vite.config.ts"]
}

(三)、一些管理工具库的安装

1、样式重置插件 normalize.css

pnpm i normalize.css --save

//在 main.ts 文件中 引入
import 'normalize.css/normalize.css' 

//验证引入是否成功
打开 f12,查看下样式中是否引入 normalize.css

2、样式框架安装 element-plus

pnpm i element-plus -S

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

//验证引入是否成功  在页面中引入一个 element 按钮组件,看看组件是否可以正常显示
 <el-button type="primary">Button</el-button>
 

3、vue-router 路由插件安装

vue-router 官网

对于vue3 项目,需要指定下 vue-router 路由的版本

(1)、安装路由插件

pnpm i vue-router@4

(2)、在项目 src 目录下新建 路由文件夹 router,在路由文件中新建 index.ts 文件,文件信息如下:

import { createWebHashHistory, createRouter } from 'vue-router'

import HomeView from '../components/HelloWorld.vue'

const routes = [{ path: '/', component: HomeView }]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

(3)、在main.ts 文件下引入路由文件

import router from './router'

(4)、注册插件

app.use(router) 

(5)、测试一下路由是否能正常跳转,做一个简单的 demo

1.清理下 App.vue ,清理之后 ,添加一个 RouterView 标签,用于测试一下路由组件是否可以正常展示 App.vue 文件清理之后如下:

<script setup lang="ts">
</script>

<template>
<div>
 <RouterView />
</div>
</template>

<style scoped></style>

2.检查下是 是否能正常展示出来 HelloWorld.vue 组件中的内容信息。

4、 pinia 持久状态库安装

pinia 官 网

(1)、安装插件

pnpm i pinia

(2)、组织 pinia 文件夹目录 在 src 下新建 stroes 文件夹,并新建 index.ts 文件,文件信息如下:

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

export const useCounterStore = defineStore('counter', () => {
 const count = ref(0)
 const doubleCount = computed(() => count.value * 2)
 function increment() {
   count.value++
 }

 return { count, doubleCount, increment }
})

(3)、在main.ts 中引入

import { createPinia } from 'pinia'

(4)、在main.ts 中使用插件

 app.use(createPinia())

(5)、测试stroes 改造一下 App.vue 文件,改造结果如下,检查界面是否出现了 0 这个数字,如果出现了说明 pinia 引入成功了。

     <script setup lang="ts">
import { useCounterStore } from './stores'

const store = useCounterStore()
const { count } = store
</script>

<template>
  <div>
    <div>{{ count }}</div>
    <RouterView />
  </div>
</template>

<style scoped></style>

5、axios 请求插件安装

1、安装插件

pnpm install axios -S

(1)封装 axios 请求文件,方便统一使用

src 目录新建 axios 文件夹

1、在axios 目录下创建 config.ts配置文件,配置一些基本的请求参数

export default {
  // 基础接口地址(根据环境区分)
  baseApi: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5173/api',
  // 请求超时时间
  timeout: 10000,
  // 默认请求头
  defaultHeaders: {
    'Content-Type': 'application/json'
  }
}

(2)在axios文件夹下面创建 axios 封装文件

import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
import config from './config'

// 定义响应数据结构(后端返回的统一格式)
interface ApiResponse<T = any> {
  code: number // 状态码(如 200 成功,500 失败)
  message: string // 提示信息
  data: T // 响应数据(泛型,动态指定)
  success: boolean // 是否成功
}

// 创建 Axios 实例
const service: AxiosInstance = axios.create({
  baseURL: config.baseApi,
  timeout: config.timeout,
  headers: config.defaultHeaders
})

// 请求拦截器
service.interceptors.request.use(
  (req: InternalAxiosRequestConfig) => {
    if (req.headers) {
      // 使用 set 方法设置默认 headers
      Object.entries(config.defaultHeaders).forEach(([key, value]) => {
        req.headers.set(key, value)
      })
    }

    // 添加 token
    const token = localStorage.getItem('token')
    if (token && req.headers) {
      req.headers.set('Authorization', `Bearer ${token}`)
    }

    return req
  },
  (error: AxiosError) => {
    return Promise.reject(error)
  }
)

// 响应拦截器:统一处理响应、错误
service.interceptors.response.use(
  (res: AxiosResponse<ApiResponse>) => {
    const responseData = res.data
    // 成功:返回响应数据中的 data 字段(简化上层使用)
    if (responseData.success || responseData.code === 200) {
      return responseData.data
    }
    // 失败:抛出错误(如业务错误,code 非 200)
    return Promise.reject(new Error(responseData.message || '请求失败'))
  },
  (error: AxiosError<ApiResponse>) => {
    // 响应错误处理(网络错误、404、500 等)
    const errorMsg = error.response?.data?.message || error.message || '网络错误'
    // 示例:根据状态码做特殊处理
    if (error.response?.status === 401) {
      // Token 过期:跳转登录页
      window.location.href = '/login'
    }
    return Promise.reject(new Error(errorMsg))
  }
)

// 定义请求方法类型(支持 GET/POST/PUT/DELETE)
type HttpMethod = 'get' | 'post' | 'put' | 'delete'

interface RequestOptions {
  method: HttpMethod
  url: string
  params?: Record<string, any> // GET/DELETE 参数(拼在 URL 上)
  data?: Record<string, any> // POST/PUT 参数(请求体)
  config?: AxiosRequestConfig // 额外 Axios 配置(如自定义头)
}
// 封装统一请求函数(泛型 T 为响应数据类型)
const request = async <T = any>(options: RequestOptions): Promise<T> => {
  try {
    const { method, url, params, data, config } = options // 解构对象参数
    const response = await service({
      method,
      url,
      params, // GET/DELETE 用 params
      data, // POST/PUT 用 data
      ...config // 合并额外配置(如自定义头)
    })
    return response as T
  } catch (error) {
    // 统一错误提示(可替换为 Element Plus/Naive UI 的 Message 组件)
    console.error('请求失败:', (error as Error).message)
    throw error // 抛出错误,让上层组件自行处理(可选)
  }
}

export default request
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
import config from './config'

// 定义响应数据结构(后端返回的统一格式)
interface ApiResponse<T = any> {
  code: number // 状态码(如 200 成功,500 失败)
  message: string // 提示信息
  data: T // 响应数据(泛型,动态指定)
  success: boolean // 是否成功
}

// 创建 Axios 实例
const service: AxiosInstance = axios.create({
  baseURL: config.baseApi,
  timeout: config.timeout,
  headers: config.defaultHeaders
})

// 请求拦截器
service.interceptors.request.use(
  (req: InternalAxiosRequestConfig) => {
    if (req.headers) {
      // 使用 set 方法设置默认 headers
      Object.entries(config.defaultHeaders).forEach(([key, value]) => {
        req.headers.set(key, value)
      })
    }

    // 添加 token
    const token = localStorage.getItem('token')
    if (token && req.headers) {
      req.headers.set('Authorization', `Bearer ${token}`)
    }

    return req
  },
  (error: AxiosError) => {
    return Promise.reject(error)
  }
)

// 响应拦截器:统一处理响应、错误
service.interceptors.response.use(
  (res: AxiosResponse<ApiResponse>) => {
    const responseData = res.data
    // 成功:返回响应数据中的 data 字段(简化上层使用)
    if (responseData.success || responseData.code === 200) {
      return responseData.data
    }
    // 失败:抛出错误(如业务错误,code 非 200)
    return Promise.reject(new Error(responseData.message || '请求失败'))
  },
  (error: AxiosError<ApiResponse>) => {
    // 响应错误处理(网络错误、404、500 等)
    const errorMsg = error.response?.data?.message || error.message || '网络错误'
    // 示例:根据状态码做特殊处理
    if (error.response?.status === 401) {
      // Token 过期:跳转登录页
      window.location.href = '/login'
    }
    return Promise.reject(new Error(errorMsg))
  }
)

// 定义请求方法类型(支持 GET/POST/PUT/DELETE)
type HttpMethod = 'get' | 'post' | 'put' | 'delete'

interface RequestOptions {
  method: HttpMethod
  url: string
  params?: Record<string, any> // GET/DELETE 参数(拼在 URL 上)
  data?: Record<string, any> // POST/PUT 参数(请求体)
  config?: AxiosRequestConfig // 额外 Axios 配置(如自定义头)
}
// 封装统一请求函数(泛型 T 为响应数据类型)
const request = async <T = any>(options: RequestOptions): Promise<T> => {
  try {
    const { method, url, params, data, config } = options // 解构对象参数
    const response = await service({
      method,
      url,
      params, // GET/DELETE 用 params
      data, // POST/PUT 用 data
      ...config // 合并额外配置(如自定义头)
    })
    return response as T
  } catch (error) {
    // 统一错误提示(可替换为 Element Plus/Naive UI 的 Message 组件)
    console.error('请求失败:', (error as Error).message)
    throw error // 抛出错误,让上层组件自行处理(可选)
  }
}

export default request

(3)使用示例(方便接下来写接口,以下代码提供一个使用示例)

在 src 文件夹下面创建一个 api 文件夹,用于后续接口统一放置 新建 index.ts 文件

import request from '../axios'

// 定义请求参数类型(分页)
interface userLoginParams {
username: string // 账户名
password: string // 密码
}

// 定义响应类型
interface userInfoResponse {
token: string
userInfo: {
  id: number
  name: string
  avatar: string
  role: string
}
}

export const getUserInfo = (data: userLoginParams) => {
return request<userInfoResponse>({
  method: 'post',
  url: '/auth/login',
  data
})
}


6、mock.js 后台模拟接口请求

(1)安装插件

pnpm install --save-dev vite-plugin-mock mockjs

(2) 封装 mockjs 在src 目录下新建 mock 文件夹,并创建 index.ts 文件 (以下为示例文件,mock 接口可按照此方式编写)

import type { MockMethod } from 'vite-plugin-mock' // 导入 Mock 方法类型
import Mock from 'mockjs' // 导入 Mockjs 生成模拟数据

//定义请求体参数类型
interface requestBody {
  query: Record<string, any>
  body: Record<string, any>
}

// 定义 Mock 接口数组(MockMethod 类型约束)
interface userInfo {
  username: string
  password: string
  userId: string
}

export default [
  // 1. 登录接口(POST)
  {
    url: '/api/auth/login', // 接口地址(需与真实接口路径一致)
    method: 'post', // 请求方法
    response: (req: requestBody) => {
      // body 是前端传递的请求体(如 username、password)
      const { username, password } = req.body

      // 模拟登录校验(实际开发可根据需求调整)
      if (username === 'admin' && password === '123456') {
        return {
          code: 200,
          success: true,
          message: '登录成功',
          data: {
            token: Mock.Random.guid(), // 生成随机 Token
            userInfo: {
              id: Mock.Random.id(),
              name: '管理员',
              avatar: Mock.Random.image('100x100', '#888888', '头像'),
              role: 'admin'
            }
          }
        }
      } else {
        return {
          code: 400,
          success: false,
          message: '用户名或密码错误',
          data: null
        }
      }
    }
  }
] as MockMethod[] // 类型断言,确保接口格式正确


(3) 配置 vite-plugin-mock mockjs

在vite.config.ts 文件中配置一下 mock 插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig({
  plugins: [vue(), viteMockServe({ mockPath: './src/mock', logger: true, enable: true })]
})

(4)测试mock 封装的接口能否正常使用

改造一下 App.vue 文件中的代码,分别为引入 api 接口,点击 按钮,查询接口信息并进行展示。 改造后的App.vue 文件如下:

<script setup lang="ts">
import { useCounterStore } from './stores'
import { getUserInfo } from './api'
import { ref } from 'vue'

const store = useCounterStore()
const { count } = store


interface userInfo {
avatar: string
}

const userInfo = ref<userInfo>({
avatar: ''
})

const getGoods = async () => {

const res = await getUserInfo({
  username: 'admin',
  password: '123456'
})
console.log(res.userInfo)
userInfo.value = res.userInfo
}
</script>

<template>
<div>
  <img :src="userInfo.avatar" alt="">
  <el-button @click="getGoods()">点击按钮查询接口信息</el-button>
  <div>{{ count }}</div>
  <RouterView />
</div>
</template>

<style scoped></style>

(四)、 项目搭建初始化结束

到这里,项目的一些基本框架就已经搭建完毕了 vue3 + ts + pinia + vue-router + element-plus + axios + mockjs