一、vite命令行创建项目
npm create vite@latest
根据提示选择模板,选择vite + vue3 + ts即可。
二、项目连接远程仓库
git init
git remote add origin https://gitee.***/niech_project/vite-vue3-template.git
git pull origin master
git checkout -b dev
三、项目加入eslint校验和自动格式化
- eslint 运行代码前就可以发现一些语法错误和潜在bug,保证代码一致性。
- prettier 是代码格式化工具,用于检查代码中的格式问题。
- 区别联系:eslint保证代码质量,prettier保证代码风格,eslint有小部分格式化功能,通常和prettier结合使用。
1. 安装eslint和prettier
- eslint: ESLint的核心代码库
- prettier:prettier格式化代码的核心库
- eslint-config-airbnb-base: airbnb的代码规范 (依赖plugin-import)
- eslint-config-prettier:eslint结合prettier的格式化
- eslint-plugin-vue:eslint在vue里的代码规范
- eslint-plugin-import:项目里面支持eslint
- eslint-plugin-prettier:将prettier结合进入eslint的插件
pnpm install eslint eslint-plugin-vue eslint-config-prettier prettier eslint-plugin-import eslint-plugin-prettier eslint-config-airbnb-base -D
:::tip
npm i module_name -D
-D 表示安装模块到开发依赖管理devDependencies中,如果你安装的库是用来打包的、解析代码的,比如vite、webpack、babel,就可以用 -d 来安装,项目上线了,这些库就没用了。也比如saas
-S 表示安装模块到生产依赖管理dependencies中,这个是项目运行需要用到的依赖,比如vue、eslint、vuex、axios,就用 -s 来安装。
:::
npm安装模块
【npm install xxx】利用 npm 安装xxx模块到当前命令行所在目录;
【npm install -g xxx】利用npm安装全局模块xxx;
【npm install xxx】安装但不写入package.json;
【npm install xxx –save】 安装并写入package.json的“dependencies”中;
【npm install xxx –save-dev】安装并写入package.json的“devDependencies”中。
npm 删除模块
【npm uninstall/remove xxx 】删除xxx模块;
【npm uninstall/remove -g xxx】删除全局模块xxx;
2. 配置eslint和prettier
- 在package.json文件scripts加入命令
"lint:create":"eslint --init"
执行npm run lint:create,自动创建.eslintrc.cjs文件
:::tip
补充一些依赖安装
@typescript-esTint/parser: ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码;
@typescript-eslint/eslint-plugin: 这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范
eslint-import-resolver-alias 让我们可以import的时候使用 @ 别名
:::
pnpm install typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-import-resolver-alias @types/eslint @types/node -D
- 修改vue.config.ts文件
pnpm install vite-plugin-eslint consola terser -D
import { defineConfig,loadEnv } from 'vite'
import path from "path";
import vue from '@vitejs/plugin-vue'
import eslintPlugin from "vite-plugin-eslint";
export default defineConfig(({ mode }) => {
return {
plugins: [
vue(),
eslintPlugin()//代码校检
],
base: "./", // 在生产中服务时的基本公共路径
publicDir: "public", // 静态资源服务的文件夹, 默认"public"
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), // 相对路径别名配置,使用 @ 代替 src
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
},
// 打包配置
build: {
target: "modules", // 设置最终构建的浏览器兼容目标。modules:支持原生 ES 模块的浏览器
outDir: "dist", // 指定输出路径
assetsDir: "assets", // 指定生成静态资源的存放路径
assetsInlineLimit: "4096", // 小于此阈值的导入或引用资源将内联为base64编码,设置为0可禁用此项。默认4096(4kb)
cssCodeSplit: true, // 启用/禁用CSS代码拆分,如果禁用,整个项目的所有CSS将被提取到一个CSS文件中,默认true
sourcemap: false, // 构建后是否生成 source map 文件
minify: "terser", // 混淆器,terser构建后文件体积更小
write: true, // 设置为 false 来禁用将构建后的文件写入磁盘
emptyOutDir: true, // 默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录。
chunkSizeWarningLimit: 500, // chunk 大小警告的限制
terserOptions: {
***press: {
drop_console: true,
drop_debugger: true,
},
}, // 去除 console debugger
}
}
})
vite-plugin-eslint: vite的一个插件,让项目可以方便的得到eslint支持,完成eslint配置后,可以快速的将其集成进vite中,便于在代码不符合eslint规范的第一时间看到提示
3. 修改添加常见配置
项目根目录创建以下配置文件
- .eslintrcignore 忽略校验文件
# .eslintrcignore
*.sh
node_modules
*.md
*.woff
*.ttf
dist
/pubilc
/docs
.husky
/bin
.eslintrc.js
perttier.config.js
/src/mock/*
/src/*
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
lerna-debug.log*
.DS_Store
dist-ssr
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
***ponents.d.ts
- .prettierrc.cjs 配置格式化规则
module.exports = {
"printWidth" : 80, // 一行最多100字符
"tabWidth": 2, // 使用2个空格缩进
"useTabs": false, // 不适用缩进符,使用空格
"semi": false, // 行尾是否要有分号
"singleQuote": true, // 使用单引号
"quoteProps": 'as-needed', // 对象的key,仅仅在必要时使用引号
"jsxSingleQuote": false, // jsx是否使用双引号
"trailing***ma": 'es5', // 尾随逗号
"bracketSpacing": true, // 大括号内的首尾需要有空格
"arrowParens": 'always', // 箭头函数,是有一个参数的时候,也需要小括号
"rangeStart": 0, // 文件格式化的范围是全部内容
"rangeEnd": Infinity,
"requirePragma": false, // 不需要写文件开头的 @prettier
"insertPragma": false, // 不需要在文件开头插入 @prettier
"proseWrap": 'always', // 使用默认执行标准
"htmlWhitespaceSensitivity": 'css', // 根据显示样式决定html是否执行
"endOfLine": 'lf' // 换行符是有lf
}
- .prettierignore 忽略格式化文件
# prettierignore
/dist/*
.local
.output.js
/node_modules/**
src/.DS_Store
**/*.svg
**/*.sh
/public/*
***ponents.d.ts
在package.json添加格式化命令
- “lint”: “eslint “src/**/*.{js,ts,vue}” --fix”, 既可以检查又可以修复部分语法问题
- “prettier-format”: “prettier --config .prettierrc.cjs “src/**/*.{js,ts,vue}” --write”, 利用prettier手动格式化一些样式问题
四、修改tsconfig.json配置别名
"baseUrl": "",
"paths": {
"@/*":["src/*"]
}
五、环境配置(开发,预发,生产环境)
开发环境:开发人员开发的环境
测试环境:测试人员测试的环境
预发环境:准备上线的环境,也可叫内测环境
生产环境:正式线上环境,投入生产的环境
这里我们配置两个环境,一个测试环境和生产环境,
开发人员和测试人员使用测试环境,修改package.json文件,添加两个命令
“build:dev”: “vue-tsc --noEmit && vite build --mode development”,
“build:pro”: “vue-tsc --noEmit && vite build --mode production”,
新建两个配置文件
.env.development:开发测试环境
# 页面标题
VITE_APP_TITLE = xxx
# 开发环境配置
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = '/stage-api'
# 是否启用代理
VITE_HTTP_PROXY = true
# 端口
VITE_PORT = 80
#本地环境接口地址
VITE_SERVE = 'http://192.168.xx.xx:8080'
.env.production:生产环境
# 页面标题
VITE_APP_TITLE = xxx
# 生产环境配置
VITE_APP_ENV = 'production'
# 开发环境
VITE_APP_BASE_API = '/stage-api'
# 是否启用代理
VITE_HTTP_PROXY = false
# 端口
VITE_PORT = 80
#本地环境接口地址
VITE_SERVE = 'http://xx.xx.xx.xx:8080'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_***PRESS = gzip
六、自动引入插件unplugin-auto-import和unplugin-vue-***ponents的使用
:::tip
vue3日常项目中定义变量需要引入ref,reactive等等比较麻烦,可以通过unplugin-auto-import给我们自动引入
:::
- 安装依赖
npm install -D unplugin-vue-***ponents unplugin-auto-import vite-plugin-style-import
npm install element-plus @element-plus/icons-vue ant-design-vue -S
- 在vite.config.ts文件中添加配置
import { defineConfig, loadEnv } from "vite";
import path from "path";
import vue from "@vitejs/plugin-vue";
import eslintPlugin from "vite-plugin-eslint";
import AutoImport from "unplugin-auto-import/vite";
import ***ponents from "unplugin-vue-***ponents/vite";
import { ElementPlusResolver } from "unplugin-vue-***ponents/resolvers";
import {
createStyleImportPlugin,
ElementPlusResolve
} from "vite-plugin-style-import";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
const { VITE_APP_BASE_API,VITE_SERVE,VITE_PORT } = env
return {
plugins: [
vue(),
eslintPlugin(),
AutoImport({
imports: ["vue", "vue-router"],
dts: "src/auto-import.d.ts", // 路径下自动生成文件夹存放全局指令
eslintrc: {
enabled: true, // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
},
resolvers: [ElementPlusResolver()]
}),
***ponents({
dts: "./src/***ponents.d.ts", // 创建ts文件
extensions: ["vue"], // 指定文件的后缀
dirs: ["src/***ponents/"], // 指定路径,自动导入自定义组件
resolvers: [ElementPlusResolver()], // 指定自动引入的组件库,也就是从插件中导出的那个
}),
// 配置自动导入element start
createStyleImportPlugin({
resolves: [ElementPlusResolve()],
libs: [
{
libraryName: "element-plus",
esModule: true,
resolveStyle: (name: string) =>
`element-plus/theme-chalk/${name}.css`,
},
],
})
],
base: "./", // 在生产中服务时的基本公共路径
publicDir: "public", // 静态资源服务的文件夹, 默认"public"
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), // 相对路径别名配置,使用 @ 代替 src
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
},
// 本地运行配置
server: {
host: '0.0.0.0',
port: VITE_PORT || 80,
https: false,
open: true,
proxy: {
[VITE_APP_BASE_API]: {
target: VITE_SERVE,
changeOrigin: true,
rewrite: path => path.replace(RegExp(`^${VITE_APP_BASE_API}`), '')
}
},
disableHostCheck: true
},
// 打包配置
build: {
target: "modules", // 设置最终构建的浏览器兼容目标。modules:支持原生 ES 模块的浏览器
outDir: "dist", // 指定输出路径
assetsDir: "assets", // 指定生成静态资源的存放路径
assetsInlineLimit: "4096", // 小于此阈值的导入或引用资源将内联为base64编码,设置为0可禁用此项。默认4096(4kb)
cssCodeSplit: true, // 启用/禁用CSS代码拆分,如果禁用,整个项目的所有CSS将被提取到一个CSS文件中,默认true
sourcemap: false, // 构建后是否生成 source map 文件
minify: "terser", // 混淆器,terser构建后文件体积更小
write: true, // 设置为 false 来禁用将构建后的文件写入磁盘
emptyOutDir: true, // 默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录。
chunkSizeWarningLimit: 500, // chunk 大小警告的限制
terserOptions: {
***press: {
drop_console: true,
drop_debugger: true,
},
}, // 去除 console debugger
},
};
});
-
在src目录下自动生成auto-import.d.ts文件,用于存放全局指令
-
和eslintrc不兼容时,加上
eslintrc: {
enabled: true, // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
}
自动生成.eslintrc-auto-import.json文件
5. 在.eslintrc.json文件中添加如下配置:
module.exports = {
// 环境 浏览器,最新ES语法,node环境
"env": {
"browser": true,
"***monjs": true,
"es2021": true
},
/**
* 扩展的eslint规范语法,可以被继承的规则,字符串数组,每个配置继承它之前的配置
* 分别是eslint-config-vue 提供的
* eslint-config-airbnb-base 提供的
* eslint-config-prettier 提供的
* eslint-config- 前缀可以简写
*/
"extends": [
"eslint:re***mended",
"plugin:@typescript-eslint/re***mended",
"plugin:vue/vue3-essential",
"airbnb-base",
"prettier",
"./.eslintrc-auto-import.json"
],
// eslint 会对代码进行校验,parser是将代码转换为ESTree(AST),ESlint会对ESTree校验
"parser": "vue-eslint-parser",
// 解析器的配置项
"parserOptions": {
// eslint的版本号,或者年份都可以
"ecmaVersion": "latest",
"parser": "@typescript-eslint/parser",
"sourceType": "module",
// 额外的语言类型
"ecmaFeatures": {
"jsx": true,
"tsx": true
}
},
// 全局自定义宏,这样在源文件中使用全局变量不会报错或警告
"globals": {
"defineProps": "readonly",
"defineEmits": "readonly",
"defineExpose": "readonly",
"withDefaults": "readonly"
},
/**
* 插件
* eslint-plugin- 前缀可以简写
* vue官方提供了一个eslint插件eslint-plugin-vue,它提供了parser和rules。
* parser为vue-eslint-parser,放在前面的parser字段里,rules放在extends字段里
*/
"plugins": [
"@typescript-eslint",
"vue"
],
"settings": {
// 设置项目内的别名
"import/resolver": {
"alias": {
"map": [["@","./src"]]
}
},
"import/extensions": [".js",".jsx",".tsx",".ts",".mjs",".cjs"]
},
/**
* rules: 自定义规则,覆盖extends继承的规则,对规则进行灵活配置
*
* "off" 或 0 ==> 关闭规则
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
*/
"rules": {
// eslint(https://eslint.bootcss.***/docs/rules/)
"no-var": "error", // 要求使用 let 或 const 而不是 var
"no-multiple-empty-lines": ["warn", { "max": 2 }], // 不允许多个空行
"no-console": "off",
"no-debugger": "error",
"no-unexpected-multiline": "error", // 禁止空余的多行
"no-useless-escape": "off", // 禁止不必要的转义字符
"import/no-unresolved": "off",
"import/extensions": "off",
"import/no-absolute-path": "off",
"import/no-extraneous-dependencies": "off",
"import/prefer-default-export": "off",
// typeScript (https://typescript-eslint.io/rules)
"@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量
"@typescript-eslint/prefer-ts-expect-error": "error", // 禁止使用 @ts-ignore
"@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。
"@typescript-eslint/semi": "off",
// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
"vue/multi-word-***ponent-names": "off", // 要求组件名称始终为 “-” 链接的单词
"vue/script-setup-uses-vars": "error", // 防止<script setup>使用的变量<template>被标记为未使用
"vue/no-mutating-props": "off", // 不允许组件 prop的改变
"vue/attribute-hyphenation": "off",// 对模板中的自定义组件强制执行属性命名样式
// self
"no-param-reassign":"off",// 不允许参数重新赋值
"no-useless-concat":"off",
"no-plusplus":"off",// 不允许一元操作符++、--
}
}
- tsconfig.json文件中添加如下配置:
include加入"src/auto-imports.d.ts"
- 安装 element-plus
npm install element-plus -S
在.eslintrc-auto-import.json文件中添加需要引入的组件:
“ElMessage”: true
- 安装saas
npm install sass -D
七、添加路由
- pnpm install vue-router 安装路由依赖;
- 在src目录下新建router文件夹index.ts文件和routes.ts文件;
index.ts内容如下
// 通过vue-router插件实现模板路由配置
import { createRouter, createWebHistory } from "vue-router";
import { constantRoutes } from "./routes.ts";
// 创建路由器
const router = createRouter({
// 路由模式hash
history: createWebHistory(),
routes: constantRoutes,
// 滚动行为
scrollBehavior() {
return {
left: 0,
top: 0,
};
},
});
export default router;
routes.ts内容如下
import Layout from '@/layout'
/**
* Note: 路由配置项
*
* hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
* alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
* // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
* // 若你想不管路由下面的 children 声明的个数都显示你的根路由
* // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', '***mon'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : {
noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
}
*/
export const constantRoutes = [
{
path: '/redirect',
***ponent: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
***ponent: () => import('@/views/redirect/index.vue')
}
]
},
{
path: '',
***ponent: Layout,
redirect: '/index',
children: [
{
path: '/index',
***ponent: () => import('@/views/home/index.vue'),
name: 'Index',
meta: { title: '系统首页', icon: 'home', affix: true }
}
]
},
{
path: '/login',
***ponent: () => import('@/views/login/index.vue'),
hidden: true
}
// {
// path: "/404",
// ***ponent: () => import("@/views/404/index.vue"),
// name: "404",
// meta: {
// title: "404",
// },
// },
];
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
// {
// path: '/system/user-auth',
// ***ponent: Layout,
// hidden: true,
// permissions: ['system:user:edit'],
// children: [
// {
// path: 'role/:userId(\\d+)',
// ***ponent: () => import('@/views/system/user/authRole'),
// name: 'AuthRole',
// meta: { title: '分配角色', activeMenu: '/system/user' }
// }
// ]
// },
// {
// path: '/system/role-auth',
// ***ponent: Layout,
// hidden: true,
// permissions: ['system:role:edit'],
// children: [
// {
// path: 'user/:roleId(\\d+)',
// ***ponent: () => import('@/views/system/role/authUser'),
// name: 'AuthUser',
// meta: { title: '分配用户', activeMenu: '/system/role' }
// }
// ]
// },
// {
// path: '/system/dict-data',
// ***ponent: Layout,
// hidden: true,
// permissions: ['system:dict:list'],
// children: [
// {
// path: 'index/:dictId(\\d+)',
// ***ponent: () => import('@/views/system/dict/data'),
// name: 'Data',
// meta: { title: '字典数据', activeMenu: '/system/dict' }
// }
// ]
// }
]
- 在main.ts文件中引入router
import { createApp } from "vue";
import "./style.scss";
import ElementPlus from 'element-plus'
import Antd from 'ant-design-vue';
import "ant-design-vue/dist/reset.css";
import App from "./App.vue";
// 引入路由
import router from "./router";
import store from "./store";
import './permission';// 权限处理
import "virtual:svg-icons-register";
import SvgIcon from '@/***ponents/SvgIcon'
import elementIcons from '@/***ponents/SvgIcon/svgicon'
const app = createApp(App);
app.use(router);
app.use(store);
app.use(ElementPlus);
app.use(Antd);
app.use(elementIcons)
app.***ponent('svg-icon', SvgIcon)
app.mount("#app");
补充layout
- src下新建layout文件夹,分别新建index.vue和***ponents文件夹
index.vue内容如下:
<template>
<div class="app-wrapper">
<side-bar class="sideBar-container" />
<div class="main-container">
<div class="fixed-header">
<nav-bar class="navBar-container" />
</div>
<app-main />
</div>
</div>
</template>
<script setup lang="ts">
import { AppMain, NavBar, SideBar } from './***ponents'
</script>
<style lang='scss' scoped>
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100vh;
width: 100%;
.sideBar-container {
width: $base-sidebar-width;
background-color: $base-menu-background;
height: calc(100% - #{$base-navbar-height});
position: fixed;
top: $base-navbar-height;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
}
.main-container {
height: 100%;
width: calc(100% - #{$base-sidebar-width});
position: relative;
left: $base-sidebar-width;
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
height: $base-navbar-height;
transition: width 0.28s;
background-color: #fff;
box-shadow: 0 4px 6px rgba(0, 0, 0, .05);
.navBar-container {
height: 100%;
// max-width: 1200px;
position: relative;
margin: auto;
will-change: transform;
// padding: 0 24px;
}
}
}
}
</style>
- 在***ponents文件夹下新建AppMain.vue文件,内容如下:
<template>
<section class="app-main">
<div class="app-main-container">
<router-view v-slot="{ ***ponent, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="['index','crm']">
<***ponent :is="***ponent" :key="route.path"/>
</keep-alive>
</transition>
</router-view>
</div>
</section>
</template>
<script setup lang="ts">
</script>
<style lang='scss' scoped>
@import "@/assets/styles/variables.module.scss";
.app-main {
height: calc(100vh - #{$base-navbar-height});
background-color: #fff;
width: 100%;
position: relative;
overflow: auto;
top:$base-navbar-height;
.app-main-container {
height: 100%;
padding: 20px;
}
}
</style>
<style lang='scss'>
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #c0c0c0;
border-radius: 3px;
}
</style>
- 在***ponents文件夹下新建index.ts文件,内容如下:
export { default as AppMain } from './AppMain'
export { default as NavBar } from './NavBar'
export { default as SideBar } from './SideBar'
- 在***ponents文件夹下新建SideBar/NavBar/InnerLink文件夹,内容如下:
SideBar新建index/SidebarItem/Link.vue文件
index.vue内容如下:
<template>
<div :style="{ backgroundColor: variables.menuLightBackground }">
<el-scrollbar wrap-class="scrollbar-wrapper" style="padding: 20px;">
<el-menu
:default-active="activeMenu"
:background-color="variables.menuLightBackground"
:text-color="variables.menuLightColor"
:unique-opened="true"
mode="vertical"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import SidebarItem from './SidebarItem'
import usePermissionStore from '@/store/modules/permission'
const route = useRoute();
const permissionStore = usePermissionStore()
const sidebarRouters = ***puted(() => permissionStore.sidebarRouters);
console.log('sidebarRouters',sidebarRouters)
const activeMenu = ***puted(() => {
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
})
</script>
<style>
.sidebar-container .el-scrollbar__wrap{
height: calc(100vh - 155px);
overflow-y: auto;
}
#app .sidebar-container .el-menu--vertical .nest-menu .el-sub-menu > .el-sub-menu__title:hover,
#app .sidebar-container .el-menu--vertical .el-menu-item:hover,
#app .sidebar-container .nest-menu .el-sub-menu > .el-sub-menu__title:hover,
#app .sidebar-container .el-sub-menu .el-menu-item:hover,
#app .sidebar-container .el-menu--vertical .nest-menu .el-sub-menu > .el-sub-menu__title .is-active,
#app .sidebar-container .el-menu--vertical .el-menu-item.is-active,
#app .sidebar-container .nest-menu .el-sub-menu > .el-sub-menu__title .is-active,
#app .sidebar-container .el-sub-menu .el-menu-item .is-active{
background-color: rgb(79 110 247 / 5%) !important;
}
.el-menu{
--el-menu-item-height:2.75rem;
--el-menu-sub-item-height:2.75rem;
border-right: none!important;
}
</style>
SidebarItem.vue内容如下:
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }" @click="handleClick(item.path)">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
<template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template v-if="item.meta" #title>
<svg-icon :icon-class="item.meta && item.meta.icon" />
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script setup>
import { isExternal } from '@/utils/validate'
import AppLink from './Link'
import { getNormalPath } from '@/utils/mis'
const props = defineProps({
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
})
const onlyOneChild = ref({});
function hasOneShowingChild(children,parent) {
if (!children) {
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
}
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
};
function handleClick(routePath){
localStorage.setItem('routePath', routePath)
}
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(props.basePath)) {
return props.basePath
}
if (routeQuery) {
const query = JSON.parse(routeQuery);
return { path: getNormalPath(`${props.basePath }/${ routePath}`), query }
}
return getNormalPath(`${props.basePath }/${ routePath}`)
}
function hasTitle(title){
if (title.length > 5) {
return title;
}
return "";
}
</script>
Link.vue内容如下:
<template>
<***ponent :is="type" v-bind="linkProps()">
<slot />
</***ponent>
</template>
<script setup>
import { isExternal } from '@/utils/validate'
const props = defineProps({
to: {
type: [String, Object],
required: true
}
})
const isExt = ***puted(() => isExternal(props.to))
const type = ***puted(() => {
if (isExt.value) {
return 'a'
}
return 'router-link'
})
function linkProps() {
if (isExt.value) {
return {
href: props.to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: props.to
}
}
</script>
NavBar新建index.vue,内容如下
<template>
<div class="navbar">
navbar
<span @click="handleLoginOut()">退出</span>
</div>
</template>
<script setup lang="ts">
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const handleLoginOut = () => {
userStore.logOut().then(() => {
window.location.href = '/index';
})
}
</script>
<style lang='scss' scoped>
.navbar {
background: #4F6EF7;
}
</style>
InnerLink/index.vue内容如下:
<template>
<div :style="'height:' + height">
<iframe
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
frameborder="no"
></iframe>
</div>
</template>
<script setup>
const props = defineProps(['src', 'iframeId'])
const { src, iframeId } = toRefs(props);
const height = ref(`${document.documentElement.clientHeight - 94.5 }px`);
</script>
八、添加类型说明文件
typescript 只能理解 .ts 文件,无法理解 .vue文件
因此需要给.vue文件加上类型说明文件
解决方法:在项目根目录或 src 文件夹下创建一个后缀为 .d.ts 的文件,并vite-env.d.ts写入以下内容:
declare module '*.vue' {
import type { Define***ponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const ***ponent: Define***ponent<{}, {}, any>
export default ***ponent
}
九、添加api,封装请求(axios)
- 安装依赖pnpm install axios --save
- 新建api文件夹,utils文件夹,及创建request.ts文件
十、pinia状态管理器
- 安装依赖:pnpm install pinia --save
- 在src目录下新建文件夹store,在store文件下新建文件index.ts和modules文件夹
- 在index.ts文件中引入pinia,创建pinia实例,挂载到根组件上
- 在main.ts文件中引入store,并挂载到根组件上
十一、pinia状态持久化处理
pnpm install pinia-plugin-persistedstate --save
persist: true// 开启持久化存储
十二、滚动条美化处理
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #c0c0c0;
border-radius: 3px;
}
十三、vue3告别.value,ref要求我们访问变量时需要加上.value
let count = ref(1)
const addCount = () => {
count.value += 1
}
尤大也提交了一份新的ref语法糖提案。
ref: count = 1
const addCount = () => {
count += 1
}
官方后来出的一种方案,在ref前加上$,该功能默认关闭,需要手动开启, vite.config.ts内容配置如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
refTransform: true // 开启ref转换
})
]
})
开启之后的写法
let count = $ref(1)
const addCount = () => {
count++
}
十四、svg-icon的应用
pnpm install vite-plugin-svg-icons -S
在vite.config.ts中配置:
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
symbolId: 'icon-[dir]-[name]',
})
assets文件夹新建icons文件夹,新建svg文件夹,新建svg文件夹下新建svg文件,文件名随意
在***ponents文件夹下新建SvgIcon文件夹,新建index.vue内容如下:
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" :fill="color" />
</svg>
</template>
<script>
export default define***ponent({
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
},
color: {
type: String,
default: ''
},
},
setup(props) {
return {
iconName: ***puted(() => `#icon-${props.iconClass}`),
svgClass: ***puted(() => {
if (props.className) {
return `svg-icon ${props.className}`
}
return 'svg-icon'
})
}
}
})
</script>
<style scope lang="scss">
.sub-el-icon,
.nav-icon {
display: inline-block;
font-size: 15px;
margin-right: 12px;
position: relative;
}
.svg-icon {
width: 1em!important;
height: 1em!important;
position: relative;
fill: currentColor;
vertical-align: -2px;
}
</style>
新建svgicon.ts文件
import * as ***ponents from '@element-plus/icons-vue'
export default {
install: (app) => {
for (const key in ***ponents) {
const ***ponentConfig = ***ponents[key];
app.***ponent(***ponentConfig.name, ***ponentConfig);
}
},
};
应用到vue文件
<svg-icon :icon-class="item.meta && item.meta.icon" />
渲染结果是home.svg
<svg class="svg-icon" aria-hidden="true"><use xlink:href="#icon-home" fill=""></use></svg>
在main.ts引入svg-icon
import "virtual:svg-icons-register";
import SvgIcon from '@/***ponents/SvgIcon'
import elementIcons from '@/***ponents/SvgIcon/svgicon'
app.use(elementIcons)
app.***ponent('svg-icon', SvgIcon)
最终vite.config.ts内容如下:
/* eslint-disable import/no-extraneous-dependencies */
import { defineConfig, loadEnv } from "vite";
import path from "path";
import vue from "@vitejs/plugin-vue";
import eslintPlugin from "vite-plugin-eslint";
import AutoImport from "unplugin-auto-import/vite";
import ***ponents from "unplugin-vue-***ponents/vite";
import { ElementPlusResolver } from "unplugin-vue-***ponents/resolvers";
import {
createStyleImportPlugin,
ElementPlusResolve
} from "vite-plugin-style-import";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
const { VITE_APP_BASE_API,VITE_SERVE,VITE_PORT } = env
return {
plugins: [
vue({
refTransform: true // 开启ref转换,不用.value,直接$ref
}),
eslintPlugin(),
AutoImport({
imports: ["vue", "vue-router"],
dts: "src/auto-import.d.ts", // 路径下自动生成文件夹存放全局指令
eslintrc: {
enabled: true, // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
},
resolvers: [ElementPlusResolver()]
}),
***ponents({
dts: "./src/***ponents.d.ts", // 创建ts文件
extensions: ["vue"], // 指定文件的后缀
dirs: ["src/***ponents/"], // 指定路径,自动导入自定义组件
resolvers: [ElementPlusResolver()], // 指定自动引入的组件库,也就是从插件中导出的那个
}),
// 配置自动导入element start
createStyleImportPlugin({
resolves: [ElementPlusResolve()],
libs: [
{
libraryName: "element-plus",
esModule: true,
resolveStyle: (name: string) =>
`element-plus/theme-chalk/${name}.css`,
},
],
}),
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
symbolId: 'icon-[dir]-[name]',
}),
VueSetupExtend()
],
base: "./", // 在生产中服务时的基本公共路径
publicDir: "public", // 静态资源服务的文件夹, 默认"public"
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), // 相对路径别名配置,使用 @ 代替 src
},
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
},
// 本地运行配置
server: {
host: '0.0.0.0',
port: VITE_PORT || 80,
https: false,
open: true,
proxy: {
[VITE_APP_BASE_API]: {
target: VITE_SERVE,
changeOrigin: true,
rewrite: path => path.replace(RegExp(`^${VITE_APP_BASE_API}`), '')
}
},
disableHostCheck: true
},
// 打包配置
build: {
target: "modules", // 设置最终构建的浏览器兼容目标。modules:支持原生 ES 模块的浏览器
outDir: "dist", // 指定输出路径
assetsDir: "assets", // 指定生成静态资源的存放路径
assetsInlineLimit: "4096", // 小于此阈值的导入或引用资源将内联为base64编码,设置为0可禁用此项。默认4096(4kb)
cssCodeSplit: true, // 启用/禁用CSS代码拆分,如果禁用,整个项目的所有CSS将被提取到一个CSS文件中,默认true
sourcemap: false, // 构建后是否生成 source map 文件
minify: "terser", // 混淆器,terser构建后文件体积更小
write: true, // 设置为 false 来禁用将构建后的文件写入磁盘
emptyOutDir: true, // 默认情况下,若 outDir 在 root 目录下,则 Vite 会在构建时清空该目录。
chunkSizeWarningLimit: 500, // chunk 大小警告的限制
terserOptions: {
***press: {
drop_console: true,
drop_debugger: true,
},
}, // 去除 console debugger
},
};
});