亮色、暗色模式适配

This commit is contained in:
李琦
2026-07-14 12:47:53 +08:00
parent 02cf290bad
commit b88a72e613
34 changed files with 1026 additions and 538 deletions

View File

@@ -1,10 +1,27 @@
<!doctype html>
<html lang="zh-CN" class="dark">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon/icon.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>艺术与代码 | 年糕崽崽的数字花园</title>
<script>
(function () {
try {
var path = location.pathname || ''
var forceDark = path === '/login' || path.indexOf('/admin') === 0
var pref = localStorage.getItem('theme-pref') || 'system'
var dark =
forceDark ||
pref === 'dark' ||
(pref !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches)
document.documentElement.classList.toggle('dark', dark)
document.documentElement.style.colorScheme = dark ? 'dark' : 'light'
} catch (e) {
document.documentElement.classList.add('dark')
}
})()
</script>
</head>
<body>
<div id="app"></div>

View File

@@ -6,11 +6,14 @@
scrolling context.
-->
<div class="min-h-screen bg-art-bg text-art-text relative flex flex-col">
<!-- Star Background (Only for frontend) -->
<!-- Ambient canvas: dark stars / light aurora+dust -->
<StarBackground v-if="showFrontendChrome" class="z-0" />
<!-- Global Noise (Only for frontend) -->
<div v-if="showFrontendChrome" class="fixed inset-0 pointer-events-none z-[60] bg-noise opacity-30 mix-blend-overlay"></div>
<!-- Light-mode static corner washes -->
<div v-if="showFrontendChrome" class="light-atmosphere" aria-hidden="true"></div>
<!-- Global Noise (Only for frontend) opacity/blend via theme CSS vars -->
<div v-if="showFrontendChrome" class="site-noise fixed inset-0 pointer-events-none z-[60] bg-noise"></div>
<!-- Toast Container -->
<div id="toast-container" class="toast-container relative z-[70]"></div>
@@ -49,7 +52,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import Header from './components/Header.vue'
import MobileMenu from './components/MobileMenu.vue'
@@ -59,17 +62,22 @@ import StarBackground from './components/StarBackground.vue'
import UserProfileModal from './components/UserProfileModal.vue'
import { useUserProfileModal } from './composables/useUserProfileModal'
import { loadPublicSettings } from './composables/usePublicSettings'
import { initTheme, setForceDark } from './composables/useTheme'
const { isOpen: profileModalOpen, activeUserId: profileUserId, closeUserProfile } = useUserProfileModal()
const route = useRoute()
initTheme()
// Check if current route is Admin or Login page
const isAdminOrLogin = computed(() => {
const path = route.path
return path.startsWith('/admin') || path === '/login'
})
watch(isAdminOrLogin, (forced) => setForceDark(forced), { immediate: true })
// 沉浸式演示页PPT 预览等)
const isPresentationPage = computed(() => route.meta.presentation === true)

View File

@@ -112,7 +112,7 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
<button
type="button"
:disabled="isExporting"
class="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 text-xs md:text-sm text-art-muted hover:text-white hover:bg-white/10 hover:border-art-accent/30 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
class="flex items-center gap-2 rounded-full border border-art-border bg-art-text/5 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 text-xs md:text-sm text-art-muted hover:text-art-text hover:bg-art-text/10 hover:border-art-accent/30 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
@click.stop="toggleMenu"
>
<Icon
@@ -135,11 +135,11 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
>
<div
v-if="isOpen"
class="absolute right-0 top-full mt-2 w-56 rounded-xl border border-white/10 bg-[#141414]/95 backdrop-blur-xl shadow-2xl z-50 overflow-hidden"
class="absolute right-0 top-full mt-2 w-56 rounded-xl border border-art-border bg-art-surface/95 backdrop-blur-xl shadow-2xl z-50 overflow-hidden"
@click.stop
>
<div class="px-3 py-2 border-b border-white/5">
<span class="text-[10px] font-bold text-white/40 uppercase tracking-wider">选择导出格式</span>
<div class="px-3 py-2 border-b border-art-text/5">
<span class="text-[10px] font-bold text-art-muted uppercase tracking-wider">选择导出格式</span>
</div>
<ul class="py-1">
@@ -147,13 +147,13 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
<!-- 格式主项 -->
<button
type="button"
class="w-full flex items-center gap-3 px-3 py-2.5 text-left text-sm text-art-muted hover:text-white hover:bg-white/5 transition-colors"
class="w-full flex items-center gap-3 px-3 py-2.5 text-left text-sm text-art-muted hover:text-art-text hover:bg-art-text/5 transition-colors"
@click="handleFormatClick(option.format, option.needsTheme)"
>
<Icon :name="option.icon" :size="16" class="text-art-accent shrink-0" />
<div class="flex-1 min-w-0">
<div class="font-medium text-white/90">{{ option.label }}</div>
<div class="text-[10px] text-white/40 truncate">{{ option.description }}</div>
<div class="font-medium text-art-text/90">{{ option.label }}</div>
<div class="text-[10px] text-art-muted truncate">{{ option.description }}</div>
</div>
<Icon
v-if="option.needsTheme"
@@ -166,20 +166,20 @@ onUnmounted(() => document.removeEventListener('click', handleClickOutside))
<!-- PDF / PNG 二级主题选项 -->
<div
v-if="option.needsTheme && expandedFormat === option.format"
class="bg-white/[0.02] border-t border-white/5"
class="bg-art-text/[0.02] border-t border-art-border"
>
<button
v-for="themeOpt in themeOptions"
:key="themeOpt.theme"
type="button"
class="w-full flex items-center gap-2 pl-10 pr-3 py-2 text-xs text-art-muted hover:text-white hover:bg-white/5 transition-colors"
class="w-full flex items-center gap-2 pl-10 pr-3 py-2 text-xs text-art-muted hover:text-art-text hover:bg-art-text/5 transition-colors"
@click="handleExport(option.format, themeOpt.theme)"
>
<span
class="w-3 h-3 rounded-full border shrink-0"
:class="themeOpt.theme === 'light'
? 'bg-white border-gray-300'
: 'bg-[#050505] border-white/30'"
: 'bg-black border-gray-600'"
></span>
{{ themeOpt.label }}
</button>

View File

