2024年最新vue3后台模板,vite5-vue3-pinia-antd-unocss

742 阅读11分钟

哈喽,大家好,最近做一个saas项目,自己之前也搭过多个vue3的项目、nuxt3的项目、 h5低代码平台。但也过了很长一段时间了,没有整理,利用这个机会采用最新的技术栈。整理一下,分享给大家。 最新vue3+vite5+pinia+unocss+antdesign vue+husky+eslint+lint-staged+prettier+commitlint搭建的后台管理系统 git源码仓库地址

新增npm插件地址vue3-antd-icons-picker

技术栈选型:

打包编译:vite

包管理:pnpm

ui框架: Ant Design Vue

状态管理:pinia

css: UnoCss

icones:icones

代码规范:eslint husky lint-staged prettier+commitlint

ui框架在项目中用过很多,elementiviewnaive-uiantd, 就ui风格上antd符合我的审美,功能也比较齐全,其次naive-ui也是很好用的一个,这次我选择antd

css为什么没选择Tailwind CSS,而采用Unocss,我们建议您阅读UnoCSS 创建者Anthony Fu 撰写的博客文章《Reimagine Atomic CSS》,以便更好地理解 UnoCSS 背后的动机。

安装环境

node版本18+或者20。我的版本是v18.18.0。

mac系统用homebrew安装环境

node版本管理工具:mac用 n,windows是用nvm,镜像源使用淘宝的。

  1. 创建项目my-vue-app修改成自己的项目名字

pnpm create vite

image.png 按照提示选择vue

image.png 选择Customize with create-vue

image.png 自己选择是否需要TypeScript,我这选择否,其他的就自行选择即可

image.png 按照命令提示进入目录初始化安装

安装husky

pnpm add husky -D

执行npx husky init初始化,它会在 .husky/ 中创建 pre-commit 脚本,并更新 package.json 中的 prepare 脚本。

npx husky init

package.json中加入以下代码

{
...
"husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }
}

安装lint-staged

pnpm add lint-staged -D

在项目根目录下创建lint.staged.config.cjs文件

// lint.staged.config.cjs
module.exports = {
  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
  'package.json': ['prettier --write'],
  '*.vue': ['eslint --fix', 'prettier --write'],
  '*.{scss,less,styl,html}': ['prettier --write'],
  '*.md': ['prettier --write']
}

package.json中加入以下代码

{
    ...
    "lint-staged": {
    "*.{js,jsx,ts,tsx,vue}": [
      "prettier --write",
      "eslint --cache --fix",
      "git add"
    ],
    "*.{vue,css,less,scss}": [
      "prettier --write"
    ],
    "*.{json,html,yml,md}": [
      "prettier --write"
    ]
  }
}

安装commitlint

pnpm add @commitlint/cli @commitlint/config-conventional -D

在根目录下新建commitlint.config.cjs文件

module.exports = {
  extends: ['@commitlint/config-conventional', 'cz'],
  prompt: {
    questions: [
      {
        type: 'input',
        name: 'subject',
        message: '请输入commit信息',
        validate: function (value) {
          if (value.length) {
            return true
          } else {
            return '请输入commit信息'
          }
        }
      }
    ],
    types: [
      { value: 'feat', name: '✨ feat:     新功能', emoji: ':sparkles:' },
      { value: 'fix', name: '🐛 fix:      修复bug', emoji: ':bug:' },
      { value: 'docs', name: '✏️  docs:     文档变更', emoji: ':memo:' },
      { value: 'style', name: '💄 style:    代码的样式美化', emoji: ':lipstick:' },
      { value: 'refactor', name: '♻️  refactor: 重构', emoji: ':recycle:' },
      { value: 'perf', name: '⚡️ perf:     性能优化', emoji: ':zap:' },
      { value: 'release', name: '🎉 release:  发布正式版', emoji: ':tada:' },
      { value: 'test', name: '✅ test:     测试', emoji: ':white_check_mark:' },
      { value: 'build', name: '📦️ build:    打包', emoji: ':package:' },
      { value: 'ci', name: '👷 ci:       CI相关更改', emoji: ':ferris_wheel:' },
      { value: 'chore', name: '🚀 chore:    构建/工程依赖/工具', emoji: ':hammer:' },
      { value: 'revert', name: '⏪️ revert:   回退', emoji: ':rewind:' }
    ],
    useEmoji: true
  }
}

