主要更新: - 新增 FilePreview 通用组件,支持视频/图片/PDF 内嵌预览 - 审核详情页添加文件信息卡片、预览/下载功能 - 审核列表和详情页添加申诉标识和申诉理由显示 - 完善三端消息通知系统(达人/代理商/品牌) - 新增达人 Brief 查看页面 - 新增品牌方消息中心页面 - 创建后端开发备忘文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
404 lines
11 KiB
TypeScript
404 lines
11 KiB
TypeScript
'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<typeof getFileCategory>) {
|
||
switch (category) {
|
||
case 'video':
|
||
return <Video className="w-6 h-6 text-purple-400" />
|
||
case 'image':
|
||
return <ImageIcon className="w-6 h-6 text-accent-green" />
|
||
case 'pdf':
|
||
return <FileText className="w-6 h-6 text-red-400" />
|
||
case 'document':
|
||
return <FileText className="w-6 h-6 text-accent-indigo" />
|
||
case 'spreadsheet':
|
||
return <FileText className="w-6 h-6 text-green-500" />
|
||
default:
|
||
return <File className="w-6 h-6 text-text-secondary" />
|
||
}
|
||
}
|
||
|
||
// 文件信息卡片组件
|
||
export function FileInfoCard({
|
||
file,
|
||
onPreview,
|
||
onDownload,
|
||
showPreviewButton = true
|
||
}: {
|
||
file: FileInfo
|
||
onPreview?: () => void
|
||
onDownload?: () => void
|
||
showPreviewButton?: boolean
|
||
}) {
|
||
const category = getFileCategory(file)
|
||
|
||
const handleDownload = () => {
|
||
if (onDownload) {
|
||
onDownload()
|
||
} else {
|
||
// 默认下载行为
|
||
const link = document.createElement('a')
|
||
link.href = file.fileUrl
|
||
link.download = file.fileName
|
||
link.click()
|
||
}
|
||
}
|
||
|
||
const handleOpenInNewTab = () => {
|
||
window.open(file.fileUrl, '_blank')
|
||
}
|
||
|
||
return (
|
||
<div className="flex items-center gap-3 p-4 rounded-xl bg-bg-elevated">
|
||
<div className="w-12 h-12 rounded-xl bg-bg-page flex items-center justify-center flex-shrink-0">
|
||
{getFileIcon(category)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-text-primary truncate">{file.fileName}</p>
|
||
<p className="text-xs text-text-tertiary">
|
||
{file.fileSize}
|
||
{file.duration && ` · ${file.duration}`}
|
||
{file.uploadedAt && ` · ${file.uploadedAt}`}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-1 flex-shrink-0">
|
||
{showPreviewButton && onPreview && (
|
||
<button
|
||
type="button"
|
||
onClick={onPreview}
|
||
className="p-2.5 rounded-lg hover:bg-bg-page transition-colors"
|
||
title="预览"
|
||
>
|
||
<Maximize2 size={18} className="text-text-secondary" />
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={handleOpenInNewTab}
|
||
className="p-2.5 rounded-lg hover:bg-bg-page transition-colors"
|
||
title="在新标签页打开"
|
||
>
|
||
<ExternalLink size={18} className="text-text-secondary" />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleDownload}
|
||
className="p-2.5 rounded-lg hover:bg-bg-page transition-colors"
|
||
title="下载"
|
||
>
|
||
<Download size={18} className="text-text-secondary" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 视频播放器组件
|
||
export function VideoPlayer({
|
||
file,
|
||
className = '',
|
||
showControls = true
|
||
}: {
|
||
file: FileInfo
|
||
className?: string
|
||
showControls?: boolean
|
||
}) {
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [error, setError] = useState(false)
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={`aspect-video bg-bg-elevated rounded-xl flex items-center justify-center ${className}`}>
|
||
<div className="text-center">
|
||
<AlertCircle className="w-12 h-12 mx-auto text-accent-coral mb-3" />
|
||
<p className="text-text-secondary">视频加载失败</p>
|
||
<Button variant="secondary" size="sm" className="mt-3" onClick={() => window.open(file.fileUrl, '_blank')}>
|
||
<ExternalLink size={14} />
|
||
在新标签页打开
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className={`relative rounded-xl overflow-hidden bg-black ${className}`}>
|
||
<video
|
||
className="w-full h-full"
|
||
controls={showControls}
|
||
poster={file.thumbnail}
|
||
onError={() => setError(true)}
|
||
onPlay={() => setIsPlaying(true)}
|
||
onPause={() => setIsPlaying(false)}
|
||
>
|
||
<source src={file.fileUrl} type={file.fileType} />
|
||
您的浏览器不支持视频播放
|
||
</video>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 图片查看器组件
|
||
export function ImageViewer({
|
||
file,
|
||
className = ''
|
||
}: {
|
||
file: FileInfo
|
||
className?: string
|
||
}) {
|
||
const [error, setError] = useState(false)
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={`aspect-video bg-bg-elevated rounded-xl flex items-center justify-center ${className}`}>
|
||
<div className="text-center">
|
||
<AlertCircle className="w-12 h-12 mx-auto text-accent-coral mb-3" />
|
||
<p className="text-text-secondary">图片加载失败</p>
|
||
<Button variant="secondary" size="sm" className="mt-3" onClick={() => window.open(file.fileUrl, '_blank')}>
|
||
<ExternalLink size={14} />
|
||
在新标签页打开
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className={`bg-bg-elevated rounded-xl overflow-hidden flex items-center justify-center ${className}`}>
|
||
<img
|
||
src={file.fileUrl}
|
||
alt={file.fileName}
|
||
className="max-w-full max-h-full object-contain"
|
||
onError={() => setError(true)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// PDF 查看器组件
|
||
export function PDFViewer({
|
||
file,
|
||
className = ''
|
||
}: {
|
||
file: FileInfo
|
||
className?: string
|
||
}) {
|
||
const [error, setError] = useState(false)
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={`aspect-[4/3] bg-bg-elevated rounded-xl flex items-center justify-center ${className}`}>
|
||
<div className="text-center">
|
||
<AlertCircle className="w-12 h-12 mx-auto text-accent-coral mb-3" />
|
||
<p className="text-text-secondary">PDF 加载失败</p>
|
||
<Button variant="secondary" size="sm" className="mt-3" onClick={() => window.open(file.fileUrl, '_blank')}>
|
||
<ExternalLink size={14} />
|
||
在新标签页打开
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className={`rounded-xl overflow-hidden ${className}`}>
|
||
<iframe
|
||
src={file.fileUrl}
|
||
className="w-full h-full min-h-[500px] border-0"
|
||
title={file.fileName}
|
||
onError={() => setError(true)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 文档预览占位组件(Word/Excel 等不支持内嵌预览的格式)
|
||
export function DocumentPlaceholder({
|
||
file,
|
||
className = ''
|
||
}: {
|
||
file: FileInfo
|
||
className?: string
|
||
}) {
|
||
const category = getFileCategory(file)
|
||
|
||
return (
|
||
<div className={`aspect-[4/3] bg-bg-elevated rounded-xl flex items-center justify-center ${className}`}>
|
||
<div className="text-center p-6">
|
||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-bg-page flex items-center justify-center">
|
||
{getFileIcon(category)}
|
||
</div>
|
||
<p className="text-text-primary font-medium mb-1">{file.fileName}</p>
|
||
<p className="text-sm text-text-tertiary mb-4">{file.fileSize}</p>
|
||
<p className="text-sm text-text-secondary mb-4">
|
||
该文件格式暂不支持在线预览
|
||
</p>
|
||
<div className="flex gap-2 justify-center">
|
||
<Button variant="secondary" onClick={() => window.open(file.fileUrl, '_blank')}>
|
||
<ExternalLink size={16} />
|
||
在新标签页打开
|
||
</Button>
|
||
<Button onClick={() => {
|
||
const link = document.createElement('a')
|
||
link.href = file.fileUrl
|
||
link.download = file.fileName
|
||
link.click()
|
||
}}>
|
||
<Download size={16} />
|
||
下载文件
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 统一的文件预览组件 - 根据文件类型自动选择预览方式
|
||
export function FilePreview({
|
||
file,
|
||
className = ''
|
||
}: {
|
||
file: FileInfo
|
||
className?: string
|
||
}) {
|
||
const category = getFileCategory(file)
|
||
|
||
switch (category) {
|
||
case 'video':
|
||
return <VideoPlayer file={file} className={className} />
|
||
case 'image':
|
||
return <ImageViewer file={file} className={className} />
|
||
case 'pdf':
|
||
return <PDFViewer file={file} className={className} />
|
||
default:
|
||
return <DocumentPlaceholder file={file} className={className} />
|
||
}
|
||
}
|
||
|
||
// 文件预览弹窗组件
|
||
export function FilePreviewModal({
|
||
file,
|
||
isOpen,
|
||
onClose
|
||
}: {
|
||
file: FileInfo | null
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
}) {
|
||
if (!file) return null
|
||
|
||
const category = getFileCategory(file)
|
||
|
||
return (
|
||
<Modal
|
||
isOpen={isOpen}
|
||
onClose={onClose}
|
||
title={file.fileName}
|
||
size="xl"
|
||
>
|
||
<div className="space-y-4">
|
||
{/* 预览区域 */}
|
||
<div className="min-h-[400px]">
|
||
<FilePreview file={file} className="h-full" />
|
||
</div>
|
||
|
||
{/* 底部信息和操作 */}
|
||
<div className="flex items-center justify-between pt-4 border-t border-border-subtle">
|
||
<div className="text-sm text-text-secondary">
|
||
<span>{file.fileSize}</span>
|
||
{file.duration && (
|
||
<>
|
||
<span className="mx-2">·</span>
|
||
<span>{file.duration}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button variant="secondary" onClick={() => window.open(file.fileUrl, '_blank')}>
|
||
<ExternalLink size={16} />
|
||
新标签页打开
|
||
</Button>
|
||
<Button onClick={() => {
|
||
const link = document.createElement('a')
|
||
link.href = file.fileUrl
|
||
link.download = file.fileName
|
||
link.click()
|
||
}}>
|
||
<Download size={16} />
|
||
下载
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
export default FilePreview
|