空运成交后可在本群继续锁舱;调价同步舱位费用桶;出单按空运段骨架落费并保留陆运车型尺寸;导出文件名乱码修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-14 15:34:24 +08:00
co-authored by Cursor
parent 92bdf56a4d
commit cc8529f569
29 changed files with 1941 additions and 276 deletions
@@ -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;