feat: 完善审核台文件预览与消息通知系统
主要更新: - 新增 FilePreview 通用组件,支持视频/图片/PDF 内嵌预览 - 审核详情页添加文件信息卡片、预览/下载功能 - 审核列表和详情页添加申诉标识和申诉理由显示 - 完善三端消息通知系统(达人/代理商/品牌) - 新增达人 Brief 查看页面 - 新增品牌方消息中心页面 - 创建后端开发备忘文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
bbc8a4f641
commit
a5a005db0c
@@ -11,6 +11,17 @@ export { Input, SearchInput, PasswordInput, type InputProps } from './ui/Input';
|
||||
export { Select, type SelectProps, type SelectOption } from './ui/Select';
|
||||
export { ProgressBar, CircularProgress, type ProgressBarProps, type CircularProgressProps } from './ui/ProgressBar';
|
||||
export { Modal, ConfirmModal, type ModalProps, type ConfirmModalProps } from './ui/Modal';
|
||||
export {
|
||||
FilePreview,
|
||||
FileInfoCard,
|
||||
FilePreviewModal,
|
||||
VideoPlayer,
|
||||
ImageViewer,
|
||||
PDFViewer,
|
||||
DocumentPlaceholder,
|
||||
getFileCategory,
|
||||
type FileInfo
|
||||
} from './ui/FilePreview';
|
||||
|
||||
// 导航组件
|
||||
export { BottomNav } from './navigation/BottomNav';
|
||||
|
||||
@@ -52,6 +52,7 @@ const brandNavItems: NavItem[] = [
|
||||
{ icon: FolderKanban, label: '项目看板', href: '/brand' },
|
||||
{ icon: PlusCircle, label: '创建项目', href: '/brand/projects/create' },
|
||||
{ icon: ClipboardCheck, label: '终审台', href: '/brand/review' },
|
||||
{ icon: Bell, label: '消息中心', href: '/brand/messages' },
|
||||
{ icon: Users, label: '代理商管理', href: '/brand/agencies' },
|
||||
{ icon: FileText, label: '规则配置', href: '/brand/rules' },
|
||||
{ icon: Bot, label: 'AI 配置', href: '/brand/ai-config' },
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
'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
|
||||
Reference in New Issue
Block a user