fix: 优化markdown解析器
This commit is contained in:
@@ -7,12 +7,9 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ExternalLink,
|
|
||||||
Grid3X3,
|
|
||||||
List
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { EnhancedMarkdownRenderer } from './EnhancedMarkdownRenderer';
|
import { EnhancedMarkdownRenderer } from './EnhancedMarkdownRenderer';
|
||||||
import { ChatMaterialCard } from './ChatMaterialCard';
|
import ImageCard from './ImageCard';
|
||||||
import { GroundingSource } from '../types/ragGrounding';
|
import { GroundingSource } from '../types/ragGrounding';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,8 +62,6 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [expandedSources, setExpandedSources] = useState(false);
|
const [expandedSources, setExpandedSources] = useState(false);
|
||||||
const [materialViewMode, setMaterialViewMode] = useState<'grid' | 'list'>('grid');
|
|
||||||
const [selectedReference, setSelectedReference] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const isUser = message.type === 'user';
|
const isUser = message.type === 'user';
|
||||||
const isAssistant = message.type === 'assistant';
|
const isAssistant = message.type === 'assistant';
|
||||||
@@ -84,11 +79,7 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
|
|||||||
}
|
}
|
||||||
}, [message.content]);
|
}, [message.content]);
|
||||||
|
|
||||||
// 处理引用点击
|
|
||||||
const handleReferenceClick = useCallback((sources: GroundingSource[], index: number) => {
|
|
||||||
setSelectedReference(selectedReference === index ? null : index);
|
|
||||||
console.log('引用点击:', { index, sources });
|
|
||||||
}, [selectedReference]);
|
|
||||||
|
|
||||||
// 处理素材卡片点击
|
// 处理素材卡片点击
|
||||||
const handleMaterialClick = useCallback((source: GroundingSource) => {
|
const handleMaterialClick = useCallback((source: GroundingSource) => {
|
||||||
@@ -138,7 +129,6 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
|
|||||||
groundingMetadata={groundingMetadata}
|
groundingMetadata={groundingMetadata}
|
||||||
enableReferences={enableReferences}
|
enableReferences={enableReferences}
|
||||||
enableMarkdown={true}
|
enableMarkdown={true}
|
||||||
onReferenceClick={handleReferenceClick}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="whitespace-pre-wrap leading-relaxed">
|
<div className="whitespace-pre-wrap leading-relaxed">
|
||||||
@@ -204,41 +194,17 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expandedSources && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
onClick={() => setMaterialViewMode('grid')}
|
|
||||||
className={`p-1 rounded ${materialViewMode === 'grid' ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
|
|
||||||
title="网格视图"
|
|
||||||
>
|
|
||||||
<Grid3X3 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setMaterialViewMode('list')}
|
|
||||||
className={`p-1 rounded ${materialViewMode === 'list' ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
|
|
||||||
title="列表视图"
|
|
||||||
>
|
|
||||||
<List className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expandedSources && (
|
{expandedSources && (
|
||||||
<div className={`
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
${materialViewMode === 'grid'
|
|
||||||
? 'material-grid compact'
|
|
||||||
: 'space-y-2'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{sources.map((source, index) => (
|
{sources.map((source, index) => (
|
||||||
<ChatMaterialCard
|
<ImageCard
|
||||||
key={index}
|
key={index}
|
||||||
source={source as GroundingSource}
|
source={source as GroundingSource}
|
||||||
size={materialViewMode === 'grid' ? 'small' : 'medium'}
|
showDetails={true}
|
||||||
showDetails={materialViewMode === 'list'}
|
className="cursor-pointer"
|
||||||
onClick={handleMaterialClick}
|
onViewLarge={() => handleMaterialClick(source as GroundingSource)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ interface EnhancedMarkdownRendererProps {
|
|||||||
export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> = ({
|
export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> = ({
|
||||||
content,
|
content,
|
||||||
groundingMetadata,
|
groundingMetadata,
|
||||||
enableReferences = true,
|
|
||||||
enableMarkdown = true,
|
enableMarkdown = true,
|
||||||
className = '',
|
className = '',
|
||||||
showStatistics = false,
|
showStatistics = false,
|
||||||
@@ -47,8 +46,7 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [validation, setValidation] = useState<ValidationResult | null>(null);
|
const [validation, setValidation] = useState<ValidationResult | null>(null);
|
||||||
const [selectedGrounding, setSelectedGrounding] = useState<any>(null);
|
const [expandedGrounding, setExpandedGrounding] = useState<any>(null);
|
||||||
const [showGroundingModal, setShowGroundingModal] = useState(false);
|
|
||||||
const [previewSource, setPreviewSource] = useState<any>(null);
|
const [previewSource, setPreviewSource] = useState<any>(null);
|
||||||
|
|
||||||
// 解析Markdown内容
|
// 解析Markdown内容
|
||||||
@@ -98,17 +96,16 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
|
|||||||
}
|
}
|
||||||
}, [parseContent, enableRealTimeParsing]);
|
}, [parseContent, enableRealTimeParsing]);
|
||||||
|
|
||||||
// 显示grounding详情
|
// 切换grounding详情展开状态
|
||||||
const showGroundingDetails = useCallback((groundingAnalysis: any) => {
|
const toggleGroundingDetails = useCallback((groundingAnalysis: any) => {
|
||||||
setSelectedGrounding(groundingAnalysis);
|
if (expandedGrounding && expandedGrounding === groundingAnalysis) {
|
||||||
setShowGroundingModal(true);
|
// 如果当前已展开相同的内容,则收起
|
||||||
}, []);
|
setExpandedGrounding(null);
|
||||||
|
} else {
|
||||||
// 关闭grounding详情
|
// 展开新的内容
|
||||||
const closeGroundingDetails = useCallback(() => {
|
setExpandedGrounding(groundingAnalysis);
|
||||||
setShowGroundingModal(false);
|
}
|
||||||
setSelectedGrounding(null);
|
}, [expandedGrounding]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 查看大图
|
// 查看大图
|
||||||
const handleViewLarge = useCallback((source: any) => {
|
const handleViewLarge = useCallback((source: any) => {
|
||||||
@@ -216,22 +213,36 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
|
|||||||
}, [groundingMetadata, parseResult]);
|
}, [groundingMetadata, parseResult]);
|
||||||
|
|
||||||
// 渲染单个Markdown节点
|
// 渲染单个Markdown节点
|
||||||
const renderNode = useCallback((node: MarkdownNode, depth: number = 0, index: number = 0): React.ReactNode => {
|
const renderNode = useCallback((node: MarkdownNode, depth: number = 0, index: number = 0, showGroundingIndicator: boolean = true): React.ReactNode => {
|
||||||
const key = `${node.range?.start?.line || 0}-${node.range?.start?.column || 0}-${node.range?.start?.offset || 0}-${depth}-${index}`;
|
const key = `${node.range?.start?.line || 0}-${node.range?.start?.column || 0}-${node.range?.start?.offset || 0}-${depth}-${index}`;
|
||||||
// 分析当前节点的引用关联
|
// 分析当前节点的引用关联
|
||||||
const groundingAnalysis = analyzeNodeGrounding(node);
|
const groundingAnalysis = analyzeNodeGrounding(node);
|
||||||
// 创建引用指示器组件
|
// 创建引用指示器组件
|
||||||
const GroundingIndicator = groundingAnalysis ? (
|
const isExpanded = expandedGrounding === groundingAnalysis;
|
||||||
|
const GroundingIndicator = (groundingAnalysis && showGroundingIndicator) ? (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center ml-1 px-1 py-0.5 text-xs bg-blue-100 text-blue-700 rounded cursor-help"
|
className={`inline-flex items-center ml-1 px-2 py-1 text-xs rounded cursor-pointer transition-all duration-200 ${
|
||||||
title={`引用了 ${groundingAnalysis.groundingInfo.sourceCount} 个来源`}
|
isExpanded
|
||||||
|
? 'bg-blue-500 text-white shadow-md'
|
||||||
|
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
||||||
|
}`}
|
||||||
|
title={`${isExpanded ? '收起' : '查看'} ${groundingAnalysis.groundingInfo.sourceCount} 个相关素材`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('📚 点击查看引用详情:', groundingAnalysis);
|
console.log('📚 点击查看引用详情:', groundingAnalysis);
|
||||||
// 显示详细的引用信息,包括图片
|
// 切换详细的引用信息展示
|
||||||
showGroundingDetails(groundingAnalysis);
|
toggleGroundingDetails(groundingAnalysis);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{groundingAnalysis.groundingInfo.sourceCount}
|
<span className="mr-1">🖼️</span>
|
||||||
|
<span>{groundingAnalysis.groundingInfo.sourceCount}</span>
|
||||||
|
<svg
|
||||||
|
className={`w-3 h-3 ml-1 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -332,7 +343,7 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
|
|||||||
case MarkdownNodeType.Strong:
|
case MarkdownNodeType.Strong:
|
||||||
return (
|
return (
|
||||||
<strong key={key}>
|
<strong key={key}>
|
||||||
{node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))}
|
{node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex, false))}
|
||||||
</strong>
|
</strong>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -424,80 +435,48 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Grounding详情内联展开 */}
|
||||||
{/* 显示引用来源信息(如果有的话) */}
|
{expandedGrounding && (
|
||||||
{enableReferences && groundingMetadata?.sources && groundingMetadata.sources.length > 0 && (
|
<div className="mt-6 border-t border-gray-200 pt-4 animate-in slide-in-from-top-2 duration-300">
|
||||||
<div className="mt-4 pt-3 border-t border-gray-200">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="text-xs text-gray-500 mb-2">
|
<h3 className="text-base font-semibold text-gray-900 flex items-center">
|
||||||
基于 {groundingMetadata.sources.length} 个来源的信息
|
<span className="mr-2">🖼️</span>
|
||||||
</div>
|
相关素材详情 ({expandedGrounding.groundingInfo?.sourceCount || 0})
|
||||||
<div className="flex flex-wrap gap-1">
|
</h3>
|
||||||
{groundingMetadata.sources.slice(0, 5).map((source, index) => (
|
|
||||||
<span
|
|
||||||
key={index}
|
|
||||||
className="inline-flex items-center px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded-full cursor-pointer hover:bg-blue-100"
|
|
||||||
title={source.title || `来源 ${index + 1}`}
|
|
||||||
onClick={() => showGroundingDetails({
|
|
||||||
groundingInfo: {
|
|
||||||
sourceCount: 1,
|
|
||||||
sources: [source]
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{groundingMetadata.sources.length > 5 && (
|
|
||||||
<span className="text-xs text-gray-400">
|
|
||||||
+{groundingMetadata.sources.length - 5} 更多
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Grounding详情模态框 */}
|
|
||||||
{showGroundingModal && selectedGrounding && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl max-h-[90vh] overflow-hidden">
|
|
||||||
{/* 模态框头部 */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">相关素材详情</h3>
|
|
||||||
<button
|
<button
|
||||||
onClick={closeGroundingDetails}
|
onClick={() => setExpandedGrounding(null)}
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
className="text-gray-400 hover:text-gray-600 transition-colors p-2 rounded-full hover:bg-gray-100"
|
||||||
|
title="收起"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 模态框内容 */}
|
<div className="bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-6 shadow-inner">
|
||||||
<div className="p-4 overflow-y-auto max-h-[calc(90vh-8rem)]">
|
{expandedGrounding.groundingInfo?.sources && expandedGrounding.groundingInfo.sources.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{selectedGrounding.groundingInfo?.sources?.map((source: any, index: number) => (
|
{expandedGrounding.groundingInfo.sources.map((source: any, index: number) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative transform hover:scale-105 transition-transform duration-200">
|
||||||
<ImageCard
|
<ImageCard
|
||||||
source={source}
|
source={source}
|
||||||
showDetails={true}
|
showDetails={true}
|
||||||
onViewLarge={handleViewLarge}
|
onViewLarge={handleViewLarge}
|
||||||
className="shadow-lg border border-gray-200 hover:shadow-xl transition-shadow"
|
className="shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300 bg-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
{/* 如果没有图片,显示提示信息 */}
|
<div className="text-center py-12 text-gray-500">
|
||||||
{(!selectedGrounding.groundingInfo?.sources || selectedGrounding.groundingInfo.sources.length === 0) && (
|
<div className="text-6xl mb-4 opacity-50">🖼️</div>
|
||||||
<div className="text-center py-8 text-gray-500">
|
<p className="text-base font-medium mb-2">暂无相关图片素材</p>
|
||||||
<div className="text-4xl mb-2">🖼️</div>
|
<p className="text-sm text-gray-400">该内容段落没有关联的图片资源</p>
|
||||||
<p>暂无相关图片素材</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 图片预览模态框 */}
|
{/* 图片预览模态框 */}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import React, { useState, useCallback } from 'react';
|
|||||||
import {
|
import {
|
||||||
Download,
|
Download,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Eye,
|
|
||||||
X,
|
X,
|
||||||
Calendar,
|
Calendar,
|
||||||
MapPin,
|
MapPin,
|
||||||
User,
|
User,
|
||||||
Shirt,
|
Shirt,
|
||||||
Image as ImageIcon,
|
|
||||||
AlertCircle
|
AlertCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GroundingSource } from '../types/ragGrounding';
|
import { GroundingSource } from '../types/ragGrounding';
|
||||||
@@ -92,13 +90,6 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
}
|
}
|
||||||
}, [onDownload, source, isDownloading]);
|
}, [onDownload, source, isDownloading]);
|
||||||
|
|
||||||
// 处理查看大图
|
|
||||||
const handleViewLarge = useCallback(() => {
|
|
||||||
if (onViewLarge) {
|
|
||||||
onViewLarge(source);
|
|
||||||
}
|
|
||||||
}, [onViewLarge, source]);
|
|
||||||
|
|
||||||
// 构建位置样式
|
// 构建位置样式
|
||||||
const positionStyle = position ? {
|
const positionStyle = position ? {
|
||||||
position: 'absolute' as const,
|
position: 'absolute' as const,
|
||||||
@@ -111,30 +102,30 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden max-w-sm ${className}`}
|
className={`bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden max-w-sm group ${className}`}
|
||||||
style={positionStyle}
|
style={positionStyle}
|
||||||
>
|
>
|
||||||
{/* 卡片头部 */}
|
{/* 图片展示区域 */}
|
||||||
<div className="flex items-center justify-between p-3 border-b border-gray-100">
|
<div className="relative bg-gray-50">
|
||||||
<div className="flex items-center space-x-2">
|
{imageUri && !imageError ? (
|
||||||
<ImageIcon className="w-4 h-4 text-pink-500" />
|
<div className="relative">
|
||||||
<h3 className="text-sm font-medium text-gray-900 truncate">{title}</h3>
|
<img
|
||||||
</div>
|
src={imageUri}
|
||||||
<div className="flex items-center space-x-1">
|
alt={description || title}
|
||||||
{onViewLarge && (
|
className={`w-full h-48 object-cover transition-all duration-300 ${
|
||||||
<button
|
imageLoaded ? 'opacity-100' : 'opacity-0'
|
||||||
onClick={handleViewLarge}
|
} group-hover:scale-105`}
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
onLoad={handleImageLoad}
|
||||||
title="查看大图"
|
onError={handleImageError}
|
||||||
>
|
/>
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</button>
|
{/* 悬浮按钮组 */}
|
||||||
)}
|
<div className="absolute top-2 right-2 flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||||
{onDownload && (
|
{onDownload && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={isDownloading}
|
disabled={isDownloading}
|
||||||
className="p-1 text-gray-400 hover:text-pink-500 transition-colors disabled:opacity-50"
|
className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all duration-200 hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
title="下载到本地"
|
title="下载到本地"
|
||||||
>
|
>
|
||||||
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
|
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
|
||||||
@@ -145,7 +136,7 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
href={imageUri}
|
href={imageUri}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="p-1 text-gray-400 hover:text-blue-500 transition-colors"
|
className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all duration-200 hover:scale-110"
|
||||||
title="在新窗口打开"
|
title="在新窗口打开"
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
@@ -154,36 +145,23 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all duration-200 hover:scale-110"
|
||||||
title="关闭"
|
title="关闭"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 图片展示区域 */}
|
{/* 加载指示器 */}
|
||||||
<div className="relative bg-gray-50">
|
|
||||||
{imageUri && !imageError ? (
|
|
||||||
<div className="relative">
|
|
||||||
<img
|
|
||||||
src={imageUri}
|
|
||||||
alt={description || title}
|
|
||||||
className={`w-full h-48 object-cover transition-opacity duration-300 ${
|
|
||||||
imageLoaded ? 'opacity-100' : 'opacity-0'
|
|
||||||
}`}
|
|
||||||
onLoad={handleImageLoad}
|
|
||||||
onError={handleImageError}
|
|
||||||
/>
|
|
||||||
{!imageLoaded && (
|
{!imageLoaded && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-pink-500"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-pink-500"></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-48 flex items-center justify-center text-gray-400">
|
<div className="h-48 flex items-center justify-center text-gray-400 bg-gray-100">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<AlertCircle className="w-8 h-8 mx-auto mb-2" />
|
<AlertCircle className="w-8 h-8 mx-auto mb-2" />
|
||||||
<p className="text-sm">图片无法加载</p>
|
<p className="text-sm">图片无法加载</p>
|
||||||
@@ -194,27 +172,29 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
|
|
||||||
{/* 详细信息区域 */}
|
{/* 详细信息区域 */}
|
||||||
{showDetails && (
|
{showDetails && (
|
||||||
<div className="p-3 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
{/* 描述 */}
|
{/* 描述 */}
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm text-gray-600 line-clamp-2">{description}</p>
|
<p className="text-sm text-gray-700 line-clamp-2 leading-relaxed">{description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 标签和信息 */}
|
||||||
|
<div className="space-y-2">
|
||||||
{/* 环境标签 */}
|
{/* 环境标签 */}
|
||||||
{environmentTags.length > 0 && (
|
{environmentTags.length > 0 && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-start space-x-2">
|
||||||
<MapPin className="w-3 h-3 text-gray-400" />
|
<MapPin className="w-3 h-3 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1 min-w-0">
|
||||||
{environmentTags.slice(0, 3).map((tag: string, index: number) => (
|
{environmentTags.slice(0, 2).map((tag: string, index: number) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full"
|
className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-md font-medium"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{environmentTags.length > 3 && (
|
{environmentTags.length > 2 && (
|
||||||
<span className="text-xs text-gray-400">+{environmentTags.length - 3}</span>
|
<span className="text-xs text-gray-500 py-1">+{environmentTags.length - 2}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,29 +202,31 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
|
|
||||||
{/* 服装类别 */}
|
{/* 服装类别 */}
|
||||||
{categories.length > 0 && (
|
{categories.length > 0 && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-start space-x-2">
|
||||||
<Shirt className="w-3 h-3 text-gray-400" />
|
<Shirt className="w-3 h-3 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1 min-w-0">
|
||||||
{categories.slice(0, 4).map((category: string, index: number) => (
|
{categories.slice(0, 2).map((category: string, index: number) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="px-2 py-0.5 bg-pink-50 text-pink-600 text-xs rounded-full"
|
className="px-2 py-1 bg-pink-50 text-pink-700 text-xs rounded-md font-medium"
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{categories.length > 4 && (
|
{categories.length > 2 && (
|
||||||
<span className="text-xs text-gray-400">+{categories.length - 4}</span>
|
<span className="text-xs text-gray-500 py-1">+{categories.length - 2}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 底部信息行 */}
|
||||||
|
<div className="flex items-center justify-between pt-1 border-t border-gray-100">
|
||||||
{/* 模特信息 */}
|
{/* 模特信息 */}
|
||||||
{models.length > 0 && (
|
{models.length > 0 && (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<User className="w-3 h-3 text-gray-400" />
|
<User className="w-3 h-3 text-gray-400" />
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-600 font-medium">
|
||||||
{models.length} 位模特
|
{models.length} 位模特
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,6 +242,8 @@ export const ImageCard: React.FC<ImageCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user