feat: 重构进度条与键盘快捷键帮助功能

* 移除 `nprogress` 依赖,替换为自定义进度条,使用 `anime.js` 实现更流畅的加载效果。
* 在 `index.html` 中优化主题检测与背景样式,提升用户体验。
* 添加键盘快捷键帮助面板,增强用户交互,支持通过快捷键显示/隐藏。
* 更新多个组件以集成新的键盘帮助功能,确保一致性和可用性。
* 优化 UI 状态管理,支持会话状态的恢复与清除,提升用户体验。
This commit is contained in:
AdingApkgg
2025-12-21 11:30:04 +08:00
parent 6e170c579c
commit c46517da8b
15 changed files with 765 additions and 356 deletions

View File

@@ -128,6 +128,37 @@ const uiStore = useUIStore()
let artalkInstance: ArtalkInstance | null = null
let isClosing = false
// 检查并滚动到指定评论
function scrollToComment() {
const hash = window.location.hash
if (!hash.startsWith('#atk-comment-')) return
// 等待 Artalk 渲染完成后滚动
const maxAttempts = 20
let attempts = 0
const tryScroll = () => {
attempts++
const targetEl = document.querySelector(hash)
if (targetEl) {
// 滚动到评论
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
// 高亮效果
targetEl.classList.add('atk-comment-highlight')
setTimeout(() => {
targetEl.classList.remove('atk-comment-highlight')
}, 2000)
} else if (attempts < maxAttempts) {
// 评论还没加载完,继续尝试
setTimeout(tryScroll, 200)
}
}
// 延迟一点开始尝试,等待 Artalk 初始化
setTimeout(tryScroll, 500)
}
// 窗口管理
const modalRef = ref<HTMLElement | null>(null)
const { isFullscreen, windowStyle, startDrag, startResize, toggleFullscreen, reset } = useWindowManager({
@@ -193,6 +224,9 @@ function initArtalk() {
site: 'Galgame 聚合搜索',
darkMode: 'auto',
})
// 尝试滚动到指定评论
scrollToComment()
} catch {
// 静默处理错误
}
@@ -353,4 +387,21 @@ onUnmounted(() => {
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #c71585, #c026d3);
}
/* 评论高亮动画 */
.atk-comment-highlight {
animation: comment-highlight 2s ease-out;
}
@keyframes comment-highlight {
0%, 50% {
background-color: rgba(255, 20, 147, 0.2);
box-shadow: 0 0 0 4px rgba(255, 20, 147, 0.3);
border-radius: 8px;
}
100% {
background-color: transparent;
box-shadow: 0 0 0 0 transparent;
}
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<Teleport to="body">
<!-- 键盘快捷键面板 -->
<Transition
:css="false"
@enter="onEnter"
@leave="onLeave"
>
<div
v-if="uiStore.isKeyboardHelpOpen"
class="fixed inset-0 z-[100] flex items-center justify-center p-4"
@click.self="close"
>
<!-- 面板 -->
<div
ref="panelRef"
class="keyboard-help-panel glassmorphism-card rounded-3xl shadow-2xl shadow-black/20 w-full max-w-md overflow-hidden"
>
<!-- 标题栏 -->
<div class="flex items-center justify-between px-5 py-4 border-b border-white/10 dark:border-slate-700/50">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-[#ff1493] to-[#d946ef] flex items-center justify-center shadow-lg shadow-pink-500/30">
<Keyboard :size="20" class="text-white" />
</div>
<div>
<h2 class="font-bold text-gray-800 dark:text-white">键盘快捷键</h2>
<p class="text-xs text-gray-500 dark:text-slate-400"> ? Esc 关闭</p>
</div>
</div>
<button
class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-500 hover:text-red-500 dark:text-gray-400 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all"
@click="close"
>
<X :size="18" />
</button>
</div>
<!-- 快捷键列表 -->
<div class="px-5 py-4 max-h-[60vh] overflow-y-auto custom-scrollbar">
<!-- 导航 -->
<div class="mb-4">
<h3 class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider mb-2">导航</h3>
<div class="space-y-2">
<div class="shortcut-row">
<span>关闭当前面板</span>
<kbd>Esc</kbd>
</div>
<div class="shortcut-row">
<span>返回首页</span>
<kbd>H</kbd>
</div>
<div class="shortcut-row">
<span>打开/关闭设置</span>
<kbd>,</kbd>
</div>
<div class="shortcut-row">
<span>打开/关闭评论</span>
<kbd>C</kbd>
</div>
<div class="shortcut-row">
<span>打开/关闭作品介绍</span>
<kbd>V</kbd>
</div>
<div class="shortcut-row">
<span>打开/关闭搜索历史</span>
<kbd>Y</kbd>
</div>
<div class="shortcut-row">
<span>站点导航</span>
<kbd>N</kbd>
</div>
</div>
</div>
<!-- 操作 -->
<div class="mb-4">
<h3 class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider mb-2">操作</h3>
<div class="space-y-2">
<div class="shortcut-row">
<span>聚焦搜索框</span>
<kbd>/</kbd>
</div>
<div class="shortcut-row">
<span>显示/隐藏快捷键帮助</span>
<kbd>?</kbd>
</div>
</div>
</div>
<!-- 滚动 -->
<div>
<h3 class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider mb-2">滚动</h3>
<div class="space-y-2">
<div class="shortcut-row">
<span>回到顶部</span>
<kbd>T</kbd>
</div>
<div class="shortcut-row">
<span>上一个平台</span>
<kbd>[</kbd>
</div>
<div class="shortcut-row">
<span>下一个平台</span>
<kbd>]</kbd>
</div>
</div>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUIStore } from '@/stores/ui'
import { animate } from '@/composables/useAnime'
import { playTransitionDown } from '@/composables/useSound'
import { Keyboard, X } from 'lucide-vue-next'
const uiStore = useUIStore()
const panelRef = ref<HTMLElement | null>(null)
function onEnter(el: Element, done: () => void) {
animate(el as HTMLElement, {
opacity: [0, 1],
scale: [0.95, 1],
duration: 200,
ease: 'outCubic',
complete: done,
})
}
function onLeave(el: Element, done: () => void) {
animate(el as HTMLElement, {
opacity: [1, 0],
scale: [1, 0.95],
duration: 150,
ease: 'inCubic',
complete: done,
})
}
function close() {
playTransitionDown()
uiStore.isKeyboardHelpOpen = false
}
</script>
<style scoped>
.keyboard-help-panel {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
}
.dark .keyboard-help-panel {
background: rgba(30, 41, 59, 0.9);
}
.shortcut-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
}
.shortcut-row span {
font-size: 0.875rem;
color: #374151;
}
.dark .shortcut-row span {
color: #cbd5e1;
}
.shortcut-row kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.75rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #ff1493, #d946ef);
border-radius: 0.5rem;
box-shadow:
0 2px 6px rgba(255, 20, 147, 0.3),
0 1px 0 rgba(255, 255, 255, 0.2) inset;
}
/* 自定义滚动条 */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 20, 147, 0.3);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 20, 147, 0.5);
}
</style>

