Quellcode durchsuchen

【增加】markdown-it 渲染、增加 highlight 高亮

cherishsince vor 11 Monaten
Ursprung
Commit
5ec08e9758

+ 2 - 1
package.json

@@ -29,6 +29,7 @@
     "@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",
@@ -52,7 +53,7 @@
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
     "lodash-es": "^4.17.21",
-    "marked": "^12.0.2",
+    "markdown-it": "^14.1.0",
     "min-dash": "^4.1.1",
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",

+ 1 - 0
src/assets/ai/copy-style2.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715606039621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M878.250667 981.333333H375.338667a104.661333 104.661333 0 0 1-104.661334-104.661333V375.338667a104.661333 104.661333 0 0 1 104.661334-104.661334h502.912a104.661333 104.661333 0 0 1 104.661333 104.661334v502.912C981.333333 934.485333 934.485333 981.333333 878.250667 981.333333zM375.338667 364.373333a10.666667 10.666667 0 0 0-10.922667 10.965334v502.912c0 6.229333 4.693333 10.922667 10.922667 10.922666h502.912a10.666667 10.666667 0 0 0 10.922666-10.922666V375.338667a10.666667 10.666667 0 0 0-10.922666-10.922667H375.338667z" fill="#ffffff" p-id="4257"></path><path d="M192.597333 753.322667H147.328A104.661333 104.661333 0 0 1 42.666667 648.661333V147.328A104.661333 104.661333 0 0 1 147.328 42.666667H650.24a104.661333 104.661333 0 0 1 104.618667 104.661333v49.962667c0 26.538667-20.309333 46.848-46.848 46.848a46.037333 46.037333 0 0 1-46.848-46.848V147.328a10.666667 10.666667 0 0 0-10.922667-10.965333H147.328a10.666667 10.666667 0 0 0-10.965333 10.965333V650.24c0 6.229333 4.693333 10.922667 10.965333 10.922667h45.269333c26.538667 0 46.848 20.309333 46.848 46.848 0 26.538667-21.845333 45.312-46.848 45.312z" fill="#ffffff" p-id="4258"></path></svg>

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

@@ -0,0 +1,22 @@
+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');
+    });
+};

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

@@ -0,0 +1,225 @@
+<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>

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

@@ -0,0 +1,30 @@
+
+// @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;

+ 42 - 20
src/views/ai/chat/index.vue

@@ -98,11 +98,12 @@
                   <el-text class="time">{{formatDate(item.createTime)}}</el-text>
                 </div>
                 <div class="left-text-container">
-                  <div class="left-text" v-html="item.content"></div>
+<!--                  <div class="left-text md-preview" v-html="item.content"></div>-->
+                  <mdPreview :content="item.content" :delay="false" />
                 </div>
                 <div class="left-btns">
                   <div class="btn-cus" @click="noCopy(item.content)">
-                    <img class="btn-image" src="@/assets/ai/copy.svg"/>
+                    <img class="btn-image" src="../../../assets/ai/copy.svg"/>
                     <el-text class="btn-cus-text">复制</el-text>
                   </div>
                   <div class="btn-cus" style="margin-left: 20px;" @click="onDelete(item.id)">
@@ -124,7 +125,7 @@
                   <el-text class="time">{{formatDate(item.createTime)}}</el-text>
                 </div>
                 <div class="right-text-container">
-                  <div class="right-text" v-html="item.content"></div>
+                  <div class="right-text">{{item.content}}</div>
                 </div>
                 <div class="right-btns">
                   <div class="btn-cus"  @click="noCopy(item.content)">
@@ -161,21 +162,30 @@
 </template>
 
 <script setup lang="ts">
-import 'highlight.js/styles/idea.css'
 import {ChatMessageApi, ChatMessageSendVO, ChatMessageVO} from "@/api/ai/chat/message"
 import {formatDate} from "@/utils/formatTime"
 import {useClipboard} from '@vueuse/core'
-import { marked } from 'marked'
-
 
+const conversationList = [
+  {
+    id: 1,
+    title: '测试标题',
+    avatar:
+      'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
+  },
+  {
+    id: 2,
+    title: '测试对话',
+    avatar:
+      'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
+  }
+]
 // 初始化 copy 到粘贴板
 const { copy } = useClipboard();
 
 const searchName = ref('') // 查询的内容
 const conversationId = ref('1781604279872581648') // 对话id
-const conversationInProgress = ref<Boolean>() // 对话进行中
-conversationInProgress.value = false
-
+const conversationInProgress = ref<false>() // 对话进行中
 const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
 
 const prompt = ref<string>() // prompt
@@ -185,7 +195,7 @@ const messageContainer: any = ref(null);
 const isScrolling = ref(false)//用于判断用户是否在滚动
 
 /** chat message 列表 */
-defineOptions({ name: 'chatMessageList' })
+// defineOptions({ name: 'chatMessageList' })
 const list = ref<ChatMessageVO[]>([]) // 列表的数据
 
 const changeConversation = (conversation) => {
@@ -257,7 +267,7 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
         const lastMessage = list.value[list.value.length - 1];
         lastMessage.content = lastMessage.content + data.content
         // markdown
-        lastMessage.content = marked(lastMessage.content)
+        // lastMessage.content = marked(lastMessage.content)
         list.value[list.value - 1] = lastMessage
       }
       // 滚动到最下面
@@ -290,7 +300,8 @@ const messageList = async () => {
     // 处理 markdown
     // marked(this.markdownText)
     res.map(item => {
-      item.content = marked(item.content)
+      // item.content = marked(item.content)
+      // item.content = md.render(item.content)
     })
 
     list.value = res;
@@ -365,6 +376,14 @@ onMounted(async () => {
   // await nextTick
   // 监听滚动事件,判断用户滚动状态
   messageContainer.value.addEventListener('scroll', handleScroll)
+  //
+  // marked.use({
+  //   async: false,
+  //   pedantic: false,
+  //   gfm: true,
+  //   tokenizer: new Tokenizer(),
+  //   renderer: renderer,
+  // });
 })
 
 
@@ -567,22 +586,25 @@ onMounted(async () => {
 
       .left-text {
         color: #393939;
-        //font-size: 14px;
+        font-size: 0.95rem;
       }
     }
 
     .right-text-container {
       display: flex;
-      flex-direction: column;
-      overflow-wrap: break-word;
-      background-color: #267fff;
-      color: #FFF;
-      box-shadow: 0 0 0 1px #267fff;
-      border-radius: 10px;
-      padding: 10px;
+      flex-direction: row-reverse;
 
       .right-text {
+        font-size: 0.95rem;
+        color: #FFF;
+        display: inline;
+        background-color: #267fff;
         color: #FFF;
+        box-shadow: 0 0 0 1px #267fff;
+        border-radius: 10px;
+        padding: 10px;
+        width: auto;
+        overflow-wrap: break-word;
       }
     }