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',
|
name: 'share',
|
||||||
component: () => import('@/views/ShareView.vue'),
|
component: () => import('@/views/ShareView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/demo/cards',
|
||||||
|
name: 'stacked-cards-demo',
|
||||||
|
component: () => import('@/components/ui/StackedCardsDemo.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue