index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. <template>
  2. <div class="fm-formula-container">
  3. <el-row justify="space-between" >
  4. <el-col :span="24">
  5. <el-card :header="$t('fm.formula.header')" shadow="never" size="small" class="formula-card">
  6. <div :id="editorId" class="formula-editor"></div>
  7. </el-card>
  8. </el-col>
  9. </el-row>
  10. <el-row justify="space-between" :gutter="10">
  11. <el-col :span="24">
  12. <el-tabs class="formula-tabs">
  13. <el-tab-pane :label="$t('fm.formula.field')">
  14. <el-scrollbar ref="scrollRef" always style="height: 100%;">
  15. <el-tree
  16. :data="modelsData"
  17. node-key="id"
  18. default-expand-all
  19. :expand-on-click-node="false"
  20. style="width: 100%;"
  21. ref="fieldTree"
  22. >
  23. <template #default="{ node }">
  24. <div style="display: flex; justify-content: space-between; width: 95%; align-items: center;" @click="handleNode(node.data)">
  25. <span>{{ node.data.name }} {<span style="font-size: 12px;">{{node.data.id}}</span>}</span>
  26. <el-tag type="info" size="mini">{{$t('fm.components.fields.' + node.data.type)}}</el-tag>
  27. </div>
  28. </template>
  29. </el-tree>
  30. </el-scrollbar>
  31. </el-tab-pane>
  32. <el-tab-pane :label="$t('fm.formula.fieldId')">
  33. <el-scrollbar ref="scrollRef" always style="height: 100%;">
  34. <el-tree
  35. :data="modelsData"
  36. node-key="id"
  37. default-expand-all
  38. :expand-on-click-node="false"
  39. style="width: 100%;"
  40. >
  41. <template #default="{ node }">
  42. <div style="display: flex; justify-content: space-between; width: 95%; align-items: center;" @click="handleFieldId(node.data)">
  43. <span>{{ node.data.name }} {<span style="font-size: 12px;">{{node.data.id}}</span>}</span>
  44. <el-tag type="info" size="mini">{{$t('fm.components.fields.' + node.data.type)}}</el-tag>
  45. </div>
  46. </template>
  47. </el-tree>
  48. </el-scrollbar>
  49. </el-tab-pane>
  50. <el-tab-pane :label="$t('fm.formula.event')">
  51. <el-scrollbar always style="height: 100%;">
  52. <el-tree
  53. v-if="localVariables.length"
  54. :data="localVariables"
  55. node-key="id"
  56. default-expand-all
  57. :expand-on-click-node="false"
  58. style="width: 100%;"
  59. >
  60. <template #default="{ node }">
  61. <div style="display: flex; justify-content: space-between; width: 90%;" @click="handleVariableNode(node.data)">
  62. <span style=" padding: 0 5px; max-width: 50%;">{{ node.data.name }}</span>
  63. </div>
  64. </template>
  65. </el-tree>
  66. <el-tree
  67. :data="argsData"
  68. node-key="id"
  69. default-expand-all
  70. :expand-on-click-node="false"
  71. style="width: 100%;"
  72. >
  73. <template #default="{ node }">
  74. <div style="display: flex; justify-content: space-between; width: 95%; align-items: center;" @click="handleArgsNode(node.data)">
  75. <span style=" padding: 0 5px; max-width: 50%;"><span>{{ node.data.name }}</span> <span v-if="node.data.desc && $i18n.locale == 'zh-cn'">({{node.data.desc}})</span></span>
  76. <span style="font-size: 12px;">{{node.data.id}}</span>
  77. </div>
  78. </template>
  79. </el-tree>
  80. </el-scrollbar>
  81. </el-tab-pane>
  82. </el-tabs>
  83. </el-col>
  84. </el-row>
  85. </div>
  86. </template>
  87. <script>
  88. import CodeMirror from 'codemirror/lib/codemirror.js'
  89. import 'codemirror/lib/codemirror.css'
  90. import 'codemirror/mode/javascript/javascript.js'
  91. import 'codemirror/theme/ayu-dark.css'
  92. import 'codemirror/addon/edit/closebrackets.js'
  93. import 'codemirror/addon/selection/active-line.js'
  94. import 'codemirror/addon/scroll/simplescrollbars.css'
  95. import 'codemirror/addon/scroll/simplescrollbars.js'
  96. export default {
  97. props: ['value', 'showArguments', 'showRow'],
  98. inject: {
  99. getFormModels: {
  100. default: () => {return new Function()}
  101. },
  102. getResponseVariables: {
  103. default: () => {return new Function()}
  104. },
  105. getLocalVariables: {
  106. default: () => {return new Function()}
  107. }
  108. },
  109. data () {
  110. let curArgsData = []
  111. if (this.showArguments) {
  112. curArgsData = [{
  113. id: 'arguments[0]',
  114. name: this.$t('fm.formula.argsData.name'),
  115. children: [
  116. {
  117. id: ' arguments[0].value',
  118. name: 'value',
  119. desc: '当前字段值',
  120. }, {
  121. id: ' arguments[0].fieldNode',
  122. name: 'fieldNode',
  123. desc: '当前字段标识',
  124. }, {
  125. id: ' arguments[0].currentRef',
  126. name: 'currentRef',
  127. desc: '当前组件实例'
  128. }
  129. ]
  130. }]
  131. if (this.showRow) {
  132. curArgsData[0].children = curArgsData[0].children.concat([
  133. {
  134. id: ' arguments[0].rowIndex',
  135. name: 'rowIndex',
  136. desc: '当前操作行下标',
  137. }, {
  138. id: ' arguments[0].row',
  139. name: 'row',
  140. desc: '当前行数据'
  141. }
  142. ])
  143. }
  144. }
  145. return {
  146. modelValue: this.value,
  147. editorId: 'formula-' + Math.random().toString(36).slice(-8),
  148. modelsData: [],
  149. argsData: curArgsData,
  150. responseVariables: [],
  151. localVariables: []
  152. }
  153. },
  154. mounted () {
  155. this.modelsData = this.getFormModels() || []
  156. let responseVariables = this.getResponseVariables() || []
  157. let localVars = this.getLocalVariables() || []
  158. if (responseVariables.length || localVars.length) {
  159. this.localVariables = [{
  160. id: '',
  161. name: this.$t('fm.formula.argsData.variable'),
  162. children: [...responseVariables, ...localVars].map(item => ({
  163. id: ' ' + item,
  164. name: item
  165. }))
  166. }]
  167. }
  168. setTimeout(() => {
  169. this.initEditor()
  170. })
  171. },
  172. methods: {
  173. initEditor () {
  174. let theme = 'default'
  175. if (document.querySelector('html').className.indexOf('dark')>-1) {
  176. theme = 'ayu-dark'
  177. }
  178. this.editor = CodeMirror(document.getElementById(this.editorId), {
  179. value: this.modelValue,
  180. lineNumbers: false,
  181. mode: 'javascript',
  182. lineWrapping: false,
  183. autofocus: true,
  184. theme: theme,
  185. autoCloseBrackets: true,
  186. styleActiveLine: true,
  187. scrollbarStyle: "simple"
  188. })
  189. this.editor.on('change', cm => {
  190. this.modelValue = cm.getValue()
  191. })
  192. this.replaceFieldContent()
  193. this.replaceArgsContent()
  194. this.replaceVarContent()
  195. this.editor.execCommand("goDocEnd")
  196. },
  197. _escapeRegExp (string) {
  198. return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  199. },
  200. replaceVarContent () {
  201. if (this.localVariables && this.localVariables.length) {
  202. this.localVariables[0].children.forEach(item => {
  203. const regex = new RegExp(item.id, 'g')
  204. const matches = []
  205. let match
  206. while ((match = regex.exec(this.value)) !== null) {
  207. // 匹配到的内容
  208. const matchedValue = match[0]
  209. // 匹配到的内容在文本中的起始位置
  210. const startIndex = match.index - this.calcLastIndex(this.value, match.index) - 1
  211. const lineIndex = this.calcLineCount(this.value, match.index)
  212. // 将匹配结果存储到数组中
  213. matches.push({
  214. value: matchedValue,
  215. startIndex: startIndex,
  216. lineIndex: lineIndex,
  217. id: item.id,
  218. name: item.name
  219. })
  220. }
  221. matches.forEach(mitem => {
  222. let widgetNode = document.createElement('span')
  223. widgetNode.className = 'cm-eventvar'
  224. widgetNode.textContent = mitem.name
  225. widgetNode.title = mitem.id
  226. this.editor.markText({line: mitem.lineIndex, ch: mitem.startIndex}, { line: mitem.lineIndex, ch: mitem.startIndex + mitem.value.length }, {
  227. atomic: true,
  228. selectLeft: true,
  229. selectRight: true,
  230. inclusiveLeft: false,
  231. inclusiveRight: false,
  232. replacedWith: widgetNode,
  233. handleMouseEvents: true
  234. })
  235. })
  236. })
  237. }
  238. },
  239. replaceArgsContent () {
  240. this.argsData.length && this.argsData[0].children.forEach(item => {
  241. const regex = new RegExp(this._escapeRegExp(item.id), 'g')
  242. const matches = []
  243. let match
  244. while ((match = regex.exec(this.value)) !== null) {
  245. // 匹配到的内容
  246. const matchedValue = match[0]
  247. // 匹配到的内容在文本中的起始位置
  248. const startIndex = match.index - this.calcLastIndex(this.value, match.index) - 1
  249. const lineIndex = this.calcLineCount(this.value, match.index)
  250. // 将匹配结果存储到数组中
  251. matches.push({
  252. value: matchedValue,
  253. startIndex: startIndex,
  254. lineIndex: lineIndex,
  255. id: item.id,
  256. name: item.name
  257. })
  258. }
  259. matches.forEach(mitem => {
  260. let widgetNode = document.createElement('span')
  261. widgetNode.className = 'cm-eventargs'
  262. widgetNode.textContent = mitem.name
  263. widgetNode.title = mitem.id
  264. this.editor.markText({line: mitem.lineIndex, ch: mitem.startIndex}, { line: mitem.lineIndex, ch: mitem.startIndex + mitem.value.length }, {
  265. atomic: true,
  266. selectLeft: true,
  267. selectRight: true,
  268. inclusiveLeft: false,
  269. inclusiveRight: false,
  270. replacedWith: widgetNode,
  271. handleMouseEvents: true
  272. })
  273. })
  274. })
  275. },
  276. replaceFieldContent () {
  277. // 定义正则表达式模式,匹配 this.getValue("xxx") 形式的内容
  278. const regex = / this\.getValue\("([^"]+)"\)/g
  279. const matches = []
  280. let match
  281. while ((match = regex.exec(this.value)) !== null) {
  282. // 匹配到的内容
  283. const matchedValue = match[0]
  284. const matchedId = match[1]
  285. // 匹配到的内容在文本中的起始位置
  286. const startIndex = match.index - this.calcLastIndex(this.value, match.index) - 1
  287. const lineIndex = this.calcLineCount(this.value, match.index)
  288. const matchNode = this.$refs.fieldTree.getNode(matchedId)
  289. if (matchNode) {
  290. // 将匹配结果存储到数组中
  291. matches.push({
  292. value: matchedValue,
  293. startIndex: startIndex,
  294. lineIndex: lineIndex,
  295. id: matchedId,
  296. data: matchNode.data
  297. })
  298. }
  299. }
  300. matches.forEach(item => {
  301. let widgetNode = document.createElement('span')
  302. widgetNode.className = 'cm-value'
  303. widgetNode.textContent = item.data.name || item.data.id
  304. widgetNode.title = item.value
  305. this.editor.markText({line: item.lineIndex, ch: item.startIndex}, { line: item.lineIndex, ch: item.startIndex + item.value.length }, {
  306. title: item.value,
  307. atomic: true,
  308. selectLeft: true,
  309. selectRight: true,
  310. inclusiveLeft: false,
  311. inclusiveRight: false,
  312. replacedWith: widgetNode,
  313. handleMouseEvents: true
  314. })
  315. })
  316. },
  317. calcLineCount (str, n) {
  318. let count = 0
  319. for (let i = 0; i < n; i++) {
  320. if (str[i] === '\n') {
  321. count++
  322. }
  323. }
  324. return count
  325. },
  326. calcLastIndex (str, n) {
  327. let lastIndex = -1
  328. for (let i = 0; i < n; i++) {
  329. if (str[i] === '\n') {
  330. lastIndex = i
  331. }
  332. }
  333. return lastIndex
  334. },
  335. handleFieldId (data) {
  336. let cursor = this.editor.getCursor()
  337. let text = `"${data.id}"`
  338. this.editor.replaceRange(text, cursor)
  339. this.editor.focus()
  340. },
  341. handleNode(data) {
  342. let cursor = this.editor.getCursor()
  343. let widgetNode = document.createElement('span')
  344. widgetNode.className = 'cm-value'
  345. widgetNode.textContent = data.name || data.id
  346. let text = ` this.getValue("${data.id}")`
  347. widgetNode.title = text
  348. this.editor.replaceRange(text, cursor)
  349. this.editor.markText({line: cursor.line, ch: cursor.ch}, { line: cursor.line, ch: cursor.ch + text.length }, {
  350. title: text,
  351. atomic: true,
  352. selectLeft: true,
  353. selectRight: true,
  354. inclusiveLeft: false,
  355. inclusiveRight: false,
  356. replacedWith: widgetNode,
  357. handleMouseEvents: true
  358. })
  359. this.editor.focus()
  360. },
  361. handleArgsNode (data) {
  362. let cursor = this.editor.getCursor()
  363. let widgetNode = document.createElement('span')
  364. widgetNode.className = 'cm-eventargs'
  365. widgetNode.textContent = data.name
  366. widgetNode.title = data.id
  367. let text = data.id
  368. this.editor.replaceRange(text, cursor)
  369. this.editor.markText({line: cursor.line, ch: cursor.ch}, { line: cursor.line, ch: cursor.ch + text.length }, {
  370. atomic: true,
  371. selectLeft: true,
  372. selectRight: true,
  373. inclusiveLeft: false,
  374. inclusiveRight: false,
  375. replacedWith: widgetNode,
  376. handleMouseEvents: true
  377. })
  378. this.editor.focus()
  379. },
  380. handleVariableNode (data) {
  381. let cursor = this.editor.getCursor()
  382. let text = data.id
  383. let widgetNode = document.createElement('span')
  384. widgetNode.className = 'cm-eventvar'
  385. widgetNode.textContent = data.name
  386. widgetNode.title = data.id
  387. this.editor.replaceRange(text, cursor)
  388. this.editor.markText({line: cursor.line, ch: cursor.ch}, { line: cursor.line, ch: cursor.ch + text.length }, {
  389. atomic: true,
  390. selectLeft: true,
  391. selectRight: true,
  392. inclusiveLeft: false,
  393. inclusiveRight: false,
  394. replacedWith: widgetNode,
  395. handleMouseEvents: true
  396. })
  397. this.editor.focus()
  398. },
  399. },
  400. watch: {
  401. value (val) {
  402. this.modelValue = val
  403. },
  404. modelValue (val) {
  405. this.$emit('input', val)
  406. }
  407. }
  408. }
  409. </script>
  410. <style lang="scss">
  411. .fm-formula-container{
  412. .formula-tabs {
  413. border: 1px solid #e0e0e0;
  414. // margin-bottom: 5px;
  415. --el-tabs-header-height: 35px;
  416. .el-tabs__header{
  417. padding: 0;
  418. background: #f5f7fa;
  419. margin: 0;
  420. margin-bottom: 5px;
  421. .el-tabs__item{
  422. font-size: 14px;
  423. font-weight: normal;
  424. padding: 0 10px;
  425. }
  426. }
  427. .el-tabs__content{
  428. height: 240px;
  429. .el-tab-pane{
  430. height: 100%;
  431. }
  432. }
  433. }
  434. .formula-card{
  435. margin-bottom: 10px;
  436. .el-card__header{
  437. padding: 8px;
  438. background: #f5f7fa;
  439. }
  440. .el-card__body{
  441. padding: 0;
  442. }
  443. }
  444. .formula-editor{
  445. height: 160px;
  446. .CodeMirror{
  447. height: 100%;
  448. font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
  449. pre.CodeMirror-line {
  450. line-height: 20px;
  451. font-size: 13px;
  452. }
  453. }
  454. .cm-field{
  455. background-color: #409EFF;
  456. color: white;
  457. margin: 2px 0;
  458. padding: 0 2px;
  459. border-radius: 4px;
  460. display: inline-block;
  461. }
  462. .cm-value{
  463. background-color: #409EFF;
  464. color: white;
  465. margin: 2px 1px 2px 2px;
  466. padding: 0 4px;
  467. border-radius: 4px;
  468. display: inline-block;
  469. font-size: 13px;
  470. }
  471. .cm-eventargs{
  472. background-color: #E6A23C;
  473. color: white;
  474. margin: 2px 1px 2px 2px;
  475. padding: 0 4px;
  476. border-radius: 4px;
  477. font-size: 13px;
  478. display: inline-block;
  479. }
  480. .cm-eventvar {
  481. background-color: rgb(250, 236, 216);
  482. color: var(--el-color-warning);
  483. padding: 0 2px;
  484. border-radius: 4px;
  485. display: inline-block;
  486. }
  487. }
  488. .el-tree-node{
  489. padding: 2px 0;
  490. }
  491. .formula-field,.formula-method{
  492. .el-card__body{
  493. height: 240px;
  494. padding: 0;
  495. }
  496. }
  497. }
  498. </style>