Quellcode durchsuchen

【优化】采用 marked 重写 md 渲染,增加 code 复制功能

cherishsince vor 11 Monaten
Ursprung
Commit
6c1298f7ef

+ 1 - 2
package.json

@@ -29,7 +29,6 @@
     "@form-create/designer": "^3.1.3",
     "@form-create/element-ui": "^3.1.24",
     "@iconify/iconify": "^3.1.1",
-    "@iktakahiro/markdown-it-katex": "^4.0.1",
     "@microsoft/fetch-event-source": "^2.0.1",
     "@videojs-player/vue": "^1.0.0",
     "@vueuse/core": "^10.9.0",
@@ -53,7 +52,7 @@
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
     "lodash-es": "^4.17.21",
-    "markdown-it": "^14.1.0",
+    "marked": "^12.0.2",
     "min-dash": "^4.1.1",
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",

+ 0 - 22
src/components/MdPreview/copy.ts

@@ -1,22 +0,0 @@
-export const copyText = (content: string) => {//复制
-    // content = content.replace(/^\s/,'')
-    navigator.clipboard.writeText(content).then(function () {
-      ElMessage({
-        message: '复制成功!',
-        type: 'success',
-      })
-    }).catch(function () {
-        (function (content) {
-            document.oncopy = function (e) {
-                e.clipboardData?.setData('text', content);
-                e.preventDefault();
-                document.oncopy = null;
-                ElMessage({
-                  message: '复制成功!',
-                  type: 'success',
-                })
-            };
-        })(content);
-        document.execCommand('copy');
-    });
-};

+ 0 - 225
src/components/MdPreview/index.vue

