基于 Vue3打造前台+中台通用提效解决方案(上)

news2024/9/25 7:23:08

基于 Vue3打造前台+中台通用提效解决方案

1、项目架构

本项目使用vite + vue3来实现前中台解决方案

2、为什么使用vite

因为,之前的项目一直都是使用webpack作为构建工具;vite出来这么久了,也没有用过;所以想在当前项目下进行使用;

2.1、为什么vite比webpack块?

webpack

假设我们的项目中有A、B两个页面。
其中A页面是项目首页,里面的代码一切正常。
B页面是一个需要经这跳转才会进入的页面,里面存在一些错误。比如︰我导入一个不存在的文件a.js 然后打印a
当我们去构建这个项目时,明明我们从来都没有进入过B页面,但是此时
webpack依然会给我们抛出一个对应的错误 `Can't resolve './a.js' in xxX`

webpack在开发时构建时,默认会去抓取并构建你的整个应用,然后才能提供服务,这就导致你的项目中,存在的任何一个错误(哪怕这个错误是在用户从来都没有进入过的页面中出现的),它依然会影响到你的整个项目构建。
也正是因为这个原因,当你的项目越大时,构建的时间就会越长,你的项目启动速度也就会越慢。

vite

同样的`Can't resolve './a.js' in xx` 错误,在我们没有进入到B页面的时候,它是不会出现的,只有当我们进入了B页面,才会突然出现这样的一个错误;

而之所以会这样的原因就是因为: vite 不会在一开始就构建你的整个项目,而是会将应用中的模块区分为依赖和源码(项目代码)两部分,对于源码部分,它会根据路由来拆分代码模块,只会去构建一开始就必须要构建的内容。
同时 vite以原生 ESM 的方式为浏览器提供源码,让浏览器接管了打包的部分工作。
因为这样的一个机制,无论你的项目有多大,它只会构建一开始必须要构建的内容,这就让 vite在构建时的速度大大提升了。
这也是vite为什么会快的一个核心原因。

2.2、vite这么快会有什么问题吗?

如果大家对ESM的构建机制有了解的话,那么应该可以发现一个问题。
那就是**vite既然以原生ESM的方式为浏览器提供源码,让浏览器接管了打包的部分工作**,那么假如我们的项目中存在 cormmonJS的内容怎么办?是不是就意味着无法解析呢?
是的!
vite 的早期版本中,确实存在这个问题,这个问题导致的最核心的麻烦就是很多的依赖无法使用。
比如axios 因为 axios 中使用了很多的 commonJS规范,这就让 vite 无法解析对应的内容(对应的 ieeue),从而会抛出一个错误,关于这个问题曾经也在viteissues中进行过激烈的讨论。

2.3、上面这个问题,官方是如何解决的呢?

因为这个问题非常的严重,所以针对于这个问题, vite在后期提供了依赖预构建的功能,其中一个非常重要的目的就是为了解决
CommonJSUMD兼容性问题。目前 vite 会先将CommonJSUMD发布的依赖项转换为ESM之后,再重新进行编译。这也可以理解为速度对业务的一个妥协。

3、初始化项目

  • 1、全局安装vite 版本2.8.5

    $ npm install -g vite@2.8.5
    
  • 2、使用vite创建项目

    $ npm init vite@latest
    # npx: installed 6 in 2.285s
    # √ Project name: ... front
    # √ Select a framework: » vue
    # √ Select a variant: » vue
    
  • 3、运行项目

    $ npm run dev
    
    

image-20220816094012941

可以看到,项目已经启动,但是没有 network地址;我们需要手动配置下

package.json

 "scripts": {
   
    "dev": "vite --host", // dev后面 加上 --host
    "build": "vite build",
    "preview": "vite preview"
  },

4、tailwindcss工具

在正式的项目开发之前,我们还需要了解另外一个工具 tailwindcss .
大家只看它的名字可能会想,这不就是一个处理css的库吗?值得我们专门拿出来一章的内容去学习?
那么我的回答可能是:“是的,这是有价值的。
tailwindcss是一个非常富有争议的库,喜欢它的人和讨厌它的人都非常多。
但是我们去查看taliwindcss下载量可以发现,它的月下载量已经达到了惊人的977万!要知道 vite也只有200多万而已。

4.1、传统的企业级开发css痛点

在前端技术巨变的现在,一直流传着一句话:每隔六个月,你要学习的前端技术就增加了一倍。
或许这句话本身只是个戏言,但是也在一定程度中反映了前端技术是变化非常快的。就像我们在上一章中提到的 vite ,在不到两年的时间里经历了三个大版本的变化。
但是大家仔细的想一下,这样的一个变化好像只适用于js 端, html、css 好像已经有很多年没有发生过大的变化

难道是因为html、css 已经足够成熟,不需要再进行改变了吗?应该也不是的,比如针对于css而言,我们在进行企业开发时,就会遇到很多问题,比如:

  • 1.有时我们需要统一设计方案,比如项目中的红色我们需要使用同样的色值,标题的文字大小我们期望在整个项目中进行统一的划分。这样的一套变量如果通过 css 来实现,那么就不得不维护一个庞大的变量组,这其实是一个非常大的心智负担。

  • 2.html结构是一个非常复杂的结构化内容,为了给这些结构指定对应的样式,那么通常我们都是通过cLssName
    来去指定。这就必
    须要求我们为这套复杂的结构指定各种各样包含语义化的 className。比如: containercontainer-box
    container-box-titlecontainer-box-5ub-title , container-box-sub-title-left-imag 大量的"无意义“命名本身就会增加很多额外的负担。

  • 3.因为 html和 css 是分离的,所以我们通常情况下在开发时,不得不在整个代码文件中,来回的上下翻滚,或者进行分屏操作。无
    论是哪一种其实都不能给我们带来一个很好地开发体验。
    4.针对于一些”复杂”的功能,比如响应式(媒体查询)、主题定制。如果我们想要通过传统的 html + css 的形式来进行实现,无
    疑是非常复杂的。

    除了上面提到的这些之外,还有很多其他的问题,感兴趣的同学可以看一下这篇文章的介绍CSS Utility Classes and “Separation of Concerns”
    总而言之,传统的 html + css 的模式存在着很多的问题,那么有什么好的方案可以解决呢?

    tailwindcss就是一个很好地方向。

