组件/Card 卡片
通用组件

Card 卡片

Card 卡片组件是一个通用的内容容器,具有玻璃拟态效果,适用于展示各种类型的信息和内容。

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

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

Card 卡片

Card 卡片组件是一个通用的内容容器,具有玻璃拟态效果,适用于展示各种类型的信息和内容。

基础用法

Basic

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-demo-shell">
    <TxCard variant="solid" background="glass" shadow="none" :padding="14">
      <div class="card-demo-title">
        Card
      </div>
      <div class="card-demo-desc">
        A generic container for content.
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-demo-shell {
  width: 520px;
  max-width: 100%;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

Basic slots

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-demo-shell">
    <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="14">
      <template #header>
        <div class="card-demo-header">
          <div class="card-demo-title">
            Card title
          </div>
          <TxButton size="small" variant="text">
            Action
          </TxButton>
        </div>
      </template>

      <div class="card-demo-desc">
        Slots: <code>header</code> / <code>default</code> / <code>footer</code>
      </div>

      <template #footer>
        <div class="card-demo-footer">
          <TxButton size="small" variant="outline">
            Cancel
          </TxButton>
          <TxButton size="small" variant="primary">
            Confirm
          </TxButton>
        </div>
      </template>
    </TxCard>
  </div>
</template>

<style scoped>
.card-demo-shell {
  width: 520px;
  max-width: 100%;
}

.card-demo-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  font-size: 12px;
  line-height: 1.5;
  color: var(--tx-text-color-secondary, #909399);
}

.card-demo-footer {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}
</style>

惯性跟随回弹(inertial)

inertial

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

const rebound = ref(0.12)
const maxOffset = ref(26)
</script>

<template>
  <div class="card-inertial-shell">
    <div class="card-inertial-control-row">
      <div class="card-inertial-label">
        rebound
      </div>
      <TxSlider
        v-model="rebound"
        :min="0"
        :max="1"
        :step="0.02"
        show-value
        :format-value="(value) => value.toFixed(2)"
        class="card-inertial-slider"
      />
    </div>

    <div class="card-inertial-control-row">
      <div class="card-inertial-label">
        maxOffset
      </div>
      <TxSlider
        v-model="maxOffset"
        :min="0"
        :max="40"
        :step="1"
        show-value
        class="card-inertial-slider"
      />
    </div>

    <TxCard
      variant="solid"
      background="glass"
      shadow="none"
      inertial
      :inertial-max-offset="maxOffset"
      :inertial-rebound="rebound"
      :padding="14"
    >
      <div class="card-demo-title">
        Inertial drag
      </div>
      <div class="card-demo-desc">
        Move mouse around on hover, leave to snap back.
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-inertial-shell {
  width: 520px;
  max-width: 100%;
  padding: 16px;
  overflow: visible;
}

.card-inertial-control-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}

