公式编辑器组件代码

这个代码实现了一个公式编辑器组件,主要用于构建和编辑包含变量和函数的规则表达式。下面是对代码逻辑的详细分析:

组件结构
组件主要分为三部分:
在这里插入图片描述

变量池:显示可用变量并支持搜索过滤

规则池:提供运算符和函数按钮,以及文本编辑区域

底部操作按钮:提供清空、取消和确认功能

主要功能

  1. 变量管理
    通过getProperties方法从API获取变量列表

变量显示格式为identifieridentifieridentifier(如temptemptemp

支持通过搜索框过滤变量

点击变量按钮可将变量插入到规则文本中

  1. 规则编辑
    提供运算符和函数按钮(+、-、*、/、and、or等)

支持文本区域直接编辑

点击运算符/函数按钮可将其插入到规则文本中

函数插入时会自动添加括号(如int( ))

  1. 历史记录
    实现撤销(undo)和重做(redo)功能

使用history数组记录所有操作状态

currentIndex跟踪当前状态位置

每次修改规则内容都会记录到历史中

  1. 数据流
    通过props接收初始规则(initialRule)

通过emit事件confirm将编辑好的规则内容返回父组件

支持通过modelValue控制显示/隐藏

关键方法
insertVariable:

在光标位置插入变量

保持光标在变量后

记录操作历史

insertSymbol:

在光标位置插入运算符或函数

自动添加空格和括号

记录操作历史

onUndo/onRedo:

撤销/重做操作

更新当前索引和规则内容

onClearRule:

清空规则内容

弹出确认对话框

onConfirm:

将最终规则内容通过事件发送给父组件

样式设计
组件采用响应式布局,主要特点:

使用flex布局组织各部分

变量列表可滚动

运算符按钮加粗显示

各部分有边框和圆角

使用场景
这个组件适用于需要构建复杂表达式或公式的场景,如:

物联网设备数据处理规则

业务规则引擎

数据转换配置

条件判断逻辑构建

组件通过提供可视化的变量和函数选择,降低了编写复杂规则的难度。

在这里插入图片描述
公式编辑器组件代码分析

<template>
  <el-dialog
    v-model="visible"
    title="公式编辑器"
    width="900px"
    :close-on-click-modal="false"
    @close="onCancel"
    append-to-body
  >
    <div class="data-flow-container">
      <div class="formula-editor flex flex-column h-full">
        <!-- 变量池 -->
        <div class="variable-pool section-container">
          <div class="section-label">变量池:</div>
          <div class="search-bar">
            <el-input v-model="searchKeyword" placeholder="模糊搜索" clearable></el-input>
          </div>
          <div class="variable-list">
            <el-button
              v-for="item in filteredVariables"
              :key="item.key"
              type="primary"
              @click="insertVariable(item)"
              :title="item.title"
              size="small"
            >
              {{ item.label }}
            </el-button>
          </div>
        </div>

        <!-- 规则池 -->
        <div class="rule-pool section-container">
          <div class="section-label flex justify-between align-center">
            <span>规则池:</span>
            <div class="history-buttons">
              <el-button :disabled="!canUndo" size="small" @click="onUndo">
                <el-icon><ArrowLeft /></el-icon>上一步
              </el-button>
              <el-button :disabled="!canRedo" size="small" @click="onRedo">
                下一步<el-icon><ArrowRight /></el-icon>
              </el-button>
            </div>
          </div>
          <div class="symbols-bar">
            <el-button
              v-for="symbol in ruleGroups"
              :key="symbol.value"
              @click="insertSymbol(symbol.value)"
              size="small"
            >
              {{ symbol.label }}
            </el-button>
          </div>
          <el-input
            v-model="ruleContent"
            type="textarea"
            :rows="4"
            placeholder="请输入规则"
            ref="ruleInput"
          ></el-input>
        </div>

        <!-- 底部按钮 -->
        <div class="footer">
          <el-button type="warning" @click="onClearRule">清空规则池</el-button>
          <el-button @click="onCancel">取消</el-button>
          <el-button type="primary" @click="onConfirm">确定</el-button>
        </div>
      </div>
    </div>
  </el-dialog>
</template>

<script setup>
import { ref, watch, computed, onMounted, nextTick } from 'vue'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import FormulaEditorApi from '@/views/hp-equipment-sentinel/api/FormulaEditor'
import DeviceModelApi from '@/views/hp-equipment-sentinel/api/DeviceModel' // getProperties
import { deepClone } from '@/utils/index.js'

const visible = ref(false)
const emit = defineEmits(['update:modelValue', 'confirm'])
const name = ref('数据流转')
let formulaGroups = ref([])
let functionGroups = ref([])
let ruleGroups = ref([])
const formulaTest = ref([])
// 变量池数据
let variables = computed(() => {
  let variables = formulaGroups.value.map((item) => {
    // console.log('clg-on -> item',item)
    return {
      label: item.label,
      key: item.name,
      name: item.name,
      title: item.description
    }
  })
  return variables
})

const searchKeyword = ref('')
const ruleContent = ref('')
const ruleInput = ref(null)

// 过滤变量
const filteredVariables = computed(() => {
  //  console.log('clg-on -> variables.value', variables.value)
  // console.log('clg-on -> searchKeyword.value', !searchKeyword.value)
  if (!searchKeyword.value) return variables.value
  return variables.value.filter(
    (item) =>
      item.label.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
      item.key.toLowerCase().includes(searchKeyword.value.toLowerCase())
  )
})
// 获取变量池数据
const getProperties = async () => {
  let params = {
    pageNum: 1,
    pageSize: 100000,
    thing_model: props.thingModel
  }
  const res = await DeviceModelApi.getProperties({ ...params, ...props.searchParam })
  if (res.data.code !== 200) {
    ElMessage.error(res.data.msg)
    return
  }
  // console.log('clg-on -> getProperties', res)
  // 修改 getProperties 方法中的映射逻辑
  formulaGroups.value = res.data.data.list.map((item) => {
    return {
      label: item.name, // 修改为显示 name
      key: item.identifier,
      name: '$' + item.identifier + '$',
      title: item.description // 保留 description 作为提示信息
    }
  })
}
// 获取规则池数据
const getFormulaConfigList = async () => {
  const res = await FormulaEditorApi.getFormulaConfig()
  if (res.data.code !== 200) {
    ElMessage.error(res.data.msg)
    return
  }
  functionGroups.value = res.data.data['functionGroups'].map((item) => {
    item.functions.forEach((jtem) => {
      jtem.label = jtem.description
      jtem.key = jtem.name
    })
    return item
  })
  const functionGroupsTemp = deepClone(functionGroups.value)
  // 通常情况下 我们只使用 逻辑预算符
  if (props.isFunctionGroups) {
    // 如果我是基本表 我需要删除 聚合函数 和 窗口函数
    ruleGroups.value =  functionGroupsTemp.filter(item=>{
      return item.key !== 'agg_functions' && item.key !== 'window_functions'
    })
  } else {
    // 全量情况下,我们才使用 逻辑预算符 和 聚合预算符
    ruleGroups.value = [...functionGroups.value]
  }


  // 方法一:获取所有函数的params
  ruleGroups.value = ruleGroups.value.reduce((acc, category) => {
    const functionParams = category.functions.map((func) => ({
      name: func.name,
      title: func.description,
      label: func.description,
      key: func.name,
      value: func.name
      //   params: func.params
    }))
    return [...acc, ...functionParams]
  }, [])
  // console.log(typeof  props.isFunctionGroups , "props.isFunctionGroups",ruleGroups.value)
}
onMounted(() => {
  getFormulaConfigList()
})

// 父级接受
const props = defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },

  searchParam: {
    type: Object,
    default: () => ({})
  },
  isFunctionGroups: {
    type: Boolean,
    default: false
  },
  initialRule: {
    // 添加初始规则属性
    type: String,
    default: ''
  },
  thingModel: {
    type: String,
    default: ''
  }
})

