baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

View 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>
);
}