@@ -1,225 +0,0 @@
-<script lang="ts" setup>
-defineOptions({ name: "md-preview" });
-import { copyText } from './copy';
-import { onMounted, ref, watch, watchEffect, type Ref } from 'vue';
-import 'highlight.js/styles/vs2015.min.css';
-import md from "./md";
-const markdown:Ref<any> = ref(null);
-const sleep = (during:number) => {
-  return new Promise(function(rs,rj){setTimeout(rs,during);})
-};
-
-
-const props = withDefaults(defineProps<{
-    content: string; // md内容
-    delay: boolean; // 延迟渲染
-}>(), {
-    content: "",
-    delay: true,
-});
-
-const runing = ref(false);
-const mdDelay = ref('');//延迟渲染的md内容
-const mdContent = ref('');//延迟渲染的md html
-const WORDS = 1;//打印字数
-const interval = ref(Math.floor(1000 / 60));//最小间隔时长
-const preTime = ref(0);
-
-const render = async () => {
-    if (props.content.length - mdDelay.value.length <= WORDS) {
-        runing.value = false;
-        mdDelay.value = props.content;
-        mdContent.value = md.render(props.content);
-    } else {
-        runing.value = true;
-        mdDelay.value = props.content.substring(0, mdDelay.value.length + WORDS);
-        mdContent.value = md.render(mdDelay.value);
-        await sleep(interval.value);
-        await render();
-    }
-    mdContent.value = md.render(props.content);
-};
-
-watchEffect(() => {
-    if (props.delay) {
-        if (!runing.value) render();
-    } else {
-        // if (runing.value) return;
-        mdDelay.value = props.content;
-        mdContent.value = md.render(props.content);
-    }
-});
-
-watch(() => props.content, (newVal, oldVal) => {
-    const now = Date.now();
-    if (preTime.value) {
-        interval.value = Math.floor((now - preTime.value) / (newVal.length - oldVal.length));
-        // console.log('间隔:', Math.floor((now - preTime.value)), 'ms', ' 每字间隔:', interval.value, 'ms', ' 变化字符:', newVal.replace(oldVal, ''));
-    }
-    preTime.value = now;
-});
-function addMarkdownEvent() {
-    markdown.value.addEventListener('click', (e:any) => {
-      if (e.target.id === 'copy') {
-        copyText(e.target?.dataset?.copy);
-      }
-    })
-}
-onMounted(()=> {
-    addMarkdownEvent();
-})
-</script>
-
-<template>
-    <div v-html="mdContent" ref="markdown" class="md-preview"></div>
-</template>
-
-<style lang="scss">
-.md-preview {
-    font-family: PingFang SC;
-    font-size: 0.95rem;
-    font-weight: 400;
-    line-height: 1.6rem;
-    letter-spacing: 0em;
-    text-align: left;
-    color: #3B3E55;
-    max-width: 100%;
-
-    pre {
-        position: relative;
-    }
-    pre code.hljs {
-        width: auto;
-    }
-    code.hljs {
-        border-radius: 6px;
-        padding-top: 20px;
-        width: auto;
-        @media screen and (min-width:1536px) {
-            width: 960px;
-        }
-
-        @media screen and (max-width:1536px) and (min-width:1024px) {
-            width: calc(100vw - 400px - 64px - 32px * 2);
-        }
-
-        @media screen and (max-width:1024px) and (min-width:768px) {
-            width: calc(100vw - 32px * 2);
-        }
-
-        @media screen and (max-width:768px) {
-            width: calc(100vw - 16px * 2);
-        }
-    }
-
-    p,
-    code.hljs {
-        margin-bottom: 16px;
-    }
-
-    p {
-        margin-bottom: 1rem !important;
-    }
-
-    /* 标题通用格式 */
-    h1,
-    h2,
-    h3,
-    h4,
-    h5,
-    h6 {
-        color: var(--color-G900);
-        margin: 24px 0 8px;
-        font-weight: 600;
-    }
-
-    h1 {
-        font-size: 22px;
-        line-height: 32px;
-    }
-
-    h2 {
-        font-size: 20px;
-        line-height: 30px;
-    }
-
-    h3 {
-        font-size: 18px;
-        line-height: 28px;
-    }
-
-    h4 {
-        font-size: 16px;
-        line-height: 26px;
-    }
-
-    h5 {
-        font-size: 16px;
-        line-height: 24px;
-    }
-
-    h6 {
-        font-size: 16px;
-        line-height: 24px;
-    }
-
-    /* 列表(有序,无序) */
-    ul,
-    ol {
-        margin: 0 0 8px 0;
-        padding: 0;
-        font-size: 16px;
-        line-height: 24px;
-        color: #3b3e55; // var(--color-CG600);
-    }
-
-    li {
-        margin: 4px 0 0 20px;
-        margin-bottom: 1rem;
-    }
-
-    ol>li {
-        list-style-type: decimal;
-        margin-bottom: 1rem;
-        // 表达式,修复有序列表序号展示不全的问题
-        // &:nth-child(n + 10) {
-        //     margin-left: 30px;
-        // }
-
-        // &:nth-child(n + 100) {
-        //     margin-left: 30px;
-        // }
-    }
-
-    ul>li {
-        list-style-type: disc;
-        font-size: 16px;
-        line-height: 24px;
-        margin-right: 11px;
-        margin-bottom: 1rem;
-        color: #3b3e55; // var(--color-G900);
-    }
-
-    ol ul,
-    ol ul>li,
-    ul ul,
-    ul ul li {
-        // list-style: circle;
-        font-size: 16px;
-        list-style: none;
-        margin-left: 6px;
-        margin-bottom: 1rem;
-    }
-
-    ul ul ul,
-    ul ul ul li,
-    ol ol,
-    ol ol>li,
-    ol ul ul,
-    ol ul ul>li,
-    ul ol,
-    ul ol>li {
-        list-style: square;
-    }
-}
-</style>

+ 0 - 30
src/components/MdPreview/md.ts

@@ -1,30 +0,0 @@
-
-// @ts-ignore
-import markdownit from 'markdown-it';
-import hljs from 'highlight.js'; // https://highlightjs.org
-import katexPlugin from '@iktakahiro/markdown-it-katex';
-const codeTool = (text: string) => `<svg id="copy" class="icon" aria-hidden="true"
-style="font-size:16px;display: inline-block;color:#fff;position:absolute;right:8px;top:6px;cursor:pointer;"
-data-copy="${text}">
-  <use xlink:href="#gt-line-copy"></use>
-</svg>`;
-
-const md = markdownit({
-    html: true,
-    linkfy: true,
-    highlight: function (str: string, lang: string) {
-        const baseText = str
-        if (lang && hljs.getLanguage(lang)) {
-            try {
-                return '<pre><code class="hljs">' +
-                    hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
-                    '</code>' + codeTool(baseText) + '</pre>';
-            } catch (__) { }
-        }
-        return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code>' + codeTool(baseText) + '</pre>';
-    }
-});
-
-md.use(katexPlugin);
-
-export default md;

+ 207 - 21
src/views/ai/chat/index.vue

@@ -88,18 +88,18 @@
           <div class="chat-list" v-for="(item, index) in list" :key="index">
             <!--  靠左 message  -->
             <div class="left-message message-item" v-if="item.type === 'system'">
-              <div class="avatar" >
+              <div class="avatar">
                 <el-avatar
                   src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
                 />
               </div>
               <div class="message">
                 <div>
