From f5bebb755de3206bdc5bacca0c6148b3c6800907 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期二, 31 三月 2026 16:43:53 +0800
Subject: [PATCH] #前端

---
 rsf-design/src/components/core/layouts/art-chat-window/index.vue                    |   15 ++-
 rsf-design/src/components/core/layouts/art-chat-window/modules/markdown-message.vue |  198 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 207 insertions(+), 6 deletions(-)

diff --git a/rsf-design/src/components/core/layouts/art-chat-window/index.vue b/rsf-design/src/components/core/layouts/art-chat-window/index.vue
index 10efb89..52e8f78 100644
--- a/rsf-design/src/components/core/layouts/art-chat-window/index.vue
+++ b/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'
diff --git a/rsf-design/src/components/core/layouts/art-chat-window/modules/markdown-message.vue b/rsf-design/src/components/core/layouts/art-chat-window/modules/markdown-message.vue
new file mode 100644
index 0000000..303550e
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-chat-window/modules/markdown-message.vue
@@ -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>

--
Gitblit v1.9.1