oa-pdf-preview-query.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * OA PDF 预览页 query 解析(短 fileUrl / 兼容旧 url)
  3. * 微信 onLoad 可能已 decode 一次;须安全二次解码,并兼容大小写 key。
  4. */
  5. const LOG_TAG = '[oa-pdf-preview]'
  6. export function pdfPreviewLog(...args) {
  7. try {
  8. console.log(LOG_TAG, ...args)
  9. } catch (_) {
  10. /* ignore */
  11. }
  12. }
  13. export function safeDecodeURIComponent(value) {
  14. const raw = String(value == null ? '' : value).trim()
  15. if (!raw) return ''
  16. try {
  17. return decodeURIComponent(raw)
  18. } catch (_) {
  19. return raw
  20. }
  21. }
  22. /** 按候选 key 取值;忽略大小写;兼容微信偶发改写 */
  23. export function pickQueryValue(query, keys) {
  24. const q = query && typeof query === 'object' ? query : {}
  25. const list = Array.isArray(keys) ? keys : [keys]
  26. for (let i = 0; i < list.length; i++) {
  27. const k = list[i]
  28. if (k == null) continue
  29. if (Object.prototype.hasOwnProperty.call(q, k) && q[k] != null) {
  30. const v = String(q[k]).trim()
  31. if (v) return v
  32. }
  33. }
  34. const lowerMap = {}
  35. Object.keys(q).forEach((k) => {
  36. lowerMap[String(k).toLowerCase()] = q[k]
  37. })
  38. for (let i = 0; i < list.length; i++) {
  39. const k = String(list[i] || '').toLowerCase()
  40. if (!k) continue
  41. if (lowerMap[k] != null) {
  42. const v = String(lowerMap[k]).trim()
  43. if (v) return v
  44. }
  45. }
  46. return ''
  47. }
  48. /**
  49. * @returns {{ fileUrl: string, legacyUrl: string, name: string, rawKeys: string[] }}
  50. */
  51. export function parsePdfPreviewQuery(query) {
  52. const rawKeys = Object.keys(query && typeof query === 'object' ? query : {})
  53. const fileUrl = safeDecodeURIComponent(
  54. pickQueryValue(query, ['fileUrl', 'fileurl', 'file_url', 'f'])
  55. )
  56. const legacyUrl = safeDecodeURIComponent(
  57. pickQueryValue(query, ['url', 'downloadUrl', 'download_url'])
  58. )
  59. const name =
  60. safeDecodeURIComponent(pickQueryValue(query, ['name', 'fileName', 'file_name', 'n'])) ||
  61. 'approval.pdf'
  62. return { fileUrl, legacyUrl, name, rawKeys }
  63. }
  64. /**
  65. * 后端 MINIO_BASE_URL 常配成同域相对路径(如 /minio/zkxw),预签名改写后也是
  66. * `/minio/zkxw/...?X-Amz-...`。微信 downloadFile 必须绝对 http(s)。
  67. *
  68. * @param {string} url
  69. * @param {{ apiBaseUrl?: string, minioBaseUrl?: string }} [bases]
  70. */
  71. export function toAbsoluteDownloadUrl(url, bases = {}) {
  72. const s = String(url || '').trim()
  73. if (!s) return ''
  74. if (/^https?:\/\//i.test(s)) return s
  75. if (s.startsWith('//')) return 'https:' + s
  76. const apiBase = String(bases.apiBaseUrl || '').trim()
  77. const minioBase = String(bases.minioBaseUrl || '').trim()
  78. const originFrom = (base) => {
  79. const m = String(base || '').match(/^(https?:\/\/[^/]+)/i)
  80. return m ? m[1] : ''
  81. }
  82. const origin = originFrom(apiBase) || originFrom(minioBase)
  83. if (s.startsWith('/')) {
  84. return origin ? origin + s : ''
  85. }
  86. // 相对 object key
  87. const minio = minioBase.replace(/\/$/, '')
  88. if (/^https?:\/\//i.test(minio)) {
  89. return minio + '/' + s.replace(/^\//, '')
  90. }
  91. if (minio.startsWith('/') && origin) {
  92. return origin + minio + '/' + s.replace(/^\//, '')
  93. }
  94. return ''
  95. }
  96. /**
  97. * 拼 MinIO 公网直链(无预签名)。
  98. * 预签名经相对路径改写后微信 downloadFile 常 403;公网直链对本环境可 200。
  99. */
  100. export function buildDirectMinioUrl(fileUrl, bases = {}) {
  101. const raw = String(fileUrl || '').trim()
  102. if (!raw) return ''
  103. const minioBase = String(bases.minioBaseUrl || '').replace(/\/$/, '')
  104. const apiBase = String(bases.apiBaseUrl || '').trim()
  105. const originMatch = (apiBase || minioBase).match(/^(https?:\/\/[^/]+)/i)
  106. const origin = originMatch ? originMatch[1] : ''
  107. // 已是绝对 http(s):去掉 query(签名)后若可识别为同域 MinIO 则规范化,否则原样去 query
  108. if (/^https?:\/\//i.test(raw)) {
  109. const noQuery = raw.split('?')[0]
  110. try {
  111. const u = new URL(noQuery)
  112. const minioPath = minioBase.replace(/^https?:\/\/[^/]+/i, '') || ''
  113. if (minioPath && u.pathname.indexOf(minioPath) === 0) {
  114. return origin && minioPath.startsWith('/')
  115. ? origin + u.pathname
  116. : noQuery
  117. }
  118. } catch (_) {
  119. /* ignore */
  120. }
  121. return noQuery
  122. }
  123. // 根相对:/minio/zkxw/key.pdf?... → 拼 origin
  124. if (raw.startsWith('/')) {
  125. const pathOnly = raw.split('?')[0]
  126. return origin ? origin + pathOnly : toAbsoluteDownloadUrl(pathOnly, bases)
  127. }
  128. // object key:去掉可能误带的 minio 路径前缀
  129. let key = raw.replace(/^\//, '').split('?')[0]
  130. const minioPath = minioBase.replace(/^https?:\/\/[^/]+/i, '').replace(/^\//, '')
  131. if (minioPath && key.indexOf(minioPath + '/') === 0) {
  132. key = key.slice(minioPath.length + 1)
  133. }
  134. return toAbsoluteDownloadUrl(key, bases)
  135. }