CzRger преди 1 месец
родител
ревизия
74722aeb7e
променени са 2 файла, в които са добавени 155 реда и са изтрити 24 реда
  1. 149 6
      src/views/workflow/instance/component/params-textarea/index.vue
  2. 6 18
      src/views/workflow/instance/component/vars/vars-popover.vue

+ 149 - 6
src/views/workflow/instance/component/params-textarea/index.vue

@@ -1,7 +1,7 @@
 <template>
   <template v-if="!readonly">
     <div
-      class="flex size-full flex-col overflow-hidden rounded-xl bg-[#f2f4f7] px-3 py-2 shadow"
+      class="relative flex size-full flex-col rounded-xl bg-[#f2f4f7] px-3 py-2 shadow"
     >
       <div
         class="flex items-center gap-2 py-1 text-xs font-semibold text-gray-700"
@@ -36,6 +36,37 @@
           @blur="state.isFocus = false"
         ></div>
       </div>
+      <Teleport to="body">
+        <div
+          class="absolute w-[300px] bg-white shadow"
+          :variable-list="true"
+          :style="state.variableListStyle"
+          v-if="state.showVariableList"
+        >
+          <div class="filter">
+            <el-input
+              v-model="state.text"
+              :prefix-icon="Search"
+              placeholder="搜索变量"
+              clearable
+            />
+          </div>
+          <div class="flex flex-col gap-1.5 p-1">
+            <template v-for="item in optionsCpt">
+              <div class="flex flex-col" v-if="item.options?.length > 0">
+                <div class="mb-1 px-2 text-sm text-gray-400">
+                  {{ item.label }}
+                </div>
+                <template v-for="son in item.options">
+                  <div class="__hover-bg px-2 py-1" @click="setVars(son)">
+                    <varsItem :item="son" />
+                  </div>
+                </template>
+              </div>
+            </template>
+          </div>
+        </div>
+      </Teleport>
     </div>
   </template>
   <template v-else>
@@ -73,12 +104,14 @@ import {
   nextTick,
   onUnmounted,
 } from 'vue'
-import { copy } from '@/utils/czr-util'
+import { copy, domRootHasAttr } from '@/utils/czr-util'
 import { ElMessage } from 'element-plus'
 import SvgIcon from '@/components/SvgIcon/index.vue'
 import varsPopover from '@/views/workflow/instance/component/vars/vars-popover.vue'
 import paramValue from './param-value.vue'
 import { useWorkflowStore } from '@/stores'
+import { Search } from '@element-plus/icons-vue'
+import varsItem from '@/views/workflow/instance/component/vars/vars-item.vue'
 
 const WorkflowStore = useWorkflowStore()
 const emit = defineEmits(['update:modelValue'])
@@ -95,8 +128,13 @@ const props = defineProps({
 const state = reactive({
   textCount: 0,
   optionsMap: new Map(),
+  options: [],
   lastSelection: null,
   isFocus: false,
+  lastSlashPosition: null,
+  variableListStyle: {},
+  showVariableList: false,
+  text: '',
 })
 const ref_textarea = ref()
 watch(
@@ -129,6 +167,7 @@ watch(
         })
       })
       state.optionsMap = map
+      state.options = all
     }
   },
   { immediate: true },
@@ -171,7 +210,6 @@ const handleInput = (e) => {
       node.parentNode.replaceChild(fragment, node)
     }
   }
