参考文章:微信小程序之实现封装一个富文本编辑器 Editor 的完整流程【附demo源码】欢迎点赞收藏

地址:https://blog.csdn.net/XH_jing/article/details/115509316

demo源码: https://github.com/jxh1997/Editor ,所以源代码均在 Github 上,下载即可使用。

我个人在demo源码的基础上稍微进行了微调。

官方文档:

editor(富文本编辑器,可以对图片、文字进行编辑):
https://developers.weixin.qq.com/miniprogram/dev/component/editor.html

rich-text(富文本):
https://developers.weixin.qq.com/miniprogram/dev/component/rich-text.html

一、组件定义

组件的目录结构如下:

richText.js

const supportDateFormat = ['YY-MM', 'YY.MM.DD', 'YY-MM-DD', 'YY/MM/DD', 'YY.MM.DD HH:MM', 'YY/MM/DD HH:MM', 'YY-MM-DD HH:MM']; //支持的日期格式
Component({
  /**
   * 组件的属性列表
   */
  properties: {
    //编辑器是否只读
    readOnly: {
      type: Boolean,
      value: false
    },
    //编辑器默认提示语
    placeholder: {
      type: String,
      value: '开始编辑吧...'
    },
    //插入的日期格式
    formatDate: {
      type: String,
      value: 'YY/MM/DD'
    },
    buttonTxt: {
      type: String,
      value: '保存'
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    formats: {}, //样式集合
    textTool: false, //文本工具是否显示,默认隐藏
  },

  /**
   * 组件的方法列表
   */
  methods: {
    //富文本工具点击事件
    toolEvent(res) {
      let {
        tool_name
      } = res.currentTarget.dataset;
      switch (tool_name) {
        case 'insertImage': //插入图片
          this.insertImageEvent();
          break;
        case 'showTextTool': //展示文字编辑工具
          this.showTextTool();
          break;
        case 'insertDate': //插入日期
          this.insertDate();
          break;
        case 'undo': //撤退(向前)
          this.undo();
          break;
        case 'redo': //撤退(向后)
          this.restore();
          break;
        case 'clear': //清除
          this.clearBeforeEvent();
          break;
      }
    },

    //编辑器初始化完成时触发
    onEditorReady() {
      console.log('编辑器初始化完成时触发')
      this.triggerEvent('onEditorReady');
      // 返回一个 SelectorQuery 对象实例。在自定义组件或包含自定义组件的页面中,应使用this.createSelectorQuery()来代替。
      // https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html 
      this.createSelectorQuery().select('#editor').context(res => {
        console.log('createSelectorQuery=>', res)
        this.editorCtx = res.context;
        let rtTxt = '';
        this.setContents(rtTxt); //设置富文本内容
      }).exec();
    },

    //设置富文本内容
    setContents(rechtext) {
      this.editorCtx.setContents({
        html: rechtext,
        success: res => {
          console.log('[setContents success]', res)
        }
      })
    },

    //撤销
    undo() {
      this.editorCtx.undo();
      this.triggerEvent('undo');
    },

    //恢复
    restore() {
      this.editorCtx.redo();
      this.triggerEvent('restore');
    },

    /**
     * 修改样式,样式item点击事件
     * @param {String} name 样式名称 
     * @param {String} value 样式值
     */
    format(res) {
      let {
        name,
        value
      } = res.target.dataset;
      if (!name) return;
      this.editorCtx.format(name, value);
    },

    // 通过 Context 方法改变编辑器内样式时触发,返回选区已设置的样式
    onStatusChange(res) {
      const formats = res.detail;
      console.log('onStatusChange=>',res)
      this.setData({
        formats
      })
    },

    //在光标位置插入下换线
    insertDivider() {
      this.editorCtx.insertDivider({
        success: res => {
          console.log('[insert divider success]', res)
        }
      })
    },

    //清空编辑器内容
    clear() {
      this.editorCtx.clear({
        success: res => {
          this.triggerEvent('clearSuccess');
        }
      })
    },

    //清空编辑器内容前的事件
    clearBeforeEvent() {
      this.triggerEvent('clearBeforeEvent');
    },

    //清除当前选区的样式
    removeFormat() {
      this.editorCtx.removeFormat();
    },

    //插入日期
    insertDate() {
      if (supportDateFormat.indexOf(this.data.formatDate) < 0) {
        console.error(`Format Date ${this.data.formatDate} error \n It should be one of them [${supportDateFormat}]`)
        return;
      }
      let formatDate = this.getThisDate(this.data.formatDate);
      this.editorCtx.insertText({
        text: formatDate
      })
    },

    //插入图片事件
    insertImageEvent() {
      //触发父组件选择图片方法
      this.triggerEvent('insertImageEvent', {});
    },

    /**
     * 插入图片方法
     * @param {String} path 图片地址,仅支持 http(s)、base64、云图片(2.8.0)、临时文件(2.8.3)
     */
    insertImageMethod(path) {
      return new Promise((resolve, reject) => {
        this.editorCtx.insertImage({
          src: path,
          data: {
            id: 'imgage',
          },
          success: res => {
            resolve(res);
          },
          fail: res => {
            reject(res);
          }
        })
      })
    },

    //保存按钮事件,获取编辑器内容
    getEditorContent() {
      this.editorCtx.getContents({
        success: res => {
          // console.log('[getContents rich text success]', res)
          this.triggerEvent('getEditorContent', {
            value: res,
          });
        }
      })
    },

    //show文本工具栏
    showTextTool() {
      this.setData({
        textTool: !this.data.textTool
      })
    },

    //编辑器聚焦时触发
    bindfocus(res) {
      this.triggerEvent('bindfocus', {
        value: res,
      });
    },
    //编辑器失去焦点时触发
    bindblur(res) {
      this.triggerEvent('bindblur', {
        value: res,
      });
    },

    //编辑器输入中时触发
    bindinput(res) {
      this.triggerEvent('bindinput', {
        value: res,
      });
    },

    /**
     * 返回当前日期
     * @format {String} 需要返回的日期格式
     */
    getThisDate(format) {
      let date = new Date(),
        year = date.getFullYear(),
        month = date.getMonth() + 1,
        day = date.getDate(),
        h = date.getHours(),
        m = date.getMinutes();

      //数值补0方法
      const zero = (value) => {
        if (value < 10) return '0' + value;
        return value;
      }

      switch (format) {
        case 'YY-MM':
          return year + '-' + zero(month);
        case 'YY.MM.DD':
          return year + '.' + zero(month) + '.' + zero(day);
        case 'YY-MM-DD':
          return year + '-' + zero(month) + '-' + zero(day);
        case 'YY.MM.DD HH:MM':
          return year + '.' + zero(month) + '.' + zero(day) + ' ' + zero(h) + ':' + zero(m);
        case 'YY/MM/DD HH:MM':
          return year + '/' + zero(month) + '/' + zero(day) + ' ' + zero(h) + ':' + zero(m);
        case 'YY-MM-DD HH:MM':
          return year + '-' + zero(month) + '-' + zero(day) + ' ' + zero(h) + ':' + zero(m);
        default:
          return year + '/' + zero(month) + '/' + zero(day);
      }
    }
  },

  /**
   * 组件生命周期函数-在组件布局完成后执行)
   */
  ready() {

  },
})

richText.json

{
    "component": true,
    "usingComponents": {}
  }

richText.wxml

<view class="whole" id="richText">
  <view style="height:{{textTool?'200':'30'}}rpx;"></view>
  <view class="editor-toolbar" bindtap="format">
    <view class="toolbar-2">
      <view class="tool-item-cell">
        <view class="tool-item-box">
          <view class="cell-rg-shadow"></view>
          <scroll-view scroll-x class="flex-sb" style="height:70rpx;white-space: nowrap;">
            <view class="tool-item">
              <i class="iconfont icon-charutupian" data-tool_name='insertImage' bindtap="toolEvent"></i>
            </view>
            <view class="tool-item">
              <i class="iconfont icon-font" data-tool_name='showTextTool' bindtap="toolEvent"></i>
            </view>
            <view class="tool-item">
              <i class="iconfont icon-format-header-1 {{formats.header === 1 ? 'ql-active' : ''}}"
                data-tool_name='text_H1' data-name="header" data-value="{{1}}" bindtap="toolEvent"></i>
            </view>
            <view class="tool-item">
              <i class="iconfont icon-date" data-tool_name='insertDate' bindtap="toolEvent"></i>
            </view>
            <view class="tool-item">
              <i class="iconfont icon-undo" data-tool_name='undo' bindtap="toolEvent"></i>
            </view>
            <view class="tool-item">
              <i class="iconfont icon-redo" data-tool_name='redo' bindtap="toolEvent"></i>
            </view>
            <view class="tool-item">
              <i class="iconfont icon-shanchu" data-tool_name='clear' bindtap="toolEvent"></i>
            </view>
          </scroll-view>
        </view>
      </view>
      <lable class='save-icon' style='background:{{appColorConfig.check_color}}' bindtap="getEditorContent">
        {{buttonTxt}}
      </lable>
    </view>
    <view class="toolbar-1" wx:if="{{textTool}}">
      <scroll-view scroll-x style="height:70rpx;white-space: nowrap;">
        <view class="tool-item">
          <i class="iconfont icon-zitijiacu {{formats.bold ? 'ql-active' : ''}}" data-name="bold"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-zitixieti {{formats.italic ? 'ql-active' : ''}}" data-name="italic"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-zitixiahuaxian {{formats.underline ? 'ql-active' : ''}}" data-name="underline"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-fengexian" bindtap='insertDivider'></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-zuoduiqi {{formats.align === 'left' ? 'ql-active' : ''}}" data-name="align"
            data-value="left"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-juzhongduiqi {{formats.align === 'center' ? 'ql-active' : ''}}" data-name="align"
            data-value="center"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-youduiqi {{formats.align === 'right' ? 'ql-active' : ''}}" data-name="align"
            data-value="right"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-zuoyouduiqi {{formats.align === 'justify' ? 'ql-active' : ''}}" data-name="align" data-value="justify"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon--checklist" data-name="list" data-value="check"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-youxupailie {{formats.list === 'ordered' ? 'ql-active' : ''}}" data-name="list" data-value="ordered"></i>
        </view>
        <view class="tool-item">
          <i class="iconfont icon-wuxupailie {{formats.list === 'bullet' ? 'ql-active' : ''}}" data-name="list"
            data-value="bullet"></i>
        </view>
      </scroll-view>
    </view>
  </view>
  <view class="page-body">
    <view class='wrapper'>
      <editor id="editor" class="ql-container" placeholder="{{placeholder}}" showImgSize showImgToolbar showImgResize bindstatuschange="onStatusChange" read-only="{{readOnly}}" bindready="onEditorReady" bindfocus='bindfocus' bindblur='bindblur' bindinput='bindinput'>
      </editor>
    </view>
  </view>
  
</view>

richText.wxss

/* components/richText/richText.wxss */
@import "./assets/iconfont.wxss";
page {
  background: #f8f8f8;
}
.page-body{
  padding-bottom: 10rpx;
}
.editor-toolbar {
  /* position: fixed;
  top: 0;
  left: 0; */
  width: 100%;
  z-index: 9999;
  margin-bottom: 30rpx;
}

.editor-toolbar i {
  display: flex;
  align-items: center;
  justify-content: center;
}

.toolbar-1 {
  padding: 5rpx 0;
  background: #e4e4e4;
}
.editor-toolbar .tool-item {
  display: inline-block;
}

.toolbar-2 {
  padding: 5rpx 20px 5rpx 10px;
  background: #f4f4f4;
  display: flex;
  align-items: center;
  justify-content:space-between;
  position: relative;
}
.toolbar-2 .tool-item-cell{
  max-width: 80%;
}
.toolbar-2 .tool-item-box{
  position: relative;
}
.toolbar-2 .cell-rg-shadow{
  position: absolute;
  right: 0;
  top: 0;
  width: 1px;
  height: 100%;
  z-index: 999;
   background:#dddddd;
}
.iconfont {
  display: inline-block;
  padding: 8px 8px;
  width: 24px;
  height: 24px;
  cursor: pointer;
  font-size: 20px;
}

.toolbar {
  box-sizing: border-box;
  border-bottom: 0;
  font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
}

.ql-container {
  box-sizing: border-box;
  padding: 12px 15px;
  width: 100%;
  min-height: 30vh;
  height: auto;
  background: #fff;
  font-size: 16px;
  line-height: 1.5;
}

.ql-active {
  color: #06c;
}

.save-icon {
  padding: 15rpx 30rpx;
  font-size: 20rpx;
  background: #bf98d2;
  color: #fff;
}
.flex{
  display: flex;
}
.flex-cc{
  display: flex;
  align-items: center;
  -ms-flex-item-align: center;
  justify-content: center;
}
.flex-sb{
  display: flex;
  align-items: center;
  -ms-flex-item-align: center;
  justify-content: space-between;
}
.flex-sa{
  display: flex;
  align-items: center;
  -ms-flex-item-align: center;
  justify-content: space-around;
}

二、组件使用

index.js

const app = getApp();
let richText = null;  //富文本编辑器实例
Page({

  /**
   * 页面的初始数据
   */
  data: {
    readOnly: false, //编辑器是否只读
    placeholder: '开始编辑吧...',
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  // 编辑器初始化完成时触发,可以获取组件实例
  onEditorReady() {
    console.log('[onEditorReady callback]')
    richText = this.selectComponent('#richText'); //获取组件实例
  },

  //设置富文本内容
  setContents(rechtext) {
    this.editorCtx.setContents({
      html: rechtext,
      success: res => {
        console.log('[setContents success]', res)
      }
    })
  },

  //撤销
  undo() {
    console.log('[undo callback]')
  },

  //恢复
  restore() {
    console.log('[restore callback]')
  },

  //清空编辑器内容
  clear() {
    this.editorCtx.clear({
      success: res => {
        console.log("[clear success]", res)
      }
    })
  },

  //清空编辑器事件
  clearBeforeEvent(){
    console.log('[clearBeforeEvent callback]')
    wx.showModal({
      cancelText: '取消',
      confirmText: '确认',
      content: '确认清空编辑器内容吗?',
      success: (result) => {
        if(result.confirm){
          richText.clear();
        }
      },
      fail: (res) => {},
    })
  },

  //清空编辑器成功回调
  clearSuccess(){
    console.log('[clearSuccess callback]')
  },

  //清除当前选区的样式
  removeFormat() {
    this.editorCtx.removeFormat();
  },

  //插入图片
  insertImageEvent() {
    wx.chooseImage({
      count: 1,
      success: res => {
        let path = res.tempFilePaths[0];
        //调用子组件方法,图片应先上传再插入,不然预览时无法查看图片。
        richText.insertImageMethod(path).then(res => {
          console.log('[insert image success callback]=>', res)
        }).catch(res => {
          console.log('[insert image fail callback]=>', res)
        });
      }
    })
  },

  //保存,获取编辑器内容
  getEditorContent(res) {
    let {
      value
    } = res.detail;
    wx.showToast({
      title: '获取编辑器内容成功',
      icon: 'none',
    })
    console.log('[getEditorContent callback]=>', value)
  },

  //show文本工具栏
  showTextTool() {
    this.setData({
      textTool: !this.data.textTool
    })
  },

  //编辑器聚焦时触发
  bindfocus(res) {
    let {
      value
    } = res.detail;
    // console.log('[bindfocus callback]=>', value)
  },

  //编辑器失去焦点时触发
  bindblur(res) {
    let {
      value
    } = res.detail;
    // console.log('[bindblur callback]=>', value)
  },

  //编辑器输入中时触发
  bindinput(res) {
    let {
      value
    } = res.detail;
    // console.log('[bindinput callback]=>', value)
    app.data.richTextContents = value.detail.html;
  },

  //预览富文本
  preview(){
    wx.navigateTo({
      url: `../preview/preview`,
    })
  }
})

富文本插入图片时将图片先保存到服务器再预览,修改insertImageEvent方法如下所示:

//插入图片
  insertImageEvent() {
    wx.chooseImage({
      count: 1,//选择图片的数量 默认为9
      success: res => {
          console.log(res)
        let path = res.tempFilePaths[0]; //tempFilePaths 图片的本地临时文件路径列表
        wx.uploadFile({
            url: app.globalData.serverApi+"/appletApi/fileUpload/appletRichTextImgUpload",
            filePath: path,//要上传文件资源的路径 
            name: 'file',
            header: {
                'content-type': 'application/json',
                'Authorization': wx.getStorageSync('token')
            },
            formData:{
                'table': 't_applet_product',
                'column': 'productDescImg'
            },
            success: (resp) => {
                var result = JSON.parse(resp.data);
                var relativePath = result.data.replace("\\","/")
                var path1 = app.globalData.staticApi+relativePath
                //调用子组件方法,图片应先上传再插入,不然预览时无法查看图片。
                richText.insertImageMethod(path1).then(res => {
                    console.log('[insert image success callback]=>', res)
                }).catch(res => {
                    console.log('[insert image fail callback]=>', res)
                });
            }
        });
        
      }
    })
  },

index.wxml

<view class="row3" style="height: 700rpx">
                <richText 
                class="richText"
                id='richText' 
                readOnly='{{readOnly}}'
                placeholder='{{placeholder}}' 
                formatDate='YY/MM/DD'
                buttonTxt='保存'
                bind:clearBeforeEvent='clearBeforeEvent'
                bind:clearSuccess='clearSuccess'
                bind:undo='undo'
                bind:restore='restore'
                bind:onEditorReady='onEditorReady' 
                bind:bindfocus='bindfocus' 
                bind:bindblur='bindblur' 
                bind:bindinput='bindinput' 
                bind:insertImageEvent='insertImageEvent' 
                bind:getEditorContent='getEditorContent'></richText>
                <view class="preview" bindtap="preview">预览</view>
            </view>

index.json

{
  "usingComponents": {
    "richText":"../../components/richText/richText"
  }
}

index.wxss

.preview{
    width: 20%;
    height: 80rpx;
    line-height: 80rpx;
    margin: 0 auto;
    margin-bottom: 50rpx;
    text-align: center;
    font-size:26rpx;
    color: #ffffff;
    background-color: #bf98d2;
    float: right;
  }

效果如下:

添加一张图片

 

三、预览页面

preview.js

const app = getApp();
Page({

  /**
   * 页面的初始数据
   */
  data: {
   
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  onEditorReady() {
    wx.createSelectorQuery().select('#editor').context(res => {
      this.editorCtx = res.context;
      this.editorCtx.setContents({
        html: app.globalData.richTextContents,
        success: res => {
          console.log('[setContents success]')
        }
      })
    }).exec()
  }
})

preview.json

{
    "usingComponents": {},
    "navigationBarTitleText": "预览富文本"
  }

preview.wxml

<view style="padding:20rpx;">
    <editor id="editor" style="height: 1200rpx;" read-only bindready="onEditorReady"></editor>
</view>

preview.wxss

.editor {
    box-sizing: border-box;
    padding: 12px 15px;
    width: 100%;
    min-height: 10vh;
    height: auto;
    background: #fff;
    font-size: 24rpx;
    line-height: 1.5;
  }

预览效果如下:

四、修改时回显富文本

1、从后台获取富文本,

// 该方法用于修改
    getProductInfoById: function(){
        var that = this;
        request.postParam("/api/product/getById",that.QueryParams,function(res){
            console.log(res)
            if(res.data.success){
                let result = res.data.data
                that.setData({
                    richTextContents: result.productDesc
                })
                console.log(that.data.richTextContents)
                that.setData({
                    material: result
                })
            
            }
        });
    },

2、在富文本中将富文本内容传递到子组件

<richText 
                    class="richText"
                    id='richText' 
                    readOnly='{{readOnly}}'
                    placeholder='{{placeholder}}'
                    content= '{{richTextContents}}'
                    formatDate='YY/MM/DD'
                    buttonTxt='保存'
                    bind:clearBeforeEvent='clearBeforeEvent'
                    bind:clearSuccess='clearSuccess'
                    bind:undo='undo'
                    bind:restore='restore'
                    bind:onEditorReady='onEditorReady' 
                    bind:bindfocus='bindfocus' 
                    bind:bindblur='bindblur' 
                    bind:bindinput='bindinput' 
                    bind:insertImageEvent='insertImageEvent' 
                    bind:getEditorContent='getEditorContent'></richText>

3、子组件接收父组件传递过来的content

properties: {
    //编辑器是否只读
    readOnly: {
      type: Boolean,
      value: false
    },
    //编辑器默认提示语
    placeholder: {
      type: String,
      value: '开始编辑吧...'
    },
    //插入的日期格式
    formatDate: {
      type: String,
      value: 'YY/MM/DD'
    },
    buttonTxt: {
      type: String,
      value: '保存'
    },
    content: {
      type: String,
      value: ''
    }
  },

4、在onEditorReady中设置富文本内容

//编辑器初始化完成时触发
    onEditorReady() {
      console.log('编辑器初始化完成时触发')
      this.triggerEvent('onEditorReady');
      // 返回一个 SelectorQuery 对象实例。在自定义组件或包含自定义组件的页面中,应使用this.createSelectorQuery()来代替。
      // https://developers.weixin.qq.com/miniprogram/dev/api/wxml/wx.createSelectorQuery.html 
      this.createSelectorQuery().select('#editor').context(res => {
        console.log('createSelectorQuery=>', res)
        this.editorCtx = res.context;
        let rtTxt = '';
        if(this.data.content!=''){
            this.setContents(this.data.content); //设置富文本内容
        } else{
            this.setContents(rtTxt); //设置富文本内容
        }
        
      }).exec();
    },

五、微信小程序 如何控制rich-text中图片的宽度

修改之前,图片要么小了,要么大了

 

使用rich-text展示富文本内容时,富文本的图片自带样式超出屏幕或者不是我们想要的大小时,我们可以使用replace给他添加额外样式,来控制图片大小

对后台返回的数据进行处理

result.productDesc =  result.productDesc.replace(/<img/g, '<img class="richImg"');

这个时候直接设置richImg样式就行

    .richImg{
        width: 100% !important;
        height: auto !important;
      }

修改之后:

 

posted on 2023-08-03 10:37  周文豪  阅读(3312)  评论(0编辑  收藏  举报