Right.vue 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <template>
  2. <el-card class="my-card h-full flex-grow">
  3. <template #header
  4. ><h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
  5. <span>思维导图预览</span>
  6. <!-- 展示在右上角 -->
  7. <el-button type="primary" v-show="isEnd" @click="downloadImage" size="small">
  8. <template #icon>
  9. <Icon icon="ph:copy-bold" />
  10. </template>
  11. 下载图片
  12. </el-button>
  13. </h3></template
  14. >
  15. <div ref="contentRef" class="hide-scroll-bar h-full box-border">
  16. <!--展示markdown的容器,最终生成的是html字符串,直接用v-html嵌入-->
  17. <div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
  18. <div class="flex flex-col items-center justify-center" v-html="html"></div>
  19. </div>
  20. <div ref="mindmapRef" class="wh-full">
  21. <svg ref="svgRef" class="w-full" :style="{ height: `${contentAreaHeight}px` }" />
  22. <div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
  23. </div>
  24. </div>
  25. </el-card>
  26. </template>
  27. <script setup lang="ts">
  28. import { Markmap } from 'markmap-view'
  29. import { Transformer } from 'markmap-lib'
  30. import { Toolbar } from 'markmap-toolbar'
  31. import markdownit from 'markdown-it'
  32. const md = markdownit()
  33. const props = defineProps<{
  34. mindmapResult: string // 生成结果
  35. isEnd: boolean // 是否结束
  36. isGenerating: boolean // 是否正在生成
  37. isStart: boolean // 开始状态,开始时需要清除html
  38. }>()
  39. const contentRef = ref<HTMLDivElement>() // 右侧出来header以下的区域
  40. const mdContainerRef = ref<HTMLDivElement>() // markdown的容器,用来滚动到底下的
  41. const mindmapRef = ref<HTMLDivElement>() // 思维导图的容器
  42. const svgRef = ref<SVGElement>() // 思维导图的渲染svg
  43. const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
  44. const html = ref('') // 生成过程中的文本
  45. const contentAreaHeight = ref(0) // 生成区域的高度,出去header部分
  46. let markMap: Markmap | null = null
  47. const transformer = new Transformer()
  48. const message = useMessage()
  49. onMounted(() => {
  50. contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
  51. /** 初始化思维导图 **/
  52. try {
  53. markMap = Markmap.create(svgRef.value!)
  54. const { el } = Toolbar.create(markMap)
  55. toolBarRef.value?.append(el)
  56. nextTick(update)
  57. } catch (e) {
  58. message.error('思维导图初始化失败')
  59. }
  60. })
  61. watch(props, ({ mindmapResult, isGenerating, isEnd, isStart }) => {
  62. // 开始生成的时候清空一下markdown的内容
  63. if (isStart) {
  64. html.value = ''
  65. }
  66. // 生成内容的时候使用markdown来渲染
  67. if (isGenerating) {
  68. html.value = md.render(mindmapResult)
  69. }
  70. if (isEnd) {
  71. update()
  72. }
  73. })
  74. const update = () => {
  75. try {
  76. const { root } = transformer.transform(processContent(props.mindmapResult))
  77. markMap?.setData(root)
  78. markMap?.fit()
  79. } catch (e) {
  80. console.error(e)
  81. }
  82. }
  83. const processContent = (text) => {
  84. const arr: string[] = []
  85. const lines = text.split('\n')
  86. for (let line of lines) {
  87. if (line.indexOf('```') !== -1) {
  88. continue
  89. }
  90. line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
  91. arr.push(line)
  92. }
  93. return arr.join('\n')
  94. }
  95. // download SVG to png file
  96. const downloadImage = () => {
  97. const svgElement = mindmapRef.value
  98. // 将 SVG 渲染到图片对象
  99. const serializer = new XMLSerializer()
  100. const source =
  101. '<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(svgRef.value!)
  102. const image = new Image()
  103. image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
  104. // 将图片对象渲染
  105. const canvas = document.createElement('canvas')
  106. canvas.width = svgElement?.offsetWidth || 0
  107. canvas.height = svgElement?.offsetHeight || 0
  108. let context = canvas.getContext('2d')
  109. context?.clearRect(0, 0, canvas.width, canvas.height)
  110. image.onload = function () {
  111. context?.drawImage(image, 0, 0)
  112. const a = document.createElement('a')
  113. a.download = 'ruoyi-mindmap.png'
  114. a.href = canvas.toDataURL(`image/png`)
  115. a.click()
  116. }
  117. }
  118. defineExpose({
  119. scrollBottom() {
  120. mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
  121. }
  122. })
  123. </script>
  124. <style lang="scss" scoped>
  125. .hide-scroll-bar {
  126. -ms-overflow-style: none;
  127. scrollbar-width: none;
  128. &::-webkit-scrollbar {
  129. width: 0;
  130. height: 0;
  131. }
  132. }
  133. .my-card {
  134. display: flex;
  135. flex-direction: column;
  136. :deep(.el-card__body) {
  137. box-sizing: border-box;
  138. flex-grow: 1;
  139. overflow-y: auto;
  140. padding: 0;
  141. @extend .hide-scroll-bar;
  142. }
  143. }
  144. // markmap的tool样式覆盖
  145. :deep(.markmap) {
  146. width: 100%;
  147. }
  148. :deep(.mm-toolbar-brand) {
  149. display: none;
  150. }
  151. :deep(.mm-toolbar) {
  152. display: flex;
  153. flex-direction: row;
  154. }
  155. </style>