19 小时以前 35722562e9e13f0504acc15b740d042ecb810199
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
/**
 * 上下文取值
 * <p>
 * 只做白名单式的属性导航,不解析、不执行任何代码:
 * 支持 a.b.c 与 a.b[0].c 两种写法,路径在数组上继续取属性时按元素逐个取值(pluck)。
 */
 
/** 路径片段:普通属性名或数组下标 */
const TOKEN_PATTERN = /[^.[\]]+/g;
 
/** 禁止访问的属性名,避免顺着原型链读到构造器 */
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
 
/** 把路径拆成片段:inspectionItems[0].actualValue → ['inspectionItems','0','actualValue'] */
export function parsePath(path: string): string[] {
  return path.match(TOKEN_PATTERN) ?? [];
}
 
function toIndex(token: string): number | undefined {
  if (!/^\d+$/.test(token)) {
    return undefined;
  }
  return Number(token);
}
 
function readTokens(source: unknown, tokens: string[], start: number): unknown {
  let current = source;
  for (let index = start; index < tokens.length; index += 1) {
    const token = tokens[index]!;
    if (current === null || current === undefined) {
      return undefined;
    }
    if (Array.isArray(current)) {
      const arrayIndex = toIndex(token);
      if (arrayIndex === undefined) {
        // 数组上继续取属性:逐元素取值,得到同长度的数组
        return current.map((item) => readTokens(item, tokens, index));
      }
      current = current[arrayIndex];
      continue;
    }
    if (typeof current === 'object') {
      if (BLOCKED_KEYS.has(token)) {
        return undefined;
      }
      current = (current as Record<string, unknown>)[token];
      continue;
    }
    // 标量后面还有路径片段,说明路径写错了
    return undefined;
  }
  return current;
}
 
/** 按路径取值,取不到返回 undefined */
export function readPath(source: unknown, path: string): unknown {
  return readTokens(source, parsePath(path), 0);
}
 
/** 展示用格式化:空值渲染为空字符串,布尔值转中文,日期只保留到秒 */
export function formatValue(value: unknown): string {
  if (value === null || value === undefined) {
    return '';
  }
  if (typeof value === 'boolean') {
    return value ? '是' : '否';
  }
  if (value instanceof Date) {
    const pad = (input: number) => String(input).padStart(2, '0');
    return (
      `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ` +
      `${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`
    );
  }
  if (Array.isArray(value)) {
    return value.map(formatValue).join('、');
  }
  return String(value);
}
 
/** 数值化:非数值返回 NaN,由调用方决定如何处理 */
export function toNumber(value: unknown): number {
  if (typeof value === 'number') {
    return value;
  }
  if (typeof value === 'boolean') {
    return value ? 1 : 0;
  }
  if (typeof value === 'string' && value.trim() !== '') {
    return Number(value);
  }
  return Number.NaN;
}
 
/** 把任意取值收敛成数值数组,供统计函数使用 */
export function toNumberArray(value: unknown): number[] {
  if (Array.isArray(value)) {
    return value
      .flat(Number.POSITIVE_INFINITY)
      .map((item) => toNumber(item))
      .filter((item) => Number.isFinite(item));
  }
  const single = toNumber(value);
  return Number.isFinite(single) ? [single] : [];
}