CzRger 1 місяць тому
батько
коміт
e91921aed2

+ 77 - 265
src/views/workflow/instance/component/params-textarea/index.vue

@@ -1,52 +1,41 @@
 <template>
-  <div class="czr-markdown-main">
-    <div class="czr-markdown-main-content" :class="`layout-${layout}`">
-      <div class="editor">
-        <textarea
-          ref="ref_textarea"
-          v-model="markdownValue"
-          @input="updateMarkdown"
-        ></textarea>
-      </div>
-      <div class="preview" v-html="markdownHtmlCpt"></div>
+  <div class="flex 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"
+    >
+      <div>回复</div>
+      <div class="ml-auto">字数</div>
+      <el-tooltip content="复制" placement="top">
+        <SvgIcon
+          class="__hover"
+          color="#364153"
+          name="copy"
+          size="16"
+          @click="onCopy(ref_textarea.innerText)"
+        />
+      </el-tooltip>
     </div>
+    <div
+      ref="ref_textarea"
+      class="text-text-secondary flex-1 py-1 text-[13px] break-words whitespace-pre-wrap outline-none select-text"
+      contenteditable="true"
+      @input="updateMarkdown"
+    ></div>
   </div>
 </template>
 
 <script setup lang="ts">
-defineOptions({
-  name: 'CzrMarkdown',
-})
 import { ref, computed, onMounted, reactive } from 'vue'
-import { marked } from 'marked'
-import DOMPurify from 'dompurify'
+import { copy } from '@/utils/czr-util'
+import { ElMessage } from 'element-plus'
+import SvgIcon from '@/components/SvgIcon/index.vue'
 
