vue3项目常用hooks——从零到一搭建一个高颜值Vue3后台管理系统(四)

什么是hooks?

简单说就是我们将文件里的一些功能抽离出去封装后达到复用的效果。看到这里你会想这不就是以前的utils换个名字吗?其实我们更倾向于把一些如ref、reactive、onMounted等vue的api抽离的函数叫做hooks函数,它区别于mixin的不好排查异常和维护。

useTable hooks

项目中通常会出现大量表格,我们除了对视图的封装外,也可以将逻辑抽离出来达到复用,这样开发效率更好,后期修改也不用每个文件每个文件的去找,只需要关注输入输出即可。 一般表格操作主要涉及数据获取、loading加载、分页切换、表格搜索、表格重置,接下来我们需要在src目录下新建hooks文件,新建useTable.ts文件

ini 复制代码
import { reactive, ref } from 'vue';

interface RequestResult<T = any> {
    code: number;
    message: string;
    data: T;
}
  
interface TableResult<T = any> {
    total: number;
    list: T[];
}
  
type RequestTableResult<T = any> = RequestResult<TableResult<T> | T>;

interface Options<T = any> {
  // api
  apiFn: (params: any) => Promise<RequestTableResult>;
  // api请求参数
  params?: Recordable;
  // api返回值不是约定的TableResult的处理
  callback?: (data: any) =>TableResult<T>;
  // 显示分页数据
  isPageable?: boolean;
  // 立即执行getList函数
  immediate?: boolean;
}

export const useTable = <T = any>(options: Options) => {
  // 列表数据
  const tableData = ref<T[]>([]);
  // loading变量
  const loading = ref<boolean>(false);
  // 请求参数
  const paramsInit = JSON.parse(JSON.stringify(options.params || {}));
  // 分页数据
  const page = reactive({
    page: 1,
    pageSize: 20,
    pageSizes: [10, 20, 30, 50],
    total: 10
  });

  const getList = async () => {
    loading.value = true;
    const isPageable = options.isPageable ?? true;
    // 根据传入的isPageable属性判断列表请求是否携带分页参数
    const pageParams = isPageable ? { page: page.page, pageSize: page.pageSize } : {};
    // 总的请求参数
    const totalParams = Object.assign({}, options.params, pageParams);
    let { data } = await options.apiFn(totalParams).finally(() => (loading.value = false));
    // 如果后端返回格式不规范,需要传入回调处理成我们想要的格式
    options.callback && (data = options.callback(data));
    // 根据是否分页取值,所以如果接口格式不正确可以传入cb处理
    tableData.value = isPageable ? data.list : data;
    page.total = data.total;
  };

  // 页码总数切换
  const handleSizeChange = async (val: number) => {
    page.page = 1;
    page.pageSize = val;
    await getList();
  };

  // 分页切换
  const handleCurrentChange = async (val: number) => {
    page.page = val;
    await getList();
  };

  // 重置搜索数据
  const resetParams = () => {
    Object.keys(paramsInit).forEach((item) => {
      options.params![item] = paramsInit[item];
    });
    getList();
  };
  
//是否默认执行getList
if (options.immediate ?? true) getList();

// 返回相关需要的变量和方法
  return {
    tableData,
    page,
    loading,
    getList,
    resetParams,
    handleSizeChange,
    handleCurrentChange
  };
};

使用表格hooks,在需要的页面引入hooks

php 复制代码
import { useTable } from '@/hooks/useTable';
// api
import { getMenuList } from '@/api/auth';

const { tableData, loading } = useTable({
  apiFn: getMenuList,
  isPageable: false
});

// 解构出列表数据和loading
const { tableData, loading } = useTable({
  apiFn: getMenuList,
  isPageable: false
});

useEcharts hooks

下面是echarts的hooks,echarts在项目中使用也比较多,不想写太多重复性代码这里也抽离出来统一封装成hooks,新建文件useEcharts.ts

typescript 复制代码
import echarts, { type ECOption } from '@/utils/echarts';
import { useDebounceFn } from '@vueuse/core';
import { useThemeStore } from '@/store/modules/theme';
import { watch, onBeforeUnmount, markRaw, Ref } from 'vue';

