gongxiangp2/components/DocumentAssetViewer.tsx
2026-05-07 16:37:22 +08:00

161 lines
5.1 KiB
TypeScript

'use client'
import { useEffect, useMemo, useState } from 'react'
import Image from 'next/image'
export interface ViewerAssetItem {
id: string
fileName: string
fileUrl: string
fileKind: 'pdf' | 'image'
}
interface DocumentAssetViewerProps {
title: string
items: ViewerAssetItem[]
}
function formatLabel(fileName: string, index: number): string {
const normalized = String(fileName || '').trim()
return normalized || `文件 ${index + 1}`
}
export default function DocumentAssetViewer({ title, items }: DocumentAssetViewerProps) {
const normalizedItems = useMemo(
() =>
items
.filter((item) => item.fileUrl)
.map((item, index) => ({
...item,
fileName: formatLabel(item.fileName, index),
})),
[items],
)
const [selectedId, setSelectedId] = useState(normalizedItems[0]?.id || '')
useEffect(() => {
setSelectedId(normalizedItems[0]?.id || '')
}, [normalizedItems])
const selectedItem = normalizedItems.find((item) => item.id === selectedId) || normalizedItems[0]
const handleOpenFile = () => {
if (!selectedItem?.fileUrl) {
return
}
window.open(selectedItem.fileUrl, '_blank', 'noopener,noreferrer')
}
const handleDownloadFile = async () => {
if (!selectedItem?.fileUrl) {
return
}
try {
const response = await fetch(selectedItem.fileUrl)
if (!response.ok) {
throw new Error('文件下载失败')
}
const blob = await response.blob()
const objectUrl = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = objectUrl
anchor.download = selectedItem.fileName
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(objectUrl)
} catch (error) {
console.error('[DocumentAssetViewer] 下载失败,改为直接打开原文件:', error)
window.open(selectedItem.fileUrl, '_blank', 'noopener,noreferrer')
}
}
if (!selectedItem) {
return (
<div className="flex min-h-[400px] items-center justify-center bg-gray-100 text-sm text-gray-500 dark:bg-gray-700 dark:text-gray-300">
暂无可预览文件
</div>
)
}
return (
<div className="w-full bg-white dark:bg-gray-800">
<div className="border-b border-gray-200 px-4 py-3 dark:border-gray-700">
<div className="mb-3 text-sm font-medium text-gray-700 dark:text-gray-200">{title}</div>
<div className="flex flex-wrap gap-2">
{normalizedItems.map((item) => {
const isActive = item.id === selectedItem.id
return (
<button
key={item.id}
type="button"
onClick={() => setSelectedId(item.id)}
className={`rounded-full px-3 py-1.5 text-xs transition-colors ${
isActive
? 'bg-gray-900 text-white dark:bg-blue-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
>
{item.fileKind === 'pdf' ? 'PDF' : '图片'} · {item.fileName}
</button>
)
})}
</div>
</div>
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-gray-700">
<div className="min-w-0">
<div className="truncate text-sm font-medium text-gray-900 dark:text-white">{selectedItem.fileName}</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{selectedItem.fileKind === 'pdf' ? 'PDF 文档预览' : '图片原图预览'}
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleOpenFile}
className="rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700"
>
打开原文件
</button>
<button
type="button"
onClick={() => {
void handleDownloadFile()
}}
className="rounded-lg bg-gray-900 px-3 py-2 text-sm text-white transition-colors hover:bg-gray-800 dark:bg-blue-600 dark:hover:bg-blue-700"
>
下载原文件
</button>
</div>
</div>
<div className="bg-gray-100 dark:bg-gray-700">
{selectedItem.fileKind === 'pdf' ? (
<iframe
src={`${selectedItem.fileUrl}#toolbar=0&navpanes=0&scrollbar=1`}
className="w-full"
style={{ height: '980px', border: 'none' }}
title={selectedItem.fileName}
/>
) : (
<div className="flex items-center justify-center p-6">
<Image
src={selectedItem.fileUrl}
alt={selectedItem.fileName}
width={790}
height={1200}
unoptimized
className="h-auto w-full object-contain"
style={{ maxWidth: '790px' }}
/>
</div>
)}
</div>
</div>
)
}