瀏覽代碼

优化markdown组件

liyanbo 1 月之前
父節點
當前提交
c63738c372
共有 3 個文件被更改,包括 560 次插入574 次删除
  1. 3 3
      .env
  2. 285 81
      src/components/MarkdownView/index.vue
  3. 272 490
      src/views/qsfl/components/AiQAChat.vue

+ 3 - 3
.env

@@ -4,8 +4,8 @@ VITE_APP_TITLE=人工智能通识课平台
 # 请求路径
 # VITE_BASE_URL='http://59.110.91.129:8088/admin-api'
 # VITE_BASE_URL='http://8.140.223.94:8088/admin-api'
-# VITE_BASE_URL='https://learn-ai.com.cn/admin-api'
-VITE_BASE_URL='http://192.168.110.8:8080/admin-api'
+ VITE_BASE_URL='https://learn-ai.com.cn/admin-api'
+#VITE_BASE_URL='http://localhost:8080/admin-api'
 
 # 登录是否强制租户
 VITE_APP_COMPULSORY_TENANT=false
@@ -33,4 +33,4 @@ VITE_APP_AI_LAB_LIGHT_TEMPLATE_ID=75
 VITE_APP_AI_LAB_HOME_TEMPLATE_ID=94
 
 # 加密密钥
-VITE_SECRET_KEY=AI_Class_2026_Secret_Key_1a2b3c4d5e6f7g8h9i0j
+VITE_SECRET_KEY=AI_Class_2026_Secret_Key_1a2b3c4d5e6f7g8h9i0j

+ 285 - 81
src/components/MarkdownView/index.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
   <div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
 </template>
 
@@ -6,9 +6,8 @@
 import MarkdownIt from 'markdown-it'
 import 'highlight.js/styles/vs2015.min.css'
 import hljs from 'highlight.js'
-import { ref, computed} from 'vue'
+import { ref, computed } from 'vue'
 