-                  <el-text class="time">{{formatDate(item.createTime)}}</el-text>
+                  <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
                 </div>
-                <div class="left-text-container">
-<!--                  <div class="left-text md-preview" v-html="item.content"></div>-->
-                  <mdPreview :content="item.content" :delay="false" />
+                <div class="left-text-container" ref="markdownViewRef">
+                  <div class="left-text markdown-view" v-html="item.content"></div>
+                  <!--                  <mdPreview :content="item.content" :delay="false" />-->
                 </div>
                 <div class="left-btns">
                   <div class="btn-cus" @click="noCopy(item.content)">
@@ -122,13 +122,13 @@
               </div>
               <div class="message">
                 <div>
-                  <el-text class="time">{{formatDate(item.createTime)}}</el-text>
+                  <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
                 </div>
                 <div class="right-text-container">
-                  <div class="right-text">{{item.content}}</div>
+                  <div class="right-text">{{ item.content }}</div>
                 </div>
                 <div class="right-btns">
-                  <div class="btn-cus"  @click="noCopy(item.content)">
+                  <div class="btn-cus" @click="noCopy(item.content)">
                     <img class="btn-image" src="@/assets/ai/copy.svg"/>
                     <el-text class="btn-cus-text">复制</el-text>
                   </div>
@@ -145,13 +145,16 @@
       </el-main>
       <el-footer class="footer-container">
         <form @submit.prevent="onSend" class="prompt-from">
-          <textarea class="prompt-input" v-model="prompt" @keyup.enter="onSend" placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"></textarea>
+          <textarea class="prompt-input" v-model="prompt" @keyup.enter="onSend"
+                    placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"></textarea>
           <div class="prompt-btns">
             <el-switch/>
-            <el-button type="primary" size="default" @click="onSend()" :loading="conversationInProgress" v-if="conversationInProgress == false">
-              {{ conversationInProgress ? '进行中' : '发送'}}
+            <el-button type="primary" size="default" @click="onSend()"
+                       :loading="conversationInProgress" v-if="conversationInProgress == false">
+              {{ conversationInProgress ? '进行中' : '发送' }}
             </el-button>
-            <el-button type="danger" size="default" @click="stopStream()" v-if="conversationInProgress == true">
+            <el-button type="danger" size="default" @click="stopStream()"
+                       v-if="conversationInProgress == true">
               停止
             </el-button>
           </div>
@@ -164,7 +167,25 @@
 <script setup lang="ts">
 import {ChatMessageApi, ChatMessageSendVO, ChatMessageVO} from "@/api/ai/chat/message"
 import {formatDate} from "@/utils/formatTime"
