Your Name 37ac749071 fix: 修复前端代码质量问题
- 创建 Toast 通知组件,替换所有 alert() 调用
- 修复 useReview hook 内存泄漏(setInterval 清理)
- 移除所有 console.error 和 console.log 语句
- 为复制操作失败添加用户友好的 toast 提示

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 12:48:22 +08:00

320 lines
13 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 { useToast } from '@/components/ui/Toast'
import {
ArrowLeft,
FileText,
Download,
Eye,
Target,
Ban,
File,
Building2,
Calendar,
Clock,
ChevronRight
} from 'lucide-react'
import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout'
import { Modal } from '@/components/ui/Modal'
import { Button } from '@/components/ui/Button'
// 代理商Brief文档类型
type AgencyBriefFile = {
id: string
name: string
size: string
uploadedAt: string
description?: string
}
// 模拟任务数据
const mockTaskInfo = {
id: 'task-001',
taskName: 'XX品牌618推广',
agencyName: '星辰传媒',
brandName: 'XX护肤品牌',
deadline: '2026-06-18',
createdAt: '2026-02-08',
}
// 模拟代理商Brief数据
const mockAgencyBrief = {
files: [
{ id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' },
{ id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' },
{ id: 'af3', name: '品牌视觉参考.pdf', size: '3.2MB', uploadedAt: '2026-02-02', description: '视觉风格和拍摄参考示例' },
{ id: 'af4', name: '产品素材包.zip', size: '15.6MB', uploadedAt: '2026-02-02', description: '产品图片、视频素材等' },
] as AgencyBriefFile[],
sellingPoints: [
{ id: 'sp1', content: 'SPF50+ PA++++', required: true },
{ id: 'sp2', content: '轻薄质地,不油腻', required: true },
{ id: 'sp3', content: '延展性好,易推开', required: false },
{ id: 'sp4', content: '适合敏感肌', required: false },
{ id: 'sp5', content: '夏日必备防晒', required: true },
],
blacklistWords: [
{ id: 'bw1', word: '最好', reason: '绝对化用语' },
{ id: 'bw2', word: '第一', reason: '绝对化用语' },
{ id: 'bw3', word: '神器', reason: '夸大宣传' },
{ id: 'bw4', word: '完美', reason: '绝对化用语' },
{ id: 'bw5', word: '100%', reason: '虚假宣传' },
],
contentRequirements: [
'视频时长60-90秒',
'需展示产品质地和使用效果',
'需在户外或阳光下拍摄',
'需提及产品核心卖点',
],
}
export default function TaskBriefPage() {
const router = useRouter()
const params = useParams()
const toast = useToast()
const [previewFile, setPreviewFile] = useState<AgencyBriefFile | null>(null)
const handleDownload = (file: AgencyBriefFile) => {
toast.info(`下载文件: ${file.name}`)
}
const handleDownloadAll = () => {
toast.info('下载全部文件')
}
const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required)
const optionalPoints = mockAgencyBrief.sellingPoints.filter(sp => !sp.required)
return (
<ResponsiveLayout role="creator">
<div className="flex flex-col gap-6 h-full">
{/* 顶部导航 */}
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-3 mb-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"
>
<ArrowLeft className="w-4 h-4" />
</button>
</div>
<h1 className="text-xl lg:text-[28px] font-bold text-text-primary">{mockTaskInfo.taskName}</h1>
<p className="text-sm lg:text-[15px] text-text-secondary">Brief文档</p>
</div>
<Button onClick={() => router.push(`/creator/task/${params.id}`)}>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
{/* 任务基本信息 */}
<div className="bg-bg-card rounded-2xl p-5 card-shadow">
<h3 className="text-base font-semibold text-text-primary mb-4"></h3>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-500/15 flex items-center justify-center">
<Building2 className="w-5 h-5 text-purple-400" />
</div>
<div>
<p className="text-xs text-text-tertiary"></p>
<p className="text-sm font-medium text-text-primary">{mockTaskInfo.agencyName}</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-indigo/15 flex items-center justify-center">
<Building2 className="w-5 h-5 text-accent-indigo" />
</div>
<div>
<p className="text-xs text-text-tertiary"></p>
<p className="text-sm font-medium text-text-primary">{mockTaskInfo.brandName}</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-green/15 flex items-center justify-center">
<Calendar className="w-5 h-5 text-accent-green" />
</div>
<div>
<p className="text-xs text-text-tertiary"></p>
<p className="text-sm font-medium text-text-primary">{mockTaskInfo.createdAt}</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-coral/15 flex items-center justify-center">
<Clock className="w-5 h-5 text-accent-coral" />
</div>
<div>
<p className="text-xs text-text-tertiary"></p>
<p className="text-sm font-medium text-text-primary">{mockTaskInfo.deadline}</p>
</div>
</div>
</div>
</div>
{/* 主要内容区域 - 可滚动 */}
<div className="flex-1 overflow-y-auto space-y-6">
{/* Brief文档列表 */}
<div className="bg-bg-card rounded-2xl p-5 card-shadow">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<File className="w-5 h-5 text-accent-indigo" />
<h3 className="text-base font-semibold text-text-primary">Brief </h3>
<span className="text-sm text-text-tertiary">({mockAgencyBrief.files.length})</span>
</div>
<Button variant="secondary" size="sm" onClick={handleDownloadAll}>
<Download className="w-4 h-4" />
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{mockAgencyBrief.files.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-4 bg-bg-elevated rounded-xl hover:bg-bg-page transition-colors"
>
<div className="flex items-center gap-3 min-w-0">
<div className="w-11 h-11 rounded-xl bg-accent-indigo/15 flex items-center justify-center flex-shrink-0">
<FileText className="w-5 h-5 text-accent-indigo" />
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-text-primary truncate">{file.name}</p>
<p className="text-xs text-text-tertiary">{file.size}</p>
{file.description && (
<p className="text-xs text-text-secondary mt-0.5 truncate">{file.description}</p>
)}
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-2">
<button
type="button"
onClick={() => setPreviewFile(file)}
className="p-2.5 hover:bg-bg-card rounded-lg transition-colors"
>
<Eye className="w-4 h-4 text-text-secondary" />
</button>
<button
type="button"
onClick={() => handleDownload(file)}
className="p-2.5 hover:bg-bg-card rounded-lg transition-colors"
>
<Download className="w-4 h-4 text-text-secondary" />
</button>
</div>
</div>
))}
</div>
</div>
{/* 内容要求 */}
<div className="bg-bg-card rounded-2xl p-5 card-shadow">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-accent-amber" />
<h3 className="text-base font-semibold text-text-primary"></h3>
</div>
<ul className="space-y-2">
{mockAgencyBrief.contentRequirements.map((req, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-text-secondary">
<span className="w-1.5 h-1.5 rounded-full bg-accent-amber mt-2 flex-shrink-0" />
{req}
</li>
))}
</ul>
</div>
{/* 卖点要求 */}
<div className="bg-bg-card rounded-2xl p-5 card-shadow">
<div className="flex items-center gap-2 mb-4">
<Target className="w-5 h-5 text-accent-green" />
<h3 className="text-base font-semibold text-text-primary"></h3>
</div>
<div className="space-y-3">
{requiredPoints.length > 0 && (
<div className="p-4 bg-accent-coral/10 rounded-xl border border-accent-coral/30">
<p className="text-xs text-accent-coral font-semibold mb-2"></p>
<div className="flex flex-wrap gap-2">
{requiredPoints.map((sp) => (
<span key={sp.id} className="px-3 py-1.5 text-sm bg-accent-coral/20 text-accent-coral rounded-lg font-medium">
{sp.content}
</span>
))}
</div>
</div>
)}
{optionalPoints.length > 0 && (
<div className="p-4 bg-bg-elevated rounded-xl">
<p className="text-xs text-text-tertiary font-semibold mb-2"></p>
<div className="flex flex-wrap gap-2">
{optionalPoints.map((sp) => (
<span key={sp.id} className="px-3 py-1.5 text-sm bg-bg-page text-text-secondary rounded-lg">
{sp.content}
</span>
))}
</div>
</div>
)}
</div>
</div>
{/* 违禁词 */}
<div className="bg-bg-card rounded-2xl p-5 card-shadow">
<div className="flex items-center gap-2 mb-4">
<Ban className="w-5 h-5 text-accent-coral" />
<h3 className="text-base font-semibold text-text-primary">使</h3>
</div>
<div className="flex flex-wrap gap-2">
{mockAgencyBrief.blacklistWords.map((bw) => (
<span
key={bw.id}
className="px-3 py-1.5 text-sm bg-accent-coral/15 text-accent-coral rounded-lg border border-accent-coral/30"
>
{bw.word}<span className="text-xs opacity-75 ml-1">{bw.reason}</span>
</span>
))}
</div>
</div>
{/* 底部操作按钮 */}
<div className="flex justify-center py-4">
<Button size="lg" onClick={() => router.push(`/creator/task/${params.id}`)}>
<ChevronRight className="w-5 h-5" />
</Button>
</div>
</div>
</div>
{/* 文件预览弹窗 */}
<Modal
isOpen={!!previewFile}
onClose={() => setPreviewFile(null)}
title={previewFile?.name || '文件预览'}
size="lg"
>
<div className="space-y-4">
<div className="aspect-[4/3] bg-bg-elevated rounded-lg flex items-center justify-center">
<div className="text-center">
<FileText className="w-12 h-12 mx-auto text-accent-indigo mb-4" />
<p className="text-text-secondary"></p>
<p className="text-xs text-text-tertiary mt-1"></p>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setPreviewFile(null)}>
</Button>
{previewFile && (
<Button onClick={() => handleDownload(previewFile)}>
<Download className="w-4 h-4" />
</Button>
)}
</div>
</div>
</Modal>
</ResponsiveLayout>
)
}