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

396 lines
18 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, useSearchParams } from 'next/navigation'
import {
ArrowLeft,
Upload,
X,
FileText,
Image,
AlertTriangle,
CheckCircle
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { cn } from '@/lib/utils'
// 申诉原因选项
const appealReasons = [
{ id: 'misjudge', label: '误判', description: 'AI或审核员误判了内容' },
{ id: 'unclear', label: '标准不清晰', description: '审核标准不明确或有歧义' },
{ id: 'evidence', label: '有证据支持', description: '有证据证明内容符合要求' },
{ id: 'context', label: '上下文理解', description: '审核未考虑完整上下文' },
{ id: 'other', label: '其他原因', description: '其他需要说明的情况' },
]
// 任务信息模拟从URL参数获取
const getTaskInfo = (taskId: string) => {
const tasks: Record<string, { title: string; issue: string; issueDesc: string; type: string; appealRemaining: number; agencyName: string }> = {
'task-003': {
title: 'ZZ饮品夏日',
issue: '检测到竞品提及',
issueDesc: '脚本第3段提及了竞品「百事可乐」可能造成品牌冲突风险。',
type: 'ai',
appealRemaining: 1,
agencyName: '星辰传媒',
},
'task-010': {
title: 'GG智能手表',
issue: '品牌调性不符',
issueDesc: '脚本整体风格偏向娱乐化,与品牌科技专业形象不匹配。',
type: 'agency',
appealRemaining: 0,
agencyName: '星辰传媒',
},
'task-011': {
title: 'HH美妆代言',
issue: '创意不够新颖',
issueDesc: '脚本采用的是常见的口播形式,缺乏创新点和记忆点。',
type: 'brand',
appealRemaining: 1,
agencyName: '晨曦文化',
},
'task-013': {
title: 'JJ旅行vlog',
issue: '背景音乐版权问题',
issueDesc: '视频中使用的背景音乐存在版权风险。',
type: 'agency',
appealRemaining: 2,
agencyName: '晨曦文化',
},
'task-015': {
title: 'LL厨房电器',
issue: '使用场景不真实',
issueDesc: '视频中的厨房场景过于整洁,缺乏真实感。',
type: 'brand',
appealRemaining: 0,
agencyName: '星辰传媒',
},
}
return tasks[taskId] || { title: '未知任务', issue: '未知问题', issueDesc: '', type: 'ai', appealRemaining: 0, agencyName: '未知代理商' }
}
export default function NewAppealPage() {
const router = useRouter()
const searchParams = useSearchParams()
const taskId = searchParams.get('taskId') || ''
const taskInfo = getTaskInfo(taskId)
const [selectedReason, setSelectedReason] = useState<string>('')
const [content, setContent] = useState('')
const [attachments, setAttachments] = useState<{ name: string; type: 'image' | 'document' }[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmitted, setIsSubmitted] = useState(false)
const [isRequestingQuota, setIsRequestingQuota] = useState(false)
const [quotaRequested, setQuotaRequested] = useState(false)
const hasAppealQuota = taskInfo.appealRemaining > 0
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (files) {
const newAttachments = Array.from(files).map(file => ({
name: file.name,
type: file.type.startsWith('image/') ? 'image' as const : 'document' as const,
}))
setAttachments([...attachments, ...newAttachments])
}
}
const removeAttachment = (index: number) => {
setAttachments(attachments.filter((_, i) => i !== index))
}
const handleSubmit = async () => {
if (!selectedReason || !content.trim()) return
setIsSubmitting(true)
// 模拟提交
await new Promise(resolve => setTimeout(resolve, 1500))
setIsSubmitting(false)
setIsSubmitted(true)
// 2秒后跳转到申诉列表
setTimeout(() => {
router.push('/creator/appeals')
}, 2000)
}
const canSubmit = selectedReason && content.trim().length >= 20 && hasAppealQuota
// 申请增加申诉次数
const handleRequestQuota = async () => {
setIsRequestingQuota(true)
await new Promise(resolve => setTimeout(resolve, 1000))
setIsRequestingQuota(false)
setQuotaRequested(true)
}
// 提交成功界面
if (isSubmitted) {
return (
<ResponsiveLayout role="creator">
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-6 max-w-md text-center">
<div className="w-20 h-20 rounded-full bg-accent-green/15 flex items-center justify-center">
<CheckCircle className="w-10 h-10 text-accent-green" />
</div>
<div className="flex flex-col gap-2">
<h2 className="text-2xl font-bold text-text-primary"></h2>
<p className="text-text-secondary">
1-3
</p>
</div>
<p className="text-sm text-text-tertiary">...</p>
</div>
</div>
</ResponsiveLayout>
)
}
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"></p>
</div>
<div className={cn(
'flex items-center gap-2 px-4 py-2 rounded-xl',
hasAppealQuota ? 'bg-accent-indigo/15' : 'bg-accent-coral/15'
)}>
<AlertTriangle className={cn('w-5 h-5', hasAppealQuota ? 'text-accent-indigo' : 'text-accent-coral')} />
<span className={cn('text-sm font-medium', hasAppealQuota ? 'text-accent-indigo' : 'text-accent-coral')}>
{taskInfo.appealRemaining}
</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">
{/* 关联任务 */}
<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-bg-elevated rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-base font-semibold text-text-primary">{taskInfo.title}</span>
<span className="px-2.5 py-1 rounded-full text-xs font-medium bg-accent-coral/15 text-accent-coral">
{taskInfo.type === 'ai' ? 'AI审核' : taskInfo.type === 'agency' ? '代理商审核' : '品牌方审核'}
</span>
</div>
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-accent-coral flex-shrink-0 mt-0.5" />
<div>
<span className="text-sm font-medium text-text-primary">{taskInfo.issue}</span>
<p className="text-xs text-text-secondary mt-1">{taskInfo.issueDesc}</p>
</div>
</div>
</div>
</div>
{/* 申诉次数不足提示 */}
{!hasAppealQuota && (
<div className="bg-accent-coral/10 border border-accent-coral/30 rounded-2xl p-4 lg:p-6">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-accent-coral flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="text-base font-semibold text-accent-coral mb-2"></h3>
<p className="text-sm text-text-secondary mb-4">
{taskInfo.agencyName}
</p>
{quotaRequested ? (
<div className="flex items-center gap-2 text-accent-green">
<CheckCircle className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</div>
) : (
<button
type="button"
onClick={handleRequestQuota}
disabled={isRequestingQuota}
className="px-4 py-2 bg-accent-coral text-white rounded-lg text-sm font-medium hover:bg-accent-coral/90 transition-colors disabled:opacity-50"
>
{isRequestingQuota ? '申请中...' : '申请增加申诉次数'}
</button>
)}
</div>
</div>
</div>
)}
{/* 申诉原因 */}
<div className={cn('bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow', !hasAppealQuota && 'opacity-50 pointer-events-none')}>
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4"> <span className="text-accent-coral">*</span></h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{appealReasons.map((reason) => (
<div
key={reason.id}
onClick={() => setSelectedReason(reason.id)}
className={cn(
'p-4 rounded-xl border-2 cursor-pointer transition-all',
selectedReason === reason.id
? 'border-accent-indigo bg-accent-indigo/5'
: 'border-border-subtle hover:border-text-tertiary'
)}
>
<span className="text-sm font-semibold text-text-primary">{reason.label}</span>
<p className="text-xs text-text-tertiary mt-1">{reason.description}</p>
</div>
))}
</div>
</div>
{/* 申诉说明 */}
<div className={cn('bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow', !hasAppealQuota && 'opacity-50 pointer-events-none')}>
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">
<span className="text-accent-coral">*</span>
<span className="text-xs text-text-tertiary font-normal ml-2">20</span>
</h3>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="请详细描述您的申诉理由,包括为什么您认为审核结果不合理..."
className="w-full h-32 lg:h-40 p-4 bg-bg-elevated rounded-xl text-sm text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo resize-none"
/>
<div className="flex justify-end mt-2">
<span className={cn(
'text-xs',
content.length >= 20 ? 'text-text-tertiary' : 'text-accent-coral'
)}>
{content.length}/20
</span>
</div>
</div>
{/* 证明材料 */}
<div className={cn('bg-bg-card rounded-2xl p-4 lg:p-6 card-shadow', !hasAppealQuota && 'opacity-50 pointer-events-none')}>
<h3 className="text-base lg:text-lg font-semibold text-text-primary mb-4">
<span className="text-xs text-text-tertiary font-normal"></span>
</h3>
<div className="flex flex-wrap gap-3">
{attachments.map((file, index) => (
<div
key={index}
className="flex items-center gap-2 px-3 py-2 bg-bg-elevated rounded-lg"
>
{file.type === 'image' ? (
<Image className="w-4 h-4 text-accent-indigo" />
) : (
<FileText className="w-4 h-4 text-accent-indigo" />
)}
<span className="text-sm text-text-primary max-w-[150px] truncate">{file.name}</span>
<button
type="button"
onClick={() => removeAttachment(index)}
className="w-5 h-5 rounded-full bg-bg-page flex items-center justify-center hover:bg-accent-coral/15"
>
<X className="w-3 h-3 text-text-tertiary" />
</button>
</div>
))}
<label className="flex items-center gap-2 px-4 py-2 bg-bg-elevated rounded-lg cursor-pointer hover:bg-bg-page transition-colors">
<Upload className="w-4 h-4 text-accent-indigo" />
<span className="text-sm text-accent-indigo"></span>
<input
type="file"
multiple
accept="image/*,.pdf,.doc,.docx"
onChange={handleFileUpload}
className="hidden"
/>
</label>
</div>
<p className="text-xs text-text-tertiary mt-3">PDFWord 10MB</p>
</div>
{/* 移动端提交按钮 */}
<div className="lg:hidden">
<button
type="button"
onClick={handleSubmit}
disabled={!canSubmit || isSubmitting}
className={cn(
'w-full py-4 rounded-xl text-base font-semibold',
canSubmit && !isSubmitting
? 'bg-accent-indigo text-white'
: 'bg-bg-elevated text-text-tertiary'
)}
>
{isSubmitting ? '提交中...' : '提交申诉'}
</button>
</div>
</div>
{/* 右侧:提交信息(仅桌面端显示) */}
<div className="hidden lg:block lg:w-[320px] lg:flex-shrink-0">
<div className="bg-bg-card rounded-2xl p-6 card-shadow sticky top-0">
<h3 className="text-lg font-semibold text-text-primary mb-5"></h3>
<div className="flex flex-col gap-4 mb-6">
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-text-primary">{taskInfo.title}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-text-primary">
{appealReasons.find(r => r.id === selectedReason)?.label || '未选择'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className={cn(
'text-sm',
content.length >= 20 ? 'text-accent-green' : 'text-text-tertiary'
)}>
{content.length}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-text-tertiary"></span>
<span className="text-sm text-text-primary">{attachments.length} </span>
</div>
</div>
<div className="bg-amber-500/10 rounded-xl p-4 mb-6">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" />
<p className="text-xs text-text-secondary">
1
</p>
</div>
</div>
<button
type="button"
onClick={handleSubmit}
disabled={!canSubmit || isSubmitting}
className={cn(
'w-full py-4 rounded-xl text-base font-semibold transition-colors',
canSubmit && !isSubmitting
? 'bg-accent-indigo text-white hover:bg-accent-indigo/90'
: 'bg-bg-elevated text-text-tertiary cursor-not-allowed'
)}
>
{isSubmitting ? '提交中...' : '提交申诉'}
</button>
</div>
</div>
</div>
</div>
</ResponsiveLayout>
)
}