258 lines
5.3 KiB
Markdown
258 lines
5.3 KiB
Markdown
# Markdown渲染Skill
|
||
|
||
## 适用场景
|
||
|
||
当前端页面需要渲染Markdown格式的文本内容时使用,适用于:
|
||
- 日志详情展示
|
||
- 文章内容展示
|
||
- 备注/说明信息展示
|
||
|
||
## 实现方案
|
||
|
||
使用纯JavaScript实现简单的Markdown渲染,无需引入第三方库。
|
||
|
||
### Vue 3 组件实现
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
|
||
const props = defineProps<{
|
||
content?: string
|
||
}>()
|
||
|
||
const renderedContent = computed(() => {
|
||
if (!props.content) return '<p class="empty-content">暂无内容</p>'
|
||
|
||
let content = props.content
|
||
|
||
// 1. 转义HTML特殊字符(安全第一)
|
||
content = content
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
|
||
// 2. 标题(按优先级从高到低)
|
||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||
|
||
// 3. 粗体和斜体
|
||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||
|
||
// 4. 代码块(多行)和行内代码
|
||
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
||
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
||
|
||
// 5. 列表
|
||
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||
|
||
// 6. 段落和换行
|
||
content = content.replace(/\n\n/g, '</p><p>')
|
||
content = content.replace(/\n/g, '<br>')
|
||
|
||
return `<p>${content}</p>`
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="markdown-body" v-html="renderedContent"></div>
|
||
</template>
|
||
```
|
||
|
||
### 样式规范
|
||
|
||
#### 管理后台(Element Plus)
|
||
|
||
```css
|
||
.markdown-body {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: #303133;
|
||
}
|
||
|
||
.markdown-body h1,
|
||
.markdown-body h2,
|
||
.markdown-body h3 {
|
||
margin: 16px 0 8px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.markdown-body h1 {
|
||
font-size: 20px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.markdown-body h2 {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.markdown-body h3 {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.markdown-body ul {
|
||
margin: 8px 0;
|
||
padding-left: 24px;
|
||
}
|
||
|
||
.markdown-body li {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.markdown-body code {
|
||
background-color: #f5f7fa;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-family: 'Courier New', Courier, monospace;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.markdown-body pre {
|
||
background-color: #f5f7fa;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.markdown-body pre code {
|
||
background-color: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.empty-content {
|
||
color: #909399;
|
||
text-align: center;
|
||
}
|
||
```
|
||
|
||
#### 移动端(Vant)
|
||
|
||
```css
|
||
.markdown-body {
|
||
font-size: 14px;
|
||
line-height: 1.8;
|
||
color: #333;
|
||
}
|
||
|
||
.markdown-body h1,
|
||
.markdown-body h2,
|
||
.markdown-body h3 {
|
||
margin: 16px 0 8px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.markdown-body h1 {
|
||
font-size: 18px;
|
||
border-bottom: 1px solid #ebedf0;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.markdown-body h2 {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.markdown-body h3 {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.markdown-body ul {
|
||
margin: 8px 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.markdown-body li {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.markdown-body code {
|
||
background-color: #f5f7fa;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-family: 'Courier New', Courier, monospace;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.markdown-body pre {
|
||
background-color: #f5f7fa;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.markdown-body pre code {
|
||
background-color: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.empty-content {
|
||
color: #969799;
|
||
text-align: center;
|
||
}
|
||
```
|
||
|
||
## 支持的Markdown语法
|
||
|
||
| 语法 | Markdown | 渲染结果 |
|
||
|------|----------|----------|
|
||
| 一级标题 | `# 标题` | `<h1>标题</h1>` |
|
||
| 二级标题 | `## 标题` | `<h2>标题</h2>` |
|
||
| 三级标题 | `### 标题` | `<h3>标题</h3>` |
|
||
| 粗体 | `**文本**` | `<strong>文本</strong>` |
|
||
| 斜体 | `*文本*` | `<em>文本</em>` |
|
||
| 行内代码 | `` `代码` `` | `<code>代码</code>` |
|
||
| 代码块 | ` ```代码块``` ` | `<pre><code>代码块</code></pre>` |
|
||
| 无序列表 | `- 列表项` | `<ul><li>列表项</li></ul>` |
|
||
| 段落换行 | 空行分隔 | `<p>` 标签 |
|
||
| 普通换行 | 单个换行 | `<br>` |
|
||
|
||
## 安全说明
|
||
|
||
**重要**:渲染前必须转义HTML特殊字符(`&`, `<`, `>`),防止XSS攻击。
|
||
|
||
```javascript
|
||
// 必须首先执行
|
||
content = content
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
```
|
||
|
||
## 使用示例
|
||
|
||
### 日志详情弹窗
|
||
|
||
```vue
|
||
<template>
|
||
<el-dialog v-model="visible" width="700px">
|
||
<template #header>
|
||
<div class="log-detail-header">
|
||
<div class="header-left">
|
||
<span class="log-date">{{ logDate }}</span>
|
||
<span class="log-title">{{ title }}</span>
|
||
</div>
|
||
<span class="log-user">{{ userName }}</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="log-detail-content">
|
||
<div class="markdown-body" v-html="renderedContent"></div>
|
||
</div>
|
||
</el-dialog>
|
||
</template>
|
||
```
|
||
|
||
## 注意事项
|
||
|
||
1. **性能考虑**:对于大量内容的渲染,考虑使用`v-memo`或虚拟滚动
|
||
2. **代码高亮**:如需语法高亮,可额外集成highlight.js或prism.js
|
||
3. **扩展语法**:当前实现为基础语法,如需表格、引用等可按需扩展
|