From a5a005db0ce69ddbe1b44b43a7f820e327530eae Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 9 Feb 2026 12:20:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E5=8F=B0=E6=96=87=E4=BB=B6=E9=A2=84=E8=A7=88=E4=B8=8E=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E9=80=9A=E7=9F=A5=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更新: - 新增 FilePreview 通用组件,支持视频/图片/PDF 内嵌预览 - 审核详情页添加文件信息卡片、预览/下载功能 - 审核列表和详情页添加申诉标识和申诉理由显示 - 完善三端消息通知系统(达人/代理商/品牌) - 新增达人 Brief 查看页面 - 新增品牌方消息中心页面 - 创建后端开发备忘文档 Co-Authored-By: Claude Opus 4.5 --- backend/BACKEND_TODO.md | 94 +++ frontend/app/agency/briefs/[id]/page.tsx | 651 ++++++++++++++++-- frontend/app/agency/messages/page.tsx | 323 +++++++-- frontend/app/agency/review/page.tsx | 319 +++++++-- .../app/agency/review/script/[id]/page.tsx | 106 ++- .../app/agency/review/video/[id]/page.tsx | 94 ++- frontend/app/brand/messages/page.tsx | 464 +++++++++++++ frontend/app/brand/review/page.tsx | 187 ++++- .../app/brand/review/script/[id]/page.tsx | 102 ++- frontend/app/brand/review/video/[id]/page.tsx | 93 ++- frontend/app/creator/messages/page.tsx | 159 ++++- frontend/app/creator/task/[id]/brief/page.tsx | 317 +++++++++ frontend/app/creator/task/[id]/page.tsx | 196 +++++- frontend/components/index.ts | 11 + frontend/components/navigation/Sidebar.tsx | 1 + frontend/components/ui/FilePreview.tsx | 403 +++++++++++ 16 files changed, 3251 insertions(+), 269 deletions(-) create mode 100644 backend/BACKEND_TODO.md create mode 100644 frontend/app/brand/messages/page.tsx create mode 100644 frontend/app/creator/task/[id]/brief/page.tsx create mode 100644 frontend/components/ui/FilePreview.tsx diff --git a/backend/BACKEND_TODO.md b/backend/BACKEND_TODO.md new file mode 100644 index 0000000..fc2ca9b --- /dev/null +++ b/backend/BACKEND_TODO.md @@ -0,0 +1,94 @@ +# 后端开发备忘 + +## 文件预览相关 API + +### 1. 文件上传与存储 +- 达人上传脚本文件(支持 .docx, .pdf, .xlsx, .txt 等) +- 达人上传视频文件(支持 .mp4, .mov, .webm 等) +- 文件存储到 OSS/S3,返回访问 URL + +### 2. 文件访问 API +``` +GET /api/files/:fileId +返回:{ url: "文件访问URL", fileName, fileSize, fileType, uploadedAt } +``` + +### 3. 文件类型转换(可选,提升体验) +- Word (.docx) → PDF +- Excel (.xlsx) → PDF +- PPT (.pptx) → PDF +- 使用 LibreOffice 或 Pandoc 实现 + +### 4. 视频流服务 +- 支持视频分段加载(Range 请求) +- 支持视频缩略图生成 + +--- + +## 审核相关 API + +### 脚本审核 +``` +GET /api/agency/review/scripts # 待审脚本列表 +GET /api/agency/review/scripts/:id # 脚本详情(含文件URL、AI分析结果) +POST /api/agency/review/scripts/:id/approve # 通过 +POST /api/agency/review/scripts/:id/reject # 驳回 +POST /api/agency/review/scripts/:id/force-pass # 强制通过 +``` + +### 视频审核 +``` +GET /api/agency/review/videos # 待审视频列表 +GET /api/agency/review/videos/:id # 视频详情(含文件URL、AI分析结果) +POST /api/agency/review/videos/:id/approve # 通过 +POST /api/agency/review/videos/:id/reject # 驳回 +POST /api/agency/review/videos/:id/force-pass # 强制通过 +``` + +### 品牌方终审 +``` +GET /api/brand/review/scripts # 待终审脚本列表 +GET /api/brand/review/scripts/:id # 脚本详情 +POST /api/brand/review/scripts/:id/approve # 终审通过 +POST /api/brand/review/scripts/:id/reject # 终审驳回 + +GET /api/brand/review/videos # 待终审视频列表 +GET /api/brand/review/videos/:id # 视频详情 +POST /api/brand/review/videos/:id/approve # 终审通过 +POST /api/brand/review/videos/:id/reject # 终审驳回 +``` + +--- + +## 申诉相关字段 + +审核列表和详情需要包含: +- `isAppeal: boolean` - 是否为申诉 +- `appealReason: string` - 申诉理由 +- `appealCount: number` - 第几次申诉 + +--- + +## 文件数据结构 + +```typescript +interface FileInfo { + id: string + fileName: string + fileSize: string // "1.5 MB" + fileType: string // "video/mp4", "application/pdf", etc. + fileUrl: string // 访问URL + uploadedAt: string // ISO 时间 + // 视频特有 + duration?: number // 秒 + thumbnail?: string // 缩略图URL +} +``` + +--- + +## 注意事项 + +1. 文件 URL 需要支持跨域访问(CORS) +2. 视频需要支持 Range 请求实现分段加载 +3. 敏感文件考虑使用签名 URL(有效期限制) diff --git a/frontend/app/agency/briefs/[id]/page.tsx b/frontend/app/agency/briefs/[id]/page.tsx index 36d9d76..bd9e127 100644 --- a/frontend/app/agency/briefs/[id]/page.tsx +++ b/frontend/app/agency/briefs/[id]/page.tsx @@ -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(null) + const [previewAgencyFile, setPreviewAgencyFile] = useState(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() {
-

{brief.projectName}

-

{brief.brandName}

+
+

{brandBrief.projectName}

+ {platform && ( + + {platform.icon} + {platform.name} + + )} +
+

+ + {brandBrief.brandName} +

+ + {/* ===== 第一部分:品牌方 Brief(只读)===== */} +
+
+ +
+

品牌方 Brief(只读)

+

+ 以下是品牌方上传的 Brief 文件和规则,仅供参考,不可编辑。 +

+
+
+
+
- {/* 左侧:Brief 文件和 AI 解析 */} -
- {/* Brief 文件 */} - - - - - Brief 文件 - - - -
+ {/* 品牌方文件 */} + + + + + + 品牌方 Brief 文件 + + {brandBrief.files.length} 个文件 + + + + + + + {brandBrief.files.slice(0, 2).map((file) => ( +
- +
+ +
-

{brief.fileName}

-

上传于 {brief.uploadedAt}

+

{file.name}

+

{file.size} · {file.uploadedAt}

- -
-
-
+ ))} + {brandBrief.files.length > 2 && ( + + )} + + + {/* 品牌方规则(只读) */} + + + + + 品牌方限制 + + + +
+

限制条件

+

{brandBrief.brandRules.restrictions}

+
+
+

竞品黑名单

+
+ {brandBrief.brandRules.competitors.map((c, i) => ( + + {c} + + ))} +
+
+
+
+
+ + {/* ===== 第二部分:代理商配置(可编辑)===== */} +
+
+ +
+

代理商配置(可编辑)

+

+ 以下配置由代理商编辑,将展示给达人查看。 +

