import { getJavaApi } from '@/utils/java-api' const imageExtension = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff?|avif|heic)$/i function decodePart(value) { try { return decodeURIComponent(value) } catch { return value } } // 数据库可能保存文件名、静态路径或完整地址;只为文件名补充存储目录。 export function resolveAttachmentUrl(value, type, base = getJavaApi()) { if (typeof value !== 'string' || !value.trim()) throw new Error('附件地址为空') let path = value.replace(/\\/g, '/') base = base.replace(/\/+$/, '') if (/^(blob:|data:)/i.test(path)) return path if (path.startsWith(base + '/')) path = path.slice(base.length) else if (/^(https?:)?\/\//i.test(path)) return new URL(path, window.location.origin).href else if (/^[a-z][a-z\d+.-]*:/i.test(path)) throw new Error('附件地址格式不正确') // 兼容已经带有 /dev-api 等代理前缀的地址,避免重复拼接。 const basePath = new URL(base, window.location.origin).pathname.replace(/\/$/, '') if (basePath && path.startsWith(basePath + '/')) path = path.slice(basePath.length) path = path.replace(/^\/+/, '') if (!/^(img|word|profile)\//i.test(path)) { const isImage = Number(type) === 1 || (Number(type) !== 2 && imageExtension.test(decodePart(path))) path = (isImage ? 'img/' : 'word/') + path } // 逐段编码,保留目录和已有编码,文件名里的 #、?、空格与中文均作为文件名处理。 const encodedPath = path.split('/').map(part => encodeURIComponent(decodePart(part))).join('/') return base + '/' + encodedPath } export function attachmentFileName(url, name) { const path = new URL(url, window.location.origin).pathname const storedName = decodePart(path.split('/').pop() || '') const filename = (name || storedName || '附件').replace(/\\/g, '/').split('/').pop() const extension = storedName.match(/\.[a-z\d]{1,10}$/i)?.[0] || '' return extension && !/\.[a-z\d]{1,10}$/i.test(filename) ? filename + extension : filename } // 历史上传接口对 BMP/WebP 等格式的目录判断不一致,404 时兼容另一存储目录。 export function alternateImageAttachmentUrl(url) { const parsed = new URL(url, window.location.origin) if (!imageExtension.test(decodePart(parsed.pathname)) || !/\/(img|word)\//.test(parsed.pathname)) return null parsed.pathname = parsed.pathname.replace(/\/(img|word)\//, (_, directory) => directory === 'img' ? '/word/' : '/img/') return parsed.href } // 下载失败的 JSON/HTML 响应不能保存成 Word、PDF 等“损坏的附件”。 export async function validateAttachmentBlob(blob, filename) { const head = (await blob.slice(0, 512).text()).trim() if (head.startsWith('{') && blob.size < 1024 * 1024) { let result try { result = JSON.parse(await blob.text()) } catch { return } if (result && ((result.code != null && Number(result.code) !== 200) || result.success === false)) { throw new Error(result.msg || result.message || '附件下载失败') } } if (/^(