-// 定义组件属性
 const props = defineProps({
   content: {
     type: String,
@@ -18,7 +17,132 @@ const props = defineProps({
 
 const contentRef = ref()
 
+/**
+ * 将表格行按 | 分割,去除首尾空字符串,返回单元格数组
+ */
+function parseTableRow(line: string): string[] {
+  const parts = line.split('|')
+  // 去除首尾空元素(因为 | 在开头和结尾)
+  if (parts[0]?.trim() === '') parts.shift()
+  if (parts[parts.length - 1]?.trim() === '') parts.pop()
+  return parts.map(p => p.trim())
+}
+
+/**
+ * 检查字符串是否为有效的表格分隔符单元格
+ * 标准格式:---, :---, ---:, :---:
+ */
+function isSeparatorCell(cell: string): boolean {
+  return /^:?-{2,}:?$/.test(cell)
+}
+
+/**
+ * 预处理 Markdown 内容,修复不规范的表格格式
+ * 主要修复:分隔符行列数不匹配、格式错误、多余的竖线、空单元格格式问题
+ */
+function normalizeMarkdownTables(content: string): string {
+  if (!content) return content
+
+  const lines = content.split('\n')
+  const result: string[] = []
+  let i = 0
+
+  while (i < lines.length) {
+    const line = lines[i]
+    const trimmedLine = line.trim()
+
+    // 检测可能的表格起始行(以 | 开头结尾,且包含多个单元格)
+    if (trimmedLine.startsWith('|') && trimmedLine.endsWith('|') && trimmedLine.includes('|', 1) && i + 1 < lines.length) {
+      const headerCells = parseTableRow(trimmedLine)
+      const expectedCols = headerCells.length
+
+      // 至少需要2列才能构成表格
+      if (expectedCols >= 2) {
+        const nextLineRaw = lines[i + 1]
+        const nextLine = nextLineRaw?.trim() || ''
+
+        // 检查下一行是否看起来像分隔符行(包含大量 | 和 -)
+        const looksLikeSeparator = nextLine.startsWith('|') && nextLine.endsWith('|')
+          && nextLine.includes('-') && /^\|[\s\-:|]+\|$/.test(nextLine)
+
+        // 或尝试宽松检测:包含足够多的 | 和 - 字符
+        const pipeCount = (nextLine.match(/\|/g) || []).length
+        const dashCount = (nextLine.match(/-/g) || []).length
+        const looseSeparatorMatch = pipeCount >= 2 && dashCount >= 3
+
+        if (looksLikeSeparator || looseSeparatorMatch) {
+          // 尝试解析分隔符行的单元格
+          let sepCells = parseTableRow(nextLine)
+
+          // 如果分隔符单元格解析后大部分是有效的分隔符格式,确认是表格
+          const validSepCount = sepCells.filter(c => isSeparatorCell(c) || /^-+$/.test(c.replace(/\s/g, ''))).length
+
+          if (validSepCount >= 1 || (pipeCount >= expectedCols - 1 && dashCount >= expectedCols * 2)) {
+            // 确认是表格,开始修复
+
+            // 修复分隔符行:确保列数与表头一致,每个单元格是 --- 格式
+            const fixedSepCells: string[] = []
+            for (let k = 0; k < expectedCols; k++) {
+              const orig = sepCells[k] || ''
+              // 保留原有的对齐标记(:)
+              let leftAlign = orig.startsWith(':')
+              let rightAlign = orig.endsWith(':')
+              fixedSepCells.push((leftAlign ? ':' : '') + '---' + (rightAlign ? ':' : ''))
+            }
+            const fixedSepLine = '| ' + fixedSepCells.join(' | ') + ' |'
+
+            const tableLines: string[] = []
+            // 修复表头行,确保单元格之间格式正确
+            tableLines.push('| ' + headerCells.join(' | ') + ' |')
+            tableLines.push(fixedSepLine)
+
+            let j = i + 2
+            // 收集并修复数据行
+            while (j < lines.length) {
+              const dataLineRaw = lines[j]
+              const dataLine = dataLineRaw?.trim() || ''
+
+              if (dataLine.startsWith('|') && dataLine.endsWith('|')) {
+                let dataCells = parseTableRow(dataLine)
+
+                // 修复数据行列数:不足补空,多余截断
+                if (dataCells.length < expectedCols) {
+                  while (dataCells.length < expectedCols) dataCells.push('')
+                } else if (dataCells.length > expectedCols) {
+                  dataCells = dataCells.slice(0, expectedCols)
+                }
+
+                tableLines.push('| ' + dataCells.join(' | ') + ' |')
+                j++
+              } else if (dataLine === '') {
+                // 空行可能表示表格结束,但也可能是表格内的换行(保守处理:表格结束)
+                break
+              } else {
+                break
+              }
+            }
+
+            result.push(...tableLines)
+            i = j
+            continue
+          }
+        }
+      }
+    }
+
+    result.push(line)
+    i++
+  }
+
+  return result.join('\n')
+}
+
+// ====== 创建 markdown-it 实例 ======
 const md = new MarkdownIt({
+  html: true,
+  linkify: true,
+  typographer: true,
+  breaks: false,
   highlight: function (str, lang) {
     if (lang && hljs.getLanguage(lang)) {
       try {
@@ -26,29 +150,65 @@ const md = new MarkdownIt({
         return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
       } catch (__) {}
     }
-    return ``
+    return `<pre><code>${escapeHtml(str)}</code></pre>`
+  }
+})
+
+// ====== 关键修复:正确启用表格 ======
+md.enable('table')
+
+function escapeHtml(text: string): string {
+  const map: Record<string, string> = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#039;'
   }
-}).enable(['table'])
+  return text.replace(/[&<>"']/g, function(m) { return map[m] })
+}
+
+// ====== 覆盖表格渲染规则 ======
+const defaultTableOpen = md.renderer.rules.table_open
+const defaultTableClose = md.renderer.rules.table_close
 
-md.renderer.rules.table_open = () => '<div class="markdown-table-wrap"><table>'
-md.renderer.rules.table_close = () => '</table></div>'
+md.renderer.rules.table_open = function(tokens, idx, options, env, self) {
+  return '<div class="markdown-table-wrap"><table>'
+}
+
+md.renderer.rules.table_close = function(tokens, idx, options, env, self) {
+  return '</table></div>'
+}
+
+// ====== 确保 tbody 正确渲染 ======
+md.renderer.rules.tbody_open = function(tokens, idx, options, env, self) {
+  return '<tbody>'
+}
+
+md.renderer.rules.tbody_close = function(tokens, idx, options, env, self) {
+  return '</tbody>'
+}
 
-/** 渲染 markdown */
 const renderedMarkdown = computed(() => {
-  return md.render(props.content)
+  if (!props.content) return ''
+  try {
+    // 先预处理修复表格格式,再渲染
+    const normalizedContent = normalizeMarkdownTables(props.content)
+    return md.render(normalizedContent)
+  } catch (e) {
+    console.error('Markdown 渲染失败:', e)
+    return props.content
+  }
 })
-
 </script>
 
 <style lang="scss">
 .markdown-view {
   font-family: 'SourceHanSansCN-Normal';
-  // font-size: 1rem;
   font-weight: 400;
-  // line-height: 1.6rem;
   letter-spacing: 0em;
   text-align: left;
-  color: block;
+  color: #e9f1ff;
   max-width: 100%;
 
   pre {
@@ -66,81 +226,45 @@ const renderedMarkdown = computed(() => {
     @media screen and (min-width: 1536px) {
       width: 960px;
     }
-
     @media screen and (max-width: 1536px) and (min-width: 1024px) {
       width: calc(100vw - 400px - 64px - 32px * 2);
     }
-
     @media screen and (max-width: 1024px) and (min-width: 768px) {
       width: calc(100vw - 32px * 2);
     }
-
     @media screen and (max-width: 768px) {
       width: calc(100vw - 16px * 2);
     }
   }
 
-  p,
-  code.hljs {
+  p, code.hljs {
     margin-bottom: 16px;
   }
 
   p {
-    //margin-bottom: 1rem !important;
     margin: 0;
     margin-bottom: 3px;
   }
 
-  /* 标题通用格式 */
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6 {
+  h1, h2, h3, h4, h5, h6 {
     color: var(--color-G900);
     margin: 24px 0 8px;
     font-weight: 600;
   }
 
-  h1 {
-    font-size: 22px;
-    line-height: 32px;
-  }
-
-  h2 {
-    font-size: 20px;
-    line-height: 30px;
-  }
-
-  h3 {
-    font-size: 18px;
-    line-height: 28px;
-  }
-
-  h4 {
-    font-size: 16px;
-    line-height: 26px;
-  }
-
-  h5 {
-    font-size: 16px;
-    line-height: 24px;
-  }
+  h1 { font-size: 22px; line-height: 32px; }
+  h2 { font-size: 20px; line-height: 30px; }
+  h3 { font-size: 18px; line-height: 28px; }
+  h4 { font-size: 16px; line-height: 26px; }
+  h5 { font-size: 16px; line-height: 24px; }
+  h6 { font-size: 16px; line-height: 24px; }
 
-  h6 {
-    font-size: 16px;
-    line-height: 24px;
-  }
-
-  /* 列表(有序,无序) */
-  ul,
-  ol {
+  ul, ol {
     margin: 0 0 8px 0;
     padding: 0;
     font-size: 16px;
     line-height: 24px;
-    color: #3b3e55; // var(--color-CG600);
+    color: #3b3e55;
   }
 
   li {
@@ -151,14 +275,6 @@ const renderedMarkdown = computed(() => {
   ol > li {
     list-style-type: decimal;
     margin-bottom: 1rem;
-    // 表达式,修复有序列表序号展示不全的问题
-    // &:nth-child(n + 10) {
-    //     margin-left: 30px;
-    // }
-
-    // &:nth-child(n + 100) {
-    //     margin-left: 30px;
-    // }
   }
 
   ul > li {
@@ -167,29 +283,117 @@ const renderedMarkdown = computed(() => {
     line-height: 24px;
     margin-right: 11px;
     margin-bottom: 1rem;
-    color: #3b3e55; // var(--color-G900);
+    color: #3b3e55;
   }
 
-  ol ul,
-  ol ul > li,
-  ul ul,
-  ul ul li {
-    // list-style: circle;
+  ol ul, ol ul > li, ul ul, ul ul li {
     font-size: 16px;
     list-style: none;
     margin-left: 6px;
     margin-bottom: 1rem;
   }
 
-  ul ul ul,
-  ul ul ul li,
-  ol ol,
-  ol ol > li,
-  ol ul ul,
-  ol ul ul > li,
-  ul ol,
-  ul ol > li {
+  ul ul ul, ul ul ul li, ol ol, ol ol > li, ol ul ul, ol ul ul > li, ul ol, ul ol > li {
     list-style: square;
   }
+
+  // 表格容器样式
+  .markdown-table-wrap {
+    display: block !important;
+    overflow-x: auto;
+    margin: 16px 0;
+    border-radius: 8px;
+    border: 1px solid rgba(174, 205, 255, 0.25) !important;
+    -webkit-overflow-scrolling: touch;
+    background: rgba(255, 255, 255, 0.02);
+
+    &::-webkit-scrollbar {
+      height: 6px;
+    }
+    &::-webkit-scrollbar-track {
+      background: rgba(255, 255, 255, 0.04);
+      border-radius: 3px;
+    }
+    &::-webkit-scrollbar-thumb {
+      background: rgba(74, 137, 231, 0.35);
+      border-radius: 3px;
+    }
+    &::-webkit-scrollbar-thumb:hover {
+      background: rgba(74, 137, 231, 0.55);
+    }
+  }
+
+  // 强制表格元素使用正确的display属性,防止被其他样式覆盖
+  table {
+    display: table !important;
+    width: 100% !important;
+    min-width: 480px !important;
+    border-collapse: collapse !important;
+    border-spacing: 0 !important;
+    margin: 0 !important;
+    font-size: 13px;
+    table-layout: auto;
+  }
+
+  thead {
+    display: table-header-group !important;
+  }
+
+  tbody {
+    display: table-row-group !important;
+  }
+
+  tr {
+    display: table-row !important;
+    page-break-inside: avoid;
+  }
+
+  th {
+    display: table-cell !important;
+    background: rgba(74, 137, 231, 0.4) !important;
+    color: #ffffff !important;
+    font-weight: 600 !important;
+    padding: 10px 14px !important;
+    border: 1px solid rgba(174, 205, 255, 0.3) !important;
+    text-align: left !important;
+    white-space: nowrap;
+    position: relative;
+  }
+
+  td {
+    display: table-cell !important;
+    color: #e0ebff !important;
+    padding: 10px 14px !important;
+    border: 1px solid rgba(174, 205, 255, 0.2) !important;
+    vertical-align: top !important;
+    text-align: left !important;
+    word-break: break-word;
+    line-height: 1.6;
+  }
+
+  // 表格斑马纹效果
+  tr:nth-child(even) td {
+    background: rgba(255, 255, 255, 0.03) !important;
+  }
+
+  tr:nth-child(odd) td {
+    background: transparent !important;
+  }
+
+  // 表格hover效果
+  tr:hover td {
+    background: rgba(74, 137, 231, 0.1) !important;
+  }
+
+  // 表格内段落样式修正
+  table p, td p, th p {
+    margin: 0 !important;
+    padding: 0 !important;
+  }
+
+  // 防止表格内容被转义成纯文本
+  td, th {
+    white-space: normal;
+  }
 }
 </style>

File diff suppressed because it is too large
+ 272 - 490
src/views/qsfl/components/AiQAChat.vue


Some files were not shown because too many files changed in this diff