1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 (/^(<!doctype html|<html)/i.test(head) && !/\.html?$/i.test(filename)) {
    throw new Error('未获取到附件内容,请确认文件存在或重新登录后重试')
  }
}