Your Name a8be7bbca9 feat: 前端剩余页面全面对接后端 API(Phase 2 完成)
为品牌方端(8页)、代理商端(10页)、达人端(6页)共24个页面添加真实API调用:
- 每页新增 USE_MOCK 条件分支,开发环境使用 mock 数据,生产环境调用真实 API
- 添加 loading 骨架屏、error toast 提示、submitting 状态管理
- 数据映射:TaskResponse → 页面视图模型,处理类型差异
- 审核操作(通过/驳回/强制通过)对接 api.reviewScript/reviewVideo
- Brief/规则/AI配置对接 api.getBrief/updateBrief/listForbiddenWords 等
- 申诉/历史/额度管理对接 api.listTasks + 状态过滤映射

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 16:29:43 +08:00

284 lines
9.8 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import {
ArrowLeft,
CheckCircle,
XCircle,
Clock,
Video,
Filter,
ChevronRight,
Loader2
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { cn } from '@/lib/utils'
import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
import { useToast } from '@/components/ui/Toast'
import type { TaskResponse } from '@/types/task'
// 历史任务状态类型
type HistoryStatus = 'completed' | 'expired' | 'cancelled'
// 历史任务数据类型
type HistoryTask = {
id: string
title: string
description: string
status: HistoryStatus
completedAt?: string
expiredAt?: string
platform: string
}
// 模拟历史数据
const mockHistory: HistoryTask[] = [
{
id: 'hist-001',
title: 'MM春季护肤品推广',
description: '产品测评 · 已发布',
status: 'completed',
completedAt: '2026-01-20',
platform: '抖音',
},
{
id: 'hist-002',
title: 'NN零食新品试吃',
description: '美食测评 · 已发布',
status: 'completed',
completedAt: '2026-01-15',
platform: '小红书',
},
{
id: 'hist-003',
title: 'OO运动装备测评',
description: '运动视频 · 已过期',
status: 'expired',
expiredAt: '2026-01-10',
platform: '抖音',
},
{
id: 'hist-004',
title: 'PP家居用品展示',
description: '生活vlog · 已取消',
status: 'cancelled',
expiredAt: '2026-01-05',
platform: 'B站',
},
{
id: 'hist-005',
title: 'QQ电子产品开箱',
description: '科技测评 · 已发布',
status: 'completed',
completedAt: '2025-12-28',
platform: '抖音',
},
{
id: 'hist-006',
title: 'RR母婴用品推荐',
description: '种草视频 · 已发布',
status: 'completed',
completedAt: '2025-12-20',
platform: '小红书',
},
]
function mapTaskResponseToHistory(task: TaskResponse): HistoryTask {
return {
id: task.id,
title: task.name,
description: task.project.name,
status: task.stage === 'completed' ? 'completed' : 'completed',
completedAt: task.updated_at?.split('T')[0],
platform: '抖音', // backend doesn't return platform info yet
}
}
// 状态配置
const statusConfig: Record<HistoryStatus, { label: string; color: string; bgColor: string; icon: React.ElementType }> = {
completed: { label: '已完成', color: 'text-accent-green', bgColor: 'bg-accent-green/15', icon: CheckCircle },
expired: { label: '已过期', color: 'text-text-tertiary', bgColor: 'bg-bg-elevated', icon: Clock },
cancelled: { label: '已取消', color: 'text-accent-coral', bgColor: 'bg-accent-coral/15', icon: XCircle },
}
// 骨架屏
function HistorySkeleton() {
return (
<div className="flex flex-col gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-bg-card rounded-2xl p-5 card-shadow animate-pulse">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-16 h-12 rounded-lg bg-bg-elevated" />
<div className="flex flex-col gap-2">
<div className="h-4 w-40 bg-bg-elevated rounded" />
<div className="h-3 w-28 bg-bg-elevated rounded" />
<div className="h-3 w-20 bg-bg-elevated rounded" />
</div>
</div>
<div className="flex items-center gap-3">
<div className="h-8 w-20 bg-bg-elevated rounded-lg" />
<div className="w-5 h-5 bg-bg-elevated rounded" />
</div>
</div>
</div>
))}
</div>
)
}
// 历史任务卡片
function HistoryCard({ task, onClick }: { task: HistoryTask; onClick: () => void }) {
const status = statusConfig[task.status]
const StatusIcon = status.icon
return (
<div
className="bg-bg-card rounded-2xl p-5 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-16 h-12 rounded-lg bg-[#1A1A1E] flex items-center justify-center flex-shrink-0">
<Video className="w-5 h-5 text-text-tertiary" />
</div>
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-text-primary">{task.title}</span>
<span className="text-sm text-text-secondary">{task.description}</span>
<div className="flex items-center gap-3 mt-1">
<span className="text-xs text-text-tertiary">{task.platform}</span>
<span className="text-xs text-text-tertiary">
{task.completedAt || task.expiredAt}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className={cn('px-3 py-1.5 rounded-lg flex items-center gap-1.5', status.bgColor)}>
<StatusIcon className={cn('w-4 h-4', status.color)} />
<span className={cn('text-sm font-medium', status.color)}>{status.label}</span>
</div>
<ChevronRight className="w-5 h-5 text-text-tertiary" />
</div>
</div>
</div>
)
}
export default function CreatorHistoryPage() {
const router = useRouter()
const toast = useToast()
const [filter, setFilter] = useState<HistoryStatus | 'all'>('all')
const [loading, setLoading] = useState(true)
const [historyTasks, setHistoryTasks] = useState<HistoryTask[]>([])
const loadHistory = useCallback(async () => {
if (USE_MOCK) {
setHistoryTasks(mockHistory)
setLoading(false)
return
}
try {
setLoading(true)
const response = await api.listTasks(1, 50, 'completed')
const mapped = response.items.map(mapTaskResponseToHistory)
setHistoryTasks(mapped)
} catch (err) {
const message = err instanceof Error ? err.message : '加载历史记录失败'
toast.error(message)
console.error('加载历史记录失败:', err)
} finally {
setLoading(false)
}
}, [toast])
useEffect(() => {
loadHistory()
}, [loadHistory])
const filteredHistory = filter === 'all' ? historyTasks : historyTasks.filter(t => t.status === filter)
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">
<button
type="button"
onClick={() => router.back()}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-elevated text-text-secondary text-sm hover:bg-bg-card transition-colors w-fit mb-2"
>
<ArrowLeft className="w-4 h-4" />
</button>
<h1 className="text-xl lg:text-[28px] font-bold text-text-primary"></h1>
<p className="text-sm lg:text-[15px] text-text-secondary"></p>
</div>
<div className="flex items-center gap-2 px-4 py-2.5 bg-bg-card rounded-xl border border-border-subtle">
<Filter className="w-[18px] h-[18px] text-text-secondary" />
<select
value={filter}
onChange={(e) => setFilter(e.target.value as HistoryStatus | 'all')}
className="bg-transparent text-sm text-text-primary focus:outline-none"
>
<option value="all"></option>
<option value="completed"></option>
<option value="expired"></option>
<option value="cancelled"></option>
</select>
</div>
</div>
{/* 统计信息 */}
<div className="flex items-center gap-6 bg-bg-card rounded-2xl p-5 card-shadow">
<div className="flex flex-col items-center gap-1 flex-1">
<span className="text-2xl font-bold text-accent-green">
{historyTasks.filter(t => t.status === 'completed').length}
</span>
<span className="text-xs text-text-tertiary"></span>
</div>
<div className="w-px h-10 bg-border-subtle" />
<div className="flex flex-col items-center gap-1 flex-1">
<span className="text-2xl font-bold text-text-tertiary">
{historyTasks.filter(t => t.status === 'expired').length}
</span>
<span className="text-xs text-text-tertiary"></span>
</div>
<div className="w-px h-10 bg-border-subtle" />
<div className="flex flex-col items-center gap-1 flex-1">
<span className="text-2xl font-bold text-accent-coral">
{historyTasks.filter(t => t.status === 'cancelled').length}
</span>
<span className="text-xs text-text-tertiary"></span>
</div>
</div>
{/* 任务列表 */}
<div className="flex flex-col gap-4 flex-1 overflow-y-auto pr-2">
{loading ? (
<HistorySkeleton />
) : filteredHistory.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<Clock 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>
) : (
filteredHistory.map((task) => (
<HistoryCard
key={task.id}
task={task}
onClick={() => router.push(`/creator/task/${task.id}`)}
/>
))
)}
</div>
</div>
</ResponsiveLayout>
)
}