185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import { ElMessage } from 'element-plus'
|
|
import { fetchAttachmentDownloadUrl, fetchAttachmentPreviewUrl } from '@/api/admin'
|
|
|
|
export type AttachmentDownloadItem = {
|
|
fileUrl?: string
|
|
fileName?: string
|
|
}
|
|
|
|
export type AttachmentPreviewKind = 'image' | 'pdf' | 'other'
|
|
|
|
/** 将 JSON 数组或逗号分隔的附件 URL 统一为逗号分隔字符串 */
|
|
export function normalizeAttachmentUrls(raw?: string | null): string {
|
|
if (!raw) return ''
|
|
const trimmed = String(raw).trim()
|
|
if (!trimmed) return ''
|
|
if (trimmed.startsWith('[')) {
|
|
try {
|
|
const parsed = JSON.parse(trimmed)
|
|
if (Array.isArray(parsed)) {
|
|
return parsed.map((url) => String(url).trim()).filter(Boolean).join(',')
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
function triggerAnchorDownload(href: string, fileName: string) {
|
|
const a = document.createElement('a')
|
|
a.href = href
|
|
a.download = fileName
|
|
a.rel = 'noopener'
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
}
|
|
|
|
function escapeHtmlAttr(value: string) {
|
|
return String(value || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
}
|
|
|
|
/** 根据文件名/URL 推断预览类型 */
|
|
export function detectAttachmentPreviewKind(file: AttachmentDownloadItem): AttachmentPreviewKind {
|
|
const name = String(file.fileName || '').toLowerCase()
|
|
const url = String(file.fileUrl || '')
|
|
.split('?')[0]
|
|
.toLowerCase()
|
|
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(name) || /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(url)) {
|
|
return 'image'
|
|
}
|
|
if (/\.pdf$/i.test(name) || /\.pdf$/i.test(url)) {
|
|
return 'pdf'
|
|
}
|
|
return 'other'
|
|
}
|
|
|
|
/** 图片用 HTML 内嵌展示,避免 Content-Type 为 octet-stream 时顶级导航触发下载 */
|
|
function openImagePreviewDocument(previewWindow: Window | null, href: string, title: string) {
|
|
const safeSrc = escapeHtmlAttr(href)
|
|
const safeTitle = escapeHtmlAttr(title || '图片预览')
|
|
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${safeTitle}</title><style>html,body{margin:0;height:100%;background:#0f172a}body{display:flex;align-items:center;justify-content:center}img{max-width:100%;max-height:100vh;object-fit:contain}</style></head><body><img src="${safeSrc}" alt="${safeTitle}"/></body></html>`
|
|
if (previewWindow) {
|
|
previewWindow.document.open()
|
|
previewWindow.document.write(html)
|
|
previewWindow.document.close()
|
|
return
|
|
}
|
|
const blobUrl = URL.createObjectURL(new Blob([html], { type: 'text/html;charset=utf-8' }))
|
|
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
|
}
|
|
|
|
/** 附件预览:优先走后端预签名 inline URL,供 PDF iframe 等内联展示。 */
|
|
export async function resolveAttachmentPreviewUrl(
|
|
file: AttachmentDownloadItem,
|
|
options?: { fullFileUrl?: (url: string) => string }
|
|
): Promise<string> {
|
|
const fileUrl = String(file.fileUrl || '').trim()
|
|
if (!fileUrl) return ''
|
|
const resolveUrl = options?.fullFileUrl || ((url: string) => url)
|
|
|
|
try {
|
|
const res = await fetchAttachmentPreviewUrl({ fileUrl, fileName: file.fileName })
|
|
if (res?.previewUrl) return res.previewUrl
|
|
} catch {
|
|
// 预签名失败时回退直连
|
|
}
|
|
|
|
return resolveUrl(fileUrl)
|
|
}
|
|
|
|
/**
|
|
* 在浏览器新标签页打开附件预览(图片 HTML 内嵌 / PDF 直开)。
|
|
* 不支持在线预览的类型会提示并返回 false。
|
|
*/
|
|
export async function openAttachmentPreviewInNewTab(
|
|
file: AttachmentDownloadItem,
|
|
options?: {
|
|
fullFileUrl?: (url: string) => string
|
|
kind?: AttachmentPreviewKind
|
|
}
|
|
): Promise<boolean> {
|
|
const fileUrl = String(file.fileUrl || '').trim()
|
|
if (!fileUrl) {
|
|
ElMessage.error('无法获取文件预览地址')
|
|
return false
|
|
}
|
|
|
|
const kind = options?.kind || detectAttachmentPreviewKind(file)
|
|
if (kind === 'other') {
|
|
ElMessage.warning('该文件类型暂不支持在线预览,请使用下载查看')
|
|
return false
|
|
}
|
|
|
|
const previewWindow = window.open('about:blank', '_blank')
|
|
try {
|
|
const previewUrl = await resolveAttachmentPreviewUrl(file, {
|
|
fullFileUrl: options?.fullFileUrl
|
|
})
|
|
const href =
|
|
previewUrl || (options?.fullFileUrl ? options.fullFileUrl(fileUrl) : fileUrl)
|
|
if (!href) {
|
|
previewWindow?.close()
|
|
ElMessage.error('无法获取文件预览地址')
|
|
return false
|
|
}
|
|
if (kind === 'image') {
|
|
openImagePreviewDocument(previewWindow, href, file.fileName || '图片预览')
|
|
return true
|
|
}
|
|
if (previewWindow) {
|
|
previewWindow.location.href = href
|
|
} else {
|
|
window.open(href, '_blank', 'noopener,noreferrer')
|
|
}
|
|
return true
|
|
} catch (error) {
|
|
console.error('打开附件预览失败:', error)
|
|
const fallback = options?.fullFileUrl ? options.fullFileUrl(fileUrl) : fileUrl
|
|
if (fallback && kind === 'image') {
|
|
openImagePreviewDocument(previewWindow, fallback, file.fileName || '图片预览')
|
|
return true
|
|
}
|
|
if (fallback && previewWindow) {
|
|
previewWindow.location.href = fallback
|
|
return true
|
|
}
|
|
previewWindow?.close()
|
|
ElMessage.error('打开预览失败,请稍后重试或使用下载')
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** 附件下载:优先走后端预签名 URL,保证跨域场景下保留原始文件名与后缀。 */
|
|
export async function downloadAttachmentFile(
|
|
file: AttachmentDownloadItem,
|
|
options?: { fullFileUrl?: (url: string) => string }
|
|
) {
|
|
const fileUrl = String(file.fileUrl || '').trim()
|
|
if (!fileUrl) return
|
|
const fileName = String(file.fileName || '').trim() || fileUrl.split('/').pop() || 'download'
|
|
|
|
try {
|
|
const res = await fetchAttachmentDownloadUrl({ fileUrl, fileName })
|
|
if (res?.downloadUrl) {
|
|
triggerAnchorDownload(res.downloadUrl, fileName)
|
|
return
|
|
}
|
|
} catch {
|
|
// 预签名失败时回退直连
|
|
}
|
|
|
|
const resolveUrl = options?.fullFileUrl || ((url: string) => url)
|
|
const href = resolveUrl(fileUrl)
|
|
if (!href) {
|
|
ElMessage.error('无法下载附件')
|
|
return
|
|
}
|
|
triggerAnchorDownload(href, fileName)
|
|
}
|