Ver código fonte

[Pref 0000] 模型管理页面代码优化及功能增强

houzekong 3 dias atrás
pai
commit
7e46be58ea

+ 146 - 0
src/views/system/algorithm/utils/config.ts

@@ -0,0 +1,146 @@
+import type { ElementGroup } from './types';
+import { buildOperandSet } from './parser';
+
+/** 模板选项 — 预设表达式 */
+export const DEFAULT_TEMPLATE_OPTIONS: { label: string; value: string }[] = [
+  { label: '压差预警1', value: '20<=sourcePressureChange && sourcePressureChange<=40' },
+  { label: '压差预警2', value: '40<sourcePressureChange && sourcePressureChange<=60' },
+  { label: '压差预警3', value: '60<sourcePressureChange && sourcePressureChange<=80' },
+  { label: '压差预警4', value: '80<sourcePressureChange' },
+  { label: '火灾预警1', value: '((coRzl<=20||coZf<=0.5*coAvg)&&coVal<=10)||o2Val<=5&&temperatureDifference<2&&co2Val<0.5' },
+  {
+    label: '火灾预警2',
+    value:
+      '((coRzl>20&&coRzl<=50)||(0.5*coAvg<coZf&&coZf<=coAvg)||(coVal>10&&coVal<=50)||(temperatureDifference>=2&&temperatureDifference<=4))&&co2Val<0.5',
+  },
+  {
+    label: '火灾预警3',
+    value:
+      '((coRzl>50&&coRzl<=100)||(coAvg<coZf&&coZf<=2*coAvg)||(coVal>=50&&coVal<=100)||(temperatureDifference>4&&temperatureDifference<=10))&&co2Val<0.5',
+  },
+  { label: '火灾预警4', value: 'coRzl>100||2*coAvg<coZf||coVal>100||temperatureDifference>10||(co2Val>=0.5&&co2Trend)' },
+  { label: '闭外火灾预警1', value: 'temperatureDifference<2||ch4Val<0.5||coVal<5||o2Val<20' },
+  { label: '闭外火灾预警2', value: 'temperatureDifference>=2&&temperatureDifference<=4||ch4Val>=0.5&&ch4Val<=1||coVal>=5&&coVal<10' },
+  {
+    label: '闭外火灾预警3',
+    value: 'temperatureDifference>4&&temperatureDifference<=10&&temperature>=26||ch4Val>=1&&ch4Val<1.5||coVal>=10&&coVal<24||c2h4Val>0&&c2h2Val==0',
+  },
+  { label: '闭外火灾预警4', value: 'temperatureDifference>10&&temperature>=30||ch4Val>=1.5&&ch4Val<2||coVal>=24||c2h2Val>0' },
+  {
+    label: 'OPEN',
+    value:
+      '(temperature<30||-3<temperature-fireAirTemperature&&temperature-fireAirTemperature<3)&&o2Val<5&&c2h4Val<0.001&&c2h2Val<0.001&&coVal<0.001&&(temperature<25||-3<temperature-fireWaterTemperature&&temperature-fireWaterTemperature<3)&&stableDays>30',
+  },
+];
+
+/** 元素库分组 — 定义表达式中可用的变量、运算符、逻辑符、括号和数值 */
+export const DEFAULT_ELEMENT_GROUPS: ElementGroup[] = [
+  {
+    key: 'gas',
+    header: '监测变量',
+    type: 'operand',
+    items: [
+      { type: 'operand', value: 'sourcePressureChange', label: '压差变化' },
+      { type: 'operand', value: 'coRzl', label: 'CO日增率' },
+      { type: 'operand', value: 'coZf', label: 'CO增幅' },
+      { type: 'operand', value: 'coAvg', label: 'CO平均值' },
+      { type: 'operand', value: 'coVal', label: 'CO值' },
+      { type: 'operand', value: 'co2Val', label: 'CO2值' },
+      { type: 'operand', value: 'co2Trend', label: 'CO2存在上升趋势' },
+      { type: 'operand', value: 'o2Val', label: 'O2值' },
+      { type: 'operand', value: 'ch4Val', label: 'CH4值' },
+      { type: 'operand', value: 'c2h4Val', label: 'C2H4值' },
+      { type: 'operand', value: 'c2h2Val', label: 'C2H2值' },
+      { type: 'operand', value: 'temperature', label: '温度' },
+      { type: 'operand', value: 'fireAirTemperature', label: '气温' },
+      { type: 'operand', value: 'fireWaterTemperature', label: '水温' },
+      { type: 'operand', value: 'temperatureDifference', label: '温差' },
+      { type: 'operand', value: 'stableDays', label: '稳定天数' },
+    ],
+    extra: [],
+  },
+  {
+    key: 'operator',
+    header: '计算符',
+    type: 'operator',
+    items: [
+      { type: 'operator', value: '>', label: '>' },
+      { type: 'operator', value: '>=', label: '≥' },
+      { type: 'operator', value: '<', label: '<' },
+      { type: 'operator', value: '<=', label: '≤' },
+      { type: 'operator', value: '==', label: '=' },
+      { type: 'operator', value: '+', label: '+' },
+      { type: 'operator', value: '-', label: '-' },
+      { type: 'operator', value: '*', label: '*' },
+      { type: 'operator', value: '/', label: '/' },
+    ],
+    extra: [],
+  },
+  {
+    key: 'logic',
+    header: '逻辑符',
+    type: 'logic',
+    items: [
+      { type: 'logic', value: '&&', label: '且' },
+      { type: 'logic', value: '||', label: '或' },
+    ],
+    extra: [],
+  },
+  {
+    key: 'paren',
+    header: '括号',
+    type: 'paren',
+    items: [
+      { type: 'leftParen', value: '(', label: '(' },
+      { type: 'rightParen', value: ')', label: ')' },
+    ],
+    extra: [],
+  },
+  {
+    key: 'number',
+    header: '数值',
+    type: 'number',
+    items: [
+      { type: 'number', value: '0', label: '0' },
+      { type: 'number', value: '10', label: '10' },
+      { type: 'number', value: '20', label: '20' },
+      { type: 'number', value: '30', label: '30' },
+      { type: 'number', value: '40', label: '40' },
+      { type: 'number', value: '50', label: '50' },
+      { type: 'number', value: '60', label: '60' },
+      { type: 'number', value: '70', label: '70' },
+      { type: 'number', value: '80', label: '80' },
+      { type: 'number', value: '90', label: '90' },
+      { type: 'number', value: '100', label: '100' },
+    ],
+    extra: [{ type: 'number', value: '0', label: '追加自定义值' }],
+  },
+];
+
+/** 显示名称映射 — 变量名/运算符 → 中文标签 */
+export const DEFAULT_DISPLAY_MAP: Record<string, string> = {
+  '&&': '且',
+  '||': '或',
+  '>=': '≥',
+  '<=': '≤',
+  '==': '=',
+  sourcePressureChange: '压差变化',
+  coRzl: 'CO日增率',
+  coZf: 'CO增幅',
+  coAvg: 'CO平均值',
+  coVal: 'CO值',
+  co2Val: 'CO2值',
+  co2Trend: 'CO2存在上升趋势',
+  o2Val: 'O2值',
+  ch4Val: 'CH4值',
+  c2h4Val: 'C2H4值',
+  c2h2Val: 'C2H2值',
+  temperature: '温度',
+  fireAirTemperature: '气温',
+  fireWaterTemperature: '水温',
+  temperatureDifference: '温差',
+  stableDays: '稳定天数',
+};
+
+/** 基于 DEFAULT_ELEMENT_GROUPS 预计算的操作数集合 */
+export const DEFAULT_OPERAND_SET: Set<string> = buildOperandSet(DEFAULT_ELEMENT_GROUPS);

