Sfoglia il codice sorgente

fix重写插件系统以提高效率

kagg886 3 mesi fa
parent
commit
072785e451

+ 132 - 0
src/components/markdown/Markdown.vue

@@ -0,0 +1,132 @@
+<template>
+  <div>
+    <MarkdownNodeRenderer v-for="(node, index) in renderedContent" :key="index" :node="node"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, defineComponent, h} from 'vue'
+import MarkdownIt from 'markdown-it';
+import {parseDocument} from 'htmlparser2'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+
+interface Props {
+  content: string
+  plugins?: Array<MarkdownPlugin<any>>
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  plugins: undefined
+})
+
+const md = new MarkdownIt({
+  html: true,
+  linkify: true,
+  typographer: true
+})
+
+for (const plugin of props.plugins ?? []) {
+  md.use(plugin.mdItPlugin, plugin.settings)
+}
+
+const renderedContent = computed(() => {
+  // Markdown模式添加安全过滤和样式类,并处理成dom ast
+  return parseDocument(
+      md.render(props.content),
+  ).children
+})
+
+
+const MarkdownNodeRenderer = defineComponent({
+  name: 'MarkdownNodeRenderer',
+  props: {
+    node: {
+      type: Object,
+      required: true,
+    },
+  },
+  setup(subprops) {
+    return () => {
+      const {node} = subprops;
+      if (node.type === 'text') {
+        return node.data
+      }
+
+      for (let i = 0; i < (props.plugins ?? []).length; i++) {
+        const plugin = (props.plugins ?? [])[i]
+        if (plugin.tagName === node.tagName) {
+          return plugin.renderer(node)
+        }
+      }
+
+      return h(
+          node.tagName,
+          {...node.attribs},
+          node.children.map((child, index) =>
+              h(MarkdownNodeRenderer, {node: child, key: index})
+          )
+      )
+
+    }
+  },
+})
+</script>
+
+<style scoped>
+
+/* 基本的markdown样式 */
+:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+  margin-top: 24px;
+  margin-bottom: 16px;
+  font-weight: 600;
+  line-height: 1.25;
+}
+
+:deep(h1) {
+  font-size: 2em;
+}
+
+:deep(h2) {
+  font-size: 1.5em;
+}
+
+:deep(h3) {
+  font-size: 1.25em;
+}
+
+:deep(p) {
+  margin-bottom: 16px;
+  line-height: 1.6;
+}
+
+:deep(pre) {
+  background-color: #f6f8fa;
+  border-radius: 6px;
+  padding: 16px;
+  overflow: auto;
+  margin: 16px 0;
+}
+
+:deep(code) {
+  background-color: #f6f8fa;
+  padding: 2px 4px;
+  border-radius: 3px;
+  font-size: 85%;
+}
+
+:deep(blockquote) {
+  border-left: 4px solid #d1d5da;
+  padding-left: 16px;
+  margin: 16px 0;
+  color: #6a737d;
+}
+
+:deep(ul), :deep(ol) {
+  margin: 16px 0;
+  padding-left: 32px;
+}
+
+:deep(li) {
+  margin: 4px 0;
+}
+</style>

+ 0 - 118
src/components/markdown/index.vue

