zhou zhou
17 小时以前 f5bebb755de3206bdc5bacca0c6148b3c6800907
#前端
1个文件已添加
1个文件已修改
213 ■■■■■ 已修改文件
rsf-design/src/components/core/layouts/art-chat-window/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-chat-window/modules/markdown-message.vue 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-chat-window/index.vue
@@ -313,12 +313,14 @@
                        : 'rounded-bl-xl bg-[var(--art-main-bg-color)] text-[var(--art-gray-900)] ring-1 ring-[var(--el-border-color-extra-light)]'
                    "
                  >
                    <div class="whitespace-pre-wrap break-words">
                      {{
                        message.role === 'assistant'
                          ? message.content || (streaming && index === messages.length - 1 ? $t('ai.drawer.thinking') : '')
                          : message.content || ''
                      }}
                    <MarkdownMessage
                      v-if="message.role === 'assistant'"
                      :content="
                        message.content || (streaming && index === messages.length - 1 ? $t('ai.drawer.thinking') : '')
                      "
                    />
                    <div v-else class="whitespace-pre-wrap break-words">
                      {{ message.content || '' }}
                    </div>
                  </div>
                </div>
@@ -403,6 +405,7 @@
</template>
<script setup>
  import MarkdownMessage from './modules/markdown-message.vue'
  import { ElMessage } from 'element-plus'
  import { useRoute, useRouter } from 'vue-router'
  import { useWindowSize } from '@vueuse/core'
rsf-design/src/components/core/layouts/art-chat-window/modules/markdown-message.vue
New file
@@ -0,0 +1,198 @@
<template>
  <div class="markdown-body assistant-markdown break-words" v-html="renderedContent"></div>
</template>
<script setup>
  const props = defineProps({
    content: {
      type: String,
      default: ''
    }
  })
  const renderedContent = computed(() => renderMarkdown(props.content))
  function renderMarkdown(content) {
    const normalized = normalizeMarkdownSource(content)
    if (!normalized) {
      return ''
    }
    const lines = normalized.split('\n')
    const html = []
    for (let index = 0; index < lines.length; ) {
      const line = lines[index].trim()
      if (!line) {
        index += 1
        continue
      }
      if (/^```/.test(line)) {
        const codeLines = []
        index += 1
        while (index < lines.length && !/^```/.test(lines[index].trim())) {
          codeLines.push(lines[index])
          index += 1
        }
        if (index < lines.length) {
          index += 1
        }
        html.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
        continue
      }
      if (/^\s*#{1,6}\s+/.test(line)) {
        html.push(renderHeadingLine(line))
        index += 1
        continue
      }
      if (/^\s*[-*]\s+/.test(line)) {
        const listLines = []
        while (index < lines.length && /^\s*[-*]\s+/.test(lines[index].trim())) {
          listLines.push(lines[index].trim())
          index += 1
        }
        html.push(`<ul>${listLines
          .map((item) => `<li>${renderMarkdownInline(item.replace(/^\s*[-*]\s+/, ''))}</li>`)
          .join('')}</ul>`)
        continue
      }
      if (/^\s*\d+\.\s+/.test(line)) {
        const listLines = []
        while (index < lines.length && /^\s*\d+\.\s+/.test(lines[index].trim())) {
          listLines.push(lines[index].trim())
          index += 1
        }
        html.push(`<ol>${listLines
          .map((item) => `<li>${renderMarkdownInline(item.replace(/^\s*\d+\.\s+/, ''))}</li>`)
          .join('')}</ol>`)
        continue
      }
      if (/^\s*>\s?/.test(line)) {
        const quoteLines = []
        while (index < lines.length && /^\s*>\s?/.test(lines[index].trim())) {
          quoteLines.push(lines[index].trim().replace(/^\s*>\s?/, ''))
          index += 1
        }
        html.push(`<blockquote>${quoteLines.map((item) => `<p>${renderMarkdownInline(item)}</p>`).join('')}</blockquote>`)
        continue
      }
      const paragraphLines = []
      while (index < lines.length) {
        const current = lines[index].trim()
        if (
          !current ||
          /^```/.test(current) ||
          /^\s*#{1,6}\s+/.test(current) ||
          /^\s*[-*]\s+/.test(current) ||
          /^\s*\d+\.\s+/.test(current) ||
          /^\s*>\s?/.test(current)
        ) {
          break
        }
        paragraphLines.push(current)
        index += 1
      }
      if (paragraphLines.length) {
        html.push(`<p>${paragraphLines.map((item) => renderMarkdownInline(item)).join('<br>')}</p>`)
      } else {
        index += 1
      }
    }
    return html.join('')
  }
  function normalizeMarkdownSource(content) {
    return String(content || '')
      .replace(/\r\n/g, '\n')
      .replace(/<br\s*\/?>/gi, '\n')
      .replace(/<\/p\s*>/gi, '\n\n')
      .replace(/<p[^>]*>/gi, '')
      .replace(/<\/div\s*>/gi, '\n')
      .replace(/<div[^>]*>/gi, '')
      .replace(/&nbsp;/gi, ' ')
      .replace(/\n{3,}/g, '\n\n')
      .trim()
  }
  function renderHeadingLine(line) {
    const level = Math.min((line.match(/^(\s*#+)/)?.[0].trim().length || 1), 6)
    const title = line.replace(/^\s*#{1,6}\s+/, '')
    return `<h${level}>${renderMarkdownInline(title)}</h${level}>`
  }
  function renderMarkdownInline(value) {
    let result = escapeHtml(value)
    result = result.replace(/`([^`]+)`/g, '<code>$1</code>')
    result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
    result = result.replace(/\*([^*\n]+)\*/g, '<em>$1</em>')
    result = result.replace(
      /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
      '<a href="$2" target="_blank" rel="noreferrer noopener">$1</a>'
    )
    return result
  }
  function escapeHtml(value) {
    return String(value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;')
  }
</script>
<style lang="scss">
  @use '@styles/core/md.scss';
</style>
<style scoped>
  :deep(.assistant-markdown) {
    color: var(--art-gray-800);
  }
  :deep(.assistant-markdown > :first-child) {
    margin-top: 0;
  }
  :deep(.assistant-markdown > :last-child) {
    margin-bottom: 0;
  }
  :deep(.assistant-markdown p) {
    margin-bottom: 0.6rem;
  }
  :deep(.assistant-markdown ul),
  :deep(.assistant-markdown ol) {
    padding-left: 1.25rem;
  }
  :deep(.assistant-markdown pre) {
    overflow-x: auto;
    border-radius: 14px;
    background: var(--el-fill-color-light);
    padding: 0.875rem 1rem;
  }
  :deep(.assistant-markdown code) {
    border-radius: 8px;
    background: var(--el-fill-color-light);
    padding: 0.1rem 0.35rem;
    font-size: 0.875em;
  }
  :deep(.assistant-markdown pre code) {
    background: transparent;
    padding: 0;
  }
</style>