mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-05-24 22:05:46 +08:00
依赖: - pnpm update --latest(vite、vue、eslint、typescript 等 14 个包到最新版) - 显式加入 vue-eslint-parser ^10.4.0 / workbox-build ^7.4.1,消除 peer 警告 代码质量: - 修复 ESLint vue parser 配置(用 vue-eslint-parser 作主 parser),lint 错误 17 → 0 - tsconfig 启用 noUncheckedIndexedAccess,修复 16 个潜在 undefined 访问 bug - 删除未使用的 src/utils/icons.ts(723 行死代码) 打包体积: - PWA precache 10.5 MB / 424 项 → 740 KB / 24 项(字体改为运行时缓存) - vendor CSS gzip 201 KB → 141 KB(禁用字体 base64 内联,保留 unicode-range 子集策略) - Artalk CSS 跟随 CommentsModal 异步加载 - 字体精简:移除未使用的 300 字重,补上用到的 600 - 删除僵尸的 fancybox manualChunks 配置 健壮性: - SSE 搜索新增 AbortController + 60s 超时,新搜索取消旧请求,组件卸载取消进行中 - settings 持久化加 300ms 防抖 + QuotaExceededError 处理 + beforeunload 强制落盘 组件拆分: - SearchHeader.vue 1330 → 1061 行,抽出 SearchErrorCard 子组件 - SettingsModal.vue 1289 → 1171 行,抽出 AdvancedApiSettings 子组件 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
531 lines
13 KiB
TypeScript
531 lines
13 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
import { ref, computed, watch } from 'vue'
|
||
|
||
// 主题模式类型
|
||
export type ThemeMode = 'system' | 'light' | 'dark'
|
||
|
||
// 持久化的 UI 状态类型
|
||
export interface PersistedUIState {
|
||
// 主题
|
||
themeMode: ThemeMode
|
||
isDarkMode: boolean
|
||
customCSS: string
|
||
|
||
// 模态框状态
|
||
isCommentsModalOpen: boolean
|
||
isVndbPanelOpen: boolean
|
||
isSettingsModalOpen: boolean
|
||
isHistoryModalOpen: boolean
|
||
isKeyboardHelpOpen: boolean
|
||
|
||
// 其他 UI 状态
|
||
showSearchHistory: boolean
|
||
showPlatformNav: boolean
|
||
|
||
// 滚动位置
|
||
scrollPosition: number
|
||
|
||
// 时间戳
|
||
lastVisitTime: number
|
||
}
|
||
|
||
const STORAGE_KEY = 'ui-state'
|
||
const SESSION_KEY = 'ui-session-state'
|
||
|
||
// 默认持久化状态
|
||
const DEFAULT_PERSISTED_STATE: PersistedUIState = {
|
||
themeMode: 'system',
|
||
isDarkMode: false,
|
||
customCSS: '',
|
||
isCommentsModalOpen: false,
|
||
isVndbPanelOpen: false,
|
||
isSettingsModalOpen: false,
|
||
isHistoryModalOpen: false,
|
||
isKeyboardHelpOpen: false,
|
||
showSearchHistory: true,
|
||
showPlatformNav: false,
|
||
scrollPosition: 0,
|
||
lastVisitTime: 0,
|
||
}
|
||
|
||
export const useUIStore = defineStore('ui', () => {
|
||
// 是否已初始化
|
||
const isInitialized = ref(false)
|
||
|
||
// 主题相关
|
||
const themeMode = ref<ThemeMode>('system')
|
||
const isDarkMode = ref(false)
|
||
const customCSS = ref('')
|
||
let systemThemeCleanup: (() => void) | null = null
|
||
|
||
// 模态框状态
|
||
const isCommentsModalOpen = ref(false)
|
||
const isVndbPanelOpen = ref(false)
|
||
const isSettingsModalOpen = ref(false)
|
||
const isHistoryModalOpen = ref(false)
|
||
const isKeyboardHelpOpen = ref(false)
|
||
|
||
// 浮动按钮状态
|
||
const showScrollToTop = ref(false)
|
||
const showPlatformNav = ref(false)
|
||
|
||
// 搜索历史显示
|
||
const showSearchHistory = ref(true)
|
||
|
||
// 背景图片
|
||
const currentBackgroundImage = ref('')
|
||
const backgroundImageLoaded = ref(false)
|
||
|
||
// 加载状态
|
||
const isLoading = ref(false)
|
||
const loadingMessage = ref('')
|
||
|
||
// 滚动位置(用于恢复)
|
||
const scrollPosition = ref(0)
|
||
|
||
// Toast 通知
|
||
const toasts = ref<{
|
||
id: string
|
||
type: 'success' | 'error' | 'info' | 'warning'
|
||
message: string
|
||
duration: number
|
||
}[]>([])
|
||
|
||
// 计算属性
|
||
const hasOpenModal = computed(() =>
|
||
isCommentsModalOpen.value ||
|
||
isVndbPanelOpen.value ||
|
||
isSettingsModalOpen.value ||
|
||
isHistoryModalOpen.value,
|
||
)
|
||
|
||
const activeModalsCount = computed(() => {
|
||
let count = 0
|
||
if (isCommentsModalOpen.value) {count++}
|
||
if (isVndbPanelOpen.value) {count++}
|
||
if (isSettingsModalOpen.value) {count++}
|
||
if (isHistoryModalOpen.value) {count++}
|
||
return count
|
||
})
|
||
|
||
// 方法 - 主题
|
||
|
||
/**
|
||
* 获取系统主题偏好
|
||
*/
|
||
function getSystemTheme(): boolean {
|
||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||
}
|
||
|
||
/**
|
||
* 应用主题到 DOM
|
||
*/
|
||
function applyTheme(dark: boolean) {
|
||
isDarkMode.value = dark
|
||
document.documentElement.classList.toggle('dark', dark)
|
||
}
|
||
|
||
/**
|
||
* 设置主题模式
|
||
*/
|
||
function setThemeMode(mode: ThemeMode) {
|
||
themeMode.value = mode
|
||
|
||
// 清理之前的系统主题监听
|
||
if (systemThemeCleanup) {
|
||
systemThemeCleanup()
|
||
systemThemeCleanup = null
|
||
}
|
||
|
||
if (mode === 'system') {
|
||
// 应用系统主题
|
||
applyTheme(getSystemTheme())
|
||
|
||
// 监听系统主题变化
|
||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||
const handler = (e: MediaQueryListEvent) => {
|
||
if (themeMode.value === 'system') {
|
||
applyTheme(e.matches)
|
||
}
|
||
}
|
||
mediaQuery.addEventListener('change', handler)
|
||
systemThemeCleanup = () => mediaQuery.removeEventListener('change', handler)
|
||
} else {
|
||
// 应用固定主题
|
||
applyTheme(mode === 'dark')
|
||
}
|
||
}
|
||
|
||
function toggleDarkMode() {
|
||
// 切换主题模式:system -> light -> dark -> system
|
||
const modes: ThemeMode[] = ['system', 'light', 'dark']
|
||
const currentIndex = modes.indexOf(themeMode.value)
|
||
const nextMode = modes[(currentIndex + 1) % modes.length]
|
||
if (nextMode) {setThemeMode(nextMode)}
|
||
}
|
||
|
||
function setDarkMode(value: boolean) {
|
||
setThemeMode(value ? 'dark' : 'light')
|
||
}
|
||
|
||
function setCustomCSS(css: string) {
|
||
customCSS.value = css
|
||
}
|
||
|
||
// 方法 - 模态框(互斥:打开一个会关闭其他)
|
||
|
||
// 关闭所有模态框(内部使用,不触发互斥逻辑)
|
||
function closeAllModals() {
|
||
isCommentsModalOpen.value = false
|
||
isVndbPanelOpen.value = false
|
||
isSettingsModalOpen.value = false
|
||
isHistoryModalOpen.value = false
|
||
isKeyboardHelpOpen.value = false
|
||
}
|
||
|
||
// 打开评论模态框(互斥)
|
||
function openCommentsModal() {
|
||
closeAllModals()
|
||
isCommentsModalOpen.value = true
|
||
}
|
||
|
||
function toggleCommentsModal() {
|
||
if (isCommentsModalOpen.value) {
|
||
isCommentsModalOpen.value = false
|
||
} else {
|
||
openCommentsModal()
|
||
}
|
||
}
|
||
|
||
// 打开 VNDB 面板(互斥)
|
||
function openVndbPanel() {
|
||
closeAllModals()
|
||
isVndbPanelOpen.value = true
|
||
}
|
||
|
||
function toggleVndbPanel() {
|
||
if (isVndbPanelOpen.value) {
|
||
isVndbPanelOpen.value = false
|
||
} else {
|
||
openVndbPanel()
|
||
}
|
||
}
|
||
|
||
// 打开设置模态框(互斥)
|
||
function openSettingsModal() {
|
||
closeAllModals()
|
||
isSettingsModalOpen.value = true
|
||
}
|
||
|
||
function toggleSettingsModal() {
|
||
if (isSettingsModalOpen.value) {
|
||
isSettingsModalOpen.value = false
|
||
} else {
|
||
openSettingsModal()
|
||
}
|
||
}
|
||
|
||
// 打开历史记录模态框(互斥)
|
||
function openHistoryModal() {
|
||
closeAllModals()
|
||
isHistoryModalOpen.value = true
|
||
}
|
||
|
||
function toggleHistoryModal() {
|
||
if (isHistoryModalOpen.value) {
|
||
isHistoryModalOpen.value = false
|
||
} else {
|
||
openHistoryModal()
|
||
}
|
||
}
|
||
|
||
// 打开键盘帮助面板(互斥)
|
||
function openKeyboardHelp() {
|
||
closeAllModals()
|
||
isKeyboardHelpOpen.value = true
|
||
}
|
||
|
||
function toggleKeyboardHelp() {
|
||
if (isKeyboardHelpOpen.value) {
|
||
isKeyboardHelpOpen.value = false
|
||
} else {
|
||
openKeyboardHelp()
|
||
}
|
||
}
|
||
|
||
// 方法 - 浮动按钮
|
||
function setShowScrollToTop(show: boolean) {
|
||
showScrollToTop.value = show
|
||
}
|
||
|
||
function togglePlatformNav() {
|
||
showPlatformNav.value = !showPlatformNav.value
|
||
}
|
||
|
||
function closePlatformNav() {
|
||
showPlatformNav.value = false
|
||
}
|
||
|
||
// 方法 - 搜索历史
|
||
function toggleSearchHistory() {
|
||
showSearchHistory.value = !showSearchHistory.value
|
||
}
|
||
|
||
// 方法 - 背景图片
|
||
function setBackgroundImage(url: string) {
|
||
currentBackgroundImage.value = url
|
||
}
|
||
|
||
function setBackgroundImageLoaded(loaded: boolean) {
|
||
backgroundImageLoaded.value = loaded
|
||
}
|
||
|
||
// 方法 - 加载状态
|
||
function setLoading(loading: boolean, message = '') {
|
||
isLoading.value = loading
|
||
loadingMessage.value = message
|
||
}
|
||
|
||
// 方法 - Toast 通知
|
||
function showToast(
|
||
type: 'success' | 'error' | 'info' | 'warning',
|
||
message: string,
|
||
duration = 3000,
|
||
) {
|
||
const id = `toast-${Date.now()}-${Math.random()}`
|
||
toasts.value.push({ id, type, message, duration })
|
||
|
||
// 自动移除
|
||
setTimeout(() => {
|
||
removeToast(id)
|
||
}, duration)
|
||
|
||
return id
|
||
}
|
||
|
||
function removeToast(id: string) {
|
||
const index = toasts.value.findIndex(t => t.id === id)
|
||
if (index !== -1) {
|
||
toasts.value.splice(index, 1)
|
||
}
|
||
}
|
||
|
||
function clearToasts() {
|
||
toasts.value = []
|
||
}
|
||
|
||
// 从 localStorage 加载持久化状态(长期偏好)
|
||
function loadPersistedState() {
|
||
try {
|
||
const saved = localStorage.getItem(STORAGE_KEY)
|
||
if (saved) {
|
||
const parsed: Partial<PersistedUIState> = JSON.parse(saved)
|
||
|
||
// 迁移旧版格式:如果没有 themeMode 但有 isDarkMode,转换为新格式
|
||
let savedThemeMode = parsed.themeMode
|
||
if (!savedThemeMode && typeof parsed.isDarkMode === 'boolean') {
|
||
savedThemeMode = parsed.isDarkMode ? 'dark' : 'light'
|
||
// 立即保存迁移后的格式,避免下次再迁移
|
||
try {
|
||
const migrated = { ...parsed, themeMode: savedThemeMode }
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(migrated))
|
||
} catch {
|
||
// 保存失败,忽略
|
||
}
|
||
}
|
||
|
||
themeMode.value = savedThemeMode ?? DEFAULT_PERSISTED_STATE.themeMode
|
||
customCSS.value = parsed.customCSS ?? DEFAULT_PERSISTED_STATE.customCSS
|
||
showSearchHistory.value = parsed.showSearchHistory ?? DEFAULT_PERSISTED_STATE.showSearchHistory
|
||
}
|
||
} catch {
|
||
// 解析失败,使用默认值
|
||
}
|
||
}
|
||
|
||
// 从 sessionStorage 加载会话状态(刷新恢复)
|
||
function loadSessionState() {
|
||
try {
|
||
const saved = sessionStorage.getItem(SESSION_KEY)
|
||
if (saved) {
|
||
const parsed: Partial<PersistedUIState> = JSON.parse(saved)
|
||
|
||
// 恢复模态框状态
|
||
isCommentsModalOpen.value = parsed.isCommentsModalOpen ?? false
|
||
isVndbPanelOpen.value = parsed.isVndbPanelOpen ?? false
|
||
isSettingsModalOpen.value = parsed.isSettingsModalOpen ?? false
|
||
isHistoryModalOpen.value = parsed.isHistoryModalOpen ?? false
|
||
isKeyboardHelpOpen.value = parsed.isKeyboardHelpOpen ?? false
|
||
|
||
// 恢复其他状态
|
||
showPlatformNav.value = parsed.showPlatformNav ?? false
|
||
scrollPosition.value = parsed.scrollPosition ?? 0
|
||
}
|
||
} catch {
|
||
// 解析失败,使用默认值
|
||
}
|
||
}
|
||
|
||
// 保存持久化状态到 localStorage(长期偏好)
|
||
function savePersistedState() {
|
||
try {
|
||
const state: Partial<PersistedUIState> = {
|
||
themeMode: themeMode.value,
|
||
customCSS: customCSS.value,
|
||
showSearchHistory: showSearchHistory.value,
|
||
lastVisitTime: Date.now(),
|
||
}
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
||
} catch {
|
||
// 保存失败,静默处理
|
||
}
|
||
}
|
||
|
||
// 保存会话状态到 sessionStorage(刷新恢复)
|
||
function saveSessionState() {
|
||
try {
|
||
const state: Partial<PersistedUIState> = {
|
||
isCommentsModalOpen: isCommentsModalOpen.value,
|
||
isVndbPanelOpen: isVndbPanelOpen.value,
|
||
isSettingsModalOpen: isSettingsModalOpen.value,
|
||
isHistoryModalOpen: isHistoryModalOpen.value,
|
||
isKeyboardHelpOpen: isKeyboardHelpOpen.value,
|
||
showPlatformNav: showPlatformNav.value,
|
||
scrollPosition: window.scrollY,
|
||
}
|
||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(state))
|
||
} catch {
|
||
// 保存失败,静默处理
|
||
}
|
||
}
|
||
|
||
// 监听需要持久化的状态变化(localStorage - 长期偏好)
|
||
watch(
|
||
[themeMode, customCSS, showSearchHistory],
|
||
() => {
|
||
if (isInitialized.value) {
|
||
savePersistedState()
|
||
}
|
||
},
|
||
)
|
||
|
||
// 监听需要保存到会话的状态变化(sessionStorage - 刷新恢复)
|
||
watch(
|
||
[
|
||
isCommentsModalOpen,
|
||
isVndbPanelOpen,
|
||
isSettingsModalOpen,
|
||
isHistoryModalOpen,
|
||
isKeyboardHelpOpen,
|
||
showPlatformNav,
|
||
],
|
||
() => {
|
||
if (isInitialized.value) {
|
||
saveSessionState()
|
||
}
|
||
},
|
||
)
|
||
|
||
// 初始化
|
||
function init() {
|
||
// 加载持久化状态(长期偏好)
|
||
loadPersistedState()
|
||
|
||
// 加载会话状态(刷新恢复)
|
||
loadSessionState()
|
||
|
||
// 应用主题模式
|
||
setThemeMode(themeMode.value)
|
||
|
||
isInitialized.value = true
|
||
|
||
// 恢复滚动位置
|
||
if (scrollPosition.value > 0) {
|
||
requestAnimationFrame(() => {
|
||
window.scrollTo(0, scrollPosition.value)
|
||
})
|
||
}
|
||
|
||
// 监听页面卸载,保存滚动位置
|
||
window.addEventListener('beforeunload', saveSessionState)
|
||
|
||
// 定期保存滚动位置(防止意外关闭)
|
||
let scrollSaveTimer: number | null = null
|
||
window.addEventListener('scroll', () => {
|
||
if (scrollSaveTimer) {
|
||
clearTimeout(scrollSaveTimer)
|
||
}
|
||
scrollSaveTimer = window.setTimeout(() => {
|
||
if (isInitialized.value) {
|
||
saveSessionState()
|
||
}
|
||
}, 500)
|
||
}, { passive: true })
|
||
}
|
||
|
||
// 清除会话状态(用于完全重置)
|
||
function clearSessionState() {
|
||
sessionStorage.removeItem(SESSION_KEY)
|
||
}
|
||
|
||
return {
|
||
// 状态
|
||
isInitialized,
|
||
themeMode,
|
||
isDarkMode,
|
||
customCSS,
|
||
isCommentsModalOpen,
|
||
isVndbPanelOpen,
|
||
isSettingsModalOpen,
|
||
isHistoryModalOpen,
|
||
showScrollToTop,
|
||
showPlatformNav,
|
||
showSearchHistory,
|
||
currentBackgroundImage,
|
||
backgroundImageLoaded,
|
||
isLoading,
|
||
loadingMessage,
|
||
toasts,
|
||
isKeyboardHelpOpen,
|
||
scrollPosition,
|
||
|
||
// 计算属性
|
||
hasOpenModal,
|
||
activeModalsCount,
|
||
|
||
// 方法
|
||
setThemeMode,
|
||
toggleDarkMode,
|
||
setDarkMode,
|
||
setCustomCSS,
|
||
// 模态框方法
|
||
openCommentsModal,
|
||
toggleCommentsModal,
|
||
openVndbPanel,
|
||
toggleVndbPanel,
|
||
openSettingsModal,
|
||
toggleSettingsModal,
|
||
openHistoryModal,
|
||
toggleHistoryModal,
|
||
openKeyboardHelp,
|
||
toggleKeyboardHelp,
|
||
closeAllModals,
|
||
setShowScrollToTop,
|
||
togglePlatformNav,
|
||
closePlatformNav,
|
||
toggleSearchHistory,
|
||
setBackgroundImage,
|
||
setBackgroundImageLoaded,
|
||
setLoading,
|
||
showToast,
|
||
removeToast,
|
||
clearToasts,
|
||
loadPersistedState,
|
||
savePersistedState,
|
||
loadSessionState,
|
||
saveSessionState,
|
||
clearSessionState,
|
||
init,
|
||
}
|
||
})
|