@@ -1,118 +0,0 @@
-<template>
-	<div>
-		<MarkdownNodeRenderer v-for="(node, index) in renderedContent" :key="index" :node="node" />
-	</div>
-</template>
-
-<script setup lang="ts">
-import { computed, defineComponent, h } from 'vue'
-import MarkdownIt from 'markdown-it';
-import { MarkdownPlugin } from '/@/components/markdown/types'
-import { parseDocument } from 'htmlparser2'
-
-interface Props {
-	content: string
-	plugins?: Array<MarkdownPlugin<any>>
-}
-
-const props = withDefaults(defineProps<Props>(), {
-	plugins: undefined
-})
-
-const md = new MarkdownIt({
-	html: true,
-	linkify: true,
-	typographer: true
-})
-
-for (const plugin of props.plugins ?? []) {
-	md.use(plugin.impl, plugin.settings())
-}
-
-const renderedContent = computed(() => {
-	// Markdown模式添加安全过滤和样式类,并处理成dom ast
-	return parseDocument(
-		md.render(props.content),
-	).children
-})
-
-
-
-const MarkdownNodeRenderer = defineComponent({
-	name: 'MarkdownNodeRenderer',
-	props: {
-		node: {
-			type: Object,
-			required: true,
-		},
-	},
-	setup(props) {
-		return () => {
-			const { node } = props;
-
-			if (node.type === 'text') {
-				return node.data
-			} else {
-				return h(
-					node.tagName,
-					{ ...node.attribs },
-					node.children.map((child, index) =>
-						h(MarkdownNodeRenderer, { node: child, key: index })
-					)
-				)
-			}
-		}
-	},
-})
-</script>
-
-<style scoped>
-
-/* 基本的markdown样式 */
-:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
-	margin-top: 24px;
-	margin-bottom: 16px;
-	font-weight: 600;
-	line-height: 1.25;
-}
-
-:deep(h1) { font-size: 2em; }
-:deep(h2) { font-size: 1.5em; }
-:deep(h3) { font-size: 1.25em; }
-
-:deep(p) {
-	margin-bottom: 16px;
-	line-height: 1.6;
-}
-
-:deep(pre) {
-	background-color: #f6f8fa;
-	border-radius: 6px;
-	padding: 16px;
-	overflow: auto;
-	margin: 16px 0;
-}
-
-:deep(code) {
-	background-color: #f6f8fa;
-	padding: 2px 4px;
-	border-radius: 3px;
-	font-size: 85%;
-}
-
-:deep(blockquote) {
-	border-left: 4px solid #d1d5da;
-	padding-left: 16px;
-	margin: 16px 0;
-	color: #6a737d;
-}
-
-:deep(ul), :deep(ol) {
-	margin: 16px 0;
-	padding-left: 32px;
-}
-
-:deep(li) {
-	margin: 4px 0;
-}
-</style>

+ 44 - 0
src/components/markdown/plugins/VueCharts.vue

@@ -0,0 +1,44 @@
+<script setup lang="ts">
+import {onMounted, onUnmounted, ref} from "vue";
+import * as echarts from 'echarts'
+
+type Props = {
+  data: string
+}
+
+const prop = defineProps<Props>()
+
+let instance: echarts.ECharts
+const dom = ref<HTMLDivElement>()
+
+const resizeHandler = () => {
+  instance?.resize()
+}
+
+onMounted(()=> {
+  let data: echarts.EChartsOption
+  try {
+    data = JSON.parse(decodeURIComponent(prop.data))
+  } catch (e) {
+    console.error(e)
+    return
+  }
+  instance = echarts.init(dom.value)
+  instance.setOption(data)
+
+  window.addEventListener('resize', resizeHandler)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeHandler)
+  instance?.dispose()
+})
+</script>
+
+<template>
+  <div ref="dom"></div>
+</template>
+
+<style scoped>
+
+</style>

+ 54 - 91
src/components/markdown/plugins/markdown-it-echarts.ts

@@ -1,108 +1,71 @@
 import MarkdownIt from "markdown-it";
-import type { RenderRule } from "markdown-it/lib/renderer.mjs";
+import type {RenderRule} from "markdown-it/lib/renderer.mjs";
 import type Token from "markdown-it/lib/token.mjs";
-import type { Options } from "markdown-it/lib/index.mjs";
+import type {Options} from "markdown-it/lib/index.mjs";
 import type Renderer from "markdown-it/lib/renderer.mjs";
-import * as echarts from 'echarts'
+import {defineMarkdownPlugin} from "../type/markdown.ts";
+import {h} from "vue";
+import VueCharts from "./VueCharts.vue";
 
