diff --git a/src/app.tsx b/src/app.tsx index 22fde88..099ad12 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -7,15 +7,15 @@ class App extends Taro.Component { pages: [ 'pages/Home', // @index('./pages/*[!Home].tsx', (pp, cc) => `'${pp.path.replace(/^\.\//, '')}',`) - 'pages/DatePicker', - 'pages/ECharts', - 'pages/Picker', - 'pages/PickerView', + // 'pages/DatePicker', + // 'pages/ECharts', + // 'pages/Picker', + // 'pages/PickerView', 'pages/Popup', - 'pages/SinglePicker', + // 'pages/SinglePicker', 'pages/Sticky', - 'pages/TimePicker', - 'pages/Transition', + // 'pages/TimePicker', + // 'pages/Transition', // @endindex ], window: { diff --git a/src/components/NavigationBar/index.tsx b/src/components/NavigationBar/index.tsx index 3ba8e7c..c0f2729 100644 --- a/src/components/NavigationBar/index.tsx +++ b/src/components/NavigationBar/index.tsx @@ -1,18 +1,18 @@ import './index.scss' -import Taro from '@tarojs/taro' -import { component } from '../component' -import { internalStore } from '../internal' +import Taro, { useState } from '@tarojs/taro' +import { functionalComponent } from '../component' import { last } from 'vtils' import { NavigationBarProps } from './props' +import { useCustomNavigationBarFullHeight, useDidEnter, useDidLeave } from '../../hooks' import { View } from '@tarojs/components' function onlyPath(url: string) { return url ? url.split('?')[0].replace(/^\/+/, '') : '' } -export default class NavigationBar extends component({ - props: NavigationBarProps, - state: { +function NavigationBar(props: typeof NavigationBarProps) { + const { setCustomNavigationBarFullHeight, resetCustomNavigationBarFullHeight } = useCustomNavigationBarFullHeight() + const [state, setState] = useState({ verticalPadding: 0 as number, horizontalPadding: 0 as number, navigationBarHeight: 0 as number, @@ -21,9 +21,14 @@ export default class NavigationBar extends component({ menuButtonWidth: 0 as number, backButtonVisible: false as boolean, homeButtonVisible: false as boolean, - }, -}) { - componentDidShow() { + }) + const actualBackgroundColor = props.backgroundColor !== 'auto' + ? props.backgroundColor + : props.textStyle === 'white' + ? '#000000' + : '#FFFFFF' + + useDidEnter(() => { const menuRect = Taro.getMenuButtonBoundingClientRect() const sysInfo = Taro.getSystemInfoSync() const verticalPadding = menuRect.top - sysInfo.statusBarHeight @@ -31,13 +36,13 @@ export default class NavigationBar extends component({ const height = menuRect.height + verticalPadding * 2 const fullHeight = sysInfo.statusBarHeight + height + setCustomNavigationBarFullHeight(fullHeight) + const pages = Taro.getCurrentPages() const backButtonVisible = pages.length > 1 - const homeButtonVisible = onlyPath(last(pages).route) !== onlyPath(this.props.homePath) - - internalStore.customNavigationBarFullHeight = fullHeight + const homeButtonVisible = onlyPath(last(pages).route) !== onlyPath(props.homePath) - this.setState({ + setState({ verticalPadding: verticalPadding, horizontalPadding: horizontalPadding, navigationBarHeight: height, @@ -47,82 +52,67 @@ export default class NavigationBar extends component({ backButtonVisible: backButtonVisible, homeButtonVisible: homeButtonVisible, }) - } + }) - componentDidHide() { - internalStore.customNavigationBarFullHeight = 0 - } + useDidLeave(resetCustomNavigationBarFullHeight) - componentWillUnmount() { - internalStore.customNavigationBarFullHeight = 0 - } - - handleBackClick = () => { + function handleBackClick() { Taro.navigateBack() } - handleHomeClick = () => { + function handleHomeClick() { Taro.navigateTo({ - url: this.props.homePath, + url: props.homePath, }) } - render() { - const { backgroundColor, textStyle, className } = this.props - const { verticalPadding, horizontalPadding, navigationBarHeight, navigationBarFullHeight, menuButtonHeight, menuButtonWidth, backButtonVisible, homeButtonVisible } = this.state - - const actualBackgroundColor = backgroundColor !== 'auto' - ? backgroundColor - : textStyle === 'white' - ? '#000000' - : '#FFFFFF' - - return ( - - - - {!backButtonVisible && !homeButtonVisible ? null : ( - - - {!backButtonVisible ? null : ( - - )} - {!(backButtonVisible && homeButtonVisible) ? null : ( - - )} - {!homeButtonVisible ? null : ( - - )} - + return ( + + + + {!state.backButtonVisible && !state.homeButtonVisible ? null : ( + + + {!state.backButtonVisible ? null : ( + + )} + {!(state.backButtonVisible && state.homeButtonVisible) ? null : ( + + )} + {!state.homeButtonVisible ? null : ( + + )} - )} - - {this.props.children} + )} + + {props.children} - ) - } + + ) } + +export default functionalComponent(NavigationBarProps)(NavigationBar) diff --git a/src/components/NavigationBar/props.ts b/src/components/NavigationBar/props.ts index 2d8c874..140f807 100644 --- a/src/components/NavigationBar/props.ts +++ b/src/components/NavigationBar/props.ts @@ -1,6 +1,6 @@ -import { RequiredProp } from '../component' +import { createProps, RequiredProp } from '../component' -export const NavigationBarProps = { +export const NavigationBarProps = createProps({ /** * 小程序主页的绝对路径,可带参数。 * @@ -23,4 +23,4 @@ export const NavigationBarProps = { * @default 'auto' */ backgroundColor: 'auto' as string, -} +}) diff --git a/src/components/Popup/index.tsx b/src/components/Popup/index.tsx index 1131fd7..76d8787 100644 --- a/src/components/Popup/index.tsx +++ b/src/components/Popup/index.tsx @@ -1,9 +1,8 @@ import MTransition from '../Transition' -import Taro from '@tarojs/taro' -import { CommonEventFunction } from '@tarojs/components/types/common' -import { component } from '../component' -import { internalStore } from '../internal' +import Taro, { useEffect, useState } from '@tarojs/taro' +import { functionalComponent } from '../component' import { MPopupProps, Position, TransitionName } from './props' +import { useCustomNavigationBarFullHeight, useZIndex } from '../../hooks' import { View } from '@tarojs/components' const positionToTransitionName: Record = { @@ -14,109 +13,73 @@ const positionToTransitionName: Record = { right: 'slideRight', } -/** - * 弹出层组件。 - */ -export default class MPopup extends component({ - props: MPopupProps, - state: { - /** zIndex 值 */ - zIndex: 0 as number, - /** 弹出层是否显示 */ - display: false as boolean, - }, -}) { - /** 计数器 */ - transitionEndCounter = 0 +function MPopup(props: typeof MPopupProps) { + const [transitionEndCount, setTransitionEndCount] = useState(0) + const [display, setDisplay] = useState(false) + const { zIndex } = useZIndex() + const { customNavigationBarFullHeight } = useCustomNavigationBarFullHeight() - componentWillMount() { - this.setState({ - zIndex: internalStore.zIndex++, - display: this.props.visible, - }) - } - - componentWillReceiveProps(nextProps: MPopup['props']) { - if (nextProps.visible !== this.props.visible) { - this.setState({ - display: true, - }) - } - } - - handleTouchMove: CommonEventFunction = e => { - e.stopPropagation() - } + useEffect( + () => { + setDisplay(true) + }, + [props.visible], + ) - handleMaskClick = () => { - if (this.props.maskClosable) { - this.props.onVisibleChange(false) + function handleMaskClick() { + if (props.maskClosable) { + props.onVisibleChange(false) } } - handleTransitionEnd = () => { + function handleTransitionEnd() { const action = () => { - this.transitionEndCounter = 0 - this.setState({ - display: this.props.visible, - }) + setTransitionEndCount(0) + setDisplay(props.visible) } - if (this.props.noMask) { + if (props.noMask) { action() } else { - this.transitionEndCounter++ - if (this.transitionEndCounter >= 2) { + if (transitionEndCount >= 1) { action() + } else { + setTransitionEndCount(transitionEndCount + 1) } } } - render() { - const { - visible, - noMask, - duration, - position, - customTransition, - className, - } = this.props - - const { - zIndex, - display, - } = this.state - - return ( - - {noMask ? null : ( - - - - )} - - - {this.props.children} - - + return ( + e.stopPropagation()}> + {props.noMask ? null : ( + + + + )} + + + {this.props.children} + - ) - } + + ) } + +export default functionalComponent(MPopupProps)(MPopup) diff --git a/src/components/Popup/props.ts b/src/components/Popup/props.ts index 711257f..d2c1405 100644 --- a/src/components/Popup/props.ts +++ b/src/components/Popup/props.ts @@ -1,11 +1,11 @@ import MTransition from '../Transition' +import { createProps, RequiredProp } from '../component' import { noop } from 'vtils' -import { RequiredProp } from '../component' export type TransitionName = MTransition['props']['name'] export type Position = 'center' | 'top' | 'bottom' | 'right' | 'left' -export const MPopupProps = { +export const MPopupProps = createProps({ /** * 弹出层是否可见。 */ @@ -55,4 +55,4 @@ export const MPopupProps = { * 可见性变化事件。 */ onVisibleChange: noop as any as RequiredProp<(visible: boolean) => void>, -} +}) diff --git a/src/components/Sticky/index.tsx b/src/components/Sticky/index.tsx index 1e52f38..1156382 100644 --- a/src/components/Sticky/index.tsx +++ b/src/components/Sticky/index.tsx @@ -1,51 +1,33 @@ -import Taro from '@tarojs/taro' -import { component } from '../component' -import { Disposer, wait } from 'vtils' -import { internalStore } from '../internal' +import Taro, { useEffect, useState } from '@tarojs/taro' +import { EventBus, wait } from 'vtils' +import { functionalComponent } from '../component' import { MStickyProps } from './props' +import { useCustomNavigationBarFullHeight, useDisposer } from '../../hooks' import { View } from '@tarojs/components' -/** 页面上存在的吸顶组件 */ -const stickyComponents: MSticky[] = [] - -/** - * 吸顶组件。 - * - * @example - * - * ```jsx - * - * 标题 - * - * ``` - */ -export default class MSticky extends component({ - props: MStickyProps, - state: { - /** 是否置顶 */ - fixed: false as boolean, - /** 内容高度,单位:px */ - contentHeight: 0 as number, - }, -}) { - /** 处置器 */ - disposer: Disposer = new Disposer() - - /** 组件索引 */ - index: number = 0 - - componentWillMount() { - this.index = stickyComponents.length - stickyComponents.push(this) - this.disposer.add(() => { - stickyComponents.splice( - stickyComponents.indexOf(this), - 1, - ) - }) - } +const bus = new EventBus<{ + changeFixed: (stickyComponentIndex: number, sourceFixed: boolean) => any, +}>() + +let stickyComponentCount = -1 + +function MSticky(props: typeof MStickyProps) { + const [fixed, setFixed] = useState(false) + const [contentHeight, setContentHeight] = useState(0) + const [stickyComponentIndex] = useState(stickyComponentCount++) + const { customNavigationBarFullHeight } = useCustomNavigationBarFullHeight() + const { addDisposer } = useDisposer() - componentDidMount() { + addDisposer([ + () => { stickyComponentCount-- }, + bus.on('changeFixed', (index, sourceFixed) => { + if (index === stickyComponentIndex && sourceFixed !== fixed) { + setFixed(!sourceFixed) + } + }), + ]) + + useEffect(() => { // 等待一段时间,确保页面渲染已经完成 wait(300).then(() => { // 获取吸顶内容的高度 @@ -53,12 +35,10 @@ export default class MSticky extends component({ .in(this.$scope) .select('.m-sticky') .boundingClientRect(({ height }) => { - this.setState({ - contentHeight: height, - }) + setContentHeight(height) // 监听吸顶内容的位置 - const top = -(this.index === 0 ? internalStore.customNavigationBarFullHeight : height) + const top = -(this.index === 0 ? customNavigationBarFullHeight : height) const intersectionObserver = wx.createIntersectionObserver(this.$scope) const relativeToViewport = intersectionObserver.relativeToViewport({ top }) as any relativeToViewport.observe( @@ -66,47 +46,137 @@ export default class MSticky extends component({ (res: wx.ObserveCallbackResult) => { const fixed = res.intersectionRatio <= 0 && res.boundingClientRect.top < -top - this.setState({ fixed }) + setFixed(fixed) // 切换前一个吸顶组件的状态 if (this.index >= 1) { - const prevSticky = stickyComponents[this.index - 1] - if (prevSticky.state.fixed !== fixed) { - prevSticky.setState({ - fixed: !fixed, - }) - } + bus.emit('changeFixed', stickyComponentIndex - 1, fixed) } }, ) - // 处置收集 - this.disposer.add( + addDisposer( () => intersectionObserver.disconnect(), ) }) .exec() }) - } - - componentWillUnmount() { - this.disposer.dispose() - } + }, [customNavigationBarFullHeight]) - render() { - const { className } = this.props - const { fixed, contentHeight } = this.state - - return ( + return ( + - - {this.props.children} - + className='m-sticky__content' + style={{ top: `${customNavigationBarFullHeight}px` }}> + {this.props.children} - ) - } + + ) } + +export default functionalComponent(MStickyProps)(MSticky) + +// /** +// * 吸顶组件。 +// * +// * @example +// * +// * ```jsx +// * +// * 标题 +// * +// * ``` +// */ +// export default class MSticky extends component({ +// props: MStickyProps, +// state: { +// /** 是否置顶 */ +// fixed: false as boolean, +// /** 内容高度,单位:px */ +// contentHeight: 0 as number, +// }, +// }) { +// /** 处置器 */ +// disposer: Disposer = new Disposer() + +// /** 组件索引 */ +// index: number = 0 + +// componentWillMount() { +// this.index = stickyComponents.length +// stickyComponents.push(this) +// this.disposer.add(() => { +// stickyComponents.splice( +// stickyComponents.indexOf(this), +// 1, +// ) +// }) +// } + +// componentDidMount() { +// // 等待一段时间,确保页面渲染已经完成 +// wait(300).then(() => { +// // 获取吸顶内容的高度 +// wx.createSelectorQuery() +// .in(this.$scope) +// .select('.m-sticky') +// .boundingClientRect(({ height }) => { +// this.setState({ +// contentHeight: height, +// }) + +// // 监听吸顶内容的位置 +// const top = -(this.index === 0 ? internalStore.customNavigationBarFullHeight : height) +// const intersectionObserver = wx.createIntersectionObserver(this.$scope) +// const relativeToViewport = intersectionObserver.relativeToViewport({ top }) as any +// relativeToViewport.observe( +// '.m-sticky', +// (res: wx.ObserveCallbackResult) => { +// const fixed = res.intersectionRatio <= 0 && res.boundingClientRect.top < -top + +// this.setState({ fixed }) + +// // 切换前一个吸顶组件的状态 +// if (this.index >= 1) { +// const prevSticky = stickyComponents[this.index - 1] +// if (prevSticky.state.fixed !== fixed) { +// prevSticky.setState({ +// fixed: !fixed, +// }) +// } +// } +// }, +// ) + +// // 处置收集 +// this.disposer.add( +// () => intersectionObserver.disconnect(), +// ) +// }) +// .exec() +// }) +// } + +// componentWillUnmount() { +// this.disposer.dispose() +// } + +// render() { +// const { className } = this.props +// const { fixed, contentHeight } = this.state + +// return ( +// +// +// {this.props.children} +// +// +// ) +// } +// } diff --git a/src/components/Sticky/props.ts b/src/components/Sticky/props.ts index 78da275..effe98f 100644 --- a/src/components/Sticky/props.ts +++ b/src/components/Sticky/props.ts @@ -1 +1,3 @@ -export const MStickyProps = {} +import { createProps } from '../component' + +export const MStickyProps = createProps({}) diff --git a/src/components/component.ts b/src/components/component.ts index 5f71175..6608926 100644 --- a/src/components/component.ts +++ b/src/components/component.ts @@ -59,7 +59,45 @@ const component = < } ) +const functionalComponent =

>(props: P) => ( + >(component: T): T => { + (component as any).defaultProps = props + ;(component as any).options = { + addGlobalClass: true, + styleIsolation: 'shared', + } + return component + } +) + +const createProps = < + P extends Record, + PP = ( + Overwrite< + PartialBy< + { [K in keyof P]: P[K] extends RequiredProp ? T : P[K] }, + { [K in keyof P]: P[K] extends RequiredProp ? never : K }[keyof P] + >, + { + /** 应用级别、页面级别的类 */ + className?: string, + /** 子节点 */ + children?: any, + } + > + ), +>(props: P): PP => { + return props as any +} + +export const componentOptions = { + addGlobalClass: true, + styleIsolation: 'shared', +} + export { RequiredProp, component, + createProps, + functionalComponent, } diff --git a/src/components/internal.ts b/src/components/internal.ts deleted file mode 100644 index e324e4b..0000000 --- a/src/components/internal.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const internalStore = { - zIndex: 5000, - customNavigationBarFullHeight: 0, -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..57201ea --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,8 @@ +// @index('./*', (pp, cc) => `export * from '${pp.path}'`) +export * from './useCustomNavBarFullHeight' +export * from './useDidEnter' +export * from './useDidLeave' +export * from './useDispose' +export * from './useDisposer' +export * from './useEffectOnce' +export * from './useZIndex' diff --git a/src/hooks/useCustomNavBarFullHeight.ts b/src/hooks/useCustomNavBarFullHeight.ts new file mode 100644 index 0000000..8b288a3 --- /dev/null +++ b/src/hooks/useCustomNavBarFullHeight.ts @@ -0,0 +1,32 @@ +import { EventBus } from 'vtils' +import { useDispose } from './useDispose' +import { useState } from '@tarojs/taro' + +let customNavigationBarFullHeight = 0 + +const bus = new EventBus<{ + setCustomNavigationBarFullHeight: (height: number) => void, +}>() + +bus.on('setCustomNavigationBarFullHeight', height => { + customNavigationBarFullHeight = height +}) + +export function useCustomNavigationBarFullHeight() { + const [height, setHeight] = useState(customNavigationBarFullHeight) + useDispose( + bus.on( + 'setCustomNavigationBarFullHeight', + height => setHeight(height), + ), + ) + return { + customNavigationBarFullHeight: height, + setCustomNavigationBarFullHeight(height: number) { + bus.emit('setCustomNavigationBarFullHeight', height) + }, + resetCustomNavigationBarFullHeight() { + bus.emit('setCustomNavigationBarFullHeight', 0) + }, + } +} diff --git a/src/hooks/useDidEnter.ts b/src/hooks/useDidEnter.ts new file mode 100644 index 0000000..7f8c6a5 --- /dev/null +++ b/src/hooks/useDidEnter.ts @@ -0,0 +1,10 @@ +import { useDidShow } from '@tarojs/taro' + +/** + * 进入页面。 + * + * @param callback 回调函数 + */ +export function useDidEnter(callback: () => any) { + useDidShow(callback) +} diff --git a/src/hooks/useDidLeave.ts b/src/hooks/useDidLeave.ts new file mode 100644 index 0000000..fc90591 --- /dev/null +++ b/src/hooks/useDidLeave.ts @@ -0,0 +1,12 @@ +import { useDidHide } from '@tarojs/taro' +import { useEffectOnce } from './useEffectOnce' + +/** + * 离开页面。 + * + * @param callback 回调函数 + */ +export function useDidLeave(callback: () => any) { + useDidHide(callback) + useEffectOnce(() => callback) +} diff --git a/src/hooks/useDispose.ts b/src/hooks/useDispose.ts new file mode 100644 index 0000000..58aa8c9 --- /dev/null +++ b/src/hooks/useDispose.ts @@ -0,0 +1,10 @@ +import { useEffectOnce } from './useEffectOnce' + +/** + * 在组件销毁时触发回调。 + * + * @param callback 回调 + */ +export function useDispose(callback: () => any) { + useEffectOnce(() => callback) +} diff --git a/src/hooks/useDisposer.ts b/src/hooks/useDisposer.ts new file mode 100644 index 0000000..4098876 --- /dev/null +++ b/src/hooks/useDisposer.ts @@ -0,0 +1,16 @@ +import { Disposer, DisposerItem } from 'vtils' +import { useDispose } from './useDispose' +import { useState } from '@tarojs/taro' + +/** + * `vtils.Disposer` 的 hook 版本。 + */ +export function useDisposer() { + const [disposer] = useState(() => new Disposer()) + useDispose(() => disposer.dispose()) + return { + addDisposer(items: DisposerItem | DisposerItem[]) { + disposer.add(items) + }, + } +} diff --git a/src/hooks/useEffectOnce.ts b/src/hooks/useEffectOnce.ts new file mode 100644 index 0000000..be173b8 --- /dev/null +++ b/src/hooks/useEffectOnce.ts @@ -0,0 +1,10 @@ +import { useEffect } from '@tarojs/taro' + +/** + * 等同于 `useEffect(effect, [])`。 + * + * @param effect 副作用 + */ +export function useEffectOnce(effect: () => any) { + useEffect(effect, []) +} diff --git a/src/hooks/useZIndex.ts b/src/hooks/useZIndex.ts new file mode 100644 index 0000000..fda6902 --- /dev/null +++ b/src/hooks/useZIndex.ts @@ -0,0 +1,12 @@ +import { useEffect, useState } from '@tarojs/taro' + +let zIndex = 5000 + +/** + * 获取一个全局唯一的 `zIndex` 值。 + */ +export function useZIndex() { + const [zIndexValue] = useState(zIndex + 1) + useEffect(() => { zIndex++ }, []) + return { zIndex: zIndexValue } +}