个人建站前端篇(七)vite + vue3企业级项目模板

一、vite命令行创建项目

npm create vite@latest

根据提示选择模板,选择vite + vue3 + ts即可。

二、项目连接远程仓库

git init
git remote add origin https://gitee.com/niech_project/vite-vue3-template.git
git pull origin master
git checkout -b dev

三、项目加入eslint校验和自动格式化

  1. eslint 运行代码前就可以发现一些语法错误和潜在bug,保证代码一致性。
  2. prettier 是代码格式化工具,用于检查代码中的格式问题。
  3. 区别联系:eslint保证代码质量,prettier保证代码风格,eslint有小部分格式化功能,通常和prettier结合使用。
1. 安装eslint和prettier
  1. eslint: ESLint的核心代码库
  2. prettier:prettier格式化代码的核心库
  3. eslint-config-airbnb-base: airbnb的代码规范 (依赖plugin-import)
  4. eslint-config-prettier:eslint结合prettier的格式化
  5. eslint-plugin-vue:eslint在vue里的代码规范
  6. eslint-plugin-import:项目里面支持eslint
  7. 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
  1. 在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
  1. 修改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: {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      }, // 去除 console debugger
    }
  }
})

vite-plugin-eslint: vite的一个插件,让项目可以方便的得到eslint支持,完成eslint配置后,可以快速的将其集成进vite中,便于在代码不符合eslint规范的第一时间看到提示

3. 修改添加常见配置

项目根目录创建以下配置文件

  1. .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?

components.d.ts

  1. .prettierrc.cjs 配置格式化规则
module.exports = {
  "printWidth" : 80, // 一行最多100字符
  "tabWidth": 2, // 使用2个空格缩进
  "useTabs": false, // 不适用缩进符,使用空格
  "semi": false, // 行尾是否要有分号
  "singleQuote": true, // 使用单引号
  "quoteProps": 'as-needed', // 对象的key,仅仅在必要时使用引号
  "jsxSingleQuote": false, // jsx是否使用双引号
  "trailingComma": 'es5', // 尾随逗号
  "bracketSpacing": true, // 大括号内的首尾需要有空格
  "arrowParens": 'always', // 箭头函数,是有一个参数的时候,也需要小括号
  "rangeStart": 0, // 文件格式化的范围是全部内容
  "rangeEnd": Infinity,
  "requirePragma": false, // 不需要写文件开头的 @prettier
  "insertPragma": false, // 不需要在文件开头插入 @prettier
  "proseWrap": 'always', // 使用默认执行标准
  "htmlWhitespaceSensitivity": 'css', // 根据显示样式决定html是否执行
  "endOfLine": 'lf' // 换行符是有lf
}
  1. .prettierignore 忽略格式化文件
# prettierignore
/dist/*
.local
.output.js
/node_modules/**
src/.DS_Store

**/*.svg
**/*.sh
/public/*
components.d.ts

在package.json添加格式化命令

  1. “lint”: “eslint “src/**/*.{js,ts,vue}” --fix”, 既可以检查又可以修复部分语法问题
  2. “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_COMPRESS = gzip

六、自动引入插件unplugin-auto-import和unplugin-vue-components的使用

:::tip
vue3日常项目中定义变量需要引入ref,reactive等等比较麻烦,可以通过unplugin-auto-import给我们自动引入
:::

  1. 安装依赖
npm install -D unplugin-vue-components unplugin-auto-import vite-plugin-style-import
npm install element-plus @element-plus/icons-vue ant-design-vue -S
  1. 在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 Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/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()]
      }),
      Components({
        dts: "./src/components.d.ts", // 创建ts文件
        extensions: ["vue"], // 指定文件的后缀
        dirs: ["src/components/"], // 指定路径,自动导入自定义组件
        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: {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      }, // 去除 console debugger
    },
  };
});


  1. 在src目录下自动生成auto-import.d.ts文件,用于存放全局指令

  2. 和eslintrc不兼容时,加上

eslintrc: {
  enabled: true,  // 1、改为true用于生成eslint配置。2、生成后改回false,避免重复生成消耗
}

自动生成.eslintrc-auto-import.json文件
5. 在.eslintrc.json文件中添加如下配置:

module.exports = {
    // 环境 浏览器,最新ES语法,node环境
    "env": {
        "browser": true,
        "commonjs": true,
        "es2021": true
    },
    /**
     * 扩展的eslint规范语法,可以被继承的规则,字符串数组,每个配置继承它之前的配置
     * 分别是eslint-config-vue 提供的
     * eslint-config-airbnb-base 提供的
     * eslint-config-prettier 提供的
     * eslint-config- 前缀可以简写
     */
    "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended",
        "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.com/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-component-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",// 不允许一元操作符++、--
    }
}




  1. tsconfig.json文件中添加如下配置:

