Files
erp/apps/web/src/utils/exprEvaluator.ts
iven 022ac951c9 feat(web): visible_when 增强 — 支持 AND/OR/NOT/括号 表达式
- 新增 utils/exprEvaluator.ts 表达式解析器
- 支持 eq/and/or/not 四种节点类型和括号分组
- 支持 == 和 != 比较运算符
- PluginCRUDPage 替换简单正则为 evaluateVisibleWhen
2026-04-17 10:57:34 +08:00

139 lines
3.4 KiB
TypeScript

/**
* visible_when 表达式解析与求值
*
* 支持语法:
* field == 'value' 等值判断
* field != 'value' 不等判断
* expr1 AND expr2 逻辑与
* expr1 OR expr2 逻辑或
* NOT expr 逻辑非
* (expr) 括号分组
*/
interface ExprNode {
type: 'eq' | 'and' | 'or' | 'not';
field?: string;
value?: string;
left?: ExprNode;
right?: ExprNode;
operand?: ExprNode;
}
function tokenize(input: string): string[] {
const tokens: string[] = [];
let i = 0;
while (i < input.length) {
if (input[i] === ' ') {
i++;
continue;
}
if (input[i] === '(' || input[i] === ')') {
tokens.push(input[i]);
i++;
continue;
}
if (input[i] === "'") {
let j = i + 1;
while (j < input.length && input[j] !== "'") j++;
tokens.push(input.substring(i, j + 1));
i = j + 1;
continue;
}
if (input[i] === '=' && input[i + 1] === '=') {
tokens.push('==');
i += 2;
continue;
}
if (input[i] === '!' && input[i + 1] === '=') {
tokens.push('!=');
i += 2;
continue;
}
let j = i;
while (
j < input.length &&
!' ()\''.includes(input[j]) &&
!(input[j] === '=' && input[j + 1] === '=') &&
!(input[j] === '!' && input[j + 1] === '=')
) {
j++;
}
tokens.push(input.substring(i, j));
i = j;
}
return tokens;
}
function parseAtom(tokens: string[]): ExprNode | null {
const token = tokens.shift();
if (!token) return null;
if (token === '(') {
const expr = parseOr(tokens);
if (tokens[0] === ')') tokens.shift();
return expr;
}
if (token === 'NOT') {
const operand = parseAtom(tokens);
return { type: 'not', operand: operand || undefined };
}
const field = token;
const op = tokens.shift();
if (op !== '==' && op !== '!=') return null;
const rawValue = tokens.shift() || '';
const value = rawValue.replace(/^'(.*)'$/, '$1');
return { type: 'eq', field, value };
}
function parseAnd(tokens: string[]): ExprNode | null {
let left = parseAtom(tokens);
while (tokens[0] === 'AND') {
tokens.shift();
const right = parseAtom(tokens);
if (left && right) {
left = { type: 'and', left, right };
}
}
return left;
}
function parseOr(tokens: string[]): ExprNode | null {
let left = parseAnd(tokens);
while (tokens[0] === 'OR') {
tokens.shift();
const right = parseAnd(tokens);
if (left && right) {
left = { type: 'or', left, right };
}
}
return left;
}
export function parseExpr(input: string): ExprNode | null {
const tokens = tokenize(input);
return parseOr(tokens);
}
export function evaluateExpr(node: ExprNode, values: Record<string, unknown>): boolean {
switch (node.type) {
case 'eq':
return String(values[node.field!] ?? '') === node.value;
case 'and':
return evaluateExpr(node.left!, values) && evaluateExpr(node.right!, values);
case 'or':
return evaluateExpr(node.left!, values) || evaluateExpr(node.right!, values);
case 'not':
return !evaluateExpr(node.operand!, values);
default:
return false;
}
}
export function evaluateVisibleWhen(
expr: string | undefined,
values: Record<string, unknown>,
): boolean {
if (!expr) return true;
const ast = parseExpr(expr);
return ast ? evaluateExpr(ast, values) : true;
}