初始化

This commit is contained in:
李琦
2026-01-15 13:51:44 +08:00
commit b7b6d3e39e
156 changed files with 38913 additions and 0 deletions

View File

@@ -0,0 +1,500 @@
<template>
<div class="code-preview-wrapper relative w-full h-full min-h-[600px] flex flex-col font-sans text-gray-200">
<!-- 背景噪点层 -->
<div class="absolute inset-0 pointer-events-none z-0 opacity-20 mix-blend-overlay bg-noise"></div>
<!-- 顶部工具栏 -->
<div class="relative z-10 flex items-center justify-between px-6 py-4 bg-black/40 backdrop-blur-xl border-b border-white/5 rounded-t-2xl">
<div class="flex items-center gap-4">
<!-- 装饰性 Mac 窗口按钮 -->
<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-white/40 tracking-widest uppercase ml-2">Code Playground</span>
<!-- 运行状态指示器 -->
<div v-if="isRunning" class="flex items-center gap-2 px-2 py-0.5 rounded-full bg-yellow-500/10 border border-yellow-500/20">
<span class="block w-1.5 h-1.5 rounded-full bg-yellow-500 animate-pulse"></span>
<span class="text-[10px] font-mono text-yellow-500">Compiling...</span>
</div>
</div>
<!-- 语言选择器 & 操作区 -->
<div class="flex items-center gap-4">
<CustomSelect
v-model="selectedLanguage"
:options="languageOptions.filter(opt => !opt.disabled)"
@update:modelValue="handleLanguageChange"
placeholder="选择语言"
style="width: 180px; font-family: monospace; font-size: 0.75rem;"
/>
<button
@click="runCode"
class="flex items-center gap-2 px-3 py-1.5 bg-[#d4b383] hover:bg-[#c4a373] text-black text-xs font-bold rounded-lg transition-colors"
title="运行代码 (Ctrl + Enter)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M5 3l14 9-14 9V3z"/></svg>
<span>RUN</span>
</button>
</div>
</div>
<!-- 主体内容区 -->
<div class="relative z-10 flex-1 flex flex-col md:flex-row bg-[#050505]/80 backdrop-blur-sm rounded-b-2xl overflow-hidden border border-t-0 border-white/5">
<!-- 左侧代码编辑器 -->
<div class="w-full md:w-1/2 relative flex flex-col border-b md:border-b-0 md:border-r border-white/5 group">
<div class="absolute inset-0 bg-[#09090b]">
<!-- 编辑器容器 -->
<div class="relative w-full h-full font-mono text-sm leading-relaxed custom-scrollbar overflow-hidden">
<pre
ref="highlightBlock"
class="absolute inset-0 p-6 m-0 pointer-events-none z-10 overflow-hidden whitespace-pre-wrap break-words"
aria-hidden="true"
><code :class="`language-${selectedLanguage} !bg-transparent !p-0 font-mono`" v-html="highlightedCode"></code></pre>
<textarea
ref="textarea"
v-model="code"
@input="handleCodeChange"
@scroll="syncScroll"
@keydown.ctrl.enter.prevent="runCode"
@keydown.meta.enter.prevent="runCode"
class="absolute inset-0 w-full h-full p-6 m-0 bg-transparent text-transparent caret-[#d4b383] z-20 resize-none border-none outline-none font-mono whitespace-pre-wrap break-words"
spellcheck="false"
placeholder="// Type your code here..."
></textarea>
</div>
</div>
</div>
<!-- 右侧实时预览 / 终端 -->
<div class="w-full md:w-1/2 bg-[#121214] flex flex-col relative">
<div class="h-8 flex items-center justify-between px-4 bg-white/5 border-b border-white/5 shrink-0">
<span class="text-[10px] font-mono text-[#d4b383] tracking-widest uppercase">
{{ isConsoleMode ? 'TERMINAL OUTPUT' : 'WEB PREVIEW' }}
</span>
<div class="flex items-center gap-2">
<button @click="clearConsole" v-if="isConsoleMode" class="text-[10px] text-white/40 hover:text-white mr-2">CLEAR</button>
<span v-if="error" class="flex items-center gap-1 text-[#ef4444] text-[10px]">
<span class="w-1.5 h-1.5 rounded-full bg-[#ef4444]"></span> ERROR
</span>
<span v-else class="flex items-center gap-1 text-green-500 text-[10px]">
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> READY
</span>
</div>
</div>
<!-- Iframe 容器 -->
<div class="flex-1 relative bg-white w-full h-full">
<iframe
ref="previewFrame"
sandbox="allow-scripts allow-modals allow-same-origin"
title="Code Preview"
class="w-full h-full border-none"
:class="{ 'bg-[#1e1e1e]': isConsoleMode }"
></iframe>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
import CustomSelect from './CustomSelect.vue'
// Props & Emits
const props = defineProps<{
initialCode?: string
initialLanguage?: string
}>()
// State
const code = ref('')
const selectedLanguage = ref(props.initialLanguage || 'html')
const error = ref('')
const isRunning = ref(false)
const previewFrame = ref<HTMLIFrameElement | null>(null)
const textarea = ref<HTMLTextAreaElement | null>(null)
const highlightBlock = ref<HTMLElement | null>(null)
const highlightedCode = ref('')
// Language options for CustomSelect
const languageOptions = [
{ value: 'html', label: 'HTML5' },
{ value: 'css', label: 'CSS3' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'vue', label: 'Vue 3 (SFC-ish)' },
{ value: 'react', label: 'React (JSX)' },
{ value: 'python', label: 'Python (Pyodide)' },
{ value: 'go', label: 'Go (Backend)' },
{ value: 'php', label: 'PHP (Backend)' },
{ value: 'node', label: 'Node.js (Backend)' }
]
// Default Templates
const templates: Record<string, string> = {
html: `<h1>Hello HTML</h1>\n<p>Edit me!</p>`,
css: `.box { \n width: 100px; \n height: 100px; \n background: gold; \n}`,
javascript: `console.log("Hello JS");\nconst x = 10;\nconsole.log(x * 2);`,
vue: `<div id="app">\n <h1>{{ message }}</h1>\n <button @click="count++">Count: {{ count }}</button>\n</div>\n\n<script type="module">\n import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n data() {\n return {\n message: 'Hello Vue 3!',\n count: 0\n }\n }\n }).mount('#app')\n<\/script>`,
react: `// React Component\nfunction App() {\n const [count, setCount] = React.useState(0);\n return (\n <div style={{padding: 20, textAlign: 'center'}}>\n <h1>Hello React</h1>\n <p>Count: {count}</p>\n <button \n onClick={() => setCount(count + 1)}\n style={{padding: '8px 16px', background: '#61dafb', border: 'none', borderRadius: 4}}\n >\n Click Me\n </button>\n </div>\n );\n}\n\n// Render\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(<App />);`,
python: `# Python via Pyodide (Wasm)\nimport sys\n\nprint(f"Hello from Python {sys.version.split()[0]}")\n\ndef fib(n):\n if n <= 1: return n\n return fib(n-1) + fib(n-2)\n\nprint(f"Fib(10) = {fib(10)}")`,
go: `package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello from Go!")\n}`,
php: `<?php\n\necho "Hello from PHP " . phpversion();\n`,
node: `console.log("Hello from Node.js " + process.version);`
}
// Computed
const isConsoleMode = computed(() => {
return ['javascript', 'python', 'go', 'php', 'node'].includes(selectedLanguage.value)
})
// Initialize Code Logic - 修复:优先使用传入的代码
if (props.initialCode) {
code.value = props.initialCode
} else {
code.value = templates[selectedLanguage.value] || ''
}
// Watchers - 修复:监听 props 变化以支持从数据库动态加载
watch(() => props.initialCode, (newVal) => {
if (newVal) {
code.value = newVal
highlightCode()
// 延迟运行,给 DOM 一点时间
setTimeout(() => runCode(), 100)
}
})
watch(() => props.initialLanguage, (newVal) => {
if (newVal) {
selectedLanguage.value = newVal
}
})
// Syntax Highlight
const highlightCode = () => {
if (!code.value) {
highlightedCode.value = ''
return
}
try {
const lang = selectedLanguage.value === 'vue' || selectedLanguage.value === 'react' ? 'javascript' : selectedLanguage.value
const result = hljs.highlight(code.value, { language: lang })
highlightedCode.value = result.value
} catch (err) {
highlightedCode.value = code.value
}
}
const syncScroll = (e: Event) => {
const target = e.target as HTMLTextAreaElement
if (highlightBlock.value) {
highlightBlock.value.scrollTop = target.scrollTop
highlightBlock.value.scrollLeft = target.scrollLeft
}
}
// --- Execution Engine ---
const runCode = async () => {
if (!previewFrame.value) return
isRunning.value = true
error.value = ''
const iframe = previewFrame.value
const doc = iframe.contentDocument || iframe.contentWindow?.document
if (!doc) return
// 重置
doc.open()
// 1. HTML / CSS
if (selectedLanguage.value === 'html') {
doc.write(code.value)
doc.close()
isRunning.value = false
}
else if (selectedLanguage.value === 'css') {
doc.write(`<html><head><style>${code.value}</style></head><body><div class="box">CSS Demo</div></body></html>`)
doc.close()
isRunning.value = false
}
// 2. JavaScript / Console
else if (selectedLanguage.value === 'javascript') {
const consoleTemplate = getConsoleTemplate(code.value)
doc.write(consoleTemplate)
doc.close()
isRunning.value = false
}
// 3. Vue 3 (Global Build)
else if (selectedLanguage.value === 'vue') {
const vueTemplate = `
<!DOCTYPE html>
<html>
<head>
<style>body { font-family: sans-serif; background: #fff; color: #333; padding: 20px; }</style>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"><\/script>
</head>
<body>
${code.value}
</body>
</html>
`
doc.write(vueTemplate)
doc.close()
isRunning.value = false
}
// 4. React (Babel Standalone)
else if (selectedLanguage.value === 'react') {
const reactTemplate = `
<!DOCTYPE html>
<html>
<head>
<style>body { font-family: sans-serif; background: #fff; color: #333; margin: 0; }</style>
<script src="https://unpkg.com/react@18/umd/react.development.js"><\/script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"><\/script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"><\/script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
${code.value}
<\/script>
</body>
</html>
`
doc.write(reactTemplate)
doc.close()
isRunning.value = false
}
// 5. Python (Pyodide Wasm)
else if (selectedLanguage.value === 'python') {
// Python 加载比较慢,显示 Loading
const pythonTemplate = `
<!DOCTYPE html>
<html>
<head>
<style>
body { background: #1e1e1e; color: #fff; font-family: monospace; padding: 20px; font-size: 14px; }
.log { border-bottom: 1px solid #333; padding: 4px 0; white-space: pre-wrap; }
.error { color: #ff6b6b; }
.loading { color: #d4b383; }
</style>
<script src="https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js"><\/script>
</head>
<body>
<div id="output">
<div class="loading">Initializing Python Environment (Pyodide)... This may take a moment.</div>
</div>
<script>
const output = document.getElementById('output');
function print(text, type = '') {
const div = document.createElement('div');
div.className = 'log ' + type;
div.textContent = text;
output.appendChild(div);
}
async function main() {
try {
if (!window.pyodide) {
window.pyodide = await loadPyodide();
// 清除 Loading 文字
output.innerHTML = '';
print("Python 3.11 Ready.", "loading");
print("------------------");
}
// 重定向 stdout
pyodide.setStdout({ batched: (msg) => print(msg) });
// 运行用户代码
await pyodide.runPythonAsync(\`${code.value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$')}\`);
} catch (err) {
print(err, 'error');
}
}
main();
<\/script>
</body>
</html>
`
doc.write(pythonTemplate)
doc.close()
isRunning.value = false
}
// 6. Backend Languages (Go, PHP, Node.js)
else if (['go', 'php', 'node'].includes(selectedLanguage.value)) {
// 显示 Loading 状态
const loadingTemplate = `
<!DOCTYPE html>
<html>
<body style="background:#1e1e1e;padding:20px;font-family:'JetBrains Mono', monospace;font-size:13px;color:#d4d4d8;margin:0;">
<div style="color: #d4b383;">Running code on server...</div>
</body>
</html>
`
doc.write(loadingTemplate)
try {
const response = await fetch('http://localhost:8081/api/run', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
language: selectedLanguage.value,
code: code.value
})
})
const result = await response.json()
let outputHtml = ''
if (result.error) {
outputHtml = `<div style="color: #ef4444; white-space: pre-wrap;">Error:\n${result.error}</div>`
} else {
outputHtml = `<div style="white-space: pre-wrap;">${result.output}</div>`
if (result.exitCode !== 0) {
outputHtml += `<div style="color: #ef4444; margin-top: 10px; border-top: 1px solid #333; padding-top: 5px;">Process exited with code ${result.exitCode}</div>`
}
}
const resultTemplate = `
<!DOCTYPE html>
<html>
<body style="background:#1e1e1e;padding:20px;font-family:'JetBrains Mono', monospace;font-size:13px;color:#d4d4d8;margin:0;">
${outputHtml}
<div style="margin-top: 20px; font-size: 11px; color: #666;">Execution time: ${result.duration}ms</div>
</body>
</html>
`
// 重新写入结果
doc.open()
doc.write(resultTemplate)
doc.close()
} catch (err) {
const errorTemplate = `
<!DOCTYPE html>
<html>
<body style="background:#1e1e1e;padding:20px;font-family:'JetBrains Mono', monospace;font-size:13px;color:#ef4444;margin:0;">
<div>Failed to connect to server. Ensure backend is running at http://localhost:8081</div>
<div style="margin-top:10px;">${err}</div>
</body>
</html>
`
doc.open()
doc.write(errorTemplate)
doc.close()
} finally {
isRunning.value = false
}
}
}
// Helper: Custom Console for JS
const getConsoleTemplate = (jsCode: string) => {
return `
<!DOCTYPE html>
<html>
<body style="background:#1e1e1e;padding:20px;font-family:'JetBrains Mono', monospace;font-size:13px;color:#d4d4d8;margin:0;">
<div id="console"></div>
<script type="module">
const logDiv = document.getElementById('console');
const originalLog = console.log;
// 劫持 console.log
console.log = function(...args) {
const line = document.createElement('div');
line.style.borderBottom = '1px solid #333';
line.style.padding = '6px 0';
// 简单的对象转字符串
line.textContent = '> ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
logDiv.appendChild(line);
originalLog.apply(console, args);
};
window.onerror = function(msg, url, line) {
const err = document.createElement('div');
err.style.color = '#ef4444';
err.style.marginTop = '8px';
err.textContent = 'Error: ' + msg;
logDiv.appendChild(err);
};
try {
${jsCode}
} catch(e) { console.error(e); }
<\/script>
</body>
</html>
`
}
const handleCodeChange = () => {
highlightCode()
// 对于非 Web 语言(如 Python/React不自动运行等待用户点击 Run
if (['html', 'css', 'javascript'].includes(selectedLanguage.value)) {
// Debounce auto-run for lightweight languages
clearTimeout(window.runTimer)
window.runTimer = setTimeout(() => runCode(), 1000)
}
}
const handleLanguageChange = () => {
code.value = templates[selectedLanguage.value] || ''
highlightCode()
runCode()
}
const clearConsole = () => {
if (previewFrame.value) {
const doc = previewFrame.value.contentDocument
if (doc) doc.body.innerHTML = '<div id="console"></div>' // 简单清空
}
}
onMounted(() => {
highlightCode()
// Initial Run
setTimeout(() => runCode(), 500)
})
</script>
<style scoped>
.bg-noise {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.07'/%3E%3C/svg%3E");
}
pre, textarea, code {
font-family: 'JetBrains Mono', 'Fira Code', monospace !important;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #d4b383;
}
</style>