include加入"src/auto-imports.d.ts"

  1. 安装 element-plus
npm install element-plus -S

在.eslintrc-auto-import.json文件中添加需要引入的组件:
“ElMessage”: true

  1. 安装saas
npm install sass -D

七、添加路由

  1. pnpm install vue-router 安装路由依赖;
  2. 在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', 'common']       // 访问路由的角色权限
 * 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',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },
  {
    path: '',
    component: Layout,
    redirect: '/index',
    children: [
      {
        path: '/index',
        component: () => import('@/views/home/index.vue'),
        name: 'Index',
        meta: { title: '系统首页', icon: 'home', affix: true }
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    hidden: true
  }
  // {
  //   path: "/404",
  //   component: () => import("@/views/404/index.vue"),
  //   name: "404",
  //   meta: {
  //     title: "404",
  //   },
  // },
];
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
  // {
  //   path: '/system/user-auth',
  //   component: Layout,
  //   hidden: true,
  //   permissions: ['system:user:edit'],
  //   children: [
  //     {
  //       path: 'role/:userId(\\d+)',
  //       component: () => import('@/views/system/user/authRole'),
  //       name: 'AuthRole',
  //       meta: { title: '分配角色', activeMenu: '/system/user' }
  //     }
  //   ]
  // },
  // {
  //   path: '/system/role-auth',
  //   component: Layout,
  //   hidden: true,
  //   permissions: ['system:role:edit'],
  //   children: [
  //     {
  //       path: 'user/:roleId(\\d+)',
  //       component: () => import('@/views/system/role/authUser'),
  //       name: 'AuthUser',
  //       meta: { title: '分配用户', activeMenu: '/system/role' }
  //     }
  //   ]
  // },
  // {
  //   path: '/system/dict-data',
  //   component: Layout,
  //   hidden: true,
  //   permissions: ['system:dict:list'],
  //   children: [
  //     {
  //       path: 'index/:dictId(\\d+)',
  //       component: () => import('@/views/system/dict/data'),
  //       name: 'Data',
  //       meta: { title: '字典数据', activeMenu: '/system/dict' }
  //     }
  //   ]
  // }
]
  1. 在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 '@/components/SvgIcon'
import elementIcons from '@/components/SvgIcon/svgicon'
 
const app = createApp(App);
app.use(router);
app.use(store);
app.use(ElementPlus);
app.use(Antd);
app.use(elementIcons)
app.component('svg-icon', SvgIcon)
app.mount("#app");

补充layout

  1. src下新建layout文件夹,分别新建index.vue和components文件夹

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 './components'
</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>

  1. 在components文件夹下新建AppMain.vue文件,内容如下:
<template>
  <section class="app-main">
    <div class="app-main-container">
      <router-view v-slot="{ Component, route }">
        <transition name="fade-transform" mode="out-in">
          <keep-alive :include="['index','crm']">
            <component :is="Component" :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>
  1. 在components文件夹下新建index.ts文件,内容如下:
export { default as AppMain } from './AppMain'
export { default as NavBar } from './NavBar'
export { default as SideBar } from './SideBar'
  1. 在components文件夹下新建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 =  computed(() => permissionStore.sidebarRouters);
