- 新增 frontend/lib/platforms.ts 共享平台配置模块 - 支持6个平台: 抖音、小红书、B站、快手、微博、微信视频号 - 品牌方终端: 项目看板、项目详情、终审台列表添加平台显示 - 代理商终端: 工作台概览、审核台、Brief配置、达人管理、 数据报表、消息中心、申诉处理添加平台显示 - 达人端: 任务列表添加平台显示 - 统一使用彩色头部条样式展示平台信息 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
546 lines
19 KiB
TypeScript
546 lines
19 KiB
TypeScript
'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>
|
||
)
|
||
}
|