当前位置: 代码网 > it编程>前端脚本>Vue.js > Vue3 + Vite + TypeScript + Element-Plus:从零到一构建企业级后台管理系统(前后端开源)

Vue3 + Vite + TypeScript + Element-Plus:从零到一构建企业级后台管理系统(前后端开源)

2024年08月02日 Vue.js 我要评论
vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 + Element Plus 版本的后台管理前端解决方案,技术栈为 Vue3 + Vite4 + TypeScript + Element Plus + Pinia + Vue Router 等当前主流框架。本篇是 vue3-element-admin v2.x 版本从 0 到 1,相较于v1.x 版本增加了对原子CSS(UnoCSS)、按需自动导入、暗黑模式的支持。

前言

本篇是 vue3-element-admin v2.x 版本从 0 到 1,相较于 主要增加了对原子css(unocss)、按需自动导入、暗黑模式的支持。

项目预览

在线预览

https://vue3.youlai.tech/

首页控制台

image-20230326161901446
暗黑模式

接口文档

接口文档

权限管理系统

用户管理角色管理
菜单管理字典管理

扩展生态

youlai-mall 有来开源商城:spring cloud微服务+ vue3-element-admin+uni-app

youlai-mall 商品管理mall-app 移动端

项目指南

功能清单

技术栈&官网

技术栈描述官网
vue3渐进式 javascript 框架https://cn.vuejs.org/
element plus基于 vue 3,面向设计师和开发者的组件库https://element-plus.gitee.io/zh-cn/
vite前端开发与构建工具https://cn.vitejs.dev/
typescript微软新推出的一种语言,是 javascript 的超集https://www.tslang.cn/
pinia新一代状态管理工具https://pinia.vuejs.org/
vue routervue.js 的官方路由https://router.vuejs.org/zh/
wangeditortypescript 开发的 web 富文本编辑器https://www.wangeditor.com/
echarts一个基于 javascript 的开源可视化图表库https://echarts.apache.org/zh/
vue-i18nvue 国际化多语言插件https://vue-i18n.intlify.dev/
vueuse基于vue组合式api的实用工具集(类比hutool工具)http://www.vueusejs.com/

前/后端源码

giteegithub
前端vue3-element-adminvue3-element-admin
后端youlai-bootyoulai-boot

接口文档

环境准备

名称备注
开发工具vscode 下载-
运行环境node 16+ 下载image-20230224222640120
vscode插件(必装)插件市场搜索 vue language features (volar) typescript vue plugin (volar) 安装,且禁用 veturimage-20230224222541797

项目初始化

按照 🍃vite 官方文档 - 搭建第一个 vite 项目 说明,执行以下命令完成 vuetypescirpt 模板项目的初始化

 npm init vite@latest vue3-element-admin --template vue-ts
  • **vue3-element-admin **: 自定义的项目名称

  • vue-tsvue + typescript 模板的标识,查看 create-vite 以获取每个模板的更多细节:vue,vue-ts,react,react-ts

初始化完成项目位于 d:\project\demo\vue3-element-admin , 使用 vscode 导入,执行以下命令启动:

npm install
npm run dev

浏览器访问 localhost:5173 预览

路径别名配置

vite 配置

typescirpt 编译器配置

// tsconfig.json
"compileroptions": {
    ...
    "baseurl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { // 路径映射,相对于baseurl
    	"@/*": ["src/*"] 
    }
}

路径别名使用

// src/app.vue
import helloworld from '/src/components/helloworld.vue'import helloworld from '@/components/helloworld.vue'

安装自动导入

概念

为了避免在多个页面重复引入 api组件,由此而产生的自动导入插件来节省重复代码和提高开发效率。

插件概念自动导入对象
unplugin-auto-import按需自动导入apiref,reactive,watch,computed 等api
unplugin-vue-components按需自动导入组件element plus 等三方库和指定目录下的自定义组件

看下自动导入插件未使用和使用的区别:

插件名未使用自动导入使用自动导入
unplugin-auto-import
unplugin-vue-components

安装插件依赖

npm install -d unplugin-auto-import unplugin-vue-components 

vite.config.ts - 自动导入配置

新建 /src/types 目录用于存放自动导入函数和组件的ts类型声明文件

import autoimport from "unplugin-auto-import/vite";
import components from "unplugin-vue-components/vite";

plugins: [
  autoimport({
    // 自动导入 vue 相关函数,如:ref, reactive, toref 等
    imports: ["vue"],
    eslintrc: {
      enabled: true, // 是否自动生成 eslint 规则,建议生成之后设置 false 
      filepath: "./.eslintrc-auto-import.json", // 指定自动导入函数 eslint 规则的文件
    },
    dts: path.resolve(pathsrc, "types", "auto-imports.d.ts"), // 指定自动导入函数ts类型声明文件路径
  }),
  components({
    dts: path.resolve(pathsrc, "types", "components.d.ts"), // 指定自动导入组件ts类型声明文件路径
  }),
]

