使用vite+vue3+ElementPlus+pina搭建中后台应用
- 新建项目
- 引入ElementPlus
-
- 安装
- 使用
- 引入样式
- 布局
- 引入Pinia
-
- 安装
- hooks-useWindow
- store存储菜单状态
- 处理菜单折叠
- 在header处理折叠
-
- [引入 @element-plus/icons-vue](#引入 @element-plus/icons-vue)
- 引入vue-router
- 网络请求
进度 2025-10-30

仓库地址
https://github.com/codehskdog/system-manager
新建项目
使用vite新建一个vue3+ts项目
我的vite版本是7.17, vue为3.5.22
npm init vite@latest

使用你喜欢的包管理工具安装依赖。
安装完成后执行
npm run dev
引入ElementPlus
安装
选择你喜欢的包管理器安装即可 npm/yarn/pnpm
pnpm install element-plus
使用
这里使用按需引入。
需要安装pnpm install -D unplugin-vue-components unplugin-auto-import
修改vite.config.ts
ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
});
在App.vue中,根组件为
html
<script setup lang="ts"></script>
<template>
<el-config-provider>
<!-- ... -->
</el-config-provider>
</template>
引入样式
尝试下


没有引入样式就正常显示了。why?自动导入插件帮我们做了
自定义主题
新建文件 src/styles/index.scss
这里可以写自己项目的样式。
然后在main.ts引入
比如我们这样写。
css
body {
width: 100vw;
height: 100vh;
margin: 0;
}
#app {
width: 100%;
height: 100%;
}
新建文件 src/styles/element/var.scss 写一些自定义的主题
控制台可能会报错,说你却少sass依赖,根据它的提示安装就好。

在这里重写一些定义
css
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #003261,
),
'success': (
'base': #21ba45,
),
'warning': (
'base': #f2711c,
),
'danger': (
'base': #db2828,
),
'error': (
'base': #db2828,
),
'info': (
'base': #42b8dd,
),
),
$button-padding-horizontal: (
'default': 80px,
)
);
配置vite.config.ts
配置别名 '@': path.resolve(__dirname, 'src'),
ts
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import path from 'path';
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [
ElementPlusResolver({
importStyle: 'sass',
}),
],
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/element/var.scss" as *;`,
},
},
},
});
主要是配置css预处理选项,然后因为是按需导入,需要配置自动导入的为scss。

布局
新建一个目录
src/layout
新建一个文件scr/layout/index.vue
layout目录下新建两个目录 src/layout/pc src/layout/mobile
分别新建index.vue

将App.vue中的

移动到src/layout/index.vue 并且在App.vue引入src/layout/index.vue

此刻可能会提示找不到定义什么的。
修改tsconfig.app.json 增加baseUrl和paths。

这里先不管 mobile/index.vue,先完善pc/index.vue
html
<template>
<el-container>
<el-aside width="200px"> <Menu /></el-aside>
<el-container>
<el-header><Header /></el-header>
<el-main><Page /></el-main>
</el-container>
</el-container>
</template>
<script setup>
import Menu from './Menu.vue';
import Header from './Header.vue';
import Page from './Page.vue';
</script>
<style></style>
在layout/index.vue引入使用

在pc目录下新增几个组件,并在pc/index.vue引入使用

修改下 el-header的默认padding和高度
这里你可以看看element-plus的定义找到header的


对于aside的宽度,我们查看下元素,发现--el-aside-width未定义

我们可以自己定义

说白了,他这个定义也就是注入到root,我们同样可以。包括定义$header-bg-color的颜色。
header.vue
html
<template>
<div class="header"></div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
.header {
width: 100%;
height: 100%;
background-color: $header-bg-color;
}
</style>
当然我这里使用的是scss,你也可以使用一些原子化的样式。
Menu.vue
html
<template>
<div class="menu"></div>
</template>
<script setup lang="ts"></script>
<style style lang="scss">
.menu {
width: 100%;
height: 100vh;
background-color: $menu-bg-color;
}
</style>

然后Page.vue
此时我们的内容存放位置el-main

所以Page暂时比较简单,先把router-view写进来,虽然还没开始使用vue-router
page.vue
html
<template>
<div class="content">
<router-view></router-view>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" coped>
.content {
width: 100%;
height: 100%;
}
</style>
到此布局简单完成。
布局就像我们一个人,无论你穿什么样子的衣服,你总是有头,身体,四肢对吧,然后我们就可以戴帽子,穿衣服,做搭配?layout/index.vue可以理解为试衣间,给我们提供数据等等。
引入Pinia
https://pinia.vuejs.org/core-concepts/
安装
pnpm add pinia
新增目录
src/stores并在此目录下增加index.ts
负责处理Pinia的注册。
ts
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
在main.ts中引入
ts
import { createApp } from 'vue';
import App from './App.vue';
import '@/styles/index.scss';
import pinia from '@/stores/index';
const app = createApp(App);
app.use(pinia).mount('#app');
hooks-useWindow
工具库-获取浏览器信息和窗口大小
修改tsconfig的配置

