fix: 代理商平台显示 + Brief 下载预览功能

- 后端 TaskResponse.ProjectInfo 新增 platform 字段
- 修复代理商 6 个页面硬编码 platform='douyin' 的问题,改为读取实际值
- Brief 预览弹窗:占位符改为 iframe/img 实际展示文件内容
  - PDF 用 iframe 在线预览
  - 图片直接展示
  - 其他类型提示下载
- Brief 下载:改用 a 标签触发下载

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-02-10 19:15:03 +08:00
parent 4ca743e7b6
commit 0ab58b7e6e
9 changed files with 126 additions and 27 deletions

View File

@ -68,6 +68,7 @@ def _task_to_response(task: Task) -> TaskResponse:
id=task.project.id, id=task.project.id,
name=task.project.name, name=task.project.name,
brand_name=task.project.brand.name if task.project.brand else None, brand_name=task.project.brand.name if task.project.brand else None,
platform=task.project.platform,
), ),
agency=AgencyInfo( agency=AgencyInfo(
id=task.agency.id, id=task.agency.id,

View File

@ -87,6 +87,7 @@ class ProjectInfo(BaseModel):
id: str id: str
name: str name: str
brand_name: Optional[str] = None brand_name: Optional[str] = None
platform: Optional[str] = None
class TaskResponse(BaseModel): class TaskResponse(BaseModel):

View File

@ -149,7 +149,7 @@ function mapTaskToAppeal(task: TaskResponse): Appeal {
taskTitle: task.name, taskTitle: task.name,
creatorId: task.creator.id, creatorId: task.creator.id,
creatorName: task.creator.name, creatorName: task.creator.name,
platform: 'douyin', // Backend does not expose platform on task; default for now platform: task.project?.platform || 'douyin',
type, type,
contentType, contentType,
reason: task.appeal_reason || '申诉', reason: task.appeal_reason || '申诉',

View File

@ -356,7 +356,7 @@ export default function BriefConfigPage() {
id: brief?.id || `no-brief-${projectId}`, id: brief?.id || `no-brief-${projectId}`,
projectName: project.name, projectName: project.name,
brandName: project.brand_name || '未知品牌', brandName: project.brand_name || '未知品牌',
platform: 'douyin', // 后端暂无 platform 字段 platform: project.platform || 'douyin',
files: briefFiles, files: briefFiles,
brandRules: { brandRules: {
restrictions: brief?.other_requirements || '暂无限制条件', restrictions: brief?.other_requirements || '暂无限制条件',
@ -418,15 +418,37 @@ export default function BriefConfigPage() {
} }
try { try {
const signedUrl = await api.getSignedUrl(file.url) const signedUrl = await api.getSignedUrl(file.url)
window.open(signedUrl, '_blank') // 使用 a 标签触发下载
const a = document.createElement('a')
a.href = signedUrl
a.target = '_blank'
a.rel = 'noopener noreferrer'
a.download = file.name
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
} catch { } catch {
toast.error('获取下载链接失败') toast.error('获取下载链接失败')
} }
} }
// 预览文件 // 预览文件
const handlePreview = (file: BriefFile) => { const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const [previewLoading, setPreviewLoading] = useState(false)
const handlePreview = async (file: BriefFile) => {
setPreviewFile(file) setPreviewFile(file)
setPreviewUrl(null)
if (!USE_MOCK && file.url) {
setPreviewLoading(true)
try {
const signedUrl = await api.getSignedUrl(file.url)
setPreviewUrl(signedUrl)
} catch {
toast.error('获取预览链接失败')
} finally {
setPreviewLoading(false)
}
}
} }
// 导出平台规则文档 // 导出平台规则文档
@ -622,8 +644,22 @@ export default function BriefConfigPage() {
})) }))
} }
const handlePreviewAgencyFile = (file: AgencyFile) => { const [previewAgencyUrl, setPreviewAgencyUrl] = useState<string | null>(null)
const [previewAgencyLoading, setPreviewAgencyLoading] = useState(false)
const handlePreviewAgencyFile = async (file: AgencyFile) => {
setPreviewAgencyFile(file) setPreviewAgencyFile(file)
setPreviewAgencyUrl(null)
if (!USE_MOCK && file.url) {
setPreviewAgencyLoading(true)
try {
const signedUrl = await api.getSignedUrl(file.url)
setPreviewAgencyUrl(signedUrl)
} catch {
toast.error('获取预览链接失败')
} finally {
setPreviewAgencyLoading(false)
}
}
} }
const handleDownloadAgencyFile = async (file: AgencyFile) => { const handleDownloadAgencyFile = async (file: AgencyFile) => {
@ -633,7 +669,14 @@ export default function BriefConfigPage() {
} }
try { try {
const signedUrl = await api.getSignedUrl(file.url) const signedUrl = await api.getSignedUrl(file.url)
window.open(signedUrl, '_blank') const a = document.createElement('a')
a.href = signedUrl
a.target = '_blank'
a.rel = 'noopener noreferrer'
a.download = file.name
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
} catch { } catch {
toast.error('获取下载链接失败') toast.error('获取下载链接失败')
} }
@ -1179,20 +1222,44 @@ export default function BriefConfigPage() {
{/* 文件预览弹窗(品牌方) */} {/* 文件预览弹窗(品牌方) */}
<Modal <Modal
isOpen={!!previewFile} isOpen={!!previewFile}
onClose={() => setPreviewFile(null)} onClose={() => { setPreviewFile(null); setPreviewUrl(null) }}
title={previewFile?.name || '文件预览'} title={previewFile?.name || '文件预览'}
size="lg" size="lg"
> >
<div className="space-y-4"> <div className="space-y-4">
<div className="aspect-[4/3] bg-bg-elevated rounded-lg flex items-center justify-center"> <div className="bg-bg-elevated rounded-lg overflow-hidden" style={{ minHeight: '400px' }}>
<div className="text-center"> {previewLoading ? (
<FileText size={48} className="mx-auto text-text-tertiary mb-4" /> <div className="flex items-center justify-center h-[400px]">
<p className="text-text-secondary"></p> <Loader2 className="animate-spin text-accent-indigo" size={32} />
<p className="text-xs text-text-tertiary mt-1"></p> <span className="ml-2 text-text-secondary">...</span>
</div> </div>
) : previewUrl && previewFile?.name.toLowerCase().endsWith('.pdf') ? (
<iframe
src={previewUrl}
className="w-full border-0 rounded-lg"
style={{ height: '500px' }}
title={previewFile?.name}
/>
) : previewUrl && /\.(jpg|jpeg|png|gif|webp)$/i.test(previewFile?.name || '') ? (
<div className="flex items-center justify-center p-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={previewUrl} alt={previewFile?.name} className="max-w-full max-h-[500px] object-contain rounded" />
</div>
) : previewUrl ? (
<div className="flex flex-col items-center justify-center h-[400px] text-center">
<FileText size={48} className="text-text-tertiary mb-4" />
<p className="text-text-secondary mb-1">线</p>
<p className="text-xs text-text-tertiary">使</p>
</div>
) : (
<div className="flex flex-col items-center justify-center h-[400px] text-center">
<FileText size={48} className="text-text-tertiary mb-4" />
<p className="text-text-secondary"></p>
</div>
)}
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setPreviewFile(null)}> <Button variant="secondary" onClick={() => { setPreviewFile(null); setPreviewUrl(null) }}>
</Button> </Button>
{previewFile && ( {previewFile && (
@ -1267,20 +1334,44 @@ export default function BriefConfigPage() {
{/* 代理商文档预览弹窗 */} {/* 代理商文档预览弹窗 */}
<Modal <Modal
isOpen={!!previewAgencyFile} isOpen={!!previewAgencyFile}
onClose={() => setPreviewAgencyFile(null)} onClose={() => { setPreviewAgencyFile(null); setPreviewAgencyUrl(null) }}
title={previewAgencyFile?.name || '文件预览'} title={previewAgencyFile?.name || '文件预览'}
size="lg" size="lg"
> >
<div className="space-y-4"> <div className="space-y-4">
<div className="aspect-[4/3] bg-bg-elevated rounded-lg flex items-center justify-center"> <div className="bg-bg-elevated rounded-lg overflow-hidden" style={{ minHeight: '400px' }}>
<div className="text-center"> {previewAgencyLoading ? (
<FileText size={48} className="mx-auto text-accent-indigo mb-4" /> <div className="flex items-center justify-center h-[400px]">
<p className="text-text-secondary"></p> <Loader2 className="animate-spin text-accent-indigo" size={32} />
<p className="text-xs text-text-tertiary mt-1"></p> <span className="ml-2 text-text-secondary">...</span>
</div> </div>
) : previewAgencyUrl && previewAgencyFile?.name.toLowerCase().endsWith('.pdf') ? (
<iframe
src={previewAgencyUrl}
className="w-full border-0 rounded-lg"
style={{ height: '500px' }}
title={previewAgencyFile?.name}
/>
) : previewAgencyUrl && /\.(jpg|jpeg|png|gif|webp)$/i.test(previewAgencyFile?.name || '') ? (
<div className="flex items-center justify-center p-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={previewAgencyUrl} alt={previewAgencyFile?.name} className="max-w-full max-h-[500px] object-contain rounded" />
</div>
) : previewAgencyUrl ? (
<div className="flex flex-col items-center justify-center h-[400px] text-center">
<FileText size={48} className="text-text-tertiary mb-4" />
<p className="text-text-secondary mb-1">线</p>
<p className="text-xs text-text-tertiary">使</p>
</div>
) : (
<div className="flex flex-col items-center justify-center h-[400px] text-center">
<FileText size={48} className="text-text-tertiary mb-4" />
<p className="text-text-secondary"></p>
</div>
)}
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setPreviewAgencyFile(null)}> <Button variant="secondary" onClick={() => { setPreviewAgencyFile(null); setPreviewAgencyUrl(null) }}>
</Button> </Button>
{previewAgencyFile && ( {previewAgencyFile && (

View File

@ -141,7 +141,7 @@ export default function AgencyBriefsPage() {
projectId: project.id, projectId: project.id,
projectName: project.name, projectName: project.name,
brandName: project.brand_name || '未知品牌', brandName: project.brand_name || '未知品牌',
platform: 'douyin', // 后端暂无 platform 字段,默认值 platform: project.platform || 'douyin',
status: hasBrief ? 'configured' : 'pending', status: hasBrief ? 'configured' : 'pending',
uploadedAt: project.created_at.split('T')[0], uploadedAt: project.created_at.split('T')[0],
configuredAt: hasBrief ? brief.updated_at.split('T')[0] : null, configuredAt: hasBrief ? brief.updated_at.split('T')[0] : null,
@ -156,7 +156,7 @@ export default function AgencyBriefsPage() {
projectId: project.id, projectId: project.id,
projectName: project.name, projectName: project.name,
brandName: project.brand_name || '未知品牌', brandName: project.brand_name || '未知品牌',
platform: 'douyin', platform: project.platform || 'douyin',
status: 'pending', status: 'pending',
uploadedAt: project.created_at.split('T')[0], uploadedAt: project.created_at.split('T')[0],
configuredAt: null, configuredAt: null,

View File

@ -245,7 +245,7 @@ export default function AgencyCreatorsPage() {
id: task.id, id: task.id,
name: task.name, name: task.name,
projectName: task.project?.name || '-', projectName: task.project?.name || '-',
platform: 'douyin', // 后端暂未返回平台信息,默认 platform: task.project?.platform || 'douyin',
stage: mapBackendStage(task.stage), stage: mapBackendStage(task.stage),
appealRemaining: task.appeal_count, appealRemaining: task.appeal_count,
appealUsed: task.is_appeal ? 1 : 0, appealUsed: task.is_appeal ? 1 : 0,

View File

@ -8,8 +8,13 @@ import { Button } from '@/components/ui/Button'
import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag' import { SuccessTag, WarningTag, ErrorTag, PendingTag } from '@/components/ui/Tag'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext' import { USE_MOCK } from '@/contexts/AuthContext'
import { getPlatformInfo } from '@/lib/platforms'
import type { TaskResponse, TaskStage } from '@/types/task' import type { TaskResponse, TaskStage } from '@/types/task'
function getPlatformLabel(platformId: string): string {
return getPlatformInfo(platformId)?.name || platformId
}
// ==================== 本地视图模型 ==================== // ==================== 本地视图模型 ====================
interface TaskViewModel { interface TaskViewModel {
id: string id: string
@ -225,7 +230,7 @@ function mapTaskResponseToViewModel(task: TaskResponse): TaskViewModel {
videoTitle: task.name, videoTitle: task.name,
creatorName: task.creator?.name || '未知达人', creatorName: task.creator?.name || '未知达人',
brandName: task.project?.brand_name || '未知品牌', brandName: task.project?.brand_name || '未知品牌',
platform: '小红书', // 后端暂无 platform 字段 platform: task.project?.platform ? getPlatformLabel(task.project.platform) : '未知平台',
status, status,
aiScore, aiScore,
finalScore, finalScore,

View File

@ -82,7 +82,7 @@ function mapTaskResponseToUI(task: TaskResponse): Task {
id: task.id, id: task.id,
title: task.name, title: task.name,
description: `${task.project.name} · ${ui.statusLabel}`, description: `${task.project.name} · ${ui.statusLabel}`,
platform: 'douyin', // 后端暂无平台字段,默认 platform: task.project?.platform || 'douyin',
scriptStage: ui.scriptStage, scriptStage: ui.scriptStage,
videoStage: ui.videoStage, videoStage: ui.videoStage,
buttonText: ui.buttonText, buttonText: ui.buttonText,

View File

@ -29,6 +29,7 @@ export interface ProjectInfo {
id: string id: string
name: string name: string
brand_name?: string | null brand_name?: string | null
platform?: string | null
} }
export interface AgencyInfo { export interface AgencyInfo {