Your Name 0bfedb95c8 feat: 为所有终端添加平台显示功能
- 新增 frontend/lib/platforms.ts 共享平台配置模块
- 支持6个平台: 抖音、小红书、B站、快手、微博、微信视频号
- 品牌方终端: 项目看板、项目详情、终审台列表添加平台显示
- 代理商终端: 工作台概览、审核台、Brief配置、达人管理、
  数据报表、消息中心、申诉处理添加平台显示
- 达人端: 任务列表添加平台显示
- 统一使用彩色头部条样式展示平台信息

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 18:53:51 +08:00

334 lines
11 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 { 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'
import { getPlatformInfo } from '@/lib/platforms'
// 消息类型
interface Message {
id: string
type: string
title: string
content: string
time: string
read: boolean
icon: typeof Bell
iconColor: string
bgColor: string
platform?: 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',
platform: 'douyin',
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',
platform: 'xiaohongshu',
},
{
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',
platform: 'xiaohongshu',
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',
platform: 'xiaohongshu',
},
{
id: 'msg-005',
type: 'review_rejected',
title: '品牌终审驳回',
content: '「健身器材开箱」视频被品牌方驳回,原因:违禁词使用。',
time: '2小时前',
read: false,
icon: XCircle,
iconColor: 'text-accent-coral',
bgColor: 'bg-accent-coral/20',
platform: 'bilibili',
},
{
id: 'msg-006',
type: 'new_project',
title: '新项目邀请',
content: '您被邀请参与「XX品牌新品推广」项目请配置 Brief。',
time: '昨天',
read: true,
icon: Users,
iconColor: 'text-purple-400',
bgColor: 'bg-purple-500/20',
platform: 'douyin',
},
{
id: 'msg-007',
type: 'warning',
title: '风险预警',
content: '达人「美妆Lisa」连续2次提交被驳回建议关注。',
time: '昨天',
read: true,
icon: AlertTriangle,
iconColor: 'text-orange-400',
bgColor: 'bg-orange-500/20',
platform: 'xiaohongshu',
},
{
id: 'msg-008',
type: 'task_submitted',
title: '新视频提交',
content: '达人「健身教练王」提交了「健身器材使用教程」视频,请及时审核。',
time: '2天前',
read: true,
icon: Video,
iconColor: 'text-purple-400',
bgColor: 'bg-purple-500/20',
platform: 'bilibili',
},
]
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
const platform = message.platform ? getPlatformInfo(message.platform) : null
return (
<Card
key={message.id}
className={`transition-all overflow-hidden ${
!isAppealRequest ? 'cursor-pointer hover:border-accent-indigo/50' : ''
} ${!message.read ? 'border-l-4 border-l-accent-indigo' : ''}`}
onClick={() => !isAppealRequest && markAsRead(message.id)}
>
{/* 平台顶部条 */}
{platform && (
<div className={`px-4 py-1.5 ${platform.bgColor} border-b ${platform.borderColor} flex items-center gap-1.5`}>
<span className="text-sm">{platform.icon}</span>
<span className={`text-xs font-medium ${platform.textColor}`}>{platform.name}</span>
</div>
)}
<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>
)
}