export const useEcharts = (elRef: HTMLDivElement, options: Ref<ECOption>) => {
  let myChart: echarts.ECharts | null = null;
  
  // 获取store的主题配置
  const themeStore = useThemeStore();
  
  // 初始化
  const initCharts = () => {
    // 适配暗黑主题
    const theme = themeStore.isDark ? 'dark' : 'default';
    myChart = markRaw(echarts.init(elRef, theme));
    setOptions(options.value);
  };

  // 设置echarts
  const setOptions = (updateOptions: ECOption) => {
    myChart && myChart.setOption({ ...updateOptions, backgroundColor: 'transparent' });
  };

 // 屏幕适配,这里使用了一个防抖
  const resize = useDebounceFn(() => {
    myChart && myChart.resize();
  }, 200);

  // 初始化执行
  initCharts();

  // 监听options
  watch(
    options,
    (newValue) => {
      setOptions(newValue);
    },
    { deep: true }
  );

  // 暗黑适配
  watch(
    () => themeStore.isDark,
    () => {
      if (myChart) {
        myChart.dispose();
        initCharts();
      }
    }
  );

  window.addEventListener('resize', resize);
  
  // 清除副作用
  onBeforeUnmount(() => {
    if (!myChart) return;
    window.removeEventListener('resize', resize);
    myChart.dispose();
  });

  // 暴露变量和方法
  return {
    myChart
  };
};

使用图表hooks,在需要的页面引入hooks

xml 复制代码
<template>
  <div ref="lineEcharts" class="w-full h-20"></div>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { ECOption } from '@/utils/echarts';
import { graphic } from 'echarts/core';
import { useEcharts } from '@/hooks/useEcharts';
defineOptions({ name: 'BotCard' });

const lineEcharts = ref<HTMLDivElement | null>(null);
const lineOptions = ref<ECOption>({
  tooltip: {
    trigger: 'axis',
    formatter(params: any) {
      return '当前销售额' + params[0].data;
    }
  },
  grid: {
    left: '3',
    right: '3',
    top: '10',
    bottom: '10'
  },
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    axisLabel: { show: false },
    boundaryGap: false,
    axisLine: {
      show: false
    },
    axisTick: {
      show: false
    }
  },
  yAxis: {
    type: 'value',
    axisLine: {
      show: false
    },
    axisLabel: {
      show: false
    },
    splitLine: {
      show: false
    }
  },
  series: [
    {
      data: [400, 600, 655, 406, 1113, 777, 900],
      type: 'line',
      smooth: true,
      showSymbol: false,
      lineStyle: {
        width: 3,
        color: new graphic.LinearGradient(0, 0, 1, 0, [
          {
            offset: 0,
            color: 'rgba(250, 220, 25, 1)'
          },
          {
            offset: 0.5,
            color: 'rgba(255, 125, 0, 1)'
          },
          {
            offset: 1,
            color: 'rgba(245, 63, 63, 1)'
          }
        ])
      }
    }
  ]
});

// 确保dom获取成功
onMounted(() => {
  useEcharts(lineEcharts.value as HTMLDivElement, lineOptions);
});
</script>

useWatermark hooks

下面是useWatermark的hooks,水印在项目中也可能会使用到,我们弄一个没法删除的水印,主要借用了MutationObserver这个方法去监听相关dom的变化,如果是属性被修改,我们重新添加属性,如果是dom被删除,我们强制增加一个水印就行了,下面统一封装成hooks,新建文件useWatermark.ts

ini 复制代码
import { useThrottleFn } from '@vueuse/core';

import { shallowRef, ref, Ref, unref } from 'vue';

interface IAttr {
  /** 字体,默认 `18px Vedana` */
  font?: string;
  /** 填充绘制图形的颜色 */
  fillStyle?: string;
  /** 水印旋转,默认 `-20` */
  rotate?: number;
  /** 水印无法删除,默认 `false` */
  forever: boolean;
}