4.2、安装tailwindcss

1、安装依赖

$ npm install -D tailwindcss@3.0.23 postcss@8.4.8 autoprefixer@10.4.2

2、创建配置文件

$ npx tailwindcss init -p
# 执行当前命令生配置文件
/** @type {import('tailwindcss').Config} */
module.exports = {
   
  content: [
  	"./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ], // 表示tailwindcss的作用范围 [src下所有目录下的所有vue/js文件, 当前index.html文件]
  theme: {
   
    extend: {
   },
  },
  plugins: [],
}

3、导入tailwindcss的基础指令组件

创建src/styles/index,scss文件

// 导入`tailwindcss`的基础指令组件
@tailwind base;
@tailwind components;
@tailwind utilities;

4、在项目的入口文件、main.js中引入src/styles/index,scss

import {
    createApp } from 'vue'
import './style.css'
+ import './styles/index.scss'

保存之后,运行后,会报没有sass依赖包的错误,所以我们需要手动安装一下

image-20220816105339932

$ yarn add sass

重启即可

如果postcss报错的话,可以将package.json中 “type”: "module"删除掉

5、安装vscode插件

工欲善其事必先利其器,想要有一个比较爽快的开发体验,那么一些好的开发辅助插件是必不可少的。
我们今天就以VSCode为例,来介绍一些咱们这次项目中需要使用到的一些辅助插件来帮助大家进行项目的开发。

5.1、Prettier 和 Code formatter 格式代码

相信对于很多同学而言代码格式问题,是一个一直让大家头疼的问题,混乱的代码格式非常不利于我们的日常开发工作,如果你的项目被ESLint管理,那么还会得到很多的错误,导致项目无法运行。那么我们有没有什么办法来让我们的代码格式变得更加漂亮呢?

答案是有的,它就是 prettier

prettier是一个让代码变得更加漂亮的工具,我们可以利用它来处理我们代码的格式化问题。
想要使用prettier,那么我们可以按照以下步骤进行:

  • 1、在vscode中插件库中安装 prettier

image-20220816145837118

  • 2、在项目的根目录下创建.prettierrc文件

    {
         
    	"semi": false,
    	"singleQuote": true,
        "trailingComma": "none"
    }
    
  • 3、在.vue.js结尾的文件中,点击右键,选择“使用…格式化文档”,选择“配置默认格式化程序”,选择“Prettier”

image-20220816150512904

image-20220816150527541

image-20220816150548579

  • 4、在vsode的设置页面,搜索“save”,找到“Format On Save” 勾选上;等到保存时会自动格式化代码

    image-20220816150824725

5.2、配置tailwindcss插件

这个插件可以帮助我们在写代码时,进行tailwindcsscss类名提示

image-20220816151044955

5.3、安装Volar插件

这个插件代替了Vuter功能,比Vuter更加贴合Vue3

image-20220816151507317

6、项目结构分析

咱们的项目分为移动端PC端两种显示结果,但是这两种显示结果通过同一套代码进行实现,也就是所谓的响应式构建方案。那么我们在分析的时候就需要分别分析(PS:此处我们只分析大的路由方案,目的是让大家对基本的项目结构有一个初步的认识,以方便我们的项目结构处理,后续具体的细节构建方案不在这次分析行为之内):

  • 1.移动端结构

  • 2.PC端结构

然后把这两种的分析方案,合并到一起,组成一个最终的架构方案。

6.1、移动端结构分析

移动端的结构相对比较简单,当我们去进行路由跳转时,它是以整个页面进行的整体路由切换。
那么由此可知,移动端不存在嵌套路由的概念,只需要在 APP.vue 中保留一个路由出口即可。

image-20220816154619643

6.2、PC端接否分析

pc端相对于移动端、多了一个固定头部的部分,所以处理起来更加复杂一点

image-20220816154910365

我们需要通过两个路由出口进行表示:

  1. App.vue :一级路由出口,用作整页路由切换

  2. Main.vue :二级路由出口,用作局部路由切换

那么由此我们可知,移动端和PC端两者的路由结构是不同的,所以这就要求我们需要根据当前用户所在设备的不同,构建不同的路由表

7、项目结构

项目的整体结构如下图所示

image-20220816160615099

首先,我们项目中使用了vuexvue-router;那么接下来我们先来安装他们吧

$ yarn add vuex@4.0.2 vue-router@4.0.14

8、企业级vite配置方案-让vite得心应手

8.1、前言

在前面的章节中我们通过 vite构建了项目,但是初始的vite配置还比较粗糙,不足以支撑企业级的项目开发。
所以说在本章中,我们就需要来配置vite 。
但是配置vite 不能想当然的进行处理,而是需要依据业务来进行配置。
所以在本章中,我们会:

  • 1.先明确项目的业务处理方赛

  • 2.依据业务需要,来配置对应的vite内容

那么明确好了本章的内容之后,就让我们一起进入业务与vite结合的世界中去吧!

8.2、明确移动瑞和PC端的构建顺序

在上一章中(项目架构基本结构处理分析)中,我们明确了项目包含移动端路由表和PC端路由表两部分,所以我们在开发的时候就需要分别来去处理移动端和pc端对应的内容。

由于tailwindcss是遵循移动端优先的,所以我们在构建项目时,遵循它的规则,移动端优先

8.3、首先我们封装isMoboleTerminal判断是否是移动端方法

我们规定、屏幕宽度大于或等于1280像素的为pc端,小于1280像素的为移动端

import {
    computed } from 'vue'
import {
    PC_DEVICE_WIDTH } from '../constants'

/**
 * 是否是移动端设备; 判断依据: 屏幕宽度小于 PC_DEVICE_WIDTH
 * @returns
 */
export const isMoboleTerminal = computed(() => {
   
  console.log(document.documentElement.clientWidth, PC_DEVICE_WIDTH)
  return document.documentElement.clientWidth < PC_DEVICE_WIDTH
})

上面封装的方法有缺陷,就是:当页面尺寸发生变化时,isMoboleTerminal的值并不会发生响应式改变;这是因为computed重新执行的条件是,内部的响应式数据发生变化computed才会执行;而此时内部没有响应式数据,所以并不会重新执行;所以我们可以监听屏幕的尺寸变化,并设置响应式宽度

这里我们不使用上面的方法,而是使用第三方插件:VueUse 这个插件就像react hook一样,提供响应式数据

  • 1、首先安装vueuse

    $ npm i @vueuse/core
    
  • 2、重构isMoboleTerminal

    import {
          computed } from 'vue'
    import {
          PC_DEVICE_WIDTH } from '../constants'
    import {
          useWindowSize } from '@vueuse/core'
    const {
          width } = useWindowSize()
    /**
     * 是否是移动端设备; 判断依据: 屏幕宽度小于 PC_DEVICE_WIDTH
     * @returns
     */
    export const isMoboleTerminal = computed(() => {
         
      return width.value < PC_DEVICE_WIDTH
    })
    
8.4、配置路由、判断当前是移动端还是pc端加载对应的路由
import {
    createRouter, createWebHistory } from 'vue-router'
import {
    isMoboleTerminal } from '../utils/flexible'
import mobileRoutes from './modules/mobile-routes'
import pcRoutes from './modules/pc-routes'

const router = createRouter({
   
  history: createWebHistory(),
  routes: isMoboleTerminal.value ? mobileRoutes : pcRoutes
})

export default router

9、vite中的一些配置

9.1、使用@符号代理src路径

vite官方给出来了,解决方案:resolve.alias

vite.config.js

export default defineConfig({
   
  resolve: {
   
    alias: {
   
      '@': path.resolve(__dirname, './src'),
      '@@': path.resolve(__dirname, './src/components')
    }
  }
})
9.2、配置开发环境下跨域代理

vite官方给出来了,解决方案:server.proxy

vite.config.js

export default defineConfig({
   
  server: {
   
      proxy: {
   
        '/prod-api': {
   
          target: ' http://localhost:3000',
          changeOrigin: true
        }
      }
    }
})

10、动态设置rem并修修改tailmindcss默认配置

因为我们做的页面需要在不同设备下使用、要想在不同设备下适用;这里移动端我们采用的是flex+rem布局的方式:

首先我们先实现下rem布局

/**
 * 首次加载成功时设置html跟标签的fontSize属性值;最大基准值为40px
 */
export const useREM = () => {
   
  const MAX_FONT_SIZE = 40
  // 当文档被解析成功时调用
  window.addEventListener('DOMContentLoaded', () => {
   
    const html = document.querySelector('html')
    // 设置屏幕基准值的标准为 屏幕的宽度 / 10
    const fontSize = window.innerWidth / 10
    html.style.fontSize = Math.min(fontSize, MAX_FONT_SIZE) + 'px'
  })
}

在mian.js中引入并调用useREM

import {
    useREM } from '@/utils/flexible'

useREM()

测试发现:字体非常大,不符合我们的预期;如下图所示

image-20220820094254567

解决办法: tailwindcss提供了配置文件,我们可以在配置文件中自定义一些样式

我们在tailwind.config.js中进行theme.extend配置

module.exports = {
   
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
   
    extend: {
   
      fontSize: {
   
        xs: ['0.25rem', {
    lineHeight: '0.35rem' }],
        sm: ['0.35rem', {
    lineHeight: '0.45rem' }],
        base: ['0.45rem', {
    lineHeight: '0.55rem' }],
        lg: ['0.55rem', {
    lineHeight: '0.65rem' }],
        xl: ['0.65rem', {
    lineHeight: '0.75rem' }]
      },
      boxShadow: {
   
        'l-white': '-10px 0 10px white' // 自定义类名样式 使用时 shadow-l-white
      }
    }
  },
  plugins: []
}

