Merge pull request #81 from Moe-Sakura/dev

Dev
This commit is contained in:
Asuna
2026-03-30 23:43:53 +08:00
committed by GitHub
19 changed files with 747 additions and 916 deletions

View File

@@ -178,6 +178,10 @@ export default tseslint.config(
extraFileExtensions: ['.vue'],
},
},
rules: {
// <script setup> 中变量在 template 使用ESLint 无法感知
'no-useless-assignment': 'off',
},
},
)

View File

@@ -5,37 +5,39 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"check": "vue-tsc --noEmit && eslint ."
},
"devDependencies": {
"@eslint/js": "^9.39.3",
"@tailwindcss/vite": "^4.2.0",
"@types/node": "^25.3.0",
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.0",
"@vitejs/plugin-vue": "^6.0.4",
"eslint": "^9.39.3",
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"@vitejs/plugin-vue": "^6.0.5",
"eslint": "^10.1.0",
"eslint-plugin-vue": "^10.8.0",
"tailwindcss": "^4.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2",
"typescript-eslint": "^8.57.2",
"vite": "^8.0.3",
"vite-plugin-pwa": "^1.2.0",
"vue-tsc": "^3.2.5",
"vue-tsc": "^3.2.6",
"workbox-window": "^7.4.0"
},
"dependencies": {
"@fontsource/noto-sans-sc": "^5.2.9",
"@tanstack/vue-virtual": "^3.13.18",
"@tanstack/vue-virtual": "^3.13.23",
"artalk": "^2.9.1",
"lucide-vue-next": "^0.575.0",
"lucide-vue-next": "^1.0.0",
"pinia": "^3.0.4",
"prismjs": "^1.30.0",
"vue": "^3.5.28",
"vue": "^3.5.31",
"vue-prism-editor": "2.0.0-alpha.2"
},
"packageManager": "pnpm@10.30.1"
"packageManager": "pnpm@10.33.0"
}