.eslintrc.cjs - 自动导入函数 eslint 规则引入

"extends": [
    "./.eslintrc-auto-import.json"
],

tsconfig.json - 自动导入ts类型声明文件引入

{
  "include": ["src/**/*.d.ts"]
}

自动导入效果

运行项目 npm run dev 自动

image-20230227234439410

整合 element plus

参考: element plus 按需自动导入

需要完成上面一节的 自动导入 的安装和配置

安装 element plus

npm install element-plus

安装自动导入 icon 依赖

npm i -d unplugin-icons

vite.config.ts 配置

参考: element-plus-best-practices - vite.config.ts

// vite.config.ts
import { elementplusresolver } from "unplugin-vue-components/resolvers";
import icons from "unplugin-icons/vite";
import iconsresolver from "unplugin-icons/resolver";

export default ({ mode }: configenv): userconfig => {

  return {
    plugins: [
      // ...
      autoimport({
        // ...  
        resolvers: [
          // 自动导入 element plus 相关函数,如:elmessage, elmessagebox... (带样式)
          elementplusresolver(),
          // 自动导入图标组件
          iconsresolver({}),
        ]
        vuetemplate: true, // 是否在 vue 模板中自动导入
        dts: path.resolve(pathsrc, 'types', 'auto-imports.d.ts') // 自动导入组件类型声明文件位置,默认根目录
          
      }),
      components({ 
        resolvers: [
          // 自动导入 element plus 组件
          elementplusresolver(),
          // 自动注册图标组件
          iconsresolver({
            enabledcollections: ["ep"] // element-plus图标库,其他图标库 https://icon-sets.iconify.design/
          }),
        ],
        dts: path.resolve(pathsrc, "types", "components.d.ts"), //  自动导入组件类型声明文件位置,默认根目录
      }),
      icons({
        // 自动安装图标库
        autoinstall: true,
      }),
    ],
  };
};

示例代码

<!-- src/components/helloworld.vue -->
<div>
  <el-button type="success"><i-ep-successfilled />success</el-button>
  <el-button type="info"><i-ep-infofilled />info</el-button>
  <el-button type="warning"><i-ep-warningfilled />warning</el-button>
  <el-button type="danger"><i-ep-warntrianglefilled />danger</el-button>
</div>

效果预览

整合 svg 图标

参考: vite-plugin-svg-icons 安装文档

安装依赖

npm install -d fast-glob@3.2.11 
npm install -d vite-plugin-svg-icons@2.0.1 

创建 src/assets/icons 目录 , 放入从 iconfont 复制的 svg 图标

main.ts 引入注册脚本

// src/main.ts
import 'virtual:svg-icons-register';

vite.config.ts 配置插件

// vite.config.ts
import { createsvgiconsplugin } from 'vite-plugin-svg-icons';

export default ({command, mode}: configenv): userconfig => {
 return (
     {
         plugins: [
             createsvgiconsplugin({
                 // 指定需要缓存的图标文件夹
                 icondirs: [path.resolve(process.cwd(), 'src/assets/icons')],
                 // 指定symbolid格式
                 symbolid: 'icon-[dir]-[name]',
             })
         ]
     }
 )
}

svg 组件封装

<!-- src/components/svgicon/index.vue -->
<script setup lang="ts">
const props = defineprops({
prefix: {
 type: string,
 default: "icon",
},
iconclass: {
 type: string,
 required: false,
},
color: {
 type: string,
},
size: {
 type: string,
 default: "1em",
},
});

const symbolid = computed(() => `#${props.prefix}-${props.iconclass}`);
</script>

<template>
<svg
 aria-hidden="true"
 class="svg-icon"
 :style="'width:' + size + ';height:' + size"
>
 <use :xlink:href="symbolid" :fill="color" />
</svg>
</template>

