通用组件
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>
带标题的卡片
Header
示例即将加载...
<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>
带操作按钮的卡片
Header + Footer actions
示例即将加载...
<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。尤其是折射与滤镜底层参数(displace、distortionScale、redOffset/greenOffset/blueOffset、filterSaturation/filterContrast/filterBrightness、refractionHaloOpacity)。 - 约定:
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' | 卡片尺寸 |
| radius | number | 18 | 圆角 |
| padding | number | - | 内边距(不传时由 size 决定) |
| glassBlur | boolean | true | 仅 background='glass' | 'refraction':是否启用 blur |
| glassBlurAmount | number | 22 | 仅 background='glass' | 'refraction':blur 强度(px) |
| glassOverlay | boolean | true | 仅 background='glass' | 'refraction':是否叠加高光遮罩层 |
| glassOverlayOpacity | number | 0.18 | 仅 background='glass' | 'refraction':高光遮罩层透明度 |
| fallbackMaskOpacity | number | 0.26 | 运动降级到 mask 时的遮罩透明度(0-1) |
| refractionStrength | number | 62 | 仅 background='refraction':折射强度(0-100),主控色散与扭曲强度 |
| refractionProfile | 'soft' | 'filmic' | 'cinematic' | 'filmic' | 仅 background='refraction':折射风格预设 |
| refractionTone | 'mist' | 'balanced' | 'vivid' | 'vivid' | 仅 background='refraction':折射色调预设(默认 vivid,减少发灰) |
| refractionAngle | number | -24 | 仅 background='refraction':色散主方向角度(度) |
| refractionLightFollowMouse | boolean | false | 仅 background='refraction':是否将高光锚点与鼠标位置绑定 |
| refractionLightFollowIntensity | number | 0.45 | 仅 background='refraction':鼠标绑定强度(0-1),控制 angle/strength 跟随权重 |
| refractionLightSpring | boolean | true | 仅 background='refraction':鼠标光源是否使用弹簧过渡 |
| refractionLightSpringStiffness | number | 0.18 | 仅 background='refraction':鼠标光源弹簧刚度(建议 0.01-0.55) |
| refractionLightSpringDamping | number | 0.84 | 仅 background='refraction':鼠标光源弹簧阻尼(建议 0.55-0.99) |
| clickable | boolean | false | 是否可点击(hover feedback) |
| loading | boolean | false | 是否显示加载状态 |
| loadingSpinnerSize | number | - | loading spinner 大小(px) |
| disabled | boolean | false | 是否禁用 |
| inertial | boolean | false | 是否启用惯性拖拽回弹 |
| inertialMaxOffset | number | 22 | 拖拽最大位移(px) |
| inertialRebound | number | 0.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;
}
最佳实践
使用建议
- 内容组织:合理使用 header、body、footer 区域组织内容
- 视觉层次:通过不同的卡片变体创建视觉层次
- 交互反馈:为可点击卡片提供明确的视觉反馈
- 响应式设计:确保卡片在不同屏幕尺寸下的良好表现
布局示例
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 组件提供了灵活的内容展示方案,结合玻璃拟态效果创造出现代感十足的用户界面。