初始化
This commit is contained in:
15858
.gitignore
vendored
15858
.gitignore
vendored
File diff suppressed because it is too large
Load Diff
1
client/.gitgnore
Normal file
1
client/.gitgnore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
33
client/src/components/CodeBlock.vue
Normal file
33
client/src/components/CodeBlock.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<code ref="codeBlock" :class="languageClass">{{ code }}</code>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, nextTick } from 'vue'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
const props = defineProps<{
|
||||
code: string
|
||||
language: string
|
||||
}>()
|
||||
|
||||
const codeBlock = ref<HTMLElement | null>(null)
|
||||
|
||||
const languageClass = `language-${props.language || 'plaintext'}`
|
||||
|
||||
const highlight = () => {
|
||||
if (codeBlock.value) {
|
||||
// Reset internal state if needed, though highlighting overwrites innerHTML
|
||||
delete codeBlock.value.dataset.highlighted
|
||||
hljs.highlightElement(codeBlock.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
highlight()
|
||||
})
|
||||
|
||||
watch(() => props.code, () => {
|
||||
nextTick(highlight)
|
||||
})
|
||||
</script>
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-[100] bg-black/90 backdrop-blur-sm flex items-center justify-center p-4 md:p-10 transition-opacity duration-300"
|
||||
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="bg-[#0a0a0c] border border-white/10 w-full max-w-6xl h-full md:h-[80vh] rounded-2xl flex flex-col overflow-hidden shadow-2xl transform transition-transform duration-300"
|
||||
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 -->
|
||||
@@ -23,10 +24,10 @@
|
||||
<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" @click="copyCode">
|
||||
<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 text-gray-300">{{ code }}</pre>
|
||||
<pre class="font-mono text-sm leading-relaxed"><code ref="codeBlock" :class="languageClass">{{ code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section -->
|
||||
@@ -72,7 +73,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed } from 'vue'
|
||||
import { ref, watch, onMounted, computed, nextTick } from 'vue'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean
|
||||
@@ -87,6 +89,24 @@ const isAnimating = ref(true)
|
||||
const mouseX = ref(0)
|
||||
const mouseY = ref(0)
|
||||
const containerRect = ref<DOMRect | null>(null)
|
||||
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) => {
|
||||
@@ -101,6 +121,11 @@ watch(() => props.isOpen, (newVal) => {
|
||||
setTimeout(() => {
|
||||
if (window.lucide) window.lucide.createIcons()
|
||||
}, 50)
|
||||
|
||||
// Highlight code
|
||||
nextTick(() => {
|
||||
highlightCode()
|
||||
})
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
isAnimating.value = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from "vue"
|
||||
import App from "./App.vue"
|
||||
import "./style.css"
|
||||
import "highlight.js/styles/atom-one-dark.css"
|
||||
import router from "./router"
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -39,24 +39,39 @@
|
||||
<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-x-auto font-mono text-sm leading-relaxed pointer-events-none opacity-80 group-hover:opacity-100 transition-opacity">
|
||||
<pre class="text-gray-300">{{ snippet.code }}</pre>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Snippet Modal -->
|
||||
<SnippetModal
|
||||
:is-open="modalOpen"
|
||||
:title="currentSnippet.title"
|
||||
:code="currentSnippet.code"
|
||||
:type="currentSnippet.type"
|
||||
@close="closeSnippet"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeMount } from 'vue'
|
||||
import { ref, onMounted, onBeforeMount, nextTick } 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 hljs from 'highlight.js'
|
||||
|
||||
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 () => {
|
||||
@@ -94,6 +109,8 @@ export const useMousePosition = () => {
|
||||
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'
|
||||
@@ -102,7 +119,7 @@ export const useMousePosition = () => {
|
||||
id: 'noise',
|
||||
title: 'SVG 噪点纹理滤镜',
|
||||
code: `<filter id="noise">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.8" /
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.8" />
|
||||
</filter>`,
|
||||
type: 'noise'
|
||||
},
|
||||
@@ -127,24 +144,23 @@ export const useMousePosition = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getSnippetType = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
javascript: 'JavaScript',
|
||||
css: 'CSS',
|
||||
html: 'HTML',
|
||||
mouse: 'React Hook',
|
||||
glass: 'CSS',
|
||||
noise: 'SVG',
|
||||
animate: 'CSS Animation'
|
||||
const getSnippetLang = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'mouse': 'javascript',
|
||||
'glass': 'css',
|
||||
'noise': 'xml',
|
||||
'animate': 'css'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
return map[type] || 'plaintext'
|
||||
}
|
||||
|
||||
const openSnippet = (snippet: any) => {
|
||||
// 使用全局函数打开代码片段模态框
|
||||
if (window.showSnippet) {
|
||||
window.showSnippet(snippet.title, snippet.code)
|
||||
}
|
||||
const openSnippet = (snippet: Snippet) => {
|
||||
currentSnippet.value = snippet
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
const closeSnippet = () => {
|
||||
modalOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
.mac-code-block::before { content: ''; position: absolute; top: 1rem; left: 1.5rem; width: 0.75rem; height: 0.75rem; border-radius: 50%; background: #ff5f56; box-shadow: 1.25rem 0 0 #ffbd2e, 2.5rem 0 0 #27c93f; }
|
||||
.code-keyword { color: #c678dd; } .code-func { color: #61afef; } .code-string { color: #98c379; } .code-comment { color: #5c6370; font-style: italic; }
|
||||
|
||||
/* Highlight.js Override */
|
||||
.hljs { background: transparent !important; padding: 0 !important; }
|
||||
|
||||
/* Layout Specifics */
|
||||
.work-detail-page { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #050505; z-index: 100; overflow-y: auto; }
|
||||
.gallery-image { width: 100%; margin-bottom: 4rem; filter: grayscale(20%); transition: filter 0.5s; }
|
||||
|
||||
Reference in New Issue
Block a user