<style scoped>
.svg-icon {
display: inline-block;
outline: none;
width: 1em;
height: 1em;
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
fill: currentcolor; /* 定义元素的颜色,currentcolor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
overflow: hidden;
}
</style>

组件使用

<!-- src/components/helloworld.vue -->
<template>
 <el-button type="info"><svg-icon icon-class="block"/>svg 本地图标</el-button>
</template>

整合 scss

安装依赖

npm i -d sass 

创建 variables.scss 变量文件,添加变量 $bg-color 定义,注意规范变量以 $ 开头

// src/styles/variables.scss
$bg-color:#242424;

vite 配置导入 scss 全局变量文件

// vite.config.ts
css: {
    // css 预处理器
    preprocessoroptions: {
        //define global scss variable
        scss: {
            javascriptenabled: true,
            additionaldata: `@use "@/styles/variables.scss" as *;`
        }
    }
}

style 标签使用scss全局变量

<!-- src/components/helloworld.vue -->
<template>
  <div class="box" />
</template>

<style lang="scss" scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: $bg-color;
}
</style>

上面导入的 scss 全局变量在 typescript 不生效的,需要创建一个以 .module.scss 结尾的文件

// src/styles/variables.module.scss

// 导出 variables.scss 文件的变量
:export{
    bgcolor:$bg-color
}

typescript 使用 scss 全局变量

<!-- src/components/helloworld.vue -->
<script setup lang="ts">
  import variables from "@/styles/variables.module.scss";
  console.log(variables.bgcolor)  
</script>

<template>
  <div style="width:100px;height:100px" :style="{ 'background-color': variables.bgcolor }" />
</template>

整合 unocss

参考:vite 安装 unocss 官方文档

安装依赖

npm install -d unocss

vite.config.ts 配置

// vite.config.ts
import unocss from 'unocss/vite'

export default {
  plugins: [
    unocss({ /* options */ }),
  ],
}

main.ts 引入 uno.css

// src/main.ts
import 'uno.css'

vscode 安装 unocss 插件

再看下具体使用方式和实际效果:

代码效果
image-20230222220856251

如果unocss 插件智能提示不生效,请参考:vscode插件unocss智能提示不生效解决

整合 pinia

参考:pinia 官方文档

安装依赖

npm install pinia

main.ts 引入 pinia

// src/main.ts
import { createpinia } from "pinia";
import app from "./app.vue";

createapp(app).use(createpinia()).mount("#app");

定义 store

根据 pinia 官方文档-核心概念 描述 ,store 定义分为选项式组合式 , 先比较下两种写法的区别:

选项式 option store组合式 setup store

至于如何选择,官方给出的建议 :选择你觉得最舒服的那一个就好

这里选择组合式,新建文件 src/store/counter.ts

// src/store/counter.ts
import { definestore } from "pinia";

export const usecounterstore = definestore("counter", () => {
  // ref变量 → state 属性
  const count = ref(0);
  // computed计算属性 → getters
  const double = computed(() => {
    return count.value * 2;
  });
  // function函数 → actions
  function increment() {
    count.value++;
  }

  return { count, double, increment };
});

父组件

<!-- src/app.vue -->
<script setup lang="ts">
import helloworld from "@/components/helloworld.vue";

import { usecounterstore } from "@/store/counter";
const counterstore = usecounterstore();
</script>

<template>
  <h1 class="text-3xl">vue3-element-admin-父组件</h1>
  <el-button type="primary" @click="counterstore.increment">count++</el-button>
  <helloworld />
</template>

子组件

<!-- src/components/helloworld.vue -->
<script setup lang="ts">
import { usecounterstore } from "@/store/counter";
const counterstore = usecounterstore();
</script>

<template>
  <el-card  class="text-left text-white border-white border-1 border-solid mt-10 bg-[#242424]" >
    <template #header> 子组件 helloworld.vue</template>
    <el-form>
      <el-form-item label="数字:"> {{ counterstore.count }}</el-form-item>
      <el-form-item label="加倍:"> {{ counterstore.double }}</el-form-item>
    </el-form>
  </el-card>
</template>

效果预览

环境变量

参考: vite 环境变量配置官方文档

env配置文件

项目根目录新建 .env.development.env.production

  • 开发环境变量配置:.env.development

    # 变量必须以 vite_ 为前缀才能暴露给外部读取
    vite_app_title = 'vue3-element-admin'
    vite_app_port = 3000
    vite_app_base_api = '/dev-api'
    
  • 生产环境变量配置:.env.production

    vite_app_title = 'vue3-element-admin'
    vite_app_port = 3000
    vite_app_base_api = '/prod-api'
    

环境变量智能提示

新建 src/types/env.d.ts文件存放环境变量ts类型声明

// src/types/env.d.ts
interface importmetaenv {
  /**
   * 应用标题
   */
  vite_app_title: string;
  /**
   * 应用端口
   */
  vite_app_port: number;
  /**
   * api基础路径(反向代理)
   */
  vite_app_base_api: string;
}

interface importmeta {
  readonly env: importmetaenv;
}

使用自定义环境变量就会有智能提示,环境变量的读取和使用请看下一节的跨域处理中的 vite.config.ts的配置。

跨域处理

跨域原理

浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。

本地开发环境通过 vite 配置反向代理解决浏览器跨域问题,生产环境则是通过 nginx 配置反向代理 。

vite.config.ts 配置代理

表面肉眼看到的请求地址: http://localhost:3000/dev-api/api/v1/users/me

真实访问的代理目标地址: http://vapi.youlai.tech/api/v1/users/me

整合 axios

参考: axios 官方文档

