Remove unused configuration files and improve TypeScript settings

- Deleted eslint.config.mjs and tailwind.config.ts as they are no longer needed.
- Updated tsconfig.json to include additional TypeScript files for better project structure.
- Refactored various components to enhance modal management and improve code clarity.
- Changed console.log statements to console.info for consistent logging practices across the application.
This commit is contained in:
AdingApkgg
2025-12-28 08:07:10 +08:00
parent 428f0eb015
commit 33811051b7
29 changed files with 726 additions and 525 deletions

View File

@@ -11,7 +11,6 @@ export default tseslint.config(
'**/.pnpm-store/**',
'**/public/**',
'**/*.config.js',
'**/*.config.mjs',
'**/*.config.ts',
'**/.history/**', // 忽略编辑器历史文件
'**/.vscode/**',

View File

@@ -81,13 +81,13 @@ export function swVersionPlugin(options: SwVersionPluginOptions = {}): Plugin {
version = generateVersion(includeGitHash, prefix)
const buildInfo = getBuildInfo()
console.log('\n📦 SW Version Plugin')
console.log(` Version: ${version}`)
console.log(` Build Time: ${buildInfo.buildTime}`)
console.info('\n📦 SW Version Plugin')
console.info(` Version: ${version}`)
console.info(` Build Time: ${buildInfo.buildTime}`)
if (buildInfo.gitCommit) {
console.log(` Git: ${buildInfo.gitBranch}@${buildInfo.gitCommit}`)
console.info(` Git: ${buildInfo.gitBranch}@${buildInfo.gitCommit}`)
}
console.log('')
console.info('')
},
// 构建完成后注入版本到 sw.js
@@ -116,7 +116,7 @@ export function swVersionPlugin(options: SwVersionPluginOptions = {}): Plugin {
writeFileSync(swFilePath, content)
console.log(`✅ SW version injected: ${version}`)
console.info(`✅ SW version injected: ${version}`)
},
}
}

View File