+ 314 - 0
src/views/system/algorithm/utils/evaluator.ts

@@ -0,0 +1,314 @@
+import type { Item, EvaluateDetail, EvaluateResult } from './types';
+import { parseExpression } from './parser';
+
+/** 运算符 → 中文描述 */
+const OPERATOR_DISPLAY: Record<string, string> = {
+  '>': '大于',
+  '>=': '大于等于',
+  '<': '小于',
+  '<=': '小于等于',
+  '==': '等于',
+  '!=': '不等于',
+};
+
+/**
+ * 将表达式中的变量名替换为实际数值,返回新的 token 列表。
+ * 若提供了 displayMap,会用中文标签标记替换后的数字 token。
+ */
+function substituteValues(
+  tokens: Item[],
+  variableValues: Record<string, number>,
+  displayMap?: Record<string, string>,
+): Item[] {
+  return tokens.map((t) => {
+    if (t.type === 'operand' && t.value in variableValues) {
+      const label = displayMap?.[t.value] || t.label || t.value;
+      return { type: 'number', value: String(variableValues[t.value]), label };
+    }
+    return { ...t };
+  });
+}
+
+/**
+ * 简易算术求值 — 支持 + - * / 和括号(不含函数调用)
+ */
+class ArithmeticEvaluator {
+  private pos: number;
+  private tokens: Item[];
+
+  constructor(tokens: Item[]) {
+    this.tokens = tokens;
+    this.pos = 0;
+  }
+
+  private peek(): Item | null {
+    return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
+  }
+
+  private consume(): Item {
+    return this.tokens[this.pos++];
+  }
+
+  parseAddSub(): number {
+    let left = this.parseMulDiv();
+    while (this.peek() && (this.peek()!.value === '+' || this.peek()!.value === '-')) {
+      const op = this.consume().value;
+      const right = this.parseMulDiv();
+      if (op === '+') left += right;
+      else left -= right;
+    }
+    return left;
+  }
+
+  private parseMulDiv(): number {
+    let left = this.parseAtom();
+    while (this.peek() && (this.peek()!.value === '*' || this.peek()!.value === '/')) {
+      const op = this.consume().value;
+      const right = this.parseAtom();
+      if (op === '*') left *= right;
+      else left /= right;
+    }
+    return left;
+  }
+
+  private parseAtom(): number {
+    const token = this.peek();
+    if (!token) throw new Error('意外的表达式结尾');
+
+    if (token.type === 'number') {
+      this.consume();
+      return parseFloat(token.value);
+    }
+
+    if (token.type === 'leftParen') {
+      this.consume();
+      const val = this.parseAddSub();
+      if (this.peek()?.type !== 'rightParen') throw new Error('缺少右括号');
+      this.consume();
+      return val;
+    }
+
+    if (token.type === 'operator' && token.value === '-') {
+      this.consume();
+      return -this.parseAtom();
+    }
+
+    throw new Error(`无法识别的操作数: ${token.value}`);
+  }
+
+  evaluate(): number {
+    return this.parseAddSub();
+  }
+}
+
+function evaluateComparison(left: number, operator: string, right: number): boolean {
+  switch (operator) {
+    case '>':  return left > right;
+    case '>=': return left >= right;
+    case '<':  return left < right;
+    case '<=': return left <= right;
+    case '==': return left === right;
+    case '!=': return left !== right;
+    default:   throw new Error(`不支持的比较运算符: ${operator}`);
+  }
+}
+
+/**
+ * 比较运算求值器 — 同时记录每个原子条件的求值详情(含可读原因)。
+ */
+class ComparisonExtractor {
+  private tokens: Item[];
+  private pos: number;
+  private details: EvaluateDetail[];
+  private displayMap: Record<string, string>;
+
+  constructor(tokens: Item[], displayMap: Record<string, string>) {
+    this.tokens = tokens;
+    this.pos = 0;
+    this.details = [];
+    this.displayMap = displayMap;
+  }
+
+  private peek(): Item | null {
+    return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
+  }
+
+  private consume(): Item {
+    return this.tokens[this.pos++];
+  }
+
+  private hasMore(): boolean {
+    return this.pos < this.tokens.length;
+  }
+
+  parseOr(): boolean {
+    let left = this.parseAnd();
+    while (this.peek() && this.peek()!.type === 'logic' && this.peek()!.value === '||') {
+      this.consume();
+      const right = this.parseAnd();
+      left = left || right;
+    }
+    return left;
+  }
+
+  private parseAnd(): boolean {
+    let left = this.parseRelation();
+    while (this.peek() && this.peek()!.type === 'logic' && this.peek()!.value === '&&') {
+      this.consume();
+      const right = this.parseRelation();
+      left = left && right;
+    }
+    return left;
+  }
+
+  private parseRelation(): boolean {
+    const token = this.peek();
+    if (!token) throw new Error('意外的表达式结尾');
+
+    if (token.type === 'leftParen') {
+      this.consume();
+      const val = this.parseOr();
+      if (this.peek()?.type !== 'rightParen') throw new Error('缺少右括号');
+      this.consume();
+      return val;
+    }
+
+    return this.parseAtomicComparison();
+  }
+
+  /**
+   * 将 token 列表渲染为人类可读的文本
+   * - 数字 token 优先使用其 label(替换变量时设的中文名,如"压差")
+   * - 运算符、逻辑符、操作数均尝试通过 displayMap 转为中文(如 >= → ≥, && → 且)
+   * - 无映射的 token 回退显示原文
+   */
+  private renderTokens(tokens: Item[]): string {
+    return tokens
+      .map((t) => {
+        // 数字 token 优先使用 label(替换变量时设的中文名)
+        if (t.type === 'number' && t.label) return t.label;
+        // 运算符尝试走 displayMap(>= → ≥)
+        if (t.type === 'operator') return this.displayMap[t.value] || t.value;
+        // 逻辑符
+        if (t.type === 'logic') return this.displayMap[t.value] || t.value;
+        // 操作数
+        if (t.type === 'operand') return this.displayMap[t.value] || t.label || t.value;
+        return t.value;
+      })
+      .join(' ');
+  }
+
+  /** 解析并求值一个原子比较条件,同时记录可读的 condition 和失败原因 */
+  private parseAtomicComparison(): boolean {
+    const comparisonOps = new Set(['>', '>=', '<', '<=', '==', '!=']);
+
+    // 读取左侧算术表达式
+    const leftTokens: Item[] = [];
+    while (this.hasMore()) {
+      const t = this.peek()!;
+      if (t.type === 'logic' && (t.value === '&&' || t.value === '||')) break;
+      if (t.type === 'leftParen' || t.type === 'rightParen') break;
+      if (t.type === 'operator' && comparisonOps.has(t.value)) break;
+      leftTokens.push(this.consume());
+    }
+
+    if (!this.peek() || !(this.peek()!.type === 'operator' && comparisonOps.has(this.peek()!.value))) {
+      const leftVal = new ArithmeticEvaluator(leftTokens).evaluate();
+      return leftVal !== 0;
+    }
+
+    const opToken = this.consume();
+    const operator = opToken.value;
+
+    // 读取右侧算术表达式
+    const rightTokens: Item[] = [];
+    while (this.hasMore()) {
+      const t = this.peek()!;
+      if (t.type === 'logic' && (t.value === '&&' || t.value === '||')) break;
+      if (t.type === 'leftParen' || t.type === 'rightParen') break;
+      rightTokens.push(this.consume());
+    }
+
+    // 渲染为可读文本
+    const leftStr = this.renderTokens(leftTokens);
+    const rightStr = this.renderTokens(rightTokens);
+    const operatorStr = OPERATOR_DISPLAY[operator] || operator;
+
+    // 求值
+    const leftVal = new ArithmeticEvaluator(leftTokens).evaluate();
+    const rightVal = new ArithmeticEvaluator(rightTokens).evaluate();
+    const passes = evaluateComparison(leftVal, operator, rightVal);
+
+    const condition = `${leftStr} ${this.displayMap[operator] || operator} ${rightStr}`;
+
+    if (passes) {
+      this.details.push({ condition, passes: true });
+    } else {
+      this.details.push({
+        condition,
+        passes: false,
+        reason: `${leftStr}当前为 ${leftVal},不满足${operatorStr}${rightStr}的条件`,
+      });
+    }
+
+    return passes;
+  }
+
+  getDetails(): EvaluateDetail[] {
+    return this.details;
+  }
+}
+
+/**
+ * 根据传入的变量值,判断表达式是否能通过,返回每个条件的详细原因。
+ *
+ * @param expr          - 表达式字符串,例如 "sourcePressure > 500 && sourcePressure < 2000"
+ * @param variableValues - 变量名到数值的映射,例如 { sourcePressure: 1000, coVal: 5 }
+ * @param operandSet    - 已知操作数集合(由 buildOperandSet 生成)
+ * @param displayMap    - 可选的变量名/运算符 → 中文标签映射,用于生成可读原因
+ * @returns 求值结果,含每个条件的通过状态和可读原因
+ *
+ * @example
+ * // displayMap 传入 DEFAULT_DISPLAY_MAP 可使原因使用中文描述
+ * evaluateExpression("sourcePressure > 500", { sourcePressure: 100 }, operandSet, displayMap)
+ * // => {
+ * //   passes: false,
+ * //   details: [{ condition: "压差 > 500", passes: false, reason: "压差当前为 100,不满足大于 500 的条件" }],
+ * //   summary: "条件不满足(0/1): 压差当前为 100,不满足大于 500 的条件"
+ * // }
+ */
+export function evaluateExpression(
+  expr: string,
+  variableValues: Record<string, number>,
+  operandSet: Set<string>,
+  displayMap?: Record<string, string>,
+): EvaluateResult {
+  const tokens = parseExpression(expr, operandSet);
+  if (tokens.length === 0) {
+    return { passes: false, details: [], summary: '表达式为空或无法解析' };
+  }
+
+  const substituted = substituteValues(tokens, variableValues, displayMap);
+
+  try {
+    const extractor = new ComparisonExtractor(substituted, displayMap || {});
+    const passes = extractor.parseOr();
+    const details = extractor.getDetails();
+
+    const passedCount = details.filter((d) => d.passes).length;
+    const failedDetails = details.filter((d) => !d.passes);
+
+    let summary: string;
+    if (passes) {
+      summary = `全部 ${details.length} 个条件满足`;
+    } else {
+      const reasons = failedDetails.map((d) => d.reason || d.condition).join(';');
+      summary = `条件不满足(${passedCount}/${details.length}): ${reasons}`;
+    }
+
+    return { passes, details, summary };
+  } catch (e: unknown) {
+    const message = e instanceof Error ? e.message : String(e);
+    return { passes: false, details: [], summary: `表达式求值出错: ${message}` };
+  }
+}

