diff --git a/apps/web/src/pages/PluginCRUDPage.tsx b/apps/web/src/pages/PluginCRUDPage.tsx index e72c414..8f3fc58 100644 --- a/apps/web/src/pages/PluginCRUDPage.tsx +++ b/apps/web/src/pages/PluginCRUDPage.tsx @@ -41,29 +41,11 @@ import { type PluginPageSchema, type PluginSectionSchema, } from '../api/plugins'; +import { evaluateVisibleWhen } from '../utils/exprEvaluator'; const { Search } = 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, - 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 { /** 如果从 tabs/detail 页面内嵌使用,通过 props 传入配置 */ pluginIdOverride?: string; @@ -608,7 +590,7 @@ export default function PluginCRUDPage({ > {fields.map((field) => { // visible_when 条件显示 - const visible = shouldShowField(formValues, field.visible_when); + const visible = evaluateVisibleWhen(field.visible_when, formValues); if (!visible) return null; return ( diff --git a/apps/web/src/utils/exprEvaluator.ts b/apps/web/src/utils/exprEvaluator.ts new file mode 100644 index 0000000..45a451d --- /dev/null +++ b/apps/web/src/utils/exprEvaluator.ts @@ -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): 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, +): boolean { + if (!expr) return true; + const ast = parseExpr(expr); + return ast ? evaluateExpr(ast, values) : true; +}