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:
parent
4ca743e7b6
commit
0ab58b7e6e
@ -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,
|
||||
|
||||
@ -87,6 +87,7 @@ class ProjectInfo(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
brand_name: Optional[str] = None
|
||||
platform: Optional[str] = None
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
|
||||
@ -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 || '申诉',
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -29,6 +29,7 @@ export interface ProjectInfo {
|
||||
id: string
|
||||
name: string
|
||||
brand_name?: string | null
|
||||
platform?: string | null
|
||||
}
|
||||
|
||||
export interface AgencyInfo {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user