Files
nl-blogs/client/src/pages/Snippets.vue
2026-06-24 17:06:22 +08:00

169 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>