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,
name=task.project.name,
brand_name=task.project.brand.name if task.project.brand else None,
platform=task.project.platform,
),
agency=AgencyInfo(
id=task.agency.id,

View File

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

View File

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

View File

@ -356,7 +356,7 @@ export default function BriefConfigPage() {
id: brief?.id || `no-brief-${projectId}`,
projectName: project.name,
brandName: project.brand_name || '未知品牌',
platform: 'douyin', // 后端暂无 platform 字段
platform: project.platform || 'douyin',
files: briefFiles,
brandRules: {
restrictions: brief?.other_requirements || '暂无限制条件',
@ -418,15 +418,37 @@ export default function BriefConfigPage() {
}
try {
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 {
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)
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)
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) => {
@ -633,7 +669,14 @@ export default function BriefConfigPage() {
}
try {
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 {
toast.error('获取下载链接失败')
}
@ -1179,20 +1222,44 @@ export default function BriefConfigPage() {
{/* 文件预览弹窗(品牌方) */}
<Modal
isOpen={!!previewFile}
onClose={() => setPreviewFile(null)}
onClose={() => { setPreviewFile(null); setPreviewUrl(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 className="bg-bg-elevated rounded-lg overflow-hidden" style={{ minHeight: '400px' }}>
{previewLoading ? (
<div className="flex items-center justify-center h-[400px]">
<Loader2 className="animate-spin text-accent-indigo" size={32} />
<span className="ml-2 text-text-secondary">...</span>
</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 className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setPreviewFile(null)}>
<Button variant="secondary" onClick={() => { setPreviewFile(null); setPreviewUrl(null) }}>
</Button>
{previewFile && (
@ -1267,20 +1334,44 @@ export default function BriefConfigPage() {
{/* 代理商文档预览弹窗 */}
<Modal
isOpen={!!previewAgencyFile}
onClose={() => setPreviewAgencyFile(null)}
onClose={() => { setPreviewAgencyFile(null); setPreviewAgencyUrl(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 className="bg-bg-elevated rounded-lg overflow-hidden" style={{ minHeight: '400px' }}>
{previewAgencyLoading ? (
<div className="flex items-center justify-center h-[400px]">
<Loader2 className="animate-spin text-accent-indigo" size={32} />
<span className="ml-2 text-text-secondary">...</span>
</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 className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setPreviewAgencyFile(null)}>
<Button variant="secondary" onClick={() => { setPreviewAgencyFile(null); setPreviewAgencyUrl(null) }}>
</Button>
{previewAgencyFile && (

View File

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

View File

@ -245,7 +245,7 @@ export default function AgencyCreatorsPage() {
id: task.id,
name: task.name,
projectName: task.project?.name || '-',
platform: 'douyin', // 后端暂未返回平台信息,默认
platform: task.project?.platform || 'douyin',
stage: mapBackendStage(task.stage),
appealRemaining: task.appeal_count,
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 { api } from '@/lib/api'
import { USE_MOCK } from '@/contexts/AuthContext'
import { getPlatformInfo } from '@/lib/platforms'
import type { TaskResponse, TaskStage } from '@/types/task'
function getPlatformLabel(platformId: string): string {
return getPlatformInfo(platformId)?.name || platformId
}
// ==================== 本地视图模型 ====================
interface TaskViewModel {
id: string
@ -225,7 +230,7 @@ function mapTaskResponseToViewModel(task: TaskResponse): TaskViewModel {
videoTitle: task.name,
creatorName: task.creator?.name || '未知达人',
brandName: task.project?.brand_name || '未知品牌',
platform: '小红书', // 后端暂无 platform 字段
platform: task.project?.platform ? getPlatformLabel(task.project.platform) : '未知平台',
status,
aiScore,
finalScore,

View File

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

View File

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