@@ -105,9 +105,9 @@ const uiStore = useUIStore()
const settingsStore = useSettingsStore()
const searchHeaderRef = ref<InstanceType<typeof SearchHeader> | null>(null)
// 切换设置面板
// 切换设置面板(互斥)
function openSettings() {
uiStore.isSettingsModalOpen = !uiStore.isSettingsModalOpen
uiStore.toggleSettingsModal()
}
// 处理历史记录选择
@@ -342,6 +342,7 @@ function cleanupBlobUrls(currentUrl: string) {
// 创建新 Map保留当前使用的 URL
const newBlobUrls = new Map<string, string>()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newBlobUrls.set(currentUrl, blobUrls.get(currentUrl)!)
// 保留最近添加的 URLMap 保持插入顺序)
@@ -408,12 +409,12 @@ async function displayNextImage() {
}
preloadImg.onerror = () => {
// 加载失败,尝试下一张
displayNextImage()
void displayNextImage()
}
preloadImg.src = blobUrl || nextImageUrl
} catch (error) {
} catch {
// 加载失败,尝试下一张
displayNextImage()
void displayNextImage()
}
}
@@ -424,10 +425,10 @@ function startFetchInterval() {
}
// 立即获取第一张
fetchAndCacheImage()
void fetchAndCacheImage()
fetchInterval = window.setInterval(() => {
fetchAndCacheImage()
void fetchAndCacheImage()
}, FETCH_INTERVAL)
}
@@ -438,10 +439,10 @@ function startDisplayInterval() {
}
// 立即显示第一张
displayNextImage()
void displayNextImage()
displayInterval = window.setInterval(() => {
displayNextImage()
void displayNextImage()
}, DISPLAY_INTERVAL)
}
@@ -466,8 +467,8 @@ onMounted(async () => {
// URL hash 优先级最高 - 覆盖会话状态
const hash = window.location.hash
if (hash.startsWith('#atk-comment-')) {
// 评论链接:打开评论面板
uiStore.isCommentsModalOpen = true
// 评论链接:打开评论面板(互斥)
uiStore.openCommentsModal()
}
// 恢复保存的搜索状态

View File

@@ -455,7 +455,7 @@ export async function fetchVndbData(gameName: string): Promise<VndbInfo | null>
// 获取封面图片 - 优先选择安全级别的图片
let mainImageUrl: string | null = null
if (result.image && result.image.url) {
if (result.image?.url) {
// 只使用 sexual <= 1 且 violence === 0 的图片
if ((result.image.sexual === 0 || result.image.sexual === 1) && result.image.violence === 0) {
mainImageUrl = result.image.url
@@ -650,7 +650,7 @@ export async function fetchVndbCharacters(vnId: string): Promise<VndbCharacter[]
age?: number
}) => {
let imageUrl: string | undefined
if (c.image && c.image.url && c.image.sexual <= 1 && c.image.violence === 0) {
if (c.image?.url && c.image.sexual <= 1 && c.image.violence === 0) {
imageUrl = c.image.url
}
@@ -669,7 +669,7 @@ export async function fetchVndbCharacters(vnId: string): Promise<VndbCharacter[]
// 使用代理替换 URL
if (ENABLE_VNDB_IMAGE_PROXY && isProxyAvailable) {
characters.forEach((char) => {
if (char.image && char.image.startsWith('https://t.vndb.org/')) {
if (char.image?.startsWith('https://t.vndb.org/')) {
char.image = proxyUrl(char.image)
}
})
@@ -735,11 +735,10 @@ export type TranslateMode = 'description' | 'tags' | 'quotes'
const DESCRIPTION_PROMPT = `你是一名专业的视觉小说Galgame/AVG本地化专家。请将游戏简介精准翻译为简体中文。
【翻译规范】
1. 格式净化:清除所有 HTML、Markdown、BBCode 等标记,仅保留纯文本
2. 结构保留:保持原文段落划分,段落间用换行分隔
3. 术语处理:使用视觉小说领域通用的中文术语
4. 人名处理:优先使用中文圈广泛接受的译名,无通用译名时保留原名
5. 内容控制:禁止添加剧透、解释性文字或主观评价
1. 结构保留:保持原文段落划分,段落间用换行分隔
2. 术语处理:使用视觉小说领域通用的中文术语
3. 人名处理:优先使用中文圈广泛接受的译名,无通用译名时保留原名
4. 内容控制:禁止添加剧透、解释性文字或主观评价
【输出要求】
仅输出翻译后的纯文本,无需任何说明`
@@ -811,7 +810,7 @@ const COMBINED_PROMPT = `你是一名专业的视觉小说Galgame/AVG
export async function translateText(
text: string,
mode: TranslateMode = 'description',
maxRetries: number = 2,
maxRetries = 2,
): Promise<string | null> {
if (!text || text.trim().length === 0) {
return null
@@ -913,7 +912,7 @@ export async function translateAllContent(
description: string | null,
tags: string[] | null,
quotes: string[] | null,
maxRetries: number = 2,
maxRetries = 2,
): Promise<TranslateAllResult> {
const result: TranslateAllResult = {
description: null,
@@ -1032,12 +1031,12 @@ function replaceVndbUrls(vndbInfo: VndbInfo) {
}
// 替换封面图片 URL
if (vndbInfo.mainImageUrl && vndbInfo.mainImageUrl.startsWith('https://t.vndb.org/')) {
if (vndbInfo.mainImageUrl?.startsWith('https://t.vndb.org/')) {
vndbInfo.mainImageUrl = proxyUrl(vndbInfo.mainImageUrl)
}
// 替换主截图 URL
if (vndbInfo.screenshotUrl && vndbInfo.screenshotUrl.startsWith('https://t.vndb.org/')) {
if (vndbInfo.screenshotUrl?.startsWith('https://t.vndb.org/')) {
vndbInfo.screenshotUrl = proxyUrl(vndbInfo.screenshotUrl)
}

View File

@@ -10,7 +10,7 @@
leave-to-class="opacity-0 scale-[0.98] translate-y-10"
>
<div
v-if="uiStore.isCommentsModalOpen"
v-show="uiStore.isCommentsModalOpen"
class="comments-modal fixed z-[100] flex flex-col shadow-2xl shadow-black/20 inset-0 md:inset-6 md:m-auto md:w-[900px] md:max-w-[calc(100%-3rem)] md:h-[760px] md:max-h-[calc(100%-3rem)] md:rounded-3xl"
>
<!-- 顶部导航栏 -->
@@ -140,7 +140,7 @@ function initArtalk() {
}
}
nextTick(() => {
void nextTick(() => {
const commentsEl = document.getElementById('Comments')
if (commentsEl) {
try {

View File

@@ -195,15 +195,15 @@ function scrollToTop() {
}
function toggleComments() {
uiStore.isCommentsModalOpen = !uiStore.isCommentsModalOpen
uiStore.toggleCommentsModal()
}
function toggleVndbPanel() {
uiStore.isVndbPanelOpen = !uiStore.isVndbPanelOpen
uiStore.toggleVndbPanel()
}
function toggleHistory() {
uiStore.isHistoryModalOpen = !uiStore.isHistoryModalOpen
uiStore.toggleHistoryModal()
}
function togglePlatformNav(withSound = false) {
@@ -257,15 +257,19 @@ function handleScrollToPlatform(platformName: string) {
}
function scrollToPlatform(platformName: string) {
const platformElements = document.querySelectorAll('[data-platform]')
const targetElement = Array.from(platformElements).find(
el => el.getAttribute('data-platform') === platformName,
) as HTMLElement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const targetElement = document.querySelector(`[data-platform="${platformName}"]`)!
if (targetElement) {
const yOffset = -80
const y = targetElement.getBoundingClientRect().top + window.pageYOffset + yOffset
window.scrollTo({ top: y, behavior: 'smooth' })
// 先瞬间滚动到目标位置附近,触发途中的 LazyRender 渲染
targetElement.scrollIntoView({ behavior: 'instant', block: 'start' })
// 等待渲染完成后,再平滑滚动到精确位置
requestAnimationFrame(() => {
setTimeout(() => {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
}, 50)
})
// 滚动后关闭导航
playTransitionDown()

View File

@@ -1,5 +1,12 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted, useAttrs } from 'vue'
// 禁用自动继承属性,手动绑定到容器
defineOptions({
inheritAttrs: false,
})
const attrs = useAttrs()
const props = defineProps<{
/** 距离可视区多少像素时开始渲染 */
@@ -51,7 +58,11 @@ onUnmounted(() => {
<template>
<div
ref="containerRef"
:style="{ minHeight: !isVisible ? (minHeight || '60px') : undefined }"
v-bind="attrs"
:style="{
minHeight: !isVisible ? (minHeight || '60px') : undefined,
scrollMarginTop: '80px'
}"
>
<slot v-if="isVisible" />
<!-- 占位符 -->

View File

@@ -465,7 +465,7 @@ import { useStatsStore } from '@/stores/stats'
import { useCacheStore } from '@/stores/cache'
import { useHistoryStore } from '@/stores/history'
import { searchGameStream, fetchVndbData } from '@/api/search'
import { playSwipe, playToggle, playCelebration, playCaution, playType } from '@/composables/useSound'
import { playSwipe, playSelect, playCelebration, playCaution, playType } from '@/composables/useSound'
import { useDebouncedClick } from '@/composables/useDebounce'
import {
Search,
@@ -733,7 +733,7 @@ function handleTyping() {
// 搜索模式切换(带音效)
function setSearchMode(mode: 'game' | 'patch') {
if (searchMode.value !== mode) {
playToggle()
playSelect()
searchMode.value = mode
}
}
@@ -844,7 +844,7 @@ function getErrorCodeInfo(error: string): ErrorCodeInfo {
const errorLower = error.toLowerCase()
// 尝试提取 HTTP 状态码
const statusMatch = error.match(/\b(4\d{2}|5\d{2})\b/)
const statusMatch = /\b(4\d{2}|5\d{2})\b/.exec(error)
if (statusMatch) {
const status = parseInt(statusMatch[1])
const statusDescriptions: Record<number, string> = {

View File

@@ -2,6 +2,7 @@
<div v-if="searchStore.hasResults" class="w-full sm:px-4 md:px-6 py-4 sm:py-6 md:py-8 animate-fade-in">
<div id="results" class="sm:max-w-5xl sm:mx-auto space-y-4 sm:space-y-6">
<!-- 使用 v-memo 优化平台卡片渲染 + LazyRender 懒渲染 -->
<!-- data-platform 放在 LazyRender 容器上确保即使内容未渲染也能被站点导航找到 -->
<LazyRender
v-for="[platformName, platformData] in searchStore.platformResults"
:key="platformName"
@@ -9,9 +10,9 @@
:once="true"
min-height="200px"
root-margin="400px 0px"
:data-platform="platformName"
>
<div
:data-platform="platformName"
class="result-card rounded-none sm:rounded-2xl animate-fade-in-up border-2 content-auto"
:class="getBorderClass(platformData.color)"
>
@@ -234,8 +235,8 @@ function getCountBadgeClass(color: string) {
}
// 获取平台图标组件
function getPlatformIconComponent(color: string): typeof Star | typeof Circle | typeof DollarSign | typeof XCircle {
const icons: Record<string, typeof Star | typeof Circle | typeof DollarSign | typeof XCircle> = {
function getPlatformIconComponent(color: string): typeof Star {
const icons: Record<string, typeof Star > = {
lime: Star,
white: Circle,
gold: DollarSign,
@@ -245,8 +246,8 @@ function getPlatformIconComponent(color: string): typeof Star | typeof Circle |
}
// 获取标签图标组件
function getTagIconComponent(tag: string): typeof CheckCircle | typeof User | typeof Coins | typeof MessageCircle | typeof Reply | typeof Server | typeof Rocket | typeof Turtle | typeof Layers | typeof Magnet | typeof Wand2 | typeof TagIcon {
const icons: Record<string, typeof CheckCircle | typeof User | typeof Coins | typeof MessageCircle | typeof Reply | typeof Server | typeof Rocket | typeof Turtle | typeof Layers | typeof Magnet | typeof Wand2 | typeof TagIcon> = {
function getTagIconComponent(tag: string): typeof CheckCircle {
const icons: Record<string, typeof CheckCircle > = {
'NoReq': CheckCircle,
'Login': User,
'LoginPay': Coins,

View File

@@ -10,7 +10,7 @@
leave-to-class="opacity-0 scale-[0.98] translate-y-10"
>
<div
v-if="isOpen"
v-show="isOpen"
class="fixed z-[100] flex flex-col settings-page shadow-2xl shadow-black/20 inset-0 md:inset-6 md:m-auto md:w-[800px] md:max-w-[calc(100%-3rem)] md:h-[700px] md:max-h-[calc(100%-3rem)] md:rounded-3xl"
>
<!-- 顶部导航栏 -->
@@ -460,7 +460,7 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { playTap, playCelebration, playToggle, playType } from '@/composables/useSound'
import { playTap, playCelebration, playSelect, playType } from '@/composables/useSound'
// Prism Editor
import { PrismEditor } from 'vue-prism-editor'
@@ -541,7 +541,7 @@ const themeOptions = [
]
function handleThemeChange(mode: ThemeMode) {
playToggle()
playSelect()
uiStore.setThemeMode(mode)
}
@@ -630,7 +630,7 @@ const localCustomApi = computed(() => {
// 选择 API 选项
function selectApiOption(option: string) {
playToggle()
playSelect()
selectedApiOption.value = option
if (option !== 'custom') {
customApiInput.value = ''

View File

@@ -108,6 +108,7 @@ async function shareSearch() {
textarea.select()
try {
// eslint-disable-next-line @typescript-eslint/no-deprecated
document.execCommand('copy')
showCopiedTip.value = true
@@ -125,7 +126,7 @@ async function shareSearch() {
// 切换键盘快捷键帮助
function toggleKeyboardHelp() {
playTap()
uiStore.isKeyboardHelpOpen = !uiStore.isKeyboardHelpOpen
uiStore.toggleKeyboardHelp()
}
// 打开设置
@@ -153,7 +154,7 @@ async function saveBackgroundImage() {
const parts = pathname.split('/')
const lastPart = parts[parts.length - 1]
if (lastPart && lastPart.includes('.')) {
if (lastPart?.includes('.')) {
// 有文件名和扩展名
const nameParts = lastPart.split('.')
extension = nameParts.pop() || 'jpg'

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,8 @@ export function useKeyboardShortcuts() {
// 聚焦搜索框
function focusSearch() {
const searchInput = document.querySelector('input[type="search"], input[placeholder*="搜索"]') as HTMLInputElement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const searchInput = document.querySelector('input[type="search"], input[placeholder*="搜索"]')!
if (searchInput) {
searchInput.focus()
searchInput.select()
@@ -74,7 +75,7 @@ export function useKeyboardShortcuts() {
// 获取平台列表
function getPlatformElements(): HTMLElement[] {
return Array.from(document.querySelectorAll('[data-platform]')) as HTMLElement[]
return Array.from(document.querySelectorAll('[data-platform]'))
}
// 跳转到指定平台
@@ -122,7 +123,8 @@ export function useKeyboardShortcuts() {
// 切换站点导航
function toggleNav() {
// 模拟点击站点导航按钮
const navBtn = document.querySelector('.nav-btn') as HTMLButtonElement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const navBtn = document.querySelector('.nav-btn')!
if (navBtn) {
navBtn.click()
}
@@ -142,20 +144,20 @@ export function useKeyboardShortcuts() {
uiStore.closeAllModals()
}
// 切换面板
// 切换面板(使用 store 中的互斥方法)
function togglePanel(panel: 'settings' | 'comments' | 'vndb' | 'history') {
switch (panel) {
case 'settings':
uiStore.isSettingsModalOpen = !uiStore.isSettingsModalOpen
uiStore.toggleSettingsModal()
break
case 'comments':
uiStore.isCommentsModalOpen = !uiStore.isCommentsModalOpen
uiStore.toggleCommentsModal()
break
case 'vndb':
uiStore.isVndbPanelOpen = !uiStore.isVndbPanelOpen
uiStore.toggleVndbPanel()
break
case 'history':
uiStore.isHistoryModalOpen = !uiStore.isHistoryModalOpen
uiStore.toggleHistoryModal()
break
}
}
@@ -269,7 +271,7 @@ export function useKeyboardShortcuts() {
// 显示/隐藏快捷键帮助
event.preventDefault()
playButton()
uiStore.isKeyboardHelpOpen = !uiStore.isKeyboardHelpOpen
uiStore.toggleKeyboardHelp()
break
}
}

View File

@@ -122,27 +122,29 @@ const soundMap: Record<SoundType, keyof typeof Snd.SOUNDS> = {
ringtone_loop: 'RINGTONE_LOOP',
}
// 播放音效
async function playSound(type: SoundType): Promise<void> {
// 播放音效fire-and-forget不返回 Promise
function playSound(type: SoundType): void {
if (!soundEnabled.value) {
return
}
try {
const snd = await getSnd()
if (!snd) {
return
void (async () => {
try {
const snd = await getSnd()
if (!snd) {
return
}
const soundKey = soundMap[type]
const sound = Snd.SOUNDS[soundKey]
if (sound) {
snd.play(sound)
}
} catch {
// 静默处理错误
}
const soundKey = soundMap[type]
const sound = Snd.SOUNDS[soundKey]
if (sound) {
snd.play(sound)
}
} catch {
// 静默处理错误
}
})()
}
// ========================================
@@ -192,10 +194,10 @@ const legacyMap: Record<LegacySoundType, SoundType> = {
close: 'transition_down',
}
// 兼容旧 API 的播放方法
async function playLegacySound(type: LegacySoundType): Promise<void> {
// 兼容旧 API 的播放方法fire-and-forget
function playLegacySound(type: LegacySoundType): void {
const mappedType = legacyMap[type]
return playSound(mappedType)
playSound(mappedType)
}
// ========================================

View File

@@ -82,7 +82,8 @@ export const vTextScroll = {
// 检查溢出
const checkOverflow = () => {
const inner = el.querySelector('.text-scroll-inner') as HTMLElement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const inner = el.querySelector('.text-scroll-inner')!
if (!inner) { return }
const isOver = inner.scrollWidth > el.clientWidth
@@ -126,7 +127,8 @@ export const vTextScroll = {
// 检查内容是否变化
requestAnimationFrame(() => {
// 获取当前实际文本内容(排除克隆的内容)
const inner = el.querySelector('.text-scroll-inner') as HTMLElement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const inner = el.querySelector('.text-scroll-inner')!
let currentContent = ''
if (inner) {

View File

@@ -100,6 +100,6 @@ export type AppConfig = typeof CONFIG
// 开发环境下打印配置(方便调试)
if (DEV_CONFIG.isDev && FEATURE_FLAGS.enableDevLogs) {
console.log('📦 Application Config:', CONFIG)
console.info('📦 Application Config:', CONFIG)
}

View File

@@ -194,7 +194,7 @@ export const mode = import.meta.env.MODE
*/
export function devLog(...args: unknown[]) {
if (config.dev.debug && isDev) {
console.log('[Dev]', ...args)
console.info('[Dev]', ...args)
}
}
@@ -218,6 +218,6 @@ export function devError(...args: unknown[]) {
// 开发环境打印配置信息
if (isDev && config.dev.debug) {
console.log('🔧 Application Config:', config)
console.log('🌍 Environment:', mode)
console.info('🔧 Application Config:', config)
console.info('🌍 Environment:', mode)
}

View File

@@ -158,10 +158,11 @@ function showUpdateToast(onUpdate: () => void) {
}
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js')
console.log('[SW] Registered')
window.addEventListener('load', () => {
void (async () => {
try {
const reg = await navigator.serviceWorker.register('/sw.js')
console.info('[SW] Registered')
// 新版本检测
reg.addEventListener('updatefound', () => {
@@ -173,10 +174,10 @@ if ('serviceWorker' in navigator) {
worker.addEventListener('statechange', () => {
// 新 SW 安装完成且有旧 SW 控制页面 = 有更新
if (worker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('[SW] Update available')
console.info('[SW] Update available')
// 显示更新提示5 秒后自动更新
showUpdateToast(() => {
console.log('[SW] Activating update...')
console.info('[SW] Activating update...')
worker.postMessage({ type: 'SKIP_WAITING' })
})
}
@@ -190,21 +191,22 @@ if ('serviceWorker' in navigator) {
return
}
refreshing = true
console.log('[SW] New version activated, reloading...')
console.info('[SW] New version activated, reloading...')
window.location.reload()
})
// 定期检查更新5 分钟)
setInterval(() => reg.update().catch(() => {}), 5 * 60 * 1000)
setInterval(() => { void reg.update().catch(() => { /* 静默处理 */ }) }, 5 * 60 * 1000)
// 页面可见时检查更新
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
reg.update().catch(() => {})
void reg.update().catch(() => { /* 静默处理 */ })
}
})
} catch {
// 静默处理
}
})()
})
}

View File

@@ -160,11 +160,11 @@ export function piniaLogger(context: PiniaPluginContext) {
store.$onAction(({ name, args, after, onError }) => {
const startTime = Date.now()
console.log(`🚀 [${store.$id}] Action "${name}" called with:`, args)
console.info(`🚀 [${store.$id}] Action "${name}" called with:`, args)
after((result) => {
const duration = Date.now() - startTime
console.log(`✅ [${store.$id}] Action "${name}" completed in ${duration}ms`, result)
console.info(`✅ [${store.$id}] Action "${name}" completed in ${duration}ms`, result)
})
onError((error) => {
@@ -175,7 +175,7 @@ export function piniaLogger(context: PiniaPluginContext) {
// 监听状态变化
store.$subscribe((mutation, state) => {
console.log(`📝 [${store.$id}] State changed:`, {
console.info(`📝 [${store.$id}] State changed:`, {
type: mutation.type,
storeId: mutation.storeId,
events: mutation.events,
@@ -210,6 +210,7 @@ export function piniaPerformance(context: PiniaPluginContext) {
if (!stats.actionDurations.has(name)) {
stats.actionDurations.set(name, [])
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
stats.actionDurations.get(name)!.push(duration)
})
})

View File

@@ -220,7 +220,7 @@ export const useSearchStore = defineStore('search', () => {
clearResults()
}
function endSearch(success: boolean = true) {
function endSearch(success = true) {
isSearching.value = false
lastSearchTime.value = Date.now()

View File

@@ -146,7 +146,7 @@ export const useStatsStore = defineStore('stats', () => {
// 检查单个服务状态
async function checkServiceStatus(serviceKey: string) {
const service = serviceStatuses.value.get(serviceKey)
if (!service || !service.url) {return}
if (!service?.url) {return}
// 设置为检测中
serviceStatuses.value.set(serviceKey, {
@@ -196,14 +196,14 @@ export const useStatsStore = defineStore('stats', () => {
// 开始定期检测
function startStatusCheck(intervalMs = 30000) {
// 立即检测一次
checkAllServices()
void checkAllServices()
// 设置定期检测
if (statusCheckInterval) {
clearInterval(statusCheckInterval)
}
statusCheckInterval = window.setInterval(() => {
checkAllServices()
void checkAllServices()
}, intervalMs)
}

View File

@@ -87,12 +87,12 @@ export const useUIStore = defineStore('ui', () => {
const scrollPosition = ref(0)
// Toast 通知
const toasts = ref<Array<{
const toasts = ref<{
id: string
type: 'success' | 'error' | 'info' | 'warning'
message: string
duration: number
}>>([])
}[]>([])
// 计算属性
const hasOpenModal = computed(() =>
@@ -175,19 +175,9 @@ export const useUIStore = defineStore('ui', () => {
customCSS.value = css
}
// 方法 - 模态框
function toggleCommentsModal() {
isCommentsModalOpen.value = !isCommentsModalOpen.value
}
function toggleVndbPanel() {
isVndbPanelOpen.value = !isVndbPanelOpen.value
}
function toggleSettingsModal() {
isSettingsModalOpen.value = !isSettingsModalOpen.value
}
// 方法 - 模态框(互斥:打开一个会关闭其他)
// 关闭所有模态框(内部使用,不触发互斥逻辑)
function closeAllModals() {
isCommentsModalOpen.value = false
isVndbPanelOpen.value = false
@@ -196,8 +186,74 @@ export const useUIStore = defineStore('ui', () => {
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() {
isHistoryModalOpen.value = !isHistoryModalOpen.value
if (isHistoryModalOpen.value) {
isHistoryModalOpen.value = false
} else {
openHistoryModal()
}
}
// 打开键盘帮助面板(互斥)
function openKeyboardHelp() {
closeAllModals()
isKeyboardHelpOpen.value = true
}
function toggleKeyboardHelp() {
if (isKeyboardHelpOpen.value) {
isKeyboardHelpOpen.value = false
} else {
openKeyboardHelp()
}
}
// 方法 - 浮动按钮
@@ -451,10 +507,17 @@ export const useUIStore = defineStore('ui', () => {
toggleDarkMode,
setDarkMode,
setCustomCSS,
// 模态框方法
openCommentsModal,
toggleCommentsModal,
openVndbPanel,
toggleVndbPanel,
openSettingsModal,
toggleSettingsModal,
openHistoryModal,
toggleHistoryModal,
openKeyboardHelp,
toggleKeyboardHelp,
closeAllModals,
setShowScrollToTop,
togglePlatformNav,

View File

@@ -71,6 +71,7 @@ export {
Heart,
Star,
Crown,
// eslint-disable-next-line @typescript-eslint/no-deprecated
Github,
Quote,
Reply,

View File

@@ -3,6 +3,9 @@
* 用于存储随机背景图片
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// IndexedDB 事务回调中 result 在 onsuccess 时必定存在
const DB_NAME = 'RandomImageCache'
const DB_VERSION = 1
const STORE_NAME = 'images'

View File

@@ -15,7 +15,7 @@ export interface SearchState {
searchQuery: string
searchMode: 'game' | 'patch'
customApi: string
platformResults: Array<[string, PlatformData]>
platformResults: [string, PlatformData][]
vndbInfo: VndbInfo | null
// 输入状态
inputQuery?: string

View File

@@ -43,8 +43,10 @@ export function watchSystemTheme(callback: (theme: 'light' | 'dark') => void): (
mediaQuery.addEventListener('change', handler)
return () => mediaQuery.removeEventListener('change', handler)
} else {
// 降级到旧的 API
// 降级到旧的 API(兼容旧浏览器)
// eslint-disable-next-line @typescript-eslint/no-deprecated
mediaQuery.addListener(handler)
// eslint-disable-next-line @typescript-eslint/no-deprecated
return () => mediaQuery.removeListener(handler)
}
}
@@ -104,7 +106,8 @@ export function applyCustomJS(js: string): void {
// 如果有新的JS执行脚本
if (js.trim()) {
try {
// 使用 Function 构造器创建并执行脚本
// 使用 Function 构造器创建并执行脚本(用户自定义脚本功能)
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const script = new Function(js)
script()
} catch (error) {

View File

@@ -51,7 +51,7 @@ export function updateURLParams(params: SearchParams): void {
searchParams.delete('api')
// 设置新参数
if (params.s && params.s.trim()) {
if (params.s?.trim()) {
searchParams.set('s', encodeURIComponent(params.s.trim()))
}
@@ -60,7 +60,7 @@ export function updateURLParams(params: SearchParams): void {
searchParams.set('mode', params.mode)
}
if (params.api && params.api.trim()) {
if (params.api?.trim()) {
searchParams.set('api', encodeURIComponent(params.api.trim()))
}
@@ -86,7 +86,7 @@ export function generateShareURL(params: SearchParams): string {
const url = new URL(window.location.origin)
const searchParams = url.searchParams
if (params.s && params.s.trim()) {
if (params.s?.trim()) {
searchParams.set('s', encodeURIComponent(params.s.trim()))
}
@@ -94,7 +94,7 @@ export function generateShareURL(params: SearchParams): string {
searchParams.set('mode', params.mode)
}
if (params.api && params.api.trim()) {
if (params.api?.trim()) {
searchParams.set('api', encodeURIComponent(params.api.trim()))
}

View File

@@ -1,5 +1,4 @@
import type { Config } from 'tailwindcss'
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
@@ -124,5 +123,5 @@ export default {
},
},
plugins: [],
} satisfies Config
}

View File

@@ -28,7 +28,7 @@
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts", "env.d.ts", "scripts/**/*.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}