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' }],
|
['link', { rel: 'icon', href: '/favicon.png' }],
|
||||||
['meta', { property: 'og:image', content: '/og-image.png' }],
|
['meta', { property: 'og:image', content: '/og-image.png' }],
|
||||||
['meta', { name: 'theme-color', content: '#C8963E' }],
|
['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 }) {
|
transformHead({ pageData }) {
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<a href="/community">社区</a>
|
<a href="/community">社区</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="home-nav-actions">
|
<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="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>
|
<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>
|
</button>
|
||||||
@ -338,18 +338,14 @@ Check /health</pre>
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
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 navEl = ref(null)
|
||||||
const scrollY = ref(0)
|
const scrollY = ref(0)
|
||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
|
|
||||||
function toggleDark() {
|
|
||||||
isDark.value = !isDark.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyCode(e) {
|
function copyCode(e) {
|
||||||
const btn = e.currentTarget
|
const btn = e.currentTarget
|
||||||
const pre = btn.parentElement.querySelector('pre')
|
const pre = btn.parentElement.querySelector('pre')
|
||||||
@ -780,6 +776,7 @@ watch(menuOpen, (open) => {
|
|||||||
.home-stat{padding:10px 0}
|
.home-stat{padding:10px 0}
|
||||||
.home-nav-links{display:none}
|
.home-nav-links{display:none}
|
||||||
.hamburger{display:grid}
|
.hamburger{display:grid}
|
||||||
|
.home-nav-actions{margin-left:auto}
|
||||||
.home-nav.nav-menu-open .home-nav-links{
|
.home-nav.nav-menu-open .home-nav-links{
|
||||||
display:flex;flex-direction:column;position:absolute;top:64px;left:0;right:0;
|
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;
|
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-stats-grid{grid-template-columns:1fr}
|
||||||
.home-trust-inner{gap:10px 16px}
|
.home-trust-inner{gap:10px 16px}
|
||||||
.home-chip{font-size:13px;padding:5px 10px}
|
.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-hero h1{font-size:clamp(28px,7vw,38px)}
|
||||||
.home-about .home-about-card{padding:24px 18px}
|
.home-about .home-about-card{padding:24px 18px}
|
||||||
.home-cta-card{padding:40px 20px}
|
.home-cta-card{padding:40px 20px}
|
||||||
@ -814,6 +814,8 @@ watch(menuOpen, (open) => {
|
|||||||
.home-brand img{width:26px;height:26px}
|
.home-brand img{width:26px;height:26px}
|
||||||
.home-badge{font-size:12px;padding:5px 10px}
|
.home-badge{font-size:12px;padding:5px 10px}
|
||||||
.home-hero{padding:100px 0 48px}
|
.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){
|
@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}
|
.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 { useRoute } from 'vitepress'
|
||||||
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import Home from './Home.vue'
|
import Home from './Home.vue'
|
||||||
|
import { initThemeManager } from './useTheme'
|
||||||
|
|
||||||
|
initThemeManager()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const isHome = computed(() => route.path === '/' || route.path === '/index.html')
|
const isHome = computed(() => route.path === '/' || route.path === '/index.html')
|
||||||
|
|||||||
@ -633,6 +633,28 @@
|
|||||||
.vp-doc .content-container {
|
.vp-doc .content-container {
|
||||||
padding: 0 16px !important;
|
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 --- */
|
/* --- Small mobile <420 --- */
|
||||||
@ -644,6 +666,13 @@
|
|||||||
padding: 0 20px !important;
|
padding: 0 20px !important;
|
||||||
font-size: 0.88rem !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 ============ */
|
/* ============ 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