1.1 需求
报表里的数据太多,用户很难一眼看出重点,也不方便快速做分析。
为了提升效率,在报表页面加入 AI 分析功能,让用户点击一下就能生成数据概览报告,快速了解当前筛选条件下的核心结论和趋势。
1.2 方案
前端只负责展示,后端调用大模型生成分析结果
流程:
- 前端只传递报表筛选参数给后端
- 后端查询数据,拼接提示词后调用大模型(要求结果以 markdown 格式返回)
- 前端将 markdown 结果转换成 html,最终渲染出来
优点:安全,Prompt 不会被篡改
缺点:接口响应较慢(未来改成流式输出的形式解决)
为什么 markdown 要转换成 html 再处理?
因为 html 能自定义结构和样式,和组件结合,方便扩展功能。
1.3 代码
(实际开发左侧输入就是后端返回的内容,可以调试样式和功能)
<template>
<div class="markdown-container">
<div class="editor-pane">
<h3>Markdown 输入</h3>
<textarea v-model="rawMarkdown" class="markdown-textarea"></textarea>
</div>
<div class="preview-pane">
<div class="controls">
<h3>渲染输出</h3>
<div>
<button @click="handleCopy">复制为富文本</button>
<button @click="handleExportPdf">下载为 PDF</button>
</div>
</div>
<div ref="mdContentRef" class="markdown-body" v-html="safeHtml"></div>
</div>
</div>
</template>
<script setup lang="ts">
// --- 1. 导入依赖 ---
import { ref, computed } from 'vue';
// 将 Markdown 解析为 HTML 的核心库
import MarkdownIt from 'markdown-it';
// 用于净化 HTML 的库,可防止在渲染用户生成内容时发生 XSS 攻击
import sanitizeHtml from 'sanitize-html';
// 用于代码块语法高亮的库
import hljs from 'highlight.js';
// 用于将 HTML 元素导出为 PDF 文件的库
import html2pdf from 'html2pdf.js';
// 导入代码高亮和 Markdown 渲染所需的样式表
import 'highlight.js/styles/github.css';
import 'github-markdown-css/github-markdown.css';
// --- 2. 状态管理 (响应式变量) ---
/**
* 一个 ref, 用于存储原始的 Markdown 字符串。
* 它可以通过文本域进行编辑,任何更改都会触发预览区的重新渲染。
* 包含多种 Markdown 元素以展示所有功能。
*/
const rawMarkdown = ref<string>(
`# 医院业务运营分析报告
## 1. 执行摘要
本报告概述了上一季度的关键运营指标。我们的患者就诊量增长了 **15%**,但每次就诊的平均收入下降了 **5%**。
---
## 2. 关键指标
| 指标 | 2025年第二季度 | 2025年第三季度 | 变化 |
| :--- | :---: | :---: | :---: |
| 总患者就诊量 | 12,500 | 14,375 | +15% |
| 平均每次就诊收入 | ¥1,050 | ¥997.50 | -5% |
| 床位占用率 | 85% | 92% | +7% |
## 3. 科室表现
### 3.1. 门诊部
门诊部的就诊量增长最为显著。
- **心脏科**: 增长 20%
- **儿科**: 增长 12%
- *骨科*: 增长 8%
### 3.2. 住院服务
床位占用率很高,表明有扩建需求。更多信息请访问 [https://google.com](https://google.com)。
## 4. 数据处理代码示例
这是用于本次分析的 Python 代码片段:
\`\`\`python
import pandas as pd
def analyze_data(file_path):
df = pd.read_csv(file_path)
# 计算关键指标
total_visits = df['visits'].sum()
print(f"总就诊量: {total_visits}")
return total_visits
analyze_data('hospital_data.csv')
\`\`\`
`
);
/**
* 一个模板 ref,用于获取对渲染后 HTML 容器 DOM 元素的直接引用。
* 这对于 PDF 导出和复制功能是必需的,因为它们需要操作真实的 DOM。
*/
const mdContentRef = ref<HTMLDivElement | null>(null);
// --- 3. MARKDOWN 解析器配置 ---
/**
* markdown-it 解析器的实例。
* 它在组件 setup 阶段只定义一次,因为其配置是静态的,
* 无需将其设为响应式的 'ref'。
*/
const mdParser = new MarkdownIt({
html: true, // 启用源文件中的 HTML 标签。后续会进行安全净化。
linkify: true, // 将类似 URL 的文本自动转换为链接。
typographer: true, // 启用智能引号和其他排版优化。
breaks: true, // 将段落中的 '\n' 转换成 <br> 标签。
/**
* 用于代码块的语法高亮函数。
* 它集成了 highlight.js 来应用样式。
* @param str 代码块内的代码字符串。
* @param lang 指定的语言 (例如, \`\`\`javascript)。
*/
highlight: (str: string, lang: string) => {
if (lang && hljs.getLanguage(lang)) {
try {
// 返回高亮后的 HTML,并用 <pre><code> 块包裹
return `<pre class="hljs"><code>${
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
}</code></pre>`;
} catch (err) {
console.error('代码高亮失败:', err);
}
}
// 如果没有指定语言或发生错误,则将代码作为纯文本返回,
// 并进行正确的 HTML 转义,以防止其被解析为 HTML。
return `<pre class="hljs"><code>${mdParser.utils.escapeHtml(str)}</code></pre>`;
},
});
// --- 4. 用于安全渲染 HTML 的计算属性 ---
/**
* 一个计算属性,它能安全地将原始 Markdown 转换为经过净化的 HTML。
* 每当 'rawMarkdown' 的值发生变化时,它都会自动重新计算。
*/
const safeHtml = computed<string>(() => {
if (!rawMarkdown.value) return '';
// 步骤 1: 将 Markdown 字符串解析为原始 HTML 字符串。
const rawHtml = mdParser.render(rawMarkdown.value);
// 步骤 2: 净化原始 HTML 以防止 XSS 跨站脚本攻击。
// 如果 Markdown 输入可能来自不受信任的源,这一步至关重要。
return sanitizeHtml(rawHtml, {
// 定义允许的 HTML 标签白名单。
allowedTags: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'ul',
'ol',
'li',
'strong',
'em',
'del',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'pre',
'code',
'a',
'br',
'hr',
'blockquote',
],
// 为特定标签定义允许的属性白名单。
allowedAttributes: {
a: ['href', 'target', 'rel'],
// highlight.js 需要 'class' 属性来为代码块设置样式。
pre: ['class'],
code: ['class'],
th: ['align'], // 允许表格头部对齐。
td: ['align'], // 允许表格单元格对齐。
},
// 定义允许的 URL 协议,以防止 'javascript:...' 等恶意链接。
allowedSchemes: ['http', 'https', 'mailto'],
/**
* 一个在标签净化后对其进行转换的函数。
* 在这里,我们用它来为所有外部链接添加安全属性。
*/
transformTags: {
a: (tagName, attribs) => {
// 检查链接是否为外部链接 (以 http 开头)。
if (attribs.href && attribs.href.startsWith('http')) {
attribs.target = '_blank'; // 在新标签页中打开。
attribs.rel = 'noopener noreferrer'; // 防止安全漏洞。
}
return { tagName, attribs };
},
},
});
});
// --- 5. 事件处理器 (用户操作) ---
/**
* 处理"复制为富文本"按钮的点击事件。
* 使用现代 Clipboard API 将渲染后的 HTML 复制到剪贴板,同时保留其格式。
*/
const handleCopy = async () => {
if (!mdContentRef.value) {
alert('渲染内容未找到。');
return;
}
try {
const html = mdContentRef.value.innerHTML;
const plainText = mdContentRef.value.textContent || '';
// 使用现代 Clipboard API 进行复制
if (navigator.clipboard && window.ClipboardItem) {
// 创建包含富文本和纯文本的 ClipboardItem
const clipboardItem = new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' }),
});
await navigator.clipboard.write([clipboardItem]);
} else {
// 降级方案:使用传统的 execCommand 方法
const tempDiv = document.createElement('div');
tempDiv.style.position = 'fixed';
tempDiv.style.left = '-9999px';
tempDiv.style.top = '0';
tempDiv.style.width = '800px';
tempDiv.innerHTML = html;
document.body.appendChild(tempDiv);
const range = document.createRange();
range.selectNodeContents(tempDiv);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
const success = document.execCommand('copy');
if (!success) throw new Error('复制命令失败。');
document.body.removeChild(tempDiv);
if (selection) {
selection.removeAllRanges();
}
}
alert('内容复制成功!');
} catch (err) {
console.error('复制内容失败:', err);
alert('复制失败,请重试。');
}
};
/**
* 处理"下载为 PDF"按钮的点击事件。
* 使用 html2pdf.js 将渲染后的内容转换为 PDF 文件并触发下载。
*/
const handleExportPdf = () => {
const contentElement = mdContentRef.value;
if (!contentElement) {
alert('渲染内容未找到。');
return;
}
try {
const filename = 'AI分析报告.pdf';
// html2pdf 配置选项
const options = {
margin: 15, // 统一边距 (单位: 毫米)
filename: filename,
image: {
type: 'jpeg' as const,
quality: 0.95, // 提高图片质量
},
html2canvas: {
scale: 3, // 更高的缩放比例以获得更好的分辨率
useCORS: true,
backgroundColor: '#ffffff', // 设置白色背景
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'portrait' as const,
},
};
// 链式调用来设置选项、指定源元素并保存 PDF。
html2pdf().set(options).from(contentElement).save();
// 注意:下载会在处理后开始,这可能需要一点时间。
// 在实际应用中,可以用一个更复杂的“加载中”状态来代替。
// alert('PDF 导出流程已开始,您的下载稍后会自动开始。');
} catch (err) {
console.error('PDF 导出失败:', err);
alert('PDF 导出失败,请重试。');
}
};
</script>
<style scoped>
/* 主容器 Flexbox 布局 */
.markdown-container {
display: flex;
gap: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
height: 90vh; /* 使其占据大部分视口高度 */
padding: 20px;
}
/* 编辑区和预览区样式 */
.editor-pane,
.preview-pane {
flex: 1; /* 每个区域占据一半宽度 */
display: flex;
flex-direction: column;
overflow: hidden; /* 隐藏溢出内容 */
border: 1px solid #d0d7de;
border-radius: 6px;
}
.editor-pane h3,
.preview-pane .controls {
padding: 10px 16px;
margin: 0;
background-color: #f6f8fa;
border-bottom: 1px solid #d0d7de;
font-size: 16px;
}
.preview-pane .controls {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0; /* 防止控制栏收缩 */
}
/* Markdown 输入文本域样式 */
.markdown-textarea {
flex-grow: 1;
padding: 16px;
border: none;
resize: none;
outline: none;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
line-height: 1.5;
}
/* 渲染输出区域样式 */
.markdown-body {
padding: 20px;
overflow-y: auto; /* 允许长内容滚动 */
flex-grow: 1;
}
/* 操作按钮样式 */
.controls button {
margin-left: 10px;
padding: 5px 12px;
font-size: 14px;
cursor: pointer;
border: 1px solid #d0d7de;
border-radius: 6px;
background-color: #f6f8fa;
}
.controls button:hover {
background-color: #e9edf7;
}
</style>
1.4 结束
2025/10/03:发布文章