Right.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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>
  14. </template>
  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. import download from '@/utils/download'
  33. const md = markdownit()
  34. const message = useMessage() // 消息弹窗
  35. const props = defineProps<{
  36. generatedContent: string // 生成结果
  37. isEnd: boolean // 是否结束
  38. isGenerating: boolean // 是否正在生成
  39. isStart: boolean // 开始状态,开始时需要清除 html
  40. }>()
  41. const contentRef = ref<HTMLDivElement>() // 右侧出来header以下的区域
  42. const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的
  43. const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器
  44. const svgRef = ref<SVGElement>() // 思维导图的渲染 svg
  45. const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
  46. const html = ref('') // 生成过程中的文本
  47. const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分
  48. let markMap: Markmap | null = null
  49. const transformer = new Transformer()
  50. onMounted(() => {
  51. contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
  52. /** 初始化思维导图 **/
  53. try {
  54. markMap = Markmap.create(svgRef.value!)
  55. const { el } = Toolbar.create(markMap)
  56. toolBarRef.value?.append(el)
  57. nextTick(update)
  58. } catch (e) {
  59. message.error('思维导图初始化失败')
  60. }
  61. })
  62. watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
  63. // 开始生成的时候清空一下 markdown 的内容
  64. if (isStart) {
  65. html.value = ''
  66. }
  67. // 生成内容的时候使用 markdown 来渲染
  68. if (isGenerating) {
  69. html.value = md.render(generatedContent)
  70. }
  71. // 生成结束时更新思维导图
  72. if (isEnd) {
  73. update()
  74. }
  75. })
  76. /** 更新思维导图的展示 */
  77. const update = () => {
  78. try {
  79. const { root } = transformer.transform(processContent(props.generatedContent))
  80. markMap?.setData(root)
  81. markMap?.fit()
  82. } catch (e) {
  83. console.error(e)
  84. }
  85. }
  86. /** 处理内容 */
  87. const processContent = (text: string) => {
  88. const arr: string[] = []
  89. const lines = text.split('\n')
  90. for (let line of lines) {
  91. if (line.indexOf('```') !== -1) {
  92. continue
  93. }
  94. line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
  95. arr.push(line)
  96. }
  97. return arr.join('\n')
  98. }
  99. /** 下载图片 */
  100. // download SVG to png file
  101. const downloadImage = () => {
  102. const svgElement = mindMapRef.value
  103. // 将 SVG 渲染到图片对象
  104. const serializer = new XMLSerializer()
  105. const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`
  106. const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`
  107. download.image({
  108. url: base64Url,
  109. canvasWidth: svgElement?.offsetWidth,
  110. canvasHeight: svgElement?.offsetHeight,
  111. drawWithImageSize: false
  112. })
  113. }
  114. defineExpose({
  115. scrollBottom() {
  116. mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
  117. }
  118. })
  119. </script>
  120. <style lang="scss" scoped>
  121. .hide-scroll-bar {
  122. -ms-overflow-style: none;
  123. scrollbar-width: none;
  124. &::-webkit-scrollbar {
  125. width: 0;
  126. height: 0;
  127. }
  128. }
  129. .my-card {
  130. display: flex;
  131. flex-direction: column;
  132. :deep(.el-card__body) {
  133. box-sizing: border-box;
  134. flex-grow: 1;
  135. overflow-y: auto;
  136. padding: 0;
  137. @extend .hide-scroll-bar;
  138. }
  139. }
  140. // markmap的tool样式覆盖
  141. :deep(.markmap) {
  142. width: 100%;
  143. }
  144. :deep(.mm-toolbar-brand) {
  145. display: none;
  146. }
  147. :deep(.mm-toolbar) {
  148. display: flex;
  149. flex-direction: row;
  150. }
  151. </style>