1484
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -146,7 +146,7 @@ function initArtalk() {
try {
artalkInstance = Artalk.init({
el: '#Comments',
pageKey: 'https://www.searchgal.top/',
pageKey: '/',
server: 'https://artalk.saop.cc',
site: '旮旯聚搜',
darkMode: 'auto',

View File

@@ -16,7 +16,6 @@
>
<!-- 面板 -->
<div
ref="panelRef"
class="keyboard-help-panel glassmorphism-card rounded-3xl shadow-2xl shadow-black/20 w-full max-w-md overflow-hidden"
>
<!-- 标题栏 -->
@@ -161,7 +160,6 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUIStore } from '@/stores/ui'
import { playTransitionDown } from '@/composables/useSound'
import {
@@ -172,7 +170,6 @@ import {
} from 'lucide-vue-next'
const uiStore = useUIStore()
const panelRef = ref<HTMLElement | null>(null)
function close() {
playTransitionDown()

View File

@@ -522,13 +522,7 @@ let searchStartTime = 0
// 友情链接
import friendsData from '@/data/friends.json'
interface FriendLink {
name: string
desc: string
url: string
logo: string
}
const friendLinks = ref<FriendLink[]>(friendsData.friends || [])
const friendLinks = ref(friendsData.friends || [])
// 友链 logo 加载失败时显示占位符
function handleFriendLogoError(e: Event) {

View File

@@ -12,7 +12,6 @@
>
<div
v-if="uiStore.isVndbPanelOpen && searchStore.vndbInfo"
ref="modalRef"
class="fixed z-50 flex flex-col vndb-page shadow-2xl shadow-black/20 inset-0 md:inset-6 md:m-auto md:w-[900px] md:max-w-[calc(100%-3rem)] md:h-[800px] md:max-h-[calc(100%-3rem)] md:rounded-3xl"
>
<!-- 顶部导航栏 -->
@@ -597,7 +596,7 @@ const translateError = ref(false)
// 标签翻译状态
const isTranslatingTags = ref(false)
const translatedTags = ref<Map<string, string>>(new Map())
const translatedTags = ref(new Map())
const showOriginalTags = ref(false)
const translateTagsError = ref(false)
@@ -608,7 +607,7 @@ const isLoadingCharacters = ref(false)
const isLoadingQuotes = ref(false)
// 名言翻译状态
const translatedQuotes = ref<Map<string, string>>(new Map())
const translatedQuotes = ref(new Map())
const isTranslatingQuotes = ref(false)
const translateQuotesError = ref(false)
const showOriginalQuotes = ref(false)
@@ -653,7 +652,6 @@ function toggleSection(section: keyof typeof expandedSections.value) {
expandedSections.value[section] = !expandedSections.value[section]
}
const modalRef = ref<HTMLElement | null>(null)
// 计算 VNDB URL
const vndbUrl = computed(() => {
@@ -924,17 +922,6 @@ function openGallery(startIndex: number) {
}
}
// 格式化性别(保留备用)
function _formatSex(sex: string): string {
const sexMap: Record<string, string> = {
'm': '男性',
'f': '女性',
'b': '双性',
'n': '无性',
}
return sexMap[sex] || sex
}
// 格式化日期
function formatDate(dateString: string): string {
if (!dateString) {return '未知'}
@@ -1106,16 +1093,6 @@ function getTagCategoryClass(category: string): string {
return categoryMap[category] || 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-400'
}
// 格式化标签分类名称(保留备用)
function _formatTagCategory(category: string): string {
const categoryMap: Record<string, string> = {
'cont': '内容',
'tech': '技术',
'ero': '色情',
}
return categoryMap[category] || category
}
// 格式化关系类型
function formatRelation(relation: string): string {
const relationMap: Record<string, string> = {

View File

@@ -129,10 +129,9 @@ export const vTextScroll = {
// 获取当前实际文本内容(排除克隆的内容)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const inner = el.querySelector('.text-scroll-inner')!
let currentContent = ''
let currentContent: string
if (inner) {
// 获取不包含克隆的文本
const clone = inner.querySelector('.text-scroll-clone')
if (clone) {
currentContent = inner.textContent?.replace(clone.textContent || '', '') || ''
@@ -140,11 +139,9 @@ export const vTextScroll = {
currentContent = inner.textContent || ''
}
} else {
// 内容被 Vue 重新渲染,需要重新包装
currentContent = el.textContent || ''
}
// 如果内容变化或结构被破坏,重新初始化
if (!inner || currentContent !== extEl._textScrollContent) {
const newContent = el.textContent || ''
extEl._textScrollContent = newContent

View File

@@ -17,13 +17,13 @@ export function useVndbTranslation() {
// 标签翻译状态
const isTranslatingTags = ref(false)
const translatedTags = ref<Map<string, string>>(new Map())
const translatedTags = ref(new Map())
const showOriginalTags = ref(false)
const translateTagsError = ref(false)
// 名言翻译状态
const isTranslatingQuotes = ref(false)
const translatedQuotes = ref<Map<string, string>>(new Map())
const translatedQuotes = ref(new Map())
const showOriginalQuotes = ref(false)
const translateQuotesError = ref(false)

View File

@@ -34,7 +34,7 @@
"name": "VNS",
"desc": "Visual Novel",
"url": "https://gal.saop.cc",
"logo": "https://gal.saop.cc/images/logo.svg"
"logo": "https://gal.saop.cc/img/logo.svg"
},
{
"name": "KisuGal",

View File

@@ -2,31 +2,16 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { VndbInfo } from './search'
interface CachedVndbInfo {
query: string
data: VndbInfo
timestamp: number
expiresAt: number
}
interface SearchResultData {
platforms: Map<string, unknown>
vndbInfo?: unknown
}
interface CachedSearchResults {
query: string
mode: 'game' | 'patch'
data: SearchResultData
timestamp: number
expiresAt: number
}
export const useCacheStore = defineStore('cache', () => {
// 状态
const vndbCache = ref<Map<string, CachedVndbInfo>>(new Map())
const searchResultsCache = ref<Map<string, CachedSearchResults>>(new Map())
const imageCache = ref<Map<string, string>>(new Map()) // URL -> base64
const vndbCache = ref(new Map())
const searchResultsCache = ref(new Map())
const imageCache = ref(new Map()) // URL -> base64
// 配置
const vndbCacheDuration = ref(30 * 60 * 1000) // 30 分钟

View File

@@ -1,16 +1,9 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface LoadingImage {
src: string
startTime: number
status: 'loading' | 'loaded' | 'error'
loadTime?: number
}
export const useLazyLoadStore = defineStore('lazyLoad', () => {
// 状态
const images = ref<Map<string, LoadingImage>>(new Map())
const images = ref(new Map())
const sessionStats = ref({
totalLoaded: 0,
totalErrors: 0,

View File

@@ -24,7 +24,7 @@ export const useSearchStore = defineStore('search', () => {
const searchQuery = ref('')
const searchMode = ref<'game' | 'patch'>('game')
const customApi = ref('')
const platformResults = ref<Map<string, PlatformData>>(new Map())
const platformResults = ref(new Map())
const vndbInfo = ref<VndbInfo | null>(null)
const isSearching = ref(false)
const searchProgress = ref({ current: 0, total: 0 })

View File

@@ -58,7 +58,7 @@ const DEFAULT_SETTINGS: UserSettings = {
export const useSettingsStore = defineStore('settings', () => {
// 状态
const settings = ref<UserSettings>({ ...DEFAULT_SETTINGS })
const settings = ref({ ...DEFAULT_SETTINGS })
const isInitialized = ref(false)
// 设置变更历史(用于撤销)

View File

@@ -51,14 +51,14 @@ export const useStatsStore = defineStore('stats', () => {
]))
// 访客统计(不蒜子)
const visitorStats = ref<VisitorStats>({
const visitorStats = ref({
pv: 0,
uv: 0,
lastUpdated: 0,
})
// 应用统计
const appStats = ref<AppStats>({
const appStats = ref({
totalSearches: 0,
gameSearches: 0,
patchSearches: 0,

View File

@@ -9,7 +9,7 @@ const CUSTOM_CSS_STORAGE_KEY = 'searchgal_custom_css'
* 获取系统主题偏好
*/
export function getSystemTheme(): 'light' | 'dark' {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
return 'dark'
}
return 'light'

View File

@@ -1,14 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
@@ -23,7 +21,6 @@
/* Vue */
"types": ["node"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}

View File

@@ -4,7 +4,6 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]

View File

@@ -19,12 +19,7 @@ export default defineConfig({
},
plugins: [
vue({
script: {
// 响应式语法糖(如需要可启用)
defineModel: true,
},
}),
vue(),
tailwindcss(),
// PWA 配置
VitePWA({
@@ -128,68 +123,47 @@ export default defineConfig({
// 构建优化
build: {
// 使用现代浏览器目标
target: 'esnext',
// 启用 CSS 代码分割
cssCodeSplit: true,
// 压缩选项
minify: 'esbuild',
// 源码映射(生产环境关闭)
sourcemap: false,
// Chunk 大小警告阈值 (KB)
chunkSizeWarningLimit: 600,
// Rollup 配置
rollupOptions: {
rolldownOptions: {
output: {
// 资源文件名
assetFileNames: (assetInfo) => {
const name = assetInfo.name || '';
// 字体文件
if (/\.(woff2?|eot|ttf|otf)$/i.test(name)) {
return 'fonts/[name]-[hash][extname]';
}
// 图片文件
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(name)) {
return 'images/[name]-[hash][extname]';
}
// CSS 文件
if (/\.css$/i.test(name)) {
return 'css/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
// JS 入口文件名
entryFileNames: 'js/[name]-[hash].js',
// JS Chunk 文件名
chunkFileNames: 'js/[name]-[hash].js',
// 手动分包
manualChunks: (id) => {
if (id.includes('node_modules')) {
// Vue 核心
if (id.includes('/vue/') || id.includes('/@vue/')) {
return 'vue-core';
}
// Pinia 状态管理
if (id.includes('/pinia/')) {
return 'pinia';
}
// UI 库
if (id.includes('/lucide-vue-next/')) {
return 'ui-libs';
}
// Artalk 评论
if (id.includes('/artalk/')) {
return 'artalk';
}
// 代码编辑器
if (id.includes('/prismjs/') || id.includes('/vue-prism-editor/')) {
return 'editor';
}
// Fancybox
if (id.includes('/@fancyapps/')) {
return 'fancybox';
}
// 其他第三方库
return 'vendor';
}
},
@@ -199,24 +173,14 @@ export default defineConfig({
// 依赖优化
optimizeDeps: {
// 预构建的依赖
include: [
'vue',
'pinia',
'lucide-vue-next',
],
// 排除不需要预构建的
exclude: ['artalk'],
},
// esbuild 配置
esbuild: {
// 生产环境移除 console 和 debugger
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
// 压缩选项
legalComments: 'none',
},
// CSS 配置
css: {
devSourcemap: true,