-class EChartsElement extends HTMLDivElement {
-  instance!: echarts.ECharts
-  private resizeHandler!: () => void
-
-  connectedCallback() {
-    const config = decodeURIComponent(this.getAttribute('config') ?? '') ?? '';
-    let data: echarts.EChartsOption
-    try {
-      data = JSON.parse(config)
-    } catch (e) {
-      console.error(e)
-      return
-    }
-    this.instance = echarts.init(this)
-    this.instance.setOption(data)
-
-    // 创建绑定了正确上下文的 resize 处理器
-    this.resizeHandler = () => {
-      this.instance?.resize()
-    }
-
-    window.addEventListener('resize', this.resizeHandler)
-  }
-
-  disconnectedCallback() {
-    // 清理 resize 事件监听器
-    if (this.resizeHandler) {
-      window.removeEventListener('resize', this.resizeHandler)
-    }
-
-    // 销毁 ECharts 实例
-    if (this.instance) {
-      this.instance.dispose()
-    }
-  }
-}
-
-
-export type EchartsPluginOptions = {
-}
-
-// 生成唯一ID
-function generateId(): string {
-  return 'echarts-' + Math.random().toString(36).substring(2, 9)
-}
 
 // 验证JSON格式
 function isValidJSON(str: string): boolean {
-  try {
-    JSON.parse(str)
-    return true
-  } catch {
-    return false
-  }
+    try {
+        JSON.parse(str)
+        return true
+    } catch {
+        return false
+    }
 }
 
 // 渲染echarts代码块
 const renderEcharts: RenderRule = (tokens: Token[], idx: number, _options: Options, env: any, _self: Renderer) => {
-  const token = tokens[idx]
-  const content = token.content.trim()
-
-  if (!content) {
-    return '<div style="padding: 16px;background-color: #fff5f5;border: 1px solid #fed7d7;border-radius: 6px;color: #c53030;margin: 16px 0;">ECharts配置不能为空</div>'
-  }
+    const token = tokens[idx]
+    const content = token.content.trim()
 
-  const id = generateId()
-  const className = env.echartsClassName || 'echarts-container'
-  // 生成完整HTML
-  return `<div is="echarts-container" style="width: 100%;height: 350px;margin: 16px 0; border-radius: 6px"  class="${className}" id="${id}" config="${encodeURIComponent(content)}"></div>`
+    if (!content) {
+        return '<div style="padding: 16px;background-color: #fff5f5;border: 1px solid #fed7d7;border-radius: 6px;color: #c53030;margin: 16px 0;">ECharts配置不能为空</div>'
+    }
+    const className = env.echartsClassName || 'echarts-container'
+    // 生成完整HTML
+    return `<echarts-container style="width: 100%;height: 350px;margin: 16px 0; border-radius: 6px"  class="${className}" data="${encodeURIComponent(content)}"></echarts-container>`
 }
 
