vue3项目常用hooks——从零到一搭建一个高颜值Vue3后台管理系统(四)
什么是hooks?
简单说就是我们将文件里的一些功能抽离出去封装后达到复用的效果。看到这里你会想这不就是以前的utils换个名字吗?其实我们更倾向于把一些如ref、reactive、onMounted等vue的api抽离的函数叫做hooks函数,它区别于mixin的不好排查异常和维护。
useTable hooks
项目中通常会出现大量表格,我们除了对视图的封装外,也可以将逻辑抽离出来达到复用,这样开发效率更好,后期修改也不用每个文件每个文件的去找,只需要关注输入输出即可。 一般表格操作主要涉及数据获取、loading加载、分页切换、表格搜索、表格重置,接下来我们需要在src目录下新建hooks文件,新建useTable.ts文件
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
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
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
<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
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
<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,快速下班的好帮手
转载自:https://juejin.cn/post/7298646156438863872