parent
e3919107ab
commit
249321ac67
|
|
@ -0,0 +1,313 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export interface CardItem {
|
||||
id: string | number
|
||||
title: string
|
||||
description?: string
|
||||
icon?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cards: CardItem[]
|
||||
maxVisible?: number
|
||||
spreadGap?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxVisible: 5,
|
||||
spreadGap: 190
|
||||
})
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const hoveredCardId = ref<string | number | null>(null)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 默认颜色调色板 - 霓虹色系
|
||||
const defaultColors = [
|
||||
'#06b6d4', // cyan
|
||||
'#8b5cf6', // purple
|
||||
'#22c55e', // green
|
||||
'#f59e0b', // amber
|
||||
'#ef4444', // red
|
||||
'#ec4899', // pink
|
||||
]
|
||||
|
||||
const cardStyle = computed(() => (index: number, total: number) => {
|
||||
const color = props.cards[index]?.color || defaultColors[index % defaultColors.length]
|
||||
const isCardHovered = hoveredCardId.value === props.cards[index]?.id
|
||||
|
||||
// 堆叠状态 - 第一张卡片(index 0)在最右边最上层,其他卡片向左依次露出边缘
|
||||
const stackOffset = 16 // 每张卡片向左露出的距离
|
||||
const stackX = -index * stackOffset
|
||||
const stackZIndex = total - index
|
||||
|
||||
if (!isExpanded.value) {
|
||||
return {
|
||||
transform: `translateX(${stackX}px)`,
|
||||
zIndex: stackZIndex,
|
||||
opacity: index < props.maxVisible ? 1 : 0,
|
||||
borderColor: color,
|
||||
boxShadow: `0 4px 20px rgba(0, 0, 0, 0.3), 0 0 0 1px ${color}20`
|
||||
}
|
||||
}
|
||||
|
||||
// 展开状态 - 最后一个卡片位置不变,其他卡片向右展开,确保不遮挡
|
||||
const spreadX = (total - 1 - index) * props.spreadGap
|
||||
const zIndexValue = isCardHovered ? 100 : total - index
|
||||
|
||||
return {
|
||||
transform: `translateX(${spreadX}px) scale(${isCardHovered ? 1.05 : 1})`,
|
||||
zIndex: zIndexValue,
|
||||
opacity: 1,
|
||||
borderColor: color,
|
||||
boxShadow: isCardHovered
|
||||
? `0 20px 40px rgba(0, 0, 0, 0.4), 0 0 30px ${color}40, 0 0 0 1px ${color}`
|
||||
: `0 8px 30px rgba(0, 0, 0, 0.3), 0 0 0 1px ${color}30`
|
||||
}
|
||||
})
|
||||
|
||||
const handleDocumentClick = (event: MouseEvent) => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
// 点击卡片组外部时收起
|
||||
if (!containerRef.value.contains(event.target as Node)) {
|
||||
if (isExpanded.value) {
|
||||
isExpanded.value = false
|
||||
hoveredCardId.value = null
|
||||
}
|
||||
} else {
|
||||
// 点击卡片组内部时展开
|
||||
if (!isExpanded.value) {
|
||||
isExpanded.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleDocumentClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="stacked-cards-container">
|
||||
<div class="cards-wrapper">
|
||||
<TransitionGroup name="card-spread">
|
||||
<div
|
||||
v-for="(card, index) in cards"
|
||||
:key="card.id"
|
||||
class="card"
|
||||
:style="cardStyle(index, cards.length)"
|
||||
@mouseenter="hoveredCardId = card.id"
|
||||
@mouseleave="hoveredCardId = null"
|
||||
>
|
||||
<div class="card-glow" :style="{ background: card.color || defaultColors[index % defaultColors.length] }" />
|
||||
<div class="card-content">
|
||||
<div v-if="card.icon" class="card-icon">
|
||||
{{ card.icon }}
|
||||
</div>
|
||||
<h3 class="card-title">{{ card.title }}</h3>
|
||||
<p v-if="card.description" class="card-description">
|
||||
{{ card.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-accent" :style="{ background: card.color || defaultColors[index % defaultColors.length] }" />
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<!-- 提示文字 -->
|
||||
<Transition name="hint-fade">
|
||||
<div v-if="!isExpanded" class="hover-hint">
|
||||
<span class="hint-icon">◉</span>
|
||||
<span>点击展开</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stacked-cards-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
padding: 2rem;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.cards-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 180px;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: absolute;
|
||||
width: 180px;
|
||||
height: 240px;
|
||||
background: linear-gradient(145deg, #1a1a2e 0%, #16162a 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: transform 0.35s ease-out, box-shadow 0.35s ease-out, opacity 0.35s ease-out;
|
||||
will-change: transform, box-shadow;
|
||||
}
|
||||
|
||||
/* 卡片发光效果 */
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
opacity: 0;
|
||||
filter: blur(60px);
|
||||
transition: opacity 0.35s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card:hover .card-glow {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
/* 卡片底部强调线 */
|
||||
.card-accent {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 1.25rem;
|
||||
color: #e4e4e7;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: 'Space Grotesk', 'JetBrains Mono', system-ui, sans-serif;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
margin: 0 0 0.5rem;
|
||||
background: linear-gradient(135deg, #fff 0%, #a1a1aa 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: #71717a;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 展开动画 */
|
||||
.card-spread-move,
|
||||
.card-spread-enter-active,
|
||||
.card-spread-leave-active {
|
||||
transition: all 0.35s ease-out;
|
||||
}
|
||||
|
||||
.card-spread-enter-from,
|
||||
.card-spread-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* 悬停提示 */
|
||||
.hover-hint {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -1.5rem;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #52525b;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
color: #06b6d4;
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.hint-fade-enter-active,
|
||||
.hint-fade-leave-active {
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.hint-fade-enter-from,
|
||||
.hint-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 640px) {
|
||||
.stacked-cards-container {
|
||||
min-height: 240px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cards-wrapper {
|
||||
width: 160px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 150px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<script setup lang="ts">
|
||||
import StackedCards from '@/components/ui/StackedCards.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const demoCards = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '设计系统',
|
||||
description: '构建一致性的视觉语言和组件库',
|
||||
icon: '🎨',
|
||||
color: '#06b6d4'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '开发工具',
|
||||
description: '高效的开发工作流和自动化',
|
||||
icon: '⚡',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '性能优化',
|
||||
description: '极速加载和流畅交互体验',
|
||||
icon: '🚀',
|
||||
color: '#22c55e'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '安全防护',
|
||||
description: '企业级安全保障和数据保护',
|
||||
icon: '🛡️',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '数据分析',
|
||||
description: '深度洞察和智能决策支持',
|
||||
icon: '📊',
|
||||
color: '#ef4444'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="demo-page">
|
||||
<div class="demo-container">
|
||||
<h1 class="page-title">层叠卡片组</h1>
|
||||
<p class="page-subtitle">悬停展开 · 视觉发现 · 触感交互</p>
|
||||
|
||||
<div class="demo-section">
|
||||
<StackedCards :cards="demoCards" />
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-card">
|
||||
<h3>使用方式</h3>
|
||||
<pre><code><StackedCards
|
||||
:cards="cardList"
|
||||
:max-visible="5"
|
||||
:spread-gap="190"
|
||||
/></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<h3>卡片数据结构</h3>
|
||||
<pre><code>interface CardItem {
|
||||
id: string | number
|
||||
title: string
|
||||
description?: string
|
||||
icon?: string
|
||||
color?: string
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.demo-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0a0a0f 0%, #0d0d14 50%, #0a0a0f 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: 'Space Grotesk', 'JetBrains Mono', system-ui, sans-serif;
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
background: linear-gradient(135deg, #fff 0%, #a1a1aa 50%, #06b6d4 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #52525b;
|
||||
letter-spacing: 0.1em;
|
||||
margin: 0 0 3rem;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: linear-gradient(145deg, #16162a 0%, #12121f 100%);
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #e4e4e7;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.info-card pre {
|
||||
background: #0a0a0f;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.info-card code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: #a1a1aa;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,6 +14,11 @@ const router = createRouter({
|
|||
name: 'share',
|
||||
component: () => import('@/views/ShareView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/demo/cards',
|
||||
name: 'stacked-cards-demo',
|
||||
component: () => import('@/components/ui/StackedCardsDemo.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue