vue3实现markdown文档转HTML
安装marked
npm install marked
<template>
<!-- 后台可添加样式编辑器 -->
<div class="markdown-editor" :class="{ 'fullscreen': isFullscreen, 'preview-mode': isPreviewMode }">
<div class="editor-container">
<!-- Markdown 输入区域 -->
<div class="markdown-input" v-show="!isPreviewMode">
<el-card class="editor-card">
<template #header>
<div class="header">
<div class="toolbar">
<!-- 导出文件 -->
<el-button-group>
<el-button size="small" @click="exportFile" title="导出文件">
<el-icon>
<Download />
</el-icon>
</el-button>
</el-button-group>
<!-- 格式化工具 -->
<el-button-group>
<el-button size="small" @click="insertMarkdown('heading')" title="标题">
<strong>H</strong>
</el-button>
<el-button size="small" @click="insertMarkdown('bold')" title="加粗">
<strong>B</strong>
</el-button>
<el-button size="small" @click="insertMarkdown('italic')" title="斜体">
<em>I</em>
</el-button>
<el-button size="small" @click="insertMarkdown('quote')" title="引用">
<el-icon>
<ChatLineSquare />
</el-icon>
</el-button>
<el-button size="small" @click="insertMarkdown('code')" title="代码">
<el-icon>
<Notebook />
</el-icon>
</el-button>
<el-button size="small" @click="insertMarkdown('link')" title="链接">
<el-icon>
<Link />
</el-icon>
</el-button>
<el-button size="small" @click="insertMarkdown('image')" title="图片">
<el-icon>
<Picture />
</el-icon>
</el-button>
<el-button size="small" @click="insertMarkdown('list')" title="列表">
<el-icon>
<List />
</el-icon>
</el-button>
</el-button-group>
<!-- 预览控制 -->
<el-button-group>
<el-button size="small" @click="togglePreview" title="预览模式" :type="isPreviewMode ? 'primary' : ''"
class="preview-button">
<el-icon>
<View />
</el-icon>
预览
</el-button>
</el-button-group>
</div>
</div>
</template>
<el-input v-model="markdownContent" type="textarea" :rows="20" placeholder="请输入 Markdown 内容..."
@input="handleContentChange" />
</el-card>
</div>
<!-- HTML 预览区域 -->
<div class="preview-container">
<el-card class="preview-card">
<template #header>
<div class="header">
<div>
<span v-show="!isPreviewMode">HTML 预览</span>
<el-button v-show="isPreviewMode" size="small" @click="togglePreview" title="退出预览"
class="preview-exit-button">
<el-icon>
<Close />
</el-icon>
退出预览
</el-button>
</div>
<div style="display: flex; gap: 10px;align-items: center;">
<!-- 预览页面导出为图片 -->
<!-- <div class="export-img">
<el-button size="small" @click="exportImg" title="下载png图片">
<el-icon>
<Download />
</el-icon>
</el-button>
</div> -->
</div>
</div>
</template>
<div class="preview-content">
<div class="card" ref="card">
<div class="card-header">
</div>
<div class="card-content">
<div class="card-content-inner" v-html="htmlContent"></div>
</div>
<div class="card-footer"></div>
</div>
</div>
</el-card>
</div>
<div>
<!-- 样式选择区 -->
<el-card class="style-card">
<template #header>
<div class="header">
<span>style 选择</span>
<el-button @click="handleAddStyle" size="small" title="添加样式"
class="preview-exit-button">
<el-icon>
<Close />
</el-icon>
添加样式
</el-button>
</div>
</template>
<!-- 样式列表 -->
<div class="style-content">
<div
v-for="style in styleList"
:key="style.value"
class="style-item"
:class="{ 'active': currentStyle === style.value }"
@click="setStyle(style.value)"
>
{{ style.name }}
</div>
</div>
</el-card>
</div>
</div>
<!-- 添加样式弹窗 -->
<AddStyleDialog ref="addStyleDialogRef" @confirm="handleStyleConfirm" @preview="handleStylePreview"></AddStyleDialog>
</div>
</template>
<script setup lang="ts">
import AddStyleDialog from './components/AddStyleDialog.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, onMounted, reactive,onUnmounted } from 'vue'
import { marked } from 'marked'
import {
Download, ChatLineSquare, Notebook, Link,
Picture, List, View, FullScreen
} from '@element-plus/icons-vue'
const addStyleDialogRef = ref()
const editorContainerRef = ref<HTMLElement>()
const addedStyles = ref<{name: string; element: HTMLStyleElement}[]>([])
const handleAddStyle = () => {
addStyleDialogRef.value?.open()
}
// 确认添加样式
const handleStyleConfirm = () => {
}
// 预览样式
const handleStylePreview = (styleData: { name: string; code: string }) => {
currentStyle.value = ''
// 移除所有旧的样式标签
const oldStyles = document.querySelectorAll('style[data-md-style]')
oldStyles.forEach(style => style.remove())
// 创建新的样式标签
const styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.setAttribute('data-md-style', 'true') // 添加标识,方便后续删除
// 根据选择的样式设置内容
styleTag.innerHTML = styleData.code
// 插入到页面头部
document.head.appendChild(styleTag)
}
// 样式列表
const styleList = [
{
name: '样式1',
value: 'style1'
},
{
name: '样式2',
value: 'style2'
}
]
// 编辑器状态
const markdownContent = ref('')
const currentStyle = ref('style1') // 默认选择样式1
const htmlContent = ref('')
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const card: any = ref('')
const isPreviewMode = ref(false)
const isFullscreen = ref(false)
// 编辑历史
const history = reactive({
past: [] as string[],
future: [] as string[]
})
// 文件操作函数
const exportFile = () => {
const blob = new Blob([markdownContent.value], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'markdown.md'
a.click()
URL.revokeObjectURL(url)
}
// 编辑历史操作
const saveHistory = () => {
history.past.push(markdownContent.value)
history.future = []
if (history.past.length > 50) {
history.past.shift()
}
}
// 预览控制
const togglePreview = () => {
isPreviewMode.value = !isPreviewMode.value
}
const toggleFullscreen = () => {
const element = document.documentElement
if (!isFullscreen.value) {
if (element.requestFullscreen) {
element.requestFullscreen()
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
isFullscreen.value = !isFullscreen.value
}
let cssData1 = `
.card {
max-width: 420px;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
line-height: 1.65;
color: #333;
margin: 24px auto;
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.card-header {
background: #ff2442;
height: 6px;
border-radius: 16px 16px 0 0;
}
.card-content {
padding: 32px;
}
.card-content-inner {
padding: 0;
}
.card-content-inner > *:first-child {
margin-top: 0;
}
.card-content-inner > *:last-child {
margin-bottom: 0;
}
.card-content-inner h1 {
font-size: 24px;
font-weight: 700;
margin: 0 0 24px;
color: #1a1a1a;
letter-spacing: -0.2px;
line-height: 1.4;
position: relative;
padding-bottom: 16px;
}
.card-content-inner h1:after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 36px;
height: 3px;
background: #ff2442;
border-radius: 2px;
}
.card-content-inner h2 {
font-size: 20px;
font-weight: 600;
margin: 32px 0 20px;
color: #2c2c2c;
padding-bottom: 8px;
border-bottom: 1px solid #f5f5f5;
}
.card-content-inner p {
font-size: 16px;
margin: 0 0 24px;
color: #444;
text-align: justify;
hyphens: auto;
}
.card-content-inner ol,
.card-content-inner ul {
padding-left: 24px;
margin: 0 0 24px;
}
.card-content-inner ol li,
.card-content-inner ul li {
margin-bottom: 12px;
padding-left: 8px;
}
.card-content-inner ol li {
position: relative;
counter-increment: list-counter;
}
.card-content-inner ol li::before {
content: counter(list-counter);
position: absolute;
left: -26px;
top: 2px;
width: 20px;
height: 20px;
background: #ffebee;
color: #ff2442;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.card-content-inner ul li::before {
content: "•";
color: #ff2442;
font-weight: bold;
display: inline-block;
width: 1em;
margin-left: -1em;
}
.card-content-inner strong {
color: #ff2442;
font-weight: 600;
}
.card-content-inner a {
color: #ff2442;
text-decoration: none;
border-bottom: 1px solid rgba(255, 36, 66, 0.3);
transition: all 0.2s ease;
}
.card-content-inner a:hover {
color: #e01e3c;
border-bottom-color: #e01e3c;
}
.card-content-inner code {
background: #fff0f2;
padding: 2px 6px;
border-radius: 4px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 14px;
color: #ff2442;
}
.card-content-inner pre {
background: #fffafb;
padding: 18px;
border-radius: 8px;
overflow-x: auto;
margin: 0 0 24px;
font-size: 14px;
line-height: 1.5;
border-left: 3px solid #ff2442;
}
.card-content-inner pre code {
background: none;
padding: 0;
color: #444;
font-size: 14px;
}
.card-content-inner blockquote {
border-left: 3px solid #ffcdd2;
padding: 4px 20px 4px 20px;
margin: 0 0 24px;
color: #666;
background: #fffafa;
border-radius: 0 8px 8px 0;
font-style: italic;
}
.card-content-inner hr {
border: 0;
height: 1px;
background: linear-gradient(to right, rgba(255, 36, 66, 0.1), transparent);
margin: 32px 0;
}
.card-footer {
padding: 16px 32px;
background: #fffafa;
border-top: 1px solid #f9f0f0;
color: #999;
font-size: 13px;
display: flex;
justify-content: space-between;
}
/* 小红书特色元素 */
.card-content-inner h1 + p {
font-size: 17px;
color: #666;
margin-top: -8px;
margin-bottom: 28px;
}
.card-content-inner img {
max-width: 100%;
border-radius: 12px;
margin: 24px 0;
display: block;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
/* 留白增强 */
.card-content-inner p + h2 {
margin-top: 36px;
}
.card-content-inner ul + h2,
.card-content-inner ol + h2 {
margin-top: 40px;
}
`
let cssData2 = `
.card {
max-width: 680px;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.06);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
line-height: 1.7;
color: #333;
margin: 40px auto;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-3px);
}
.card-header {
background: linear-gradient(135deg, #6e8efb, #a777e3);
height: 6px;
border-radius: 12px 12px 0 0;
}
.card-content {
padding: 40px;
}
.card-content-inner {
padding: 0;
}
.card-content-inner > *:first-child {
margin-top: 0;
}
.card-content-inner > *:last-child {
margin-bottom: 0;
}
.card-content-inner h1 {
font-size: 28px;
font-weight: 700;
margin: 0 0 30px;
color: #1a1a1a;
letter-spacing: -0.01em;
line-height: 1.3;
}
.card-content-inner h2 {
font-size: 22px;
font-weight: 600;
margin: 40px 0 20px;
color: #2c2c2c;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.card-content-inner h3 {
font-size: 18px;
font-weight: 600;
margin: 35px 0 15px;
color: #3a3a3a;
}
.card-content-inner p {
font-size: 17px;
margin: 0 0 28px;
color: #444;
text-align: justify;
hyphens: auto;
}
.card-content-inner ol,
.card-content-inner ul {
padding-left: 24px;
margin: 0 0 28px;
}
.card-content-inner ol li,
.card-content-inner ul li {
margin-bottom: 12px;
padding-left: 12px;
}
.card-content-inner ol li {
position: relative;
counter-increment: list-counter;
}
.card-content-inner ol li::before {
content: counter(list-counter);
position: absolute;
left: -24px;
top: 0;
width: 24px;
height: 24px;
background: #f5f7ff;
color: #6e8efb;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 500;
}
.card-content-inner ul li::before {
content: "•";
color: #a777e3;
font-weight: bold;
display: inline-block;
width: 1em;
margin-left: -1em;
}
.card-content-inner strong {
color: #2c2c2c;
font-weight: 600;
}
.card-content-inner em {
font-style: italic;
color: #555;
}
.card-content-inner a {
color: #6e8efb;
text-decoration: none;
border-bottom: 1px solid rgba(110, 142, 251, 0.3);
transition: all 0.2s ease;
}
.card-content-inner a:hover {
color: #a777e3;
border-bottom-color: #a777e3;
}
.card-content-inner code {
background: #f8f9ff;
padding: 3px 6px;
border-radius: 4px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 15px;
color: #6e8efb;
}
.card-content-inner pre {
background: #f8f9ff;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
margin: 0 0 30px;
font-size: 15px;
line-height: 1.5;
border-left: 3px solid #a777e3;
}
.card-content-inner pre code {
background: none;
padding: 0;
color: #444;
font-size: 15px;
}
.card-content-inner blockquote {
border-left: 3px solid #e0e0e0;
padding: 4px 20px 4px 24px;
margin: 0 0 30px;
color: #555;
font-style: italic;
background: #fafbff;
border-radius: 0 8px 8px 0;
}
.card-content-inner hr {
border: 0;
height: 1px;
background: #f0f0f0;
margin: 40px 0;
}
.card-footer {
padding: 20px 40px;
background: #fafbff;
border-top: 1px solid #f0f0f0;
color: #777;
font-size: 14px;
display: flex;
justify-content: space-between;
}
/* 留白增强 */
.card-content-inner p + h2,
.card-content-inner ul + h2,
.card-content-inner ol + h2 {
margin-top: 50px;
}
.card-content-inner p + h3 {
margin-top: 40px;
}
.card-content-inner img {
max-width: 100%;
border-radius: 8px;
margin: 30px 0;
display: block;
}
`
const setStyle = (e:any) => {
currentStyle.value = e
// 移除所有旧的样式标签
const oldStyles = document.querySelectorAll('style[data-md-style]')
oldStyles.forEach(style => style.remove())
// 创建新的样式标签
const styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.setAttribute('data-md-style', 'true') // 添加标识,方便后续删除
// 根据选择的样式设置内容
styleTag.innerHTML = currentStyle.value === 'style1' ? cssData1 : cssData2
// 插入到页面头部
document.head.appendChild(styleTag)
}
// 配置marked选项
marked.setOptions({
breaks: true, // 将回车转换为 <br>
gfm: true, // 启用 GitHub 风格的 Markdown
// sanitize: false, // 允许HTML标签
})
// 使用marked进行Markdown转HTML
const convertMarkdownToHtml = (markdown: string): any => {
return marked(markdown)
}
// 处理内容变化
const handleContentChange = () => {
saveHistory()
updatePreview()
}
// 更新预览
const updatePreview = () => {
htmlContent.value = convertMarkdownToHtml(markdownContent.value)
}
// 在光标位置插入Markdown语法
const insertMarkdown = (type: string) => {
const textarea = document.querySelector('.el-textarea__inner') as HTMLTextAreaElement
if (!textarea) return
const start = textarea.selectionStart
const end = textarea.selectionEnd
const selected = markdownContent.value.substring(start, end)
let insertion = ''
switch (type) {
case 'bold':
insertion = `**${selected || '粗体文本'}**`
break
case 'italic':
insertion = `*${selected || '斜体文本'}*`
break
case 'heading':
insertion = `\n## ${selected || '标题'}\n`
break
case 'link':
insertion = `[${selected || '链接文本'}](https://example.com)`
break
case 'list':
insertion = `\n- ${selected || '列表项'}\n- 列表项\n- 列表项\n`
break
case 'quote':
insertion = `\n> ${selected || '引用文本'}\n`
break
case 'code':
insertion = selected ?
`\`\`\`\n${selected}\n\`\`\`` :
`\`\`\`\n代码块\n\`\`\``
break
case 'image':
insertion = ``
break
default:
return
}
const newContent =
markdownContent.value.substring(0, start) +
insertion +
markdownContent.value.substring(end)
markdownContent.value = newContent
// 保存历史记录
saveHistory()
// 更新预览
updatePreview()
// 聚焦回文本区域
setTimeout(() => {
textarea.focus()
const newCursorPos = start + insertion.length
textarea.setSelectionRange(newCursorPos, newCursorPos)
}, 0)
}
// 复制HTML到剪贴板
const copyHtml = () => {
navigator.clipboard.writeText(htmlContent.value)
.then(() => {
ElMessage({
message: 'HTML已复制到剪贴板',
type: 'success',
duration: 2000
})
})
.catch(err => {
ElMessage({
message: '复制失败: ' + err,
type: 'error',
duration: 2000
})
})
}
// 组件挂载时初始化
onMounted(() => {
// 设置示例Markdown内容
markdownContent.value = `
# MD2Card
> MD2Card 是一个 markdown 转知识卡片工具,可以让你用 Markdown 制作优雅的图文海报。 🌟

## 它的主要功能:
1. 将 Markdown 转化为**知识卡片**
2. 多种主题风格任你选择
3. 长文自动拆分,或者根据 markdown --- 横线拆分
4. 可以复制图片到剪贴板,或者下载为PNG、SVG图片
5. 所见即所得
6. 免费
`
updatePreview()
// 初始化应用默认样式
setStyle(currentStyle.value)
})
onUnmounted(()=>{
// 移除所有的样式标签
const oldStyles = document.querySelectorAll('style[data-md-style]')
oldStyles.forEach(style => style.remove())
})
</script>
<style scoped lang="scss">
.markdown-editor {
height: calc(100vh - 40px);
box-sizing: border-box;
overflow: hidden;
}
.markdown-editor.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
padding: 0;
}
.editor-container {
height: 100%;
display: flex;
.markdown-input,
.preview-container {
flex: 1;
height: 100%;
overflow-y: auto;
}
.style-card{
width: 260px;
}
}
.editor-card,
.preview-card,
.style-card {
height: 100%;
position: relative;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
.preview-content{
height: auto;
}
:deep(.el-card__body) {
flex: 1;
overflow: auto;
padding: 0;
display: flex;
flex-direction: column;
}
/* Editor card specific styles */
&.editor-card {
:deep(.el-textarea) {
flex: 1;
display: flex;
flex-direction: column;
.el-textarea__inner {
flex: 1;
resize: none;
border: none;
box-shadow: none;
padding: 20px;
}
}
}
/* Preview card specific styles */
&.preview-card {
height: 100%;
:deep(.el-card__body) {
// height: 100%;
// overflow: auto;
padding: 0;
display: flex;
flex-direction: column;
}
.card {
flex: 1;
min-height: 0; /* 修复flex容器滚动问题 */
padding: 20px;
}
.card-content {
height: auto;
}
}
/* Style card specific styles */
&.style-card {
.style-content {
flex: 1;
overflow: auto;
padding: 10px;
.style-item {
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid #e0deed;
color: #0f0a29;
background: #fff;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.2s ease;
&:hover {
border-color: #a598e5;
}
&.active {
border-color: #4c33cc;
color: #4c33cc;
background: #f6f5fc;
font-weight: 500;
}
}
}
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar {
display: flex;
gap: 10px;
}
/* 工具栏按钮组之间的分隔 */
.toolbar .el-button-group+.el-button-group {
margin-left: 8px;
padding-left: 8px;
border-left: 1px solid #dcdfe6;
}
/* 激活状态的按钮样式 */
:deep(.el-button--primary) {
background-color: var(--el-color-primary-light-3);
border-color: var(--el-color-primary-light-3);
color: #fff;
}
/* 预览模式下的布局调整 */
.preview-button {
z-index: 1000;
}
.preview-mode {
:deep(.el-row) {
.el-col:first-child {
display: none;
}
.el-col:last-child {
width: 100%;
position: relative;
.preview-exit-button {
position: absolute;
top: 76px;
left: 26px;
z-index: 2000;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
color: var(--el-text-color-primary);
&:hover {
background-color: var(--el-color-primary-light-9);
}
}
}
}
.header .preview-button {
display: none;
}
}
/* 预览按钮样式 */
.preview-button {
margin-right: 10px;
}
/* 预览模式下的退出按钮样式 */
.preview-exit-button {
/* position: absolute;
top: 10px;
left: 10px;
z-index: 2000; */
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
color: var(--el-text-color-primary);
&:hover {
background-color: var(--el-color-primary-light-9);
}
}
/* 全屏模式下的样式调整 */
.fullscreen {
:deep(.el-main) {
padding: 0;
}
.editor-card,
.preview-card {
border-radius: 0;
}
}
/* 响应式布局 */
@media screen and (max-width: 768px) {
.toolbar {
flex-wrap: wrap;
}
.toolbar .el-button-group {
margin-bottom: 8px;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>
添加弹窗
<template>
<el-dialog v-model="dialogVisible" title="添加CSS样式" width="700px" top="5vh">
<div class="css-editor-container">
<!-- 样式名称输入框 -->
<el-input
v-model="styleName"
placeholder="样式名称(如:my-style)"
style="margin-bottom: 15px"
/>
<!-- 带有基本语法高亮的 CSS 编辑器 -->
<div class="code-editor-wrapper">
<textarea
v-model="cssCode"
@input="updateHighlight"
class="code-input"
spellcheck="false"
placeholder="请输入CSS代码..."
></textarea>
</div>
</div>
<!-- 底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="handlePreview">预览</el-button>
<el-button type="primary" @click="handleConfirm">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed } from 'vue'
const dialogVisible = ref(false)
const styleName = ref('')
const cssCode = ref('')
const updateHighlight = () => {
// 自动更新高亮
}
// 打开对话框
const open = () => {
dialogVisible.value = true
cssCode.value = ''
styleName.value = ''
}
// 确认按钮处理函数
const emit = defineEmits(['confirm','preview'])
const handleConfirm = () => {
// 打印输入的CSS样式和样式名称
console.log('样式名称:', styleName.value)
console.log('CSS样式:', cssCode.value)
// // 触发confirm事件,将数据传递给父组件
// emit('confirm', {
// name: styleName.value.trim(),
// code: cssCode.value.trim()
// })
// // 关闭对话框
// dialogVisible.value = false
}
const handlePreview = () => {
// 预览CSS样式
console.log('预览CSS样式:', cssCode.value)
// // 触发confirm事件,将数据传递给父组件
emit('preview', {
name: styleName.value.trim(),
code: cssCode.value.trim()
})
// 关闭对话框
dialogVisible.value = false
}
defineExpose({ open })
</script>
<style scoped>
.css-editor-container {
display: flex;
flex-direction: column;
}
.code-editor-wrapper {
position: relative;
height: 300px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
margin-bottom: 15px;
}
.code-highlight {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
overflow: auto;
background: #f5f7fa;
pointer-events: none;
z-index: 1;
}
.code-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
color: #333;
background: transparent;
border: none;
outline: none;
resize: none;
z-index: 2;
}
/* 语法高亮样式 */
.comment {
color: #999;
font-style: italic;
}
.punctuation {
color: #333;
}
.selector {
color: #905;
}
.property {
color: #07a;
}
.value {
color: #690;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
注:
这是一个静态的功能参考,在这里的样式使用的给定的样式切换,后续对接接口后可换成一个下拉列表,选择后通过接口返回的css样式数据来更换一样的效果。这里预想的是回台直接返回样式的一个字符串格式内容,其他的想法可自行修改。
这里的css样式只是使用了一个文本输入框实现了功能效果,不带样式代码高亮显示的功能
样式可使用deepseek来生成
复制这段文字,修改生成条件就可生成不同的样式的css代码,直接粘贴到css代码输入框中,点击预览就可看到新css样式的效果。
/*
可以让 deepseek 等大模型实现css,实现卡片样式自定义,以下是发送的提示词模板:
我需要在 md2card.com 实现自定义小红书卡片样式
以下是 md2card.com 中卡片的HTML结构:
```html
<div class="card">
<div class="card-header"></div>
<div class="card-content">
<div class="card-content-inner">
<h1 data-text="标题">标题</h1>
<h2 data-text="标题二">标题二</h2>
<p>内容</p>
<ol>
<li data-index="0">列表</li>
</ol>
</div>
</div>
<div class="card-footer"></div>
</div>
`card-content-inner` 为 markdown 编译后的内容区域,还包括标题、列表、引用、代码、加粗等常见 markdown 编译的内容
请为我设计一张卡片,其风格为“简约现代”,可以进一步融入“留白”的设计理念,应用“小红书知识分享”的场景
【设计风格要求】:
【输入要求】:只需要返回 css 代码
*/
.card {
}