新增目录 src/utils 负责存放公共工具方法。并新增index.ts文件

新增enums.ts文件
ts
// 屏幕尺寸枚举
export enum ScreenSize {
XS = 'xs', // <768px
SM = 'sm', // 768px~991px
MD = 'md', // 992px~1199px
XL = 'xl', // 1200px~1919px
FULL = 'full', // >=1920px
}
// 浏览器类型枚举
export enum BrowserType {
CHROME = 'chrome',
SAFARI = 'safari',
FIREFOX = 'firefox',
OPERA = 'opera',
MSIE = 'msie',
OTHER = 'other',
}
// 设备标签枚举
export enum DeviceTag {
PC = 'pc',
MOBILE = 'mobile',
PAD = 'pad',
ANDROID_PAD = 'androidPad',
}
// 浏览器前缀枚举
export enum BrowserPrefix {
WEBKIT = 'webkit',
MS = 'ms',
MOZ = 'Moz',
O = 'O',
}
utils/index.ts
ts
import { ScreenSize, BrowserType, DeviceTag, BrowserPrefix } from './enums'; // 导入枚举
export function getBrowser() {
const docEl = document.documentElement;
const ua = navigator.userAgent.toLowerCase();
const { platform } = navigator;
const { clientWidth: width, clientHeight: height } = docEl;
// 1. 检测浏览器类型
let browserType: BrowserType = BrowserType.OTHER;
if (ua.includes('firefox')) {
browserType = BrowserType.FIREFOX;
} else if (ua.includes('chrome')) {
browserType = BrowserType.CHROME;
} else if (ua.includes('safari')) {
browserType = BrowserType.SAFARI;
} else if (ua.includes('opera') || ua.includes('opr')) {
browserType = BrowserType.OPERA;
} else if (ua.includes('msie') || ua.includes('trident')) {
browserType = BrowserType.MSIE;
}
// 2. 检测设备类型
const isTouchDevice =
'ontouchstart' in window || ua.includes('touch') || ua.includes('mobile');
let deviceTag: DeviceTag = DeviceTag.PC;
if (isTouchDevice) {
if (ua.includes('ipad')) {
deviceTag = DeviceTag.PAD;
} else if (ua.includes('mobile')) {
deviceTag = DeviceTag.MOBILE;
} else if (ua.includes('android')) {
deviceTag = DeviceTag.ANDROID_PAD;
}
}
// 3. 确定浏览器前缀(使用枚举映射)
const prefixMap: Record<BrowserType, BrowserPrefix> = {
[BrowserType.CHROME]: BrowserPrefix.WEBKIT,
[BrowserType.SAFARI]: BrowserPrefix.WEBKIT,
[BrowserType.FIREFOX]: BrowserPrefix.MOZ,
[BrowserType.OPERA]: BrowserPrefix.O,
[BrowserType.MSIE]: BrowserPrefix.MS,
[BrowserType.OTHER]: BrowserPrefix.WEBKIT,
};
const browserPrefix = prefixMap[browserType];
// 4. 检测操作系统平台
const osPlatform = ua.includes('android')
? 'android'
: platform.toLowerCase();
// 5. 屏幕尺寸分级(使用 ScreenSize 枚举)
let screenSize: ScreenSize = ScreenSize.FULL;
if (width < 768) {
screenSize = ScreenSize.XS;
} else if (width < 992) {
screenSize = ScreenSize.SM;
} else if (width < 1200) {
screenSize = ScreenSize.MD;
} else if (width < 1920) {
screenSize = ScreenSize.XL;
}
// 6. 衍生辅助判断
const isIOS = /\(i[^;]+;( u;)? cpu.+mac os x/.test(ua);
const isPC = deviceTag === DeviceTag.PC;
const isMobile = !isPC;
const isMini = screenSize === ScreenSize.XS || isMobile;
return {
width,
height,
type: browserType,
prefix: browserPrefix,
plat: osPlatform,
tag: deviceTag,
screen: screenSize,
isMobile,
isIOS,
isPC,
isMini,
};
}
useWindow.ts
ts
import { onMounted, onUnmounted, ref } from 'vue';
import { getBrowser } from '@/utils/index'; // 导入浏览器信息检测函数
import {
ScreenSize,
BrowserType,
DeviceTag,
BrowserPrefix,
} from '@/utils/enums'; // 导入类型
export type BrowserInfo = {
width: number;
height: number;
type: BrowserType;
prefix: BrowserPrefix;
plat: string;
tag: DeviceTag;
screen: ScreenSize;
isMobile: boolean;
isIOS: boolean;
isPC: boolean;
isMini: boolean;
};
export const useWindow = () => {
// 用ref存储响应式浏览器信息
const browserInfo = ref<BrowserInfo>({} as BrowserInfo);
const needCollapse = ref(false);
// 更新浏览器信息的方法
const updateBrowserInfo = () => {
browserInfo.value = getBrowser();
const need = [ScreenSize.XS, ScreenSize.SM, ScreenSize.MD];
if (need.includes(browserInfo.value.screen)) {
needCollapse.value = true;
} else {
needCollapse.value = false;
}
};
// 初始化信息
updateBrowserInfo();
// 监听窗口变化(无节流,实时触发)
window.addEventListener('resize', updateBrowserInfo);
onUnmounted(() => {
// 移除监听,避免内存泄漏
window.removeEventListener('resize', updateBrowserInfo);
});
return {
browserInfo,
needCollapse,
// 手动更新方法
updateBrowserInfo,
};
};
store存储菜单状态

ts
import { useWindow } from '@/hooks/useWindow';
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
export const useGlobalStore = defineStore('global', () => {
const { browserInfo, needCollapse } = useWindow();
watch(
() => browserInfo.value.screen,
(v) => {
menuCollapsed.value = needCollapse.value;
}
);
const menuCollapsed = ref(needCollapse.value);
const collapsedMenu = () => {
menuCollapsed.value = true;
};
const expandMenu = () => {
menuCollapsed.value = false;
};
return { menuCollapsed, collapsedMenu, expandMenu, browserInfo };
});
处理菜单折叠
这个时候可以先把el-aside的宽度设置为auto

在Menu.vue
html
<template>
<el-menu default-active="2" :collapse-transition="false" :collapse="menuCollapsed" class="menu">
<el-menu-item index="2">
<template #title>Navigator Two</template>
</el-menu-item>
<el-menu-item index="3" disabled>
<template #title>Navigator Three</template>
</el-menu-item>
<el-menu-item index="4">
<template #title>Navigator Four</template>
</el-menu-item>
</el-menu>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useGlobalStore } from '@/stores/modules/global';
import { storeToRefs } from 'pinia';
const store = useGlobalStore();
const { menuCollapsed } = storeToRefs(store);
const width = computed(() => {
return menuCollapsed.value ? '80px' : '200px';
});
</script>
<style style lang="scss" scoped>
.menu {
width: 100%;
height: 100vh;
width: v-bind(width);
}
</style>
在header处理折叠
引入 @element-plus/icons-vue
pnpm install @element-plus/icons-vue

