feat: 三层明暗模式 + 移动端代码块适配
- 新增 useTheme.js 单例主题管理器:用户手动选择 > 系统偏好 > 时间自动切换(8AM/6PM) - config.ts 添加 head 内联脚本防止主题闪烁 - 移动端导航按钮右对齐(margin-left:auto) - 首页和文档页代码块移动端溢出修复
This commit is contained in:
parent
4c610fde51
commit
a09d805bd9
@ -63,6 +63,18 @@ export default defineConfig({
|
||||
['link', { rel: 'icon', href: '/favicon.png' }],
|
||||
['meta', { property: 'og:image', content: '/og-image.png' }],
|
||||
['meta', { name: 'theme-color', content: '#C8963E' }],
|
||||
['script', {}, `
|
||||
(function(){
|
||||
var E='mengstack-theme-explicit',V='vitepress-theme-appearance';
|
||||
var d=false,e=localStorage.getItem(E);
|
||||
if(e!==null){d=e==='true'}
|
||||
else if(window.matchMedia&&window.matchMedia('(prefers-color-scheme:dark)').matches){d=true}
|
||||
else if(window.matchMedia&&window.matchMedia('(prefers-color-scheme:light)').matches){d=false}
|
||||
else{var h=new Date().getHours();d=h<8||h>=18}
|
||||
if(d)document.documentElement.classList.add('dark');
|
||||
try{localStorage.setItem(V,String(d))}catch(x){}
|
||||
})()
|
||||
`],
|
||||
],
|
||||
|
||||
transformHead({ pageData }) {
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<a href="/community">社区</a>
|
||||
</nav>
|
||||
<div class="home-nav-actions">
|
||||
<button class="home-icon-btn" aria-label="切换明暗主题" title="切换主题" @click="toggleDark">
|
||||
<button class="home-icon-btn" aria-label="切换明暗主题" title="切换主题" @click="handleToggle">
|
||||
<svg aria-hidden="true" class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||
<svg aria-hidden="true" class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>
|
||||
</button>
|
||||
@ -338,18 +338,14 @@ Check /health</pre>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { initThemeManager } from './useTheme'
|
||||
|
||||
const { isDark } = useData()
|
||||
const { isDark, handleToggle } = initThemeManager()
|
||||
|
||||
const navEl = ref(null)
|
||||
const scrollY = ref(0)
|
||||
const menuOpen = ref(false)
|
||||
|
||||
function toggleDark() {
|
||||
isDark.value = !isDark.value
|
||||
}
|
||||
|
||||
function copyCode(e) {
|
||||
const btn = e.currentTarget
|
||||
const pre = btn.parentElement.querySelector('pre')
|
||||
@ -780,6 +776,7 @@ watch(menuOpen, (open) => {
|
||||
.home-stat{padding:10px 0}
|
||||
.home-nav-links{display:none}
|
||||
.hamburger{display:grid}
|
||||
.home-nav-actions{margin-left:auto}
|
||||
.home-nav.nav-menu-open .home-nav-links{
|
||||
display:flex;flex-direction:column;position:absolute;top:64px;left:0;right:0;
|
||||
background:var(--home-bg);border-bottom:1px solid var(--home-border);padding:12px 20px 18px;gap:2px;
|
||||
@ -799,7 +796,10 @@ watch(menuOpen, (open) => {
|
||||
.home-stats-grid{grid-template-columns:1fr}
|
||||
.home-trust-inner{gap:10px 16px}
|
||||
.home-chip{font-size:13px;padding:5px 10px}
|
||||
.home-terminal-body{font-size:12.5px;padding:16px}
|
||||
.home-terminal-body{font-size:12px;padding:14px 12px;-webkit-overflow-scrolling:touch}
|
||||
.home-terminal{border-radius:10px}
|
||||
.home-codeblock{padding:12px 14px;border-radius:8px;-webkit-overflow-scrolling:touch}
|
||||
.home-codeblock pre{font-size:11.5px;line-height:1.6}
|
||||
.home-hero h1{font-size:clamp(28px,7vw,38px)}
|
||||
.home-about .home-about-card{padding:24px 18px}
|
||||
.home-cta-card{padding:40px 20px}
|
||||
@ -814,6 +814,8 @@ watch(menuOpen, (open) => {
|
||||
.home-brand img{width:26px;height:26px}
|
||||
.home-badge{font-size:12px;padding:5px 10px}
|
||||
.home-hero{padding:100px 0 48px}
|
||||
.home-terminal-body{font-size:11px;padding:12px 10px}
|
||||
.home-codeblock pre{font-size:11px}
|
||||
}
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
.home-root *,.home-root *::before,.home-root *::after{animation-duration:.01ms!important;transition-duration:.01ms!important;scroll-behavior:auto!important}
|
||||
|
||||
@ -3,6 +3,9 @@ import DefaultTheme from 'vitepress/theme'
|
||||
import { useRoute } from 'vitepress'
|
||||
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import Home from './Home.vue'
|
||||
import { initThemeManager } from './useTheme'
|
||||
|
||||
initThemeManager()
|
||||
|
||||
const route = useRoute()
|
||||
const isHome = computed(() => route.path === '/' || route.path === '/index.html')
|
||||
|
||||
@ -633,6 +633,28 @@
|
||||
.vp-doc .content-container {
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
/* Code blocks mobile */
|
||||
.vp-doc div[class*='language-'] {
|
||||
margin: 12px 0 !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
.vp-doc div[class*='language-'] pre {
|
||||
font-size: 12.5px !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.vp-doc pre {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.vp-doc pre code {
|
||||
white-space: pre;
|
||||
word-break: keep-all;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Small mobile <420 --- */
|
||||
@ -644,6 +666,13 @@
|
||||
padding: 0 20px !important;
|
||||
font-size: 0.88rem !important;
|
||||
}
|
||||
.vp-doc div[class*='language-'] pre {
|
||||
font-size: 11.5px !important;
|
||||
}
|
||||
.vp-doc pre {
|
||||
font-size: 11.5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ HOME LAYOUT OVERRIDES ============ */
|
||||
|
||||
125
.vitepress/theme/useTheme.js
Normal file
125
.vitepress/theme/useTheme.js
Normal file
@ -0,0 +1,125 @@
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const EXPLICIT_KEY = 'mengstack-theme-explicit'
|
||||
const VP_KEY = 'vitepress-theme-appearance'
|
||||
const DAY_START = 8
|
||||
const NIGHT_START = 18
|
||||
|
||||
let initialized = false
|
||||
let _isDark = null
|
||||
let _handleToggle = null
|
||||
|
||||
function isTimeDark() {
|
||||
const h = new Date().getHours()
|
||||
return h < DAY_START || h >= NIGHT_START
|
||||
}
|
||||
|
||||
function getSystemPref() {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) return null
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return true
|
||||
if (window.matchMedia('(prefers-color-scheme: light)').matches) return false
|
||||
return null
|
||||
}
|
||||
|
||||
export function initThemeManager() {
|
||||
if (initialized) return { isDark: _isDark, handleToggle: _handleToggle }
|
||||
|
||||
const vp = useData()
|
||||
const isDark = vp.isDark
|
||||
initialized = true
|
||||
_isDark = isDark
|
||||
|
||||
let explicitChoice = null
|
||||
let timer = null
|
||||
let sysListener = null
|
||||
let observer = null
|
||||
let suppressObs = false
|
||||
|
||||
function sync(value) {
|
||||
suppressObs = true
|
||||
isDark.value = value
|
||||
try {
|
||||
localStorage.setItem(VP_KEY, String(value))
|
||||
document.documentElement.classList.toggle('dark', value)
|
||||
} catch {}
|
||||
setTimeout(() => { suppressObs = false }, 150)
|
||||
}
|
||||
|
||||
function doAutoSwitch() {
|
||||
const shouldBeDark = isTimeDark()
|
||||
if (isDark.value !== shouldBeDark) {
|
||||
sync(shouldBeDark)
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggle() {
|
||||
explicitChoice = !isDark.value
|
||||
try { localStorage.setItem(EXPLICIT_KEY, String(explicitChoice)) } catch {}
|
||||
if (timer) { clearInterval(timer); timer = null }
|
||||
sync(explicitChoice)
|
||||
}
|
||||
|
||||
_handleToggle = handleToggle
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(EXPLICIT_KEY)
|
||||
if (stored !== null) explicitChoice = stored === 'true'
|
||||
} catch {}
|
||||
|
||||
if (explicitChoice !== null) {
|
||||
sync(explicitChoice)
|
||||
} else {
|
||||
const sysPref = getSystemPref()
|
||||
if (sysPref !== null) {
|
||||
sync(sysPref)
|
||||
} else {
|
||||
sync(isTimeDark())
|
||||
}
|
||||
}
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (explicitChoice !== null) return
|
||||
if (getSystemPref() !== null) return
|
||||
doAutoSwitch()
|
||||
}, 30000)
|
||||
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
sysListener = (e) => {
|
||||
if (explicitChoice !== null) return
|
||||
sync(e.matches)
|
||||
}
|
||||
mql.addEventListener('change', sysListener)
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
observer = new MutationObserver(() => {
|
||||
if (suppressObs) return
|
||||
const dark = document.documentElement.classList.contains('dark')
|
||||
if (explicitChoice === null) {
|
||||
explicitChoice = dark
|
||||
try { localStorage.setItem(EXPLICIT_KEY, String(explicitChoice)) } catch {}
|
||||
}
|
||||
isDark.value = dark
|
||||
try { localStorage.setItem(VP_KEY, String(dark)) } catch {}
|
||||
})
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) clearInterval(timer)
|
||||
if (sysListener && typeof window !== 'undefined' && window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', sysListener)
|
||||
}
|
||||
if (observer) observer.disconnect()
|
||||
initialized = false
|
||||
})
|
||||
|
||||
return { isDark, handleToggle }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user