feat(web): visible_when 增强 — 支持 AND/OR/NOT/括号 表达式
- 新增 utils/exprEvaluator.ts 表达式解析器 - 支持 eq/and/or/not 四种节点类型和括号分组 - 支持 == 和 != 比较运算符 - PluginCRUDPage 替换简单正则为 evaluateVisibleWhen
This commit is contained in:
@@ -41,29 +41,11 @@ import {
|
|||||||
type PluginPageSchema,
|
type PluginPageSchema,
|
||||||
type PluginSectionSchema,
|
type PluginSectionSchema,
|
||||||
} from '../api/plugins';
|
} from '../api/plugins';
|
||||||
|
import { evaluateVisibleWhen } from '../utils/exprEvaluator';
|
||||||
|
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
/** visible_when 表达式解析 */
|
|
||||||
function parseVisibleWhen(expression: string): { field: string; value: string } | null {
|
|
||||||
const regex = /^(\w+)\s*==\s*'([^']*)'$/;
|
|
||||||
const match = expression.trim().match(regex);
|
|
||||||
if (!match) return null;
|
|
||||||
return { field: match[1], value: match[2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 判断字段是否应该显示 */
|
|
||||||
function shouldShowField(
|
|
||||||
allValues: Record<string, unknown>,
|
|
||||||
visibleWhen: string | undefined,
|
|
||||||
): boolean {
|
|
||||||
if (!visibleWhen) return true;
|
|
||||||
const parsed = parseVisibleWhen(visibleWhen);
|
|
||||||
if (!parsed) return true;
|
|
||||||
return String(allValues[parsed.field] ?? '') === parsed.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginCRUDPageProps {
|
interface PluginCRUDPageProps {
|
||||||
/** 如果从 tabs/detail 页面内嵌使用,通过 props 传入配置 */
|
/** 如果从 tabs/detail 页面内嵌使用,通过 props 传入配置 */
|
||||||
pluginIdOverride?: string;
|
pluginIdOverride?: string;
|
||||||
@@ -608,7 +590,7 @@ export default function PluginCRUDPage({
|
|||||||
>
|
>
|
||||||
{fields.map((field) => {
|
{fields.map((field) => {
|
||||||
// visible_when 条件显示
|
// visible_when 条件显示
|
||||||
const visible = shouldShowField(formValues, field.visible_when);
|
const visible = evaluateVisibleWhen(field.visible_when, formValues);
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
138
apps/web/src/utils/exprEvaluator.ts
Normal file
138
apps/web/src/utils/exprEvaluator.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user