Skip to content

Vue

通过响应数据动态修改css的变量值

方法一:

vue
<template>
  <div class="root">
    <!-- Your component content here -->
      <div class="right" :style="{ '--foo': color }"></div>
  </div>
</template>

<script setup lang="ts">
  import { ref } from 'vue';
  const color = ref<string>('#FF0000');
</script>

<style scoped lang="less">
  .root {
    .right {
        flex: 1;
        background-color: var(--foo, #7f583f);//--foo变名,#7f583f是默认值
      }
  }
  /* Your styles here */
</style>

方法二(vue3中)通过v-bind:

vue
<template>
  <div class="root">
    <!-- Your component content here -->
      <div class="right"></div>
  </div>
</template>

<script setup lang="ts">
  import { ref } from 'vue';
  const color = ref<string>('#FF0000');
</script>

<style scoped lang="less">
  .root {
    .right {
        flex: 1;
        background-color: v-bind(color);
      }
  }
  /* Your styles here */
</style>

vue3中通过ref获取元素的原始信息比如宽

vue
<template>
	<div ref="demo"> </div>
</template>
<script setup>
    const demo = ref(null)
    demo.value.$el.offsetWidth //元素的宽度
</script>

formatComponentInstance 函数通过创建包装组件和使用 Map 来缓存和管理这些组件实例

vue
<template>
  <div class="index">
    <div class="right">
      <div class="routerView">
        <router-view v-slot="{ Component, route }">
          <keep-alive :include="['/task']">
            <component :is="formatComponentInstance(Component, route)" :key="route.fullPath" />
          </keep-alive>
        </router-view>
      </div>
    </div>

  </div>
  <!-- 创建任务弹窗 -->
</template>

<script setup>

// 用来存已经创建的组件
const wrapperMap = new Map();
// 将router传个我们的组件重新换一个新的组件,原组件包里面
function formatComponentInstance(component, route) {
  let wrapper;
  if (component) {
    const wrapperName = route.path;
    if (wrapperMap.has(wrapperName)) {
      wrapper = wrapperMap.get(wrapperName);
    } else {
      wrapper = {
        name: wrapperName,
        render() {
          return h(component);
        },
      };
      wrapperMap.set(wrapperName, wrapper);
    }
    return h(wrapper);
  }
}

</script>

<style lang="scss" scoped>

</style>

formatComponentInstance 函数的主要作用是通过对 Vue 路由组件进行包装,以实现组件缓存的优化和管理。具体来说,它创建一个新的组件实例来包装原始的路由组件,并使用一个 Map 来存储这些包装过的组件,从而避免每次路由切换时都创建新的组件实例。

详细解释 formatComponentInstance 的作用

  1. 缓存组件实例: 这个函数使用 wrapperMap(一个 Map 对象)来存储已经创建的组件包装实例。每个路由路径(route.path)对应一个包装组件。这种方法可以避免重复创建相同路由的组件实例,提高性能。
  2. 组件包装:formatComponentInstance 被调用时,它会检查当前路由路径是否已经有一个包装组件。如果有,它会直接从 wrapperMap 中获取这个包装组件。如果没有,它会创建一个新的包装组件并存储在 wrapperMap 中。
  3. 使用 h 函数动态渲染组件: h(component) 是 Vue 3 中的一个函数,用来创建 VNode。通过 h(component),包装组件可以动态渲染传入的路由组件。这种包装组件的创建方式允许对组件进行定制,比如添加额外的逻辑或状态。
  4. 返回包装后的组件: 最后,formatComponentInstance 返回一个包装后的组件实例,供 router-view 使用。这样,当用户切换到不同的路由时,keep-alive 组件可以根据 wrapperMap 进行组件实例的缓存和复用,而不是每次都重新创建。

代码的执行流程

  • 路由访问: 当用户访问特定路由时,router-view 触发并提供当前路由对应的 Componentroute
  • 调用 formatComponentInstance: 使用 Componentroute 调用 formatComponentInstance,根据 route.path 来判断是否已经存在包装组件。
  • 返回或创建包装组件: 如果包装组件已经存在,则直接返回;如果不存在,则创建新的包装组件,并将其存储在 wrapperMap 中。
  • 渲染包装组件: 使用 keep-alive 包裹,确保需要缓存的路由组件在切换时不会被销毁,从而保留其状态。

总结

formatComponentInstance 函数通过创建包装组件和使用 Map 来缓存和管理这些组件实例,帮助实现路由组件的缓存和状态保留。这种设计提高了应用的性能,特别是在需要保留组件状态的复杂场景中。

useDefer() 解决vue白屏时间

js
import {ref} from 'vue'

export function useDefer(maxCount = 100){ //最大帧率
    const frameCount = ref(0) //目前渲染了多少帧
    let rafId
    function updateFrameCount(){
       rafId = requestAnimationFrame(()=>{
            frameCount.value++
            if(frameCount.value >= maxCount) return
            updateFrameCount()
        })
    }
    updateFrameCount()
    onUnMounted(()=>{
        cancelAnimationFrame(rafId)
    })
    return function defer(n){
        return frameCount.value >= n
    }
}

组件中使用

vue
<template>
  <div>
   <div v-for="(item,index) in 100">
       <Hello v-if="defer(n)"></Hello>
   </div>
  </div>
</template>

<script setup>
import { useDefer } from '../../hooks/useDefer'
import { Hello } from '../../components/Hello'
const defer = useDefer()

</script>

<style lang="scss" scoped></style>

Hooks

usePagination 分页

js
import { reactive } from 'vue';
import _ from 'lodash';
const defaultPaginationData = {
  pageNo: 1,
  pageSize: 10,
  total: 0,
  pageSizes: [5, 10, 15],
  layout: 'total, sizes, prev, pager, next, jumper',
};
/**
 * @description 分页hooks
 * @param {defaultPaginationData} [_paginationData] 分页配置项
 * @param {function } [fn] pageNo/pageSize改变的时候触发函数
 */
export function usePagination(_paginationData = {}, fn) {
  const paginationData = reactive({ ...defaultPaginationData, ..._paginationData });

  const handleCurrentChange = (value) => {
    paginationData.pageNo = value;
    fn && fn();
  };

  const handleSizeChange = _.debounce((value) => {
    paginationData.pageNo = 1;
    paginationData.pageSize = value;
    fn && fn();
  }, 500);

  return {
    paginationData,
    handleCurrentChange,
    handleSizeChange,
  };
}

useMessageBox 用于提示用户操作

需要配合element-plus

js
import { ElMessageBox, ElMessage } from 'element-plus';
export function useMessageBox() {
  /**
   * @description 确认弹窗
   * @param {object} options
   * @param {string} [options.content] 提示内容
   * @param {string} [options.cancelAlert] 取消后的提示内容
   * @param {string} [options.confirmAlert] 确认后的提示的内容
   * @param {function} [options.success] 操作成功的回调函数
   * @param {function} [options.cancle] 操作取消的回调函数
   */
  function dialogShow(options) {
    ElMessageBox.confirm(options.content || '确认当前操作', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
      'lock-scroll': false, //保持body标签的宽度不变
    })
      .then(() => {
        options.success && options.success();
        ElMessage({
          type: 'success',
          message: options.confirmAlert || '操作成功',
        });
      })
      .catch(() => {
        options.cancle && options.cancle();
        ElMessage({
          type: 'info',
          message: options.cancelAlert || '操作取消',
        });
      });
  }
  return {
    dialogShow,
  };
}