-
   if (lastReplacedSpan) {
     const newRange = document.createRange()
     newRange.setStartAfter(lastReplacedSpan)
@@ -180,7 +218,56 @@ const handleInput = (e) => {
     newSelection.removeAllRanges()
     newSelection.addRange(newRange)
   }
-  state.textCount = e.target.innerText?.length || 0
+  state.textCount = e.target.innerText?.trim().length || 0
+  const position = getCaretPosition()
+  if (position) {
+    const startContainer = position.range.startContainer
+    const startOffset = position.range.startOffset
+    // 获取光标所在的父元素
+    let currentNode = startContainer
+    if (currentNode.nodeType === Node.TEXT_NODE) {
+      currentNode = currentNode.parentNode
+    }
+    // 获取所有同级元素
+    const siblings = currentNode.childNodes
+    let count = 0
+    // 遍历光标前的元素
+    for (let i = 0; i < siblings.length; i++) {
+      const sibling = siblings[i]
+      // 如果遇到当前元素,检查光标位置(如果是文本节点)
+      if (sibling === currentNode) {
+        if (currentNode.nodeType === Node.TEXT_NODE) {
+          // 对于文本节点,检查是否在开始位置
+          if (startOffset > 0) break
+        } else {
+          // 对于元素节点,检查是否在元素前面
+          if (i > 0 && siblings[i - 1] === currentNode) break
+        }
+      }
+      // 检查是否是 div 且有 vars-dom 类
+      if (
+        sibling.nodeType === Node.ELEMENT_NODE &&
+        sibling.tagName === 'DIV' &&
+        sibling.classList.contains('vars-dom')
+      ) {
+        count++
+      }
+      if (sibling === currentNode) break
+    }
+    console.log(count)
+    // 检查是否输入了 /
+    const textBeforeCaret = e.target.innerText.substring(
+      0,
+      position.offset + count,
+    )
+    console.log(textBeforeCaret)
+    if (textBeforeCaret.endsWith('/')) {
+      state.lastSlashPosition = position.offset - 1 // 保存 / 的位置
+      showVariablePopup(position.range)
+    } else {
+      state.showVariableList = false
+    }
+  }
   emitValue()
 }
 const handlePaste = (e) => {
@@ -208,7 +295,7 @@ const initVarsDom = (vars) => {
   return dom
 }
 const setVars = (vars) => {
-  // onCopy(`{{#${vars.nodeId}_${vars.key}#}}`)
+  state.showVariableList = false
   const nodeToInsert: any = initVarsDom(vars)
   // 检查是否有有效的选择范围且在当前容器内
   if (state.lastSelection) {
@@ -258,6 +345,53 @@ const emitValue = () => {
     emit('update:modelValue', newDom.innerHTML)
   }
 }
+// 获取光标位置
+const getCaretPosition = () => {
+  const selection: any = window.getSelection()
+  if (selection.rangeCount === 0) return null
+
+  const range = selection.getRangeAt(0)
+  const preCaretRange = range.cloneRange()
+  preCaretRange.selectNodeContents(ref_textarea.value)
+  preCaretRange.setEnd(range.endContainer, range.endOffset)
+
+  return {
+    offset: preCaretRange.toString().length,
+    range: range,
+  }
+}
+// 显示变量弹窗
+const showVariablePopup = (range) => {
+  const rect = range.getBoundingClientRect()
+
+  state.variableListStyle = {
+    position: 'absolute',
+    left: `${rect.left + window.scrollX}px`,
+    top: `${rect.bottom + window.scrollY}px`,
+    zIndex: 1000,
+  }
+
+  state.showVariableList = true
+}
+const onMouseDown = (e) => {
+  if (!domRootHasAttr(e.target, 'variable-list')) {
+    state.showVariableList = false
+  }
+}
+const optionsCpt = computed(() => {
+  if (!state.text) {
+    return state.options
+  }
+  return state.options
+    .map((v) => {
+      const obj = { ...v }
+      obj.options = obj.options.filter(
+        (s) => s.label.includes(state.text) || s.key.includes(state.text),
+      )
+      return obj
+    })
+    .filter((v) => v.options.length > 0)
+})
 onMounted(() => {
   if (!props.readonly && props.modelValue) {
     ref_textarea.value.innerHTML = props.modelValue
@@ -270,13 +404,22 @@ onMounted(() => {
   }
   if (!props.readonly) {
     document.addEventListener('selectionchange', handleSelectionchange)
+    document.addEventListener('mousedown', onMouseDown)
   }
 })
 onUnmounted(() => {
   if (!props.readonly) {
     document.removeEventListener('selectionchange', handleSelectionchange)
+    document.removeEventListener('mousedown', onMouseDown)
   }
 })
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+@use '@/views/workflow/instance/component/style';
+
+.filter {
+  padding: 10px;
+  border-bottom: style.$borderStyle;
+}
+</style>

+ 6 - 18
src/views/workflow/instance/component/vars/vars-popover.vue

@@ -14,7 +14,7 @@
           <slot name="default" />
         </div>
       </template>
-      <div class="vars-select-block" :vars-select-block="true">
+      <div :vars-select-block="true">
         <div class="filter">
           <el-input
             v-model="state.text"
@@ -23,9 +23,9 @@
             clearable
           />
         </div>
-        <div class="list">
+        <div class="flex flex-col gap-1.5 p-1">
           <template v-for="item in optionsCpt">
-            <div class="list-group" v-if="item.options?.length > 0">
+            <div class="flex flex-col" v-if="item.options?.length > 0">
               <div class="mb-1 px-2 text-sm text-gray-400">
                 {{ item.label }}
               </div>
@@ -122,20 +122,8 @@ watch(
 <style lang="scss" scoped>
 @use '@/views/workflow/instance/component/style';
 
-.vars-select-block {
-  .filter {
-    padding: 10px;
-    border-bottom: style.$borderStyle;
-  }
-  .list {
-    padding: 4px;
-    display: flex;
-    flex-direction: column;
-    gap: 6px;
-    .list-group {
-      display: flex;
-      flex-direction: column;
-    }
-  }
+.filter {
+  padding: 10px;
+  border-bottom: style.$borderStyle;
 }
 </style>