feat: 完善审核台文件预览与消息通知系统
主要更新: - 新增 FilePreview 通用组件,支持视频/图片/PDF 内嵌预览 - 审核详情页添加文件信息卡片、预览/下载功能 - 审核列表和详情页添加申诉标识和申诉理由显示 - 完善三端消息通知系统(达人/代理商/品牌) - 新增达人 Brief 查看页面 - 新增品牌方消息中心页面 - 创建后端开发备忘文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
bbc8a4f641
commit
a5a005db0c
@@ -4,30 +4,84 @@ import { useState } from 'react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, WarningTag, ErrorTag } from '@/components/ui/Tag'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { SuccessTag } from '@/components/ui/Tag'
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
Upload,
|
||||
Download,
|
||||
Eye,
|
||||
Target,
|
||||
Ban,
|
||||
AlertTriangle,
|
||||
Sparkles,
|
||||
FileDown,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Building2,
|
||||
Info,
|
||||
Plus,
|
||||
X,
|
||||
Save,
|
||||
Sparkles,
|
||||
Target,
|
||||
Ban,
|
||||
AlertTriangle
|
||||
Upload,
|
||||
Trash2,
|
||||
File
|
||||
} from 'lucide-react'
|
||||
import { getPlatformInfo } from '@/lib/platforms'
|
||||
|
||||
// 模拟 Brief 详情
|
||||
const mockBrief = {
|
||||
// 文件类型
|
||||
type BriefFile = {
|
||||
id: string
|
||||
name: string
|
||||
type: 'brief' | 'rule' | 'reference'
|
||||
size: string
|
||||
uploadedAt: string
|
||||
}
|
||||
|
||||
// 模拟品牌方 Brief(只读)
|
||||
const mockBrandBrief = {
|
||||
id: 'brief-001',
|
||||
projectName: 'XX品牌618推广',
|
||||
brandName: 'XX护肤品牌',
|
||||
platform: 'douyin',
|
||||
// 品牌方上传的文件列表
|
||||
files: [
|
||||
{ id: 'f1', name: 'XX品牌618推广Brief.pdf', type: 'brief' as const, size: '2.3MB', uploadedAt: '2026-02-01' },
|
||||
{ id: 'f2', name: '产品卖点说明.docx', type: 'reference' as const, size: '1.2MB', uploadedAt: '2026-02-01' },
|
||||
{ id: 'f3', name: '品牌视觉指南.pdf', type: 'reference' as const, size: '5.8MB', uploadedAt: '2026-02-01' },
|
||||
],
|
||||
// 品牌方配置的规则(只读)
|
||||
brandRules: {
|
||||
restrictions: '不可提及竞品,不可使用绝对化用语',
|
||||
competitors: ['安耐晒', '资生堂', '兰蔻'],
|
||||
},
|
||||
}
|
||||
|
||||
// 代理商上传的Brief文档(可编辑)
|
||||
type AgencyFile = {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
uploadedAt: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 代理商自己的配置(可编辑)
|
||||
const mockAgencyConfig = {
|
||||
status: 'configured',
|
||||
fileName: 'XX品牌618推广Brief.pdf',
|
||||
uploadedAt: '2026-02-01',
|
||||
configuredAt: '2026-02-02',
|
||||
// 代理商上传的Brief文档(给达人看的)
|
||||
agencyFiles: [
|
||||
{ id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' },
|
||||
{ id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' },
|
||||
] as AgencyFile[],
|
||||
// AI 解析出的内容
|
||||
aiParsedContent: {
|
||||
productName: 'XX品牌防晒霜',
|
||||
targetAudience: '18-35岁女性',
|
||||
contentRequirements: '需展示产品质地、使用效果,视频时长30-60秒',
|
||||
},
|
||||
// 代理商配置的卖点(可编辑)
|
||||
sellingPoints: [
|
||||
{ id: 'sp1', content: 'SPF50+ PA++++', required: true },
|
||||
{ id: 'sp2', content: '轻薄质地,不油腻', required: true },
|
||||
@@ -35,40 +89,104 @@ const mockBrief = {
|
||||
{ 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: '绝对化用语' },
|
||||
],
|
||||
aiParsedContent: {
|
||||
productName: 'XX品牌防晒霜',
|
||||
targetAudience: '18-35岁女性',
|
||||
contentRequirements: '需展示产品质地、使用效果',
|
||||
restrictions: '不可提及竞品,不可使用绝对化用语',
|
||||
}
|
||||
|
||||
// 平台规则
|
||||
const platformRules = {
|
||||
douyin: {
|
||||
name: '抖音',
|
||||
rules: [
|
||||
{ category: '广告法违禁词', items: ['最', '第一', '顶级', '极致', '绝对', '永久', '万能', '特效'] },
|
||||
{ category: '医疗相关禁用', items: ['治疗', '药用', '医学', '临床', '处方'] },
|
||||
{ category: '虚假宣传', items: ['100%', '纯天然', '无副作用', '立竿见影'] },
|
||||
],
|
||||
},
|
||||
xiaohongshu: {
|
||||
name: '小红书',
|
||||
rules: [
|
||||
{ category: '广告法违禁词', items: ['最', '第一', '顶级', '极品', '绝对'] },
|
||||
{ category: '功效承诺禁用', items: ['包治', '根治', '祛除', '永久'] },
|
||||
],
|
||||
},
|
||||
bilibili: {
|
||||
name: 'B站',
|
||||
rules: [
|
||||
{ category: '广告法违禁词', items: ['最', '第一', '顶级', '极致'] },
|
||||
{ category: '虚假宣传', items: ['100%', '纯天然', '无副作用'] },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default function BriefConfigPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const [brief, setBrief] = useState(mockBrief)
|
||||
|
||||
// 品牌方 Brief(只读)
|
||||
const [brandBrief] = useState(mockBrandBrief)
|
||||
|
||||
// 代理商配置(可编辑)
|
||||
const [agencyConfig, setAgencyConfig] = useState(mockAgencyConfig)
|
||||
const [newSellingPoint, setNewSellingPoint] = useState('')
|
||||
const [newBlacklistWord, setNewBlacklistWord] = useState('')
|
||||
const [isAIParsing, setIsAIParsing] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// 弹窗状态
|
||||
const [showFilesModal, setShowFilesModal] = useState(false)
|
||||
const [showAgencyFilesModal, setShowAgencyFilesModal] = useState(false)
|
||||
const [previewFile, setPreviewFile] = useState<BriefFile | null>(null)
|
||||
const [previewAgencyFile, setPreviewAgencyFile] = useState<AgencyFile | null>(null)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isAIParsing, setIsAIParsing] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const platform = getPlatformInfo(brandBrief.platform)
|
||||
const rules = platformRules[brandBrief.platform as keyof typeof platformRules] || platformRules.douyin
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = (file: BriefFile) => {
|
||||
alert(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
// 预览文件
|
||||
const handlePreview = (file: BriefFile) => {
|
||||
setPreviewFile(file)
|
||||
}
|
||||
|
||||
// 导出平台规则文档
|
||||
const handleExportRules = async () => {
|
||||
setIsExporting(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
setIsExporting(false)
|
||||
alert('平台规则文档已导出!')
|
||||
}
|
||||
|
||||
// AI 解析
|
||||
const handleAIParse = async () => {
|
||||
setIsAIParsing(true)
|
||||
// 模拟 AI 解析
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
setIsAIParsing(false)
|
||||
alert('AI 解析完成!')
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('配置已保存!')
|
||||
}
|
||||
|
||||
// 卖点操作
|
||||
const addSellingPoint = () => {
|
||||
if (!newSellingPoint.trim()) return
|
||||
setBrief(prev => ({
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
sellingPoints: [...prev.sellingPoints, { id: `sp${Date.now()}`, content: newSellingPoint, required: false }]
|
||||
}))
|
||||
@@ -76,14 +194,14 @@ export default function BriefConfigPage() {
|
||||
}
|
||||
|
||||
const removeSellingPoint = (id: string) => {
|
||||
setBrief(prev => ({
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
sellingPoints: prev.sellingPoints.filter(sp => sp.id !== id)
|
||||
}))
|
||||
}
|
||||
|
||||
const toggleRequired = (id: string) => {
|
||||
setBrief(prev => ({
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
sellingPoints: prev.sellingPoints.map(sp =>
|
||||
sp.id === id ? { ...sp, required: !sp.required } : sp
|
||||
@@ -91,9 +209,10 @@ export default function BriefConfigPage() {
|
||||
}))
|
||||
}
|
||||
|
||||
// 违禁词操作
|
||||
const addBlacklistWord = () => {
|
||||
if (!newBlacklistWord.trim()) return
|
||||
setBrief(prev => ({
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
blacklistWords: [...prev.blacklistWords, { id: `bw${Date.now()}`, word: newBlacklistWord, reason: '自定义' }]
|
||||
}))
|
||||
@@ -101,17 +220,45 @@ export default function BriefConfigPage() {
|
||||
}
|
||||
|
||||
const removeBlacklistWord = (id: string) => {
|
||||
setBrief(prev => ({
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
blacklistWords: prev.blacklistWords.filter(bw => bw.id !== id)
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
setIsSaving(false)
|
||||
alert('配置已保存!')
|
||||
// 代理商文档操作
|
||||
const handleUploadAgencyFile = async () => {
|
||||
setIsUploading(true)
|
||||
// 模拟上传
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
const newFile: AgencyFile = {
|
||||
id: `af${Date.now()}`,
|
||||
name: '新上传文档.pdf',
|
||||
size: '1.2MB',
|
||||
uploadedAt: new Date().toISOString().split('T')[0],
|
||||
description: '新上传的文档'
|
||||
}
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
agencyFiles: [...prev.agencyFiles, newFile]
|
||||
}))
|
||||
setIsUploading(false)
|
||||
alert('文档上传成功!')
|
||||
}
|
||||
|
||||
const removeAgencyFile = (id: string) => {
|
||||
setAgencyConfig(prev => ({
|
||||
...prev,
|
||||
agencyFiles: prev.agencyFiles.filter(f => f.id !== id)
|
||||
}))
|
||||
}
|
||||
|
||||
const handlePreviewAgencyFile = (file: AgencyFile) => {
|
||||
setPreviewAgencyFile(file)
|
||||
}
|
||||
|
||||
const handleDownloadAgencyFile = (file: AgencyFile) => {
|
||||
alert(`下载文件: ${file.name}`)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -122,48 +269,214 @@ export default function BriefConfigPage() {
|
||||
<ArrowLeft size={20} className="text-text-primary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-bold text-text-primary">{brief.projectName}</h1>
|
||||
<p className="text-sm text-text-secondary">{brief.brandName}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-bold text-text-primary">{brandBrief.projectName}</h1>
|
||||
{platform && (
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${platform.bgColor} ${platform.textColor} border ${platform.borderColor}`}>
|
||||
<span>{platform.icon}</span>
|
||||
{platform.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary flex items-center gap-2 mt-1">
|
||||
<Building2 size={14} />
|
||||
{brandBrief.brandName}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={handleExportRules} disabled={isExporting}>
|
||||
<FileDown size={16} />
|
||||
{isExporting ? '导出中...' : '导出规则'}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
<Save size={16} />
|
||||
{isSaving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ===== 第一部分:品牌方 Brief(只读)===== */}
|
||||
<div className="p-4 bg-purple-500/10 rounded-lg border border-purple-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<Building2 size={20} className="text-purple-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-purple-400 font-medium">品牌方 Brief(只读)</p>
|
||||
<p className="text-sm text-purple-400/80 mt-1">
|
||||
以下是品牌方上传的 Brief 文件和规则,仅供参考,不可编辑。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:Brief 文件和 AI 解析 */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Brief 文件 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-accent-indigo" />
|
||||
Brief 文件
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between p-4 bg-bg-elevated rounded-lg">
|
||||
{/* 品牌方文件 */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-purple-400" />
|
||||
品牌方 Brief 文件
|
||||
<span className="text-sm font-normal text-text-secondary">
|
||||
{brandBrief.files.length} 个文件
|
||||
</span>
|
||||
</span>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowFilesModal(true)}>
|
||||
<Eye size={14} />
|
||||
查看全部
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{brandBrief.files.slice(0, 2).map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText size={32} className="text-accent-indigo" />
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/15 flex items-center justify-center">
|
||||
<FileText size={20} className="text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{brief.fileName}</p>
|
||||
<p className="text-sm text-text-secondary">上传于 {brief.uploadedAt}</p>
|
||||
<p className="font-medium text-text-primary text-sm">{file.name}</p>
|
||||
<p className="text-xs text-text-secondary">{file.size} · {file.uploadedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" size="sm">
|
||||
预览
|
||||
<Button variant="ghost" size="sm" onClick={() => handlePreview(file)}>
|
||||
<Eye size={14} />
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm">
|
||||
<Upload size={14} />
|
||||
重新上传
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDownload(file)}>
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{brandBrief.files.length > 2 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFilesModal(true)}
|
||||
className="w-full p-3 text-sm text-purple-400 hover:bg-purple-500/5 rounded-lg transition-colors"
|
||||
>
|
||||
查看全部 {brandBrief.files.length} 个文件 →
|
||||
</button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 品牌方规则(只读) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle size={18} className="text-orange-400" />
|
||||
品牌方限制
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-xs text-text-tertiary mb-2">限制条件</p>
|
||||
<p className="text-sm text-text-primary">{brandBrief.brandRules.restrictions}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-tertiary mb-2">竞品黑名单</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{brandBrief.brandRules.competitors.map((c, i) => (
|
||||
<span key={i} className="px-2 py-1 text-xs bg-orange-500/15 text-orange-400 rounded border border-orange-500/30">
|
||||
{c}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* ===== 第二部分:代理商配置(可编辑)===== */}
|
||||
<div className="p-4 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles size={20} className="text-accent-indigo flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-accent-indigo font-medium">代理商配置(可编辑)</p>
|
||||
<p className="text-sm text-accent-indigo/80 mt-1">
|
||||
以下配置由代理商编辑,将展示给达人查看。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 代理商Brief文档管理 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<File size={18} className="text-accent-indigo" />
|
||||
代理商 Brief 文档
|
||||
<span className="text-sm font-normal text-text-secondary">
|
||||
{agencyConfig.agencyFiles.length} 个文件(达人可见)
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowAgencyFilesModal(true)}>
|
||||
<Eye size={14} />
|
||||
管理文档
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleUploadAgencyFile} disabled={isUploading}>
|
||||
<Upload size={14} />
|
||||
{isUploading ? '上传中...' : '上传文档'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{agencyConfig.agencyFiles.map((file) => (
|
||||
<div key={file.id} className="p-4 bg-accent-indigo/5 rounded-lg border border-accent-indigo/20 hover:border-accent-indigo/40 transition-colors">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center flex-shrink-0">
|
||||
<FileText size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-text-primary text-sm truncate">{file.name}</p>
|
||||
<p className="text-xs text-text-tertiary mt-0.5">{file.size} · {file.uploadedAt}</p>
|
||||
{file.description && (
|
||||
<p className="text-xs text-text-secondary mt-1 line-clamp-2">{file.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border-subtle">
|
||||
<Button variant="ghost" size="sm" onClick={() => handlePreviewAgencyFile(file)} className="flex-1">
|
||||
<Eye size={14} />
|
||||
预览
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDownloadAgencyFile(file)} className="flex-1">
|
||||
<Download size={14} />
|
||||
下载
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => removeAgencyFile(file.id)} className="text-accent-coral hover:text-accent-coral">
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* 上传占位卡片 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUploadAgencyFile}
|
||||
disabled={isUploading}
|
||||
className="p-4 rounded-lg border-2 border-dashed border-border-subtle hover:border-accent-indigo/50 transition-colors flex flex-col items-center justify-center gap-2 min-h-[140px]"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-bg-elevated flex items-center justify-center">
|
||||
<Plus size={20} className="text-text-tertiary" />
|
||||
</div>
|
||||
<span className="text-sm text-text-secondary">上传新文档</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-accent-indigo/10 rounded-lg border border-accent-indigo/20">
|
||||
<p className="text-xs text-accent-indigo flex items-center gap-2">
|
||||
<Info size={14} />
|
||||
以上文档将展示给达人查看,请确保内容准确完整。
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:AI解析 + 卖点配置 */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* AI 解析结果 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -182,37 +495,33 @@ export default function BriefConfigPage() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-bg-elevated rounded-lg">
|
||||
<p className="text-xs text-text-tertiary mb-1">产品名称</p>
|
||||
<p className="text-text-primary">{brief.aiParsedContent.productName}</p>
|
||||
<p className="text-text-primary font-medium">{agencyConfig.aiParsedContent.productName}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-bg-elevated rounded-lg">
|
||||
<p className="text-xs text-text-tertiary mb-1">目标人群</p>
|
||||
<p className="text-text-primary">{brief.aiParsedContent.targetAudience}</p>
|
||||
<p className="text-text-primary font-medium">{agencyConfig.aiParsedContent.targetAudience}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-bg-elevated rounded-lg col-span-2">
|
||||
<p className="text-xs text-text-tertiary mb-1">内容要求</p>
|
||||
<p className="text-text-primary">{brief.aiParsedContent.contentRequirements}</p>
|
||||
</div>
|
||||
<div className="p-3 bg-bg-elevated rounded-lg col-span-2">
|
||||
<p className="text-xs text-text-tertiary mb-1">限制条件</p>
|
||||
<p className="text-text-primary">{brief.aiParsedContent.restrictions}</p>
|
||||
<p className="text-text-primary">{agencyConfig.aiParsedContent.contentRequirements}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 卖点配置 */}
|
||||
{/* 卖点配置(可编辑) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Target size={18} className="text-accent-green" />
|
||||
卖点配置
|
||||
<span className="text-sm font-normal text-text-secondary ml-2">
|
||||
{brief.sellingPoints.length} 个卖点
|
||||
{agencyConfig.sellingPoints.length} 个卖点
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{brief.sellingPoints.map((sp) => (
|
||||
{agencyConfig.sellingPoints.map((sp) => (
|
||||
<div key={sp.id} className="flex items-center gap-3 p-3 bg-bg-elevated rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
@@ -249,22 +558,53 @@ export default function BriefConfigPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 平台规则 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<AlertTriangle size={18} className="text-accent-amber" />
|
||||
{rules.name}平台规则
|
||||
</span>
|
||||
<Button variant="secondary" size="sm" onClick={handleExportRules} disabled={isExporting}>
|
||||
<FileDown size={14} />
|
||||
导出
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{rules.rules.map((rule, index) => (
|
||||
<div key={index}>
|
||||
<p className="text-sm font-medium text-text-primary mb-2">{rule.category}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{rule.items.map((item, i) => (
|
||||
<span key={i} className="px-2 py-1 text-xs bg-accent-amber/15 text-accent-amber rounded border border-accent-amber/30">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 右侧:违禁词配置 */}
|
||||
<div className="space-y-6">
|
||||
{/* 违禁词配置(可编辑) */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Ban size={18} className="text-accent-coral" />
|
||||
违禁词配置
|
||||
<span className="text-sm font-normal text-text-secondary ml-2">
|
||||
{brief.blacklistWords.length} 个
|
||||
{agencyConfig.blacklistWords.length} 个
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{brief.blacklistWords.map((bw) => (
|
||||
{agencyConfig.blacklistWords.map((bw) => (
|
||||
<div key={bw.id} className="flex items-center justify-between p-3 bg-accent-coral/10 rounded-lg border border-accent-coral/30">
|
||||
<div>
|
||||
<span className="font-medium text-accent-coral">「{bw.word}」</span>
|
||||
@@ -295,22 +635,193 @@ export default function BriefConfigPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 配置信息 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock size={18} className="text-text-tertiary" />
|
||||
配置状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">状态</span>
|
||||
<SuccessTag>已配置</SuccessTag>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">配置时间</span>
|
||||
<span className="text-text-primary">{agencyConfig.configuredAt}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 配置提示 */}
|
||||
<div className="p-4 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
|
||||
<div className="p-4 bg-accent-green/10 rounded-lg border border-accent-green/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle size={20} className="text-accent-indigo flex-shrink-0 mt-0.5" />
|
||||
<CheckCircle size={20} className="text-accent-green flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-accent-indigo font-medium">配置说明</p>
|
||||
<ul className="text-xs text-accent-indigo/80 mt-1 space-y-1">
|
||||
<p className="text-sm text-accent-green font-medium">配置说明</p>
|
||||
<ul className="text-xs text-accent-green/80 mt-1 space-y-1">
|
||||
<li>• 必选卖点必须在内容中提及</li>
|
||||
<li>• 违禁词会触发 AI 审核警告</li>
|
||||
<li>• 修改配置后需重新保存</li>
|
||||
<li>• 此配置将展示给达人查看</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 文件列表弹窗 */}
|
||||
<Modal
|
||||
isOpen={showFilesModal}
|
||||
onClose={() => setShowFilesModal(false)}
|
||||
title="品牌方 Brief 文件"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{brandBrief.files.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-purple-500/15 flex items-center justify-center">
|
||||
<FileText size={24} className="text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{file.name}</p>
|
||||
<p className="text-sm text-text-secondary">{file.size} · 上传于 {file.uploadedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={() => handlePreview(file)}>
|
||||
<Eye size={14} />
|
||||
预览
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => handleDownload(file)}>
|
||||
<Download size={14} />
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 文件预览弹窗(品牌方) */}
|
||||
<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 size={48} className="mx-auto text-text-tertiary 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 size={16} />
|
||||
下载文件
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 代理商文档管理弹窗 */}
|
||||
<Modal
|
||||
isOpen={showAgencyFilesModal}
|
||||
onClose={() => setShowAgencyFilesModal(false)}
|
||||
title="管理代理商 Brief 文档"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm text-text-secondary">
|
||||
以下文档将展示给达人查看,可以添加、删除或预览文档
|
||||
</p>
|
||||
<Button size="sm" onClick={handleUploadAgencyFile} disabled={isUploading}>
|
||||
<Upload size={14} />
|
||||
{isUploading ? '上传中...' : '上传文档'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{agencyConfig.agencyFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between p-4 bg-bg-elevated rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<FileText size={24} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text-primary">{file.name}</p>
|
||||
<p className="text-sm text-text-secondary">{file.size} · 上传于 {file.uploadedAt}</p>
|
||||
{file.description && (
|
||||
<p className="text-xs text-text-tertiary mt-1">{file.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="secondary" size="sm" onClick={() => handlePreviewAgencyFile(file)}>
|
||||
<Eye size={14} />
|
||||
预览
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => handleDownloadAgencyFile(file)}>
|
||||
<Download size={14} />
|
||||
下载
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => removeAgencyFile(file.id)} className="text-accent-coral hover:text-accent-coral">
|
||||
<Trash2 size={14} />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{agencyConfig.agencyFiles.length === 0 && (
|
||||
<div className="py-12 text-center">
|
||||
<File size={48} className="mx-auto text-text-tertiary mb-4" />
|
||||
<p className="text-text-secondary">暂无文档</p>
|
||||
<p className="text-sm text-text-tertiary mt-1">点击上方按钮上传文档</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 代理商文档预览弹窗 */}
|
||||
<Modal
|
||||
isOpen={!!previewAgencyFile}
|
||||
onClose={() => setPreviewAgencyFile(null)}
|
||||
title={previewAgencyFile?.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 size={48} className="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={() => setPreviewAgencyFile(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
{previewAgencyFile && (
|
||||
<Button onClick={() => handleDownloadAgencyFile(previewAgencyFile)}>
|
||||
<Download size={16} />
|
||||
下载文件
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
|
||||
@@ -15,14 +16,37 @@ import {
|
||||
Clock,
|
||||
Check,
|
||||
MoreVertical,
|
||||
PlusCircle
|
||||
PlusCircle,
|
||||
UserCheck,
|
||||
UserX,
|
||||
Bot,
|
||||
Settings,
|
||||
CalendarClock,
|
||||
Building2,
|
||||
Eye
|
||||
} from 'lucide-react'
|
||||
import { getPlatformInfo } from '@/lib/platforms'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// 消息类型
|
||||
type MessageType =
|
||||
| 'appeal_quota_request' // 达人申请增加申诉次数
|
||||
| 'task_submitted' // 达人提交了脚本/视频
|
||||
| 'review_complete' // 品牌终审通过
|
||||
| 'review_rejected' // 品牌终审驳回
|
||||
| 'new_project' // 被品牌邀请参与项目
|
||||
| 'warning' // 风险预警
|
||||
| 'creator_accept' // 达人接受签约邀请
|
||||
| 'creator_reject' // 达人拒绝签约邀请
|
||||
| 'ai_review_complete' // AI审核完成,待代理商审核
|
||||
| 'brand_config_updated' // 品牌方更新了配置
|
||||
| 'task_deadline' // 任务截止提醒
|
||||
| 'brand_brief_updated' // 品牌方更新了Brief
|
||||
| 'system_notice' // 系统通知
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
type: string
|
||||
type: MessageType
|
||||
title: string
|
||||
content: string
|
||||
time: string
|
||||
@@ -31,6 +55,11 @@ interface Message {
|
||||
iconColor: string
|
||||
bgColor: string
|
||||
platform?: string
|
||||
taskId?: string
|
||||
projectId?: string
|
||||
creatorName?: string
|
||||
hasAction?: boolean
|
||||
actionType?: 'review' | 'view' | 'config'
|
||||
// 申诉次数请求专用字段
|
||||
appealRequest?: {
|
||||
creatorName: string
|
||||
@@ -62,18 +91,48 @@ const mockMessages: Message[] = [
|
||||
},
|
||||
{
|
||||
id: 'msg-002',
|
||||
type: 'ai_review_complete',
|
||||
title: 'AI审核完成',
|
||||
content: '达人「小美护肤」的脚本【夏日护肤推广】已通过AI审核,请及时进行人工审核',
|
||||
time: '10分钟前',
|
||||
read: false,
|
||||
icon: Bot,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo/20',
|
||||
platform: 'xiaohongshu',
|
||||
taskId: 'task-006',
|
||||
hasAction: true,
|
||||
actionType: 'review',
|
||||
},
|
||||
{
|
||||
id: 'msg-003',
|
||||
type: 'creator_accept',
|
||||
title: '达人已签约',
|
||||
content: '达人「美妆达人小王」已接受您的签约邀请,可以开始分配任务',
|
||||
time: '20分钟前',
|
||||
read: false,
|
||||
icon: UserCheck,
|
||||
iconColor: 'text-accent-green',
|
||||
bgColor: 'bg-accent-green/20',
|
||||
creatorName: '美妆达人小王',
|
||||
},
|
||||
{
|
||||
id: 'msg-004',
|
||||
type: 'task_submitted',
|
||||
title: '新脚本提交',
|
||||
content: '达人「小美护肤」提交了「夏日护肤推广脚本」,请及时审核。',
|
||||
time: '10分钟前',
|
||||
content: '达人「小美护肤」提交了「夏日护肤推广脚本」,请及时审核',
|
||||
time: '30分钟前',
|
||||
read: false,
|
||||
icon: FileText,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo/20',
|
||||
platform: 'xiaohongshu',
|
||||
taskId: 'task-006',
|
||||
hasAction: true,
|
||||
actionType: 'review',
|
||||
},
|
||||
{
|
||||
id: 'msg-003',
|
||||
id: 'msg-005',
|
||||
type: 'appeal_quota_request',
|
||||
title: '申诉次数申请',
|
||||
content: '达人「美妆达人小王」申请增加「双11护肤品种草」的申诉次数',
|
||||
@@ -91,75 +150,165 @@ const mockMessages: Message[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'msg-004',
|
||||
id: 'msg-006',
|
||||
type: 'task_deadline',
|
||||
title: '任务即将截止',
|
||||
content: '【XX品牌618推广】任务将于3天后截止,还有5位达人未提交脚本',
|
||||
time: '1小时前',
|
||||
read: false,
|
||||
icon: CalendarClock,
|
||||
iconColor: 'text-orange-400',
|
||||
bgColor: 'bg-orange-500/20',
|
||||
platform: 'douyin',
|
||||
hasAction: true,
|
||||
actionType: 'view',
|
||||
},
|
||||
{
|
||||
id: 'msg-007',
|
||||
type: 'review_complete',
|
||||
title: '品牌终审通过',
|
||||
content: '「新品口红试色」视频已通过品牌方终审。',
|
||||
content: '【XX品牌618推广】达人「美妆小红」的视频「新品口红试色」已通过品牌方终审,可通知达人发布',
|
||||
time: '1小时前',
|
||||
read: false,
|
||||
icon: CheckCircle,
|
||||
iconColor: 'text-accent-green',
|
||||
bgColor: 'bg-accent-green/20',
|
||||
platform: 'xiaohongshu',
|
||||
taskId: 'task-004',
|
||||
},
|
||||
{
|
||||
id: 'msg-005',
|
||||
id: 'msg-008',
|
||||
type: 'review_rejected',
|
||||
title: '品牌终审驳回',
|
||||
content: '「健身器材开箱」视频被品牌方驳回,原因:违禁词使用。',
|
||||
content: '【BB运动饮料】达人「健身教练王」的视频「健身器材开箱」被品牌方驳回,原因:违禁词使用,请通知达人修改',
|
||||
time: '2小时前',
|
||||
read: false,
|
||||
icon: XCircle,
|
||||
iconColor: 'text-accent-coral',
|
||||
bgColor: 'bg-accent-coral/20',
|
||||
platform: 'bilibili',
|
||||
taskId: 'task-010',
|
||||
},
|
||||
{
|
||||
id: 'msg-006',
|
||||
id: 'msg-009',
|
||||
type: 'brand_config_updated',
|
||||
title: '品牌规则更新',
|
||||
content: '品牌方「XX护肤品牌」更新了违禁词配置,新增8个违禁词',
|
||||
time: '3小时前',
|
||||
read: true,
|
||||
icon: Settings,
|
||||
iconColor: 'text-accent-amber',
|
||||
bgColor: 'bg-accent-amber/20',
|
||||
hasAction: true,
|
||||
actionType: 'config',
|
||||
},
|
||||
{
|
||||
id: 'msg-010',
|
||||
type: 'brand_brief_updated',
|
||||
title: 'Brief更新通知',
|
||||
content: '品牌方更新了【XX品牌618推广】的Brief要求,请查看最新内容',
|
||||
time: '4小时前',
|
||||
read: true,
|
||||
icon: FileText,
|
||||
iconColor: 'text-accent-indigo',
|
||||
bgColor: 'bg-accent-indigo/20',
|
||||
projectId: 'proj-001',
|
||||
hasAction: true,
|
||||
actionType: 'view',
|
||||
},
|
||||
{
|
||||
id: 'msg-011',
|
||||
type: 'creator_reject',
|
||||
title: '达人已拒绝',
|
||||
content: '达人「时尚博主Anna」拒绝了您的签约邀请',
|
||||
time: '昨天 14:30',
|
||||
read: true,
|
||||
icon: UserX,
|
||||
iconColor: 'text-accent-coral',
|
||||
bgColor: 'bg-accent-coral/20',
|
||||
creatorName: '时尚博主Anna',
|
||||
},
|
||||
{
|
||||
id: 'msg-012',
|
||||
type: 'new_project',
|
||||
title: '新项目邀请',
|
||||
content: '您被邀请参与「XX品牌新品推广」项目,请配置 Brief。',
|
||||
content: '您被邀请参与「XX品牌新品推广」项目,请配置 Brief',
|
||||
time: '昨天',
|
||||
read: true,
|
||||
icon: Users,
|
||||
icon: Building2,
|
||||
iconColor: 'text-purple-400',
|
||||
bgColor: 'bg-purple-500/20',
|
||||
platform: 'douyin',
|
||||
projectId: 'proj-001',
|
||||
hasAction: true,
|
||||
actionType: 'config',
|
||||
},
|
||||
{
|
||||
id: 'msg-007',
|
||||
id: 'msg-013',
|
||||
type: 'warning',
|
||||
title: '风险预警',
|
||||
content: '达人「美妆Lisa」连续2次提交被驳回,建议关注。',
|
||||
content: '达人「美妆Lisa」连续2次提交被驳回,建议关注并提供指导',
|
||||
time: '昨天',
|
||||
read: true,
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-orange-400',
|
||||
bgColor: 'bg-orange-500/20',
|
||||
platform: 'xiaohongshu',
|
||||
creatorName: '美妆Lisa',
|
||||
},
|
||||
{
|
||||
id: 'msg-008',
|
||||
id: 'msg-015',
|
||||
type: 'task_submitted',
|
||||
title: '新视频提交',
|
||||
content: '达人「健身教练王」提交了「健身器材使用教程」视频,请及时审核。',
|
||||
content: '达人「健身教练王」提交了「健身器材使用教程」视频,请及时审核',
|
||||
time: '2天前',
|
||||
read: true,
|
||||
icon: Video,
|
||||
iconColor: 'text-purple-400',
|
||||
bgColor: 'bg-purple-500/20',
|
||||
platform: 'bilibili',
|
||||
taskId: 'task-009',
|
||||
hasAction: true,
|
||||
actionType: 'review',
|
||||
},
|
||||
{
|
||||
id: 'msg-016',
|
||||
type: 'system_notice',
|
||||
title: '系统通知',
|
||||
content: '平台违禁词库已更新,「抖音」平台新增美妆类目违禁词56个',
|
||||
time: '3天前',
|
||||
read: true,
|
||||
icon: Bell,
|
||||
iconColor: 'text-text-secondary',
|
||||
bgColor: 'bg-bg-elevated',
|
||||
},
|
||||
]
|
||||
|
||||
export default function AgencyMessagesPage() {
|
||||
const router = useRouter()
|
||||
const [messages, setMessages] = useState(mockMessages)
|
||||
const [filter, setFilter] = useState<'all' | 'unread'>('all')
|
||||
const [filter, setFilter] = useState<'all' | 'unread' | 'pending'>('all')
|
||||
|
||||
const unreadCount = messages.filter(m => !m.read).length
|
||||
const pendingAppealRequests = messages.filter(m => m.appealRequest?.status === 'pending').length
|
||||
const pendingReviewCount = messages.filter(m =>
|
||||
!m.read && (m.type === 'task_submitted' || m.type === 'ai_review_complete')
|
||||
).length
|
||||
|
||||
const filteredMessages = filter === 'all' ? messages : messages.filter(m => !m.read)
|
||||
const getFilteredMessages = () => {
|
||||
switch (filter) {
|
||||
case 'unread':
|
||||
return messages.filter(m => !m.read)
|
||||
case 'pending':
|
||||
return messages.filter(m =>
|
||||
m.type === 'task_submitted' || m.type === 'ai_review_complete' || m.type === 'appeal_quota_request'
|
||||
)
|
||||
default:
|
||||
return messages
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMessages = getFilteredMessages()
|
||||
|
||||
const markAsRead = (id: string) => {
|
||||
setMessages(prev => prev.map(m => m.id === id ? { ...m, read: true } : m))
|
||||
@@ -186,6 +335,77 @@ export default function AgencyMessagesPage() {
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理消息点击
|
||||
const handleMessageClick = (message: Message) => {
|
||||
if (message.type === 'appeal_quota_request') return // 申诉请求不跳转
|
||||
if (message.type === 'system_notice') return // 系统通知不跳转
|
||||
markAsRead(message.id)
|
||||
|
||||
// 根据消息类型决定跳转
|
||||
switch (message.type) {
|
||||
case 'creator_accept':
|
||||
case 'creator_reject':
|
||||
// 达人签约相关 -> 达人管理
|
||||
router.push('/agency/creators')
|
||||
break
|
||||
case 'warning':
|
||||
// 风险预警 -> 达人管理
|
||||
router.push('/agency/creators')
|
||||
break
|
||||
case 'brand_config_updated':
|
||||
// 品牌规则更新 -> Brief配置
|
||||
router.push('/agency/briefs')
|
||||
break
|
||||
case 'task_deadline':
|
||||
// 任务截止提醒 -> 任务列表
|
||||
if (message.projectId) {
|
||||
router.push(`/agency/briefs/${message.projectId}`)
|
||||
} else {
|
||||
router.push('/agency/review')
|
||||
}
|
||||
break
|
||||
default:
|
||||
// 默认逻辑
|
||||
if (message.taskId) {
|
||||
router.push(`/agency/review/${message.taskId}`)
|
||||
} else if (message.projectId) {
|
||||
router.push(`/agency/briefs/${message.projectId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleAction = (message: Message, e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
markAsRead(message.id)
|
||||
|
||||
switch (message.actionType) {
|
||||
case 'review':
|
||||
if (message.taskId) {
|
||||
router.push(`/agency/review/${message.taskId}`)
|
||||
} else {
|
||||
router.push('/agency/review')
|
||||
}
|
||||
break
|
||||
case 'view':
|
||||
if (message.projectId) {
|
||||
router.push(`/agency/briefs/${message.projectId}`)
|
||||
} else if (message.type === 'task_deadline') {
|
||||
router.push('/agency/review')
|
||||
} else {
|
||||
router.push('/agency/creators')
|
||||
}
|
||||
break
|
||||
case 'config':
|
||||
if (message.projectId) {
|
||||
router.push(`/agency/briefs/${message.projectId}`)
|
||||
} else {
|
||||
router.push('/agency/briefs')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
@@ -209,20 +429,32 @@ export default function AgencyMessagesPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
filter === 'all' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
)}
|
||||
>
|
||||
全部消息
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilter('unread')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
filter === 'unread' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
)}
|
||||
>
|
||||
未读消息 ({unreadCount})
|
||||
未读 ({unreadCount})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilter('pending')}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
filter === 'pending' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
)}
|
||||
>
|
||||
待处理
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -237,26 +469,28 @@ export default function AgencyMessagesPage() {
|
||||
return (
|
||||
<Card
|
||||
key={message.id}
|
||||
className={`transition-all overflow-hidden ${
|
||||
!isAppealRequest ? 'cursor-pointer hover:border-accent-indigo/50' : ''
|
||||
} ${!message.read ? 'border-l-4 border-l-accent-indigo' : ''}`}
|
||||
onClick={() => !isAppealRequest && markAsRead(message.id)}
|
||||
className={cn(
|
||||
'transition-all overflow-hidden',
|
||||
!isAppealRequest && 'cursor-pointer hover:border-accent-indigo/50',
|
||||
!message.read && 'border-l-4 border-l-accent-indigo'
|
||||
)}
|
||||
onClick={() => handleMessageClick(message)}
|
||||
>
|
||||
{/* 平台顶部条 */}
|
||||
{platform && (
|
||||
<div className={`px-4 py-1.5 ${platform.bgColor} border-b ${platform.borderColor} flex items-center gap-1.5`}>
|
||||
<div className={cn('px-4 py-1.5 border-b flex items-center gap-1.5', platform.bgColor, platform.borderColor)}>
|
||||
<span className="text-sm">{platform.icon}</span>
|
||||
<span className={`text-xs font-medium ${platform.textColor}`}>{platform.name}</span>
|
||||
<span className={cn('text-xs font-medium', platform.textColor)}>{platform.name}</span>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${message.bgColor} flex items-center justify-center flex-shrink-0`}>
|
||||
<div className={cn('w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0', message.bgColor)}>
|
||||
<Icon size={20} className={message.iconColor} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className={`font-medium ${!message.read ? 'text-text-primary' : 'text-text-secondary'}`}>
|
||||
<h3 className={cn('font-medium', !message.read ? 'text-text-primary' : 'text-text-secondary')}>
|
||||
{message.title}
|
||||
</h3>
|
||||
{!message.read && (
|
||||
@@ -275,10 +509,24 @@ export default function AgencyMessagesPage() {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary mt-1">{message.content}</p>
|
||||
<p className="text-xs text-text-tertiary mt-2 flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{message.time}
|
||||
</p>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<p className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{message.time}
|
||||
</p>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
{message.hasAction && !isAppealRequest && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={(e) => handleAction(message, e)}
|
||||
>
|
||||
<Eye size={14} />
|
||||
{message.actionType === 'review' ? '去审核' : message.actionType === 'config' ? '去配置' : '查看'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 申诉次数请求操作按钮 */}
|
||||
{isAppealRequest && appealStatus === 'pending' && (
|
||||
@@ -308,11 +556,6 @@ export default function AgencyMessagesPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isAppealRequest && (
|
||||
<Button variant="ghost" size="sm" onClick={(e) => e.stopPropagation()}>
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -16,8 +16,10 @@ import {
|
||||
ChevronRight,
|
||||
Download,
|
||||
Eye,
|
||||
File
|
||||
File,
|
||||
MessageSquareWarning
|
||||
} from 'lucide-react'
|
||||
import { Modal } from '@/components/ui/Modal'
|
||||
import { getPlatformInfo } from '@/lib/platforms'
|
||||
|
||||
// 模拟脚本待审列表
|
||||
@@ -34,6 +36,7 @@ const mockScriptTasks = [
|
||||
riskLevel: 'low' as const,
|
||||
submittedAt: '2026-02-06 14:30',
|
||||
hasHighRisk: false,
|
||||
isAppeal: false, // 是否为申诉
|
||||
},
|
||||
{
|
||||
id: 'script-002',
|
||||
@@ -47,6 +50,8 @@ const mockScriptTasks = [
|
||||
riskLevel: 'medium' as const,
|
||||
submittedAt: '2026-02-06 12:15',
|
||||
hasHighRisk: true,
|
||||
isAppeal: true, // 申诉重审
|
||||
appealReason: '已修改违规用词,请求重新审核',
|
||||
},
|
||||
{
|
||||
id: 'script-003',
|
||||
@@ -60,6 +65,7 @@ const mockScriptTasks = [
|
||||
riskLevel: 'low' as const,
|
||||
submittedAt: '2026-02-06 10:00',
|
||||
hasHighRisk: false,
|
||||
isAppeal: false,
|
||||
},
|
||||
{
|
||||
id: 'script-004',
|
||||
@@ -73,6 +79,8 @@ const mockScriptTasks = [
|
||||
riskLevel: 'high' as const,
|
||||
submittedAt: '2026-02-06 09:00',
|
||||
hasHighRisk: true,
|
||||
isAppeal: true,
|
||||
appealReason: '对驳回原因有异议,内容符合要求',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -91,6 +99,7 @@ const mockVideoTasks = [
|
||||
duration: '02:15',
|
||||
submittedAt: '2026-02-06 15:00',
|
||||
hasHighRisk: false,
|
||||
isAppeal: false,
|
||||
},
|
||||
{
|
||||
id: 'video-002',
|
||||
@@ -105,6 +114,8 @@ const mockVideoTasks = [
|
||||
duration: '03:42',
|
||||
submittedAt: '2026-02-06 13:45',
|
||||
hasHighRisk: true,
|
||||
isAppeal: true,
|
||||
appealReason: '已按要求重新剪辑,删除了争议片段',
|
||||
},
|
||||
{
|
||||
id: 'video-003',
|
||||
@@ -119,6 +130,7 @@ const mockVideoTasks = [
|
||||
duration: '04:20',
|
||||
submittedAt: '2026-02-06 11:30',
|
||||
hasHighRisk: true,
|
||||
isAppeal: false,
|
||||
},
|
||||
{
|
||||
id: 'video-004',
|
||||
@@ -133,6 +145,7 @@ const mockVideoTasks = [
|
||||
duration: '01:45',
|
||||
submittedAt: '2026-02-06 10:15',
|
||||
hasHighRisk: false,
|
||||
isAppeal: false,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -152,13 +165,18 @@ function ScoreTag({ score }: { score: number }) {
|
||||
type ScriptTask = typeof mockScriptTasks[0]
|
||||
type VideoTask = typeof mockVideoTasks[0]
|
||||
|
||||
function ScriptTaskCard({ task }: { task: ScriptTask }) {
|
||||
function ScriptTaskCard({ task, onPreview }: { task: ScriptTask; onPreview: (task: ScriptTask) => void }) {
|
||||
const riskConfig = riskLevelConfig[task.riskLevel]
|
||||
const platform = getPlatformInfo(task.platform)
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
// TODO: 实现实际下载逻辑
|
||||
alert(`下载文件: ${task.fileName}`)
|
||||
}
|
||||
|
||||
const handlePreview = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onPreview(task)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -168,6 +186,13 @@ function ScriptTaskCard({ task }: { task: ScriptTask }) {
|
||||
<div className={`px-4 py-1.5 ${platform.bgColor} border-b ${platform.borderColor} flex items-center gap-1.5`}>
|
||||
<span className="text-sm">{platform.icon}</span>
|
||||
<span className={`text-xs font-medium ${platform.textColor}`}>{platform.name}</span>
|
||||
{/* 申诉标识 */}
|
||||
{task.isAppeal && (
|
||||
<span className="ml-auto flex items-center gap-1 px-2 py-0.5 text-xs bg-accent-amber/30 text-accent-amber rounded-full font-medium">
|
||||
<MessageSquareWarning size={12} />
|
||||
申诉
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
@@ -180,53 +205,74 @@ function ScriptTaskCard({ task }: { task: ScriptTask }) {
|
||||
<span className={`text-xs ${riskConfig.textColor}`}>{riskConfig.label}</span>
|
||||
</div>
|
||||
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-page mb-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<File size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.fileName}</p>
|
||||
<p className="text-xs text-text-tertiary">{task.fileSize}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="下载文件"
|
||||
>
|
||||
<Download size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
{/* 申诉理由 */}
|
||||
{task.isAppeal && task.appealReason && (
|
||||
<div className="mb-3 p-2.5 rounded-lg bg-accent-amber/10 border border-accent-amber/30">
|
||||
<p className="text-xs text-accent-amber font-medium mb-1">申诉理由</p>
|
||||
<p className="text-sm text-text-secondary">{task.appealReason}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部:时间 + 审核按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
<Link href={`/agency/review/script/${task.id}`}>
|
||||
<Button size="sm" className={`${
|
||||
task.riskLevel === 'high' ? 'bg-accent-coral hover:bg-accent-coral/80' :
|
||||
task.riskLevel === 'medium' ? 'bg-accent-amber hover:bg-accent-amber/80' :
|
||||
'bg-accent-green hover:bg-accent-green/80'
|
||||
} text-white`}>
|
||||
审核
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-page mb-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-indigo/15 flex items-center justify-center">
|
||||
<File size={20} className="text-accent-indigo" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.fileName}</p>
|
||||
<p className="text-xs text-text-tertiary">{task.fileSize}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePreview}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="预览文件"
|
||||
>
|
||||
<Eye size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="下载文件"
|
||||
>
|
||||
<Download size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 底部:时间 + 审核按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
<Link href={`/agency/review/script/${task.id}`}>
|
||||
<Button size="sm" className={`${
|
||||
task.riskLevel === 'high' ? 'bg-accent-coral hover:bg-accent-coral/80' :
|
||||
task.riskLevel === 'medium' ? 'bg-accent-amber hover:bg-accent-amber/80' :
|
||||
'bg-accent-green hover:bg-accent-green/80'
|
||||
} text-white`}>
|
||||
{task.isAppeal ? '审核申诉' : '审核'}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoTaskCard({ task }: { task: VideoTask }) {
|
||||
function VideoTaskCard({ task, onPreview }: { task: VideoTask; onPreview: (task: VideoTask) => void }) {
|
||||
const riskConfig = riskLevelConfig[task.riskLevel]
|
||||
const platform = getPlatformInfo(task.platform)
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
// TODO: 实现实际下载逻辑
|
||||
alert(`下载文件: ${task.fileName}`)
|
||||
}
|
||||
|
||||
const handlePreview = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onPreview(task)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -236,6 +282,13 @@ function VideoTaskCard({ task }: { task: VideoTask }) {
|
||||
<div className={`px-4 py-1.5 ${platform.bgColor} border-b ${platform.borderColor} flex items-center gap-1.5`}>
|
||||
<span className="text-sm">{platform.icon}</span>
|
||||
<span className={`text-xs font-medium ${platform.textColor}`}>{platform.name}</span>
|
||||
{/* 申诉标识 */}
|
||||
{task.isAppeal && (
|
||||
<span className="ml-auto flex items-center gap-1 px-2 py-0.5 text-xs bg-accent-amber/30 text-accent-amber rounded-full font-medium">
|
||||
<MessageSquareWarning size={12} />
|
||||
申诉
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4">
|
||||
@@ -248,41 +301,57 @@ function VideoTaskCard({ task }: { task: VideoTask }) {
|
||||
<span className={`text-xs ${riskConfig.textColor}`}>{riskConfig.label}</span>
|
||||
</div>
|
||||
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-page mb-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/15 flex items-center justify-center">
|
||||
<Video size={20} className="text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.fileName}</p>
|
||||
<p className="text-xs text-text-tertiary">{task.fileSize} · {task.duration}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="下载文件"
|
||||
>
|
||||
<Download size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
{/* 申诉理由 */}
|
||||
{task.isAppeal && task.appealReason && (
|
||||
<div className="mb-3 p-2.5 rounded-lg bg-accent-amber/10 border border-accent-amber/30">
|
||||
<p className="text-xs text-accent-amber font-medium mb-1">申诉理由</p>
|
||||
<p className="text-sm text-text-secondary">{task.appealReason}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 底部:时间 + 审核按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
<Link href={`/agency/review/video/${task.id}`}>
|
||||
<Button size="sm" className={`${
|
||||
task.riskLevel === 'high' ? 'bg-accent-coral hover:bg-accent-coral/80' :
|
||||
task.riskLevel === 'medium' ? 'bg-accent-amber hover:bg-accent-amber/80' :
|
||||
'bg-accent-green hover:bg-accent-green/80'
|
||||
} text-white`}>
|
||||
审核
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-bg-page mb-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-purple-500/15 flex items-center justify-center">
|
||||
<Video size={20} className="text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-text-primary truncate">{task.fileName}</p>
|
||||
<p className="text-xs text-text-tertiary">{task.fileSize} · {task.duration}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePreview}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="预览视频"
|
||||
>
|
||||
<Eye size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
className="p-2 rounded-lg hover:bg-bg-elevated transition-colors"
|
||||
title="下载文件"
|
||||
>
|
||||
<Download size={18} className="text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 底部:时间 + 审核按钮 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-tertiary flex items-center gap-1">
|
||||
<Clock size={12} />
|
||||
{task.submittedAt}
|
||||
</span>
|
||||
<Link href={`/agency/review/video/${task.id}`}>
|
||||
<Button size="sm" className={`${
|
||||
task.riskLevel === 'high' ? 'bg-accent-coral hover:bg-accent-coral/80' :
|
||||
task.riskLevel === 'medium' ? 'bg-accent-amber hover:bg-accent-amber/80' :
|
||||
'bg-accent-green hover:bg-accent-green/80'
|
||||
} text-white`}>
|
||||
{task.isAppeal ? '审核申诉' : '审核'}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -291,6 +360,8 @@ function VideoTaskCard({ task }: { task: VideoTask }) {
|
||||
export default function AgencyReviewListPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'script' | 'video'>('all')
|
||||
const [previewScript, setPreviewScript] = useState<ScriptTask | null>(null)
|
||||
const [previewVideo, setPreviewVideo] = useState<VideoTask | null>(null)
|
||||
|
||||
const filteredScripts = mockScriptTasks.filter(task =>
|
||||
task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
@@ -302,6 +373,10 @@ export default function AgencyReviewListPage() {
|
||||
task.creatorName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
|
||||
// 计算申诉数量
|
||||
const appealScriptCount = mockScriptTasks.filter(t => t.isAppeal).length
|
||||
const appealVideoCount = mockVideoTasks.filter(t => t.isAppeal).length
|
||||
|
||||
return (
|
||||
<div className="space-y-6 min-h-0">
|
||||
{/* 页面标题 */}
|
||||
@@ -318,6 +393,12 @@ export default function AgencyReviewListPage() {
|
||||
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 rounded font-medium">
|
||||
{mockVideoTasks.length} 视频
|
||||
</span>
|
||||
{(appealScriptCount + appealVideoCount) > 0 && (
|
||||
<span className="px-2 py-1 bg-accent-amber/20 text-accent-amber rounded font-medium flex items-center gap-1">
|
||||
<MessageSquareWarning size={14} />
|
||||
{appealScriptCount + appealVideoCount} 申诉
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -381,7 +462,7 @@ export default function AgencyReviewListPage() {
|
||||
<CardContent className="space-y-3">
|
||||
{filteredScripts.length > 0 ? (
|
||||
filteredScripts.map((task) => (
|
||||
<ScriptTaskCard key={task.id} task={task} />
|
||||
<ScriptTaskCard key={task.id} task={task} onPreview={setPreviewScript} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
@@ -408,7 +489,7 @@ export default function AgencyReviewListPage() {
|
||||
<CardContent className="space-y-3">
|
||||
{filteredVideos.length > 0 ? (
|
||||
filteredVideos.map((task) => (
|
||||
<VideoTaskCard key={task.id} task={task} />
|
||||
<VideoTaskCard key={task.id} task={task} onPreview={setPreviewVideo} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-text-tertiary">
|
||||
@@ -420,6 +501,94 @@ export default function AgencyReviewListPage() {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 脚本预览弹窗 */}
|
||||
<Modal
|
||||
isOpen={!!previewScript}
|
||||
onClose={() => setPreviewScript(null)}
|
||||
title={previewScript?.fileName || '脚本预览'}
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{previewScript?.isAppeal && previewScript?.appealReason && (
|
||||
<div className="p-3 rounded-lg bg-accent-amber/10 border border-accent-amber/30">
|
||||
<p className="text-xs text-accent-amber font-medium mb-1 flex items-center gap-1">
|
||||
<MessageSquareWarning size={12} />
|
||||
申诉理由
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary">{previewScript.appealReason}</p>
|
||||
</div>
|
||||
)}
|
||||
<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-between items-center">
|
||||
<div className="text-sm text-text-secondary">
|
||||
<span>{previewScript?.fileName}</span>
|
||||
<span className="mx-2">·</span>
|
||||
<span>{previewScript?.fileSize}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={() => setPreviewScript(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={() => alert(`下载文件: ${previewScript?.fileName}`)}>
|
||||
<Download size={16} />
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 视频预览弹窗 */}
|
||||
<Modal
|
||||
isOpen={!!previewVideo}
|
||||
onClose={() => setPreviewVideo(null)}
|
||||
title={previewVideo?.fileName || '视频预览'}
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{previewVideo?.isAppeal && previewVideo?.appealReason && (
|
||||
<div className="p-3 rounded-lg bg-accent-amber/10 border border-accent-amber/30">
|
||||
<p className="text-xs text-accent-amber font-medium mb-1 flex items-center gap-1">
|
||||
<MessageSquareWarning size={12} />
|
||||
申诉理由
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary">{previewVideo.appealReason}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="aspect-video bg-bg-elevated rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Video className="w-12 h-12 mx-auto text-purple-400 mb-4" />
|
||||
<p className="text-text-secondary">视频播放区域</p>
|
||||
<p className="text-xs text-text-tertiary mt-1">实际开发中将嵌入视频播放器</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm text-text-secondary">
|
||||
<span>{previewVideo?.fileName}</span>
|
||||
<span className="mx-2">·</span>
|
||||
<span>{previewVideo?.fileSize}</span>
|
||||
<span className="mx-2">·</span>
|
||||
<span>{previewVideo?.duration}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="secondary" onClick={() => setPreviewVideo(null)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={() => alert(`下载文件: ${previewVideo?.fileName}`)}>
|
||||
<Download size={16} />
|
||||
下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ import {
|
||||
User,
|
||||
Clock,
|
||||
Eye,
|
||||
Shield
|
||||
Shield,
|
||||
Download,
|
||||
MessageSquareWarning
|
||||
} from 'lucide-react'
|
||||
import { FilePreview, FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview'
|
||||
|
||||
// 模拟脚本任务数据
|
||||
const mockScriptTask = {
|
||||
@@ -28,6 +31,19 @@ const mockScriptTask = {
|
||||
submittedAt: '2026-02-06 14:30',
|
||||
aiScore: 88,
|
||||
status: 'agent_reviewing',
|
||||
// 文件信息
|
||||
file: {
|
||||
id: 'file-001',
|
||||
fileName: '夏日护肤推广_脚本v2.docx',
|
||||
fileSize: '245 KB',
|
||||
fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
fileUrl: '/demo/scripts/script-001.docx', // 实际开发时替换为真实URL
|
||||
uploadedAt: '2026-02-06 14:30',
|
||||
} as FileInfo,
|
||||
// 申诉信息
|
||||
isAppeal: false,
|
||||
appealReason: '',
|
||||
// 脚本内容(AI解析后的结构化内容,用于展示)
|
||||
scriptContent: {
|
||||
opening: '大家好!今天给大家分享一款超级好用的夏日护肤神器~',
|
||||
productIntro: '这款XX品牌的防晒霜,SPF50+,PA++++,真的是夏天出门必备!质地轻薄不油腻,涂上去清清爽爽的。',
|
||||
@@ -80,7 +96,8 @@ export default function AgencyScriptReviewPage() {
|
||||
const [showForcePassModal, setShowForcePassModal] = useState(false)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
const [forcePassReason, setForcePassReason] = useState('')
|
||||
const [viewMode, setViewMode] = useState<'simple' | 'preview'>('preview')
|
||||
const [viewMode, setViewMode] = useState<'file' | 'parsed'>('file') // 'file' 显示原文件, 'parsed' 显示解析内容
|
||||
const [showFilePreview, setShowFilePreview] = useState(false)
|
||||
|
||||
const task = mockScriptTask
|
||||
|
||||
@@ -118,7 +135,15 @@ export default function AgencyScriptReviewPage() {
|
||||
<ArrowLeft size={20} className="text-text-primary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-bold text-text-primary">{task.title}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-bold text-text-primary">{task.title}</h1>
|
||||
{task.isAppeal && (
|
||||
<span className="flex items-center gap-1 px-2 py-1 text-xs bg-accent-amber/20 text-accent-amber rounded-full font-medium">
|
||||
<MessageSquareWarning size={12} />
|
||||
申诉
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<User size={14} />
|
||||
@@ -130,32 +155,63 @@ export default function AgencyScriptReviewPage() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode(viewMode === 'simple' ? 'preview' : 'simple')}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm text-text-secondary hover:text-text-primary hover:bg-bg-elevated rounded-lg"
|
||||
>
|
||||
<Eye size={16} />
|
||||
{viewMode === 'simple' ? '展开预览' : '简洁模式'}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 p-1 bg-bg-elevated rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('file')}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
viewMode === 'file' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
原文件
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('parsed')}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
viewMode === 'parsed' ? 'bg-bg-card text-text-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
AI解析
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 申诉理由 */}
|
||||
{task.isAppeal && task.appealReason && (
|
||||
<div className="p-4 rounded-xl bg-accent-amber/10 border border-accent-amber/30">
|
||||
<p className="text-sm text-accent-amber font-medium mb-1 flex items-center gap-1">
|
||||
<MessageSquareWarning size={14} />
|
||||
申诉理由
|
||||
</p>
|
||||
<p className="text-text-secondary">{task.appealReason}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 审核流程进度条 */}
|
||||
<ReviewProgressBar taskStatus={task.status} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* 左侧:脚本内容 */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{viewMode === 'simple' ? (
|
||||
{/* 文件信息卡片 */}
|
||||
<FileInfoCard
|
||||
file={task.file}
|
||||
onPreview={() => setShowFilePreview(true)}
|
||||
/>
|
||||
|
||||
{viewMode === 'file' ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center">
|
||||
<FileText size={48} className="mx-auto text-accent-indigo mb-4" />
|
||||
<p className="text-text-primary font-medium">{task.title}</p>
|
||||
<p className="text-sm text-text-secondary mt-2">点击"展开预览"查看脚本内容</p>
|
||||
<Button variant="secondary" className="mt-4" onClick={() => setViewMode('preview')}>
|
||||
<Eye size={16} />
|
||||
展开预览
|
||||
</Button>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-accent-indigo" />
|
||||
文件预览
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FilePreview file={task.file} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -163,7 +219,8 @@ export default function AgencyScriptReviewPage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText size={18} className="text-accent-indigo" />
|
||||
脚本内容
|
||||
AI 解析内容
|
||||
<span className="text-xs font-normal text-text-tertiary ml-2">(AI 自动提取的结构化内容)</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -353,6 +410,13 @@ export default function AgencyScriptReviewPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 文件预览弹窗 */}
|
||||
<FilePreviewModal
|
||||
file={task.file}
|
||||
isOpen={showFilePreview}
|
||||
onClose={() => setShowFilePreview(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,12 @@ import {
|
||||
User,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle
|
||||
XCircle,
|
||||
Download,
|
||||
ExternalLink,
|
||||
MessageSquareWarning
|
||||
} from 'lucide-react'
|
||||
import { FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview'
|
||||
|
||||
// 模拟视频任务数据
|
||||
const mockVideoTask = {
|
||||
@@ -30,6 +34,20 @@ const mockVideoTask = {
|
||||
duration: 135,
|
||||
aiScore: 85,
|
||||
status: 'agent_reviewing',
|
||||
// 文件信息
|
||||
file: {
|
||||
id: 'file-video-001',
|
||||
fileName: '夏日护肤_成片v2.mp4',
|
||||
fileSize: '128 MB',
|
||||
fileType: 'video/mp4',
|
||||
fileUrl: '/demo/videos/video-001.mp4', // 实际开发时替换为真实URL
|
||||
uploadedAt: '2026-02-06 15:00',
|
||||
duration: '02:15',
|
||||
thumbnail: '/demo/videos/video-001-thumb.jpg',
|
||||
} as FileInfo,
|
||||
// 申诉信息
|
||||
isAppeal: false,
|
||||
appealReason: '',
|
||||
hardViolations: [
|
||||
{
|
||||
id: 'v1',
|
||||
@@ -105,6 +123,8 @@ export default function AgencyVideoReviewPage() {
|
||||
const [forcePassReason, setForcePassReason] = useState('')
|
||||
const [saveAsException, setSaveAsException] = useState(false)
|
||||
const [checkedViolations, setCheckedViolations] = useState<Record<string, boolean>>({})
|
||||
const [showFilePreview, setShowFilePreview] = useState(false)
|
||||
const [videoError, setVideoError] = useState(false)
|
||||
|
||||
const task = mockVideoTask
|
||||
|
||||
@@ -149,7 +169,15 @@ export default function AgencyVideoReviewPage() {
|
||||
<ArrowLeft size={20} className="text-text-primary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-xl font-bold text-text-primary">{task.title}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-bold text-text-primary">{task.title}</h1>
|
||||
{task.isAppeal && (
|
||||
<span className="flex items-center gap-1 px-2 py-1 text-xs bg-accent-amber/20 text-accent-amber rounded-full font-medium">
|
||||
<MessageSquareWarning size={12} />
|
||||
申诉
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-text-secondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<User size={14} />
|
||||
@@ -163,22 +191,61 @@ export default function AgencyVideoReviewPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 申诉理由 */}
|
||||
{task.isAppeal && task.appealReason && (
|
||||
<div className="p-4 rounded-xl bg-accent-amber/10 border border-accent-amber/30">
|
||||
<p className="text-sm text-accent-amber font-medium mb-1 flex items-center gap-1">
|
||||
<MessageSquareWarning size={14} />
|
||||
申诉理由
|
||||
</p>
|
||||
<p className="text-text-secondary">{task.appealReason}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 审核流程进度条 */}
|
||||
<ReviewProgressBar taskStatus={task.status} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
{/* 左侧:视频播放器 (3/5) */}
|
||||
<div className="lg:col-span-3 space-y-4">
|
||||
{/* 文件信息卡片 */}
|
||||
<FileInfoCard
|
||||
file={task.file}
|
||||
onPreview={() => setShowFilePreview(true)}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="aspect-video bg-gray-900 rounded-t-lg flex items-center justify-center relative">
|
||||
<button
|
||||
type="button"
|
||||
className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition-colors"
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
>
|
||||
{isPlaying ? <Pause size={32} className="text-white" /> : <Play size={32} className="text-white ml-1" />}
|
||||
</button>
|
||||
{/* 真实视频播放器 */}
|
||||
<div className="aspect-video bg-gray-900 rounded-t-lg overflow-hidden relative">
|
||||
{videoError ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Play size={48} className="mx-auto text-white/50 mb-3" />
|
||||
<p className="text-white/70 mb-3">视频加载失败</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => window.open(task.file.fileUrl, '_blank')}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
在新标签页打开
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<video
|
||||
className="w-full h-full"
|
||||
controls
|
||||
poster={task.file.thumbnail}
|
||||
onError={() => setVideoError(true)}
|
||||
onPlay={() => setIsPlaying(true)}
|
||||
onPause={() => setIsPlaying(false)}
|
||||
>
|
||||
<source src={task.file.fileUrl} type={task.file.fileType} />
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
)}
|
||||
</div>
|
||||
{/* 智能进度条 */}
|
||||
<div className="p-4 border-t border-border-subtle">
|
||||
@@ -416,6 +483,13 @@ export default function AgencyVideoReviewPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 文件预览弹窗 */}
|
||||
<FilePreviewModal
|
||||
file={task.file}
|
||||
isOpen={showFilePreview}
|
||||
onClose={() => setShowFilePreview(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user