useLoading 全局加载动画

需要配element-plus,建议在app.vue中引入,用provide提供全局确保loading是同一个作用域下的

js
import { ElLoading } from 'element-plus';
export function useLoading() {
  const loading = ref(null);
  /**
   * @description 加载loading
   * @param {object} options loading配置项
   * @param {string} options.target 需要加载的目标
   * @param {boolean} options.body 需要加载的目标
   * @param {boolean} options.fullscreen 全屏
   * @param {string} options.text 加载文字
   * @param {string} options.background 背景颜色
   * @param {string} options.customClass 自定义类名
   * @param {string} options.spinner 自定义图标
   * @param {function} callBack 回调
   * @param {ElLoading} callBack.value 拿到ElLoading实列
   */
  function loadingShow(options, callBack) {
    loading.value = ElLoading.service({
      lock: true,
      text: 'Loading',
      background: 'rgba(0, 0, 0, 0.7)',
      ...options,
    });
    callBack && callBack(loading.value);
  }
  return { loadingShow, loading };
}

创建计算属性

js
import { computed } from 'vue'
export function useComputed(fn) {
    const map = new Map();
    return function (...args) {
        const key = JSON.stringify(args);
        if(map.has(key)) return map.get(key)
        const result = computed(() => fn(...args))
        map.set(key, result)
        return result
    }
}

Vue自定义指令

