mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-03-19 05:39:45 +08:00
- Integrated new Pinia plugins for state persistence, snapshots, and tab synchronization in main.ts, improving state management capabilities. - Updated SearchHeader.vue to utilize caching for VNDB data, enhancing performance and user experience by reducing unnecessary API calls. - Refactored SearchHistoryModal.vue to leverage the history store for managing search history, improving data handling and user interaction. - Enhanced StatsCorner.vue to display visitor statistics and search counts, providing users with better insights into application usage. - Improved settings management in settings.ts by adding undo/redo functionality for setting changes, enhancing user control over preferences.
288 lines
10 KiB
Vue
288 lines
10 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<!-- 历史记录面板 - 右下角弹出 -->
|
||
<Transition
|
||
enter-active-class="transition-all duration-200 ease-out"
|
||
enter-from-class="opacity-0 scale-90 translate-y-2"
|
||
enter-to-class="opacity-100 scale-100 translate-y-0"
|
||
leave-active-class="transition-all duration-150 ease-in"
|
||
leave-from-class="opacity-100 scale-100 translate-y-0"
|
||
leave-to-class="opacity-0 scale-90 translate-y-2"
|
||
>
|
||
<div
|
||
v-if="uiStore.isHistoryModalOpen"
|
||
class="history-modal fixed z-[100] flex flex-col
|
||
bottom-20 right-4 w-80 max-h-[60vh]
|
||
rounded-2xl shadow-2xl shadow-black/20"
|
||
>
|
||
<!-- 顶部导航栏 -->
|
||
<div class="history-header flex-shrink-0 flex items-center justify-between px-4 py-3 border-b border-white/10 dark:border-slate-700/50 rounded-t-2xl">
|
||
<!-- 标题 -->
|
||
<div class="flex items-center gap-2">
|
||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center shadow-md shadow-amber-500/30">
|
||
<History :size="16" class="text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 class="text-sm font-bold text-gray-800 dark:text-white">搜索历史</h1>
|
||
<p v-if="historyStore.historyCount > 0" class="text-xs text-gray-500 dark:text-slate-400">{{ historyStore.historyCount }} 条记录</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="flex items-center gap-1">
|
||
<button
|
||
v-if="history.length > 0"
|
||
v-ripple="'rgba(239, 68, 68, 0.3)'"
|
||
class="w-7 h-7 rounded-full flex items-center justify-center text-red-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all"
|
||
title="清空历史"
|
||
@click="handleClearHistory"
|
||
>
|
||
<Trash2 :size="14" />
|
||
</button>
|
||
<button
|
||
class="w-7 h-7 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700 transition-all"
|
||
@click="closeModal"
|
||
>
|
||
<X :size="16" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域 -->
|
||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||
<div class="p-2">
|
||
<!-- 无历史记录时显示 -->
|
||
<div
|
||
v-if="history.length === 0"
|
||
class="flex flex-col items-center justify-center py-8 text-center"
|
||
>
|
||
<div class="w-12 h-12 rounded-xl bg-amber-50 dark:bg-amber-900/20 flex items-center justify-center mb-3">
|
||
<Clock :size="24" class="text-amber-400/50" />
|
||
</div>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||
暂无搜索历史
|
||
</p>
|
||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||
搜索后会自动记录
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 历史记录列表 -->
|
||
<div v-else class="space-y-1">
|
||
<TransitionGroup
|
||
enter-active-class="transition-all duration-200 ease-out"
|
||
enter-from-class="opacity-0 translate-x-2"
|
||
enter-to-class="opacity-100 translate-x-0"
|
||
leave-active-class="transition-all duration-150 ease-in"
|
||
leave-from-class="opacity-100"
|
||
leave-to-class="opacity-0 scale-95"
|
||
move-class="transition-all duration-200"
|
||
>
|
||
<div
|
||
v-for="(item, index) in history"
|
||
:key="item.query + item.mode + index"
|
||
v-ripple
|
||
role="button"
|
||
tabindex="0"
|
||
class="history-item group w-full flex items-center gap-2.5 px-3 py-2 rounded-xl transition-all duration-150 cursor-pointer"
|
||
@click="handleSelectHistory(item)"
|
||
@keydown.enter="handleSelectHistory(item)"
|
||
@keydown.space.prevent="handleSelectHistory(item)"
|
||
>
|
||
<!-- 模式标签 + 图标 -->
|
||
<span
|
||
class="flex items-center gap-1 text-[10px] font-bold px-1.5 py-0.5 rounded uppercase tracking-wide flex-shrink-0"
|
||
:class="item.mode === 'game'
|
||
? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||
: 'bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400'"
|
||
>
|
||
<component :is="item.mode === 'game' ? Gamepad2 : Wrench" :size="10" />
|
||
{{ item.mode === 'game' ? '游戏' : '补丁' }}
|
||
</span>
|
||
|
||
<!-- 搜索图标 + 关键词 -->
|
||
<div class="flex-1 flex items-center gap-1.5 min-w-0">
|
||
<Search :size="12" class="text-gray-400 dark:text-slate-500 shrink-0 group-hover:text-amber-500 transition-colors" />
|
||
<span
|
||
v-text-scroll
|
||
class="flex-1 text-sm font-medium text-gray-700 dark:text-slate-200 text-left group-hover:text-amber-600 dark:group-hover:text-amber-400 transition-colors"
|
||
>
|
||
{{ item.query }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 结果数 + 图标 -->
|
||
<span
|
||
v-if="item.resultCount"
|
||
class="flex items-center gap-1 text-[10px] text-gray-400 dark:text-gray-500 flex-shrink-0"
|
||
:title="`${item.resultCount} 条结果`"
|
||
>
|
||
<Hash :size="10" />
|
||
{{ item.resultCount }}
|
||
</span>
|
||
|
||
<!-- 删除按钮 -->
|
||
<button
|
||
type="button"
|
||
class="w-6 h-6 rounded-md flex items-center justify-center opacity-0 group-hover:opacity-100 hover:bg-red-100 dark:hover:bg-red-900/30 transition-all flex-shrink-0"
|
||
@click.stop="handleRemoveItem(index)"
|
||
>
|
||
<X :size="12" class="text-gray-400 hover:text-red-500" />
|
||
</button>
|
||
</div>
|
||
</TransitionGroup>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
||
import { useUIStore } from '@/stores/ui'
|
||
import { useHistoryStore } from '@/stores/history'
|
||
import type { SearchHistory } from '@/utils/persistence'
|
||
import { playSelect, playTap, playCaution, playTransitionUp, playTransitionDown } from '@/composables/useSound'
|
||
import { History, Trash2, X, Gamepad2, Wrench, Hash, Clock, Search } from 'lucide-vue-next'
|
||
|
||
const uiStore = useUIStore()
|
||
const historyStore = useHistoryStore()
|
||
|
||
// 使用 historyStore 的响应式数据
|
||
const history = computed(() => historyStore.searchHistory)
|
||
|
||
const emit = defineEmits<{
|
||
select: [history: SearchHistory]
|
||
}>()
|
||
|
||
// 加载历史记录
|
||
function loadHistory() {
|
||
historyStore.loadHistory()
|
||
}
|
||
|
||
// 选择历史记录
|
||
function handleSelectHistory(item: SearchHistory) {
|
||
playSelect()
|
||
|
||
// 先发送事件(让父组件更新 URL)
|
||
emit('select', item)
|
||
|
||
// 然后关闭面板
|
||
uiStore.isHistoryModalOpen = false
|
||
}
|
||
|
||
// 清空历史
|
||
function handleClearHistory() {
|
||
playCaution()
|
||
if (confirm('确定要清空所有搜索历史吗?')) {
|
||
historyStore.clearHistory()
|
||
}
|
||
}
|
||
|
||
// 删除单条记录
|
||
function handleRemoveItem(index: number) {
|
||
playTap()
|
||
historyStore.removeHistory(index)
|
||
}
|
||
|
||
// 关闭模态框
|
||
function closeModal() {
|
||
playTransitionDown()
|
||
uiStore.isHistoryModalOpen = false
|
||
}
|
||
|
||
// 键盘事件
|
||
function handleKeydown(e: globalThis.KeyboardEvent) {
|
||
if (!uiStore.isHistoryModalOpen) {return}
|
||
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault()
|
||
closeModal()
|
||
}
|
||
}
|
||
|
||
// 监听面板打开时加载数据
|
||
watch(() => uiStore.isHistoryModalOpen, (isOpen) => {
|
||
if (isOpen) {
|
||
playTransitionUp()
|
||
loadHistory()
|
||
}
|
||
})
|
||
|
||
onMounted(() => {
|
||
window.addEventListener('keydown', handleKeydown)
|
||
if (uiStore.isHistoryModalOpen) {
|
||
loadHistory()
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('keydown', handleKeydown)
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
/* 历史记录面板 - 右下角弹出 (亮色模式) */
|
||
.history-modal {
|
||
background: linear-gradient(
|
||
180deg,
|
||
rgba(var(--color-bg-light, 255, 255, 255), var(--opacity-panel, 0.85)) 0%,
|
||
rgba(255, 253, 245, var(--opacity-panel-hover, 0.9)) 100%
|
||
);
|
||
border: var(--border-thin, 1px) solid rgba(var(--color-warning, 251, 191, 36), var(--opacity-border, 0.15));
|
||
border-radius: var(--radius-2xl, 1.5rem);
|
||
}
|
||
|
||
/* 历史记录面板 - 右下角弹出 (暗色模式) */
|
||
.dark .history-modal {
|
||
background: linear-gradient(
|
||
180deg,
|
||
rgba(var(--color-bg-dark, 30, 41, 59), var(--opacity-panel-dark, 0.88)) 0%,
|
||
rgba(30, 27, 17, var(--opacity-panel-dark-hover, 0.92)) 100%
|
||
) !important;
|
||
border: var(--border-thin, 1px) solid rgba(var(--color-warning, 251, 191, 36), var(--opacity-border-dark, 0.2)) !important;
|
||
}
|
||
|
||
/* 头部样式 */
|
||
.history-header {
|
||
background: rgba(var(--color-bg-light, 255, 255, 255), var(--opacity-header, 0.7));
|
||
}
|
||
|
||
.dark .history-header {
|
||
background: rgba(var(--color-bg-dark, 30, 41, 59), var(--opacity-header-dark, 0.7)) !important;
|
||
}
|
||
|
||
/* 历史记录项 */
|
||
.history-item {
|
||
background: transparent;
|
||
}
|
||
|
||
.history-item:hover {
|
||
background: rgba(251, 191, 36, 0.08);
|
||
}
|
||
|
||
.dark .history-item:hover {
|
||
background: rgba(251, 191, 36, 0.12) !important;
|
||
}
|
||
|
||
/* 自定义滚动条 */
|
||
.custom-scrollbar::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.custom-scrollbar::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||
background: linear-gradient(180deg, #f59e0b, #d97706);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||
background: linear-gradient(180deg, #d97706, #b45309);
|
||
}
|
||
</style>
|