Files
beige/frontend/src/components/review/ReviewCard.tsx
2026-07-01 13:49:28 +08:00

180 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}