/**
|
* 报销单附件文件名规范校验(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: "" };
|
}
|