You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Vue3+Vuetify3的博客自定义Markdown组件开发咨询

Vue3 + Vuetify3 博客编辑组件解决方案

问题1:图片引用方案的合理性与优化建议

你提出的用[image1]这类标记引用图片的方案是可行且易落地的,属于轻量型自定义Markdown扩展,适配博客编辑场景。若要进一步优化,可做以下调整:

  • 扩展标记支持图片描述(提升可访问性):比如改成[image1:海边日落],渲染时将描述作为图片的alt属性
  • 约定连续标记自动分组:比如[image1][image2][image3][image4]会自动分成两组,前3张一行,第4张单独一行

这个方案的核心优势:

  • 文本与图片资源分离,存储时只需保存文本内容和图片ID-URL映射,便于后续修改迁移
  • 用户学习成本低,无需掌握复杂Markdown语法
  • 解析逻辑简单,避免富文本编辑器的沉重体积

问题2:带图片标记的文本转渲染HTML实现方案

核心思路

  1. 解析文本:把原始文本拆分为「纯文本段」和「图片标记组」
  2. 映射图片URL:将标记中的图片ID替换为实际上传后的URL
  3. 分组渲染:连续图片按最多3张/行分组,用Vuetify布局组件实现响应式排列
  4. 安全渲染:避免直接使用v-html带来的XSS风险,优先采用组件化渲染

代码实现示例

1. 文本解析工具函数

// utils/parseBlogContent.js
export function parseBlogContent(rawText, imageMap) {
  // 匹配图片标记:[imageX] 或 [imageX:描述]
  const imageRegex = /\[image(\d+)(?::([^\]]+))?\]/g;
  const fragments = [];
  let lastIndex = 0;
  let match;

  while ((match = imageRegex.exec(rawText)) !== null) {
    // 添加纯文本段
    if (match.index > lastIndex) {
      fragments.push({
        type: 'text',
        content: rawText.slice(lastIndex, match.index)
      });
    }
    // 添加图片项
    const imageId = `image${match[1]}`;
    fragments.push({
      type: 'image',
      url: imageMap[imageId],
      alt: match[2] || `图片${match[1]}`
    });
    lastIndex = match.index + match[0].length;
  }

  // 添加最后一段纯文本
  if (lastIndex < rawText.length) {
    fragments.push({
      type: 'text',
      content: rawText.slice(lastIndex)
    });
  }

  // 将连续的图片项分组(最多3张一组)
  const groupedFragments = [];
  let currentImageGroup = [];
  fragments.forEach(frag => {
    if (frag.type === 'image') {
      currentImageGroup.push(frag);
      // 满3张或遇到文本时,结束当前分组
      if (currentImageGroup.length === 3) {
        groupedFragments.push({ type: 'image-group', images: currentImageGroup });
        currentImageGroup = [];
      }
    } else {
      if (currentImageGroup.length > 0) {
        groupedFragments.push({ type: 'image-group', images: currentImageGroup });
        currentImageGroup = [];
      }
      groupedFragments.push(frag);
    }
  });
  // 处理剩余的图片
  if (currentImageGroup.length > 0) {
    groupedFragments.push({ type: 'image-group', images: currentImageGroup });
  }

  return groupedFragments;
}

2. 博客内容渲染组件

<!-- components/BlogRenderer.vue -->
<template>
  <div class="blog-content">
    <template v-for="(frag, idx) in parsedContent" :key="idx">
      <!-- 纯文本段 -->
      <span v-if="frag.type === 'text'" class="text-block">{{ frag.content }}</span>
      <!-- 图片组 -->
      <div v-else-if="frag.type === 'image-group'" class="image-container d-flex flex-wrap justify-center gap-2">
        <div
          v-for="(img, imgIdx) in frag.images"
          :key="imgIdx"
          class="square-image"
          :style="{
            backgroundImage: `url(${img.url})`,
            backgroundSize: 'cover',
            backgroundPosition: 'center'
          }"
          :title="img.alt"
        ></div>
      </div>
    </template>
  </div>
</template>

<script setup>
import { computed } from 'vue';
import { parseBlogContent } from '../utils/parseBlogContent';

const props = defineProps({
  rawText: {
    type: String,
    required: true
  },
  imageMap: {
    type: Object,
    required: true
  }
});

const parsedContent = computed(() => {
  return parseBlogContent(props.rawText, props.imageMap);
});
</script>

<style scoped>
.blog-content {
  line-height: 1.6;
}

.text-block {
  display: block;
  margin: 0.5rem 0;
}

.image-container {
  margin: 1rem 0;
}

.square-image {
  width: 180px;
  height: 180px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>

3. 使用组件示例

<!-- 父组件中调用 -->
<template>
  <BlogRenderer
    :rawText="userInputText"
    :imageMap="uploadedImages"
  />
</template>

<script setup>
import { ref } from 'vue';
import BlogRenderer from './components/BlogRenderer.vue';

// 用户输入的原始文本
const userInputText = ref("这是第一段文本[image1:海边][image2:山景][image3:城市]这是第二段文本[image4:夜景]");
// 上传后的图片映射(key为图片ID,value为URL)
const uploadedImages = ref({
  image1: 'https://example.com/image1.jpg',
  image2: 'https://example.com/image2.jpg',
  image3: 'https://example.com/image3.jpg',
  image4: 'https://example.com/image4.jpg'
});
</script>

关键注意事项

  • XSS防护:若必须用v-html渲染带格式的文本,一定要用DOMPurify净化后再渲染
  • 响应式适配:可将.square-image宽度改为flex: 0 0 calc(33.333% - 1rem),配合flex-wrap实现移动端自动1张/行
  • 上传联动:图片上传后自动生成[imageX]标记并插入到文本光标位置,提升用户体验

内容的提问来源于stack exchange,提问作者lpares12

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 05:49:59