.card-inertial-label {
  min-width: 84px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.card-inertial-slider {
  flex: 1;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

带标题的卡片

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-demo-shell">
    <TxCard variant="solid" background="glass" shadow="none" :padding="14">
      <template #header>
        <div class="card-demo-title">
          Card title
        </div>
      </template>
      <div class="card-demo-desc">
        Using <code>header</code> slot
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-demo-shell {
  width: 520px;
  max-width: 100%;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

带操作按钮的卡片

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-actions-shell">
    <TxCard variant="solid" background="glass" shadow="soft" :radius="18" :padding="14">
      <template #header>
        <div class="card-actions-header">
          <div class="card-actions-title">
            User info
          </div>
          <TxButton size="small" variant="text">
            More
          </TxButton>
        </div>
      </template>

      <div class="card-actions-content">
        <div class="card-actions-avatar" />
        <div class="card-actions-content-col">
          <div class="card-actions-title">
            Zhang San
          </div>
          <div class="card-actions-subtitle">
            Frontend Engineer
          </div>
        </div>
      </div>

      <template #footer>
        <div class="card-actions-footer">
          <TxButton size="small" variant="outline">
            Cancel
          </TxButton>
          <TxButton size="small" variant="primary">
            Confirm
          </TxButton>
        </div>
      </template>
    </TxCard>
  </div>
</template>

<style scoped>
.card-actions-shell {
  width: 520px;
  max-width: 100%;
}

.card-actions-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.card-actions-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-actions-content {
  display: flex;
  gap: 12px;
  align-items: center;
}

.card-actions-avatar {
  width: 34px;
  height: 34px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tx-color-primary, #409eff) 24%, transparent);
}

.card-actions-content-col {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.card-actions-subtitle {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.card-actions-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
</style>

卡片变体

提供不同的视觉样式:

variants

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-variants-wrap">
    <div class="card-variants-grid">
      <TxCard variant="solid" background="glass" shadow="none" :padding="14">
        <div class="card-demo-title">
          solid
        </div>
        <div class="card-demo-desc">
          Border + hover feedback
        </div>
      </TxCard>

      <TxCard variant="dashed" background="glass" shadow="none" :padding="14">
        <div class="card-demo-title">
          dashed
        </div>
        <div class="card-demo-desc">
          Dashed border
        </div>
      </TxCard>

      <TxCard variant="plain" background="glass" shadow="none" :padding="14">
        <div class="card-demo-title">
          plain
        </div>
        <div class="card-demo-desc">
          No border, no hover
        </div>
      </TxCard>
    </div>
  </div>
</template>

<style scoped>
.card-variants-wrap {
  width: 100%;
  overflow-x: auto;
}

.card-variants-grid {
  min-width: 680px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

不同背景下的效果(refraction / glass / blur)

单卡片对比:背后提供文本与图形内容,通过开关切换 background(refraction / glass / blur / mask)。推荐优先使用 refraction

Card backgrounds (scroll)

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

type Bg = 'blur' | 'glass' | 'refraction' | 'mask'
type CardVariant = 'solid' | 'dashed' | 'plain'
type CardShadow = 'none' | 'soft' | 'medium'
type RefractionProfile = 'soft' | 'filmic' | 'cinematic'
type RefractionPresetGroup = 'core' | 'filmic'

interface RefractionPreset {
  id: string
  name: string
  group: RefractionPresetGroup
  profile: RefractionProfile
  strength: number
  angle: number
  blurAmount: number
  overlayOpacity: number
  followIntensity: number
  spring: boolean
  springStiffness: number
  springDamping: number
}

const refractionPresetGroups: Array<{ id: RefractionPresetGroup, name: string }> = [
  { id: 'filmic', name: 'Filmic Set' },
  { id: 'core', name: 'Core Set' },
]

const refractionPresets: RefractionPreset[] = [
  {
    id: 'soft-mist',
    name: 'Soft Mist',
    group: 'core',
    profile: 'soft',
    strength: 42,
    angle: -16,
    blurAmount: 16,
    overlayOpacity: 0.16,
    followIntensity: 0.34,
    spring: true,
    springStiffness: 0.14,
    springDamping: 0.88,
  },
  {
    id: 'filmic-classic',
    name: 'Filmic Classic',
    group: 'core',
    profile: 'filmic',
    strength: 62,
    angle: -24,
    blurAmount: 22,
    overlayOpacity: 0.22,
    followIntensity: 0.5,
    spring: true,
    springStiffness: 0.18,
    springDamping: 0.84,
  },
  {
    id: 'cinematic-rgb',
    name: 'Cinematic RGB',
    group: 'core',
    profile: 'cinematic',
    strength: 82,
    angle: -34,
    blurAmount: 26,
    overlayOpacity: 0.28,
    followIntensity: 0.64,
    spring: true,
    springStiffness: 0.24,
    springDamping: 0.78,
  },
  {
    id: 'sharp-glass',
    name: 'Sharp Glass',
    group: 'core',
    profile: 'filmic',
    strength: 70,
    angle: 14,
    blurAmount: 10,
    overlayOpacity: 0.12,
    followIntensity: 0.46,
    spring: false,
    springStiffness: 0.2,
    springDamping: 0.8,
  },
  {
    id: 'filmic-bloom',
    name: 'Bloom Halo',
    group: 'filmic',
    profile: 'filmic',
    strength: 66,
    angle: -18,
    blurAmount: 30,
    overlayOpacity: 0.34,
    followIntensity: 0.56,
    spring: true,
    springStiffness: 0.16,
    springDamping: 0.86,
  },
  {
    id: 'filmic-chrome',
    name: 'Chrome Split',
    group: 'filmic',
    profile: 'cinematic',
    strength: 88,
    angle: -42,
    blurAmount: 24,
    overlayOpacity: 0.24,
    followIntensity: 0.7,
    spring: true,
    springStiffness: 0.28,
    springDamping: 0.76,
  },
  {
    id: 'filmic-prism',
    name: 'Prism Noir',
    group: 'filmic',
    profile: 'cinematic',
    strength: 78,
    angle: 28,
    blurAmount: 18,
    overlayOpacity: 0.2,
    followIntensity: 0.62,
    spring: true,
    springStiffness: 0.22,
    springDamping: 0.8,
  },
]

const bg = ref<Bg>('refraction')
const variant = ref<CardVariant>('solid')
const shadow = ref<CardShadow>('soft')
const radius = ref(18)
const padding = ref(14)
const clickable = ref(false)
const loading = ref(false)
const disabled = ref(false)
const inertial = ref(false)
const inertialMaxOffset = ref(22)
const inertialRebound = ref(0.12)

const glassBlur = ref(true)
const glassBlurAmount = ref(22)
const glassOverlay = ref(true)
const glassOverlayOpacity = ref(0.22)

const refractionStrength = ref(62)
const refractionProfile = ref<RefractionProfile>('filmic')
const refractionAngle = ref(-24)
const refractionFollowMouse = ref(true)
const refractionFollowIntensity = ref(0.5)
const refractionLightSpring = ref(true)
const refractionLightSpringStiffness = ref(0.18)
const refractionLightSpringDamping = ref(0.84)
const activePresetGroup = ref<RefractionPresetGroup>('filmic')
const activePresetId = ref('filmic-bloom')

const visibleRefractionPresets = computed(() =>
  refractionPresets.filter(item => item.group === activePresetGroup.value),
)

const sliderCommon = { showValue: true as const }
const formatFixed2 = (value: number) => value.toFixed(2)
const formatDeg = (value: number) => `${Math.round(value)}deg`

function applyRefractionPreset(id: string) {
  const preset = refractionPresets.find(item => item.id === id)
  if (!preset)
    return

  activePresetId.value = preset.id
  activePresetGroup.value = preset.group
  bg.value = 'refraction'
  refractionProfile.value = preset.profile
  refractionStrength.value = preset.strength
  refractionAngle.value = preset.angle
  glassBlurAmount.value = preset.blurAmount
  glassOverlayOpacity.value = preset.overlayOpacity
  refractionFollowIntensity.value = preset.followIntensity
  refractionLightSpring.value = preset.spring
  refractionLightSpringStiffness.value = preset.springStiffness
  refractionLightSpringDamping.value = preset.springDamping
}

function switchRefractionPresetGroup(group: RefractionPresetGroup) {
  activePresetGroup.value = group
  const nextPreset = refractionPresets.find(item => item.group === group)
  if (!nextPreset)
    return
  applyRefractionPreset(nextPreset.id)
}

applyRefractionPreset(activePresetId.value)
</script>

<template>
  <div class="tx-card-bg-demo">
    <TxRadioGroup v-model="bg">
      <TxRadio value="refraction">
        refraction
      </TxRadio>
      <TxRadio value="glass">
        glass
      </TxRadio>
      <TxRadio value="blur">
        blur
      </TxRadio>
      <TxRadio value="mask">
        mask
      </TxRadio>
    </TxRadioGroup>

    <div class="tx-card-bg-controls">
      <div class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">variant</span>
        <TxRadioGroup v-model="variant" size="small">
          <TxRadio value="solid">
            solid
          </TxRadio>
          <TxRadio value="dashed">
            dashed
          </TxRadio>
          <TxRadio value="plain">
            plain
          </TxRadio>
        </TxRadioGroup>
      </div>

      <div class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">shadow</span>
        <TxRadioGroup v-model="shadow" size="small">
          <TxRadio value="none">
            none
          </TxRadio>
          <TxRadio value="soft">
            soft
          </TxRadio>
          <TxRadio value="medium">
            medium
          </TxRadio>
        </TxRadioGroup>
      </div>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">radius</span>
        <TxSlider
          v-model="radius"
          :min="10"
          :max="28"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">padding</span>
        <TxSlider
          v-model="padding"
          :min="8"
          :max="24"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">clickable</span>
        <TxSwitch v-model="clickable" />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">loading</span>
        <TxSwitch v-model="loading" />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">disabled</span>
        <TxSwitch v-model="disabled" />
      </label>

      <label class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">inertial</span>
        <TxSwitch v-model="inertial" />
      </label>

      <label v-if="inertial" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">max offset</span>
        <TxSlider
          v-model="inertialMaxOffset"
          :min="0"
          :max="40"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="inertial" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">rebound</span>
        <TxSlider
          v-model="inertialRebound"
          :min="0"
          :max="1"
          :step="0.02"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'glass' || bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">glass blur</span>
        <TxSwitch v-model="glassBlur" />
      </label>

      <label v-if="(bg === 'glass' || bg === 'refraction') && glassBlur" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">blur</span>
        <TxSlider
          v-model="glassBlurAmount"
          :min="0"
          :max="40"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'glass' || bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">overlay</span>
        <TxSwitch v-model="glassOverlay" />
      </label>

      <label v-if="(bg === 'glass' || bg === 'refraction') && glassOverlay" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">opacity</span>
        <TxSlider
          v-model="glassOverlayOpacity"
          :min="0"
          :max="0.6"
          :step="0.02"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <div v-if="bg === 'refraction'" class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">preset</span>
        <div class="tx-card-bg-preset-group-row">
          <TxButton
            v-for="group in refractionPresetGroups"
            :key="group.id"
            class="tx-card-bg-preset-group"
            :variant="activePresetGroup === group.id ? 'primary' : 'outline'"
            size="small"
            @click="switchRefractionPresetGroup(group.id)"
          >
            {{ group.name }}
          </TxButton>
        </div>
        <div class="tx-card-bg-preset-row">
          <TxButton
            v-for="preset in visibleRefractionPresets"
            :key="preset.id"
            class="tx-card-bg-preset"
            :variant="activePresetId === preset.id ? 'primary' : 'outline'"
            size="small"
            @click="applyRefractionPreset(preset.id)"
          >
            {{ preset.name }}
          </TxButton>
        </div>
      </div>

      <label v-if="bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">strength</span>
        <TxSlider
          v-model="refractionStrength"
          :min="0"
          :max="100"
          :step="1"
          v-bind="sliderCommon"
          class="tx-card-bg-control-slider"
        />
      </label>

      <div v-if="bg === 'refraction'" class="tx-card-bg-control tx-card-bg-control--block">
        <span class="tx-card-bg-control-label">profile</span>
        <TxRadioGroup v-model="refractionProfile" size="small">
          <TxRadio value="soft">
            soft
          </TxRadio>
          <TxRadio value="filmic">
            filmic
          </TxRadio>
          <TxRadio value="cinematic">
            cinematic
          </TxRadio>
        </TxRadioGroup>
      </div>

      <label v-if="bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">angle</span>
        <TxSlider
          v-model="refractionAngle"
          :min="-180"
          :max="180"
          :step="1"
          v-bind="sliderCommon"
          :format-value="formatDeg"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'refraction'" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">follow mouse</span>
        <TxSwitch v-model="refractionFollowMouse" />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">follow intensity</span>
        <TxSlider
          v-model="refractionFollowIntensity"
          :min="0"
          :max="1"
          :step="0.02"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">light spring</span>
        <TxSwitch v-model="refractionLightSpring" />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse && refractionLightSpring" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">spring stiffness</span>
        <TxSlider
          v-model="refractionLightSpringStiffness"
          :min="0.01"
          :max="0.55"
          :step="0.01"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>

      <label v-if="bg === 'refraction' && refractionFollowMouse && refractionLightSpring" class="tx-card-bg-control">
        <span class="tx-card-bg-control-label">spring damping</span>
        <TxSlider
          v-model="refractionLightSpringDamping"
          :min="0.55"
          :max="0.99"
          :step="0.01"
          v-bind="sliderCommon"
          :format-value="formatFixed2"
          class="tx-card-bg-control-slider"
        />
      </label>
    </div>

    <div class="tx-card-bg-stage">
      <div class="tx-card-bg-scroll">
        <article class="tx-card-bg-article">
          <div class="tx-card-bg-hero" />

          <h3 class="tx-card-bg-title">
            Behind content: Article layout
          </h3>
          <p class="tx-card-bg-meta">
            Dec 25, 2025 · 5 min read
          </p>

          <p class="tx-card-bg-p">
            This demo intentionally uses neutral text and image blocks to help you judge how
            refraction / glass / blur / mask behaves on the same background.
          </p>

          <div class="tx-card-bg-grid">
            <div class="tx-card-bg-img" />
            <div class="tx-card-bg-img is-light" />
          </div>

          <p class="tx-card-bg-p">
            Scroll the content behind. The card stays floating above, similar to a sticky overlay.
            Use the switch to change background.
          </p>

          <div class="tx-card-bg-grid">
            <div class="tx-card-bg-img is-wide" />
            <div class="tx-card-bg-img is-wide is-light" />
          </div>

          <p class="tx-card-bg-p">
            End.
          </p>
          <div class="tx-card-bg-spacer" />
        </article>
      </div>

      <div class="tx-card-bg-overlay">
        <div class="tx-card-bg-card">
          <TxCard
            :variant="variant"
            :background="bg"
            :shadow="shadow"
            :radius="radius"
            :padding="padding"
            :clickable="clickable"
            :loading="loading"
            :disabled="disabled"
            :inertial="inertial"
            :inertial-max-offset="inertialMaxOffset"
            :inertial-rebound="inertialRebound"
            :glass-blur="glassBlur"
            :glass-blur-amount="glassBlurAmount"
            :glass-overlay="glassOverlay"
            :glass-overlay-opacity="glassOverlayOpacity"
            :refraction-strength="refractionStrength"
            :refraction-profile="refractionProfile"
            :refraction-angle="refractionAngle"
            :refraction-light-follow-mouse="refractionFollowMouse"
            :refraction-light-follow-intensity="refractionFollowIntensity"
            :refraction-light-spring="refractionLightSpring"
            :refraction-light-spring-stiffness="refractionLightSpringStiffness"
            :refraction-light-spring-damping="refractionLightSpringDamping"
            class="tx-card-bg-card__inner"
          >
            <template #header>
              <div class="tx-card-bg-card-header">
                <div class="tx-card-bg-card-title">
                  TxCard
                </div>
                <div class="tx-card-bg-card-meta">
                  bg={{ bg }} · {{ refractionProfile }}
                </div>
              </div>
            </template>

            <div class="tx-card-bg-skeleton">
              <div class="tx-card-bg-skeleton-line" />
              <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--w78" />
              <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--w64" />
              <div class="tx-card-bg-skeleton-row">
                <div class="tx-card-bg-skeleton-avatar" />
                <div class="tx-card-bg-skeleton-col">
                  <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--small" />
                  <div class="tx-card-bg-skeleton-line tx-card-bg-skeleton-line--small tx-card-bg-skeleton-line--w70" />
                </div>
              </div>
            </div>
          </TxCard>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.tx-card-bg-demo {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

.tx-card-bg-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}

.tx-card-bg-control {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  font-size: 13px;
}

.tx-card-bg-control-slider {
  width: 240px;
}

.tx-card-bg-control--block {
  width: 100%;
  align-items: flex-start;
}

.tx-card-bg-control-label {
  color: var(--tx-text-color-secondary, #909399);
}

.tx-card-bg-preset-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.tx-card-bg-preset-group-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 8px;
}

.tx-card-bg-stage {
  position: relative;
  height: 420px;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 65%, transparent);
  background:
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--tx-fill-color-lighter, #fafafa) 75%, transparent) 0%,
      color-mix(in srgb, var(--tx-bg-color-overlay, #fff) 82%, transparent) 100%
    );
}

.tx-card-bg-scroll {
  position: absolute;
  inset: 0;
  overflow-y: auto;
}

.tx-card-bg-article {
  padding: 22px;
  line-height: 1.7;
}

.tx-card-bg-hero {
  height: 120px;
  border-radius: 14px;
  background:
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--tx-text-color-primary, #303133) 8%, transparent),
      color-mix(in srgb, var(--tx-text-color-primary, #303133) 3%, transparent)
    );
  border: 1px solid color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 72%, transparent);
  margin-bottom: 14px;
}

.tx-card-bg-title {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--tx-text-color-primary, #303133);
}

.tx-card-bg-meta {
  margin: 0 0 14px 0;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.tx-card-bg-p {
  margin: 0 0 14px 0;
  font-size: 13px;
  color: var(--tx-text-color-regular, #606266);
}

.tx-card-bg-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin: 14px 0;
}

.tx-card-bg-img {
  height: 96px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 72%, transparent);
}

.tx-card-bg-img.is-light {
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 6%, transparent);
}

.tx-card-bg-img.is-wide {
  height: 120px;
}

.tx-card-bg-spacer {
  height: 240px;
}

.tx-card-bg-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 22px;
  pointer-events: none;
}

.tx-card-bg-card {
  position: relative;
  width: 100%;
  max-width: 320px;
  pointer-events: auto;
}

.tx-card-bg-card__inner {
  position: relative;
  z-index: 1;
  --tx-card-fake-background: color-mix(in srgb, var(--tx-bg-color-overlay, #fff) 94%, transparent);
}

.tx-card-bg-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.tx-card-bg-card-title {
  font-weight: 700;
  color: var(--tx-text-color-primary, #303133);
}

.tx-card-bg-card-meta {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.tx-card-bg-skeleton {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.tx-card-bg-skeleton-row {
  display: flex;
  gap: 8px;
  margin-top: 6px;
}

.tx-card-bg-skeleton-col {
  flex: 1;
  display: grid;
  gap: 6px;
}

.tx-card-bg-skeleton-line {
  height: 12px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 12%, transparent);
}

.tx-card-bg-skeleton-line--small {
  height: 10px;
}

.tx-card-bg-skeleton-line--w78 {
  width: 78%;
}

.tx-card-bg-skeleton-line--w70 {
  width: 70%;
}

.tx-card-bg-skeleton-line--w64 {
  width: 64%;
}

.tx-card-bg-skeleton-avatar {
  width: 34px;
  height: 34px;
  border-radius: 12px;
  background: color-mix(in srgb, var(--tx-text-color-primary, #303133) 12%, transparent);
}
</style>

Empty 布局

Card with Empty

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-empty-shell">
    <TxCard class="card-empty-mask" variant="plain" background="mask" shadow="none" :padding="16">
      <TxEmpty title="Nothing here" description="Create your first item to get started.">
        <template #action>
          <TxButton variant="primary" size="small">
            Create
          </TxButton>
        </template>
      </TxEmpty>
    </TxCard>

    <div class="card-empty-block">
      <TxEmpty title="Empty only" description="No Card wrapper, pure empty block." compact />
    </div>
  </div>
</template>

<style scoped>
.card-empty-shell {
  display: grid;
  gap: 12px;
  width: 420px;
  max-width: 100%;
}

.card-empty-mask {
  --tx-card-fake-background: color-mix(in srgb, var(--tx-fill-color-lighter, #fafafa) 82%, transparent);
}

.card-empty-block {
  padding: 14px;
  border-radius: 14px;
  border: 1px dashed color-mix(in srgb, var(--tx-border-color-light, #e4e7ed) 70%, transparent);
}
</style>

与组件结合方式(Popover / SearchSelect)

Card compositions

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

const open = ref(false)
const value = ref('')
const selectValue = ref('option1')
const cascaderValue = ref<any>()
const treeValue = ref<any>()

const cascaderOptions = [
  {
    value: 'zhejiang',
    label: 'Zhejiang',
    children: [
      { value: 'hangzhou', label: 'Hangzhou', children: [{ value: 'xihu', label: 'West Lake' }] },
      { value: 'ningbo', label: 'Ningbo', children: [{ value: 'jiangbei', label: 'Jiangbei' }] },
    ],
  },
  {
    value: 'jiangsu',
    label: 'Jiangsu',
    children: [{ value: 'nanjing', label: 'Nanjing', children: [{ value: 'gulou', label: 'Gulou' }] }],
  },
]

const treeNodes = [
  {
    key: 'docs',
    label: 'docs',
    children: [
      { key: 'card', label: 'card.md' },
      { key: 'select', label: 'select.md' },
    ],
  },
  {
    key: 'packages',
    label: 'packages',
    children: [{ key: 'components', label: 'components' }],
  },
]
</script>

<template>
  <div class="card-compositions-shell">
    <div class="card-compositions-row">
      <TxPopover
        v-model="open"
        :offset="8"
        :reference-full-width="false"
        panel-variant="solid"
        panel-background="glass"
        panel-shadow="soft"
        :panel-radius="18"
        :panel-padding="10"
      >
        <template #reference>
          <TxButton variant="primary">
            Popover panel
          </TxButton>
        </template>

        <div class="card-compositions-popover-content">
          <div class="card-compositions-meta">
            Popover panel uses TxCard
          </div>
          <TxButton size="small" @click="open = false">
            Close
          </TxButton>
        </div>
      </TxPopover>

      <div class="card-compositions-meta">
        panelVariant/panelBackground/panelShadow/panelRadius/panelPadding
      </div>
    </div>

    <TxSearchSelect
      v-model="value"
      placeholder="Search..."
      :options="[
        { value: 'a', label: 'Alpha' },
        { value: 'b', label: 'Beta' },
        { value: 'g', label: 'Gamma' },
      ]"
      panel-variant="solid"
      panel-background="glass"
      panel-shadow="soft"
      :panel-radius="18"
      :panel-padding="6"
    />

    <div class="card-compositions-grid">
      <TuffSelect v-model="selectValue" placeholder="Select">
        <TuffSelectItem value="option1" label="Option 1" />
        <TuffSelectItem value="option2" label="Option 2" />
        <TuffSelectItem value="option3" label="Option 3" />
      </TuffSelect>

      <TxCascader v-model="cascaderValue" :options="cascaderOptions" placeholder="Cascader" />
    </div>

    <TxTreeSelect v-model="treeValue" :nodes="treeNodes" placeholder="TreeSelect" />
  </div>
</template>

<style scoped>
.card-compositions-shell {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 520px;
  max-width: 100%;
}

.card-compositions-row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
}

.card-compositions-popover-content {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.card-compositions-meta {
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}

.card-compositions-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}
</style>

卡片尺寸

size

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-size-grid">
    <TxCard size="small" variant="solid" background="glass" shadow="none" :padding="undefined">
      <div class="card-demo-title">
        size=small
      </div>
      <div class="card-demo-desc">
        Padding controlled by size
      </div>
    </TxCard>

    <TxCard size="medium" variant="solid" background="glass" shadow="none">
      <div class="card-demo-title">
        size=medium
      </div>
      <div class="card-demo-desc">
        Default
      </div>
    </TxCard>

    <TxCard size="large" variant="solid" background="glass" shadow="none">
      <div class="card-demo-title">
        size=large
      </div>
      <div class="card-demo-desc">
        More padding
      </div>
    </TxCard>
  </div>
</template>

<style scoped>
.card-size-grid {
  display: grid;
  gap: 10px;
  width: 520px;
  max-width: 100%;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

布局属性(padding / radius)

layout props

示例即将加载...
<script setup lang="ts">
</script>

<template>
  <div class="card-layout-wrap">
    <div class="card-layout-grid">
      <TxCard variant="solid" background="glass" shadow="none" :padding="10" :radius="10">
        <div class="card-demo-title">
          padding
        </div>
        <div class="card-demo-desc">
          padding=10
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" :padding="18" :radius="10">
        <div class="card-demo-title">
          padding
        </div>
        <div class="card-demo-desc">
          padding=18
        </div>
      </TxCard>

      <TxCard variant="plain" background="glass" shadow="none" :padding="14" :radius="22">
        <div class="card-demo-title">
          radius
        </div>
        <div class="card-demo-desc">
          radius=22
        </div>
      </TxCard>
    </div>
  </div>
</template>

<style scoped>
.card-layout-wrap {
  width: 100%;
  overflow-x: auto;
}

.card-layout-grid {
  min-width: 520px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

状态(clickable / loading / disabled)

states

示例即将加载...
<script setup lang="ts">
function onClick() {}
</script>

<template>
  <div class="card-states-wrap">
    <div class="card-states-grid">
      <TxCard variant="solid" background="glass" shadow="none" clickable :padding="14" @click="onClick">
        <div class="card-demo-title">
          clickable
        </div>
        <div class="card-demo-desc">
          Hover/active feedback
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" :loading="true" :padding="14" :loading-spinner-size="12">
        <div class="card-demo-title">
          loading
        </div>
        <div class="card-demo-desc">
          spinner=12
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" :loading="true" :padding="14" :loading-spinner-size="20">
        <div class="card-demo-title">
          loading
        </div>
        <div class="card-demo-desc">
          spinner=20
        </div>
      </TxCard>

      <TxCard variant="solid" background="glass" shadow="none" disabled :padding="14">
        <div class="card-demo-title">
          disabled
        </div>
        <div class="card-demo-desc">
          Reduced opacity
        </div>
      </TxCard>
    </div>
  </div>
</template>

<style scoped>
.card-states-wrap {
  width: 100%;
  overflow-x: auto;
}

.card-states-grid {
  min-width: 720px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
}

.card-demo-title {
  font-weight: 600;
  color: var(--tx-text-color-primary, #303133);
}

.card-demo-desc {
  margin-top: 6px;
  font-size: 12px;
  color: var(--tx-text-color-secondary, #909399);
}
</style>

选型建议(TxBaseSurface vs TxCard)

  • 需要更高封装与业务直出:优先用 TxCard。它已包含 variant/shadow/slots/loading/inertial 与 pointer-light + spring 的交互能力。
  • 需要底层材质细调:改用 TxBaseSurface。尤其是折射与滤镜底层参数(displacedistortionScaleredOffset/greenOffset/blueOffsetfilterSaturation/filterContrast/filterBrightnessrefractionHaloOpacity)。
  • 约定:TxCard 维持“开箱即用”稳定 API;材质实验或品牌化渲染直接在 TxBaseSurface 层完成。

API 参考

Props

属性类型默认值说明
variant'solid' | 'dashed' | 'plain''solid'边框与交互形态
background'blur' | 'glass' | 'refraction' | 'mask''refraction'背景风格:blur=filter 层,glass=glass 层,refraction=glass+filter(默认推荐),mask=mask 层
shadow'none' | 'soft' | 'medium''none'阴影强度
size'small' | 'medium' | 'large''medium'卡片尺寸
radiusnumber18圆角
paddingnumber-内边距(不传时由 size 决定)
glassBlurbooleantruebackground='glass' | 'refraction':是否启用 blur
glassBlurAmountnumber22background='glass' | 'refraction':blur 强度(px)
glassOverlaybooleantruebackground='glass' | 'refraction':是否叠加高光遮罩层
glassOverlayOpacitynumber0.18background='glass' | 'refraction':高光遮罩层透明度
fallbackMaskOpacitynumber0.26运动降级到 mask 时的遮罩透明度(0-1)
refractionStrengthnumber62background='refraction':折射强度(0-100),主控色散与扭曲强度
refractionProfile'soft' | 'filmic' | 'cinematic''filmic'background='refraction':折射风格预设
refractionTone'mist' | 'balanced' | 'vivid''vivid'background='refraction':折射色调预设(默认 vivid,减少发灰)
refractionAnglenumber-24background='refraction':色散主方向角度(度)
refractionLightFollowMousebooleanfalsebackground='refraction':是否将高光锚点与鼠标位置绑定
refractionLightFollowIntensitynumber0.45background='refraction':鼠标绑定强度(0-1),控制 angle/strength 跟随权重
refractionLightSpringbooleantruebackground='refraction':鼠标光源是否使用弹簧过渡
refractionLightSpringStiffnessnumber0.18background='refraction':鼠标光源弹簧刚度(建议 0.01-0.55)
refractionLightSpringDampingnumber0.84background='refraction':鼠标光源弹簧阻尼(建议 0.55-0.99)
clickablebooleanfalse是否可点击(hover feedback)
loadingbooleanfalse是否显示加载状态
loadingSpinnerSizenumber-loading spinner 大小(px)
disabledbooleanfalse是否禁用
inertialbooleanfalse是否启用惯性拖拽回弹
inertialMaxOffsetnumber22拖拽最大位移(px)
inertialReboundnumber0.12回弹弹性(0=更粘更稳,1=更弹簧)

Events

事件名参数说明
click(event: MouseEvent)点击卡片时触发

Slots

插槽名说明
default卡片主要内容
header卡片头部内容
footer卡片底部内容
cover卡片封面图片

样式定制

CSS 变量

EXAMPLE.CSS
.custom-card {
  --tx-card-fake-background: rgba(255, 255, 255, 0.8);
  --tx-card-padding: 24px;
}

主题定制

EXAMPLE.CSS
:root {
  /* 卡片背景 */
  --tx-card-background-default: rgba(255, 255, 255, 0.8);
  --tx-card-background-glass: rgba(255, 255, 255, 0.1);
  
  /* 卡片边框 */
  --tx-card-border-default: 1px solid rgba(0, 0, 0, 0.1);
  --tx-card-border-outlined: 1px solid var(--tx-color-border);
  
  /* 卡片阴影 */
  --tx-card-shadow-default: 0 2px 8px rgba(0, 0, 0, 0.1);
  --tx-card-shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.15);
  
  /* 卡片尺寸 */
  --tx-card-padding-small: 16px;
  --tx-card-padding-medium: 20px;
  --tx-card-padding-large: 24px;
}

最佳实践

使用建议

  1. 内容组织:合理使用 header、body、footer 区域组织内容
  2. 视觉层次:通过不同的卡片变体创建视觉层次
  3. 交互反馈:为可点击卡片提供明确的视觉反馈
  4. 响应式设计:确保卡片在不同屏幕尺寸下的良好表现

布局示例

EXAMPLE.VUE
<template>
  <div class="card-grid">
    <TxCard 
      v-for="item in items" 
      :key="item.id"
      clickable
      @click="handleItemClick(item)"
    >
      <template #header>
        <h3>{{ item.title }}</h3>
      </template>
      
      <p>{{ item.description }}</p>
      
      <template #footer>
        <div class="card-actions">
          <TxButton size="small" variant="text">编辑</TxButton>
          <TxButton size="small" variant="text">删除</TxButton>
        </div>
      </template>
    </TxCard>
  </div>
</template>

TouchX UI 的 Card 组件提供了灵活的内容展示方案,结合玻璃拟态效果创造出现代感十足的用户界面。