修改pre-commit

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 pre-commit"
echo "npx --no-install lint-staged"
npx --no-install lint-staged

image.png

执行下面命令

npx husky add .husky/commit-msg 'npx --no-install commitlint -e "$HUSKY_GIT_PARAMS"'

echo "npx --no -- commitlint --edit $1" > .husky/commit-msg

package.json中husky的配置,增加commit-msg配置

"husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint --config commitlint.config.cjs -E HUSKY_GIT_PARAMS"
    }
}

在.husky目录下会创建commit-msg并写入以下代码

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 pre-commit"
echo "npx --no-install lint-staged"
npx --no-install lint-staged

image.png

安装 cz 建议全局安装下

pnpm install -g commitizen  
pnpm install -g cz-conventional-changelog

mac执行

echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

windows执行

echo { "path": "cz-conventional-changelog" } > C:\Users\12105.czrc
commitizen init cz-conventional-changelog --save --save-exact

项目下安装 commitizen

pnpm install commitizen cz-conventional-changelog -D

package.json中增加命令

"scripts": {
    ...
    "lint:lint-staged": "lint-staged",
    "prepare": "husky install",
    "prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
    "lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
    "commit": "git add . && git cz",
    "commitlint": "commitlint --edit"
  }

安装 cz-customizable

自动化选择提交commit

pnpm add cz-customizable commitlint-config-cz -D

在根目录下创建.cz-config.cjs

module.exports = {
  types: [
    {
      value: 'feat',
      name: '✨ feat:     新功能'
    },
    {
      value: 'fix',
      name: '🐛 fix:      修复bug'
    },
    {
      value: 'build',
      name: '📦️ build:    打包'
    },
    {
      value: 'perf',
      name: '⚡️ perf:     性能优化'
    },
    {
      value: 'release',
      name: '🎉 release:  发布正式版'
    },
    {
      value: 'style',
      name: '💄 style:    代码的样式美化'
    },
    {
      value: 'refactor',
      name: '♻️  refactor: 重构'
    },
    {
      value: 'docs',
      name: '✏️  docs:     文档变更'
    },
    {
      value: 'test',
      name: '✅ test:     测试'
    },
    {
      value: 'revert',
      name: '⏪️ revert:   回退'
    },
    {
      value: 'chore',
      name: '🚀 chore:    构建/工程依赖/工具'
    },
    {
      value: 'ci',
      name: '👷 ci:       CI相关更改'
    }
  ],
  messages: {
    type: '请选择提交类型(必填)',
    customScope: '请输入文件修改范围(可选)',
    subject: '请简要描述提交(必填)',
    body: '请输入详细描述(可选)',
    breaking: '列出任何BREAKING CHANGES(可选)',
    footer: '请输入要关闭的issue(可选)',
    confirmCommit: '确定提交此说明吗?'
  },
  allowCustomScopes: true,
  // 跳过问题
  skipQuestions: ['body', 'footer'],
  subjectLimit: 72
}

package.json新增

{
    ...
  "config": {
    "commitizen": {
      "path": "./node_modules/cz-customizable"
    },
    "cz-customizable": {
      "config": ".cz-config.cjs"
    }
  }
}

安装 eslint-plugin-simple-import-sort 解决依赖引入排序

修改.eslintrc.cjs文件

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  plugins: ['simple-import-sort'],
  rules: {
    'simple-import-sort/imports': 'error',
    'simple-import-sort/exports': 'error'
  },
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-prettier/skip-formatting'
  ],
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 'latest'
  },
  globals: {
    process: true,
    module: true
  }
}

自此代码规范就完成了。让我们测试下提交效果,执行

pnpm commit

image.png

