Your Name a5a005db0c feat: 完善审核台文件预览与消息通知系统
主要更新:
- 新增 FilePreview 通用组件,支持视频/图片/PDF 内嵌预览
- 审核详情页添加文件信息卡片、预览/下载功能
- 审核列表和详情页添加申诉标识和申诉理由显示
- 完善三端消息通知系统(达人/代理商/品牌)
- 新增达人 Brief 查看页面
- 新增品牌方消息中心页面
- 创建后端开发备忘文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 12:20:47 +08:00

404 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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