Your Name 2f9b7f05fd feat(creator): 完成达人端前端页面开发
- 新增申诉中心页面(列表、详情、新建申诉)
- 新增申诉次数管理页面(按任务显示配额,支持向代理商申请)
- 新增个人中心页面(达人ID复制、菜单导航)
- 新增个人信息编辑、账户设置、消息通知设置页面
- 新增帮助中心和历史记录页面
- 新增脚本提交和视频提交页面
- 优化消息中心页面(消息详情跳转)
- 优化任务详情页面布局和交互
- 更新 ResponsiveLayout、Sidebar、ReviewSteps 通用组件

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

337 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 { 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>
)
}