组件/Tabs 标签页
通用组件

Tabs 标签页

用于在同一页面内切换不同内容区域(偏 Windows 风格的左侧导航 Tabs)。

该页面由 AI 迁移生成,请谨慎使用

内容已迁移完成,但仍建议结合源码和人工评审结果使用。

Tabs 标签页

用于在同一页面内切换不同内容区域(偏 Windows 风格的左侧导航 Tabs)。

基础用法

Tabs

示例即将加载...
<script setup lang="ts">
import { ref } from 'vue'

const active = ref('General')
</script>

<template>
  <div style="height: 320px;">
    <TxTabs v-model="active">
      <TxTabItem name="General" icon-class="i-carbon-settings" activation>
        <div style="padding: 8px;">
          <h3 style="margin: 0 0 8px;">
            General
          </h3>
          <p style="margin: 0; color: var(--tx-text-color-secondary);">
            Basic settings content
          </p>
        </div>
      </TxTabItem>
      <TxTabItem name="Account" icon-class="i-carbon-user">
        <div style="padding: 8px;">
          <h3 style="margin: 0 0 8px;">
            Account
          </h3>
          <p style="margin: 0; color: var(--tx-text-color-secondary);">
            Account settings content
          </p>
        </div>
      </TxTabItem>
      <TxTabItem name="About" icon-class="i-carbon-information">
        <div style="padding: 8px;">
          <h3 style="margin: 0 0 8px;">
            About
          </h3>
          <p style="margin: 0; color: var(--tx-text-color-secondary);">
            About content
          </p>
        </div>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

Indicator Showcase

Indicator variants & motions

示例即将加载...
<script setup lang="ts">
import { computed, ref } from 'vue'

type Motion = 'stretch' | 'warp' | 'glide' | 'snap' | 'spring'

type TabValue = 'A' | 'B' | 'C'

const motion = ref<Motion>('stretch')
const active = ref<TabValue>('A')

const variants = computed(() => {
  return [
    { value: 'line', label: 'line' },
    { value: 'pill', label: 'pill' },
    { value: 'block', label: 'block' },
    { value: 'dot', label: 'dot' },
    { value: 'outline', label: 'outline' },
  ] as const
})

const motionOptions = [
  { value: 'stretch', label: 'stretch' },
  { value: 'warp', label: 'warp' },
  { value: 'glide', label: 'glide' },
  { value: 'snap', label: 'snap' },
  { value: 'spring', label: 'spring' },
] as const

function next() {
  active.value = active.value === 'A' ? 'B' : active.value === 'B' ? 'C' : 'A'
}
</script>

<template>
  <div class="tx-demo tx-demo__col" style="max-width: 860px;">
    <TxCard variant="plain" background="mask" :padding="14" :radius="14">
      <div class="tx-demo__row" style="gap: 10px; flex-wrap: wrap;">
        <label class="tx-demo__row" style="gap: 8px;">
          <span class="tx-demo__label">motion</span>
          <TuffSelect v-model="motion" style="min-width: 190px;">
            <TuffSelectItem v-for="opt in motionOptions" :key="opt.value" :value="opt.value" :label="opt.label" />
          </TuffSelect>
        </label>

        <label class="tx-demo__row" style="gap: 8px;">
          <span class="tx-demo__label">auto</span>
          <TxButton size="small" @click="next">Next</TxButton>
        </label>

        <div style="opacity: 0.7; font-size: 12px;">
          active: <b>{{ active }}</b>
        </div>
      </div>
    </TxCard>

    <div class="tx-demo__col" style="gap: 12px;">
      <TxCard
        v-for="v in variants"
        :key="v.value"
        variant="plain"
        background="mask"
        :padding="12"
        :radius="14"
      >
        <div class="tx-demo__label" style="margin-bottom: 8px;">
          {{ v.label }}
        </div>

        <TxTabs
          v-model="active"
          placement="top"
          :content-scrollable="false"
          :indicator-variant="v.value"
          :indicator-motion="motion"
          :animation="{ indicator: { durationMs: 180 }, content: true }"
        >
          <TxTabItem name="A" activation>
            Overview
          </TxTabItem>
          <TxTabItem name="B">
            Features
          </TxTabItem>
          <TxTabItem name="C">
            Pricing
          </TxTabItem>
        </TxTabs>
      </TxCard>
    </div>
  </div>
</template>

动态内容尺寸(manual, rich content)

Dynamic Content (manual)

示例即将加载...
<script setup lang="ts">
import { computed, ref } from 'vue'

const active = ref<'Overview' | 'Details' | 'Form'>('Overview')

const expanded = ref(false)
const count = ref(3)
const items = computed(() => Array.from({ length: count.value }).map((_, i) => `Item ${i + 1}`))

const query = ref('')
</script>

