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

530 lines
17 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 {
UserPlus,
ClipboardList,
CheckCircle,
PenLine,
ScanSearch,
Building2,
XCircle,
BadgeCheck,
Video,
MessageCircle,
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { cn } from '@/lib/utils'
// 消息类型
type MessageType =
| 'invite' // 代理商邀请
| 'new_task' // 新任务分配
| 'pass' // 审核通过
| 'need_fix' // 需要修改
| 'ai_complete' // AI审核完成
| 'agency_pass' // 代理商审核通过
| 'agency_reject' // 代理商审核驳回
| 'brand_pass' // 品牌方审核通过
| 'brand_reject' // 品牌方审核驳回
| 'video_ai' // 视频AI审核完成
| 'appeal' // 申诉结果
| 'video_agency_reject' // 视频代理商驳回
| 'video_brand_reject' // 视频品牌方驳回
type Message = {
id: string
type: MessageType
title: string
content: string
time: string
read: boolean
taskId?: string
hasActions?: boolean // 是否有操作按钮(邀请类型)
}
// 消息配置
const messageConfig: Record<MessageType, {
icon: React.ElementType
iconColor: string
bgColor: string
}> = {
invite: { icon: UserPlus, iconColor: 'text-purple-400', bgColor: 'bg-purple-500/20' },
new_task: { icon: ClipboardList, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' },
pass: { icon: CheckCircle, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' },
need_fix: { icon: PenLine, iconColor: 'text-accent-coral', bgColor: 'bg-accent-coral/20' },
ai_complete: { icon: ScanSearch, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' },
agency_pass: { icon: Building2, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' },
agency_reject: { icon: XCircle, iconColor: 'text-accent-coral', bgColor: 'bg-accent-coral/20' },
brand_pass: { icon: BadgeCheck, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' },
brand_reject: { icon: XCircle, iconColor: 'text-accent-coral', bgColor: 'bg-accent-coral/20' },
video_ai: { icon: Video, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' },
appeal: { icon: MessageCircle, iconColor: 'text-accent-blue', bgColor: 'bg-accent-blue/20' },
video_agency_reject: { icon: XCircle, iconColor: 'text-accent-coral', bgColor: 'bg-accent-coral/20' },
video_brand_reject: { icon: XCircle, iconColor: 'text-accent-coral', bgColor: 'bg-accent-coral/20' },
}
// 12条消息数据
const mockMessages: Message[] = [
{
id: 'msg-001',
type: 'invite',
title: '代理商邀请',
content: '「星辰传媒」邀请您成为签约达人,加入后可接收该代理商分配的推广任务',
time: '5分钟前',
read: false,
hasActions: true,
},
{
id: 'msg-002',
type: 'new_task',
title: '新任务分配',
content: '您有一个新任务【XX品牌618推广】请在3天内提交脚本',
time: '10分钟前',
read: false,
taskId: 'task-001',
},
{
id: 'msg-003',
type: 'pass',
title: '审核通过',
content: '恭喜您的视频【AA数码新品发布】已通过审核可安排发布',
time: '2小时前',
read: true,
taskId: 'task-004',
},
{
id: 'msg-004',
type: 'need_fix',
title: '需要修改',
content: '您的视频【ZZ饮品夏日】有2处需修改00:15竞品露出、00:42口播违规词点击查看详情',
time: '昨天 14:30',
read: true,
taskId: 'task-003',
},
{
id: 'msg-005',
type: 'ai_complete',
title: 'AI审核完成',
content: '您的脚本【CC服装春季款】AI预审已完成已进入代理商审核',
time: '30分钟前',
read: true,
taskId: 'task-006',
},
{
id: 'msg-006',
type: 'agency_pass',
title: '代理商审核通过',
content: '您的脚本【DD家电测评】已通过代理商审核等待品牌方终审',
time: '1小时前',
read: true,
taskId: 'task-007',
},
{
id: 'msg-007',
type: 'agency_reject',
title: '代理商审核驳回',
content: '您的脚本【HH美妆代言】被代理商驳回原因品牌调性不符请修改后重新提交',
time: '2小时前',
read: true,
taskId: 'task-010',
},
{
id: 'msg-008',
type: 'brand_pass',
title: '品牌方审核通过',
content: '恭喜您的脚本【EE食品试吃】已通过品牌方终审请在7天内上传视频',
time: '昨天 14:30',
read: true,
taskId: 'task-008',
},
{
id: 'msg-009',
type: 'brand_reject',
title: '品牌方审核驳回',
content: '您的脚本【II数码配件】被品牌方驳回原因产品卖点不够突出请修改后重新提交',
time: '昨天 16:45',
read: true,
taskId: 'task-011',
},
{
id: 'msg-010',
type: 'video_ai',
title: '视频AI审核完成',
content: '您的视频【JJ旅行vlog】AI预审已完成已进入代理商审核环节',
time: '今天 09:15',
read: true,
taskId: 'task-013',
},
{
id: 'msg-011',
type: 'appeal',
title: '申诉结果通知',
content: '您的申诉已通过AI已学习您的反馈感谢您帮助我们改进系统',
time: '3天前',
read: false,
},
{
id: 'msg-012',
type: 'video_agency_reject',
title: '视频代理商审核驳回',
content: '您的视频【KK宠物用品】被代理商驳回原因背景音乐版权问题请修改后重新提交',
time: '今天 11:20',
read: true,
taskId: 'task-014',
},
{
id: 'msg-013',
type: 'video_brand_reject',
title: '视频品牌方审核驳回',
content: '您的视频【MM厨房电器】被品牌方驳回原因产品使用场景不够真实请修改后重新提交',
time: '昨天 18:30',
read: true,
taskId: 'task-015',
},
]
// 消息卡片组件
function MessageCard({
message,
onRead,
onNavigate,
onAcceptInvite,
onIgnoreInvite,
}: {
message: Message
onRead: () => void
onNavigate: () => void
onAcceptInvite?: () => void
onIgnoreInvite?: () => void
}) {
const config = messageConfig[message.type]
const Icon = config.icon
return (
<div
className={cn(
'rounded-xl p-4 flex gap-4 cursor-pointer transition-colors',
message.read
? 'bg-transparent border border-bg-elevated'
: 'bg-bg-elevated'
)}
onClick={() => {
onRead()
if (message.taskId) onNavigate()
}}
>
{/* 图标 */}
<div className={cn('w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0', config.bgColor)}>
<Icon size={24} className={config.iconColor} />
</div>
{/* 内容 */}
<div className="flex-1 flex flex-col gap-2">
{/* 头部 */}
<div className="flex items-center justify-between">
<span className="text-[15px] font-semibold text-text-primary">{message.title}</span>
<span className="text-xs text-text-secondary">{message.time}</span>
</div>
{/* 描述 */}
<p className="text-sm text-text-secondary leading-relaxed">{message.content}</p>
{/* 邀请类型的操作按钮 */}
{message.hasActions && (
<div className="flex items-center gap-3 pt-2">
<button
type="button"
className="px-4 py-2 rounded-md bg-accent-indigo text-white text-sm font-medium hover:bg-accent-indigo/90 transition-colors"
onClick={(e) => {
e.stopPropagation()
onAcceptInvite?.()
}}
>
</button>
<button
type="button"
className="px-4 py-2 rounded-md border border-border-subtle text-text-secondary text-sm font-medium hover:bg-bg-elevated transition-colors"
onClick={(e) => {
e.stopPropagation()
onIgnoreInvite?.()
}}
>
</button>
</div>
)}
</div>
{/* 未读标记 */}
{!message.read && (
<div className="w-2.5 h-2.5 rounded-full bg-accent-indigo flex-shrink-0 mt-1" />
)}
</div>
)
}
// 邀请确认弹窗
function InviteConfirmModal({
isOpen,
type,
agencyName,
onClose,
onConfirm,
}: {
isOpen: boolean
type: 'accept' | 'ignore'
agencyName: string
onClose: () => void
onConfirm: () => void
}) {
if (!isOpen) return null
const isAccept = type === 'accept'
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-bg-card rounded-2xl p-6 w-full max-w-md mx-4 card-shadow">
<h3 className="text-xl font-bold text-text-primary mb-2">
{isAccept ? '确认接受邀请' : '确认忽略邀请'}
</h3>
<p className="text-sm text-text-secondary mb-6">
{isAccept
? `您确定要接受「${agencyName}」的签约邀请吗?接受后您将成为该代理商的签约达人,可以接收推广任务。`
: `您确定要忽略「${agencyName}」的邀请吗?您可以稍后在消息中心重新查看此邀请。`}
</p>
<div className="flex items-center gap-3">
<button
type="button"
onClick={onClose}
className="flex-1 py-3 rounded-xl bg-bg-elevated text-text-primary text-sm font-medium"
>
</button>
<button
type="button"
onClick={onConfirm}
className={cn(
'flex-1 py-3 rounded-xl text-sm font-semibold',
isAccept ? 'bg-accent-indigo text-white' : 'bg-accent-coral text-white'
)}
>
{isAccept ? '确认接受' : '确认忽略'}
</button>
</div>
</div>
</div>
)
}
// 成功提示弹窗
function SuccessModal({
isOpen,
message,
onClose,
}: {
isOpen: boolean
message: string
onClose: () => void
}) {
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-bg-card rounded-2xl p-8 w-full max-w-sm mx-4 card-shadow flex flex-col items-center gap-4">
<div className="w-16 h-16 rounded-full bg-accent-green/15 flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-accent-green" />
</div>
<p className="text-base font-semibold text-text-primary text-center">{message}</p>
<button
type="button"
onClick={onClose}
className="px-8 py-2.5 rounded-xl bg-accent-indigo text-white text-sm font-medium"
>
</button>
</div>
</div>
)
}
export default function CreatorMessagesPage() {
const router = useRouter()
const [messages, setMessages] = useState(mockMessages)
const [confirmModal, setConfirmModal] = useState<{ isOpen: boolean; type: 'accept' | 'ignore'; messageId: string }>({
isOpen: false,
type: 'accept',
messageId: '',
})
const [successModal, setSuccessModal] = useState<{ isOpen: boolean; message: string }>({
isOpen: false,
message: '',
})
const markAsRead = (id: string) => {
setMessages(prev => prev.map(msg =>
msg.id === id ? { ...msg, read: true } : msg
))
}
const markAllAsRead = () => {
setMessages(prev => prev.map(msg => ({ ...msg, read: true })))
}
// 根据消息类型跳转到对应页面
const navigateByMessage = (message: Message) => {
// 标记已读
markAsRead(message.id)
// 根据消息类型决定跳转目标
switch (message.type) {
case 'invite':
// 邀请消息不跳转,在卡片内有操作按钮
break
case 'new_task':
// 新任务 -> 跳转到任务详情(上传脚本)
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'pass':
// 审核通过 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'need_fix':
// 需要修改 -> 跳转到任务详情(查看问题)
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'ai_complete':
// AI审核完成 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'agency_pass':
// 代理商审核通过 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'agency_reject':
// 代理商审核驳回 -> 跳转到任务详情(查看驳回原因)
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'brand_pass':
// 品牌方审核通过 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'brand_reject':
// 品牌方审核驳回 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'video_ai':
// 视频AI审核完成 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'appeal':
// 申诉结果 -> 跳转到申诉中心
router.push('/creator/appeals')
break
case 'video_agency_reject':
// 视频代理商驳回 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
case 'video_brand_reject':
// 视频品牌方驳回 -> 跳转到任务详情
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
break
default:
if (message.taskId) router.push(`/creator/task/${message.taskId}`)
}
}
const handleAcceptInvite = (messageId: string) => {
setConfirmModal({ isOpen: true, type: 'accept', messageId })
}
const handleIgnoreInvite = (messageId: string) => {
setConfirmModal({ isOpen: true, type: 'ignore', messageId })
}
const handleConfirmAction = () => {
const { type, messageId } = confirmModal
setConfirmModal({ ...confirmModal, isOpen: false })
// 更新消息状态
setMessages(prev => prev.map(msg =>
msg.id === messageId ? { ...msg, hasActions: false, read: true } : msg
))
// 显示成功提示
setSuccessModal({
isOpen: true,
message: type === 'accept'
? '已成功接受邀请!您现在可以接收该代理商分配的推广任务了。'
: '已忽略该邀请。如需重新查看,请联系代理商。',
})
}
// 获取当前确认弹窗的代理商名称
const getAgencyName = () => {
const message = messages.find(m => m.id === confirmModal.messageId)
if (message?.content.includes('「')) {
const match = message.content.match(/「([^」]+)」/)
return match ? match[1] : '该代理商'
}
return '该代理商'
}
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-[24px] font-bold text-text-primary"></h1>
<p className="text-sm text-text-secondary"></p>
</div>
<button
type="button"
className="px-4 py-2.5 rounded-lg bg-bg-elevated text-text-primary text-sm"
onClick={markAllAsRead}
>
</button>
</div>
{/* 消息列表 - 可滚动 */}
<div className="bg-bg-card rounded-2xl p-4 lg:p-6 flex-1 overflow-hidden">
<div className="flex flex-col gap-4 h-full overflow-y-auto pr-2">
{messages.map((message) => (
<MessageCard
key={message.id}
message={message}
onRead={() => markAsRead(message.id)}
onNavigate={() => navigateByMessage(message)}
onAcceptInvite={() => handleAcceptInvite(message.id)}
onIgnoreInvite={() => handleIgnoreInvite(message.id)}
/>
))}
</div>
</div>
</div>
{/* 确认弹窗 */}
<InviteConfirmModal
isOpen={confirmModal.isOpen}
type={confirmModal.type}
agencyName={getAgencyName()}
onClose={() => setConfirmModal({ ...confirmModal, isOpen: false })}
onConfirm={handleConfirmAction}
/>
{/* 成功提示弹窗 */}
<SuccessModal
isOpen={successModal.isOpen}
message={successModal.message}
onClose={() => setSuccessModal({ ...successModal, isOpen: false })}
/>
</ResponsiveLayout>
)
}