使用Expand和Fold
先去掉 背景色,加个border区分。
html
<template>
<div class="header">
<el-icon size="20" class="icon">
<component
:is="menuCollapsed ? Expand : Fold"
@click="changeMenuCollapsed"
/>
</el-icon>
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '@/stores/modules/global';
import { Expand, Fold } from '@element-plus/icons-vue';
const { menuCollapsed, changeMenuCollapsed } = useGlobalStore();
</script>
<style scoped lang="scss">
.header {
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 10px;
width: 100%;
height: 100%;
border-bottom: 1px solid #e3e3e3;
// background-color: $header-bg-color;
.icon {
cursor: pointer;
}
}
</style>
引入vue-router
安装pnpm add vue-router@4
新建目录src/router
新建目录src/views
在views目录下新建两个vue文件,随便输入点内容

html
<template>
<div>Home</div>
</template>
<script></script>
<style></style>
在src/router下新增index.ts
ts
import { createWebHashHistory, createRouter } from 'vue-router';
const routes = [
{ path: '/', component: () => import('@/views/Home.vue') },
{ path: '/about', component: () => import('@/views/About.vue') },
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
在main.ts中引入

有了router后再改下Menu
html
<template>
<el-menu
default-active="/"
:collapse="menuCollapsed"
:collapse-transition="false"
class="menu"
router
>
<el-menu-item index="/">
<el-icon>
<HomeFilled />
</el-icon>
<template #title>首页</template>
</el-menu-item>
<el-menu-item index="/about">
<el-icon>
<Setting />
</el-icon>
<template #title>About</template>
</el-menu-item>
</el-menu>
</template>
<script setup lang="ts">
import { HomeFilled, Setting } from '@element-plus/icons-vue';
import { computed, ref, watch } from 'vue';
import { useGlobalStore } from '@/stores/modules/global';
import { storeToRefs } from 'pinia';
const store = useGlobalStore();
const { menuCollapsed } = storeToRefs(store);
const width = computed(() => {
return menuCollapsed.value ? '80px' : '200px';
});
</script>
<style style lang="scss" scoped>
.menu {
width: 100%;
height: 100vh;
width: v-bind(width);
}
</style>
到此就有了一个大概的框架。

网络请求
一般我们的页面渲染可以理解为Render(data)
我们负责写Render,而后端提供data。
网络请求接入。
一般使用axios,或者可以尝试下umi的一个中间件umi-request。
umi-request文档地址
安装
pnpm install --save umi-request

我们也用创建实例的方式

新增文件src/api/index.ts

PS:2025-10-30暂停前端开发,先搭建后端