TextComponent.vue 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. <script setup lang="ts">
  2. import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue';
  3. import MarkdownIt from 'markdown-it';
  4. import mdKatex from '@traptitech/markdown-it-katex';
  5. import mila from 'markdown-it-link-attributes';
  6. import hljs from 'highlight.js';
  7. import { useBasicLayout } from '../store/useBasicLayout';
  8. import { copyToClip } from '@/utils/copy';
  9. import { t } from '@/locales';
  10. interface Props {
  11. inversion?: boolean;
  12. error?: boolean;
  13. text?: string;
  14. loading?: boolean;
  15. asRawText?: boolean;
  16. }
  17. const props = defineProps<Props>();
  18. const { isMobile } = useBasicLayout();
  19. const textRef = ref<HTMLElement>();
  20. const mdi = new MarkdownIt({
  21. html: false,
  22. linkify: true,
  23. highlight(code, language) {
  24. const validLang = !!(language && hljs.getLanguage(language));
  25. if (validLang) {
  26. const lang = language ?? '';
  27. return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
  28. }
  29. return highlightBlock(hljs.highlightAuto(code).value, '');
  30. },
  31. });
  32. mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } });
  33. mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' });
  34. const wrapClass = computed(() => {
  35. return [
  36. 'text-wrap',
  37. 'min-w-[20px]',
  38. 'rounded-md',
  39. isMobile.value ? 'p-2' : 'px-3 py-2',
  40. props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',
  41. // 黑色模式下,左边是黑色1e1e20,右边是绿色a1dc95
  42. props.inversion ? 'dark:bg-[#63e2b7]' : 'dark:bg-[#1e1e20]',
  43. props.inversion ? 'message-request' : 'message-reply',
  44. { 'text-red-500': props.error },
  45. ];
  46. });
  47. const text = computed(() => {
  48. const value = props.text ?? '';
  49. if (!props.asRawText) {
  50. return mdi.render(value);
  51. }
  52. return value;
  53. });
  54. function highlightBlock(str: string, lang?: string) {
  55. 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>`;
  56. }
  57. function addCopyEvents() {
  58. if (textRef.value) {
  59. const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
  60. copyBtn.forEach((btn) => {
  61. btn.addEventListener('click', () => {
  62. const code = btn.parentElement?.nextElementSibling?.textContent;
  63. if (code) {
  64. copyToClip(code).then(() => {
  65. btn.textContent = t('chat.copied');
  66. setTimeout(() => {
  67. btn.textContent = t('chat.copyCode');
  68. }, 1000);
  69. });
  70. }
  71. });
  72. });
  73. }
  74. }
  75. function removeCopyEvents() {
  76. if (textRef.value) {
  77. const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy');
  78. copyBtn.forEach((btn) => {
  79. btn.removeEventListener('click', () => {});
  80. });
  81. }
  82. }
  83. onMounted(() => {
  84. addCopyEvents();
  85. });
  86. onUpdated(() => {
  87. addCopyEvents();
  88. });
  89. onUnmounted(() => {
  90. removeCopyEvents();
  91. });
  92. </script>
  93. <template>
  94. <div class="text-black" :class="wrapClass">
  95. <div ref="textRef" class="leading-relaxed break-words">
  96. <div v-if="!inversion">
  97. <div v-if="!asRawText" class="markdown-body" v-html="text"></div>
  98. <div v-else class="whitespace-pre-wrap" v-text="text"></div>
  99. </div>
  100. <div v-else class="whitespace-pre-wrap" v-text="text"></div>
  101. <template v-if="loading && !text">
  102. <span class="dark:text-white w-[4px] h-[20px] block animate-blink"></span>
  103. </template>
  104. </div>
  105. </div>
  106. </template>
  107. <style lang="less">
  108. @import 'styles';
  109. </style>