From 4646f8779723a75c5679749ce1fece8a8be18595 Mon Sep 17 00:00:00 2001 From: obroccolio Date: Tue, 18 Aug 2026 23:13:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E6=8B=96=E5=8A=A8=E6=9D=BE=E6=89=8B?= =?UTF-8?q?=E4=BB=8E=E6=89=8B=E6=8C=87=E4=BD=8D=E7=BD=AE=E5=B9=B3=E6=BB=91?= =?UTF-8?q?=E5=A5=91=E5=90=88=E5=88=B0=E7=9B=AE=E6=A0=87=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拖动状态与位置状态分离 - 松手先恢复过渡但保留手指位置,下一帧再吸附到目标 tab,避免从旧位置跳变 --- dist/custom-tab-bar/index.js | 2 +- src/custom-tab-bar/index.tsx | 26 +++++++++++++++++++------- tools/custom-tab-bar.test.mjs | 13 +++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/dist/custom-tab-bar/index.js b/dist/custom-tab-bar/index.js index 8fb0242..c9cb552 100644 --- a/dist/custom-tab-bar/index.js +++ b/dist/custom-tab-bar/index.js @@ -1 +1 @@ -"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[777],{7914:function(n,e,t){var a=t(7842),c=t(5544),r=t(118),u=t(758),i=t.n(u),o=t(6540),s=t(1457),l=t(9703),f=t(6606),h=t(8199);function g(n,e,t){return Number.isFinite(n)?Math.min(t,Math.max(e,n)):e}function v(n){return g(n,0,100)}function d(n,e){return!Number.isFinite(n)||e<=0?0:g(Math.floor(v(n)/100*e),0,e-1)}function m(n,e,t){return Math.abs(e-n)>=t}function p(n){var e=n/750;return{left:36*e,width:Math.max(0,n-72*e)}}var b=t(4848),x=[{pagePath:"/pages/index/index",label:"\u9996\u9875",icon:(0,h.uq)("/icon/\u9996\u9875.svg"),iconActive:(0,h.uq)("/icon/\u9996\u9875-fill.svg")},{pagePath:"/pages/shop/index",label:"\u5546\u54c1",icon:(0,h.uq)("/icon/\u5546\u54c1.svg"),iconActive:(0,h.uq)("/icon/\u5546\u54c1-fill.svg")},{pagePath:"/pages/designList/index",label:"\u8bbe\u8ba1\u6e05\u5355",icon:(0,h.uq)("/icon/\u8c03\u8272\u76d8.svg"),iconActive:(0,h.uq)("/icon/\u8c03\u8272\u76d8-fill.svg")},{pagePath:"/pages/orders/index",label:"\u8ba2\u5355",icon:(0,h.uq)("/icon/\u5305\u88f9.svg"),iconActive:(0,h.uq)("/icon/\u5305\u88f9-fill.svg")},{pagePath:"/pages/profile/index",label:"\u6211\u7684",icon:(0,h.uq)("/icon/\u4e2a\u4eba.svg"),iconActive:(0,h.uq)("/icon/\u4e2a\u4eba-fill.svg")}],C=100/x.length,S=12;function P(n){var e=x.findIndex(function(e){return n.endsWith(e.pagePath)});return x[e>=0?e:0].pagePath}function q(){var n=i().getCurrentInstance().router,e=n&&n.path?n.path:"pages/index/index";return P(e)}function N(){var n=(0,o.useState)((0,s.eW)((0,s.O4)())),e=(0,c.A)(n,2),t=e[0],a=e[1],u=(0,o.useState)(q),h=(0,c.A)(u,2),g=h[0],N=h[1],T=(0,o.useState)(null),w=(0,c.A)(T,2),j=w[0],A=w[1],M=(0,o.useRef)(null),E=(0,o.useRef)(0),I=(0,o.useRef)(null);(0,o.useEffect)(function(){var n=function(n){return a(n)};return i().eventCenter.on(l.Wo,n),function(){i().eventCenter.off(l.Wo,n)}},[]),(0,o.useEffect)(function(){var n=function(n){!n||"dark"!==n.theme&&"light"!==n.theme||((0,s.Rq)(n.theme),"auto"===(0,s.O4)()&&a(n.theme))},e=i();return e.onThemeChange&&e.onThemeChange(n),function(){e.offThemeChange&&e.offThemeChange(n)}},[]),(0,o.useEffect)(function(){var n=function(n){var e=i().getCurrentInstance().router,t=e&&e.path?e.path:"pages/index/index";N(P(n||t))};return i().eventCenter.on("tabBarChange",n),function(){i().eventCenter.off("tabBarChange",n)}},[]),(0,o.useEffect)(function(){(0,f.G)(t)},[t]);var W=function(n){N(P(n)),i().switchTab({url:n}),i().eventCenter.trigger("tabBarChange",n)},O=function(n){var e=n.touches&&n.touches[0];if(e){var t=p(i().getSystemInfoSync().windowWidth);I.current=t,E.current=e.clientX,M.current=null,A(null)}},R=function(n){var e=I.current,t=n.touches&&n.touches[0];if(e&&t&&(null!==M.current||m(E.current,t.clientX,S))){var a=v((t.clientX-e.left)/e.width*100);M.current=a,A(a)}},X=function(n){var e=I.current;if(e){var t;if(null!==M.current)t=d(M.current,x.length);else{var a=n.changedTouches&&n.changedTouches[0],c=a?a.clientX:E.current;t=d(v((c-e.left)/e.width*100),x.length)}var r=x[t];r&&r.pagePath!==g&&W(r.pagePath),I.current=null,E.current=0,M.current=null,A(null)}},k=function(){I.current=null,E.current=0,M.current=null,A(null)},y=Math.max(0,x.findIndex(function(n){return n.pagePath===g}));return(0,b.jsxs)(r.Ss,{className:"tab-bar-root theme-".concat(t),children:[(0,b.jsx)(r.Ss,{className:"tab-bar-gradient"}),(0,b.jsxs)(r.Ss,{className:"custom-tab-bar theme-".concat(t),onTouchStart:O,onTouchMove:R,onTouchEnd:X,onTouchCancel:k,catchMove:!0,children:[(0,b.jsx)(r.Ss,{className:"tab-bar-caustic"}),(0,b.jsx)(r.Ss,{className:"tab-bar-rim"}),(0,b.jsx)(r.Ss,{className:"tab-indicator ".concat(null!==j?"dragging":""),style:{left:"".concat(null===j?(y+.5)*C:j,"%")}}),x.map(function(n){var e=n.pagePath===g;return(0,b.jsxs)(r.Ss,{className:"tab-item ".concat(e?"active":""),children:[(0,b.jsx)(r._V,{className:"tab-icon-img ".concat(e?"active":""),src:e?n.iconActive:n.icon,mode:"aspectFit"}),(0,b.jsx)(r.EY,{className:"tab-label",children:n.label})]},n.pagePath)})]})]})}Component((0,a.createComponentConfig)(N,"custom-tab-bar/index"))}},function(n){var e=function(e){return n(n.s=e)};n.O(0,[907,96,76],function(){return e(7914)});n.O()}]); \ No newline at end of file +"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[777],{7914:function(e,n,t){var a=t(7842),c=t(5544),r=t(118),u=t(758),i=t.n(u),o=t(6540),s=t(1457),l=t(9703),f=t(6606),h=t(8199);function g(e,n,t){return Number.isFinite(e)?Math.min(t,Math.max(n,e)):n}function v(e){return g(e,0,100)}function d(e,n){return!Number.isFinite(e)||n<=0?0:g(Math.floor(v(e)/100*n),0,n-1)}function m(e,n,t){return Math.abs(n-e)>=t}function p(e){var n=e/750;return{left:36*n,width:Math.max(0,e-72*n)}}var b=t(4848),x=[{pagePath:"/pages/index/index",label:"\u9996\u9875",icon:(0,h.uq)("/icon/\u9996\u9875.svg"),iconActive:(0,h.uq)("/icon/\u9996\u9875-fill.svg")},{pagePath:"/pages/shop/index",label:"\u5546\u54c1",icon:(0,h.uq)("/icon/\u5546\u54c1.svg"),iconActive:(0,h.uq)("/icon/\u5546\u54c1-fill.svg")},{pagePath:"/pages/designList/index",label:"\u8bbe\u8ba1\u6e05\u5355",icon:(0,h.uq)("/icon/\u8c03\u8272\u76d8.svg"),iconActive:(0,h.uq)("/icon/\u8c03\u8272\u76d8-fill.svg")},{pagePath:"/pages/orders/index",label:"\u8ba2\u5355",icon:(0,h.uq)("/icon/\u5305\u88f9.svg"),iconActive:(0,h.uq)("/icon/\u5305\u88f9-fill.svg")},{pagePath:"/pages/profile/index",label:"\u6211\u7684",icon:(0,h.uq)("/icon/\u4e2a\u4eba.svg"),iconActive:(0,h.uq)("/icon/\u4e2a\u4eba-fill.svg")}],C=100/x.length,S=12;function P(e){var n=x.findIndex(function(n){return e.endsWith(n.pagePath)});return x[n>=0?n:0].pagePath}function T(){var e=i().getCurrentInstance().router,n=e&&e.path?e.path:"pages/index/index";return P(n)}function q(){var e=(0,o.useState)((0,s.eW)((0,s.O4)())),n=(0,c.A)(e,2),t=n[0],a=n[1],u=(0,o.useState)(T),h=(0,c.A)(u,2),g=h[0],q=h[1],N=(0,o.useState)(null),w=(0,c.A)(N,2),A=w[0],j=w[1],M=(0,o.useState)(!1),E=(0,c.A)(M,2),I=E[0],R=E[1],W=(0,o.useRef)(!1),k=(0,o.useRef)(null),O=(0,o.useRef)(0),X=(0,o.useRef)(null);(0,o.useEffect)(function(){var e=function(e){return a(e)};return i().eventCenter.on(l.Wo,e),function(){i().eventCenter.off(l.Wo,e)}},[]),(0,o.useEffect)(function(){var e=function(e){!e||"dark"!==e.theme&&"light"!==e.theme||((0,s.Rq)(e.theme),"auto"===(0,s.O4)()&&a(e.theme))},n=i();return n.onThemeChange&&n.onThemeChange(e),function(){n.offThemeChange&&n.offThemeChange(e)}},[]),(0,o.useEffect)(function(){var e=function(e){var n=i().getCurrentInstance().router,t=n&&n.path?n.path:"pages/index/index";q(P(e||t))};return i().eventCenter.on("tabBarChange",e),function(){i().eventCenter.off("tabBarChange",e)}},[]),(0,o.useEffect)(function(){(0,f.G)(t)},[t]);var y=function(e){q(P(e)),i().switchTab({url:e}),i().eventCenter.trigger("tabBarChange",e)},B=function(e){var n=e.touches&&e.touches[0];if(n){var t=p(i().getSystemInfoSync().windowWidth);X.current=t,O.current=n.clientX,W.current=!1,R(!1),k.current=null,j(null)}},F=function(e){var n=X.current,t=e.touches&&e.touches[0];if(n&&t){if(!W.current){if(!m(O.current,t.clientX,S))return;W.current=!0,R(!0)}var a=v((t.clientX-n.left)/n.width*100);k.current=a,j(a)}},J=function(e){var n=X.current;if(n){var t;if(W.current){var a;t=d(null!==(a=k.current)&&void 0!==a?a:0,x.length),W.current=!1,R(!1),i().nextTick(function(){k.current=null,j(null)})}else{var c=e.changedTouches&&e.changedTouches[0],r=c?c.clientX:O.current;t=d(v((r-n.left)/n.width*100),x.length)}var u=x[t];u&&u.pagePath!==g&&y(u.pagePath),X.current=null,O.current=0}},G=function(){X.current=null,O.current=0,W.current=!1,R(!1),k.current=null,j(null)},L=Math.max(0,x.findIndex(function(e){return e.pagePath===g}));return(0,b.jsxs)(r.Ss,{className:"tab-bar-root theme-".concat(t),children:[(0,b.jsx)(r.Ss,{className:"tab-bar-gradient"}),(0,b.jsxs)(r.Ss,{className:"custom-tab-bar theme-".concat(t),onTouchStart:B,onTouchMove:F,onTouchEnd:J,onTouchCancel:G,catchMove:!0,children:[(0,b.jsx)(r.Ss,{className:"tab-bar-caustic"}),(0,b.jsx)(r.Ss,{className:"tab-bar-rim"}),(0,b.jsx)(r.Ss,{className:"tab-indicator ".concat(I?"dragging":""),style:{left:"".concat(null===A?(L+.5)*C:A,"%")}}),x.map(function(e){var n=e.pagePath===g;return(0,b.jsxs)(r.Ss,{className:"tab-item ".concat(n?"active":""),children:[(0,b.jsx)(r._V,{className:"tab-icon-img ".concat(n?"active":""),src:n?e.iconActive:e.icon,mode:"aspectFit"}),(0,b.jsx)(r.EY,{className:"tab-label",children:e.label})]},e.pagePath)})]})]})}Component((0,a.createComponentConfig)(q,"custom-tab-bar/index"))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[907,96,76],function(){return n(7914)});e.O()}]); \ No newline at end of file diff --git a/src/custom-tab-bar/index.tsx b/src/custom-tab-bar/index.tsx index 43f579d..4a6b26f 100644 --- a/src/custom-tab-bar/index.tsx +++ b/src/custom-tab-bar/index.tsx @@ -41,6 +41,8 @@ export default function CustomTabBar() { // 微信自定义 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) @@ -105,6 +107,8 @@ export default function CustomTabBar() { const frame = getTabBarFrame(Taro.getSystemInfoSync().windowWidth) barFrameRef.current = frame startXRef.current = touch.clientX + isDraggingRef.current = false + setIsDragging(false) dragPercentRef.current = null setDragPercent(null) } @@ -113,8 +117,10 @@ export default function CustomTabBar() { const frame = barFrameRef.current const touch = e.touches && e.touches[0] if (!frame || !touch) return - if (dragPercentRef.current === null && !hasDragged(startXRef.current, touch.clientX, DRAG_THRESHOLD_PX)) { - 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 @@ -125,8 +131,14 @@ export default function CustomTabBar() { const frame = barFrameRef.current if (!frame) return let targetIndex: number - if (dragPercentRef.current !== null) { - targetIndex = tabIndexAtPercent(dragPercentRef.current, TABS.length) + 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 @@ -136,13 +148,13 @@ export default function CustomTabBar() { if (target && target.pagePath !== activeTab) switchTab(target.pagePath) barFrameRef.current = null startXRef.current = 0 - dragPercentRef.current = null - setDragPercent(null) } const cancelDrag = () => { barFrameRef.current = null startXRef.current = 0 + isDraggingRef.current = false + setIsDragging(false) dragPercentRef.current = null setDragPercent(null) } @@ -163,7 +175,7 @@ export default function CustomTabBar() { {TABS.map((tab) => { diff --git a/tools/custom-tab-bar.test.mjs b/tools/custom-tab-bar.test.mjs index 585a489..65eae09 100644 --- a/tools/custom-tab-bar.test.mjs +++ b/tools/custom-tab-bar.test.mjs @@ -1,7 +1,12 @@ import test from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import path from 'node:path' import { clampPercent, getTabBarFrame, hasDragged, tabIndexAtPercent } from '../src/custom-tab-bar/tabBarGeometry.js' +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + test('maps a drag percent to the tab under the finger', () => { assert.equal(tabIndexAtPercent(10, 5), 0) assert.equal(tabIndexAtPercent(30, 5), 1) @@ -28,3 +33,11 @@ test('derives the floating bar frame from window width', () => { assert.deepEqual(getTabBarFrame(375), { left: 18, width: 339 }) assert.deepEqual(getTabBarFrame(750), { left: 36, width: 678 }) }) + +test('enables the slide transition before snapping the indicator to the target', () => { + const source = readFileSync(path.join(root, 'src/custom-tab-bar/index.tsx'), 'utf8') + assert.match( + source, + /isDraggingRef\.current = false\s+setIsDragging\(false\)\s+Taro\.nextTick\(\(\) => \{[\s\S]*?setDragPercent\(null\)/ + ) +})