|
|
@@ -1,5 +1,5 @@
|
|
|
<template>
|
|
|
- <div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
|
|
|
+ <div ref="contentRef" class="markdown-view" :class="`markdown-view--${props.theme}`" v-html="renderedMarkdown" @click="handleCitationClick"></div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
@@ -12,9 +12,20 @@ const props = defineProps({
|
|
|
content: {
|
|
|
type: String,
|
|
|
required: true
|
|
|
+ },
|
|
|
+ citationIds: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ theme: {
|
|
|
+ type: String,
|
|
|
+ default: 'light',
|
|
|
+ validator: (value: string) => ['light', 'dark'].includes(value)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+const emit = defineEmits(['citation-click'])
|
|
|
+
|
|
|
const contentRef = ref()
|
|
|
|
|
|
/**
|
|
|
@@ -157,6 +168,31 @@ const md = new MarkdownIt({
|
|
|
// ====== 关键修复:正确启用表格 ======
|
|
|
md.enable('table')
|
|
|
|
|
|
+md.inline.ruler.before('text', 'citation_mark', (state, silent) => {
|
|
|
+ const match = /^\[S(\d+)]/.exec(state.src.slice(state.pos, state.posMax))
|
|
|
+ const citationIds = Array.isArray(state.env.citationIds) ? state.env.citationIds : []
|
|
|
+ if (!match || !citationIds.includes(Number(match[1]))) return false
|
|
|
+ if (silent) return true
|
|
|
+
|
|
|
+ const token = state.push('citation_mark', '', 0)
|
|
|
+ token.meta = { id: Number(match[1]) }
|
|
|
+ token.content = match[0]
|
|
|
+ state.pos += match[0].length
|
|
|
+ return true
|
|
|
+})
|
|
|
+
|
|
|
+md.renderer.rules.citation_mark = (tokens, idx) => {
|
|
|
+ const id = tokens[idx].meta.id
|
|
|
+ return `<button class="citation-mark" type="button" data-citation-id="${id}">${tokens[idx].content}</button>`
|
|
|
+}
|
|
|
+
|
|
|
+function handleCitationClick(event: MouseEvent) {
|
|
|
+ const target = event.target as HTMLElement | null
|
|
|
+ const mark = target?.closest<HTMLElement>('[data-citation-id]')
|
|
|
+ const segmentId = Number(mark?.dataset.citationId)
|
|
|
+ if (Number.isFinite(segmentId)) emit('citation-click', segmentId)
|
|
|
+}
|
|
|
+
|
|
|
function escapeHtml(text: string): string {
|
|
|
const map: Record<string, string> = {
|
|
|
'&': '&',
|
|
|
@@ -194,7 +230,7 @@ const renderedMarkdown = computed(() => {
|
|
|
try {
|
|
|
// 先预处理修复表格格式,再渲染
|
|
|
const normalizedContent = normalizeMarkdownTables(props.content)
|
|
|
- return md.render(normalizedContent)
|
|
|
+ return md.render(normalizedContent, { citationIds: props.citationIds })
|
|
|
} catch (e) {
|
|
|
console.error('Markdown 渲染失败:', e)
|
|
|
return props.content
|
|
|
@@ -204,13 +240,40 @@ const renderedMarkdown = computed(() => {
|
|
|
|
|
|
<style lang="scss">
|
|
|
.markdown-view {
|
|
|
+ --md-text: #000;
|
|
|
+ --md-heading: #000;
|
|
|
+ --md-table-wrap-bg: rgba(15, 23, 42, .02);
|
|
|
+ --md-table-border: rgba(15, 23, 42, .18);
|
|
|
+ --md-table-head-bg: #edf3fa;
|
|
|
+ --md-table-head-text: #000;
|
|
|
+ --md-table-cell-text: #000;
|
|
|
+ --md-table-row-even: rgba(15, 23, 42, .035);
|
|
|
+ --md-table-row-hover: rgba(74, 137, 231, .1);
|
|
|
+ --md-scroll-track: rgba(15, 23, 42, .05);
|
|
|
+ --md-scroll-thumb: rgba(74, 137, 231, .35);
|
|
|
+ --md-scroll-thumb-hover: rgba(74, 137, 231, .55);
|
|
|
font-family: 'SourceHanSansCN-Normal';
|
|
|
font-weight: 400;
|
|
|
letter-spacing: 0em;
|
|
|
text-align: left;
|
|
|
- color: #e9f1ff;
|
|
|
+ color: var(--md-text);
|
|
|
max-width: 100%;
|
|
|
|
|
|
+ &.markdown-view--dark {
|
|
|
+ --md-text: #f0f5ff;
|
|
|
+ --md-heading: #f0f5ff;
|
|
|
+ --md-table-wrap-bg: rgba(255, 255, 255, .02);
|
|
|
+ --md-table-border: rgba(174, 205, 255, .25);
|
|
|
+ --md-table-head-bg: rgba(74, 137, 231, .4);
|
|
|
+ --md-table-head-text: #fff;
|
|
|
+ --md-table-cell-text: #e8f0ff;
|
|
|
+ --md-table-row-even: rgba(255, 255, 255, .03);
|
|
|
+ --md-table-row-hover: rgba(74, 137, 231, .1);
|
|
|
+ --md-scroll-track: rgba(255, 255, 255, .04);
|
|
|
+ --md-scroll-thumb: rgba(74, 137, 231, .35);
|
|
|
+ --md-scroll-thumb-hover: rgba(74, 137, 231, .55);
|
|
|
+ }
|
|
|
+
|
|
|
pre {
|
|
|
position: relative;
|
|
|
}
|
|
|
@@ -247,7 +310,7 @@ const renderedMarkdown = computed(() => {
|
|
|
}
|
|
|
|
|
|
h1, h2, h3, h4, h5, h6 {
|
|
|
- color: var(--color-G900);
|
|
|
+ color: var(--md-heading);
|
|
|
margin: 24px 0 8px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
@@ -264,7 +327,7 @@ const renderedMarkdown = computed(() => {
|
|
|
padding: 0;
|
|
|
font-size: 16px;
|
|
|
line-height: 24px;
|
|
|
- color: #3b3e55;
|
|
|
+ color: var(--md-text);
|
|
|
}
|
|
|
|
|
|
li {
|
|
|
@@ -283,7 +346,7 @@ const renderedMarkdown = computed(() => {
|
|
|
line-height: 24px;
|
|
|
margin-right: 11px;
|
|
|
margin-bottom: 1rem;
|
|
|
- color: #3b3e55;
|
|
|
+ color: var(--md-text);
|
|
|
}
|
|
|
|
|
|
ol ul, ol ul > li, ul ul, ul ul li {
|
|
|
@@ -303,23 +366,23 @@ const renderedMarkdown = computed(() => {
|
|
|
overflow-x: auto;
|
|
|
margin: 16px 0;
|
|
|
border-radius: 8px;
|
|
|
- border: 1px solid rgba(174, 205, 255, 0.25) !important;
|
|
|
+ border: 1px solid var(--md-table-border) !important;
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
- background: rgba(255, 255, 255, 0.02);
|
|
|
+ background: var(--md-table-wrap-bg);
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
height: 6px;
|
|
|
}
|
|
|
&::-webkit-scrollbar-track {
|
|
|
- background: rgba(255, 255, 255, 0.04);
|
|
|
+ background: var(--md-scroll-track);
|
|
|
border-radius: 3px;
|
|
|
}
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
- background: rgba(74, 137, 231, 0.35);
|
|
|
+ background: var(--md-scroll-thumb);
|
|
|
border-radius: 3px;
|
|
|
}
|
|
|
&::-webkit-scrollbar-thumb:hover {
|
|
|
- background: rgba(74, 137, 231, 0.55);
|
|
|
+ background: var(--md-scroll-thumb-hover);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -350,11 +413,11 @@ const renderedMarkdown = computed(() => {
|
|
|
|
|
|
th {
|
|
|
display: table-cell !important;
|
|
|
- background: rgba(74, 137, 231, 0.4) !important;
|
|
|
- color: #ffffff !important;
|
|
|
+ background: var(--md-table-head-bg) !important;
|
|
|
+ color: var(--md-table-head-text) !important;
|
|
|
font-weight: 600 !important;
|
|
|
padding: 10px 14px !important;
|
|
|
- border: 1px solid rgba(174, 205, 255, 0.3) !important;
|
|
|
+ border: 1px solid var(--md-table-border) !important;
|
|
|
text-align: left !important;
|
|
|
white-space: nowrap;
|
|
|
position: relative;
|
|
|
@@ -362,9 +425,9 @@ const renderedMarkdown = computed(() => {
|
|
|
|
|
|
td {
|
|
|
display: table-cell !important;
|
|
|
- color: #e0ebff !important;
|
|
|
+ color: var(--md-table-cell-text) !important;
|
|
|
padding: 10px 14px !important;
|
|
|
- border: 1px solid rgba(174, 205, 255, 0.2) !important;
|
|
|
+ border: 1px solid var(--md-table-border) !important;
|
|
|
vertical-align: top !important;
|
|
|
text-align: left !important;
|
|
|
word-break: break-word;
|
|
|
@@ -373,7 +436,7 @@ const renderedMarkdown = computed(() => {
|
|
|
|
|
|
// 表格斑马纹效果
|
|
|
tr:nth-child(even) td {
|
|
|
- background: rgba(255, 255, 255, 0.03) !important;
|
|
|
+ background: var(--md-table-row-even) !important;
|
|
|
}
|
|
|
|
|
|
tr:nth-child(odd) td {
|
|
|
@@ -382,7 +445,7 @@ const renderedMarkdown = computed(() => {
|
|
|
|
|
|
// 表格hover效果
|
|
|
tr:hover td {
|
|
|
- background: rgba(74, 137, 231, 0.1) !important;
|
|
|
+ background: var(--md-table-row-hover) !important;
|
|
|
}
|
|
|
|
|
|
// 表格内段落样式修正
|