{!!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(
From 8f8a9fbc9ab6dd3a588fc9a0af3d5d77f793d355 Mon Sep 17 00:00:00 2001
From: Kasim <249813821@qq.com>
Date: Tue, 13 Oct 2026 11:24:17 +0800
Subject: [PATCH 14/14] =?UTF-8?q?refactor(tabs):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E5=8F=AF=E6=BB=91=E5=8A=A8=E6=80=A7=E5=88=A4=E6=96=AD=EF=BC=8C?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3=E5=92=8C=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/packages/tabs/__test__/tabs.spec.tsx | 20 ++++++++++++---
src/packages/tabs/doc.en-US.md | 3 +++
src/packages/tabs/doc.md | 3 +++
src/packages/tabs/doc.taro.md | 3 +++
src/packages/tabs/doc.zh-TW.md | 3 +++
src/packages/tabs/tabs.scss | 32 ++++++++++++------------
src/packages/tabs/tabs.taro.tsx | 26 ++++++++++---------
src/packages/tabs/tabs.tsx | 25 ++++++++++++++++--
src/packages/tabs/utils.ts | 24 ++++++++++++++++++
src/styles/variables.scss | 2 +-
10 files changed, 106 insertions(+), 35 deletions(-)
diff --git a/src/packages/tabs/__test__/tabs.spec.tsx b/src/packages/tabs/__test__/tabs.spec.tsx
index 787903c614..5f6d46056f 100644
--- a/src/packages/tabs/__test__/tabs.spec.tsx
+++ b/src/packages/tabs/__test__/tabs.spec.tsx
@@ -3,7 +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'
+import { getCardFlushSides, isContentScrollable } from '../utils'
test('base Tabs', () => {
const { getByTestId } = render(
@@ -27,7 +27,19 @@ test('base tabs props', () => {
expect(el2.length > 0).toBe(true)
expect(el3).toHaveClass('nut-tabs-titles-smile')
- expect(el3).not.toHaveClass('nut-tabs-titles-scrollable')
+ // 兼容旧版的恒定可滑动类名(已废弃)仍保留,确保业务侧既有样式覆盖不失效
+ expect(el3).toHaveClass('nut-tabs-titles-scrollable')
+})
+
+test('isContentScrollable treats boundary width correctly', () => {
+ // 内容与容器等宽:不可滑动
+ expect(isContentScrollable(200, 200)).toBe(false)
+ // 仅超出 1px:落在容差内,不判定为可滑动
+ expect(isContentScrollable(201, 200)).toBe(false)
+ // 超出超过 1px:判定为可滑动
+ expect(isContentScrollable(202, 200)).toBe(true)
+ // 内容小于容器:不可滑动
+ expect(isContentScrollable(180, 200)).toBe(false)
})
test('card flush sides follow scrollable, align and rtl', () => {
@@ -88,8 +100,8 @@ test('card tabs apply flush modifier to active item', () => {
)
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')
+ // 兼容旧版的恒定可滑动类名(已废弃)仍保留
+ expect(titles).toHaveClass('nut-tabs-titles-scrollable')
const items = container.querySelectorAll('.nut-tabs-titles-item')
expect(items[0]).toHaveClass('nut-tabs-titles-item-active--flush-left')
diff --git a/src/packages/tabs/doc.en-US.md b/src/packages/tabs/doc.en-US.md
index d07f50526e..e551cf2b65 100644
--- a/src/packages/tabs/doc.en-US.md
+++ b/src/packages/tabs/doc.en-US.md
@@ -277,6 +277,7 @@ The component provides the following CSS variables, which can be used to customi
| \--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 | `var(--nutui-color-background-component)` |
+| \--nutui-tabs-button-active-border | Inner border of the selected button-type title | `inset 0 0 0 1px $color-primary` |
| \--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%)` |
@@ -285,4 +286,6 @@ The component provides the following CSS variables, which can be used to customi
| \--nutui-tabs-tabpane-padding | Padding of the Tabpane content | `24px 20px` |
| \--nutui-tabs-tabpane-background-color | BackgroundColor of the Tabpane content | `#fff` |
+> Note: since v16, `--nutui-tabs-button-active-border` is applied as a `box-shadow` (used for the inner border), so its value must be a valid box-shadow (e.g. `inset 0 0 0 1px #ff0f23`). The legacy `border` shorthand (e.g. `1px solid #ff0f23`) no longer takes effect.
+
diff --git a/src/packages/tabs/doc.md b/src/packages/tabs/doc.md
index 961d6d41bb..571f1f89ff 100644
--- a/src/packages/tabs/doc.md
+++ b/src/packages/tabs/doc.md
@@ -277,6 +277,7 @@ import { Tabs } from '@nutui/nutui-react'
| \--nutui-tabs-button-active-font-size | 按钮模式选中标题的字号 | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | 按钮模式选中标题的字重 | `$font-weight-bold` |
| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `var(--nutui-color-background-component)` |
+| \--nutui-tabs-button-active-border | 按钮模式选中标题的内描边 | `inset 0 0 0 1px $color-primary` |
| \--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%)` |
@@ -285,4 +286,6 @@ import { Tabs } from '@nutui/nutui-react'
| \--nutui-tabs-tabpane-padding | Tabpane 的内边距 | `24px 20px` |
| \--nutui-tabs-tabpane-background-color | Tabpane 的背景色 | `#fff` |
+> 备注:`--nutui-tabs-button-active-border` 自 v16 起通过 `box-shadow` 生效(用于内描边),取值需为合法的 box-shadow 值(如 `inset 0 0 0 1px #ff0f23`);旧版的 `border` 简写(如 `1px solid #ff0f23`)不再生效。
+
diff --git a/src/packages/tabs/doc.taro.md b/src/packages/tabs/doc.taro.md
index a81997546c..a249343a6a 100644
--- a/src/packages/tabs/doc.taro.md
+++ b/src/packages/tabs/doc.taro.md
@@ -275,6 +275,7 @@ import { Tabs } from '@nutui/nutui-react-taro'
| \--nutui-tabs-button-active-font-size | 按钮模式选中标题的字号 | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | 按钮模式选中标题的字重 | `$font-weight-bold` |
| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `var(--nutui-color-background-component)` |
+| \--nutui-tabs-button-active-border | 按钮模式选中标题的内描边 | `inset 0 0 0 1px $color-primary` |
| \--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%)` |
@@ -283,4 +284,6 @@ import { Tabs } from '@nutui/nutui-react-taro'
| \--nutui-tabs-tabpane-padding | Tabpane 的内边距 | `24px 20px` |
| \--nutui-tabs-tabpane-background-color | Tabpane 的背景色 | `#fff` |
+> 备注:`--nutui-tabs-button-active-border` 自 v16 起通过 `box-shadow` 生效(用于内描边),取值需为合法的 box-shadow 值(如 `inset 0 0 0 1px #ff0f23`);旧版的 `border` 简写(如 `1px solid #ff0f23`)不再生效。
+
diff --git a/src/packages/tabs/doc.zh-TW.md b/src/packages/tabs/doc.zh-TW.md
index 88753dcf4b..a1155b0b82 100644
--- a/src/packages/tabs/doc.zh-TW.md
+++ b/src/packages/tabs/doc.zh-TW.md
@@ -275,6 +275,7 @@ import { Tabs } from '@nutui/nutui-react'
| \--nutui-tabs-button-active-font-size | 按鈕模式選中標題的字號 | `$font-size-base` |
| \--nutui-tabs-button-active-font-weight | 按鈕模式選中標題的字重 | `$font-weight-bold` |
| \--nutui-tabs-button-background-color | 按鈕模式未選中標題的背景色 | `var(--nutui-color-background-component)` |
+| \--nutui-tabs-button-active-border | 按鈕模式選中標題的內描邊 | `inset 0 0 0 1px $color-primary` |
| \--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%)` |
@@ -283,4 +284,6 @@ import { Tabs } from '@nutui/nutui-react'
| \--nutui-tabs-tabpane-padding | Tabpane 的內邊距 | `24px 20px` |
| \--nutui-tabs-tabpane-background-color | Tabpane 的背景色 | `#fff` |
+> 備註:`--nutui-tabs-button-active-border` 自 v16 起透過 `box-shadow` 生效(用於內描邊),取值需為合法的 box-shadow 值(如 `inset 0 0 0 1px #ff0f23`);舊版的 `border` 簡寫(如 `1px solid #ff0f23`)不再生效。
+
diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss
index de48e54e36..ff7fe8170f 100644
--- a/src/packages/tabs/tabs.scss
+++ b/src/packages/tabs/tabs.scss
@@ -34,14 +34,14 @@
&-left {
justify-content: flex-start;
.nut-tabs-titles-item {
- padding: 0 scale-px(8px);
+ padding: 0 8px;
}
}
&-right {
justify-content: flex-end;
.nut-tabs-titles-item {
- padding: 0 scale-px(8px);
+ padding: 0 8px;
}
.nut-tabs-list {
@@ -107,8 +107,8 @@
}
&-smile-icon {
- width: scale-px(12px);
- height: scale-px(12px);
+ width: 12px;
+ height: 12px;
}
&-active {
@@ -137,8 +137,8 @@
/* #ifndef dynamic*/
overflow: unset;
/* #endif */
- width: scale-px(12px);
- height: scale-px(12px);
+ width: 12px;
+ height: 12px;
.nut-icon {
color: $tabs-titles-item-active-color;
@@ -171,7 +171,7 @@
&-card {
.nut-tabs-titles-item {
- padding: 0 scale-px(8px);
+ padding: 0 8px;
flex-direction: column;
justify-content: center;
line-height: initial;
@@ -180,7 +180,7 @@
&-active {
position: relative;
z-index: 1;
- margin: 0 scale-px(24px);
+ margin: 0 24px;
font-weight: $font-weight-bold;
background-color: $tabs-titles-item-active-background-color;
@@ -188,20 +188,20 @@
.nut-tabs-titles-item-shoulder-right {
position: absolute;
bottom: 0;
- width: scale-px(24px);
+ width: 24px;
height: 100%;
display: block;
pointer-events: none;
}
.nut-tabs-titles-item-shoulder-left {
- left: scale-px(-24px);
+ left: -24px;
// 左肩复用右肩图形,水平镜像即可,无需第二份资源
transform: scaleX(-1);
}
.nut-tabs-titles-item-shoulder-right {
- right: scale-px(-24px);
+ right: -24px;
}
// 贴边端由 TSX 按可滑动性、对齐方式与书写方向(RTL)统一计算,
@@ -227,14 +227,14 @@
&-button {
.nut-tabs-titles-item {
- padding: 0 scale-px(4px);
+ padding: 0 4px;
.nut-tabs-titles-item-text {
flex: 1;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
- padding: 0 scale-px(8px);
+ padding: 0 8px;
background: $tabs-tab-button-background-color;
border-radius: $tabs-tab-button-border-radius;
font-size: $tabs-tab-button-font-size;
@@ -347,14 +347,14 @@
&-active {
background-color: $tabs-titles-item-active-background-color;
.nut-tabs-titles-item-line {
- left: scale-px(8px);
+ left: 8px;
width: $tabs-vertical-tab-line-width;
height: $tabs-vertical-tab-line-height;
background: $tabs-vertical-tab-line-color;
}
.nut-tabs-titles-item-smile {
- right: scale-px(3px);
+ right: 3px;
bottom: 12%;
left: auto;
transform: rotate(320deg);
@@ -488,7 +488,7 @@
}
.nut-tabs-titles-item-smile {
- left: scale-px(3px);
+ left: 3px;
right: auto;
transform: rotate(-320deg);
}
diff --git a/src/packages/tabs/tabs.taro.tsx b/src/packages/tabs/tabs.taro.tsx
index 0573c73ac4..0d098dfdea 100644
--- a/src/packages/tabs/tabs.taro.tsx
+++ b/src/packages/tabs/tabs.taro.tsx
@@ -17,10 +17,13 @@ import raf from '@/utils/raf'
import { useUuid } from '@/hooks/use-uuid'
import { useRtl } from '../configprovider/configprovider.taro'
import { CardShoulder } from './icon.taro'
-import { getCardFlushSides } from './utils'
+import {
+ getCardActiveMarginTotal,
+ getCardFlushSides,
+ isContentScrollable,
+} from './utils'
import { TabsTitle, TaroTabsProps } from '@/types'
import { td } from '@/utils/taro/platform'
-import { calcByProfile } from '@/utils'
const defaultProps = {
...ComponentDefaults,
@@ -34,13 +37,6 @@ const defaultProps = {
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) => {
@@ -118,6 +114,8 @@ export const Tabs: FunctionComponent> & {
)
const classesTitle = classNames(`${classPrefix}-titles`, {
[`${classPrefix}-titles-${activeType}`]: activeType,
+ // @deprecated 兼容旧版恒定的可滑动类名,保留以确保业务侧既有样式覆盖不失效,后续版本移除
+ [`${classPrefix}-titles-scrollable`]: true,
[`${classPrefix}-titles-${align}`]: align,
})
@@ -169,10 +167,14 @@ export const Tabs: FunctionComponent> & {
(sum: number, curr: RectItem) => sum + curr.width,
0
)
- // 卡片模式 active 卡片 margin 不在 boundingClientRect 内,按 CARD_ACTIVE_MARGIN_TOTAL 预留,
- // 与 scrollIntoView 的钳制口径一致,避免临界内容误判为放得下
+ // 布局未就绪(宽度为 0)时不判定,保留初始状态
+ if (totalWidth <= 0) return
+ // 与 H5 端保持同一口径:各项宽度之和 + 激活卡片预留外边距 vs 容器可视宽度
setIsScrollable(
- totalWidth + getCardActiveMarginTotal() - navRect.width > 1
+ isContentScrollable(
+ totalWidth + getCardActiveMarginTotal(),
+ navRect.width
+ )
)
})
})
diff --git a/src/packages/tabs/tabs.tsx b/src/packages/tabs/tabs.tsx
index 42d8e4e5ee..495ad88242 100644
--- a/src/packages/tabs/tabs.tsx
+++ b/src/packages/tabs/tabs.tsx
@@ -15,7 +15,11 @@ import { usePropsValue } from '@/hooks/use-props-value'
import { useForceUpdate } from '@/hooks/use-force-update'
import { useRtl } from '../configprovider'
import { CardShoulder } from './icon'
-import { getCardFlushSides } from './utils'
+import {
+ getCardActiveMarginTotal,
+ getCardFlushSides,
+ isContentScrollable,
+} from './utils'
import { TabsTitle, WebTabsProps } from '@/types'
const defaultProps = {
@@ -67,7 +71,22 @@ export const Tabs: FunctionComponent> & {
if (activeType !== 'card' || direction !== 'horizontal' || !align) return
const nav = navRef.current
if (!nav) return
- setIsScrollable(nav.scrollWidth - nav.clientWidth > 1)
+ const items = nav.querySelectorAll(
+ `.${classPrefix}-titles-item`
+ )
+ // 与 Taro 端保持同一口径:各项宽度之和 + 激活卡片预留外边距 vs 容器可视宽度
+ const totalWidth = Array.from(items).reduce(
+ (sum, item) => sum + item.getBoundingClientRect().width,
+ 0
+ )
+ // 布局未就绪(宽度为 0)时不判定,保留初始状态
+ if (totalWidth <= 0) return
+ setIsScrollable(
+ isContentScrollable(
+ totalWidth + getCardActiveMarginTotal(),
+ nav.clientWidth
+ )
+ )
}, [activeType, direction, align])
// 挂载后立即测量,避免首帧样式闪变
@@ -166,6 +185,8 @@ export const Tabs: FunctionComponent> & {
)
const classesTitle = classNames(`${classPrefix}-titles`, {
[`${classPrefix}-titles-${activeType}`]: activeType,
+ // @deprecated 兼容旧版恒定的可滑动类名,保留以确保业务侧既有样式覆盖不失效,后续版本移除
+ [`${classPrefix}-titles-scrollable`]: true,
[`${classPrefix}-titles-${align}`]: align,
})
diff --git a/src/packages/tabs/utils.ts b/src/packages/tabs/utils.ts
index a8e7555456..73f46e32e4 100644
--- a/src/packages/tabs/utils.ts
+++ b/src/packages/tabs/utils.ts
@@ -1,4 +1,28 @@
import { PositionX } from '@/types'
+import { calcByProfile } from '@/utils/scale-f'
+
+/**
+ * 卡片模式下激活卡片左右外边距之和(设计稿基准值:24px * 2),
+ * 对应 tabs.scss 中激活项的 `margin: 0 24px`。
+ * boundingClientRect/offsetWidth 不含外边距,估算可滑动性时需按此回补。
+ */
+export const CARD_ACTIVE_MARGIN_TOTAL = 48
+
+/** 按当前缩放换算激活卡片预留的总外边距,与 SCSS 的 scale-px 共用同一缩放系数 */
+export const getCardActiveMarginTotal = () =>
+ calcByProfile(CARD_ACTIVE_MARGIN_TOTAL)
+
+/**
+ * 内容是否超出容器而需要横向滑动。
+ * H5 与 Taro 共用同一判定口径,避免临界宽度下两端得出不同结论、出现一端贴边一端留肩。
+ *
+ * @param contentWidth 内容总宽度(各项宽度之和 + 激活卡片预留外边距)
+ * @param containerWidth 标题容器可视宽度
+ */
+export const isContentScrollable = (
+ contentWidth: number,
+ containerWidth: number
+) => contentWidth - containerWidth > 1
export interface CardFlushSides {
left: boolean
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index 3ec3b79f7b..8019cd1aa0 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -2373,7 +2373,7 @@ $tabs-tab-button-active-background-color: var(
) !default;
$tabs-tab-button-active-border: var(
--nutui-tabs-button-active-border,
- inset 0 0 0 scale-px(1px) $color-primary
+ inset 0 0 0 1px $color-primary
) !default;
$tabs-vertical-titles-width: var(
--nutui-tabs-vertical-titles-width,