实现方式一
1、效果图
2、实现代码
组件弹框实现 样式自己调整 公式的数字与汉字元素、符号 建立元素表 动态获取
完整代码(calculate.vue)
<template><div id="formulaPage"><divref="formulaView"class="formulaView"contentEditable="false"@click="recordPosition"@keyup="editEnter($event)"@copy="copy($event)"@paste="paste($event)"/><div class="infomationContent"><div class="infomationContent-leftFlexbox"><div class="tab"><ul style="width: 200px"><liv-for="(v, i) in Num":key="i"class="numberClass":style="{ background: v.backgroundColor }"@click="addItem($event, 2, v, false)">{{ v.displayValue }}</li></ul></div><div class="tab"><ul style="width: 500px"><liv-for="(v, i) in customType":key="i"class="typeClass":style="{ background: v.backgroundColor }":class="{ noclick: v.isFlag }"@click="addItem($event, 2, v, v.isFlag)">{{ v.displayValue }}</li></ul></div><div class="tab"><ul style="width: 200px"><liv-for="(v, i) in Symbol":key="i"class="symbolClass":style="{ background: v.backgroundColor }"@click="addItem($event, 2, v, false)">{{ v.displayValue }}</li></ul></div></div><div class="infomationContent-rightBtnbox"><div class="imgboxinfo" style="margin-bottom: 32px"><img src="@/assets/images/rolback.png" alt="" style="width: 40px; height: 40px" @click="deleteHumerText" /></div><div class="imgboxinfo"><img src="@/assets/images/calSave.png" alt="" style="width: 40px; height: 40px" @click="onSaveformulaEvent" /></div></div></div><!-- <div class="footerComtent"><el-button type="primary" style="width: 200px; height: 30px" @click="onSaveformulaEvent">保存</el-button></div> --><!-- <button @click="parsingFormula('(长+宽+高)*2')">反向解析公式</button> --><!-- <button @click="deleteHumerText()">删除</button> --></div></template><script>export default {name: 'FormulaPage',props: {content: { type: String, default: '' },recoverdate: {type: Array,default: () => []},domflag: {type: String,default: ''}},data() {return {// 权限分类 元素分类不可用集合btnsRuleArray: {offer: ['thickness', 'nailmouth', 'bellsocket', 'gramweight'], // 报价weight: ['thickness', 'unitprice'], // 重量areavolume: ['gramweight', 'unitprice'] // 体积、面积、纸宽、纸长},// 公式字符串formulaStr: '',// 公式编辑器最后光标位置formulaLastRange: null,Num: [{"displayValue":"1","saveValue":"1"},{"displayValue":"2","saveValue":"2"},{"displayValue":"3","saveValue":"3"},{"displayValue":"4","saveValue":"4"},{"displayValue":"5","saveValue":"5"},{"displayValue":"6","saveValue":"6"},{"displayValue":"7","saveValue":"7"},{"displayValue":"8","saveValue":"8"},{"displayValue":"9","saveValue":"9"},{"displayValue":"0","saveValue":"0"},{"displayValue":".","saveValue":"."}],Symbol: [{"displayValue":"+","saveValue":"+"},{"displayValue":"-","saveValue":"-"},{"displayValue":"x","saveValue":"*"},{"displayValue":"÷","saveValue":"/"},{"displayValue":"(","saveValue":"("},{"displayValue":")","saveValue":")"},{"displayValue":"{","saveValue":"{"},{"displayValue":"}","saveValue":"}"},],customType: [{"displayValue":"上报产量","saveValue":"report_production"},{"displayValue":"实际产量","saveValue":"reality_production"},{"displayValue":"红矿","saveValue":"red_ore"},{"displayValue":"青堆","saveValue":"black_stack"},{"displayValue":"红堆","saveValue":"red_stack"},{"displayValue":"煤存","saveValue":"coal_storage"},{"displayValue":"槽存","saveValue":"slot_storage"},{"displayValue":"土耗","saveValue":"soil_used"},{"displayValue":"土存","saveValue":"soil_stock"},{"displayValue":"煤耗","saveValue":"coal_used"},{"displayValue":"产量","saveValue":"production"},{"displayValue":"用电量","saveValue":"meter_used"}],backFormulaStrArray: [] // 按键值存储编辑器数据}},created() {this.backFormulaStrArray = []if (this.recoverdate) {this.backFormulaStrArray = this.recoverdatethis.$nextTick(function () {this.parsingFormulaCustom(this.recoverdate)})}// this.getList()},methods: {/** 查询所有公式元素 */getList() {listElement({ pageNum: 1, pageSize: 50 }).then(response => {response.rows.forEach(item => {item.isFlag = falseif (this.domflag) {const newLegth = this.btnsRuleArray[this.domflag].filter(e => e == item.saveValue)if (newLegth.length > 0) {item.isFlag = true}}})const numberDate = response.rows.filter(e => {return e.elementType == 'number'}).sort((a, b) => {return a.order - b.order})const formulaElementDate = response.rows.filter(e => {return e.elementType == 'formulaElement'}).sort((a, b) => {return a.order - b.order})const symbolDate = response.rows.filter(e => {return e.elementType == 'symbol'}).sort((a, b) => {return a.order - b.order})this.Num = numberDatethis.Symbol = symbolDatethis.customType = formulaElementDate})},// 删除操作---从最后删除deleteHumerText() {if (this.backFormulaStrArray.length > 0) {this.backFormulaStrArray.splice(-1, 1)while (this.$refs.formulaView.firstChild) {this.$refs.formulaView.removeChild(this.$refs.formulaView.firstChild)}this.backFormulaStrArray.forEach(item => {const fd = document.createDocumentFragment()const empty = document.createTextNode(' ')const formulaEl = document.createTextNode(' ' + item.displayValue + ' ')fd.appendChild(empty)fd.appendChild(formulaEl)fd.appendChild(empty)this.$refs.formulaView.appendChild(fd)// 创建新的光标对象var range = document.createRange()// 光标对象的范围界定range.selectNodeContents(formulaEl)// 光标位置定位range.setStart(formulaEl, formulaEl.data.length - 1)// 使光标开始和光标结束重叠range.collapse(true)// 清除选定对象的所有光标对象window.getSelection().removeAllRanges()// 插入新的光标对象window.getSelection().addRange(range)// 保存新光标this.recordPosition()})}},// 保存数据 回调父组件onSaveformulaEvent() {const strParams = this.backFormulaStrArray.map(e => e.saveValue).join('')const strParamsValue = this.backFormulaStrArray.map(e => e.displayValue).join('')const newbackFormulaStrArray = []this.backFormulaStrArray.forEach(item => {const obj = {displayValue: item.displayValue,elementType: item.elementType,saveValue: item.saveValue}newbackFormulaStrArray.push(obj)})this.parsingFormula('')this.$emit('onChangeSuccess', strParams, strParamsValue, newbackFormulaStrArray)this.backFormulaStrArray = []console.log(strParams)console.log(strParamsValue)console.log(newbackFormulaStrArray)// const ifLegal1 = [...strParamsValue].reduce((a, i) => (i === '(' ? a + 1 : a - 1), 0)},isValidate(str) {const inArr = []const arr = str.split('')for (const s of arr) {if (s === '{' || s === '[' || s === '(') {// 入栈inArr.push(s)}if (s === '}' || s === ']' || s === ')') {let tempswitch (s) {case '}':temp = '{'breakcase ']':temp = '['breakcase ')':temp = '('break}// 出栈const out = inArr.pop()if (temp !== out) {return false}}}return true},// 获取公式getFormula: function () {var nodes = this.$refs.formulaView.childNodesvar str = ''for (let i = 0; i < nodes.length; i++) {var el = nodes[i]if (el.nodeName == 'SPAN') {// console.log(el);str += '#' + el.innerHTML.trim() + '#'} else {// console.log(el.data);str += el.data ? el.data.trim() : ''}}// console.log(str);this.formulaStr = str},// 点选时记录光标位置recordPosition: function () {// 保存最后光标点this.formulaLastRange = window.getSelection().getRangeAt(0)},// 添加字段 type 1 字段 2 公式addItem: function (e, type, itemRows, isusable) {if (isusable) {e.preventDefault()return false}if (this.backFormulaStrArray.length > 0) {if (itemRows.elementType == 'formulaElement' || itemRows.elementType == 'symbol') {// 检验连续相同两个元素不能重复 符号/元素if (this.backFormulaStrArray[this.backFormulaStrArray.length - 1].displayValue == itemRows.displayValue) {e.preventDefault()return false}// 检验不同元素连续两个不能是用一个类型if (this.backFormulaStrArray[this.backFormulaStrArray.length - 1].elementType == 'formulaElement' &&itemRows.elementType == 'formulaElement') {e.preventDefault()return false}if (this.backFormulaStrArray[this.backFormulaStrArray.length - 1].elementType == 'number' &&itemRows.elementType == 'formulaElement') {e.preventDefault()return false}}if (itemRows.elementType == 'number' && itemRows.saveValue == '.') {if (this.backFormulaStrArray[this.backFormulaStrArray.length - 1].displayValue == itemRows.displayValue) {e.preventDefault()return false}}// this.backFormulaStrArray.forEach(item => {// console.log(item.displayValue,itemRows.displayValue)// const reg = RegExp(`(${item.displayValue})\1`, 'g'))// console.log(strParamsValue.match(reg))// })}this.backFormulaStrArray.push(itemRows)const isValiStr = this.backFormulaStrArray.map(e => e.displayValue).join('')if (!this.isValidate(isValiStr)) {this.backFormulaStrArray.splice(-1, 1)e.preventDefault()return false}// 当前元素所有子节点var nodes = this.$refs.formulaView.childNodes// 当前子元素偏移量var offset = this.formulaLastRange && this.formulaLastRange.startOffset// 当前光标后的元素var nextEl = this.formulaLastRange && this.formulaLastRange.endContainer// 创建节点片段var fd = document.createDocumentFragment()// 创建字段节点 空白间隔节点 公式节点var spanEl = document.createElement('span')spanEl.setAttribute('contentEditable', false)// 标识为新添加元素 用于定位光标spanEl.setAttribute('new-el', true)spanEl.innerHTML = e.target.innerHTMLvar empty = document.createTextNode(' ')var formulaEl = document.createTextNode(' ' + e.target.innerHTML + ' ')// 区分文本节点替换 还是父节点插入if (nextEl && nextEl.className != 'formulaView') {// 获取文本节点内容var content = nextEl.data// 添加前段文本fd.appendChild(document.createTextNode(content.substr(0, offset) + ' '))fd.appendChild(type == 1 ? spanEl : formulaEl)// 添加后段文本fd.appendChild(document.createTextNode(' ' + content.substr(offset)))// 替换节点this.$refs.formulaView.replaceChild(fd, nextEl)} else {// 添加前段文本fd.appendChild(empty)fd.appendChild(type == 1 ? spanEl : formulaEl)fd.appendChild(empty)// 如果有偏移元素且不是最后节点 中间插入节点 最后添加节点if (nodes.length && nodes.length > offset) {this.$refs.formulaView.insertBefore(fd, nextEl && nextEl.className != 'formulaView' ? nextEl : nodes[offset])} else {this.$refs.formulaView.appendChild(fd)}}// 遍历光标偏移数据 删除标志var elOffSet = 0for (let i = 0; i < nodes.length; i++) {const el = nodes[i]// console.log(el,el.nodeName == 'SPAN'&&el.getAttribute('new-el'));if (el.nodeName == 'SPAN' && el.getAttribute('new-el')) {elOffSet = iel.removeAttribute('new-el')}}// 创建新的光标对象var range = document.createRange()// 光标对象的范围界定range.selectNodeContents(type == 1 ? this.$refs.formulaView : formulaEl)// 光标位置定位range.setStart(type == 1 ? this.$refs.formulaView : formulaEl,type == 1 ? elOffSet + 1 : formulaEl.data.length - 1)// 使光标开始和光标结束重叠range.collapse(true)// 清除选定对象的所有光标对象window.getSelection().removeAllRanges()// 插入新的光标对象window.getSelection().addRange(range)// 保存新光标this.recordPosition()},// 复制copy: function (e) {// 选中复制内容e.preventDefault()//var selContent = document.getSelection().toString().split('\n')[0]// 替换选中内容e.clipboardData.setData('text/plain', selContent)},// 输入回车editEnter: function (e) {e.preventDefault()if (e.keyCode == 13) {// 获取标签内容 并把多个换行替换成1个var content = this.$refs.formulaView.innerHTML.replace(/(<div><br><\/div>){2,2}/g, '<div><br></div>')// 记录是否第一行回车var divCount = this.$refs.formulaView.querySelectorAll('div')// var tE = this.$refs.formulaView.querySelect('div');// console.log(this.$refs.formulaView.childNodes);// console.log(this.$refs.formulaView.querySelectorAll("div"));// 获取当前元素内所有子节点var childNodes = this.$refs.formulaView.childNodes// 记录当前光标子节点位置var rangeIndex = 0for (let i = 0; i < childNodes.length; i++) {var one = childNodes[i]if (one.nodeName == 'DIV') {rangeIndex = i}}// 如果有替换则进行光标复位if (divCount.length >= 1) {// 替换回车插入的div标签content = content.replace(/<div>|<\/div>/g, function (word) {// console.log(word);if (word == '<div>') {// 如果是第一行不在替换brreturn divCount.length > 1 ? ' ' : ' <br>'} else if (word == '</div>') {return ' '}})// 更新替换内容,光标复位this.$refs.formulaView.innerHTML = content// 创建新的光标对象var range = document.createRange()// 光标对象的范围界定为新建的表情节点range.selectNodeContents(this.$refs.formulaView)// 光标位置定位在表情节点的最大长度range.setStart(this.$refs.formulaView, rangeIndex + (divCount.length > 1 ? 0 : 1))// 使光标开始和光标结束重叠range.collapse(true)// 清除选定对象的所有光标对象window.getSelection().removeAllRanges()// 插入新的光标对象window.getSelection().addRange(range)}}// 保存最后光标点this.formulaLastRange = window.getSelection().getRangeAt(0)},// 获取粘贴事件paste: function (e) {e.preventDefault()// var txt=e.clipboardData.getData();// console.log(e, e.clipboardData.getData());return ''},// 公式反向解析parsingFormula: function (formulaStr) {// 渲染视口var view = this.$refs.formulaView// 反向解析公式var str = formulaStr.replace(/#(.+?)#/g, function (word, $1) {// console.log(word,$1);return "<span contentEditable='false'>" + $1 + '</span>'})// console.log(str,fd.innerHTML);view.innerHTML = str// this.$refs.formulaView.appendChild(fd);// 创建新的光标对象var range = document.createRange()// 光标对象的范围界定为新建的表情节点range.selectNodeContents(view)// 光标位置定位在表情节点的最大长度range.setStart(view, view.childNodes.length)// 使光标开始和光标结束重叠range.collapse(true)// 清除选定对象的所有光标对象window.getSelection().removeAllRanges()// 插入新的光标对象window.getSelection().addRange(range)// 保存新光标this.recordPosition()},parsingFormulaCustom(arrayDatelist) {arrayDatelist.forEach(item => {// 当前元素所有子节点const nodes = this.$refs.formulaView.childNodes// 当前子元素偏移量const offset = this.formulaLastRange && this.formulaLastRange.startOffset// 当前光标后的元素const nextEl = this.formulaLastRange && this.formulaLastRange.endContainerconst fd = document.createDocumentFragment()const empty = document.createTextNode(' ')const formulaEl = document.createTextNode(' ' + item.displayValue + ' ')if (nextEl && nextEl.className != 'formulaView') {var content = nextEl.datafd.appendChild(document.createTextNode(content.substr(0, offset) + ' '))fd.appendChild(formulaEl)fd.appendChild(document.createTextNode(' ' + content.substr(offset)))this.$refs.formulaView.replaceChild(fd, nextEl)} else {fd.appendChild(empty)fd.appendChild(formulaEl)fd.appendChild(empty)// 如果有偏移元素且不是最后节点 中间插入节点 最后添加节点if (nodes.length && nodes.length > offset) {this.$refs.formulaView.insertBefore(fd,nextEl && nextEl.className != 'formulaView' ? nextEl : nodes[offset])} else {this.$refs.formulaView.appendChild(fd)}}// 创建新的光标对象var range = document.createRange()// 光标对象的范围界定range.selectNodeContents(formulaEl)// 光标位置定位range.setStart(formulaEl, formulaEl.data.length - 1)// 使光标开始和光标结束重叠range.collapse(true)// 清除选定对象的所有光标对象window.getSelection().removeAllRanges()// 插入新的光标对象window.getSelection().addRange(range)// 保存新光标this.recordPosition()})}}}</script><style lang="scss">#formulaPage {width: 100%;height: 100%;> .formulaView {margin-bottom: 20px;min-height: 130px;width: 100%;padding: 5px;border: 5px solid rgb(198, 226, 255);resize: both;overflow: auto;line-height: 25px;font-size: 20px;span {user-select: none;display: inline-block;/* // margin: 0 3px; */height: 20px;line-height: 20px;/* // letter-spacing: 2px; */border-radius: 3px;white-space: nowrap;&:first-child {margin-left: 0;}}}.footerComtent {margin-top: 20px;display: flex;align-items: center;justify-content: center;}.infomationContent {display: flex;&-leftFlexbox {flex: 1;display: flex;> .tab {flex: 1;> ul {margin: 0;padding: 0;display: flex;flex-wrap: wrap;/* // justify-content: space-between; */&:after {content: '';display: table;clear: both;}> li {/* // margin-right: 20px; */margin: 5px 10px;width: 65px;text-align: center;float: left;padding: 0 10px;height: 30px;line-height: 30px;border-radius: 5px;font-weight: bold;border: 1px solid #fff;list-style-type: none;cursor: pointer;-moz-user-select: none; /* 火狐 */-webkit-user-select: none; /* 谷歌、Safari */-ms-user-select: none; /* IE10+ */user-select: none;user-drag: none;box-shadow: 0px 0px 2px 2px rgba(55, 114, 203, 0.2), /*下面深蓝色立体阴影*/ 0px 0px 6px 1px #4379d0,/*内部暗色阴影*/ 0 -15px 2px 2px rgba(55, 114, 203, 0.1) inset;color: #333333;}> li:hover {color: #fff;/* // color: #409eff; */border-color: #c6e2ff;background-color: #ecf5ff;}> li:active {color: #3a8ee6;border-color: #3a8ee6;outline: none;}.numberClass {width: 40px;}.typeClass {width: 100px}.symbolClass {}.noclick {cursor: not-allowed !important;background: #bcc0c4 !important;color: #e4eaf1 !important;border: none !important;}}}}&-rightBtnbox {width: 50px;padding-top: 10px;box-sizing: border-box;.imgboxinfo {width: 40px;height: 40px;img {cursor: pointer;}}}}}</style>
在父组件中引用即可
实现方式二
1、效果图
2、实现代码
<!-- 配置计算公式 --><el-dialog title="计算公式配置" :visible.sync="dragOpen" append-to-body :close-on-click-modal="false" width="1050px"><el-form label-width="90px" size="medium"><el-row><el-col :span="24"><el-form-item label="调度指标:"><div style="border: 1px solid #c2ddf8;padding: 5px;"><draggable class="dragArea list-group" :list="indexLists" :group="{ name: 'people', pull: 'clone', put: false }" animation="300"><el-tag v-for="tag in indexLists" :key="tag.id" effect="dark" class="fomula-tag" style="margin-right: 5px;" type="success">{{tag.dictLabel}}</el-tag></draggable></div></el-form-item></el-col><el-col :span="24"></el-col></el-row><el-row><el-col :span="24"><el-form-item label="运算符号:"><div style="border: 1px solid #c2ddf8;padding: 5px;"><draggable class="dragArea list-group" :list="operatorOptions" :group="{ name: 'people', pull: 'clone', put: false }" animation="300"><el-tag v-for="tag in operatorOptions" :key="tag.id" effect="dark" class="fomula-tag" @click="tagClick(tag)" style="margin-right: 5px;background-color: #7c9096;border: none;width: 40px;text-align: center">{{tag.dictLabel}}</el-tag></draggable></div></el-form-item></el-col><el-col :span="24"></el-col></el-row><el-row><el-col :span="24"><el-form-item label="算数符号:"><div style="border: 1px solid #c2ddf8;padding: 5px;"><draggable class="dragArea list-group" :list="nums" :group="{ name: 'people', pull: 'clone', put: false }" animation="300"><el-tag v-for="tag in nums" :key="tag.id" effect="dark" class="fomula-tag" @click="tagClick(tag)" style="margin-right: 5px;background-color: #2bcdff;border: none;width: 40px;text-align: center">{{tag.dictLabel}}</el-tag></draggable></div></el-form-item></el-col><el-col :span="24"></el-col></el-row><el-row><el-col :span="24"><el-form-item label="结果集:"><div style="border: 5px solid #c2ddf8;padding: 5px;position: relative;"><draggable class="list-group" :list="resultR" group="people" animation="300" style="height:100px;" @change="log"><el-tag v-for="tag in resultR" :key="tag.id" effect="dark" @click="tagClick(tag)" closable @close="handleClose(tag)" style="margin-right: 5px;">{{tag.dictLabel}}</el-tag></draggable><el-popover placement="left" title="公式:" trigger="hover" :content="previewFormula"><i class="el-icon-view show-fomula" slot="reference"></i></el-popover></div></el-form-item></el-col><el-col :span="24"><div class="explain">{{explain}}</div></el-col></el-row></el-form><div slot="footer" class="dialog-footer"><el-button type="primary" @click="submitDrag" v-if="this.confDisabled">保 存</el-button><el-button @click="cancelDrag">取 消</el-button></div></el-dialog>
js方法
import draggable from 'vuedraggable'
watch: {'resultR': {handler: function () {this.refreshFormula();}},},methods: {log() {this.refreshFormula();},refreshFormula() {this.previewFormula = "";if (this.resultR !== undefined && this.resultR !== null && this.resultR !== []) {for (let i = 0; i < this.resultR.length; i++) {this.previewFormula += this.resultR[i].dictLabel;}}console.log("this.previewFormula")console.log(this.previewFormula)},
handleDrag(item) {indexList({ "factory": item.factory }).then(resp => {this.indexLists = resp.data;this.resultFormula = this.form.indicatorFormulaif (typeof this.resultFormula !== 'undefined' && this.resultFormula.length > 0) {const strArr = this.resultFormula.split(',')this.resultR = [];for (let i = 0; i < strArr.length; i++) {let indexBoolean = falsefor (let j = 0; j < this.indexLists.length; j++) {if (this.indexLists[j].dictValue === strArr[i]) {this.resultR.push(this.indexLists[j]);indexBoolean = true}}if (!indexBoolean) {let opBoolean = falsefor (let z = 0; z < this.operatorOptions.length; z++) {if (this.operatorOptions[z].dictValue === strArr[i]) {this.resultR.push(this.operatorOptions[z]);opBoolean = true}}if (!opBoolean) {for (let c = 0; c < this.nums.length; c++) {if (this.nums[c].dictValue === strArr[i]) {this.resultR.push(this.nums[c]);}}}}}}this.dragOpen = true;})},submitDrag() {console.log(this.resultR)this.resultFormula = ''for (let i = 0; i < this.resultR.length; i++) {this.resultFormula = this.resultFormula + this.resultR[i].dictValue + ","}this.$set(this.form, 'indicatorFormula', this.resultFormula.substring(0, this.resultFormula.length - 1));this.dragOpen = false;this.resultR = [];this.previewFormula = '';},cancelDrag() {this.dragOpen = false;this.previewFormula = '';},tagClick(item) {},handleClose(item) {this.resultR.splice(this.resultR.findIndex(i => i.dictValue === item.dictValue), 1);},
}
css代码
<style>.mpc-model-dialogue-box .el-dialog {width: 29%;height: auto;}.clearfix .el-checkbox__inner {margin-bottom: 8px;}.clearfix .el-checkbox__label {width: 100%;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;font-size: 16px;font-weight: bold;color: #409EFF;}.cardClass .el-card__header {padding: 10px 15px 0px;}.show-fomula {position: absolute;bottom: 10px;right: 10px;font-size: 20px;color: #237aeb;}.show-fomula:hover {opacity: 0.5;transition: 1s;transform: scale(1.3);}.fomula-tag:hover {opacity: 0.5;transform: scale(1.1);background-color: #2d52f7 !important;}.edit-fomula:hover {opacity: 0.5;transition: 1s;transform: scale(1.3);color: #43bb0b;}.explain {font-weight: bold;color: red;font-size: 12px;float: right;}
</style>