ソースを参照

优化markdown组件支持外部定义主题

liyanbo 1 ヶ月 前
コミット
a50583ed22
2 ファイル変更309 行追加25 行削除
  1. 81 18
      src/components/MarkdownView/index.vue
  2. 228 7
      src/views/qsfl/components/AiQAChat.vue

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

@@ -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> = {
     '&': '&amp;',
@@ -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;
   }
 
   // 表格内段落样式修正

ファイルの差分が大きいため隠しています
+ 228 - 7
src/views/qsfl/components/AiQAChat.vue


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません