feat: 完成代理商/品牌方前端及文档更新
代理商端前端: - 新增达人管理页面(含任务申诉次数管理) - 新增消息中心(含申诉次数申请审批) - 新增 Brief 管理(列表、详情) - 新增审核中心(脚本审核、视频审核) - 新增数据报表页面 品牌方端前端: - 优化首页仪表盘布局 - 新增项目管理(列表、详情、创建) - 新增代理商管理页面 - 新增审核中心(脚本终审、视频终审) - 新增系统设置页面 文档更新: - 申诉次数改为按任务分配(每任务初始1次) - 更新 PRD、FeatureSummary、User_Role_Interfaces 等文档 - 更新 UI 设计规范和开发计划 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
2f9b7f05fd
commit
4753626e5a
@@ -0,0 +1,315 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
|
||||
import {
|
||||
Bell,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
FileText,
|
||||
Video,
|
||||
Users,
|
||||
Clock,
|
||||
Check,
|
||||
MoreVertical,
|
||||
PlusCircle
|
||||
} from 'lucide-react'
|
||||
|
||||
// 消息类型
|
||||
interface Message {
|
||||
id: string
|
||||
type: string
|
||||
title: string
|
||||
content: string
|
||||
time: string
|
||||
read: boolean
|
||||
icon: typeof Bell
|
||||
iconColor: string
|
||||
bgColor: string
|
||||
// 申诉次数请求专用字段
|
||||
appealRequest?: {
|
||||
creatorName: string
|
||||
taskName: string
|
||||
taskId: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟消息列表
|
||||
const mockMessages: Message[] = [
|
||||
{
|
||||
id: 'msg-001',
|
||||
type: 'appeal_quota_request',
|
||||
title: '申诉次数申请',
|
||||
content: '达人「李小红」申请增加「618美妆推广视频」的申诉次数',
|
||||
time: '5分钟前',
|
||||
read: false,
|
||||
icon: PlusCircle,
|
||||
iconColor: 'text-accent-amber',
|
||||
bgColor: 'bg-accent-amber/20',
|
||||
appealRequest: {
|
||||
creatorName: '李小红',
|
||||
taskName: '618美妆推广视频',
|
||||
taskId: 'task-001',
|
||||
status: 'pending',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'msg-002',
|
||||
type: 'task_submitted',
|
||||
title: '新脚本提交',
|
||||
content: '达人「小美护肤」提交了「夏日护肤推广脚本」,请及时审核。',
|
||||
time: '10分钟前',
|
||||
read: false,
|
||||
icon: FileText,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo/20',
|
||||
},
|
||||
{
|
||||
id: 'msg-003',
|
||||
type: 'appeal_quota_request',
|
||||
title: '申诉次数申请',
|
||||
content: '达人「美妆达人小王」申请增加「双11护肤品种草」的申诉次数',
|
||||
time: '30分钟前',
|
||||
read: false,
|
||||
icon: PlusCircle,
|
||||
iconColor: 'text-accent-amber',
|
||||
bgColor: 'bg-accent-amber/20',
|
||||
appealRequest: {
|
||||
creatorName: '美妆达人小王',
|
||||
taskName: '双11护肤品种草',
|
||||
taskId: 'task-002',
|
||||
status: 'pending',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'msg-004',
|
||||
type: 'review_complete',
|
||||
title: '品牌终审通过',
|
||||
content: '「新品口红试色」视频已通过品牌方终审。',
|
||||
time: '1小时前',
|
||||
read: false,
|
||||
icon: CheckCircle,
|
||||
iconColor: 'text-accent-green',
|
||||
bgColor: 'bg-accent-green/20',
|
||||
},
|
||||
{
|
||||
id: 'msg-005',
|
||||
type: 'review_rejected',
|
||||
title: '品牌终审驳回',
|
||||
content: '「健身器材开箱」视频被品牌方驳回,原因:违禁词使用。',
|
||||
time: '2小时前',
|
||||
read: false,
|
||||
icon: XCircle,
|
||||
iconColor: 'text-accent-coral',
|
||||
bgColor: 'bg-accent-coral/20',
|
||||
},
|
||||
{
|
||||
id: 'msg-006',
|
||||
type: 'new_project',
|
||||
title: '新项目邀请',
|
||||
content: '您被邀请参与「XX品牌新品推广」项目,请配置 Brief。',
|
||||
time: '昨天',
|
||||
read: true,
|
||||
icon: Users,
|
||||
iconColor: 'text-purple-400',
|
||||
bgColor: 'bg-purple-500/20',
|
||||
},
|
||||
{
|
||||
id: 'msg-007',
|
||||
type: 'warning',
|
||||
title: '风险预警',
|
||||
content: '达人「美妆Lisa」连续2次提交被驳回,建议关注。',
|
||||
time: '昨天',
|
||||
read: true,
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-orange-400',
|
||||
bgColor: 'bg-orange-500/20',
|
||||
},
|
||||
{
|
||||
id: 'msg-008',
|
||||
type: 'task_submitted',
|
||||
title: '新视频提交',
|
||||
content: '达人「健身教练王」提交了「健身器材使用教程」视频,请及时审核。',
|
||||
time: '2天前',
|
||||
read: true,
|
||||
icon: Video,
|
||||
iconColor: 'text-purple-400',
|
||||
bgColor: 'bg-purple-500/20',
|
||||
},
|
||||
]
|
||||
|
||||
export default function AgencyMessagesPage() {
|
||||
const [messages, setMessages] = useState(mockMessages)
|
||||
const [filter, setFilter] = useState<'all' | 'unread'>('all')
|
||||
|
||||
const unreadCount = messages.filter(m => !m.read).length
|
||||
const pendingAppealRequests = messages.filter(m => m.appealRequest?.status === 'pending').length
|
||||
|
||||
const filteredMessages = filter === 'all' ? messages : messages.filter(m => !m.read)
|
||||
|
||||
const markAsRead = (id: string) => {
|
||||
setMessages(prev => prev.map(m => m.id === id ? { ...m, read: true } : m))
|
||||
}
|
||||
|
||||
const markAllAsRead = () => {
|
||||
setMessages(prev => prev.map(m => ({ ...m, read: true })))
|
||||
}
|
||||
|
||||
// 处理申诉次数请求
|
||||
const handleAppealRequest = (messageId: string, action: 'approve' | 'reject') => {
|
||||
setMessages(prev => prev.map(m => {
|
||||
if (m.id === messageId && m.appealRequest) {
|
||||
return {
|
||||
...m,
|
||||
read: true,
|
||||
appealRequest: {
|
||||
...m.appealRequest,
|
||||
status: action === 'approve' ? 'approved' : 'rejected',
|
||||
},
|
||||
}
|
||||
}
|
||||
return m
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-text-primary">消息中心</h1>
|
||||
{unreadCount > 0 && (
|
||||
<span className="px-2 py-1 bg-accent-coral/20 text-accent-coral text-sm font-medium rounded-lg">
|
||||
{unreadCount} 条未读
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="secondary" onClick={markAllAsRead} disabled={unreadCount === 0}>
|
||||
<Check size={16} />
|
||||
全部已读
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 筛选 */}
|
||||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg w-fit">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
filter === 'all' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
全部消息
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilter('unread')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
filter === 'unread' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
未读消息 ({unreadCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<div className="space-y-3">
|
||||
{filteredMessages.map((message) => {
|
||||
const Icon = message.icon
|
||||
const isAppealRequest = message.type === 'appeal_quota_request'
|
||||
const appealStatus = message.appealRequest?.status
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={message.id}
|
||||
className={`transition-all ${
|
||||
!isAppealRequest ? 'cursor-pointer hover:border-accent-indigo/50' : ''
|
||||
} ${!message.read ? 'border-l-4 border-l-accent-indigo' : ''}`}
|
||||
onClick={() => !isAppealRequest && markAsRead(message.id)}
|
||||
>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${message.bgColor} flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon size={20} className={message.iconColor} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className={`font-medium ${!message.read ? 'text-text-primary' : 'text-text-secondary'}`}>
|
||||
{message.title}
|
||||
</h3>
|
||||
{!message.read && (
|
||||
<span className="w-2 h-2 bg-accent-coral rounded-full" />
|
||||
)}
|
||||
{/* 申诉请求状态标签 */}
|
||||
{isAppealRequest && appealStatus === 'approved' && (
|
||||
<span className="px-2 py-0.5 bg-accent-green/15 text-accent-green text-xs font-medium rounded-full">
|
||||
已同意
|
||||
</span>
|
||||
)}
|
||||
{isAppealRequest && appealStatus === 'rejected' && (
|
||||
<span className="px-2 py-0.5 bg-accent-coral/15 text-accent-coral text-xs font-medium rounded-full">
|
||||
已拒绝
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary mt-1">{message.content}</p>
|
||||
<p className="text-xs text-text-tertiary mt-2 flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{message.time}
|
||||
</p>
|
||||
|
||||
{/* 申诉次数请求操作按钮 */}
|
||||
{isAppealRequest && appealStatus === 'pending' && (
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAppealRequest(message.id, 'approve')
|
||||
}}
|
||||
>
|
||||
<CheckCircle size={14} />
|
||||
同意 (+1次)
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAppealRequest(message.id, 'reject')
|
||||
}}
|
||||
>
|
||||
<XCircle size={14} />
|
||||
拒绝
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isAppealRequest && (
|
||||
<Button variant="ghost" size="sm" onClick={(e) => e.stopPropagation()}>
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredMessages.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<Bell size={48} className="mx-auto text-text-tertiary opacity-50 mb-4" />
|
||||
<p className="text-text-secondary">
|
||||
{filter === 'unread' ? '没有未读消息' : '暂无消息'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user