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(currentTabPath) const [dragPercent, setDragPercent] = useState(null) const [isDragging, setIsDragging] = useState(false) const isDraggingRef = useRef(false) const dragPercentRef = useRef(null) const startXRef = useRef(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 ( {TABS.map((tab) => { const isActive = tab.pagePath === activeTab return ( {tab.label} ) })} ) }