mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-03-26 06:59:44 +08:00
feat: 性能优化与组件改进
* 在 `index.html` 中添加性能优化的 meta 标签,提升页面加载速度。 * 更新 `App.vue` 中的背景层,使用 GPU 加速和懒加载策略,优化性能。 * 在多个组件中引入 GPU 加速和渲染隔离的 CSS 类,提升动画和交互性能。 * 更新 `FloatingButtons.vue` 和 `SearchHeader.vue` 的样式,确保在不同主题下的视觉一致性。 * 优化 `useClickEffect.ts` 中的点击特效实现,使用对象池和 CSS 变量减少 DOM 操作和样式计算。 * 在 `base.css` 中添加全局性能优化工具类,提升整体渲染效率。
This commit is contained in:
@@ -100,6 +100,10 @@
|
||||
<link rel="dns-prefetch" href="https://artalk.saop.cc" />
|
||||
<link rel="dns-prefetch" href="https://status.searchgal.homes" />
|
||||
|
||||
<!-- 性能优化 Meta 标签 -->
|
||||
<meta http-equiv="x-dns-prefetch-control" content="on" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
|
||||
<style>
|
||||
/* ============================================
|
||||
第三方库样式覆盖
|
||||
@@ -196,7 +200,7 @@
|
||||
src="https://registry.npmmirror.com/js-asuna/latest/files/js/bsz.pure.mini.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="touch-optimize">
|
||||
<!-- 禁止无 JavaScript 用户访问 -->
|
||||
<noscript>
|
||||
<style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^25.0.1",
|
||||
"@types/node": "^25.0.2",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -44,10 +44,10 @@ importers:
|
||||
version: 9.39.2
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))
|
||||
version: 4.1.18(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))
|
||||
'@types/node':
|
||||
specifier: ^25.0.1
|
||||
version: 25.0.1
|
||||
specifier: ^25.0.2
|
||||
version: 25.0.2
|
||||
'@types/nprogress':
|
||||
specifier: ^0.2.3
|
||||
version: 0.2.3
|
||||
@@ -59,7 +59,7 @@ importers:
|
||||
version: 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))
|
||||
version: 6.0.3(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))
|
||||
eslint:
|
||||
specifier: ^9.39.2
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
@@ -77,7 +77,7 @@ importers:
|
||||
version: 8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
vite:
|
||||
specifier: ^7.2.7
|
||||
version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)
|
||||
version: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)
|
||||
vue-tsc:
|
||||
specifier: ^3.1.8
|
||||
version: 3.1.8(typescript@5.9.3)
|
||||
@@ -545,8 +545,8 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@25.0.1':
|
||||
resolution: {integrity: sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==}
|
||||
'@types/node@25.0.2':
|
||||
resolution: {integrity: sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==}
|
||||
|
||||
'@types/nprogress@0.2.3':
|
||||
resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
|
||||
@@ -1710,18 +1710,18 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
|
||||
|
||||
'@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))':
|
||||
'@tailwindcss/vite@4.1.18(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.18
|
||||
'@tailwindcss/oxide': 4.1.18
|
||||
tailwindcss: 4.1.18
|
||||
vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)
|
||||
vite: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@25.0.1':
|
||||
'@types/node@25.0.2':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
@@ -1820,10 +1820,10 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.49.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@6.0.3(vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)
|
||||
vite: 7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
'@volar/language-core@2.4.26':
|
||||
@@ -2533,7 +2533,7 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1):
|
||||
vite@7.2.7(@types/node@25.0.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -2542,7 +2542,7 @@ snapshots:
|
||||
rollup: 4.53.2
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.1
|
||||
'@types/node': 25.0.2
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
|
||||
168
src/App.vue
168
src/App.vue
@@ -9,8 +9,11 @@
|
||||
id="app"
|
||||
class="min-h-screen relative"
|
||||
>
|
||||
<!-- 背景层容器 -->
|
||||
<div id="background-container" class="fixed inset-0 z-[-2] overflow-hidden">
|
||||
<!-- 背景层容器 - GPU 加速 -->
|
||||
<div
|
||||
id="background-container"
|
||||
class="fixed inset-0 z-[-2] overflow-hidden gpu-layer"
|
||||
>
|
||||
<!-- 默认背景纹理(无图片时显示) -->
|
||||
<div
|
||||
id="background-pattern"
|
||||
@@ -27,7 +30,7 @@
|
||||
:animate="getTransitionVariant('animate')"
|
||||
:exit="getTransitionVariant('exit')"
|
||||
:transition="bgTransition"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat will-change-transform"
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat will-change-transform gpu-layer"
|
||||
:style="{ backgroundImage: `url(${backgroundImageUrl})` }"
|
||||
/>
|
||||
</AnimatePresence>
|
||||
@@ -63,7 +66,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { Motion, AnimatePresence } from 'motion-v'
|
||||
import { imageDB } from '@/utils/imageDB'
|
||||
import { useSearchStore } from '@/stores/search'
|
||||
@@ -75,16 +78,21 @@ import {
|
||||
saveCustomCSS,
|
||||
applyCustomCSS,
|
||||
} from '@/utils/theme'
|
||||
|
||||
// 关键组件 - 同步加载
|
||||
import StatsCorner from '@/components/StatsCorner.vue'
|
||||
import TopToolbar from '@/components/TopToolbar.vue'
|
||||
import SearchHeader from '@/components/SearchHeader.vue'
|
||||
import SearchResults from '@/components/SearchResults.vue'
|
||||
import FloatingButtons from '@/components/FloatingButtons.vue'
|
||||
import CommentsModal from '@/components/CommentsModal.vue'
|
||||
import VndbPanel from '@/components/VndbPanel.vue'
|
||||
import SettingsModal from '@/components/SettingsModal.vue'
|
||||
import SearchHistoryModal from '@/components/SearchHistoryModal.vue'
|
||||
import UpdateToast from '@/components/UpdateToast.vue'
|
||||
|
||||
// 非关键组件 - 异步懒加载(用户交互时才加载)
|
||||
const CommentsModal = defineAsyncComponent(() => import('@/components/CommentsModal.vue'))
|
||||
const VndbPanel = defineAsyncComponent(() => import('@/components/VndbPanel.vue'))
|
||||
const SettingsModal = defineAsyncComponent(() => import('@/components/SettingsModal.vue'))
|
||||
const SearchHistoryModal = defineAsyncComponent(() => import('@/components/SearchHistoryModal.vue'))
|
||||
const UpdateToast = defineAsyncComponent(() => import('@/components/UpdateToast.vue'))
|
||||
|
||||
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||
import { useClickEffect } from '@/composables/useClickEffect'
|
||||
|
||||
@@ -137,10 +145,11 @@ function handleSwUpdate() {
|
||||
}
|
||||
}
|
||||
const randomImageUrl = ref('')
|
||||
const imageCache = ref<string[]>([])
|
||||
const imageCacheSet = ref<Set<string>>(new Set()) // 用于快速查重
|
||||
const imageBlobUrls = ref<Map<string, string>>(new Map()) // URL -> Blob URL 映射
|
||||
const shuffledQueue = ref<string[]>([])
|
||||
// 使用 shallowRef 优化大型数据结构的响应式性能
|
||||
const imageCache = shallowRef<string[]>([])
|
||||
const imageCacheSet = shallowRef<Set<string>>(new Set()) // 用于快速查重
|
||||
const imageBlobUrls = shallowRef<Map<string, string>>(new Map()) // URL -> Blob URL 映射
|
||||
const shuffledQueue = shallowRef<string[]>([])
|
||||
let fetchInterval: number | null = null
|
||||
let displayInterval: number | null = null
|
||||
let systemThemeCleanup: (() => void) | null = null
|
||||
@@ -149,6 +158,10 @@ const MAX_CACHE_SIZE = 10000 // 最大缓存 10000 张图片
|
||||
const CLEANUP_BATCH_SIZE = 2000 // 每次清理 2000 张
|
||||
const FETCH_INTERVAL = 5000 // 5秒获取一次
|
||||
const DISPLAY_INTERVAL = 10000 // 10秒切换一次
|
||||
const MAX_BLOB_URLS = 20 // 最大同时保持的 Blob URL 数量(内存优化)
|
||||
|
||||
// 导入性能优化工具
|
||||
import { scheduleIdleTask } from '@/composables/usePerformance'
|
||||
|
||||
// 背景动画相关
|
||||
const currentBgKey = ref(0) // 用于触发 AnimatePresence 动画
|
||||
@@ -228,21 +241,23 @@ async function loadCacheFromDB() {
|
||||
const urls = await imageDB.getAllUrls()
|
||||
|
||||
if (urls.length > 0) {
|
||||
// 去重处理
|
||||
// 去重处理 - shallowRef 需要重新赋值
|
||||
const uniqueUrls = [...new Set(urls)]
|
||||
imageCache.value = uniqueUrls
|
||||
imageCacheSet.value = new Set(uniqueUrls)
|
||||
|
||||
// 预加载部分图片的 Blob URL(前10张)
|
||||
// 预加载部分图片的 Blob URL(前10张)- 使用新 Map 触发更新
|
||||
const preloadCount = Math.min(10, uniqueUrls.length)
|
||||
const newBlobUrls = new Map(imageBlobUrls.value)
|
||||
for (let i = 0; i < preloadCount; i++) {
|
||||
const url = uniqueUrls[i]
|
||||
const blob = await imageDB.getImageByUrl(url)
|
||||
if (blob) {
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
imageBlobUrls.value.set(url, blobUrl)
|
||||
newBlobUrls.set(url, blobUrl)
|
||||
}
|
||||
}
|
||||
imageBlobUrls.value = newBlobUrls
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -265,7 +280,8 @@ function shuffleArray<T>(array: T[]): T[] {
|
||||
// 重新洗牌队列
|
||||
function reshuffleQueue() {
|
||||
if (imageCache.value.length > 0) {
|
||||
shuffledQueue.value = shuffleArray(imageCache.value)
|
||||
// shallowRef 需要重新赋值才能触发更新
|
||||
shuffledQueue.value = shuffleArray([...imageCache.value])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,10 +322,16 @@ async function fetchAndCacheImage() {
|
||||
// 保存到 IndexedDB
|
||||
await imageDB.addImage(finalUrl, blob)
|
||||
|
||||
// 添加到内存缓存
|
||||
imageCache.value.push(finalUrl)
|
||||
imageCacheSet.value.add(finalUrl)
|
||||
imageBlobUrls.value.set(finalUrl, blobUrl)
|
||||
// 添加到内存缓存 - shallowRef 需要重新赋值触发更新
|
||||
const newCache = [...imageCache.value, finalUrl]
|
||||
const newCacheSet = new Set(imageCacheSet.value)
|
||||
newCacheSet.add(finalUrl)
|
||||
const newBlobUrls = new Map(imageBlobUrls.value)
|
||||
newBlobUrls.set(finalUrl, blobUrl)
|
||||
|
||||
imageCache.value = newCache
|
||||
imageCacheSet.value = newCacheSet
|
||||
imageBlobUrls.value = newBlobUrls
|
||||
|
||||
// 限制缓存大小 - 大于 10000 张即清理最早的 2000 张
|
||||
const count = await imageDB.getCount()
|
||||
@@ -318,17 +340,31 @@ async function fetchAndCacheImage() {
|
||||
const deletedCount = await imageDB.deleteOldestBatch(CLEANUP_BATCH_SIZE)
|
||||
|
||||
// 同步更新内存缓存 - 移除前 deletedCount 张
|
||||
const cleanedCache = imageCache.value.slice(deletedCount)
|
||||
const cleanedSet = new Set(cleanedCache)
|
||||
const cleanedBlobUrls = new Map<string, string>()
|
||||
|
||||
// 清理被删除图片的 Blob URL
|
||||
for (let i = 0; i < deletedCount; i++) {
|
||||
const removed = imageCache.value.shift()
|
||||
const removed = imageCache.value[i]
|
||||
if (removed) {
|
||||
imageCacheSet.value.delete(removed)
|
||||
const oldBlobUrl = imageBlobUrls.value.get(removed)
|
||||
if (oldBlobUrl) {
|
||||
URL.revokeObjectURL(oldBlobUrl)
|
||||
imageBlobUrls.value.delete(removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保留剩余的 Blob URL
|
||||
imageBlobUrls.value.forEach((url, key) => {
|
||||
if (cleanedSet.has(key)) {
|
||||
cleanedBlobUrls.set(key, url)
|
||||
}
|
||||
})
|
||||
|
||||
imageCache.value = cleanedCache
|
||||
imageCacheSet.value = cleanedSet
|
||||
imageBlobUrls.value = cleanedBlobUrls
|
||||
}
|
||||
|
||||
// 如果队列为空,重新洗牌
|
||||
@@ -356,6 +392,31 @@ async function fetchAndCacheImage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 清理过多的 Blob URL 以释放内存
|
||||
function cleanupBlobUrls(currentUrl: string) {
|
||||
const blobUrls = imageBlobUrls.value
|
||||
if (blobUrls.size <= MAX_BLOB_URLS) { return }
|
||||
|
||||
// 创建新 Map,保留当前使用的 URL
|
||||
const newBlobUrls = new Map<string, string>()
|
||||
newBlobUrls.set(currentUrl, blobUrls.get(currentUrl)!)
|
||||
|
||||
// 保留最近添加的 URL(Map 保持插入顺序)
|
||||
const entries = Array.from(blobUrls.entries())
|
||||
const toKeep = entries.slice(-MAX_BLOB_URLS + 1) // 保留最后 N-1 个
|
||||
|
||||
// 释放旧的 Blob URL
|
||||
for (const [url, blobUrl] of entries) {
|
||||
if (url !== currentUrl && !toKeep.some(([u]) => u === url)) {
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
} else if (url !== currentUrl) {
|
||||
newBlobUrls.set(url, blobUrl)
|
||||
}
|
||||
}
|
||||
|
||||
imageBlobUrls.value = newBlobUrls
|
||||
}
|
||||
|
||||
// 从洗牌队列中取出下一张图片(预加载后再切换)
|
||||
async function displayNextImage() {
|
||||
// 如果队列为空,重新洗牌
|
||||
@@ -366,8 +427,11 @@ async function displayNextImage() {
|
||||
reshuffleQueue()
|
||||
}
|
||||
|
||||
// 从队列中取出第一张图片
|
||||
const nextImageUrl = shuffledQueue.value.shift()
|
||||
// 从队列中取出第一张图片 - shallowRef 需要重新赋值
|
||||
const queue = [...shuffledQueue.value]
|
||||
const nextImageUrl = queue.shift()
|
||||
shuffledQueue.value = queue
|
||||
|
||||
if (!nextImageUrl) {return}
|
||||
|
||||
try {
|
||||
@@ -379,7 +443,10 @@ async function displayNextImage() {
|
||||
const blob = await imageDB.getImageByUrl(nextImageUrl)
|
||||
if (blob) {
|
||||
blobUrl = URL.createObjectURL(blob)
|
||||
imageBlobUrls.value.set(nextImageUrl, blobUrl)
|
||||
// shallowRef 需要重新赋值
|
||||
const newBlobUrls = new Map(imageBlobUrls.value)
|
||||
newBlobUrls.set(nextImageUrl, blobUrl)
|
||||
imageBlobUrls.value = newBlobUrls
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +459,9 @@ async function displayNextImage() {
|
||||
// 图片加载完成,更新 key 触发 motion-v 动画
|
||||
randomImageUrl.value = nextImageUrl
|
||||
currentBgKey.value++
|
||||
|
||||
// 清理过多的 Blob URL 以释放内存
|
||||
cleanupBlobUrls(nextImageUrl)
|
||||
}
|
||||
preloadImg.onerror = () => {
|
||||
// 加载失败,尝试下一张
|
||||
@@ -445,6 +515,8 @@ function stopAllIntervals() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// === 关键任务:立即执行 ===
|
||||
|
||||
// 初始化 UI Store(恢复持久化状态)
|
||||
uiStore.init()
|
||||
|
||||
@@ -452,27 +524,33 @@ onMounted(async () => {
|
||||
const systemTheme = getSystemTheme()
|
||||
applyTheme(systemTheme)
|
||||
|
||||
// 应用自定义 CSS
|
||||
if (uiStore.customCSS) {
|
||||
applyCustomCSS(uiStore.customCSS)
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
systemThemeCleanup = watchSystemTheme((theme) => {
|
||||
applyTheme(theme)
|
||||
})
|
||||
|
||||
// 监听 SW 更新事件
|
||||
window.addEventListener('sw-update-available', (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const customEvent = event as any
|
||||
swRegistration = customEvent.detail?.registration || null
|
||||
uiStore.setShowUpdateToast(true)
|
||||
})
|
||||
|
||||
// 恢复保存的搜索状态
|
||||
searchStore.restoreState()
|
||||
|
||||
// === 非关键任务:空闲时执行 ===
|
||||
|
||||
scheduleIdleTask(() => {
|
||||
// 应用自定义 CSS
|
||||
if (uiStore.customCSS) {
|
||||
applyCustomCSS(uiStore.customCSS)
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
systemThemeCleanup = watchSystemTheme((theme) => {
|
||||
applyTheme(theme)
|
||||
})
|
||||
|
||||
// 监听 SW 更新事件
|
||||
window.addEventListener('sw-update-available', (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const customEvent = event as any
|
||||
swRegistration = customEvent.detail?.registration || null
|
||||
uiStore.setShowUpdateToast(true)
|
||||
})
|
||||
}, { timeout: 2000 })
|
||||
|
||||
// === 背景图片初始化:稍后执行 ===
|
||||
|
||||
// 初始化 IndexedDB
|
||||
await imageDB.init()
|
||||
|
||||
|
||||
@@ -283,7 +283,14 @@ onUnmounted(() => {
|
||||
0 3px 10px rgba(255, 105, 180, 0.2),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.5) inset;
|
||||
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* 性能优化:仅过渡 transform 和 opacity(GPU 加速) */
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s ease-out,
|
||||
border-color 0.3s ease-out;
|
||||
|
||||
/* 强制 GPU 层 */
|
||||
transform: translate3d(0, 0, 0);
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@@ -314,12 +321,13 @@ onUnmounted(() => {
|
||||
0 6px 20px rgba(255, 105, 180, 0.35),
|
||||
0 0 40px rgba(255, 20, 147, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.7) inset;
|
||||
transform: translateY(-4px) scale(1.08) rotate(5deg);
|
||||
/* 使用 translate3d 保持 GPU 层 */
|
||||
transform: translate3d(0, -4px, 0) scale(1.08) rotate(5deg);
|
||||
border-color: rgba(255, 20, 147, 0.5);
|
||||
}
|
||||
|
||||
.fab-button:active {
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
transform: translate3d(0, -2px, 0) scale(1.02);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(255, 20, 147, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.4) inset;
|
||||
@@ -413,10 +421,14 @@ onUnmounted(() => {
|
||||
rgba(255, 255, 255, 0.95) 0%,
|
||||
rgba(248, 250, 252, 0.98) 100%
|
||||
);
|
||||
backdrop-filter: blur(40px) saturate(1.5);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(1.5);
|
||||
/* 降低 blur 值以提升性能 */
|
||||
backdrop-filter: blur(20px) saturate(1.4);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
overflow: hidden;
|
||||
/* 渲染隔离 */
|
||||
contain: layout paint;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* 站点导航面板 - 右下角弹出 (暗色模式) */
|
||||
@@ -440,19 +452,22 @@ onUnmounted(() => {
|
||||
border-bottom: 1px solid rgba(255, 105, 180, 0.15);
|
||||
}
|
||||
|
||||
/* 导航项 */
|
||||
/* 导航项 - GPU 加速动画 */
|
||||
.nav-item {
|
||||
background: transparent;
|
||||
animation: navItemSlideIn 0.3s ease-out both;
|
||||
/* 强制 GPU 层 */
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.2s ease-out, background 0.2s ease-out;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 20, 147, 0.08), rgba(217, 70, 239, 0.05));
|
||||
transform: translateX(4px);
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
|
||||
.nav-item:active {
|
||||
transform: translateX(2px) scale(0.98);
|
||||
transform: translate3d(2px, 0, 0) scale(0.98);
|
||||
}
|
||||
|
||||
.dark .nav-item:hover {
|
||||
@@ -462,11 +477,11 @@ onUnmounted(() => {
|
||||
@keyframes navItemSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
transform: translate3d(-10px, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,100 +42,144 @@
|
||||
class="search-form w-full max-w-2xl px-2 sm:px-0 animate-fade-in-up"
|
||||
@submit.prevent="triggerSearch"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Search Input with Button Inside - 使用 Tailwind -->
|
||||
<div class="relative">
|
||||
<!-- 搜索图标 -->
|
||||
<Search
|
||||
:size="20"
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-theme-primary/60 dark:text-theme-accent/70 pointer-events-none z-10"
|
||||
<div class="flex flex-col gap-5">
|
||||
<!-- Search Input Container - Google 风格 -->
|
||||
<div
|
||||
class="search-input-wrapper group relative"
|
||||
:class="{ 'is-searching': searchStore.isSearching }"
|
||||
>
|
||||
<!-- 外层发光效果 -->
|
||||
<div
|
||||
class="absolute -inset-0.5 rounded-[1.25rem] opacity-0 group-hover:opacity-100 group-focus-within:opacity-100
|
||||
bg-gradient-to-r from-[#ff1493]/30 via-[#d946ef]/20 to-[#ff69b4]/30
|
||||
blur-lg transition-opacity duration-500"
|
||||
:class="{ 'opacity-100': searchStore.isSearching }"
|
||||
/>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="游戏或补丁关键字词*"
|
||||
required
|
||||
class="w-full pl-12 pr-32 py-4 text-base rounded-2xl
|
||||
text-gray-900 dark:text-slate-100
|
||||
placeholder:text-gray-400 dark:placeholder:text-slate-400
|
||||
glassmorphism-input
|
||||
shadow-lg shadow-theme-primary/10 dark:shadow-theme-accent/15
|
||||
hover:shadow-xl hover:shadow-theme-primary/15 dark:hover:shadow-theme-accent/20
|
||||
focus:shadow-2xl focus:shadow-theme-primary/20 dark:focus:shadow-theme-accent/25
|
||||
focus:scale-[1.01]
|
||||
transition-all duration-300 outline-none font-medium"
|
||||
@keydown.enter.prevent="triggerSearch"
|
||||
/>
|
||||
|
||||
<!-- 搜索按钮 - 内嵌在输入框右侧 -->
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="searchStore.searchDisabled"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2
|
||||
px-6 py-2.5 rounded-xl
|
||||
bg-gradient-pink text-white font-bold text-sm
|
||||
border border-white/30 dark:border-white/20
|
||||
shadow-md shadow-theme-primary/20
|
||||
hover:shadow-lg hover:shadow-theme-primary/30 hover:scale-105
|
||||
active:scale-95
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-all duration-200
|
||||
flex items-center gap-2 z-10 glass-gpu"
|
||||
@click.prevent="triggerSearch"
|
||||
>
|
||||
<Search :size="16" />
|
||||
<span v-if="!searchStore.isSearching && !isSearchLocked" class="hidden sm:inline">搜索</span>
|
||||
<span v-else-if="isSearchLocked && !searchStore.isSearching" class="hidden sm:inline">稍候...</span>
|
||||
<span v-else class="hidden sm:inline">{{ searchStore.searchProgress.current }}/{{ searchStore.searchProgress.total }}</span>
|
||||
</button>
|
||||
<!-- 输入框容器 -->
|
||||
<div class="search-box relative flex items-center rounded-2xl overflow-hidden">
|
||||
<!-- 进度填充层 - 输入框本身就是进度条 -->
|
||||
<div
|
||||
v-if="searchStore.isSearching"
|
||||
class="search-progress-fill absolute inset-0 z-0 pointer-events-none
|
||||
bg-gradient-to-r from-[#ff1493]/20 via-[#d946ef]/15 to-[#ff69b4]/20
|
||||
dark:from-[#ff1493]/25 dark:via-[#d946ef]/20 dark:to-[#ff69b4]/25"
|
||||
:style="{
|
||||
clipPath: `inset(0 ${100 - (searchStore.searchProgress.total > 0 ? (searchStore.searchProgress.current / searchStore.searchProgress.total) * 100 : 0)}% 0 0)`
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- 搜索图标 / 加载动画 -->
|
||||
<div class="absolute left-4 sm:left-5 z-20 pointer-events-none">
|
||||
<component
|
||||
:is="searchStore.isSearching ? Loader2 : Search"
|
||||
:size="22"
|
||||
:class="[
|
||||
searchStore.isSearching
|
||||
? 'text-[#ff1493] dark:text-[#ff69b4] animate-spin'
|
||||
: 'text-[#ff1493]/50 dark:text-[#ff69b4]/60 group-hover:text-[#ff1493]/70 dark:group-hover:text-[#ff69b4]/80 group-focus-within:text-[#ff1493] dark:group-focus-within:text-[#ff69b4] group-focus-within:scale-110',
|
||||
'transition-all duration-300'
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
:placeholder="searchMode === 'game' ? '搜索游戏...' : '搜索补丁...'"
|
||||
:disabled="searchStore.isSearching"
|
||||
required
|
||||
class="search-input relative z-10 w-full pl-12 sm:pl-14 pr-14 sm:pr-20 py-4 sm:py-5
|
||||
text-base sm:text-lg rounded-2xl
|
||||
text-gray-800 dark:text-slate-100
|
||||
placeholder:text-gray-400/80 dark:placeholder:text-slate-400/70
|
||||
glassmorphism-input
|
||||
transition-all duration-300 outline-none font-medium
|
||||
tracking-wide
|
||||
disabled:cursor-not-allowed"
|
||||
:class="{ 'bg-transparent!': searchStore.isSearching }"
|
||||
@keydown.enter.prevent="triggerSearch"
|
||||
/>
|
||||
|
||||
<!-- 右侧:回车提示 / 进度指示 -->
|
||||
<div class="absolute right-3 sm:right-4 z-20 flex items-center">
|
||||
<!-- 搜索时显示进度 -->
|
||||
<span
|
||||
v-if="searchStore.isSearching"
|
||||
class="text-sm font-bold text-[#ff1493] dark:text-[#ff69b4] tabular-nums"
|
||||
>
|
||||
{{ searchStore.searchProgress.current }}/{{ searchStore.searchProgress.total }}
|
||||
</span>
|
||||
|
||||
<!-- 非搜索时显示回车提示 -->
|
||||
<kbd
|
||||
v-else
|
||||
class="enter-hint hidden sm:inline-flex items-center gap-1.5
|
||||
px-2.5 py-1.5 rounded-lg text-xs font-medium
|
||||
bg-gray-100/80 dark:bg-slate-700/60
|
||||
text-gray-500 dark:text-slate-400
|
||||
border border-gray-200/50 dark:border-slate-600/50
|
||||
group-focus-within:bg-[#ff1493]/10 group-focus-within:text-[#ff1493]
|
||||
dark:group-focus-within:bg-[#ff69b4]/15 dark:group-focus-within:text-[#ff69b4]
|
||||
group-focus-within:border-[#ff1493]/30 dark:group-focus-within:border-[#ff69b4]/30
|
||||
transition-all duration-200"
|
||||
>
|
||||
<CornerDownLeft :size="14" />
|
||||
<span>Enter</span>
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Mode Selector - 使用 Tailwind 胶囊开关 -->
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<div
|
||||
class="relative flex p-1.5 rounded-full glassmorphism-mode-switch"
|
||||
>
|
||||
<!-- Search Mode Selector - 现代胶囊切换器 -->
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="mode-switch relative flex p-1 rounded-2xl glassmorphism-mode-switch">
|
||||
<!-- 滑动背景指示器 -->
|
||||
<div
|
||||
class="absolute top-1.5 bottom-1.5 rounded-full
|
||||
bg-gradient-pink
|
||||
shadow-md shadow-theme-primary/30
|
||||
transition-all duration-300 ease-out"
|
||||
class="mode-indicator absolute top-1 bottom-1 rounded-xl
|
||||
bg-gradient-to-r from-[#ff1493] to-[#d946ef]
|
||||
shadow-lg shadow-[#ff1493]/30
|
||||
transition-all duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)]"
|
||||
:style="{
|
||||
left: searchMode === 'game' ? '6px' : 'calc(50% + 2px)',
|
||||
width: 'calc(50% - 8px)'
|
||||
left: searchMode === 'game' ? '4px' : 'calc(50% + 0px)',
|
||||
width: 'calc(50% - 4px)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- 游戏按钮 -->
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 px-6 py-2 rounded-full font-semibold
|
||||
class="mode-btn relative z-10 px-5 sm:px-7 py-2.5 rounded-xl font-semibold
|
||||
transition-all duration-300
|
||||
flex items-center gap-2 text-sm whitespace-nowrap"
|
||||
flex items-center gap-2.5 text-sm whitespace-nowrap"
|
||||
:class="searchMode === 'game'
|
||||
? 'text-white drop-shadow-md'
|
||||
: 'text-gray-700 dark:text-slate-300 hover:text-theme-primary dark:hover:text-theme-accent'"
|
||||
? 'text-white'
|
||||
: 'text-gray-600 dark:text-slate-400 hover:text-[#ff1493] dark:hover:text-[#ff69b4]'"
|
||||
@click="setSearchMode('game')"
|
||||
>
|
||||
<Gamepad2 :size="18" />
|
||||
<Gamepad2
|
||||
:size="18"
|
||||
:class="searchMode === 'game' ? 'drop-shadow-[0_1px_2px_rgba(0,0,0,0.3)]' : ''"
|
||||
/>
|
||||
<span>游戏</span>
|
||||
</button>
|
||||
|
||||
<!-- 补丁按钮 -->
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 px-6 py-2 rounded-full font-semibold
|
||||
class="mode-btn relative z-10 px-5 sm:px-7 py-2.5 rounded-xl font-semibold
|
||||
transition-all duration-300
|
||||
flex items-center gap-2 text-sm whitespace-nowrap"
|
||||
flex items-center gap-2.5 text-sm whitespace-nowrap"
|
||||
:class="searchMode === 'patch'
|
||||
? 'text-white drop-shadow-md'
|
||||
: 'text-gray-700 dark:text-slate-300 hover:text-theme-primary dark:hover:text-theme-accent'"
|
||||
? 'text-white'
|
||||
: 'text-gray-600 dark:text-slate-400 hover:text-[#ff1493] dark:hover:text-[#ff69b4]'"
|
||||
@click="setSearchMode('patch')"
|
||||
>
|
||||
<Wrench :size="18" />
|
||||
<Wrench
|
||||
:size="18"
|
||||
:class="searchMode === 'patch' ? 'drop-shadow-[0_1px_2px_rgba(0,0,0,0.3)]' : ''"
|
||||
/>
|
||||
<span>补丁</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -451,6 +495,8 @@ import {
|
||||
WifiOff,
|
||||
Clock,
|
||||
Server,
|
||||
Loader2,
|
||||
CornerDownLeft,
|
||||
} from 'lucide-vue-next'
|
||||
import { getSearchParamsFromURL, updateURLParams, onURLParamsChange } from '@/utils/urlParams'
|
||||
import { saveSearchHistory } from '@/utils/persistence'
|
||||
@@ -977,12 +1023,14 @@ defineExpose({
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(255, 20, 147, 0.05);
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
/* GPU 加速 */
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.shortcut-item:hover {
|
||||
background: rgba(255, 20, 147, 0.1);
|
||||
transform: translateY(-1px);
|
||||
transform: translate3d(0, -1px, 0);
|
||||
}
|
||||
|
||||
.dark .shortcut-item {
|
||||
@@ -1064,4 +1112,179 @@ defineExpose({
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
搜索输入框增强样式
|
||||
============================================ */
|
||||
|
||||
/* 搜索输入框 - 隐藏默认的清除按钮和搜索图标 */
|
||||
.search-input::-webkit-search-cancel-button,
|
||||
.search-input::-webkit-search-decoration,
|
||||
.search-input::-webkit-search-results-button,
|
||||
.search-input::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* 搜索框容器 */
|
||||
.search-box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 进度填充层 - 输入框即进度条 */
|
||||
.search-progress-fill {
|
||||
transition: clip-path 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 搜索中状态 - 输入框整体效果 */
|
||||
.search-input-wrapper.is-searching .search-box {
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(255, 20, 147, 0.4),
|
||||
0 0 25px rgba(255, 20, 147, 0.2),
|
||||
0 0 50px rgba(255, 20, 147, 0.1);
|
||||
}
|
||||
|
||||
.dark .search-input-wrapper.is-searching .search-box {
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(255, 105, 180, 0.5),
|
||||
0 0 25px rgba(255, 105, 180, 0.25),
|
||||
0 0 50px rgba(255, 105, 180, 0.15);
|
||||
}
|
||||
|
||||
/* 搜索中输入框透明背景 */
|
||||
.search-input-wrapper.is-searching .glassmorphism-input {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 搜索框基础背景 - 搜索时显示 */
|
||||
.search-input-wrapper.is-searching .search-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.85) 0%,
|
||||
rgba(255, 228, 242, 0.75) 100%
|
||||
);
|
||||
backdrop-filter: blur(12px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dark .search-input-wrapper.is-searching .search-box::before {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(30, 41, 59, 0.9) 0%,
|
||||
rgba(51, 65, 85, 0.85) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* 模式切换指示器动画 */
|
||||
.mode-indicator {
|
||||
will-change: left, width;
|
||||
}
|
||||
|
||||
/* 模式按钮点击反馈 */
|
||||
.mode-btn:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* 输入框聚焦时的边框动画 */
|
||||
.search-input-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 1rem;
|
||||
padding: 2px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
transparent 0%,
|
||||
rgba(255, 20, 147, 0.4) 25%,
|
||||
rgba(217, 70, 239, 0.4) 50%,
|
||||
rgba(255, 105, 180, 0.4) 75%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input-wrapper:focus-within::after {
|
||||
opacity: 1;
|
||||
animation: borderRotate 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes borderRotate {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 模式切换按钮 hover 效果 */
|
||||
.mode-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mode-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 0.75rem;
|
||||
background: linear-gradient(135deg, rgba(255, 20, 147, 0.1), rgba(217, 70, 239, 0.05));
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mode-btn:not(.active):hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 640px) {
|
||||
.search-input-wrapper {
|
||||
/* 确保触摸目标足够大 */
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动效模式 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.search-input-wrapper::after,
|
||||
.search-btn::before {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.mode-indicator {
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div v-if="searchStore.hasResults" class="w-full px-2 sm:px-4 md:px-6 py-4 sm:py-6 md:py-8 animate-fade-in">
|
||||
<div id="results" class="max-w-5xl mx-auto space-y-4 sm:space-y-6">
|
||||
<!-- 使用 v-memo 优化平台卡片渲染,仅在关键数据变化时重新渲染 -->
|
||||
<div
|
||||
v-for="[platformName, platformData] in searchStore.platformResults"
|
||||
:key="platformName"
|
||||
v-memo="[platformName, platformData.items.length, platformData.displayedCount, platformData.error]"
|
||||
:data-platform="platformName"
|
||||
class="result-card glassmorphism-card rounded-2xl shadow-xl hover:shadow-2xl transition-all duration-300 animate-fade-in-up border-2"
|
||||
class="result-card glassmorphism-card rounded-2xl shadow-xl hover:shadow-2xl transition-shadow duration-300 animate-fade-in-up border-2 content-auto"
|
||||
:class="getBorderClass(platformData.color)"
|
||||
>
|
||||
<div class="p-4 sm:p-5 md:p-6">
|
||||
@@ -74,18 +76,18 @@
|
||||
<span class="text-red-700 dark:text-red-300 font-medium">{{ platformData.error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<div v-if="getDisplayedResults(platformData).length > 0" class="results-list space-y-2">
|
||||
<!-- 搜索结果列表 - 使用 contain 优化布局性能 -->
|
||||
<div v-if="getDisplayedResults(platformData).length > 0" class="results-list space-y-2 contain-layout">
|
||||
<div
|
||||
v-for="(result, index) in getDisplayedResults(platformData)"
|
||||
:key="index"
|
||||
:key="result.url || index"
|
||||
class="result-item group p-3 sm:p-4 rounded-xl
|
||||
bg-gradient-to-r from-white/70 to-white/50 dark:from-slate-700/70 dark:to-slate-700/50
|
||||
hover:from-white/90 hover:to-white/70 dark:hover:from-slate-700/90 dark:hover:to-slate-700/70
|
||||
border border-gray-200/50 dark:border-slate-600/50
|
||||
hover:border-theme-primary/40 dark:hover:border-theme-accent/40
|
||||
hover:shadow-lg hover:shadow-theme-primary/10 dark:hover:shadow-theme-accent/15
|
||||
transition-all duration-300"
|
||||
transition-colors duration-200"
|
||||
>
|
||||
<!-- 标题行 -->
|
||||
<div class="flex items-start gap-2 sm:gap-3">
|
||||
@@ -344,19 +346,31 @@ function getTagLabel(tag: string) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 平台卡片 - 延迟渲染优化 */
|
||||
.result-card {
|
||||
animation-delay: calc(var(--index, 0) * 0.1s);
|
||||
/* content-visibility 自动延迟屏幕外内容渲染 */
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 400px;
|
||||
}
|
||||
|
||||
/* 结果项 - 仅使用 transform 动画(GPU 加速) */
|
||||
.result-item {
|
||||
transition: all 0.2s ease;
|
||||
/* 使用 translate3d 强制 GPU 层 */
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.2s ease-out, background 0.2s ease-out, border-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
transform: translateX(4px);
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
|
||||
/* Tailwind 动画 */
|
||||
/* 结果列表布局隔离 */
|
||||
.results-list {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* Tailwind 动画 - 优化为仅使用 transform 和 opacity */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
@@ -381,11 +395,11 @@ function getTagLabel(tag: string) {
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transform: translate3d(0, 20px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -200,14 +200,14 @@ async function saveBackgroundImage() {
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
|
||||
/* 液态玻璃效果 - 艳粉主题 */
|
||||
/* 液态玻璃效果 - 艳粉主题(降低 blur 提升性能) */
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.85) 0%,
|
||||
rgba(255, 228, 242, 0.7) 100%
|
||||
);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
backdrop-filter: blur(12px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
||||
|
||||
/* 艳粉边框和阴影 */
|
||||
border: 1.5px solid rgba(255, 20, 147, 0.2);
|
||||
@@ -221,8 +221,14 @@ async function saveBackgroundImage() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
user-select: none;
|
||||
|
||||
/* 性能优化:GPU 加速 + 只过渡 transform 和 opacity */
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s ease-out,
|
||||
border-color 0.3s ease-out;
|
||||
contain: layout paint;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@@ -246,8 +252,8 @@ async function saveBackgroundImage() {
|
||||
rgba(51, 65, 85, 0.85) 0%,
|
||||
rgba(30, 41, 59, 0.75) 100%
|
||||
);
|
||||
backdrop-filter: blur(20px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
||||
backdrop-filter: blur(12px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
||||
|
||||
border: 1.5px solid rgba(255, 105, 180, 0.3);
|
||||
box-shadow:
|
||||
@@ -259,12 +265,8 @@ async function saveBackgroundImage() {
|
||||
}
|
||||
|
||||
.toolbar-button:hover {
|
||||
transform: scale(1.05) translateY(-2px);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.95) 0%,
|
||||
rgba(255, 228, 242, 0.85) 100%
|
||||
);
|
||||
/* 使用 translate3d 保持 GPU 层 */
|
||||
transform: translate3d(0, -2px, 0) scale(1.05);
|
||||
box-shadow:
|
||||
0 12px 24px rgba(255, 20, 147, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.9) inset,
|
||||
@@ -274,11 +276,6 @@ async function saveBackgroundImage() {
|
||||
}
|
||||
|
||||
.dark .toolbar-button:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(51, 65, 85, 0.95) 0%,
|
||||
rgba(30, 41, 59, 0.85) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 12px 24px rgba(255, 105, 180, 0.3),
|
||||
0 0 0 1px rgba(255, 105, 180, 0.2) inset,
|
||||
@@ -288,7 +285,7 @@ async function saveBackgroundImage() {
|
||||
}
|
||||
|
||||
.toolbar-button:active {
|
||||
transform: scale(0.95);
|
||||
transform: translate3d(0, 0, 0) scale(0.95);
|
||||
}
|
||||
|
||||
/* GitHub 按钮特殊样式 */
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* 全局点击特效 composable
|
||||
* 在屏幕任意位置点击都会显示涟漪特效
|
||||
*
|
||||
* 性能优化:
|
||||
* 1. 使用对象池复用 DOM 元素,避免频繁创建/销毁
|
||||
* 2. 使用 CSS 变量而非 inline style,减少样式计算
|
||||
* 3. 使用 requestAnimationFrame 批量处理
|
||||
*/
|
||||
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
@@ -23,42 +28,68 @@ const defaultOptions: ClickEffectOptions = {
|
||||
let isInitialized = false
|
||||
let currentOptions = { ...defaultOptions }
|
||||
|
||||
// 创建点击特效
|
||||
// 对象池 - 复用 DOM 元素
|
||||
const effectPool: HTMLDivElement[] = []
|
||||
const POOL_SIZE = 10 // 最大池大小
|
||||
const activeEffects = new Set<HTMLDivElement>()
|
||||
|
||||
// 从池中获取或创建元素
|
||||
function getEffectElement(): HTMLDivElement {
|
||||
let effect = effectPool.pop()
|
||||
|
||||
if (!effect) {
|
||||
effect = document.createElement('div')
|
||||
effect.className = 'global-click-effect'
|
||||
}
|
||||
|
||||
activeEffects.add(effect)
|
||||
return effect
|
||||
}
|
||||
|
||||
// 回收元素到池中
|
||||
function recycleEffect(effect: HTMLDivElement) {
|
||||
activeEffects.delete(effect)
|
||||
|
||||
// 重置样式
|
||||
effect.classList.remove('effect-active')
|
||||
|
||||
// 从 DOM 移除
|
||||
if (effect.parentNode) {
|
||||
effect.parentNode.removeChild(effect)
|
||||
}
|
||||
|
||||
// 如果池未满,回收元素
|
||||
if (effectPool.length < POOL_SIZE) {
|
||||
effectPool.push(effect)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建点击特效 - 优化版本
|
||||
function createClickEffect(x: number, y: number) {
|
||||
if (!currentOptions.enabled) {return}
|
||||
|
||||
const effect = document.createElement('div')
|
||||
effect.className = 'global-click-effect'
|
||||
|
||||
const effect = getEffectElement()
|
||||
const size = currentOptions.size || 100
|
||||
const halfSize = size / 2
|
||||
|
||||
effect.style.cssText = `
|
||||
position: fixed;
|
||||
left: ${x - size / 2}px;
|
||||
top: ${y - size / 2}px;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
pointer-events: none;
|
||||
z-index: 99999;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, ${currentOptions.color} 0%, transparent 70%);
|
||||
transform: scale(0);
|
||||
animation: global-click-ripple ${currentOptions.duration}ms ease-out forwards;
|
||||
`
|
||||
|
||||
// 使用 CSS 变量设置位置和大小
|
||||
effect.style.setProperty('--effect-x', `${x - halfSize}px`)
|
||||
effect.style.setProperty('--effect-y', `${y - halfSize}px`)
|
||||
effect.style.setProperty('--effect-size', `${size}px`)
|
||||
|
||||
// 添加到 DOM 并触发动画
|
||||
document.body.appendChild(effect)
|
||||
|
||||
// 动画结束后移除
|
||||
effect.addEventListener('animationend', () => {
|
||||
effect.remove()
|
||||
|
||||
// 使用 requestAnimationFrame 确保样式已应用
|
||||
requestAnimationFrame(() => {
|
||||
effect.classList.add('effect-active')
|
||||
})
|
||||
|
||||
// 备用移除(防止动画事件未触发)
|
||||
// 动画结束后回收
|
||||
const duration = currentOptions.duration || 600
|
||||
setTimeout(() => {
|
||||
if (effect.parentNode) {
|
||||
effect.remove()
|
||||
}
|
||||
}, (currentOptions.duration || 600) + 100)
|
||||
recycleEffect(effect)
|
||||
}, duration + 50)
|
||||
}
|
||||
|
||||
// 处理点击事件
|
||||
@@ -75,43 +106,48 @@ function handleTouch(event: TouchEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// 注入全局样式
|
||||
// 注入全局样式 - 优化版本,使用 CSS 变量
|
||||
function injectStyles() {
|
||||
if (document.getElementById('global-click-effect-styles')) {return}
|
||||
|
||||
const style = document.createElement('style')
|
||||
style.id = 'global-click-effect-styles'
|
||||
style.textContent = `
|
||||
@keyframes global-click-ripple {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
.global-click-effect {
|
||||
position: fixed;
|
||||
left: var(--effect-x, 0);
|
||||
top: var(--effect-y, 0);
|
||||
width: var(--effect-size, 100px);
|
||||
height: var(--effect-size, 100px);
|
||||
pointer-events: none;
|
||||
z-index: 99999;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255, 20, 147, 0.4) 0%, transparent 70%);
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
mix-blend-mode: screen;
|
||||
will-change: transform, opacity;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.global-click-effect {
|
||||
mix-blend-mode: screen;
|
||||
.global-click-effect.effect-active {
|
||||
animation: global-click-ripple 600ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.dark .global-click-effect {
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
/* 额外的粒子效果 */
|
||||
@keyframes particle-fly {
|
||||
@keyframes global-click-ripple {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: translate(var(--tx), var(--ty)) scale(0);
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -140,6 +176,17 @@ function destroyClickEffect() {
|
||||
document.removeEventListener('mousedown', handleClick)
|
||||
document.removeEventListener('touchstart', handleTouch)
|
||||
|
||||
// 清理所有活动特效
|
||||
activeEffects.forEach(effect => {
|
||||
if (effect.parentNode) {
|
||||
effect.parentNode.removeChild(effect)
|
||||
}
|
||||
})
|
||||
activeEffects.clear()
|
||||
|
||||
// 清空对象池
|
||||
effectPool.length = 0
|
||||
|
||||
const styles = document.getElementById('global-click-effect-styles')
|
||||
if (styles) {
|
||||
styles.remove()
|
||||
|
||||
187
src/composables/usePerformance.ts
Normal file
187
src/composables/usePerformance.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 性能优化 Composable
|
||||
* 提供统一的性能优化工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 使用 requestIdleCallback 调度低优先级任务
|
||||
* 带有降级回退支持
|
||||
*/
|
||||
export function scheduleIdleTask(
|
||||
callback: IdleRequestCallback,
|
||||
options?: IdleRequestOptions,
|
||||
): number {
|
||||
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
|
||||
return window.requestIdleCallback(callback, options)
|
||||
}
|
||||
// 降级到 setTimeout,模拟空闲时执行
|
||||
return setTimeout(() => {
|
||||
callback({
|
||||
didTimeout: false,
|
||||
timeRemaining: () => 50,
|
||||
})
|
||||
}, 16) as unknown as number
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消空闲任务
|
||||
*/
|
||||
export function cancelIdleTask(handle: number): void {
|
||||
if (typeof window !== 'undefined' && 'cancelIdleCallback' in window) {
|
||||
window.cancelIdleCallback(handle)
|
||||
} else {
|
||||
clearTimeout(handle)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 requestAnimationFrame 调度动画帧任务
|
||||
* 返回 Promise 便于 async/await 使用
|
||||
*/
|
||||
export function nextFrame(): Promise<DOMHighResTimeStamp> {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待多个动画帧(用于确保 DOM 更新完成)
|
||||
*/
|
||||
export async function waitFrames(count = 2): Promise<void> {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await nextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测用户是否偏好减少动效
|
||||
*/
|
||||
export function prefersReducedMotion(): boolean {
|
||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测设备是否为低性能设备
|
||||
* 基于 hardwareConcurrency 和 deviceMemory
|
||||
*/
|
||||
export function isLowEndDevice(): boolean {
|
||||
const nav = navigator as Navigator & {
|
||||
deviceMemory?: number
|
||||
connection?: { effectiveType?: string; saveData?: boolean }
|
||||
}
|
||||
|
||||
// 检测 CPU 核心数
|
||||
const cores = navigator.hardwareConcurrency || 4
|
||||
if (cores <= 2) {return true}
|
||||
|
||||
// 检测设备内存(Chrome 特性)
|
||||
if (nav.deviceMemory && nav.deviceMemory <= 2) {return true}
|
||||
|
||||
// 检测网络连接类型
|
||||
if (nav.connection) {
|
||||
if (nav.connection.saveData) {return true}
|
||||
if (nav.connection.effectiveType === 'slow-2g' ||
|
||||
nav.connection.effectiveType === '2g') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建节流的滚动处理函数
|
||||
* 使用 requestAnimationFrame 优化
|
||||
*/
|
||||
export function createScrollHandler(
|
||||
callback: (scrollY: number) => void,
|
||||
): () => void {
|
||||
let ticking = false
|
||||
let lastScrollY = 0
|
||||
|
||||
const handler = () => {
|
||||
lastScrollY = window.scrollY
|
||||
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
callback(lastScrollY)
|
||||
ticking = false
|
||||
})
|
||||
ticking = true
|
||||
}
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 resize 观察器
|
||||
* 使用 ResizeObserver 替代 resize 事件
|
||||
*/
|
||||
export function createResizeObserver(
|
||||
element: Element,
|
||||
callback: (entry: ResizeObserverEntry) => void,
|
||||
): ResizeObserver {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
callback(entry)
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(element)
|
||||
return observer
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载图片
|
||||
*/
|
||||
export function preloadImage(src: string): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = src
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载多张图片(并发限制)
|
||||
*/
|
||||
export async function preloadImages(
|
||||
srcs: string[],
|
||||
concurrency = 3,
|
||||
): Promise<HTMLImageElement[]> {
|
||||
const results: HTMLImageElement[] = []
|
||||
const queue = [...srcs]
|
||||
|
||||
const worker = async () => {
|
||||
while (queue.length > 0) {
|
||||
const src = queue.shift()
|
||||
if (src) {
|
||||
try {
|
||||
const img = await preloadImage(src)
|
||||
results.push(img)
|
||||
} catch {
|
||||
// 静默处理失败的图片
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并发 worker
|
||||
const workers = Array.from({ length: concurrency }, () => worker())
|
||||
await Promise.all(workers)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟执行(用于非关键任务)
|
||||
*/
|
||||
export function defer<T>(fn: () => T, delay = 0): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(fn())
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export const useSearchStore = defineStore('search', () => {
|
||||
})
|
||||
const totalResults = computed(() =>
|
||||
Array.from(platformResults.value.values())
|
||||
.reduce((sum, platform) => sum + platform.items.length, 0)
|
||||
.reduce((sum, platform) => sum + platform.items.length, 0),
|
||||
)
|
||||
|
||||
// 方法
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
/* ============================================
|
||||
性能优化 - 全局渲染提示
|
||||
============================================ */
|
||||
|
||||
/* 启用硬件加速的根元素 */
|
||||
html {
|
||||
/* 文本渲染优化 */
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* body 基础样式 */
|
||||
body {
|
||||
/* 字体栈 - Google Noto Sans SC + Apple System 备用 */
|
||||
@@ -72,5 +84,151 @@
|
||||
.lozad[data-loaded="true"] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
性能优化工具类
|
||||
============================================ */
|
||||
|
||||
/* GPU 加速层 - 用于需要频繁重绘的元素 */
|
||||
.gpu-layer {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
/* 隔离渲染层 - 防止布局抖动影响其他元素 */
|
||||
.contain-layout {
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.contain-paint {
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
.contain-strict {
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.contain-content {
|
||||
contain: content;
|
||||
}
|
||||
|
||||
/* 内容可见性优化 - 延迟渲染屏幕外内容 */
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 500px;
|
||||
}
|
||||
|
||||
/* 高性能动画 - 仅使用 transform 和 opacity */
|
||||
.perf-animate {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* 滚动性能优化 */
|
||||
.scroll-contain {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* 触摸优化 */
|
||||
.touch-optimize {
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 减少重绘的过渡动画 */
|
||||
.transition-gpu {
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
响应式性能优化
|
||||
============================================ */
|
||||
|
||||
/* 减少动效偏好 - 尊重用户系统设置 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 低数据模式 - 减少动画复杂度 */
|
||||
@media (prefers-reduced-data: reduce) {
|
||||
.gpu-layer {
|
||||
transform: none;
|
||||
perspective: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
批量动画延迟 - 用于列表项交错动画
|
||||
============================================ */
|
||||
|
||||
.stagger-1 { animation-delay: 50ms; }
|
||||
.stagger-2 { animation-delay: 100ms; }
|
||||
.stagger-3 { animation-delay: 150ms; }
|
||||
.stagger-4 { animation-delay: 200ms; }
|
||||
.stagger-5 { animation-delay: 250ms; }
|
||||
|
||||
/* ============================================
|
||||
渲染优化提示
|
||||
============================================ */
|
||||
|
||||
/* 仅在 hover 时激活 will-change(节省 GPU 内存) */
|
||||
.will-change-hover:hover {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* 固定元素渲染隔离 */
|
||||
.fixed-layer {
|
||||
position: fixed;
|
||||
contain: layout paint;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* 图片渲染优化 */
|
||||
.img-optimize {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
/* 高质量图片渲染 */
|
||||
.img-quality {
|
||||
image-rendering: smooth;
|
||||
image-rendering: high-quality;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
交互优化
|
||||
============================================ */
|
||||
|
||||
/* 防止双击缩放(移动端) */
|
||||
.no-zoom {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* 硬件加速滚动 */
|
||||
.scroll-smooth-gpu {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* 可拖拽区域优化 */
|
||||
.draggable {
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.draggable:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
saturate(var(--glass-saturate))
|
||||
brightness(var(--glass-brightness));
|
||||
border: 1px solid var(--glass-border-light);
|
||||
will-change: backdrop-filter;
|
||||
}
|
||||
|
||||
.dark .glass {
|
||||
@@ -61,67 +60,108 @@
|
||||
.glassmorphism-input {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.75) 0%,
|
||||
rgba(var(--glass-pink-pale), 0.55) 100%
|
||||
rgba(255, 255, 255, 0.88) 0%,
|
||||
rgba(255, 240, 248, 0.75) 50%,
|
||||
rgba(255, 228, 242, 0.65) 100%
|
||||
);
|
||||
backdrop-filter:
|
||||
blur(var(--glass-blur))
|
||||
saturate(var(--glass-saturate))
|
||||
brightness(var(--glass-brightness));
|
||||
blur(16px)
|
||||
saturate(200%)
|
||||
brightness(1.08);
|
||||
-webkit-backdrop-filter:
|
||||
blur(var(--glass-blur))
|
||||
saturate(var(--glass-saturate))
|
||||
brightness(var(--glass-brightness));
|
||||
border: 1.5px solid var(--glass-border-light);
|
||||
blur(16px)
|
||||
saturate(200%)
|
||||
brightness(1.08);
|
||||
border: 2px solid rgba(var(--glass-pink), 0.15);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--glass-pink), 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
transition: all 0.25s ease-out;
|
||||
will-change: transform, box-shadow;
|
||||
0 4px 24px rgba(var(--glass-pink), 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.04),
|
||||
inset 0 2px 0 rgba(255, 255, 255, 0.9),
|
||||
inset 0 -1px 0 rgba(var(--glass-pink), 0.05);
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
box-shadow 0.3s ease-out,
|
||||
border-color 0.3s ease-out,
|
||||
background 0.3s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-input:hover {
|
||||
border-color: var(--glass-border-light-hover);
|
||||
border-color: rgba(var(--glass-pink), 0.25);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.92) 0%,
|
||||
rgba(255, 240, 248, 0.82) 50%,
|
||||
rgba(255, 228, 242, 0.72) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(var(--glass-pink), 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
0 8px 32px rgba(var(--glass-pink), 0.12),
|
||||
0 2px 4px rgba(0, 0, 0, 0.04),
|
||||
inset 0 2px 0 rgba(255, 255, 255, 1),
|
||||
inset 0 -1px 0 rgba(var(--glass-pink), 0.08);
|
||||
}
|
||||
|
||||
.glassmorphism-input:focus {
|
||||
border-color: rgba(var(--glass-pink), 0.35);
|
||||
border-color: rgba(var(--glass-pink), 0.45);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.95) 0%,
|
||||
rgba(255, 245, 250, 0.88) 50%,
|
||||
rgba(255, 235, 245, 0.8) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(var(--glass-pink), 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 1),
|
||||
0 0 0 3px rgba(var(--glass-pink), 0.08);
|
||||
transform: scale(1.005);
|
||||
0 12px 40px rgba(var(--glass-pink), 0.18),
|
||||
0 4px 8px rgba(0, 0, 0, 0.04),
|
||||
0 0 0 4px rgba(var(--glass-pink), 0.1),
|
||||
inset 0 2px 0 rgba(255, 255, 255, 1),
|
||||
inset 0 -1px 0 rgba(var(--glass-pink), 0.1);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.dark .glassmorphism-input {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(30, 41, 59, 0.82) 0%,
|
||||
rgba(51, 65, 85, 0.72) 100%
|
||||
rgba(30, 41, 59, 0.88) 0%,
|
||||
rgba(45, 55, 75, 0.78) 50%,
|
||||
rgba(55, 65, 85, 0.7) 100%
|
||||
);
|
||||
border-color: var(--glass-border-dark);
|
||||
border: 2px solid rgba(var(--glass-pink-light), 0.2);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--glass-pink-light), 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
0 4px 24px rgba(var(--glass-pink-light), 0.15),
|
||||
0 1px 2px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
inset 0 -1px 0 rgba(var(--glass-pink-light), 0.1);
|
||||
}
|
||||
|
||||
.dark .glassmorphism-input:hover {
|
||||
border-color: var(--glass-border-dark-hover);
|
||||
border-color: rgba(var(--glass-pink-light), 0.3);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(35, 46, 64, 0.92) 0%,
|
||||
rgba(50, 60, 80, 0.82) 50%,
|
||||
rgba(60, 70, 90, 0.75) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(var(--glass-pink-light), 0.18),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
0 8px 36px rgba(var(--glass-pink-light), 0.2),
|
||||
0 2px 4px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(var(--glass-pink-light), 0.12);
|
||||
}
|
||||
|
||||
.dark .glassmorphism-input:focus {
|
||||
border-color: rgba(var(--glass-pink-light), 0.45);
|
||||
border-color: rgba(var(--glass-pink-light), 0.5);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(40, 51, 69, 0.95) 0%,
|
||||
rgba(55, 65, 85, 0.88) 50%,
|
||||
rgba(65, 75, 95, 0.8) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(var(--glass-pink-light), 0.25),
|
||||
0 12px 48px rgba(var(--glass-pink-light), 0.28),
|
||||
0 4px 8px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 4px rgba(var(--glass-pink-light), 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||
0 0 0 3px rgba(var(--glass-pink-light), 0.12);
|
||||
transform: scale(1.005);
|
||||
inset 0 -1px 0 rgba(var(--glass-pink-light), 0.15);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
/* ========== 搜索按钮玻璃效果 ========== */
|
||||
@@ -141,8 +181,7 @@
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--glass-pink), 0.18),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
transition: all 0.2s ease-out;
|
||||
will-change: transform, box-shadow;
|
||||
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, border-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-button:hover:not(:disabled) {
|
||||
@@ -209,8 +248,7 @@
|
||||
box-shadow:
|
||||
0 2px 8px rgba(var(--glass-pink), 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
transition: all 0.2s ease-out;
|
||||
will-change: transform;
|
||||
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, border-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-search-button:hover:not(:disabled) {
|
||||
@@ -254,46 +292,52 @@
|
||||
.glassmorphism-mode-switch {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.65) 0%,
|
||||
rgba(var(--glass-pink-pale), 0.5) 100%
|
||||
rgba(255, 255, 255, 0.82) 0%,
|
||||
rgba(255, 245, 250, 0.72) 50%,
|
||||
rgba(255, 235, 245, 0.65) 100%
|
||||
);
|
||||
backdrop-filter:
|
||||
blur(var(--glass-blur))
|
||||
saturate(var(--glass-saturate));
|
||||
blur(14px)
|
||||
saturate(180%);
|
||||
-webkit-backdrop-filter:
|
||||
blur(var(--glass-blur))
|
||||
saturate(var(--glass-saturate));
|
||||
border: 1.5px solid var(--glass-border-light);
|
||||
blur(14px)
|
||||
saturate(180%);
|
||||
border: 2px solid rgba(var(--glass-pink), 0.12);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--glass-pink), 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
transition: all 0.25s ease-out;
|
||||
0 4px 20px rgba(var(--glass-pink), 0.1),
|
||||
0 1px 2px rgba(0, 0, 0, 0.03),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-mode-switch:hover {
|
||||
border-color: var(--glass-border-light-hover);
|
||||
border-color: rgba(var(--glass-pink), 0.2);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(var(--glass-pink), 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.95);
|
||||
0 6px 24px rgba(var(--glass-pink), 0.15),
|
||||
0 2px 4px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.dark .glassmorphism-mode-switch {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(30, 41, 59, 0.78) 0%,
|
||||
rgba(51, 65, 85, 0.68) 100%
|
||||
rgba(30, 41, 59, 0.85) 0%,
|
||||
rgba(45, 55, 75, 0.75) 50%,
|
||||
rgba(55, 65, 85, 0.68) 100%
|
||||
);
|
||||
border-color: var(--glass-border-dark);
|
||||
border: 2px solid rgba(var(--glass-pink-light), 0.18);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--glass-pink-light), 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
0 4px 20px rgba(var(--glass-pink-light), 0.15),
|
||||
0 1px 2px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.dark .glassmorphism-mode-switch:hover {
|
||||
border-color: var(--glass-border-dark-hover);
|
||||
border-color: rgba(var(--glass-pink-light), 0.28);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(var(--glass-pink-light), 0.18),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
0 6px 28px rgba(var(--glass-pink-light), 0.22),
|
||||
0 2px 4px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* ========== 模态框液态玻璃效果 ========== */
|
||||
@@ -316,8 +360,7 @@
|
||||
0 8px 32px rgba(var(--glass-pink), 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s ease-out;
|
||||
will-change: transform, box-shadow;
|
||||
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out, border-color 0.3s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-modal:hover {
|
||||
@@ -366,8 +409,7 @@
|
||||
0 8px 32px rgba(var(--glass-pink), 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease-out;
|
||||
will-change: transform;
|
||||
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out, border-color 0.3s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-panel:hover {
|
||||
@@ -416,8 +458,7 @@
|
||||
0 2px 8px rgba(var(--glass-pink), 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease-out;
|
||||
will-change: transform;
|
||||
transition: transform 0.2s ease-out, background 0.2s ease-out, box-shadow 0.2s ease-out, border-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-toolbar-button:hover {
|
||||
@@ -490,8 +531,7 @@
|
||||
0 4px 16px rgba(var(--glass-pink), 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
border-radius: 16px;
|
||||
transition: all 0.25s ease-out;
|
||||
will-change: transform;
|
||||
transition: transform 0.25s ease-out, box-shadow 0.25s ease-out, border-color 0.25s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-card:hover {
|
||||
@@ -539,8 +579,7 @@
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--glass-pink), 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
transition: all 0.2s ease-out;
|
||||
will-change: transform, box-shadow;
|
||||
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, border-color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glassmorphism-fab:hover {
|
||||
@@ -632,3 +671,51 @@
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 低电量模式 / 省电模式降级 */
|
||||
@media (prefers-reduced-data: reduce) {
|
||||
.glass,
|
||||
.glassmorphism-input,
|
||||
.glassmorphism-button,
|
||||
.glassmorphism-modal,
|
||||
.glassmorphism-panel,
|
||||
.glassmorphism-card,
|
||||
.glassmorphism-fab {
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 性能提示:will-change 自动管理 ========== */
|
||||
/* 仅在 hover 时激活 will-change,避免长期占用 GPU 内存 */
|
||||
.glassmorphism-card:hover,
|
||||
.glassmorphism-panel:hover,
|
||||
.glassmorphism-modal:hover,
|
||||
.glassmorphism-fab:hover,
|
||||
.glassmorphism-button:hover {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* 触摸设备的优化 */
|
||||
@media (hover: none) {
|
||||
.glassmorphism-card,
|
||||
.glassmorphism-panel,
|
||||
.glassmorphism-modal,
|
||||
.glassmorphism-fab,
|
||||
.glassmorphism-button {
|
||||
/* 触摸设备禁用 hover 状态的 will-change 以节省资源 */
|
||||
will-change: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 渲染隔离 - 防止 backdrop-filter 影响其他层 */
|
||||
.glass,
|
||||
.glassmorphism-input,
|
||||
.glassmorphism-button,
|
||||
.glassmorphism-modal,
|
||||
.glassmorphism-panel,
|
||||
.glassmorphism-card,
|
||||
.glassmorphism-fab {
|
||||
isolation: isolate;
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user