View File

@@ -0,0 +1,188 @@
<template>
<div class="custom-select-container relative group" @click="toggleDropdown" ref="containerRef">
<!-- 选中值显示区域 -->
<div class="selected-value flex items-center" :class="{ 'placeholder': !selectedOption }">
<span class="flex-1 truncate">{{ selectedOption ? selectedOption.label : placeholder }}</span>
</div>
<!-- 下拉箭头 -->
<div class="dropdown-arrow ml-2" :class="{ 'open': isDropdownOpen }">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<!-- 下拉选项列表 -->
<div v-if="isDropdownOpen" class="dropdown-options absolute top-full left-0 right-0 mt-1 z-[9999]" ref="dropdownRef">
<div
v-for="option in options"
:key="option.value"
class="option-item"
:class="{ 'selected': option.value === modelValue }"
@click.stop="selectOption(option)"
>
{{ option.label }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
// Props 定义
interface Option {
value: string | number
label: string
}
const props = defineProps<{
modelValue: string | number
options: Option[]
placeholder?: string
}>()
// Emits 定义
const emit = defineEmits<{
'update:modelValue': [value: string | number]
}>()
// 组件状态
const isDropdownOpen = ref(false)
const containerRef = ref<HTMLDivElement | null>(null)
const dropdownRef = ref<HTMLDivElement | null>(null)
// 计算属性:当前选中的选项
const selectedOption = computed(() => {
return props.options.find(option => option.value === props.modelValue)
})
// 切换下拉列表显示/隐藏
const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value
}
// 选择选项
const selectOption = (option: Option) => {
emit('update:modelValue', option.value)
isDropdownOpen.value = false
}
// 点击外部关闭下拉列表
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
isDropdownOpen.value = false
}
}
// 监听modelValue变化更新选中状态
watch(() => props.modelValue, () => {
// 当外部更新modelValue时不需要额外操作selectedOption会自动更新
})
// 生命周期钩子
onMounted(() => {
// 添加点击外部关闭事件监听
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
// 移除事件监听
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.custom-select-container {
width: 100%;
display: flex;
align-items: center;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
color: #ececec;
font-family: 'Inter', sans-serif;
font-size: 0.95rem;
}
.custom-select-container:hover {
border-color: #d4b383;
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.2);
}
.selected-value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.selected-value.placeholder {
color: #888888;
}
.dropdown-arrow {
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
color: #888888;
}
.dropdown-arrow.open {
transform: rotate(180deg);
}
.dropdown-options {
z-index: 99999999999999999999;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-height: 200px;
overflow-y: auto;
}
.option-item {
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.option-item:hover {
background: rgba(212, 179, 131, 0.1);
color: #d4b383;
}
.option-item.selected {
background: rgba(212, 179, 131, 0.2);
color: #d4b383;
font-weight: 500;
}
/* 滚动条样式 */
.dropdown-options::-webkit-scrollbar {
width: 6px;
}
.dropdown-options::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.dropdown-options::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.dropdown-options::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>

View File

@@ -0,0 +1,12 @@
<template>
<footer class="py-8 text-center border-t border-white/5 relative z-10">
<div class="flex justify-center items-center gap-4 mb-4">
<a @click="window.open('admin.html', '_blank')" class="text-xs text-art-muted/30 hover:text-art-accent cursor-pointer">管理入口</a>
</div>
<p class="text-art-muted text-xs font-mono tracking-widest opacity-50">&copy; 2024 年糕崽崽. 保留所有权利.</p>
</footer>
</template>
<script setup lang="ts">
// Footer组件的逻辑可以在这里添加
</script>

View File

@@ -0,0 +1,122 @@
<template>
<header class="fixed top-0 w-full z-50 glass-nav transition-all duration-300" id="main-header">
<div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<div class="cursor-pointer group" @click="goTo('/')">
<span class="font-serif text-2xl italic tracking-wider text-white group-hover:text-art-accent transition-colors">年糕崽崽.Dev</span>
</div>
<nav class="hidden md:flex items-center gap-10">
<button
@click="goTo('/')"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
:class="{ 'text-white after:w-full': activeNav === 'home' }"
data-target="home"
>
首页
</button>
<button
@click="goTo('/blog')"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
:class="{ 'text-white after:w-full': activeNav === 'blog' }"
data-target="blog"
>
思考
</button>
<button
@click="goTo('/works')"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
:class="{ 'text-white after:w-full': activeNav === 'works' }"
data-target="works"
>
作品
</button>
<button
@click="goTo('/snippets')"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
:class="{ 'text-white after:w-full': activeNav === 'snippets' }"
data-target="snippets"
>
代码
</button>
<button
@click="goTo('/about')"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-white transition-colors link-underline"
:class="{ 'text-white after:w-full': activeNav === 'about' }"
data-target="about"
>
关于
</button>
</nav>
<div class="hidden md:flex items-center gap-4">
<a href="#" class="text-art-muted hover:text-white transition-colors"><i data-lucide="github" class="w-5 h-5"></i></a>
<button @click="goTo('/services')" class="px-5 py-2 text-xs font-bold tracking-widest uppercase border border-white/20 hover:border-art-accent hover:text-art-accent transition-all rounded-full">
合作
</button>
</div>
<button class="md:hidden text-white" @click="toggleMobileMenu">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
</div>
</header>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const activeNav = ref('home')
const toggleMobileMenu = () => {
const menu = document.getElementById('mobile-menu')
if (menu) {
if (menu.classList.contains('menu-closed')) {
menu.classList.remove('menu-closed')
menu.classList.add('menu-open')
document.body.style.overflow = 'hidden'
} else {
menu.classList.remove('menu-open')
menu.classList.add('menu-closed')
document.body.style.overflow = ''
}
}
}
const navigate = (target: string) => {
router.push(target)
toggleMobileMenu()
}
const updateActiveNav = () => {
const path = route.path
if (path === '/') activeNav.value = 'home'
else if (path === '/blog') activeNav.value = 'blog'
else if (path === '/works' || path.startsWith('/works/')) activeNav.value = 'works'
else if (path === '/snippets') activeNav.value = 'snippets'
else if (path === '/services') activeNav.value = 'services'
else if (path === '/about') activeNav.value = 'about'
}
onMounted(() => {
updateActiveNav()
// 初始化Lucide图标
if (window.lucide) {
window.lucide.createIcons()
}
})
watch(
() => route.path,
() => {
updateActiveNav()
}
)
const goTo = (target: string) => {
router.push(target)
}
</script>
<style scoped>
/* 组件特定样式可以在这里添加 */
</style>

View File

@@ -0,0 +1,40 @@
<template>
<i
:data-lucide="name"
:class="className"
:style="style"
ref="iconRef"
></i>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
interface Props {
name: string
className?: string
style?: Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
className: '',
style: () => ({})
})
const iconRef = ref<HTMLElement | null>(null)
const updateIcon = () => {
if (window.lucide && iconRef.value) {
// 重新创建图标
window.lucide.createIcons()
}
}
onMounted(() => {
updateIcon()
})
watch(() => props.name, () => {
updateIcon()
})
</script>

View File

@@ -0,0 +1,194 @@
<template>
<div id="inquiry-modal" class="fixed inset-0 z-[100] bg-[#050505]/95 backdrop-blur-xl hidden items-center justify-center p-4 transition-opacity duration-300">
<div class="relative w-full max-w-4xl bg-[#080808] border border-white/10 overflow-hidden flex flex-col md:flex-row shadow-2xl">
<div class="hidden md:flex w-1/3 bg-[#0c0c0c] border-r border-white/5 p-12 flex-col justify-between relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-full bg-noise opacity-10"></div>
<div class="absolute -top-20 -left-20 w-64 h-64 bg-art-accent/10 rounded-full blur-[80px]"></div>
<div class="relative z-10">
<h3 class="font-serif text-3xl text-white italic mb-6 leading-tight">
Let's build<br>something<br>unreal.
</h3>
<p class="font-mono text-xs text-white/40 leading-relaxed">
告诉我你的构想。<br>
我将提供架构与代码,<br>
将想象变为现实。
</p>
</div>
<div class="relative z-10 font-mono text-[10px] text-white/20 tracking-widest">
LAT: 30.2741° N<br>
LONG: 120.1551° E<br>
HANGZHOU, CN
</div>
</div>
<div class="w-full md:w-2/3 p-10 md:p-16 relative bg-[#080808]">
<button @click="closeModal" class="absolute top-6 right-6 text-white/30 hover:text-white transition-colors z-20">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
<form id="inquiry-form" class="space-y-10 relative z-10" @submit.prevent="handleSubmit">
<div class="space-y-8">
<p class="font-mono text-xs text-art-accent uppercase tracking-widest border-b border-white/10 pb-2 mb-6">01 // 身份识别</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="relative group/input">
<input
name="name"
type="text"
class="peer w-full bg-transparent border-b border-white/20 py-2 text-white font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent"
placeholder="Name"
v-model="formData.name"
>
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">您的称呼</label>
</div>
<div class="relative group/input">
<input
name="company"
type="text"
class="peer w-full bg-transparent border-b border-white/20 py-2 text-white font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent"
placeholder="Company"
v-model="formData.company"
>
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">公司 / 组织</label>
</div>
</div>
<div class="relative group/input">
<input
name="contact"
type="text"
class="peer w-full bg-transparent border-b border-white/20 py-2 text-white font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent"
placeholder="Contact"
v-model="formData.contact"
>
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">联系方式 (Email / WeChat)</label>
</div>
</div>
<div class="space-y-6">
<p class="font-mono text-xs text-art-accent uppercase tracking-widest border-b border-white/10 pb-2 mb-6">02 // 项目细节</p>
<div class="space-y-3">
<label class="text-xs font-mono text-white/30 block mb-2">预估预算范围</label>
<div class="flex flex-wrap gap-3">
<label class="cursor-pointer group">
<input
type="radio"
name="budget"
value="10k-50k"
class="peer hidden"
v-model="formData.budget"
>
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">10k - 50k</span>
</label>
<label class="cursor-pointer group">
<input
type="radio"
name="budget"
value="50k-200k"
class="peer hidden"
v-model="formData.budget"
>
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">50k - 200k</span>
</label>
<label class="cursor-pointer group">
<input
type="radio"
name="budget"
value="200k+"
class="peer hidden"
v-model="formData.budget"
>
<span class="px-4 py-2 border border-white/10 rounded-none text-xs font-mono text-white/50 hover:border-art-accent hover:text-white transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">200k +</span>
</label>
</div>
</div>
<div class="relative group/input mt-8">
<textarea
name="description"
rows="1"
class="peer w-full bg-transparent border-b border-white/20 py-2 text-white font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent resize-none min-h-[40px]"
placeholder="Brief"
v-model="formData.description"
@input="autoResizeTextarea"
></textarea>
<label class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all peer-focus:-top-4 peer-focus:text-art-accent peer-not-placeholder-shown:-top-4 peer-not-placeholder-shown:text-white/50 pointer-events-none">一句话描述需求</label>
</div>
</div>
<div class="pt-6">
<button
type="submit"
class="group relative w-full overflow-hidden bg-white text-black rounded-full font-bold hover:scale-105 transition-transform"
>
<div class="absolute inset-0 w-0 bg-art-accent transition-all duration-[250ms] ease-out group-hover:w-full"></div>
<span class="relative flex items-center justify-between z-10 px-8 py-4">
<span class="font-mono uppercase tracking-widest text-sm">INITIATE_PROTOCOL // 发送</span>
<i data-lucide="arrow-right" class="w-4 h-4 transition-transform group-hover:translate-x-1"></i>
</span>
</button>
<p class="mt-4 text-[10px] text-white/20 font-mono text-center tracking-widest">SECURED BY DIGITAL FINGERPRINTING</p>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formData = ref({
name: '',
company: '',
contact: '',
budget: '',
description: ''
})
const openModal = () => {
const modal = document.getElementById('inquiry-modal')
if (modal) {
modal.classList.remove('hidden')
modal.classList.add('flex')
document.body.style.overflow = 'hidden'
}
}
const closeModal = () => {
const modal = document.getElementById('inquiry-modal')
if (modal) {
modal.classList.remove('flex')
modal.classList.add('hidden')
document.body.style.overflow = ''
}
}
const handleSubmit = () => {
console.log('Form submitted:', formData.value)
// 这里可以添加表单提交逻辑
closeModal()
showToast('合作咨询已发送我们会尽快与您联系')
}
const autoResizeTextarea = (event: Event) => {
const textarea = event.target as HTMLTextAreaElement
textarea.style.height = ''
textarea.style.height = textarea.scrollHeight + 'px'
}
const showToast = (message: string, type: string = 'success') => {
const container = document.getElementById('toast-container')
if (container) {
const toast = document.createElement('div')
toast.className = `toast ${type === 'success' ? 'toast-success' : 'toast-error'}`
toast.innerHTML = `<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5 ${type === 'success' ? 'text-[#d4b383]' : 'text-red-500'}"></i><span class="text-sm font-medium">${message}</span>`
container.appendChild(toast)
if (window.lucide) {
window.lucide.createIcons()
}
requestAnimationFrame(() => toast.classList.add('show'))
setTimeout(() => {
toast.classList.remove('show')
setTimeout(() => toast.remove(), 400)
}, 3000)
}
}
// 暴露方法给全局使用
window.openInquiry = openModal
window.closeInquiry = closeModal
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div id="mobile-menu" class="fixed inset-0 z-40 bg-art-bg/95 backdrop-blur-xl flex flex-col items-center justify-center space-y-8 transition-all duration-300 menu-closed md:hidden">
<button @click="toggleMobileMenu" class="absolute top-6 right-6 text-white"><i data-lucide="x" class="w-8 h-8"></i></button>
<a @click="navigate('/')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">首页</a>
<a @click="navigate('/blog')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">思考</a>
<a @click="navigate('/works')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">作品</a>
<a @click="navigate('/snippets')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">代码</a>
<a @click="navigate('/about')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">关于</a>
<a @click="window.open('admin.html', '_blank')" class="font-mono text-sm text-art-muted mt-8">管理入口</a>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const toggleMobileMenu = () => {
const menu = document.getElementById('mobile-menu')
if (menu) {
menu.classList.remove('menu-open')
menu.classList.add('menu-closed')
document.body.style.overflow = ''
}
}
const navigate = (target: string) => {
router.push(target)
toggleMobileMenu()
}
</script>

View File

@@ -0,0 +1,97 @@
<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 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>
<div class="flex-1 overflow-hidden">
<!-- 使用CodePreview组件 -->
<CodePreview
:initial-code="modalCode"
:initial-language="detectLanguage(modalCode)"
@code-change="handleCodeChange"
@language-change="handleLanguageChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CodePreview from './CodePreview.vue'
const modalTitle = ref('Code')
const modalCode = ref('')
// 检测代码语言
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 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)
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)'
}
setTimeout(() => {
modal.classList.remove('flex')
modal.classList.add('hidden')
document.body.style.overflow = ''
}, 300)
}
}
const handleCodeChange = (newCode: string) => {
modalCode.value = newCode
}
const handleLanguageChange = (newLanguage: string) => {
// 可以在这里处理语言变化事件
console.log('Language changed to:', newLanguage)
}
// 暴露方法给全局使用
window.showSnippet = showSnippet
window.closeSnippet = closeSnippet
</script>

