【增加】markdown-it 渲染、增加 highlight 高亮
This commit is contained in:
parent
10beee84fb
commit
5ec08e9758
@ -29,6 +29,7 @@
|
|||||||
"@form-create/designer": "^3.1.3",
|
"@form-create/designer": "^3.1.3",
|
||||||
"@form-create/element-ui": "^3.1.24",
|
"@form-create/element-ui": "^3.1.24",
|
||||||
"@iconify/iconify": "^3.1.1",
|
"@iconify/iconify": "^3.1.1",
|
||||||
|
"@iktakahiro/markdown-it-katex": "^4.0.1",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@videojs-player/vue": "^1.0.0",
|
"@videojs-player/vue": "^1.0.0",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
@ -52,7 +53,7 @@
|
|||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^12.0.2",
|
"markdown-it": "^14.1.0",
|
||||||
"min-dash": "^4.1.1",
|
"min-dash": "^4.1.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
1
src/assets/ai/copy-style2.svg
Normal file
1
src/assets/ai/copy-style2.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1.5 KiB |
22
src/components/MdPreview/copy.ts
Normal file
22
src/components/MdPreview/copy.ts
Normal file
@ -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
src/components/MdPreview/index.vue
Normal file
225
src/components/MdPreview/index.vue
Normal file
@ -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
src/components/MdPreview/md.ts
Normal file
30
src/components/MdPreview/md.ts
Normal file
@ -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;
|
@ -98,11 +98,12 @@
|
|||||||
<el-text class="time">{{formatDate(item.createTime)}}</el-text>
|
<el-text class="time">{{formatDate(item.createTime)}}</el-text>
|
||||||
</div>
|
</div>
|
||||||
<div class="left-text-container">
|
<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>
|
||||||
<div class="left-btns">
|
<div class="left-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"/>
|
<img class="btn-image" src="../../../assets/ai/copy.svg"/>
|
||||||
<el-text class="btn-cus-text">复制</el-text>
|
<el-text class="btn-cus-text">复制</el-text>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-cus" style="margin-left: 20px;" @click="onDelete(item.id)">
|
<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>
|
<el-text class="time">{{formatDate(item.createTime)}}</el-text>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-text-container">
|
<div class="right-text-container">
|
||||||
<div class="right-text" v-html="item.content"></div>
|
<div class="right-text">{{item.content}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-btns">
|
<div class="right-btns">
|
||||||
<div class="btn-cus" @click="noCopy(item.content)">
|
<div class="btn-cus" @click="noCopy(item.content)">
|
||||||
@ -161,21 +162,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import 'highlight.js/styles/idea.css'
|
|
||||||
import {ChatMessageApi, ChatMessageSendVO, ChatMessageVO} from "@/api/ai/chat/message"
|
import {ChatMessageApi, ChatMessageSendVO, ChatMessageVO} from "@/api/ai/chat/message"
|
||||||
import {formatDate} from "@/utils/formatTime"
|
import {formatDate} from "@/utils/formatTime"
|
||||||
import {useClipboard} from '@vueuse/core'
|
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 到粘贴板
|
// 初始化 copy 到粘贴板
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
const searchName = ref('') // 查询的内容
|
const searchName = ref('') // 查询的内容
|
||||||
const conversationId = ref('1781604279872581648') // 对话id
|
const conversationId = ref('1781604279872581648') // 对话id
|
||||||
const conversationInProgress = ref<Boolean>() // 对话进行中
|
const conversationInProgress = ref<false>() // 对话进行中
|
||||||
conversationInProgress.value = false
|
|
||||||
|
|
||||||
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
||||||
|
|
||||||
const prompt = ref<string>() // prompt
|
const prompt = ref<string>() // prompt
|
||||||
@ -185,7 +195,7 @@ const messageContainer: any = ref(null);
|
|||||||
const isScrolling = ref(false)//用于判断用户是否在滚动
|
const isScrolling = ref(false)//用于判断用户是否在滚动
|
||||||
|
|
||||||
/** chat message 列表 */
|
/** chat message 列表 */
|
||||||
defineOptions({ name: 'chatMessageList' })
|
// defineOptions({ name: 'chatMessageList' })
|
||||||
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
||||||
|
|
||||||
const changeConversation = (conversation) => {
|
const changeConversation = (conversation) => {
|
||||||
@ -257,7 +267,7 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
|
|||||||
const lastMessage = list.value[list.value.length - 1];
|
const lastMessage = list.value[list.value.length - 1];
|
||||||
lastMessage.content = lastMessage.content + data.content
|
lastMessage.content = lastMessage.content + data.content
|
||||||
// markdown
|
// markdown
|
||||||
lastMessage.content = marked(lastMessage.content)
|
// lastMessage.content = marked(lastMessage.content)
|
||||||
list.value[list.value - 1] = lastMessage
|
list.value[list.value - 1] = lastMessage
|
||||||
}
|
}
|
||||||
// 滚动到最下面
|
// 滚动到最下面
|
||||||
@ -290,7 +300,8 @@ const messageList = async () => {
|
|||||||
// 处理 markdown
|
// 处理 markdown
|
||||||
// marked(this.markdownText)
|
// marked(this.markdownText)
|
||||||
res.map(item => {
|
res.map(item => {
|
||||||
item.content = marked(item.content)
|
// item.content = marked(item.content)
|
||||||
|
// item.content = md.render(item.content)
|
||||||
})
|
})
|
||||||
|
|
||||||
list.value = res;
|
list.value = res;
|
||||||
@ -365,6 +376,14 @@ onMounted(async () => {
|
|||||||
// await nextTick
|
// await nextTick
|
||||||
// 监听滚动事件,判断用户滚动状态
|
// 监听滚动事件,判断用户滚动状态
|
||||||
messageContainer.value.addEventListener('scroll', handleScroll)
|
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 {
|
.left-text {
|
||||||
color: #393939;
|
color: #393939;
|
||||||
//font-size: 14px;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-text-container {
|
.right-text-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row-reverse;
|
||||||
overflow-wrap: break-word;
|
|
||||||
background-color: #267fff;
|
|
||||||
color: #FFF;
|
|
||||||
box-shadow: 0 0 0 1px #267fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.right-text {
|
.right-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
color: #FFF;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user