2026-01-15 13:51:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<section id="snippets" class="page-section block animate-slide-down">
|
|
|
|
|
|
<div class="max-w-7xl mx-auto pt-32 px-6 pb-20">
|
|
|
|
|
|
<div class="mb-12 text-center">
|
|
|
|
|
|
<h2 class="font-serif text-5xl italic text-white mb-6">代码实验室</h2>
|
|
|
|
|
|
<p class="text-art-muted">点击下方卡片查看代码与实时效果。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Loading state -->
|
|
|
|
|
|
<div v-if="loading" class="flex justify-center items-center h-64">
|
|
|
|
|
|
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Error state -->
|
|
|
|
|
|
<div v-else-if="error" class="text-center text-red-500 py-20">
|
|
|
|
|
|
<p class="mb-4">{{ error }}</p>
|
|
|
|
|
|
<button @click="fetchSnippetsData" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
|
|
|
|
|
|
重试
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Snippets list -->
|
|
|
|
|
|
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-8" id="snippets-grid">
|
|
|
|
|
|
<!-- Snippets will be rendered here -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="snippet in snippets"
|
|
|
|
|
|
:key="snippet.id"
|
2026-01-15 14:26:45 +08:00
|
|
|
|
class="art-card rounded-xl p-0 overflow-hidden bg-[#0d0d0d] border border-white/10 cursor-pointer group hover:border-art-accent/50 transition-colors"
|
2026-01-15 13:51:44 +08:00
|
|
|
|
@click="openSnippet(snippet)"
|
|
|
|
|
|
>
|
2026-01-15 14:26:45 +08:00
|
|
|
|
<!-- Mac Window Header -->
|
|
|
|
|
|
<div class="flex items-center justify-between px-4 py-3 bg-white/5 border-b border-white/5">
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<div class="w-3 h-3 rounded-full bg-[#ff5f56]"></div>
|
|
|
|
|
|
<div class="w-3 h-3 rounded-full bg-[#ffbd2e]"></div>
|
|
|
|
|
|
<div class="w-3 h-3 rounded-full bg-[#27c93f]"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="text-xs font-mono text-art-muted group-hover:text-white transition-colors">{{ snippet.title }}</span>
|
|
|
|
|
|
<div class="text-xs font-mono text-art-accent opacity-0 group-hover:opacity-100 transition-opacity">点击运行 -></div>
|
2026-01-15 13:51:44 +08:00
|
|
|
|
</div>
|
2026-01-15 14:26:45 +08:00
|
|
|
|
<!-- Code Preview (Styled) -->
|
2026-01-15 14:57:36 +08:00
|
|
|
|
<div class="p-6 overflow-hidden font-mono text-sm leading-relaxed pointer-events-none opacity-80 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
|
<pre class="whitespace-pre-wrap break-all line-clamp-4"><CodeBlock :code="snippet.code" :language="getSnippetLang(snippet.type)" /></pre>
|
2026-01-15 13:51:44 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-15 14:57:36 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- Snippet Modal -->
|
|
|
|
|
|
<SnippetModal
|
|
|
|
|
|
:is-open="modalOpen"
|
|
|
|
|
|
:title="currentSnippet.title"
|
|
|
|
|
|
:code="currentSnippet.code"
|
|
|
|
|
|
:type="currentSnippet.type"
|
|
|
|
|
|
@close="closeSnippet"
|
|
|
|
|
|
/>
|
2026-01-15 13:51:44 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-01-20 09:13:51 +08:00
|
|
|
|
import { ref, onMounted } from 'vue'
|
2026-01-15 13:51:44 +08:00
|
|
|
|
import { fetchSnippets, Snippet } from '../services/api'
|
|
|
|
|
|
import { useScrollAnimation } from '../composables/useScrollAnimation'
|
2026-01-15 14:57:36 +08:00
|
|
|
|
import SnippetModal from '../components/SnippetModal.vue'
|
|
|
|
|
|
import CodeBlock from '../components/CodeBlock.vue'
|
2026-01-15 13:51:44 +08:00
|
|
|
|
|
|
|
|
|
|
const snippets = ref<Snippet[]>([])
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const error = ref('')
|
|
|
|
|
|
|
2026-01-15 14:57:36 +08:00
|
|
|
|
const modalOpen = ref(false)
|
|
|
|
|
|
const currentSnippet = ref<Snippet>({ id: '', title: '', code: '', type: '' })
|
|
|
|
|
|
|
2026-01-15 14:24:30 +08:00
|
|
|
|
const { initObserver } = useScrollAnimation()
|
|
|
|
|
|
|
2026-01-15 13:51:44 +08:00
|
|
|
|
const fetchSnippetsData = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
try {
|
|
|
|
|
|
const snippetsData = await fetchSnippets()
|
|
|
|
|
|
snippets.value = snippetsData
|
2026-01-15 14:24:30 +08:00
|
|
|
|
// 初始化动画 - 在数据加载完成后
|
|
|
|
|
|
initObserver()
|
2026-01-15 13:51:44 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Error fetching snippets:', err)
|
|
|
|
|
|
error.value = '获取代码片段失败,请稍后重试'
|
|
|
|
|
|
// 如果API调用失败,使用静态数据作为备选
|
|
|
|
|
|
snippets.value = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'mouse',
|
|
|
|
|
|
title: 'React 鼠标追踪 Hook',
|
|
|
|
|
|
code: `import { useState, useEffect } from 'react';
|
|
|
|
|
|
|
|
|
|
|
|
export const useMousePosition = () => {
|
|
|
|
|
|
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const update = (e) => setPos({ x: e.clientX, y: e.clientY });
|
|
|
|
|
|
window.addEventListener('mousemove', update);
|
|
|
|
|
|
return () => window.removeEventListener('mousemove', update);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
return pos;
|
|
|
|
|
|
};`,
|
|
|
|
|
|
type: 'mouse'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'glass',
|
|
|
|
|
|
title: 'CSS 极致毛玻璃效果',
|
|
|
|
|
|
code: `.glass-panel {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
backdrop-filter: blur(16px);
|
2026-01-15 14:57:36 +08:00
|
|
|
|
-webkit-backdrop-filter: blur(16px);
|
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
2026-01-15 13:51:44 +08:00
|
|
|
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}`,
|
|
|
|
|
|
type: 'glass'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'noise',
|
|
|
|
|
|
title: 'SVG 噪点纹理滤镜',
|
|
|
|
|
|
code: `<filter id="noise">
|
2026-01-15 14:57:36 +08:00
|
|
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.8" />
|
2026-01-15 13:51:44 +08:00
|
|
|
|
</filter>`,
|
|
|
|
|
|
type: 'noise'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'animate',
|
|
|
|
|
|
title: 'CSS 流畅动画',
|
|
|
|
|
|
code: `.animate-float {
|
|
|
|
|
|
animation: float 6s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes float {
|
|
|
|
|
|
0%, 100% { transform: translateY(0); }
|
|
|
|
|
|
50% { transform: translateY(-20px); }
|
|
|
|
|
|
}`,
|
|
|
|
|
|
type: 'animate'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-01-15 14:24:30 +08:00
|
|
|
|
// 静态数据加载后也要初始化动画
|
|
|
|
|
|
initObserver()
|
2026-01-15 13:51:44 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 14:57:36 +08:00
|
|
|
|
const getSnippetLang = (type: string) => {
|
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
|
'mouse': 'javascript',
|
|
|
|
|
|
'glass': 'css',
|
|
|
|
|
|
'noise': 'xml',
|
|
|
|
|
|
'animate': 'css'
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
2026-01-15 14:57:36 +08:00
|
|
|
|
return map[type] || 'plaintext'
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 14:57:36 +08:00
|
|
|
|
const openSnippet = (snippet: Snippet) => {
|
|
|
|
|
|
currentSnippet.value = snippet
|
|
|
|
|
|
modalOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const closeSnippet = () => {
|
|
|
|
|
|
modalOpen.value = false
|
2026-01-15 13:51:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-01-15 14:24:30 +08:00
|
|
|
|
fetchSnippetsData()
|
2026-01-15 13:51:44 +08:00
|
|
|
|
// 初始化Lucide图标
|
|
|
|
|
|
if (window.lucide) {
|
|
|
|
|
|
window.lucide.createIcons()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|