View File

@@ -0,0 +1,335 @@
<template>
<div class="admin-layout">
<!-- Sidebar Navigation -->
<aside class="admin-sidebar">
<div class="sidebar-header">
<h1 class="sidebar-title">管理后台</h1>
</div>
<nav class="sidebar-nav">
<ul>
<li>
<router-link to="/admin/dashboard" class="nav-link" active-class="active">
<span class="nav-icon">📊</span>
<span class="nav-text">仪表盘</span>
</router-link>
</li>
<li>
<router-link to="/admin/users" class="nav-link" active-class="active">
<span class="nav-icon">👥</span>
<span class="nav-text">用户管理</span>
</router-link>
</li>
<li>
<router-link to="/admin/roles" class="nav-link" active-class="active">
<span class="nav-icon">🔒</span>
<span class="nav-text">角色管理</span>
</router-link>
</li>
<li>
<router-link to="/admin/posts" class="nav-link" active-class="active">
<span class="nav-icon">📝</span>
<span class="nav-text">文章管理</span>
</router-link>
</li>
<li>
<router-link to="/admin/works" class="nav-link" active-class="active">
<span class="nav-icon">🎨</span>
<span class="nav-text">作品管理</span>
</router-link>
</li>
<li>
<router-link to="/admin/snippets" class="nav-link" active-class="active">
<span class="nav-icon">💻</span>
<span class="nav-text">代码片段</span>
</router-link>
</li>
<li>
<router-link to="/admin/settings" class="nav-link" active-class="active">
<span class="nav-icon"></span>
<span class="nav-text">系统配置</span>
</router-link>
</li>
<li>
<router-link to="/admin/logs" class="nav-link" active-class="active">
<span class="nav-icon">📋</span>
<span class="nav-text">操作日志</span>
</router-link>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<button @click="handleLogout" class="logout-btn">
<span class="nav-icon">🚪</span>
<span class="nav-text">退出登录</span>
</button>
</div>
</aside>
<!-- Main Content Area -->
<main class="admin-main">
<!-- Top Navigation Bar -->
<header class="admin-header">
<div class="header-left">
<button class="toggle-btn" @click="toggleSidebar">
</button>
</div>
<div class="header-right">
<div class="user-info">
<span class="username">{{ currentUser.username }}</span>
<span class="role">({{ currentUser.role }})</span>
</div>
</div>
</header>
<!-- Content Container -->
<div class="admin-content">
<router-view />
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from '../../composables/useToast'
const router = useRouter()
const toast = useToast()
const isSidebarOpen = ref(true)
const currentUser = ref(JSON.parse(localStorage.getItem('user') || '{}'))
const toggleSidebar = () => {
isSidebarOpen.value = !isSidebarOpen.value
}
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
toast.showToast('退出登录成功', 'success')
router.push('/login')
}
// Check if user is authenticated
onMounted(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/login')
}
})
</script>
<style scoped>
.admin-layout {
display: flex;
min-height: 100vh;
background-color: #050505;
color: #ececec;
}
/* Sidebar Styles */
.admin-sidebar {
width: 250px;
background: rgba(5, 5, 5, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-right: 1px solid rgba(255, 255, 255, 0.03);
color: white;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
.sidebar-title {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
color: #d4b383;
font-family: 'Playfair Display', serif;
font-style: italic;
}
.sidebar-nav {
flex: 1;
padding: 1rem 0;
}
.sidebar-nav ul {
list-style: none;
padding: 0;
margin: 0;
}
.nav-link {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
border-left: 3px solid transparent;
position: relative;
}
.nav-link:hover {
background-color: rgba(212, 179, 131, 0.1);
color: white;
border-left-color: rgba(212, 179, 131, 0.3);
}
.nav-link.active {
background-color: rgba(212, 179, 131, 0.15);
color: #d4b383;
border-left-color: #d4b383;
box-shadow: 0 0 15px rgba(212, 179, 131, 0.1);
}
.nav-icon {
margin-right: 0.75rem;
font-size: 1.1rem;
}
.nav-text {
font-size: 0.95rem;
font-family: 'Inter', sans-serif;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.03);
}
.logout-btn {
display: flex;
align-items: center;
width: 100%;
padding: 0.75rem 1.5rem;
background-color: transparent;
color: #d4b383;
border: none;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
font-size: 0.95rem;
font-family: 'Inter', sans-serif;
}
.logout-btn:hover {
background-color: rgba(212, 179, 131, 0.1);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(212, 179, 131, 0.1);
}
/* Main Content Styles */
.admin-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header Styles */
.admin-header {
background: rgba(5, 5, 5, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
padding: 0 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.toggle-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
padding: 0.5rem;
border-radius: 0.375rem;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.toggle-btn:hover {
background-color: rgba(212, 179, 131, 0.1);
color: #d4b383;
transform: scale(1.1);
}
.user-info {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.8);
font-family: 'Inter', sans-serif;
}
.username {
font-weight: 600;
color: #d4b383;
}
.role {
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
}
/* Content Area */
.admin-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
/* Scrollbar Styles */
.admin-content::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.admin-content::-webkit-scrollbar-track {
background: transparent;
}
.admin-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
transition: all 0.3s ease;
}
.admin-content::-webkit-scrollbar-thumb:hover {
background: #d4b383;
box-shadow: 0 0 10px rgba(212, 179, 131, 0.4);
}
.admin-content::-webkit-scrollbar-corner {
background: transparent;
}
/* Responsive Design */
@media (max-width: 768px) {
.admin-sidebar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
z-index: 1000;
transform: translateX(0);
}
.admin-sidebar.collapsed {
transform: translateX(-100%);
}
}
</style>