'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' && ( {/* 品牌调性 + 视频时长 */}
setBrandTone(e.target.value)} placeholder="例如:年轻、活力、清新" />
setMinDuration(e.target.value ? parseInt(e.target.value) : undefined)} placeholder="最短" /> ~ setMaxDuration(e.target.value ? parseInt(e.target.value) : undefined)} placeholder="最长" />
{/* 其他要求 */}