(一)Vue3+JS+Vite+pnpm 仿若依后台 从零搭建
技术栈:Vue3 (JS 非 TS) + Vite + Element‑Plus + Axios + mock 本地模拟数据,同时支持真实后端网络请求,布局菜单、登录、路由守卫对齐若依风格
1、创建项目
pnpm create vue@latest
选项照着选:
✔ Project name: ... szty-admin
✔ Add TypeScript? ... No
✔ Add JSX Support? ... No
✔ Add Vue Router for Single Page Application development? ... Yes ✅路由必须
✔ Add Pinia for state management? ... Yes ✅状态管理(存token用户信息,仿若依store)
✔ Add Vitest for Unit testing? ... No
✔ Add an End‑to‑End Testing Solution? ... No
✔ Add ESLint for code quality? ... Yes
✔ Add Prettier for code formatting? ... No
cd szty-admin
pnpm install
安装必须依赖
# element‑plus UI库
pnpm add element-plus
# axios网络请求
pnpm add axios
# mock模拟数据 vite插件
pnpm add -D vite-plugin-mock mockjs
2、目录改造(对齐若依目录结构)
删除 src/components/HelloWorld.vue
完整 src 目录结构
src
├─ api # 请求接口,和若依一样
│ └─ user.js
├─ assets
├─ components # 公共组件
├─ layout # 主布局【仿若依Layout】
│ ├─ index.vue # 主布局容器(侧边菜单+头部+主内容)
│ ├─ Sidebar.vue # 侧边栏菜单
│ └─ Header.vue # 顶部导航栏
├─ mock # 本地mock模拟数据
│ ├─ user.js
│ └─ menu.js
├─ router
│ └─ index.js # 路由、路由守卫
├─ stores # pinia代替vuex,存储token用户信息
│ └─ user.js
├─ utils
│ └─ request.js # axios封装(仿若依request拦截器)
├─ views # 页面
│ ├─ login # 登录页
│ │ └─ index.vue
│ └─ home # 首页
│ └─ index.vue
├─ App.vue
└─ main.js
3、配置 vite.config.js 开启 mock
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
viteMockServe({
mockPath: './src/mock', // mock文件夹位置
localEnabled: true, // 开发环境开启mock本地模拟数据
prodEnabled: false // 生产关闭mock,走真实后端接口
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
4、mock 模拟数据 src/mock/user.js
// 登录mock接口,本地模拟返回token用户信息
export default [
{
url: '/api/login',
method: 'post',
response: ({ body }) => {
const { username, password } = body
if (username === 'admin' && password === '123456') {
return {
code: 200,
msg: '登录成功',
data: {
token: 'admin-token-123456',
username: 'admin'
}
}
} else {
return { code: 500, msg: '账号密码错误' }
}
}
},
{
url: '/api/getMenu',
method: 'get',
response: () => {
// 本地写死菜单数据,和若依返回菜单格式一致
return {
code:200,
data:[
{
path:'/home',
name:'Home',
meta:{title:'首页',icon:'House'},
component:'home/index'
}
]
}
}
}
]
5、axios 封装 src/utils/request.js
可以切换:mock 本地模拟数据 / 真实后端网络请求
import axios from 'axios'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: '/api', // mock使用 /api;对接后端改成 http://127.0.0.1:8080/api
timeout: 5000
})
// 请求拦截器 带上token
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if(token){
config.headers['Authorization'] = token
}
return config
})
//响应拦截器
service.interceptors.response.use(res=>{
const data = res.data
if(data.code !== 200){
ElMessage.error(data.msg || '请求失败')
return Promise.reject(data)
}
return data
},err=>{
ElMessage.error('网络异常')
return Promise.reject(err)
})
export default service
6、api 接口层 src/api/user.js
import request from '@/utils/request'
//登录
export function login(data){
return request({
url:'/login',
method:'post',
data
})
}
//获取菜单
export function getMenu(){
return request({
url:'/getMenu',
method:'get'
})
}
7、pinia 用户状态 src/stores/user.js
import { defineStore } from 'pinia'
import { login } from '@/api/user'
export const useUserStore = defineStore('user',{
state(){
return {
token: localStorage.getItem('token') || '',
username:''
}
},
actions:{
async loginAction(form){
const res = await login(form)
this.token = res.data.token
localStorage.setItem('token',res.data.token)
return res
},
logout(){
this.token = ''
localStorage.removeItem('token')
}
}
})
8、路由配置 src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
//布局
const Layout = ()=>import('@/layout/index.vue')
const routes = [
{
path:'/login',
component:()=>import('@/views/login/index.vue')
},
{
path:'/',
component:Layout,
redirect:'/home',
children:[
{
path:'home',
name:'home',
component:()=>import('@/views/home/index.vue'),
meta:{title:'首页'}
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
//路由守卫,仿若依,没token跳登录
router.beforeEach((to,from,next)=>{
const userStore = useUserStore()
const token = userStore.token
if(to.path === '/login'){
next()
}else{
if(token){
next()
}else{
next('/login')
}
}
})
export default router
9、主布局 src/layout/index.vue
<template>
<el-container style="height:100vh">
<el-aside width="220px">
<Sidebar />
</el-aside>
<el-container>
<el-header height="60px">
<Header />
</el-header>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import Sidebar from './Sidebar.vue'
import Header from './Header.vue'
</script>
src/layout/Sidebar.vue 侧边菜单
<template>
<el-menu router default-active="$route.path" background-color="#304156" text-color="#fff">
<el-menu-item index="/home">首页</el-menu-item>
</el-menu>
</template>
src/layout/Header.vue 顶部栏
<template>
<div class="header">
<span>后台管理系统</span>
<el-button @click="handleLogout" type="text">退出登录</el-button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
const handleLogout = ()=>{
userStore.logout()
router.push('/login')
}
</script>
<style scoped>
.header{
display:flex;
justify-content: space-between;
align-items:center;
height:100%;
padding:0 20px;
}
</style>
10、登录页面 src/views/login/index.vue
<template>
<div class="login">
<el-card style="width:400px">
<el-form ref="formRef" :model="form">
<el-form-item label="账号">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password"></el-input>
</el-form-item>
<el-button type="primary" @click="submit">登录</el-button>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
const form = ref({
username:'admin',
password:'123456'
})
const submit = async ()=>{
await userStore.loginAction(form.value)
router.push('/')
}
</script>
<style scoped>
.login{
height:100vh;
display:flex;
justify-content:center;
align-items:center;
background:#f5f7fa;
}
</style>
11、首页 src/views/home/index.vue
<template>
<div>欢迎登录后台管理系统</div>
</template>
12、main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.use(ElementPlus)
app.mount('#app')
13、App.vue
<template>
<router-view></router-view>
</template>
<style>
*{margin:0;padding:0}
</style>
运行项目
pnpm dev
账号admin,密码123456


源码
两种模式切换
- 本地 mock 模拟(现在用的)
vite.config.js中localEnabled:true,所有接口走 src/mock 模拟数据,不需要后端服务。 - 对接真实后端网络访问
- 修改
src/utils/request.js的baseURL改为后端地址baseURL:'http://127.0.0.1:8080/api' - vite.config.js
localEnabled:false,关闭 mock,全部请求发送到后端。
和若依逻辑完全对齐:登录存 token、路由守卫拦截、axios 请求 / 响应拦截、api 接口分层、layout 布局。