| <script lang="ts" setup> |
| import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue' |
| import MarkdownIt from 'markdown-it' |
| import mdKatex from '@traptitech/markdown-it-katex' |
| import mila from 'markdown-it-link-attributes' |
| import hljs from 'highlight.js' |
| import { useBasicLayout } from '@/hooks/useBasicLayout' |
| import { t } from '@/locales' |
| import { copyToClip } from '@/utils/copy' |
|
|
| interface Props { |
| inversion?: boolean |
| error?: boolean |
| text?: string |
| loading?: boolean |
| asRawText?: boolean |
| } |
|
|
| const props = defineProps<Props>() |
|
|
| const { isMobile } = useBasicLayout() |
|
|
| const textRef = ref<HTMLElement>() |
|
|
| const mdi = new MarkdownIt({ |
| html: false, |
| linkify: true, |
| highlight(code, language) { |
| const validLang = !!(language && hljs.getLanguage(language)) |
| if (validLang) { |
| const lang = language ?? '' |
| return highlightBlock(hljs.highlight(code, { language: lang }).value, lang) |
| } |
| return highlightBlock(hljs.highlightAuto(code).value, '') |
| }, |
| }) |
|
|
| mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } }) |
| mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' }) |
|
|
| const wrapClass = computed(() => { |
| return [ |
| 'text-wrap', |
| 'min-w-[20px]', |
| 'rounded-md', |
| isMobile.value ? 'p-2' : 'px-3 py-2', |
| props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]', |
| props.inversion ? 'dark:bg-[#a1dc95]' : 'dark:bg-[#1e1e20]', |
| props.inversion ? 'message-request' : 'message-reply', |
| { 'text-red-500': props.error }, |
| ] |
| }) |
|
|
| const text = computed(() => { |
| const value = props.text ?? '' |
| if (!props.asRawText) |
| return mdi.render(value) |
| return value |
| }) |
|
|
| function highlightBlock(str: string, lang?: string) { |
| return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>` |
| } |
|
|
| function addCopyEvents() { |
| if (textRef.value) { |
| const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy') |
| copyBtn.forEach((btn) => { |
| btn.addEventListener('click', () => { |
| const code = btn.parentElement?.nextElementSibling?.textContent |
| if (code) { |
| copyToClip(code).then(() => { |
| btn.textContent = '复制成功' |
| setTimeout(() => { |
| btn.textContent = '复制代码' |
| }, 1000) |
| }) |
| } |
| }) |
| }) |
| } |
| } |
|
|
| function removeCopyEvents() { |
| if (textRef.value) { |
| const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy') |
| copyBtn.forEach((btn) => { |
| btn.removeEventListener('click', () => {}) |
| }) |
| } |
| } |
|
|
| onMounted(() => { |
| addCopyEvents() |
| }) |
|
|
| onUpdated(() => { |
| addCopyEvents() |
| }) |
|
|
| onUnmounted(() => { |
| removeCopyEvents() |
| }) |
| </script> |
|
|
| <template> |
| <div class="text-black" :class="wrapClass"> |
| <div ref="textRef" class="leading-relaxed break-words"> |
| <div v-if="!inversion"> |
| <div v-if="!asRawText" class="markdown-body" v-html="text" /> |
| <div v-else class="whitespace-pre-wrap" v-text="text" /> |
| </div> |
| <div v-else class="whitespace-pre-wrap" v-text="text" /> |
| <template v-if="loading"> |
| <span class="dark:text-white w-[4px] h-[20px] block animate-blink" /> |
| </template> |
| </div> |
| </div> |
| </template> |
|
|
| <style lang="less"> |
| @import url(./style.less); |
| </style> |
|
|