安装依赖

npm install axios

axios 工具类封装

//  src/utils/request.ts
import axios, { internalaxiosrequestconfig, axiosresponse } from 'axios';
import { useuserstorehook } from '@/store/modules/user';

// 创建 axios 实例
const service = axios.create({
  baseurl: import.meta.env.vite_app_base_api,
  timeout: 50000,
  headers: { 'content-type': 'application/json;charset=utf-8' }
});

// 请求拦截器
service.interceptors.request.use(
  (config: internalaxiosrequestconfig) => {
    const userstore = useuserstorehook();
    if (userstore.token) {
      config.headers.authorization = userstore.token;
    }
    return config;
  },
  (error: any) => {
    return promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response: axiosresponse) => {
    const { code, msg } = response.data;
    // 登录成功
    if (code === '00000') {
      return response.data;
    }

    elmessage.error(msg || '系统出错');
    return promise.reject(new error(msg || 'error'));
  },
  (error: any) => {
    if (error.response.data) {
      const { code, msg } = error.response.data;
      // token 过期,跳转登录页
      if (code === 'a0230') {
        elmessagebox.confirm('当前页面已失效,请重新登录', '提示', {
          confirmbuttontext: '确定',
          type: 'warning'
        }).then(() => {
          localstorage.clear(); // @vueuse/core 自动导入
          window.location.href = '/';
        });
      }else{
          elmessage.error(msg || '系统出错');
      }
    }
    return promise.reject(error.message);
  }
);

// 导出 axios 实例
export default service;

登录接口实战

访问 vue3-element-admin 在线接口文档, 查看登录接口请求参数和响应数据类型

点击 生成代码 获取登录响应数据 typescript 类型定义

将类型定义复制到 src/api/auth/types.ts 文件中

/**
 * 登录请求参数
 */
export interface logindata {
  /**
   * 用户名
   */
  username: string;
  /**
   * 密码
   */
  password: string;
}

/**
 * 登录响应
 */
export interface loginresult {
  /**
   * 访问token
   */
  accesstoken?: string;
  /**
   * 过期时间(单位:毫秒)
   */
  expires?: number;
  /**
   * 刷新token
   */
  refreshtoken?: string;
  /**
   * token 类型
   */
  tokentype?: string;
}

登录 api 定义

// src/api/auth/index.ts
import request from '@/utils/request';
import { axiospromise } from 'axios';
import { logindata, loginresult } from './types';

/**
 * 登录api 
 * 
 * @param data {logindata}
 * @returns
 */
export function loginapi(data: logindata): axiospromise<loginresult> {
  return request({
    url: '/api/v1/auth/login',
    method: 'post',
    params: data
  });
}

登录 api 调用

// src/store/modules/user.ts
import { loginapi } from '@/api/auth';
import { logindata } from '@/api/auth/types';

/**
 * 登录调用
 *
 * @param {logindata}
 * @returns
 */
function login(logindata: logindata) {
  return new promise<void>((resolve, reject) => {
    loginapi(logindata)
      .then(response => {
        const { tokentype, accesstoken } = response.data;
        token.value = tokentype + ' ' + accesstoken; // bearer eyjhbgcioijiuzi1nij9.xxx.xxx
        resolve();
      })
      .catch(error => {
        reject(error);
      });
  });
}

动态路由

安装 vue-router

npm install vue-router@next

路由实例

创建路由实例,顺带初始化静态路由,而动态路由需要用户登录,根据用户拥有的角色进行权限校验后进行初始化

// src/router/index.ts
import { createrouter, createwebhashhistory, routerecordraw } from 'vue-router';

export const layout = () => import('@/layout/index.vue');

// 静态路由
export const constantroutes: routerecordraw[] = [
  {
    path: '/redirect',
    component: layout,
    meta: { hidden: true },
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },

  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },

  {
    path: '/',
    component: layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'dashboard',
        meta: { title: 'dashboard', icon: 'homepage', affix: true }
      }
    ]
  }
];

/**
 * 创建路由
 */
const router = createrouter({
  history: createwebhashhistory(),
  routes: constantroutes as routerecordraw[],
  // 刷新时,滚动条位置还原
  scrollbehavior: () => ({ left: 0, top: 0 })
});

/**
 * 重置路由
 */
export function resetrouter() {
  router.replace({ path: '/login' });
  location.reload();
}

export default router;

全局注册路由实例

// main.ts
import router from "@/router";

app.use(router).mount('#app')

动态权限路由

路由守卫 src/permission.ts ,获取当前登录用户的角色信息进行动态路由的初始化

最终调用 permissionstore.generateroutes(roles) 方法生成动态路由

// src/store/modules/permission.ts 
import { listroutes } from '@/api/menu';

