当 UI 库弹窗无法满足自定义需求时,需要我们自己开发简单的弹窗组件。弹窗组件与普通业务组件开发没有太大区别,重点在多弹窗之间的关系控制。例如: 弹窗 1,弹窗 2 由于触发时机不同,需要不同的层叠关系,后触发的始终在最前端,点击弹窗头改变层叠关系。 单一弹窗多处调用等。这里封装基础的管理钩子,简化这些问题的处理。
功能目标
- 单例,多例弹窗
- 可配置弹窗自定义参数
- 可接收弹窗自定义事件
- 层级控制
- 自定义定位
该钩子的目的主要为了处理弹窗之间的控制关系,具体如何渲染交由调用方
快速使用
// 主容器
import { usePopContainer, buildDefaultPopBind, position } from '@/hooks/usePop'
import UserInfoPop form './UserInfoPop.vue'
// 快捷工具,将内部钩子通过依赖注入,共享给子组件
const [popMap, popTools] = usePopContainer()
const popBind = buildDefaultPopBind(popTools, popTools.componentsCache)
const userPop = popBind('userInfo', UserInfoPop, {
position: { // 组件定位
top: 200
},
userId: 'xxx', // 组件porps
@close(){ // 组件事件
console.log('close')
}
})
// 调用
userPop.open()
setTimeout(userPop.close, 1000 * 3)
<!-- template -->
<template v-for="(pop, popId) of popMap">
<!-- 渲染弹窗列表 -->
<component :is="pop.component" :key="popId" v-bind="pop.props" v-on="pop.on">
</component>
</template>
多处调用
同一弹窗,多实例 add
// 容器注册
const [popMap, popTools] = usePopContainer()
// 新增弹窗1
popTools.add(popId1, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 新增弹窗2
popTools.add(popId2, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 覆盖弹窗1
// popId 为弹窗唯一标识, 如果popId相同,组件配置将被替换
popTools.add(popId1, {
component: UserPop, // 弹窗组件
useId: 'yyy', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
所有弹窗都通过 popId,查找 or 判断唯一性。
配置参数:以@ 开头的都将组为组件的事件被绑定, 除了 @[事件名] component 其他属性都将作为 props,包括 style 等属性
移除 remove
const [popMap, popTools] = usePopContainer()
popTools.add(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 移除
popTools.remove(popId)
替换 replace
// 主容器
const [popMap, popTools] = usePopContainer()
// 子组件A
popTools.replace(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 子组件B
popTools.replace(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
当有多处调用同一弹窗,而只需要最新的触发弹窗时,使用 replace. 该方法其实就是 remove add 的包装方法
更新 update
const [popMap, popTools] = usePopContainer()
popTools.replace(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 更新参数
popTools.update(popId, {
useId: 'yyy'
})
通过 popId 查询弹窗,将新传入的参数与原配置做合并
预注册 componentsCache
const [popMap, popTools] = usePopContainer()
// 局部预先注册
// 注册只是预先缓存
popTools.componentsCache.add(popId, {
component: UserPop,
id: 'xxx',
'@cloes': () => {...}
})
// 调用
popTools.add(popId, { component: popId })
// of
popTools.replace(popId, { component: popId })
// add将从componentsCache查询预注册配置
除了局部缓存, componentsCache, 模块还导出了 globalComponentsCache 全局公共缓存。
依赖注入
为了方便父子组件调用,提供了 usePopContainer usePopChildren 方法,
// 父组件
const [popMap, popTools] = usePopContainer()
// 子组件
const { popTools } = usePopChildren()
popTools.add({
// ...
})
函数接收依赖注入标识, 为传入标识时,使用默认标识
usePop 工具函数
add(popId, options) 创建弹窗
update(popId, options) 更新弹窗配置(定位, props,events)
remove(popId) 移除弹窗
replace(popId, options) 替换,如果多处调用同一弹窗,希望只显示唯一同类弹窗时,
使用该函数,多个弹窗公用相同的 popId
clearAllPop() 清空所有弹窗
updateIndex(popId) 更新弹窗层级
downIndex(popId) 层级下降一级
topIndex(popId) 层级置顶
core 实现
import { shallowRef, unref, provide, inject } from "vue";
import { merge } from "lodash-es";
import { splitProps, counter } from "./utils";
export const DEFAULT_POP_SIGN = "DEFAULT_POP_SIGN";
// 全局层级累加器
export const counterStore = counter();
/**
* 预先pop注册表
* @summary
* 便捷多处pop调用, 调用pop显示方法时,
* 直接通过名称查询对应的组件预设
* 将调用与事件配置解耦
* @returns
*/
function componentsRegistry() {
let componentsCache = new Map([]);
function has(componentName) {
return componentsCache.has(componentName);
}
function add(componentName, options) {
componentsCache.set(componentName, options);
}
function remove(componentName) {
if (has(componentName)) {
componentsCache.delete(componentName);
}
}
function fined(componentName) {
return componentsCache.get(componentName);
}
function clear() {
componentsCache = new Map([]);
}
function getComponents() {
return [...componentsCache.values()];
}
function getComponentNames() {
return [...componentsCache.keys()];
}
return {
has,
add,
remove,
fined,
clear,
getComponents,
getComponentNames,
};
}
export const globalComponentsCache = componentsRegistry();
/**
* 弹窗控制器
* @summary
* 提供多弹窗控制逻辑:
* 1. 单例, 多例: 通过不同的 popId 控制弹窗实例的个数
* 2. 参数接收: open接收初始传给pop的事件和参数配置, update 提供参数更新
* 3. 事件回调: options 配置属性 { @[事件名称]:事件回调 } 将作为事件绑定到pop上
* 4. 动态叠加: 内部将为组件配置 zIndex, 组件内需要自定义接收该参数,判断如何处理层叠关系
* 5. 定位: 定位需要弹窗组件接收 position props 内部绑定样式
*
* @tips
* 这里定位为了兼容 useMove做了接口调整,原接口直接输出定位样式。当前出position属性,
* 组件内需要自行处理定位样式。 这里存在 style 合并和透传的的问题, 通过透传的style与
* props 内定义的style将分开处理, 即最终的结果时两个style的集合, 且透传的style优先级高于
* prop。所以如果直出定位样式,通过透传绑定给弹窗组件,后续的useMove拖拽样式将始终被透传样式覆盖
*
* @api
* - add(popId, options) 创建弹窗
* - update(popId, options) 更新弹窗配置(定位, props,events)
* - remove(popId) 移除弹窗
* - replace(popId, options) 替换,如果多处调用同一弹窗,希望只显示唯一同类弹窗时,
* 使用该函数,多个弹窗公用相同的popId
* - clearAllPop() 清空所有弹窗
* - updateIndex(popId) 更新弹窗层级
* - downIndex(popId) 层级下降一级
* - topIndex(popId) 层级置顶
*
* @example01 - 一般使用
*
* const [
* pops,
* popTools
* ] = usePop()
*
*
* // 容器组件
* <component
* v-for='(pop, popId) of pops'
* :is='pop'
* v-bind='pop.props' // 接收定位样式
* v-on='pop.on' // 接收回调事件
* :key='popId'>
* </component>
*
* // 调用弹窗
* popTools.add('popId', {
* component: POP, // 弹窗组件
* position: { top: 200 } // 弹窗定位
* title: 'xxx', // 弹窗自定义props
* @click(e){ // 弹窗事件
* ....
* }
* })
*
*
* @example02 - 预注册
* 通过预注册组件,再次调用时,只需要传入对应注册名称,而不需要具体的配置项
* const [ pops, popTools ] = usePop()
*
* // 注册本地弹窗
* popTools.componentsCache.add('userInfo', {
* component: CMP,
* opsition: { ... }
* ...
* })
*
* // 调用
* popTools.add('userInfo', { component: 'userInfo' })
*
*/
export function usePop() {
const components = shallowRef({});
const componentsCache = componentsRegistry();
function has(popId) {
return !!unref(components)[popId];
}
/**
* 添加pop
* @param popId
* @param options
* @returns
*/
function add(popId, options = {}) {
if (has(popId)) {
return false;
}
let { component, ..._options } = options;
// 全局缓存
if (globalComponentsCache.has(component)) {
const { component: cacheComponents, ...cacheOptions } =
globalComponentsCache.fined(component);
component = cacheComponents;
_options = { ...cacheOptions, ..._options };
}
// 局部缓存
if (componentsCache.has(component)) {
const { component: cacheComponents, ...cacheOptions } =
componentsCache.fined(component);
component = cacheComponents;
_options = { ...cacheOptions, ..._options };
}
counterStore.add();
const newOptions = splitProps({
..._options,
zIndex: counterStore.getCount(),
});
components.value = {
...components.value,
[popId]: {
popId,
component,
...newOptions,
},
};
}
/**
* 更新组件参数
* @param {*} popId
* @param {*} options
* @returns
*/
function update(popId, options = {}) {
if (!has(popId)) {
return false;
}
const { component, ...oldOptions } = components.value[popId];
const newOptions = splitProps(options);
components.value = {
...components.value,
[popId]: {
component,
...merge(oldOptions, newOptions),
},
};
}
/**
* 移除pop
* @param popId
*/
function remove(popId) {
if (has(popId)) {
const newCmp = components.value;
delete newCmp[popId];
components.value = {
...newCmp,
};
}
}
/**
* 多处调用同一pop时, 替换原显示pop。
* @param popId
* @param options
*/
function replace(popId, options) {
remove(popId);
add(popId, options);
}
function clearAllPop() {
components.value = {};
}
/**
* 向上一层级
* @param popId
* @returns
*/
function updateIndex(popId) {
if (!has(popId)) {
return;
}
const currentComponent = unref(components)[popId];
const upComponent = Object.values(unref(components)).fined(
(i) => i.zIndex > currentComponent.zIndex
);
const currentIndex = currentComponent.zIndex;
const upIndex = upComponent.zIndex;
update(currentIndex.popId, {
zIndex: upIndex,
});
update(upComponent.popId, {
zIndex: currentIndex,
});
}
/**
* 向下一层级
* @param {*} popId
* @returns
*/
function downIndex(popId) {
if (!has(popId)) {
return;
}
const currentComponent = unref(components)[popId];
const upComponent = Object.values(unref(components)).fined(
(i) => i.zIndex < currentComponent.zIndex
);
const currentIndex = currentComponent.zIndex;
const upIndex = upComponent.zIndex;
update(currentIndex.popId, {
zIndex: upIndex,
});
update(upComponent.popId, {
zIndex: currentIndex,
});
}
/**
* 顶层
* @param popId
* @returns
*/
function topIndex(popId) {
if (!has(popId)) {
return;
}
counterStore.add();
update(popId, {
zIndex: counterStore.getCount(),
});
}
return [
components,
{
has,
add,
remove,
update,
replace,
clearAllPop,
topIndex,
updateIndex,
downIndex,
componentsCache,
},
];
}
/**
* 嵌套结构下的弹窗钩子
*/
// 容器钩子
export function usePopContainer(provideKey = DEFAULT_POP_SIGN) {
const [popMap, popTools] = usePop();
provide(provideKey, {
popTools,
});
return [popMap, popTools];
}
// 子容器钩子
export function usePopChildren(provideKey = DEFAULT_POP_SIGN) {
return inject(provideKey, {});
}
core utils
export function isEvent(propName) {
const rule = /^@/i;
return rule.test(propName);
}
// @click => click
export function eventNameTransition(name) {
return name.replace("@", "");
}
// 拆分事件与属性
export function splitProps(cmpProps) {
return Object.entries(cmpProps).reduce(
(acc, [propName, propValue]) => {
if (isEvent(propName)) {
// 自定义事件
acc.on[eventNameTransition(propName)] = propValue;
} else {
acc.props[propName] = propValue;
}
return acc;
},
{ on: {}, props: {} }
);
}
export function counter(initCount = 0, step = 1) {
let count = initCount;
function add(customStep) {
count += customStep || step;
}
function reduce(customStep) {
count -= customStep || step;
}
function reset(customStep) {
count = customStep || initCount;
}
function getCount() {
return count;
}
return {
add,
reduce,
reset,
getCount,
};
}
工具
import { merge } from "lodash-es";
/**
* 注册并返回弹窗快捷方法
* @param {*} popTools
* @returns
*/
export function buildDefaultPopBind(popTools, componentsCache) {
return (popId, component, options) => {
componentsCache.add(popId, {
component,
// 默认定位
position: position(),
...bindDefaultEvents(popTools, popId),
...options,
});
return {
open(options) {
popTools.add(popId, { component: popId, ...options });
},
close() {
popTools.remove(popId);
},
update(options) {
popTools.update(popId, { component: popId, ...options });
},
replace(options) {
popTools.replace(popId, { component: popId, ...options });
},
};
};
}
export const DEFAULT_POSITION = {
top: 240,
left: 0,
right: 0,
};
export function position(options = DEFAULT_POSITION) {
return merge({}, DEFAULT_POSITION, options);
}
export function bindDefaultEvents(popTools, popId) {
return {
"@headerMousedown"() {
popTools.topIndex(popId);
},
"@close"(e) {
popTools.remove(popId);
},
};
}
标签:vue,const,popId,component,usePop,popTools,options,弹窗
From: https://www.cnblogs.com/wp-leonard/p/17141877.html