<template>
  <div style="display: grid; gap: 10px; min-height: 120px; max-width: 100%;">
    <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
      <TxButton size="small" @click="expanded = !expanded">
        Toggle details
      </TxButton>
      <TxButton size="small" :disabled="count <= 0" @click="count--">
        - Item
      </TxButton>
      <TxButton size="small" @click="count++">
        + Item
      </TxButton>
    </div>

    <TxTabs
      v-model="active"
      placement="left"
      :content-scrollable="false"
      auto-width
      :animation="{ size: { enabled: true, durationMs: 260, easing: 'ease' } }"
    >
      <TxTabItem name="Overview" activation icon-class="i-carbon-dashboard">
        <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="12">
          <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="font-weight: 650;">
              Overview
            </div>
            <div style="font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
              This tab is intentionally compact.
            </div>
            <div style="display: flex; gap: 8px; align-items: center;">
              <div style="font-size: 12px;">
                Search
              </div>
              <div style="width: 220px; max-width: 100%;">
                <TxSearchInput v-model="query" placeholder="Try typing..." />
              </div>
            </div>
          </div>
        </TxCard>
      </TxTabItem>

      <TxTabItem name="Details" icon-class="i-carbon-list">
        <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="12">
          <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
              <div style="font-weight: 650;">
                Details
              </div>
              <div style="font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
                Items: {{ items.length }}
              </div>
            </div>

            <div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;">
              <div
                v-for="it in items"
                :key="it"
                style="border-radius: 12px; padding: 10px; border: 1px solid color-mix(in srgb, var(--tx-border-color, #dcdfe6) 65%, transparent);"
              >
                <div style="font-weight: 600;">
                  {{ it }}
                </div>
                <div style="margin-top: 4px; font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
                  Dynamic grid cell
                </div>
              </div>
            </div>

            <div v-if="expanded" style="display: flex; flex-direction: column; gap: 6px;">
              <div style="font-weight: 600;">
                Expanded block
              </div>
              <div style="font-size: 12px; color: var(--tx-text-color-secondary, #909399);">
                This area appears/disappears and should trigger AutoSizer refresh.
              </div>
              <div
                style="height: 110px; border-radius: 12px; background: color-mix(in srgb, var(--tx-color-primary, #409eff) 12%, transparent);"
              />
            </div>
          </div>
        </TxCard>
      </TxTabItem>

      <TxTabItem name="Form" icon-class="i-carbon-settings">
        <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="12">
          <div style="display: flex; flex-direction: column; gap: 10px;">
            <div style="font-weight: 650;">
              Form
            </div>
            <div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;">
              <TxSearchInput v-model="query" placeholder="Field A" />
              <TxSearchInput v-model="query" placeholder="Field B" />
            </div>

            <div
              style="height: 160px; border-radius: 12px; background: color-mix(in srgb, var(--tx-color-success, #67c23a) 10%, transparent);"
            />
          </div>
        </TxCard>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

布局方向(placement)

Placement + Header Slot

示例即将加载...
<script setup lang="ts">
import { ref } from 'vue'

const activeTop = ref('A')
const activeRight = ref('General')
const actionWide = ref(false)
</script>

<template>
  <div style="display: flex; flex-direction: column; gap: 12px; align-items: stretch;">
    <div style="height: 240px;">
      <TxTabs v-model="activeTop" placement="top" auto-width :animation="{ indicator: { durationMs: 220, easing: 'ease' } }">
        <TxTabHeader v-slot="{ props }">
          <div style="display: flex; align-items: center; width: 100%; padding: 10px 12px;">
            <div style="font-weight: 600;">
              {{ props.node?.props?.name }}
            </div>
          </div>
        </TxTabHeader>

        <template #nav-right>
          <TxButton size="small" type="primary" @click="actionWide = !actionWide">
            {{ actionWide ? 'More Actions' : 'Action' }}
          </TxButton>
        </template>

        <TxTabItem name="A" activation>
          <div style="padding: 8px;">
            Top - A
          </div>
        </TxTabItem>
        <TxTabItem name="B">
          <div style="padding: 8px;">
            Top - B
          </div>
        </TxTabItem>
        <TxTabItem name="C">
          <div style="padding: 8px;">
            Top - C
          </div>
        </TxTabItem>
      </TxTabs>
    </div>

    <div style="height: 240px;">
      <TxTabs v-model="activeRight" placement="right">
        <TxTabItem name="General" icon-class="i-carbon-settings" activation>
          <div style="padding: 8px;">
            Right - General
          </div>
        </TxTabItem>
        <TxTabItem name="Account" icon-class="i-carbon-user">
          <div style="padding: 8px;">
            Right - Account
          </div>
        </TxTabItem>
      </TxTabs>
    </div>
  </div>
</template>

高度跟随内容(animation.size)