export const usepermissionstore = definestore('permission', () => {
  const routes = ref<routerecordraw[]>([]);

  function setroutes(newroutes: routerecordraw[]) {
    routes.value = constantroutes.concat(newroutes);
  }
  /**
   * 生成动态路由
   *
   * @param roles 用户角色集合
   * @returns
   */
  function generateroutes(roles: string[]) {
    return new promise<routerecordraw[]>((resolve, reject) => {
      // 接口获取所有路由
      listroutes()
        .then(({ data: asyncroutes }) => {
          // 根据角色获取有访问权限的路由
          const accessedroutes = filterasyncroutes(asyncroutes, roles);
          setroutes(accessedroutes);
          resolve(accessedroutes);
        })
        .catch(error => {
          reject(error);
        });
    });
  }
  // 导出 store 的动态路由数据 routes 
  return { routes, setroutes, generateroutes };
});

接口获取得到的路由数据

根据路由数据 (routes)生成菜单的关键代码

src/layout/componets/sidebar/index.vuesrc/layout/componets/sidebar/sidebaritem.vue
image-20230326145836872

按钮权限

除了 vue 内置的一系列指令 (比如 v-modelv-show) 之外,vue 还允许你注册自定义的指令 (custom directives),以下就通过自定义指令的方式实现按钮权限控制。

参考:vue 官方文档-自定义指令

**自定义指令 **

// src/directive/permission/index.ts

import { useuserstorehook } from '@/store/modules/user';
import { directive, directivebinding } from 'vue';

/**
 * 按钮权限
 */
export const hasperm: directive = {
  mounted(el: htmlelement, binding: directivebinding) {
    // 「超级管理员」拥有所有的按钮权限
    const { roles, perms } = useuserstorehook();
    if (roles.includes('root')) {
      return true;
    }
    // 「其他角色」按钮权限校验
    const { value } = binding;
    if (value) {
      const requiredperms = value; // dom绑定需要的按钮权限标识

      const hasperm = perms?.some(perm => {
        return requiredperms.includes(perm);
      });

      if (!hasperm) {
        el.parentnode && el.parentnode.removechild(el);
      }
    } else {
      throw new error(
        "need perms! like v-has-perm=\"['sys:user:add','sys:user:edit']\""
      );
    }
  }
};

全局注册自定义指令

// src/directive/index.ts
import type { app } from 'vue';

import { hasperm } from './permission';

// 全局注册 directive 方法
export function setupdirective(app: app<element>) {
  // 使 v-hasperm 在所有组件中都可用
  app.directive('hasperm', hasperm);
}
// src/main.ts
import { setupdirective } from '@/directive';

const app = createapp(app);
// 全局注册 自定义指令(directive)
setupdirective(app);

组件使用自定义指令

// src/views/system/user/index.vue
<el-button v-hasperm="['sys:user:add']">新增</el-button>
<el-button v-hasperm="['sys:user:delete']">删除</el-button>

国际化

element plus 国际化

简单的使用方式请参考 element plus 官方文档-国际化示例,以下介绍 vue3-element-admin 整合 pinia 实现国际化语言切换。

element plus 提供了一个 vue 组件 configprovider 用于全局配置国际化的设置。

<!-- src/app.vue -->
<script setup lang="ts">
import { elconfigprovider } from 'element-plus';
import { useappstore } from '@/store/modules/app';
const appstore = useappstore();
</script>

<template>
  <el-config-provider :locale="appstore.locale" >
    <router-view />
  </el-config-provider>
</template>

定义 store

// src/store/modules/app.ts
import { definestore } from 'pinia';
import { usestorage } from '@vueuse/core';
import defaultsettings from '@/settings';

// 导入 element plus 中英文语言包
import zhcn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';

// setup
export const useappstore = definestore('app', () => {
    
  const language = usestorage('language', defaultsettings.language);
    
  /**
   * 根据语言标识读取对应的语言包
   */
  const locale = computed(() => {
    if (language?.value == 'en') {
      return en;
    } else {
      return zhcn;
    }
  });

  /**
   * 切换语言
   */
  function changelanguage(val: string) {
    language.value = val;
  }

  return {
    language,
    locale,
    changelanguage
  };
});

切换语言组件调用

<!-- src/components/langselect/index.vue -->
<script setup lang="ts">
import { usei18n } from 'vue-i18n';
import svgicon from '@/components/svgicon/index.vue';
import { useappstore } from '@/store/modules/app';

const appstore = useappstore();
const { locale } = usei18n();

function handlelanguagechange(lang: string) {
  locale.value = lang;
  appstore.changelanguage(lang);
  if (lang == 'en') {
    elmessage.success('switch language successful!');
  } else {
    elmessage.success('切换语言成功!');
  }
}
</script>