export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.body)) {
  let style = '';
  let attr: IAttr | undefined = undefined;
  let observer: MutationObserver | null = null;
  const id = Symbol('watermark').toString();
  const watermarkEl = shallowRef<HTMLElement | null>(null);

  // 绘制文字背景图
  function createBase64(text: string, attr?: IAttr) {
    const can = document.createElement('canvas');
    can.width = 200;
    can.height = 120;

    const cans = can.getContext('2d');
    if (cans) {
      cans.rotate(((attr?.rotate ?? -20) * Math.PI) / 120);
      cans.font = attr?.font ?? '18px Vedana';
      cans.fillStyle = attr?.fillStyle ?? 'rgba(0, 0, 0, 0.12)';
      cans.textAlign = 'left';
      cans.textBaseline = 'middle';
      cans.fillText(text, can.width / 20, can.height);
    }
    return can.toDataURL('image/png');
  }

  // 绘制水印层
  const createWatermark = (text: string, attr?: IAttr) => {
    if (unref(watermarkEl)) {
      updateWatermark({ text, attr });
      return id;
    }
    const div = document.createElement('div');
    watermarkEl.value = div;
    div.id = id;
    div.style.pointerEvents = 'none';
    div.style.top = '0px';
    div.style.left = '0px';
    div.style.position = 'absolute';
    div.style.zIndex = '100000';
    const el = unref(appendEl);
    if (!el) return id;
    el.style.position = 'relative';
    const { clientHeight: height, clientWidth: width } = el;
    updateWatermark({ text, width, height, attr });
    el.appendChild(div);
    return id;
  };

  // 页面随窗口调整更新水印
  function updateWatermark(
    options: {
      width?: number;
      height?: number;
      text?: string;
      attr?: IAttr;
    } = {}
  ) {
    const el = unref(watermarkEl);
    if (!el) return;
    if (options.width) {
      el.style.width = `${options.width}px`;
    }
    if (options.height) {
      el.style.height = `${options.height}px`;
    }
    if (options.text) {
      el.style.background = `url(${createBase64(options.text, options.attr)}) left top repeat`;
    }
    style = el.style.cssText;
  }

  // 对外提供的设置水印方法
  function setWatermark(text: string, attrProps?: IAttr) {
    if (attrProps) attr = attrProps;
    createWatermark(text, attr);
    attr?.forever && createObserver(text, attr);
    window.addEventListener('resize', func);
  }

  const func = useThrottleFn(function () {
    const el = unref(appendEl);
    if (!el) return;
    const { clientHeight: height, clientWidth: width } = el;
    updateWatermark({ height, width });
  }, 30);

  // 清除水印
  const clear = () => {
    const domId = unref(watermarkEl);
    watermarkEl.value = null;
    observer?.disconnect();
    observer ??= null;
    const el = unref(appendEl);
    if (!el) return;
    domId && el.removeChild(domId);
    window.removeEventListener('resize', func);
  };

  // 监听 DOM 变化
  const createObserver = (text: string, attr?: IAttr) => {
    const domId = unref(watermarkEl);
    if (!domId) return;
    observer = new MutationObserver((mutationsList) => {
      if (mutationsList.length) {
        const { removedNodes, type, target } = mutationsList[0];
        const currStyle = domId.getAttribute('style');
        if (removedNodes[0] === domId) {
          watermarkEl.value = null;
          observer!.disconnect();
          createWatermark(text, attr);
        } else if (type === 'attributes' && target === domId && currStyle !== style) {
          domId.setAttribute('style', style);
        }
      }
    });
    observer.observe(unref(appendEl)!, {
      childList: true,
      attributes: true,
      subtree: true
    });
  };

  return { setWatermark, clear };
}

使用水印hooks,在需要的页面引入hooks

xml 复制代码
<template>
  <el-card shadow="never" class="h-full">
    <el-button @click="setWatermark('测试水印')">设置全屏水印</el-button>
    <el-button @click="clear()">清除水印</el-button>
    <div ref="divRef" class="my-4 w-350 h-100 border-dotted border-2 border-primary"></div>
  </el-card>
</template>

<script lang="ts" setup>
import { useWatermark } from '@/hooks/useWatermark';

import { onBeforeUnmount, ref, onMounted } from 'vue';

const divRef = ref();

const { setWatermark, clear } = useWatermark();
const { setWatermark: setPartWatermark, clear: clearPart } = useWatermark(divRef);

onMounted(() => {
  setPartWatermark('不可删除的水印', {
    forever: true,
    fillStyle: '#2ecc71'
  });
});

onBeforeUnmount(() => {
  clear();
  clearPart();
});
</script>

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

本文项目地址:Element-Admin

学会hooks,快速下班的好帮手

相关推荐
学习ing小白1 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
一只小阿乐1 小时前
前端web端项目运行的时候没有ip访问地址
vue.js·vue·vue3·web端
计算机学姐1 小时前
基于python+django+vue的旅游网站系统
开发语言·vue.js·python·mysql·django·旅游·web3.py
真的很上进1 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er1 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063711 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl1 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码1 小时前
vue 实现tab菜单切换
前端·javascript·vue.js
2301_765347542 小时前
Vue3 Day7-全局组件、指令以及pinia
前端·javascript·vue.js
ch_s_t2 小时前
新峰商城之分类三级联动实现
前端·html