index.vue 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. <template>
  2. <div class="flex flex-col rounded-xl bg-[#f2f4f7] px-3 py-2 shadow">
  3. <div
  4. class="flex items-center gap-2 py-1 text-xs font-semibold text-gray-700"
  5. >
  6. <div>回复</div>
  7. <div class="ml-auto">字数</div>
  8. <el-tooltip content="复制" placement="top">
  9. <SvgIcon
  10. class="__hover"
  11. color="#364153"
  12. name="copy"
  13. size="16"
  14. @click="onCopy(ref_textarea.innerText)"
  15. />
  16. </el-tooltip>
  17. </div>
  18. <div
  19. ref="ref_textarea"
  20. class="text-text-secondary flex-1 py-1 text-[13px] break-words whitespace-pre-wrap outline-none select-text"
  21. contenteditable="true"
  22. @input="updateMarkdown"
  23. ></div>
  24. </div>
  25. </template>
  26. <script setup lang="ts">
  27. import { ref, computed, onMounted, reactive } from 'vue'
  28. import { copy } from '@/utils/czr-util'
  29. import { ElMessage } from 'element-plus'
  30. import SvgIcon from '@/components/SvgIcon/index.vue'
  31. const emit = defineEmits(['update:modelValue'])
  32. const props = defineProps({
  33. modelValue: {
  34. type: String,
  35. default: '',
  36. },
  37. })
  38. const state = reactive({
  39. optionsMap: new Map([
  40. ['123', { label: '变量1', id: '123', type: 'String' }],
  41. ['223', { label: '变量变量变量变量变量2', id: '223', type: 'String' }],
  42. [
  43. '333',
  44. {
  45. label: '变量变量变量变量变量变量变量变量变量变量变量3',
  46. id: '333',
  47. type: 'Number',
  48. },
  49. ],
  50. ]),
  51. })
  52. const ref_textarea = ref()
  53. const updateMarkdown = (e) => {
  54. const selection: any = window.getSelection()
  55. const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null
  56. let lastReplacedSpan: any = null
  57. const regex = /\{\{#([^#]+)#\}\}/g
  58. const walker = document.createTreeWalker(e.target, NodeFilter.SHOW_TEXT, null)
  59. while (walker.nextNode()) {
  60. const node: any = walker.currentNode
  61. const text: any = node.nodeValue
  62. if (regex.test(text)) {
  63. regex.lastIndex = 0
  64. const fragment = document.createDocumentFragment()
  65. let lastIndex = 0
  66. let match
  67. while ((match = regex.exec(text)) !== null) {
  68. if (match.index > lastIndex) {
  69. fragment.appendChild(
  70. document.createTextNode(text.substring(lastIndex, match.index)),
  71. )
  72. }
  73. const span = document.createElement('span')
  74. span.setAttribute('contenteditable', 'false')
  75. span.className = 'border-1 bg-red-400'
  76. const key = match[1]
  77. span.textContent = state.optionsMap.get(key)?.label || ''
  78. fragment.appendChild(span)
  79. lastReplacedSpan = span
  80. lastIndex = regex.lastIndex
  81. }
  82. if (lastIndex < text.length) {
  83. fragment.appendChild(document.createTextNode(text.substring(lastIndex)))
  84. }
  85. node.parentNode.replaceChild(fragment, node)
  86. }
  87. }
  88. if (lastReplacedSpan) {
  89. const newRange = document.createRange()
  90. newRange.setStartAfter(lastReplacedSpan)
  91. newRange.collapse(true)
  92. const newSelection: any = window.getSelection()
  93. newSelection.removeAllRanges()
  94. newSelection.addRange(newRange)
  95. }
  96. }
  97. const onCopy = (text) => {
  98. copy(text)
  99. ElMessage.success('复制成功!')
  100. }
  101. onMounted(() => {})
  102. </script>
  103. <style scoped lang="scss"></style>