// 监听 modelValue 变化
watch(
  () => props.modelValue,
  (newVal) => {
    visible.value = newVal

    if (visible.value) {
      getProperties()
    }

    if (newVal) {
      // 打开弹窗时清空规则
      ruleContent.value = props.initialRule || ''
      // 重置历史记录
      history.value = [ruleContent.value]
      currentIndex.value = 0
    }
  },
  { immediate: true },
  { deep: true }
)

const onCancel = () => {
  emit('update:modelValue', false)
}

// 符号列表
const symbols = [
  { label: ')', value: ')' },
  { label: '+', value: '+' },
  { label: '-', value: '-' },
  { label: '*', value: '*' },
  { label: '/', value: '/' },
  { label: '==', value: '==' },
  { label: '!=', value: '!=' },
  { label: '>', value: '>' },
  { label: '<', value: '<' },
  { label: '>=', value: '>=' },
  { label: '<=', value: '<=' },
  { label: 'and', value: 'and' },
  { label: 'or', value: 'or' },
  { label: 'int()', value: 'int()' },
  { label: 'float()', value: 'float()' },
  { label: 'string()', value: 'string()' }
]

// 操作历史记录
const history = ref([])
const currentIndex = ref(-1)

// 记录操作
const recordOperation = (content) => {
  // 如果当前不是在最新的状态,需要清除后面的历史
  if (currentIndex.value < history.value.length - 1) {
    history.value = history.value.slice(0, currentIndex.value + 1)
  }
  history.value.push(content)
  currentIndex.value = history.value.length - 1
}

