import { ref } from 'vue'
import { fetchAuthenticatedBlob } from '@/api/index'
import type { ChatAttachment } from '@/types'
/**
* 统一受保护附件 Loader
*
* 项目使用 Bearer token 认证(localStorage),
和 不会自动携带
* Authorization header。此 composable 统一处理鉴权 fetch → blob → ObjectURL。
*
* 使用方式:
* - 图片:loadAllImages() 批量加载 → blobUrls[key] 绑定
* - 文件:downloadFile() 点击时鉴权下载
* - 图片点击放大:openImage() 同步开窗避免弹窗拦截
* - 组件卸载:revokeAll() 释放所有 blob URL
*/
export function useAuthenticatedAttachment() {
/** 已加载的 blob URL 映射(key = storedName || url) */
const blobUrls = ref>({})
/** 跟踪所有创建的 blob URL,用于清理 */
const trackedUrls: string[] = []
/** 正在加载的 URL 集合,防止重复请求 */
const loading = new Set()
/**
* 加载单个附件的 blob URL
*/
async function loadBlobUrl(url: string, key: string): Promise {
// 跳过:已加载、正在加载、本地 blob
if (blobUrls.value[key] || loading.has(key) || url.startsWith('blob:')) return blobUrls.value[key] || url
loading.add(key)
try {
const blob = await fetchAuthenticatedBlob(url)
const objectUrl = URL.createObjectURL(blob)
blobUrls.value[key] = objectUrl
trackedUrls.push(objectUrl)
return objectUrl
} catch (e) {
console.warn('[useAuthenticatedAttachment] Failed to load:', url, e)
return null
} finally {
loading.delete(key)
}
}
/**
* 批量加载所有图片附件的 blob URL
*/
async function loadAllImages(attachments: ChatAttachment[]) {
const imageAtts = attachments.filter(a => a.contentType?.startsWith('image/'))
for (const att of imageAtts) {
const key = att.storedName || att.url
if (!att.previewUrl && att.url && !blobUrls.value[key]) {
await loadBlobUrl(att.url, key)
}
}
}
/**
* 批量加载所有视频附件的 blob URL
*/
async function loadAllVideos(attachments: ChatAttachment[]) {
const videoAtts = attachments.filter(a => a.contentType?.startsWith('video/'))
for (const att of videoAtts) {
const key = att.storedName || att.url
if (!att.previewUrl && att.url && !blobUrls.value[key]) {
await loadBlobUrl(att.url, key)
}
}
}
/**
* 批量加载所有音频附件的 blob URL(