Files
beige/frontend/src/components/review/ReviewCard.tsx
yangqianqian df1856d793 上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:08:13 +08:00

121 lines
4.9 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 { ReviewQueueItem } from '@/types/index';
import { ScoreDimBars } from '@/components/tasks/ScoreDimBars';
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) ?? [];
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 && (
<p className="text-xs text-text-secondary mt-1 line-clamp-2">
{item.selected_text.content}
</p>
)}
{/* 质量分:审核可判断(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 && (
<img src={item.selected_image.url} alt="已选图预览"
className="w-16 h-16 rounded-lg object-cover flex-shrink-0" />
)}
</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 && (
<>
<p className="text-xs text-text-secondary line-clamp-2">
{set.selected_text.content}
</p>
{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) => (
<img
key={img!.candidate_id}
src={img!.url}
alt={`${set.strategy}已选图`}
className="h-16 w-12 flex-shrink-0 rounded-md object-cover"
/>
))}
</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>
</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>
);
}