169 lines
5.6 KiB
Vue
169 lines
5.6 KiB
Vue
<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"
|
||
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"
|
||
@click="openSnippet(snippet)"
|
||
>
|
||
<!-- 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>
|
||
</div>
|
||
<!-- Code Preview (Styled) -->
|
||
<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="snippetLang(snippet)" /></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Snippet Modal -->
|
||
<SnippetModal
|
||
:is-open="modalOpen"
|
||
:title="currentSnippet.title"
|
||
:code="currentSnippet.code"
|
||
:type="currentSnippet.type"
|
||
:code-type="currentSnippet.codeType"
|
||
:description="currentSnippet.description"
|
||
@close="closeSnippet"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { fetchSnippets, Snippet } from '../services/api'
|
||
import { useScrollAnimation } from '../composables/useScrollAnimation'
|
||
import SnippetModal from '../components/SnippetModal.vue'
|
||
import CodeBlock from '../components/CodeBlock.vue'
|
||
import { resolveHighlightLanguage } from '../utils/codeHighlight'
|
||
|
||
const snippets = ref<Snippet[]>([])
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
|
||
const modalOpen = ref(false)
|
||
const currentSnippet = ref<Snippet>({ id: '', title: '', code: '', type: '' })
|
||
|
||
const { initObserver } = useScrollAnimation()
|
||
|
||
const fetchSnippetsData = async () => {
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
const snippetsData = await fetchSnippets()
|
||
snippets.value = snippetsData
|
||
// 初始化动画 - 在数据加载完成后
|
||
initObserver()
|
||
} 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);
|
||
-webkit-backdrop-filter: blur(16px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||
}`,
|
||
type: 'glass'
|
||
},
|
||
{
|
||
id: 'noise',
|
||
title: 'SVG 噪点纹理滤镜',
|
||
code: `<filter id="noise">
|
||
<feTurbulence type="fractalNoise" baseFrequency="0.8" />
|
||
</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'
|
||
}
|
||
]
|
||
// 静态数据加载后也要初始化动画
|
||
initObserver()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const snippetLang = (snippet: Snippet) => {
|
||
return resolveHighlightLanguage(snippet.codeType?.name || snippet.type)
|
||
}
|
||
|
||
const openSnippet = (snippet: Snippet) => {
|
||
currentSnippet.value = snippet
|
||
modalOpen.value = true
|
||
}
|
||
|
||
const closeSnippet = () => {
|
||
modalOpen.value = false
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchSnippetsData()
|
||
// 初始化Lucide图标
|
||
if (window.lucide) {
|
||
window.lucide.createIcons()
|
||
}
|
||
})
|
||
</script> |