Auto Size (contentScrollable=false)

示例即将加载...
<script setup lang="ts">
import { ref } from 'vue'

const active = ref('Long')
</script>

<template>
  <div style="min-height: 120px;">
    <TxTabs
      v-model="active"
      placement="left"
      :content-scrollable="false"
      :animation="{ size: { enabled: true, durationMs: 260, easing: 'ease' } }"
    >
      <TxTabItem name="Long" activation>
        <div style="padding: 10px;">
          <div style="font-weight: 600; margin-bottom: 8px;">
            Long Content
          </div>
          <div style="height: 260px; border-radius: 10px; background: color-mix(in srgb, var(--tx-color-primary, #409eff) 12%, transparent);" />
        </div>
      </TxTabItem>
      <TxTabItem name="Short">
        <div style="padding: 10px;">
          <div style="font-weight: 600; margin-bottom: 8px;">
            Short Content
          </div>
          <div style="height: 90px; border-radius: 10px; background: color-mix(in srgb, var(--tx-color-success, #67c23a) 12%, transparent);" />
        </div>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

关闭动画(indicator/content)

Disable Animations

示例即将加载...
<script setup lang="ts">
import { ref } from 'vue'

const active = ref('Left1')
</script>

<template>
  <div style="height: 240px;">
    <TxTabs
      v-model="active"
      placement="bottom"
      :animation="{ indicator: false, content: false }"
    >
      <TxTabItem name="Left1" activation>
        <div style="padding: 10px;">
          Bottom - No animations
        </div>
      </TxTabItem>
      <TxTabItem name="Left2">
        <div style="padding: 10px;">
          Bottom - No animations 2
        </div>
      </TxTabItem>
      <TxTabItem name="Left3">
        <div style="padding: 10px;">
          Bottom - No animations 3
        </div>
      </TxTabItem>
    </TxTabs>
  </div>
</template>

API

TxTabs Props

属性名类型默认值说明
modelValuestring-当前激活 tab(v-model)
defaultValuestring-默认激活 tab(当未传 modelValue 时)
placement'left' | 'right' | 'top' | 'bottom''left'Tabs 布局位置
offsetnumber0指示条定位偏移
navMinWidthnumber220左侧导航最小宽度
navMaxWidthnumber320左侧导航最大宽度
contentPaddingnumber12内容区 padding
contentScrollablebooleantrue内容是否可滚动(关闭后可用于 autoHeight/autoWidth 的尺寸测量)
autoHeightbooleanfalse自动高度(需要 contentScrollable=false + animation.size.enabled=true
autoWidthbooleanfalse自动宽度(需要 animation.size.enabled=true
indicatorVariant'line' | 'pill' | 'block' | 'dot' | 'outline''line'指示器样式
indicatorMotion'stretch' | 'warp' | 'glide' | 'snap' | 'spring''stretch'指示器动效风格
indicatorMotionStrengthnumber1指示器动效强度(数值越大越“Q弹”,0 基本关闭 scale 弹性)
animationTabsAnimation-动画配置(size/nav/indicator/content)
animation.sizeboolean | { enabled?; durationMs?; easing? }-内容区尺寸动画(高度跟随内容,仅在 contentScrollable=false 时生效)。未传时使用 autoHeight/autoWidth 启用状态与 autoHeightDurationMs/autoHeightEasing 默认值
animation.navboolean | { enabled?; durationMs?; easing? }-导航容器动画(nav 宽度/布局变化过渡)
animation.indicatorboolean | { enabled?; durationMs?; easing? }-指示条动画
animation.contentboolean | { enabled? }-内容切换过渡(zoom 动画)
autoHeightDurationMsnumber250animation.size.durationMs 未指定时的默认时长
autoHeightEasingstringeaseanimation.size.easing 未指定时的默认缓动

Slots

名称参数说明
default-放置 TxTabItem / TxTabItemGroup / TxTabHeader
nav-right-顶部/底部 Tabs 的导航右侧区域(适合放按钮、搜索等),宽度变化会配合 autoWidth 跟随内容

Expose

名称类型说明
refresh() => void触发内容尺寸重新测量(AutoSizer passthrough)
flip(action: () => void | Promise<void>) => Promise<void>包裹一次变更并使用 FLIP 尺寸过渡
action(fn: (el: HTMLElement) => void | Promise<void>, optionsOrDetect?: any) => Promise<any>AutoSizer 的 action wrapper(用于丝滑自动切换)
size() => { width: number; height: number } | undefined当前测量的尺寸信息

TxTabItem Props

属性名类型默认值说明
namestring-tab 名称(唯一 key)
iconClassstring''图标 class
disabledbooleanfalse禁用
activationbooleanfalse是否作为初始激活项

Events

事件名参数说明
changestring切换时触发
update:modelValuestringv-model 更新