Files
BYW-VoiceHub/app/composables/useTheme.ts
T
Henry_Du bd8df1f026
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 6m31s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
feat(songs): 歌曲管理支持批量驳回、投稿留言自动审查与排期范围可见性过滤
- 新增 /api/admin/songs/batch-reject.post 批量驳回接口(事务清排期/投票/扣接纳数/黑名单去重/异步通知)
- requireSongAdmin 三角色校验 + createApiError 统一错误包装
- scheduleDaysBefore/After 四字段排期可见范围限制(开放 API 与公开排期接口三处缓存路径 + DB 条件过滤)
- submissionNoteAutoModeration 基于黑名单 KEYWORD/LANGUAGE/GENRE 自动判定留言 publicStatus,命中自动关闭 submissionNotePublic
- 更新 songs/request.post 与 songs/[id]/update.put 接入自动审查逻辑
2026-09-06 17:03:20 +08:00

244 lines
6.4 KiB
TypeScript

import { computed, ref, watch, nextTick } from 'vue'
const STORAGE_KEY = 'voicehub:theme'
type ThemeName = string & {}
const currentTheme = ref<string | null>(null)
const initialized = ref(false)
function isBrowser() {
return typeof window !== 'undefined' && typeof document !== 'undefined'
}
function getMetaThemeColor() {
if (!isBrowser()) return null
return document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
}
function parseEnabledThemesRaw(raw: unknown): string[] {
if (Array.isArray(raw)) return raw.filter(Boolean) as string[]
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return []
if (s.startsWith('[') || s.startsWith('{')) {
try {
const parsed = JSON.parse(s)
if (Array.isArray(parsed)) return parsed.filter(Boolean) as string[]
if (parsed && Array.isArray((parsed as any).items)) return (parsed as any).items.filter(Boolean) as string[]
} catch (_) {
// fallback
}
}
return s.split(',').map((x) => x.trim()).filter(Boolean)
}
return []
}
function applyTheme(theme: string) {
if (!isBrowser()) return
const root = document.documentElement
const appRoot = document.querySelector<HTMLElement>('div.app')
root.setAttribute('data-theme', theme)
if (appRoot) appRoot.setAttribute('data-theme', theme)
const effective = resolveEffectiveTheme(theme)
if (effective === 'dark') {
root.classList.add('dark')
root.classList.remove('light')
root.style.colorScheme = 'dark'
} else if (effective === 'light') {
root.classList.add('light')
root.classList.remove('dark')
root.style.colorScheme = 'light'
} else {
root.classList.remove('dark', 'light')
root.style.colorScheme = 'light dark'
}
const meta = getMetaThemeColor()
if (meta) {
meta.content = effective === 'light' ? '#f8fafc' : '#111111'
}
try {
localStorage.setItem(STORAGE_KEY, theme)
} catch {
/* ignore storage errors */
}
}
function detectSystemTheme(): 'dark' | 'light' {
if (!isBrowser()) return 'dark'
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
return prefersDark ? 'dark' : 'light'
}
function resolveEffectiveTheme(theme: string): 'dark' | 'light' | string {
if (theme === 'System' || theme === 'system') {
return detectSystemTheme()
}
if (theme === 'dark' || theme.endsWith('Dark')) return 'dark'
if (theme === 'light' || theme.endsWith('Light')) return 'light'
return theme
}
export function useTheme() {
const config = useRuntimeConfig()
const { isLoaded: siteConfigLoaded, defaultTheme: siteDefaultTheme, enabledThemes: siteEnabledThemes, initSiteConfig } = useSiteConfig()
const enabledThemes = computed<string[]>(() => {
if (siteConfigLoaded.value && siteEnabledThemes.value.length > 0) {
return [...new Set(siteEnabledThemes.value)]
}
const fromRuntime = parseEnabledThemesRaw(config.public?.enabledThemes)
if (fromRuntime.length > 0) return fromRuntime
return ['System', 'dark', 'light']
})
const defaultTheme = computed<string>(() => {
if (siteConfigLoaded.value && siteDefaultTheme.value) {
return siteDefaultTheme.value
}
const fromRuntime = config.public?.defaultTheme
if (typeof fromRuntime === 'string' && fromRuntime) {
return fromRuntime
}
return 'System'
})
const theme = computed<string>({
get: () => currentTheme.value ?? defaultTheme.value,
set: (next: string) => setTheme(next)
})
const isDark = computed(() => {
return resolveEffectiveTheme(theme.value) === 'dark'
})
function isThemeEnabled(name: string) {
return enabledThemes.value.includes(name)
}
function firstValidTheme(candidate: string): string {
const list = enabledThemes.value
if (list.includes(candidate)) return candidate
if (list.includes(defaultTheme.value)) return defaultTheme.value
return list[0] ?? 'System'
}
function setTheme(next: string, { persist = true } = {}) {
const candidate = firstValidTheme(next)
if (currentTheme.value === candidate && initialized.value) return
currentTheme.value = candidate
applyTheme(candidate)
if (!persist && isBrowser()) {
try {
localStorage.removeItem(STORAGE_KEY)
} catch {
/* ignore */
}
}
}
function toggleTheme() {
const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['System', 'dark', 'light']
const current = theme.value
const idx = order.indexOf(current)
const next = order[(idx + 1) % order.length]
setTheme(next)
}
function resetToDefault() {
setTheme(defaultTheme.value, { persist: false })
}
function onSystemThemeChange() {
const stored = (() => {
try {
return localStorage.getItem(STORAGE_KEY)
} catch {
return null
}
})()
if (!stored) {
applyTheme(theme.value)
}
}
async function init() {
if (initialized.value || !isBrowser()) return
if (!siteConfigLoaded.value) {
try {
await initSiteConfig()
await nextTick()
} catch (_) {
/* ignore */
}
}
initialized.value = true
let stored: string | null = null
try {
stored = localStorage.getItem(STORAGE_KEY)
} catch {
stored = null
}
if (stored) {
const valid = firstValidTheme(stored)
if (valid === stored || enabledThemes.value.includes(stored)) {
setTheme(stored)
watchSystemTheme()
return
}
}
setTheme(defaultTheme.value, { persist: false })
watchSystemTheme()
}
function watchSystemTheme() {
if (!isBrowser() || !window.matchMedia) return
const mql = window.matchMedia('(prefers-color-scheme: dark)')
if (typeof mql.addEventListener === 'function') {
mql.addEventListener('change', onSystemThemeChange)
} else if (typeof (mql as any).addListener === 'function') {
;(mql as any).addListener(onSystemThemeChange)
}
}
watch(defaultTheme, () => {
if (!initialized.value) return
const stored = (() => {
try {
return localStorage.getItem(STORAGE_KEY)
} catch {
return null
}
})()
if (!stored) {
setTheme(defaultTheme.value, { persist: false })
}
})
return {
theme,
currentTheme,
defaultTheme,
enabledThemes,
isDark,
isThemeEnabled,
setTheme,
toggleTheme,
resetToDefault,
init
}
}