image-20220820095829409

配置完成生效

11、在vite中封装通用的svg

我们之前在webpack中封装了通用的svg图标、但是在vite中没有进行分装;所以在本项目中我们对svg图标进行通用封装

image-20220820110904744

我们先看一下文件目录

  • 1、封装svg-icon通用组件libs/svg-icon/index.vue

    <template>
      <svg aria-hidden="true">
        <use :xlink:href="symbolId" :fill="color" :class="fillClass" />
      </svg>
    </template>
    
    <script setup>
    import { computed } from 'vue'
    
    const props = defineProps({
      // 图标名称
      name: {
        type: String,
        required: true
      },
      // 颜色
      color: {
        type: String
      },
      // 类名
      fillClass: {
        type: String
      }
    })
    
    // 生成图标唯一id #icon-xxx
    const symbolId = computed(() => `#icon-${props.name}`)
    </script>
    
  • 2、导出注册组件对象 libs/index.js

    import SvgIcon from './svg-icon/index.vue'
    
    // 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
    export default {
         
      install(app) {
         
        app.component('svg-icon', SvgIcon)
      }
    }
    
  • 3、在mian.js中注册组件对象

    import libs from '@/libs'
    createApp(App).use(router).use(libs).mount('#app')
    
  • 4、安装vite-plugin-svg-icons插件,并配置vite

    $ yarn add vite-plugin-svg-icons -D
    

    vite.config.js

    import {
          defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import path from 'path'
    import {
          createSvgIconsPlugin } from 'vite-plugin-svg-icons'
    
    // https://vitejs.dev/config/
    export default defineConfig({
         
      plugins: [
        vue(),
        // svg配置
        createSvgIconsPlugin({
         
          // 指定需要缓存的图标文件夹
          iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
          // 指定symbolId格式
          symbolId: 'icon-[name]'
        })
      ],
    })
    
  • 5、在main.js中注册 import 'virtual:svg-icons-register'

    
    import libs from '@/libs'
    import 'virtual:svg-icons-register' // 为固定格式
    
    createApp(App).use(router).use(libs).mount('#app')
    
  • 6、在组件中使用svg

    <svg-icon
        name="hamburger"
        class="px-1 w-4 h-4 fixed top-0 right-[-2px] z-20 shadow-l-white bg-white"
      />
    

image-20220820111656263

12、实现移动端navigation头部效果

需要实现的效果如下:

20220820_144232

实现思路:

  • 1、滑块绝对定位动态改变滑块的 leftwidth值, 来改变滑块的位置
  • 2、left值计算公式: 滚动x距离 + 点击元素距离屏幕左边的距离
  • 3、width值计算公式: 点击元素的宽度

实现细节:

  • 对于获取v-for生成的子元素的实例,需要使用回调函数获取

    <ul ref="ulEle">
        <li v-for="item in data" :ref="getEleFn"></li>
    </ul>
    
    <script setup>
        import { ref } from 'vue'
        // 获取普通元素的实例,可以使用ref(null)获取
        const ulEle = ref(null)
        //对于获取`v-for`生成的子元素的实例,需要使用回调函数获取
    	const getEleFn = (el) => {
            console.log(el)
        }
    </script>
    
  • 在初始化时,我们需要在li元素渲染完成之后触发一下重新设置一下滑块绝对定位动态改变滑块的 leftwidth值;我们可以监听渲染list的响应式数据是否改变,并且在改变后通过nextTick触发设置选中第一个元素

    // 监听data初次数据渲染之后,将slider条设置到第一项
    watch(
      () => props.data,
      () => {
         
        nextTick(() => {
         
          curretIndex.value = 0
        })
      }
    )
    

完整实例

<template>
  <ul
    class="relative z-10 text-xs bg-white flex overflow-auto p-1 text-zinc-600"
    ref="ulEle"
  >
    <li
      class="absolute top-1 h-[22.5px] bg-zinc-900 rounded-lg duration-200 z-10"
      :style="sliderStyle"
    ></li>
    <li
      v-for="(category, index) in data"
      :key="category.id"
      class="shrink-0 px-1.5 py-0.5 last:mr-6 z-10"
      :class="{ 'text-zinc-50': index === curretIndex }"
      @click="handleSelectCategory(index)"
      :ref="storeLiEle"
    >
      {
  { category.name }}
    </li>
  </ul>
  <svg-icon
    name="hamburger"
    class="px-1 w-4 h-4 fixed top-0 right-[-2px] z-20 shadow-l-white bg-white"
  />
</template>

<script setup>
import { ref, watch, nextTick } from 'vue'
import { useScroll } from '@vueuse/core'
const props = defineProps({
  data: {
    type: Array,
    required: true
  }
})

// 默认选中索引
const curretIndex = ref(-1)
const sliderStyle = ref({
  left: '10px',
  width: '0px'
})
//  ul dom元素
const ulEle = ref(null)
// li dom元素容器
const liEles = ref(new Set())

// ulScrollLeft 向左滚动的距离
const { x: ulScrollLeft } = useScroll(ulEle)

// 选中索引
const handleSelectCategory = (index) => {
  curretIndex.value = index
}
// 获取v-for遍历的子元素dom节点时,需要使用回调函数获取; 注意: 每次页面更新之后storeLiEle,都会重新执行一遍,这样会导致liEles中存储的都是重复的元素
// 所以可以使用Set来存储数据,避免存入重复的数据, 也可以在obBeforeUpdate前设置liEles.value的值为初始化值
const storeLiEle = (el) => {
  liEles.value.add(el)
}

watch(curretIndex, (newIndex, oldIndex) => {
  // 获取点击元素的距离左边屏幕的距离和元素的宽度
  const liEle = Array.from(liEles.value)[newIndex]
  if (!liEle) return false
  const { left, width } = liEle.getBoundingClientRect()
  sliderStyle.value = {
    left: `${left + ulScrollLeft.value}px`,
    width: `${width}px`
  }
})

// 监听data初次数据渲染之后,将slider条设置到第一项
watch(
  () => props.data,
  () => {
    nextTick(() => {
      curretIndex.value = 0
    })
  }
)
</script>

12.1、现在增加一个新功能:点击之后将点击项展示在屏幕的正中央,并且加上过渡**

实现思路

  • 1、在list菜单列表的数据发生改变后,获取每一项如果想要展示在中间需要滚动的距离

    菜单展示中间需要向左滚动的距离l = 每一项距离屏幕左边的距离 - 1/2屏幕的宽度 + 1/2自身的宽度 
    
  • 2、在点击时获取【被点击项向左滚动的距离l】,使得ul平滑滚动到指定位置(本案例使用自定义封装的平滑滚动函数)

    export const scrollTransition = () => {
         
      let timer = null
      return function exec ({
         el = document.body, position = 0, direction = 'v',  time = 150} = options) {
         
        clearInterval(timer)
        // 每步的时间 ms
        const TIME_EVERY_STEP = 5 
        // 最大滚动距离
        const maxScrollSize = el.scrollWidth - el.offsetWidth
        // 限定position的有效滚动范围
        position = Math.max(Math.min(position, maxScrollSize), 0)
        // 可以分为多少步
        let steps = Math.ceil(time / TIME_EVERY_STEP)
        const stepSize = (position - el.scrollLeft) / steps // 每步的长度
        
        timer = setInterval(() => {
         
          // console.log(el.scrollLeft , position)
          if (el.scrollLeft !== Number.parseInt(position) && position >= 0) {
         
            if (stepSize >= 0) {
         
              let scrollX = el.scrollLeft + stepSize >= position ? position :  el.scrollLeft + stepSize
              el.scrollLeft = scrollX
            } else {
         
              let scrollX = el.scrollLeft + stepSize <= position ? position :  el.scrollLeft + stepSize
              el.scrollLeft = scrollX
            }
            
          } else {
         
            clearInterval(timer)
          }
        }, TIME_EVERY_STEP)
      }
    }
    
    
  • 3、我们来处理下滑块的位置,因为滑块的位置是根据被选中项的getBoundingClientRect的属性值决定的;所以我们只要保证,在滑块获取getBoundingClientRect属性是在页面渲染之后即可;所以我们可以使用nextTick保证在页面dom元素发生变化后改变滑块的值

    watch(curretIndex, (newIndex, oldIndex) => {
         
      // 保证渲染之后再进行计算元素的位置, 在这里加上nextTick
      nextTick(() => {
         
        // 获取点击元素的距离左边屏幕的距离和元素的宽度
        const liEle = Array.from(liEles.value)[newIndex]
        if (!liEle) return false
        const {
          left, width } = liEle.getBoundingClientRect()
        sliderStyle.value = {
         
          left: `${
           left + ulScrollLeft.value}px`,
          width: `${
           width}px`
        }
      })
    })
    

实现代码

<template>
  <ul
    class="relative z-10 text-sm bg-white flex overflow-auto p-1 text-zinc-600"
    ref="ulEle"
  >
    <li
      class="absolute top-1 h-[22.5px] bg-zinc-900 rounded-lg duration-200 z-10"
      :style="sliderStyle"
    ></li>
    <li
      v-for="(category, index) in data"
      :key="category.id"
      class="shrink-0 px-1.5 py-0.5 last:mr-6 z-10"
      :class="{ 'text-zinc-50': index === curretIndex }"
      @click="handleSelectCategory(index)"
      :ref="storeLiEle"
    >
      {
   {
    category.name }}
    </li>
  </ul>
  <svg-icon
    name="hamburger"
    class="px-1 w-4 h-4 fixed top-0 right-[-2px] z-20 shadow-l-white bg-white"
    @click="visible = true"
  />
  <popup v-model="visible" class="aaa" style="color: red">
    <Menu :categorys="data" @handleSelectCategory="handleSelectCategory" />
  </popup>
</template>

<script setup>
import {
    ref, watch, nextTick } from 'vue'
import {
    useScroll } from '@vueuse/core'
import Menu from '@/views/main/components/menu/index.vue'
import {
    scrollTransition } from '@/utils'
const run = scrollTransition()
const props = defineProps({
   
  data: {
   
    type: Array,
    required: true
  }
})

// 默认选中索引
const curretIndex = ref(-1)
const sliderStyle = ref({
   
  left: '10px',
  width: '0px',
  bottom: 0,
})
//  ul dom元素
const ulEle = ref(null)
// li dom元素容器
const liEles = ref(new Set())
// 每一项在屏幕中央时,需要向左滚动的距离
const scrollRaces = ref([])

// ulScrollLeft 向左滚动的距离
const {
    x: ulScrollLeft } = useScroll(ulEle)

const visible = ref(false)

// 选中索引
const handleSelectCategory = (index) => {
   
  curretIndex.value = index
  visible.value = false
  // ulEle.value.scrollTo(scrollRaces.value[index], 0)
  run({
    el: ulEle.value, position: scrollRaces.value[index], direction: 'l', time: 200 })
}
// 获取v-for遍历的子元素dom节点时,需要使用回调函数获取; 注意: 每次页面更新之后storeLiEle,都会重新执行一遍,这样会导致liEles中存储的都是重复的元素
// 所以可以使用Set来存储数据,避免存入重复的数据, 也可以在obBeforeUpdate前设置liEles.value的值为初始化值
const storeLiEle = (el) => {
   
  liEles.value.add(el)
}

watch(curretIndex, (newIndex, oldIndex) => {
   
  // 保证渲染之后再进行计算元素的位置
  nextTick(() => {
   
    // 获取点击元素的距离左边屏幕的距离和元素的宽度
    const liEle = Array.from(liEles.value)[newIndex]
    if (!liEle) return false
    const {
    left, width, height } = liEle.getBoundingClientRect()
    sliderStyle.value = {
   
      left: `${
     left + ulScrollLeft.value}px`,
      width: `${
     width}px`,
      height: `${
     height}px`
    }
  })
}, {
   
  immediate: true
})

// 监听data初次数据渲染之后,将slider条设置到第一项
watch(
  () => props.data,
  () => {
   
    nextTick(() => {
   
      if (props.data.length <= 0) return
      curretIndex.value = 0
      // 获取1/2屏幕的宽度
      const halfScreenWidth = window.innerWidth / 2
      // 每一项向左滚动的距离 = 每一项距离屏幕左边的距离 - 1/2屏幕的宽度 + 1/2自身的宽度 
      scrollRaces.value = Array.from(liEles.value).map(el => el.getBoundingClientRect().left - halfScreenWidth + el.offsetWidth / 2)
    })
  }, {
   
    immediate: true
  }
)
</script>

<style scoped>
/* ul {
  scroll-behavior: smooth;
} */
</style>

20220822_104005

13、封装通用组件 - popup

当我们点击面包屑按钮时,会有一个弹出窗口 popup自低而上弹出,那么这样的一个功能,我们一样可以把它处理为项目的通用组件
那么想要处理popup的话,首先就需要先搞清楚 popup的能力。

  • 1.当 popup展开时,内容视图应该不属于任何一个组件内部,而应该直接被插入到 body下面

  • 2、popup应该包含两部分内容,一部分为背景蒙版,一部分为内容的包裹容器

  • 3、popup应该通过一个双向绑定进行控制展示和隐藏

  • 4、popup展示时,滚动应该被锁定

  • 5、内容区域应该接收所有的attrs,并且应该通过插槽让调用方指定其内容

那么明确好了这些能力之后,接下来大家可以先根据这些能力进行下通用组件 popup 的构建尝试,尝试之后再继续来看咱们的后续内容。

libs/popup/index.vue

<template>
  <Teleport to="body">
    <Transition name="popup-mask" mode="out-in">
      <!-- 遮罩层 -->
      <div
        class="fixed left-0 top-0 right-0 bottom-0 bg-black/80 z-30"
        @click="onMask"
        v-if="modelValue"
      ></div>
    </Transition>

    <Transition name="popup-slide" mode="out-in">
      <!-- 内容区域 -->
      <div
        class="bg-white overflow-y-auto z-30 fixed left-0 bottom-0 right-0"
        :style="style"
        v-bind="$attrs"
        v-if="modelValue"
      >
        <slot />
      </div>
    </Transition>
  </Teleport>
</template>

<script setup>
import { watch } from 'vue'
const props = defineProps({
  modelValue: Boolean,
  style: String | Object
})
const emits = defineEmits(['update:modelValue'])

const onMask = () => {
  emits('update:modelValue', false)
}

watch(
  () => props.modelValue,
  (v) => {
    const body = document.querySelector('body')
    let initStyle = ''
    if (v) {
      initStyle = body.style.overflow
      body.style.overflow = 'hidden'
    } else {
      body.style.overflow = initStyle
    }
  }
)
</script>

<style scoped lang="scss">
.popup-mask-enter-from,
.popup-mask-leave-to {
  opacity: 0;
}
.popup-mask-enter-active,
.popup-mask-leave-active {
  transition: all 0.3s;
}

.popup-slide-enter-from,
.popup-slide-leave-to {
  transform: translateY(100%);
}
.popup-slide-enter-active,
.popup-slide-leave-active {
  transition: all 0.3s;
}
</style>

通用组件注册

import SvgIcon from './svg-icon/index.vue'
import Popup from './popup/index.vue'

// 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
export default {
   
  install(app) {
   
    app.component('svg-icon', SvgIcon)
    app.component('Popup', Popup)
  }
}

在使用通用组件

  <Popup v-model="visible" class="aaa" style="color: red" />
 const visible = ref(false)

20220820_172315

14、Vite通用组件自动化注册

目前我们在项目中已经完成了两个通用组件,将来我们还会完成更多的通用组件开发。那么如果每次开发完成一个通用组件之后,都去手动进行注册,未免有些过于麻烦了,所以我们期望通过 vite 提供的功能,进行通用组件的自动化注册
那么,如果想要完成这个功能的话,就需要使用到两个关键的知识点:

  • 1、vite的Glob 导入功能:该功能可以帮助我们在文件系统中导入多个模块

    const modules = import.meta.glob('./dir/*.js')
    // 以上将会被转译为下面的样子:
    const modules = {
         
      './dir/foo.js': () => import('./dir/foo.js'),
      './dir/bar.js': () => import('./dir/bar.js')
    }
    
  • 2、vue的 defineAsyncComponent方法:该方法可以创建一个按需加载的异步组件
    基于以上两个方法,实现组件自动注册

我们先来看下现在的代码

import SvgIcon from './svg-icon/index.vue'
import Popup from './popup/index.vue'

// 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
export default {
   
  install(app) {
   
    app.component('svg-icon', SvgIcon)
    app.component('Popup', Popup)
  }
}

改成动态导入的形式

import {
    defineAsyncComponent } from 'vue'

// 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
export default {
   
  install(app) {
   
    // 1、获取当前文件下所有以index.vue结尾的文件
    const components = import.meta.glob('./*/index.vue')
    for (const [path, fn] of Object.entries(components)) {
   
      // 2、根据path生成组件名称, defineAsyncComponent生成动态组件
      const componentName = path.replace(/(\.\/)|(\/index\.vue)/g, '')
      const Com = defineAsyncComponent(fn)
      // 3、将组件注册到app上
      app.component(componentName, Com)
    }
  }
}

15、封装通用的组件 - button

需要实现的组件如下

image-20220823102101628

实现代码

<template>
  <button
    class="duration-300 inline-flex items-center justify-center active:scale-105"
    :class="[
      sizeClass,
      typeClass,
      plainClass,
      block ? 'block' : '',
      { 'opacity-50 active:scale-100': isDisbaled }
    ]"
    :disabled="isDisbaled"
    @mouseover="mouseIsOver = true"
    @mouseleave="mouseIsOver = false"
  >
    <svg-icon
      v-if="loading"
      name="loading"
      class="w-[1em] h-[1em] duration-300 animate-spin"
      :class="{ 'mr-0.5': !!$slots.default || icon }"
      :color="svgColorClass"
    />
    <svg-icon
      v-if="icon"
      :name="icon"
      class="w-[1em] h-[1em] duration-300"
      :class="{ 'mr-0.5': !!$slots.default && icon }"
      :color="svgColorClass"
    />
    <slot />
  </button>
