> & {
return (
titleItemsRef.current.push(ref)}
onClick={() => tabChange(item)}
className={classNames(`${classPrefix}-titles-item`, {
[`nut-tabs-titles-item-active`]:
From 645a0ec335d376104639f07cebf3c7e3ffdfc873 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Tue, 15 Sep 2026 20:29:10 +0800
Subject: [PATCH 04/13] =?UTF-8?q?chore(tabs):=20=E5=90=AF=E7=94=A8=20v16?=
=?UTF-8?q?=20=E7=89=88=E6=9C=AC=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/config.json b/src/config.json
index 89d6d1e8c7..ab7407947b 100644
--- a/src/config.json
+++ b/src/config.json
@@ -480,7 +480,7 @@
"v15": 2,
"author": "oasis",
"dd": true,
- "v16": false
+ "v16": true
}
]
},
From 6038957425030e67e97ccf9fc3f714502707a0c1 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Wed, 16 Sep 2026 20:38:57 +0800
Subject: [PATCH 05/13] =?UTF-8?q?style(tabs):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E5=8D=A1=E7=89=87=E6=A8=A1=E5=BC=8F=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/demos/h5/demo4.tsx | 1 +
src/packages/tabs/demos/h5/demo8.tsx | 1 +
src/packages/tabs/demos/taro/demo4.tsx | 1 +
src/packages/tabs/demos/taro/demo8.tsx | 1 +
src/packages/tabs/tabs.scss | 67 +++++++++++++++++++++++---
5 files changed, 63 insertions(+), 8 deletions(-)
diff --git a/src/packages/tabs/demos/h5/demo4.tsx b/src/packages/tabs/demos/h5/demo4.tsx
index aaca44d0ea..751113330f 100644
--- a/src/packages/tabs/demos/h5/demo4.tsx
+++ b/src/packages/tabs/demos/h5/demo4.tsx
@@ -7,6 +7,7 @@ const Demo4 = () => {
<>
{
setTabvalue(value)
}}
diff --git a/src/packages/tabs/demos/h5/demo8.tsx b/src/packages/tabs/demos/h5/demo8.tsx
index 7b28f4963c..d706c25598 100644
--- a/src/packages/tabs/demos/h5/demo8.tsx
+++ b/src/packages/tabs/demos/h5/demo8.tsx
@@ -7,6 +7,7 @@ const Demo8 = () => {
<>
{
diff --git a/src/packages/tabs/demos/taro/demo4.tsx b/src/packages/tabs/demos/taro/demo4.tsx
index 6c1a3df1ff..812b065967 100644
--- a/src/packages/tabs/demos/taro/demo4.tsx
+++ b/src/packages/tabs/demos/taro/demo4.tsx
@@ -6,6 +6,7 @@ const Demo4 = () => {
return (
{
setTabvalue(value)
}}
diff --git a/src/packages/tabs/demos/taro/demo8.tsx b/src/packages/tabs/demos/taro/demo8.tsx
index 580f732675..ebc9dfba71 100644
--- a/src/packages/tabs/demos/taro/demo8.tsx
+++ b/src/packages/tabs/demos/taro/demo8.tsx
@@ -6,6 +6,7 @@ const Demo8 = () => {
return (
{
setTabvalue(value)
diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss
index 52bf7cb238..7c800200cc 100644
--- a/src/packages/tabs/tabs.scss
+++ b/src/packages/tabs/tabs.scss
@@ -45,12 +45,6 @@
}
}
- &-card#{&}-left {
- .nut-tabs-titles-item {
- padding: 0 22px;
- }
- }
-
&-item {
position: relative;
display: flex;
@@ -172,10 +166,55 @@
&-card {
.nut-tabs-titles-item {
+ padding: 0 8px;
+ flex-direction: column;
+ justify-content: center;
+ line-height: initial;
+ background: transparent;
+
&-active {
+ position: relative;
+ z-index: 1;
+ margin: 0 24px;
font-weight: $font-weight-bold;
- background-color: $white;
- border-radius: $radius-base $radius-base 0 0;
+ background-color: $tabs-titles-item-active-background-color;
+
+ &::before,
+ &::after {
+ position: absolute;
+ bottom: 0;
+ width: 24px;
+ height: 100%;
+ content: '';
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ }
+
+ &::before {
+ left: -24px;
+ background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg transform='matrix(-1,0,0,1,48,0)'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%23ffffff' transform='translate(24, 0)'/%3E%3C/g%3E%3C/svg%3E");
+ }
+
+ &::after {
+ right: -24px;
+ background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%23ffffff'/%3E%3C/svg%3E");
+ }
+
+ &:first-child {
+ margin-left: 0;
+
+ &::before {
+ display: none;
+ }
+ }
+
+ &:last-child {
+ margin-right: 0;
+
+ &::after {
+ display: none;
+ }
+ }
}
}
}
@@ -234,6 +273,18 @@
}
}
+.nut-theme-dark {
+ .nut-tabs-titles-card .nut-tabs-titles-item-active {
+ &::before {
+ background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg transform='matrix(-1,0,0,1,48,0)'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%231f2226' transform='translate(24, 0)'/%3E%3C/g%3E%3C/svg%3E");
+ }
+
+ &::after {
+ background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%231f2226'/%3E%3C/svg%3E");
+ }
+ }
+}
+
.nut-tabs-vertical {
.nut-tabs-ellipsis {
white-space: break-spaces;
From 750042de402b666ff9d2c096ab177761ee270dab Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Fri, 18 Sep 2026 10:39:30 +0800
Subject: [PATCH 06/13] =?UTF-8?q?style(tabs):=20=E8=B0=83=E6=95=B4?=
=?UTF-8?q?=E9=80=89=E9=A1=B9=E5=8D=A1=E6=A0=87=E9=A2=98=E7=9A=84=E5=86=85?=
=?UTF-8?q?=E8=BE=B9=E8=B7=9D=EF=BC=8C=E4=BC=98=E5=8C=96=E8=BE=B9=E7=BC=98?=
=?UTF-8?q?=E7=95=99=E7=99=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/tabs.scss | 52 ++++++++++++++++++++++++++++++++++++-
1 file changed, 51 insertions(+), 1 deletion(-)
diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss
index 7c800200cc..ddf8bd80a9 100644
--- a/src/packages/tabs/tabs.scss
+++ b/src/packages/tabs/tabs.scss
@@ -41,7 +41,7 @@
&-right {
justify-content: flex-end;
.nut-tabs-titles-item {
- padding: 0 22px;
+ padding: 0 8px;
}
}
@@ -273,6 +273,29 @@
}
}
+// 标题栏边缘留白:该端 item 外侧 padding 翻倍,使边缘留白 = 相邻 tab 间距
+// 均分补首尾两端,左对齐仅补左端,右对齐仅补右端;水平方向生效
+.nut-tabs-horizontal {
+ .nut-tabs-titles:not(.nut-tabs-titles-right)
+ .nut-tabs-titles-item:first-child {
+ padding-left: calc(#{$tabs-titles-gap} * 2);
+ }
+
+ .nut-tabs-titles:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child {
+ padding-right: calc(#{$tabs-titles-gap} * 2);
+ }
+
+ .nut-tabs-titles-button {
+ &:not(.nut-tabs-titles-right) .nut-tabs-titles-item:first-child {
+ padding-left: 8px;
+ }
+
+ &:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child {
+ padding-right: 8px;
+ }
+ }
+}
+
.nut-theme-dark {
.nut-tabs-titles-card .nut-tabs-titles-item-active {
&::before {
@@ -425,6 +448,33 @@
}
}
+// RTL 边缘留白镜像:视觉左端=物理右侧、视觉右端=物理左侧,翻转补端方向
+[dir='rtl'] .nut-tabs-horizontal,
+.nut-rtl .nut-tabs-horizontal {
+ .nut-tabs-titles:not(.nut-tabs-titles-right)
+ .nut-tabs-titles-item:first-child {
+ padding-left: $tabs-titles-gap;
+ padding-right: calc(#{$tabs-titles-gap} * 2);
+ }
+
+ .nut-tabs-titles:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child {
+ padding-right: $tabs-titles-gap;
+ padding-left: calc(#{$tabs-titles-gap} * 2);
+ }
+
+ .nut-tabs-titles-button {
+ &:not(.nut-tabs-titles-right) .nut-tabs-titles-item:first-child {
+ padding-left: 4px;
+ padding-right: 8px;
+ }
+
+ &:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child {
+ padding-right: 4px;
+ padding-left: 8px;
+ }
+ }
+}
+
[dir='rtl'] .nut-tabs-vertical,
.nut-rtl .nut-tabs-vertical {
.nut-tabs-titles {
From 11b4f6892aec2a4e7b9b4aa402a2b36bfca23ef5 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Fri, 18 Sep 2026 10:55:45 +0800
Subject: [PATCH 07/13] =?UTF-8?q?style(tabs):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E9=80=89=E9=A1=B9=E5=8D=A1=E6=A0=87=E9=A2=98=E7=9A=84=E5=AD=97?=
=?UTF-8?q?=E4=BD=93=E7=B2=97=E7=BB=86=E4=B8=BA400?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/demos/h5/demo24.tsx | 1 +
src/packages/tabs/demos/taro/demo24.tsx | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/packages/tabs/demos/h5/demo24.tsx b/src/packages/tabs/demos/h5/demo24.tsx
index f294e07f48..b566643cf8 100644
--- a/src/packages/tabs/demos/h5/demo24.tsx
+++ b/src/packages/tabs/demos/h5/demo24.tsx
@@ -32,6 +32,7 @@ const Demo24 = () => {
style={{
textAlign: 'center',
fontSize: '11px',
+ fontWeight: '400',
lineHeight: '16px',
color: active
? 'var(--nutui-tabs-titles-item-active-color, #ff0f23)'
diff --git a/src/packages/tabs/demos/taro/demo24.tsx b/src/packages/tabs/demos/taro/demo24.tsx
index 859b4897bd..8bd2d14185 100644
--- a/src/packages/tabs/demos/taro/demo24.tsx
+++ b/src/packages/tabs/demos/taro/demo24.tsx
@@ -33,6 +33,7 @@ const Demo24 = () => {
style={{
textAlign: 'center',
fontSize: pxTransform(11),
+ fontWeight: '400',
lineHeight: pxTransform(16),
color: active
? 'var(--nutui-tabs-titles-item-active-color, #ff0f23)'
From 3ae1c70c8f403e73ab0a028e7bc3c6aa373841fb Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Fri, 18 Sep 2026 15:35:20 +0800
Subject: [PATCH 08/13] =?UTF-8?q?chore(tabs):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=B7=A6=E5=AF=B9=E9=BD=90=E8=BE=85=E5=8A=A9=E6=93=8D=E4=BD=9C?=
=?UTF-8?q?=E7=A4=BA=E4=BE=8B=E5=8F=8A=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/demo.taro.tsx | 5 ++
src/packages/tabs/demo.tsx | 5 ++
src/packages/tabs/demos/h5/demo25.tsx | 90 +++++++++++++++++++++++++
src/packages/tabs/demos/taro/demo25.tsx | 89 ++++++++++++++++++++++++
src/packages/tabs/doc.en-US.md | 8 +++
src/packages/tabs/doc.md | 8 +++
src/packages/tabs/doc.taro.md | 8 +++
src/packages/tabs/doc.zh-TW.md | 8 +++
8 files changed, 221 insertions(+)
create mode 100644 src/packages/tabs/demos/h5/demo25.tsx
create mode 100644 src/packages/tabs/demos/taro/demo25.tsx
diff --git a/src/packages/tabs/demo.taro.tsx b/src/packages/tabs/demo.taro.tsx
index 522ece65ac..70fa174123 100644
--- a/src/packages/tabs/demo.taro.tsx
+++ b/src/packages/tabs/demo.taro.tsx
@@ -29,6 +29,7 @@ import Demo21 from './demos/taro/demo21'
import Demo22 from './demos/taro/demo22'
import Demo23 from './demos/taro/demo23'
import Demo24 from './demos/taro/demo24'
+import Demo25 from './demos/taro/demo25'
const TabsDemo = () => {
const [translated] = useTranslate({
@@ -51,6 +52,7 @@ const TabsDemo = () => {
title7: 'Title 字体尺寸:20px 12px',
title8: '自定义标签栏',
title15: '自定义标签栏-辅助信息',
+ title16: '左对齐-辅助操作',
title9: 'Tabpane 自动高度',
title10: 'CSS 粘性布局',
title11: 'Title 左对齐',
@@ -77,6 +79,7 @@ const TabsDemo = () => {
title7: 'Title FontSize: 20px 12px',
title8: 'Custom Tab Bar',
title15: 'Custom Tab Bar - Auxiliary Info',
+ title16: 'Left Align - Auxiliary Operation',
title9: 'Tabpane Auto Height',
title10: 'CSS Sticky',
title11: 'Title Left Align',
@@ -145,6 +148,8 @@ const TabsDemo = () => {
{translated.title15}
+ {translated.title16}
+
>
)
diff --git a/src/packages/tabs/demo.tsx b/src/packages/tabs/demo.tsx
index 7ff4903ba8..1049710ff3 100644
--- a/src/packages/tabs/demo.tsx
+++ b/src/packages/tabs/demo.tsx
@@ -24,6 +24,7 @@ import Demo21 from './demos/h5/demo21'
import Demo22 from './demos/h5/demo22'
import Demo23 from './demos/h5/demo23'
import Demo24 from './demos/h5/demo24'
+import Demo25 from './demos/h5/demo25'
const TabsDemo = () => {
const [translated] = useTranslate({
@@ -45,6 +46,7 @@ const TabsDemo = () => {
title7: 'Title 字体尺寸:20px 12px',
title8: '自定义标签栏',
title15: '自定义标签栏-辅助信息',
+ title16: '左对齐-辅助操作',
title9: 'Tabpane 自动高度',
title10: 'CSS 粘性布局',
title11: 'Title 左对齐',
@@ -70,6 +72,7 @@ const TabsDemo = () => {
title7: 'Title FontSize: 20px 12px',
title8: 'Custom Tab Bar',
title15: 'Custom Tab Bar - Auxiliary Info',
+ title16: 'Left Align - Auxiliary Operation',
title9: 'Tabpane Auto Height',
title10: 'CSS Sticky',
title11: 'Title Left Align',
@@ -130,6 +133,8 @@ const TabsDemo = () => {
{translated.title15}
+ {translated.title16}
+
>
)
diff --git a/src/packages/tabs/demos/h5/demo25.tsx b/src/packages/tabs/demos/h5/demo25.tsx
new file mode 100644
index 0000000000..1fe8ef0e23
--- /dev/null
+++ b/src/packages/tabs/demos/h5/demo25.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from 'react'
+import { Tabs } from '@nutui/nutui-react'
+import { Search } from '@nutui/icons-react'
+
+const Demo25 = () => {
+ const [tabvalue, setTabvalue] = useState('c1')
+ const list = [
+ { title: 'Tab longitem 1', paneKey: 'c1' },
+ { title: 'Tab longitem 2', paneKey: 'c2' },
+ { title: 'Tab3', paneKey: 'c3' },
+ { title: 'Tab4', paneKey: 'c4' },
+ { title: 'Tab5', paneKey: 'c5' },
+ { title: 'Tab6', paneKey: 'c6' },
+ ]
+
+ const maskUrl =
+ 'https://img20.360buyimg.com/img/jfs/t1/528620/16/3371/845/6aacb069F6a9923f5/0276020048fb8b3a.png'
+
+ const renderTitles = () => {
+ const items = list.map((item) => {
+ const active = tabvalue === item.paneKey
+ return (
+ setTabvalue(item.paneKey)}
+ className={`nut-tabs-titles-item ${active ? 'nut-tabs-titles-item-active' : ''}`}
+ >
+ {item.title}
+
+
+ )
+ })
+ // 末尾占位:预留 >= 辅助操作宽度的空白,滚到最右端时末项文字不被辅助操作遮挡
+ items.push(
+
+ )
+ return items
+ }
+
+ return (
+
+
+ {list.map((item) => (
+
+ {item.title}
+
+ ))}
+
+
+

+
+
+
+
+
+ )
+}
+export default Demo25
diff --git a/src/packages/tabs/demos/taro/demo25.tsx b/src/packages/tabs/demos/taro/demo25.tsx
new file mode 100644
index 0000000000..473dbd9b47
--- /dev/null
+++ b/src/packages/tabs/demos/taro/demo25.tsx
@@ -0,0 +1,89 @@
+import React, { useState } from 'react'
+import { View, Text, Image } from '@tarojs/components'
+import { Tabs, pxTransform } from '@nutui/nutui-react-taro'
+import { Search } from '@nutui/icons-react-taro'
+
+const Demo25 = () => {
+ const [tabvalue, setTabvalue] = useState('c1')
+ const list = [
+ { title: 'Tab longitem 1', paneKey: 'c1' },
+ { title: 'Tab longitem 2', paneKey: 'c2' },
+ { title: 'Tab3', paneKey: 'c3' },
+ { title: 'Tab4', paneKey: 'c4' },
+ { title: 'Tab5', paneKey: 'c5' },
+ { title: 'Tab6', paneKey: 'c6' },
+ ]
+
+ const maskUrl =
+ 'https://img20.360buyimg.com/img/jfs/t1/528620/16/3371/845/6aacb069F6a9923f5/0276020048fb8b3a.png'
+
+ const renderTitles = () => {
+ const items = list.map((item) => {
+ const active = tabvalue === item.paneKey
+ return (
+ setTabvalue(item.paneKey)}
+ className={`nut-tabs-titles-item ${active ? 'nut-tabs-titles-item-active' : ''}`}
+ >
+ {item.title}
+
+
+ )
+ })
+ // 末尾占位:预留 >= 辅助操作宽度的空白,滚到最右端时末项文字不被辅助操作遮挡
+ items.push(
+
+ )
+ return items
+ }
+
+ return (
+
+
+ {list.map((item) => (
+
+ {item.title}
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+export default Demo25
diff --git a/src/packages/tabs/doc.en-US.md b/src/packages/tabs/doc.en-US.md
index ef853b2cfa..b6057ddae0 100644
--- a/src/packages/tabs/doc.en-US.md
+++ b/src/packages/tabs/doc.en-US.md
@@ -208,6 +208,14 @@ When autoHeight is set to true, nut-tabs and nut-tabs\_\_content will change wit
:::
+### Left Align - Auxiliary Operation
+
+:::demo
+
+
+
+:::
+
## Tabs
### Props
diff --git a/src/packages/tabs/doc.md b/src/packages/tabs/doc.md
index 3b50019405..696b29a971 100644
--- a/src/packages/tabs/doc.md
+++ b/src/packages/tabs/doc.md
@@ -208,6 +208,14 @@ import { Tabs } from '@nutui/nutui-react'
:::
+### 左对齐-辅助操作
+
+:::demo
+
+
+
+:::
+
## Tabs
### Props
diff --git a/src/packages/tabs/doc.taro.md b/src/packages/tabs/doc.taro.md
index 2ad8385b3b..a4f50b110d 100644
--- a/src/packages/tabs/doc.taro.md
+++ b/src/packages/tabs/doc.taro.md
@@ -206,6 +206,14 @@ import { Tabs } from '@nutui/nutui-react-taro'
:::
+### 左对齐-辅助操作
+
+:::demo
+
+
+
+:::
+
## Tabs
### Props
diff --git a/src/packages/tabs/doc.zh-TW.md b/src/packages/tabs/doc.zh-TW.md
index e76d2b2f90..1a5716badb 100644
--- a/src/packages/tabs/doc.zh-TW.md
+++ b/src/packages/tabs/doc.zh-TW.md
@@ -206,6 +206,14 @@ import { Tabs } from '@nutui/nutui-react'
:::
+### 左對齊-輔助操作
+
+:::demo
+
+
+
+:::
+
## Tabs
### Props
From 983094b72fabadd86c37dae6d9c5301cde74e75e Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Mon, 21 Sep 2026 20:19:55 +0800
Subject: [PATCH 09/13] =?UTF-8?q?feat(tabs):=20=E6=B7=BB=E5=8A=A0=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=A0=87=E9=A2=98=E6=A0=8F?=
=?UTF-8?q?=E5=8F=AF=E6=BB=91=E5=8A=A8=E7=9A=84=E6=94=AF=E6=8C=81=E5=8F=8A?=
=?UTF-8?q?=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/tabs.scss | 65 +++++++++++++++++++++++++++++++++
src/packages/tabs/tabs.taro.tsx | 50 ++++++++++++++++++++++---
src/packages/tabs/tabs.tsx | 36 +++++++++++++++++-
3 files changed, 144 insertions(+), 7 deletions(-)
diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss
index ddf8bd80a9..ac8e247665 100644
--- a/src/packages/tabs/tabs.scss
+++ b/src/packages/tabs/tabs.scss
@@ -43,6 +43,10 @@
.nut-tabs-titles-item {
padding: 0 8px;
}
+
+ .nut-tabs-list {
+ justify-content: flex-end;
+ }
}
&-item {
@@ -219,6 +223,30 @@
}
}
+ &-card.nut-tabs-titles-not-scrollable {
+ &:not(.nut-tabs-titles-right) .nut-tabs-titles-item-active {
+ // LTR 左对齐(及 RTL 左对齐类):末项一侧恢复肩部与间距
+ &:last-child {
+ margin-right: 24px;
+
+ &::after {
+ display: block;
+ }
+ }
+ }
+
+ &.nut-tabs-titles-right .nut-tabs-titles-item-active {
+ // LTR 右对齐(及 RTL 右对齐类):首项一侧恢复肩部与间距
+ &:first-child {
+ margin-left: 24px;
+
+ &::before {
+ display: block;
+ }
+ }
+ }
+ }
+
&-button {
.nut-tabs-titles-item {
padding: 0 4px;
@@ -475,6 +503,43 @@
}
}
+// RTL 卡片模式留白端镜像:对齐类(flex-start/flex-end)随书写方向翻转视觉端,
+// 贴边端/留白端与 DOM 首末项的对应关系同 LTR 相反,贴边侧直角、留白侧恢复肩部需按物理边重写
+[dir='rtl'] .nut-tabs-titles-card.nut-tabs-titles-not-scrollable,
+.nut-rtl .nut-tabs-titles-card.nut-tabs-titles-not-scrollable {
+ // RTL 左对齐类:内容贴视觉右端,首项贴边(直角在物理右侧)、末项邻留白
+ &:not(.nut-tabs-titles-right) .nut-tabs-titles-item-active {
+ &:first-child {
+ margin-left: 24px;
+ margin-right: 0;
+
+ &::before {
+ display: block;
+ }
+
+ &::after {
+ display: none;
+ }
+ }
+ }
+
+ // RTL 右对齐类:内容贴视觉左端,末项贴边(直角在物理左侧)、首项邻留白
+ &.nut-tabs-titles-right .nut-tabs-titles-item-active {
+ &:last-child {
+ margin-left: 0;
+ margin-right: 24px;
+
+ &::before {
+ display: none;
+ }
+
+ &::after {
+ display: block;
+ }
+ }
+ }
+}
+
[dir='rtl'] .nut-tabs-vertical,
.nut-rtl .nut-tabs-vertical {
.nut-tabs-titles {
diff --git a/src/packages/tabs/tabs.taro.tsx b/src/packages/tabs/tabs.taro.tsx
index 296991282a..ecd41f6cda 100644
--- a/src/packages/tabs/tabs.taro.tsx
+++ b/src/packages/tabs/tabs.taro.tsx
@@ -1,4 +1,10 @@
-import React, { FunctionComponent, useEffect, useRef, useState } from 'react'
+import React, {
+ FunctionComponent,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from 'react'
import { ScrollView, View } from '@tarojs/components'
import classNames from 'classnames'
import { JoySmile } from '@nutui/icons-react-taro'
@@ -58,6 +64,10 @@ export const Tabs: FunctionComponent> & {
const titleItemsRef = useRef([])
+ // 卡片模式下,标题栏可横向滑动时激活卡片贴合两端边缘;不可滑动时(全部 tab 放得下)
+ // 贴边端直角、留白端恢复双肩
+ const [isScrollable, setIsScrollable] = useState(!align)
+
const getTitles = () => {
const titles: TabsTitle[] = []
React.Children.forEach(children, (child, idx) => {
@@ -99,10 +109,12 @@ export const Tabs: FunctionComponent> & {
)
const classesTitle = classNames(`${classPrefix}-titles`, {
[`${classPrefix}-titles-${activeType}`]: activeType,
+ [`${classPrefix}-titles-not-scrollable`]:
+ activeType === 'card' && !isScrollable,
[`${classPrefix}-titles-${align}`]: align,
})
- const getRect = (selector: string) => {
+ const getRect = useCallback((selector: string) => {
return new Promise((resolve) => {
const query = createSelectorQuery()
const scope = tabsRef.current?._scope
@@ -114,8 +126,8 @@ export const Tabs: FunctionComponent> & {
resolve(rect[0])
})
})
- }
- const getAllRect = (selector: string) => {
+ }, [])
+ const getAllRect = useCallback((selector: string) => {
return new Promise((resolve) => {
const query = createSelectorQuery()
const scope = tabsRef.current?._scope
@@ -127,7 +139,7 @@ export const Tabs: FunctionComponent> & {
resolve(rect[0])
})
})
- }
+ }, [])
type RectItem = {
bottom: number
dataset: { sid: string }
@@ -138,6 +150,28 @@ export const Tabs: FunctionComponent> & {
top: number
width: number
}
+ const measureScrollable = useCallback(() => {
+ if (activeType !== 'card' || direction !== 'horizontal' || !align) return
+ raf(() => {
+ Promise.all([
+ getRect(`#nut-tabs-titles-${name || uuid}`),
+ getAllRect(`#nut-tabs-titles-${name || uuid} .nut-tabs-titles-item`),
+ ]).then(([navRect, itemRects]: any) => {
+ if (!navRect || !itemRects || !itemRects.length) return
+ const totalWidth = itemRects.reduce(
+ (sum: number, curr: RectItem) => sum + curr.width,
+ 0
+ )
+ // 卡片模式 active 卡片 margin 不在 boundingClientRect 内,按最大 48px 预留,
+ // 与 scrollIntoView 的钳制口径一致,避免临界内容误判为放得下
+ setIsScrollable(totalWidth + 48 - navRect.width > 1)
+ })
+ })
+ }, [activeType, direction, name, uuid, getRect, getAllRect, align])
+ // 标题/内容变化后重新布局再测;挂载后首帧由默认 false 呈现对称样式,测量完异步翻转
+ useEffect(() => {
+ measureScrollable()
+ }, [measureScrollable, children])
const scrollWithAnimation = useRef(false)
const [scrollLeft, setScrollLeft] = useState(0)
const [scrollTop, setScrollTop] = useState(0)
@@ -180,9 +214,13 @@ export const Tabs: FunctionComponent> & {
const left = titleRects
.slice(0, index)
.reduce((prev: number, curr: RectItem) => prev + curr.width, 0)
+ // 卡片模式 active 卡片带 margin: 0 24px(贴边侧为 0),boundingClientRect 不含 margin,
+ // 矩形宽求和会少算,钳制点偏短导致滚动到末尾时末项卡片显示不全;
+ // 按最大 48px 预留,超设部分由 ScrollView 自动截断到真实最大滚动距离
+ const marginExtra = activeType === 'card' ? 48 : 0
to = Math.min(
Math.max(0, left - (navRect.width - titleRect.width) / 2),
- totalWidth - navRect.width
+ totalWidth + marginExtra - navRect.width
)
}
diff --git a/src/packages/tabs/tabs.tsx b/src/packages/tabs/tabs.tsx
index 4c7a964232..7ea9523cc6 100644
--- a/src/packages/tabs/tabs.tsx
+++ b/src/packages/tabs/tabs.tsx
@@ -1,4 +1,11 @@
-import React, { FunctionComponent, useEffect, useRef } from 'react'
+import React, {
+ FunctionComponent,
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState,
+} from 'react'
import classNames from 'classnames'
import { JoySmile } from '@nutui/icons-react'
import { ComponentDefaults } from '@/utils/typings'
@@ -51,6 +58,31 @@ export const Tabs: FunctionComponent> & {
const navRef = useRef(null)
+ // 卡片模式下,标题栏可横向滑动时激活卡片贴合两端边缘;不可滑动时(全部 tab 放得下)
+ // 贴边端直角、留白端恢复双肩,依赖该标记切换样式
+ const [isScrollable, setIsScrollable] = useState(!align)
+ const measureScrollable = useCallback(() => {
+ if (activeType !== 'card' || direction !== 'horizontal' || !align) return
+ const nav = navRef.current
+ if (!nav) return
+ setIsScrollable(nav.scrollWidth - nav.clientWidth > 1)
+ }, [activeType, direction, align])
+
+ // 挂载后立即测量,避免首帧样式闪变
+ useLayoutEffect(() => {
+ measureScrollable()
+ }, [measureScrollable])
+
+ // 标题/内容变化后重新布局,再测一次;窗口尺寸变化时也重测
+ useEffect(() => {
+ raf(measureScrollable)
+ }, [children, measureScrollable])
+ useEffect(() => {
+ const onResize = () => raf(measureScrollable)
+ window.addEventListener('resize', onResize)
+ return () => window.removeEventListener('resize', onResize)
+ }, [measureScrollable])
+
const scrollDirection = (
nav: HTMLDivElement,
to: number,
@@ -131,6 +163,8 @@ export const Tabs: FunctionComponent> & {
const classesTitle = classNames(`${classPrefix}-titles`, {
[`${classPrefix}-titles-${activeType}`]: activeType,
[`${classPrefix}-titles-scrollable`]: true,
+ [`${classPrefix}-titles-not-scrollable`]:
+ activeType === 'card' && !isScrollable,
[`${classPrefix}-titles-${align}`]: align,
})
From fd0fccffafa5622617a896b6fae46cd0f193e6c4 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Mon, 21 Sep 2026 21:08:44 +0800
Subject: [PATCH 10/13] =?UTF-8?q?refactor(tabs):=20=E7=A7=BB=E9=99=A4?=
=?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E8=83=8C=E6=99=AF=E5=9B=BE?=
=?UTF-8?q?=E5=92=8C=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/demos/h5/demo25.tsx | 17 ++++++++---------
src/packages/tabs/demos/taro/demo25.tsx | 18 +++++++++---------
2 files changed, 17 insertions(+), 18 deletions(-)
diff --git a/src/packages/tabs/demos/h5/demo25.tsx b/src/packages/tabs/demos/h5/demo25.tsx
index 1fe8ef0e23..6c514d4147 100644
--- a/src/packages/tabs/demos/h5/demo25.tsx
+++ b/src/packages/tabs/demos/h5/demo25.tsx
@@ -13,9 +13,6 @@ const Demo25 = () => {
{ title: 'Tab6', paneKey: 'c6' },
]
- const maskUrl =
- 'https://img20.360buyimg.com/img/jfs/t1/528620/16/3371/845/6aacb069F6a9923f5/0276020048fb8b3a.png'
-
const renderTitles = () => {
const items = list.map((item) => {
const active = tabvalue === item.paneKey
@@ -46,7 +43,6 @@ const Demo25 = () => {
@@ -66,10 +62,13 @@ const Demo25 = () => {
height: 'var(--nutui-tabs-titles-height, 36px)',
}}
>
-
{
justifyContent: 'center',
height: '100%',
width: 'calc(36px * var(--nut-scale-f, 1))',
- background: 'var(--nutui-tabs-titles-background-color, #fff)',
+ background: 'var(--nutui-color-background, #f0f2f7)',
}}
>
diff --git a/src/packages/tabs/demos/taro/demo25.tsx b/src/packages/tabs/demos/taro/demo25.tsx
index 473dbd9b47..2d578fea90 100644
--- a/src/packages/tabs/demos/taro/demo25.tsx
+++ b/src/packages/tabs/demos/taro/demo25.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
-import { View, Text, Image } from '@tarojs/components'
+import { View, Text } from '@tarojs/components'
import { Tabs, pxTransform } from '@nutui/nutui-react-taro'
import { Search } from '@nutui/icons-react-taro'
@@ -14,9 +14,6 @@ const Demo25 = () => {
{ title: 'Tab6', paneKey: 'c6' },
]
- const maskUrl =
- 'https://img20.360buyimg.com/img/jfs/t1/528620/16/3371/845/6aacb069F6a9923f5/0276020048fb8b3a.png'
-
const renderTitles = () => {
const items = list.map((item) => {
const active = tabvalue === item.paneKey
@@ -46,7 +43,6 @@ const Demo25 = () => {
@@ -66,9 +62,13 @@ const Demo25 = () => {
height: 'var(--nutui-tabs-titles-height, 36px)',
}}
>
-
{
justifyContent: 'center',
height: '100%',
width: pxTransform(36),
- background: 'var(--nutui-tabs-titles-background-color, #fff)',
+ background: 'var(--nutui-color-background, #f0f2f7)',
}}
>
From 8bb2ac527475835eb5b0f3e7df9c13908e298ef9 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Mon, 21 Sep 2026 22:03:13 +0800
Subject: [PATCH 11/13] =?UTF-8?q?feat(tabs):=20=E6=9B=B4=E6=96=B0=E9=80=89?=
=?UTF-8?q?=E9=A1=B9=E5=8D=A1=E6=A0=87=E9=A2=98=E6=A0=B7=E5=BC=8F=EF=BC=8C?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8A=A8=E6=80=81=E7=BC=A9=E6=94=BE=E5=92=8C?=
=?UTF-8?q?=E8=83=8C=E6=99=AF=E8=89=B2=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/demos/h5/demo22.tsx | 10 ++++++++--
src/packages/tabs/demos/h5/demo24.tsx | 8 +++++---
src/packages/tabs/demos/h5/demo25.tsx | 20 ++++++++++++++++----
src/packages/tabs/demos/h5/demo4.tsx | 6 +++++-
src/packages/tabs/demos/h5/demo8.tsx | 6 +++++-
src/packages/tabs/demos/taro/demo25.tsx | 14 +++++++++++---
src/packages/tabs/demos/taro/demo4.tsx | 8 ++++++--
src/packages/tabs/demos/taro/demo8.tsx | 8 ++++++--
src/packages/tabs/doc.en-US.md | 4 ++--
src/packages/tabs/doc.md | 4 ++--
src/packages/tabs/doc.taro.md | 4 ++--
src/packages/tabs/doc.zh-TW.md | 4 ++--
src/styles/variables-daojia.scss | 2 +-
src/styles/variables-jmapp.scss | 2 +-
src/styles/variables-jrkf.scss | 2 +-
src/styles/variables.scss | 4 ++--
16 files changed, 75 insertions(+), 31 deletions(-)
diff --git a/src/packages/tabs/demos/h5/demo22.tsx b/src/packages/tabs/demos/h5/demo22.tsx
index a06e85e072..a9051bf676 100644
--- a/src/packages/tabs/demos/h5/demo22.tsx
+++ b/src/packages/tabs/demos/h5/demo22.tsx
@@ -11,7 +11,10 @@ const Demo22 = () => {
onChange={(value) => {
setTabvalue(value)
}}
- style={{ '--nutui-tabs-titles-font-size': '20px' }}
+ style={{
+ '--nutui-tabs-titles-font-size':
+ 'calc(20px * var(--nut-scale-font, 1))',
+ }}
>
Tab longitem
Tab 2
@@ -22,7 +25,10 @@ const Demo22 = () => {
onChange={(value) => {
setTabvalue1(value)
}}
- style={{ '--nutui-tabs-titles-font-size': '12px' }}
+ style={{
+ '--nutui-tabs-titles-font-size':
+ 'calc(12px * var(--nut-scale-font, 1))',
+ }}
>
Tab longitem
Tab 2
diff --git a/src/packages/tabs/demos/h5/demo24.tsx b/src/packages/tabs/demos/h5/demo24.tsx
index b566643cf8..86604d84fa 100644
--- a/src/packages/tabs/demos/h5/demo24.tsx
+++ b/src/packages/tabs/demos/h5/demo24.tsx
@@ -12,7 +12,9 @@ const Demo24 = () => {
return (
{
return list.map((item) => {
const active = tabvalue === item.paneKey
@@ -31,9 +33,9 @@ const Demo24 = () => {
{
position: 'relative',
}}
>
-
+
{list.map((item) => (
{item.title}
@@ -59,12 +67,13 @@ const Demo25 = () => {
right: 0,
display: 'flex',
alignItems: 'center',
- height: 'var(--nutui-tabs-titles-height, 36px)',
+ height:
+ 'var(--nutui-tabs-titles-height, calc(36px * var(--nut-scale-f, 1)))',
}}
>
{
background: 'var(--nutui-color-background, #f0f2f7)',
}}
>
-
+
diff --git a/src/packages/tabs/demos/h5/demo4.tsx b/src/packages/tabs/demos/h5/demo4.tsx
index 751113330f..592fe0aee7 100644
--- a/src/packages/tabs/demos/h5/demo4.tsx
+++ b/src/packages/tabs/demos/h5/demo4.tsx
@@ -7,7 +7,11 @@ const Demo4 = () => {
<>
{
setTabvalue(value)
}}
diff --git a/src/packages/tabs/demos/h5/demo8.tsx b/src/packages/tabs/demos/h5/demo8.tsx
index d706c25598..37fba43131 100644
--- a/src/packages/tabs/demos/h5/demo8.tsx
+++ b/src/packages/tabs/demos/h5/demo8.tsx
@@ -7,7 +7,11 @@ const Demo8 = () => {
<>
{
diff --git a/src/packages/tabs/demos/taro/demo25.tsx b/src/packages/tabs/demos/taro/demo25.tsx
index 2d578fea90..0b3286fc5d 100644
--- a/src/packages/tabs/demos/taro/demo25.tsx
+++ b/src/packages/tabs/demos/taro/demo25.tsx
@@ -45,7 +45,15 @@ const Demo25 = () => {
position: 'relative',
}}
>
-
+
{list.map((item) => (
{item.title}
@@ -59,7 +67,7 @@ const Demo25 = () => {
right: 0,
display: 'flex',
alignItems: 'center',
- height: 'var(--nutui-tabs-titles-height, 36px)',
+ height: `var(--nutui-tabs-titles-height, ${pxTransform(36)})`,
}}
>
{
background: 'var(--nutui-color-background, #f0f2f7)',
}}
>
-
+
diff --git a/src/packages/tabs/demos/taro/demo4.tsx b/src/packages/tabs/demos/taro/demo4.tsx
index 812b065967..72ef13827f 100644
--- a/src/packages/tabs/demos/taro/demo4.tsx
+++ b/src/packages/tabs/demos/taro/demo4.tsx
@@ -1,12 +1,16 @@
import React, { useState } from 'react'
-import { Tabs } from '@nutui/nutui-react-taro'
+import { Tabs, pxTransform } from '@nutui/nutui-react-taro'
const Demo4 = () => {
const [tabvalue, setTabvalue] = useState('0')
return (
{
setTabvalue(value)
}}
diff --git a/src/packages/tabs/demos/taro/demo8.tsx b/src/packages/tabs/demos/taro/demo8.tsx
index ebc9dfba71..2cafbfb418 100644
--- a/src/packages/tabs/demos/taro/demo8.tsx
+++ b/src/packages/tabs/demos/taro/demo8.tsx
@@ -1,12 +1,16 @@
import React, { useState } from 'react'
-import { Tabs } from '@nutui/nutui-react-taro'
+import { Tabs, pxTransform } from '@nutui/nutui-react-taro'
const Demo8 = () => {
const [tabvalue, setTabvalue] = useState('0')
return (
{
setTabvalue(value)
diff --git a/src/packages/tabs/doc.en-US.md b/src/packages/tabs/doc.en-US.md
index b6057ddae0..d07f50526e 100644
--- a/src/packages/tabs/doc.en-US.md
+++ b/src/packages/tabs/doc.en-US.md
@@ -254,7 +254,7 @@ The component provides the following CSS variables, which can be used to customi
| Name | Description | Default |
| --- | --- | --- |
| \--nutui-tabs-titles-height | height of titles in horizontal direction | `36px` |
-| \--nutui-tabs-titles-background-color | Tab title background color | `$color-background` |
+| \--nutui-tabs-titles-background-color | Tab title background color | `$color-background-overlay` |
| \--nutui-tabs-titles-gap | Tab title margin | `8px` |
| \--nutui-tabs-titles-font-size | Tab title font size | `$font-size-md` |
| \--nutui-tabs-titles-item-font-family | Tab title font family | `PingFangSC-Semibold` |
@@ -276,7 +276,7 @@ The component provides the following CSS variables, which can be used to customi
| \--nutui-tabs-button-font-size | Font size of unselected button-type title | `$font-size-base` |
| \--nutui-tabs-button-active-font-size | Font size of selected button-type title | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | Font weight of selected button-type title | `$font-weight-bold` |
-| \--nutui-tabs-button-background-color | Background color of unselected button-type title | `$color-background-overlay` |
+| \--nutui-tabs-button-background-color | Background color of unselected button-type title | `var(--nutui-color-background-component)` |
| \--nutui-tabs-vertical-titles-width | Width of vertical titles | `100px` |
| \--nutui-tabs-vertical-titles-item-height | height of vertical titles | `40px` |
| \--nutui-tabs-vertical-tab-line-color | vertical line color | `linear-gradient(180deg, $color-primary 0%, rgba(#fa2c19, 0.15) 100%)` |
diff --git a/src/packages/tabs/doc.md b/src/packages/tabs/doc.md
index 696b29a971..961d6d41bb 100644
--- a/src/packages/tabs/doc.md
+++ b/src/packages/tabs/doc.md
@@ -254,7 +254,7 @@ import { Tabs } from '@nutui/nutui-react'
| 名称 | 说明 | 默认值 |
| --- | --- | --- |
| \--nutui-tabs-titles-height | 水平方向标题的高度 | `36px` |
-| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background` |
+| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background-overlay` |
| \--nutui-tabs-titles-gap | Tab 标题的左右 margin | `8px` |
| \--nutui-tabs-titles-font-size | Tab 标题的字号 | `$font-size-md` |
| \--nutui-tabs-titles-item-font-family | Tab 标题的字体 | `PingFangSC-Semibold` |
@@ -276,7 +276,7 @@ import { Tabs } from '@nutui/nutui-react'
| \--nutui-tabs-button-font-size | 按钮模式未选中标题的字号 | `$font-size-base` |
| \--nutui-tabs-button-active-font-size | 按钮模式选中标题的字号 | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | 按钮模式选中标题的字重 | `$font-weight-bold` |
-| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `$color-background-overlay` |
+| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `var(--nutui-color-background-component)` |
| \--nutui-tabs-vertical-titles-width | 垂直方向标题的宽度 | `100px` |
| \--nutui-tabs-vertical-titles-item-height | 垂直方向标题的高度 | `40px` |
| \--nutui-tabs-vertical-tab-line-color | 垂直方向线条颜色 | `linear-gradient(180deg, $color-primary 0%, rgba(#FF0F23, 0.15) 100%)` |
diff --git a/src/packages/tabs/doc.taro.md b/src/packages/tabs/doc.taro.md
index a4f50b110d..a81997546c 100644
--- a/src/packages/tabs/doc.taro.md
+++ b/src/packages/tabs/doc.taro.md
@@ -252,7 +252,7 @@ import { Tabs } from '@nutui/nutui-react-taro'
| 名称 | 说明 | 默认值 |
| --- | --- | --- |
| \--nutui-tabs-titles-height | 水平方向标题的高度 | `36px` |
-| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background` |
+| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background-overlay` |
| \--nutui-tabs-titles-gap | Tab 标题的左右 margin | `8px` |
| \--nutui-tabs-titles-font-size | Tab 标题的字号 | `$font-size-md` |
| \--nutui-tabs-titles-item-font-family | Tab 标题的字体 | `PingFangSC-Semibold` |
@@ -274,7 +274,7 @@ import { Tabs } from '@nutui/nutui-react-taro'
| \--nutui-tabs-button-font-size | 按钮模式未选中标题的字号 | `$font-size-base` |
| \--nutui-tabs-button-active-font-size | 按钮模式选中标题的字号 | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | 按钮模式选中标题的字重 | `$font-weight-bold` |
-| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `$color-background-overlay` |
+| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `var(--nutui-color-background-component)` |
| \--nutui-tabs-vertical-titles-width | 垂直方向标题的宽度 | `100px` |
| \--nutui-tabs-vertical-titles-item-height | 垂直方向标题的高度 | `40px` |
| \--nutui-tabs-vertical-tab-line-color | 垂直方向线条颜色 | `linear-gradient(180deg, $color-primary 0%, rgba(#FF0F23, 0.15) 100%)` |
diff --git a/src/packages/tabs/doc.zh-TW.md b/src/packages/tabs/doc.zh-TW.md
index 1a5716badb..88753dcf4b 100644
--- a/src/packages/tabs/doc.zh-TW.md
+++ b/src/packages/tabs/doc.zh-TW.md
@@ -252,7 +252,7 @@ import { Tabs } from '@nutui/nutui-react'
| 名稱 | 說明 | 默認值 |
| --- | --- | --- |
| \--nutui-tabs-titles-height | 水平方向標題的高度 | `36px` |
-| \--nutui-tabs-titles-background-color | Tab 標題的背景色 | `$color-background` |
+| \--nutui-tabs-titles-background-color | Tab 標題的背景色 | `$color-background-overlay` |
| \--nutui-tabs-titles-gap | Tab 標題的左右 margin | `8px` |
| \--nutui-tabs-titles-font-size | Tab 標題的字號 | `$font-size-md` |
| \--nutui-tabs-titles-item-font-family | Tab 標題的字體 | `PingFangSC-Semibold` |
@@ -274,7 +274,7 @@ import { Tabs } from '@nutui/nutui-react'
| \--nutui-tabs-button-font-size | 按鈕模式未選中標題的字號 | `$font-size-base` |
| \--nutui-tabs-button-active-font-size | 按鈕模式選中標題的字號 | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | 按鈕模式選中標題的字重 | `$font-weight-bold` |
-| \--nutui-tabs-button-background-color | 按鈕模式未選中標題的背景色 | `$color-background-overlay` |
+| \--nutui-tabs-button-background-color | 按鈕模式未選中標題的背景色 | `var(--nutui-color-background-component)` |
| \--nutui-tabs-vertical-titles-width | 垂直方向標題的寬度 | `100px` |
| \--nutui-tabs-vertical-titles-item-height | 垂直方向標題的高度 | `40px` |
| \--nutui-tabs-vertical-tab-line-color | 垂直方向線條顏色 | `linear-gradient(180deg, $color-primary 0%, rgba(#FF0F23, 0.15) 100%)` |
diff --git a/src/styles/variables-daojia.scss b/src/styles/variables-daojia.scss
index 54aab27e91..13222d0031 100644
--- a/src/styles/variables-daojia.scss
+++ b/src/styles/variables-daojia.scss
@@ -1331,7 +1331,7 @@ $tabs-tab-button-active-font-size: var(--nutui-tabs-button-active-font-size,
$tabs-tab-button-active-font-weight: var(--nutui-tabs-button-active-font-weight,
$font-weight-bold) !default;
$tabs-tab-button-background-color: var(--nutui-tabs-button-background-color,
- $color-background-overlay) !default;
+ var(--nutui-color-background-component)) !default;
$tabs-tab-button-active-background-color: var(--nutui-tabs-button-active-background-color,
$color-primary-light-pressed) !default;
$tabs-tab-button-active-border: var(--nutui-tabs-button-active-border,
diff --git a/src/styles/variables-jmapp.scss b/src/styles/variables-jmapp.scss
index 0d9bf845f7..fd0eda8336 100644
--- a/src/styles/variables-jmapp.scss
+++ b/src/styles/variables-jmapp.scss
@@ -2201,7 +2201,7 @@ $tabs-tab-button-active-font-weight: var(
) !default;
$tabs-tab-button-background-color: var(
--nutui-tabs-button-background-color,
- $color-background-overlay
+ var(--nutui-color-background-component)
) !default;
$tabs-tab-button-active-background-color: var(
--nutui-tabs-button-active-background-color,
diff --git a/src/styles/variables-jrkf.scss b/src/styles/variables-jrkf.scss
index 5632ccb3af..be91289109 100644
--- a/src/styles/variables-jrkf.scss
+++ b/src/styles/variables-jrkf.scss
@@ -2308,7 +2308,7 @@ $tabs-tab-button-active-font-weight: var(
) !default;
$tabs-tab-button-background-color: var(
--nutui-tabs-button-background-color,
- $color-background-overlay
+ var(--nutui-color-background-component)
) !default;
$tabs-tab-button-active-background-color: var(
--nutui-tabs-button-active-background-color,
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index adcbf17b57..4b5ad0014e 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -2108,7 +2108,7 @@ $pagination-lite-active-background-color: var(
$tabs-titles-height: var(--nutui-tabs-titles-height, scale-px(36px)) !default;
$tabs-titles-background-color: var(
--nutui-tabs-titles-background-color,
- $color-background
+ $color-background-overlay
) !default;
$tabs-titles-gap: var(--nutui-tabs-titles-gap, scale-px(8px)) !default;
$tabs-titles-font-size: var(
@@ -2178,7 +2178,7 @@ $tabs-tab-button-active-font-weight: var(
) !default;
$tabs-tab-button-background-color: var(
--nutui-tabs-button-background-color,
- $color-background-overlay
+ var(--nutui-color-background-component)
) !default;
$tabs-tab-button-active-background-color: var(
--nutui-tabs-button-active-background-color,
From 990cbf28405d5fe82dd2e290a037425d54327766 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Fri, 9 Oct 2026 17:22:59 +0800
Subject: [PATCH 12/13] =?UTF-8?q?feat(tabs):=20=E6=9B=B4=E6=96=B0=E9=80=89?=
=?UTF-8?q?=E9=A1=B9=E5=8D=A1=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E8=82=A9=E9=83=A8=E5=9B=BE=E6=A0=87=E6=94=AF=E6=8C=81=E5=92=8C?=
=?UTF-8?q?=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/demos/h5/demo5.tsx | 2 +-
src/packages/tabs/demos/taro/demo5.tsx | 2 +-
src/packages/tabs/icon.taro.tsx | 41 +++++++++++++++++++++
src/packages/tabs/icon.tsx | 49 ++++++++++++++++++++++++++
src/packages/tabs/tabs.scss | 44 ++++++++---------------
src/packages/tabs/tabs.taro.tsx | 16 +++++++--
src/packages/tabs/tabs.tsx | 16 +++++++--
7 files changed, 135 insertions(+), 35 deletions(-)
create mode 100644 src/packages/tabs/icon.taro.tsx
create mode 100644 src/packages/tabs/icon.tsx
diff --git a/src/packages/tabs/demos/h5/demo5.tsx b/src/packages/tabs/demos/h5/demo5.tsx
index c2eee56a22..9dd70d98ea 100644
--- a/src/packages/tabs/demos/h5/demo5.tsx
+++ b/src/packages/tabs/demos/h5/demo5.tsx
@@ -11,7 +11,7 @@ const Demo5 = () => {
setTabvalue(value)
}}
activeType="button"
- style={{ fontFamily: 'PingFang SC' }}
+ style={{ '--nutui-tabs-titles-item-font-family': 'PingFang SC' }}
>
Tab longitem
Tab 2
diff --git a/src/packages/tabs/demos/taro/demo5.tsx b/src/packages/tabs/demos/taro/demo5.tsx
index 8b5dfed1a3..168c99dbf2 100644
--- a/src/packages/tabs/demos/taro/demo5.tsx
+++ b/src/packages/tabs/demos/taro/demo5.tsx
@@ -10,7 +10,7 @@ const Demo5 = () => {
setTabvalue(value)
}}
activeType="button"
- style={{ fontFamily: 'PingFang SC' }}
+ style={{ '--nutui-tabs-titles-item-font-family': 'PingFang SC' }}
>
Tab longitem
Tab 2
diff --git a/src/packages/tabs/icon.taro.tsx b/src/packages/tabs/icon.taro.tsx
new file mode 100644
index 0000000000..72a7a94c2c
--- /dev/null
+++ b/src/packages/tabs/icon.taro.tsx
@@ -0,0 +1,41 @@
+import React, { FC, CSSProperties } from 'react'
+import { View } from '@tarojs/components'
+
+const shoulderColor =
+ 'var(--nutui-tabs-titles-item-active-background-color, var(--nutui-color-background-overlay, #ffffff))'
+
+const shoulderLeft =
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDQwIiBmaWxsPSJub25lIj48ZyB0cmFuc2Zvcm09Im1hdHJpeCgtMSwwLDAsMSw0OCwwKSI+PHBhdGggZD0iTTAgMEwxLjUyMTM2OTMgMEM1LjI5ODc5NjcgMCA4LjU2MTQ2OTEgMi42NDIyMDYyIDkuMzQ2NjQwNiA2LjMzNzEzMDFMMTUuMTUzMzYgMzMuNjYyODY4QzE1LjkzODUzMSAzNy4zNTc3OTIgMTkuMjAxMjAyIDQwIDIyLjk3ODYzIDQwTDI0IDQwTDIyLjk4IDQwTDAgNDBMMCAwWiIgZmlsbD0iI2ZmZmZmZiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQsIDApIi8+PC9nPjwvc3ZnPg=='
+
+const shoulderRight =
+ 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDQwIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMCAwTDEuNTIxMzY5MyAwQzUuMjk4Nzk2NyAwIDguNTYxNDY5MSAyLjY0MjIwNjIgOS4zNDY2NDA2IDYuMzM3MTMwMUwxNS4xNTMzNiAzMy42NjI4NjhDMTUuOTM4NTMxIDM3LjM1Nzc5MiAxOS4yMDEyMDIgNDAgMjIuOTc4NjMgNDBMMjQgNDBMMjIuOTggNDBMMCA0MEwwIDBaIiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+'
+
+export interface CardShoulderProps {
+ className?: string
+}
+
+const CardShoulder: FC = ({
+ className,
+ url,
+}) => {
+ const mask = `url('${url}') 0 0/100% 100% no-repeat`
+ const maskStyle = (
+ process.env.TARO_ENV === 'h5'
+ ? { mask, WebkitMask: mask }
+ : { mask, '-webkitMask': mask }
+ ) as CSSProperties
+ return (
+
+ )
+}
+
+export const CardShoulderLeft: FC = ({ className }) => {
+ return
+}
+
+export const CardShoulderRight: FC = ({ className }) => {
+ return
+}
diff --git a/src/packages/tabs/icon.tsx b/src/packages/tabs/icon.tsx
new file mode 100644
index 0000000000..4265c6a41b
--- /dev/null
+++ b/src/packages/tabs/icon.tsx
@@ -0,0 +1,49 @@
+import React, { FC } from 'react'
+
+const shoulderColor =
+ 'var(--nutui-tabs-titles-item-active-background-color, var(--nutui-color-background-overlay, #ffffff))'
+
+export interface CardShoulderProps {
+ className?: string
+}
+
+export const CardShoulderLeft: FC = ({ className }) => {
+ return (
+
+ )
+}
+
+export const CardShoulderRight: FC = ({ className }) => {
+ return (
+
+ )
+}
diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss
index 97716fc176..af6363f41d 100644
--- a/src/packages/tabs/tabs.scss
+++ b/src/packages/tabs/tabs.scss
@@ -77,6 +77,7 @@
&-text {
color: $tabs-titles-item-color;
+ font-family: $tabs-titles-item-font-family;
}
&-smile,
@@ -183,31 +184,28 @@
font-weight: $font-weight-bold;
background-color: $tabs-titles-item-active-background-color;
- &::before,
- &::after {
+ .nut-tabs-titles-item-shoulder-left,
+ .nut-tabs-titles-item-shoulder-right {
position: absolute;
bottom: 0;
width: 24px;
height: 100%;
- content: '';
- background-repeat: no-repeat;
- background-size: 100% 100%;
+ display: block;
+ pointer-events: none;
}
- &::before {
+ .nut-tabs-titles-item-shoulder-left {
left: -24px;
- background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg transform='matrix(-1,0,0,1,48,0)'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%23ffffff' transform='translate(24, 0)'/%3E%3C/g%3E%3C/svg%3E");
}
- &::after {
+ .nut-tabs-titles-item-shoulder-right {
right: -24px;
- background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%23ffffff'/%3E%3C/svg%3E");
}
&:first-child {
margin-left: 0;
- &::before {
+ .nut-tabs-titles-item-shoulder-left {
display: none;
}
}
@@ -215,7 +213,7 @@
&:last-child {
margin-right: 0;
- &::after {
+ .nut-tabs-titles-item-shoulder-right {
display: none;
}
}
@@ -229,7 +227,7 @@
&:last-child {
margin-right: 24px;
- &::after {
+ .nut-tabs-titles-item-shoulder-right {
display: block;
}
}
@@ -240,7 +238,7 @@
&:first-child {
margin-left: 24px;
- &::before {
+ .nut-tabs-titles-item-shoulder-left {
display: block;
}
}
@@ -324,18 +322,6 @@
}
}
-.nut-theme-dark {
- .nut-tabs-titles-card .nut-tabs-titles-item-active {
- &::before {
- background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg transform='matrix(-1,0,0,1,48,0)'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%231f2226' transform='translate(24, 0)'/%3E%3C/g%3E%3C/svg%3E");
- }
-
- &::after {
- background-image: url("data:image/svg+xml,%3Csvg fill='none' height='40' version='1.1' viewBox='0 0 24 40' width='24' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cpath d='M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' fill='%231f2226'/%3E%3C/svg%3E");
- }
- }
-}
-
.nut-tabs-vertical {
.nut-tabs-ellipsis {
white-space: break-spaces;
@@ -513,11 +499,11 @@
margin-left: 24px;
margin-right: 0;
- &::before {
+ .nut-tabs-titles-item-shoulder-left {
display: block;
}
- &::after {
+ .nut-tabs-titles-item-shoulder-right {
display: none;
}
}
@@ -529,11 +515,11 @@
margin-left: 0;
margin-right: 24px;
- &::before {
+ .nut-tabs-titles-item-shoulder-left {
display: none;
}
- &::after {
+ .nut-tabs-titles-item-shoulder-right {
display: block;
}
}
diff --git a/src/packages/tabs/tabs.taro.tsx b/src/packages/tabs/tabs.taro.tsx
index ecd41f6cda..ff113d8562 100644
--- a/src/packages/tabs/tabs.taro.tsx
+++ b/src/packages/tabs/tabs.taro.tsx
@@ -16,6 +16,7 @@ import { useForceUpdate } from '@/hooks/use-force-update'
import raf from '@/utils/raf'
import { useUuid } from '@/hooks/use-uuid'
import { useRtl } from '../configprovider/configprovider.taro'
+import { CardShoulderLeft, CardShoulderRight } from './icon.taro'
import { TabsTitle, TaroTabsProps } from '@/types'
import { td } from '@/utils/taro/platform'
@@ -294,6 +295,8 @@ export const Tabs: FunctionComponent> & {
{!!title && typeof title === 'function'
? title()
: titles.current.map((item, index) => {
+ const isActive =
+ !item.disabled && String(item.value) === String(value)
return (
> & {
id={`scrollIntoView${index}`}
onClick={() => tabChange(item)}
className={classNames(`${classPrefix}-titles-item`, {
- [`nut-tabs-titles-item-active`]:
- !item.disabled && String(item.value) === String(value),
+ [`nut-tabs-titles-item-active`]: isActive,
[`nut-tabs-titles-item-disabled`]: item.disabled,
[`nut-tabs-titles-item-${align}`]: align,
})}
>
+ {activeType === 'card' && isActive && (
+ <>
+
+
+ >
+ )}
{activeType === 'line' && (
> & {
{!!title && typeof title === 'function'
? title()
: titles.current.map((item) => {
+ const isActive =
+ !item.disabled && String(item.value) === String(value)
return (
tabChange(item)}
className={classNames(`${classPrefix}-titles-item`, {
- [`nut-tabs-titles-item-active`]:
- !item.disabled && String(item.value) === String(value),
+ [`nut-tabs-titles-item-active`]: isActive,
[`nut-tabs-titles-item-disabled`]: item.disabled,
[`nut-tabs-titles-item-${align}`]: align,
})}
>
+ {activeType === 'card' && isActive && (
+ <>
+
+
+ >
+ )}
{activeType === 'line' && (
Date: Mon, 12 Oct 2026 16:16:21 +0800
Subject: [PATCH 13/13] =?UTF-8?q?refactor(tabs):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=8D=A1=E7=89=87=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=EF=BC=8C?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E8=82=A9=E9=83=A8=E5=9B=BE=E6=A0=87=E5=92=8C?=
=?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=AE=A1=E7=AE=97?=
=?UTF-8?q?=E5=87=BD=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/__test__/tabs.spec.tsx | 85 ++++++++++++++++++++-
src/packages/tabs/icon.taro.tsx | 21 +-----
src/packages/tabs/icon.tsx | 33 ++------
src/packages/tabs/tabs.scss | 95 +++++-------------------
src/packages/tabs/tabs.taro.tsx | 48 ++++++++----
src/packages/tabs/tabs.tsx | 28 +++++--
src/packages/tabs/utils.ts | 52 +++++++++++++
src/styles/variables-daojia.scss | 2 +-
src/styles/variables-jmapp.scss | 2 +-
src/styles/variables-jrkf.scss | 2 +-
src/styles/variables.scss | 2 +-
11 files changed, 223 insertions(+), 147 deletions(-)
create mode 100644 src/packages/tabs/utils.ts
diff --git a/src/packages/tabs/__test__/tabs.spec.tsx b/src/packages/tabs/__test__/tabs.spec.tsx
index 4a0d244b4b..787903c614 100644
--- a/src/packages/tabs/__test__/tabs.spec.tsx
+++ b/src/packages/tabs/__test__/tabs.spec.tsx
@@ -3,6 +3,7 @@ import { render, waitFor, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom'
import { Tabs } from '../tabs'
import { TabPane } from '../../tabpane/tabpane'
+import { getCardFlushSides } from '../utils'
test('base Tabs', () => {
const { getByTestId } = render(
@@ -26,7 +27,89 @@ test('base tabs props', () => {
expect(el2.length > 0).toBe(true)
expect(el3).toHaveClass('nut-tabs-titles-smile')
- expect(el3).toHaveClass('nut-tabs-titles-scrollable')
+ expect(el3).not.toHaveClass('nut-tabs-titles-scrollable')
+})
+
+test('card flush sides follow scrollable, align and rtl', () => {
+ // 可滑动(未设置 align):首项贴物理左端、末项贴物理右端
+ expect(getCardFlushSides(0, 3, { scrollable: true, rtl: false })).toEqual({
+ left: true,
+ right: false,
+ })
+ expect(getCardFlushSides(2, 3, { scrollable: true, rtl: false })).toEqual({
+ left: false,
+ right: true,
+ })
+ expect(getCardFlushSides(1, 3, { scrollable: true, rtl: false })).toEqual({
+ left: false,
+ right: false,
+ })
+ // RTL 镜像:首项贴物理右端、末项贴物理左端
+ expect(getCardFlushSides(0, 3, { scrollable: true, rtl: true })).toEqual({
+ left: false,
+ right: true,
+ })
+ expect(getCardFlushSides(2, 3, { scrollable: true, rtl: true })).toEqual({
+ left: true,
+ right: false,
+ })
+ // 不可滑动:内容靠向 align 一侧,只有贴靠该侧的那一项才贴边
+ expect(
+ getCardFlushSides(0, 3, { scrollable: false, rtl: false, align: 'left' })
+ ).toEqual({ left: true, right: false })
+ expect(
+ getCardFlushSides(2, 3, { scrollable: false, rtl: false, align: 'left' })
+ ).toEqual({ left: false, right: false })
+ expect(
+ getCardFlushSides(2, 3, { scrollable: false, rtl: false, align: 'right' })
+ ).toEqual({ left: false, right: true })
+ expect(
+ getCardFlushSides(0, 3, { scrollable: false, rtl: true, align: 'left' })
+ ).toEqual({ left: false, right: true })
+ expect(
+ getCardFlushSides(2, 3, { scrollable: false, rtl: true, align: 'right' })
+ ).toEqual({ left: true, right: false })
+ // 单项卡片两端同时贴边
+ expect(getCardFlushSides(0, 1, { scrollable: true, rtl: false })).toEqual({
+ left: true,
+ right: true,
+ })
+})
+
+test('card tabs apply flush modifier to active item', () => {
+ const { container } = render(
+
+
+ Tab 1
+
+
+ Tab 2
+
+
+ )
+ const titles = container.querySelectorAll('.nut-tabs-titles')[0]
+ expect(titles).not.toHaveClass('nut-tabs-titles-scrollable')
+ expect(titles).not.toHaveClass('nut-tabs-titles-not-scrollable')
+
+ const items = container.querySelectorAll('.nut-tabs-titles-item')
+ expect(items[0]).toHaveClass('nut-tabs-titles-item-active--flush-left')
+ expect(items[0]).not.toHaveClass('nut-tabs-titles-item-active--flush-right')
+})
+
+test('card tabs flush modifier honours align when not scrollable', () => {
+ const { container } = render(
+
+
+ Tab 1
+
+
+ Tab 2
+
+
+ )
+ const items = container.querySelectorAll('.nut-tabs-titles-item')
+ expect(items[1]).toHaveClass('nut-tabs-titles-item-active--flush-right')
+ expect(items[1]).not.toHaveClass('nut-tabs-titles-item-active--flush-left')
})
test('base tabs props', () => {
diff --git a/src/packages/tabs/icon.taro.tsx b/src/packages/tabs/icon.taro.tsx
index 72a7a94c2c..a3e6cd437b 100644
--- a/src/packages/tabs/icon.taro.tsx
+++ b/src/packages/tabs/icon.taro.tsx
@@ -4,21 +4,16 @@ import { View } from '@tarojs/components'
const shoulderColor =
'var(--nutui-tabs-titles-item-active-background-color, var(--nutui-color-background-overlay, #ffffff))'
-const shoulderLeft =
- 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDQwIiBmaWxsPSJub25lIj48ZyB0cmFuc2Zvcm09Im1hdHJpeCgtMSwwLDAsMSw0OCwwKSI+PHBhdGggZD0iTTAgMEwxLjUyMTM2OTMgMEM1LjI5ODc5NjcgMCA4LjU2MTQ2OTEgMi42NDIyMDYyIDkuMzQ2NjQwNiA2LjMzNzEzMDFMMTUuMTUzMzYgMzMuNjYyODY4QzE1LjkzODUzMSAzNy4zNTc3OTIgMTkuMjAxMjAyIDQwIDIyLjk3ODYzIDQwTDI0IDQwTDIyLjk4IDQwTDAgNDBMMCAwWiIgZmlsbD0iI2ZmZmZmZiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQsIDApIi8+PC9nPjwvc3ZnPg=='
-
-const shoulderRight =
+// 卡片肩部只保留这一份图形(右肩形状);左肩在样式层水平镜像复用,不再单独维护第二份资源
+const shoulder =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDQwIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMCAwTDEuNTIxMzY5MyAwQzUuMjk4Nzk2NyAwIDguNTYxNDY5MSAyLjY0MjIwNjIgOS4zNDY2NDA2IDYuMzM3MTMwMUwxNS4xNTMzNiAzMy42NjI4NjhDMTUuOTM4NTMxIDM3LjM1Nzc5MiAxOS4yMDEyMDIgNDAgMjIuOTc4NjMgNDBMMjQgNDBMMjIuOTggNDBMMCA0MEwwIDBaIiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+'
export interface CardShoulderProps {
className?: string
}
-const CardShoulder: FC
= ({
- className,
- url,
-}) => {
- const mask = `url('${url}') 0 0/100% 100% no-repeat`
+export const CardShoulder: FC = ({ className }) => {
+ const mask = `url('${shoulder}') 0 0/100% 100% no-repeat`
const maskStyle = (
process.env.TARO_ENV === 'h5'
? { mask, WebkitMask: mask }
@@ -31,11 +26,3 @@ const CardShoulder: FC = ({
/>
)
}
-
-export const CardShoulderLeft: FC = ({ className }) => {
- return
-}
-
-export const CardShoulderRight: FC = ({ className }) => {
- return
-}
diff --git a/src/packages/tabs/icon.tsx b/src/packages/tabs/icon.tsx
index 4265c6a41b..ed95eaf8e3 100644
--- a/src/packages/tabs/icon.tsx
+++ b/src/packages/tabs/icon.tsx
@@ -3,33 +3,15 @@ import React, { FC } from 'react'
const shoulderColor =
'var(--nutui-tabs-titles-item-active-background-color, var(--nutui-color-background-overlay, #ffffff))'
+// 卡片肩部只保留这一份图形(右肩形状);左肩在样式层水平镜像复用,不再单独维护第二份资源
+const SHOULDER_PATH =
+ 'M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z'
+
export interface CardShoulderProps {
className?: string
}
-export const CardShoulderLeft: FC = ({ className }) => {
- return (
-
- )
-}
-
-export const CardShoulderRight: FC = ({ className }) => {
+export const CardShoulder: FC = ({ className }) => {
return (
)
}
diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss
index af6363f41d..b3e5c588ad 100644
--- a/src/packages/tabs/tabs.scss
+++ b/src/packages/tabs/tabs.scss
@@ -107,8 +107,8 @@
}
&-smile-icon {
- width: scale-px(40px);
- height: scale-px(20px);
+ width: scale-px(12px);
+ height: scale-px(12px);
}
&-active {
@@ -137,8 +137,8 @@
/* #ifndef dynamic*/
overflow: unset;
/* #endif */
- width: scale-px(40px);
- height: scale-px(20px);
+ width: scale-px(12px);
+ height: scale-px(12px);
.nut-icon {
color: $tabs-titles-item-active-color;
@@ -171,7 +171,7 @@
&-card {
.nut-tabs-titles-item {
- padding: 0 8px;
+ padding: 0 scale-px(8px);
flex-direction: column;
justify-content: center;
line-height: initial;
@@ -180,7 +180,7 @@
&-active {
position: relative;
z-index: 1;
- margin: 0 24px;
+ margin: 0 scale-px(24px);
font-weight: $font-weight-bold;
background-color: $tabs-titles-item-active-background-color;
@@ -188,21 +188,25 @@
.nut-tabs-titles-item-shoulder-right {
position: absolute;
bottom: 0;
- width: 24px;
+ width: scale-px(24px);
height: 100%;
display: block;
pointer-events: none;
}
.nut-tabs-titles-item-shoulder-left {
- left: -24px;
+ left: scale-px(-24px);
+ // 左肩复用右肩图形,水平镜像即可,无需第二份资源
+ transform: scaleX(-1);
}
.nut-tabs-titles-item-shoulder-right {
- right: -24px;
+ right: scale-px(-24px);
}
- &:first-child {
+ // 贴边端由 TSX 按可滑动性、对齐方式与书写方向(RTL)统一计算,
+ // 样式层只认这两个修饰类:贴边侧取消留白并回退成直角(隐藏对应肩部)
+ &.nut-tabs-titles-item-active--flush-left {
margin-left: 0;
.nut-tabs-titles-item-shoulder-left {
@@ -210,7 +214,7 @@
}
}
- &:last-child {
+ &.nut-tabs-titles-item-active--flush-right {
margin-right: 0;
.nut-tabs-titles-item-shoulder-right {
@@ -221,30 +225,6 @@
}
}
- &-card.nut-tabs-titles-not-scrollable {
- &:not(.nut-tabs-titles-right) .nut-tabs-titles-item-active {
- // LTR 左对齐(及 RTL 左对齐类):末项一侧恢复肩部与间距
- &:last-child {
- margin-right: 24px;
-
- .nut-tabs-titles-item-shoulder-right {
- display: block;
- }
- }
- }
-
- &.nut-tabs-titles-right .nut-tabs-titles-item-active {
- // LTR 右对齐(及 RTL 右对齐类):首项一侧恢复肩部与间距
- &:first-child {
- margin-left: 24px;
-
- .nut-tabs-titles-item-shoulder-left {
- display: block;
- }
- }
- }
- }
-
&-button {
.nut-tabs-titles-item {
padding: 0 scale-px(4px);
@@ -374,8 +354,8 @@
}
.nut-tabs-titles-item-smile {
- right: scale-px(-12px);
- bottom: -2%;
+ right: scale-px(3px);
+ bottom: 12%;
left: auto;
transform: rotate(320deg);
}
@@ -413,7 +393,7 @@
.nut-tabs-titles-item-smile {
left: 50%;
right: auto;
- bottom: scale-px(-3px);
+ bottom: 4%;
transform: translate(-50%, 0) rotate(0deg);
}
}
@@ -489,43 +469,6 @@
}
}
-// RTL 卡片模式留白端镜像:对齐类(flex-start/flex-end)随书写方向翻转视觉端,
-// 贴边端/留白端与 DOM 首末项的对应关系同 LTR 相反,贴边侧直角、留白侧恢复肩部需按物理边重写
-[dir='rtl'] .nut-tabs-titles-card.nut-tabs-titles-not-scrollable,
-.nut-rtl .nut-tabs-titles-card.nut-tabs-titles-not-scrollable {
- // RTL 左对齐类:内容贴视觉右端,首项贴边(直角在物理右侧)、末项邻留白
- &:not(.nut-tabs-titles-right) .nut-tabs-titles-item-active {
- &:first-child {
- margin-left: 24px;
- margin-right: 0;
-
- .nut-tabs-titles-item-shoulder-left {
- display: block;
- }
-
- .nut-tabs-titles-item-shoulder-right {
- display: none;
- }
- }
- }
-
- // RTL 右对齐类:内容贴视觉左端,末项贴边(直角在物理左侧)、首项邻留白
- &.nut-tabs-titles-right .nut-tabs-titles-item-active {
- &:last-child {
- margin-left: 0;
- margin-right: 24px;
-
- .nut-tabs-titles-item-shoulder-left {
- display: none;
- }
-
- .nut-tabs-titles-item-shoulder-right {
- display: block;
- }
- }
- }
-}
-
[dir='rtl'] .nut-tabs-vertical,
.nut-rtl .nut-tabs-vertical {
.nut-tabs-titles {
@@ -545,7 +488,7 @@
}
.nut-tabs-titles-item-smile {
- left: scale-px(-12px);
+ left: scale-px(3px);
right: auto;
transform: rotate(-320deg);
}
diff --git a/src/packages/tabs/tabs.taro.tsx b/src/packages/tabs/tabs.taro.tsx
index ff113d8562..0573c73ac4 100644
--- a/src/packages/tabs/tabs.taro.tsx
+++ b/src/packages/tabs/tabs.taro.tsx
@@ -16,9 +16,11 @@ import { useForceUpdate } from '@/hooks/use-force-update'
import raf from '@/utils/raf'
import { useUuid } from '@/hooks/use-uuid'
import { useRtl } from '../configprovider/configprovider.taro'
-import { CardShoulderLeft, CardShoulderRight } from './icon.taro'
+import { CardShoulder } from './icon.taro'
+import { getCardFlushSides } from './utils'
import { TabsTitle, TaroTabsProps } from '@/types'
import { td } from '@/utils/taro/platform'
+import { calcByProfile } from '@/utils'
const defaultProps = {
...ComponentDefaults,
@@ -31,6 +33,14 @@ const defaultProps = {
} as TaroTabsProps
const classPrefix = 'nut-tabs'
+
+// 卡片模式下激活卡片两侧共预留 margin: 0 scale-px(24px),而 boundingClientRect 不含 margin,
+// 估算可滑动性与滚动钳制时都需按最大 48px(2 * 24px)回补,统一在此维护;
+// 设计基准值经 calcByProfile 换算,与 SCSS 的 scale-px 共用同一缩放系数 --nut-scale-f
+const CARD_ACTIVE_MARGIN_TOTAL = 48
+
+const getCardActiveMarginTotal = () => calcByProfile(CARD_ACTIVE_MARGIN_TOTAL)
+
export const Tabs: FunctionComponent> & {
TabPane: typeof TabPane
} = (props) => {
@@ -63,8 +73,6 @@ export const Tabs: FunctionComponent> & {
onChange,
})
- const titleItemsRef = useRef([])
-
// 卡片模式下,标题栏可横向滑动时激活卡片贴合两端边缘;不可滑动时(全部 tab 放得下)
// 贴边端直角、留白端恢复双肩
const [isScrollable, setIsScrollable] = useState(!align)
@@ -110,8 +118,6 @@ export const Tabs: FunctionComponent> & {
)
const classesTitle = classNames(`${classPrefix}-titles`, {
[`${classPrefix}-titles-${activeType}`]: activeType,
- [`${classPrefix}-titles-not-scrollable`]:
- activeType === 'card' && !isScrollable,
[`${classPrefix}-titles-${align}`]: align,
})
@@ -163,9 +169,11 @@ export const Tabs: FunctionComponent> & {
(sum: number, curr: RectItem) => sum + curr.width,
0
)
- // 卡片模式 active 卡片 margin 不在 boundingClientRect 内,按最大 48px 预留,
+ // 卡片模式 active 卡片 margin 不在 boundingClientRect 内,按 CARD_ACTIVE_MARGIN_TOTAL 预留,
// 与 scrollIntoView 的钳制口径一致,避免临界内容误判为放得下
- setIsScrollable(totalWidth + 48 - navRect.width > 1)
+ setIsScrollable(
+ totalWidth + getCardActiveMarginTotal() - navRect.width > 1
+ )
})
})
}, [activeType, direction, name, uuid, getRect, getAllRect, align])
@@ -217,8 +225,9 @@ export const Tabs: FunctionComponent> & {
.reduce((prev: number, curr: RectItem) => prev + curr.width, 0)
// 卡片模式 active 卡片带 margin: 0 24px(贴边侧为 0),boundingClientRect 不含 margin,
// 矩形宽求和会少算,钳制点偏短导致滚动到末尾时末项卡片显示不全;
- // 按最大 48px 预留,超设部分由 ScrollView 自动截断到真实最大滚动距离
- const marginExtra = activeType === 'card' ? 48 : 0
+ // 按 CARD_ACTIVE_MARGIN_TOTAL 预留,超设部分由 ScrollView 自动截断到真实最大滚动距离
+ const marginExtra =
+ activeType === 'card' ? getCardActiveMarginTotal() : 0
to = Math.min(
Math.max(0, left - (navRect.width - titleRect.width) / 2),
totalWidth + marginExtra - navRect.width
@@ -297,26 +306,37 @@ export const Tabs: FunctionComponent> & {
: titles.current.map((item, index) => {
const isActive =
!item.disabled && String(item.value) === String(value)
+ const flush =
+ isActive &&
+ activeType === 'card' &&
+ direction === 'horizontal'
+ ? getCardFlushSides(index, titles.current.length, {
+ scrollable: isScrollable,
+ rtl,
+ align,
+ })
+ : { left: false, right: false }
return (
- titleItemsRef.current.push(ref)
- }
id={`scrollIntoView${index}`}
onClick={() => tabChange(item)}
className={classNames(`${classPrefix}-titles-item`, {
[`nut-tabs-titles-item-active`]: isActive,
[`nut-tabs-titles-item-disabled`]: item.disabled,
[`nut-tabs-titles-item-${align}`]: align,
+ [`${classPrefix}-titles-item-active--flush-left`]:
+ flush.left,
+ [`${classPrefix}-titles-item-active--flush-right`]:
+ flush.right,
})}
>
{activeType === 'card' && isActive && (
<>
-
-
>
diff --git a/src/packages/tabs/tabs.tsx b/src/packages/tabs/tabs.tsx
index 01a6ee20f1..42d8e4e5ee 100644
--- a/src/packages/tabs/tabs.tsx
+++ b/src/packages/tabs/tabs.tsx
@@ -14,7 +14,8 @@ import raf from '@/utils/raf'
import { usePropsValue } from '@/hooks/use-props-value'
import { useForceUpdate } from '@/hooks/use-force-update'
import { useRtl } from '../configprovider'
-import { CardShoulderLeft, CardShoulderRight } from './icon'
+import { CardShoulder } from './icon'
+import { getCardFlushSides } from './utils'
import { TabsTitle, WebTabsProps } from '@/types'
const defaultProps = {
@@ -79,10 +80,12 @@ export const Tabs: FunctionComponent> & {
raf(measureScrollable)
}, [children, measureScrollable])
useEffect(() => {
+ // 仅卡片模式需要按可用宽度重测;其余模式提前返回,避免挂无用的 resize 监听
+ if (activeType !== 'card' || direction !== 'horizontal' || !align) return
const onResize = () => raf(measureScrollable)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
- }, [measureScrollable])
+ }, [activeType, direction, align, measureScrollable])
const scrollDirection = (
nav: HTMLDivElement,
@@ -163,9 +166,6 @@ export const Tabs: FunctionComponent> & {
)
const classesTitle = classNames(`${classPrefix}-titles`, {
[`${classPrefix}-titles-${activeType}`]: activeType,
- [`${classPrefix}-titles-scrollable`]: true,
- [`${classPrefix}-titles-not-scrollable`]:
- activeType === 'card' && !isScrollable,
[`${classPrefix}-titles-${align}`]: align,
})
@@ -199,9 +199,17 @@ export const Tabs: FunctionComponent> & {
{!!title && typeof title === 'function'
? title()
- : titles.current.map((item) => {
+ : titles.current.map((item, index) => {
const isActive =
!item.disabled && String(item.value) === String(value)
+ const flush =
+ isActive && activeType === 'card' && direction === 'horizontal'
+ ? getCardFlushSides(index, titles.current.length, {
+ scrollable: isScrollable,
+ rtl,
+ align,
+ })
+ : { left: false, right: false }
return (
> & {
[`nut-tabs-titles-item-active`]: isActive,
[`nut-tabs-titles-item-disabled`]: item.disabled,
[`nut-tabs-titles-item-${align}`]: align,
+ [`${classPrefix}-titles-item-active--flush-left`]:
+ flush.left,
+ [`${classPrefix}-titles-item-active--flush-right`]:
+ flush.right,
})}
>
{activeType === 'card' && isActive && (
<>
-
-
>
diff --git a/src/packages/tabs/utils.ts b/src/packages/tabs/utils.ts
new file mode 100644
index 0000000000..a8e7555456
--- /dev/null
+++ b/src/packages/tabs/utils.ts
@@ -0,0 +1,52 @@
+import { PositionX } from '@/types'
+
+export interface CardFlushSides {
+ left: boolean
+ right: boolean
+}
+
+const NO_FLUSH: CardFlushSides = { left: false, right: false }
+
+/**
+ * 计算卡片模式下激活卡片应贴靠的物理边。
+ *
+ * 贴边侧需取消该侧外边距并隐藏对应肩部装饰(另一侧保留肩部与留白)。
+ * 样式层只按 `nut-tabs-titles-item-active--flush-left/right` 两个修饰类渲染,
+ * 可滑动性与 RTL 镜像关系全部在此处收敛,避免 SCSS 里四套选择器各自维护。
+ *
+ * @param index 当前项在标题列表中的下标
+ * @param count 标题总数
+ * @param options scrollable 是否可横向滑动,rtl 是否书写方向为右到左,align 对齐方式
+ */
+export const getCardFlushSides = (
+ index: number,
+ count: number,
+ options: { scrollable: boolean; rtl: boolean; align?: PositionX }
+): CardFlushSides => {
+ if (count <= 0 || index < 0 || index >= count) return NO_FLUSH
+
+ // 起始边/结束边:书写方向决定其在物理方向上的映射(RTL 呈镜像)
+ const startSide: PositionX = options.rtl ? 'right' : 'left'
+ const endSide: PositionX = options.rtl ? 'left' : 'right'
+ const isFirst = index === 0
+ const isLast = index === count - 1
+
+ // 内容可滑动时,DOM 首/末项分别贴住起始/结束边
+ if (options.scrollable) {
+ return {
+ left: (isFirst && startSide === 'left') || (isLast && endSide === 'left'),
+ right:
+ (isFirst && startSide === 'right') || (isLast && endSide === 'right'),
+ }
+ }
+
+ // 不可滑动时(仅在设置了 align 时出现),内容整体靠向 align 指定的一侧,
+ // 只有贴靠该侧的那一项才是贴边项
+ const packedSide = options.align === 'right' ? endSide : startSide
+ const packedItem = packedSide === startSide ? isFirst : isLast
+
+ return {
+ left: packedItem && packedSide === 'left',
+ right: packedItem && packedSide === 'right',
+ }
+}
diff --git a/src/styles/variables-daojia.scss b/src/styles/variables-daojia.scss
index 80baefbdd7..7b339d695b 100644
--- a/src/styles/variables-daojia.scss
+++ b/src/styles/variables-daojia.scss
@@ -1436,7 +1436,7 @@ $tabs-vertical-tab-line-width: var(--nutui-tabs-vertical-tab-line-width,
$tabs-vertical-tab-line-height: var(--nutui-tabs-vertical-tab-line-height,
12px) !default;
$tabs-titles-item-smile-bottom: var(--nutui-tabs-titles-item-smile-bottom,
- -25%) !default;
+ 4%) !default;
$tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, 24px 20px) !default;
$tabs-tabpane-background-color: var(--nutui-tabs-tabpane-background-color,
#fff) !default;
diff --git a/src/styles/variables-jmapp.scss b/src/styles/variables-jmapp.scss
index 771c632e5c..a1f79f54fc 100644
--- a/src/styles/variables-jmapp.scss
+++ b/src/styles/variables-jmapp.scss
@@ -2344,7 +2344,7 @@ $tabs-vertical-tab-line-height: var(
) !default;
$tabs-titles-item-smile-bottom: var(
--nutui-tabs-titles-item-smile-bottom,
- -25%
+ 4%
) !default;
$tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, 24px 20px) !default;
$tabs-tabpane-background-color: var(
diff --git a/src/styles/variables-jrkf.scss b/src/styles/variables-jrkf.scss
index 58ca989978..8a70775be0 100644
--- a/src/styles/variables-jrkf.scss
+++ b/src/styles/variables-jrkf.scss
@@ -2447,7 +2447,7 @@ $tabs-vertical-tab-line-height: var(
) !default;
$tabs-titles-item-smile-bottom: var(
--nutui-tabs-titles-item-smile-bottom,
- -25%
+ 4%
) !default;
$tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, 24px 20px) !default;
$tabs-tabpane-background-color: var(
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index 3aaa006ad9..25c09c2134 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -2358,7 +2358,7 @@ $tabs-vertical-tab-line-height: var(
) !default;
$tabs-titles-item-smile-bottom: var(
--nutui-tabs-titles-item-smile-bottom,
- -25%
+ 4%
) !default;
$tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, scale-px(24px) scale-px(20px)) !default;
$tabs-tabpane-background-color: var(