Files
shanxia2.0/web-admin/src/utils/download-attachment.ts
T
2026-08-08 20:43:48 +08:00

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, '&lt;')
.replace(/>/g, '&gt;')
}
/** 根据文件名/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)
}