注册指令

js
//main.js
import './assets/main.css';

import { createApp } from 'vue';
import App from './App.vue';
import 'normalize.css';
import 'element-plus/dist/index.css';
import router from './router';
import pinia from './stores';
import lazyLoad from './directives/lazyLoad';
import loadMore from './directives/loadMore';
const app = createApp(App);
app.directive('lazyload', lazyLoad);
app.directive('loadMore', loadMore);
app.use(router).use(pinia).mount('#app');

v-lazy

js
// lazyLoad.js
// 图片懒加载
export default {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([{ isIntersecting }]) => {
      if (isIntersecting) {
        // 懒加载
        el.src = binding.value;
        // 取消监听
        observer.unobserve(el);
      }
    });
    observer.observe(el);
  },
};
//main.js

v-loadMore

js
let observer;
export default {
  mounted(el, binding) {
    observer = new IntersectionObserver(
      ([{ isIntersecting }]) => {
        if (isIntersecting) {
          // 懒加载
          binding.value();
        }
      },
      {
        threshold: 0,
      },
    );
    observer.observe(el);
  },
  beforeUnmount(el, binding, vnode) {
    observer.unobserve(el);
  },
};

使用

vue
<template>
	 <div class="drawer">
      <el-drawer v-model="drawer" direction="rtl" :lock-scroll="false">
        <el-scrollbar height="100%">
          <ul class="preview">
            <li v-for="item in images" :key="item.fileName">
              <div class="img">
                <img v-lazyload="item.filePath" src="@renderer/assets/images/empty.png" alt="" />
              </div>
              <div class="title"></div>
            </li>
          </ul>
             <!-- 当该元素出现的时候执行loading函数  -->
          <div class="loading" v-if="loading" v-loadMore="load"><text style="color: black">加载中...</text></div>
        </el-scrollbar>
      </el-drawer>
    </div>
</template>
<script>
	const loading = ref(true);
    const load = () => {
      console.log('load');
      if (pageSize * (pageNo - 1) < detailData.value.assetsList.length) {
        loading.value = true;
        images.value.push(...detailData.value.assetsList.slice((pageNo - 1) * pageSize, pageNo *pageSize));
        console.log(images.value);
        pageNo++;
    } else {
        loading.value = false;
      }
    };
</script>

录音

HTML
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>录音功能</title>
</head>
<body>
    <h1>录音功能示例</h1>
    <button id="startRecord">开始录音</button>
    <button id="stopRecord" disabled>停止录音</button>
    <audio id="audioPlayback" controls></audio>
    <script src="record.js"></script>
</body>
</html>
<script>
	let mediaRecorder;
    let audioChunks = [];

    document.getElementById('startRecord').addEventListener('click', async () => {
        if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                mediaRecorder = new MediaRecorder(stream);

                mediaRecorder.ondataavailable = (event) => {
                    audioChunks.push(event.data);
                };

                mediaRecorder.onstop = () => {
                    const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
                    const audioUrl = URL.createObjectURL(audioBlob);
                    const audio = document.getElementById('audioPlayback');
                    audio.src = audioUrl;
                    audioChunks = [];
                };

                mediaRecorder.start();
                document.getElementById('startRecord').disabled = true;
                document.getElementById('stopRecord').disabled = false;
            } catch (err) {
                console.error('录音失败:', err);
                alert('无法访问麦克风,请检查权限设置。');
            }
        } else {
            alert('您的浏览器不支持录音功能');
        }
    });

    document.getElementById('stopRecord').addEventListener('click', () => {
        mediaRecorder.stop();
        document.getElementById('startRecord').disabled = false;
        document.getElementById('stopRecord').disabled = true;
    });
</script>

echarts立体柱状图

方法一:

