/** * 报销单附件文件名规范校验(2026-10-15) * * 业务口径(业务方确认): * 1. 必须包含中文(至少 1 个汉字)——"为方便查验附件,请用中文规范命名附件!" * 2. 去掉扩展名后的主干,按字符数计算不能超过 20 个字——"为方便查验附件,附件名字长度不能超过20个字!" * 3. 不能包含空格及 / \ : * ? " < > | # % 等特殊字符 * 4. 同一明细行内不能重名(比较时忽略扩展名大小写与首尾空格) * * 使用场景: * - 上传成功回调:不合格 → 弹提示且不加入列表; * - 保存/提交前:调用 checkDetails 统一校验,不合格则拦下不允许保存; * - 后端保存接口另有同口径的二次校验(防止绕过前端)。 */ /** 主干(去扩展名)最大字符数 */ export const MAX_MAIN_LENGTH = 20; /** 汉字 */ const RE_CHINESE = /[\u4e00-\u9fa5]/; /** 非法字符:空格(含各类空白)+ / \ : * ? " < > | # % */ const RE_ILLEGAL = /[\s/\\:*?"<>|#%]/; export const MSG_NO_CHINESE = "为方便查验附件,请用中文规范命名附件!"; export const MSG_TOO_LONG = "为方便查验附件,附件名字长度不能超过20个字!"; export const MSG_ILLEGAL = '为方便查验附件,附件名不能包含空格及 / \\ : * ? " < > | # % 等特殊字符!'; export const MSG_DUPLICATE = "为方便查验附件,同一行内不能有重名的附件!"; /** * 取主干(去掉最后一个扩展名) * @param {string} fileName 原始文件名 * @returns {string} 主干 */ export function mainName(fileName) { const s = (fileName || "").trim(); const i = s.lastIndexOf("."); return i > 0 ? s.substring(0, i) : s; } /** * 单个文件名校验 * @param {string} fileName 原始文件名 * @returns {{ok: boolean, msg: string, main: string}} */ export function checkFileName(fileName) { const name = (fileName || "").trim(); if (!name) { return { ok: false, msg: MSG_NO_CHINESE, main: "" }; } if (RE_ILLEGAL.test(name)) { return { ok: false, msg: MSG_ILLEGAL, main: mainName(name) }; } const main = mainName(name); if (!RE_CHINESE.test(main)) { return { ok: false, msg: MSG_NO_CHINESE, main }; } // 按字符数计(用扩展运算符按码点计数,中文/字母/数字/全角各计 1) if ([...main].length > MAX_MAIN_LENGTH) { return { ok: false, msg: MSG_TOO_LONG, main }; } return { ok: true, msg: "", main }; } /** 取附件项显示名(不同页面字段名可能是 name / fileName,取不到就返回空串表示"不判断") */ function itemName(item) { if (!item) return ""; if (typeof item === "string") return item; return item.name || item.fileName || ""; } /** * 一组附件校验(含同行重名) * @param {Array} list 附件数组,元素形如 {name, url} * @returns {{ok: boolean, msg: string, index: number}} */ export function checkFileList(list) { const arr = list || []; const seen = {}; for (let i = 0; i < arr.length; i++) { const name = itemName(arr[i]); if (!name) continue; // 取不到原文件名则不判断,避免误拦 const r = checkFileName(name); if (!r.ok) { return { ok: false, msg: r.msg, index: i }; } const key = name.trim().toLowerCase(); if (seen[key]) { return { ok: false, msg: MSG_DUPLICATE, index: i }; } seen[key] = true; } return { ok: true, msg: "", index: -1 }; } /** * 保存前校验整单明细(发票 + 附件) * @param {Array} details 明细行数组,行内含 invoicefilesList / annexfilesList(数组) * @returns {{ok: boolean, msg: string}} */ export function checkDetails(details) { const rows = details || []; for (let i = 0; i < rows.length; i++) { const groups = [ { label: "发票", list: rows[i] && rows[i].invoicefilesList }, { label: "附件", list: rows[i] && rows[i].annexfilesList } ]; for (let g = 0; g < groups.length; g++) { const arr = groups[g].list || []; const seen = {}; for (let j = 0; j < arr.length; j++) { const name = itemName(arr[j]); if (!name) continue; const r = checkFileName(name); if (!r.ok) { return { ok: false, msg: "附件文件名不符合要求,无法保存:第" + (i + 1) + "行第" + (j + 1) + "个(" + name + ")——" + r.msg }; } const key = name.trim().toLowerCase(); if (seen[key]) { return { ok: false, msg: "附件文件名不符合要求,无法保存:第" + (i + 1) + "行第" + (j + 1) + "个(" + name + ")——" + MSG_DUPLICATE }; } seen[key] = true; } } } return { ok: true, msg: "" }; } /** * 整单里所有附件(不分行)的扁平校验,供只有单个文件列表的页面使用 * @param {Array} lists 多个附件数组 * @returns {{ok: boolean, msg: string}} */ export function checkAllLists(lists) { const all = lists || []; for (let i = 0; i < all.length; i++) { const r = checkFileList(all[i]); if (!r.ok) { return { ok: false, msg: r.msg }; } } return { ok: true, msg: "" }; }