Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ backend-cf/.dev.vars

frontend/node_modules/
frontend/.nuxt/
frontend/.output/

# wrangler files
.wrangler
Expand Down
165 changes: 151 additions & 14 deletions frontend/app.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,168 @@
<template>
<div class="min-h-screen flex flex-col">
<header class="bg-white border-b border-gray-200 sticky top-0 z-50">
<!-- 全局透明遮罩 -->
<Transition name="fade">
<div
v-if="mobileMenuOpen"
@click.self="mobileMenuOpen = false"
class="fixed inset-0 z-40 md:hidden"
></div>
</Transition>

<header class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 sticky top-0 z-50">
<div class="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
<NuxtLink to="/" class="text-lg font-bold text-primary-600 hover:text-primary-700">

<!-- 1. 左侧区域:Logo -->
<NuxtLink to="/" class="text-lg font-bold text-primary-600 hover:text-primary-700 shrink-0">
Trans‑Search
</NuxtLink>
<nav class="flex items-center gap-1">
<NuxtLink to="/" class="px-3 py-1.5 rounded-md text-sm hover:bg-gray-100" active-class="bg-primary-50 text-primary-700 font-medium">
搜索

<!-- 2. 中央区域:桌面端导航 -->
<nav class="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
<NuxtLink to="/" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium">
<Icon name="mdi:magnify" class="w-4 h-4 shrink-0 -mt-px" />
<span>搜索</span>
</NuxtLink>
<NuxtLink to="/tree" class="px-3 py-1.5 rounded-md text-sm hover:bg-gray-100" active-class="bg-primary-50 text-primary-700 font-medium">
分类
<NuxtLink to="/tree" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium">
<Icon name="mdi:file-tree" class="w-4 h-4 shrink-0 -mt-px" />
<span>分类</span>
</NuxtLink>
<NuxtLink to="/stats" class="px-3 py-1.5 rounded-md text-sm hover:bg-gray-100" active-class="bg-primary-50 text-primary-700 font-medium">
统计
<NuxtLink to="/stats" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium">
<Icon name="mdi:chart-bar" class="w-4 h-4 shrink-0 -mt-px" />
<span>统计</span>
</NuxtLink>
<NuxtLink to="/config" class="px-3 py-1.5 rounded-md text-sm hover:bg-gray-100" active-class="bg-primary-50 text-primary-700 font-medium">
管理
<NuxtLink to="/config" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium">
<Icon name="mdi:cog" class="w-4 h-4 shrink-0 -mt-px" />
<span>管理</span>
</NuxtLink>
<NuxtLink to="/about" class="px-3 py-1.5 rounded-md text-sm hover:bg-gray-100" active-class="bg-primary-50 text-primary-700 font-medium">
关于
<NuxtLink to="/about" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm hover:bg-gray-100 dark:hover:bg-gray-800 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium">
<Icon name="mdi:information-outline" class="w-4 h-4 shrink-0 -mt-px" />
<span>关于</span>
</NuxtLink>
</nav>

<!-- 3. 右侧区域:桌面端暗色切换 + 移动端按钮组 -->
<div class="flex items-center gap-1 shrink-0">
<!-- 桌面端暗色模式切换 -->
<button
class="hidden md:flex p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
aria-label="切换暗色模式"
@click="toggleDark"
>
<Icon v-if="isDark" name="mdi:white-balance-sunny" class="w-4 h-4 shrink-0 -mt-px" />
<Icon v-else name="mdi:moon-waning-crescent" class="w-4 h-4 shrink-0 -mt-px" />
</button>

<!-- 移动端:暗色切换 + 汉堡菜单 -->
<button
class="md:hidden p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 dark:text-gray-400"
aria-label="切换暗色模式"
@click="toggleDark"
>
<Icon v-if="isDark" name="mdi:white-balance-sunny" class="w-4 h-4 shrink-0" />
<Icon v-else name="mdi:moon-waning-crescent" class="w-4 h-4 shrink-0" />
</button>

<button
class="md:hidden p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300"
aria-label="菜单"
:aria-expanded="mobileMenuOpen"
@click="mobileMenuOpen = !mobileMenuOpen"
>
<Icon v-if="!mobileMenuOpen" name="mdi:menu" class="w-5 h-5 shrink-0" />
<Icon v-else name="mdi:close" class="w-5 h-5 shrink-0" />
</button>
</div>
</div>

