feat: Refine adult content fuzzy matching logic and enhance UI with direct video links and dedicated loading/empty states.

This commit is contained in:
kuekhaoyang
2025-11-29 23:16:53 +08:00
parent 2d6011abf6
commit 69b60913a4
4 changed files with 66 additions and 78 deletions
+10 -4
View File
@@ -76,17 +76,17 @@ export async function GET() {
return label.replace(/[视频片区专]/g, '');
};
// Helper to check if two labels match fuzzily (>= 2 chars overlap)
// Helper to check if two labels match fuzzily (>= 4 chars overlap)
const isFuzzyMatch = (label1: string, label2: string) => {
const clean1 = cleanLabel(label1);
const clean2 = cleanLabel(label2);
// If either is too short after cleaning, require exact match
if (clean1.length < 2 || clean2.length < 2) {
if (clean1.length < 4 || clean2.length < 4) {
return clean1 === clean2;
}
// Check for 2+ char overlap
// Check for 4+ char overlap
let overlapCount = 0;
const set1 = new Set(clean1.split(''));
for (const char of clean2) {
@@ -94,7 +94,7 @@ export async function GET() {
overlapCount++;
}
}
return overlapCount >= 2;
return overlapCount >= 4;
};
// Process results
@@ -104,6 +104,9 @@ export async function GET() {
categories.forEach((cat: Category) => {
const typeName = cat.type_name.trim();
// Skip empty type names
if (!typeName) return;
const value = `${sourceId}:${cat.type_id}`;
// Try to find a fuzzy match in existing categories
@@ -131,6 +134,9 @@ export async function GET() {
// Convert merged categories to tags
mergedCategories.forEach((cat) => {
// Skip categories with no values (shouldn't happen with current logic but good for safety)
if (cat.values.length === 0) return;
// Create a unique ID based on the label (using base64 to be safe)
const id = Buffer.from(cat.label).toString('base64');
+3 -24
View File
@@ -1,6 +1,5 @@
'use client';
import { useState, useEffect } from 'react';
import { TagManager } from '@/components/home/TagManager';
import { AdultContentGrid } from './AdultContentGrid';
import { useAdultTagManager } from '@/lib/hooks/useAdultTagManager';
@@ -25,7 +24,6 @@ export function AdultContent({ onSearch }: AdultContentProps) {
handleDeleteTag,
handleRestoreDefaults,
handleDragEnd,
loading: tagsLoading,
} = useAdultTagManager();
// Get the category value from selected tag
@@ -33,38 +31,18 @@ export function AdultContent({ onSearch }: AdultContentProps) {
const {
videos,
loading: contentLoading,
loading,
hasMore,
prefetchRef,
loadMoreRef,
} = useAdultContent(categoryValue);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const handleVideoClick = (video: any) => {
if (onSearch) {
onSearch(video.vod_name);
}
};
if (!mounted || tagsLoading) {
return (
<div className="animate-fade-in">
<div className="mb-6 h-8 w-24 bg-[var(--glass-bg)] rounded animate-pulse" />
<div className="mb-8 h-10 w-full bg-[var(--glass-bg)] rounded animate-pulse" />
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{[...Array(10)].map((_, i) => (
<div key={i} className="aspect-[2/3] bg-[var(--glass-bg)] rounded-[var(--radius-2xl)] animate-pulse" />
))}
</div>
</div>
);
}
return (
<div className="animate-fade-in">
<TagManager
@@ -84,9 +62,10 @@ export function AdultContent({ onSearch }: AdultContentProps) {
onDragEnd={handleDragEnd}
onJustAddedTagHandled={() => setJustAddedTag(false)}
/>
<AdultContentGrid
videos={videos}
loading={contentLoading}
loading={loading}
hasMore={hasMore}
onVideoClick={handleVideoClick}
prefetchRef={prefetchRef}
+50 -50
View File
@@ -31,28 +31,39 @@ export function AdultContentGrid({
prefetchRef,
loadMoreRef,
}: AdultContentGridProps) {
if (videos.length === 0 && !loading) {
return <AdultGridEmpty />;
}
return (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6">
{videos.map((video) => (
<div
<Link
key={`${video.source}-${video.vod_id}`}
href={`/secret?q=${encodeURIComponent(video.vod_name)}`}
className="group cursor-pointer"
onClick={() => onVideoClick?.(video)}
onClick={(e) => {
// Allow default behavior for modifier keys (new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
e.preventDefault();
onVideoClick?.(video);
}}
style={{
contain: 'layout style paint',
contentVisibility: 'auto'
}}
>
<Card hover className="overflow-hidden p-0 h-full" blur={false}>
<div className="relative aspect-[2/3] overflow-hidden bg-[var(--glass-bg)]">
<div className="relative aspect-[2/3] overflow-hidden bg-[var(--glass-bg)] rounded-[var(--radius-2xl)]">
{video.vod_pic ? (
<Image
src={video.vod_pic}
alt={video.vod_name}
fill
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
className="object-cover transition-transform duration-300 group-hover:scale-105"
className="object-cover transition-transform duration-300 group-hover:scale-105 rounded-[var(--radius-2xl)]"
loading="eager"
unoptimized
/>
@@ -80,62 +91,51 @@ export function AdultContentGrid({
)}
</div>
</Card>
</div>
</Link>
))}
</div>
{/* Prefetch Trigger */}
{/* Prefetch Trigger - Earlier */}
{hasMore && !loading && <div ref={prefetchRef} className="h-1" />}
{/* Loading Indicator */}
{loading && (
<div className="flex justify-center py-12">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-[var(--accent-color)] border-t-transparent"></div>
<p className="text-sm text-[var(--text-color-secondary)]">...</p>
</div>
</div>
)}
{loading && <AdultGridLoading />}
{/* Intersection Observer Target */}
{hasMore && !loading && <div ref={loadMoreRef} className="h-20" />}
{/* No More Content */}
{!loading && !hasMore && videos.length > 0 && (
<div className="text-center py-12">
<p className="text-[var(--text-color-secondary)]"></p>
</div>
)}
{!hasMore && videos.length > 0 && <AdultGridNoMore />}
</>
);
}
{/* Empty State */}
{!loading && videos.length === 0 && (
<div className="text-center py-20">
<div className="flex justify-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-[var(--text-color-secondary)]"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M7 3v18" />
<path d="M3 7.5h4" />
<path d="M3 12h18" />
<path d="M3 16.5h4" />
<path d="M17 3v18" />
<path d="M17 7.5h4" />
<path d="M17 16.5h4" />
</svg>
</div>
<p className="text-[var(--text-color-secondary)]"></p>
</div>
)}
function AdultGridLoading() {
return (
<div className="flex justify-center py-12">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-[var(--accent-color)] border-t-transparent"></div>
<p className="text-sm text-[var(--text-color-secondary)]">...</p>
</div>
</div>
);
}
function AdultGridNoMore() {
return (
<div className="text-center py-12">
<p className="text-[var(--text-color-secondary)]"></p>
</div>
);
}
function AdultGridEmpty() {
// Using require to avoid top-level import issues if any, matching MovieGrid pattern
const { Icons } = require('@/components/ui/Icon');
return (
<div className="text-center py-20">
<Icons.Film size={64} className="text-[var(--text-color-secondary)] mx-auto mb-4" />
<p className="text-[var(--text-color-secondary)]"></p>
</div>
);
}
+3
View File
@@ -27,6 +27,9 @@ export const MovieCard = memo(function MovieCard({ movie, onMovieClick }: MovieC
<Link
href={`/?q=${encodeURIComponent(movie.title)}`}
onClick={(e) => {
// Allow default behavior for modifier keys (new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
e.preventDefault();
onMovieClick(movie);
}}