+ 11 - 0
src/views/system/algorithm/utils/index.ts

@@ -0,0 +1,11 @@
+export type { Item, ItemType, ElementGroup, ValidationResult, EvaluateDetail, EvaluateResult } from './types';
+
+export { buildOperandSet, parseExpression, formatDisplay } from './parser';
+export { validateExpression, checkTopLevelMixed, addParenthesesForAnd } from './validator';
+export { evaluateExpression } from './evaluator';
+export {
+  DEFAULT_TEMPLATE_OPTIONS,
+  DEFAULT_ELEMENT_GROUPS,
+  DEFAULT_DISPLAY_MAP,
+  DEFAULT_OPERAND_SET,
+} from './config';

+ 66 - 0
src/views/system/algorithm/utils/parser.ts

@@ -0,0 +1,66 @@
+import type { Item, ElementGroup } from './types';
+
+/**
+ * 从元素库分组中构建操作数名集合(用于词法解析时识别操作数)
+ */
+export function buildOperandSet(elementGroups: ElementGroup[]): Set<string> {
+  const set = new Set<string>();
+  elementGroups.forEach((group) => {
+    if (group.type === 'operand') group.items.forEach((item) => set.add(item.value));
+  });
+  return set;
+}
+
+/**
+ * 将表达式字符串解析为 Item 数组(词法分析)
+ *
+ * 识别:操作数(变量名)、比较/算术运算符、逻辑运算符、括号、数字
+ */
+export function parseExpression(str: string, operandSet: Set<string>): Item[] {
+  if (!str?.trim()) return [];
+
+  const operandPattern = [...operandSet].map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
+  const regex = new RegExp(`(${operandPattern}|==|>=|<=|&&|\\|\\||-?\\d+(?:\\.\\d+)?|[>+\\-*/<()])`, 'g');
+  const rawTokens = str.match(regex);
+
+  if (!rawTokens) return [];
+
+  const items: Item[] = [];
+
+  for (const token of rawTokens) {
+    if (token === '(') {
+      items.push({ type: 'leftParen', value: token });
+      continue;
+    }
+    if (token === ')') {
+      items.push({ type: 'rightParen', value: token });
+      continue;
+    }
+    if (token === '&&' || token === '||') {
+      items.push({ type: 'logic', value: token });
+      continue;
+    }
+    if (['>', '>=', '<', '<=', '==', '+', '-', '*', '/'].includes(token)) {
+      items.push({ type: 'operator', value: token });
+      continue;
+    }
+    if (operandSet.has(token)) {
+      items.push({ type: 'operand', value: token });
+      continue;
+    }
+    if (/^-?\d+(?:\.\d+)?$/.test(token)) {
+      items.push({ type: 'number', value: token });
+      continue;
+    }
+  }
+
+  return items;
+}
+
+/**
+ * 将 Item 格式化为可读的展示文本
+ */
+export function formatDisplay(item: Item, displayMap: Record<string, string>): string {
+  if (item.type === 'number') return item.value;
+  return displayMap[item.value] || item.value;
+}

