CKEditor5 组件数据双向绑定

本文介绍了如何在Vue后端分离项目中集成CKEditor5来实现富文本编辑功能。首先通过npm安装所需依赖,然后创建自定义组件,并配置CKEditor5的工具栏和语言。在组件中监听内容变化并实现数据双向绑定。最后展示了如何在父组件中调用编辑器组件,并在表单提交时获取编辑器的内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目背景:

最近搞了一个若依的请后端分离项目,需要进行富文本编辑,发现CKEditor5 看上去挺不错的。
首先安装组件

	npm install --save 
	@ckeditor/ckeditor5-vue2 
	@ckeditor/ckeditor5-dev-webpack-plugin
	 @ckeditor/ckeditor5-dev-utils
	  postcss-loader@3 raw-loader@0.5.1

先新建一个组件

<template>
  <div id="ck-editer">
    <ckeditor id="editor"
              :editor="editor"
              @ready="onReady"
              @input="onChange"
              v-model="editorData"
              :config="editorConfig" ></ckeditor>
  </div>
</template>

<script>

import CKEditor from '@ckeditor/ckeditor5-vue2'
import '@ckeditor/ckeditor5-build-decoupled-document/build/translations/zh-cn'
import DecoupledEditor from '@ckeditor/ckeditor5-build-decoupled-document'
//  import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'
import request from '@/utils/request' // 若依自带的请求
export default {
    props:{
        value : String  
    },
components: {
    ckeditor: CKEditor.component //声明组件名称
  },

  data () {
    return {
      editor: DecoupledEditor,
      editorData: undefined,
      textData : undefined,
      // placeholder:,
      editorConfig: { //定义组件插件
        placeholder: '请输入详细内容',
         toolbar: ['heading', 'fontSize',         
          'bold',
          'italic',
          'underline',
          'strikethrough',
          '|', 
          'highlight',
          'highlight:yellowMarker',
          'highlight:greenMarker', 
          'highlight:pinkMarker',
          'highlight:blueMarker',
          'fontFamily', 
          'alignment', 
          'imageStyle:full', 'imageStyle:alignLeft', 'imageStyle:alignRight',
           '|',
          'alignment',
          '|',
          'numberedList',
          'bulletedList',
          '|',
          'indent',
          'outdent',
          '|',
          'codeBlock',
          'link',
          'blockquote',
          'imageUpload',
          'insertTable',
          'mediaEmbed',
          'undo', 'redo'],
      fontSize: {
        options: [8, 9, 10, 11, 12, 'default', 14, 16, 18, 20, 22, 24, 26, 28, 36, 44, 48, 72],
      },
        language: 'zh-cn',
        // extraPlugins : 'uploadimage',
        // uploadUrl  : process.env.VUE_APP_BASE_API + "/common/upload",
      }
    }
  },
  watch:{
    editorData(e){
     //   console.log("编辑器内容"+e);
     //   console.log(this.textData);
      if (e && e !== this.textData) { //这个地方数据和页面上的数据进行联动
        this.textData =e;
      // 编辑器内容发生变化时,告知外部,实现 v-model 双向监听效果
        this.$emit("input", e);
      }
    }
  },
  methods: {

   // 富文本初始方法 
  onReady (editor) {
  //这个地方是导入文件上传的 适配器 所以需要下面
  editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
                    return new MyUploadAdapter( loader );
                };
  // editor.execute( 'codeBlock', { language: 'css' } );
     editor.ui
        .getEditableElement()
        .parentElement.insertBefore(
          editor.ui.view.toolbar.element,
          editor.ui.getEditableElement()
        )   

    },
  //内容改变方式
  onChange(editor){
    // console.log(document.getElementById('editor').childNodes[0]);
    // console.log(this.editorData)
    // this.value = document.getElementById('editor').childNodes[0]
  }
  }
}
//文件上传适配器
class MyUploadAdapter {
  constructor( loader ) {
    // Save Loader instance to update upload progress.
    this.loader = loader;
  }
 
  async upload() {
    const data = new FormData();
    data.append('typeOption', 'upload_image');
    data.append('file', await this.loader.file);
 
    return new Promise((resolve, reject) => {
      return request({
          url: '/minio/upload',
          method: 'post',
          data: data
        })
    });
  }

  async abort() {
        // Reject the promise returned from the upload() method.
        server.abortUpload();
    }
}
</script>

组件的调用 ,删除了其他不需要展示的代码

