mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-01-25 19:42:23 +08:00
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:
@@ -11,7 +11,6 @@ export default tseslint.config(
|
||||
'**/.pnpm-store/**',
|
||||
'**/public/**',
|
||||
'**/*.config.js',
|
||||
'**/*.config.mjs',
|
||||
'**/*.config.ts',
|
||||
'**/.history/**', // 忽略编辑器历史文件
|
||||
'**/.vscode/**',
|
||||
@@ -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}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
23
src/App.vue
23
src/App.vue
@@ -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)!)
|
||||
|
||||
// 保留最近添加的 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()
|
||||
}
|
||||
|
||||
// 恢复保存的搜索状态
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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" />
|
||||
<!-- 占位符 -->
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
20
src/main.ts
20
src/main.ts
@@ -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 {
|
||||
// 静默处理
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -71,6 +71,7 @@ export {
|
||||
Heart,
|
||||
Star,
|
||||
Crown,
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
Github,
|
||||
Quote,
|
||||
Reply,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" }]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user