使用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暂停前端开发,先搭建后端