mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-05-07 00:15:56 +08:00
Remove anime.js dependency and refactor SearchResults and SettingsModal components for improved performance and consistency
- Eliminated the use of anime.js in favor of simpler CSS transitions and effects. - Updated SearchResults.vue to implement virtual scrolling for better performance with large result sets. - Refactored SettingsModal.vue to enhance button interactions and remove unnecessary animation directives. - Improved overall styling and structure for a more cohesive user experience across components.
This commit is contained in:
66
src/components/ResultItem.vue
Normal file
66
src/components/ResultItem.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { Link as LinkIcon } from 'lucide-vue-next'
|
||||
|
||||
defineProps<{
|
||||
index: number
|
||||
source: {
|
||||
title: string
|
||||
url: string
|
||||
}
|
||||
}>()
|
||||
|
||||
// 从URL中提取路径
|
||||
function extractPath(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.pathname + urlObj.search + urlObj.hash
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="result-item group p-3 sm:p-4 rounded-xl
|
||||
bg-white/60 dark:bg-slate-700/60
|
||||
hover:bg-white/80 dark:hover:bg-slate-700/80
|
||||
border border-gray-200/40 dark:border-slate-600/40
|
||||
hover:border-theme-primary/30 dark:hover:border-theme-accent/30"
|
||||
>
|
||||
<!-- 标题行 -->
|
||||
<div class="flex items-start gap-2 sm:gap-3">
|
||||
<span class="text-theme-primary dark:text-theme-accent text-sm font-bold mt-0.5 shrink-0 opacity-60 group-hover:opacity-100">
|
||||
{{ index + 1 }}.
|
||||
</span>
|
||||
<a
|
||||
:href="source.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-800 dark:text-slate-200 group-hover:text-theme-primary dark:group-hover:text-theme-accent font-semibold flex-1 text-sm sm:text-base break-words leading-relaxed"
|
||||
>
|
||||
{{ source.title }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 资源相对路径(从URL中提取) -->
|
||||
<div v-if="source.url" class="flex items-center gap-2 mt-2 ml-6 sm:ml-8">
|
||||
<LinkIcon :size="12" class="text-theme-primary/50 dark:text-theme-accent/50" />
|
||||
<span class="text-xs text-gray-500 dark:text-slate-400 break-all font-mono bg-gray-100/80 dark:bg-slate-800/80 px-2 py-1 rounded">
|
||||
{{ extractPath(source.url) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 结果项 - 简化动画,仅使用 transform */
|
||||
.result-item {
|
||||
transition: transform 0.15s ease-out, background-color 0.15s ease-out;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,46 +81,33 @@
|
||||
<span class="text-red-700 dark:text-red-300 font-medium">{{ platformData.error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果列表 - 使用 contain 优化布局性能 + 懒渲染 -->
|
||||
<div v-if="getDisplayedResults(platformData).length > 0" class="results-list space-y-2 contain-layout">
|
||||
<LazyRender
|
||||
v-for="(result, index) in getDisplayedResults(platformData)"
|
||||
:key="result.url || index"
|
||||
:once="true"
|
||||
min-height="72px"
|
||||
root-margin="300px 0px"
|
||||
>
|
||||
<div
|
||||
class="result-item group p-3 sm:p-4 rounded-xl
|
||||
bg-white/60 dark:bg-slate-700/60
|
||||
hover:bg-white/80 dark:hover:bg-slate-700/80
|
||||
border border-gray-200/40 dark:border-slate-600/40
|
||||
hover:border-theme-primary/30 dark:hover:border-theme-accent/30"
|
||||
>
|
||||
<!-- 标题行 -->
|
||||
<div class="flex items-start gap-2 sm:gap-3">
|
||||
<span class="text-theme-primary dark:text-theme-accent text-sm font-bold mt-0.5 shrink-0 opacity-60 group-hover:opacity-100">
|
||||
{{ index + 1 }}.
|
||||
</span>
|
||||
<a
|
||||
:href="result.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-800 dark:text-slate-200 group-hover:text-theme-primary dark:group-hover:text-theme-accent font-semibold flex-1 text-sm sm:text-base break-words leading-relaxed"
|
||||
>
|
||||
{{ result.title }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 资源相对路径(从URL中提取) -->
|
||||
<div v-if="result.url" class="flex items-center gap-2 mt-2 ml-6 sm:ml-8">
|
||||
<LinkIcon :size="12" class="text-theme-primary/50 dark:text-theme-accent/50" />
|
||||
<span class="text-xs text-gray-500 dark:text-slate-400 break-all font-mono bg-gray-100/80 dark:bg-slate-800/80 px-2 py-1 rounded">
|
||||
{{ extractPath(result.url) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 搜索结果列表 - 使用虚拟滚动优化大量结果 -->
|
||||
<div v-if="getDisplayedResults(platformData).length > 0" class="results-list contain-layout">
|
||||
<!-- 结果数量少于 15 时使用普通列表 -->
|
||||
<template v-if="getDisplayedResults(platformData).length < 15">
|
||||
<div class="space-y-2">
|
||||
<ResultItem
|
||||
v-for="(result, index) in getDisplayedResults(platformData)"
|
||||
:key="result.url || index"
|
||||
:index="index"
|
||||
:source="result"
|
||||
/>
|
||||
</div>
|
||||
</LazyRender>
|
||||
</template>
|
||||
<!-- 结果数量超过 15 时使用虚拟滚动 -->
|
||||
<VirtualList
|
||||
v-else
|
||||
class="virtual-list-container"
|
||||
:data-key="'url'"
|
||||
:data-sources="getDisplayedResults(platformData)"
|
||||
:estimate-size="80"
|
||||
:keeps="20"
|
||||
item-class="mb-2"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<ResultItem :index="index" :source="item" />
|
||||
</template>
|
||||
</VirtualList>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多按钮 -->
|
||||
@@ -162,12 +149,13 @@ import { useSearchStore } from '@/stores/search'
|
||||
import type { PlatformData } from '@/stores/search'
|
||||
import { playTap } from '@/composables/useSound'
|
||||
import LazyRender from '@/components/LazyRender.vue'
|
||||
import ResultItem from '@/components/ResultItem.vue'
|
||||
import VirtualList from 'vue-virtual-scroll-list'
|
||||
import {
|
||||
ExternalLink,
|
||||
AlertTriangle,
|
||||
Crown,
|
||||
List,
|
||||
Link as LinkIcon,
|
||||
ArrowDown,
|
||||
CheckCircle,
|
||||
Star,
|
||||
@@ -189,18 +177,6 @@ import {
|
||||
|
||||
const searchStore = useSearchStore()
|
||||
|
||||
// 从URL中提取路径
|
||||
function extractPath(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
// 返回路径部分(去掉域名)
|
||||
return urlObj.pathname + urlObj.search + urlObj.hash
|
||||
} catch {
|
||||
// 如果URL解析失败,返回完整URL
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
// 获取站点所有结果的唯一标签
|
||||
function getUniqueTags(platformData: PlatformData) {
|
||||
const allTags = new Set<string>()
|
||||
@@ -386,6 +362,12 @@ function getTagLabel(tag: string) {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* 虚拟滚动列表容器 */
|
||||
.virtual-list-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Tailwind 动画 - 优化为仅使用 transform 和 opacity */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
|
||||
@@ -12,13 +12,11 @@
|
||||
>
|
||||
<!-- 顶部导航栏 -->
|
||||
<div
|
||||
v-anime:100="'slideUp'"
|
||||
class="flex-shrink-0 flex items-center justify-between px-4 sm:px-6 py-3 sm:py-4 border-b border-white/10 dark:border-slate-700/50 glassmorphism-navbar select-none md:rounded-t-3xl"
|
||||
>
|
||||
<!-- 返回按钮 - 仅移动端 -->
|
||||
<button
|
||||
v-tap
|
||||
class="flex items-center gap-1 text-[#ff1493] dark:text-[#ff69b4] font-medium transition-colors md:hidden"
|
||||
class="flex items-center gap-1 text-[#ff1493] dark:text-[#ff69b4] font-medium transition-colors active:scale-95 md:hidden"
|
||||
@click="close"
|
||||
>
|
||||
<ChevronLeft :size="24" />
|
||||
@@ -35,8 +33,7 @@
|
||||
<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"
|
||||
class="px-4 py-1.5 rounded-full text-white text-sm font-semibold bg-[#ff1493] hover:bg-[#e0117f] active:scale-95 transition-all shadow-lg shadow-pink-500/25"
|
||||
@click="save"
|
||||
>
|
||||
保存
|
||||
@@ -58,7 +55,6 @@
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 py-6 sm:py-8 space-y-6">
|
||||
<!-- 主题设置卡片 -->
|
||||
<div
|
||||
v-anime:100="'cardIn'"
|
||||
class="settings-card"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
@@ -113,7 +109,6 @@
|
||||
|
||||
<!-- API 设置卡片 -->
|
||||
<div
|
||||
v-anime:150="'cardIn'"
|
||||
class="settings-card"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
@@ -127,11 +122,10 @@
|
||||
</div>
|
||||
|
||||
<!-- API 选项列表 -->
|
||||
<div v-anime-stagger:50="'slideRight'" class="space-y-2">
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="option in apiOptions"
|
||||
:key="option.value"
|
||||
v-tap
|
||||
type="button"
|
||||
:class="[
|
||||
'w-full flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 sm:p-4 rounded-xl transition-all duration-200 text-left',
|
||||
@@ -217,7 +211,6 @@
|
||||
|
||||
<!-- 自定义样式卡片 -->
|
||||
<div
|
||||
v-anime:200="'cardIn'"
|
||||
class="settings-card"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
@@ -257,7 +250,6 @@
|
||||
|
||||
<!-- 重置区域 -->
|
||||
<div
|
||||
v-anime:250="'cardIn'"
|
||||
class="settings-card bg-red-50/50 dark:bg-red-950/20 border-red-200/50 dark:border-red-900/30"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -271,8 +263,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-tap
|
||||
class="px-4 py-2 rounded-xl text-red-600 dark:text-red-400 font-medium bg-white dark:bg-slate-800 border border-red-200 dark:border-red-800/50 hover:bg-red-50 dark:hover:bg-red-950/50 transition-colors"
|
||||
class="px-4 py-2 rounded-xl text-red-600 dark:text-red-400 font-medium bg-white dark:bg-slate-800 border border-red-200 dark:border-red-800/50 hover:bg-red-50 dark:hover:bg-red-950/50 active:scale-95 transition-all"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
|
||||
@@ -34,18 +34,11 @@ import { vRipple } from './directives/vRipple'
|
||||
// 文本滚动指令
|
||||
import { vTextScroll } from './composables/useTextScroll'
|
||||
|
||||
// Anime.js 动画指令
|
||||
import { vAnime, vAnimeStagger, vHover, vTap } from './composables/useAnime'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册全局指令
|
||||
app.directive('ripple', vRipple)
|
||||
app.directive('text-scroll', vTextScroll)
|
||||
app.directive('anime', vAnime)
|
||||
app.directive('anime-stagger', vAnimeStagger)
|
||||
app.directive('hover', vHover)
|
||||
app.directive('tap', vTap)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 配置 Pinia 插件
|
||||
|
||||
@@ -90,10 +90,6 @@ export default defineConfig({
|
||||
if (id.includes('/lucide-vue-next/')) {
|
||||
return 'ui-libs';
|
||||
}
|
||||
// 动画库
|
||||
if (id.includes('/animejs/')) {
|
||||
return 'anime';
|
||||
}
|
||||
// Artalk 评论
|
||||
if (id.includes('/artalk/')) {
|
||||
return 'artalk';
|
||||
@@ -125,7 +121,6 @@ export default defineConfig({
|
||||
'vue',
|
||||
'pinia',
|
||||
'lucide-vue-next',
|
||||
'animejs',
|
||||
],
|
||||
// 排除不需要预构建的
|
||||
exclude: ['artalk'],
|
||||
|
||||
Reference in New Issue
Block a user