diff --git a/eslint.config.js b/eslint.config.js index 21a4f75..33cb79f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,12 +22,23 @@ export default tseslint.config( // JavaScript 基础规则 js.configs.recommended, - // TypeScript 推荐规则 - ...tseslint.configs.recommended, + // TypeScript 最严格规则(带类型检查) + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, // Vue 推荐规则 ...pluginVue.configs['flat/recommended'], + // TypeScript 类型检查配置 + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + // 自定义规则 { files: ['**/*.{js,mjs,cjs,ts,vue}'], @@ -76,25 +87,47 @@ export default tseslint.config( }, }, rules: { - // TypeScript 规则 - '@typescript-eslint/no-explicit-any': 'warn', // 允许 any,但给出警告 + // TypeScript 规则(严格但实用) + '@typescript-eslint/no-explicit-any': 'warn', // any 警告(允许但不推荐) '@typescript-eslint/no-unused-vars': [ - 'warn', + 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'none', // 忽略 catch 块中未使用的错误变量 }, ], - '@typescript-eslint/no-non-null-assertion': 'off', // 允许非空断言(IndexedDB 需要) + '@typescript-eslint/no-non-null-assertion': 'warn', // 非空断言警告 + '@typescript-eslint/no-floating-promises': 'warn', // Promise 未处理警告 + '@typescript-eslint/await-thenable': 'error', // await 必须用于 Promise + '@typescript-eslint/require-await': 'off', // 允许空 async 函数 + '@typescript-eslint/restrict-template-expressions': 'off', // 允许模板字符串中使用任意类型 + '@typescript-eslint/no-confusing-void-expression': 'off', // 允许 void 表达式 + '@typescript-eslint/no-unnecessary-condition': 'off', // 关闭,避免误报 + // 关闭过于严格的 unsafe 规则(实际项目中误报太多) + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', // || 和 ?? 各有用途 + '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', // catch 变量类型 + '@typescript-eslint/no-deprecated': 'warn', // 弃用 API 警告而非错误 + '@typescript-eslint/no-extraneous-class': 'off', // 允许静态类(类型定义需要) + '@typescript-eslint/no-unnecessary-type-parameters': 'off', // 允许单次使用的泛型参数 + '@typescript-eslint/restrict-plus-operands': 'off', // + 操作符类型检查 + '@typescript-eslint/no-implied-eval': 'warn', // 隐式 eval 警告 + '@typescript-eslint/no-empty-function': 'off', // 允许空函数 + '@typescript-eslint/no-misused-promises': 'warn', // Promise 误用降为警告 // Vue 规则 'vue/multi-word-component-names': 'off', // 允许单词组件名 - 'vue/no-v-html': 'warn', // v-html 警告而非错误 + 'vue/no-v-html': 'warn', // v-html 警告(XSS 风险) 'vue/require-default-prop': 'off', // 不强制要求默认 prop - 'vue/require-prop-types': 'warn', + 'vue/require-prop-types': 'error', // 必须定义 prop 类型 'vue/html-self-closing': [ - 'warn', + 'error', { html: { void: 'always', @@ -107,24 +140,24 @@ export default tseslint.config( ], 'vue/max-attributes-per-line': 'off', // 不限制每行属性数量 'vue/singleline-html-element-content-newline': 'off', - 'vue/html-indent': ['warn', 2], + 'vue/html-indent': ['error', 2], - // 通用规则 - 'no-console': 'off', // 允许 console(开发时有用) - 'no-debugger': 'warn', + // 通用规则(严格模式) + 'no-console': ['warn', { allow: ['warn', 'error', 'info'] }], // 允许 warn/error/info + 'no-debugger': 'error', 'no-unused-vars': 'off', // 使用 TypeScript 的规则 - 'prefer-const': 'warn', + 'prefer-const': 'error', 'no-var': 'error', - 'eqeqeq': ['warn', 'always'], - 'curly': ['warn', 'all'], - 'semi': ['warn', 'never'], // 不使用分号 - 'quotes': ['warn', 'single', { avoidEscape: true }], - 'comma-dangle': ['warn', 'always-multiline'], - 'arrow-spacing': 'warn', - 'object-curly-spacing': ['warn', 'always'], - 'array-bracket-spacing': ['warn', 'never'], + 'eqeqeq': ['error', 'always'], + 'curly': ['error', 'all'], + 'semi': ['error', 'never'], // 不使用分号 + 'quotes': ['error', 'single', { avoidEscape: true }], + 'comma-dangle': ['error', 'always-multiline'], + 'arrow-spacing': 'error', + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'], 'space-before-function-paren': [ - 'warn', + 'error', { anonymous: 'always', named: 'never', diff --git a/scripts/sw-version-plugin.ts b/scripts/sw-version-plugin.ts index 8f0287e..48ac502 100644 --- a/scripts/sw-version-plugin.ts +++ b/scripts/sw-version-plugin.ts @@ -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}`) }, } } diff --git a/src/App.vue b/src/App.vue index 597fddd..5c30cf8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -105,9 +105,9 @@ const uiStore = useUIStore() const settingsStore = useSettingsStore() const searchHeaderRef = ref | null>(null) -// 切换设置面板 +// 切换设置面板(互斥) function openSettings() { - uiStore.isSettingsModalOpen = !uiStore.isSettingsModalOpen + uiStore.toggleSettingsModal() } // 处理历史记录选择 @@ -342,6 +342,7 @@ function cleanupBlobUrls(currentUrl: string) { // 创建新 Map,保留当前使用的 URL const newBlobUrls = new Map() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion newBlobUrls.set(currentUrl, blobUrls.get(currentUrl)!) // 保留最近添加的 URL(Map 保持插入顺序) @@ -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() } // 恢复保存的搜索状态 diff --git a/src/api/search.ts b/src/api/search.ts index e980416..1e13dde 100644 --- a/src/api/search.ts +++ b/src/api/search.ts @@ -455,7 +455,7 @@ export async function fetchVndbData(gameName: string): Promise // 获取封面图片 - 优先选择安全级别的图片 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 { 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 { - 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 { 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 { 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) } diff --git a/src/components/CommentsModal.vue b/src/components/CommentsModal.vue index 82bf69b..0fe301b 100644 --- a/src/components/CommentsModal.vue +++ b/src/components/CommentsModal.vue @@ -10,7 +10,7 @@ leave-to-class="opacity-0 scale-[0.98] translate-y-10" >
@@ -140,7 +140,7 @@ function initArtalk() { } } - nextTick(() => { + void nextTick(() => { const commentsEl = document.getElementById('Comments') if (commentsEl) { try { diff --git a/src/components/FloatingButtons.vue b/src/components/FloatingButtons.vue index 2ff5217..dffd54e 100644 --- a/src/components/FloatingButtons.vue +++ b/src/components/FloatingButtons.vue @@ -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() diff --git a/src/components/LazyRender.vue b/src/components/LazyRender.vue index 6d0f5e3..6d2ac33 100644 --- a/src/components/LazyRender.vue +++ b/src/components/LazyRender.vue @@ -1,5 +1,12 @@