这样看起来就舒服多了。选择修改的类型,输入msg提交就可以了,不按这规范是提交不成功的,eslint格式校验没通过也提交不了代码。这样对代码质量控制就有保障了。

image.png 必须按规范提交才会成功

image.png 这里的警告是说提交的时候不要加命令 git add,最好是手动add。可以去掉。

安装 ant-design-vue 我选择按需引入

pnpm add ant-design-vue@4.x unplugin-vue-components dayjs -D

修改vite.config.js

import { fileURLToPath, URL } from 'node:url'

import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    vueDevTools(),
    Components({
      resolvers: [
        AntDesignVueResolver({
          importStyle: false // css in js
        })
      ]
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

安装UnoCss

pnpm add unocss -D

在main.js(main.ts)中增加

// main.js
import 'virtual:uno.css'

安装 自定义svg图标和preset-icons预设

pnpm add @unocss/preset-uno @unocss/preset-icons @iconify/iconify @iconify/utils vite-plugin-purge-icons vite-plugin-svg-icons -D

这里@iconify/iconify可以不安装,可以只安装指定的图标库如:@iconify-json/mdi看下面的uno.config.js的方法

安装(可选) preset-rem-to-px

pnpm add @unocss/preset-rem-to-px -D

修改vite.config.js

import { fileURLToPath, URL } from 'node:url'

import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
import UnoCSS from 'unocss/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    vueDevTools(),
    UnoCSS(),
    Components({
      resolvers: [
        AntDesignVueResolver({
          importStyle: false // css in js
        }),
        IconsResolver({
          // prefix: 'icon', // 自动引入的Icon组件统一前缀,默认为 i,设置false为不需要前缀
          // {prefix}-{collection}-{icon} 使用组件解析器时,您必须遵循名称转换才能正确推断图标。
          // alias: { park: 'icon-park' } 集合的别名
          enabledCollections: ['ep'] // 这是可选的,默认启用 Iconify 支持的所有集合['mdi']
        })
      ]
    }),
    Icons({ autoInstall: true, compiler: 'vue3' }),
    createSvgIconsPlugin({
      iconDirs: [resolve(process.cwd(), 'src/assets/svg')]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      assets: fileURLToPath(new URL('./src/assets', import.meta.url)),
      components: fileURLToPath(new URL('./src/components', import.meta.url)),
      config: fileURLToPath(new URL('./src/config', import.meta.url)),
      utils: fileURLToPath(new URL('./src/utils', import.meta.url)),
      service: fileURLToPath(new URL('./src/service', import.meta.url)),
      views: fileURLToPath(new URL('./src/views', import.meta.url)),
      store: fileURLToPath(new URL('./src/stores', import.meta.url)),
      constants: fileURLToPath(new URL('./src/constants', import.meta.url))
    },
    extensions: ['.js', '.vue', '.json', '.scss', '*']
  }
})

修改uno.config.js,这collections就可以配置需要的图标集,使用的话会自动下载对应的依赖。 customizations这个处理svg相关配置。颜色,大小等属性。可以配置props,如{width:2em,height:2em}等配置。详细文档建议大家看下官网的,官网很详细了。

import presetRemToPx from '@unocss/preset-rem-to-px'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import { defineConfig, presetAttributify, presetIcons, presetTypography, presetUno } from 'unocss'

