Files
deerflow-code/frontend-web/src/open-canvas/components/ai-writing/MaterialItem.tsx
T
2026-05-29 11:19:58 +08:00

121 lines
4.3 KiB
TypeScript

import { useState } from 'react';
import { ChevronRight, ExternalLink, Sparkles } from 'lucide-react';
import { cn } from '@/lib/utils';
import { buildIntranetSourceUrl } from '@/core/config';
import type { AIMaterial } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
interface MaterialItemProps {
material: AIMaterial;
checked: boolean;
onToggle: (id: string) => void;
}
function relevanceColor(score: number): string {
if (score >= 0.8) return 'bg-green-500';
if (score >= 0.5) return 'bg-orange-400';
return 'bg-red-400';
}
function relevanceLabel(score: number): string {
if (score >= 0.8) return '高相关';
if (score >= 0.5) return '中相关';
return '低相关';
}
export function MaterialItem({ material, checked, onToggle }: MaterialItemProps) {
const [expanded, setExpanded] = useState(false);
const pct = Math.round(material.relevanceScore * 100);
const isSkill = material.sourceType === 'skill';
return (
<div
className={cn(
'rounded-md border text-xs transition-colors',
checked ? 'border-border bg-background' : 'border-border/50 bg-muted/30',
isSkill && checked && 'border-violet-300/70 dark:border-violet-800/60',
)}
>
{/* 头部行 */}
<div className="flex items-start gap-2 px-2.5 py-2">
{/* Checkbox */}
<input
type="checkbox"
checked={checked}
onChange={() => onToggle(material.id)}
className="mt-0.5 h-3.5 w-3.5 shrink-0 rounded border-input accent-primary cursor-pointer"
/>
{/* 标题 + 展开按钮 */}
<button
type="button"
onClick={() => setExpanded((v) => !v)}
className="flex flex-1 items-start gap-1.5 text-left"
>
{isSkill && (
<span className="mt-0.5 inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-[10px] font-medium bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-200 shrink-0">
<Sparkles className="size-2.5" />
技能
</span>
)}
<span className={cn('flex-1 font-medium leading-snug line-clamp-2', !checked && 'text-muted-foreground')}>
{material.title}
</span>
<ChevronRight
className={cn(
'mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
</button>
</div>
{/* 元信息行 */}
<div className="flex items-center gap-2 px-2.5 pb-2 pl-8">
<span className="text-muted-foreground truncate max-w-[120px]">{material.source}</span>
{material.publishedAt && (
<>
<span className="text-muted-foreground/40">·</span>
<span className="text-muted-foreground shrink-0">{material.publishedAt}</span>
</>
)}
{/* 相关度色条 */}
<div className="flex flex-1 items-center gap-1 min-w-0">
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden min-w-[40px]">
<div
className={cn('h-full rounded-full transition-all', relevanceColor(material.relevanceScore))}
style={{ width: `${pct}%` }}
/>
</div>
<span className={cn('shrink-0', relevanceColor(material.relevanceScore).replace('bg-', 'text-'))}>
{pct}%
</span>
<span className="text-muted-foreground/60 shrink-0">({relevanceLabel(material.relevanceScore)})</span>
</div>
</div>
{/* 展开内容 */}
<CollapseTransition open={expanded}>
<div className="px-2.5 pb-2.5 pl-8 space-y-1.5 border-t">
<p className="text-muted-foreground leading-relaxed pt-2 line-clamp-6">
{material.content.slice(0, 300)}
{material.content.length > 300 && '…'}
</p>
{material.recUuid && (
<button
type="button"
onClick={() =>
window.open(buildIntranetSourceUrl(material.recUuid), '_blank', 'noopener,noreferrer')
}
className="inline-flex items-center gap-1 text-primary hover:underline"
>
查看原文
<ExternalLink className="size-2.5" />
</button>
)}
</div>
</CollapseTransition>
</div>
);
}