<template>
  <el-dropdown trigger="click" @command="handlelanguagechange">
    <div>
      <svg-icon icon-class="language" />
    </div>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item
          :disabled="appstore.language === 'zh-cn'"
          command="zh-cn"
        >
          中文
        </el-dropdown-item>
        <el-dropdown-item :disabled="appstore.language === 'en'" command="en">
          english
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

element plus 分页组件看下国际化的效果

vue-i18n 自定义国际化

参考:vue-i18n 官方文档 - installation

安装 vue-i18n

npm install vue-i18n@9

自定义语言包

创建 src/lang/package 语言包目录,存放自定义的语言文件

中文语言包 zh-cn.ts英文语言包 en.ts

创建 i18n 实例

// src/lang/index.ts
import { createi18n } from 'vue-i18n';
import { useappstore } from '@/store/modules/app';

const appstore = useappstore();
// 本地语言包
import enlocale from './package/en';
import zhcnlocale from './package/zh-cn';

const messages = {
  'zh-cn': {
    ...zhcnlocale
  },
  en: {
    ...enlocale
  }
};
// 创建 i18n 实例
const i18n = createi18n({
  legacy: false,
  locale: appstore.language,
  messages: messages
});
// 导出 i18n 实例
export default i18n;

i18n 全局注册

// main.ts

// 国际化
import i18n from '@/lang/index';

app.use(i18n).mount('#app');

登录页面国际化使用

<span>{{ $t("login.title") }}</span>

在登录页面 src/view/login/index.vue 查看如何使用

效果预览

暗黑模式

element plus 2.2.0 版本开始支持暗黑模式,启用方式参考 element plus 官方文档 - 暗黑模式, 官方也提供了示例 element-plus-vite-starter 模版

这里根据官方文档和示例讲述 vue3-element-admin 是如何使用 vueuse 的 usedark 方法实现暗黑模式的动态切换。

导入 element plus 暗黑模式变量

// src/main.ts
import 'element-plus/theme-chalk/dark/css-vars.css'

切换暗黑模式设置

<!-- src/layout/components/settings/index.vue -->
<script setup lang="ts">

import iconepsunny from '~icons/ep/sunny';
import iconepmoon from '~icons/ep/moon';

/**
 * 暗黑模式
 */
const settingsstore = usesettingsstore();
const isdark = usedark();
const toggledark = () => usetoggle(isdark);

</script>

<template>
  <div class="settings-container">
    <h3 class="text-base font-bold">项目配置</h3>
    <el-divider>主题</el-divider>

    <div class="flex justify-center" @click.stop>
      <el-switch
        v-model="isdark"
        @change="toggledark"
        inline-prompt
        :active-icon="iconepmoon"
        :inactive-icon="iconepsunny"
        active-color="var(--el-fill-color-dark)"
        inactive-color="var(--el-color-primary)"
      />
    </div>
  </div>
</template>

自定义变量

除了 element plus 组件样式之外,应用中还有很多自定义的组件和样式,像这样的:

应对自定义组件样式实现暗黑模式步骤如下:

新建 src/styles/dark.scss

html.dark {
  /* 修改自定义元素的样式 */   
  .navbar {
    background-color: #141414;
  }
}

在 element plus 的样式之后导入它

// main.ts
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/styles/dark.scss';

效果预览

组件封装

wangeditor 富文本

参考: wangeditor 官方文档

安装 wangeditor

npm install @wangeditor/editor @wangeditor/editor-for-vue@next 

wangeditor 组件封装

<!-- src/components/wangeditor/index.vue -->
<template>
  <div style="border: 1px solid #ccc">
    <!-- 工具栏 -->
    <toolbar
      :editor="editorref"
      :defaultconfig="toolbarconfig"
      style="border-bottom: 1px solid #ccc"
      :mode="mode"
    />
    <!-- 编辑器 -->
    <editor
      :defaultconfig="editorconfig"
      v-model="defaulthtml"
      @onchange="handlechange"
      style="height: 500px; overflow-y: hidden"
      :mode="mode"
      @oncreated="handlecreated"
    />
  </div>
</template>

<script setup lang="ts">
import { onbeforeunmount, shallowref, reactive, torefs } from 'vue';
import { editor, toolbar } from '@wangeditor/editor-for-vue';

// api 引用
import { uploadfileapi } from '@/api/file';

const props = defineprops({
  modelvalue: {
    type: [string],
    default: ''
  }
});

const emit = defineemits(['update:modelvalue']);

// 编辑器实例,必须用 shallowref
const editorref = shallowref();

const state = reactive({
  toolbarconfig: {},
  editorconfig: {
    placeholder: '请输入内容...',
    menu_conf: {
      uploadimage: {
        // 自定义图片上传
        async customupload(file: any, insertfn: any) {
          uploadfileapi(file).then(response => {
            const url = response.data.url;
            insertfn(url);
          });
        }
      }
    }
  },
  defaulthtml: props.modelvalue,
  mode: 'default'
});

