Files
SearcjGal-frontend/src/stores/ui.ts
AdingApkgg 3d414157a0 依赖升级 + 代码质量、打包体积与组件结构优化
依赖:
- 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>
2026-05-14 07:23:19 +08:00

531 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
})