@@ -123,7 +123,7 @@ const themeClasses = computed(() => {
case 'onImage':
return {
wrapper: '',
avatarBorder: 'border-white/30 bg-white/10',
avatarBorder: 'border-white/20 bg-white/10',
placeholder: 'text-white/80',
name: 'text-white',
email: 'text-white/70'
@@ -131,17 +131,17 @@ const themeClasses = computed(() => {
case 'onDark':
return {
wrapper: '',
avatarBorder: 'border-white/20 bg-white/5',
avatarBorder: 'border-art-border bg-art-text/5',
placeholder: 'text-art-accent',
name: 'text-white',
name: 'text-art-text',
email: 'text-art-muted'
}
default:
return {
wrapper: '',
avatarBorder: 'border-white/20 bg-white/5',
avatarBorder: 'border-art-border bg-art-text/5',
placeholder: 'text-art-accent',
name: 'text-white',
name: 'text-art-text',
email: 'text-art-muted'
}
}

View File

@@ -5,7 +5,7 @@
<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="relative z-10 flex items-center justify-between px-6 py-4 bg-black/40 backdrop-blur-xl border-b border-art-border rounded-t-2xl">
<div class="flex items-center gap-4">
<!-- 装饰性 Mac 窗口按钮 -->
<div class="flex gap-2">
@@ -13,7 +13,7 @@
<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>
<span class="text-xs font-mono text-art-muted 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">
@@ -44,10 +44,10 @@
</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="relative z-10 flex-1 flex flex-col md:flex-row bg-art-bg/80 backdrop-blur-sm rounded-b-2xl overflow-hidden border border-t-0 border-art-border">
<!-- 左侧代码编辑器 -->
<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="w-full md:w-1/2 relative flex flex-col border-b md:border-b-0 md:border-r border-art-border 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">
@@ -73,13 +73,13 @@
</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">
<div class="w-full md:w-1/2 bg-art-surface flex flex-col relative">
<div class="h-8 flex items-center justify-between px-4 bg-art-text/5 border-b border-art-border shrink-0">
<span class="text-[10px] font-mono text-art-accent 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>
<button @click="clearConsole" v-if="isConsoleMode" class="text-[10px] text-art-muted hover:text-art-text 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>

View File

@@ -98,18 +98,18 @@ onUnmounted(() => {
display: flex;
align-items: center;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--art-card-bg);
border: 1px solid var(--art-border);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
color: #ececec;
color: rgb(var(--art-text));
font-family: 'Inter', sans-serif;
font-size: 0.95rem;
}
.custom-select-container:hover {
border-color: #d4b383;
border-color: rgb(var(--art-accent));
box-shadow: 0 0 0 3px rgba(212, 179, 131, 0.2);
}
@@ -120,7 +120,7 @@ onUnmounted(() => {
}
.selected-value.placeholder {
color: #888888;
color: rgb(var(--art-muted));
}
.dropdown-arrow {
@@ -128,7 +128,7 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
color: #888888;
color: rgb(var(--art-muted));
}
.dropdown-arrow.open {
@@ -137,14 +137,15 @@ onUnmounted(() => {
.dropdown-options {
z-index: 99999999999999999999;
background: rgba(12, 12, 14, 0.72);
background: rgb(var(--art-surface) / 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.12);
border: 1px solid var(--art-border);
border-radius: 0.5rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
box-shadow: var(--art-shadow);
max-height: 200px;
overflow-y: auto;
color: rgb(var(--art-text));
}
.option-item {
@@ -158,12 +159,12 @@ onUnmounted(() => {
.option-item:hover {
background: rgba(212, 179, 131, 0.15);
color: #d4b383;
color: rgb(var(--art-accent));
}
.option-item.selected {
background: rgba(212, 179, 131, 0.2);
color: #d4b383;
color: rgb(var(--art-accent));
font-weight: 500;
}
@@ -173,16 +174,16 @@ onUnmounted(() => {
}
.dropdown-options::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
background: transparent;
border-radius: 3px;
}
.dropdown-options::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
background: var(--art-scroll-thumb);
border-radius: 3px;
}
.dropdown-options::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
background: var(--art-scroll-thumb-hover);
}
</style>

View File

@@ -99,13 +99,13 @@ onBeforeUnmount(() => destroyPlayer())
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/85 backdrop-blur-sm p-4 md:p-8"
@click.self="handleClose"
>
<div class="relative w-full max-w-5xl bg-[#0d0d0d] rounded-xl border border-white/10 overflow-hidden shadow-2xl">
<div class="relative w-full max-w-5xl bg-[#0d0d0d] rounded-xl border border-art-border overflow-hidden shadow-2xl">
<!-- 标题栏 -->
<div class="flex items-center justify-between px-4 py-3 border-b border-white/10">
<h3 class="text-sm md:text-base text-white font-medium truncate pr-4">{{ title }}</h3>
<div class="flex items-center justify-between px-4 py-3 border-b border-art-border">
<h3 class="text-sm md:text-base text-art-text font-medium truncate pr-4">{{ title }}</h3>
<button
type="button"
class="shrink-0 p-2 rounded-full hover:bg-white/10 text-white/70 hover:text-white transition-colors"
class="shrink-0 p-2 rounded-full hover:bg-art-text/10 text-art-text/70 hover:text-art-text transition-colors"
@click="handleClose"
>
<Icon name="x" :size="20" />

View File

@@ -1,5 +1,5 @@
<template>
<footer class="py-8 text-center border-t border-white/5 relative z-10">
<footer class="py-8 text-center border-t border-art-border relative z-10">
<div class="flex justify-center items-center gap-4 mb-4">
<a
v-if="contactEmail"

View File

@@ -4,8 +4,8 @@
<div class="max-w-7xl mx-auto px-4 md:px-6 h-16 md:h-20 flex items-center justify-between">
<!-- Logo -->
<div class="cursor-pointer group flex items-center gap-1" @click="goTo('/')">
<span class="font-serif text-xl md:text-2xl italic tracking-wider text-white group-hover:text-art-accent transition-colors">{{ siteTitle || '年糕崽崽' }}</span>
<span class="font-serif text-xl md:text-2xl italic tracking-wider text-art-accent group-hover:text-white transition-colors">.Blog</span>
<span class="font-serif text-xl md:text-2xl italic tracking-wider text-art-text group-hover:text-art-accent transition-colors">{{ siteTitle || '年糕崽崽' }}</span>
<span class="font-serif text-xl md:text-2xl italic tracking-wider text-art-accent group-hover:text-art-text transition-colors">.Blog</span>
</div>
<!-- Desktop Nav -->
@@ -13,56 +13,56 @@
<button
v-if="visibleMenus.includes('home')"
@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' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'home' }"
>
首页
</button>
<button
v-if="visibleMenus.includes('blog')"
@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' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'blog' }"
>
思考
</button>
<button
v-if="visibleMenus.includes('columns')"
@click="goTo('/columns')"
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 === 'columns' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'columns' }"
>
专栏
</button>
<button
v-if="visibleMenus.includes('works')"
@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' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'works' }"
>
作品
</button>
<button
v-if="visibleMenus.includes('videos')"
@click="goTo('/videos')"
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 === 'videos' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'videos' }"
>
视频
</button>
<button
v-if="visibleMenus.includes('snippets')"
@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' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'snippets' }"
>
代码
</button>
<button
v-if="visibleMenus.includes('about')"
@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' }"
class="nav-item text-sm font-medium tracking-wide text-art-muted hover:text-art-text transition-colors link-underline"
:class="{ 'text-art-text after:w-full': activeNav === 'about' }"
>
关于
</button>
@@ -70,21 +70,30 @@
<!-- Desktop Actions -->
<div class="hidden md:flex items-center gap-4">
<a href="#" class="text-art-muted hover:text-white transition-colors"><Icon name="github" :size="20" /></a>
<a href="/admin" target="_blank" class="text-art-muted hover:text-white transition-colors" title="后台管理">
<button
type="button"
@click="cycleTheme"
class="text-art-muted hover:text-art-text transition-colors p-1"
:title="themeLabel"
:aria-label="themeLabel"
>
<Icon :name="themeIcon" :size="20" />
</button>
<a href="#" class="text-art-muted hover:text-art-text transition-colors"><Icon name="github" :size="20" /></a>
<a href="/admin" target="_blank" class="text-art-muted hover:text-art-text transition-colors" title="后台管理">
<Icon name="layout-dashboard" :size="20" />
</a>
<button
v-if="visibleMenus.includes('services')"
@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"
class="px-5 py-2 text-xs font-bold tracking-widest uppercase border border-art-border hover:border-art-accent hover:text-art-accent transition-all rounded-full"
>
合作
</button>
</div>
<!-- Mobile Menu Button -->
<button class="md:hidden text-white p-2 -mr-2 active:scale-95 transition-transform" @click="isMobileMenuOpen = true">
<button class="md:hidden text-art-text p-2 -mr-2 active:scale-95 transition-transform" @click="isMobileMenuOpen = true">
<Icon name="menu" :size="24" />
</button>
</div>
@@ -100,13 +109,13 @@
</Transition>
<Transition name="slide-right">
<div v-if="isMobileMenuOpen" class="fixed top-0 right-0 bottom-0 w-64 bg-[#1a1a1a] z-[101] shadow-2xl border-l border-white/10 flex flex-col md:hidden">
<div v-if="isMobileMenuOpen" class="fixed top-0 right-0 bottom-0 w-64 bg-art-surface z-[101] shadow-2xl border-l border-art-border flex flex-col md:hidden">
<!-- Drawer Header -->
<div class="h-16 px-6 flex items-center justify-between border-b border-white/5">
<span class="font-serif text-lg italic tracking-wider text-white">
<div class="h-16 px-6 flex items-center justify-between border-b border-art-border">
<span class="font-serif text-lg italic tracking-wider text-art-text">
Menu
</span>
<button class="text-white/70 hover:text-white p-2 -mr-2" @click="closeMobileMenu">
<button class="text-art-muted hover:text-art-text p-2 -mr-2" @click="closeMobileMenu">
<Icon name="x" :size="24" />
</button>
</div>
@@ -117,68 +126,76 @@
<button
v-if="visibleMenus.includes('home')"
@click="goTo('/')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'home' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'home' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
首页
</button>
<button
v-if="visibleMenus.includes('blog')"
@click="goTo('/blog')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'blog' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'blog' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
思考
</button>
<button
v-if="visibleMenus.includes('columns')"
@click="goTo('/columns')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'columns' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'columns' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
专栏
</button>
<button
v-if="visibleMenus.includes('works')"
@click="goTo('/works')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'works' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'works' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
作品
</button>
<button
v-if="visibleMenus.includes('videos')"
@click="goTo('/videos')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'videos' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'videos' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
视频
</button>
<button
v-if="visibleMenus.includes('snippets')"
@click="goTo('/snippets')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'snippets' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'snippets' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
代码
</button>
<button
v-if="visibleMenus.includes('about')"
@click="goTo('/about')"
class="text-left text-base font-medium py-2 border-b border-white/5 transition-colors"
:class="activeNav === 'about' ? 'text-art-accent' : 'text-art-muted hover:text-white'"
class="text-left text-base font-medium py-2 border-b border-art-border transition-colors"
:class="activeNav === 'about' ? 'text-art-accent' : 'text-art-muted hover:text-art-text'"
>
关于
</button>
</nav>
<div class="pt-6 border-t border-white/10 space-y-4">
<div class="pt-6 border-t border-art-border space-y-4">
<button
type="button"
@click="cycleTheme"
class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm w-full"
>
<Icon :name="themeIcon" :size="18" />
<span>{{ themeLabel }}</span>
</button>
<div class="flex items-center gap-4">
<a href="#" class="flex items-center gap-2 text-art-muted hover:text-white transition-colors text-sm">
<a href="#" class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm">
<Icon name="github" :size="18" />
<span>GitHub</span>
</a>
<a href="/admin" target="_blank" class="flex items-center gap-2 text-art-muted hover:text-white transition-colors text-sm">
<a href="/admin" target="_blank" class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors text-sm">
<Icon name="layout-dashboard" :size="18" />
<span>管理后台</span>
</a>
@@ -186,7 +203,7 @@
<button
v-if="visibleMenus.includes('services')"
@click="goTo('/services')"
class="w-full py-3 text-xs font-bold tracking-widest uppercase border border-white/20 hover:bg-white/5 hover:border-art-accent hover:text-art-accent transition-all rounded-lg text-center text-white"
class="w-full py-3 text-xs font-bold tracking-widest uppercase border border-art-border hover:bg-art-bg hover:border-art-accent hover:text-art-accent transition-all rounded-lg text-center text-art-text"
>
商业合作
</button>
@@ -202,11 +219,13 @@
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { usePublicSettings } from '../composables/usePublicSettings'
import { useTheme } from '../composables/useTheme'
import Icon from './Icon.vue'
const router = useRouter()
const route = useRoute()
const { cache, getStringSetting } = usePublicSettings()
const { themeIcon, themeLabel, cycleTheme } = useTheme()
const activeNav = ref('home')
const siteTitle = ref<string>('')
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'videos', 'snippets', 'about', 'services'])

View File

@@ -3,34 +3,34 @@
<Transition name="inquiry-fade">
<div
v-if="isOpen"
class="fixed inset-0 z-[100] bg-[#050505]/95 backdrop-blur-xl flex items-center justify-center p-4"
class="fixed inset-0 z-[100] bg-art-bg/95 backdrop-blur-xl flex items-center justify-center p-4"
@click.self="closeModal"
>
<div class="relative w-full max-w-4xl bg-[#080808] border border-white/10 overflow-y-auto overflow-x-hidden flex flex-col md:flex-row shadow-2xl max-h-[95vh]">
<div class="hidden md:flex w-1/3 bg-[#0c0c0c] border-r border-white/5 p-12 flex-col justify-between relative overflow-hidden shrink-0">
<div class="relative w-full max-w-4xl bg-art-bg border border-art-border overflow-y-auto overflow-x-hidden flex flex-col md:flex-row shadow-2xl max-h-[95vh]">
<div class="hidden md:flex w-1/3 bg-art-bg border-r border-art-border p-12 flex-col justify-between relative overflow-hidden shrink-0">
<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">
<h3 class="font-serif text-3xl text-art-text italic mb-6 leading-tight">
共创<br>无限<br>可能
</h3>
<p class="font-mono text-xs text-white/40 leading-relaxed">
<p class="font-mono text-xs text-art-muted leading-relaxed">
告诉我你的构想<br>
我将提供架构与代码<br>
将想象变为现实
</p>
</div>
<div class="relative z-10 font-mono text-[10px] text-white/20 tracking-widest">
<div class="relative z-10 font-mono text-[10px] text-art-muted/60 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]">
<div class="w-full md:w-2/3 p-10 md:p-16 relative bg-art-bg">
<button
type="button"
@click="closeModal"
class="absolute top-6 right-6 p-2 text-white/30 hover:text-white hover:bg-white/5 rounded-full transition-colors z-20"
class="absolute top-6 right-6 p-2 text-art-muted hover:text-art-text hover:bg-art-text/5 rounded-full transition-colors z-20"
aria-label="关闭"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -40,37 +40,37 @@
</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>
<p class="font-mono text-xs text-art-accent uppercase tracking-widest border-b border-art-border 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="inquiry-input 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"
class="inquiry-input peer w-full bg-transparent border-b border-art-border py-2 text-art-text font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent"
placeholder=" "
v-model="formData.name"
required
>
<label
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-white/50 peer-autofill:-top-4 peer-autofill:text-white/50"
class="absolute left-0 top-2 text-art-muted text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-art-muted peer-autofill:-top-4 peer-autofill:text-art-muted"
>您的称呼</label>
</div>
<div class="relative group/input">
<input
name="company"
type="text"
class="inquiry-input 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"
class="inquiry-input peer w-full bg-transparent border-b border-art-border py-2 text-art-text font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent"
placeholder=" "
v-model="formData.company"
>
<label
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-white/50 peer-autofill:-top-4 peer-autofill:text-white/50"
class="absolute left-0 top-2 text-art-muted text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-art-muted peer-autofill:-top-4 peer-autofill:text-art-muted"
>公司 / 组织</label>
</div>
</div>
<div class="space-y-4">
<label class="text-xs font-mono text-white/30 block">联系方式类型</label>
<label class="text-xs font-mono text-art-muted block">联系方式类型</label>
<RadioGroup
name="contactMethod"
:options="contactOptions"
@@ -90,34 +90,34 @@
<input
name="contactValue"
type="text"
class="inquiry-input 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"
class="inquiry-input peer w-full bg-transparent border-b border-art-border py-2 text-art-text font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent"
placeholder=" "
v-model="formData.contactValue"
required
>
<label
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-white/50 peer-autofill:-top-4 peer-autofill:text-white/50"
class="absolute left-0 top-2 text-art-muted text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-art-muted peer-autofill:-top-4 peer-autofill:text-art-muted"
>{{ getContactLabel }}</label>
</div>
</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>
<p class="font-mono text-xs text-art-accent uppercase tracking-widest border-b border-art-border pb-2 mb-6">02 项目细节</p>
<div class="space-y-3">
<label class="text-xs font-mono text-white/30 block mb-2">预估预算范围</label>
<label class="text-xs font-mono text-art-muted block mb-2">预估预算范围</label>
<div class="flex flex-wrap gap-3">
<label class="cursor-pointer group">
<input type="radio" name="budget" value="1w-5w" 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">1w - 5w</span>
<span class="px-4 py-2 border border-art-border rounded-none text-xs font-mono text-art-muted hover:border-art-accent hover:text-art-text transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">1w - 5w</span>
</label>
<label class="cursor-pointer group">
<input type="radio" name="budget" value="5w-20w" 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">5w - 20w</span>
<span class="px-4 py-2 border border-art-border rounded-none text-xs font-mono text-art-muted hover:border-art-accent hover:text-art-text transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">5w - 20w</span>
</label>
<label class="cursor-pointer group">
<input type="radio" name="budget" value="20w+" 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">20w +</span>
<span class="px-4 py-2 border border-art-border rounded-none text-xs font-mono text-art-muted hover:border-art-accent hover:text-art-text transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent">20w +</span>
</label>
</div>
</div>
@@ -126,13 +126,13 @@
<textarea
name="description"
:rows="3"
class="inquiry-input 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] max-h-[100px]"
class="inquiry-input peer w-full bg-transparent border-b border-art-border py-2 text-art-text font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent resize-none min-h-[40px] max-h-[100px]"
placeholder=" "
v-model="formData.description"
@input="autoResizeTextarea"
></textarea>
<label
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-white/50 peer-autofill:-top-4 peer-autofill:text-white/50"
class="absolute left-0 top-2 text-art-muted text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-art-muted peer-autofill:-top-4 peer-autofill:text-art-muted"
>一句话描述需求</label>
</div>
</div>
@@ -154,7 +154,7 @@
</svg>
</span>
</button>
<p class="mt-4 text-[10px] text-white/20 font-mono text-center tracking-widest">SECURED BY DIGITAL FINGERPRINTING</p>
<p class="mt-4 text-[10px] text-art-muted/60 font-mono text-center tracking-widest">SECURED BY DIGITAL FINGERPRINTING</p>
</div>
</form>
</div>
@@ -255,7 +255,7 @@ const showToast = (message: string, type: string = 'success') => {
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>`
toast.innerHTML = `<i data-lucide="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5 ${type === 'success' ? 'text-art-accent' : 'text-red-500'}"></i><span class="text-sm font-medium">${message}</span>`
container.appendChild(toast)
if ((window as any).lucide) {
(window as any).lucide.createIcons()
@@ -291,9 +291,9 @@ onUnmounted(() => {
.inquiry-input:-webkit-autofill:hover,
.inquiry-input:-webkit-autofill:focus,
.inquiry-input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 1000px #080808 inset !important;
-webkit-text-fill-color: white !important;
caret-color: white;
-webkit-box-shadow: 0 0 0 1000px rgb(var(--art-input-autofill)) inset !important;
-webkit-text-fill-color: rgb(var(--art-input-autofill-text)) !important;
caret-color: rgb(var(--art-input-autofill-text));
transition: background-color 5000s ease-in-out 0s;
}

View File

@@ -1,15 +1,19 @@
<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 v-if="visibleMenus.includes('home')" @click="navigate('/')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">首页</a>
<a v-if="visibleMenus.includes('blog')" @click="navigate('/blog')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">思考</a>
<a v-if="visibleMenus.includes('columns')" @click="navigate('/columns')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">专栏</a>
<a v-if="visibleMenus.includes('works')" @click="navigate('/works')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">作品</a>
<a v-if="visibleMenus.includes('videos')" @click="navigate('/videos')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">视频</a>
<a v-if="visibleMenus.includes('snippets')" @click="navigate('/snippets')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">代码</a>
<a v-if="visibleMenus.includes('about')" @click="navigate('/about')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">关于</a>
<a v-if="visibleMenus.includes('services')" @click="navigate('/services')" class="font-serif text-3xl italic text-white hover:text-art-accent cursor-pointer">合作</a>
<a @click="openAdmin" class="font-mono text-sm text-art-muted mt-8">管理入口</a>
<button @click="toggleMobileMenu" class="absolute top-6 right-6 text-art-text"><i data-lucide="x" class="w-8 h-8"></i></button>
<a v-if="visibleMenus.includes('home')" @click="navigate('/')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">首页</a>
<a v-if="visibleMenus.includes('blog')" @click="navigate('/blog')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">思考</a>
<a v-if="visibleMenus.includes('columns')" @click="navigate('/columns')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">专栏</a>
<a v-if="visibleMenus.includes('works')" @click="navigate('/works')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">作品</a>
<a v-if="visibleMenus.includes('videos')" @click="navigate('/videos')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">视频</a>
<a v-if="visibleMenus.includes('snippets')" @click="navigate('/snippets')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">代码</a>
<a v-if="visibleMenus.includes('about')" @click="navigate('/about')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">关于</a>
<a v-if="visibleMenus.includes('services')" @click="navigate('/services')" class="font-serif text-3xl italic text-art-text hover:text-art-accent cursor-pointer">合作</a>
<button type="button" @click="cycleTheme" class="font-mono text-sm text-art-muted hover:text-art-accent flex items-center gap-2">
<Icon :name="themeIcon" :size="18" />
{{ themeLabel }}
</button>
<a @click="openAdmin" class="font-mono text-sm text-art-muted mt-4">管理入口</a>
</div>
</template>
@@ -17,9 +21,12 @@
import { useRouter } from 'vue-router'
import { onMounted, onUpdated, ref, watch } from 'vue'
import { usePublicSettings } from '../composables/usePublicSettings'
import { useTheme } from '../composables/useTheme'
import Icon from './Icon.vue'
const router = useRouter()
const { cache, getStringSetting } = usePublicSettings()
const { themeIcon, themeLabel, cycleTheme } = useTheme()
const visibleMenus = ref<string[]>(['home', 'blog', 'columns', 'works', 'videos', 'snippets', 'about', 'services'])
const applyMenuSettings = () => {
@@ -50,7 +57,7 @@ const navigate = (target: string) => {
}
const openAdmin = () => {
window.open('admin.html', '_blank')
window.open('/admin', '_blank')
}
const refreshIcons = () => {

View File

@@ -1,12 +1,12 @@
<template>
<div
v-if="visible"
class="absolute left-0 right-0 top-full mt-2 z-30 bg-[#121214] border border-white/10 rounded-xl shadow-2xl overflow-hidden"
class="absolute left-0 right-0 top-full mt-2 z-30 bg-art-surface border border-art-border rounded-xl shadow-2xl overflow-hidden"
>
<div v-if="history.length > 0" class="p-3 border-b border-white/5">
<div v-if="history.length > 0" class="p-3 border-b border-art-border">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-art-muted uppercase tracking-wider">搜索历史</span>
<button type="button" class="text-xs text-art-accent hover:text-white" @mousedown.prevent="emit('clear-history')">
<button type="button" class="text-xs text-art-accent hover:text-art-text" @mousedown.prevent="emit('clear-history')">
清空
</button>
</div>
@@ -15,12 +15,12 @@
v-for="item in history"
:key="`history-${item}`"
type="button"
class="group inline-flex items-center gap-1 px-3 py-1.5 rounded-full bg-white/5 hover:bg-white/10 text-sm text-white/80"
class="group inline-flex items-center gap-1 px-3 py-1.5 rounded-full bg-art-text/5 hover:bg-art-text/10 text-sm text-art-text/80"
@mousedown.prevent="emit('select', item)"
>
<span>{{ item }}</span>
<span
class="text-white/30 hover:text-white"
class="text-art-muted hover:text-art-text"
@mousedown.prevent.stop="emit('remove-history', item)"
>×</span>
</button>

View File

@@ -10,7 +10,7 @@
>
<div class="absolute inset-0 bg-black/90 backdrop-blur-sm" @click="close"></div>
<div
class="relative z-[101] bg-[#0a0a0c] border-0 md:border border-white/10 flex flex-col overflow-hidden shadow-2xl transform transition-all duration-300"
class="relative z-[101] bg-[#0a0a0c] border-0 md:border border-art-border flex flex-col overflow-hidden shadow-2xl transform transition-all duration-300"
:class="[
isAnimating ? 'scale-95' : 'scale-100',
isMaximized
@@ -18,7 +18,7 @@
: 'w-full max-w-6xl h-[100dvh] max-h-[100dvh] md:h-[80vh] md:max-h-[80vh] rounded-none md:rounded-2xl'
]"
>
<div class="h-14 md:h-16 border-b border-white/10 flex items-center gap-3 px-4 md:px-6 bg-white/5 shrink-0 min-w-0">
<div class="h-14 md:h-16 border-b border-art-border flex items-center gap-3 px-4 md:px-6 bg-art-text/5 shrink-0 min-w-0">
<div class="flex gap-2 shrink-0">
<button
type="button"
@@ -42,8 +42,8 @@
<span class="font-bold text-white truncate text-sm md:text-base min-w-0 flex-1">{{ title }}</span>
</div>
<div v-if="posts && posts.length > 0" class="px-4 md:px-6 py-2 bg-white/[0.02] border-b border-white/10 flex items-center gap-2 shrink-0 overflow-x-auto hide-scrollbar">
<span class="text-xs text-art-muted font-mono shrink-0">关联文章</span>
<div v-if="posts && posts.length > 0" class="px-4 md:px-6 py-2 bg-white/[0.02] border-b border-art-border flex items-center gap-2 shrink-0 overflow-x-auto hide-scrollbar">
<span class="text-xs text-white/50 font-mono shrink-0">关联文章</span>
<a
v-for="p in posts"
:key="p.id"
@@ -58,7 +58,7 @@
</div>
<!-- Mobile tabs -->
<div class="md:hidden flex border-b border-white/10 shrink-0">
<div class="md:hidden flex border-b border-art-border shrink-0">
<button
type="button"
class="flex-1 px-4 py-2.5 text-xs font-mono transition-colors"
@@ -84,13 +84,13 @@
<div class="flex-1 flex flex-col md:flex-row min-h-0 overflow-hidden">
<!-- Left: code panel -->
<div
class="flex flex-col min-h-0 bg-[#0d0d0d] md:border-r border-white/10"
class="flex flex-col min-h-0 bg-[#0d0d0d] md:border-r border-art-border"
:class="[
codePanelCollapsed ? 'hidden' : 'w-full md:w-1/2',
mobileTab === 'code' ? 'flex flex-1' : 'hidden md:flex'
]"
>
<div class="sticky top-0 z-10 flex justify-end px-3 md:px-4 py-2 bg-[#0d0d0d]/95 backdrop-blur-sm border-b border-white/5 shrink-0">
<div class="sticky top-0 z-10 flex justify-end px-3 md:px-4 py-2 bg-[#0d0d0d]/95 backdrop-blur-sm border-b border-art-border shrink-0">
<button
class="text-xs bg-white/10 px-2 py-1 rounded text-white/50 hover:text-white transition-colors"
@click="copyCode"
@@ -105,21 +105,21 @@
<!-- Right: preview / description -->
<div
class="relative flex flex-col min-h-0 overflow-hidden bg-[#1a1a1a]"
class="relative flex flex-col min-h-0 overflow-hidden bg-art-surface"
:class="[
codePanelCollapsed ? 'w-full md:w-full flex-1' : 'w-full md:w-1/2',
mobileTab === 'preview' ? 'flex flex-1' : 'hidden md:flex'
]"
:style="previewBackgroundStyle"
>
<div v-if="showHtmlPreview" class="hidden md:flex items-center border-b border-white/10 shrink-0">
<div v-if="showHtmlPreview" class="hidden md:flex items-center border-b border-art-border shrink-0">
<button
v-for="tab in rightTabs"
:key="tab.id"
class="flex-1 px-4 py-2.5 text-xs font-mono transition-colors"
:class="activeRightTab === tab.id
? 'text-art-accent bg-white/5 border-b-2 border-art-accent'
: 'text-white/40 hover:text-white/70'"
? 'text-art-accent bg-art-text/5 border-b-2 border-art-accent'
: 'text-art-muted hover:text-art-text/70'"
@click="activeRightTab = tab.id"
>
{{ tab.label }}
@@ -127,21 +127,21 @@
<button
v-if="showCollapseToggle"
type="button"
class="shrink-0 px-3 py-2 text-xs font-mono text-art-muted hover:text-white border-l border-white/10 transition-colors"
class="shrink-0 px-3 py-2 text-xs font-mono text-art-muted hover:text-art-text border-l border-art-border transition-colors"
@click="codePanelCollapsed = !codePanelCollapsed"
>
{{ codePanelCollapsed ? '展开源码' : '收起源码' }}
</button>
</div>
<div v-if="showHtmlPreview" class="md:hidden flex border-b border-white/10 shrink-0">
<div v-if="showHtmlPreview" class="md:hidden flex border-b border-art-border shrink-0">
<button
v-for="tab in rightTabs"
:key="tab.id"
class="flex-1 px-3 py-2 text-xs font-mono transition-colors"
:class="activeRightTab === tab.id
? 'text-art-accent bg-white/5 border-b-2 border-art-accent'
: 'text-white/40'"
? 'text-art-accent bg-art-text/5 border-b-2 border-art-accent'
: 'text-art-muted'"
@click="activeRightTab = tab.id"
>
{{ tab.label }}
@@ -154,14 +154,14 @@
sandbox="allow-scripts"
:srcdoc="code"
/>
<div v-else class="flex-1 overflow-auto p-4 md:p-8 text-white/80 min-h-0">
<div v-else class="flex-1 overflow-auto p-4 md:p-8 text-art-text/80 min-h-0">
<h4 v-if="!showHtmlPreview" class="text-sm text-art-muted mb-3">备注</h4>
<article
v-if="description"
class="snippet-description prose prose-invert prose-sm max-w-none text-art-muted leading-relaxed"
class="snippet-description prose prose-sm max-w-none text-art-muted leading-relaxed"
v-html="renderedDescription"
/>
<p v-else class="text-sm text-white/30 font-mono">暂无备注</p>
<p v-else class="text-sm text-art-muted font-mono">暂无备注</p>
</div>
</div>
</div>
@@ -300,10 +300,10 @@ const copyCode = () => {
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%)`,
linear-gradient(45deg, var(--art-checker) 25%, transparent 25%),
linear-gradient(-45deg, var(--art-checker) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--art-checker) 75%),
linear-gradient(-45deg, transparent 75%, var(--art-checker) 75%)`,
backgroundSize: '20px 20px',
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px'
}
@@ -333,7 +333,7 @@ const previewBackgroundStyle = {
}
:deep(.snippet-description.prose :not(pre) > code) {
color: #d4b383;
color: rgb(var(--art-accent));
background-color: rgba(212, 179, 131, 0.1);
padding: 0.2em 0.4em;
border-radius: 0.3em;
@@ -344,7 +344,7 @@ const previewBackgroundStyle = {
:deep(.snippet-description.prose h1),
:deep(.snippet-description.prose h2),
:deep(.snippet-description.prose h3) {
color: #fff;
color: rgb(var(--art-text));
font-family: serif;
margin-top: 1.5em;
margin-bottom: 0.6em;
@@ -353,17 +353,17 @@ const previewBackgroundStyle = {
:deep(.snippet-description.prose h2) {
font-size: 1.25rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid var(--art-border);
padding-bottom: 0.3rem;
}
:deep(.snippet-description.prose h3) {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
color: rgb(var(--art-text) / 0.9);
}
:deep(.snippet-description.prose a) {
color: #d4b383;
color: rgb(var(--art-accent));
text-decoration: none;
border-bottom: 1px dashed rgba(212, 179, 131, 0.5);
transition: all 0.2s;
@@ -371,21 +371,21 @@ const previewBackgroundStyle = {
}
:deep(.snippet-description.prose a:hover) {
color: #fff;
color: rgb(var(--art-text));
border-bottom-style: solid;
}
:deep(.snippet-description.prose blockquote) {
border-left: 3px solid #d4b383;
background: rgba(255, 255, 255, 0.02);
border-left: 3px solid rgb(var(--art-accent));
background: var(--art-card-bg);
padding: 0.8rem 1rem;
border-radius: 0 0.5rem 0.5rem 0;
color: rgba(255, 255, 255, 0.7);
color: rgb(var(--art-muted));
font-style: italic;
}
:deep(.snippet-description.prose ul li::marker) {
color: #d4b383;
color: rgb(var(--art-accent));
}
:deep(.snippet-description.prose img) {

View File

@@ -1,26 +1,32 @@
<template>
<canvas ref="canvasRef" class="fixed inset-0 w-full h-full z-0 pointer-events-none"></canvas>
<canvas ref="canvasRef" class="ambient-bg-canvas fixed inset-0 w-full h-full z-0 pointer-events-none"></canvas>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useTheme } from '../composables/useTheme'
const canvasRef = ref<HTMLCanvasElement | null>(null)
const { resolvedTheme } = useTheme()
// --- 配置参数 ---
const config = {
starCount: 150, // 恢复到经典数量,避免过于密集
meteorChance: 0.003, // 自然流星概率
starCount: 150,
dustCount: 80,
meteorChance: 0.003,
orbCount: 3,
}
// 类型定义
let running = false
let reduceMotion = false
let time = 0
interface Star {
x: number
y: number
radius: number
alpha: number
fadingOut: boolean
speed: number // 闪烁速度
speed: number
}
interface Meteor {
@@ -52,19 +58,54 @@ interface Particle {
size: number
}
// 状态
interface Dust {
x: number
y: number
radius: number
alpha: number
fadingOut: boolean
speed: number
baseAlpha: number
}
interface Orb {
baseX: number
baseY: number
radius: number
ampX: number
ampY: number
phaseX: number
phaseY: number
speed: number
alpha: number
color: string
}
let ctx: CanvasRenderingContext2D | null = null
let animationFrameId: number
let width = 0
let height = 0
// 对象池
const stars: Star[] = []
const meteors: Meteor[] = []
const waves: Wave[] = []
const particles: Particle[] = []
const dust: Dust[] = []
const orbs: Orb[] = []
const prefersReducedMotion = () =>
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
const clearPools = () => {
stars.length = 0
meteors.length = 0
waves.length = 0
particles.length = 0
dust.length = 0
orbs.length = 0
}
// 初始化星星 (经典随机分布)
const initStars = () => {
stars.length = 0
for (let i = 0; i < config.starCount; i++) {
@@ -74,56 +115,78 @@ const initStars = () => {
radius: Math.random() * 1.5,
alpha: Math.random(),
fadingOut: Math.random() > 0.5,
speed: Math.random() * 0.02 + 0.005
speed: Math.random() * 0.02 + 0.005,
})
}
}
// 渲染循环
const render = () => {
if (!ctx || !canvasRef.value) return
// 1. 清空画布
ctx.clearRect(0, 0, width, height)
// 2. 绘制星星 (经典呼吸闪烁)
ctx.fillStyle = '#FFF'
stars.forEach(star => {
if (star.fadingOut) {
star.alpha -= star.speed
if (star.alpha < 0) {
star.alpha = 0
star.fadingOut = false
const initDust = () => {
dust.length = 0
for (let i = 0; i < config.dustCount; i++) {
const baseAlpha = Math.random() * 0.35 + 0.15
dust.push({
x: Math.random() * width,
y: Math.random() * height,
radius: Math.random() * 1.8 + 0.4,
alpha: baseAlpha,
fadingOut: Math.random() > 0.5,
speed: Math.random() * 0.008 + 0.003,
baseAlpha,
})
}
}
const initOrbs = () => {
orbs.length = 0
const palette = [
'rgba(212, 179, 131, 0.22)',
'rgba(196, 150, 90, 0.16)',
'rgba(232, 200, 150, 0.18)',
]
const placements = [
{ bx: 0.15, by: 0.2 },
{ bx: 0.82, by: 0.35 },
{ bx: 0.45, by: 0.78 },
]
for (let i = 0; i < config.orbCount; i++) {
const p = placements[i] || { bx: Math.random(), by: Math.random() }
orbs.push({
baseX: p.bx * width,
baseY: p.by * height,
radius: Math.min(width, height) * (0.28 + Math.random() * 0.12),
ampX: 40 + Math.random() * 60,
ampY: 30 + Math.random() * 50,
phaseX: Math.random() * Math.PI * 2,
phaseY: Math.random() * Math.PI * 2,
speed: 0.00025 + Math.random() * 0.0002,
alpha: 0.55 + Math.random() * 0.25,
color: palette[i % palette.length],
})
}
}
const initForTheme = () => {
clearPools()
if (resolvedTheme.value === 'dark') {
initStars()
} else {
star.alpha += star.speed
if (star.alpha > 1) {
star.alpha = 1
star.fadingOut = true
initOrbs()
initDust()
}
}
ctx!.globalAlpha = star.alpha
ctx!.beginPath()
ctx!.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
ctx!.fill()
const createMeteor = () => {
meteors.push({
x: Math.random() * width,
y: 0,
length: Math.random() * 80 + 20,
speed: Math.random() * 10 + 5,
angle: Math.PI / 4,
alpha: 1,
width: Math.random() * 1.5 + 0.5,
})
ctx.globalAlpha = 1
// 3. 绘制流星
updateAndDrawMeteors()
// 4. 绘制波纹 (Shockwave - 随机颜色)
updateAndDrawWaves()
// 5. 绘制粒子 (Sparks - 物理效果)
updateAndDrawParticles()
animationFrameId = requestAnimationFrame(render)
}
// --- 辅助绘制函数 ---
const updateAndDrawMeteors = () => {
if (Math.random() < config.meteorChance) createMeteor()
@@ -134,7 +197,6 @@ const updateAndDrawMeteors = () => {
const endX = m.x - m.length * Math.cos(m.angle)
const endY = m.y - m.length * Math.sin(m.angle)
// 流星尾巴渐变
const gradient = ctx.createLinearGradient(m.x, m.y, endX, endY)
gradient.addColorStop(0, `rgba(255, 255, 255, ${m.alpha})`)
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
@@ -155,19 +217,20 @@ const updateAndDrawMeteors = () => {
}
}
const updateAndDrawWaves = () => {
const updateAndDrawWaves = (isLight: boolean) => {
for (let i = waves.length - 1; i >= 0; i--) {
const w = waves[i]
if (!ctx) break
ctx.beginPath()
ctx.arc(w.x, w.y, w.radius, 0, Math.PI * 2)
// 使用 HSL 随机色
ctx.strokeStyle = `hsla(${w.hue}, 70%, 60%, ${w.alpha})`
ctx.strokeStyle = isLight
? `hsla(${w.hue}, 45%, 48%, ${w.alpha * 0.85})`
: `hsla(${w.hue}, 70%, 60%, ${w.alpha})`
ctx.lineWidth = w.lineWidth
ctx.stroke()
w.radius += 2
w.radius += isLight ? 1.6 : 2
w.alpha -= 0.02
w.lineWidth *= 0.95
@@ -187,10 +250,9 @@ const updateAndDrawParticles = () => {
p.x += p.vx
p.y += p.vy
p.vy += 0.05 // 重力
p.vy += 0.05
p.vx *= 0.96
p.vy *= 0.96
p.life -= 0.02
p.size *= 0.96
@@ -198,38 +260,130 @@ const updateAndDrawParticles = () => {
}
}
const createMeteor = () => {
const startX = Math.random() * width
const angle = Math.PI / 4 // 45度
meteors.push({
x: startX,
y: 0,
length: Math.random() * 80 + 20,
speed: Math.random() * 10 + 5,
angle: angle,
alpha: 1,
width: Math.random() * 1.5 + 0.5
})
const drawDark = () => {
if (!ctx) return
ctx.fillStyle = '#FFF'
stars.forEach((star) => {
if (!reduceMotion) {
if (star.fadingOut) {
star.alpha -= star.speed
if (star.alpha < 0) {
star.alpha = 0
star.fadingOut = false
}
} else {
star.alpha += star.speed
if (star.alpha > 1) {
star.alpha = 1
star.fadingOut = true
}
}
}
// 交互处理
const handleClick = (e: MouseEvent) => {
const hue = Math.floor(Math.random() * 360)
ctx!.globalAlpha = star.alpha
ctx!.beginPath()
ctx!.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
ctx!.fill()
})
ctx.globalAlpha = 1
if (!reduceMotion) {
updateAndDrawMeteors()
}
updateAndDrawWaves(false)
updateAndDrawParticles()
}
const drawLight = () => {
if (!ctx) return
// Aurora orbs
orbs.forEach((orb) => {
const x = reduceMotion
? orb.baseX
: orb.baseX + Math.sin(time * orb.speed + orb.phaseX) * orb.ampX
const y = reduceMotion
? orb.baseY
: orb.baseY + Math.cos(time * orb.speed * 0.85 + orb.phaseY) * orb.ampY
const mid = orb.color.replace(/,\s*[\d.]+\)$/, ', 0.08)')
const gradient = ctx!.createRadialGradient(x, y, 0, x, y, orb.radius)
gradient.addColorStop(0, orb.color)
gradient.addColorStop(0.45, mid)
gradient.addColorStop(1, 'rgba(212, 179, 131, 0)')
ctx!.globalAlpha = orb.alpha
ctx!.fillStyle = gradient
ctx!.beginPath()
ctx!.arc(x, y, orb.radius, 0, Math.PI * 2)
ctx!.fill()
})
ctx.globalAlpha = 1
// Gold dust
dust.forEach((d) => {
if (!reduceMotion) {
if (d.fadingOut) {
d.alpha -= d.speed
if (d.alpha < d.baseAlpha * 0.25) {
d.alpha = d.baseAlpha * 0.25
d.fadingOut = false
}
} else {
d.alpha += d.speed
if (d.alpha > d.baseAlpha) {
d.alpha = d.baseAlpha
d.fadingOut = true
}
}
}
ctx!.globalAlpha = d.alpha
ctx!.fillStyle = 'rgb(196, 155, 95)'
ctx!.beginPath()
ctx!.arc(d.x, d.y, d.radius, 0, Math.PI * 2)
ctx!.fill()
})
ctx.globalAlpha = 1
updateAndDrawWaves(true)
updateAndDrawParticles()
}
const render = () => {
if (!running || !ctx || !canvasRef.value) return
ctx.clearRect(0, 0, width, height)
if (!reduceMotion) time += 16
if (resolvedTheme.value === 'dark') {
drawDark()
} else {
drawLight()
}
animationFrameId = requestAnimationFrame(render)
}
const handleClick = (e: MouseEvent) => {
if (!running || reduceMotion) return
const isLight = resolvedTheme.value === 'light'
const hue = isLight ? 35 + Math.floor(Math.random() * 12) : Math.floor(Math.random() * 360)
// 1. 波纹
waves.push({
x: e.clientX,
y: e.clientY,
radius: 0,
alpha: 1,
lineWidth: 3,
hue: hue
alpha: isLight ? 0.9 : 1,
lineWidth: isLight ? 2.5 : 3,
hue,
})
// 2. 粒子
const particleCount = 10
for (let i = 0; i < particleCount; i++) {
const angle = (Math.PI * 2 / particleCount) * i
const angle = ((Math.PI * 2) / particleCount) * i
const speed = Math.random() * 3 + 1
particles.push({
x: e.clientX,
@@ -237,41 +391,75 @@ const handleClick = (e: MouseEvent) => {
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
color: `hsla(${hue}, 80%, 70%, 1)`, // 粒子跟随波纹颜色
size: Math.random() * 2 + 1
color: isLight
? `hsla(${hue}, 50%, 52%, 0.9)`
: `hsla(${hue}, 80%, 70%, 1)`,
size: Math.random() * 2 + 1,
})
}
// 3. 点击触发流星
createMeteor()
if (!isLight) createMeteor()
}
const handleResize = () => {
if (canvasRef.value) {
if (!canvasRef.value) return
width = window.innerWidth
height = window.innerHeight
canvasRef.value.width = width
canvasRef.value.height = height
initStars()
initForTheme()
}
const startLoop = () => {
if (running) return
running = true
render()
}
const stopLoop = () => {
running = false
cancelAnimationFrame(animationFrameId)
if (ctx && canvasRef.value) {
ctx.clearRect(0, 0, width, height)
}
}
const syncMotionPreference = () => {
reduceMotion = prefersReducedMotion()
}
let motionQuery: MediaQueryList | null = null
const onMotionChange = () => {
syncMotionPreference()
}
onMounted(() => {
if (canvasRef.value) {
if (!canvasRef.value) return
ctx = canvasRef.value.getContext('2d')
syncMotionPreference()
handleResize()
window.addEventListener('resize', handleResize)
window.addEventListener('click', handleClick)
render()
}
motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
motionQuery.addEventListener('change', onMotionChange)
// Always run ambient layer for both themes (static draw when reduced motion)
startLoop()
})
watch(resolvedTheme, () => {
clearPools()
initForTheme()
if (!running) startLoop()
})
onUnmounted(() => {
cancelAnimationFrame(animationFrameId)
stopLoop()
window.removeEventListener('resize', handleResize)
window.removeEventListener('click', handleClick)
motionQuery?.removeEventListener('change', onMotionChange)
})
</script>

View File

@@ -8,13 +8,13 @@
<div class="absolute inset-0 bg-black/80 backdrop-blur-sm" @click="close" />
<div
class="relative z-10 w-full max-w-2xl max-h-[90vh] overflow-y-auto admin-card border border-white/10 shadow-2xl animate-reveal custom-scrollbar"
class="relative z-10 w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-art-surface border border-art-border rounded-lg shadow-2xl animate-reveal custom-scrollbar"
role="dialog"
aria-modal="true"
>
<button
type="button"
class="absolute top-4 right-4 z-10 text-white/40 hover:text-white transition-colors"
class="absolute top-4 right-4 z-10 text-art-muted hover:text-art-text transition-colors"
aria-label="关闭"
@click="close"
>
@@ -25,17 +25,17 @@
<div v-else-if="error" class="p-12 text-center text-art-error">{{ error }}</div>
<template v-else-if="profile">
<!-- Header -->
<div class="p-6 md:p-8 border-b border-white/5">
<div class="p-6 md:p-8 border-b border-art-border">
<div class="flex items-start gap-4 md:gap-6">
<div class="shrink-0 w-16 h-16 md:w-20 md:h-20 rounded-full overflow-hidden border border-white/20 bg-white/5 flex items-center justify-center">
<div class="shrink-0 w-16 h-16 md:w-20 md:h-20 rounded-full overflow-hidden border border-art-border bg-art-text/5 flex items-center justify-center">
<img v-if="profile.avatar" :src="profile.avatar" :alt="profile.username" class="w-full h-full object-cover" />
<span v-else class="text-2xl font-semibold text-art-accent uppercase">{{ profile.username.charAt(0) }}</span>
</div>
<div class="min-w-0 flex-1 pt-1">
<h2 class="text-xl md:text-2xl font-serif text-white mb-1">{{ profile.username }}</h2>
<h2 class="text-xl md:text-2xl font-serif text-art-text mb-1">{{ profile.username }}</h2>
<p v-if="profile.email" class="text-sm text-art-muted truncate">{{ profile.email }}</p>
<p v-if="profile.phone" class="text-sm text-art-muted mt-0.5">{{ profile.phone }}</p>
<p v-if="profile.bio" class="text-sm text-white/70 mt-3 leading-relaxed">{{ profile.bio }}</p>
<p v-if="profile.bio" class="text-sm text-art-text/70 mt-3 leading-relaxed">{{ profile.bio }}</p>
</div>
</div>
@@ -48,14 +48,14 @@
>
<button
type="button"
class="text-sm text-art-accent hover:text-white transition-colors link-underline cursor-pointer"
class="text-sm text-art-accent hover:text-art-text transition-colors link-underline cursor-pointer"
>
点击查看微信二维码加好友
</button>
<Transition name="fade">
<div
v-if="showQr"
class="absolute left-0 top-full mt-2 z-20 p-4 bg-art-surface border border-white/10 rounded-lg shadow-xl min-w-[180px]"
class="absolute left-0 top-full mt-2 z-20 p-4 bg-art-surface border border-art-border rounded-lg shadow-xl min-w-[180px]"
>
<img
v-if="qrImageSrc"
@@ -73,7 +73,7 @@
<!-- Posts -->
<div class="p-6 md:p-8 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 class="text-sm font-medium text-white/80 mb-3 uppercase tracking-wider">最近发布</h3>
<h3 class="text-sm font-medium text-art-text/80 mb-3 uppercase tracking-wider">最近发布</h3>
<ul v-if="recentPosts.length" class="space-y-2">
<li v-for="post in recentPosts" :key="post.id">
<router-link
@@ -88,7 +88,7 @@
<p v-else class="text-xs text-art-muted">暂无文章</p>
</div>
<div>
<h3 class="text-sm font-medium text-white/80 mb-3 uppercase tracking-wider">浏览最多</h3>
<h3 class="text-sm font-medium text-art-text/80 mb-3 uppercase tracking-wider">浏览最多</h3>
<ul v-if="popularPosts.length" class="space-y-2">
<li v-for="post in popularPosts" :key="post.id">
<router-link
@@ -97,7 +97,7 @@
@click="close"
>
{{ post.title }}
<span v-if="post.readCount" class="text-xs text-white/30 ml-1">({{ post.readCount }})</span>
<span v-if="post.readCount" class="text-xs text-art-muted ml-1">({{ post.readCount }})</span>
</router-link>
</li>
</ul>

View File

@@ -14,20 +14,20 @@
>
<label
v-if="variant === 'inquiry'"
class="absolute left-0 top-2 text-white/30 text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-white/50 peer-autofill:-top-4 peer-autofill:text-white/50"
class="absolute left-0 top-2 text-art-muted text-xs font-mono transition-all pointer-events-none peer-focus:-top-4 peer-focus:text-art-accent peer-[:not(:placeholder-shown)]:-top-4 peer-[:not(:placeholder-shown)]:text-art-muted peer-autofill:-top-4 peer-autofill:text-art-muted"
>{{ label }}</label>
<!-- Autocomplete Dropdown -->
<div v-if="showDropdown && (suggestions.length > 0 || matchedUsers.length > 0)" class="absolute z-[9999] left-0 right-0 top-full mt-1 bg-[#1a1a1a] border border-white/10 shadow-lg max-h-48 overflow-y-auto rounded-b-md">
<div v-if="matchedUsers.length > 0" class="border-b border-white/5">
<div v-if="showDropdown && (suggestions.length > 0 || matchedUsers.length > 0)" class="absolute z-[9999] left-0 right-0 top-full mt-1 bg-art-surface border border-art-border shadow-lg max-h-48 overflow-y-auto rounded-b-md">
<div v-if="matchedUsers.length > 0" class="border-b border-art-border">
<div class="px-3 py-1.5 text-[10px] text-art-muted uppercase tracking-wider">已注册用户</div>
<div
v-for="user in matchedUsers"
:key="user.id"
class="px-4 py-2 text-sm text-white/70 hover:bg-art-accent hover:text-black cursor-pointer transition-colors flex items-center gap-2"
class="px-4 py-2 text-sm text-art-text/70 hover:bg-art-accent hover:text-black cursor-pointer transition-colors flex items-center gap-2"
@mousedown.prevent="selectUser(user)"
>
<div class="w-6 h-6 rounded-full overflow-hidden bg-white/10 shrink-0 flex items-center justify-center text-xs text-art-accent">
<div class="w-6 h-6 rounded-full overflow-hidden bg-art-text/10 shrink-0 flex items-center justify-center text-xs text-art-accent">
<img v-if="user.avatar" :src="user.avatar" :alt="user.username" class="w-full h-full object-cover" />
<span v-else>{{ user.username.charAt(0).toUpperCase() }}</span>
</div>
@@ -40,7 +40,7 @@
<div
v-for="(suggestion, index) in suggestions"
:key="'s-' + index"
class="px-4 py-2 text-sm text-white/70 hover:bg-art-accent hover:text-black cursor-pointer transition-colors font-mono"
class="px-4 py-2 text-sm text-art-text/70 hover:bg-art-accent hover:text-black cursor-pointer transition-colors font-mono"
@mousedown.prevent="selectSuggestion(suggestion)"
>
{{ suggestion }}
@@ -77,7 +77,7 @@ const inputClass = computed(() => {
if (props.variant === 'admin') {
return 'admin-input w-full peer'
}
return 'inquiry-input 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'
return 'inquiry-input peer w-full bg-transparent border-b border-art-border py-2 text-art-text font-serif text-lg focus:border-art-accent outline-none transition-colors placeholder-transparent'
})
const showDropdown = ref(false)
@@ -146,9 +146,9 @@ const selectUser = (user: User) => {
.inquiry-input:-webkit-autofill:hover,
.inquiry-input:-webkit-autofill:focus,
.inquiry-input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 1000px #080808 inset !important;
-webkit-text-fill-color: white !important;
caret-color: white;
-webkit-box-shadow: 0 0 0 1000px rgb(var(--art-input-autofill)) inset !important;
-webkit-text-fill-color: rgb(var(--art-input-autofill-text)) !important;
caret-color: rgb(var(--art-input-autofill-text));
transition: background-color 5000s ease-in-out 0s;
}
</style>

View File

@@ -9,7 +9,7 @@
@change="$emit('update:modelValue', option.value)"
class="peer hidden"
>
<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 flex items-center gap-2">
<span class="px-4 py-2 border border-art-border rounded-none text-xs font-mono text-art-muted hover:border-art-accent hover:text-art-text transition-all peer-checked:bg-art-accent peer-checked:text-black peer-checked:border-art-accent flex items-center gap-2">
<i v-if="option.icon" :data-lucide="option.icon" class="w-3 h-3"></i>
{{ option.label }}
</span>

View File

@@ -0,0 +1,112 @@
import { computed, ref, type Ref } from 'vue'
export type ThemePref = 'system' | 'light' | 'dark'
export type ResolvedTheme = 'light' | 'dark'
const STORAGE_KEY = 'theme-pref'
const themePref = ref<ThemePref>('system')
const resolvedTheme = ref<ResolvedTheme>('dark')
const forceDark = ref(false)
let initialized = false
let mediaQuery: MediaQueryList | null = null
function getSystemTheme(): ResolvedTheme {
if (typeof window === 'undefined') return 'dark'
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
function readStoredPref(): ThemePref {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw === 'light' || raw === 'dark' || raw === 'system') return raw
} catch {
/* ignore */
}
return 'system'
}
function resolve(pref: ThemePref): ResolvedTheme {
if (forceDark.value) return 'dark'
if (pref === 'system') return getSystemTheme()
return pref
}
function applyToDocument(theme: ResolvedTheme) {
const root = document.documentElement
root.classList.toggle('dark', theme === 'dark')
root.style.colorScheme = theme
resolvedTheme.value = theme
}
function recompute() {
applyToDocument(resolve(themePref.value))
}
function onSystemChange() {
if (themePref.value === 'system' && !forceDark.value) {
recompute()
}
}
export function setForceDark(enabled: boolean) {
forceDark.value = enabled
recompute()
}
export function setThemePref(pref: ThemePref) {
themePref.value = pref
try {
localStorage.setItem(STORAGE_KEY, pref)
} catch {
/* ignore */
}
recompute()
}
export function cycleTheme() {
const order: ThemePref[] = ['system', 'light', 'dark']
const idx = order.indexOf(themePref.value)
setThemePref(order[(idx + 1) % order.length])
}
export function initTheme() {
if (initialized) {
recompute()
return
}
initialized = true
themePref.value = readStoredPref()
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', onSystemChange)
recompute()
}
export function useTheme() {
if (typeof window !== 'undefined' && !initialized) {
initTheme()
}
const themeIcon = computed(() => {
if (themePref.value === 'system') return 'monitor'
if (themePref.value === 'light') return 'sun'
return 'moon'
})
const themeLabel = computed(() => {
if (themePref.value === 'system') return '跟随系统'
if (themePref.value === 'light') return '亮色'
return '暗色'
})
return {
themePref: themePref as Ref<ThemePref>,
resolvedTheme: resolvedTheme as Ref<ResolvedTheme>,
themeIcon,
themeLabel,
setThemePref,
cycleTheme,
setForceDark,
initTheme,
}
}

View File

@@ -9,7 +9,7 @@
<!-- Error state -->
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="fetchProfileData" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="fetchProfileData" class="px-4 py-2 bg-art-accent text-art-text rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
@@ -19,7 +19,7 @@
<div class="space-y-8">
<div>
<span class="text-art-accent font-mono text-sm tracking-widest uppercase">个人档案</span>
<h2 class="font-serif text-5xl italic text-white mt-2 mb-6">关于我</h2>
<h2 class="font-serif text-5xl italic text-art-text mt-2 mb-6">关于我</h2>
</div>
<div class="flex items-center gap-6">
<div class="w-24 h-24 rounded-full overflow-hidden border-2 border-art-accent/50 shadow-2xl">
@@ -30,22 +30,22 @@
>
</div>
<div class="space-y-2">
<h3 class="text-2xl font-bold text-white">{{ profile.name }}</h3>
<h3 class="text-2xl font-bold text-art-text">{{ profile.name }}</h3>
<div class="flex items-center gap-2 text-sm text-art-muted">
<i data-lucide="map-pin" class="w-4 h-4 text-art-accent"></i>
<span>{{ profile.location }}</span>
</div>
</div>
</div>
<div class="prose prose-invert text-art-muted font-light leading-relaxed">
<div class="prose text-art-muted font-light leading-relaxed">
<p v-html="profile.bio"></p>
</div>
<div class="bg-white/5 border border-white/5 p-6 rounded-xl backdrop-blur-sm mt-6">
<h4 class="text-white font-bold mb-4 flex items-center gap-2">
<div class="bg-art-text/5 border border-art-border p-6 rounded-xl backdrop-blur-sm mt-6">
<h4 class="text-art-text font-bold mb-4 flex items-center gap-2">
<i data-lucide="contact" class="w-4 h-4 text-art-accent"></i> 联系方式
</h4>
<div class="space-y-3 text-sm">
<a :href="'mailto:' + profile.contact.email" class="flex items-center gap-2 text-art-muted hover:text-white transition-colors">
<a :href="'mailto:' + profile.contact.email" class="flex items-center gap-2 text-art-muted hover:text-art-text transition-colors">
<i data-lucide="mail" class="w-4 h-4 text-art-accent"></i>
<span>{{ profile.contact.email }}</span>
</a>
@@ -57,28 +57,28 @@
</div>
</div>
<div class="space-y-8">
<div class="bg-white/5 border border-white/5 p-8 rounded-2xl backdrop-blur-sm">
<h3 class="text-xl font-bold text-white mb-6 flex items-center gap-2">
<div class="bg-art-text/5 border border-art-border p-8 rounded-2xl backdrop-blur-sm">
<h3 class="text-xl font-bold text-art-text mb-6 flex items-center gap-2">
<i data-lucide="cpu" class="w-5 h-5 text-art-accent"></i> 技术栈
</h3>
<div class="flex flex-wrap gap-2 mb-10">
<span
v-for="(tech, index) in profile.techStack"
:key="index"
class="px-3 py-1 bg-white/5 rounded-full text-xs text-white/80 border border-white/10"
class="px-3 py-1 bg-art-text/5 rounded-full text-xs text-art-text/80 border border-art-border"
>
{{ tech }}
</span>
</div>
<h3 class="text-xl font-bold text-white mb-6 flex items-center gap-2">
<h3 class="text-xl font-bold text-art-text mb-6 flex items-center gap-2">
<i data-lucide="briefcase" class="w-5 h-5 text-art-accent"></i> 经历
</h3>
<div class="space-y-6 relative border-l border-white/10 ml-2 pl-6">
<div class="space-y-6 relative border-l border-art-border ml-2 pl-6">
<div v-for="(exp, index) in profile.experiences" :key="index" class="relative">
<div class="absolute -left-[29px] top-1.5 w-3 h-3 rounded-full" :class="index === 0 ? 'bg-art-accent' : 'bg-white/20'"></div>
<div class="absolute -left-[29px] top-1.5 w-3 h-3 rounded-full" :class="index === 0 ? 'bg-art-accent' : 'bg-art-text/20'"></div>
<div class="text-sm font-mono mb-1" :class="index === 0 ? 'text-art-accent' : 'text-art-muted'">{{ exp.year }}</div>
<div class="text-white font-bold">{{ exp.role }}</div>
<div class="text-art-text font-bold">{{ exp.role }}</div>
<div class="text-xs text-art-muted">{{ exp.company }}</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@
<section id="blog" class="page-section block animate-slide-down">
<div class="max-w-4xl mx-auto pt-32 px-6 pb-20">
<div class="mb-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">深度思考</h2>
<h2 class="font-serif text-5xl italic text-art-text mb-6">深度思考</h2>
<p class="text-art-muted max-w-lg mx-auto mb-8">关于前端技术交互设计以及数字艺术的深度思考</p>
<!-- Search UI -->
@@ -14,7 +14,7 @@
@blur="handleSearchBlur"
@keyup.enter="handleSearch"
placeholder="搜索文章标题或内容..."
class="w-full bg-white/5 border border-white/10 rounded-full px-6 py-3 text-white placeholder-white/30 focus:outline-none focus:border-art-accent transition-colors"
class="w-full bg-art-text/5 border border-art-border rounded-full px-6 py-3 text-art-text placeholder-art-muted focus:outline-none focus:border-art-accent transition-colors"
/>
<SearchSuggestDropdown
:visible="showSearchSuggest"
@@ -28,13 +28,13 @@
<button
v-if="searchQuery"
@click="clearSearch"
class="absolute right-12 top-1/2 -translate-y-1/2 text-white/30 hover:text-white transition-colors"
class="absolute right-12 top-1/2 -translate-y-1/2 text-art-muted hover:text-art-text transition-colors"
>
<i data-lucide="x" class="w-4 h-4"></i>
</button>
<button
@click="handleSearch"
class="absolute right-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-art-accent transition-colors"
class="absolute right-4 top-1/2 -translate-y-1/2 text-art-muted hover:text-art-accent transition-colors"
>
<i data-lucide="search" class="w-5 h-5"></i>
</button>
@@ -69,7 +69,7 @@
<button
v-if="hasActiveFilters"
@click="clearFilters"
class="text-xs text-art-accent hover:text-white transition-colors px-3 py-1 border border-art-accent/30 rounded hover:border-art-accent"
class="text-xs text-art-accent hover:text-art-text transition-colors px-3 py-1 border border-art-accent/30 rounded hover:border-art-accent"
>
清除筛选
</button>
@@ -84,7 +84,7 @@
<!-- Error state -->
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="() => fetchBlogPosts(true)" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="() => fetchBlogPosts(true)" class="px-4 py-2 bg-art-accent text-art-text rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
@@ -103,7 +103,7 @@
<article
v-for="post in blogPosts"
:key="post.id"
class="group cursor-pointer border-b border-white/5 pb-12"
class="group cursor-pointer border-b border-art-border pb-12"
@click="$router.push('/blog/' + post.id)"
>
<div class="flex flex-col md:flex-row gap-8 items-start">
@@ -120,15 +120,15 @@
size="sm"
/>
<div v-if="post.tags && post.tags.length > 0" class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in post.tags" :key="tag.id" class="text-xs text-white/40 bg-white/5 px-1 rounded">#{{ tag.name }}</span>
<span v-for="tag in post.tags" :key="tag.id" class="text-xs text-art-muted bg-art-text/5 px-1 rounded">#{{ tag.name }}</span>
</div>
</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" v-html="highlightText(post.title)">
<h3 class="font-serif text-3xl text-art-text group-hover:text-art-accent transition-colors leading-tight" v-html="highlightText(post.title)">
</h3>
<p class="text-art-muted font-light leading-relaxed" v-html="highlightText(post.excerpt)">
</p>
<div class="text-xs text-art-accent font-medium mt-4 group-hover:text-white transition-colors">阅读全文 -></div>
<div class="text-xs text-art-accent font-medium mt-4 group-hover:text-art-text transition-colors">阅读全文 -></div>
</div>
</div>
</article>

View File

@@ -4,7 +4,7 @@
<!-- 小屏fixed 返回 -->
<button
@click="$router.push('/blog')"
class="xl:hidden fixed top-20 md:top-24 left-4 md:left-6 z-40 group flex items-center gap-2 rounded-full border border-white/10 bg-[#050505]/80 backdrop-blur-md px-3 py-2 text-art-muted hover:text-white transition-colors"
class="xl:hidden fixed top-20 md:top-24 left-4 md:left-6 z-40 group flex items-center gap-2 rounded-full border border-art-border bg-art-bg/80 backdrop-blur-md px-3 py-2 text-art-muted hover:text-art-text transition-colors"
>
<Icon name="arrow-left" :size="16" class="transform group-hover:-translate-x-1 transition-transform" />
<span class="text-sm font-medium tracking-wide">返回列表</span>
@@ -15,7 +15,7 @@
<button
v-if="loading || error"
@click="$router.push('/blog')"
class="hidden xl:flex fixed top-32 left-[max(1.5rem,calc((100vw-90rem)/2+1.5rem))] z-40 group items-center gap-2 rounded-full border border-white/10 bg-[#050505]/80 backdrop-blur-md px-3 py-2 text-art-muted hover:text-white transition-colors"
class="hidden xl:flex fixed top-32 left-[max(1.5rem,calc((100vw-90rem)/2+1.5rem))] z-40 group items-center gap-2 rounded-full border border-art-border bg-art-bg/80 backdrop-blur-md px-3 py-2 text-art-muted hover:text-art-text transition-colors"
>
<Icon name="arrow-left" :size="16" class="transform group-hover:-translate-x-1 transition-transform" />
<span class="text-sm font-medium tracking-wide">返回列表</span>
@@ -29,10 +29,10 @@
<!-- Error state -->
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="fetchBlogDetail" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="fetchBlogDetail" class="px-4 py-2 bg-art-accent text-art-text rounded hover:bg-opacity-80 transition-colors">
重试
</button>
<button @click="$router.push('/blog')" class="mt-4 px-4 py-2 border border-white/30 text-white rounded hover:bg-white/5 transition-colors">
<button @click="$router.push('/blog')" class="mt-4 px-4 py-2 border border-art-border text-art-text rounded hover:bg-art-text/5 transition-colors">
返回文章列表
</button>
</div>
@@ -44,15 +44,15 @@
<aside class="hidden xl:block w-64 flex-shrink-0 order-1 sticky top-32 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar">
<button
@click="$router.push('/blog')"
class="group flex items-center gap-2 rounded-full border border-white/10 bg-white/5 backdrop-blur-md px-3 py-2 mb-6 text-art-muted hover:text-white hover:bg-white/10 transition-colors w-fit"
class="group flex items-center gap-2 rounded-full border border-art-border bg-art-text/5 backdrop-blur-md px-3 py-2 mb-6 text-art-muted hover:text-art-text hover:bg-art-text/10 transition-colors w-fit"
>
<Icon name="arrow-left" :size="16" class="transform group-hover:-translate-x-1 transition-transform" />
<span class="text-sm font-medium tracking-wide">返回列表</span>
</button>
<div class="space-y-6">
<!-- 专栏跳转卡片 -->
<div v-if="post.columnId && post.columnName" class="p-5 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-white/10">
<h3 class="text-xs font-bold text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<div v-if="post.columnId && post.columnName" class="p-5 rounded-xl bg-art-text/5 border border-art-border backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-art-text/10">
<h3 class="text-xs font-bold text-art-text uppercase tracking-wider mb-3 flex items-center gap-2">
<Icon name="book-open" :size="14" class="text-art-accent" />
所属专栏
</h3>
@@ -61,9 +61,9 @@
<a
@click.prevent="router.push(`/columns/${post.columnId}`)"
href="#"
class="block p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group mb-4"
class="block p-3 rounded-lg bg-art-text/5 border border-art-border hover:bg-art-text/10 hover:border-art-accent/50 transition-all duration-200 group mb-4"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-1">
<div class="text-sm font-medium text-art-text group-hover:text-art-accent transition-colors mb-1">
{{ post.columnName }}
</div>
<div class="text-xs text-art-muted flex items-center gap-1">
@@ -77,7 +77,7 @@
<!-- 展开/收起按钮 -->
<button
@click="isColumnExpanded = !isColumnExpanded"
class="w-full flex items-center justify-between text-xs text-art-muted hover:text-white transition-colors py-1 px-2 rounded hover:bg-white/5"
class="w-full flex items-center justify-between text-xs text-art-muted hover:text-art-text transition-colors py-1 px-2 rounded hover:bg-art-text/5"
>
<span class="font-medium">文章目录</span>
<div class="flex items-center gap-1">
@@ -91,7 +91,7 @@
</button>
<!-- 文章列表 -->
<nav class="space-y-0.5 border-t border-white/10 pt-2">
<nav class="space-y-0.5 border-t border-art-border pt-2">
<a
v-for="columnPost in displayedColumnPosts"
:key="columnPost.id"
@@ -101,8 +101,8 @@
class="block text-xs py-2 px-3 rounded transition-all duration-200 relative group"
:class="[
columnPost.id === post.id
? 'text-white pl-4'
: 'text-art-muted hover:text-white hover:bg-white/5'
? 'text-art-text pl-4'
: 'text-art-muted hover:text-art-text hover:bg-art-text/5'
]"
>
<div class="line-clamp-2 leading-relaxed">
@@ -120,17 +120,17 @@
</div>
<!-- 分类搜索卡片 -->
<div v-if="post.categoryId && post.categoryName" class="p-5 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-white/10">
<h3 class="text-xs font-bold text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<div v-if="post.categoryId && post.categoryName" class="p-5 rounded-xl bg-art-text/5 border border-art-border backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-art-text/10">
<h3 class="text-xs font-bold text-art-text uppercase tracking-wider mb-3 flex items-center gap-2">
<Icon name="folder" :size="14" class="text-art-accent" />
分类
</h3>
<a
@click.prevent="handleCategoryClick(post.categoryId)"
href="#"
class="block p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-200 group"
class="block p-3 rounded-lg bg-art-text/5 border border-art-border hover:bg-art-text/10 hover:border-art-accent/50 transition-all duration-200 group"
>
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors mb-1">
<div class="text-sm font-medium text-art-text group-hover:text-art-accent transition-colors mb-1">
{{ post.categoryName }}
</div>
<div class="text-xs text-art-muted flex items-center gap-1">
@@ -141,8 +141,8 @@
</div>
<!-- 标签搜索卡片 -->
<div v-if="post.tags && post.tags.length > 0" class="p-5 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-white/10">
<h3 class="text-xs font-bold text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<div v-if="post.tags && post.tags.length > 0" class="p-5 rounded-xl bg-art-text/5 border border-art-border backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-art-text/10">
<h3 class="text-xs font-bold text-art-text uppercase tracking-wider mb-3 flex items-center gap-2">
<Icon name="tags" :size="14" class="text-art-accent" />
标签
</h3>
@@ -152,7 +152,7 @@
:key="tag.id"
@click.prevent="handleTagClick(tag.id)"
href="#"
class="px-2.5 py-1 rounded-md bg-white/5 border border-white/10 hover:bg-art-accent/20 hover:border-art-accent/50 hover:text-art-accent text-xs text-art-muted transition-all duration-200"
class="px-2.5 py-1 rounded-md bg-art-text/5 border border-art-border hover:bg-art-accent/20 hover:border-art-accent/50 hover:text-art-accent text-xs text-art-muted transition-all duration-200"
>
{{ tag.name }}
</a>
@@ -164,16 +164,16 @@
<!-- 2. 中间文章主内容 -->
<div class="flex-1 min-w-0 max-w-full order-2">
<!-- 文章头图与信息 -->
<div class="border-b border-white/5 pb-6 md:pb-8 mb-6 md:mb-10">
<div class="border-b border-art-border pb-6 md:pb-8 mb-6 md:mb-10">
<div class="flex flex-wrap items-center justify-between gap-2 md:gap-3 mb-4 md:mb-6">
<div class="flex flex-wrap items-center gap-2 md:gap-3">
<span class="px-2 py-0.5 md:px-3 md:py-1 border border-white/20 rounded-full text-[10px] md:text-xs font-mono text-art-accent uppercase tracking-wider">{{ post.categoryName || '未分类' }}</span>
<span class="px-2 py-0.5 md:px-3 md:py-1 border border-art-border rounded-full text-[10px] md:text-xs font-mono text-art-accent uppercase tracking-wider">{{ post.categoryName || '未分类' }}</span>
<span class="text-xs md:text-sm text-art-muted">{{ post.date }}</span>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="flex items-center gap-2 rounded-full border border-white/10 bg-white/5 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 text-xs md:text-sm text-art-muted hover:text-white hover:bg-white/10 hover:border-art-accent/30 transition-all duration-200"
class="flex items-center gap-2 rounded-full border border-art-border bg-art-text/5 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 text-xs md:text-sm text-art-muted hover:text-art-text hover:bg-art-text/10 hover:border-art-accent/30 transition-all duration-200"
@click="router.push(`/blog/${postId}/ppt`)"
>
<Icon name="presentation" :size="14" />
@@ -182,7 +182,7 @@
<ArticleExportMenu :post="post" :rendered-html="renderedContent" />
</div>
</div>
<h1 class="font-serif text-2xl md:text-5xl lg:text-6xl text-white leading-snug md:leading-tight mb-6 md:mb-8 break-words">{{ post.title }}</h1>
<h1 class="font-serif text-2xl md:text-5xl lg:text-6xl text-art-text leading-snug md:leading-tight mb-6 md:mb-8 break-words">{{ post.title }}</h1>
<AuthorInfo
:name="post.userName"
:email="post.userEmail"
@@ -196,14 +196,14 @@
<!-- 文章内容 -->
<article
ref="articleRef"
class="blog-content prose prose-invert min-w-0 max-w-full text-art-muted leading-relaxed prose-base md:prose-lg"
class="blog-content prose min-w-0 max-w-full text-art-muted leading-relaxed prose-base md:prose-lg"
>
<div v-html="renderedContent"></div>
</article>
<!-- 相关代码片段 -->
<div v-if="post.snippets && post.snippets.length > 0" class="mt-8 md:mt-12 border-t border-white/10 pt-8 md:pt-10">
<h3 class="text-base md:text-lg font-bold text-white mb-4 md:mb-6 flex items-center gap-2">
<div v-if="post.snippets && post.snippets.length > 0" class="mt-8 md:mt-12 border-t border-art-border pt-8 md:pt-10">
<h3 class="text-base md:text-lg font-bold text-art-text mb-4 md:mb-6 flex items-center gap-2">
<Icon name="code-2" :size="18" class="text-art-accent" />
相关代码
</h3>
@@ -212,10 +212,10 @@
v-for="snippet in post.snippets"
:key="snippet.id"
type="button"
class="w-full text-left rounded-xl overflow-hidden bg-[#0d0d0d] border border-white/10 hover:border-art-accent/50 transition-colors group"
class="w-full text-left rounded-xl overflow-hidden bg-[#0d0d0d] border border-art-border hover:border-art-accent/50 transition-colors group"
@click="openRelatedSnippet(snippet.id)"
>
<div class="flex items-center justify-between px-3 py-2 bg-white/5 border-b border-white/5 gap-2">
<div class="flex items-center justify-between px-3 py-2 bg-art-text/5 border-b border-art-border gap-2">
<div class="flex gap-1.5 shrink-0">
<div class="w-2 h-2 rounded-full bg-[#ff5f56]"></div>
<div class="w-2 h-2 rounded-full bg-[#ffbd2e]"></div>
@@ -224,16 +224,16 @@
<span class="text-[10px] font-mono text-art-accent uppercase truncate">{{ snippet.codeType?.name || snippet.type }}</span>
</div>
<div class="p-4">
<div class="text-sm font-medium text-white group-hover:text-art-accent transition-colors line-clamp-2">{{ snippet.title }}</div>
<div class="text-sm font-medium text-art-text group-hover:text-art-accent transition-colors line-clamp-2">{{ snippet.title }}</div>
</div>
</button>
</div>
</div>
<!-- 移动端专属信息补充区 -->
<div class="xl:hidden mt-8 md:mt-12 border-t border-white/10 pt-6 md:pt-8 space-y-4 md:space-y-6">
<div class="xl:hidden mt-8 md:mt-12 border-t border-art-border pt-6 md:pt-8 space-y-4 md:space-y-6">
<div v-if="post.tags && post.tags.length > 0">
<h4 class="text-xs font-bold text-white uppercase tracking-wider mb-2 md:mb-3 flex items-center gap-2">
<h4 class="text-xs font-bold text-art-text uppercase tracking-wider mb-2 md:mb-3 flex items-center gap-2">
<Icon name="tags" :size="14" class="text-art-accent" />
标签
</h4>
@@ -243,7 +243,7 @@
:key="tag.id"
@click.prevent="handleTagClick(tag.id)"
href="#"
class="px-2.5 py-1 rounded-full bg-white/5 border border-white/10 text-xs text-art-muted hover:text-white hover:bg-white/10 transition-colors"
class="px-2.5 py-1 rounded-full bg-art-text/5 border border-art-border text-xs text-art-muted hover:text-art-text hover:bg-art-text/10 transition-colors"
>
# {{ tag.name }}
</a>
@@ -253,18 +253,18 @@
<div class="flex flex-wrap gap-3 md:gap-4">
<div v-if="post.columnName" class="flex items-center gap-2 text-xs md:text-sm text-art-muted">
<Icon name="book-open" :size="14" class="text-art-accent" />
专栏: <span class="text-white">{{ post.columnName }}</span>
专栏: <span class="text-art-text">{{ post.columnName }}</span>
</div>
<div v-if="post.categoryName" class="flex items-center gap-2 text-xs md:text-sm text-art-muted">
<Icon name="folder" :size="14" class="text-art-accent" />
分类: <span class="text-white">{{ post.categoryName }}</span>
分类: <span class="text-art-text">{{ post.categoryName }}</span>
</div>
</div>
</div>
<!-- 底部推荐阅读 -->
<div class="mt-8 md:mt-20 border-t border-white/10 pt-8 md:pt-10">
<h3 class="text-base md:text-lg font-bold text-white mb-4 md:mb-6 flex items-center gap-2">
<div class="mt-8 md:mt-20 border-t border-art-border pt-8 md:pt-10">
<h3 class="text-base md:text-lg font-bold text-art-text mb-4 md:mb-6 flex items-center gap-2">
<Icon name="sparkles" :size="18" class="text-art-accent" />
推荐阅读
</h3>
@@ -286,9 +286,9 @@
:href="href"
target="_blank"
rel="noopener noreferrer"
class="block p-4 md:p-5 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 hover:border-art-accent/50 transition-all duration-300 group h-full"
class="block p-4 md:p-5 rounded-xl bg-art-text/5 border border-art-border hover:bg-art-text/10 hover:border-art-accent/50 transition-all duration-300 group h-full"
>
<div class="text-sm md:text-base font-medium text-white group-hover:text-art-accent transition-colors mb-2 md:mb-3 line-clamp-2">
<div class="text-sm md:text-base font-medium text-art-text group-hover:text-art-accent transition-colors mb-2 md:mb-3 line-clamp-2">
{{ recPost.title }}
</div>
<div class="flex items-center justify-between gap-2 mt-auto">
@@ -310,8 +310,8 @@
<!-- 3. 右侧边栏目录 (TOC) - 仅在大屏显示 -->
<aside class="hidden xl:block w-64 flex-shrink-0 order-3 sticky top-32 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar">
<div class="p-5 rounded-xl bg-white/5 border border-white/10 backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-white/10">
<h3 class="text-xs font-bold text-white uppercase tracking-wider mb-4 flex items-center gap-2">
<div class="p-5 rounded-xl bg-art-text/5 border border-art-border backdrop-blur-md shadow-lg transition-all duration-300 hover:bg-art-text/10">
<h3 class="text-xs font-bold text-art-text uppercase tracking-wider mb-4 flex items-center gap-2">
<Icon name="list" :size="14" class="text-art-accent" />
目录导航
</h3>
@@ -322,12 +322,12 @@
v-if="toc.length > 0"
></div>
<ul class="space-y-1 border-l border-white/10 ml-[1px]">
<ul class="space-y-1 border-l border-art-border ml-[1px]">
<li v-for="item in toc" :key="item.id">
<a
@click.prevent="scrollToHeading(item.id)"
href="#"
class="block text-xs py-1.5 transition-all duration-200 border-l-2 border-transparent pl-4 hover:text-white truncate"
class="block text-xs py-1.5 transition-all duration-200 border-l-2 border-transparent pl-4 hover:text-art-text truncate"
:class="[
activeId === item.id ? 'text-art-accent font-medium' : 'text-art-muted',
item.level > 2 ? 'pl-8' : 'pl-4'
@@ -352,7 +352,7 @@
<button
v-if="toc.length > 0 && !loading && !error"
@click="showMobileToc = true"
class="xl:hidden fixed bottom-6 right-4 z-50 w-10 h-10 md:w-12 md:h-12 bg-art-accent/90 backdrop-blur text-white rounded-full shadow-lg shadow-art-accent/20 flex items-center justify-center hover:bg-art-accent active:scale-95 transition-all duration-200"
class="xl:hidden fixed bottom-6 right-4 z-50 w-10 h-10 md:w-12 md:h-12 bg-art-accent/90 backdrop-blur text-art-text rounded-full shadow-lg shadow-art-accent/20 flex items-center justify-center hover:bg-art-accent active:scale-95 transition-all duration-200"
aria-label="目录"
>
<Icon name="list" :size="18" />
@@ -361,13 +361,13 @@
<!-- Mobile TOC Drawer -->
<div v-if="showMobileToc" class="fixed inset-0 z-[100] xl:hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity" @click="showMobileToc = false"></div>
<div class="absolute bottom-0 left-0 right-0 bg-[#1a1a1a] border-t border-white/10 rounded-t-2xl max-h-[70vh] flex flex-col animate-slide-up shadow-2xl">
<div class="flex items-center justify-between p-4 border-b border-white/10">
<h3 class="text-sm font-bold text-white flex items-center gap-2">
<div class="absolute bottom-0 left-0 right-0 bg-art-surface border-t border-art-border rounded-t-2xl max-h-[70vh] flex flex-col animate-slide-up shadow-2xl">
<div class="flex items-center justify-between p-4 border-b border-art-border">
<h3 class="text-sm font-bold text-art-text flex items-center gap-2">
<Icon name="list" :size="16" class="text-art-accent" />
文章目录
</h3>
<button @click="showMobileToc = false" class="text-art-muted hover:text-white p-2">
<button @click="showMobileToc = false" class="text-art-muted hover:text-art-text p-2">
<Icon name="x" :size="20" />
</button>
</div>
@@ -379,7 +379,7 @@
<a
@click.prevent="scrollToHeading(item.id); showMobileToc = false"
href="#"
class="block text-sm py-1 transition-all duration-200 hover:text-white"
class="block text-sm py-1 transition-all duration-200 hover:text-art-text"
:class="[
activeId === item.id ? 'text-art-accent font-medium' : 'text-art-muted',
item.level > 2 ? 'pl-4' : ''
@@ -812,7 +812,7 @@ onBeforeUnmount(() => {
}
:deep(.prose :not(pre) > code) {
color: #d4b383;
color: rgb(var(--art-accent));
background-color: rgba(212, 179, 131, 0.1);
padding: 0.2em 0.4em;
border-radius: 0.3em;
@@ -822,7 +822,7 @@ onBeforeUnmount(() => {
}
:deep(.prose h1), :deep(.prose h2), :deep(.prose h3) {
color: #fff;
color: rgb(var(--art-text));
font-family: serif;
margin-top: 1.5em;
margin-bottom: 0.6em;
@@ -840,7 +840,7 @@ onBeforeUnmount(() => {
:deep(.prose h2) {
font-size: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid var(--art-border);
padding-bottom: 0.3rem;
}
@media (min-width: 768px) {
@@ -852,7 +852,7 @@ onBeforeUnmount(() => {
:deep(.prose h3) {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.9);
color: rgb(var(--art-text) / 0.9);
}
@media (min-width: 768px) {
:deep(.prose h3) {
@@ -861,7 +861,7 @@ onBeforeUnmount(() => {
}
:deep(.prose a) {
color: #d4b383;
color: rgb(var(--art-accent));
text-decoration: none;
border-bottom: 1px dashed rgba(212, 179, 131, 0.5);
transition: all 0.2s;
@@ -869,16 +869,16 @@ onBeforeUnmount(() => {
}
:deep(.prose a:hover) {
color: #fff;
color: rgb(var(--art-text));
border-bottom-style: solid;
}
:deep(.prose blockquote) {
border-left: 3px solid #d4b383;
background: rgba(255, 255, 255, 0.02);
border-left: 3px solid rgb(var(--art-accent));
background: var(--art-card-bg);
padding: 0.8rem 1rem;
border-radius: 0 0.5rem 0.5rem 0;
color: rgba(255, 255, 255, 0.7);
color: rgb(var(--art-muted));
font-style: italic;
}
@media (min-width: 768px) {
@@ -888,7 +888,7 @@ onBeforeUnmount(() => {
}
:deep(.prose ul li::marker) {
color: #d4b383;
color: rgb(var(--art-accent));
}
:deep(.prose img) {
@@ -908,12 +908,12 @@ onBeforeUnmount(() => {
}
:deep(.code-block-wrapper::-webkit-scrollbar-thumb) {
background-color: rgba(255, 255, 255, 0.1);
background-color: var(--art-scroll-thumb);
border-radius: 4px;
}
:deep(.code-block-wrapper::-webkit-scrollbar-thumb:hover) {
background-color: rgba(255, 255, 255, 0.2);
background-color: var(--art-scroll-thumb-hover);
}
:deep(.code-block-wrapper::-webkit-scrollbar-corner) {

View File

@@ -2,7 +2,7 @@
<section class="page-section block animate-slide-down">
<div class="max-w-4xl mx-auto pt-32 px-6 pb-20">
<div class="mb-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">分类</h2>
<h2 class="font-serif text-5xl italic text-art-text mb-6">分类</h2>
<p class="text-art-muted max-w-lg mx-auto mb-8">按照主题浏览文章</p>
</div>
@@ -16,14 +16,14 @@
<div
v-for="category in categories"
:key="category.id"
class="group p-6 rounded-lg border border-white/5 bg-white/5 hover:bg-white/10 hover:border-art-accent/30 transition-all cursor-pointer"
class="group p-6 rounded-lg border border-art-border bg-art-text/5 hover:bg-art-text/10 hover:border-art-accent/30 transition-all cursor-pointer"
@click="router.push(`/blog?category=${category.id}`)"
>
<div class="flex items-start justify-between mb-4">
<span class="text-3xl">📂</span>
<span class="font-mono text-xs text-art-muted group-hover:text-art-accent transition-colors">#{{ category.slug }}</span>
</div>
<h3 class="text-xl font-serif text-white mb-2 group-hover:text-art-accent transition-colors">{{ category.name }}</h3>
<h3 class="text-xl font-serif text-art-text mb-2 group-hover:text-art-accent transition-colors">{{ category.name }}</h3>
<p class="text-sm text-art-muted leading-relaxed line-clamp-3">{{ category.description || '暂无描述' }}</p>
</div>
</div>

View File

@@ -10,7 +10,7 @@
<!-- Header -->
<div class="mb-16 text-center">
<div class="inline-block mb-4 px-3 py-1 rounded-full border border-art-accent/30 text-art-accent text-xs font-mono">专栏</div>
<h2 class="font-serif text-4xl italic text-white mb-6">{{ column.name }}</h2>
<h2 class="font-serif text-4xl italic text-art-text mb-6">{{ column.name }}</h2>
<p class="text-art-muted max-w-lg mx-auto leading-relaxed mb-6">{{ column.description }}</p>
<!-- 统计信息 -->
@@ -31,7 +31,7 @@
<article
v-for="post in posts"
:key="post.id"
class="group cursor-pointer border-b border-white/5 pb-8"
class="group cursor-pointer border-b border-art-border pb-8"
@click="router.push('/blog/' + post.id)"
>
<div class="flex flex-col md:flex-row gap-6 items-start">
@@ -48,23 +48,23 @@
size="sm"
/>
<div v-if="post.tags && post.tags.length > 0" class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in post.tags" :key="tag.id" class="text-xs text-white/40 bg-white/5 px-1 rounded">#{{ tag.name }}</span>
<span v-for="tag in post.tags" :key="tag.id" class="text-xs text-art-muted bg-art-text/5 px-1 rounded">#{{ tag.name }}</span>
</div>
</div>
<div class="md:w-3/4 space-y-3">
<h3 class="font-serif text-2xl text-white group-hover:text-art-accent transition-colors leading-tight">
<h3 class="font-serif text-2xl text-art-text group-hover:text-art-accent transition-colors leading-tight">
{{ post.title }}
</h3>
<p class="text-art-muted font-light leading-relaxed line-clamp-2">
{{ post.excerpt }}
</p>
<div class="text-xs text-art-accent font-medium mt-4 group-hover:text-white transition-colors">阅读全文 -></div>
<div class="text-xs text-art-accent font-medium mt-4 group-hover:text-art-text transition-colors">阅读全文 -></div>
</div>
</div>
</article>
</div>
<div v-else class="text-center py-10 border border-dashed border-white/10 rounded-lg">
<div v-else class="text-center py-10 border border-dashed border-art-border rounded-lg">
<p class="text-art-muted">该专栏暂无文章</p>
</div>
</div>

View File

@@ -2,7 +2,7 @@
<section class="page-section block animate-slide-down">
<div class="max-w-6xl mx-auto pt-32 px-6 pb-20">
<div class="mb-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">专栏</h2>
<h2 class="font-serif text-5xl italic text-art-text mb-6">专栏</h2>
<p class="text-art-muted max-w-lg mx-auto mb-8">系列文章集合系统性地学习</p>
</div>
@@ -16,7 +16,7 @@
<div
v-for="column in columns"
:key="column.id"
class="group relative overflow-hidden rounded-xl border border-white/5 bg-white/5 aspect-[16/9] cursor-pointer"
class="group relative overflow-hidden rounded-xl border border-art-border bg-art-text/5 aspect-[16/9] cursor-pointer"
@click="router.push(`/columns/${column.id}`)"
>
<!-- Cover Image -->
@@ -27,14 +27,23 @@
>
<div class="absolute inset-0 bg-black/60 group-hover:bg-black/50 transition-colors duration-500"></div>
</div>
<div v-else class="absolute inset-0 bg-gradient-to-br from-white/5 to-white/10 group-hover:from-white/10 group-hover:to-white/20 transition-colors"></div>
<div v-else class="absolute inset-0 bg-gradient-to-br from-art-text/5 to-art-text/10 group-hover:from-art-text/10 group-hover:to-art-text/15 transition-colors"></div>
<!-- Content -->
<div class="absolute inset-0 p-8 flex flex-col justify-end">
<h3 class="text-3xl font-serif text-white mb-3 group-hover:text-art-accent transition-colors">{{ column.name }}</h3>
<p class="text-art-muted leading-relaxed line-clamp-2">{{ column.description || '暂无描述' }}</p>
<h3
class="text-3xl font-serif mb-3 group-hover:text-art-accent transition-colors"
:class="column.cover ? 'text-white' : 'text-art-text'"
>{{ column.name }}</h3>
<p
class="leading-relaxed line-clamp-2"
:class="column.cover ? 'text-white/70' : 'text-art-muted'"
>{{ column.description || '暂无描述' }}</p>
<div class="mt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 transform translate-y-2 group-hover:translate-y-0">
<span class="text-xs font-bold tracking-widest uppercase text-white border-b border-art-accent pb-1">查看详情</span>
<span
class="text-xs font-bold tracking-widest uppercase border-b border-art-accent pb-1"
:class="column.cover ? 'text-white' : 'text-art-text'"
>查看详情</span>
</div>
</div>
</div>

View File

@@ -4,16 +4,16 @@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 mb-24 items-center animate-reveal">
<div class="lg:col-span-7 space-y-8">
<p class="text-art-accent font-mono text-sm tracking-widest uppercase mb-4">前端架构师 & 创意开发者</p>
<h1 class="font-serif text-5xl md:text-7xl lg:text-8xl leading-[1.1] text-white text-glow">
<h1 class="font-serif text-5xl md:text-7xl lg:text-8xl leading-[1.1] text-art-text text-glow">
<span class="block italic opacity-80">设计</span>
<span class="block font-bold">铸造数字</span>
<span class="block font-bold pl-12 md:pl-24">灵魂</span>
</h1>
<p class="text-art-muted text-lg md:text-xl max-w-xl leading-relaxed mt-8 font-light">
在代码的逻辑与设计的感性之间寻找平衡我是<span class="text-white font-bold">年糕崽崽</span>不仅仅构建页面更在构建<span class="text-white border-b border-white/20 pb-0.5">沉浸式体验</span>
在代码的逻辑与设计的感性之间寻找平衡我是<span class="text-art-text font-bold">年糕崽崽</span>不仅仅构建页面更在构建<span class="text-art-text border-b border-art-border pb-0.5">沉浸式体验</span>
</p>
<div class="pt-8 flex items-center gap-6">
<button @click="$router.push('/works')" class="group flex items-center gap-2 text-white border-b border-white pb-1 hover:text-art-accent hover:border-art-accent transition-all">
<button @click="$router.push('/works')" class="group flex items-center gap-2 text-art-text border-b border-art-text pb-1 hover:text-art-accent hover:border-art-accent transition-all">
<span>浏览作品集</span>
<Icon name="arrow-right" :size="16" class="transform group-hover:translate-x-1 transition-transform" />
</button>
@@ -21,16 +21,16 @@
</div>
<div class="lg:col-span-5 relative h-[400px] lg:h-[600px] w-full flex items-center justify-center">
<div class="relative w-full h-full">
<div class="absolute inset-0 border border-white/10 rounded-full rotate-12 scale-90"></div>
<div class="absolute inset-0 border border-white/5 rounded-full -rotate-6 scale-75"></div>
<div class="absolute inset-0 border border-art-border rounded-full rotate-12 scale-90"></div>
<div class="absolute inset-0 border border-art-border rounded-full -rotate-6 scale-75"></div>
<div
id="home-hero-work"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-80 bg-gradient-to-br from-white/10 to-transparent backdrop-blur-md border border-white/10 rounded-sm shadow-2xl rotate-6 hover:rotate-0 transition-transform duration-700 z-10 flex flex-col p-6 justify-between cursor-pointer"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-80 bg-gradient-to-br from-art-text/5 to-transparent backdrop-blur-md border border-art-border rounded-sm shadow-2xl rotate-6 hover:rotate-0 transition-transform duration-700 z-10 flex flex-col p-6 justify-between cursor-pointer"
@click="$router.push('/works/' + latestWork.id)"
>
<div class="text-white/50 text-xs">最新作品</div>
<div class="font-serif text-3xl italic text-white">{{ latestWork.title }}</div>
<div class="flex justify-end"><Icon name="arrow-up-right" :size="24" class="text-white" /></div>
<div class="text-art-muted text-xs">最新作品</div>
<div class="font-serif text-3xl italic text-art-text">{{ latestWork.title }}</div>
<div class="flex justify-end"><Icon name="arrow-up-right" :size="24" class="text-art-text" /></div>
</div>
</div>
</div>
@@ -38,8 +38,8 @@
<!-- Bento Grid -->
<div class="space-y-6 animate-reveal" style="animation-delay: 0.2s;">
<div class="flex items-end justify-between border-b border-white/10 pb-4 mb-8">
<h2 class="font-serif text-3xl italic text-white">精选内容</h2>
<div class="flex items-end justify-between border-b border-art-border pb-4 mb-8">
<h2 class="font-serif text-3xl italic text-art-text">精选内容</h2>
<span class="font-mono text-xs text-art-muted">下滑探索更多</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 auto-rows-[300px]" id="bento-grid">
@@ -73,26 +73,26 @@
class="art-card rounded-2xl p-8 flex flex-col justify-between group"
>
<div class="flex justify-between items-start">
<Icon name="code-2" :size="32" class="text-white/40 group-hover:text-art-accent transition-colors" />
<Icon name="arrow-up-right" :size="20" class="text-white/20" />
<Icon name="code-2" :size="32" class="text-art-muted group-hover:text-art-accent transition-colors" />
<Icon name="arrow-up-right" :size="20" class="text-art-muted/60" />
</div>
<div>
<div class="text-4xl font-mono font-bold text-white mb-2">120+</div>
<div class="text-4xl font-mono font-bold text-art-text mb-2">120+</div>
<div class="text-sm text-art-muted">开源提交 (Commits)</div>
<div class="text-xs text-art-muted mt-2 opacity-60">Vue, React, Three.js</div>
</div>
</div>
<div
class="art-card rounded-2xl p-6 md:p-8 flex flex-col justify-between group cursor-pointer bg-[#050505]"
class="art-card rounded-2xl p-6 md:p-8 flex flex-col justify-between group cursor-pointer bg-art-bg"
@click="$router.push('/snippets')"
>
<div class="font-mono text-xs text-art-muted mb-4">// React Hook: useArt</div>
<div class="font-mono text-sm text-gray-400 overflow-hidden opacity-60 group-hover:opacity-100 transition-opacity">
<div class="font-mono text-sm text-art-muted overflow-hidden opacity-60 group-hover:opacity-100 transition-opacity">
<span class="code-keyword">const</span> <span class="code-func">useCreative</span> = () => {<br>
&nbsp;&nbsp;<span class="code-keyword">return</span> <span class="code-string">"Innovation"</span>;<br>
}
</div>
<div class="mt-4 flex items-center gap-2 text-sm font-medium text-white">
<div class="mt-4 flex items-center gap-2 text-sm font-medium text-art-text">
<span>访问代码实验室</span>
<Icon name="chevron-right" :size="16" class="text-art-accent" />
</div>
@@ -102,10 +102,10 @@
@click="$router.push('/services')"
>
<div class="space-y-4">
<h3 class="font-serif text-3xl text-white">需要独特的前端架构</h3>
<h3 class="font-serif text-3xl text-art-text">需要独特的前端架构</h3>
<p class="text-art-muted max-w-sm">不论是 WebGL 3D 交互网站还是高性能的 SaaS 管理系统我都能提供专业的解决方案</p>
</div>
<div class="h-16 w-16 rounded-full border border-white/20 flex items-center justify-center group-hover:bg-white group-hover:text-black transition-all duration-300">
<div class="h-16 w-16 rounded-full border border-art-border flex items-center justify-center group-hover:bg-art-text group-hover:text-black transition-all duration-300">
<Icon name="arrow-right" :size="24" />
</div>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<section id="services" class="page-section block animate-slide-down">
<div class="py-16 text-center">
<h2 class="font-serif text-5xl italic text-white mb-6">共创数字未来</h2>
<h2 class="font-serif text-5xl italic text-art-text mb-6">共创数字未来</h2>
<p class="text-art-muted text-lg max-w-2xl mx-auto">用代码构建骨架用设计触动灵魂</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-32 px-4" id="services-grid">
@@ -11,18 +11,18 @@
<i data-lucide="layers" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">前端架构设计</h3>
<h3 class="text-2xl font-bold text-art-text mb-4">前端架构设计</h3>
<p class="text-art-muted text-sm leading-relaxed">为复杂的大型应用提供可扩展的架构方案</p>
</div>
</div>
</div>
<div class="tilt-card group h-96">
<div class="tilt-inner flex flex-col justify-between border-art-accent/20 bg-white/5">
<div class="tilt-inner flex flex-col justify-between border-art-accent/20 bg-art-text/5">
<div class="w-16 h-16 rounded-2xl bg-art-accent/10 flex items-center justify-center text-art-accent mb-6 border border-art-accent/20 group-hover:scale-110 transition-transform duration-500 shadow-[0_0_30px_-10px_rgba(212,179,131,0.3)]">
<i data-lucide="wand-2" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">创意交互开发</h3>
<h3 class="text-2xl font-bold text-art-text mb-4">创意交互开发</h3>
<p class="text-art-muted text-sm leading-relaxed">利用 WebGL (Three.js) GSAP 打造令人过目难忘的着陆页</p>
</div>
</div>
@@ -33,7 +33,7 @@
<i data-lucide="smartphone" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4">跨平台应用</h3>
<h3 class="text-2xl font-bold text-art-text mb-4">跨平台应用</h3>
<p class="text-art-muted text-sm leading-relaxed">使用 UniApp React Native 开发高质量的移动端应用</p>
</div>
</div>
@@ -42,7 +42,7 @@
<!-- Process Accordion -->
<div class="mb-32 px-4">
<h3 class="font-serif text-3xl text-white text-center mb-12 italic">创作流程</h3>
<h3 class="font-serif text-3xl text-art-text text-center mb-12 italic">创作流程</h3>
<div class="process-accordion">
<div class="process-step">
<img src="https://images.unsplash.com/photo-1531403009284-440f080d1e12?q=80&w=2670&auto=format&fit=crop" class="step-bg" alt="Discovery">
@@ -83,13 +83,13 @@
<div class="py-20 mb-32 overflow-hidden relative">
<div class="absolute left-0 top-0 w-20 h-full bg-gradient-to-r from-art-bg to-transparent z-10"></div>
<div class="absolute right-0 top-0 w-20 h-full bg-gradient-to-l from-art-bg to-transparent z-10"></div>
<h3 class="text-3xl font-serif text-white text-center mb-12 italic">客户原声</h3>
<h3 class="text-3xl font-serif text-art-text text-center mb-12 italic">客户原声</h3>
<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>
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="fetchServicesData" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="fetchServicesData" class="px-4 py-2 bg-art-accent text-black rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
@@ -133,22 +133,22 @@
</div>
<!-- CTA -->
<div class="max-w-4xl mx-auto text-center px-6 mb-20">
<div class="p-12 rounded-3xl bg-gradient-to-b from-white/10 to-transparent border border-white/10 relative overflow-hidden">
<div class="p-12 rounded-3xl bg-gradient-to-b from-art-text/5 to-transparent border border-art-border relative overflow-hidden">
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-64 h-64 bg-art-accent/20 blur-[100px] -z-10"></div>
<h2 class="text-4xl md:text-5xl font-serif text-white mb-6">准备好开始了吗</h2>
<h2 class="text-4xl md:text-5xl font-serif text-art-text mb-6">准备好开始了吗</h2>
<p class="text-art-muted mb-8 max-w-xl mx-auto">无论是一个疯狂的想法还是一个具体的业务需求我都乐意倾听</p>
<button @click="openInquiry" class="inline-flex items-center gap-2 px-8 py-4 bg-white text-black rounded-full font-bold hover:scale-105 transition-transform"><span>发起合作咨询</span><i data-lucide="arrow-right" class="w-5 h-5"></i></button>
</div>
</div>
<!-- Partners -->
<div class="max-w-4xl mx-auto px-6 mb-32 border-t border-white/5 pt-12">
<div class="max-w-4xl mx-auto px-6 mb-32 border-t border-art-border pt-12">
<p class="text-center text-xs font-mono text-art-muted tracking-widest uppercase mb-8">Trusted By</p>
<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>
<div v-else-if="error" class="text-center text-red-500 py-20">
<p class="mb-4">{{ error }}</p>
<button @click="fetchServicesData" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="fetchServicesData" class="px-4 py-2 bg-art-accent text-black rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
@@ -156,7 +156,7 @@
<span
v-for="partner in partners"
:key="partner.id"
class="partner-logo text-xl font-bold text-white cursor-pointer"
class="partner-logo text-xl font-bold text-art-text cursor-pointer"
:class="getPartnerFontClass(partner.name)"
>
{{ partner.name }}

View File

@@ -2,7 +2,7 @@
<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>
<h2 class="font-serif text-5xl italic text-art-text mb-6">代码实验室</h2>
<p class="text-art-muted">点击下方卡片查看代码与实时效果</p>
</div>
@@ -14,7 +14,7 @@
<!-- 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 @click="fetchSnippetsData" class="px-4 py-2 bg-art-accent text-art-text rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
@@ -25,17 +25,17 @@
<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"
class="art-card rounded-xl p-0 overflow-hidden bg-[#0d0d0d] border border-art-border 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 items-center justify-between px-4 py-3 bg-art-text/5 border-b border-art-border">
<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 truncate min-w-0 flex-1">{{ snippet.title }}</span>
<span class="text-xs font-mono text-art-muted group-hover:text-art-text transition-colors truncate min-w-0 flex-1">{{ snippet.title }}</span>
<a
v-if="snippet.posts && snippet.posts.length > 0"
:href="`/blog/${snippet.posts[0].id}`"

View File

@@ -1,7 +1,7 @@
<template>
<section class="page-section block">
<div class="max-w-6xl mx-auto pb-12 md:pb-20">
<button @click="router.push('/videos')" class="mb-8 text-art-muted hover:text-white flex items-center gap-2 text-sm">
<button @click="router.push('/videos')" class="mb-8 text-art-muted hover:text-art-text flex items-center gap-2 text-sm">
<Icon name="arrow-left" :size="16" /> 返回视频
</button>
@@ -13,7 +13,7 @@
<div v-if="album.cover" class="aspect-[21/9] rounded-xl overflow-hidden mb-6 max-h-80">
<img :src="album.cover" class="w-full h-full object-cover" alt="" />
</div>
<h1 class="font-serif text-4xl text-white mb-3">{{ album.name }}</h1>
<h1 class="font-serif text-4xl text-art-text mb-3">{{ album.name }}</h1>
<p class="text-art-muted max-w-2xl">{{ album.description }}</p>
</div>
@@ -21,16 +21,16 @@
<div
v-for="video in videos"
:key="video.id"
class="group cursor-pointer rounded-xl overflow-hidden border border-white/10 hover:border-art-accent/40 transition-colors"
class="group cursor-pointer rounded-xl overflow-hidden border border-art-border hover:border-art-accent/40 transition-colors"
@click="router.push(`/videos/${video.id}`)"
>
<div class="aspect-video relative bg-black/40">
<img v-if="video.cover" :src="video.cover" class="w-full h-full object-cover" alt="" />
<div class="absolute inset-0 flex items-center justify-center">
<Icon name="play" :size="24" class="text-white opacity-70 group-hover:opacity-100" />
<Icon name="play" :size="24" class="text-art-text opacity-70 group-hover:opacity-100" />
</div>
</div>
<div class="p-4"><h3 class="text-white text-sm font-medium line-clamp-2">{{ video.title }}</h3></div>
<div class="p-4"><h3 class="text-art-text text-sm font-medium line-clamp-2">{{ video.title }}</h3></div>
</div>
</div>
<div v-if="videos.length === 0" class="text-center py-12 text-art-muted">专辑内暂无视频</div>

View File

@@ -1,7 +1,7 @@
<template>
<section class="page-section block">
<div class="max-w-6xl mx-auto pb-12 md:pb-20">
<button @click="$router.back()" class="mb-8 text-art-muted hover:text-white flex items-center gap-2 text-sm">
<button @click="$router.back()" class="mb-8 text-art-muted hover:text-art-text flex items-center gap-2 text-sm">
<Icon name="arrow-left" :size="16" /> 返回
</button>
@@ -10,13 +10,13 @@
<p class="mb-4">{{ error }}</p>
<button
type="button"
class="px-4 py-2 border border-white/20 rounded-full text-sm text-white hover:border-art-accent transition-colors"
class="px-4 py-2 border border-art-border rounded-full text-sm text-art-text hover:border-art-accent transition-colors"
@click="load"
>重试</button>
</div>
<template v-else-if="video">
<h1 class="font-serif text-3xl md:text-4xl text-white mb-6">{{ video.title }}</h1>
<h1 class="font-serif text-3xl md:text-4xl text-art-text mb-6">{{ video.title }}</h1>
<div
v-if="video.videoUrl"
@@ -25,7 +25,7 @@
></div>
<div
v-else
class="w-full aspect-video rounded-xl overflow-hidden bg-black/40 border border-white/10 flex items-center justify-center mb-6"
class="w-full aspect-video rounded-xl overflow-hidden bg-black/40 border border-art-border flex items-center justify-center mb-6"
>
<p class="text-art-muted text-sm">暂无视频地址</p>
</div>
@@ -34,7 +34,7 @@
<div v-if="video.albumContext" class="mb-8">
<div class="flex items-center justify-between mb-4">
<p class="text-sm text-art-muted">
来自专辑 <span class="text-white">{{ video.albumContext.albumName }}</span>
来自专辑 <span class="text-art-text">{{ video.albumContext.albumName }}</span>
</p>
<router-link
:to="`/videos/albums/${video.albumContext.albumId}`"
@@ -49,10 +49,10 @@
class="shrink-0 w-36 cursor-pointer group"
@click="router.push(`/videos/${pv.id}`)"
>
<div class="aspect-video rounded-lg overflow-hidden border border-white/10 group-hover:border-art-accent/50 transition-colors bg-black/40">
<div class="aspect-video rounded-lg overflow-hidden border border-art-border group-hover:border-art-accent/50 transition-colors bg-black/40">
<img v-if="pv.cover" :src="pv.cover" class="w-full h-full object-cover opacity-80 group-hover:opacity-100" alt="" />
</div>
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-white">{{ pv.title }}</p>
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-art-text">{{ pv.title }}</p>
</div>
<!-- 当前视频高亮 -->
<div class="shrink-0 w-36">
@@ -68,10 +68,10 @@
class="shrink-0 w-36 cursor-pointer group"
@click="router.push(`/videos/${nv.id}`)"
>
<div class="aspect-video rounded-lg overflow-hidden border border-white/10 group-hover:border-art-accent/50 transition-colors bg-black/40">
<div class="aspect-video rounded-lg overflow-hidden border border-art-border group-hover:border-art-accent/50 transition-colors bg-black/40">
<img v-if="nv.cover" :src="nv.cover" class="w-full h-full object-cover opacity-80 group-hover:opacity-100" alt="" />
</div>
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-white">{{ nv.title }}</p>
<p class="text-xs text-art-muted mt-1 line-clamp-2 group-hover:text-art-text">{{ nv.title }}</p>
</div>
</div>
</div>
@@ -81,7 +81,7 @@
<span v-if="video.categoryName">{{ video.categoryName }}</span>
<span v-if="video.createdAt">发布于 {{ video.createdAt }}</span>
</div>
<p v-if="video.description" class="text-white/80 leading-relaxed whitespace-pre-wrap">{{ video.description }}</p>
<p v-if="video.description" class="text-art-text/80 leading-relaxed whitespace-pre-wrap">{{ video.description }}</p>
</div>
</template>
</div>

View File

@@ -2,7 +2,7 @@
<section id="videos" class="page-section block animate-slide-down">
<div class="max-w-6xl mx-auto pb-12 md:pb-20">
<div class="mb-12">
<h2 class="font-serif text-5xl italic text-white mb-4">视频</h2>
<h2 class="font-serif text-5xl italic text-art-text mb-4">视频</h2>
<p class="text-art-muted">精选视频内容与系列专辑</p>
</div>
@@ -11,7 +11,7 @@
<button
type="button"
class="px-4 py-1.5 rounded-full text-sm border transition-colors"
:class="activeCategory === 0 ? 'border-art-accent text-art-accent bg-art-accent/10' : 'border-white/10 text-art-muted hover:text-white'"
:class="activeCategory === 0 ? 'border-art-accent text-art-accent bg-art-accent/10' : 'border-art-border text-art-muted hover:text-art-text'"
@click="selectCategory(0)"
>全部</button>
<button
@@ -19,7 +19,7 @@
:key="cat.id"
type="button"
class="px-4 py-1.5 rounded-full text-sm border transition-colors"
:class="activeCategory === cat.id ? 'border-art-accent text-art-accent bg-art-accent/10' : 'border-white/10 text-art-muted hover:text-white'"
:class="activeCategory === cat.id ? 'border-art-accent text-art-accent bg-art-accent/10' : 'border-art-border text-art-muted hover:text-art-text'"
@click="selectCategory(cat.id)"
>{{ cat.name }}</button>
</div>
@@ -32,10 +32,10 @@
v-for="cat in categories"
:key="'card-' + cat.id"
class="group p-5 rounded-xl border cursor-pointer transition-colors"
:class="activeCategory === cat.id ? 'border-art-accent/50 bg-art-accent/5' : 'border-white/10 hover:border-art-accent/30 hover:bg-white/5'"
:class="activeCategory === cat.id ? 'border-art-accent/50 bg-art-accent/5' : 'border-art-border hover:border-art-accent/30 hover:bg-art-text/5'"
@click="selectCategory(cat.id)"
>
<h4 class="text-white font-medium group-hover:text-art-accent transition-colors">{{ cat.name }}</h4>
<h4 class="text-art-text font-medium group-hover:text-art-accent transition-colors">{{ cat.name }}</h4>
<p v-if="cat.description" class="text-xs text-art-muted mt-2 line-clamp-2">{{ cat.description }}</p>
<div class="flex gap-4 mt-3 text-xs text-art-muted">
<span>{{ cat.videoCount ?? 0 }} 个视频</span>
@@ -44,7 +44,7 @@
</div>
</div>
</div>
<div v-else-if="!loading && !error" class="mb-10 text-center text-art-muted text-sm py-6 border border-dashed border-white/10 rounded-xl">
<div v-else-if="!loading && !error" class="mb-10 text-center text-art-muted text-sm py-6 border border-dashed border-art-border rounded-xl">
暂无视频分类请先在后台创建分类
</div>
@@ -54,7 +54,7 @@
<p class="text-red-400 mb-4">{{ error }}</p>
<button
type="button"
class="px-4 py-2 border border-white/20 rounded-full text-sm text-white hover:border-art-accent hover:text-art-accent transition-colors"
class="px-4 py-2 border border-art-border rounded-full text-sm text-art-text hover:border-art-accent hover:text-art-accent transition-colors"
@click="loadAll"
>重试</button>
</div>
@@ -67,7 +67,7 @@
<div
v-for="album in albums"
:key="album.id"
class="group rounded-xl overflow-hidden border border-white/10 hover:border-art-accent/40 transition-colors"
class="group rounded-xl overflow-hidden border border-art-border hover:border-art-accent/40 transition-colors"
>
<div
class="aspect-video bg-gray-900 overflow-hidden cursor-pointer"
@@ -78,7 +78,7 @@
<!-- 专辑预览视频缩略图 -->
<div
v-if="album.previewVideos?.length"
class="flex gap-1 px-2 py-2 bg-black/30 border-t border-white/5 overflow-x-auto"
class="flex gap-1 px-2 py-2 bg-black/30 border-t border-art-border overflow-x-auto"
>
<div
v-for="pv in album.previewVideos"
@@ -87,13 +87,13 @@
@click.stop="router.push(`/videos/${pv.id}`)"
>
<img v-if="pv.cover" :src="pv.cover" class="w-full h-full object-cover" alt="" />
<div v-else class="w-full h-full bg-white/5 flex items-center justify-center">
<Icon name="play" :size="12" class="text-white/50" />
<div v-else class="w-full h-full bg-art-text/5 flex items-center justify-center">
<Icon name="play" :size="12" class="text-art-muted" />
</div>
</div>
</div>
<div class="p-4 cursor-pointer" @click="router.push(`/videos/albums/${album.id}`)">
<h4 class="text-white font-medium group-hover:text-art-accent transition-colors">{{ album.name }}</h4>
<h4 class="text-art-text font-medium group-hover:text-art-accent transition-colors">{{ album.name }}</h4>
<p class="text-xs text-art-muted mt-1">
{{ album.videoCount ?? 0 }} 个视频
<span v-if="album.latestVideoUpdatedAt" class="ml-2">· 更新于 {{ album.latestVideoUpdatedAt }}</span>
@@ -103,7 +103,7 @@
</div>
</div>
</div>
<div v-else class="mb-10 text-center text-art-muted text-sm py-8 border border-dashed border-white/10 rounded-xl">
<div v-else class="mb-10 text-center text-art-muted text-sm py-8 border border-dashed border-art-border rounded-xl">
暂无专辑
</div>
@@ -114,19 +114,19 @@
<div
v-for="video in videos"
:key="video.id"
class="group cursor-pointer rounded-xl overflow-hidden border border-white/10 hover:border-art-accent/40 transition-colors"
class="group cursor-pointer rounded-xl overflow-hidden border border-art-border hover:border-art-accent/40 transition-colors"
@click="router.push(`/videos/${video.id}`)"
>
<div class="aspect-video bg-black/40 relative overflow-hidden">
<img v-if="video.cover" :src="video.cover" class="w-full h-full object-cover" alt="" />
<div class="absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/10 transition-colors">
<div class="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center">
<Icon name="play" :size="20" class="text-white ml-0.5" />
<div class="w-12 h-12 rounded-full border border-art-border flex items-center justify-center">
<Icon name="play" :size="20" class="text-art-text ml-0.5" />
</div>
</div>
</div>
<div class="p-4">
<h4 class="text-white font-medium line-clamp-2">{{ video.title }}</h4>
<h4 class="text-art-text font-medium line-clamp-2">{{ video.title }}</h4>
<p class="text-xs text-art-muted mt-1">
{{ video.categoryName || '未分类' }}
<span v-if="video.createdAt" class="ml-2">· 发布于 {{ video.createdAt }}</span>

View File

@@ -1,22 +1,22 @@
<template>
<section id="work-detail" class="work-detail-page page-section block">
<button @click="$router.push('/works')" class="fixed top-8 right-8 z-[60] mix-blend-difference text-white hover:scale-110 transition-transform">
<div class="rounded-full border border-white/20 p-4 backdrop-blur-md bg-white/5"><Icon name="x" :size="24" /></div>
<button @click="$router.push('/works')" class="fixed top-8 right-8 z-[60] text-art-text hover:text-art-accent hover:scale-110 transition-all">
<div class="rounded-full border border-art-border p-4 backdrop-blur-md bg-art-surface/80"><Icon name="x" :size="24" /></div>
</button>
<div class="fixed top-0 left-0 h-1 bg-art-accent z-[60] w-0 transition-all duration-100" id="scroll-progress"></div>
<!-- Loading state -->
<div v-if="loading" class="fixed inset-0 bg-black flex items-center justify-center z-[99]">
<div v-if="loading" class="fixed inset-0 bg-art-bg flex items-center justify-center z-[99]">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-art-accent"></div>
</div>
<!-- Error state -->
<div v-else-if="error" class="fixed inset-0 bg-black flex flex-col items-center justify-center z-[99] p-8">
<div v-else-if="error" class="fixed inset-0 bg-art-bg flex flex-col items-center justify-center z-[99] p-8">
<p class="text-red-500 text-xl mb-4">{{ error }}</p>
<button @click="fetchWorkDetails" class="px-6 py-3 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="fetchWorkDetails" class="px-6 py-3 bg-art-accent text-black rounded hover:bg-opacity-80 transition-colors">
重试
</button>
<button @click="$router.push('/works')" class="mt-4 px-6 py-3 border border-white/30 text-white rounded hover:bg-white/5 transition-colors">
<button @click="$router.push('/works')" class="mt-4 px-6 py-3 border border-art-border text-art-text rounded hover:bg-art-text/5 transition-colors">
返回作品列表
</button>
</div>
@@ -35,15 +35,15 @@
<!-- Play Button Overlay有视频时显示点击弹层播放不覆盖主图 -->
<div
v-if="work.videoUrl"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-24 h-24 rounded-full border border-white/20 bg-white/5 backdrop-blur-md flex items-center justify-center cursor-pointer hover:scale-110 hover:bg-white/10 transition-all duration-500 group-hover:border-art-accent/50 z-20"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-24 h-24 rounded-full border border-art-border bg-art-text/5 backdrop-blur-md flex items-center justify-center cursor-pointer hover:scale-110 hover:bg-art-text/10 transition-all duration-500 group-hover:border-art-accent/50 z-20"
@click="videoModalOpen = true"
>
<Icon name="play" :size="32" class="text-white fill-white ml-1" />
<div class="absolute inset-0 rounded-full border border-white/20 animate-pulse-slow"></div>
<div class="absolute inset-0 rounded-full border border-white/30 animate-pulse-slow"></div>
</div>
<div class="absolute bottom-0 left-0 p-8 md:p-20 w-full">
<div class="flex items-end justify-between border-t border-white/30 pt-8 animate-slide-down">
<div class="flex items-end justify-between border-t border-art-border pt-8 animate-slide-down">
<div>
<span id="work-category" class="font-mono text-art-accent text-sm tracking-[0.2em] uppercase mb-2 block">{{ work.category }}</span>
<h1 id="work-title" class="font-serif text-5xl md:text-8xl text-white leading-[0.9] mix-blend-overlay">{{ work.title }}</h1>
@@ -53,11 +53,11 @@
</div>
</div>
<div class="flex flex-col md:flex-row max-w-[1920px] mx-auto min-h-screen">
<div class="md:w-1/3 p-8 md:p-16 md:sticky md:top-0 md:h-screen md:max-h-screen overflow-y-auto custom-scrollbar flex flex-col justify-between border-r border-white/5 bg-[#050505]">
<div class="md:w-1/3 p-8 md:p-16 md:sticky md:top-0 md:h-screen md:max-h-screen overflow-y-auto custom-scrollbar flex flex-col justify-between border-r border-art-border bg-art-bg">
<div class="space-y-12">
<div>
<h3 class="font-mono text-xs text-art-muted uppercase tracking-widest mb-4">关于项目</h3>
<div id="work-desc" class="text-white/80 font-light leading-relaxed text-lg font-serif" v-html="work.desc"></div>
<div id="work-desc" class="text-art-text/80 font-light leading-relaxed text-lg font-serif" v-html="work.desc"></div>
</div>
<div>
<h3 class="font-mono text-xs text-art-muted uppercase tracking-widest mb-6">技术全景</h3>
@@ -78,13 +78,13 @@
id="work-link-live"
:href="work.links.live"
target="_blank"
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
class="group flex items-center justify-between w-full py-4 border-t border-art-border hover:bg-art-text/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<span class="font-serif text-xl italic text-art-text group-hover:text-art-accent transition-colors flex items-center gap-3">
<Icon name="globe" :size="20" />
访问线上项目
</span>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
<Icon name="arrow-up-right" :size="20" class="text-art-text group-hover:rotate-45 transition-transform" />
</a>
<!-- Demo Link -->
<a
@@ -92,13 +92,13 @@
id="work-link-demo"
:href="work.links.demo"
target="_blank"
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
class="group flex items-center justify-between w-full py-4 border-t border-art-border hover:bg-art-text/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<span class="font-serif text-xl italic text-art-text group-hover:text-art-accent transition-colors flex items-center gap-3">
<Icon name="play-circle" :size="20" />
查看演示
</span>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
<Icon name="arrow-up-right" :size="20" class="text-art-text group-hover:rotate-45 transition-transform" />
</a>
<!-- GitHub Link -->
<a
@@ -106,17 +106,17 @@
id="work-link-github"
:href="work.links.github"
target="_blank"
class="group flex items-center justify-between w-full py-4 border-t border-white/10 hover:bg-white/5 transition-colors"
class="group flex items-center justify-between w-full py-4 border-t border-art-border hover:bg-art-text/5 transition-colors"
>
<span class="font-serif text-xl italic text-white group-hover:text-art-accent transition-colors flex items-center gap-3">
<span class="font-serif text-xl italic text-art-text group-hover:text-art-accent transition-colors flex items-center gap-3">
<Icon name="github" :size="20" />
查看源码
</span>
<Icon name="arrow-up-right" :size="20" class="text-white group-hover:rotate-45 transition-transform" />
<Icon name="arrow-up-right" :size="20" class="text-art-text group-hover:rotate-45 transition-transform" />
</a>
</div>
</div>
<div class="md:w-2/3 bg-[#080808] flex flex-col">
<div class="md:w-2/3 bg-art-bg flex flex-col">
<div id="work-gallery" class="flex flex-col flex-grow">
<template v-for="(img, index) in work.gallery" :key="index">
<!-- Alternating layout logic -->
@@ -136,15 +136,15 @@
</template>
</div>
<div
class="h-[40vh] flex items-center justify-center border-t border-white/5 bg-[#050505] cursor-pointer group hover:bg-white/5 transition-colors"
class="h-[40vh] flex items-center justify-center border-t border-art-border bg-art-bg cursor-pointer group hover:bg-art-text/5 transition-colors"
@click="openNextWork"
>
<div class="text-center">
<p class="font-mono text-xs text-art-muted mb-4 tracking-widest">下一个作品</p>
<h2 class="font-serif text-5xl text-white group-hover:italic transition-all">返回作品列表</h2>
<h2 class="font-serif text-5xl text-art-text group-hover:italic transition-all">返回作品列表</h2>
</div>
</div>
<!-- <footer class="py-8 text-center border-t border-white/5 relative z-10 bg-[#050505]"><p class="text-art-muted text-xs font-mono tracking-widest opacity-50">&copy; 2024 年糕崽崽. 保留所有权利.</p></footer>-->
<!-- <footer class="py-8 text-center border-t border-art-border relative z-10 bg-art-bg"><p class="text-art-muted text-xs font-mono tracking-widest opacity-50">&copy; 2024 年糕崽崽. 保留所有权利.</p></footer>-->
</div>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<section id="works" class="page-section block animate-slide-down">
<div class="mb-32">
<h2 class="font-serif text-5xl italic text-white mb-6">精选作品</h2>
<h2 class="font-serif text-5xl italic text-art-text mb-6">精选作品</h2>
<p class="text-art-muted">这里展示了我参与设计和开发的核心项目</p>
</div>
<!-- Loading state -->
@@ -11,7 +11,7 @@
<!-- Error state -->
<div v-else-if="error" class="text-center text-red-500">
<p>{{ error }}</p>
<button @click="fetchWorksData" class="mt-4 px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
<button @click="fetchWorksData" class="mt-4 px-4 py-2 bg-art-accent text-black rounded hover:bg-opacity-80 transition-colors">
重试
</button>
</div>
@@ -26,14 +26,11 @@
<!-- 作品图片 -->
<div
:class="index % 2 === 1 ? 'md:order-2' : 'md:order-1'"
class="relative aspect-[4/3] overflow-hidden rounded-sm bg-gray-900 border border-white/5"
class="relative aspect-[4/3] overflow-hidden rounded-sm bg-art-surface border border-art-border"
>
<div
class="absolute inset-0 z-10"
:class="[
index % 2 === 0 ? 'bg-gradient-to-tr from-purple-900/40 to-blue-900/40' : 'bg-gradient-to-tr from-orange-900/40 to-red-900/40',
'mix-blend-color-burn'
]"
class="absolute inset-0 z-10 dark:mix-blend-color-burn"
:class="index % 2 === 0 ? 'bg-gradient-to-tr from-purple-900/40 to-blue-900/40' : 'bg-gradient-to-tr from-orange-900/40 to-red-900/40'"
></div>
<div class="absolute inset-0 bg-black/30 group-hover:bg-transparent transition-colors z-20"></div>
<img
@@ -51,7 +48,7 @@
<div class="font-mono text-xs text-art-accent">{{ work.year }} · {{ work.category }}</div>
<!-- 作品标题 -->
<h3
class="text-4xl font-serif text-white group-hover:text-art-accent transition-colors"
class="text-4xl font-serif text-art-text group-hover:text-art-accent transition-colors"
>
{{ work.title }}
</h3>
@@ -62,7 +59,7 @@
<span
v-for="(stack, stackIndex) in work.techStack.flatMap(tech => tech.items)"
:key="stackIndex"
class="px-3 py-1 border border-white/10 rounded-full text-xs text-white/70"
class="px-3 py-1 border border-art-border rounded-full text-xs text-art-text/70"
>
{{ stack }}
</span>
@@ -74,7 +71,7 @@
:href="work.links.live"
target="_blank"
@click.stop
class="inline-flex items-center gap-2 px-4 py-2 border border-white/10 rounded-full text-xs text-white/70 hover:border-art-accent hover:text-art-accent transition-all"
class="inline-flex items-center gap-2 px-4 py-2 border border-art-border rounded-full text-xs text-art-text/70 hover:border-art-accent hover:text-art-accent transition-all"
>
<Icon name="globe" :size="12" />
线上项目
@@ -84,7 +81,7 @@
:href="work.links.demo"
target="_blank"
@click.stop
class="inline-flex items-center gap-2 px-4 py-2 border border-white/10 rounded-full text-xs text-white/70 hover:border-art-accent hover:text-art-accent transition-all"
class="inline-flex items-center gap-2 px-4 py-2 border border-art-border rounded-full text-xs text-art-text/70 hover:border-art-accent hover:text-art-accent transition-all"
>
<Icon name="play-circle" :size="12" />
演示
@@ -94,7 +91,7 @@
:href="work.links.github"
target="_blank"
@click.stop
class="inline-flex items-center gap-2 px-4 py-2 border border-white/10 rounded-full text-xs text-white/70 hover:border-art-accent hover:text-art-accent transition-all"
class="inline-flex items-center gap-2 px-4 py-2 border border-art-border rounded-full text-xs text-art-text/70 hover:border-art-accent hover:text-art-accent transition-all"
>
<Icon name="github" :size="12" />
源码
@@ -102,7 +99,7 @@
</div>
<!-- 查看详情按钮 -->
<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">
<span class="inline-flex items-center gap-2 text-sm font-medium text-art-text border-b border-transparent hover:border-art-accent hover:text-art-accent transition-all">
查看项目详情 <Icon name="arrow-right" :size="16" />
</span>
</div>

View File

@@ -2,40 +2,133 @@
@tailwind components;
@tailwind utilities;
/* Theme tokens — light (:root) / dark (.dark) */
:root {
--art-bg: 250 248 244;
--art-surface: 255 255 255;
--art-border: rgba(0, 0, 0, 0.08);
--art-text: 23 23 23;
--art-muted: 107 107 107;
--art-accent: 212 179 131;
--art-error: 239 68 68;
--art-admin-bg: 10 10 10;
--art-admin-card: 18 18 20;
--art-admin-hover: 26 26 28;
--art-nav-bg: rgba(250, 248, 244, 0.82);
--art-card-bg: rgba(255, 255, 255, 0.55);
--art-card-border: rgba(0, 0, 0, 0.07);
--art-card-shine: rgba(212, 179, 131, 0.08);
--art-hover-border: rgba(212, 179, 131, 0.55);
--art-overlay: rgba(0, 0, 0, 0.45);
--art-shadow: 0 18px 40px -12px rgba(80, 55, 20, 0.14);
--art-scroll-thumb: rgba(0, 0, 0, 0.12);
--art-scroll-thumb-hover: rgba(0, 0, 0, 0.25);
--art-toast-bg: rgba(255, 255, 255, 0.95);
--art-toast-border: rgba(0, 0, 0, 0.08);
--art-tilt-bg: rgba(255, 255, 255, 0.65);
--art-tilt-border: rgba(0, 0, 0, 0.07);
--art-process-bg: 255 255 255;
--art-process-hover: 248 245 240;
--art-process-border: rgba(0, 0, 0, 0.08);
--art-step-number: rgba(0, 0, 0, 0.08);
--art-tech-item-bg: rgba(0, 0, 0, 0.03);
--art-tech-item-border: rgba(0, 0, 0, 0.08);
--art-tech-item-text: 82 82 91;
--art-danmaku-bg: rgba(255, 255, 255, 0.7);
--art-danmaku-border: rgba(0, 0, 0, 0.08);
--art-danmaku-text: 63 63 70;
--art-text-glow: 0 0 36px rgba(212, 179, 131, 0.35);
--art-drawer-bg: 255 255 255;
--art-input-autofill: 250 248 244;
--art-input-autofill-text: 23 23 23;
--art-checker: #ebe6de;
--art-noise-opacity: 0.04;
--art-noise-blend: multiply;
--art-aurora-1: rgba(212, 179, 131, 0.14);
--art-aurora-2: rgba(196, 150, 90, 0.1);
}
.dark {
--art-bg: 5 5 5;
--art-surface: 18 18 20;
--art-border: rgba(255, 255, 255, 0.08);
--art-text: 236 236 236;
--art-muted: 136 136 136;
--art-accent: 212 179 131;
--art-error: 239 68 68;
--art-admin-bg: 10 10 10;
--art-admin-card: 18 18 20;
--art-admin-hover: 26 26 28;
--art-nav-bg: rgba(5, 5, 5, 0.7);
--art-card-bg: rgba(255, 255, 255, 0.02);
--art-card-border: rgba(255, 255, 255, 0.03);
--art-card-shine: rgba(255, 255, 255, 0.02);
--art-hover-border: rgba(212, 179, 131, 0.3);
--art-overlay: rgba(0, 0, 0, 0.9);
--art-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.5);
--art-scroll-thumb: rgba(255, 255, 255, 0.1);
--art-scroll-thumb-hover: rgba(255, 255, 255, 0.2);
--art-toast-bg: rgba(20, 20, 23, 0.9);
--art-toast-border: rgba(255, 255, 255, 0.1);
--art-tilt-bg: rgba(255, 255, 255, 0.03);
--art-tilt-border: rgba(255, 255, 255, 0.05);
--art-process-bg: 18 18 20;
--art-process-hover: 26 26 28;
--art-process-border: rgba(255, 255, 255, 0.1);
--art-step-number: rgba(255, 255, 255, 0.1);
--art-tech-item-bg: rgba(255, 255, 255, 0.03);
--art-tech-item-border: rgba(255, 255, 255, 0.08);
--art-tech-item-text: 161 161 170;
--art-danmaku-bg: rgba(255, 255, 255, 0.05);
--art-danmaku-border: rgba(255, 255, 255, 0.1);
--art-danmaku-text: 212 212 216;
--art-text-glow: 0 0 30px rgba(255, 255, 255, 0.1);
--art-drawer-bg: 26 26 26;
--art-input-autofill: 5 5 5;
--art-input-autofill-text: 236 236 236;
--art-checker: #1a1a1a;
--art-noise-opacity: 0.3;
--art-noise-blend: overlay;
--art-aurora-1: transparent;
--art-aurora-2: transparent;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 3px; transition: all 0.3s ease; }
::-webkit-scrollbar-thumb:hover { background: #d4b383; box-shadow: 0 0 10px rgba(212, 179, 131, 0.4); }
::-webkit-scrollbar-thumb { background: var(--art-scroll-thumb); border-radius: 3px; transition: all 0.3s ease; }
::-webkit-scrollbar-thumb:hover { background: rgb(var(--art-accent)); box-shadow: 0 0 10px rgba(212, 179, 131, 0.4); }
::-webkit-scrollbar-corner { background: transparent; }
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.05); }
.custom-scrollbar:hover::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); }
.custom-scrollbar::-webkit-scrollbar-thumb { background: var(--art-scroll-thumb); }
.custom-scrollbar:hover::-webkit-scrollbar-thumb { background: var(--art-scroll-thumb-hover); }
/* Glass & Utils - Keep for Frontend */
.glass-nav { 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); }
.art-card { background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.03); transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); position: relative; overflow: hidden; }
.art-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.02) 100%); opacity: 0; transition: opacity 0.5s ease; }
.art-card:hover { transform: translateY(-5px); border-color: rgba(212, 179, 131, 0.3); box-shadow: 0 20px 40px -10px rgba(0,0,0,0.5); }
.glass-nav { background: var(--art-nav-bg); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-bottom: 1px solid var(--art-card-border); }
.art-card { background: var(--art-card-bg); border: 1px solid var(--art-card-border); transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); position: relative; overflow: hidden; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.art-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(180deg, transparent 0%, var(--art-card-shine) 100%); opacity: 0; transition: opacity 0.5s ease; }
.art-card:hover { transform: translateY(-5px); border-color: var(--art-hover-border); box-shadow: var(--art-shadow); }
.art-card:hover::before { opacity: 1; }
.text-glow { text-shadow: 0 0 30px rgba(255,255,255,0.1); }
.text-glow { text-shadow: var(--art-text-glow); }
.link-underline { position: relative; }
.link-underline::after { content: ''; position: absolute; width: 0; height: 1px; bottom: -2px; left: 0; background-color: #d4b383; transition: width 0.3s ease; }
.link-underline::after { content: ''; position: absolute; width: 0; height: 1px; bottom: -2px; left: 0; background-color: rgb(var(--art-accent)); transition: width 0.3s ease; }
.link-underline:hover::after { width: 100%; }
/* Modal & Toast */
.modal-overlay { background: rgba(0, 0, 0, 0.9); backdrop-filter: blur(10px); }
.modal-overlay { background: var(--art-overlay); backdrop-filter: blur(10px); }
.toast-container { position: fixed; bottom: 2rem; right: 2rem; z-index: 100; display: flex; flex-direction: column; gap: 0.5rem; pointer-events: none; }
.toast { background: rgba(20, 20, 23, 0.9); border: 1px solid rgba(255,255,255,0.1); backdrop-filter: blur(12px); padding: 1rem 1.5rem; border-radius: 0.5rem; color: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5); display: flex; align-items: center; gap: 0.75rem; transform: translateX(100%); transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); pointer-events: auto; min-width: 300px; }
.toast { background: var(--art-toast-bg); border: 1px solid var(--art-toast-border); backdrop-filter: blur(12px); padding: 1rem 1.5rem; border-radius: 0.5rem; color: rgb(var(--art-text)); box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.15); display: flex; align-items: center; gap: 0.75rem; transform: translateX(100%); transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); pointer-events: auto; min-width: 300px; }
.toast.show { transform: translateX(0); }
.toast-success { border-left: 3px solid #d4b383; }
.toast-error { border-left: 3px solid #ef4444; }
.toast-success { border-left: 3px solid rgb(var(--art-accent)); }
.toast-error { border-left: 3px solid rgb(var(--art-error)); }
/* Form Error */
.input-error { border-bottom-color: #ef4444 !important; }
.error-msg { color: #ef4444; font-family: 'Inter', sans-serif; font-size: 0.75rem; margin-top: 0.4rem; display: block; animation: fadeIn 0.3s forwards; }
.input-error { border-bottom-color: rgb(var(--art-error)) !important; }
.error-msg { color: rgb(var(--art-error)); font-family: 'Inter', sans-serif; font-size: 0.75rem; margin-top: 0.4rem; display: block; animation: fadeIn 0.3s forwards; }
@keyframes fadeIn { to { opacity: 1; } }
/* Admin UI System - New */
@@ -93,7 +186,7 @@
}
}
/* Code Block - Keep for Frontend, Admin Editor uses its own */
/* Code Block - Keep for Frontend, Admin Editor uses its own (always dark) */
.mac-code-block { background: #1e1e1e; padding: 3rem 1.5rem 1.5rem 1.5rem; border-radius: 0.75rem; overflow-x: auto; margin-bottom: 0; border: 1px solid rgba(255,255,255,0.1); position: relative; box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5); }
.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; }
@@ -102,7 +195,7 @@
.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; }
.work-detail-page { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgb(var(--art-bg)); z-index: 100; overflow-y: auto; }
.gallery-image { width: 100%; margin-bottom: 4rem; filter: grayscale(20%); transition: filter 0.5s; }
.gallery-image:hover { filter: grayscale(0%); }
@@ -111,32 +204,32 @@
/* Tilt & Service Styles */
.tilt-card { position: relative; transform-style: preserve-3d; transform: perspective(1000px); transition: transform 0.1s ease; }
.tilt-inner { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px); border-radius: 1rem; padding: 2rem; height: 100%; position: relative; overflow: hidden; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); }
.tilt-inner { background: var(--art-tilt-bg); border: 1px solid var(--art-tilt-border); backdrop-filter: blur(10px); border-radius: 1rem; padding: 2rem; height: 100%; position: relative; overflow: hidden; box-shadow: inset 0 0 0 1px var(--art-tilt-border); }
.tilt-inner::before { content: ""; position: absolute; height: 100%; width: 100%; top: 0; left: 0; background: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(212, 179, 131, 0.15), transparent 40%); z-index: 0; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
.tilt-card:hover .tilt-inner::before { opacity: 1; }
.process-accordion { display: flex; gap: 1rem; height: 400px; width: 100%; }
.process-step { position: relative; flex: 1; background: #121214; border: 1px solid rgba(255,255,255,0.1); border-radius: 1.5rem; overflow: hidden; transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1); cursor: default; display: flex; flex-direction: column; justify-content: flex-end; padding: 2rem; }
.process-step:hover { flex: 3; background: #1a1a1c; border-color: rgba(212, 179, 131, 0.4); }
.process-step { position: relative; flex: 1; background: rgb(var(--art-process-bg)); border: 1px solid var(--art-process-border); border-radius: 1.5rem; overflow: hidden; transition: all 0.5s cubic-bezier(0.25, 1, 0.5, 1); cursor: default; display: flex; flex-direction: column; justify-content: flex-end; padding: 2rem; }
.process-step:hover { flex: 3; background: rgb(var(--art-process-hover)); border-color: rgba(212, 179, 131, 0.4); }
.process-step::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.8) 100%); z-index: 1; }
.step-bg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.3; transition: opacity 0.5s, transform 0.5s; z-index: 0; filter: grayscale(100%); }
.process-step:hover .step-bg { opacity: 0.6; transform: scale(1.05); filter: grayscale(0%); }
.step-content { position: relative; z-index: 10; transform: translateY(0); transition: transform 0.5s; }
.step-number { font-family: 'Playfair Display', serif; font-size: 4rem; color: rgba(255,255,255,0.1); line-height: 1; margin-bottom: 1rem; transition: color 0.3s; }
.process-step:hover .step-number { color: #d4b383; }
.step-desc { max-height: 0; opacity: 0; overflow: hidden; transition: all 0.5s ease; color: #888888; font-size: 0.9rem; line-height: 1.6; }
.step-number { font-family: 'Playfair Display', serif; font-size: 4rem; color: var(--art-step-number); line-height: 1; margin-bottom: 1rem; transition: color 0.3s; }
.process-step:hover .step-number { color: rgb(var(--art-accent)); }
.step-desc { max-height: 0; opacity: 0; overflow: hidden; transition: all 0.5s ease; color: rgba(255,255,255,0.75); font-size: 0.9rem; line-height: 1.6; }
.process-step:hover .step-desc { max-height: 100px; opacity: 1; margin-top: 1rem; }
@media (max-width: 768px) { .process-accordion { flex-direction: column; height: auto; } .process-step { height: 120px; } .process-step:hover { flex: none; height: 200px; } }
/* Tech Stack Grid */
.tech-category-title { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; letter-spacing: 0.1em; color: #d4b383; text-transform: uppercase; margin-bottom: 0.75rem; padding-bottom: 0.25rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1); }
.tech-category-title { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; letter-spacing: 0.1em; color: rgb(var(--art-accent)); text-transform: uppercase; margin-bottom: 0.75rem; padding-bottom: 0.25rem; border-bottom: 1px solid var(--art-border); }
.tech-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; }
.tech-item { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); padding: 0.35rem 0.75rem; font-family: 'Inter', sans-serif; font-size: 0.8rem; color: #a1a1aa; transition: all 0.2s; cursor: default; }
.tech-item:hover { border-color: #d4b383; color: white; background: rgba(212, 179, 131, 0.1); }
.tech-item { background: var(--art-tech-item-bg); border: 1px solid var(--art-tech-item-border); padding: 0.35rem 0.75rem; font-family: 'Inter', sans-serif; font-size: 0.8rem; color: rgb(var(--art-tech-item-text)); transition: all 0.2s; cursor: default; }
.tech-item:hover { border-color: rgb(var(--art-accent)); color: rgb(var(--art-text)); background: rgba(212, 179, 131, 0.1); }
/* Danmaku Styles */
.danmaku-item { background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.1); padding: 0.75rem 1.25rem; border-radius: 9999px; display: inline-flex; align-items: center; gap: 0.75rem; white-space: nowrap; color: #d4d4d8; font-size: 0.875rem; transition: all 0.3s; }
.danmaku-item:hover { background: rgba(255, 255, 255, 0.1); border-color: #d4b383; transform: scale(1.05); color: white; z-index: 10; }
.danmaku-item { background: var(--art-danmaku-bg); backdrop-filter: blur(8px); border: 1px solid var(--art-danmaku-border); padding: 0.75rem 1.25rem; border-radius: 9999px; display: inline-flex; align-items: center; gap: 0.75rem; white-space: nowrap; color: rgb(var(--art-danmaku-text)); font-size: 0.875rem; transition: all 0.3s; }
.danmaku-item:hover { background: rgba(212, 179, 131, 0.1); border-color: rgb(var(--art-accent)); transform: scale(1.05); color: rgb(var(--art-text)); z-index: 10; }
.danmaku-row { display: flex; gap: 2rem; width: max-content; }
.partner-logo { filter: grayscale(100%) opacity(0.5); transition: all 0.4s ease; }
.partner-logo:hover { filter: grayscale(0%) opacity(1); transform: scale(1.1); }
@@ -161,14 +254,40 @@
/* Base Styles */
body {
font-family: 'Inter', 'Noto Sans SC', sans-serif;
background-color: #050505;
color: #ececec;
background-color: rgb(var(--art-bg));
color: rgb(var(--art-text));
overflow-x: hidden;
}
/* Light paper atmosphere — fixed corner gold washes */
.light-atmosphere {
display: none;
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background:
radial-gradient(ellipse 55% 45% at 8% 12%, var(--art-aurora-1), transparent 70%),
radial-gradient(ellipse 50% 40% at 92% 18%, var(--art-aurora-2), transparent 65%),
radial-gradient(ellipse 45% 35% at 70% 88%, rgba(212, 179, 131, 0.1), transparent 70%);
}
html:not(.dark) .light-atmosphere {
display: block;
}
.site-noise {
opacity: var(--art-noise-opacity);
mix-blend-mode: var(--art-noise-blend);
}
.ambient-bg-canvas {
display: block;
}
/* Selection */
::selection {
background-color: #d4b383;
background-color: rgb(var(--art-accent));
color: black;
}
@@ -176,3 +295,14 @@ body {
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
.art-card:hover {
transform: none;
}
.animate-marquee,
.animate-marquee-reverse,
.animate-pulse-slow {
animation: none !important;
}
}

View File

@@ -14,16 +14,16 @@ export default {
},
colors: {
art: {
bg: '#050505',
surface: '#121214',
border: 'rgba(255,255,255,0.08)',
text: '#ececec',
muted: '#888888',
accent: '#d4b383',
error: '#ef4444',
'admin-bg': '#0a0a0a',
'admin-card': '#121214',
'admin-hover': '#1a1a1c',
bg: 'rgb(var(--art-bg) / <alpha-value>)',
surface: 'rgb(var(--art-surface) / <alpha-value>)',
border: 'var(--art-border)',
text: 'rgb(var(--art-text) / <alpha-value>)',
muted: 'rgb(var(--art-muted) / <alpha-value>)',
accent: 'rgb(var(--art-accent) / <alpha-value>)',
error: 'rgb(var(--art-error) / <alpha-value>)',
'admin-bg': 'rgb(var(--art-admin-bg) / <alpha-value>)',
'admin-card': 'rgb(var(--art-admin-card) / <alpha-value>)',
'admin-hover': 'rgb(var(--art-admin-hover) / <alpha-value>)',
}
},
backgroundImage: {