const { toolbarconfig, editorconfig, defaulthtml, mode } = torefs(state);

const handlecreated = (editor: any) => {
  editorref.value = editor; // 记录 editor 实例,重要!
};

function handlechange(editor: any) {
  emit('update:modelvalue', editor.gethtml());
}

// 组件销毁时,也及时销毁编辑器
onbeforeunmount(() => {
  const editor = editorref.value;
  if (editor == null) return;
  editor.destroy();
});
</script>

<style src="@wangeditor/editor/dist/css/style.css"></style>

使用案例

<!-- wangeditor富文本编辑器示例 -->
<script setup lang="ts">
import editor from '@/components/wangeditor/index.vue';
const value = ref('初始内容');
</script>

<template>
  <div class="app-container">
    <editor v-model="value" style="height: 600px" />
  </div>
</template>

效果预览

echarts 图表

参考:📊 echarts 官方示例

安装 echarts

npm install echarts

组件封装

<!-- src/views/dashboard/components/chart/barchart.vue --> 
<template>
  <el-card>
    <template #header> 线 + 柱混合图 </template>
    <div :id="id" :class="classname" :style="{ height, width }" />
  </el-card>
</template>

<script setup lang="ts">
import * as echarts from 'echarts';

const props = defineprops({
  id: {
    type: string,
    default: 'barchart'
  },
  classname: {
    type: string,
    default: ''
  },
  width: {
    type: string,
    default: '200px',
    required: true
  },
  height: {
    type: string,
    default: '200px',
    required: true
  }
});

const options = {
  grid: {
    left: '2%',
    right: '2%',
    bottom: '10%',
    containlabel: true
  },
  tooltip: {
    trigger: 'axis',
    axispointer: {
      type: 'cross',
      crossstyle: {
        color: '#999'
      }
    }
  },
  legend: {
    x: 'center',
    y: 'bottom',
    data: ['收入', '毛利润', '收入增长率', '利润增长率'],
    textstyle: {
      color: '#999'
    }
  },
  xaxis: [
    {
      type: 'category',
      data: ['浙江', '北京', '上海', '广东', '深圳'],
      axispointer: {
        type: 'shadow'
      }
    }
  ],
  yaxis: [
    {
      type: 'value',
      min: 0,
      max: 10000,
      interval: 2000,
      axislabel: {
        formatter: '{value} '
      }
    },
    {
      type: 'value',
      min: 0,
      max: 100,
      interval: 20,
      axislabel: {
        formatter: '{value}%'
      }
    }
  ],
  series: [
    {
      name: '收入',
      type: 'bar',
      data: [7000, 7100, 7200, 7300, 7400],
      barwidth: 20,
      itemstyle: {
        color: new echarts.graphic.lineargradient(0, 0, 0, 1, [
          { offset: 0, color: '#83bff6' },
          { offset: 0.5, color: '#188df0' },
          { offset: 1, color: '#188df0' }
        ])
      }
    },
    {
      name: '毛利润',
      type: 'bar',
      data: [8000, 8200, 8400, 8600, 8800],
      barwidth: 20,
      itemstyle: {
        color: new echarts.graphic.lineargradient(0, 0, 0, 1, [
          { offset: 0, color: '#25d73c' },
          { offset: 0.5, color: '#1bc23d' },
          { offset: 1, color: '#179e61' }
        ])
      }
    },
    {
      name: '收入增长率',
      type: 'line',
      yaxisindex: 1,
      data: [60, 65, 70, 75, 80],
      itemstyle: {
        color: '#67c23a'
      }
    },
    {
      name: '利润增长率',
      type: 'line',
      yaxisindex: 1,
      data: [70, 75, 80, 85, 90],
      itemstyle: {
        color: '#409eff'
      }
    }
  ]
};

onmounted(() => {
  // 图表初始化
  const chart = echarts.init(
    document.getelementbyid(props.id) as htmldivelement
  );
  chart.setoption(options);

  // 大小自适应
  window.addeventlistener('resize', () => {
    chart.resize();
  });
});
</script>

组件使用

<script setup lang="ts">
import barchart from './components/barchart.vue';
</script>

<template>
  <barchart id="barchart" height="400px"width="300px" />
</template>

效果预览

图标选择器

组件封装

<!-- src/components/iconselect/index.vue -->
<script setup lang="ts">
const props = defineprops({
  modelvalue: {
    type: string,
    require: false
  }
});

const emit = defineemits(['update:modelvalue']);
const inputvalue = toref(props, 'modelvalue');

const visible = ref(false); // 弹窗显示状态

const iconnames: string[] = []; // 所有的图标名称集合

const filtervalue = ref(''); // 筛选的值
const filtericonnames = ref<string[]>([]); // 过滤后的图标名称集合