+ 44 - 0
src/views/system/algorithm/utils/types.ts

@@ -0,0 +1,44 @@
+/** 表达式中的元素类型 */
+export type ItemType = 'operand' | 'operator' | 'logic' | 'number' | 'leftParen' | 'rightParen';
+
+/** 表达式中的一个元素单元 */
+export interface Item {
+  type: ItemType;
+  value: string;
+  label?: string;
+}
+
+/** 元素库的分组 */
+export interface ElementGroup {
+  key: string;
+  header: string;
+  type: string;
+  items: Item[];
+  extra: Item[];
+}
+
+/** 表达式校验结果 */
+export interface ValidationResult {
+  valid: boolean;
+  message?: string;
+}
+
+/** 单个原子条件的求值详情 */
+export interface EvaluateDetail {
+  /** 条件的人类可读描述,如 "压差 > 500" */
+  condition: string;
+  /** 是否通过 */
+  passes: boolean;
+  /** 不通过时的原因说明,如 "压差当前为 100,不满足大于 500 的条件" */
+  reason?: string;
+}
+
+/** 表达式求值结果 */
+export interface EvaluateResult {
+  /** 整体是否通过 */
+  passes: boolean;
+  /** 每个原子条件的详细结果 */
+  details: EvaluateDetail[];
+  /** 人类可读的总结 */
+  summary: string;
+}

