'use client'; import React, { useMemo, useState, useCallback, useImperativeHandle, useEffect, } from 'react'; import { MODAL_EVENTS, DATA_EVENTS } from '../constants/modalEvents'; import { modalTypeMap } from '../constants/modalTypes'; import { isBeforeModalPauseError } from '../core/beforeModalGuard'; import { createCloseHandler } from '../core/closeLifecycle'; import mergeModalCustom from '../core/mergeModalCustom'; import { resolveDefaultData } from '../core/resolveDefaultData'; import type { ModalRef, ModalRefOption, ModalModalOptions, ExtendedModalType, ModalVisibleProps, CancelModalMethod, UseCommonRefReturn, ResolveCustomMap, ResolveVisibleKey, ResolveHookOptions, ModalTypeItem, MergeableModalTypeMap, } from '../types/modal'; import { isFunction } from '../utils'; /** 运行时挂载生命周期回调的内部 modal 形状(不对外导出) */ type ModalRefInternal< P extends ExtendedModalType, T extends Record, U, C extends Record, VK extends string | undefined, R, > = ModalRef & { beforeModal?: ModalRefOption['beforeModal']; init?: ModalRefOption['init']; afterModal?: ModalRefOption['afterModal']; beforeCloseModal?: ModalRefOption['beforeCloseModal']; afterCloseModal?: ModalRefOption['afterCloseModal']; }; type PropsState< P extends ExtendedModalType, T extends Record, U, C extends Record, R, > = { visible: false | ModalVisibleProps, data: T, options: ModalRefOption, modalOptions: ModalModalOptions, promise: null | Promise | PromiseLike, }; /** * 通用弹窗/抽屉 Hook,提供稳定 modal 实例与 $refs 实时数据通路。 * @param modalType - 弹层类型 modal/drawer/popover * @param ref - 转发的 ref * @param defaultData - 默认数据或工厂函数 * @param options - Hook 级配置与生命周期回调 * @param deps - 触发 options 回调刷新的依赖 * @returns modal 实例、data 与 setData */ function useCommonRef< P extends ExtendedModalType, T extends Record, U = any, C extends Record = {}, VK extends string | undefined = undefined, R = unknown, const O extends ModalRefOption = ModalRefOption, >( modalType: P, ref: React.ForwardedRef< ModalRef< P, T, U, ResolveCustomMap>, ResolveVisibleKey>, R > >, defaultData: Partial | (() => Partial) = {}, options?: O, deps: React.DependencyList = [], ): UseCommonRefReturn< P, T, U, ResolveCustomMap>, ResolveVisibleKey>, R > { const hookOptions = (options ?? {}) as ModalRefOption; const [props, setProps] = useState>(() => ({ visible: false, data: resolveDefaultData(defaultData) as T, options: {}, modalOptions: {}, promise: null, })); const $refs = useMemo(() => ({} as { props: PropsState, defaultData: typeof defaultData, hookOptions: ModalRefOption, useImperativeHandle?: boolean, // eslint-disable-next-line react-hooks/exhaustive-deps }), []); $refs.props = props; $refs.defaultData = defaultData; $refs.hookOptions = hookOptions as ModalRefOption; if ($refs.useImperativeHandle === undefined) { $refs.useImperativeHandle = hookOptions.useImperativeHandle !== false; } else if ( process.env.NODE_ENV !== 'production' && hookOptions.useImperativeHandle !== undefined && Boolean(hookOptions.useImperativeHandle) !== $refs.useImperativeHandle ) { // eslint-disable-next-line no-console console.warn( '[useCommonRef] options.useImperativeHandle 约定为常量,不应在运行时变更', ); } // modal identity 稳定;实时数据走 $refs,custom/回调由 useEffect 同步 const modal = useMemo( () => { const closeFn = createCloseHandler(() => { Object.assign($refs.props, { visible: false, promise: null }); setProps({ ...$refs.props }); }); const ret = { get visible() { return Boolean($refs.props.visible); }, get data(): Omit { return $refs.props.data; }, get props() { const map = (modalTypeMap[modalType as keyof MergeableModalTypeMap] ?? { visible: 'visible', onClose: 'onClose' }) as ModalTypeItem; const mergedOptions = { ...$refs.hookOptions, ...$refs.props.options }; const visibleKey = mergedOptions.visibleKey || map.visible; return { [visibleKey]: Boolean($refs.props.visible), [map.onClose]: this.cancelModal as CancelModalMethod, }; }, get modalPromise() { return $refs.props.promise; }, get options() { return { ...$refs.hookOptions, ...$refs.props.options }; }, get modalOptions() { return $refs.props.modalOptions; }, modal(newData: Partial = {}, modalOptions: ModalModalOptions = {}): Promise { const runOpen = async (): Promise => { if ($refs.props.visible) { if (!$refs.props.visible.pending) { throw new Error('modal is already opened!'); } try { await $refs.props.visible.pending; } catch (error) { // 等待上一次关闭完成 } if ($refs.props.visible) { throw new Error('modal is still opened!'); } } $refs.props.modalOptions = modalOptions || {}; const resolvedDefault = (resolveDefaultData($refs.defaultData) || {}); let newModalData: T = { ...resolvedDefault } as T; if (newData) { Object.keys(newData).forEach((key) => { const value = newData[key]; if (value === undefined) return; (newModalData as any)[key] = value; }); } if (this.beforeModal) { let pauseResult: any; let pause = false; let isError = false; const result = await this.beforeModal( newModalData, (result: any, _isError = false) => { pause = true; pauseResult = result; isError = _isError; }, modalOptions, ) || newModalData; if (result && result !== newModalData) { newModalData = { ...resolvedDefault, ...result } as T; } if (pause) { return isBeforeModalPauseError(pauseResult, isError) ? Promise.reject(pauseResult) : pauseResult; } } const dataEvents: Record any> = {}; if (this.modalOptions.modalDataEvent) { DATA_EVENTS.forEach((key: string) => { if (!(newModalData as any)[key]) return; dataEvents[key] = (newModalData as any)[key]; delete (newModalData as any)[key]; }); } return new Promise((resolve, reject) => { const visibleRef: ModalVisibleProps = { resolve(this: typeof modal, value: any) { return visibleRef.pending = closeFn.call(this, async (next) => { if (dataEvents.onOK) { const newValue = await dataEvents.onOK(value); if (newValue !== undefined) value = newValue; } if (this.modalOptions.beforeEndModal) { const newValue = await this.modalOptions.beforeEndModal(value); if (newValue !== undefined) value = newValue; } resolve(value); next(); return value; }, 'end'); }, reject(this: typeof modal, value: any) { if (this.modalOptions.alwaysResolve || this.options.alwaysResolve) { return visibleRef.resolve.call(this, value); } return visibleRef.pending = closeFn.call(this, async (next) => { if (dataEvents.onCancel) { const newValue = await dataEvents.onCancel(value); if (newValue !== undefined) value = newValue; } if (this.modalOptions.beforeCancelModal) { const newValue = await this.modalOptions.beforeCancelModal(value); if (newValue !== undefined) value = newValue; } reject(value); next(); return value; }, 'cancel'); }, }; Object.assign($refs.props, { data: newModalData, visible: visibleRef, }); setProps({ ...$refs.props }); setTimeout(() => { const { init: _init, afterModal } = this; const { modalOptions: openOptions } = this; if (this.visible) { _init && _init.call(this, newModalData, openOptions); afterModal && afterModal.call(this, newModalData, openOptions); if (openOptions.afterModal) { openOptions.afterModal.call(this, newModalData, openOptions); } } }); }); }; const promise = runOpen().catch((err) => { $refs.props.promise = null; throw err; }); $refs.props.promise = promise; return promise; }, } as ModalRefInternal>, R>; if ($refs.hookOptions.custom) { mergeModalCustom(ret as Record, $refs.hookOptions.custom); } ret.endModal = (async function (result?: any, onDone?: () => void) { const endRet = $refs.props.visible && (await $refs.props.visible.resolve.call(this, result)); isFunction(onDone) && onDone(); return endRet; }).bind(ret); ret.cancelModal = (async function (ex?: any, onDone?: () => void) { const cancelRet = $refs.props.visible && (await $refs.props.visible.reject.call(this, ex || 'cancel')); isFunction(onDone) && onDone(); return cancelRet; }).bind(ret); return ret as ModalRefInternal>, R>; }, // eslint-disable-next-line react-hooks/exhaustive-deps [$refs], ) as unknown as ModalRef< P, T, U, ResolveCustomMap>, ResolveVisibleKey>, R >; // useImperativeHandle 约定为常量;仅首 render 绑定到 $refs,后续变更仅 dev 警告 if ($refs.useImperativeHandle) { // eslint-disable-next-line react-hooks/rules-of-hooks useImperativeHandle(ref, () => modal, [modal]); } useEffect(() => { const target = modal as Record; MODAL_EVENTS.forEach((eventName) => { const cb = hookOptions[eventName]; if (cb) target[eventName] = cb; else delete target[eventName]; }); if (hookOptions.custom) { mergeModalCustom(target, hookOptions.custom); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modal, hookOptions, ...deps]); // setData 引用稳定;对象式更新读 $refs.props const setData = useCallback( (newData: T | ((data: T) => T)) => { if (isFunction(newData)) { return setProps((prev) => ({ ...prev, data: { ...prev.data, ...(newData as any)(prev.data), }, })); } return setProps({ ...$refs.props, data: { ...$refs.props.data, ...newData } }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [$refs], ); return { modal, data: props.data, setData, }; } export default useCommonRef;