-// markdown-it插件
-//@ts-ignore
-// eslint-disable-next-line no-unused-vars
-function echartsPlugin(md: MarkdownIt, options: EchartsPluginOptions = {}) {
-  // 保存原始的fence渲染器
-  const defaultRender = md.renderer.rules.fence ?? function(tokens, idx, options, _env, renderer) {
-    return renderer.renderToken(tokens, idx, options)
-  }
-
-  if (customElements.get('echarts-container') === undefined) {
-    customElements.define('echarts-container', EChartsElement, { extends: 'div' })
-  }
-
-  // 重写fence渲染器
-  md.renderer.rules.fence = function(tokens, idx, options, env, renderer) {
-    const token = tokens[idx]
-    const info = token.info ? token.info.trim() : ''
 
-    // 检查是否是echarts代码块
-    if (info === 'echarts' && isValidJSON(token.content.trim())) {
-      console.log(`prepare renderer`,tokens,idx,options,env,renderer,tokens[idx])
-      return renderEcharts(tokens, idx, options, env, renderer)
+const EChartsPlugin = defineMarkdownPlugin({
+    tagName: 'echarts-container',
+    mdItPlugin: function (md: MarkdownIt) {
+        // 保存原始的fence渲染器
+        const defaultRender = md.renderer.rules.fence ?? function (tokens, idx, options, _env, renderer) {
+            return renderer.renderToken(tokens, idx, options)
+        }
+
+        // if (customElements.get('echarts-container') === undefined) {
+        //   customElements.define('echarts-container', EChartsElement, { extends: 'div' })
+        // }
+
+        // 重写fence渲染器
+        md.renderer.rules.fence = function (tokens, idx, options, env, renderer) {
+            const token = tokens[idx]
+            const info = token.info ? token.info.trim() : ''
+
+            // 检查是否是echarts代码块
+            if (info === 'echarts' && isValidJSON(token.content.trim())) {
+                return renderEcharts(tokens, idx, options, env, renderer)
+            }
+
+            // 其他代码块使用默认渲染器
+            return defaultRender(tokens, idx, options, env, renderer)
+        }
+    },
+    renderer: (node: {attribs: Record<string, string>}) => {
+        return h(VueCharts, {
+                data: node.attribs.data,
+                charts: node.attribs.id,
+                style: 'width: 100%;height: 350px;margin: 16px 0; border-radius: 6px'
+            }
+        )
     }
+})
 
-    // 其他代码块使用默认渲染器
-    return defaultRender(tokens, idx, options, env, renderer)
-  }
-}
-
-export default echartsPlugin
+export default EChartsPlugin

+ 23 - 0
src/components/markdown/type/markdown.ts

@@ -0,0 +1,23 @@
+import type { PluginWithOptions } from 'markdown-it/index.mjs'
+import type { VNode } from 'vue'
+
+export type MarkdownPlugin<Settings> = {
+	tagName: string
+	mdItPlugin: PluginWithOptions<Settings>
+	// eslint-disable-next-line no-unused-vars
+	renderer: (element: {attribs: Record<string, string>}) => VNode
+	settings: Settings
+}
+
+export function defineMarkdownPlugin<Settings>(
+	data: Omit<MarkdownPlugin<Settings>, 'settings'>
+
+	// eslint-disable-next-line no-unused-vars
+): (arg0?: Settings) => MarkdownPlugin<Settings> {
+	return (arg0) => {
+		return {
+			...data,
+			settings: arg0 as Settings,
+		}
+	}
+}

+ 0 - 14
src/components/markdown/types.ts

@@ -1,14 +0,0 @@
-import type {PluginWithOptions} from "markdown-it/index.mjs";
-
-export type MarkdownPlugin<Settings> = {
-	impl: PluginWithOptions<Settings>,
-	settings: () => Settings
-}
-
-
-export function defineMarkdownPlugin<Settings>(impl: PluginWithOptions<Settings>,settings: ()=>Settings):MarkdownPlugin<Settings> {
-	return {
-		impl: impl,
-		settings: settings
-	}
-}

+ 5 - 6
src/views/assistant/index.vue

@@ -1,15 +1,13 @@
 <script setup lang="ts">
-import { defineMarkdownPlugin, MarkdownPlugin } from '/@/components/markdown/types'
-import echartsPlugin, { EchartsPluginOptions } from '/@/components/markdown/plugins/markdown-it-echarts'
-import Markdown from '/@/components/markdown/index.vue'
 import { ref, nextTick, onMounted } from 'vue'
 import { Local } from '/@/utils/storage'
 import { User, ChatDotRound, Delete, Edit } from '@element-plus/icons-vue'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/markdown-it-echarts'
+import Markdown from '/@/components/markdown/Markdown.vue'
 
 const plugins: Array<MarkdownPlugin<any>> = [
-	defineMarkdownPlugin<EchartsPluginOptions>(echartsPlugin, () => {
-		return {}
-	})
+	EChartsPlugin()
 ]
 
 const getUserInfos = ref<{
@@ -294,6 +292,7 @@ onMounted(() => {
 							<Markdown
 								:content="message.content"
 								:plugins="plugins"
+								class="markdown-content"
 							/>
 						</div>
 					</div>