Your Name 2f9b7f05fd feat(creator): 完成达人端前端页面开发
- 新增申诉中心页面(列表、详情、新建申诉)
- 新增申诉次数管理页面(按任务显示配额,支持向代理商申请)
- 新增个人中心页面(达人ID复制、菜单导航)
- 新增个人信息编辑、账户设置、消息通知设置页面
- 新增帮助中心和历史记录页面
- 新增脚本提交和视频提交页面
- 优化消息中心页面(消息详情跳转)
- 优化任务详情页面布局和交互
- 更新 ResponsiveLayout、Sidebar、ReviewSteps 通用组件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:38:01 +08:00

517 lines
18 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'
// 任务阶段状态类型
type StageStatus = 'pending' | 'current' | 'done' | 'error'
// 任务数据类型
type Task = {
id: string
title: string
description: 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',
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',
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处问题 · 需修改后重新提交',
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: '开箱测评 · 审核通过 · 可发布',
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审核中 · 等待结果',
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: '穿搭展示 · 脚本待代理商审核',
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: '开箱视频 · 脚本待品牌终审',
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: '美食测评 · 脚本通过 · 待上传视频',
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审核中',
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: '功能展示 · 脚本代理商不通过',
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: '品牌代言 · 脚本品牌不通过',
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: '配件展示 · 视频代理商审核中',
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: '旅行记录 · 视频代理商不通过',
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: '宠物日常 · 视频品牌终审中',
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: '使用演示 · 视频品牌不通过',
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 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 p-5 flex flex-col gap-4 card-shadow cursor-pointer hover:bg-bg-elevated/30 transition-colors"
onClick={onClick}
>
{/* 任务主行 */}
<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>
)
}
// 任务状态筛选选项
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>
)
}