// 是否可以撤销
const canUndo = computed(() => currentIndex.value > 0)

// 是否可以重做
const canRedo = computed(() => currentIndex.value < history.value.length - 1)

// 撤销操作
const onUndo = () => {
  if (canUndo.value) {
    currentIndex.value--
    ruleContent.value = history.value[currentIndex.value]
  }
}

// 重做操作
const onRedo = () => {
  if (canRedo.value) {
    currentIndex.value++
    ruleContent.value = history.value[currentIndex.value]
  }
}
const onConfirm = () => {
  // console.log('clg-on -> ruleContent.value',ruleContent.value)
  emit('confirm', ruleContent.value)
  visible.value = false
}
// 修改插入变量的方法
const insertVariable = (variable) => {
  const input = ruleInput.value.textarea
  const position = input.selectionStart
  const before = ruleContent.value.slice(0, position)
  const after = ruleContent.value.slice(position)
  const newContent = before + variable.key + after
  ruleContent.value = newContent
  recordOperation(newContent) // 记录操作

  nextTick(() => {
    input.setSelectionRange(position + variable.key.length, position + variable.key.length)
    input.focus()
  })
}

// 修改插入符号的方法
const insertSymbol = (symbol) => {
  console.log('clg-on -> symbol',symbol)
  symbol = ' ' + symbol + '(  '
  const input = ruleInput.value.textarea
  const position = input.selectionStart
  const before = ruleContent.value.slice(0, position)
  const after = ruleContent.value.slice(position)
  const newContent = before + symbol + after
  ruleContent.value = newContent + '  )'
  recordOperation(newContent) // 记录操作

  nextTick(() => {
    input.setSelectionRange(position + symbol.length, position + symbol.length)
    input.focus()
  })
}

// 修改清空规则池的方法
const onClearRule = () => {
  ElMessageBox.confirm('确认清空规则池内容吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  })
    .then(() => {
      ruleContent.value = ''
      recordOperation('') // 记录清空操作
      ElMessage.success('已清空规则池')
    })
    .catch(() => {
      // 取消清空
    })
}
</script>

<style scoped lang="scss">
.data-flow-container {
  background-color: #fff;
  height: 100%;
  //padding: 20px;
  border-radius: 4px;
  //box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.formula-editor {
  padding: 20px;
  gap: 16px;

  .section-container {
    border: 1px solid #ddd;
    padding: 16px;
    border-radius: 4px;
    margin-bottom: 16px;
  }

  .section-label {
    font-size: 14px;
    font-weight: 500;
    color: #333;
    margin-bottom: 16px;

    .history-buttons {
      display: flex;
      gap: 8px;

      :deep(.el-button) {
        display: flex;
        align-items: center;
        gap: 4px;
      }
    }
  }

  .variable-pool {
    .search-bar {
      margin-bottom: 16px;
    }

    .variable-list {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      max-height: 200px;
      overflow: scroll;
    }
  }

  .rule-pool {
    .symbols-bar {
      margin-bottom: 16px;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      font-weight: 800;
      :deep(.el-button) {
        font-weight: 800;
        color: #333;
      }
    }
  }

  .footer {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
  }
}

.gateway-manager {
  flex: 1;
  padding: 20px;
  background-color: #fff;
  overflow: hidden;

  .tips {
    font-size: 14px;
    font-weight: normal;
    line-height: 22px;
    letter-spacing: 0px;
    color: #606266;
  }

  .input-width {
    width: 220px;
  }
}

.table-container {
  flex: 1;
  overflow: hidden;
  margin: 16px 0;
}
</style>

// 父组件调用
<FormulaEditor
          v-model="formulaVisible"  // 开关
          :initial-rule="formData.condition"  // 回显
          :thing-model="String(formData.thing_model)" // id
          :isFunctionGroups="true" // 全量 非全量
          @submit="handleFormulaSubmit"
          @confirm="handleFormulaConfirm" 
        />
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值