对比新文件 |
| | |
| | | <template> |
| | | <el-card class="my-card h-full flex-grow"> |
| | | <template #header> |
| | | <h3 class="m-0 px-7 shrink-0 flex items-center justify-between"> |
| | | <span>思维导图预览</span> |
| | | <!-- 展示在右上角 --> |
| | | <el-button v-show="isEnd" size="small" type="primary" @click="downloadImage"> |
| | | <template #icon> |
| | | <Icon icon="ph:copy-bold" /> |
| | | </template> |
| | | 下载图片 |
| | | </el-button> |
| | | </h3> |
| | | </template> |
| | | |
| | | <div ref="contentRef" class="hide-scroll-bar h-full box-border"> |
| | | <!--展示 markdown 的容器,最终生成的是 html 字符串,直接用 v-html 嵌入--> |
| | | <div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto"> |
| | | <div class="flex flex-col items-center justify-center" v-html="html"></div> |
| | | </div> |
| | | |
| | | <div ref="mindMapRef" class="wh-full"> |
| | | <svg ref="svgRef" :style="{ height: `${contentAreaHeight}px` }" class="w-full" /> |
| | | <div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import { Markmap } from 'markmap-view' |
| | | import { Transformer } from 'markmap-lib' |
| | | import { Toolbar } from 'markmap-toolbar' |
| | | import markdownit from 'markdown-it' |
| | | import download from '@/utils/download' |
| | | |
| | | const md = markdownit() |
| | | const message = useMessage() // 消息弹窗 |
| | | |
| | | const props = defineProps<{ |
| | | generatedContent: string // 生成结果 |
| | | isEnd: boolean // 是否结束 |
| | | isGenerating: boolean // 是否正在生成 |
| | | isStart: boolean // 开始状态,开始时需要清除 html |
| | | }>() |
| | | const contentRef = ref<HTMLDivElement>() // 右侧出来 header 以下的区域 |
| | | const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的 |
| | | const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器 |
| | | const svgRef = ref<SVGElement>() // 思维导图的渲染 svg |
| | | const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等 |
| | | const html = ref('') // 生成过程中的文本 |
| | | const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分 |
| | | let markMap: Markmap | null = null |
| | | const transformer = new Transformer() |
| | | |
| | | onMounted(() => { |
| | | contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度 |
| | | /** 初始化思维导图 **/ |
| | | try { |
| | | markMap = Markmap.create(svgRef.value!) |
| | | const { el } = Toolbar.create(markMap) |
| | | toolBarRef.value?.append(el) |
| | | nextTick(update) |
| | | } catch (e) { |
| | | message.error('思维导图初始化失败') |
| | | } |
| | | }) |
| | | |
| | | watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => { |
| | | // 开始生成的时候清空一下 markdown 的内容 |
| | | if (isStart) { |
| | | html.value = '' |
| | | } |
| | | // 生成内容的时候使用 markdown 来渲染 |
| | | if (isGenerating) { |
| | | html.value = md.render(generatedContent) |
| | | } |
| | | // 生成结束时更新思维导图 |
| | | if (isEnd) { |
| | | update() |
| | | } |
| | | }) |
| | | |
| | | /** 更新思维导图的展示 */ |
| | | const update = () => { |
| | | try { |
| | | const { root } = transformer.transform(processContent(props.generatedContent)) |
| | | markMap?.setData(root) |
| | | markMap?.fit() |
| | | } catch (e) { |
| | | console.error(e) |
| | | } |
| | | } |
| | | |
| | | /** 处理内容 */ |
| | | const processContent = (text: string) => { |
| | | const arr: string[] = [] |
| | | const lines = text.split('\n') |
| | | for (let line of lines) { |
| | | if (line.indexOf('```') !== -1) { |
| | | continue |
| | | } |
| | | line = line.replace(/([*_~`>])|(\d+\.)\s/g, '') |
| | | arr.push(line) |
| | | } |
| | | return arr.join('\n') |
| | | } |
| | | |
| | | /** 下载图片:download SVG to png file */ |
| | | const downloadImage = () => { |
| | | const svgElement = mindMapRef.value |
| | | // 将 SVG 渲染到图片对象 |
| | | const serializer = new XMLSerializer() |
| | | const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}` |
| | | const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}` |
| | | download.image({ |
| | | url: base64Url, |
| | | canvasWidth: svgElement?.offsetWidth, |
| | | canvasHeight: svgElement?.offsetHeight, |
| | | drawWithImageSize: false |
| | | }) |
| | | } |
| | | |
| | | defineExpose({ |
| | | scrollBottom() { |
| | | mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight) |
| | | } |
| | | }) |
| | | </script> |
| | | <style lang="scss" scoped> |
| | | .hide-scroll-bar { |
| | | -ms-overflow-style: none; |
| | | scrollbar-width: none; |
| | | |
| | | &::-webkit-scrollbar { |
| | | width: 0; |
| | | height: 0; |
| | | } |
| | | } |
| | | |
| | | .my-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-card__body) { |
| | | box-sizing: border-box; |
| | | flex-grow: 1; |
| | | overflow-y: auto; |
| | | padding: 0; |
| | | @extend .hide-scroll-bar; |
| | | } |
| | | } |
| | | |
| | | // markmap的tool样式覆盖 |
| | | :deep(.markmap) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.mm-toolbar-brand) { |
| | | display: none; |
| | | } |
| | | |
| | | :deep(.mm-toolbar) { |
| | | display: flex; |
| | | flex-direction: row; |
| | | } |
| | | </style> |