前言

本文将完整展示如何在Uni-app框架下使用Vue3实现一个功能完善的富文本编辑器。我们将从组件结构、功能实现到样式设计,全面介绍这个编辑器的开发过程。

<template>
  <view class="editor_box">
    <!-- 操作栏 -->
    <view class="tooltar" v-if="showToolbar">
      <view v-for="(item, index) in ToolBarList" @tap="item.fnc()" :key="index">
        <image :src="item.icon" class="img" />
      </view>
    </view>
    <!-- 编辑器主体 -->
    <editor
      :id="`${id}`"
      :placeholder="'请输入内容'"
      @ready="editorReady"
      @input="handleEditorChange"
      style="color: black; padding: 12rpx"
      class="ql-container"
    />
  </view>
</template>

<script setup lang="ts">
import { ref, nextTick, getCurrentInstance } from "vue";

// 组件属性定义
interface EditorProps {
  showToolbar?: boolean;  // 是否显示工具栏
  defaultContent?: string; // 默认内容
  id?: string;  // 编辑器ID
}

// 格式化类型定义
type formatType = "bold" | "italic" | "header" | "align" | "list";
type formatVal = "h1" | "h2" | "h3" | "left" | "right" | "ordered" | "bullet";

// 设置默认属性
const props = withDefaults(defineProps<EditorProps>(), {
  showToolbar: true,
});

// 工具栏按钮配置
const ToolBarList = [
  { fnc: () => insertImage(), icon: '图片路径' },
  { fnc: () => formatStyle("italic"), icon: '图片路径' },
  { fnc: () => formatStyle("bold"), icon: '图片路径' },
  { fnc: () => formatStyle("header", "h1"), icon: '图片路径' },
  { fnc: () => formatStyle("header", "h2"), icon: '图片路径' },
  { fnc: () => formatStyle("header", "h3"), icon: '图片路径' },
  { fnc: () => formatStyle("align", "left"), icon: '图片路径' },
  { fnc: () => formatStyle("align", "right"), icon: '图片路径' },
  { fnc: () => formatStyle("list", "ordered"), icon: '图片路径' },
  { fnc: () => formatStyle("list", "bullet"), icon: '图片路径' },
  { fnc: () => undo(), icon: '图片路径' },
];

// 定义事件
const emits = defineEmits(["editorChange"]);

// 响应式变量
const editorCtx = ref<any>(null); // 编辑器上下文
const isEditorReady = ref(false); // 编辑器是否准备就绪
const instance = getCurrentInstance() as any;

// 编辑器准备就绪回调
const editorReady = () => {
  nextTick(() => {
    const query = uni.createSelectorQuery().in(instance?.proxy);
    query
      .select(`#${props?.id}`)
      .context((res: any) => {
        if (res?.context) {
          editorCtx.value = res?.context;
          isEditorReady.value = true;
          if (props.defaultContent) {
            setEditorContent(props.defaultContent);
          }
        } else {
          console.error("获取编辑器上下文失败", res);
        }
      })
      .exec();
  });
};

// 设置编辑器内容
const setEditorContent = (html: string) => {
  return new Promise((resolve) => {
    if (!editorCtx.value) return resolve(false);

    editorCtx.value.setContents({
      html,
      success: () => {
        console.log("内容回显成功");
        resolve(true);
        // 初始化,向父组件赋值
        setTimeout(() => {
          handleEditorChange();
        }, 500);
      },
      fail: (err: any) => {
        console.error("内容回显失败:", err);
        resolve(false);
      },
    });
  });
};

// 撤销操作
const undo = () => {
  if (editorCtx.value) {
    editorCtx.value.undo();
  }
};

// 格式化文本样式
const formatStyle = (type: formatType, val?: formatVal) => {
  editorCtx.value.format(type, val);
};

// 插入图片
const insertImage = () => {
  uni.chooseImage({
    count: 1,
    sizeType: ["compressed"],
    sourceType: ["album", "camera"],
    success: (res) => {
      const filePath = res.tempFilePaths[0];

      // 方案1:直接使用本地路径(简单测试)
      if (editorCtx.value) {
        editorCtx.value.insertImage({
          src: filePath,
          width: "100%",
          success: () => {
            uni.showToast({ title: "插入成功", icon: "none" });
          },
        });
      }
    },
    fail: (err) => {
      console.log("选择图片失败", err);
    },
  });
};

