'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]}
)}
)
}