js
const data = [220, 182, 191, 234, 290, 330, 310, 100];
let option = {
      tooltip: {
        trigger: 'axis',
        formatter: '{b} : {c}',
        axisPointer: {
          // 坐标轴指示器,坐标轴触发有效
          type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
        },
      },
      grid: {
        top: 30,
        bottom: 40,
      },
      xAxis: {
        data: ['高等植物', '哺乳动物', '鸟类', '两栖动物', '陆生昆虫', '水生生物', '爬行动物', '大型真菌'],
        //坐标轴
        axisLine: {
          lineStyle: {
            color: '#3eb2e8',
          },
        },
        //坐标值标注
        axisLabel: {
          show: true,
          color: '#F1F7F4',
          rotate: -30,
        },
      },
      yAxis: {
        name: '单位:种',
        nameTextStyle: {
          color: '#F1F7F4',
          fontSize: 12,
          padding: [0, 0, 0, -10], //name文字位置 对应 上右下左
        },

        //坐标轴
        axisLine: {
          show: false,
        },
        //坐标值标注
        axisLabel: {
          show: true,
          color: '#F1F7F4',
        },
        //分格线
        splitLine: {
          lineStyle: {
            color: '#4784e8',
          },
        },
      },
      series: [
        {
          // /数据图
          data: data,
          type: 'bar',
          barWidth: 10,
          label: {
            show: true,
            color: '#fff',
          },
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
              { offset: 0, color: '#36d2ff' },
              { offset: 1, color: '#d0dd5f' },
            ]),
          },
          z: 1,
        },
        {
          // /数据图
          data: data,
          type: 'bar',
          barWidth: 10,
          barGap: 0,//两条柱状图之间的距离
          label: {
            show: true,
            color: '#fff',
          },
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
              { offset: 0, color: '#36d2ff' },
              { offset: 1, color: '#d0dd5f' },
            ]),
          },
          z: 1,
        },
        {
          // 最上面菱形
          data: data,
          type: 'pictorialBar',
          symbol: 'diamond',
          symbolSize: ['20', '10'],
          symbolPosition: 'end',
          symbolOffset: ['', '-50%'],
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
              { offset: 0, color: '#d0dd5f' },
              { offset: 1, color: '#939c3d' },
            ]),
          },
          z: 2,
        },
        {
          // 最下面的菱形
          data: data,
          type: 'pictorialBar',
          symbol: 'diamond',
          symbolSize: ['20', '10'],
          symbolPosition: 'start',
          symbolOffset: ['', '50%'],
          itemStyle: {
            color: '#36d2ff',
          },
          z: 2,
        },
      ],
    }

一条数据绘制两条柱状和上下面的菱形,注意第二条柱状图的barGap: 0,不然就不会挨在一起。这种方式实际效果两条柱状图之间还是有点缝隙。效果不佳

方法二:

js
const data = [220, 182, 191, 234, 290, 330, 310, 100];
let {
      tooltip: {
        trigger: 'axis',
        formatter: '{b} : {c}',
        axisPointer: {
          // 坐标轴指示器,坐标轴触发有效
          type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
        },
      },
      grid: {
        top: 30,
        bottom: 40,
      },
      xAxis: {
        data: ['高等植物', '哺乳动物', '鸟类', '两栖动物', '陆生昆虫', '水生生物', '爬行动物', '大型真菌'],
        //坐标轴
        axisLine: {
          lineStyle: {
            color: '#3eb2e8',
          },
        },
        //坐标值标注
        axisLabel: {
          show: true,
          color: '#F1F7F4',
          rotate: -30,
        },
      },
      yAxis: {
        name: '单位:种',
        nameTextStyle: {
          color: '#F1F7F4',
          fontSize: 12,
          padding: [0, 0, 0, -10], //name文字位置 对应 上右下左
        },

        //坐标轴
        axisLine: {
          show: false,
        },
        //坐标值标注
        axisLabel: {
          show: true,
          color: '#F1F7F4',
        },
        //分格线
        splitLine: {
          lineStyle: {
            color: '#4784e8',
          },
        },
      },
      series: [
        {
          // /数据图
          data: data,
          type: 'bar',
          barWidth: 20,
          label: {
            show: true,
            color: '#fff',
          },
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
              { offset: 0, color: '#36d2ff' },
              { offset: 1, color: '#d0dd5f' },
            ]),
          },
          z: 1,
        },
        {
          // 最上面菱形
          data: data,
          type: 'pictorialBar',
          symbol: 'diamond',
          symbolSize: ['20', '10'],
          symbolPosition: 'end',
          symbolOffset: ['', '-50%'],
          itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
              { offset: 0, color: '#d0dd5f' },
              { offset: 1, color: '#939c3d' },
            ]),
          },
          z: 2,
        },
        {
          // 最下面的菱形
          data: data,
          type: 'pictorialBar',
          symbol: 'diamond',
          symbolSize: ['20', '10'],
          symbolPosition: 'start',
          symbolOffset: ['', '50%'],
          itemStyle: {
            color: '#36d2ff',
          },
          z: 2,
        },
      ],
    }

这种方式效果较好 但是立体的侧边颜色无法单独调