|
@@ -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>
|