vue3实现markdown文档转HTML并可更换样式

发布于:2025-06-25 ⋅ 阅读:(17) ⋅ 点赞:(0)

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 = `![${selected || '图片描述'}](https://example.com/image.jpg)`
      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 制作优雅的图文海报。 🌟

![](https://picsum.photos/600/300)


## 它的主要功能:

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 {
}


网站公告

今日签到

点亮在社区的每一天
去签到