Your Name 66748d4f19 feat(agency): 优化审核台列表页并新增申诉处理功能
审核台列表页优化:
- 每个任务项显示文件名、文件大小
- 添加下载按钮支持文件下载
- 点击"审核"按钮才跳转到详情页
- 按风险等级显示不同颜色的状态标签和按钮

新增申诉处理功能:
- 申诉列表页:展示所有达人申诉,支持搜索和状态筛选
- 申诉详情页:查看申诉内容、附件、原审核问题
- 处理决策面板:通过/驳回申诉并填写处理意见
- 侧边栏添加"申诉处理"菜单入口

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:54:53 +08:00

349 lines
14 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'
import { useState } from 'react'
import { useRouter, useParams } from 'next/navigation'
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
} from 'lucide-react'
// 申诉状态类型
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
// 模拟申诉详情数据
const mockAppealDetail = {
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',
// 附件
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',
},
}
// 状态配置
const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
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 params = useParams()
const [appeal] = useState(mockAppealDetail)
const [replyContent, setReplyContent] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const status = statusConfig[appeal.status]
const StatusIcon = status.icon
const handleApprove = async () => {
if (!replyContent.trim()) {
alert('请填写处理意见')
return
}
setIsSubmitting(true)
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000))
alert('申诉已通过')
router.push('/agency/appeals')
}
const handleReject = async () => {
if (!replyContent.trim()) {
alert('请填写驳回原因')
return
}
setIsSubmitting(true)
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1000))
alert('申诉已驳回')
router.push('/agency/appeals')
}
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => router.back()}
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
>
<ArrowLeft size={20} className="text-text-secondary" />
</button>
<div>
<h1 className="text-2xl font-bold text-text-primary"></h1>
<p className="text-sm text-text-secondary mt-0.5">: {appeal.id}</p>
</div>
</div>
<div className={`flex items-center gap-2 px-4 py-2 rounded-xl ${status.bgColor}`}>
<StatusIcon size={18} className={status.color} />
<span className={`font-medium ${status.color}`}>{status.label}</span>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧:申诉详情 */}
<div className="lg:col-span-2 space-y-6">
{/* 申诉人信息 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User size={18} className="text-accent-indigo" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-accent-indigo to-purple-500 flex items-center justify-center text-white font-bold text-lg">
{appeal.creatorAvatar}
</div>
<div>
<p className="font-medium text-text-primary">{appeal.creatorName}</p>
<p className="text-sm text-text-secondary">ID: {appeal.creatorId}</p>
</div>
</div>
<div className="mt-4 pt-4 border-t border-border-subtle grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-text-tertiary"></span>
<p className="text-text-primary mt-1">{appeal.taskTitle}</p>
</div>
<div>
<span className="text-text-tertiary"></span>
<p className="text-text-primary mt-1">{appeal.taskInfo.projectName}</p>
</div>
<div>
<span className="text-text-tertiary"></span>
<p className="text-text-primary mt-1 flex items-center gap-1">
{appeal.contentType === 'script' ? <FileText size={14} /> : <Video size={14} />}
{appeal.contentType === 'script' ? '脚本' : '视频'}
</p>
</div>
<div>
<span className="text-text-tertiary"></span>
<p className="text-text-primary mt-1">{appeal.createdAt}</p>
</div>
</div>
</CardContent>
</Card>
{/* 原审核问题 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle size={18} className="text-accent-coral" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="p-4 rounded-xl bg-accent-coral/10 border border-accent-coral/20">
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-0.5 text-xs rounded bg-accent-coral/20 text-accent-coral">
{appeal.originalIssue.type === 'ai' ? 'AI检测' : '人工审核'}
</span>
<span className="font-medium text-text-primary">{appeal.originalIssue.title}</span>
</div>
<p className="text-sm text-text-secondary">{appeal.originalIssue.description}</p>
<p className="text-xs text-text-tertiary mt-2">: {appeal.originalIssue.location}</p>
</div>
</CardContent>
</Card>
{/* 申诉内容 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare size={18} className="text-accent-indigo" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<span className="text-sm text-text-tertiary"></span>
<p className="text-text-primary mt-1 font-medium">{appeal.reason}</p>
</div>
<div>
<span className="text-sm text-text-tertiary"></span>
<p className="text-text-primary mt-1 leading-relaxed">{appeal.content}</p>
</div>
{/* 附件 */}
{appeal.attachments.length > 0 && (
<div>
<span className="text-sm text-text-tertiary"></span>
<div className="mt-2 space-y-2">
{appeal.attachments.map((att) => (
<div
key={att.id}
className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated"
>
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
{att.type === 'image' ? (
<ImageIcon size={20} className="text-accent-indigo" />
) : (
<File size={20} className="text-accent-indigo" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-primary truncate">{att.name}</p>
<p className="text-xs text-text-tertiary">{att.size}</p>
</div>
<button
type="button"
className="p-2 rounded-lg hover:bg-bg-page transition-colors"
title="下载"
>
<Download size={16} className="text-text-secondary" />
</button>
</div>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
</div>
{/* 右侧:处理面板 */}
<div className="space-y-6">
{/* 相关文件 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText size={18} className="text-accent-indigo" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-elevated">
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
<File size={20} className="text-accent-indigo" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-primary truncate">
{appeal.taskInfo.scriptFileName}
</p>
<p className="text-xs text-text-tertiary">{appeal.taskInfo.scriptFileSize}</p>
</div>
<button
type="button"
className="p-2 rounded-lg hover:bg-bg-page transition-colors"
title="下载"
>
<Download size={16} className="text-text-secondary" />
</button>
</div>
</CardContent>
</Card>
{/* 处理决策 */}
{appeal.status === 'pending' || appeal.status === 'processing' ? (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm text-text-secondary mb-2 block"></label>
<textarea
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="请输入处理意见或驳回原因..."
className="w-full h-32 p-3 rounded-xl bg-bg-elevated border border-border-subtle text-text-primary placeholder-text-tertiary resize-none focus:outline-none focus:ring-2 focus:ring-accent-indigo"
/>
</div>
<div className="flex gap-3">
<Button
variant="primary"
className="flex-1 bg-accent-green hover:bg-accent-green/80"
onClick={handleApprove}
disabled={isSubmitting}
>
<CheckCircle size={16} />
</Button>
<Button
variant="secondary"
className="flex-1 border-accent-coral text-accent-coral hover:bg-accent-coral/10"
onClick={handleReject}
disabled={isSubmitting}
>
<XCircle size={16} />
</Button>
</div>
<p className="text-xs text-text-tertiary text-center">
</p>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className={`p-4 rounded-xl ${status.bgColor}`}>
<div className="flex items-center gap-2 mb-2">
<StatusIcon size={18} className={status.color} />
<span className={`font-medium ${status.color}`}>
{appeal.status === 'approved' ? '申诉已通过' : '申诉已驳回'}
</span>
</div>
<p className="text-sm text-text-secondary">
{appeal.status === 'approved'
? '经核实,达人申诉理由成立,已撤销原审核问题。'
: '经核实,原审核问题有效,申诉不成立。'}
</p>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}