// 编辑器内容变化处理
const handleEditorChange = (e?: any) => {
  // 获取编辑器内容
  getEditorContent().then((content) => {
    // 向父组件传递内容
    emits("editorChange", content);
  });
};

// 获取编辑器内容
const getEditorContent = () => {
  return new Promise((resolve) => {
    if (editorCtx.value) {
      editorCtx.value.getContents({
        success: (res: any) => {
          resolve({
            html: res?.html,
            text: res?.text,
          });
        },
      });
    }
  });
};
</script>

<style scoped lang="scss">
.editor_box {
  width: 100%;
  height: auto;
  box-sizing: border-box;
  background: #f7f7f7;
  border-radius: 12rpx;

  .ql-container {
    box-sizing: border-box;
    padding: 24rpx 32rpx;
    width: 100%;
    min-height: 30vh;
    max-height: 70vh;
    height: 100%;
    margin-top: 20rpx;
    overflow-y: scroll;
    line-height: 1.5;
  }
}

.tooltar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  padding: 10rpx 12rpx;
  border-bottom: 1rpx solid #ebebeb;
  view {
    .img {
      width: 24rpx;
      height: 24rpx;
    }
  }
}
</style>

关键功能解析

1. 编辑器初始化流程

  1. 组件挂载后触发editorReady事件

  2. 使用uni.createSelectorQuery()获取编辑器实例

  3.  //注意事项这是vue2写法 在vue3中不能哪来使用
    
    uni.createSelectorQuery().select('#editor').context((res) => 
    {               
         this.editorCtx = res.context           
        
     }).exec()
    
    
    //因为是组件化封装,为了确保编译到微信小程序可以查询出来实例必须采用下方写法
    const instance = getCurrentInstance() as any; 
     const query = uni.createSelectorQuery().in(instance?.proxy); 
       query.select(`#${props?.id}`).context((res: any) => {  
          console.log("🚀 ~ editorReady ~ res:", res);  
          if (res?.context) {     
         editorCtx.value = res?.context;    
         } else {     
         console.error("获取编辑器上下文失败", res); 
           }    
      }).exec(); 
     });​

  4. 将编辑器上下文保存到editorCtx变量中

  5. 如果提供了默认内容,调用setEditorContent进行内容回显

2. 工具栏功能实现

工具栏通过ToolBarList数组配置,每个按钮包含:

  • 图标资源

  • 点击事件处理函数

支持的功能包括:

  • 文本加粗/斜体

  • 设置标题级别(H1/H2/H3)

  • 文本对齐(左/右)

  • 列表样式(有序/无序)

  • 插入图片

  • 撤销操作

3. 内容同步机制

编辑器内容变化时:

  1. 触发@input事件调用handleEditorChange

  2. 通过getEditorContent获取当前内容

  3. 使用emits将内容传递给父组件

4. 图片插入实现

图片插入流程:

  1. 调用uni.chooseImage选择图片

  2. 获取图片临时路径

  3. 使用editorCtx.value.insertImage插入图片

  4. 设置图片宽度为100%自适应

使用说明

组件引入

<template>
  <Editor 
    :showToolbar="true" 
    :defaultContent="content" 
    @editorChange="onEditorChange"
  />
</template>

<script setup>
import Editor from '@/components/Editor.vue';

const content = ref('<p>正文内容</p>');

const onEditorChange = (content) => {
  console.log('编辑器内容变化:', content);
};
</script>

总结

本文完整展示了基于Uni-app和Vue3的富文本编辑器实现方案。该编辑器具有以下特点:

  1. 功能完善:支持常见的文本编辑操作

  2. 响应式设计:适配不同尺寸屏幕

  3. 易于扩展:可以方便地添加新的工具栏功能

  4. 良好的交互体验:提供内容变化实时反馈

  5. 兼容h5和小程序平台(通过uni.createSelectorQuery().in(instance?.proxy)解决vue3版本组件化封装内部拿不到实例)

开发者可以根据实际需求,进一步扩展功能或调整样式,如添加更多格式化选项、实现图片上传到服务器等功能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