</template>

<script>
const defineType = {
  primary:
    'bg-blue-400 hover:bg-blue-500 duration-300 text-white rounded-sm border border-blue-400',
  warning:
    'bg-amber-400 hover:bg-amber-500 duration-300 text-white rounded-sm border border-amber-400',
  danger:
    'bg-red-400 hover:bg-red-500 duration-300 text-white rounded-sm border border-red-400',
  success:
    'bg-emerald-400 hover:bg-emerald-500 duration-300 text-white rounded-sm border border-emerald-400',
  default:
    'bg-white hover:bg-zinc-200 duration-300 text-zinc-600 rounded-sm border border-white-400'
}

const defineSize = {
  small: 'py-0.5 px-0.5 text-xs',
  middle: 'py-[6px] px-1 text-sm',
  default: 'py-[8px] px-1.5 text-sm',
  large: 'py-1 px-2 text-sm'
}
</script>

<script setup>
import { computed, ref, useSlots } from 'vue'
// const slot = useSlots()
// console.log(slot.default)
const mouseIsOver = ref(false)
const props = defineProps({
  type: {
    type: String,
    default: 'primary', // 'primary', 'warning', 'danger', 'success', 'default'
    validator(key) {
      const isContant = Object.keys(defineType).includes(key)
      if (!isContant) {
        throw new Error(
          `type must be 【${Object.keys(defineType).join('、')}】`
        )
      }
      return true
    }
  },
  size: {
    type: String,
    default: 'middle', // large , default, middle, small
    validator(key) {
      const isContant = Object.keys(defineSize).includes(key)
      if (!isContant) {
        throw new Error(
          `size must be 【${Object.keys(defineSize).join('、')}】`
        )
      }
      return true
    }
  },
  icon: {
    type: String
  },
  loading: {
    type: Boolean,
    default: false
  },
  block: {
    type: Boolean,
    default: false
  },
  plain: {
    type: Boolean,
    default: false
  },
  icon: {
    type: String
  },
  disabled: {
    type: Boolean,
    default: false
  }
})