+ 136 - 0
src/views/system/algorithm/utils/validator.ts

@@ -0,0 +1,136 @@
+import type { Item, ValidationResult } from './types';
+import { parseExpression } from './parser';
+
+/**
+ * 校验表达式语法是否合法
+ *
+ * 检查项:空表达式、括号匹配、操作符/操作数相邻位置合理性
+ */
+export function validateExpression(expr: string, operandSet: Set<string>): ValidationResult {
+  if (!expr?.trim()) return { valid: false, message: '表达式不能为空' };
+
+  // 括号匹配
+  let count = 0;
+  for (const char of expr) {
+    if (char === '(') count++;
+    if (char === ')') count--;
+    if (count < 0) return { valid: false, message: '括号不匹配' };
+  }
+  if (count !== 0) return { valid: false, message: '括号不匹配' };
+
+  // 基本语法:检查操作符是否孤立(简单启发)
+  const tokens = parseExpression(expr, operandSet);
+  if (tokens.length === 0) return { valid: false, message: '无法解析表达式' };
+
+  for (let i = 0; i < tokens.length - 1; i++) {
+    const cur = tokens[i];
+    const next = tokens[i + 1];
+
+    if (cur.type === 'operand' && (next.type === 'number' || next.type === 'operand')) {
+      return { valid: false, message: `监测变量 ${cur.value} 位置可能不正确` };
+    }
+    if (cur.type === 'operator' && (next.type === 'operator' || next.type === 'logic' || next.type === 'rightParen')) {
+      return { valid: false, message: `运算符 ${cur.value} 后缺少操作数` };
+    }
+    if (cur.type === 'logic' && (next.type === 'logic' || next.type === 'rightParen')) {
+      return { valid: false, message: `逻辑符 ${cur.value} 位置可能不正确` };
+    }
+    if (cur.type === 'number' && (next.type === 'operand' || next.type === 'number')) {
+      return { valid: false, message: `数值 ${cur.value} 位置可能不正确` };
+    }
+  }
+
+  return { valid: true };
+}
+
+/**
+ * 判断 items 最外层(depth=0)是否同时存在 && 和 ||
+ */
+export function checkTopLevelMixed(items: Item[]): boolean {
+  let depth = 0;
+  let hasAnd = false;
+  let hasOr = false;
+  for (const item of items) {
+    if (item.type === 'leftParen') {
+      depth++;
+    } else if (item.type === 'rightParen') {
+      depth--;
+    } else if (item.type === 'logic' && depth === 0) {
+      if (item.value === '&&') hasAnd = true;
+      else if (item.value === '||') hasOr = true;
+    }
+  }
+  return hasAnd && hasOr;
+}
+
+/**
+ * 基于 items 为混合优先级表达式自动添加括号
+ *
+ * 仅在最外层 || 分隔的段中,对未加括号且包含 && 的段添加括号
+ */
+export function addParenthesesForAnd(items: Item[]): string {
+  if (!items.length) return items.map((i) => i.value).join(' ');
+
+  // 1. 计算每个 token 在整体表达式中的嵌套深度
+  const depths: number[] = [];
+  let depth = 0;
+  for (const t of items) {
+    if (t.type === 'leftParen') {
+      depths.push(depth);
+      depth++;
+    } else if (t.type === 'rightParen') {
+      depth--;
+      depths.push(depth);
+    } else {
+      depths.push(depth);
+    }
+  }
+
+  // 2. 按最外层 (depth === 0) 的 || 切分成段
+  const segments: Item[][] = [];
+  let current: Item[] = [];
+  for (let i = 0; i < items.length; i++) {
+    if (items[i].type === 'logic' && items[i].value === '||' && depths[i] === 0) {
+      if (current.length) segments.push(current);
+      current = [];
+    } else {
+      current.push(items[i]);
+    }
+  }
+  if (current.length) segments.push(current);
+
+  // 3. 辅助:判断段是否已被一整对括号包裹
+  const isEnclosed = (seg: Item[]): boolean => {
+    if (seg.length < 2) return false;
+    if (seg[0].type !== 'leftParen' || seg[seg.length - 1].type !== 'rightParen') return false;
+    let bal = 0;
+    for (let i = 1; i < seg.length - 1; i++) {
+      if (seg[i].type === 'leftParen') bal++;
+      else if (seg[i].type === 'rightParen') bal--;
+      if (bal < 0) return false;
+    }
+    return bal === 0;
+  };
+
+  // 4. 辅助:判断段内是否存在最外层 (深度为0) 的 &&
+  const hasTopLevelAnd = (seg: Item[]): boolean => {
+    let d = 0;
+    for (const t of seg) {
+      if (t.type === 'leftParen') d++;
+      else if (t.type === 'rightParen') d--;
+      else if (t.type === 'logic' && t.value === '&&' && d === 0) return true;
+    }
+    return false;
+  };
+
+  // 5. 重建表达式,对需要的段添加括号
+  return segments
+    .map((seg) => {
+      const str = seg.map((i) => i.value).join(' ');
+      if (!isEnclosed(seg) && hasTopLevelAnd(seg)) {
+        return `( ${str} )`;
+      }
+      return str;
+    })
+    .join(' || ');
+}