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

275 lines
10 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 } 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>
)
}