const typeClass = computed(() =>
  defineType[props.type] ? defineType[props.type] : defineType.primary
)

const sizeClass = computed(() =>
  defineSize[props.size] ? defineSize[props.size] : defineType.middle
)

const plainClass = computed(() =>
  props.plain
    ? `bg-transparent ${
        props.type === 'primary'
          ? 'text-blue-400 hover:text-white'
          : props.type === 'warning'
          ? 'text-amber-400 hover:text-white'
          : props.type === 'danger'
          ? 'text-red-400 hover:text-white'
          : props.type === 'success'
          ? 'text-emerald-400 hover:text-white'
          : props.type === 'default'
          ? 'text-zinc-700 hover:text-white'
          : ''
      }`
    : ''
)
const svgColorClass = computed(() =>
  props.plain && !mouseIsOver.value
    ? `${
        props.type === 'primary'
          ? 'rgb(96, 165, 250)'
          : props.type === 'default'
          ? 'rgb(63, 63, 70)'
          : props.type === 'danger'
          ? 'rgb(248, 113, 113)'
          : props.type === 'success'
          ? 'rgb(52, 211, 153)'
          : props.type === 'warning'
          ? 'rgb(251, 191, 36)'
          : '#ffffff'
      }`
    : '#ffffff'
)
const isDisbaled = computed(() => props.disabled || props.loading)
</script>

