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,
|
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,
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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 || '申诉',
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user