'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useToast } from '@/components/ui/Toast'
import { Card, CardContent } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import {
ArrowLeft,
FileText,
Shield,
Plus,
Trash2,
AlertTriangle,
CheckCircle,
Bot,
Users,
Save,
Upload,
ChevronDown,
ChevronUp,
Loader2
} from 'lucide-react'
import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
import { useOSSUpload } from '@/hooks/useOSSUpload'
import type { BriefResponse, BriefCreateRequest, SellingPoint, BlacklistWord, BriefAttachment } from '@/types/brief'
// ==================== Mock 数据 ====================
const mockBrief: BriefResponse = {
id: 'bf-001',
project_id: 'proj-001',
project_name: 'XX品牌618推广',
selling_points: [
{ content: '视频时长:60-90秒', required: true },
{ content: '必须展示产品使用过程', required: true },
{ content: '需要口播品牌slogan:"XX品牌,夏日焕新"', required: true },
{ content: '背景音乐需使用品牌指定曲库', required: false },
],
blacklist_words: [
{ word: '最好', reason: '违反广告法' },
{ word: '第一', reason: '违反广告法' },
{ word: '绝对', reason: '夸大宣传' },
{ word: '100%', reason: '夸大宣传' },
],
competitors: ['竞品A', '竞品B', '竞品C'],
brand_tone: '年轻、活力、清新',
min_duration: 60,
max_duration: 90,
other_requirements: '本次618大促营销活动,需要达人围绕夏日护肤、美妆新品进行内容创作。',
attachments: [
{ id: 'att-001', name: '品牌视觉指南.pdf', url: 'https://example.com/brand-guide.pdf' },
{ id: 'att-002', name: '产品资料包.zip', url: 'https://example.com/product-pack.zip' },
],
created_at: '2026-02-01T00:00:00Z',
updated_at: '2026-02-05T00:00:00Z',
}
const mockRules = {
aiReview: {
enabled: true,
strictness: 'medium',
checkItems: [
{ id: 'forbidden_words', name: '违禁词检测', enabled: true },
{ id: 'competitor', name: '竞品提及检测', enabled: true },
{ id: 'brand_tone', name: '品牌调性检测', enabled: true },
{ id: 'duration', name: '视频时长检测', enabled: true },
{ id: 'music', name: '背景音乐检测', enabled: false },
],
},
manualReview: {
scriptRequired: true,
videoRequired: true,
agencyCanApprove: true,
brandFinalReview: true,
},
appealRules: {
maxAppeals: 3,
appealDeadline: 48,
},
}
// 严格程度选项
const strictnessOptions = [
{ value: 'low', label: '宽松', description: '仅检测明显违规内容' },
{ value: 'medium', label: '标准', description: '平衡检测,推荐使用' },
{ value: 'high', label: '严格', description: '严格检测,可能有较多误判' },
]
function ConfigSkeleton() {
return (
{[1, 2, 3, 4].map(i => (
))}
)
}
export default function ProjectConfigPage() {
const router = useRouter()
const params = useParams()
const toast = useToast()
const projectId = params.id as string
const { upload, isUploading, progress: uploadProgress } = useOSSUpload('general')
// Brief state
const [briefExists, setBriefExists] = useState(false)
const [loading, setLoading] = useState(true)
const [projectName, setProjectName] = useState('')
// Brief form fields
const [brandTone, setBrandTone] = useState('')
const [otherRequirements, setOtherRequirements] = useState('')
const [minDuration, setMinDuration] = useState()
const [maxDuration, setMaxDuration] = useState()
const [sellingPoints, setSellingPoints] = useState([])
const [blacklistWords, setBlacklistWords] = useState([])
const [competitors, setCompetitors] = useState([])
const [attachments, setAttachments] = useState([])
// Rules state (local only — no per-project backend API yet)
const [rules, setRules] = useState(mockRules)
const [isSaving, setIsSaving] = useState(false)
const [activeSection, setActiveSection] = useState('brief')
// Input fields
const [newSellingPoint, setNewSellingPoint] = useState('')
const [newBlacklistWord, setNewBlacklistWord] = useState('')
const [newBlacklistReason, setNewBlacklistReason] = useState('')
const [newCompetitor, setNewCompetitor] = useState('')
const populateBrief = (data: BriefResponse) => {
setProjectName(data.project_name || '')
setBrandTone(data.brand_tone || '')
setOtherRequirements(data.other_requirements || '')
setMinDuration(data.min_duration ?? undefined)
setMaxDuration(data.max_duration ?? undefined)
setSellingPoints(data.selling_points || [])
setBlacklistWords(data.blacklist_words || [])
setCompetitors(data.competitors || [])
setAttachments(data.attachments || [])
}
const loadBrief = useCallback(async () => {
if (USE_MOCK) {
populateBrief(mockBrief)
setBriefExists(true)
setLoading(false)
return
}
try {
const data = await api.getBrief(projectId)
populateBrief(data)
setBriefExists(true)
} catch (err: any) {
if (err?.response?.status === 404) {
setBriefExists(false)
} else {
console.error('Failed to load brief:', err)
toast.error('加载Brief失败')
}
} finally {
setLoading(false)
}
}, [projectId, toast])
useEffect(() => {
loadBrief()
}, [loadBrief])
const handleSaveBrief = async () => {
setIsSaving(true)
try {
const briefData: BriefCreateRequest = {
selling_points: sellingPoints,
blacklist_words: blacklistWords,
competitors,
brand_tone: brandTone || undefined,
min_duration: minDuration,
max_duration: maxDuration,
other_requirements: otherRequirements || undefined,
attachments,
}
if (USE_MOCK) {
await new Promise(resolve => setTimeout(resolve, 1000))
} else if (briefExists) {
await api.updateBrief(projectId, briefData)
} else {
await api.createBrief(projectId, briefData)
setBriefExists(true)
}
toast.success('Brief配置已保存')
} catch (err) {
console.error('Failed to save brief:', err)
toast.error('保存失败,请重试')
} finally {
setIsSaving(false)
}
}
// Selling points
const addSellingPoint = () => {
if (newSellingPoint.trim()) {
setSellingPoints([...sellingPoints, { content: newSellingPoint.trim(), required: false }])
setNewSellingPoint('')
}
}
const removeSellingPoint = (index: number) => {
setSellingPoints(sellingPoints.filter((_, i) => i !== index))
}
const toggleSellingPointRequired = (index: number) => {
setSellingPoints(sellingPoints.map((sp, i) =>
i === index ? { ...sp, required: !sp.required } : sp
))
}
// Blacklist words
const addBlacklistWord = () => {
if (newBlacklistWord.trim()) {
setBlacklistWords([...blacklistWords, { word: newBlacklistWord.trim(), reason: newBlacklistReason.trim() || '品牌规范' }])
setNewBlacklistWord('')
setNewBlacklistReason('')
}
}
const removeBlacklistWord = (index: number) => {
setBlacklistWords(blacklistWords.filter((_, i) => i !== index))
}
// Competitors
const addCompetitorItem = () => {
if (newCompetitor.trim() && !competitors.includes(newCompetitor.trim())) {
setCompetitors([...competitors, newCompetitor.trim()])
setNewCompetitor('')
}
}
const removeCompetitor = (name: string) => {
setCompetitors(competitors.filter(c => c !== name))
}
// Attachment upload
const handleAttachmentUpload = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0]
if (!file) return
if (USE_MOCK) {
setAttachments([...attachments, {
id: `att-${Date.now()}`,
name: file.name,
url: `mock://${file.name}`,
}])
return
}
try {
const result = await upload(file)
setAttachments([...attachments, {
id: `att-${Date.now()}`,
name: file.name,
url: result.url,
}])
} catch {
toast.error('文件上传失败')
}
}
const removeAttachment = (id: string) => {
setAttachments(attachments.filter(a => a.id !== id))
}
// AI check item toggles (local state only)
const toggleAiCheckItem = (itemId: string) => {
setRules({
...rules,
aiReview: {
...rules.aiReview,
checkItems: rules.aiReview.checkItems.map(item =>
item.id === itemId ? { ...item, enabled: !item.enabled } : item
),
},
})
}
const SectionHeader = ({ title, icon: Icon, section }: { title: string; icon: React.ElementType; section: string }) => (
)
if (loading) return
return (
{/* 顶部导航 */}
Brief和规则配置
{projectName || `项目 ${projectId}`}
{/* Brief配置 */}
{activeSection === 'brief' && (
{/* 品牌调性 + 视频时长 */}
{/* 其他要求 */}
{/* 卖点 / 创作要求 */}
{sellingPoints.map((sp, index) => (
{sp.content}
{sp.required && 必选}
))}
{/* 禁止词 */}
{blacklistWords.map((bw, index) => (
{bw.word}
{bw.reason &&
— {bw.reason}}
))}
{/* 竞品品牌 */}
{competitors.map((name) => (
{name}
))}
{/* 参考资料 */}
{attachments.map((att) => (
{att.name}
{att.size && {att.size}}
))}
)}
{/* AI审核规则 */}
{activeSection === 'ai' && (
{/* AI审核开关 */}
{rules.aiReview.enabled && (
<>
{/* 严格程度 */}
{strictnessOptions.map((option) => (
))}
{/* 检测项目 */}
{rules.aiReview.checkItems.map((item) => (
{item.name}
))}
>
)}
)}
{/* 人工审核规则 */}
{activeSection === 'manual' && (
脚本需要人工审核
脚本提交后需要代理商/品牌方审核
视频需要人工审核
视频提交后需要代理商/品牌方审核
代理商终审权限
允许代理商直接通过/驳回内容,无需品牌方审核
)}
{/* 申诉规则 */}
{activeSection === 'appeal' && (
)}
)
}