yxh
4 小时以前 c75027743ea7c3ecc2d0885f01d11d2ad78679a3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
 * 报销单附件文件名规范校验(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: "" };
}