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

546 lines
19 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 {
Video,
Search,
SlidersHorizontal,
ChevronDown,
Upload,
Bot,
Users,
Building2,
Check,
X,
Loader2
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { cn } from '@/lib/utils'
import { platformOptions, getPlatformInfo } from '@/lib/platforms'
// 任务阶段状态类型
type StageStatus = 'pending' | 'current' | 'done' | 'error'
// 任务数据类型
type Task = {
id: string
title: string
description: string
platform: string // 发布平台
// 脚本阶段
scriptStage: {
submit: StageStatus
ai: StageStatus
agency: StageStatus
brand: StageStatus
}
// 视频阶段
videoStage: {
submit: StageStatus
ai: StageStatus
agency: StageStatus
brand: StageStatus
}
// 按钮配置
buttonText: string
buttonType: 'upload' | 'view' | 'fix'
// 阶段颜色
scriptColor: 'blue' | 'indigo' | 'coral' | 'green'
videoColor: 'tertiary' | 'blue' | 'indigo' | 'coral' | 'green'
}
// 15个任务数据覆盖所有状态
const mockTasks: Task[] = [
{
id: 'task-001',
title: 'XX品牌618推广',
description: '产品种草视频 · 时长要求 60-90秒 · 截止: 2026-02-10',
platform: 'douyin',
scriptStage: { submit: 'current', ai: 'pending', agency: 'pending', brand: 'pending' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '上传脚本',
buttonType: 'upload',
scriptColor: 'blue',
videoColor: 'tertiary',
},
{
id: 'task-002',
title: 'YY美妆新品',
description: '口播测评 · 已上传视频 · 提交于: 今天 14:30',
platform: 'xiaohongshu',
scriptStage: { submit: 'done', ai: 'current', agency: 'pending', brand: 'pending' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'indigo',
videoColor: 'tertiary',
},
{
id: 'task-003',
title: 'ZZ饮品夏日',
description: '探店Vlog · 发现2处问题 · 需修改后重新提交',
platform: 'bilibili',
scriptStage: { submit: 'done', ai: 'error', agency: 'pending', brand: 'pending' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看修改',
buttonType: 'fix',
scriptColor: 'coral',
videoColor: 'tertiary',
},
{
id: 'task-004',
title: 'AA数码新品发布',
description: '开箱测评 · 审核通过 · 可发布',
platform: 'douyin',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'green',
videoColor: 'green',
},
{
id: 'task-005',
title: 'BB运动饮料',
description: '运动场景 · 脚本AI审核中 · 等待结果',
platform: 'kuaishou',
scriptStage: { submit: 'done', ai: 'current', agency: 'pending', brand: 'pending' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'indigo',
videoColor: 'tertiary',
},
{
id: 'task-006',
title: 'CC服装春季款',
description: '穿搭展示 · 脚本待代理商审核',
platform: 'xiaohongshu',
scriptStage: { submit: 'done', ai: 'done', agency: 'current', brand: 'pending' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'indigo',
videoColor: 'tertiary',
},
{
id: 'task-007',
title: 'DD家电测评',
description: '开箱视频 · 脚本待品牌终审',
platform: 'bilibili',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'current' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'indigo',
videoColor: 'tertiary',
},
{
id: 'task-008',
title: 'EE食品试吃',
description: '美食测评 · 脚本通过 · 待上传视频',
platform: 'douyin',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'current', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '上传视频',
buttonType: 'upload',
scriptColor: 'green',
videoColor: 'blue',
},
{
id: 'task-009',
title: 'FF护肤品',
description: '使用教程 · 视频AI审核中',
platform: 'xiaohongshu',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'done', ai: 'current', agency: 'pending', brand: 'pending' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'green',
videoColor: 'indigo',
},
{
id: 'task-010',
title: 'GG智能手表',
description: '功能展示 · 脚本代理商不通过',
platform: 'weibo',
scriptStage: { submit: 'done', ai: 'done', agency: 'error', brand: 'pending' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看修改',
buttonType: 'fix',
scriptColor: 'coral',
videoColor: 'tertiary',
},
{
id: 'task-011',
title: 'HH美妆代言',
description: '品牌代言 · 脚本品牌不通过',
platform: 'xiaohongshu',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'error' },
videoStage: { submit: 'pending', ai: 'pending', agency: 'pending', brand: 'pending' },
buttonText: '查看修改',
buttonType: 'fix',
scriptColor: 'coral',
videoColor: 'tertiary',
},
{
id: 'task-012',
title: 'II数码配件',
description: '配件展示 · 视频代理商审核中',
platform: 'bilibili',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'done', ai: 'done', agency: 'current', brand: 'pending' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'green',
videoColor: 'indigo',
},
{
id: 'task-013',
title: 'JJ旅行vlog',
description: '旅行记录 · 视频代理商不通过',
platform: 'wechat',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'done', ai: 'done', agency: 'error', brand: 'pending' },
buttonText: '查看修改',
buttonType: 'fix',
scriptColor: 'green',
videoColor: 'coral',
},
{
id: 'task-014',
title: 'KK宠物用品',
description: '宠物日常 · 视频品牌终审中',
platform: 'douyin',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'current' },
buttonText: '查看详情',
buttonType: 'view',
scriptColor: 'green',
videoColor: 'indigo',
},
{
id: 'task-015',
title: 'LL厨房电器',
description: '使用演示 · 视频品牌不通过',
platform: 'kuaishou',
scriptStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'done' },
videoStage: { submit: 'done', ai: 'done', agency: 'done', brand: 'error' },
buttonText: '查看修改',
buttonType: 'fix',
scriptColor: 'green',
videoColor: 'coral',
},
]
// 步骤图标组件
function StepIcon({ status, icon }: { status: StageStatus; icon: 'upload' | 'bot' | 'users' | 'building' }) {
const IconComponent = {
upload: Upload,
bot: Bot,
users: Users,
building: Building2,
}[icon]
const getStyle = () => {
switch (status) {
case 'done':
return 'bg-accent-green'
case 'current':
return 'bg-accent-indigo'
case 'error':
return 'bg-accent-coral'
default:
return 'bg-bg-elevated border-[1.5px] border-border-subtle'
}
}
const getIconColor = () => {
if (status === 'done' || status === 'current' || status === 'error') return 'text-white'
return 'text-text-tertiary'
}
return (
<div className={cn('w-7 h-7 rounded-full flex items-center justify-center', getStyle())}>
{status === 'done' && <Check size={14} className={getIconColor()} />}
{status === 'current' && <Loader2 size={14} className={cn(getIconColor(), 'animate-spin')} />}
{status === 'error' && <X size={14} className={getIconColor()} />}
{status === 'pending' && <IconComponent size={14} className={getIconColor()} />}
</div>
)
}
// 进度条组件
function ProgressBar({ stage, color }: {
stage: { submit: StageStatus; ai: StageStatus; agency: StageStatus; brand: StageStatus }
color: string
}) {
const steps = [
{ key: 'submit', label: '提交', icon: 'upload' as const, status: stage.submit },
{ key: 'ai', label: 'AI审核', icon: 'bot' as const, status: stage.ai },
{ key: 'agency', label: '代理商', icon: 'users' as const, status: stage.agency },
{ key: 'brand', label: '品牌', icon: 'building' as const, status: stage.brand },
]
const getLineColor = (fromStatus: StageStatus) => {
if (fromStatus === 'done') return 'bg-accent-green'
return 'bg-border-subtle'
}
const getLabelColor = (status: StageStatus) => {
if (status === 'done') return 'text-text-secondary'
if (status === 'current') return 'text-accent-indigo font-semibold'
if (status === 'error') return 'text-accent-coral font-semibold'
return 'text-text-tertiary'
}
return (
<div className="flex items-center w-full">
{steps.map((step, index) => (
<div key={step.key} className="flex items-center flex-1">
<div className="flex flex-col items-center gap-1.5 w-14">
<StepIcon status={step.status} icon={step.icon} />
<span className={cn('text-[10px]', getLabelColor(step.status))}>{step.label}</span>
</div>
{index < steps.length - 1 && (
<div className={cn('h-0.5 flex-1', getLineColor(step.status))} />
)}
</div>
))}
</div>
)
}
// 任务卡片组件
function TaskCard({ task, onClick }: { task: Task; onClick: () => void }) {
const platform = getPlatformInfo(task.platform)
const getStageColor = (color: string) => {
switch (color) {
case 'blue': return 'text-accent-blue'
case 'indigo': return 'text-accent-indigo'
case 'coral': return 'text-accent-coral'
case 'green': return 'text-accent-green'
default: return 'text-text-tertiary'
}
}
const getButtonStyle = () => {
switch (task.buttonType) {
case 'upload':
return 'bg-accent-green text-white'
case 'fix':
return 'bg-accent-coral text-white'
default:
return 'bg-transparent border-[1.5px] border-accent-indigo text-accent-indigo'
}
}
return (
<div
className="bg-bg-card rounded-2xl overflow-hidden card-shadow cursor-pointer hover:bg-bg-elevated/30 transition-colors"
onClick={onClick}
>
{/* 平台顶部条 */}
{platform && (
<div className={`px-5 py-2 ${platform.bgColor} border-b ${platform.borderColor} flex items-center gap-2`}>
<span className="text-base">{platform.icon}</span>
<span className={`text-sm font-medium ${platform.textColor}`}>{platform.name}</span>
</div>
)}
<div className="p-5 flex flex-col gap-4">
{/* 任务主行 */}
<div className="flex items-center justify-between">
{/* 左侧:缩略图 + 信息 */}
<div className="flex items-center gap-4">
<div className="w-20 h-[60px] rounded-lg bg-[#1A1A1E] flex items-center justify-center flex-shrink-0">
<Video className="w-6 h-6 text-text-tertiary" />
</div>
<div className="flex flex-col gap-1.5">
<span className="text-base font-semibold text-text-primary">{task.title}</span>
<span className="text-[13px] text-text-secondary">{task.description}</span>
</div>
</div>
{/* 右侧:操作按钮 */}
<button
type="button"
className={cn('px-5 py-2.5 rounded-[10px] text-sm font-semibold', getButtonStyle())}
onClick={(e) => { e.stopPropagation(); onClick() }}
>
{task.buttonText}
</button>
</div>
{/* 进度条容器 */}
<div className="flex flex-col gap-3 pt-3">
{/* 脚本阶段 */}
<div className="flex items-center gap-2">
<span className={cn('text-xs font-semibold w-8', getStageColor(task.scriptColor))}></span>
<div className="flex-1">
<ProgressBar stage={task.scriptStage} color={task.scriptColor} />
</div>
</div>
{/* 视频阶段 */}
<div className="flex items-center gap-2">
<span className={cn('text-xs font-semibold w-8', getStageColor(task.videoColor))}></span>
<div className="flex-1">
<ProgressBar stage={task.videoStage} color={task.videoColor} />
</div>
</div>
</div>
</div>
</div>
)
}
// 任务状态筛选选项
type TaskFilter = 'all' | 'pending' | 'reviewing' | 'rejected' | 'completed'
const filterOptions: { value: TaskFilter; label: string }[] = [
{ value: 'all', label: '全部状态' },
{ value: 'pending', label: '待提交' },
{ value: 'reviewing', label: '审核中' },
{ value: 'rejected', label: '已驳回' },
{ value: 'completed', label: '已完成' },
]
// 根据任务状态获取筛选分类
const getTaskFilterCategory = (task: Task): TaskFilter => {
// 如果视频阶段全部完成,则为已完成
if (task.videoStage.brand === 'done') return 'completed'
// 如果有任何阶段为 error则为已驳回
if (
task.scriptStage.ai === 'error' ||
task.scriptStage.agency === 'error' ||
task.scriptStage.brand === 'error' ||
task.videoStage.ai === 'error' ||
task.videoStage.agency === 'error' ||
task.videoStage.brand === 'error'
) return 'rejected'
// 如果脚本阶段待提交或视频阶段待提交(且脚本已完成)
if (task.scriptStage.submit === 'current' || (task.scriptStage.brand === 'done' && task.videoStage.submit === 'current')) return 'pending'
// 其他情况为审核中
return 'reviewing'
}
export default function CreatorTasksPage() {
const router = useRouter()
const [searchQuery, setSearchQuery] = useState('')
const [filter, setFilter] = useState<TaskFilter>('all')
const [showFilterDropdown, setShowFilterDropdown] = useState(false)
const [tasks] = useState<Task[]>(mockTasks)
const handleTaskClick = (taskId: string) => {
router.push(`/creator/task/${taskId}`)
}
// 过滤任务
const filteredTasks = tasks.filter(task => {
// 搜索过滤
const matchesSearch = searchQuery === '' ||
task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
task.description.toLowerCase().includes(searchQuery.toLowerCase())
// 状态过滤
const matchesFilter = filter === 'all' || getTaskFilterCategory(task) === filter
return matchesSearch && matchesFilter
})
const currentFilterLabel = filterOptions.find(opt => opt.value === filter)?.label || '全部状态'
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-2xl lg:text-[28px] font-bold text-text-primary"></h1>
<p className="text-sm lg:text-[15px] text-text-secondary">
{filter === 'all' ? `${tasks.length} 个任务` : `${currentFilterLabel} ${filteredTasks.length}`}
</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="relative">
<button
type="button"
onClick={() => setShowFilterDropdown(!showFilterDropdown)}
className={cn(
'flex items-center gap-2 px-4 py-2.5 rounded-xl border text-sm font-medium transition-colors',
filter !== 'all'
? 'bg-accent-indigo/10 border-accent-indigo text-accent-indigo'
: 'bg-bg-card border-border-subtle text-text-secondary'
)}
>
<SlidersHorizontal className="w-[18px] h-[18px]" />
<span>{currentFilterLabel}</span>
<ChevronDown className={cn('w-4 h-4 transition-transform', showFilterDropdown && 'rotate-180')} />
</button>
{showFilterDropdown && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setShowFilterDropdown(false)}
/>
<div className="absolute right-0 top-full mt-2 w-40 bg-bg-card rounded-xl border border-border-subtle card-shadow z-20 overflow-hidden">
{filterOptions.map((option) => (
<button
key={option.value}
type="button"
onClick={() => {
setFilter(option.value)
setShowFilterDropdown(false)
}}
className={cn(
'w-full px-4 py-3 text-left text-sm transition-colors',
filter === option.value
? 'bg-accent-indigo/10 text-accent-indigo font-medium'
: 'text-text-primary hover:bg-bg-elevated'
)}
>
{option.label}
</button>
))}
</div>
</>
)}
</div>
</div>
</div>
{/* 任务列表 - 可滚动 */}
<div className="flex flex-col gap-4 flex-1 overflow-y-auto pr-2">
{filteredTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<Search className="w-12 h-12 text-text-tertiary mb-4" />
<p className="text-text-secondary"></p>
<p className="text-sm text-text-tertiary mt-1"></p>
</div>
) : (
filteredTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onClick={() => handleTaskClick(task.id)}
/>
))
)}
</div>
</div>
</ResponsiveLayout>
)
}