/** * 报价模板 API(主账 /inquiry/quoteTemplate)。 * 列表/上传/映射/启停/样例预览;禁止浏览器直连对象存储填表以外的 TMS/LLM。 */ import { defHttp } from '/@/utils/http/axios'; import { useGlobSetting } from '/@/hooks/setting'; import { getToken } from '/@/utils/auth'; enum Api { list = '/inquiry/quoteTemplate/list', upload = '/inquiry/quoteTemplate/upload', version = '/inquiry/quoteTemplate/version', versions = '/inquiry/quoteTemplate/versions', mapping = '/inquiry/quoteTemplate/mapping', enable = '/inquiry/quoteTemplate/enable', disable = '/inquiry/quoteTemplate/disable', setDefault = '/inquiry/quoteTemplate/setDefault', editMeta = '/inquiry/quoteTemplate/editMeta', delete = '/inquiry/quoteTemplate/delete', previewSample = '/inquiry/quoteTemplate/previewSample', previewSheet = '/inquiry/quoteTemplate/previewSheet', previewSnapshot = '/inquiry/quoteTemplate/previewSnapshot', download = '/inquiry/quoteTemplate/download', reparse = '/inquiry/quoteTemplate/reparse', } export const getTemplateList = (params) => defHttp.get({ url: Api.list, params }); /** * 上传模板文件(新建或追加版本)。 * 禁止手写 Content-Type: multipart/form-data:缺 boundary 会被 Tomcat 直接拒收。 * FormData 交给浏览器自动带 boundary;解析可能超过默认 10 秒,单独放宽超时。 */ export const uploadTemplate = (data: FormData) => defHttp.post( { url: Api.upload, data, timeout: 120 * 1000, }, { errorMessageMode: 'message' }, ); export const getTemplateVersion = (versionId: string) => defHttp.get({ url: Api.version, params: { versionId } }); /** 上传原表 Excel 样式快照(格子原文 + 黄区/映射;旧接口保留) */ export const getTemplateSheetPreview = (versionId: string) => defHttp.get({ url: Api.previewSheet, params: { versionId }, timeout: 60 * 1000 }); /** * 原表截图预览(主账本机 LibreOffice 转 PNG/PDF)。 * 走 blob:带鉴权;首次转换可能较慢。 */ export async function fetchTemplateSnapshotBlob(versionId: string): Promise<{ blob: Blob; contentType: string }> { const glob = useGlobSetting(); const base = (glob.apiUrl || '').replace(/\/$/, ''); const url = `${base}${Api.previewSnapshot}?versionId=${encodeURIComponent(versionId)}`; const token = getToken() || ''; const controller = new AbortController(); const timer = window.setTimeout(() => controller.abort(), 150 * 1000); try { const res = await fetch(url, { method: 'GET', headers: { 'X-Access-Token': String(token), }, signal: controller.signal, }); const ctype = (res.headers.get('content-type') || '').toLowerCase(); if (!res.ok || ctype.includes('application/json')) { let msg = `预览失败 HTTP ${res.status}`; try { const j = await res.json(); if (j?.message) msg = String(j.message); } catch { /* ignore */ } throw new Error(msg); } const blob = await res.blob(); return { blob, contentType: ctype || blob.type || 'application/octet-stream' }; } finally { window.clearTimeout(timer); } } export const listTemplateVersions = (templateId: string) => defHttp.get({ url: Api.versions, params: { templateId } }); export const saveTemplateMapping = (versionId: string, mappingJson: string) => defHttp.put({ url: Api.mapping, data: { versionId, mappingJson } }); export const enableTemplate = (templateId: string, versionId: string) => defHttp.post({ url: Api.enable, params: { templateId, versionId } }); export const disableTemplate = (templateId: string) => defHttp.post({ url: Api.disable, params: { templateId } }); /** * 删除模板逻辑头及全部版本;不可恢复。 * 走 POST(与 Jeecg 惯例一致):部分网关/Nginx 对 DELETE 会直接 404。 * joinParamsToUrl:非 GET 时 axios 会把 params 挪进 JSON 体,必须拼到查询串给 @RequestParam。 */ export const deleteTemplate = (templateId: string) => defHttp.post( { url: Api.delete, params: { templateId } }, { joinParamsToUrl: true }, ); export const setTemplateDefault = (templateId: string, isDefault = true) => defHttp.post({ url: Api.setDefault, params: { templateId, isDefault } }); /** 只改名称/业务/关键词/默认标记,不换文件;走 JSON 体对接主账 @RequestBody */ export const editTemplateMeta = (data) => defHttp.put({ url: Api.editMeta, data }); /** * 对最新 Excel 版重新提交 AI 异步解析(已解析也可点;解析中跳过)。 */ export const reparseTemplate = (templateId: string) => defHttp.post( { url: Api.reparse, params: { id: templateId }, timeout: 60 * 1000 }, { joinParamsToUrl: true }, ); /** * 带鉴权下载样例预览 / 原文件。 * window.open+query token 遇 500 JSON 时浏览器只显示失败;改为 blob 下载并提示 message。 */ export async function downloadTemplateFile(kind: 'previewSample' | 'download', versionId: string, fileNameHint?: string) { const glob = useGlobSetting(); const base = (glob.apiUrl || '').replace(/\/$/, ''); const path = kind === 'previewSample' ? Api.previewSample : Api.download; const url = `${base}${path}?versionId=${encodeURIComponent(versionId)}`; const token = getToken() || ''; const res = await fetch(url, { method: 'GET', headers: { 'X-Access-Token': String(token), }, }); const ctype = (res.headers.get('content-type') || '').toLowerCase(); if (!res.ok || ctype.includes('application/json')) { let msg = `下载失败 HTTP ${res.status}`; try { const j = await res.json(); if (j?.message) msg = String(j.message); } catch { /* ignore */ } throw new Error(msg); } const blob = await res.blob(); let name = fileNameHint || (kind === 'previewSample' ? 'preview.xlsx' : 'template.bin'); const cd = res.headers.get('content-disposition') || ''; const m = /filename\*=UTF-8''([^;]+)|filename=\"?([^\";]+)\"?/i.exec(cd); if (m) { name = decodeURIComponent((m[1] || m[2] || name).trim()); } const href = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = href; a.download = name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(href); } /** 带 token 下载样例预览 / 原文件(新窗口,兼容旧调用) */ export function buildTemplateFileUrl(kind: 'previewSample' | 'download', versionId: string) { const glob = useGlobSetting(); const base = (glob.apiUrl || '').replace(/\/$/, ''); const path = kind === 'previewSample' ? Api.previewSample : Api.download; const token = getToken() || ''; return `${base}${path}?versionId=${encodeURIComponent(versionId)}&token=${encodeURIComponent(String(token))}`; }