'use client' import { useState, useEffect, useCallback } from 'react' import { Plus, FileText, Trash2, Edit, Search, Eye, Loader2 } from 'lucide-react' import { Card, CardContent } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { Modal } from '@/components/ui/Modal' import { SuccessTag, PendingTag } from '@/components/ui/Tag' import { useToast } from '@/components/ui/Toast' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { ProjectResponse } from '@/types/project' import type { BriefResponse } from '@/types/brief' // Brief + Project 联合视图 interface BriefItem { projectId: string projectName: string projectStatus: string brief: BriefResponse | null updatedAt: string } // ==================== Mock 数据 ==================== const mockBriefItems: BriefItem[] = [ { projectId: 'PJ000001', projectName: '2024 夏日护肤活动', projectStatus: 'active', brief: { id: 'BF000001', project_id: 'PJ000001', brand_tone: '清新自然', selling_points: [{ content: 'SPF50+ PA++++', required: true }, { content: '轻薄不油腻', required: false }], blacklist_words: [{ word: '最好', reason: '极限词' }], competitors: ['竞品A'], min_duration: 30, max_duration: 180, other_requirements: '需在开头3秒内展示产品', attachments: [], created_at: '2024-01-15', updated_at: '2024-02-01', }, updatedAt: '2024-02-01', }, { projectId: 'PJ000002', projectName: '新品口红上市', projectStatus: 'active', brief: { id: 'BF000002', project_id: 'PJ000002', brand_tone: '时尚摩登', selling_points: [{ content: '持久不脱色', required: true }], blacklist_words: [], competitors: [], min_duration: 15, max_duration: 120, other_requirements: '', attachments: [], created_at: '2024-02-01', updated_at: '2024-02-03', }, updatedAt: '2024-02-03', }, { projectId: 'PJ000003', projectName: '年货节活动', projectStatus: 'completed', brief: null, updatedAt: '2024-01-20', }, ] function BriefSkeleton() { return (
{[1, 2, 3].map(i => (
))}
) } export default function BriefsPage() { const toast = useToast() const [briefItems, setBriefItems] = useState([]) const [loading, setLoading] = useState(true) const [searchQuery, setSearchQuery] = useState('') // 查看详情 const [showDetailModal, setShowDetailModal] = useState(false) const [selectedItem, setSelectedItem] = useState(null) const loadData = useCallback(async () => { if (USE_MOCK) { setBriefItems(mockBriefItems) setLoading(false) return } try { const projectRes = await api.listProjects(1, 100) const items: BriefItem[] = [] // 并行获取每个项目的 Brief const briefPromises = projectRes.items.map(async (project: ProjectResponse) => { try { const brief = await api.getBrief(project.id) return { projectId: project.id, projectName: project.name, projectStatus: project.status, brief, updatedAt: brief.updated_at || project.updated_at, } } catch { // Brief 不存在返回 null return { projectId: project.id, projectName: project.name, projectStatus: project.status, brief: null, updatedAt: project.updated_at, } } }) const results = await Promise.all(briefPromises) setBriefItems(results) } catch (err) { console.error('Failed to load briefs:', err) toast.error('加载 Brief 列表失败') } finally { setLoading(false) } }, [toast]) useEffect(() => { loadData() }, [loadData]) const filteredItems = briefItems.filter((item) => item.projectName.toLowerCase().includes(searchQuery.toLowerCase()) ) // 查看 Brief 详情 const viewBriefDetail = (item: BriefItem) => { setSelectedItem(item) setShowDetailModal(true) } return (

Brief 管理

查看各项目的 Brief 配置情况,在项目设置中编辑 Brief

{/* 搜索 */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2.5 border border-border-subtle rounded-xl bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo" />
{/* Brief 列表 */} {loading ? ( ) : (
{filteredItems.map((item) => (
{item.brief ? ( 已配置 ) : ( 未配置 )}

{item.projectName}

{item.brief ? ( <> {item.brief.brand_tone && `调性: ${item.brief.brand_tone}`} {(item.brief.selling_points?.length ?? 0) > 0 && ` · ${item.brief.selling_points!.length} 个卖点`} ) : ( '该项目尚未配置 Brief' )}

{item.brief && (
{item.brief.selling_points?.length || 0} 个卖点 {item.brief.blacklist_words?.length || 0} 个违禁词 {item.brief.min_duration && item.brief.max_duration && ( {item.brief.min_duration}-{item.brief.max_duration}秒 )}
)}
更新于 {item.updatedAt?.split('T')[0] || '-'}
{item.brief && ( )}
))} {filteredItems.length === 0 && !loading && (

没有找到匹配的项目

)}
)} {/* Brief 详情弹窗 */} { setShowDetailModal(false) setSelectedItem(null) }} title={selectedItem?.projectName ? `Brief - ${selectedItem.projectName}` : 'Brief 详情'} size="lg" > {selectedItem?.brief && (

{selectedItem.projectName}

{selectedItem.brief.brand_tone && (

品牌调性: {selectedItem.brief.brand_tone}

)}
已配置
{/* 卖点列表 */} {(selectedItem.brief.selling_points?.length ?? 0) > 0 && (

卖点要求

{selectedItem.brief.selling_points!.map((sp, idx) => (
{sp.content} {sp.required && ( 必须 )}
))}
)} {/* 违禁词 */} {(selectedItem.brief.blacklist_words?.length ?? 0) > 0 && (

违禁词

{selectedItem.brief.blacklist_words!.map((bw, idx) => ( {bw.word} {bw.reason && ({bw.reason})} ))}
)} {/* 时长要求 */} {(selectedItem.brief.min_duration || selectedItem.brief.max_duration) && (

{selectedItem.brief.min_duration || '-'}秒

最短时长

{selectedItem.brief.max_duration || '-'}秒

最长时长

)} {/* 其他要求 */} {selectedItem.brief.other_requirements && (

其他要求

{selectedItem.brief.other_requirements}

)} {/* 时间信息 */}
创建时间: {selectedItem.brief.created_at?.split('T')[0]}
最后更新: {selectedItem.brief.updated_at?.split('T')[0]}
)}
) }