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