console.log('sidebarRouters',sidebarRouters)
const activeMenu = computed(() => {
  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>
  <component :is="type" v-bind="linkProps()">
    <slot />
  </component>
</template>

<script setup>
import { isExternal } from '@/utils/validate'

const props = defineProps({
  to: {
    type: [String, Object],
    required: true
  }
})

const isExt = computed(() => isExternal(props.to))

const type = computed(() => {
  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 { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

九、添加api,封装请求(axios)

  1. 安装依赖pnpm install axios --save
  2. 新建api文件夹,utils文件夹,及创建request.ts文件

十、pinia状态管理器

  1. 安装依赖:pnpm install pinia --save
  2. 在src目录下新建文件夹store,在store文件下新建文件index.ts和modules文件夹
  3. 在index.ts文件中引入pinia,创建pinia实例,挂载到根组件上
  4. 在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文件,文件名随意
在components文件夹下新建SvgIcon文件夹,新建index.vue内容如下:

<template>
  <svg :class="svgClass" aria-hidden="true">
    <use :xlink:href="iconName" :fill="color" />
  </svg>
</template>

<script>
export default defineComponent({
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    },
    color: {
      type: String,
      default: ''
    },
  },
  setup(props) {
    return {
      iconName: computed(() => `#icon-${props.iconClass}`),
      svgClass: computed(() => {
        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 components from '@element-plus/icons-vue'

export default {
    install: (app) => {
        for (const key in components) {
            const componentConfig = components[key];
            app.component(componentConfig.name, componentConfig);
        }
    },
};

应用到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 '@/components/SvgIcon'
import elementIcons from '@/components/SvgIcon/svgicon'
app.use(elementIcons)
app.component('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 Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/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()]
      }),
      Components({
        dts: "./src/components.d.ts", // 创建ts文件
        extensions: ["vue"], // 指定文件的后缀
        dirs: ["src/components/"], // 指定路径,自动导入自定义组件
        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: {
        compress: {
          drop_console: true,
          drop_debugger: true,
        },
      }, // 去除 console debugger
    },
  };
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/414432.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《大模型时代-ChatGPT开启通用人工智能浪潮》精华摘抄

原书很长&#xff0c;有19.3w字&#xff0c;本文尝试浓缩一下其中的精华。 知识点 GPT相关 谷歌发布LaMDA、BERT和PaLM-E&#xff0c;PaLM 2 Facebook的母公司Meta推出LLaMA&#xff0c;并在博客上免费公开LLM&#xff1a;OPT-175B。 在GPT中&#xff0c;P代表经过预训练(…

pclpy Ransac平面分割算法输出的索引从点云中提取点云的子集

pclpy Ransac平面分割算法输出的索引从点云中提取点云的子集 一、算法原理二、代码三、结果1.sor统计滤波2.Ransac内点分割平面3.Ransac外点分割平面 四、相关数据 一、算法原理 1、Ransac介绍 RANSAC(RAndom SAmple Consensus,随机采样一致)算法是从一组含有“外点”(outlier…

4核8G服务器选阿里云还是腾讯云?价格性能对比

4核8G云服务器多少钱一年&#xff1f;阿里云ECS服务器u1价格955.58元一年&#xff0c;腾讯云轻量4核8G12M带宽价格是646元15个月&#xff0c;阿腾云atengyun.com整理4核8G云服务器价格表&#xff0c;包括一年费用和1个月收费明细&#xff1a; 云服务器4核8G配置收费价格 阿里…

7.整数反转

题目&#xff1a;给一个32位的有符号整数x&#xff0c;返回将x中的数字部分反转后的结果。 如果反转后整数超过32位的有符号整数的范围[−2^31, 2^31 − 1]&#xff0c;就返回0. 解题思路&#xff1a;在没有辅助栈或数组的帮助下弹出和推入数字&#xff0c;可以使用如下数学方…

数据结构-关键路径

介绍 在AOV网的基础上&#xff0c;如果用对应边来表示活动持续时间&#xff0c;这种有向图被称为AOE网在AOE网中&#xff0c;入度为0的为源点&#xff0c;出度为0的为汇点&#xff0c;整张网看做是一件事情完成的过程&#xff0c;那么这两个点就是事情的开始和结束。每个活动持…

LVS负载均衡服务器

简介: LVS (Linux Virtual Server):四层路由设备&#xff0c;是由中国人章文松研发的(阿里巴巴的副总裁)根据用户请求的IP与端口号实现将用户的请求分发至不同的主机。 工作原理: LVS工作在一台server上提供Directory(负载均衡器)的功能&#xff0c;本身并不提供服务&#xff…

安卓平板主板_安卓平板电脑主板MTK联发科|高通|紫光展锐方案

安卓平板电脑主板选择了MTK联发科方案&#xff0c;并且可以选配高通或者紫光展锐平台方案&#xff0c;为用户提供更强劲的性能和定制化的服务。主板搭载了联发科MT6771处理器&#xff0c;采用12nm制程工艺&#xff0c;拥有八核Cortex-A73Coretex-A53架构&#xff0c;主频为2.0G…

全景分屏对比模式,差异化展示更直观!

在生活中&#xff0c;对比现象无处不在&#xff0c;人们通过对比可以做出更好的判断甚至是选择。 而在家装设计行业&#xff0c;伴随着商业服务的升级&#xff0c;一些企业也在通过一些方式&#xff0c;满足客户的对比需求&#xff0c;从而提高服务质量。 全景分屏对比模式&a…

vue2 + axios + mock.js封装过程,包含mock.js获取数据时报404状态的解决记录,带图文,超详细!!!

vue axios mock.js 以下是封装的过程&#xff0c;记录一下 1、首先先了解什么是mock.js的用途及特点 官网地址&#xff1a;Mock.js (mockjs.com) 作用&#xff1a;生成随机数据&#xff0c;拦截 Ajax 请求 优势&#xff1a; 2、了解axios的原理及使用 官网地址&#xff1a…

智能手表的革命性突破:TRIZ理论引领未来穿戴技术!

在科技日新月异的今天&#xff0c;智能手表已经从单纯的计时工具转变为集健康监测、信息通讯、娱乐休闲等多功能于一体的智能穿戴设备。而基于TRIZ理论的智能手表更是在这一变革中扮演着引领者的角色。TRIZ&#xff0c;即发明问题解决理论&#xff0c;是一套系统的创新方法学&a…

opencv图像腐蚀

腐蚀&#xff08;Erosion&#xff09;是一种形态学图像处理操作&#xff0c;用于移除图像中的小白点、细小物体或者边缘。它通过将结构元素应用于图像上的像素来实现。 以下是opencv实现图像腐蚀的代码 #include <opencv2/highgui/highgui.hpp> #include <opencv2/im…

postman访问k8s api

第一种方式&#xff1a; kubectl -n kubesphere-system get sa kubesphere -oyaml apiVersion: v1 kind: ServiceAccount metadata:annotations:meta.helm.sh/release-name: ks-coremeta.helm.sh/release-namespace: kubesphere-systemcreationTimestamp: "2023-07-24T07…

Unity中字符串拼接0GC方案

本文主要分析C#字符串拼接产生GC的原因&#xff0c;以及介绍名为ZString的库&#xff0c;它可以将字符串生成的内存分配为零。 在C#中&#xff0c;字符串拼接通常有三种方式&#xff1a; 直接使用号连接&#xff1b;string.format;使用StringBuilder&#xff1b; 下面分别细…

express+mysql+vue,从零搭建一个商城管理系统4--mysql数据库链接

提示&#xff1a;学习express&#xff0c;搭建管理系统 文章目录 前言一、创建express_service数据库二、安装mysql三、新建config文件夹四、新建config/db.js五、index.js引入db.js文件六、启动项目预览总结 前言 需求&#xff1a;主要学习express&#xff0c;所以先写service…

高级语言期末2011级A卷(软件学院)

1.编写函数&#xff0c;判定正整数m和n&#xff08;均至少为2&#xff09;是否满足&#xff1a;数m为数n可分解的最小质因数&#xff08;数n可分解的最小质因数为整除n的最小质数&#xff09; 提示&#xff1a;判定m为质数且m是n的最小因数 #include <stdio.h> #include…

导览系统厂家|景区电子导览|手绘地图|AR导览|语音导览系统

随着元宇宙、VR、AR等新技术的快速发展&#xff0c;旅游服务也更加多元化、智能化。景区导览系统作为旅游服务的重要组成部分&#xff0c;其形式更加多元化智能化。智能导览系统作为一种新的服务方式&#xff0c;能够为游客提供更加便捷的旅游服务和游览体验&#xff0c;也逐渐…

【经验】vscode 鼠标拖曳不能选中整行文字,只能选中纵向矩形范围

1、问题描述 不知道昨天操作vscode设置界面时&#xff0c;误选择了啥&#xff0c;导致鼠标拖曳不能选中整行文字&#xff0c;只能选中纵向矩形范围&#xff0c;现象如下&#xff1a; 2、解决方法 1&#xff09;打开设置界面 点击左下角按键&#xff0c;选择“设置” 2&…

在CentOS上使用Docker搭建Halo博客并实现远程访问的详细指南

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;网络奇遇记、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. Docker部署Halo1.1 检查Docker版本1.2 在Docker中部署Halo 二. Linux安装Cpol…

Oracle中序列

1. Sequence 定义 在Oracle中可以用SEQUENCE生成自增字段。Sequence序列是Oracle中用于生成数字序列的对象&#xff0c;可以创建一个唯一的数字作为主键。 2. 为什么要用 Sequence 你可能有疑问为什么要使用序列&#xff1f; 不能使用一个存储主键的表并每次递增吗&#xf…

第102讲:MySQL多实例与Mycat分布式读写分离的架构实践

文章目录 1.Mycat读写分离分布式架构规划2.在两台服务器中搭建八个MySQL实例2.1.安装MySQL软件2.2.创建每个MySQL实例的数据目录并初始化2.3.准备每个实例的配置文件2.4.准备每个实例的启动脚本2.6启动每台机器的MySQL多实例2.7.为每个MySQL实例设置密码2.8.查看每个MySQL实例的…