180 lines
7.4 KiB
TypeScript
180 lines
7.4 KiB
TypeScript
'use client';
|
||
/**
|
||
* ReviewCard — 单条待审笔记卡
|
||
* 通过(+5) / 打回(-3,必填原因)
|
||
*/
|
||
import { useState } from 'react';
|
||
import { ReviewQueueItem } from '@/types/index';
|
||
import { ImageLightbox } from '@/components/tasks/ImageLightbox';
|
||
import { ScoreDimBars } from '@/components/tasks/ScoreDimBars';
|
||
import { TextLightbox } from '@/components/tasks/TextLightbox';
|
||
|
||
interface ReviewCardProps {
|
||
item: ReviewQueueItem;
|
||
onApprove: () => void;
|
||
onReject: () => void;
|
||
}
|
||
|
||
export function ReviewCard({ item, onApprove, onReject }: ReviewCardProps) {
|
||
const sets = item.selected_sets?.filter((s) => s.selected_text || s.selected_images.length) ?? [];
|
||
const [zoomText, setZoomText] = useState<{
|
||
content: string;
|
||
label?: string;
|
||
score?: number;
|
||
} | null>(null);
|
||
const [zoomImage, setZoomImage] = useState<{ url: string; caption?: string } | null>(null);
|
||
|
||
return (
|
||
<article
|
||
className="border border-border-default rounded-xl bg-surface-primary p-4"
|
||
aria-label={`待审笔记:${item.theme}`}
|
||
>
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span className="text-xs text-text-secondary">{item.product_name}</span>
|
||
<span className="text-xs text-text-tertiary">·</span>
|
||
<span className="text-xs text-text-secondary">{item.operator_name}</span>
|
||
</div>
|
||
<h3 className="font-medium text-text-primary text-sm line-clamp-1">{item.theme}</h3>
|
||
{sets.length === 0 && item.selected_text && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setZoomText({
|
||
content: item.selected_text!.content,
|
||
label: item.selected_text!.angle_label,
|
||
score: item.selected_text!.score?.total,
|
||
})}
|
||
className="mt-1 block w-full text-left text-xs text-text-secondary hover:text-brand-orange"
|
||
>
|
||
<span className="line-clamp-2">{item.selected_text.content}</span>
|
||
<span className="mt-1 block text-[11px] text-text-tertiary">点击看全文</span>
|
||
</button>
|
||
)}
|
||
{/* 质量分:审核可判断(C2)。合格线80=红线,不用组件默认85 */}
|
||
{sets.length === 0 && item.selected_text?.score && (
|
||
<ScoreDimBars score={item.selected_text.score} passLine={80} />
|
||
)}
|
||
</div>
|
||
{sets.length === 0 && item.selected_image && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setZoomImage({
|
||
url: item.selected_image!.url,
|
||
caption: `已选图 · 套${item.selected_image!.strategy}`,
|
||
})}
|
||
className="w-16 h-16 rounded-lg overflow-hidden flex-shrink-0 cursor-zoom-in"
|
||
aria-label="放大已选图"
|
||
>
|
||
<img src={item.selected_image.url} alt="已选图预览"
|
||
className="h-full w-full object-cover" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{sets.length > 0 && (
|
||
<div className="mt-4 grid gap-3">
|
||
{sets.map((set) => (
|
||
<section key={set.strategy} className="rounded-lg border border-border-default p-3">
|
||
<div className="mb-2 flex items-center justify-between gap-3">
|
||
<span className="text-xs font-semibold text-brand-orange">套{set.strategy}</span>
|
||
<span className="text-xs text-text-tertiary">{set.selected_images.length} 张图</span>
|
||
</div>
|
||
{set.selected_text && (
|
||
<>
|
||
<button
|
||
type="button"
|
||
onClick={() => setZoomText({
|
||
content: set.selected_text!.content,
|
||
label: `套${set.strategy} · ${set.selected_text!.angle_label}`,
|
||
score: set.selected_text!.score?.total,
|
||
})}
|
||
className="block w-full text-left text-xs text-text-secondary hover:text-brand-orange"
|
||
>
|
||
<span className="line-clamp-2">{set.selected_text.content}</span>
|
||
<span className="mt-1 block text-[11px] text-text-tertiary">点击看全文</span>
|
||
</button>
|
||
{set.selected_text.score && (
|
||
<ScoreDimBars score={set.selected_text.score} passLine={80} />
|
||
)}
|
||
</>
|
||
)}
|
||
{set.selected_images.length > 0 && (
|
||
<div className="mt-2 flex gap-2 overflow-x-auto">
|
||
{set.selected_images.filter(Boolean).map((img) => (
|
||
<button
|
||
key={img!.candidate_id}
|
||
type="button"
|
||
onClick={() => setZoomImage({
|
||
url: img!.url,
|
||
caption: `套${set.strategy}${img!.role ? ` · ${img!.role}` : ''}`,
|
||
})}
|
||
className="h-16 w-12 flex-shrink-0 cursor-zoom-in overflow-hidden rounded-md"
|
||
aria-label={`放大套${set.strategy}已选图`}
|
||
>
|
||
<img
|
||
src={img!.url}
|
||
alt={`套${set.strategy}已选图`}
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</section>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作栏 — 飞轮最强信号 */}
|
||
<div className="flex gap-3 mt-4 pt-4 border-t border-border-default">
|
||
<button onClick={onApprove}
|
||
aria-label={`通过笔记:${item.theme}`}
|
||
className="flex-1 bg-brand-green text-white rounded-lg py-2 text-sm font-medium hover:bg-green-600">
|
||
✓ 通过
|
||
</button>
|
||
<button onClick={onReject}
|
||
aria-label={`打回笔记:${item.theme}`}
|
||
className="flex-1 bg-surface-tertiary text-status-error border border-status-error rounded-lg py-2 text-sm font-medium hover:bg-red-50">
|
||
✗ 打回 + 原因
|
||
</button>
|
||
</div>
|
||
<TextLightbox
|
||
text={zoomText?.content ?? null}
|
||
label={zoomText?.label}
|
||
score={zoomText?.score}
|
||
onClose={() => setZoomText(null)}
|
||
/>
|
||
<ImageLightbox
|
||
url={zoomImage?.url ?? null}
|
||
caption={zoomImage?.caption}
|
||
onClose={() => setZoomImage(null)}
|
||
/>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
/** 审核列表骨架屏 */
|
||
export function ReviewSkeleton() {
|
||
return (
|
||
<div className="space-y-4" aria-busy="true" aria-label="加载待审队列中">
|
||
{[1, 2, 3].map((i) => (
|
||
<div key={i} className="border border-border-default rounded-xl p-4 animate-pulse space-y-3">
|
||
<div className="flex gap-4">
|
||
<div className="flex-1 space-y-2">
|
||
<div className="h-3 bg-surface-tertiary rounded w-1/3" />
|
||
<div className="h-4 bg-surface-tertiary rounded w-1/2" />
|
||
<div className="h-3 bg-surface-tertiary rounded w-3/4" />
|
||
</div>
|
||
<div className="w-16 h-16 bg-surface-tertiary rounded-lg flex-shrink-0" />
|
||
</div>
|
||
<div className="flex gap-3 pt-3 border-t border-border-default">
|
||
<div className="flex-1 h-8 bg-surface-tertiary rounded-lg" />
|
||
<div className="flex-1 h-8 bg-surface-tertiary rounded-lg" />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|