vuetify学习第7天之v-editor---自定义封装富文本编辑器


目录


文章目录

  • 1、vue-quill-editor
  • 2、自定义封装
  • 3、常用属性和事件
  • 4、应用案例
  • ***后记*** :


内容


1、vue-quill-editor

Quill editor component for Vue.

基于 Quill、适用于 Vue 的富文本编辑器,支持服务端渲染和单页应用。

详情请参考github地址:https://github.com/surmon-china/vue-quill-editor#readme

2、自定义封装

  • 优点:vueQuillEditor组件为轻量级富文本编辑器,可全局或者部分导入,实现丰富的图文混排。
  • 缺点
  • 图片上传:图片转化为base64编码,当图片较大时,占用很多空间
  • 视频资源:只显示url地址。

根据以上考虑,我们基于vueQuillEditor封装自定义富文本编辑器,主要变更功能为图片上传,实现图片上传至给定url地址功能,同时添加字符统计功能。

  • Editor.vue源代码2-1:
<template>
  <div class="myEditor">
    <quilleditor v-model="content" ref="myTextEditor" :options="editorOption" @change="onChange">
      <div id="toolbar" slot="toolbar">
        <select class="ql-size">
          <option value="small"></option>
          <!-- Note a missing, thus falsy value, is used to reset to default -->
          <option selected></option>
          <option value="large"></option>
          <option value="huge"></option>
        </select>
        <!-- Add subscript and superscript buttons -->
        <span class="ql-formats">
          <button class="ql-script" value="sub"></button>
        </span>
        <span class="ql-formats">
          <button class="ql-script" value="super"></button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-bold"></button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-italic"></button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-blockquote"></button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-list" value="ordered"></button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-list" value="bullet"></button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-link"></button>
        </span>
        <span class="ql-formats">
          <button type="button" @click="imgClick" style="outline:none">
            <svg viewBox="0 0 18 18">
              <rect class="ql-stroke" height="10" width="12" x="3" y="4" />
              <circle class="ql-fill" cx="6" cy="7" r="1" />
              <polyline class="ql-even ql-fill" points="5 12 5 11 7 9 8 10 11 7 13 9 13 12 5 12" />
            </svg>
          </button>
        </span>
        <span class="ql-formats">
          <button type="button" class="ql-video"></button>
        </span>
      </div>
    </quilleditor>
    <div class="limit">
      当前已输入
      <span>{{currentChars}}</span> 个字符,您还可以输入
      <span>{{remainChars}}</span> 个字符。
    </div>
  </div>
</template>
<script>
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";

import { quillEditor } from "vue-quill-editor";

export default {
  name: "v-editor",
  props: {
    value: {
      type: String
    },
    /*上传图片的地址*/
    uploadUrl: {
      type: String,
      default: "/"
    },
    /*上传图片的file控件name*/
    fileName: {
      type: String,
      default: "file"
    },
    maxUploadSize: {
      type: Number,
      default: 1024 * 1024 * 500
    }
  },
  data() {
    return {
      content: "",
      editorOption: {
        modules: {
          toolbar: "#toolbar"
        }
      }
    };
  },
  methods: {
    onChange() {
      this.$emit("input", this.content);
    },
    /*选择上传图片切换*/
    onFileChange(e) {
      var fileInput = e.target;
      if (fileInput.files.length === 0) {
        return;
      }
      this.editor.focus();
      if (fileInput.files[0].size > this.maxUploadSize) {
        this.$message("图片不能大于500KB,图片尺寸过大");
      }
      var data = new FormData();
      data.append(this.fileName, fileInput.files[0]);
      this.axios.post(this.uploadUrl, data).then(res => {
        if (res.data) {
          this.editor.insertEmbed(
            this.editor.getSelection().index,
            "image",
            res.data
          );
        }
      });
    },
    /*点击上传图片按钮*/
    imgClick() {
      if (!this.uploadUrl) {
        console.log("no editor uploadUrl");
        return;
      }
      /*内存创建input file*/
      var input = document.createElement("input");
      input.type = "file";
      input.name = this.fileName;
      input.accept = "image/jpeg,image/png,image/jpg,image/gif";
      input.onchange = this.onFileChange;
      input.click();
    }
  },
  computed: {
    editor() {
      return this.$refs.myTextEditor.quill;
    },
    // 当前已输入字符数
    currentChars() {
      return this.content.length;
    },
    // 剩余可输入字符数
    remainChars() {
      let num = 10000 - Number(this.content.length);
      return num > 0 ? num : 0;
    }
  },
  components: {
    quilleditor: quillEditor
  },
  mounted() {
    this.content = this.value;
    this.currentChars;
    this.remainChars;
  },
  watch: {
    value(newVal) {
      if (this.editor) {
        if (newVal !== this.content) {
          this.content = newVal;
        }
      }
    }
  }
};
</script>

<style lang='scss' scoped>
.quill-editor {
  height: 400px;
}


