- 新增申诉中心页面(列表、详情、新建申诉) - 新增申诉次数管理页面(按任务显示配额,支持向代理商申请) - 新增个人中心页面(达人ID复制、菜单导航) - 新增个人信息编辑、账户设置、消息通知设置页面 - 新增帮助中心和历史记录页面 - 新增脚本提交和视频提交页面 - 优化消息中心页面(消息详情跳转) - 优化任务详情页面布局和交互 - 更新 ResponsiveLayout、Sidebar、ReviewSteps 通用组件 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import {
|
||
MessageCircle,
|
||
Clock,
|
||
CheckCircle,
|
||
XCircle,
|
||
ChevronRight,
|
||
AlertTriangle,
|
||
Filter,
|
||
Search
|
||
} from 'lucide-react'
|
||
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
// 申诉状态类型
|
||
type AppealStatus = 'pending' | 'processing' | 'approved' | 'rejected'
|
||
|
||
// 申诉数据类型
|
||
type Appeal = {
|
||
id: string
|
||
taskId: string
|
||
taskTitle: string
|
||
type: 'ai' | 'agency' | 'brand'
|
||
reason: string
|
||
content: string
|
||
status: AppealStatus
|
||
createdAt: string
|
||
updatedAt?: string
|
||
result?: string
|
||
}
|
||
|
||
// 模拟申诉数据
|
||
const mockAppeals: Appeal[] = [
|
||
{
|
||
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已学习此案例。',
|
||
},
|
||
{
|
||
id: 'appeal-002',
|
||
taskId: 'task-010',
|
||
taskTitle: 'GG智能手表',
|
||
type: 'agency',
|
||
reason: '审核标准不清晰',
|
||
content: '代理商反馈品牌调性不符,但Brief中并未明确说明科技专业形象的具体要求。请明确审核标准。',
|
||
status: 'processing',
|
||
createdAt: '2026-02-04 09:15',
|
||
},
|
||
{
|
||
id: 'appeal-003',
|
||
taskId: 'task-011',
|
||
taskTitle: 'HH美妆代言',
|
||
type: 'brand',
|
||
reason: '创意理解差异',
|
||
content: '品牌方认为创意不够新颖,但该创意形式在同类型产品推广中效果显著,已附上数据支持。',
|
||
status: 'pending',
|
||
createdAt: '2026-02-05 14:00',
|
||
},
|
||
{
|
||
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: '经核实,该音乐虽有授权,但授权范围不包含商业广告用途。建议更换音乐后重新提交。',
|
||
},
|
||
]
|
||
|
||
// 状态配置
|
||
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' },
|
||
}
|
||
|
||
// 申诉卡片组件
|
||
function AppealCard({ appeal, onClick }: { appeal: Appeal; onClick: () => void }) {
|
||
const status = statusConfig[appeal.status]
|
||
const type = typeConfig[appeal.type]
|
||
const StatusIcon = status.icon
|
||
|
||
return (
|
||
<div
|
||
className="bg-bg-card rounded-2xl p-5 card-shadow cursor-pointer hover:bg-bg-elevated/30 transition-colors"
|
||
onClick={onClick}
|
||
>
|
||
{/* 头部 */}
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className={cn('w-10 h-10 rounded-xl flex items-center justify-center', status.bgColor)}>
|
||
<StatusIcon className={cn('w-5 h-5', status.color)} />
|
||
</div>
|
||
<div className="flex flex-col gap-0.5">
|
||
<span className="text-base font-semibold text-text-primary">{appeal.taskTitle}</span>
|
||
<span className="text-xs text-text-tertiary">申诉编号: {appeal.id}</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<span className={cn('px-2.5 py-1 rounded-full text-xs font-medium', status.bgColor, status.color)}>
|
||
{status.label}
|
||
</span>
|
||
<ChevronRight className="w-5 h-5 text-text-tertiary" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内容 */}
|
||
<div className="flex flex-col gap-3">
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<span className="text-text-tertiary">申诉对象:</span>
|
||
<span className={cn('font-medium', type.color)}>{type.label}</span>
|
||
</div>
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<span className="text-text-tertiary">申诉原因:</span>
|
||
<span className="text-text-primary">{appeal.reason}</span>
|
||
</div>
|
||
<p className="text-sm text-text-secondary line-clamp-2">{appeal.content}</p>
|
||
</div>
|
||
|
||
{/* 底部时间 */}
|
||
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border-subtle">
|
||
<span className="text-xs text-text-tertiary">提交时间: {appeal.createdAt}</span>
|
||
{appeal.updatedAt && (
|
||
<span className="text-xs text-text-tertiary">更新时间: {appeal.updatedAt}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 申诉次数入口卡片
|
||
function AppealQuotaEntryCard({ onClick }: { onClick: () => void }) {
|
||
return (
|
||
<div
|
||
className="bg-bg-card rounded-2xl p-5 card-shadow cursor-pointer hover:bg-bg-elevated/30 transition-colors"
|
||
onClick={onClick}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
|
||
<AlertTriangle className="w-5 h-5 text-accent-indigo" />
|
||
</div>
|
||
<div className="flex flex-col gap-0.5">
|
||
<span className="text-base font-semibold text-text-primary">申诉次数管理</span>
|
||
<span className="text-sm text-text-secondary">查看各任务的申诉次数,向代理商申请增加</span>
|
||
</div>
|
||
</div>
|
||
<ChevronRight className="w-5 h-5 text-text-tertiary" />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function CreatorAppealsPage() {
|
||
const router = useRouter()
|
||
const [filter, setFilter] = useState<AppealStatus | 'all'>('all')
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [appeals] = useState<Appeal[]>(mockAppeals)
|
||
|
||
// 搜索和筛选
|
||
const filteredAppeals = appeals.filter(appeal => {
|
||
const matchesSearch = searchQuery === '' ||
|
||
appeal.taskTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
appeal.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
appeal.reason.toLowerCase().includes(searchQuery.toLowerCase())
|
||
const matchesFilter = filter === 'all' || appeal.status === filter
|
||
return matchesSearch && matchesFilter
|
||
})
|
||
|
||
const handleAppealClick = (appealId: string) => {
|
||
router.push(`/creator/appeals/${appealId}`)
|
||
}
|
||
|
||
// 跳转到申诉次数管理页面
|
||
const handleGoToQuotaPage = () => {
|
||
router.push('/creator/appeal-quota')
|
||
}
|
||
|
||
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">
|
||
<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="flex items-center gap-3">
|
||
<div className="flex items-center gap-2 px-4 py-2.5 bg-bg-card rounded-xl border border-border-subtle">
|
||
<Search className="w-[18px] h-[18px] text-text-secondary" />
|
||
<input
|
||
type="text"
|
||
placeholder="搜索申诉..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="bg-transparent text-sm text-text-primary placeholder-text-tertiary focus:outline-none w-32"
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-2 px-4 py-2.5 bg-bg-card rounded-xl border border-border-subtle">
|
||
<Filter className="w-[18px] h-[18px] text-text-secondary" />
|
||
<select
|
||
value={filter}
|
||
onChange={(e) => setFilter(e.target.value as AppealStatus | 'all')}
|
||
className="bg-transparent text-sm text-text-primary focus:outline-none"
|
||
>
|
||
<option value="all">全部状态</option>
|
||
<option value="pending">待处理</option>
|
||
<option value="processing">处理中</option>
|
||
<option value="approved">已通过</option>
|
||
<option value="rejected">已驳回</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 申诉次数管理入口 */}
|
||
<AppealQuotaEntryCard onClick={handleGoToQuotaPage} />
|
||
|
||
{/* 申诉列表 */}
|
||
<div className="flex flex-col gap-4 flex-1 overflow-y-auto pr-2">
|
||
<h2 className="text-lg font-semibold text-text-primary">申诉记录 ({filteredAppeals.length})</h2>
|
||
{filteredAppeals.length > 0 ? (
|
||
filteredAppeals.map((appeal) => (
|
||
<AppealCard
|
||
key={appeal.id}
|
||
appeal={appeal}
|
||
onClick={() => handleAppealClick(appeal.id)}
|
||
/>
|
||
))
|
||
) : (
|
||
<div className="flex flex-col items-center justify-center py-16">
|
||
<MessageCircle className="w-12 h-12 text-text-tertiary/50 mb-4" />
|
||
<p className="text-text-secondary text-center">
|
||
{searchQuery || filter !== 'all'
|
||
? '没有找到匹配的申诉记录'
|
||
: '暂无申诉记录'}
|
||
</p>
|
||
{(searchQuery || filter !== 'all') && (
|
||
<button
|
||
type="button"
|
||
onClick={() => { setSearchQuery(''); setFilter('all'); }}
|
||
className="mt-3 text-sm text-accent-indigo hover:underline"
|
||
>
|
||
清除筛选条件
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ResponsiveLayout>
|
||
)
|
||
}
|