This commit is contained in:
李琦
2026-01-15 14:26:45 +08:00
parent 18d3e993f9
commit 20e6e9fb3f
5 changed files with 194 additions and 98 deletions

View File

@@ -1,97 +1,182 @@
<template>
<div id="snippet-modal" class="fixed inset-0 z-[100] modal-overlay hidden flex items-center justify-center p-4 md:p-10 opacity-0 transition-opacity duration-300">
<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 scale-95 transition-transform duration-300" id="modal-content">
<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="{ 'opacity-0': isAnimating, 'opacity-100': !isAnimating }"
>
<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="{ '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" id="modal-title">{{ modalTitle }}</span></div>
<button @click="closeSnippet" 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 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>
<div class="flex-1 overflow-hidden">
<!-- 使用CodePreview组件 -->
<CodePreview
:initial-code="modalCode"
:initial-language="detectLanguage(modalCode)"
@code-change="handleCodeChange"
@language-change="handleLanguageChange"
/>
<!-- 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" @click="copyCode">
复制
</button>
<pre class="font-mono text-sm leading-relaxed text-gray-300">{{ 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>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CodePreview from './CodePreview.vue'
import { ref, watch, onMounted, computed } from 'vue'
const modalTitle = ref('Code')
const modalCode = ref('')
const props = defineProps<{
isOpen: boolean
title: string
code: string
type: string
}>()
// 检测代码语言
const detectLanguage = (code: string): string => {
// 简单的语言检测逻辑,可以根据需要扩展
if (code.includes('<html') || code.includes('<div') || code.includes('<body')) {
return 'html'
} else if (code.includes('import React') || code.includes('React.')) {
return 'react'
} else if (code.includes('<template') || code.includes('export default')) {
return 'vue'
} else if (code.includes('class ') && code.includes('{')) {
return 'typescript'
} else if (code.includes('function ') || code.includes('const ') || code.includes('let ') || code.includes('var ')) {
return 'javascript'
} else if (code.includes('@import') || code.includes('{') && code.includes(':') && code.includes(';')) {
return 'css'
} else if (code.includes('print(') || code.includes('def ')) {
return 'python'
} else {
return 'html'
}
}
const emit = defineEmits(['close'])
const showSnippet = (title: string, code: string) => {
modalTitle.value = title
modalCode.value = code
const modal = document.getElementById('snippet-modal')
if (modal) {
modal.classList.remove('hidden')
modal.classList.add('flex')
setTimeout(() => {
modal.style.opacity = '1'
const content = document.getElementById('modal-content')
if (content) {
content.style.transform = 'scale(1)'
}
}, 10)
const isAnimating = ref(true)
const mouseX = ref(0)
const mouseY = ref(0)
const containerRect = ref<DOMRect | null>(null)
// Watch isOpen to handle entry/exit animations
watch(() => props.isOpen, (newVal) => {
if (newVal) {
document.body.style.overflow = 'hidden'
}
}
const closeSnippet = () => {
const modal = document.getElementById('snippet-modal')
if (modal) {
modal.style.opacity = '0'
const content = document.getElementById('modal-content')
if (content) {
content.style.transform = 'scale(0.95)'
}
// Entry animation
setTimeout(() => {
modal.classList.remove('flex')
modal.classList.add('hidden')
document.body.style.overflow = ''
}, 300)
isAnimating.value = false
}, 10)
// Initialize icons if needed
setTimeout(() => {
if (window.lucide) window.lucide.createIcons()
}, 50)
} else {
document.body.style.overflow = ''
isAnimating.value = true
}
})
const close = () => {
isAnimating.value = true
setTimeout(() => {
emit('close')
}, 300)
}
const handleCodeChange = (newCode: string) => {
modalCode.value = newCode
const copyCode = () => {
navigator.clipboard.writeText(props.code)
alert('代码已复制')
}
const handleLanguageChange = (newLanguage: string) => {
// 可以在这里处理语言变化事件
console.log('Language changed to:', newLanguage)
// 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)
}
// 暴露方法给全局使用
window.showSnippet = showSnippet
window.closeSnippet = closeSnippet
</script>
// 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>

View File

@@ -22,22 +22,28 @@
<!-- Blog posts list -->
<div v-else class="space-y-12" id="blog-list-container">
<!-- Blog posts will be rendered here -->
<div
<article
v-for="post in blogPosts"
:key="post.id"
class="art-card rounded-2xl p-8 group cursor-pointer transition-all"
class="group cursor-pointer border-b border-white/5 pb-12"
@click="$router.push('/blog/' + post.id)"
>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
<div class="flex items-center gap-3">
<span class="px-2 py-1 text-[10px] font-mono border border-white/20 rounded-full text-white backdrop-blur-sm">{{ post.category }}</span>
<span class="text-xs font-mono text-art-muted">{{ post.date }}</span>
<div class="flex flex-col md:flex-row gap-8 items-start">
<div class="md:w-1/4 pt-2">
<span class="font-mono text-xs text-art-accent block mb-1">{{ post.category }}</span>
<span class="text-sm text-art-muted">{{ post.date }}</span>
</div>
<div class="md:w-3/4 space-y-4">
<h3 class="font-serif text-3xl text-white group-hover:text-art-accent transition-colors leading-tight">
{{ post.title }}
</h3>
<p class="text-art-muted font-light leading-relaxed">
{{ post.excerpt }}
</p>
<div class="text-xs text-art-accent font-medium mt-4 group-hover:text-white transition-colors">阅读全文 -></div>
</div>
<i data-lucide="arrow-up-right" class="w-5 h-5 text-white/50 transform group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform"></i>
</div>
<h3 class="text-2xl md:text-3xl font-serif text-white mb-4 group-hover:text-art-accent transition-colors">{{ post.title }}</h3>
<p class="text-art-muted leading-relaxed">{{ post.excerpt }}</p>
</div>
</article>
</div>
</div>
</section>

View File

@@ -25,17 +25,22 @@
<div
v-for="snippet in snippets"
:key="snippet.id"
class="art-card rounded-2xl p-8 group cursor-pointer transition-all hover:translate-y-[-5px]"
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)"
>
<div class="flex items-center gap-3 mb-4">
<i data-lucide="code-2" class="w-6 h-6 text-art-accent"></i>
<span class="font-mono text-sm text-art-muted">{{ getSnippetType(snippet.type) }}</span>
<!-- 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>
<h3 class="text-2xl font-serif text-white mb-4 group-hover:text-art-accent transition-colors">{{ snippet.title }}</h3>
<pre class="font-mono text-sm text-art-muted leading-relaxed overflow-hidden text-ellipsis line-clamp-4">{{ snippet.code }}</pre>
<div class="flex items-center justify-end mt-6">
<i data-lucide="arrow-up-right" class="w-5 h-5 text-white/50 transform group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform"></i>
<!-- 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>
</div>
</div>

View File

@@ -45,7 +45,7 @@
</div>
<!-- 作品信息 -->
<div
:class="['md:order-2', index % 2 === 1 ? 'md:order-1' : 'md:order-2']"
:class="['md:order-2', index % 2 === 1 ? 'md:order-1 md:text-right' : 'md:order-2']"
class="space-y-6"
>
<!-- 年份和分类 -->
@@ -59,7 +59,7 @@
<!-- 作品描述 -->
<p class="text-art-muted font-light leading-relaxed" v-html="work.desc"></p>
<!-- 技术栈 -->
<div class="flex flex-wrap gap-3">
<div class="flex flex-wrap gap-3" :class="[index % 2 === 1 ? 'md:justify-end' : '']">
<span
v-for="(stack, stackIndex) in work.techStack.flatMap(tech => tech.items)"
:key="stackIndex"
@@ -69,7 +69,7 @@
</span>
</div>
<!-- 查看详情按钮 -->
<div class="pt-4">
<div class="pt-4" :class="[index % 2 === 1 ? 'flex md:justify-end' : '']">
<span class="inline-flex items-center gap-2 text-sm font-medium text-white border-b border-transparent hover:border-art-accent hover:text-art-accent transition-all">
查看项目详情 <i data-lucide="arrow-right" class="w-4 h-4"></i>
</span>

View File

@@ -305,7 +305,7 @@ CREATE TABLE `operation_logs` (
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role_id`, `role`, `is_active`, `created_at`, `updated_at`) VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$J9q3NfJ2X8H7Q5z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q', 1, 'admin', 1, '2026-01-13 16:10:14', '2026-01-14 12:57:55');
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role_id`, `role`, `is_active`, `created_at`, `updated_at`) VALUES (1, 'lq', 'liqiworker@gmail.com', '$2a$10$yl.1yCZ2PSTsW14byyDQ3uekuEiqnC4VBr/TzR6OvMqFVRuiiHu5e', 1, 'admin', 1, '2026-01-13 16:10:14', '2026-01-14 12:57:55');
INSERT INTO `users` (`id`, `username`, `email`, `password_hash`, `role_id`, `role`, `is_active`, `created_at`, `updated_at`) VALUES (2, 'editor', 'editor@example.com', '$2a$10$J9q3NfJ2X8H7Q5z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q7z5Q', 2, 'editor', 1, '2026-01-13 16:10:14', '2026-01-13 16:10:14');
-- ----------------------------