-const renderer: any = new marked.Renderer()
-renderer.link = ({ href, text }) => {
-  return `<a href="${href}">${text}</a>`
-}
-renderer.text = ({ text }) => {
-  return text.replace(/\$\{([^}]+)\}/g, (match, p1) => {
-    return `<span class='border-1 bg-red-400'>${state.optionsMap.get(p1)?.label}</span>`
-  })
-}
-DOMPurify.addHook('afterSanitizeAttributes', (node) => {
-  if (node.tagName === 'A') {
-    node.setAttribute('target', '_blank')
-    node.setAttribute('rel', 'noopener noreferrer')
-  }
-})
 const emit = defineEmits(['update:modelValue'])
 const props = defineProps({
   modelValue: {
     type: String,
     default: '',
   },
-  layout: {
-    type: String,
-    default: 'x',
-  },
-  rendererFunc: <any>{},
 })
 const state = reactive({
   optionsMap: new Map([
@@ -62,240 +51,63 @@ const state = reactive({
     ],
   ]),
 })
-const Menus: any = [
-  {
-    icon: 'czr_md-title',
-    title: '标题',
-    options: [
-      { title: '一级标题', start: '# ', end: '' },
-      { title: '二级标题', start: '## ', end: '' },
-      { title: '三级标题', start: '### ', end: '' },
-      { title: '四级标题', start: '#### ', end: '' },
-      { title: '五级标题', start: '##### ', end: '' },
-      {
-        label: 'H6',
-        title: '六级标题',
-        start: '###### ',
-        end: '',
-      },
-    ],
-  },
-  { icon: 'czr_md-bold', title: '加粗', start: '**', end: '**' },
-  { icon: 'czr_md-em', title: '斜体', start: '*', end: '*' },
-  { icon: 'czr_md-bold-em', title: '加粗斜体', start: '***', end: '***' },
-  {
-    icon: 'czr_md-s',
-    title: '删除线',
-    start: '~~',
-    end: '~~',
-  },
-  {
-    icon: 'czr_md-u',
-    title: '下划线',
-    start: '<u>',
-    end: '</u>',
-    split: true,
-  },
-  { icon: 'czr_md-code', title: '代码块', start: '\n```\n', end: '\n```' },
-  {
-    icon: 'czr_md-link',
-    title: '链接',
-    start: '[链接](',
-    end: ')',
-  },
-  { icon: 'czr_md-img', title: '图片', start: '![图片](', end: ')' },
-]
-const ref_textarea = ref(null)
-const markdownValue = ref(props.modelValue)
-
-const markdownHtmlCpt = computed(() => {
-  return DOMPurify.sanitize(marked(markdownValue.value) as any)
-})
-
-const updateMarkdown = () => {
-  emit('update:modelValue', markdownValue.value)
-}
-
-const insertText = (before, after) => {
-  const dom: any = ref_textarea.value
-  if (dom) {
-    const start = dom.selectionStart
-    const end = dom.selectionEnd
-    const selectedText = markdownValue.value.substring(start, end)
-    markdownValue.value =
-      markdownValue.value.substring(0, start) +
-      before +
-      selectedText +
-      after +
-      markdownValue.value.substring(end)
-    // 移动光标位置
-    setTimeout(() => {
-      dom.selectionStart = start + before.length
-      dom.selectionEnd = end + before.length
-      dom.focus()
-    }, 0)
-    emit('update:modelValue', markdownValue.value)
-  }
-}
-onMounted(() => {
-  marked.setOptions({
-    renderer: renderer,
-    breaks: true,
-    gfm: true,
-  })
-  console.log(props)
-})
-</script>
-
-<style scoped lang="scss">
-.czr-markdown-main {
-  width: 100%;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
-  .czr-markdown-main-menus {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-    box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
-    padding: 10px;
-    .split {
-      width: 1px;
-      height: 16px;
-      background-color: rgba(0, 0, 0, 0.3);
-    }
-  }
-  .czr-markdown-main-content {
-    flex: 1;
-    display: flex;
-    overflow: hidden;
-    &.layout-y {
-      flex-direction: column;
-      .editor {
-        border-bottom: 1px solid rgba(0, 0, 0, 0.15);
-      }
-    }
-    &.layout-x {
-      .editor {
-        border-right: 1px solid rgba(0, 0, 0, 0.15);
-      }
-    }
-    .editor,
-    .preview {
-      flex: 1;
-      padding: 14px;
-    }
-    .editor {
-      textarea {
-        width: 100%;
-        height: 100%;
-        resize: none;
-      }
-    }
-    :deep(.preview) {
-      overflow: auto;
-      /* 通用标题样式 */
-      h1,
-      h2,
-      h3,
-      h4,
-      h5,
-      h6 {
-        font-family:
-          -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
-          Arial, sans-serif;
-        font-weight: 600;
-        line-height: 1.25;
-        margin-top: 1.5em;
-        margin-bottom: 0.5em;
-        color: #222;
-      }
-
-      /* H1 样式 */
-      h1 {
-        font-size: 2em;
-        border-bottom: 1px solid #eaecef;
-        padding-bottom: 0.3em;
-        margin-top: 0;
-      }
-
-      /* H2 样式 */
-      h2 {
-        font-size: 1.5em;
-        border-bottom: 1px solid #eaecef;
-        padding-bottom: 0.3em;
-      }
-
-      /* H3 样式 */
-      h3 {
-        font-size: 1.25em;
-      }
-
-      /* H4 样式 */
-      h4 {
-        font-size: 1em;
-      }
-
-      /* H5 样式 */
-      h5 {
-        font-size: 0.875em;
-      }
-
-      /* H6 样式 */
-      h6 {
-        font-size: 0.85em;
-        color: #6a737d;
-      }
-      /* 默认链接样式 */
-      a {
-        color: #0366d6; /* 链接颜色 */
-        text-decoration: none; /* 去掉下划线 */
-        transition: color 0.2s; /* 颜色过渡效果 */
-        /* 鼠标悬停样式 */
-        &:hover {
-          color: #0550a8; /* 悬停颜色 */
-          text-decoration: underline; /* 显示下划线 */
-        }
-        /* 已访问链接 */
-        &:visited {
-          color: #5a32a3; /* 紫色表示已访问 */
-        }
-
-        /* 活动链接 (点击时) */
-        &:active {
-          color: #d63384;
+const ref_textarea = ref()
+const updateMarkdown = (e) => {
+  const selection: any = window.getSelection()
+  const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null
+  let lastReplacedSpan: any = null
+
+  const regex = /\{\{#([^#]+)#\}\}/g
+  const walker = document.createTreeWalker(e.target, NodeFilter.SHOW_TEXT, null)
+
+  while (walker.nextNode()) {
+    const node: any = walker.currentNode
+    const text: any = node.nodeValue
+
+    if (regex.test(text)) {
+      regex.lastIndex = 0
+      const fragment = document.createDocumentFragment()
+      let lastIndex = 0
+      let match
+
+      while ((match = regex.exec(text)) !== null) {
+        if (match.index > lastIndex) {
+          fragment.appendChild(
+            document.createTextNode(text.substring(lastIndex, match.index)),
+          )
         }
+        const span = document.createElement('span')
+        span.setAttribute('contenteditable', 'false')
+        span.className = 'border-1 bg-red-400'
+        const key = match[1]
+        span.textContent = state.optionsMap.get(key)?.label || ''
+        fragment.appendChild(span)
+        lastReplacedSpan = span
+        lastIndex = regex.lastIndex
       }
 
-      /*
- * 暗色系代码块样式
- * 适用于夜间/暗色模式的代码展示
- */
-
-      // 基础代码块样式
-      pre {
-        background-color: #1e1e1e; // 深灰背景
-        border-radius: 6px; // 圆角
-        padding: 1.25rem; // 内边距
-        margin: 1.5rem 0; // 外边距
-        overflow: auto; // 溢出滚动
-        border: 1px solid #333; // 边框
-        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); // 阴影效果
-
-        // 代码内容样式
-        code {
-          display: block;
-          color: #d4d4d4; // 主文本颜色
-          font-family:
-            'Fira Code', 'Consolas', 'Monaco', monospace; // 等宽字体栈
-          font-size: 0.95em;
-          line-height: 1.5;
-          text-shadow: none;
-          white-space: pre;
-        }
+      if (lastIndex < text.length) {
+        fragment.appendChild(document.createTextNode(text.substring(lastIndex)))
       }
+
+      node.parentNode.replaceChild(fragment, node)
     }
   }
+
+  if (lastReplacedSpan) {
+    const newRange = document.createRange()
+    newRange.setStartAfter(lastReplacedSpan)
+    newRange.collapse(true)
+    const newSelection: any = window.getSelection()
+    newSelection.removeAllRanges()
+    newSelection.addRange(newRange)
+  }
 }
-</style>
+const onCopy = (text) => {
+  copy(text)
+  ElMessage.success('复制成功!')
+}
+onMounted(() => {})
+</script>
+
+<style scoped lang="scss"></style>

+ 1 - 1
src/views/workflow/instance/component/vars/vars-value.vue

@@ -47,7 +47,7 @@ const nodeDataCpt = computed(() => {
 .vars-value {
   background-color: #ffffff;
   border-radius: 4px;
-  padding: 2px 8px;
+  padding: 4px 8px;
   display: flex;
   align-items: center;
   flex-wrap: wrap;