-import {useClipboard} from '@vueuse/core'
+import {useClipboard} from "@vueuse/core";
+// 转换 markdown
+import {marked} from 'marked';
+// 代码高亮 https://highlightjs.org/
+import 'highlight.js/styles/vs2015.min.css';
+import hljs from 'highlight.js';
+
+
+// 自定义渲染器
+const renderer = {
+  code(code, language, c) {
+    const highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
+    const copyHtml = `<div id="copy" data-copy='${code}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
+    return `<pre>${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
+  },
+};
+marked.use({
+  renderer: renderer,
+})
 
 const conversationList = [
   {
@@ -181,11 +202,12 @@ const conversationList = [
   }
 ]
 // 初始化 copy 到粘贴板
-const { copy } = useClipboard();
+const {copy} = useClipboard();
 
 const searchName = ref('') // 查询的内容
 const conversationId = ref('1781604279872581648') // 对话id
-const conversationInProgress = ref<false>() // 对话进行中
+const conversationInProgress = ref<Boolean>() // 对话进行中
+conversationInProgress.value = false
 const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
 
 const prompt = ref<string>() // prompt
@@ -249,7 +271,8 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
   try {
     // 发送 event stream
     let isFirstMessage = true
-    ChatMessageApi.sendStream(userMessage.id, conversationInAbortController.value,(message) => {
+    let content = ''
+    ChatMessageApi.sendStream(userMessage.id, conversationInAbortController.value, (message) => {
       console.log('message', message)
       const data = JSON.parse(message.data) as unknown as ChatMessageVO
       // 如果没有内容结束链接
@@ -264,10 +287,9 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
         isFirstMessage = false;
         list.value.push(data)
       } else {
+        content = content + data.content
         const lastMessage = list.value[list.value.length - 1];
-        lastMessage.content = lastMessage.content + data.content
-        // markdown
-        // lastMessage.content = marked(lastMessage.content)
+        lastMessage.content = marked(content) as unknown as string
         list.value[list.value - 1] = lastMessage
       }
       // 滚动到最下面
@@ -301,7 +323,9 @@ const messageList = async () => {
     // marked(this.markdownText)
     res.map(item => {
       // item.content = marked(item.content)
-      // item.content = md.render(item.content)
+      if (item.type !== 'user') {
+        item.content = marked(item.content)
+      }
     })
 
     list.value = res;
@@ -376,7 +400,17 @@ onMounted(async () => {
   // await nextTick
   // 监听滚动事件,判断用户滚动状态
   messageContainer.value.addEventListener('scroll', handleScroll)
-  //
+  // 添加 copy 监听
+  messageContainer.value.addEventListener('click', (e: any) => {
+    console.log(e)
+    if (e.target.id === 'copy') {
+      copy(e.target?.dataset?.copy)
+      ElMessage({
+        message: '复制成功!',
+        type: 'success',
+      })
+    }
+  })
   // marked.use({
   //   async: false,
   //   pedantic: false,
@@ -682,3 +716,155 @@ onMounted(async () => {
   }
 }
 </style>
+
+<style lang="scss">
+.markdown-view {
+  font-family: PingFang SC;
+  font-size: 0.95rem;
+  font-weight: 400;
+  line-height: 1.6rem;
+  letter-spacing: 0em;
+  text-align: left;
+  color: #3B3E55;
+  max-width: 100%;
+
+  pre {
+    position: relative;
+  }
+
+  pre code.hljs {
+    width: auto;
+  }
+
+  code.hljs {
+    border-radius: 6px;
+    padding-top: 20px;
+    width: auto;
+    @media screen and (min-width: 1536px) {
+      width: 960px;
+    }
+
+    @media screen and (max-width: 1536px) and (min-width: 1024px) {
+      width: calc(100vw - 400px - 64px - 32px * 2);
+    }
+
+    @media screen and (max-width: 1024px) and (min-width: 768px) {
+      width: calc(100vw - 32px * 2);
+    }
+
+    @media screen and (max-width: 768px) {
+      width: calc(100vw - 16px * 2);
+    }
+  }
+
+  p,
+  code.hljs {
+    margin-bottom: 16px;
+  }
+
+  p {
+    margin-bottom: 1rem !important;
+  }
+
+  /* 标题通用格式 */
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
+    color: var(--color-G900);
+    margin: 24px 0 8px;
+    font-weight: 600;
+  }
+
+  h1 {
+    font-size: 22px;
+    line-height: 32px;
+  }
+
+  h2 {
+    font-size: 20px;
+    line-height: 30px;
+  }
+
+  h3 {
+    font-size: 18px;
+    line-height: 28px;
+  }
+
+  h4 {
+    font-size: 16px;
+    line-height: 26px;
+  }
+
+  h5 {
+    font-size: 16px;
+    line-height: 24px;
+  }
+
+  h6 {
+    font-size: 16px;
+    line-height: 24px;
+  }
+
+  /* 列表(有序,无序) */
+  ul,
+  ol {
+    margin: 0 0 8px 0;
+    padding: 0;
+    font-size: 16px;
+    line-height: 24px;
+    color: #3b3e55; // var(--color-CG600);
+  }
+
+  li {
+    margin: 4px 0 0 20px;
+    margin-bottom: 1rem;
+  }
+
+  ol > li {
+    list-style-type: decimal;
+    margin-bottom: 1rem;
+    // 表达式,修复有序列表序号展示不全的问题
+    // &:nth-child(n + 10) {
+    //     margin-left: 30px;
+    // }
+
+    // &:nth-child(n + 100) {
+    //     margin-left: 30px;
+    // }
+  }
+
+  ul > li {
+    list-style-type: disc;
+    font-size: 16px;
+    line-height: 24px;
+    margin-right: 11px;
+    margin-bottom: 1rem;
+    color: #3b3e55; // var(--color-G900);
+  }
+
+  ol ul,
+  ol ul > li,
+  ul ul,
+  ul ul li {
+    // list-style: circle;
+    font-size: 16px;
+    list-style: none;
+    margin-left: 6px;
+    margin-bottom: 1rem;
+  }
+
+  ul ul ul,
+  ul ul ul li,
+  ol ol,
+  ol ol > li,
+  ol ul ul,
+  ol ul ul > li,
+  ul ol,
+  ul ol > li {
+    list-style: square;
+  }
+}
+</style>