diff --git a/.gitignore b/.gitignore index 2b3e889..eee2459 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ backend-cf/.dev.vars frontend/node_modules/ frontend/.nuxt/ +frontend/.output/ # wrangler files .wrangler diff --git a/frontend/app.vue b/frontend/app.vue index 381db79..4c731ee 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -1,31 +1,168 @@ + + + + \ No newline at end of file diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index f0f4e6d..5045a42 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -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 { + @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; } diff --git a/frontend/composables/useDarkMode.ts b/frontend/composables/useDarkMode.ts new file mode 100644 index 0000000..ef8864c --- /dev/null +++ b/frontend/composables/useDarkMode.ts @@ -0,0 +1,105 @@ +export function useDarkMode() { + const colorMode = useCookie("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, + } +} \ No newline at end of file diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 4681883..3343f5b 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,7 +1,17 @@ export default defineNuxtConfig({ compatibilityDate: "2026-07-14", devtools: { enabled: true }, - modules: ["@nuxtjs/tailwindcss"], + modules: ["@nuxtjs/tailwindcss", "@nuxt/icon"], + icon: { + serverBundle: { + collections: ["mdi"], + }, + }, + tailwindcss: { + config: { + darkMode: "class", + }, + }, nitro: { preset: "cloudflare_module", devProxy: { @@ -15,8 +25,46 @@ export default defineNuxtConfig({ head: { title: "Trans-Search", meta: [{ name: "description", content: "跨性别信息聚合搜索" }], + + // 防止 SSR 首屏深色闪烁 + script: [ + { + children: ` +(function () { + try { + var cookie = document.cookie.match( + /(?:^|; )dark-mode=([^;]*)/ + ); + + var preference = cookie + ? decodeURIComponent(cookie[1]) + : ""; + + var dark = false; + + if (preference === "dark") { + dark = true; + } else if (preference === "light") { + dark = false; + } else { + dark = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + } + + document.documentElement.classList.toggle( + "dark", + dark + ); + } catch (_) {} +})(); + `, + type: "text/javascript", + }, + ], }, }, + css: ["~/assets/css/main.css"], runtimeConfig: { public: { diff --git a/frontend/package.json b/frontend/package.json index 28d1ad5..bde0a43 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,14 @@ "preview": "nuxt preview" }, "dependencies": { + "@nuxtjs/tailwindcss": "^6.13.0", "nuxt": "^3.15.0", - "vue": "^3.5.0", - "@nuxtjs/tailwindcss": "^6.13.0" + "vue": "^3.5.0" }, "devDependencies": { + "@iconify-json/fa-solid": "^1.2.2", + "@iconify-json/mdi": "^1.2.3", + "@nuxt/icon": "^1.15.0", "tailwindcss": "^3.4.0" } } diff --git a/frontend/pages/about.vue b/frontend/pages/about.vue index 9c9b320..b468f38 100644 --- a/frontend/pages/about.vue +++ b/frontend/pages/about.vue @@ -1,13 +1,13 @@