<style></style>

16、封装通用组件 - popover

通用组件popover应具备以下功能:

  • 1、指定两个插槽、分别插入触发内容和弹出内容
  • 2、触发弹出内容的方式分为多种,clickhoverfocusmanual
  • 3、可以设定弹出层相对于触发元素的位置 bottom,bottom-start, bottom-end, top, top-start, top-end
  • 4、将弹出层指定挂载到body元素上、并且当页面滚动和页面尺寸发生变化时、弹出层也应虽则触发元素的位置改变而改变
  • 5、弹出层展示和隐藏时要有过渡效果

实现思路

  • 1、对用户指定的属性值进行校验
  • 2、当页面挂载之后获取父元素的 宽度高度距离屏幕左边left距离屏幕顶边top
  • 3、当触发弹出元素显示后,立即获取显示元素的宽度高度, 结合触发元素的属性与显示的位置,计算出弹出元素应该显示到的位置 left, top
  • 4、当页面滚动/尺寸发生改变、重新计算生成新的显示到的位置 left, top
  • 5、根据触发方式对应的显示和隐藏弹出元素;(注意: 在hover触发下、鼠标触发元素触发弹出元素显示后、然后再移动到显示元素上时,我们需要处理一下,避免弹出层先隐藏再展示的bug; 处理方法可以使用setTimeout延时修改元素的隐藏、在定时器触发之前、如果触发元素的显示、则先清理定时器)