<!-- 移动端 Dropdown 菜单 -->
<Transition name="dropdown">
<nav
v-if="mobileMenuOpen"
class="absolute top-full right-4 mt-2 w-48 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1 z-50 flex flex-col md:hidden"
>
<NuxtLink to="/" class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium" @click="mobileMenuOpen = false">
<Icon name="mdi:magnify" class="w-4 h-4 shrink-0 -mt-px" />
<span>搜索</span>
</NuxtLink>
<NuxtLink to="/tree" class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium" @click="mobileMenuOpen = false">
<Icon name="mdi:file-tree" class="w-4 h-4 shrink-0 -mt-px" />
<span>分类</span>
</NuxtLink>
<NuxtLink to="/stats" class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium" @click="mobileMenuOpen = false">
<Icon name="mdi:chart-bar" class="w-4 h-4 shrink-0 -mt-px" />
<span>统计</span>
</NuxtLink>
<NuxtLink to="/config" class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium" @click="mobileMenuOpen = false">
<Icon name="mdi:cog" class="w-4 h-4 shrink-0 -mt-px" />
<span>管理</span>
</NuxtLink>
<NuxtLink to="/about" class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300" active-class="bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 font-medium" @click="mobileMenuOpen = false">
<Icon name="mdi:information-outline" class="w-4 h-4 shrink-0 -mt-px" />
<span>关于</span>
</NuxtLink>
</nav>
</Transition>
</header>
<main class="flex-1">

<main class="flex-1 bg-gray-50 dark:bg-gray-950">
<NuxtPage />
</main>
</div>
</template>

<script setup lang="ts">
const mobileMenuOpen = ref(false)
const { isDark, toggle: toggleDark } = useDarkMode()

// 1. 菜单打开时锁定背景滚动
watch(mobileMenuOpen, (isOpen) => {
if (import.meta.client) {
document.body.style.overflow = isOpen ? 'hidden' : ''
}
})

// 2. 路由切换时关闭菜单
const router = useRouter()
router.afterEach(() => {
mobileMenuOpen.value = false
})

// 3. 按 Esc 键关闭菜单
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && mobileMenuOpen.value) {
mobileMenuOpen.value = false
}
}

onMounted(() => window.addEventListener('keydown', handleKeydown))
onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
</script>

<style>
/* Dropdown 弹出动画 */
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-8px) scale(0.95);
}
.dropdown-enter-to,
.dropdown-leave-from {
opacity: 1;
transform: translateY(0) scale(1);
}

/* 遮罩层淡入淡出 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
45 changes: 44 additions & 1 deletion frontend/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@
@tailwind components;
@tailwind utilities;

/* 1. 全局基础样式(亮色模式) */
html {
color-scheme: light !important; /* 强制亮色原生控件 */
}

body {
@apply bg-gray-50 text-gray-900 min-h-screen;
@apply bg-gray-50 text-gray-900 min-h-screen transition-colors duration-300;
}

/* 2. 当 JS 加上 .dark 类时,强制深色模式 */
html.dark {
color-scheme: dark !important; /* 【关键】强制深色原生控件(解决滚动条/表单亮色问题) */
}

html.dark body {
Comment thread
ayyyyano marked this conversation as resolved.
@apply bg-gray-950 text-gray-100;
}

/* 3. 手动接管深色模式下的滚动条样式,确保视觉绝对统一 */
html.dark {
--scrollbar-bg: #1f2937; /* gray-800 */
--scrollbar-thumb: #4b5563; /* gray-600 */
--scrollbar-thumb-hover: #6b7280; /* gray-500 */
}

/* Webkit 内核浏览器(Chrome, Safari, Edge) */
html.dark ::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: var(--scrollbar-bg);
}

html.dark ::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: 4px;
}

html.dark ::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover);
}

/* Firefox 浏览器 */
html.dark * {
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg);
scrollbar-width: thin;
}
105 changes: 105 additions & 0 deletions frontend/composables/useDarkMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
export function useDarkMode() {
const colorMode = useCookie<string>("dark-mode", {
default: () => "",
watch: true,
maxAge: 60 * 60 * 24 * 365,
})


const isDark = computed({
get: () => {
if (colorMode.value === "dark") {
return true
}

if (colorMode.value === "light") {
return false
}

// SSR 阶段读取不到系统偏好
// 首屏状态已经由 nuxt.config.ts script 设置
if (import.meta.client) {
return document.documentElement
.classList.contains("dark")
}

return false
},

set(value: boolean) {
colorMode.value = value
? "dark"
: "light"
},
})


function applyTheme(value: boolean) {
document.documentElement.classList.toggle(
"dark",
value
)
}


if (import.meta.client) {

watch(
isDark,
(value) => {
applyTheme(value)
},
{
immediate: true,
}
)


const mediaQuery =
window.matchMedia(
"(prefers-color-scheme: dark)"
)


const handleSystemChange = (
e: MediaQueryListEvent
) => {

// 只有跟随系统时响应
if (!colorMode.value) {
applyTheme(e.matches)
}
}


mediaQuery.addEventListener(
"change",
handleSystemChange
)


onUnmounted(() => {
mediaQuery.removeEventListener(
"change",
handleSystemChange
)
})
}


function toggle() {
isDark.value = !isDark.value
}


function clearPreference() {
colorMode.value = ""
}


return {
isDark,
toggle,
clearPreference,
}
}
Loading