const iconselectorref = ref(null);
/**
 * 加载 icon
 */
function loadicons() {
  const icons = import.meta.glob('../../assets/icons/*.svg');
  for (const icon in icons) {
    const iconname = icon.split('assets/icons/')[1].split('.svg')[0];
    iconnames.push(iconname);
  }
  filtericonnames.value = iconnames;
}

/**
 * 筛选图标
 */
function handlefilter() {
  if (filtervalue.value) {
    filtericonnames.value = iconnames.filter(iconname =>
      iconname.includes(filtervalue.value)
    );
  } else {
    filtericonnames.value = iconnames;
  }
}

/**
 * 选择图标
 */
function handleselect(iconname: string) {
  emit('update:modelvalue', iconname);
  visible.value = false;
}

/**
 * 点击容器外的区域关闭弹窗 vueuse onclickoutside
 */
onclickoutside(iconselectorref, () => (visible.value = false));

onmounted(() => {
  loadicons();
});
</script>

<template>
  <div class="iconselect-container" ref="iconselectorref">
    <el-input
      v-model="inputvalue"
      readonly
      @click="visible = !visible"
      placeholder="点击选择图标"
    >
      <template #prepend>
        <svg-icon :icon-class="inputvalue" />
      </template>
    </el-input>

    <el-popover
      shadow="none"
      :visible="visible"
      placement="bottom-end"
      trigger="click"
      width="400"
    >
      <template #reference>
        <div
          @click="visible = !visible"
          class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]"
        >
          <i-ep-caret-top v-show="visible"></i-ep-caret-top>
          <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
        </div>
      </template>

      <!-- 下拉选择弹窗 -->
      <el-input
        class="p-2"
        v-model="filtervalue"
        placeholder="搜索图标"
        clearable
        @input="handlefilter"
      />
      <el-divider border-style="dashed" />

      <el-scrollbar height="300px">
        <ul class="icon-list">
          <li
            class="icon-item"
            v-for="(iconname, index) in filtericonnames"
            :key="index"
            @click="handleselect(iconname)"
          >
            <el-tooltip :content="iconname" placement="bottom" effect="light">
              <svg-icon
                color="var(--el-text-color-regular)"
                :icon-class="iconname"
              />
            </el-tooltip>
          </li>
        </ul>
      </el-scrollbar>
    </el-popover>
  </div>
</template>

组件使用

<!-- src/views/demo/iconselect.vue -->
<script setup lang="ts">
const iconname = ref('edit');
</script>

<template>
  <div class="app-container">
    <icon-select v-model="iconname" />
  </div>
</template>

效果预览

规范配置

代码统一规范

【vue3-element-admin】eslint+prettier+stylelint+editorconfig 约束和统一前端代码规范

git 提交规范

【vue3-element-admin】husky + lint-staged + commitlint + commitizen + cz-git 配置 git 提交规范

启动部署

项目启动

# 安装 pnpm
npm install pnpm -g

# 安装依赖
pnpm install

# 项目运行
pnpm run dev

项目部署

# 项目打包
pnpm run build:prod

生成的静态文件在工程根目录 dist 文件夹

faq

1: defineprops is not defined

  • 问题描述

    ‘defineprops’ is not defined.eslint no-undef

  • 解决方案

    根据 eslint 官方解决方案描述,解析器使用 vue-eslint-parser v9.0.0 + 版本

    安装 vue-eslint-parser 解析器

    npm install -d vue-eslint-parser
    

    .eslintrc.js 关键配置( v9.0.0 及以上版本无需配置编译宏 vue/setup-compiler-macros)如下 :

      parser: 'vue-eslint-parser',
      extends: [
        'eslint:recommended',
    	// ...		
      ],
    

    重启 vscode 已无报错提示

2: vite 首屏加载慢(白屏久)

  • 问题描述

    参考文章:为什么有人说 vite 快,有人却说 vite 慢

    vite 启动时,并不像 webpack 那样做一个全量的打包构建,所以启动速度非常快。启动以后,浏览器发起请求时, dev server 要把请求需要的资源发送给浏览器,中间需要经历预构建、对请求文件做路径解析、加载源文件、对源文件做转换,然后才能把内容返回给浏览器,这个时间耗时蛮久的,导致白屏时间较长。

    解决方案升级 vite 4.3 版本
    https://github.com/vitejs/vite/blob/main/packages/vite/changelog.md

结语

本篇从项目介绍、环境准备、vscode 的代码规范配置 、整合各种框架 、再到最后的启动部署,完整讲述如何基于 vue3 + vite4 + typescript + element plus 等主流技术栈从 0 到 1构建一个企业应用级管理前端框架。

项目有问题建议 issue 或者可以通过项目 关于我们 加入交流群反馈。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com