208 lines
7.2 KiB
Vue
208 lines
7.2 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="isOpen"
|
|
class="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-10 transition-opacity duration-300 pointer-events-auto"
|
|
:class="{ 'opacity-0': isAnimating, 'opacity-100': !isAnimating }"
|
|
>
|
|
<div class="absolute inset-0 bg-black/90 backdrop-blur-sm" @click="close"></div>
|
|
<div
|
|
class="relative z-[101] bg-[#0a0a0c] border border-white/10 w-full max-w-6xl h-[80vh] rounded-2xl flex flex-col overflow-hidden shadow-2xl transform transition-transform duration-300"
|
|
:class="{ 'scale-95': isAnimating, 'scale-100': !isAnimating }"
|
|
>
|
|
<!-- Header -->
|
|
<div class="h-16 border-b border-white/10 flex items-center justify-between px-6 bg-white/5">
|
|
<div class="flex items-center gap-3">
|
|
<i data-lucide="code-2" class="text-art-accent"></i>
|
|
<span class="font-bold text-white">{{ title }}</span>
|
|
</div>
|
|
<button @click="close" class="p-2 hover:bg-white/10 rounded-full text-white/50 hover:text-white transition-colors">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 flex flex-col md:flex-row h-full overflow-hidden">
|
|
<!-- Code Section -->
|
|
<div class="w-full md:w-1/2 bg-[#0d0d0d] overflow-auto p-6 border-r border-white/10 relative group">
|
|
<button class="absolute top-4 right-4 text-xs bg-white/10 px-2 py-1 rounded text-white/50 hover:text-white z-10" @click="copyCode">
|
|
复制
|
|
</button>
|
|
<pre class="font-mono text-sm leading-relaxed"><code ref="codeBlock" :class="languageClass">{{ code }}</code></pre>
|
|
</div>
|
|
|
|
<!-- Preview Section -->
|
|
<div class="w-full md:w-1/2 relative flex items-center justify-center overflow-hidden bg-[#1a1a1a]" :style="previewBackgroundStyle">
|
|
|
|
<!-- Mouse Preview -->
|
|
<div v-if="type === 'mouse'" class="flex flex-col items-center justify-center p-8 border border-white/20 rounded-xl bg-black/50 backdrop-blur text-white transition-all duration-75" :style="mouseBoxStyle">
|
|
<div class="text-4xl font-mono mb-2">{{ mouseX }}, {{ mouseY }}</div>
|
|
<div class="text-xs text-white/50">在此区域移动鼠标</div>
|
|
</div>
|
|
|
|
<!-- Glass Preview -->
|
|
<div v-else-if="type === 'glass'" class="relative w-64 h-64 flex items-center justify-center">
|
|
<div class="absolute top-0 left-0 w-32 h-32 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-float"></div>
|
|
<div class="absolute bottom-0 right-0 w-32 h-32 bg-yellow-500 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-float" style="animation-delay: 2s"></div>
|
|
<div class="glass-panel relative z-10 w-48 h-32 flex items-center justify-center text-white font-bold text-lg">
|
|
Glass Card
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Noise Preview -->
|
|
<div v-else-if="type === 'noise'" class="w-64 h-64 bg-[#222] rounded-xl relative overflow-hidden flex items-center justify-center border border-white/10">
|
|
<svg class="absolute inset-0 w-full h-full opacity-50">
|
|
<filter id="noiseFilter">
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
|
|
</filter>
|
|
<rect width="100%" height="100%" filter="url(#noiseFilter)" opacity="1"/>
|
|
</svg>
|
|
<div class="relative z-10 text-white font-bold text-2xl tracking-widest">NOISE</div>
|
|
</div>
|
|
|
|
<!-- Animation Preview -->
|
|
<div v-else-if="type === 'animate'" class="flex flex-col items-center gap-4">
|
|
<div class="w-16 h-16 bg-art-accent rounded-lg animate-float"></div>
|
|
<div class="text-white/50 font-mono">Floating Animation</div>
|
|
</div>
|
|
|
|
<div v-else class="text-white/30 font-mono">预览不可用</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch, computed, nextTick } from 'vue'
|
|
import hljs from 'highlight.js'
|
|
|
|
const props = defineProps<{
|
|
isOpen: boolean
|
|
title: string
|
|
code: string
|
|
type: string
|
|
}>()
|
|
|
|
const emit = defineEmits(['close'])
|
|
|
|
const isAnimating = ref(true)
|
|
const mouseX = ref(0)
|
|
const mouseY = ref(0)
|
|
const codeBlock = ref<HTMLElement | null>(null)
|
|
|
|
const languageClass = computed(() => {
|
|
const map: Record<string, string> = {
|
|
'mouse': 'javascript',
|
|
'glass': 'css',
|
|
'noise': 'xml',
|
|
'animate': 'css'
|
|
}
|
|
return `language-${map[props.type] || 'plaintext'}`
|
|
})
|
|
|
|
const highlightCode = () => {
|
|
if (codeBlock.value) {
|
|
delete codeBlock.value.dataset.highlighted
|
|
hljs.highlightElement(codeBlock.value)
|
|
}
|
|
}
|
|
|
|
// Watch isOpen to handle entry/exit animations
|
|
watch(() => props.isOpen, (newVal) => {
|
|
if (newVal) {
|
|
document.body.style.overflow = 'hidden'
|
|
// Entry animation
|
|
setTimeout(() => {
|
|
isAnimating.value = false
|
|
}, 10)
|
|
|
|
// Initialize icons if needed
|
|
setTimeout(() => {
|
|
if (window.lucide) window.lucide.createIcons()
|
|
}, 50)
|
|
|
|
// Highlight code
|
|
nextTick(() => {
|
|
highlightCode()
|
|
})
|
|
} else {
|
|
document.body.style.overflow = ''
|
|
isAnimating.value = true
|
|
}
|
|
})
|
|
|
|
const close = () => {
|
|
isAnimating.value = true
|
|
setTimeout(() => {
|
|
emit('close')
|
|
}, 300)
|
|
}
|
|
|
|
const copyCode = () => {
|
|
navigator.clipboard.writeText(props.code)
|
|
alert('代码已复制')
|
|
}
|
|
|
|
// Mouse movement logic for 'mouse' type
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (props.type !== 'mouse') return
|
|
|
|
// Find the preview container
|
|
const container = (e.target as HTMLElement).closest('.preview-area') || (e.target as HTMLElement)
|
|
if (!container) return
|
|
|
|
const rect = container.getBoundingClientRect()
|
|
mouseX.value = Math.floor(e.clientX - rect.left)
|
|
mouseY.value = Math.floor(e.clientY - rect.top)
|
|
}
|
|
|
|
// Global mouse listener when modal is open and type is mouse
|
|
watch(() => [props.isOpen, props.type], ([open, type]) => {
|
|
if (open && type === 'mouse') {
|
|
window.addEventListener('mousemove', handleMouseMove)
|
|
} else {
|
|
window.removeEventListener('mousemove', handleMouseMove)
|
|
}
|
|
})
|
|
|
|
const mouseBoxStyle = computed(() => {
|
|
// Parallax effect
|
|
const xOffset = (mouseX.value - 200) / 20 // Approx center offset
|
|
const yOffset = (mouseY.value - 200) / 20
|
|
return {
|
|
transform: `translate(${xOffset}px, ${yOffset}px)`
|
|
}
|
|
})
|
|
|
|
const previewBackgroundStyle = {
|
|
backgroundImage: `
|
|
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
|
|
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
|
|
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
|
|
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)`,
|
|
backgroundSize: '20px 20px',
|
|
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px'
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.glass-panel {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.animate-float {
|
|
animation: float 6s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translateY(0); }
|
|
50% { transform: translateY(-20px); }
|
|
}
|
|
</style> |