feat(ui): 底栏可拖动切换 + 透明度降至 0.24
- 滑块随手指实时移动,松手后跳转到目标 tab - 抽出 tabBarGeometry 计算拖拽命中,配套单测 - 底栏浅色/深色底色 0.32 -> 0.24
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
height: 116rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.32);
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
display: flex;
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
/* 自定义组件样式隔离:两套主题配色在组件内自包含,不依赖全局 app.wxss */
|
||||
.custom-tab-bar.theme-dark {
|
||||
background: rgba(25, 25, 25, 0.32);
|
||||
background: rgba(25, 25, 25, 0.24);
|
||||
backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
@@ -127,12 +127,14 @@
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 6rpx;
|
||||
width: 120rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50rpx;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
transition: left 0.25s ease;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.16));
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow:
|
||||
@@ -150,6 +152,10 @@
|
||||
0 4rpx 10rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tab-indicator.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.tab-icon-img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useEffect, useState } from 'react'
|
||||
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 { clampDragX, getTabBarFrame, tabIndexAtX } from './tabBarGeometry'
|
||||
import './index.scss'
|
||||
|
||||
const TABS = [
|
||||
@@ -15,6 +16,8 @@ const TABS = [
|
||||
{ pagePath: '/pages/profile/index', label: '我的', icon: assetUrl('/icon/个人.svg'), iconActive: assetUrl('/icon/个人-fill.svg') }
|
||||
]
|
||||
|
||||
const TAB_CENTER_STEP = 100 / TABS.length
|
||||
|
||||
function matchTab(path: string): string {
|
||||
const idx = TABS.findIndex((t) => path.endsWith(t.pagePath))
|
||||
return TABS[idx >= 0 ? idx : 0].pagePath
|
||||
@@ -36,6 +39,9 @@ export default function CustomTabBar() {
|
||||
|
||||
// 微信自定义 tabBar 不会自动感知 page 路由变化,用 state 保持选中态响应式
|
||||
const [activeTab, setActiveTab] = useState<string>(currentTabPath)
|
||||
const [dragX, setDragX] = useState<number | null>(null)
|
||||
const dragXRef = useRef<number | null>(null)
|
||||
const barFrameRef = useRef<{ left: number; width: number } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (t: 'light' | 'dark') => setResolvedTheme(t)
|
||||
@@ -91,21 +97,68 @@ export default function CustomTabBar() {
|
||||
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
|
||||
const nextX = clampDragX(touch.clientX - frame.left, frame.width)
|
||||
dragXRef.current = nextX
|
||||
setDragX(nextX)
|
||||
}
|
||||
|
||||
const moveDrag = (e: any) => {
|
||||
if (dragXRef.current === null) return
|
||||
const frame = barFrameRef.current
|
||||
const touch = e.touches && e.touches[0]
|
||||
if (!frame || !touch) return
|
||||
const nextX = clampDragX(touch.clientX - frame.left, frame.width)
|
||||
dragXRef.current = nextX
|
||||
setDragX(nextX)
|
||||
}
|
||||
|
||||
const endDrag = () => {
|
||||
const frame = barFrameRef.current
|
||||
const x = dragXRef.current
|
||||
if (frame && x !== null) {
|
||||
const idx = tabIndexAtX(x, frame.width, TABS.length)
|
||||
const target = TABS[idx]
|
||||
if (target && target.pagePath !== activeTab) switchTab(target.pagePath)
|
||||
}
|
||||
barFrameRef.current = null
|
||||
dragXRef.current = null
|
||||
setDragX(null)
|
||||
}
|
||||
|
||||
const cancelDrag = () => {
|
||||
barFrameRef.current = null
|
||||
dragXRef.current = null
|
||||
setDragX(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}`}>
|
||||
<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 ${dragX !== null ? 'dragging' : ''}`}
|
||||
style={{ left: dragX === null ? `calc(${(activeIndex + 0.5) * TAB_CENTER_STEP}%)` : `${dragX}px` }}
|
||||
/>
|
||||
{TABS.map((tab) => {
|
||||
const isActive = tab.pagePath === activeTab
|
||||
return (
|
||||
<View
|
||||
key={tab.pagePath}
|
||||
className={`tab-item ${isActive ? 'active' : ''}`}
|
||||
onTap={() => switchTab(tab.pagePath)}
|
||||
>
|
||||
{isActive && <View className='tab-indicator' />}
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export function clamp(value, min, max) {
|
||||
if (!Number.isFinite(value)) return min
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
export function clampDragX(x, barWidth) {
|
||||
return clamp(x, 0, Math.max(0, barWidth))
|
||||
}
|
||||
|
||||
export function tabIndexAtX(x, barWidth, tabCount) {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(barWidth) || barWidth <= 0 || tabCount <= 0) {
|
||||
return 0
|
||||
}
|
||||
const ratio = clampDragX(x, barWidth) / barWidth
|
||||
return clamp(Math.floor(ratio * tabCount), 0, tabCount - 1)
|
||||
}
|
||||
|
||||
export function getTabBarFrame(windowWidth) {
|
||||
const rpx = windowWidth / 750
|
||||
return { left: 36 * rpx, width: Math.max(0, windowWidth - 72 * rpx) }
|
||||
}
|
||||
Reference in New Issue
Block a user