View File

@@ -79,8 +79,21 @@
@keydown.enter.prevent="triggerSearch"
/>
<!-- 右侧回车提示 / 进度指示 -->
<div class="absolute right-3 sm:right-4 z-20 flex items-center">
<!-- 右侧清除按钮 + 回车提示 / 进度指示 -->
<div class="absolute right-3 sm:right-4 z-20 flex items-center gap-2">
<!-- 清除按钮 - 有输入且非搜索时显示 -->
<button
v-if="searchQuery && !searchStore.isSearching"
type="button"
class="w-6 h-6 flex items-center justify-center rounded-full
text-gray-400 hover:text-[#ff1493] dark:hover:text-[#ff69b4]
hover:bg-[#ff1493]/10 dark:hover:bg-[#ff69b4]/15
transition-all duration-200"
@click="clearSearch"
>
<XCircle :size="18" />
</button>
<!-- 搜索时显示进度 -->
<span
v-if="searchStore.isSearching"
@@ -376,78 +389,6 @@
</li>
</ul>
<!-- 快捷键提示 -->
<div class="mt-6 pt-5 border-t border-gray-200/50 dark:border-slate-700/50">
<h3 class="text-base sm:text-lg font-bold text-gray-700 dark:text-slate-200 mb-4 flex items-center gap-2">
<Keyboard :size="18" class="text-theme-primary dark:text-theme-accent" />
键盘快捷键
</h3>
<!-- 导航类 -->
<div class="mb-4">
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 mb-2 uppercase tracking-wide">导航</p>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<div class="shortcut-item">
<kbd>Esc</kbd>
<span>关闭面板</span>
</div>
<div class="shortcut-item">
<kbd>H</kbd>
<span>返回首页</span>
</div>
<div class="shortcut-item">
<kbd>,</kbd>
<span>设置</span>
</div>
<div class="shortcut-item">
<kbd>C</kbd>
<span>评论</span>
</div>
<div class="shortcut-item">
<kbd>V</kbd>
<span>作品介绍</span>
</div>
<div class="shortcut-item">
<kbd>Y</kbd>
<span>搜索历史</span>
</div>
<div class="shortcut-item">
<kbd>N</kbd>
<span>站点导航</span>
</div>
</div>
</div>
<!-- 操作类 -->
<div class="mb-4">
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 mb-2 uppercase tracking-wide">操作</p>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<div class="shortcut-item">
<kbd>/</kbd>
<span>聚焦搜索</span>
</div>
</div>
</div>
<!-- 滚动类 -->
<div>
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 mb-2 uppercase tracking-wide">滚动</p>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<div class="shortcut-item">
<kbd>T</kbd>
<span>回到顶部</span>
</div>
<div class="shortcut-item">
<kbd>[</kbd>
<span>上一平台</span>
</div>
<div class="shortcut-item">
<kbd>]</kbd>
<span>下一平台</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -469,7 +410,6 @@ import {
User,
Rocket,
Magnet,
Keyboard,
X,
RefreshCw,
Wifi,
@@ -478,6 +418,7 @@ import {
Server,
Loader2,
CornerDownLeft,
XCircle,
} from 'lucide-vue-next'
import { getSearchParamsFromURL, updateURLParams, onURLParamsChange } from '@/utils/urlParams'
import { saveSearchHistory } from '@/utils/persistence'
@@ -498,11 +439,15 @@ onMounted(() => {
// 优先从 URL 读取参数
const urlParams = getSearchParamsFromURL()
if (urlParams.s) {
searchQuery.value = urlParams.s
searchMode.value = urlParams.mode || 'game'
customApi.value = urlParams.api || ''
} else if (searchStore.searchQuery) {
// URL 参数可以独立生效mode 和 api 不依赖 s
const hasURLParams = urlParams.s || urlParams.mode || urlParams.api
if (hasURLParams) {
// 从 URL 恢复
if (urlParams.s) searchQuery.value = urlParams.s
if (urlParams.mode) searchMode.value = urlParams.mode
if (urlParams.api) customApi.value = urlParams.api
} else if (searchStore.searchQuery || searchStore.searchMode !== 'game') {
// 否则从 store 恢复
searchQuery.value = searchStore.searchQuery
searchMode.value = searchStore.searchMode
@@ -821,6 +766,11 @@ function getErrorCodeInfo(error: string): ErrorCodeInfo {
}
// 清除搜索输入
function clearSearch() {
searchQuery.value = ''
}
// 防抖搜索 - 防止快速连续触发
function triggerSearch() {
if (isSearchLocked.value || searchStore.isSearching) {
@@ -975,67 +925,6 @@ defineExpose({
}
}
/* 快捷键样式 */
.shortcut-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(255, 20, 147, 0.05);
border-radius: 0.75rem;
/* 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: translate3d(0, -1px, 0);
}
.dark .shortcut-item {
background: rgba(255, 105, 180, 0.1);
}
.dark .shortcut-item:hover {
background: rgba(255, 105, 180, 0.15);
}
.shortcut-item kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.75rem;
height: 1.75rem;
padding: 0 0.5rem;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #ff1493, #d946ef);
border-radius: 0.5rem;
box-shadow:
0 2px 4px rgba(255, 20, 147, 0.3),
0 1px 0 rgba(255, 255, 255, 0.2) inset;
}
.dark .shortcut-item kbd {
background: linear-gradient(135deg, #ff69b4, #e879f9);
box-shadow:
0 2px 6px rgba(255, 105, 180, 0.4),
0 1px 0 rgba(255, 255, 255, 0.15) inset;
}
.shortcut-item span {
font-size: 0.8125rem;
color: #6b7280;
font-weight: 500;
}
.dark .shortcut-item span {
color: #94a3b8;
}
/* 错误卡片样式 */
.error-card {
background: linear-gradient(135deg, rgba(254, 242, 242, 0.95), rgba(254, 226, 226, 0.95));

View File

@@ -7,7 +7,7 @@
@leave="onLeave"
>
<div
v-if="isOpen"
v-if="isOpen"
ref="modalRef"
:class="[
'fixed z-[100] flex flex-col settings-page shadow-2xl shadow-black/20',
@@ -16,7 +16,7 @@
: 'inset-0 md:inset-6 md:m-auto md:w-[600px] md:min-w-[400px] md:max-w-[800px] md:h-[500px] md:max-h-[calc(100%-3rem)] md:rounded-3xl'
]"
:style="windowStyle"
>
>
<!-- 调整大小手柄 -->
<WindowResizeHandles
:is-fullscreen="isFullscreen"
@@ -51,13 +51,13 @@
<!-- 右侧按钮组 -->
<div class="flex items-center gap-2">
<!-- 保存按钮 -->
<!-- 保存按钮 -->
<button
v-tap
class="px-4 py-1.5 rounded-full text-white text-sm font-semibold bg-gradient-to-r from-[#ff1493] to-[#d946ef] shadow-lg shadow-pink-500/25"
@click="save"
>
保存
class="px-4 py-1.5 rounded-full text-white text-sm font-semibold bg-gradient-to-r from-[#ff1493] to-[#d946ef] shadow-lg shadow-pink-500/25"
@click="save"
>
保存
</button>
<!-- 全屏按钮 - 仅桌面端 -->

View File

@@ -33,6 +33,15 @@
<Github :size="20" />
</a>
<!-- 键盘快捷键按钮 -->
<button
aria-label="键盘快捷键"
class="toolbar-button keyboard-button"
@click="toggleKeyboardHelp"
>
<Keyboard :size="20" />
</button>
<!-- 设置按钮 -->
<button
aria-label="设置"
@@ -47,11 +56,13 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useSearchStore } from '@/stores/search'
import { useUIStore } from '@/stores/ui'
import { generateShareURL } from '@/utils/urlParams'
import { Check, Download, Share2, Github, Settings } from 'lucide-vue-next'
import { Check, Download, Share2, Github, Keyboard, Settings } from 'lucide-vue-next'
import { playTap, playCelebration, playNotification, playSwipe } from '@/composables/useSound'
const searchStore = useSearchStore()
const uiStore = useUIStore()
// Props
const props = defineProps<{
@@ -111,6 +122,12 @@ async function shareSearch() {
}
}
// 切换键盘快捷键帮助
function toggleKeyboardHelp() {
playTap()
uiStore.isKeyboardHelpOpen = !uiStore.isKeyboardHelpOpen
}
// 打开设置
function openSettings() {
playTap()