'use client' import { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import { useToast } from '@/components/ui/Toast' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { ArrowLeft, MessageSquare, Clock, CheckCircle, XCircle, AlertTriangle, User, FileText, Video, Download, File, Send, Image as ImageIcon, Loader2 } from 'lucide-react' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { TaskResponse } from '@/types/task' // 申诉状态类型 type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected' // 申诉详情类型 interface AppealDetail { id: string taskId: string taskTitle: string creatorId: string creatorName: string creatorAvatar: string type: 'ai' | 'agency' contentType: 'script' | 'video' reason: string content: string status: AppealStatus createdAt: string appealCount: number attachments: { id: string; name: string; size: string; type: string }[] originalIssue: { type: string title: string description: string location: string } taskInfo: { projectName: string scriptFileName: string scriptFileSize: string } } // 模拟申诉详情数据 const mockAppealDetail: AppealDetail = { id: 'appeal-001', taskId: 'task-001', taskTitle: '夏日护肤推广脚本', creatorId: 'creator-001', creatorName: '小美护肤', creatorAvatar: '小', type: 'ai' as const, contentType: 'script' as const, reason: 'AI误判', content: '脚本中提到的"某品牌"是泛指,并非特指竞品,AI系统可能误解了语境。我在脚本中使用的是泛化表述,并没有提及任何具体的竞品名称。请代理商重新审核此处,谢谢!', status: 'pending' as AppealStatus, createdAt: '2026-02-06 10:30', appealCount: 1, // 附件 attachments: [ { id: 'att-001', name: '品牌授权证明.pdf', size: '1.2 MB', type: 'pdf' }, { id: 'att-002', name: '脚本原文截图.png', size: '345 KB', type: 'image' }, ], // 原审核问题 originalIssue: { type: 'ai', title: '疑似竞品提及', description: '脚本第3段提到"某品牌",可能涉及竞品露出风险。', location: '脚本第3段,第2行', }, // 相关任务信息 taskInfo: { projectName: 'XX品牌618推广', scriptFileName: '夏日护肤推广_脚本v2.docx', scriptFileSize: '245 KB', }, } // Derive a UI-compatible appeal detail from a TaskResponse function mapTaskToAppealDetail(task: TaskResponse) { const isVideoStage = task.stage.startsWith('video') const contentType: 'script' | 'video' = isVideoStage ? 'video' : 'script' const type: 'ai' | 'agency' = task.stage.includes('ai') ? 'ai' : 'agency' let status: AppealStatus = 'pending' if (task.stage === 'completed') { status = 'approved' } else if (task.stage === 'rejected') { status = 'rejected' } else if (task.stage.includes('review')) { status = 'processing' } const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).replace(/\//g, '-') } // Extract original issue from AI results if available const aiResult = isVideoStage ? task.video_ai_result : task.script_ai_result const agencyComment = isVideoStage ? task.video_agency_comment : task.script_agency_comment const originalIssueTitle = aiResult?.violations?.[0]?.type || agencyComment || '审核问题' const originalIssueDesc = aiResult?.violations?.[0]?.content || agencyComment || '' const originalIssueLocation = aiResult?.violations?.[0]?.source || '' return { id: task.id, taskId: task.id, taskTitle: task.name, creatorId: task.creator.id, creatorName: task.creator.name, creatorAvatar: task.creator.name.charAt(0), type, contentType, reason: task.appeal_reason || '申诉', content: task.appeal_reason || '', status, createdAt: formatDate(task.updated_at), appealCount: task.appeal_count, attachments: [] as { id: string; name: string; size: string; type: string }[], originalIssue: { type: type === 'ai' ? 'ai' : 'agency', title: originalIssueTitle, description: originalIssueDesc, location: originalIssueLocation, }, taskInfo: { projectName: task.project.name, scriptFileName: isVideoStage ? (task.video_file_name || '视频文件') : (task.script_file_name || '脚本文件'), scriptFileSize: '-', }, } } // 状态配置 const statusConfig: Record = { pending: { label: '待处理', color: 'text-accent-amber', bgColor: 'bg-accent-amber/15', icon: Clock }, processing: { label: '处理中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15', icon: MessageSquare }, approved: { label: '已通过', color: 'text-accent-green', bgColor: 'bg-accent-green/15', icon: CheckCircle }, rejected: { label: '已驳回', color: 'text-accent-coral', bgColor: 'bg-accent-coral/15', icon: XCircle }, } export default function AgencyAppealDetailPage() { const router = useRouter() const toast = useToast() const params = useParams() const taskId = params.id as string const [appeal, setAppeal] = useState(mockAppealDetail) const [replyContent, setReplyContent] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [loading, setLoading] = useState(true) const fetchAppeal = useCallback(async () => { if (USE_MOCK) { setAppeal(mockAppealDetail) setLoading(false) return } try { setLoading(true) const task = await api.getTask(taskId) setAppeal(mapTaskToAppealDetail(task)) } catch (err) { console.error('Failed to fetch appeal detail:', err) toast.error('加载申诉详情失败') } finally { setLoading(false) } }, [taskId, toast]) useEffect(() => { fetchAppeal() }, [fetchAppeal]) const status = statusConfig[appeal.status] const StatusIcon = status.icon const handleApprove = async () => { if (!replyContent.trim()) { toast.error('请填写处理意见') return } setIsSubmitting(true) try { if (USE_MOCK) { await new Promise(resolve => setTimeout(resolve, 1000)) } else { // Determine if this is script or video review based on the appeal's content type const isVideo = appeal.contentType === 'video' if (isVideo) { await api.reviewVideo(taskId, { action: 'pass', comment: replyContent }) } else { await api.reviewScript(taskId, { action: 'pass', comment: replyContent }) } } toast.success('申诉已通过') router.push('/agency/appeals') } catch (err) { console.error('Failed to approve appeal:', err) toast.error('操作失败,请重试') setIsSubmitting(false) } } const handleReject = async () => { if (!replyContent.trim()) { toast.error('请填写驳回原因') return } setIsSubmitting(true) try { if (USE_MOCK) { await new Promise(resolve => setTimeout(resolve, 1000)) } else { const isVideo = appeal.contentType === 'video' if (isVideo) { await api.reviewVideo(taskId, { action: 'reject', comment: replyContent }) } else { await api.reviewScript(taskId, { action: 'reject', comment: replyContent }) } } toast.success('申诉已驳回') router.push('/agency/appeals') } catch (err) { console.error('Failed to reject appeal:', err) toast.error('操作失败,请重试') setIsSubmitting(false) } } if (loading) { return (

加载中...

) } return (
{/* 顶部导航 */}

申诉处理

申诉编号: {appeal.id}

{status.label}
{/* 左侧:申诉详情 */}
{/* 申诉人信息 */} 申诉人信息
{appeal.creatorAvatar}

{appeal.creatorName}

达人ID: {appeal.creatorId}

任务名称

{appeal.taskTitle}

所属项目

{appeal.taskInfo.projectName}

内容类型

{appeal.contentType === 'script' ? :

提交时间

{appeal.createdAt}

{/* 原审核问题 */} 原审核问题
{appeal.originalIssue.type === 'ai' ? 'AI检测' : '人工审核'} {appeal.originalIssue.title}

{appeal.originalIssue.description}

{appeal.originalIssue.location && (

位置: {appeal.originalIssue.location}

)}
{/* 申诉内容 */} 申诉内容
申诉原因

{appeal.reason}

详细说明

{appeal.content}

申诉次数

{appeal.appealCount} 次

{/* 附件 */} {appeal.attachments.length > 0 && (
附件材料
{appeal.attachments.map((att) => (
{att.type === 'image' ? ( ) : ( )}

{att.name}

{att.size}

))}
)}
{/* 右侧:处理面板 */}
{/* 相关文件 */} 相关文件

{appeal.taskInfo.scriptFileName}

{appeal.taskInfo.scriptFileSize}

{/* 处理决策 */} {appeal.status === 'pending' || appeal.status === 'processing' ? ( 处理决策