123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- <script setup lang="ts">
- 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 '../store/useBasicLayout';
- import { copyToClip } from '@/utils/copy';
- import { t } from '@/locales';
- 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]',
- // 黑色模式下,左边是黑色1e1e20,右边是绿色a1dc95
- props.inversion ? 'dark:bg-[#63e2b7]' : '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">复制</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 = t('chat.copied');
- setTimeout(() => {
- btn.textContent = t('chat.copyCode');
- }, 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>
- <div v-else class="whitespace-pre-wrap" v-text="text"></div>
- </div>
- <div v-else class="whitespace-pre-wrap" v-text="text"></div>
- <template v-if="loading && !text">
- <span class="dark:text-white w-[4px] h-[20px] block animate-blink"></span>
- </template>
- </div>
- </div>
- </template>
- <style lang="less">
- @import 'styles';
- </style>
|