/** * OA PDF 预览页 query 解析(短 fileUrl / 兼容旧 url) * 微信 onLoad 可能已 decode 一次;须安全二次解码,并兼容大小写 key。 */ const LOG_TAG = '[oa-pdf-preview]' export function pdfPreviewLog(...args) { try { console.log(LOG_TAG, ...args) } catch (_) { /* ignore */ } } export function safeDecodeURIComponent(value) { const raw = String(value == null ? '' : value).trim() if (!raw) return '' try { return decodeURIComponent(raw) } catch (_) { return raw } } /** 按候选 key 取值;忽略大小写;兼容微信偶发改写 */ export function pickQueryValue(query, keys) { const q = query && typeof query === 'object' ? query : {} const list = Array.isArray(keys) ? keys : [keys] for (let i = 0; i < list.length; i++) { const k = list[i] if (k == null) continue if (Object.prototype.hasOwnProperty.call(q, k) && q[k] != null) { const v = String(q[k]).trim() if (v) return v } } const lowerMap = {} Object.keys(q).forEach((k) => { lowerMap[String(k).toLowerCase()] = q[k] }) for (let i = 0; i < list.length; i++) { const k = String(list[i] || '').toLowerCase() if (!k) continue if (lowerMap[k] != null) { const v = String(lowerMap[k]).trim() if (v) return v } } return '' } /** * @returns {{ fileUrl: string, legacyUrl: string, name: string, rawKeys: string[] }} */ export function parsePdfPreviewQuery(query) { const rawKeys = Object.keys(query && typeof query === 'object' ? query : {}) const fileUrl = safeDecodeURIComponent( pickQueryValue(query, ['fileUrl', 'fileurl', 'file_url', 'f']) ) const legacyUrl = safeDecodeURIComponent( pickQueryValue(query, ['url', 'downloadUrl', 'download_url']) ) const name = safeDecodeURIComponent(pickQueryValue(query, ['name', 'fileName', 'file_name', 'n'])) || 'approval.pdf' return { fileUrl, legacyUrl, name, rawKeys } } /** * 后端 MINIO_BASE_URL 常配成同域相对路径(如 /minio/zkxw),预签名改写后也是 * `/minio/zkxw/...?X-Amz-...`。微信 downloadFile 必须绝对 http(s)。 * * @param {string} url * @param {{ apiBaseUrl?: string, minioBaseUrl?: string }} [bases] */ export function toAbsoluteDownloadUrl(url, bases = {}) { const s = String(url || '').trim() if (!s) return '' if (/^https?:\/\//i.test(s)) return s if (s.startsWith('//')) return 'https:' + s const apiBase = String(bases.apiBaseUrl || '').trim() const minioBase = String(bases.minioBaseUrl || '').trim() const originFrom = (base) => { const m = String(base || '').match(/^(https?:\/\/[^/]+)/i) return m ? m[1] : '' } const origin = originFrom(apiBase) || originFrom(minioBase) if (s.startsWith('/')) { return origin ? origin + s : '' } // 相对 object key const minio = minioBase.replace(/\/$/, '') if (/^https?:\/\//i.test(minio)) { return minio + '/' + s.replace(/^\//, '') } if (minio.startsWith('/') && origin) { return origin + minio + '/' + s.replace(/^\//, '') } return '' } /** * 拼 MinIO 公网直链(无预签名)。 * 预签名经相对路径改写后微信 downloadFile 常 403;公网直链对本环境可 200。 */ export function buildDirectMinioUrl(fileUrl, bases = {}) { const raw = String(fileUrl || '').trim() if (!raw) return '' const minioBase = String(bases.minioBaseUrl || '').replace(/\/$/, '') const apiBase = String(bases.apiBaseUrl || '').trim() const originMatch = (apiBase || minioBase).match(/^(https?:\/\/[^/]+)/i) const origin = originMatch ? originMatch[1] : '' // 已是绝对 http(s):去掉 query(签名)后若可识别为同域 MinIO 则规范化,否则原样去 query if (/^https?:\/\//i.test(raw)) { const noQuery = raw.split('?')[0] try { const u = new URL(noQuery) const minioPath = minioBase.replace(/^https?:\/\/[^/]+/i, '') || '' if (minioPath && u.pathname.indexOf(minioPath) === 0) { return origin && minioPath.startsWith('/') ? origin + u.pathname : noQuery } } catch (_) { /* ignore */ } return noQuery } // 根相对:/minio/zkxw/key.pdf?... → 拼 origin if (raw.startsWith('/')) { const pathOnly = raw.split('?')[0] return origin ? origin + pathOnly : toAbsoluteDownloadUrl(pathOnly, bases) } // object key:去掉可能误带的 minio 路径前缀 let key = raw.replace(/^\//, '').split('?')[0] const minioPath = minioBase.replace(/^https?:\/\/[^/]+/i, '').replace(/^\//, '') if (minioPath && key.indexOf(minioPath + '/') === 0) { key = key.slice(minioPath.length + 1) } return toAbsoluteDownloadUrl(key, bases) }