2
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
Reference in New Issue
Block a user