实现代码

<template>
  <div ref="popoverRoot" class="select-none inline-flex" @click.stop>
    <slot name="reference" />
  </div>
  <Teleport to="body">
    <transition name="popover-tip">
      <div
        v-if="tipVisible"
        ref="tipRoot"
        class="fixed shadow-lg p-1 rounded-sm border border-zinc-100 z-20 bg-white"
        :style="tipStyle"
        @click.stop
      >
        <slot />
      </div>
    </transition>
  </Teleport>
</template>

<script>
const PLACEMENTS = [
  'bottom',
  'bottom-start',
  'bottom-end',
  'top',
  'top-start',
  'top-end'
]
const TRIGGERS = ['click', 'focus', 'hover', 'manual']
</script>

<script setup>
import { ref, watch, computed, nextTick } from 'vue'
import useRootPosition from './useRootPosition'
import useTrigger from './useTrigger'
const props = defineProps({
  placement: {
    // 弹框显示位置
    type: String,

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

25.基于springboot + vue实现的前后端分离-停车管理系统(项目 + 论文)

项目介绍 系统包含用户和管理员两个角色 用户&#xff1a;登录、注册、个人中心、预定停车位、缴费信息 管理员&#xff1a;登录、用户信息管理、车位信息管理、车位费用管理、停泊车辆管理、车辆进出管理、登录日志查询 技术选型 开发工具&#xff1a;IDEA 服务器&#…

本科毕业设计:计及并网依赖性的分布式能源系统优化研究。(C语言实现)(内包含NSGA II优化算法)(一)

目录 前言 1、分布式能源系统模型介绍 2、运行策略 前言 本篇文章介绍的是我的毕业设计&#xff0c;我将C语言将其实现。 1、分布式能源系统模型介绍 这是我将研究的分布式能源系统的框架&#xff0c;内部供能装置包括&#xff1a;太阳能光伏板&#xff1b;sofc燃料电池、太阳…

Leetcode1642. 可以到达的最远建筑

Every day a Leetcode 题目来源&#xff1a;1642. 可以到达的最远建筑 解法1&#xff1a;反悔贪心 在移动的过程中&#xff0c;我们会需要若干次需要使用砖块或者梯子的情况。假设当前我们需要移动到下一建筑物&#xff0c;但必须使用 1 架梯子或者 Δh 个砖块&#xff0c;那…

docker 创建RedHat8.5镜像

确定要创建的小红帽版本&#xff0c;可以进入官网查看 https://hub.docker.com/search?qRedHat 复制命令到安装docker的机器上&#xff0c;拉取小红帽镜像。 docker pull redhat/ubi8:latest 执行完成后&#xff0c;查看镜像是否拉取成功 docker images |grep redhat 如图…

《Spring Security 简易速速上手小册》第3章 用户认证机制(2024 最新版)

文章目录 3.1 认证流程3.1.1 基础知识详解认证流程的核心概念认证流程的步骤 3.1.2 主要案例&#xff1a;内存用户认证案例 Demo&#xff1a;快速启动你的 Spring Boot 守护程序 3.1.3 拓展案例 1&#xff1a;数据库用户认证案例 Demo&#xff1a;让数据库守护你的秘密 3.1.4 拓…

vue3三级嵌套复选框(element-plus)

一、功能描述 当选择第一级的复选框时下面所有内容全选和取消全选&#xff0c;当选择第二的复选框时第三级的所有内容全选和取消全选。只要有一个第三级的内容没有选&#xff0c;二级和一级则不能勾上。第三级内容全选上了&#xff0c;第二级复选框就钩上。第二级也是同样的道理…

【暗月安全】2021年渗透测试全套培训视频

参与培训需要遵守国家法律法规&#xff0c;相关知识只做技术研究&#xff0c;请勿用于违法用途&#xff0c;造成任何后果自负与本人无关。 中华人民共和国网络安全法&#xff08;2017 年 6 月 1 日起施行&#xff09; 第二十二条 任何个人和组织不得从事入侵他人网络、干扰他…

折线图实现柱状阴影背景的demo

这个是一个由官网的基础折线图实现的流程&#xff0c;将涉及到的知识点附上个人浅薄的见解&#xff0c;源码在最后&#xff0c;需要的可自取。 折线图 成果展示代码注解参数backgroundColordataZoomlegendtitlexAxisyAxisgridseries 源码 成果展示 官网的基础折线图&#xff…

Android之Handler原理解析与问题分享

一、Handler运行原理剖析 1.关系剖析图 如果把整个Handler交互看做一个工厂&#xff0c;Thread就是动力MessageQueue是履带Looper是转轴Loooper的loop方法就是开关&#xff0c;当调用loop方法时整个工厂开始循环工作&#xff0c;处理来自send和post提交到MessageQueue的消息&a…

Nodejs 第四十五章(redis发布订阅+事务)

发布订阅 发布-订阅是一种消息传递模式&#xff0c;其中消息发布者&#xff08;发布者&#xff09;将消息发送到频道&#xff08;channel&#xff09;&#xff0c;而订阅者&#xff08;订阅者&#xff09;可以订阅一个或多个频道以接收消息。这种模式允许消息的解耦&#xff0…

006-CSS-常见问题汇总

常见问题汇总 1、伪元素与伪类2、偏门但好用的样式3、文字溢出三个点展示4、空白折叠问题5、文字的垂直居中6、 Vue项目中 在父组件中修改子组件样式7、BFC 概念7.1、兄弟元素外边距合并7.2、父子元素外边距塌陷 8、box-sizing8.1、box-sizing: border-box8.2、box-sizing: con…

11. Nginx进阶-HTTPS

简介 基本概述 SSL SSL是安全套接层。 主要用于认证用户和服务器&#xff0c;确保数据发送到正确的客户机和服务器上。 SSL可以加密数据&#xff0c;防止数据中途被窃取。 SSL也可以维护数据的完整性&#xff0c;确保数据在传输过程中不被改变。 HTTPS HTTPS就是基于SSL来…

1.1_2 性能指标——速率、带宽、吞吐量

文章目录 1.1_2 性能指标——速率、带宽、吞吐量&#xff08;一&#xff09;速率&#xff08;二&#xff09;带宽&#xff08;三&#xff09;吞吐量 1.1_2 性能指标——速率、带宽、吞吐量 &#xff08;一&#xff09;速率 速率即数据率或称数据传输率或比特率。 速率就是“快…

【代码】Python3|无GUI环境中使用Seaborn作图的学习路线及代码(阴影折线图)

我有个需求是需要画图&#xff0c;让GPT帮我生成了一下学习计划。 学习路线依照GPT的来的&#xff0c;使用的Prompt工具是https://github.com/JushBJJ/Mr.-Ranedeer-AI-Tutor。 文章目录 PrerequisiteMain Curriculum1.1 Seaborn介绍Seaborn基础保存图形为文件练习 1.2 单变量数…

瑞芯微RK3588 C++部署Yolov8检测和分割模型

最近这一个月在研究国产瑞芯微板子上部署yolov8的检测和分割模型&#xff0c;踩了很多坑&#xff0c;记录一下部署的过程和遇到的一些问题&#xff1a; 1 环境搭建 需要的环境和代码主要包括&#xff1a; &#xff08;1&#xff09;rknn-toolkit2-1.5.2&#xff1a;工具链&am…

uniapp开发android原生插件

一、下载原生开发SDK Android 离线SDK - 正式版 | uni小程序SDK (dcloud.net.cn)、 https://nativesupport.dcloud.net.cn/AppDocs/download/android.html 将开发uniappa原生android的插件解压到ben本地目录&#xff0c;目录结构如下&#xff1a; 接下就可以使用 UniPlugin-Hel…

12 状态优先级

概念 cpu需要执行很多进程&#xff0c;有很多进程排在队列中&#xff0c;每个进程加载后运行一定的时间段&#xff0c;然后切换下一个进程。cpu如何判断进程需不需要加载&#xff0c;什么时候加载&#xff0c;依靠进程的状态和优先级属性来判断&#xff0c;进程调度&#xff0…

Node.js与Webpack笔记(一)

这里使用的16.19.0版本&#xff0c;官网和github没找到&#xff0c;去黑马2023年课程里找 篇幅较大会卡&#xff0c;此篇幅不写Webpack部分&#xff0c;留着下一篇 初识 1.什么是Node.js? Node.js 是一个独立的 JavaScript 运行环境&#xff0c;能独立执行 JS 代码&#xff…

A/D转换

硬件电路模型 模数转换代码 main.c #include <REGX52.H> #include "LCD1602.h" #include "Delay.h" #include "XPT2046.h"unsigned int ADValue; int main(){LCD_Init();LCD_ShowString(1,1,"ADJ NTC RG");while(1){ADValue …

iOS 17.0 UIGraphicsBeginImageContextWithOptions 崩溃处理

在升级到iOS17后你会发现&#xff0c;之前版本运行的很好&#xff0c;这个版本突然会出现一个运行闪退。报错日志为*** Assertion failure in void _UIGraphicsBeginImageContextWithOptions(CGSize, BOOL, CGFloat, BOOL)(), UIGraphics.m:410 跟踪到具体的报错位置如下所示&a…