121 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|