Skip to content

Commit fc2f4e3

Browse files
committed
feat: 助手对话markdown显示增强-HTML代码块头部新增复制/预览(新窗口渲染)/下载(html文件)3个按钮(事件委托实现,其他语言代码块保持复制按钮),新增```echart代码块图表渲染(内容为ECharts option JSON,渲染为320px高度图表,解析失败友好提示),引入echarts依赖,MdContent组件升级(对话气泡与最终输出均生效)
1 parent b7b39ba commit fc2f4e3

3 files changed

Lines changed: 96 additions & 9 deletions

File tree

‎JuggleNet6.Frontend/package-lock.json‎

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎JuggleNet6.Frontend/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@vue-flow/minimap": "^1.5.4",
1818
"axios": "^1.13.6",
1919
"docx": "^9.6.1",
20+
"echarts": "^6.1.0",
2021
"element-plus": "^2.13.6",
2122
"file-saver": "^2.0.5",
2223
"highlight.js": "^11.12.0",

‎JuggleNet6.Frontend/src/components/MdContent.vue‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="md-content" v-html="renderedHtml" />
2+
<div class="md-content" v-html="renderedHtml" @click="onCodeAction" />
33
</template>
44

55
<script setup lang="ts">
@@ -10,6 +10,7 @@ import 'highlight.js/styles/github.css'
1010
import katex from 'katex'
1111
import 'katex/dist/katex.min.css'
1212
import mermaid from 'mermaid'
13+
import * as echarts from 'echarts'
1314
1415
const props = defineProps<{ content: string }>()
1516
const renderedHtml = ref('')
@@ -33,47 +34,98 @@ function protectFormulas(text: string): { text: string; blocks: string[] } {
3334
return { text, blocks }
3435
}
3536
36-
/** 渲染 markdown:代码高亮 + mermaid 图表 + 公式占位恢复 */
37+
/** 渲染 markdown:代码高亮(HTML 带复制/预览/下载)+ mermaid/echart 图表 + 公式占位恢复 */
3738
function renderMarkdown(raw: string): string {
38-
const { text, blocks } = protectFormulas(raw)
39-
// mermaid 代码块 → 占位符
39+
let { text, blocks } = protectFormulas(raw)
40+
// echart 代码块 → 图表占位符
41+
const chartBlocks: string[] = []
42+
text = text.replace(/```echart\s*\n([\s\S]+?)```/g, (_m, code: string) => {
43+
chartBlocks.push(code.trim())
44+
return `@@CHART${chartBlocks.length - 1}@@`
45+
})
46+
// mermaid 代码块 → 收集(渲染后追加)
4047
const mermaidBlocks: string[] = []
4148
text.replace(/```mermaid\s*\n([\s\S]+?)```/g, (_m, code: string) => {
4249
mermaidBlocks.push(code.trim())
4350
return ''
4451
})
45-
// marked 渲染(自定义 code renderer 加高亮与复制按钮)
52+
// marked 渲染(自定义 code renderer 加高亮与操作按钮)
4653
const renderer = new marked.Renderer()
4754
renderer.code = ({ text: code, lang }) => {
4855
const langName = (lang || '').split(' ')[0]
4956
let highlighted = ''
5057
try { highlighted = langName && hljs.getLanguage(langName) ? hljs.highlight(code, { language: langName }).value : hljs.highlightAuto(code).value }
5158
catch { highlighted = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') }
52-
return `<div class="code-block"><div class="code-head"><span>${langName || 'code'}</span><button class="copy-btn" data-code="${encodeURIComponent(code)}" onclick="navigator.clipboard.writeText(decodeURIComponent(this.dataset.code));this.textContent='已复制';setTimeout(()=>this.textContent='复制',1500)">复制</button></div><pre><code class="hljs">${highlighted}</code></pre></div>`
59+
const enc = encodeURIComponent(code)
60+
// HTML 代码块:复制/预览/下载;其他语言:仅复制
61+
const actions = langName.toLowerCase() === 'html'
62+
? `<button class="copy-btn" data-action="copy" data-code="${enc}">复制</button><button class="copy-btn" data-action="preview" data-code="${enc}">预览</button><button class="copy-btn" data-action="download" data-code="${enc}">下载</button>`
63+
: `<button class="copy-btn" data-action="copy" data-code="${enc}">复制</button>`
64+
return `<div class="code-block"><div class="code-head"><span>${langName || 'code'}</span><div class="code-actions">${actions}</div></div><pre><code class="hljs">${highlighted}</code></pre></div>`
5365
}
5466
marked.use({ renderer, gfm: true, breaks: true })
5567
let html = marked.parse(text) as string
5668
// 恢复公式占位
5769
blocks.forEach((b, i) => { html = html.replace(`@@BLOCK${i}@@`, b) })
58-
// mermaid 图表
70+
// 恢复 echart 图表占位
71+
chartBlocks.forEach((code, i) => {
72+
html = html.replace(`@@CHART${i}@@`, `<div class="echart-box" data-option="${encodeURIComponent(code)}"></div>`)
73+
})
74+
// mermaid 图表(追加到末尾渲染)
5975
mermaidBlocks.forEach((code, i) => {
6076
const id = `mermaid_${Date.now()}_${i}`
6177
html += `<div class="mermaid" id="${id}">${code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`
6278
})
6379
return html
6480
}
6581
82+
/** 代码块操作按钮(复制/预览/下载,事件委托) */
83+
function onCodeAction(e: MouseEvent) {
84+
const btn = (e.target as HTMLElement).closest?.('[data-action]') as HTMLElement | null
85+
if (!btn) return
86+
const code = decodeURIComponent(btn.dataset.code || '')
87+
const action = btn.dataset.action
88+
if (action === 'copy') {
89+
navigator.clipboard?.writeText(code).then(
90+
() => { btn.textContent = '已复制'; setTimeout(() => { btn.textContent = '复制' }, 1500) },
91+
() => { /* 剪贴板不可用则忽略 */ }
92+
)
93+
} else if (action === 'preview') {
94+
const w = window.open('', '_blank')
95+
if (!w) return
96+
w.document.write(code)
97+
w.document.close()
98+
} else if (action === 'download') {
99+
const blob = new Blob([code], { type: 'text/html' })
100+
const a = document.createElement('a')
101+
a.href = URL.createObjectURL(blob)
102+
a.download = 'preview.html'
103+
a.click()
104+
URL.revokeObjectURL(a.href)
105+
}
106+
}
107+
66108
async function render() {
67109
renderedHtml.value = renderMarkdown(props.content || '')
68110
await nextTick()
69111
// mermaid 渲染
70-
const els = document.querySelectorAll('.mermaid:not([data-processed])')
71-
for (const el of Array.from(els)) {
112+
const mermaidEls = document.querySelectorAll('.mermaid:not([data-processed])')
113+
for (const el of Array.from(mermaidEls)) {
72114
try {
73115
await mermaid.run({ nodes: [el as HTMLElement] })
74116
el.setAttribute('data-processed', '1')
75117
} catch { el.innerHTML = '<span style="color:#f56c6c">图表渲染失败</span>' }
76118
}
119+
// echart 图表渲染(```echart 代码块内容为 ECharts option JSON)
120+
const chartEls = document.querySelectorAll('.echart-box:not([data-processed])')
121+
for (const el of Array.from(chartEls)) {
122+
try {
123+
const optionJson = decodeURIComponent((el as HTMLElement).dataset.option || '{}')
124+
const chart = echarts.init(el as HTMLElement)
125+
chart.setOption(JSON.parse(optionJson))
126+
el.setAttribute('data-processed', '1')
127+
} catch { el.innerHTML = '<span style="color:#f56c6c;padding:8px">图表配置解析失败(```echart 内应为 ECharts option JSON)</span>' }
128+
}
77129
}
78130
79131
watch(() => props.content, render)
@@ -92,10 +144,12 @@ onMounted(render)
92144
.md-content :deep(a) { color: #409eff; }
93145
.code-block { margin: 6px 0; border: 1px solid #e4e7ed; border-radius: 6px; overflow: hidden; }
94146
.code-head { display: flex; justify-content: space-between; align-items: center; padding: 4px 10px; background: #f6f8fa; font-size: 11px; color: #909399; }
147+
.code-actions { display: flex; gap: 6px; }
95148
.copy-btn { border: none; background: #ecf5ff; color: #409eff; font-size: 11px; border-radius: 4px; padding: 2px 8px; cursor: pointer; }
96149
.code-block pre { margin: 0; padding: 8px 10px; overflow-x: auto; background: #fafafa; font-size: 12px; }
97150
.code-block code { font-family: Consolas, Monaco, 'Courier New', monospace; }
98151
.katex-block { overflow-x: auto; padding: 6px 0; }
99152
.md-content :deep(.mermaid) { margin: 8px 0; text-align: center; }
100153
.md-content :deep(.mermaid svg) { max-width: 100%; }
154+
.md-content :deep(.echart-box) { width: 100%; height: 320px; margin: 8px 0; border: 1px solid #f0f2f5; border-radius: 6px; }
101155
</style>

0 commit comments

Comments
 (0)