baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
98
frontend/src/app/review/page.tsx
Normal file
98
frontend/src/app/review/page.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
// 屏5 组长审核台:通过(+5) / 打回必填原因(-3)
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '@/lib/api';
|
||||
import { ReviewQueueItem, RejectRequest, PaginatedResponse, ApiError } from '@/types/index';
|
||||
import { RejectModal } from '@/components/review/RejectModal';
|
||||
import { ReviewCard, ReviewSkeleton } from '@/components/review/ReviewCard';
|
||||
import { getErrorAction } from '@/types/errors';
|
||||
|
||||
export default function ReviewPage() {
|
||||
const [queue, setQueue] = useState<ReviewQueueItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rejectTarget, setRejectTarget] = useState<number | null>(null);
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
|
||||
|
||||
useEffect(() => { loadQueue(); }, []);
|
||||
|
||||
async function loadQueue() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.get<PaginatedResponse<ReviewQueueItem>>('/api/v1/review/queue');
|
||||
setQueue(data.items);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message: string, type: 'success' | 'error') {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
}
|
||||
|
||||
async function handleApprove(taskId: number) {
|
||||
try {
|
||||
await api.post(`/api/v1/review/${taskId}/approve`);
|
||||
setQueue((q) => q.filter((item) => item.task_id !== taskId));
|
||||
showToast('已通过,进入成品库', 'success');
|
||||
} catch (err) {
|
||||
const apiErr = err as ApiError;
|
||||
showToast(apiErr?.message || getErrorAction(apiErr?.code).message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject(taskId: number, reason: string) {
|
||||
const req: RejectRequest = { reason };
|
||||
await api.post(`/api/v1/review/${taskId}/reject`, req);
|
||||
setQueue((q) => q.filter((item) => item.task_id !== taskId));
|
||||
showToast('已打回,任务回到挑选状态', 'success');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-text-primary">
|
||||
待审笔记{queue.length > 0 && <span className="ml-2 text-sm font-normal text-text-secondary">({queue.length})</span>}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Toast 提示 */}
|
||||
{toast && (
|
||||
<div role="alert" aria-live="assertive"
|
||||
className={`fixed top-4 right-4 z-50 px-4 py-3 rounded-xl shadow-lg text-sm text-white ${
|
||||
toast.type === 'success' ? 'bg-status-success' : 'bg-status-error'
|
||||
}`}>
|
||||
{toast.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<ReviewSkeleton />
|
||||
) : queue.length === 0 ? (
|
||||
<div className="text-center py-16 text-text-tertiary">
|
||||
<span className="text-4xl block mb-2" aria-hidden="true">✓</span>
|
||||
暂无待审笔记
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4" aria-label="待审笔记列表">
|
||||
{queue.map((item) => (
|
||||
<ReviewCard key={item.task_id} item={item}
|
||||
onApprove={() => handleApprove(item.task_id)}
|
||||
onReject={() => setRejectTarget(item.task_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 text-sm text-text-secondary bg-surface-tertiary rounded-xl px-4 py-3">
|
||||
通过的笔记进入成品库,运营可导出文案+图+标签,人工去小红书/达人处发布。
|
||||
</div>
|
||||
|
||||
{rejectTarget !== null && (
|
||||
<RejectModal taskId={rejectTarget}
|
||||
onConfirm={(reason) => handleReject(rejectTarget, reason)}
|
||||
onClose={() => setRejectTarget(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user