Files
SearcjGal-frontend/src/components/SearchResults.vue
AdingApkgg 5aa86ff03c Enhance settings management and custom code application in App.vue and SettingsModal.vue
- Integrated custom JavaScript and HTML functionality into the application, allowing users to apply their scripts and markup dynamically.
- Updated API configuration handling in search.ts to utilize settings from the store, improving flexibility and maintainability.
- Refactored theme application logic to ensure custom styles are applied correctly during component lifecycle events.
- Improved the user interface in SettingsModal.vue with an IDE-style code editor for better user experience when editing custom code.
2025-12-27 02:17:43 +08:00

378 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-if="searchStore.hasResults" class="w-full sm:px-4 md:px-6 py-4 sm:py-6 md:py-8 animate-fade-in">
<div id="results" class="sm:max-w-5xl sm:mx-auto space-y-4 sm:space-y-6">
<!-- 使用 v-memo 优化平台卡片渲染 + LazyRender 懒渲染 -->
<LazyRender
v-for="[platformName, platformData] in searchStore.platformResults"
:key="platformName"
v-memo="[platformName, platformData.name, platformData.color, platformData.items.length, platformData.displayedCount, platformData.error, platformData.url]"
:once="true"
min-height="200px"
root-margin="400px 0px"
>
<div
:data-platform="platformName"
class="result-card rounded-none sm:rounded-2xl animate-fade-in-up border-2 content-auto"
:class="getBorderClass(platformData.color)"
>
<div class="p-4 sm:p-5 md:p-6">
<!-- 站点标题行网站名称 + 推荐标签 + 资源标签 + 结果数 -->
<div
class="flex flex-wrap items-center gap-2 sm:gap-3 mb-4 pb-3 border-b"
:class="getBorderBottomClass(platformData.color)"
>
<!-- 网站名称可点击 -->
<a
v-if="platformData.url"
:href="platformData.url"
target="_blank"
rel="noopener noreferrer"
class="text-xl sm:text-2xl font-bold flex items-center gap-2 hover:opacity-80 cursor-pointer"
:class="getHeaderTextColor(platformData.color)"
:title="`访问 ${platformData.name}`"
>
<component :is="getPlatformIconComponent(platformData.color)" :size="24" />
{{ platformData.name }}
<ExternalLink :size="16" class="opacity-70" />
</a>
<div
v-else
class="text-xl sm:text-2xl font-bold flex items-center gap-2"
:class="getHeaderTextColor(platformData.color)"
>
<component :is="getPlatformIconComponent(platformData.color)" :size="24" />
{{ platformData.name }}
</div>
<!-- 推荐/付费标签 -->
<span
v-if="getRecommendText(platformData.color)"
class="px-3 py-1 rounded-full text-xs font-bold shadow-md flex items-center gap-1.5"
:class="getRecommendChipClass(platformData.color)"
>
<component :is="platformData.color === 'red' ? AlertTriangle : Crown" :size="14" />
{{ getRecommendText(platformData.color) }}
</span>
<!-- 站点的所有标签去重 -->
<template v-for="tag in getUniqueTags(platformData)" :key="tag">
<span
:class="getTagClass(tag)"
class="px-2.5 py-1 rounded-lg text-xs font-bold shadow-sm flex items-center gap-1.5 border"
>
<component :is="getTagIconComponent(tag)" :size="12" />
<span>{{ getTagLabel(tag) }}</span>
</span>
</template>
<!-- 结果数量 -->
<span
class="ml-auto px-3 py-1.5 rounded-full font-bold text-sm shadow-md flex items-center gap-2 shrink-0"
:class="getCountBadgeClass(platformData.color)"
>
<List :size="16" />
{{ platformData.items.length }}
</span>
</div>
<!-- 错误信息 -->
<div v-if="platformData.error" class="flex items-center gap-3 p-4 mb-4 bg-red-50/90 dark:bg-red-900/50 border-2 border-red-300 dark:border-red-700 rounded-xl">
<AlertTriangle :size="20" class="text-red-600 dark:text-red-400" />
<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 contain-layout space-y-2">
<ResultItem
v-for="(result, index) in getDisplayedResults(platformData)"
:key="result.url || index"
:index="index"
:source="result"
/>
</div>
<!-- 加载更多按钮 -->
<div v-if="platformData.items.length > platformData.displayedCount" class="load-more mt-6 flex justify-center">
<button
class="px-6 py-3 rounded-xl
bg-theme-primary/90 dark:bg-theme-accent/90 text-white font-bold
border border-white/20
hover:scale-105 active:scale-95
transition-transform duration-200
flex items-center gap-2"
@click="loadMore(platformName)"
>
<ArrowDown :size="18" />
<span>加载更多 ({{ Math.min(20, platformData.items.length - platformData.displayedCount) }})</span>
</button>
</div>
<!-- 已加载全部提示 -->
<div v-else-if="platformData.items.length > 10" class="all-loaded mt-6 text-center">
<span class="text-sm text-gray-500 dark:text-slate-400 flex items-center justify-center gap-2">
<CheckCircle :size="16" class="text-theme-primary dark:text-theme-accent" />
<span>已加载全部 {{ platformData.items.length }} 条结果</span>
</span>
</div>
<div v-else-if="platformData.items.length === 0" class="no-results text-gray-500 text-center py-4">
该平台暂无搜索结果
</div>
</div>
</div>
</LazyRender>
</div>
</div>
</template>
<script setup lang="ts">
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 {
ExternalLink,
AlertTriangle,
Crown,
List,
ArrowDown,
CheckCircle,
Star,
Circle,
DollarSign,
XCircle,
User,
Coins,
MessageCircle,
Reply,
Server,
Rocket,
Turtle,
Layers,
Magnet,
Wand2,
Tag as TagIcon,
} from 'lucide-vue-next'
const searchStore = useSearchStore()
// 获取站点所有结果的唯一标签
function getUniqueTags(platformData: PlatformData) {
const allTags = new Set<string>()
platformData.items.forEach(item => {
if (item.tags && item.tags.length > 0) {
item.tags.forEach(tag => allTags.add(tag))
}
})
return Array.from(allTags)
}
// 获取要显示的结果(根据 displayedCount
function getDisplayedResults(platformData: PlatformData) {
return platformData.items.slice(0, platformData.displayedCount || 10)
}
// 加载更多
function loadMore(platformName: string) {
playTap()
searchStore.loadMoreResults(platformName, 20)
}
// 新增:卡片边框颜色(去掉 hover 过渡)
function getBorderClass(color: string) {
const classes: Record<string, string> = {
lime: 'border-lime-300 dark:border-lime-700/50',
white: 'border-gray-300 dark:border-slate-600',
gold: 'border-yellow-300 dark:border-yellow-700/50',
red: 'border-red-300 dark:border-red-700/50',
}
return classes[color] || 'border-gray-300 dark:border-slate-600'
}
// 新增:标题区域底部边框
function getBorderBottomClass(color: string) {
const classes: Record<string, string> = {
lime: 'border-lime-200 dark:border-lime-800/30',
white: 'border-gray-200 dark:border-slate-700',
gold: 'border-yellow-200 dark:border-yellow-800/30',
red: 'border-red-200 dark:border-red-800/30',
}
return classes[color] || 'border-gray-200 dark:border-slate-700'
}
// 新增:标题文字颜色(更鲜艳)
function getHeaderTextColor(color: string) {
const classes: Record<string, string> = {
lime: 'text-lime-600 dark:text-lime-400',
white: 'text-gray-700 dark:text-gray-300',
gold: 'text-yellow-600 dark:text-yellow-400',
red: 'text-red-600 dark:text-red-400',
}
return classes[color] || 'text-gray-700 dark:text-gray-300'
}
// 新增:推荐标签样式(使用纯色,避免渐变)
function getRecommendChipClass(color: string) {
const classes: Record<string, string> = {
lime: 'bg-lime-500 text-white border-lime-600',
gold: 'bg-yellow-500 text-white border-yellow-600',
red: 'bg-red-500 text-white border-red-600',
}
return classes[color] || ''
}
// 新增:结果数量徽章(根据平台颜色)
function getCountBadgeClass(color: string) {
const classes: Record<string, string> = {
lime: 'bg-lime-100 dark:bg-lime-900/40 text-lime-700 dark:text-lime-300 border border-lime-300 dark:border-lime-700',
white: 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-slate-200 border border-gray-300 dark:border-slate-600',
gold: 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300 border border-yellow-300 dark:border-yellow-700',
red: 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 border border-red-300 dark:border-red-700',
}
return classes[color] || 'bg-gray-100 dark:bg-slate-700 text-gray-700 dark:text-slate-200'
}
// 获取平台图标组件
function getPlatformIconComponent(color: string): typeof Star | typeof Circle | typeof DollarSign | typeof XCircle {
const icons: Record<string, typeof Star | typeof Circle | typeof DollarSign | typeof XCircle> = {
lime: Star,
white: Circle,
gold: DollarSign,
red: XCircle,
}
return icons[color] || Circle
}
// 获取标签图标组件
function getTagIconComponent(tag: string): typeof CheckCircle | typeof User | typeof Coins | typeof MessageCircle | typeof Reply | typeof Server | typeof Rocket | typeof Turtle | typeof Layers | typeof Magnet | typeof Wand2 | typeof TagIcon {
const icons: Record<string, typeof CheckCircle | typeof User | typeof Coins | typeof MessageCircle | typeof Reply | typeof Server | typeof Rocket | typeof Turtle | typeof Layers | typeof Magnet | typeof Wand2 | typeof TagIcon> = {
'NoReq': CheckCircle,
'Login': User,
'LoginPay': Coins,
'LoginRep': MessageCircle,
'Rep': Reply,
'SuDrive': Server,
'NoSplDrive': Rocket,
'SplDrive': Turtle,
'MixDrive': Layers,
'BTmag': Magnet,
'magic': Wand2,
}
return icons[tag] || TagIcon
}
function getRecommendText(color: string) {
const texts: Record<string, string> = {
lime: '推荐',
gold: '付费',
red: '不推荐',
}
return texts[color] || ''
}
// 标签样式映射(根据 Cloudflare Workers API 文档)- 优化配色
function getTagClass(tag: string) {
const classes: Record<string, string> = {
'NoReq': 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 border-green-400 dark:border-green-600',
'Login': 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 border-blue-400 dark:border-blue-600',
'LoginPay': 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300 border-yellow-400 dark:border-yellow-600',
'LoginRep': 'bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 border-purple-400 dark:border-purple-600',
'Rep': 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300 border-indigo-400 dark:border-indigo-600',
'SuDrive': 'bg-pink-100 dark:bg-pink-900/40 text-pink-700 dark:text-pink-300 border-pink-400 dark:border-pink-600',
'NoSplDrive': 'bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-300 border-emerald-400 dark:border-emerald-600',
'SplDrive': 'bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-300 border-orange-400 dark:border-orange-600',
'MixDrive': 'bg-cyan-100 dark:bg-cyan-900/40 text-cyan-700 dark:text-cyan-300 border-cyan-400 dark:border-cyan-600',
'BTmag': 'bg-violet-100 dark:bg-violet-900/40 text-violet-700 dark:text-violet-300 border-violet-400 dark:border-violet-600',
'magic': 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 border-red-400 dark:border-red-600',
}
return classes[tag] || 'bg-gray-100 dark:bg-gray-800/40 text-gray-600 dark:text-gray-400 border-gray-400 dark:border-gray-600'
}
// 标签图标映射
// 标签文本映射
function getTagLabel(tag: string) {
const labels: Record<string, string> = {
'NoReq': '直接下载',
'Login': '需登录',
'LoginPay': '需积分',
'LoginRep': '登录+留言',
'Rep': '需留言',
'SuDrive': '自建盘',
'NoSplDrive': '不限速',
'SplDrive': '限速盘',
'MixDrive': '混合盘',
'BTmag': 'BT/磁力',
'magic': '需魔法',
}
return labels[tag] || tag
}
</script>
<style scoped>
/* 平台卡片 - 半透明效果 */
.result-card {
animation-delay: calc(var(--index, 0) * 0.1s);
content-visibility: auto;
contain-intrinsic-size: auto 400px;
background: rgba(var(--color-bg-light, 255, 255, 255), var(--opacity-card, 0.8)) !important;
border: var(--border-thin, 1px) solid rgba(var(--color-primary, 255, 20, 147), var(--opacity-border, 0.15)) !important;
box-shadow: var(--shadow-md, 0 4px 16px rgba(0, 0, 0, 0.1)) !important;
}
/* 暗色模式 */
.dark .result-card {
background: rgba(var(--color-bg-dark, 30, 41, 59), var(--opacity-card-dark, 0.8)) !important;
border-color: rgba(var(--color-primary-light, 255, 105, 180), var(--opacity-border-dark, 0.2)) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25) !important;
}
/* 结果项 - 简化动画,仅使用 transform */
.result-item {
transition: transform 0.15s ease-out, background-color 0.15s ease-out;
}
.result-item:hover {
transform: translateX(4px);
}
/* 结果列表布局隔离 */
.results-list {
contain: layout style;
}
/* Tailwind 动画 - 优化为仅使用 transform 和 opacity */
.animate-fade-in {
animation: fadeIn 0.5s ease-out;
}
.animate-fade-in-up {
animation: fadeInUp 0.6s ease-out;
}
.animation-delay-1000 {
animation-delay: 1s;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
</style>