V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tiRolin
V2EX  ›  Vue.js

使用 Ant design X vue 组件库怎么解决无法正确展示 MarkDown 格式字符串的问题?

  •  
  •   tiRolin · 1 天前 · 580 次点击

    是这样的,我毕设搞了一个 AI 对话,但是呢,这个 AI 对话返回的字符串是 markdown 格式,但是我前端无法正确解析,最后字符串格式就会变成下面这样

    这里显然没有正确解析出样式来,我试了很多方法都没能解决这个问题,各个 ai 问了个遍了,然后去网上找了好多方法,还是没搞定,下面是我这个页面的源码,有没有大佬帮我看看到底该怎么解决这个问题啊?小弟我感激不尽啊

    <template>
      <div class="layout">
        <div class="menu">
          <RouterLink to="/" class="re-home">返回首页</RouterLink>
          <!-- Logo -->
          <div class="logo">
            <img :src="logo" draggable="false" alt="logo" class="logo-img" />
            <span class="logo-span">柑橘智能问答</span>
          </div>
        </div>
        <div class="chat">
          <!-- 消息列表 -->
          <Bubble.List
            :items="messages"
            class="messages"
            style="flex: 1"
            :messageRender="renderMarkdown"
            :roles="{
              ai: {
                placement: 'start',
                typing: { step: 5, interval: 20 },
                styles: {
                  content: {
                    borderRadius: '16px'
                  }
                }
              },
              local: {
                placement: 'end',
                variant: 'shadow'
              }
            }"
          />
    
          <!-- 输入框 -->
          <Sender
            :value="content"
            class="sender"
            @submit="onSubmit"
            @update:value="(val) => (content = val)"
          />
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref, h } from 'vue'
    import { Conversations, Prompts, Sender, Bubble } from 'ant-design-x-vue'
    import { Edit, Delete } from '@element-plus/icons-vue'
    import reqAIChat from '@/api/chat/index'
    import { message } from 'ant-design-vue'
    import { ElMessage } from 'element-plus'
    import logo from '@/assets/images/logo.png'
    import { marked } from 'marked'
    import hljs from 'highlight.js'
    import 'highlight.js/styles/github.css'
    marked.setOptions({
      highlight: (code) => hljs.highlightAuto(code).value,
      breaks: true,
      gfm: true
    })
    
    marked.use({
      renderer: {
        link(href, title, text) {
          return `<a href="${href}" target="_blank" rel="noopener">${text}</a>`
        }
      }
    })
    const renderMarkdown = (content, item) => {
      if (item?.isMarkdown) {
        return h('Typography', { class: 'markdown-container' }, [
          h('div', {
            innerHTML: marked.parse(content), // Vue 3 直接注入 HTML
            class: 'markdown-body'
          })
        ])
      }
      return content // 普通文本直接返回
    }
    const content = ref('')
    
    const messages = ref([
      {
        key: '1',
        content:
          '## Markdown 测试\n[链接]( https://x.ant.design)1. **橙( Orange )**:包括甜橙和酸橙,甜橙中又分为脐橙、瓦伦西亚橙等。2. **柑橘( Mandarin )**:也称为蜜橘,包括各种小型、易剥皮的柑橘。3. **柚子( Pomelo )**:也称为文旦,是柑橘类中最大的一种。4. **柠檬( Lemon )**:以其酸味和香气闻名。5. **青柠( Lime )**:比柠檬小,酸味较轻。6. **葡萄柚( Grapefruit )**:大小和形状类似葡萄,味道可以是甜的也可以是酸的。',
        role: 'ai',
        variant: 'primary',
        isMarkdown: true
      }
    ])
    const activeKey = ref('0')
    const conversationsItems = ref(
      Array.from({ length: 2 }).map((_, index) => ({
        key: `item${index + 1}`,
        label: `未命名对话 ${index + 1}`
      }))
    )
    const onSubmit = async (nextContent) => {
      if (!nextContent) return
      messages.value.push({
        key: `${messages.value.length + 1}`,
        content: nextContent,
        role: 'local',
        variant: 'shadow'
      })
      content.value = ''
      try {
        const res = await reqAIChat({ message: nextContent })
        if (!res) {
          ElMessage({
            message: '请求失败',
            type: 'error'
          })
          return
        }
    
        messages.value.push({
          key: `${messages.value.length + 1}`,
          content: res,
          role: 'ai',
          variant: 'primary',
          isMarkdown: true
        })
      } catch (error) {
        ElMessage({
          message: '请求出错',
          type: 'error'
        })
      }
    }
    const onAddConversation = () => {
      conversationsItems.value.push({
        key: `${conversationsItems.value.length + 1}`,
        label: `未命名对话${conversationsItems.value.length + 1}`
      })
      activeKey.value = `${conversationsItems.value.length + 1}`
    }
    
    const menuConfig = (conversation) => {
      return {
        items: [
          {
            label: '编辑',
            key: 'edit',
            icon: () => h(Edit)
          },
          {
            label: '删除',
            key: 'delete',
            icon: () => h(Delete),
            danger: true
          }
        ],
        onClick: (menuInfo) => {
          message.info(`点击 ${conversation.key} - ${menuInfo.key}`)
        }
      }
    }
    </script>
    
    <style scoped>
    .markdown-body {
      padding: 12px 16px;
      line-height: 1.7;
    
      /* 必须继承字体 */
      font-family: inherit;
    
      /* 代码块样式 */
      pre {
        padding: 12px;
        border-radius: 8px;
        background: #f6f8fa !important;
      }
    
      code {
        font-family: 'SFMono-Regular', Consolas, monospace;
      }
    }
    .markdown-body {
      line-height: 1.6;
      color: #333;
    }
    
    .re-home {
      display: inline-block;
      padding: 10px 20px;
      margin: 12px;
      background: #ffa500;
      color: white;
      text-decoration: none;
      border-radius: 4px;
      font-size: 14px;
      font-weight: bold;
      text-align: center;
      transition: background-color 0.3s ease;
    }
    .layout {
      width: 100%;
      min-width: 1000px;
      height: 722px;
      display: flex;
      background: #fff;
      font-family: AlibabaPuHuiTi, sans-serif;
    }
    
    .menu {
      padding: 24px 0;
      /* background: #fff; */
      width: 280px;
      height: 100%;
      display: flex;
      flex-direction: column;
      background: rgba(245, 245, 245, 0.5);
    }
    
    .logo {
      display: flex;
      height: 72px;
      align-items: center;
      justify-content: start;
      padding: 0 24px;
    }
    
    .logo-img {
      width: 24px;
      height: 24px;
    }
    
    .logo-span {
      margin: 0 8px;
      font-weight: bold;
      color: #333;
      font-size: 16px;
    }
    
    .add-btn {
      background: #1677ff0f;
      border: 1px solid #1677ff34;
      width: calc(100% - 24px);
      cursor: pointer;
      margin: 0 12px 24px 12px;
      font-size: 14px;
      height: 32px;
      padding: 4px 15px;
      border-radius: 6px;
    }
    .add-btn:hover {
      color: #69b1ff;
    }
    
    .chat {
      height: 100%;
      width: 100%;
      max-width: 700px;
      margin: 0 auto;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      padding: 24px;
      gap: 16px;
    }
    </style>
    
    

    这里 content 里已经有一些写死的内容了,那是我用来测试的,就是想着如果可以解析了那么这些写死的内容是会被正确解析上去的,但是我看了好久都没搞懂该怎么解决这个问题,所以这个写死的内容也一直留着了

    4 条回复    2025-03-17 21:59:32 +08:00
    learnshare
        1
    learnshare  
       1 天前
    跟所用的 UI 框架无关,需要将 Markdown 渲染为 HTML
    https://github.com/markdown-it/markdown-it
    learnshare
        2
    learnshare  
       1 天前
    很久以前的代码可以参考,但相关 API 有点不同
    https://github.com/LearnShare/markdown.css/blob/master/index.js#L27
    Linho1219
        3
    Linho1219  
       1 天前
    翻了文档,Bubble.List 应该是不支持 messageRender 属性的,只有 Bubble 支持。试了一下,把 .List 去掉,content 传入字符串就能正常渲染了。所以可以考虑用 v-for 和 Bubble 一起来实现。

    文档: https://antd-design-x-vue.netlify.app/component/bubble#api

    多看文档、在能够复现问题的前提下将项目文件裁剪到最小!出问题的就是 Bubble ,带了一箩筐页面和样式什么的,发出来这么长。把不影响的部分先删掉也方便你自己定位问题。
    都毕设了,我想你应该是有能力独立解决这个问题的。我之前都没碰过这个 ant design X ,花一会时间也找到了
    shintendo
        4
    shintendo  
       1 天前
    楼上+1 ,提问时尽量找最小复现,在这个过程中很可能自己就定位到问题了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5127 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 08:34 · PVG 16:34 · LAX 01:34 · JFK 04:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.