export default defineConfig({
  shortcuts: {
    'bg-base': 'bg-[#f0f0f0] dark:bg-[#20202a]',
    'card-base': 'bg-[#ffffff] dark:bg-[#1a1a1a]',
    // 这里定义你自己的一些组合变量
  },
  presets: [
    presetUno(),
    presetTypography(),
    presetAttributify(),
    presetRemToPx({
      baseFontSize: 4 // 默认16
    }),
    presetIcons({
      extraProperties: {
        display: 'inline-block',
        'vertical-align': 'middle'
      },
      customizations: {
        transform(svg, collection, icon) {
          // do not apply fill to this icons on this collection
          if (collection === 'custom' && icon === 'my-icon') return svg
          return svg.replace(/#fff/, 'currentColor')
        },
        custom: FileSystemIconLoader('./src/assets/svg', (svg) =>
          svg.replace(/#fff/, 'currentColor')
        )
      },
      collections: {
        carbon: () => import('@iconify-json/carbon/icons.json').then((i) => i.default),
        mdi: () => import('@iconify-json/mdi/icons.json').then((i) => i.default),
        antd: () => import('@iconify-json/ant-design/icons.json').then((i) => i.default)
      }
    })
  ]
})

src/assets目录下创建一个svg的目录,存放自己的svg图标

image.png 修改main.js增加配置

...
import 'virtual:svg-icons-register'

使用方式

<div class="i-custom:logo color-primary"></div>
or
# presetAttributify安装后可以这么使用
<div i-custom:logo color-primary></div>

image.png

image.png

安装pinia持久化pinia-plugin-persistedstate

pnpm add pinia-plugin-persistedstate -D

pinia支持option和setup的语法可以看官方文档,选择自己喜欢的就行 如option写法

image.png

setup写法

image.png 单独设置pinia持久化,可以定义一个getStoreKey获取不同环境存储的变量。paths是可以指定缓存的字段。所有需要缓存的话,则可以直接persist:true即可。

{
persist: {
    // 修改存储中使用的键名称,默认为当前 Store的 id
    key: getStoreKey('user'),
    // 修改为 sessionStorage,默认为 localStorage
    // storage: window.sessionStorage,
    // 部分持久化状态的点符号路径数组,[]意味着没有状态被持久化(默认为undefined,持久化整个状态)
    paths: ['openRouteList', 'userInfo', 'currentRouteName', 'token', 'permissions']
  }
}

还可以支持插件比如做undo,redo这样的插件统一处理,这里就不展开讲了。 可以统一导出到index中

image.png 使用如下

<script setup>
import useStore from 'store' // 这是在vite.config.js设置别名了哦
import { storeToRefs } from 'pinia'
const { common, user } = useStore()
// 解构且保持响应式
const { locale, i18nLocal, theme } = storeToRefs(common)
// 这可以使用local这些了
// 调用方法可以
user.login()
// 当然也可以这样访问store
user.token
</script>

修改main.js增加配置

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'

const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)

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

安装Vueuse 强烈推荐

pnpm add @vueuse/core -D

配置 axios

pnpm add axios -D

在src下创建service目录,新建index.js

image.png

image.png 统一处理错误

/**
 * 请求失败后的错误统一处理
 * @param {Number} status 请求失败的状态码
 */
const errorHandle = (status, msg) => {
  // 状态码判断
  switch (status) {
    case 302:
      message.error(msg || '接口重定向了!')
      break
    case 400:
      message.error(msg || '发出的请求有错误,服务器没有进行新建或修改数据的操作')
      break
    // 401: 未登录
    // 未登录则跳转登录页面,并携带当前页面的路径
    // 在登录成功后返回当前页面,这一步需要在登录页操作。
    case 401: //重定向
      message.error(msg || '登录失效')
      router.replace({
        path: '/login'
      })
      break
    // 403 token过期
    // 清除token并跳转登录页
    case 403:
      message.error(msg || '登录过期,用户得到授权,但是访问是被禁止的')
      // store.commit('token', null);
      setTimeout(() => {
        router.replace({
          path: '/login'
        })
      }, 1000)
      break
    case 404:
      message.error(msg || '网络请求不存在')
      break
    case 406:
      message.error(msg || '请求的格式不可得')
      break
    case 408:
      message.error(msg || ' 请求超时!')
      break
    case 410:
      message.error(msg || '请求的资源被永久删除,且不会再得到的')
      break
    case 422:
      message.error(msg || '当创建一个对象时,发生一个验证错误')
      break
    case 500:
      message.error(msg || '服务器发生错误,请检查服务器')
      break
    case 502:
      message.error(msg || '网关错误')
      break
    case 503:
      message.error(msg || '服务不可用,服务器暂时过载或维护')
      break
    case 504:
      message.error(msg || '网关超时')
      break
    default:
      message.error(msg || '其他错误错误')
  }
}

移除重复请求

// 移除重复请求
const removePending = (config) => {
  for (const key in pending) {
    const item = +key
    const list = pending[key]
    // 当前请求在数组中存在时执行函数体
    if (
      list.url === config.url &&
      list.method === config.method &&
      JSON.stringify(list.params) === JSON.stringify(config.params) &&
      JSON.stringify(list.data) === JSON.stringify(config.data)
    ) {
      // 执行取消操作
      list.cancel('操作太频繁,请稍后再试')
      // 从数组中移除记录
      pending.splice(item, 1)
    }
  }
}

新建request.js 导出get,post,del,put请求

image.png

新建api.js,各个业务的请求放constants目录导出

image.png

安装vue-i18n

pnpm add vue-i18n -D

在项目src下创建languages目录 下面新建对应的语言json和导出js文件

image.png 例如:获取菜单请求如图 image.png

修改languages/index.js

import { createI18n } from 'vue-i18n'
//引入同级目录下文件
const modules = import.meta.glob('./*', { eager: true })
//假设你还有其他目录下的语言文件 它的路径是 src/views/home/locales/en-US.json
//那么你就可以 使用 :lower:(小写) :upper:(大写) 来引入文件
// const viewModules = import.meta.globEager(
//   "../views/**/locales/[[:lower:]][[:lower:]]-[[:upper:]][[:upper:]].json"
// )

function getLangAll() {
  let message = {}
  getLangFiles(modules, message)
  // getLangFiles(viewModules, message)
  return message
}
/**
 * 获取所有语言文件
 * @param {Object} mList
 */
function getLangFiles(mList, msg) {
  for (let path in mList) {
    if (mList[path].default) {
      //  获取文件名
      let pathName = path.substr(path.lastIndexOf('/') + 1, 5)

      if (msg[pathName]) {
        msg[pathName] = {
          ...mList[pathName],
          ...mList[path].default
        }
      } else {
        msg[pathName] = mList[path].default
      }
    }
  }
}

//注册i8n实例并引入语言文件
const i18n = createI18n({
  legacy: false,
  locale: 'zh-CN',
  messages: getLangAll()
})

export default i18n //将i18n暴露出去,在main.js中引入挂载

image.png

image.png

image.png


import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'

import { routers } from '@/router/modules/default
...
const { common, user } = useStore()
const { locale, i18nLocal, theme } = storeToRefs(common)
const { userInfo } = storeToRefs(user)
const router = useRouter()
const route = useRoute()
const i18n = useI18n({ useScope: 'global' })
const defaultRoute = route.name ? route.name : 'analysis'
dayjs.locale(locale)
i18n.locale.value = i18nLocal.value

image.png

image.png

需要多语言的就在语言文件中配置对应的变量,多语言就完成了。

theme跟随系统主题变化

这里用到了matchMedia,监听则可以跟随系统主题自动切换。逻辑就是更改root上的var变量和prefers-color-scheme,更改对应主题的变量 image.png

image.png

菜单 icon配置

antd的icon是可支持自定义的svg,我们处理这个菜单icon的时候,只需要保存@iconify/iconify上对应的class即可。再写一个对应的@iconify/iconify的svg解析组件。可以定义自己的可配置图标集数据。

image.png 如使用antd的icon的话,如图所示。

image.png

这里我们需要用到vite的vite-plugin-svg-icons

import svgIcons from 'virtual:svg-icons-names'
...
function getSvgIcons() {
  return svgIcons.map((icon) => icon.replace('icon-', ''))
}
function getIcons() {
  const prefix = iconsData.prefix
  return iconsData.icons.map((icon) => `${prefix}:${icon}`)
}
// 如果菜单直接使用的svg如antd的Icon则使用getSvgIcons,如果使用的iconify的css图标。那则直接使用

新建一个Icon的组件,这里我们可以是使用iconify的渲染方法renderSVG

import { renderSVG } from '@iconify/iconify'

image.png

效果图

image.png

image.png

image.png

image.png

image.png

image.png