- 新增申诉中心页面(列表、详情、新建申诉) - 新增申诉次数管理页面(按任务显示配额,支持向代理商申请) - 新增个人中心页面(达人ID复制、菜单导航) - 新增个人信息编辑、账户设置、消息通知设置页面 - 新增帮助中心和历史记录页面 - 新增脚本提交和视频提交页面 - 优化消息中心页面(消息详情跳转) - 优化任务详情页面布局和交互 - 更新 ResponsiveLayout、Sidebar、ReviewSteps 通用组件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
337 lines
14 KiB
TypeScript
337 lines
14 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { useParams, useRouter } from 'next/navigation'
|
||
import {
|
||
ArrowLeft,
|
||
MessageCircle,
|
||
Clock,
|
||
CheckCircle,
|
||
XCircle,
|
||
FileText,
|
||
Image,
|
||
Send,
|
||
AlertTriangle
|
||
} from 'lucide-react'
|
||
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
// 申诉状态类型
|
||
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
|
||
|
||
// 申诉详情数据类型
|
||
type AppealDetail = {
|
||
id: string
|
||
taskId: string
|
||
taskTitle: string
|
||
type: 'ai' | 'agency' | 'brand'
|
||
reason: string
|
||
content: string
|
||
status: AppealStatus
|
||
createdAt: string
|
||
updatedAt?: string
|
||
result?: string
|
||
attachments?: { name: string; type: 'image' | 'document'; url: string }[]
|
||
timeline?: { time: string; action: string; operator?: string }[]
|
||
originalIssue?: { title: string; description: string }
|
||
}
|
||
|
||
// 模拟申诉详情数据
|
||
const mockAppealDetails: Record<string, AppealDetail> = {
|
||
'appeal-001': {
|
||
id: 'appeal-001',
|
||
taskId: 'task-003',
|
||
taskTitle: 'ZZ饮品夏日',
|
||
type: 'ai',
|
||
reason: '误判',
|
||
content: '视频中出现的是我们自家品牌的历史产品,并非竞品。已附上品牌授权证明。',
|
||
status: 'approved',
|
||
createdAt: '2026-02-01 10:30',
|
||
updatedAt: '2026-02-02 15:20',
|
||
result: '经核实,该产品确为品牌方授权产品,申诉通过。AI已学习此案例,后续将避免类似误判。',
|
||
attachments: [
|
||
{ name: '品牌授权书.pdf', type: 'document', url: '#' },
|
||
{ name: '产品对比图.jpg', type: 'image', url: '#' },
|
||
],
|
||
timeline: [
|
||
{ time: '2026-02-01 10:30', action: '提交申诉' },
|
||
{ time: '2026-02-01 14:15', action: '进入处理队列' },
|
||
{ time: '2026-02-02 09:00', action: '开始审核', operator: '审核员 A' },
|
||
{ time: '2026-02-02 15:20', action: '申诉通过', operator: '审核员 A' },
|
||
],
|
||
originalIssue: {
|
||
title: '检测到竞品 Logo',
|
||
description: '画面中 0:15-0:18 出现竞品「百事可乐」的 Logo,可能造成合规风险。',
|
||
},
|
||
},
|
||
'appeal-002': {
|
||
id: 'appeal-002',
|
||
taskId: 'task-010',
|
||
taskTitle: 'GG智能手表',
|
||
type: 'agency',
|
||
reason: '审核标准不清晰',
|
||
content: '代理商反馈品牌调性不符,但Brief中并未明确说明科技专业形象的具体要求。请明确审核标准。',
|
||
status: 'processing',
|
||
createdAt: '2026-02-04 09:15',
|
||
timeline: [
|
||
{ time: '2026-02-04 09:15', action: '提交申诉' },
|
||
{ time: '2026-02-04 11:30', action: '进入处理队列' },
|
||
{ time: '2026-02-05 10:00', action: '开始审核', operator: '审核员 B' },
|
||
],
|
||
originalIssue: {
|
||
title: '品牌调性不符',
|
||
description: '脚本整体风格偏向娱乐化,与品牌科技专业形象不匹配。',
|
||
},
|
||
},
|
||
'appeal-003': {
|
||
id: 'appeal-003',
|
||
taskId: 'task-011',
|
||
taskTitle: 'HH美妆代言',
|
||
type: 'brand',
|
||
reason: '创意理解差异',
|
||
content: '品牌方认为创意不够新颖,但该创意形式在同类型产品推广中效果显著,已附上数据支持。',
|
||
status: 'pending',
|
||
createdAt: '2026-02-05 14:00',
|
||
attachments: [
|
||
{ name: '同类案例数据.xlsx', type: 'document', url: '#' },
|
||
],
|
||
timeline: [
|
||
{ time: '2026-02-05 14:00', action: '提交申诉' },
|
||
],
|
||
originalIssue: {
|
||
title: '创意不够新颖',
|
||
description: '脚本采用的是常见的口播形式,缺乏创新点和记忆点。',
|
||
},
|
||
},
|
||
'appeal-004': {
|
||
id: 'appeal-004',
|
||
taskId: 'task-013',
|
||
taskTitle: 'JJ旅行vlog',
|
||
type: 'agency',
|
||
reason: '版权问题异议',
|
||
content: '使用的背景音乐来自无版权音乐库 Epidemic Sound,已购买商用授权。附上授权证明截图。',
|
||
status: 'rejected',
|
||
createdAt: '2026-01-28 11:30',
|
||
updatedAt: '2026-01-30 16:45',
|
||
result: '经核实,该音乐虽有授权,但授权范围不包含商业广告用途。建议更换音乐后重新提交。',
|
||
attachments: [
|
||
{ name: '授权截图.png', type: 'image', url: '#' },
|
||
],
|
||
timeline: [
|
||
{ time: '2026-01-28 11:30', action: '提交申诉' },
|
||
{ time: '2026-01-28 15:00', action: '进入处理队列' },
|
||
{ time: '2026-01-29 09:30', action: '开始审核', operator: '审核员 C' },
|
||
{ time: '2026-01-30 16:45', action: '申诉驳回', operator: '审核员 C' },
|
||
],
|
||
originalIssue: {
|
||
title: '背景音乐版权问题',
|
||
description: '视频中使用的背景音乐「XXX」存在版权风险,平台可能会限流或下架。',
|
||
},
|
||
},
|
||
}
|
||
|
||
// 状态配置
|
||
const statusConfig: Record<AppealStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
|
||
pending: { label: '待处理', color: 'text-amber-500', bgColor: 'bg-amber-500/15', icon: Clock },
|
||
processing: { label: '处理中', color: 'text-accent-indigo', bgColor: 'bg-accent-indigo/15', icon: MessageCircle },
|
||
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 },
|
||
}
|
||
|
||
// 类型配置
|
||
const typeConfig: Record<string, { label: string; color: string }> = {
|
||
ai: { label: 'AI审核', color: 'text-accent-indigo' },
|
||
agency: { label: '代理商审核', color: 'text-purple-400' },
|
||
brand: { label: '品牌方审核', color: 'text-accent-blue' },
|
||
}
|
||
|
||
export default function AppealDetailPage() {
|
||
const params = useParams()
|
||
const router = useRouter()
|
||
const appealId = params.id as string
|
||
const [newComment, setNewComment] = useState('')
|
||
|
||
const appeal = mockAppealDetails[appealId]
|
||
|
||
if (!appeal) {
|
||
return (
|
||
<ResponsiveLayout role="creator">
|
||
<div className="flex items-center justify-center h-full">
|
||
<div className="flex flex-col items-center gap-4">
|
||
<XCircle className="w-16 h-16 text-text-tertiary" />
|
||
<p className="text-lg text-text-secondary">申诉记录不存在</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => router.back()}
|
||
className="px-6 py-2.5 rounded-xl bg-accent-indigo text-white text-sm font-medium"
|
||
>
|
||
返回申诉列表
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</ResponsiveLayout>
|
||
)
|
||
}
|
||
|
||
const status = statusConfig[appeal.status]
|
||
const type = typeConfig[appeal.type]
|
||
const StatusIcon = status.icon
|
||
|
||
return (
|
||
<ResponsiveLayout role="creator">
|
||
<div className="flex flex-col gap-6 h-full">
|
||
{/* 顶部栏 */}
|
||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||
<div className="flex flex-col gap-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => router.back()}
|
||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-elevated text-text-secondary text-sm hover:bg-bg-card transition-colors w-fit mb-2"
|
||
>
|
||
<ArrowLeft className="w-4 h-4" />
|
||
返回
|
||
</button>
|
||
<h1 className="text-xl lg:text-[28px] font-bold text-text-primary">申诉详情</h1>
|
||
<p className="text-sm lg:text-[15px] text-text-secondary">申诉编号: {appeal.id}</p>
|
||
</div>
|
||
<div className={cn('px-4 py-2 rounded-xl flex items-center gap-2', status.bgColor)}>
|
||
<StatusIcon className={cn('w-5 h-5', status.color)} />
|
||
<span className={cn('font-semibold', status.color)}>{status.label}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内容区 - 响应式布局 */}
|
||
<div className="flex flex-col lg:flex-row gap-6 flex-1 min-h-0 overflow-y-auto lg:overflow-hidden">
|
||
{/* 左侧:申诉信息 */}
|
||
<div className="flex-1 flex flex-col gap-5 lg:overflow-y-auto lg:pr-2">
|
||
{/* 原始问题 */}
|
||
{appeal.originalIssue && (
|
||
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow">
|
||
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">原始审核问题</h3>
|
||
<div className="bg-accent-coral/10 rounded-xl p-4">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<AlertTriangle className="w-5 h-5 text-accent-coral" />
|
||
<span className="font-semibold text-text-primary">{appeal.originalIssue.title}</span>
|
||
</div>
|
||
<p className="text-sm text-text-secondary">{appeal.originalIssue.description}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 申诉内容 */}
|
||
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow">
|
||
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">申诉内容</h3>
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex flex-col lg:flex-row lg:items-center gap-2 lg:gap-6">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-text-tertiary">关联任务:</span>
|
||
<span className="text-sm font-medium text-text-primary">{appeal.taskTitle}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-text-tertiary">申诉对象:</span>
|
||
<span className={cn('text-sm font-medium', type.color)}>{type.label}</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm text-text-tertiary">申诉原因:</span>
|
||
<span className="text-sm font-medium text-text-primary">{appeal.reason}</span>
|
||
</div>
|
||
<div className="bg-bg-elevated rounded-xl p-4">
|
||
<p className="text-sm text-text-secondary leading-relaxed">{appeal.content}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 附件 */}
|
||
{appeal.attachments && appeal.attachments.length > 0 && (
|
||
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow">
|
||
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">证明材料</h3>
|
||
<div className="flex flex-wrap gap-3">
|
||
{appeal.attachments.map((attachment, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex items-center gap-3 px-4 py-3 bg-bg-elevated rounded-xl cursor-pointer hover:bg-bg-page transition-colors"
|
||
>
|
||
{attachment.type === 'image' ? (
|
||
<Image className="w-5 h-5 text-accent-indigo" />
|
||
) : (
|
||
<FileText className="w-5 h-5 text-accent-indigo" />
|
||
)}
|
||
<span className="text-sm text-text-primary">{attachment.name}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 处理结果 */}
|
||
{appeal.result && (
|
||
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow">
|
||
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">处理结果</h3>
|
||
<div className={cn(
|
||
'rounded-xl p-4',
|
||
appeal.status === 'approved' ? 'bg-accent-green/10' : 'bg-accent-coral/10'
|
||
)}>
|
||
<p className="text-sm text-text-secondary leading-relaxed">{appeal.result}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 补充说明(处理中状态可用) */}
|
||
{(appeal.status === 'pending' || appeal.status === 'processing') && (
|
||
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow">
|
||
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">补充说明</h3>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="text"
|
||
placeholder="输入补充说明..."
|
||
value={newComment}
|
||
onChange={(e) => setNewComment(e.target.value)}
|
||
className="flex-1 px-4 py-3 bg-bg-elevated rounded-xl text-sm text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="px-5 py-3 rounded-xl bg-accent-indigo text-white text-sm font-medium flex items-center gap-2"
|
||
>
|
||
<Send className="w-4 h-4" />
|
||
发送
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧:时间线 */}
|
||
<div className="lg:w-[320px] lg:flex-shrink-0">
|
||
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow lg:h-full">
|
||
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-5">处理进度</h3>
|
||
<div className="flex flex-col gap-0">
|
||
{appeal.timeline?.map((item, index) => (
|
||
<div key={index} className="flex gap-4">
|
||
<div className="flex flex-col items-center">
|
||
<div className={cn(
|
||
'w-3 h-3 rounded-full',
|
||
index === (appeal.timeline?.length || 0) - 1 ? 'bg-accent-indigo' : 'bg-text-tertiary'
|
||
)} />
|
||
{index < (appeal.timeline?.length || 0) - 1 && (
|
||
<div className="w-0.5 h-16 bg-border-subtle" />
|
||
)}
|
||
</div>
|
||
<div className="flex flex-col gap-1 pb-6">
|
||
<span className="text-xs text-text-tertiary">{item.time}</span>
|
||
<span className="text-sm font-medium text-text-primary">{item.action}</span>
|
||
{item.operator && (
|
||
<span className="text-xs text-text-secondary">{item.operator}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ResponsiveLayout>
|
||
)
|
||
}
|