.limit {
  height: 30px;
  border: 1px solid #ccc;
  line-height: 30px;
  text-align: right;
  padding: 0;
  margin: 0;
  margin: 10px 0;

  span {
    color: #ee2a7b;
  }
}
</style>

3、常用属性和事件

  • 常用属性详解

名称

类型

默认值

说明

value

string

‘’

编辑器双向绑定输入内容

options

object

{}

编辑器工具类配置

uploadUrl

string

‘’

图片上传地址

fileName

string

file

图片上传后台获取变量名称

  • 常用事件详解

名称

参数

说明

change

$event

blur

$event

focus

$event

ready

{$event, html, text }

4、应用案例

  • 需求:视频学习到了商品添加功能,涉及商品描述,需要图文混排,故选择富文本编辑器组件。
  • 商品表单源代码4-1:
<template>
  <v-stepper v-model="step">
    <v-stepper-header>
      <v-stepper-step :complete="step > 1" step="1">基本信息</v-stepper-step>

      <v-divider></v-divider>

      <v-stepper-step :complete="step > 2" step="2">商品描述</v-stepper-step>
      <v-divider></v-divider>

      <v-stepper-step :complete="step > 3" step="3">规格参数</v-stepper-step>
      <v-divider></v-divider>

      <v-stepper-step step="4">SKU属性</v-stepper-step>
    </v-stepper-header>

    <v-stepper-items>
      <v-stepper-content step="1">
        <v-row align="center">
          <v-col cols="6">
            <v-cascader-single
              v-model="goods.categories"
              label="请选择商品分类"
              url="/item/category/list"
              required
              showAllLevels
            />
          </v-col>
          <v-col cols="5">
            <v-select
              :items="brandList"
              v-model="goods.brandID"
              label="请选择品牌"
              item-value="id"
              item-text="name"
              dense
            >
              <template v-slot:selection="{ item }">
                <v-chip color="primary" close>{{ item.name }}</v-chip>
              </template>
            </v-select>
          </v-col>
        </v-row>
        <v-text-field label="商品标题" v-model="goods.title" required counter="200"></v-text-field>
        <v-text-field label="商品卖点" v-model="goods.subTitle" counter="200"></v-text-field>
        <v-textarea
          label="包装清单"
          v-model="spuDetail.packingList"
          counter="1000"
          :rows="3"
          no-resize
        ></v-textarea>
        <v-textarea label="售后服务" v-model="goods.aterService" counter="1000" :rows="3" no-resize></v-textarea>
      </v-stepper-content>
      <v-stepper-content step="2">
        <v-editor v-model="spuDetail.description" uploadUrl="/upload/image"></v-editor>
      </v-stepper-content>
      <v-stepper-content step="3">规格参数</v-stepper-content>
      <v-stepper-content step="4">SKU属性</v-stepper-content>
    </v-stepper-items>
  </v-stepper>
</template>

<script>
export default {
  name: "goods-form",
  props: {
    oldGoods: {
      type: Object
    },
    isEdit: {
      type: Boolean,
      default: false
    },
    step: {
      type: Number,
      default: 1
    }
  },
  data() {
    return {
      valid: false, // 表单校验结果标记
      goods: {
        categories: [], // 商品分类

        brandID: "", // 商品品牌
        title: "", // 商品标题
        subTitle: "" // 商品卖点
      },
      spuDetail: {
        packingList: "", // 包装清单
        afterService: "", // 售后服务
        descirption: "" // 商品描述
      },
      brandList: [], // 品牌列表
      brandName: "" // 选中品牌名称
    };
  },
  methods: {
    submit() {
      // 表单校验
      if (this.$refs.myGoodsForm.validate()) {
        // 定义一个请求参数对象,通过解构表达式来获取goods中的属性
        const { categories, letter, ...params } = this.goods;
        // 数据库中只要保存分类的id即可,因此我们对categories的值进行处理,只保留id,并转为字符串
        params.cids = categories.map(c => c.id).join(",");
        // 将字母都处理为大写
        params.letter = letter.toUpperCase();
        // 将数据提交到后台
        // this.$http.post('/item/goods', this.$qs.stringify(params))
        this.$http({
          method: this.isEdit ? "put" : "post",
          url: "/item/goods",
          data: this.$qs.stringify(params)
        })
          .then(() => {
            // 关闭窗口
            this.$emit("close");
            this.$message.success("保存成功!");
          })
          .catch(() => {
            this.$message.error("保存失败!");
          });
      }
    },
    clear() {
      // 重置表单
      this.$refs.myGoodsForm.reset();
      // 需要手动清空商品分类
      this.categories = [];
    }
  },
  watch: {
    "goods.categories": {
      // 监控分类的变化
      handler() {
        // 根据分类最后一级id查询品牌数据
        this.axios
          .get("/item/goods/cateBrand/" + this.goods.categories[2].id)
          .then(resp => {
            if (resp.status !== 200) {
              // 报错提示
            }
            this.brandList = resp.data;
          });
      },
      deep: true
    }
  }
};
</script>

<style scoped>
</style>
  • 效果图示4-1: