|
|
@@ -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> = {
|
|
|
+ '&': '&',
|
|
|
+ '<': '<',
|
|
|
+ '>': '>',
|
|
|
+ '"': '"',
|
|
|
+ "'": '''
|
|
|
}
|
|
|
-}).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>
|