<template>
  <div class="app-container">
    <!-- 添加或修改食谱对话框 -->
    <el-dialog :title="title" :visible.sync="true" fullscreen>
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="菜名" prop="name">
          <el-input v-model="form.name" placeholder="请输入菜名" />
        </el-form-item>
        <el-form-item label="展示图" prop="img">
           <single-upload v-model="form.img" style="width:300px;display:line-block;margin-left:10px;"/>
        </el-form-item>
        <el-form-item label="菜系">
          <el-select v-model="form.cuisines" placeholder="请选择菜系">
            <el-option
              v-for="dict in cuisinesOptions"
              :key="dict.dictValue"
              :label="dict.dictLabel"
              :value="dict.dictValue"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="类别">
          <el-select v-model="form.type" placeholder="请选择类别">
            <el-option
              v-for="dict in typeOptions"
              :key="dict.dictValue"
              :label="dict.dictLabel"
              :value="dict.dictValue"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="食材" prop="ingredientsIds">
          <el-select  v-model="form.ingredientsIds" multiple filterable remote reserve-keyword  :loading="loading" placeholder="请输入食材" >
              <el-option v-for="item in ingredientsOptions"
               :key="item.id"
               :label="item.name"
               :value="item.id"
              >
              </el-option>
          </el-select>
        </el-form-item>
        <!-- 调用了editor 组件 -->
        <el-form-item label="详细步骤" prop="manufacturing">
            <editor v-model="form.manufacturing"  ref="editor"   @onChange="onChange" :width="680" :height="360" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { listFood, getFood, delFood, addFood, updateFood, exportFood } from "@/api/food/food";
import {listIngredients} from "@/api/food/ingredients";
// import editor from '@/components/Editor'
import singleUpload from '@/components/Upload/singleUpload'
import editor from '@/components/CKEditor'
export default {
  components :{singleUpload ,editor},
  dicts: ['food_type','cuisines'],
  data() {
    return {
      // 遮罩层
      loading: true,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        name: undefined,
        img: undefined,
        type: undefined,
        ingredientsIds: undefined,
        manufacturing: undefined,
      },
      // 表单参数
      form: {},
      // 表单校验
      rules: {
      }     //文件上传
    };
  },
 
  created() {
    this.getDicts("food_type").then(response => {
      this.typeOptions = response.data;
    });
    this.getDicts("cuisines").then(response =>{
      this.cuisinesOptions = response.data;
    })
  },
  methods: {
    // 取消按钮
    cancel() {
      this.open = false;
      this.reset();
    },
    // 表单重置
    reset() {
      this.form = {
        id: undefined,
        name: undefined,
        img: undefined,
        type: undefined,
        ingredientsIds: undefined,
        manufacturing: undefined,
        createTime: undefined,
        createBy: undefined
      };
      this.resetForm("form");
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.reset();
      this.open = true;
      this.title = "添加食谱";
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
      this.reset();
      const id = row.id || this.ids

       let that = this;
      getFood(id).then(response => {
        this.form = response.data;
        this.form.ingredientsIds =  response.ingredients
        this.form.type = response.data.type.toString();
        this.open = true;
        this.title = "修改食谱";
   this.$nextTick(() => {
		//这个地方对于初始化加载数据的时候进行双向渲染, 但是需要提前加载好,所以用 this.$nextTick()
       that.$refs.editor.editorData  = that.form.manufacturing ;  
      //  that.$refs.editor.textData  = that.form.manufacturing ;
     }) 
      });
  
    },
    /** 提交按钮 */
    submitForm: function() {
      let that = this;
      this.$refs["form"].validate(valid => {
        if (valid) {
          let ids =this.form.ingredientsIds;
          console.log(this.form);
          this.form.ingredientsIds = ids.toString();
          if (this.form.id != undefined) {
            // 修改form id 
            updateFood(this.form).then(response => {
              if (response.code === 200) {
                that.$refs.editor.editorData  = '' ;  
                that.$modal.msgSuccess("修改成功");
                // alert("修改成功")
                that.open = false;
                that.getList();
              } else {
                that.msgError(response.msg);
              }
            });
          } else {
            addFood(this.form).then(response => {
              if (response.code === 200) {
                this.$modal.msgSuccess("新增成功");
                this.open = false;
                this.getList();
              } else {
                this.msgError(response.msg);
              }
            });
          }
        }
      });
    },

    onChange(editor){
      // debugger
      console.log("aa"+editor);
      var that = this;  
      that.form.manufacturing = editor;
}
  }
};
</script>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值