+
+
+
+ + {/* 代理商Brief文档管理 */} + + + + + + 代理商 Brief 文档 + + {agencyConfig.agencyFiles.length} 个文件(达人可见) + + +
+ + +
+
+
+ +
+ {agencyConfig.agencyFiles.map((file) => ( +
+
+
+ +
+
+

{file.name}

+

{file.size} · {file.uploadedAt}

+ {file.description && ( +

{file.description}

+ )} +
+
+
+ + + +
+
+ ))} + {/* 上传占位卡片 */} + +
+
+

+ + 以上文档将展示给达人查看,请确保内容准确完整。 +

+
+
+
+ +
+ {/* 左侧:AI解析 + 卖点配置 */} +
{/* AI 解析结果 */} @@ -182,37 +495,33 @@ export default function BriefConfigPage() {

产品名称

-

{brief.aiParsedContent.productName}

+

{agencyConfig.aiParsedContent.productName}

目标人群

-

{brief.aiParsedContent.targetAudience}

+

{agencyConfig.aiParsedContent.targetAudience}

内容要求

-

{brief.aiParsedContent.contentRequirements}

-
-
-

限制条件

-

{brief.aiParsedContent.restrictions}

+

{agencyConfig.aiParsedContent.contentRequirements}

- {/* 卖点配置 */} + {/* 卖点配置(可编辑) */} 卖点配置 - {brief.sellingPoints.length} 个卖点 + {agencyConfig.sellingPoints.length} 个卖点 - {brief.sellingPoints.map((sp) => ( + {agencyConfig.sellingPoints.map((sp) => (
+ + + + {rules.rules.map((rule, index) => ( +
+

{rule.category}

+
+ {rule.items.map((item, i) => ( + + {item} + + ))} +
+
+ ))} +
+
{/* 右侧:违禁词配置 */}
+ {/* 违禁词配置(可编辑) */} 违禁词配置 - {brief.blacklistWords.length} 个 + {agencyConfig.blacklistWords.length} 个 - {brief.blacklistWords.map((bw) => ( + {agencyConfig.blacklistWords.map((bw) => (
「{bw.word}」 @@ -295,22 +635,193 @@ export default function BriefConfigPage() { + {/* 配置信息 */} + + + + + 配置状态 + + + +
+ 状态 + 已配置 +
+
+ 配置时间 + {agencyConfig.configuredAt} +
+
+
+ {/* 配置提示 */} -
+
- +
-

配置说明

-
    +

    配置说明

    +
    • • 必选卖点必须在内容中提及
    • • 违禁词会触发 AI 审核警告
    • -
    • • 修改配置后需重新保存
    • +
    • • 此配置将展示给达人查看
+ + {/* 文件列表弹窗 */} + setShowFilesModal(false)} + title="品牌方 Brief 文件" + size="lg" + > +
+ {brandBrief.files.map((file) => ( +
+
+
+ +
+
+

{file.name}

+

{file.size} · 上传于 {file.uploadedAt}

+
+
+
+ + +
+
+ ))} +
+
+ + {/* 文件预览弹窗(品牌方) */} + setPreviewFile(null)} + title={previewFile?.name || '文件预览'} + size="lg" + > +
+
+
+ +

文件预览区域

+

实际开发中将嵌入文件预览组件

+
+
+
+ + {previewFile && ( + + )} +
+
+
+ + {/* 代理商文档管理弹窗 */} + setShowAgencyFilesModal(false)} + title="管理代理商 Brief 文档" + size="lg" + > +
+
+

+ 以下文档将展示给达人查看,可以添加、删除或预览文档 +

+ +
+
+ {agencyConfig.agencyFiles.map((file) => ( +
+
+
+ +
+
+

{file.name}

+

{file.size} · 上传于 {file.uploadedAt}

+ {file.description && ( +

{file.description}

+ )} +
+
+
+ + + +
+
+ ))} + {agencyConfig.agencyFiles.length === 0 && ( +
+ +

暂无文档

+

点击上方按钮上传文档

+
+ )} +
+
+
+ + {/* 代理商文档预览弹窗 */} + setPreviewAgencyFile(null)} + title={previewAgencyFile?.name || '文件预览'} + size="lg" + > +
+
+
+ +

文件预览区域

+

实际开发中将嵌入文件预览组件

+
+
+
+ + {previewAgencyFile && ( + + )} +
+
+
) } diff --git a/frontend/app/agency/messages/page.tsx b/frontend/app/agency/messages/page.tsx index 33b3fdb..6c9ca44 100644 --- a/frontend/app/agency/messages/page.tsx +++ b/frontend/app/agency/messages/page.tsx @@ -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 (
{/* 页面标题 */} @@ -209,20 +429,32 @@ export default function AgencyMessagesPage() { +
@@ -237,26 +469,28 @@ export default function AgencyMessagesPage() { return ( !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 && ( -
+
{platform.icon} - {platform.name} + {platform.name}
)}
-
+
-

+

{message.title}

{!message.read && ( @@ -275,10 +509,24 @@ export default function AgencyMessagesPage() { )}

{message.content}

-

- - {message.time} -

+
+

+ + {message.time} +

+ + {/* 操作按钮 */} + {message.hasAction && !isAppealRequest && ( + + )} +
{/* 申诉次数请求操作按钮 */} {isAppealRequest && appealStatus === 'pending' && ( @@ -308,11 +556,6 @@ export default function AgencyMessagesPage() {
)}
- {!isAppealRequest && ( - - )}
diff --git a/frontend/app/agency/review/page.tsx b/frontend/app/agency/review/page.tsx index 2f7391f..0d5c9fb 100644 --- a/frontend/app/agency/review/page.tsx +++ b/frontend/app/agency/review/page.tsx @@ -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 }) {
{platform.icon} {platform.name} + {/* 申诉标识 */} + {task.isAppeal && ( + + + 申诉 + + )}
)}
@@ -180,53 +205,74 @@ function ScriptTaskCard({ task }: { task: ScriptTask }) { {riskConfig.label}
- {/* 文件信息 */} -
-
- -
-
-

{task.fileName}

-

{task.fileSize}

-
- -
+ {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

申诉理由

+

{task.appealReason}

+
+ )} - {/* 底部:时间 + 审核按钮 */} -
- - - {task.submittedAt} - - - - -
+ {/* 文件信息 */} +
+
+ +
+
+

{task.fileName}

+

{task.fileSize}

+
+ + +
+ + {/* 底部:时间 + 审核按钮 */} +
+ + + {task.submittedAt} + + + + +
) } -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 }) {
{platform.icon} {platform.name} + {/* 申诉标识 */} + {task.isAppeal && ( + + + 申诉 + + )}
)}
@@ -248,41 +301,57 @@ function VideoTaskCard({ task }: { task: VideoTask }) { {riskConfig.label}
- {/* 文件信息 */} -
-
-
-
-

{task.fileName}

-

{task.fileSize} · {task.duration}

-
- -
+ {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

申诉理由

+

{task.appealReason}

+
+ )} - {/* 底部:时间 + 审核按钮 */} -
- - - {task.submittedAt} - - - - -
+ {/* 文件信息 */} +
+
+
+
+

{task.fileName}

+

{task.fileSize} · {task.duration}

+
+ + +
+ + {/* 底部:时间 + 审核按钮 */} +
+ + + {task.submittedAt} + + + + +
) @@ -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(null) + const [previewVideo, setPreviewVideo] = useState(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 (
{/* 页面标题 */} @@ -318,6 +393,12 @@ export default function AgencyReviewListPage() { {mockVideoTasks.length} 视频 + {(appealScriptCount + appealVideoCount) > 0 && ( + + + {appealScriptCount + appealVideoCount} 申诉 + + )}
@@ -381,7 +462,7 @@ export default function AgencyReviewListPage() { {filteredScripts.length > 0 ? ( filteredScripts.map((task) => ( - + )) ) : (
@@ -408,7 +489,7 @@ export default function AgencyReviewListPage() { {filteredVideos.length > 0 ? ( filteredVideos.map((task) => ( - + )) ) : (
@@ -420,6 +501,94 @@ export default function AgencyReviewListPage() { )}
+ + {/* 脚本预览弹窗 */} + setPreviewScript(null)} + title={previewScript?.fileName || '脚本预览'} + size="lg" + > +
+ {previewScript?.isAppeal && previewScript?.appealReason && ( +
+

+ + 申诉理由 +

+

{previewScript.appealReason}

+
+ )} +
+
+ +

脚本预览区域

+

实际开发中将嵌入文档预览组件

+
+
+
+
+ {previewScript?.fileName} + · + {previewScript?.fileSize} +
+
+ + +
+
+
+
+ + {/* 视频预览弹窗 */} + setPreviewVideo(null)} + title={previewVideo?.fileName || '视频预览'} + size="lg" + > +
+ {previewVideo?.isAppeal && previewVideo?.appealReason && ( +
+

+ + 申诉理由 +

+

{previewVideo.appealReason}

+
+ )} +
+
+
+
+
+
+ {previewVideo?.fileName} + · + {previewVideo?.fileSize} + · + {previewVideo?.duration} +
+
+ + +
+
+
+
) } diff --git a/frontend/app/agency/review/script/[id]/page.tsx b/frontend/app/agency/review/script/[id]/page.tsx index 417421b..ce7ca57 100644 --- a/frontend/app/agency/review/script/[id]/page.tsx +++ b/frontend/app/agency/review/script/[id]/page.tsx @@ -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() {
-

{task.title}

+
+

{task.title}

+ {task.isAppeal && ( + + + 申诉 + + )} +
@@ -130,32 +155,63 @@ export default function AgencyScriptReviewPage() {
- +
+
+ + +
+
+ {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

+ + 申诉理由 +

+

{task.appealReason}

+
+ )} + {/* 审核流程进度条 */}
{/* 左侧:脚本内容 */}
- {viewMode === 'simple' ? ( + {/* 文件信息卡片 */} + setShowFilePreview(true)} + /> + + {viewMode === 'file' ? ( - - -

{task.title}

-

点击"展开预览"查看脚本内容

- + + + + 文件预览 + + + +
) : ( @@ -163,7 +219,8 @@ export default function AgencyScriptReviewPage() { - 脚本内容 + AI 解析内容 + (AI 自动提取的结构化内容) @@ -353,6 +410,13 @@ export default function AgencyScriptReviewPage() {
+ + {/* 文件预览弹窗 */} + setShowFilePreview(false)} + /> ) } diff --git a/frontend/app/agency/review/video/[id]/page.tsx b/frontend/app/agency/review/video/[id]/page.tsx index cba3c48..11bdf32 100644 --- a/frontend/app/agency/review/video/[id]/page.tsx +++ b/frontend/app/agency/review/video/[id]/page.tsx @@ -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>({}) + const [showFilePreview, setShowFilePreview] = useState(false) + const [videoError, setVideoError] = useState(false) const task = mockVideoTask @@ -149,7 +169,15 @@ export default function AgencyVideoReviewPage() {
-

{task.title}

+
+

{task.title}

+ {task.isAppeal && ( + + + 申诉 + + )} +
@@ -163,22 +191,61 @@ export default function AgencyVideoReviewPage() {
+ {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

+ + 申诉理由 +

+

{task.appealReason}

+
+ )} + {/* 审核流程进度条 */}
{/* 左侧:视频播放器 (3/5) */}
+ {/* 文件信息卡片 */} + setShowFilePreview(true)} + /> + -
- + {/* 真实视频播放器 */} +
+ {videoError ? ( +
+
+ +

视频加载失败

+ +
+
+ ) : ( + + )}
{/* 智能进度条 */}
@@ -416,6 +483,13 @@ export default function AgencyVideoReviewPage() {
+ + {/* 文件预览弹窗 */} + setShowFilePreview(false)} + />
) } diff --git a/frontend/app/brand/messages/page.tsx b/frontend/app/brand/messages/page.tsx new file mode 100644 index 0000000..fcd009e --- /dev/null +++ b/frontend/app/brand/messages/page.tsx @@ -0,0 +1,464 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { Card, CardContent } from '@/components/ui/Card' +import { Button } from '@/components/ui/Button' +import { + Bell, + CheckCircle, + XCircle, + AlertTriangle, + FileText, + Video, + Users, + Clock, + Check, + MoreVertical, + Building2, + FolderPlus, + Settings, + MessageCircle, + CalendarClock, + Megaphone, + FileCheck, + UserCheck, + Eye +} from 'lucide-react' +import { getPlatformInfo } from '@/lib/platforms' +import { cn } from '@/lib/utils' + +// 消息类型 +type MessageType = + | 'agency_review_pass' // 代理商审核通过,待品牌终审 + | 'script_pending' // 新脚本待终审 + | 'video_pending' // 新视频待终审 + | 'project_created' // 项目创建成功 + | 'agency_accept' // 代理商接受项目邀请 + | 'creators_assigned' // 代理商配置达人到项目 + | 'content_published' // 内容已发布 + | 'rule_updated' // 规则更新生效 + | 'review_timeout' // 审核超时提醒 + | 'creator_appeal' // 达人发起申诉 + | 'brief_config_updated' // 代理商更新了Brief配置 + | 'batch_review_done' // 批量审核完成 + | 'system_notice' // 系统通知 + +type Message = { + id: string + type: MessageType + title: string + content: string + time: string + read: boolean + platform?: string + projectId?: string + taskId?: string + agencyName?: string + hasAction?: boolean + actionType?: 'review' | 'view' +} + +// 消息配置 +const messageConfig: Record = { + agency_review_pass: { icon: FileCheck, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' }, + script_pending: { icon: FileText, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' }, + video_pending: { icon: Video, iconColor: 'text-purple-400', bgColor: 'bg-purple-500/20' }, + project_created: { icon: FolderPlus, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' }, + agency_accept: { icon: UserCheck, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' }, + creators_assigned: { icon: Users, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' }, + content_published: { icon: Megaphone, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' }, + rule_updated: { icon: Settings, iconColor: 'text-accent-amber', bgColor: 'bg-accent-amber/20' }, + review_timeout: { icon: CalendarClock, iconColor: 'text-orange-400', bgColor: 'bg-orange-500/20' }, + creator_appeal: { icon: MessageCircle, iconColor: 'text-accent-amber', bgColor: 'bg-accent-amber/20' }, + brief_config_updated: { icon: FileText, iconColor: 'text-accent-indigo', bgColor: 'bg-accent-indigo/20' }, + batch_review_done: { icon: CheckCircle, iconColor: 'text-accent-green', bgColor: 'bg-accent-green/20' }, + system_notice: { icon: Bell, iconColor: 'text-text-secondary', bgColor: 'bg-bg-elevated' }, +} + +// 模拟消息数据 +const mockMessages: Message[] = [ + { + id: 'msg-001', + type: 'creators_assigned', + title: '达人已分配', + content: '「星辰传媒」已为【XX品牌618推广】项目配置了5位达人,可查看达人列表', + time: '5分钟前', + read: false, + platform: 'douyin', + agencyName: '星辰传媒', + projectId: 'proj-001', + hasAction: true, + actionType: 'view', + }, + { + id: 'msg-002', + type: 'script_pending', + title: '脚本待终审', + content: '【XX品牌618推广】「星辰传媒」的达人「小美护肤」脚本已通过代理商审核,请进行终审', + time: '10分钟前', + read: false, + platform: 'xiaohongshu', + agencyName: '星辰传媒', + taskId: 'task-007', + hasAction: true, + actionType: 'review', + }, + { + id: 'msg-003', + type: 'video_pending', + title: '视频待终审', + content: '【BB运动饮料】「光影传媒」的达人「健身教练王」视频已通过代理商审核,请进行终审', + time: '30分钟前', + read: false, + platform: 'bilibili', + agencyName: '光影传媒', + taskId: 'task-014', + hasAction: true, + actionType: 'review', + }, + { + id: 'msg-003b', + type: 'creators_assigned', + title: '达人已分配', + content: '「光影传媒」已为【BB运动饮料】项目配置了3位达人,可查看达人列表', + time: '1小时前', + read: false, + platform: 'bilibili', + agencyName: '光影传媒', + projectId: 'proj-002', + hasAction: true, + actionType: 'view', + }, + { + id: 'msg-004', + type: 'review_timeout', + title: '审核超时提醒', + content: '有5条内容已等待终审超过48小时,请及时处理避免影响达人创作进度', + time: '1小时前', + read: false, + hasAction: true, + actionType: 'review', + }, + { + id: 'msg-005', + type: 'agency_accept', + title: '代理商已加入', + content: '「星辰传媒」已接受您的项目邀请,加入【XX品牌618推广】项目', + time: '2小时前', + read: true, + agencyName: '星辰传媒', + projectId: 'proj-001', + }, + { + id: 'msg-007', + type: 'project_created', + title: '项目创建成功', + content: '您的项目【XX品牌618推广】已创建成功,可以开始邀请代理商参与', + time: '昨天 14:30', + read: true, + projectId: 'proj-001', + hasAction: true, + actionType: 'view', + }, + { + id: 'msg-008', + type: 'content_published', + title: '内容已发布', + content: '【XX品牌618推广】项目已有12条视频发布,累计播放量达50万+', + time: '昨天 10:00', + read: true, + platform: 'douyin', + projectId: 'proj-001', + }, + { + id: 'msg-009', + type: 'brief_config_updated', + title: 'Brief配置更新', + content: '「星辰传媒」更新了【XX品牌618推广】的Brief配置,新增3个卖点要求', + time: '昨天 09:15', + read: true, + agencyName: '星辰传媒', + projectId: 'proj-001', + hasAction: true, + actionType: 'view', + }, + { + id: 'msg-010', + type: 'creator_appeal', + title: '达人申诉通知', + content: '达人「美妆Lisa」对【新品口红试色】的驳回结果发起申诉,请关注处理进度', + time: '2天前', + read: true, + taskId: 'task-003', + }, + { + id: 'msg-011', + type: 'rule_updated', + title: '规则更新生效', + content: '您配置的品牌规则【禁用竞品词库】已更新生效,新增15个违禁词', + time: '2天前', + read: true, + }, + { + id: 'msg-012', + type: 'batch_review_done', + title: '批量审核完成', + content: '您今日已完成8条内容终审,其中通过6条,驳回2条', + time: '3天前', + read: true, + }, + { + id: 'msg-013', + type: 'system_notice', + title: '系统通知', + content: '平台违禁词库已更新,新增「抖音」平台美妆类目违禁词56个', + time: '3天前', + read: true, + }, +] + +export default function BrandMessagesPage() { + const router = useRouter() + const [messages, setMessages] = useState(mockMessages) + const [filter, setFilter] = useState<'all' | 'unread' | 'pending'>('all') + + const unreadCount = messages.filter(m => !m.read).length + const pendingReviewCount = messages.filter(m => + !m.read && (m.type === 'agency_review_pass' || m.type === 'script_pending' || m.type === 'video_pending') + ).length + + const getFilteredMessages = () => { + switch (filter) { + case 'unread': + return messages.filter(m => !m.read) + case 'pending': + return messages.filter(m => + m.type === 'agency_review_pass' || m.type === 'script_pending' || m.type === 'video_pending' || m.type === 'review_timeout' + ) + default: + return messages + } + } + + const filteredMessages = getFilteredMessages() + + const markAsRead = (id: string) => { + setMessages(prev => prev.map(m => m.id === id ? { ...m, read: true } : m)) + } + + const markAllAsRead = () => { + setMessages(prev => prev.map(m => ({ ...m, read: true }))) + } + + const handleMessageClick = (message: Message) => { + if (message.type === 'system_notice') return // 系统通知不跳转 + markAsRead(message.id) + + // 根据消息类型跳转 + switch (message.type) { + case 'script_pending': + if (message.taskId) router.push(`/brand/review/script/${message.taskId}`) + else router.push('/brand/review') + break + case 'video_pending': + if (message.taskId) router.push(`/brand/review/video/${message.taskId}`) + else router.push('/brand/review') + break + case 'creator_appeal': + // 达人申诉 -> 终审台 + router.push('/brand/review') + break + case 'rule_updated': + // 规则更新 -> 规则配置 + router.push('/brand/rules') + break + case 'batch_review_done': + // 批量审核完成 -> 终审台 + router.push('/brand/review') + break + case 'agency_review_pass': + case 'review_timeout': + // 待终审内容 -> 终审台 + router.push('/brand/review') + break + default: + if (message.projectId) { + router.push(`/brand/projects/${message.projectId}`) + } + } + } + + const handleAction = (message: Message, e: React.MouseEvent) => { + e.stopPropagation() + markAsRead(message.id) + + if (message.actionType === 'review') { + if (message.taskId) { + if (message.type === 'script_pending') { + router.push(`/brand/review/script/${message.taskId}`) + } else { + router.push(`/brand/review/video/${message.taskId}`) + } + } else { + router.push('/brand/review') + } + } else if (message.actionType === 'view') { + if (message.projectId) { + router.push(`/brand/projects/${message.projectId}`) + } else if (message.type === 'rule_updated') { + router.push('/brand/rules') + } + } + } + + return ( +
+ {/* 页面标题 */} +
+
+

消息中心

+ {unreadCount > 0 && ( + + {unreadCount} 条未读 + + )} + {pendingReviewCount > 0 && ( + + {pendingReviewCount} 条待审 + + )} +
+ +
+ + {/* 筛选标签 */} +
+ + + +
+ + {/* 消息列表 */} +
+ {filteredMessages.map((message) => { + const config = messageConfig[message.type] + const Icon = config.icon + const platform = message.platform ? getPlatformInfo(message.platform) : null + + return ( + handleMessageClick(message)} + > + {/* 平台顶部条 */} + {platform && ( +
+ {platform.icon} + {platform.name} +
+ )} + + +
+ {/* 图标 */} +
+ +
+ + {/* 内容 */} +
+
+

+ {message.title} +

+ {!message.read && ( + + )} +
+

{message.content}

+
+

+ + {message.time} +

+ + {/* 操作按钮 */} + {message.hasAction && ( + + )} +
+
+
+
+
+ ) + })} +
+ + {/* 空状态 */} + {filteredMessages.length === 0 && ( +
+ +

+ {filter === 'unread' ? '没有未读消息' : filter === 'pending' ? '没有待处理消息' : '暂无消息'} +

+
+ )} +
+ ) +} diff --git a/frontend/app/brand/review/page.tsx b/frontend/app/brand/review/page.tsx index aae2ea1..3f5b579 100644 --- a/frontend/app/brand/review/page.tsx +++ b/frontend/app/brand/review/page.tsx @@ -14,8 +14,13 @@ import { User, Building, ChevronRight, - AlertTriangle + AlertTriangle, + Download, + Eye, + File, + MessageSquareWarning } from 'lucide-react' +import { Modal } from '@/components/ui/Modal' import { getPlatformInfo } from '@/lib/platforms' // 模拟脚本待审列表 @@ -23,6 +28,8 @@ const mockScriptTasks = [ { id: 'script-001', title: '夏日护肤推广脚本', + fileName: '夏日护肤推广_脚本v2.docx', + fileSize: '245 KB', creatorName: '小美护肤', agencyName: '星耀传媒', projectName: 'XX品牌618推广', @@ -31,10 +38,13 @@ const mockScriptTasks = [ submittedAt: '2026-02-06 14:30', hasHighRisk: false, agencyApproved: true, + isAppeal: false, }, { id: 'script-002', title: '新品口红试色脚本', + fileName: '口红试色_脚本v1.docx', + fileSize: '312 KB', creatorName: '美妆Lisa', agencyName: '创意无限', projectName: 'XX品牌618推广', @@ -43,6 +53,8 @@ const mockScriptTasks = [ submittedAt: '2026-02-06 12:15', hasHighRisk: true, agencyApproved: true, + isAppeal: true, + appealReason: '已修改违规用词,请求品牌方重新审核', }, ] @@ -51,6 +63,8 @@ const mockVideoTasks = [ { id: 'video-001', title: '夏日护肤推广', + fileName: '夏日护肤_成片v2.mp4', + fileSize: '128 MB', creatorName: '小美护肤', agencyName: '星耀传媒', projectName: 'XX品牌618推广', @@ -60,10 +74,13 @@ const mockVideoTasks = [ submittedAt: '2026-02-06 15:00', hasHighRisk: false, agencyApproved: true, + isAppeal: false, }, { id: 'video-002', title: '新品口红试色', + fileName: '口红试色_终版.mp4', + fileSize: '256 MB', creatorName: '美妆Lisa', agencyName: '创意无限', projectName: 'XX品牌618推广', @@ -73,10 +90,14 @@ const mockVideoTasks = [ submittedAt: '2026-02-06 13:45', hasHighRisk: true, agencyApproved: true, + isAppeal: true, + appealReason: '已按要求重新剪辑,删除了争议片段,请求终审', }, { id: 'video-003', title: '健身器材开箱', + fileName: '健身器材_开箱v3.mp4', + fileSize: '198 MB', creatorName: '健身教练王', agencyName: '美妆达人MCN', projectName: 'XX运动品牌', @@ -86,6 +107,7 @@ const mockVideoTasks = [ submittedAt: '2026-02-06 11:30', hasHighRisk: false, agencyApproved: true, + isAppeal: false, }, ] @@ -95,10 +117,33 @@ function ScoreTag({ score }: { score: number }) { return {score}分 } -function TaskCard({ task, type }: { task: typeof mockScriptTasks[0] | typeof mockVideoTasks[0]; type: 'script' | 'video' }) { +type ScriptTask = typeof mockScriptTasks[0] +type VideoTask = typeof mockVideoTasks[0] + +function TaskCard({ + task, + type, + onPreview +}: { + task: ScriptTask | VideoTask + type: 'script' | 'video' + onPreview: (task: ScriptTask | VideoTask, type: 'script' | 'video') => void +}) { const href = type === 'script' ? `/brand/review/script/${task.id}` : `/brand/review/video/${task.id}` const platform = getPlatformInfo(task.platform) + const handlePreview = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + onPreview(task, type) + } + + const handleDownload = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + alert(`下载文件: ${task.fileName}`) + } + return (
@@ -107,6 +152,13 @@ function TaskCard({ task, type }: { task: typeof mockScriptTasks[0] | typeof moc
{platform.icon} {platform.name} + {/* 申诉标识 */} + {task.isAppeal && ( + + + 申诉 + + )}
)}
@@ -134,6 +186,49 @@ function TaskCard({ task, type }: { task: typeof mockScriptTasks[0] | typeof moc
+ + {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

申诉理由

+

{task.appealReason}

+
+ )} + + {/* 文件信息 */} +
+
+ {type === 'script' ? ( + + ) : ( +
+
+

{task.fileName}

+

+ {task.fileSize} + {'duration' in task && ` · ${task.duration}`} +

+
+ + +
+
{task.projectName} @@ -141,11 +236,6 @@ function TaskCard({ task, type }: { task: typeof mockScriptTasks[0] | typeof moc {task.submittedAt}
- {'duration' in task && ( -
- 时长: {task.duration} -
- )}
@@ -155,6 +245,7 @@ function TaskCard({ task, type }: { task: typeof mockScriptTasks[0] | typeof moc export default function BrandReviewListPage() { const [searchQuery, setSearchQuery] = useState('') const [activeTab, setActiveTab] = useState<'all' | 'script' | 'video'>('all') + const [previewTask, setPreviewTask] = useState<{ task: ScriptTask | VideoTask; type: 'script' | 'video' } | null>(null) const filteredScripts = mockScriptTasks.filter(task => task.title.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -166,6 +257,14 @@ export default function BrandReviewListPage() { task.creatorName.toLowerCase().includes(searchQuery.toLowerCase()) ) + // 计算申诉数量 + const appealScriptCount = mockScriptTasks.filter(t => t.isAppeal).length + const appealVideoCount = mockVideoTasks.filter(t => t.isAppeal).length + + const handlePreview = (task: ScriptTask | VideoTask, type: 'script' | 'video') => { + setPreviewTask({ task, type }) + } + return (
{/* 页面标题 */} @@ -182,6 +281,12 @@ export default function BrandReviewListPage() { {mockVideoTasks.length} 视频 + {(appealScriptCount + appealVideoCount) > 0 && ( + + + {appealScriptCount + appealVideoCount} 申诉 + + )}
@@ -245,7 +350,7 @@ export default function BrandReviewListPage() { {filteredScripts.length > 0 ? ( filteredScripts.map((task) => ( - + )) ) : (
@@ -272,7 +377,7 @@ export default function BrandReviewListPage() { {filteredVideos.length > 0 ? ( filteredVideos.map((task) => ( - + )) ) : (
@@ -284,6 +389,70 @@ export default function BrandReviewListPage() { )}
+ + {/* 预览弹窗 */} + setPreviewTask(null)} + title={previewTask?.task.fileName || '文件预览'} + size="lg" + > +
+ {/* 申诉理由 */} + {previewTask?.task.isAppeal && previewTask?.task.appealReason && ( +
+

+ + 申诉理由 +

+

{previewTask.task.appealReason}

+
+ )} + + {/* 预览区域 */} + {previewTask?.type === 'video' ? ( +
+
+
+
+ ) : ( +
+
+ +

脚本预览区域

+

实际开发中将嵌入文档预览组件

+
+
+ )} + + {/* 文件信息和操作 */} +
+
+ {previewTask?.task.fileName} + · + {previewTask?.task.fileSize} + {previewTask?.type === 'video' && 'duration' in (previewTask?.task || {}) && ( + <> + · + {(previewTask.task as VideoTask).duration} + + )} +
+
+ + +
+
+
+
) } diff --git a/frontend/app/brand/review/script/[id]/page.tsx b/frontend/app/brand/review/script/[id]/page.tsx index da39404..c7393aa 100644 --- a/frontend/app/brand/review/script/[id]/page.tsx +++ b/frontend/app/brand/review/script/[id]/page.tsx @@ -19,8 +19,10 @@ import { Eye, Download, Shield, - MessageSquare + MessageSquare, + MessageSquareWarning } from 'lucide-react' +import { FilePreview, FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview' // 模拟脚本任务数据 const mockScriptTask = { @@ -32,6 +34,18 @@ const mockScriptTask = { submittedAt: '2026-02-06 14:30', aiScore: 88, status: 'brand_reviewing', + // 文件信息 + file: { + id: 'file-001', + fileName: '夏日护肤推广_脚本v2.docx', + fileSize: '245 KB', + fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + fileUrl: '/demo/scripts/script-001.docx', + uploadedAt: '2026-02-06 14:30', + } as FileInfo, + // 申诉信息 + isAppeal: false, + appealReason: '', scriptContent: { opening: '大家好!今天给大家分享一款超级好用的夏日护肤神器~', productIntro: '这款XX品牌的防晒霜,SPF50+,PA++++,真的是夏天出门必备!质地轻薄不油腻,涂上去清清爽爽的。', @@ -88,7 +102,8 @@ export default function BrandScriptReviewPage() { const [showApproveModal, setShowApproveModal] = useState(false) const [showRejectModal, setShowRejectModal] = useState(false) const [rejectReason, setRejectReason] = useState('') - const [viewMode, setViewMode] = useState<'simple' | 'preview'>('preview') + const [viewMode, setViewMode] = useState<'file' | 'parsed'>('file') + const [showFilePreview, setShowFilePreview] = useState(false) const task = mockScriptTask @@ -116,7 +131,15 @@ export default function BrandScriptReviewPage() {
-

{task.title}

+
+

{task.title}

+ {task.isAppeal && ( + + + 申诉 + + )} +
@@ -133,33 +156,62 @@ export default function BrandScriptReviewPage() {
- +
+ + +
+ {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

+ + 申诉理由 +

+

{task.appealReason}

+
+ )} + {/* 审核流程进度条 */}
{/* 左侧:脚本内容 */}
- {viewMode === 'simple' ? ( + {/* 文件信息卡片 */} + setShowFilePreview(true)} + /> + + {viewMode === 'file' ? ( - - -

{task.title}

-

点击"展开预览"查看脚本内容

- + + + + 文件预览 + + + +
) : ( @@ -167,7 +219,8 @@ export default function BrandScriptReviewPage() { - 脚本内容 + AI 解析内容 + (AI 自动提取的结构化内容) @@ -358,6 +411,13 @@ export default function BrandScriptReviewPage() {
+ + {/* 文件预览弹窗 */} + setShowFilePreview(false)} + /> ) } diff --git a/frontend/app/brand/review/video/[id]/page.tsx b/frontend/app/brand/review/video/[id]/page.tsx index ebdbea9..33cbaa6 100644 --- a/frontend/app/brand/review/video/[id]/page.tsx +++ b/frontend/app/brand/review/video/[id]/page.tsx @@ -19,8 +19,11 @@ import { Clock, CheckCircle, XCircle, - MessageSquare + MessageSquare, + ExternalLink, + MessageSquareWarning } from 'lucide-react' +import { FileInfoCard, FilePreviewModal, type FileInfo } from '@/components/ui/FilePreview' // 模拟视频任务数据 const mockVideoTask = { @@ -33,6 +36,20 @@ const mockVideoTask = { duration: 135, // 秒 aiScore: 85, status: 'brand_reviewing', + // 文件信息 + file: { + id: 'file-video-001', + fileName: '夏日护肤_成片v2.mp4', + fileSize: '128 MB', + fileType: 'video/mp4', + fileUrl: '/demo/videos/video-001.mp4', + uploadedAt: '2026-02-06 15:00', + duration: '02:15', + thumbnail: '/demo/videos/video-001-thumb.jpg', + } as FileInfo, + // 申诉信息 + isAppeal: false, + appealReason: '', agencyReview: { reviewer: '张经理', result: 'approved', @@ -111,6 +128,8 @@ export default function BrandVideoReviewPage() { const [showRejectModal, setShowRejectModal] = useState(false) const [rejectReason, setRejectReason] = useState('') const [checkedViolations, setCheckedViolations] = useState>({}) + const [showFilePreview, setShowFilePreview] = useState(false) + const [videoError, setVideoError] = useState(false) const task = mockVideoTask @@ -145,7 +164,15 @@ export default function BrandVideoReviewPage() {
-

{task.title}

+
+

{task.title}

+ {task.isAppeal && ( + + + 申诉 + + )} +
@@ -163,22 +190,61 @@ export default function BrandVideoReviewPage() {
+ {/* 申诉理由 */} + {task.isAppeal && task.appealReason && ( +
+

+ + 申诉理由 +

+

{task.appealReason}

+
+ )} + {/* 审核流程进度条 */}
{/* 左侧:视频播放器 (3/5) */}
+ {/* 文件信息卡片 */} + setShowFilePreview(true)} + /> + -
- + {/* 真实视频播放器 */} +
+ {videoError ? ( +
+
+ +

视频加载失败

+ +
+
+ ) : ( + + )}
{/* 智能进度条 */}
@@ -408,6 +474,13 @@ export default function BrandVideoReviewPage() {
+ + {/* 文件预览弹窗 */} + setShowFilePreview(false)} + />
) } diff --git a/frontend/app/creator/messages/page.tsx b/frontend/app/creator/messages/page.tsx index b28f8c9..1fb60b4 100644 --- a/frontend/app/creator/messages/page.tsx +++ b/frontend/app/creator/messages/page.tsx @@ -13,7 +13,13 @@ import { BadgeCheck, Video, MessageCircle, + CalendarClock, + FileText, + Bell, + Eye, + Clock } from 'lucide-react' +import { Button } from '@/components/ui/Button' import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' import { cn } from '@/lib/utils' @@ -29,9 +35,16 @@ type MessageType = | 'brand_pass' // 品牌方审核通过 | 'brand_reject' // 品牌方审核驳回 | 'video_ai' // 视频AI审核完成 - | 'appeal' // 申诉结果 - | 'video_agency_reject' // 视频代理商驳回 - | 'video_brand_reject' // 视频品牌方驳回 + | 'appeal' // 申诉结果(通用) + | 'appeal_success' // 申诉成功(违规被撤销) + | 'appeal_failed' // 申诉失败(维持原判) + | 'appeal_quota_approved' // 申请增加申诉次数成功 + | 'appeal_quota_rejected' // 申请增加申诉次数失败 + | 'video_agency_reject' // 视频代理商驳回 + | 'video_brand_reject' // 视频品牌方驳回 + | 'task_deadline' // 任务截止提醒 + | 'brief_updated' // Brief更新通知 + | 'system_notice' // 系统通知 type Message = { id: string @@ -42,6 +55,8 @@ type Message = { read: boolean taskId?: string hasActions?: boolean // 是否有操作按钮(邀请类型) + agencyName?: string // 代理商名称(新任务类型) + taskName?: string // 任务名称(新任务类型) } // 消息配置 @@ -61,8 +76,15 @@ const messageConfig: Record void onNavigate: () => void + onViewBrief?: () => void onAcceptInvite?: () => void onIgnoreInvite?: () => void }) { @@ -205,14 +285,18 @@ function MessageCard({ return (
{ - onRead() - if (message.taskId) onNavigate() + if (message.type !== 'new_task' && message.type !== 'invite') { + onRead() + if (message.taskId) onNavigate() + } }} > {/* 图标 */} @@ -231,6 +315,34 @@ function MessageCard({ {/* 描述 */}

{message.content}

+ {/* 新任务类型的操作按钮 */} + {message.type === 'new_task' && message.taskId && ( +
+ + +
+ )} + {/* 邀请类型的操作按钮 */} {message.hasActions && (
@@ -386,9 +498,17 @@ export default function CreatorMessagesPage() { // 邀请消息不跳转,在卡片内有操作按钮 break case 'new_task': - // 新任务 -> 跳转到任务详情(上传脚本) + // 新任务 -> 跳转到Brief查看页 + if (message.taskId) router.push(`/creator/task/${message.taskId}/brief`) + break + case 'task_deadline': + // 任务截止提醒 -> 跳转到任务详情 if (message.taskId) router.push(`/creator/task/${message.taskId}`) break + case 'brief_updated': + // Brief更新 -> 跳转到Brief查看页 + if (message.taskId) router.push(`/creator/task/${message.taskId}/brief`) + break case 'pass': // 审核通过 -> 跳转到任务详情 if (message.taskId) router.push(`/creator/task/${message.taskId}`) @@ -425,6 +545,18 @@ export default function CreatorMessagesPage() { // 申诉结果 -> 跳转到申诉中心 router.push('/creator/appeals') break + case 'appeal_success': + case 'appeal_failed': + // 申诉成功/失败 -> 跳转到任务详情 + if (message.taskId) router.push(`/creator/task/${message.taskId}`) + else router.push('/creator/appeals') + break + case 'appeal_quota_approved': + case 'appeal_quota_rejected': + // 申诉次数申请结果 -> 跳转到任务详情 + if (message.taskId) router.push(`/creator/task/${message.taskId}`) + else router.push('/creator/appeal-quota') + break case 'video_agency_reject': // 视频代理商驳回 -> 跳转到任务详情 if (message.taskId) router.push(`/creator/task/${message.taskId}`) @@ -501,6 +633,9 @@ export default function CreatorMessagesPage() { message={message} onRead={() => markAsRead(message.id)} onNavigate={() => navigateByMessage(message)} + onViewBrief={() => { + if (message.taskId) router.push(`/creator/task/${message.taskId}/brief`) + }} onAcceptInvite={() => handleAcceptInvite(message.id)} onIgnoreInvite={() => handleIgnoreInvite(message.id)} /> diff --git a/frontend/app/creator/task/[id]/brief/page.tsx b/frontend/app/creator/task/[id]/brief/page.tsx new file mode 100644 index 0000000..7010352 --- /dev/null +++ b/frontend/app/creator/task/[id]/brief/page.tsx @@ -0,0 +1,317 @@ +'use client' + +import { useState } from 'react' +import { useRouter, useParams } from 'next/navigation' +import { + ArrowLeft, + FileText, + Download, + Eye, + Target, + Ban, + File, + Building2, + Calendar, + Clock, + ChevronRight +} from 'lucide-react' +import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' +import { Modal } from '@/components/ui/Modal' +import { Button } from '@/components/ui/Button' + +// 代理商Brief文档类型 +type AgencyBriefFile = { + id: string + name: string + size: string + uploadedAt: string + description?: string +} + +// 模拟任务数据 +const mockTaskInfo = { + id: 'task-001', + taskName: 'XX品牌618推广', + agencyName: '星辰传媒', + brandName: 'XX护肤品牌', + deadline: '2026-06-18', + createdAt: '2026-02-08', +} + +// 模拟代理商Brief数据 +const mockAgencyBrief = { + files: [ + { id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' }, + { id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' }, + { id: 'af3', name: '品牌视觉参考.pdf', size: '3.2MB', uploadedAt: '2026-02-02', description: '视觉风格和拍摄参考示例' }, + { id: 'af4', name: '产品素材包.zip', size: '15.6MB', uploadedAt: '2026-02-02', description: '产品图片、视频素材等' }, + ] as AgencyBriefFile[], + sellingPoints: [ + { id: 'sp1', content: 'SPF50+ PA++++', required: true }, + { id: 'sp2', content: '轻薄质地,不油腻', required: true }, + { id: 'sp3', content: '延展性好,易推开', required: false }, + { 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: '绝对化用语' }, + { id: 'bw5', word: '100%', reason: '虚假宣传' }, + ], + contentRequirements: [ + '视频时长:60-90秒', + '需展示产品质地和使用效果', + '需在户外或阳光下拍摄', + '需提及产品核心卖点', + ], +} + +export default function TaskBriefPage() { + const router = useRouter() + const params = useParams() + const [previewFile, setPreviewFile] = useState(null) + + const handleDownload = (file: AgencyBriefFile) => { + alert(`下载文件: ${file.name}`) + } + + const handleDownloadAll = () => { + alert('下载全部文件') + } + + const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required) + const optionalPoints = mockAgencyBrief.sellingPoints.filter(sp => !sp.required) + + return ( + +
+ {/* 顶部导航 */} +
+
+
+ +
+

{mockTaskInfo.taskName}

+

查看任务要求和Brief文档

+
+ +
+ + {/* 任务基本信息 */} +
+

任务信息

+
+
+
+ +
+
+

代理商

+

{mockTaskInfo.agencyName}

+
+
+
+
+ +
+
+

品牌方

+

{mockTaskInfo.brandName}

+
+
+
+
+ +
+
+

分配时间

+

{mockTaskInfo.createdAt}

+
+
+
+
+ +
+
+

截止日期

+

{mockTaskInfo.deadline}

+
+
+
+
+ + {/* 主要内容区域 - 可滚动 */} +
+ {/* Brief文档列表 */} +
+
+
+ +

Brief 文档

+ ({mockAgencyBrief.files.length}个文件) +
+ +
+
+ {mockAgencyBrief.files.map((file) => ( +
+
+
+ +
+
+

{file.name}

+

{file.size}

+ {file.description && ( +

{file.description}

+ )} +
+
+
+ + +
+
+ ))} +
+
+ + {/* 内容要求 */} +
+
+ +

内容要求

+
+
    + {mockAgencyBrief.contentRequirements.map((req, index) => ( +
  • + + {req} +
  • + ))} +
+
+ + {/* 卖点要求 */} +
+
+ +

卖点要求

+
+
+ {requiredPoints.length > 0 && ( +
+

必选卖点(必须在内容中提及)

+
+ {requiredPoints.map((sp) => ( + + {sp.content} + + ))} +
+
+ )} + {optionalPoints.length > 0 && ( +
+

可选卖点(建议提及)

+
+ {optionalPoints.map((sp) => ( + + {sp.content} + + ))} +
+
+ )} +
+
+ + {/* 违禁词 */} +
+
+ +

违禁词(请勿在内容中使用)

+
+
+ {mockAgencyBrief.blacklistWords.map((bw) => ( + + 「{bw.word}」{bw.reason} + + ))} +
+
+ + {/* 底部操作按钮 */} +
+ +
+
+
+ + {/* 文件预览弹窗 */} + setPreviewFile(null)} + title={previewFile?.name || '文件预览'} + size="lg" + > +
+
+
+ +

文件预览区域

+

实际开发中将嵌入文件预览组件

+
+
+
+ + {previewFile && ( + + )} +
+
+
+
+ ) +} diff --git a/frontend/app/creator/task/[id]/page.tsx b/frontend/app/creator/task/[id]/page.tsx index c63d261..54c824b 100644 --- a/frontend/app/creator/task/[id]/page.tsx +++ b/frontend/app/creator/task/[id]/page.tsx @@ -6,8 +6,11 @@ import { Upload, Check, X, Folder, Bell, MessageCircle, XCircle, CheckCircle, Loader2, Scan, ArrowLeft, Bot, Users, Building2, Clock, FileText, Video, - ChevronRight, AlertTriangle + ChevronRight, AlertTriangle, Download, Eye, Target, Ban, + ChevronDown, ChevronUp, File } from 'lucide-react' +import { Modal } from '@/components/ui/Modal' +import { Button } from '@/components/ui/Button' import { ResponsiveLayout } from '@/components/layout/ResponsiveLayout' import { cn } from '@/lib/utils' @@ -50,6 +53,36 @@ type TaskData = { scriptContent?: string } +// 代理商Brief文档(达人可查看) +type AgencyBriefFile = { + id: string + name: string + size: string + uploadedAt: string + description?: string +} + +const mockAgencyBrief = { + files: [ + { id: 'af1', name: '达人拍摄指南.pdf', size: '1.5MB', uploadedAt: '2026-02-02', description: '详细的拍摄流程和注意事项' }, + { id: 'af2', name: '产品卖点话术.docx', size: '800KB', uploadedAt: '2026-02-02', description: '推荐使用的话术和表达方式' }, + { id: 'af3', name: '品牌视觉参考.pdf', size: '3.2MB', uploadedAt: '2026-02-02', description: '视觉风格和拍摄参考示例' }, + ] as AgencyBriefFile[], + sellingPoints: [ + { id: 'sp1', content: 'SPF50+ PA++++', required: true }, + { id: 'sp2', content: '轻薄质地,不油腻', required: true }, + { id: 'sp3', content: '延展性好,易推开', required: false }, + { 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: '绝对化用语' }, + ], +} + // 所有15个任务的详细数据 const allTasksData: Record = { 'task-001': { @@ -355,6 +388,164 @@ function ReviewProgressBar({ task }: { task: TaskData }) { ) } +// Brief文档查看组件 +function AgencyBriefSection() { + const [isExpanded, setIsExpanded] = useState(true) + const [previewFile, setPreviewFile] = useState(null) + + const handleDownload = (file: AgencyBriefFile) => { + alert(`下载文件: ${file.name}`) + } + + const requiredPoints = mockAgencyBrief.sellingPoints.filter(sp => sp.required) + const optionalPoints = mockAgencyBrief.sellingPoints.filter(sp => !sp.required) + + return ( + <> +
+
+
+ + Brief 文档与要求 +
+ +
+ + {isExpanded && ( +
+ {/* Brief文档列表 */} +
+

+ + 参考文档 +

+
+ {mockAgencyBrief.files.map((file) => ( +
+
+
+ +
+
+

{file.name}

+

{file.size}

+
+
+
+ + +
+
+ ))} +
+
+ + {/* 卖点要求 */} +
+

+ + 卖点要求 +

+
+ {requiredPoints.length > 0 && ( +
+

必选卖点(必须提及)

+
+ {requiredPoints.map((sp) => ( + + {sp.content} + + ))} +
+
+ )} + {optionalPoints.length > 0 && ( +
+

可选卖点

+
+ {optionalPoints.map((sp) => ( + + {sp.content} + + ))} +
+
+ )} +
+
+ + {/* 违禁词 */} +
+

+ + 违禁词(请勿使用) +

+
+ {mockAgencyBrief.blacklistWords.map((bw) => ( + + 「{bw.word}」 + + ))} +
+
+
+ )} +
+ + {/* 文件预览弹窗 */} + setPreviewFile(null)} + title={previewFile?.name || '文件预览'} + size="lg" + > +
+
+
+ +

文件预览区域

+

实际开发中将嵌入文件预览组件

+
+
+
+ + {previewFile && ( + + )} +
+
+
+ + ) +} + // 上传界面 function UploadView({ task }: { task: TaskData }) { const [isDragging, setIsDragging] = useState(false) @@ -362,6 +553,9 @@ function UploadView({ task }: { task: TaskData }) { return (
+ {/* Brief文档区域 - 仅脚本阶段显示 */} + {isScript && } +

diff --git a/frontend/components/index.ts b/frontend/components/index.ts index a22b38e..1659a69 100644 --- a/frontend/components/index.ts +++ b/frontend/components/index.ts @@ -11,6 +11,17 @@ export { Input, SearchInput, PasswordInput, type InputProps } from './ui/Input'; export { Select, type SelectProps, type SelectOption } from './ui/Select'; export { ProgressBar, CircularProgress, type ProgressBarProps, type CircularProgressProps } from './ui/ProgressBar'; export { Modal, ConfirmModal, type ModalProps, type ConfirmModalProps } from './ui/Modal'; +export { + FilePreview, + FileInfoCard, + FilePreviewModal, + VideoPlayer, + ImageViewer, + PDFViewer, + DocumentPlaceholder, + getFileCategory, + type FileInfo +} from './ui/FilePreview'; // 导航组件 export { BottomNav } from './navigation/BottomNav'; diff --git a/frontend/components/navigation/Sidebar.tsx b/frontend/components/navigation/Sidebar.tsx index 1a87e4e..06cc27b 100644 --- a/frontend/components/navigation/Sidebar.tsx +++ b/frontend/components/navigation/Sidebar.tsx @@ -52,6 +52,7 @@ const brandNavItems: NavItem[] = [ { icon: FolderKanban, label: '项目看板', href: '/brand' }, { icon: PlusCircle, label: '创建项目', href: '/brand/projects/create' }, { icon: ClipboardCheck, label: '终审台', href: '/brand/review' }, + { icon: Bell, label: '消息中心', href: '/brand/messages' }, { icon: Users, label: '代理商管理', href: '/brand/agencies' }, { icon: FileText, label: '规则配置', href: '/brand/rules' }, { icon: Bot, label: 'AI 配置', href: '/brand/ai-config' }, diff --git a/frontend/components/ui/FilePreview.tsx b/frontend/components/ui/FilePreview.tsx new file mode 100644 index 0000000..290c90b --- /dev/null +++ b/frontend/components/ui/FilePreview.tsx @@ -0,0 +1,403 @@ +'use client' + +import { useState } from 'react' +import { + FileText, + Video, + Image as ImageIcon, + File, + Download, + ExternalLink, + Play, + Pause, + Maximize2, + X, + AlertCircle +} from 'lucide-react' +import { Button } from './Button' +import { Modal } from './Modal' + +// 文件信息类型 +export interface FileInfo { + id: string + fileName: string + fileSize: string + fileType: string // MIME type: "video/mp4", "application/pdf", etc. + fileUrl: string + uploadedAt?: string + duration?: string // 视频时长 "02:15" + thumbnail?: string // 视频缩略图 +} + +// 根据文件名或MIME类型判断文件类别 +export function getFileCategory(file: FileInfo): 'video' | 'image' | 'pdf' | 'document' | 'spreadsheet' | 'other' { + const fileName = file.fileName.toLowerCase() + const mimeType = file.fileType.toLowerCase() + + // 视频 + if (mimeType.startsWith('video/') || /\.(mp4|mov|webm|avi|mkv)$/.test(fileName)) { + return 'video' + } + // 图片 + if (mimeType.startsWith('image/') || /\.(jpg|jpeg|png|gif|webp|svg)$/.test(fileName)) { + return 'image' + } + // PDF + if (mimeType === 'application/pdf' || fileName.endsWith('.pdf')) { + return 'pdf' + } + // Word 文档 + if ( + mimeType.includes('word') || + mimeType.includes('document') || + /\.(doc|docx|txt|rtf)$/.test(fileName) + ) { + return 'document' + } + // Excel 表格 + if ( + mimeType.includes('sheet') || + mimeType.includes('excel') || + /\.(xls|xlsx|csv)$/.test(fileName) + ) { + return 'spreadsheet' + } + + return 'other' +} + +// 获取文件图标 +function getFileIcon(category: ReturnType) { + switch (category) { + case 'video': + return