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

616 lines
25 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, useParams } from 'next/navigation'
import Link from 'next/link'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Modal } from '@/components/ui/Modal'
import { SuccessTag, PendingTag, ErrorTag } from '@/components/ui/Tag'
import {
ArrowLeft,
Calendar,
Users,
FileText,
Video,
Clock,
CheckCircle,
XCircle,
ChevronRight,
Plus,
Settings,
Search,
Building2,
MoreHorizontal,
Trash2,
Check,
Pencil
} from 'lucide-react'
import { getPlatformInfo } from '@/lib/platforms'
// 模拟项目详情数据
const mockProject = {
id: 'proj-001',
name: 'XX品牌618推广',
platform: 'douyin',
status: 'active',
deadline: '2026-06-18',
createdAt: '2026-02-01',
description: '618大促活动营销内容审核项目',
stats: {
scriptTotal: 20,
scriptPassed: 15,
scriptPending: 3,
scriptRejected: 2,
videoTotal: 20,
videoPassed: 12,
videoPending: 5,
videoRejected: 3,
},
agencies: [
{ id: 'AG789012', name: '星耀传媒', creatorCount: 8, passRate: 92 },
{ id: 'AG456789', name: '创意无限', creatorCount: 5, passRate: 88 },
],
recentTasks: [
{ id: 'task-001', type: 'video', creatorName: '小美护肤', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'pending', submittedAt: '2026-02-06 14:30' },
{ id: 'task-002', type: 'script', creatorName: '美妆Lisa', agencyId: 'AG789012', agencyName: '星耀传媒', status: 'approved', submittedAt: '2026-02-06 12:15' },
{ id: 'task-003', type: 'video', creatorName: '健身王', agencyId: 'AG456789', agencyName: '创意无限', status: 'rejected', submittedAt: '2026-02-06 10:00' },
{ id: 'task-004', type: 'script', creatorName: '时尚达人', agencyId: 'AG456789', agencyName: '创意无限', status: 'pending', submittedAt: '2026-02-05 16:45' },
],
}
// 模拟品牌方已添加的代理商(来自代理商管理)
const mockManagedAgencies = [
{ id: 'AG789012', name: '星耀传媒', companyName: '上海星耀文化传媒有限公司' },
{ id: 'AG456789', name: '创意无限', companyName: '深圳创意无限广告有限公司' },
{ id: 'AG123456', name: '美妆达人MCN', companyName: '杭州美妆达人网络科技有限公司' },
{ id: 'AG111111', name: '蓝海科技', companyName: '北京蓝海数字科技有限公司' },
{ id: 'AG222222', name: '云创网络', companyName: '杭州云创网络技术有限公司' },
{ id: 'AG333333', name: '天府传媒', companyName: '成都天府传媒集团有限公司' },
]
function StatCard({ title, value, icon: Icon, color }: { title: string; value: number | string; icon: React.ElementType; color: string }) {
return (
<Card>
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-text-secondary">{title}</p>
<p className={`text-2xl font-bold ${color}`}>{value}</p>
</div>
<div className={`w-10 h-10 rounded-lg ${color.replace('text-', 'bg-')}/20 flex items-center justify-center`}>
<Icon size={20} className={color} />
</div>
</div>
</CardContent>
</Card>
)
}
function TaskStatusTag({ status }: { status: string }) {
switch (status) {
case 'approved': return <SuccessTag></SuccessTag>
case 'pending': return <PendingTag></PendingTag>
case 'rejected': return <ErrorTag></ErrorTag>
default: return <PendingTag></PendingTag>
}
}
export default function ProjectDetailPage() {
const router = useRouter()
const params = useParams()
const projectId = params.id as string
const [project, setProject] = useState(mockProject)
// 添加代理商相关状态
const [showAddModal, setShowAddModal] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [selectedAgencies, setSelectedAgencies] = useState<string[]>([])
// 代理商操作菜单
const [activeAgencyMenu, setActiveAgencyMenu] = useState<string | null>(null)
// 删除确认
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [agencyToDelete, setAgencyToDelete] = useState<typeof project.agencies[0] | null>(null)
// 编辑截止日期
const [showDeadlineModal, setShowDeadlineModal] = useState(false)
const [newDeadline, setNewDeadline] = useState(project.deadline)
// 保存截止日期
const handleSaveDeadline = () => {
if (!newDeadline) return
setProject({ ...project, deadline: newDeadline })
setShowDeadlineModal(false)
}
const scriptPassRate = Math.round((project.stats.scriptPassed / project.stats.scriptTotal) * 100)
const videoPassRate = Math.round((project.stats.videoPassed / project.stats.videoTotal) * 100)
// 过滤可添加的代理商(排除已在项目中的)
const availableAgencies = mockManagedAgencies.filter(
agency => !project.agencies.some(a => a.id === agency.id)
)
// 搜索过滤
const filteredAgencies = availableAgencies.filter(agency =>
searchQuery === '' ||
agency.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agency.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
agency.companyName.toLowerCase().includes(searchQuery.toLowerCase())
)
// 切换选择
const toggleSelectAgency = (agencyId: string) => {
setSelectedAgencies(prev =>
prev.includes(agencyId)
? prev.filter(id => id !== agencyId)
: [...prev, agencyId]
)
}
// 确认添加
const handleAddAgencies = () => {
const newAgencies = mockManagedAgencies
.filter(a => selectedAgencies.includes(a.id))
.map(a => ({ id: a.id, name: a.name, creatorCount: 0, passRate: 0 }))
setProject({
...project,
agencies: [...project.agencies, ...newAgencies]
})
setShowAddModal(false)
setSelectedAgencies([])
setSearchQuery('')
}
// 移除代理商
const handleRemoveAgency = async () => {
if (!agencyToDelete) return
setProject({
...project,
agencies: project.agencies.filter(a => a.id !== agencyToDelete.id)
})
setShowDeleteModal(false)
setAgencyToDelete(null)
}
const platform = getPlatformInfo(project.platform)
return (
<div className="space-y-6">
{/* 顶部导航 */}
<div className="flex items-center gap-4">
<button type="button" onClick={() => router.back()} className="p-2 hover:bg-bg-elevated rounded-full">
<ArrowLeft size={20} className="text-text-primary" />
</button>
<div className="flex-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-text-primary">{project.name}</h1>
{platform && (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${platform.bgColor} ${platform.textColor} border ${platform.borderColor}`}>
<span>{platform.icon}</span>
{platform.name}
</span>
)}
</div>
<p className="text-sm text-text-secondary">{project.description}</p>
</div>
<SuccessTag></SuccessTag>
</div>
{/* 项目信息 */}
<div className="flex items-center gap-6 text-sm text-text-secondary">
<span className="flex items-center gap-2">
<Calendar size={16} />
: {project.deadline}
<button
type="button"
onClick={() => {
setNewDeadline(project.deadline)
setShowDeadlineModal(true)
}}
className="p-1 rounded hover:bg-bg-elevated transition-colors"
title="修改截止日期"
>
<Pencil size={14} className="text-text-tertiary hover:text-accent-indigo" />
</button>
</span>
<span className="flex items-center gap-2">
<Clock size={16} />
: {project.createdAt}
</span>
</div>
{/* Brief和规则配置 - 大按钮 */}
<Link href={`/brand/projects/${projectId}/config`}>
<Card className="hover:border-accent-indigo transition-colors cursor-pointer">
<CardContent className="py-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
<Settings size={24} className="text-accent-indigo" />
</div>
<div>
<p className="font-semibold text-text-primary">Brief和规则配置</p>
<p className="text-sm text-text-secondary">BriefAI检测项等</p>
</div>
</div>
<ChevronRight size={20} className="text-text-tertiary" />
</div>
</CardContent>
</Card>
</Link>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard title="脚本通过率" value={`${scriptPassRate}%`} icon={FileText} color="text-accent-green" />
<StatCard title="视频通过率" value={`${videoPassRate}%`} icon={Video} color="text-accent-indigo" />
<StatCard title="参与代理商" value={project.agencies.length} icon={Users} color="text-purple-400" />
<StatCard title="待审核任务" value={project.stats.scriptPending + project.stats.videoPending} icon={Clock} color="text-orange-400" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 审核进度 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* 脚本审核 */}
<div>
<div className="flex items-center justify-between mb-3">
<span className="flex items-center gap-2 text-text-primary font-medium">
<FileText size={16} />
</span>
<span className="text-sm text-text-secondary">
{project.stats.scriptPassed}/{project.stats.scriptTotal}
</span>
</div>
<div className="flex h-4 rounded-full overflow-hidden bg-bg-elevated">
<div className="bg-accent-green" style={{ width: `${(project.stats.scriptPassed / project.stats.scriptTotal) * 100}%` }} />
<div className="bg-yellow-500" style={{ width: `${(project.stats.scriptPending / project.stats.scriptTotal) * 100}%` }} />
<div className="bg-accent-coral" style={{ width: `${(project.stats.scriptRejected / project.stats.scriptTotal) * 100}%` }} />
</div>
<div className="flex gap-6 mt-2 text-xs">
<span className="flex items-center gap-1 text-accent-green">
<CheckCircle size={12} /> {project.stats.scriptPassed}
</span>
<span className="flex items-center gap-1 text-yellow-500">
<Clock size={12} /> {project.stats.scriptPending}
</span>
<span className="flex items-center gap-1 text-accent-coral">
<XCircle size={12} /> {project.stats.scriptRejected}
</span>
</div>
</div>
{/* 视频审核 */}
<div>
<div className="flex items-center justify-between mb-3">
<span className="flex items-center gap-2 text-text-primary font-medium">
<Video size={16} />
</span>
<span className="text-sm text-text-secondary">
{project.stats.videoPassed}/{project.stats.videoTotal}
</span>
</div>
<div className="flex h-4 rounded-full overflow-hidden bg-bg-elevated">
<div className="bg-accent-green" style={{ width: `${(project.stats.videoPassed / project.stats.videoTotal) * 100}%` }} />
<div className="bg-yellow-500" style={{ width: `${(project.stats.videoPending / project.stats.videoTotal) * 100}%` }} />
<div className="bg-accent-coral" style={{ width: `${(project.stats.videoRejected / project.stats.videoTotal) * 100}%` }} />
</div>
<div className="flex gap-6 mt-2 text-xs">
<span className="flex items-center gap-1 text-accent-green">
<CheckCircle size={12} /> {project.stats.videoPassed}
</span>
<span className="flex items-center gap-1 text-yellow-500">
<Clock size={12} /> {project.stats.videoPending}
</span>
<span className="flex items-center gap-1 text-accent-coral">
<XCircle size={12} /> {project.stats.videoRejected}
</span>
</div>
</div>
</CardContent>
</Card>
{/* 代理商列表 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users size={16} />
<span className="text-sm font-normal text-text-tertiary">({project.agencies.length})</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{project.agencies.map((agency) => (
<div key={agency.id} className="flex items-center justify-between p-3 rounded-lg bg-bg-elevated">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
<Building2 size={18} className="text-accent-indigo" />
</div>
<div>
<p className="font-medium text-text-primary text-sm">{agency.name}</p>
<p className="text-xs text-text-tertiary">{agency.creatorCount} · {agency.passRate}%</p>
</div>
</div>
<div className="relative">
<button
type="button"
onClick={() => setActiveAgencyMenu(activeAgencyMenu === agency.id ? null : agency.id)}
className="p-1.5 rounded hover:bg-bg-page transition-colors"
>
<MoreHorizontal size={16} className="text-text-tertiary" />
</button>
{activeAgencyMenu === agency.id && (
<div className="absolute right-0 top-8 z-10 w-32 py-1 bg-bg-card rounded-lg shadow-lg border border-border-subtle">
<button
type="button"
onClick={() => {
setAgencyToDelete(agency)
setShowDeleteModal(true)
setActiveAgencyMenu(null)
}}
className="w-full px-3 py-2 text-left text-sm text-accent-coral hover:bg-bg-elevated flex items-center gap-2"
>
<Trash2 size={14} />
</button>
</div>
)}
</div>
</div>
))}
{/* 添加代理商按钮 */}
<button
type="button"
onClick={() => setShowAddModal(true)}
className="w-full p-3 rounded-lg border-2 border-dashed border-border-subtle hover:border-accent-indigo hover:bg-accent-indigo/5 transition-all flex items-center justify-center gap-2 text-text-tertiary hover:text-accent-indigo"
>
<Plus size={18} />
<span className="text-sm font-medium"></span>
</button>
</CardContent>
</Card>
</div>
{/* 最近任务 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span></span>
<Link href="/brand/review">
<Button variant="ghost" size="sm">
<ChevronRight size={16} />
</Button>
</Link>
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border-subtle text-left text-sm text-text-secondary">
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3 font-medium"></th>
</tr>
</thead>
<tbody>
{project.recentTasks.map((task) => (
<tr key={task.id} className="border-b border-border-subtle last:border-0 hover:bg-bg-elevated">
<td className="py-4">
<span className="flex items-center gap-2">
{task.type === 'script' ? <FileText size={16} className="text-accent-indigo" /> : <Video size={16} className="text-purple-400" />}
{task.type === 'script' ? '脚本' : '视频'}
</span>
</td>
<td className="py-4 text-text-primary">{task.creatorName}</td>
<td className="py-4">
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-bg-elevated text-sm">
<Building2 size={14} className="text-accent-indigo" />
<span className="text-text-secondary">{task.agencyName}</span>
</span>
</td>
<td className="py-4"><TaskStatusTag status={task.status} /></td>
<td className="py-4 text-sm text-text-tertiary">{task.submittedAt}</td>
<td className="py-4">
<Link href={`/brand/review/${task.type}/${task.id}`}>
<Button size="sm" variant={task.status === 'pending' ? 'primary' : 'secondary'}>
{task.status === 'pending' ? '审核' : '查看'}
</Button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* 添加代理商弹窗 */}
<Modal
isOpen={showAddModal}
onClose={() => {
setShowAddModal(false)
setSearchQuery('')
setSelectedAgencies([])
}}
title="添加代理商"
size="lg"
>
<div className="space-y-4">
{/* 搜索框 */}
<div className="relative">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-tertiary" />
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="搜索代理商名称或ID..."
className="pl-10"
/>
</div>
{/* 代理商列表 */}
<div className="max-h-80 overflow-y-auto space-y-2">
{filteredAgencies.length > 0 ? (
filteredAgencies.map((agency) => {
const isSelected = selectedAgencies.includes(agency.id)
return (
<button
key={agency.id}
type="button"
onClick={() => toggleSelectAgency(agency.id)}
className={`w-full flex items-center gap-3 p-3 rounded-xl border-2 transition-all text-left ${
isSelected
? 'border-accent-indigo bg-accent-indigo/5'
: 'border-transparent bg-bg-elevated hover:bg-bg-page'
}`}
>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
isSelected ? 'bg-accent-indigo' : 'bg-accent-indigo/15'
}`}>
{isSelected ? (
<Check size={20} className="text-white" />
) : (
<Building2 size={20} className="text-accent-indigo" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium text-text-primary">{agency.name}</p>
<span className="text-xs text-text-tertiary font-mono">{agency.id}</span>
</div>
<p className="text-sm text-text-secondary truncate">{agency.companyName}</p>
</div>
</button>
)
})
) : (
<div className="text-center py-8 text-text-tertiary">
{availableAgencies.length === 0 ? (
<>
<Users size={32} className="mx-auto mb-2 opacity-50" />
<p></p>
</>
) : (
<>
<Search size={32} className="mx-auto mb-2 opacity-50" />
<p></p>
</>
)}
</div>
)}
</div>
{/* 已选择提示 */}
{selectedAgencies.length > 0 && (
<div className="flex items-center justify-between pt-3 border-t border-border-subtle">
<span className="text-sm text-text-secondary">
<span className="text-accent-indigo font-medium">{selectedAgencies.length}</span>
</span>
<Button variant="primary" onClick={handleAddAgencies}>
<Plus size={16} />
</Button>
</div>
)}
{/* 底部提示 */}
<p className="text-xs text-text-tertiary pt-2">
"代理商管理"
</p>
</div>
</Modal>
{/* 删除确认弹窗 */}
<Modal
isOpen={showDeleteModal}
onClose={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}
title="移除代理商"
>
<div className="space-y-4">
<p className="text-text-secondary">
<span className="text-text-primary font-medium">{agencyToDelete?.name}</span>
</p>
<p className="text-sm text-accent-coral">
</p>
<div className="flex gap-3">
<Button variant="secondary" className="flex-1" onClick={() => { setShowDeleteModal(false); setAgencyToDelete(null) }}>
</Button>
<Button
variant="primary"
className="flex-1 bg-accent-coral hover:bg-accent-coral/80"
onClick={handleRemoveAgency}
>
</Button>
</div>
</div>
</Modal>
{/* 编辑截止日期弹窗 */}
<Modal
isOpen={showDeadlineModal}
onClose={() => setShowDeadlineModal(false)}
title="修改截止日期"
>
<div className="space-y-4">
<div className="p-3 rounded-lg bg-bg-elevated">
<p className="text-sm text-text-secondary"></p>
<p className="font-medium text-text-primary">{project.name}</p>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-2">
</label>
<div className="relative">
<Calendar size={18} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-tertiary" />
<input
type="date"
value={newDeadline}
onChange={(e) => setNewDeadline(e.target.value)}
className="w-full pl-12 pr-4 py-3 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
/>
</div>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="secondary"
className="flex-1"
onClick={() => setShowDeadlineModal(false)}
>
</Button>
<Button
variant="primary"
className="flex-1"
onClick={handleSaveDeadline}
disabled={!newDeadline}
>
</Button>
</div>
</div>
</Modal>
</div>
)
}