Taro+React实现微信小程序账单功能

367 阅读2分钟

背景

最近公司有个需求,小程序需要展示用户账单明细。要求:title滑动至顶部时固定;日期可筛选。

效果

bill.gif

实现

思路

页面滚动时要知道窗口最高点在页面的什么地方,也就是scrollTop的位置,还需要知道每个月份账单(即下面的bill-details-column)的top和bottom所处的位置,如果scrollTop在这top和bottom之间,则需要固定title。

组件

这里使用的ScrollView作为容器,不用View的原因是弹出日历组件时会有滑动穿透问题;相应的useReachBottom等api也无法使用。

页面结构

父组件-容器

<ScrollView className='bill-list-column'>
      {bills.map((item, index) => (
       <View key={item.title}>
        <BillDetails />
       </View>
        )}
 </ScrollView>

子组件 - BillDetails

    <View className='bill-details-column'>
    // 需要固定的title
     <View className='details-title' >

     </View>
     <View  className='details-content'>
        // 月份下账单内容
     </View>
     // 日期组件
     <DatePicker />
   </View>

获取当月账单的高度

可以使用createSelectorQuery方法返回一个, SelectorQuery 对象实例,容器高度、top、left、right、bootom位置等信息都可以拿到。

// 先定义一个方法
const selectorQueryArray = (
  selector: string,
): Promise<TaroGeneral.IAnyObject> =>
  new Promise(resolve => {
    Taro.createSelectorQuery()
      .selectAll(selector)
      .boundingClientRect(res => {
        resolve(res)
      })
      .exec()
  })
  
//  调用,这里必须要使用 Taro.nextTick,很重要,否则拿不到数据
  const [nodes, setNodes] = useState<TaroGeneral.IAnyObject>([])// 节点信息
  
    Taro.nextTick(async () => {
      const detailsNode = await selectorQueryArray('.bill-details-column')
      const titleNode = await selectorQueryArray('.details-title') // title信息也需要,这里后面再说

      const nodesArr =
        detailsNode.map((item, index) => ({
          columnTop: item.top,
          columnBottom: item.bottom,
          columnHeight: item.height,
          titleHeight: titleNode[index].height,
        }))
        //赋值
      setNodes(nodesArr)
    })

获取scrollTop的位置

ScrollView的onScroll方法监听页面滚动时触发,返回的res.detail.scrollTop 就是我们需要的值

 const [fixedIndex, setFixedIndex] = useState() // 需要固定的title下标
 
  const onScroll = res => {
    if (!nodes.length) return
 
    const index = nodes.findIndex(
      ({ columnTop, columnBottom }) =>
        res.detail.scrollTop >= columnTop &&
        res.detail.scrollTop <= columnBottom,
    )
    
    setFixedIndex(index)
  }

固定title

  1. 再判断需要固定的下标和bills map生成BillDetails的下标是否相等,相等则需要固定。这里两个是一样的,即nodes的个数和BillDetails的个数是相等的。
  2. details-title固定后会脱离文档流,下面的content内容区域会被title遮挡,这里加个padding就好了,padding的高度就是前面获取到的titleNode
// 子组件内部-BillDetail  
// isTitleFixed:是否需要固定title,父组件传入
// node.titleHeight 
  const titleStyle: CSSProperties = useMemo(() => {
    if (isTitleFixed) {
      return { position: 'fixed', top: 0, width: '100%' }
    }
    return { position: 'relative' }
  }, [isTitleFixed])
  
  return   
    <View className='details-title' style={titleStyle}></View>
    <View
      className='details-content'
      style={{ paddingTop: isTitleFixed ? node?.titleHeight : 0 }}
    ></View>

到这里核心功能就都已经完成了,其余是一些优化就放在下面的整体代码里了

所有代码

// 父组件
  const [nodes, setNodes] = useState<TaroGeneral.IAnyObject>([])
  const [fixedIndex, setFixedIndex] = useState() // 需要固定的title下标
  const [scrollTop, setScrollTop] = useState<number | undefined>(undefined)

  useEffect(() => {
    Taro.nextTick(async () => {
      const detailsNode = await selectorQueryArray('.bill-details-column')
      const titleNode = await selectorQueryArray('.details-title')

      const nodesArr =
        detailsNode.map((item, index) => ({
          columnTop: item.top,
          columnBottom: item.bottom,
          columnHeight: item.height,
          titleHeight: titleNode[index].height,
        })) 
      setNodes(nodesArr)
    })
  }, [])
  
 const selectorQueryArray = (
  selector: string,
): Promise<TaroGeneral.IAnyObject> =>
  new Promise(resolve => {
    Taro.createSelectorQuery()
      .selectAll(selector)
      .boundingClientRect(res => {
        resolve(res)
      })
      .exec()
  })
  
  const pageScroll = throttle(res => {
    if (!nodes.length) return
    setScrollTop(undefined) // 滚动时设置为undefined
    const index = nodes.findIndex(
      ({ columnTop, columnBottom }) =>
        res.detail.scrollTop >= columnTop &&
        res.detail.scrollTop <= columnBottom,
    )
    setFixedIndex(index)
  }, 500)
  
 
  const updateData = () => {
  // 可以替代 useReachBottom
    console.log('onScrollToLower', pageHeight)
  }
  
  return (
    <ScrollView
      className='bill-list-column'
      scrollY
      enhanced
      enableFlex
      scrollWithAnimation
      scrollTop={scrollTop}
      onScroll={pageScroll}
      onScrollToLower={updateData}
    >
      {
        bills.map((item, index) => (
          <View key={item.title}>
            <BillDetails
              node={nodes[index]}
              isTitleFixed={fixedIndex === index}
              billItem={item}
              onDateConfirm={start => {
                setScrollTop(0) // 切换日期是回到顶部
              }}
            />
          </View>
        ))
      }
    </ScrollView>
  )
// 子组件 BillDetails
interface BillDetailsProps {
  billItem: BillItem
  isTitleFixed: boolean
  node: any
  onDateConfirm: (start: string) => void
}

  const titleStyle: CSSProperties = useMemo(() => {
    if (isTitleFixed) {
      return { position: 'fixed', top: 0, width: '100%' }
    }
    return { position: 'relative' }
  }, [isTitleFixed])
  
    return (
    <View className='bill-details-column'>
      <View className='details-title' style={titleStyle}>
      </View>
      <View
        className='details-content'
        style={{ paddingTop: isTitleFixed ? node?.titleHeight : 0 }}
      >
      </View>
      <DatePicker />
    </View>
  )

总结

有了思路整体来说难度不算很大,核心点在于获取子组件高度并判断窗口最高点在哪个子组件中。以上就是所有内容了,如有问题或者不合理的地方欢迎指正。