审核台列表页优化: - 每个任务项显示文件名、文件大小 - 添加下载按钮支持文件下载 - 点击"审核"按钮才跳转到详情页 - 按风险等级显示不同颜色的状态标签和按钮 新增申诉处理功能: - 申诉列表页:展示所有达人申诉,支持搜索和状态筛选 - 申诉详情页:查看申诉内容、附件、原审核问题 - 处理决策面板:通过/驳回申诉并填写处理意见 - 侧边栏添加"申诉处理"菜单入口 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
349 lines
14 KiB
TypeScript
349 lines
14 KiB
TypeScript
'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>
|
||
)
|
||
}
|