194 lines
7.2 KiB
TypeScript
194 lines
7.2 KiB
TypeScript
import { View, Text, Image } from '@tarojs/components'
|
|
import Taro from '@tarojs/taro'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { getTheme, resolveTheme, setDetectedSystemTheme } from '../utils/store'
|
|
import { THEME_CHANGE_EVENT } from '../context/ThemeContext'
|
|
import { applyPageBackground } from '../utils/themeBackground'
|
|
import { assetUrl } from '../utils/asset'
|
|
import { clampPercent, getTabBarFrame, hasDragged, tabIndexAtPercent } from './tabBarGeometry'
|
|
import './index.scss'
|
|
|
|
const TABS = [
|
|
{ pagePath: '/pages/index/index', label: '首页', icon: assetUrl('/icon/首页.svg'), iconActive: assetUrl('/icon/首页-fill.svg') },
|
|
{ pagePath: '/pages/shop/index', label: '商品', icon: assetUrl('/icon/商品.svg'), iconActive: assetUrl('/icon/商品-fill.svg') },
|
|
{ pagePath: '/pages/designList/index', label: '设计清单', icon: assetUrl('/icon/调色盘.svg'), iconActive: assetUrl('/icon/调色盘-fill.svg') },
|
|
{ pagePath: '/pages/orders/index', label: '订单', icon: assetUrl('/icon/包裹.svg'), iconActive: assetUrl('/icon/包裹-fill.svg') },
|
|
{ pagePath: '/pages/profile/index', label: '我的', icon: assetUrl('/icon/个人.svg'), iconActive: assetUrl('/icon/个人-fill.svg') }
|
|
]
|
|
|
|
const TAB_CENTER_STEP = 100 / TABS.length
|
|
const DRAG_THRESHOLD_PX = 12
|
|
|
|
function matchTab(path: string): string {
|
|
const idx = TABS.findIndex((t) => path.endsWith(t.pagePath))
|
|
return TABS[idx >= 0 ? idx : 0].pagePath
|
|
}
|
|
|
|
function currentTabPath(): string {
|
|
const router = Taro.getCurrentInstance().router
|
|
const path = router && router.path ? router.path : 'pages/index/index'
|
|
return matchTab(path)
|
|
}
|
|
|
|
type ThemeChangeApi = {
|
|
onThemeChange?: (listener: (res: { theme?: string }) => void) => void
|
|
offThemeChange?: (listener: (res: { theme?: string }) => void) => void
|
|
}
|
|
|
|
export default function CustomTabBar() {
|
|
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(resolveTheme(getTheme()))
|
|
|
|
// 微信自定义 tabBar 不会自动感知 page 路由变化,用 state 保持选中态响应式
|
|
const [activeTab, setActiveTab] = useState<string>(currentTabPath)
|
|
const [dragPercent, setDragPercent] = useState<number | null>(null)
|
|
const [isDragging, setIsDragging] = useState<boolean>(false)
|
|
const isDraggingRef = useRef<boolean>(false)
|
|
const dragPercentRef = useRef<number | null>(null)
|
|
const startXRef = useRef<number>(0)
|
|
const barFrameRef = useRef<{ left: number; width: number } | null>(null)
|
|
|
|
useEffect(() => {
|
|
const handler = (t: 'light' | 'dark') => setResolvedTheme(t)
|
|
Taro.eventCenter.on(THEME_CHANGE_EVENT, handler)
|
|
return () => {
|
|
Taro.eventCenter.off(THEME_CHANGE_EVENT, handler)
|
|
}
|
|
}, [])
|
|
|
|
// 自定义 tabBar 不处在 React Tree 里,额外直接监听系统主题,保证“跟随系统”时也即时切换
|
|
useEffect(() => {
|
|
const handler = (res: { theme?: string }) => {
|
|
if (res && (res.theme === 'dark' || res.theme === 'light')) {
|
|
setDetectedSystemTheme(res.theme)
|
|
if (getTheme() === 'auto') {
|
|
setResolvedTheme(res.theme)
|
|
}
|
|
}
|
|
}
|
|
const themeApi = Taro as typeof Taro & ThemeChangeApi
|
|
if (themeApi.onThemeChange) {
|
|
themeApi.onThemeChange(handler)
|
|
}
|
|
return () => {
|
|
if (themeApi.offThemeChange) {
|
|
themeApi.offThemeChange(handler)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
// 其它入口(如页面内部调用 switchTab)也会回到 tabBar,收到事件后重新同步选中态
|
|
useEffect(() => {
|
|
const handler = (path?: string) => {
|
|
const router = Taro.getCurrentInstance().router
|
|
const currentPath = router && router.path ? router.path : 'pages/index/index'
|
|
setActiveTab(matchTab(path || currentPath))
|
|
}
|
|
Taro.eventCenter.on('tabBarChange', handler)
|
|
return () => {
|
|
Taro.eventCenter.off('tabBarChange', handler)
|
|
}
|
|
}, [])
|
|
|
|
// tabBar 是 tab 页切换时必定会挂载/更新的组件:在这里也刷新一次页面背景,
|
|
// 兜底处理“登录后切到受保护页面”时自定义导航区背景,避免残留白色
|
|
useEffect(() => {
|
|
applyPageBackground(resolvedTheme)
|
|
}, [resolvedTheme])
|
|
|
|
const switchTab = (url: string) => {
|
|
setActiveTab(matchTab(url))
|
|
Taro.switchTab({ url })
|
|
Taro.eventCenter.trigger('tabBarChange', url)
|
|
}
|
|
|
|
const startDrag = (e: any) => {
|
|
const touch = e.touches && e.touches[0]
|
|
if (!touch) return
|
|
const frame = getTabBarFrame(Taro.getSystemInfoSync().windowWidth)
|
|
barFrameRef.current = frame
|
|
startXRef.current = touch.clientX
|
|
isDraggingRef.current = false
|
|
setIsDragging(false)
|
|
dragPercentRef.current = null
|
|
setDragPercent(null)
|
|
}
|
|
|
|
const moveDrag = (e: any) => {
|
|
const frame = barFrameRef.current
|
|
const touch = e.touches && e.touches[0]
|
|
if (!frame || !touch) return
|
|
if (!isDraggingRef.current) {
|
|
if (!hasDragged(startXRef.current, touch.clientX, DRAG_THRESHOLD_PX)) return
|
|
isDraggingRef.current = true
|
|
setIsDragging(true)
|
|
}
|
|
const nextPercent = clampPercent(((touch.clientX - frame.left) / frame.width) * 100)
|
|
dragPercentRef.current = nextPercent
|
|
setDragPercent(nextPercent)
|
|
}
|
|
|
|
const endDrag = (e: any) => {
|
|
const frame = barFrameRef.current
|
|
if (!frame) return
|
|
let targetIndex: number
|
|
if (isDraggingRef.current) {
|
|
targetIndex = tabIndexAtPercent(dragPercentRef.current ?? 0, TABS.length)
|
|
isDraggingRef.current = false
|
|
setIsDragging(false)
|
|
Taro.nextTick(() => {
|
|
dragPercentRef.current = null
|
|
setDragPercent(null)
|
|
})
|
|
} else {
|
|
const touch = e.changedTouches && e.changedTouches[0]
|
|
const x = touch ? touch.clientX : startXRef.current
|
|
targetIndex = tabIndexAtPercent(clampPercent(((x - frame.left) / frame.width) * 100), TABS.length)
|
|
}
|
|
const target = TABS[targetIndex]
|
|
if (target && target.pagePath !== activeTab) switchTab(target.pagePath)
|
|
barFrameRef.current = null
|
|
startXRef.current = 0
|
|
}
|
|
|
|
const cancelDrag = () => {
|
|
barFrameRef.current = null
|
|
startXRef.current = 0
|
|
isDraggingRef.current = false
|
|
setIsDragging(false)
|
|
dragPercentRef.current = null
|
|
setDragPercent(null)
|
|
}
|
|
|
|
const activeIndex = Math.max(0, TABS.findIndex((t) => t.pagePath === activeTab))
|
|
|
|
return (
|
|
<View className={`tab-bar-root theme-${resolvedTheme}`}>
|
|
<View className='tab-bar-gradient' />
|
|
<View
|
|
className={`custom-tab-bar theme-${resolvedTheme}`}
|
|
onTouchStart={startDrag}
|
|
onTouchMove={moveDrag}
|
|
onTouchEnd={endDrag}
|
|
onTouchCancel={cancelDrag}
|
|
catchMove
|
|
>
|
|
<View className='tab-bar-caustic' />
|
|
<View className='tab-bar-rim' />
|
|
<View
|
|
className={`tab-indicator ${isDragging ? 'dragging' : ''}`}
|
|
style={{ left: dragPercent === null ? `${(activeIndex + 0.5) * TAB_CENTER_STEP}%` : `${dragPercent}%` }}
|
|
/>
|
|
{TABS.map((tab) => {
|
|
const isActive = tab.pagePath === activeTab
|
|
return (
|
|
<View key={tab.pagePath} className={`tab-item ${isActive ? 'active' : ''}`}>
|
|
<Image className={`tab-icon-img ${isActive ? 'active' : ''}`} src={isActive ? tab.iconActive : tab.icon} mode='aspectFit' />
|
|
<Text className='tab-label'>{tab.label}</Text>
|
|
</View>
|
|
)
|
|
})}
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|