空运成交后可在本群继续锁舱;调价同步舱位费用桶;出单按空运段骨架落费并保留陆运车型尺寸;导出文件名乱码修复。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,9 +24,10 @@ export {
|
||||
|
||||
/**
|
||||
* 按当前筛选导出账号 Excel(Jeecg /sys/user/exportXls,与列表 QueryGenerator 同源)。
|
||||
* 文件名兜底用中文:Jeecg AutoPoi 的 Content-Disposition 常把「用户列表」编成乱码。
|
||||
*/
|
||||
export const exportAccounts = (params: Record<string, any>) =>
|
||||
downloadAuthGet(getExportUrl, params, '账号列表.xlsx');
|
||||
downloadAuthGet(getExportUrl, params, '账号列表.xls');
|
||||
|
||||
export type AccountRow = {
|
||||
id: string;
|
||||
|
||||
@@ -1,17 +1,87 @@
|
||||
/**
|
||||
* 带鉴权下载二进制文件(Excel 等)。
|
||||
* 为何:window.open 带 token 遇 JSON 错误页难提示;统一走 fetch + blob。
|
||||
* 文件名:优先 RFC5987 filename*=UTF-8'';Jeecg AutoPoi 常把 UTF-8 字节塞进 filename=,
|
||||
* 浏览器会显示成「ç…」乱码,需 latin1→utf8 还原,还原失败则用 fallback。
|
||||
*/
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
|
||||
export type AuthDownloadParams = Record<string, string | number | undefined | null>;
|
||||
|
||||
/**
|
||||
* 从 Content-Disposition 解析下载文件名。
|
||||
* @param cd Content-Disposition 头
|
||||
* @param fallbackName 解析失败或仍乱码时使用
|
||||
*/
|
||||
export function parseDownloadFilename(cd: string, fallbackName: string): string {
|
||||
const header = String(cd || '');
|
||||
if (!header) return fallbackName;
|
||||
|
||||
const star = /filename\*=(?:UTF-8''|utf-8'')([^;]+)/i.exec(header);
|
||||
if (star?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(star[1].trim().replace(/^"+|"+$/g, ''));
|
||||
} catch {
|
||||
/* 继续试 filename= */
|
||||
}
|
||||
}
|
||||
|
||||
const plain = /filename=\"?([^\";]+)\"?/i.exec(header);
|
||||
if (!plain?.[1]) return fallbackName;
|
||||
|
||||
let name = plain[1].trim();
|
||||
try {
|
||||
name = decodeURIComponent(name);
|
||||
} catch {
|
||||
/* 非百分号编码则原样 */
|
||||
}
|
||||
|
||||
const recovered = recoverUtf8Mojibake(name);
|
||||
if (recovered) {
|
||||
name = recovered;
|
||||
}
|
||||
|
||||
// 仍无汉字且 fallback 有中文 → 用业务侧给定名(账号导出「账号列表.xls」)
|
||||
if (looksLikeMojibake(name) && /[\u4e00-\u9fff]/.test(fallbackName)) {
|
||||
return fallbackName;
|
||||
}
|
||||
return name || fallbackName;
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 字节被当成 Latin-1/ISO-8859-1 字符时的还原。
|
||||
* 「用户列表」→「稅」这类乱码可恢复。
|
||||
*/
|
||||
function recoverUtf8Mojibake(s: string): string | null {
|
||||
try {
|
||||
const bytes = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const code = s.charCodeAt(i);
|
||||
if (code > 255) return null;
|
||||
bytes[i] = code;
|
||||
}
|
||||
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
||||
if (/[\u4e00-\u9fff]/.test(decoded)) {
|
||||
return decoded;
|
||||
}
|
||||
} catch {
|
||||
/* 不是可恢复的 UTF-8 乱码 */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 像乱码:无汉字,但有一串 Latin-1 高位字符 */
|
||||
function looksLikeMojibake(s: string): boolean {
|
||||
if (/[\u4e00-\u9fff]/.test(s)) return false;
|
||||
return /[\u00c0-\u00ff]{2,}/.test(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 下载附件并触发浏览器保存。
|
||||
* @param path 相对 API 路径,如 /inquiry/workOrder/view/export
|
||||
* @param params query 参数(空值跳过)
|
||||
* @param fallbackName 无 Content-Disposition 时的文件名
|
||||
* @param fallbackName 无可靠 Content-Disposition 时的文件名
|
||||
*/
|
||||
export async function downloadAuthGet(
|
||||
path: string,
|
||||
@@ -46,12 +116,32 @@ export async function downloadAuthGet(
|
||||
throw new Error(msg);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
let name = fallbackName;
|
||||
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 name = parseDownloadFilename(cd, fallbackName);
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7800/ingest/a69bf0d0-32e3-486e-80eb-ae1824ba17a1', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '489366' },
|
||||
body: JSON.stringify({
|
||||
sessionId: '489366',
|
||||
runId: 'post-fix',
|
||||
hypothesisId: 'FN1',
|
||||
location: 'inquiryAuthDownload.ts:downloadAuthGet',
|
||||
message: '导出文件名解析',
|
||||
data: {
|
||||
path,
|
||||
cd,
|
||||
fallbackName,
|
||||
resolvedName: name,
|
||||
hasStar: /filename\*=/i.test(cd),
|
||||
hasCjk: /[\u4e00-\u9fff]/.test(name),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
|
||||
const href = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
|
||||
Reference in New Issue
Block a user