mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-19 06:30:35 +08:00
Limit long-lived page and component retention while virtualizing large card views to keep runtime memory lower. Defer heavy editor, chart, workflow, calendar, and icon code so the app loads less JavaScript up front.
225 lines
5.5 KiB
TypeScript
225 lines
5.5 KiB
TypeScript
// 主题管理器 - 动态加载主题CSS
|
||
export interface ThemeConfig {
|
||
name: string
|
||
cssPath: string
|
||
isLoaded: boolean
|
||
}
|
||
|
||
class ThemeManager {
|
||
private themes: Map<string, ThemeConfig> = new Map()
|
||
private currentTheme: string = 'default'
|
||
private loadedLinks: Map<string, HTMLLinkElement> = new Map()
|
||
private themeListeners: Map<(theme: string) => void, EventListener> = new Map()
|
||
|
||
constructor() {
|
||
// 注册所有可用主题
|
||
this.registerTheme('default', '')
|
||
this.registerTheme('light', '')
|
||
this.registerTheme('dark', '')
|
||
this.registerTheme('purple', '')
|
||
this.registerTheme('auto', '')
|
||
// 只有透明主题有特定的CSS文件
|
||
this.registerTheme('transparent', './src/styles/themes/transparent.css')
|
||
}
|
||
|
||
/**
|
||
* 注册主题
|
||
*/
|
||
registerTheme(name: string, cssPath: string): void {
|
||
this.themes.set(name, {
|
||
name,
|
||
cssPath,
|
||
isLoaded: false,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 获取当前主题
|
||
*/
|
||
getCurrentTheme(): string {
|
||
return this.currentTheme
|
||
}
|
||
|
||
/**
|
||
* 设置主题
|
||
*/
|
||
async setTheme(themeName: string): Promise<void> {
|
||
if (!this.themes.has(themeName)) {
|
||
console.warn(`Theme "${themeName}" not found`)
|
||
return
|
||
}
|
||
|
||
const theme = this.themes.get(themeName)!
|
||
|
||
// 清理其他主题的CSS(除了当前要设置的主题)
|
||
this.unloadOtherThemes()
|
||
|
||
// 如果主题有CSS文件,则加载CSS
|
||
if (theme.cssPath) {
|
||
try {
|
||
await this.loadThemeCSS(themeName, theme.cssPath)
|
||
} catch (error) {
|
||
console.error(`Failed to load CSS for theme "${themeName}":`, error)
|
||
// 即使CSS加载失败,也继续应用主题(使用默认样式)
|
||
}
|
||
}
|
||
|
||
// 应用主题(无论是否有CSS文件)
|
||
this.applyTheme(themeName)
|
||
}
|
||
|
||
/**
|
||
* 加载主题CSS文件
|
||
*/
|
||
private async loadThemeCSS(themeName: string, cssPath: string): Promise<void> {
|
||
// 如果已经加载过,直接返回
|
||
if (this.loadedLinks.has(themeName)) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 动态导入CSS模块
|
||
if (themeName === 'transparent') {
|
||
await import('@/styles/themes/transparent.scss')
|
||
this.themes.get(themeName)!.isLoaded = true
|
||
return
|
||
}
|
||
|
||
// 对于其他主题,使用传统的link方式
|
||
const link = document.createElement('link')
|
||
link.rel = 'stylesheet'
|
||
link.type = 'text/css'
|
||
link.href = cssPath
|
||
link.id = `theme-${themeName}`
|
||
|
||
// 等待CSS加载完成
|
||
await new Promise<void>((resolve, reject) => {
|
||
link.onload = () => {
|
||
this.loadedLinks.set(themeName, link)
|
||
this.themes.get(themeName)!.isLoaded = true
|
||
resolve()
|
||
}
|
||
link.onerror = () => {
|
||
reject(new Error(`Failed to load theme CSS: ${cssPath}`))
|
||
}
|
||
})
|
||
|
||
// 添加到head
|
||
document.head.appendChild(link)
|
||
} catch (error) {
|
||
console.error(`Error loading theme "${themeName}":`, error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用主题到DOM
|
||
*/
|
||
private applyTheme(themeName: string): void {
|
||
// 移除之前的主题属性
|
||
document.documentElement.removeAttribute('data-theme')
|
||
|
||
// 设置新主题(除了default主题)
|
||
if (themeName !== 'default') {
|
||
document.documentElement.setAttribute('data-theme', themeName)
|
||
}
|
||
|
||
this.currentTheme = themeName
|
||
|
||
// 触发主题变更事件
|
||
this.dispatchThemeChangeEvent(themeName)
|
||
}
|
||
|
||
/**
|
||
* 卸载主题CSS
|
||
*/
|
||
unloadTheme(themeName: string): void {
|
||
const theme = this.themes.get(themeName)
|
||
if (!theme) return
|
||
|
||
// 对于动态导入的CSS,我们无法直接卸载,但可以标记为未加载
|
||
if (themeName === 'transparent') {
|
||
theme.isLoaded = false
|
||
return
|
||
}
|
||
|
||
// 对于传统link方式加载的CSS
|
||
const link = this.loadedLinks.get(themeName)
|
||
if (link) {
|
||
link.remove()
|
||
this.loadedLinks.delete(themeName)
|
||
theme.isLoaded = false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 卸载所有主题CSS(除了当前主题)
|
||
*/
|
||
unloadOtherThemes(): void {
|
||
for (const [themeName] of this.themes) {
|
||
if (themeName !== this.currentTheme && this.themes.get(themeName)?.isLoaded) {
|
||
this.unloadTheme(themeName)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取已注册的主题列表
|
||
*/
|
||
getAvailableThemes(): string[] {
|
||
return Array.from(this.themes.keys())
|
||
}
|
||
|
||
/**
|
||
* 检查主题是否已加载
|
||
*/
|
||
isThemeLoaded(themeName: string): boolean {
|
||
return this.themes.get(themeName)?.isLoaded || false
|
||
}
|
||
|
||
/**
|
||
* 触发主题变更事件
|
||
*/
|
||
private dispatchThemeChangeEvent(themeName: string): void {
|
||
const event = new CustomEvent('themechange', {
|
||
detail: { theme: themeName },
|
||
})
|
||
document.dispatchEvent(event)
|
||
}
|
||
|
||
/**
|
||
* 监听主题变更事件
|
||
*/
|
||
onThemeChange(callback: (theme: string) => void): void {
|
||
if (this.themeListeners.has(callback)) {
|
||
return
|
||
}
|
||
|
||
const listener: EventListener = event => {
|
||
callback((event as CustomEvent<{ theme: string }>).detail.theme)
|
||
}
|
||
|
||
this.themeListeners.set(callback, listener)
|
||
document.addEventListener('themechange', listener)
|
||
}
|
||
|
||
/**
|
||
* 移除主题变更监听器
|
||
*/
|
||
offThemeChange(callback: (theme: string) => void): void {
|
||
const listener = this.themeListeners.get(callback)
|
||
if (!listener) {
|
||
return
|
||
}
|
||
|
||
document.removeEventListener('themechange', listener)
|
||
this.themeListeners.delete(callback)
|
||
}
|
||
}
|
||
|
||
// 创建单例实例
|
||
export const themeManager = new ThemeManager()
|
||
|
||
// 导出类型
|
||
export type { ThemeManager }
|