/**
|
* 上下文取值
|
* <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] : [];
|
}
|