|
@@ -10,15 +10,6 @@
|
|
|
<a-tag v-else-if="item.type === 'logic'" class="expr-tag">|</a-tag>
|
|
<a-tag v-else-if="item.type === 'logic'" class="expr-tag">|</a-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</a-space>
|
|
</a-space>
|
|
|
- <!-- <div class="flex flex-wrap" :style="{ width: `calc(100% - ${isOperator ? '100' : '0'}px)` }" wrap>
|
|
|
|
|
- <div v-for="(group, id) in splitedItems" :key="`temp${id}`" class="w-50% flex justify-start items-center mb-10px" compact>
|
|
|
|
|
- <template v-for="(item, index) in group" :key="index">
|
|
|
|
|
- <a-input-number v-if="item.type === 'number'" v-model:value="item.value" style="width: 30px" placeholder="-" @blur="confirm" />
|
|
|
|
|
- <span v-else-if="item.type === 'operand'" class="w-90px text-center">{{ formatDisplay(item) }}</span>
|
|
|
|
|
- <span v-else class="p-5px">{{ formatDisplay(item) }}</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div> -->
|
|
|
|
|
<a-button v-if="isOperator" class="w-100px" type="primary" @click="enterAdvancedMode"> 高级编辑 </a-button>
|
|
<a-button v-if="isOperator" class="w-100px" type="primary" @click="enterAdvancedMode"> 高级编辑 </a-button>
|
|
|
</div>
|
|
</div>
|
|
|
<a-row v-else :gutter="8">
|
|
<a-row v-else :gutter="8">
|
|
@@ -28,7 +19,6 @@
|
|
|
<a-collapse class="relation-card" v-model:activeKey="activeKey" :bordered="false">
|
|
<a-collapse class="relation-card" v-model:activeKey="activeKey" :bordered="false">
|
|
|
<a-collapse-panel v-for="group in elementGroups" :key="group.key" :header="group.header">
|
|
<a-collapse-panel v-for="group in elementGroups" :key="group.key" :header="group.header">
|
|
|
<div class="chip-container">
|
|
<div class="chip-container">
|
|
|
- <!-- @dragstart="onDragStart($event, item)" -->
|
|
|
|
|
<div
|
|
<div
|
|
|
v-for="item in group.items"
|
|
v-for="item in group.items"
|
|
|
:key="item.value"
|
|
:key="item.value"
|
|
@@ -44,7 +34,6 @@
|
|
|
<div v-for="item in group.extra" :key="item.type" class="mt-8px">
|
|
<div v-for="item in group.extra" :key="item.type" class="mt-8px">
|
|
|
<a-input-number v-model:value="item.value" :defaultValue="0">
|
|
<a-input-number v-model:value="item.value" :defaultValue="0">
|
|
|
<template #addonAfter>
|
|
<template #addonAfter>
|
|
|
- <!-- @dragstart="onDragStart($event, item)" -->
|
|
|
|
|
<span style="cursor: pointer" draggable="true" @click="appendItem(item)"> 追加 </span>
|
|
<span style="cursor: pointer" draggable="true" @click="appendItem(item)"> 追加 </span>
|
|
|
</template>
|
|
</template>
|
|
|
</a-input-number>
|
|
</a-input-number>
|
|
@@ -72,7 +61,6 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<div class="relation-card">
|
|
<div class="relation-card">
|
|
|
- <!-- :class="{ 'drag-over': isDragOver }" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop="onDrop"-->
|
|
|
|
|
<div class="canvas-container">
|
|
<div class="canvas-container">
|
|
|
<a-empty v-if="!items.length" description="从左侧点击或拖拽元素" />
|
|
<a-empty v-if="!items.length" description="从左侧点击或拖拽元素" />
|
|
|
<div v-else class="expression-list">
|
|
<div v-else class="expression-list">
|
|
@@ -121,142 +109,33 @@
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
<script lang="ts" setup>
|
|
|
import { ref, computed, watch, h } from 'vue';
|
|
import { ref, computed, watch, h } from 'vue';
|
|
|
|
|
+ import { useRoute } from 'vue-router';
|
|
|
import { cloneDeep } from 'lodash-es';
|
|
import { cloneDeep } from 'lodash-es';
|
|
|
import { message, Modal } from 'ant-design-vue';
|
|
import { message, Modal } from 'ant-design-vue';
|
|
|
import { DeleteOutlined } from '@ant-design/icons-vue';
|
|
import { DeleteOutlined } from '@ant-design/icons-vue';
|
|
|
|
|
+ import {
|
|
|
|
|
+ parseExpression,
|
|
|
|
|
+ formatDisplay as _formatDisplay,
|
|
|
|
|
+ validateExpression,
|
|
|
|
|
+ checkTopLevelMixed,
|
|
|
|
|
+ addParenthesesForAnd,
|
|
|
|
|
+ DEFAULT_TEMPLATE_OPTIONS,
|
|
|
|
|
+ DEFAULT_ELEMENT_GROUPS,
|
|
|
|
|
+ DEFAULT_DISPLAY_MAP,
|
|
|
|
|
+ DEFAULT_OPERAND_SET,
|
|
|
|
|
+ } from '../utils';
|
|
|
|
|
+ import type { Item, ElementGroup } from '../utils';
|
|
|
|
|
+
|
|
|
|
|
+ // 常量配置(从当前路由 meta.compact 读取,默认 false)
|
|
|
|
|
+ const route = useRoute();
|
|
|
|
|
+ const isOperator = computed(() => !!route.meta?.compact);
|
|
|
|
|
+ const activeKey = ref<string[]>(['gas', 'operator', 'logic', 'number', 'paren']);
|
|
|
|
|
|
|
|
- // 类型定义
|
|
|
|
|
- interface Item {
|
|
|
|
|
- type: 'operand' | 'operator' | 'logic' | 'number' | 'leftParen' | 'rightParen';
|
|
|
|
|
- value: string;
|
|
|
|
|
- label?: string;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const templateOptions = ref(DEFAULT_TEMPLATE_OPTIONS);
|
|
|
|
|
|
|
|
- interface ElementGroup {
|
|
|
|
|
- key: string;
|
|
|
|
|
- header: string;
|
|
|
|
|
- type: string;
|
|
|
|
|
- items: Item[];
|
|
|
|
|
- extra: Item[];
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const elementGroups: ElementGroup[] = DEFAULT_ELEMENT_GROUPS;
|
|
|
|
|
|
|
|
- // 常量配置
|
|
|
|
|
- const isOperator = ref(false);
|
|
|
|
|
- const activeKey = ref<string[]>(['gas', 'operator', 'logic', 'number', 'paren']);
|
|
|
|
|
-
|
|
|
|
|
- const templateOptions = ref([
|
|
|
|
|
- { 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',
|
|
|
|
|
- },
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- const elementGroups: 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: '追加自定义值' }],
|
|
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ const operandSet = DEFAULT_OPERAND_SET;
|
|
|
|
|
|
|
|
const tagColors: Record<string, string> = {
|
|
const tagColors: Record<string, string> = {
|
|
|
operand: 'blue',
|
|
operand: 'blue',
|
|
@@ -267,29 +146,10 @@
|
|
|
rightParen: 'default',
|
|
rightParen: 'default',
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const displayMap: 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: '稳定天数',
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ const displayMap: Record<string, string> = DEFAULT_DISPLAY_MAP;
|
|
|
|
|
+
|
|
|
|
|
+ // 模板调用的 formatDisplay,闭包捕获 displayMap
|
|
|
|
|
+ const formatDisplay = (item: Item) => _formatDisplay(item, displayMap);
|
|
|
|
|
|
|
|
// Props 和 Emits
|
|
// Props 和 Emits
|
|
|
const props = defineProps<{ modelValue: string }>();
|
|
const props = defineProps<{ modelValue: string }>();
|
|
@@ -309,78 +169,10 @@
|
|
|
|
|
|
|
|
// Computed
|
|
// Computed
|
|
|
const expressionString = computed(() => items.value.map((i) => i.value).join(' '));
|
|
const expressionString = computed(() => items.value.map((i) => i.value).join(' '));
|
|
|
- // const splitedItems = computed(() => {
|
|
|
|
|
- // return items.value.reduce(
|
|
|
|
|
- // (arr: Item[][], ele) => {
|
|
|
|
|
- // console.log('debug rr', arr, ele);
|
|
|
|
|
- // if (ele.type === 'logic') {
|
|
|
|
|
- // arr.push([]);
|
|
|
|
|
- // }
|
|
|
|
|
- // if (['operand', 'operator', 'number'].includes(ele.type)) {
|
|
|
|
|
- // last(arr)!.push(ele);
|
|
|
|
|
- // }
|
|
|
|
|
- // return arr;
|
|
|
|
|
- // },
|
|
|
|
|
- // [[]]
|
|
|
|
|
- // );
|
|
|
|
|
- // });
|
|
|
|
|
-
|
|
|
|
|
- // 工具函数
|
|
|
|
|
- const buildOperandSet = (): Set<string> => {
|
|
|
|
|
- const set = new Set<string>();
|
|
|
|
|
- elementGroups.forEach((group) => {
|
|
|
|
|
- if (group.type === 'operand') group.items.forEach((item) => set.add(item.value));
|
|
|
|
|
- });
|
|
|
|
|
- return set;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const formatDisplay = (item: Item): string => {
|
|
|
|
|
- if (item.type === 'number') return item.value;
|
|
|
|
|
- return displayMap[item.value] || item.value;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const parseExpression = (str: string): Item[] => {
|
|
|
|
|
- if (!str?.trim()) return [];
|
|
|
|
|
- const operandSet = buildOperandSet();
|
|
|
|
|
- const operandPattern = [...operandSet].map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
|
|
|
- const regex = new RegExp(`(${operandPattern}|==|>=|<=|&&|\\|\\||-?\\d+(?:\\.\\d+)?|[>+\\-*/<()])`, 'g');
|
|
|
|
|
- const rawTokens = str.match(regex) || [];
|
|
|
|
|
- const tokens = rawTokens.map((t) => t.trim()).filter((t) => t !== '');
|
|
|
|
|
-
|
|
|
|
|
- const items: Item[] = [];
|
|
|
|
|
- for (const token of tokens) {
|
|
|
|
|
- if (token === '(') {
|
|
|
|
|
- items.push({ type: 'leftParen', value: '(' });
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- if (token === ')') {
|
|
|
|
|
- items.push({ type: 'rightParen', value: ')' });
|
|
|
|
|
- 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;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // 初始化:将表达式字符串同步到画布与内部状态
|
|
|
|
|
|
|
+ /** 将表达式字符串解析并同步到画布 */
|
|
|
const initializeFromExpression = (expr: string): void => {
|
|
const initializeFromExpression = (expr: string): void => {
|
|
|
- const parsed = parseExpression(expr);
|
|
|
|
|
|
|
+ const parsed = parseExpression(expr, operandSet);
|
|
|
items.value = parsed.length ? parsed : [];
|
|
items.value = parsed.length ? parsed : [];
|
|
|
// 同步简易模式输入框(如果处于简易模式,显示最新表达式)
|
|
// 同步简易模式输入框(如果处于简易模式,显示最新表达式)
|
|
|
inputString.value = expr;
|
|
inputString.value = expr;
|
|
@@ -388,196 +180,34 @@
|
|
|
errorMessage.value = '';
|
|
errorMessage.value = '';
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 画布操作(追加、删除、清空、插入括号)
|
|
|
|
|
|
|
+ /** 追加一个元素到画布末尾 */
|
|
|
const appendItem = (item: Item): void => {
|
|
const appendItem = (item: Item): void => {
|
|
|
items.value.push(cloneDeep(item));
|
|
items.value.push(cloneDeep(item));
|
|
|
syncAfterChange();
|
|
syncAfterChange();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /** 移除指定索引处的元素 */
|
|
|
const removeItem = (index: number): void => {
|
|
const removeItem = (index: number): void => {
|
|
|
items.value.splice(index, 1);
|
|
items.value.splice(index, 1);
|
|
|
syncAfterChange();
|
|
syncAfterChange();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /** 清空画布所有元素 */
|
|
|
const clearCanvas = (): void => {
|
|
const clearCanvas = (): void => {
|
|
|
items.value = [];
|
|
items.value = [];
|
|
|
syncAfterChange();
|
|
syncAfterChange();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 画布变更后同步:更新简易模式输入框,并向上通知(但不提交)
|
|
|
|
|
|
|
+ /** 画布变更后同步到简易模式输入框(不触发提交) */
|
|
|
const syncAfterChange = (): void => {
|
|
const syncAfterChange = (): void => {
|
|
|
const expr = expressionString.value;
|
|
const expr = expressionString.value;
|
|
|
inputString.value = expr;
|
|
inputString.value = expr;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 拖拽逻辑
|
|
|
|
|
- // const isDragOver = ref<boolean>(false);
|
|
|
|
|
- // const onDragStart = (e: DragEvent, item: Item): void => {
|
|
|
|
|
- // e.dataTransfer?.setData('text/plain', JSON.stringify(item));
|
|
|
|
|
- // };
|
|
|
|
|
- // const onDragOver = (e: DragEvent): void => {
|
|
|
|
|
- // e.preventDefault();
|
|
|
|
|
- // isDragOver.value = true;
|
|
|
|
|
- // };
|
|
|
|
|
- // const onDragLeave = (): void => {
|
|
|
|
|
- // isDragOver.value = false;
|
|
|
|
|
- // };
|
|
|
|
|
- // const onDrop = (e: DragEvent): void => {
|
|
|
|
|
- // e.preventDefault();
|
|
|
|
|
- // isDragOver.value = false;
|
|
|
|
|
- // try {
|
|
|
|
|
- // const data = e.dataTransfer?.getData('text/plain');
|
|
|
|
|
- // if (!data) return;
|
|
|
|
|
- // const item: Item = JSON.parse(data);
|
|
|
|
|
- // let droppedItem = cloneDeep(item);
|
|
|
|
|
- // const container = e.currentTarget as HTMLElement;
|
|
|
|
|
- // const tags = [...container.querySelectorAll('.expr-tag')] as HTMLElement[];
|
|
|
|
|
- // let index = tags.length;
|
|
|
|
|
- // const mouseX = e.clientX;
|
|
|
|
|
- // for (let i = 0; i < tags.length; i++) {
|
|
|
|
|
- // const rect = tags[i].getBoundingClientRect();
|
|
|
|
|
- // if (mouseX < rect.left + rect.width / 2) {
|
|
|
|
|
- // index = i;
|
|
|
|
|
- // break;
|
|
|
|
|
- // }
|
|
|
|
|
- // }
|
|
|
|
|
- // items.value.splice(index, 0, droppedItem);
|
|
|
|
|
- // syncAfterChange();
|
|
|
|
|
- // } catch {
|
|
|
|
|
- // message.error('拖拽失败');
|
|
|
|
|
- // }
|
|
|
|
|
- // };
|
|
|
|
|
-
|
|
|
|
|
- // 校验与优先级处理
|
|
|
|
|
- const validateExpression = (expr: string): { valid: boolean; message?: string } => {
|
|
|
|
|
- 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);
|
|
|
|
|
- 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 为混合优先级表达式自动添加括号
|
|
|
|
|
- * 仅在最外层 || 分隔的段中,对未加括号且包含 && 的段添加括号
|
|
|
|
|
- */
|
|
|
|
|
- const addParenthesesForAnd = (_expr: string): string => {
|
|
|
|
|
- const tokens = items.value;
|
|
|
|
|
- if (!tokens.length) return _expr;
|
|
|
|
|
-
|
|
|
|
|
- // 1. 计算每个 token 在整体表达式中的嵌套深度
|
|
|
|
|
- const depths: number[] = [];
|
|
|
|
|
- let depth = 0;
|
|
|
|
|
- for (const t of tokens) {
|
|
|
|
|
- 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 < tokens.length; i++) {
|
|
|
|
|
- if (tokens[i].type === 'logic' && tokens[i].value === '||' && depths[i] === 0) {
|
|
|
|
|
- if (current.length) segments.push(current);
|
|
|
|
|
- current = [];
|
|
|
|
|
- } else {
|
|
|
|
|
- current.push(tokens[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(' || ');
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 判断 items 最外层(depth=0)是否同时存在 && 和 ||
|
|
|
|
|
- */
|
|
|
|
|
- const checkTopLevelMixed = (): boolean => {
|
|
|
|
|
- let depth = 0;
|
|
|
|
|
- let hasAnd = false;
|
|
|
|
|
- let hasOr = false;
|
|
|
|
|
- for (const item of items.value) {
|
|
|
|
|
- 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;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // 提交逻辑
|
|
|
|
|
|
|
+ /** 提交逻辑 */
|
|
|
const confirm = async (): Promise<void> => {
|
|
const confirm = async (): Promise<void> => {
|
|
|
const expr = expressionString.value;
|
|
const expr = expressionString.value;
|
|
|
- const validation = validateExpression(expr);
|
|
|
|
|
|
|
+ const validation = validateExpression(expr, operandSet);
|
|
|
if (!validation.valid) {
|
|
if (!validation.valid) {
|
|
|
errorMessage.value = validation.message || '表达式不合法';
|
|
errorMessage.value = validation.message || '表达式不合法';
|
|
|
return;
|
|
return;
|
|
@@ -585,13 +215,13 @@
|
|
|
errorMessage.value = '';
|
|
errorMessage.value = '';
|
|
|
|
|
|
|
|
// 基于 items 判断最外层是否存在未加括号的 && 与 || 混合
|
|
// 基于 items 判断最外层是否存在未加括号的 && 与 || 混合
|
|
|
- const hasMixed = checkTopLevelMixed();
|
|
|
|
|
|
|
+ const hasMixed = checkTopLevelMixed(items.value);
|
|
|
let finalExpr = expr;
|
|
let finalExpr = expr;
|
|
|
|
|
|
|
|
if (hasMixed) {
|
|
if (hasMixed) {
|
|
|
await new Promise<void>((resolve) => {
|
|
await new Promise<void>((resolve) => {
|
|
|
const originalExpr = expr;
|
|
const originalExpr = expr;
|
|
|
- const suggestedExpr = addParenthesesForAnd(expr);
|
|
|
|
|
|
|
+ const suggestedExpr = addParenthesesForAnd(items.value);
|
|
|
|
|
|
|
|
Modal.confirm({
|
|
Modal.confirm({
|
|
|
title: '运算符优先级提醒',
|
|
title: '运算符优先级提醒',
|
|
@@ -612,7 +242,7 @@
|
|
|
onOk: () => {
|
|
onOk: () => {
|
|
|
finalExpr = suggestedExpr;
|
|
finalExpr = suggestedExpr;
|
|
|
// 同步到画布(仅当用户选择自动添加时才更新画布)
|
|
// 同步到画布(仅当用户选择自动添加时才更新画布)
|
|
|
- const parsed = parseExpression(finalExpr);
|
|
|
|
|
|
|
+ const parsed = parseExpression(finalExpr, operandSet);
|
|
|
if (parsed.length) items.value = parsed;
|
|
if (parsed.length) items.value = parsed;
|
|
|
resolve();
|
|
resolve();
|
|
|
},
|
|
},
|
|
@@ -629,7 +259,7 @@
|
|
|
message.success('已更新');
|
|
message.success('已更新');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 重置
|
|
|
|
|
|
|
+ /** 重置整个组件状态 */
|
|
|
const reset = (): void => {
|
|
const reset = (): void => {
|
|
|
items.value = [];
|
|
items.value = [];
|
|
|
inputString.value = '';
|
|
inputString.value = '';
|
|
@@ -639,14 +269,14 @@
|
|
|
emit('change', '');
|
|
emit('change', '');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 进入高级模式
|
|
|
|
|
|
|
+ /** 切换至高级编辑模式,将外部传入的表达式加载到画布 */
|
|
|
const enterAdvancedMode = (): void => {
|
|
const enterAdvancedMode = (): void => {
|
|
|
// 将当前 modelValue 初始化到画布
|
|
// 将当前 modelValue 初始化到画布
|
|
|
initializeFromExpression(props.modelValue);
|
|
initializeFromExpression(props.modelValue);
|
|
|
compact.value = false;
|
|
compact.value = false;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 返回简易模式
|
|
|
|
|
|
|
+ /** 从高级模式返回简易模式,如有变更则提示用户确认 */
|
|
|
const backToCompact = async (): Promise<void> => {
|
|
const backToCompact = async (): Promise<void> => {
|
|
|
const currentExpr = expressionString.value;
|
|
const currentExpr = expressionString.value;
|
|
|
if (currentExpr === props.modelValue) {
|
|
if (currentExpr === props.modelValue) {
|
|
@@ -670,6 +300,7 @@
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /** 应用选中的快捷模板 */
|
|
|
const applyTemplate = (): void => {
|
|
const applyTemplate = (): void => {
|
|
|
if (!selectedTemplate.value) return;
|
|
if (!selectedTemplate.value) return;
|
|
|
initializeFromExpression(selectedTemplate.value);
|
|
initializeFromExpression(selectedTemplate.value);
|
|
@@ -677,10 +308,11 @@
|
|
|
message.success('模板已应用');
|
|
message.success('模板已应用');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ /** 应用手动输入的表达式字符串 */
|
|
|
const applyManualInput = (): void => {
|
|
const applyManualInput = (): void => {
|
|
|
const expr = manualInput.value.trim();
|
|
const expr = manualInput.value.trim();
|
|
|
if (!expr) return;
|
|
if (!expr) return;
|
|
|
- const validation = validateExpression(expr);
|
|
|
|
|
|
|
+ const validation = validateExpression(expr, operandSet);
|
|
|
if (!validation.valid) {
|
|
if (!validation.valid) {
|
|
|
errorMessage.value = validation.message || '表达式不合法';
|
|
errorMessage.value = validation.message || '表达式不合法';
|
|
|
return;
|
|
return;
|
|
@@ -695,29 +327,19 @@
|
|
|
(newVal) => {
|
|
(newVal) => {
|
|
|
if (newVal !== undefined) {
|
|
if (newVal !== undefined) {
|
|
|
inputString.value = newVal;
|
|
inputString.value = newVal;
|
|
|
- // 如果当前处于高级模式,也同步画布
|
|
|
|
|
- // if (!compact.value) {
|
|
|
|
|
- const parsed = parseExpression(newVal);
|
|
|
|
|
|
|
+ const parsed = parseExpression(newVal, operandSet);
|
|
|
items.value = parsed.length ? parsed : [];
|
|
items.value = parsed.length ? parsed : [];
|
|
|
- // }
|
|
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
{ immediate: true }
|
|
{ immediate: true }
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 暴露校验方法供表单 rules 使用
|
|
|
|
|
- defineExpose({ validate: () => validateExpression(props.modelValue) });
|
|
|
|
|
|
|
+ /** 对外暴露校验方法,供表单 rules 使用 */
|
|
|
|
|
+ defineExpose({ validate: () => validateExpression(props.modelValue, operandSet) });
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
<style scoped lang="less">
|
|
|
.relation-builder {
|
|
.relation-builder {
|
|
|
- // :deep(.ant-card) {
|
|
|
|
|
- // height: 100%;
|
|
|
|
|
- // }
|
|
|
|
|
- // :deep(.ant-card-body) {
|
|
|
|
|
- // height: calc(100% - 57px);
|
|
|
|
|
- // overflow-y: auto;
|
|
|
|
|
- // }
|
|
|
|
|
.relation-card {
|
|
.relation-card {
|
|
|
height: 680px;
|
|
height: 680px;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|