vue使用tinymce5富文本编辑器

目录

 效果图:

​ ​​​​客户端:

view/scwz/index.vue:

view/wzlb/index.vue:

view/wzlbEdit.vue:

util/index.js

router/index.js

 util/alioss.js

request.js

api/testmd/index.js

main.js

vue.config.js

服务端

models/Testmd.js

routes/testmd.js

app.js

db.js


项目gitee地址: https://gitee.com/LiDaJiang/git_vue2_koa2_mongoose_boke_alioss

 效果图:

 

安装tinymce和@tinymce/tinymce-vue

cnpm i -S tinymce@5.7.1 @tinymce/tinymce-vue@^3 

版本分别为:"@tinymce/tinymce-vue": "^3.2.8", "tinymce": "^5.7.1",

然后会发现是英文的,想换位中文。

在public中新建文件夹tinymce,在tinymce中导入zh_CN.js即可

为了让视频在上传后可以实时预览,需要修改tinymce/plugins/media/plugin.js

大概在第619行的位置

return `<video width="${data.width}" height="${data.height}"  controls="controls" ${data.poster ? poster = data.poster : ""} src="${data.source}"</video>`;

提前把nodemodule/tinymce/media/

客户端和服务端需要改动的结构目录:

客户端:

view/scwz/index.vue:

<!--scwz 上传文章-->
<template>
  <div class="scwz">
    <el-card v-loading="isLoading">
      <div slot="header" class="clearfix">
        <span style="font-size: 23px">上传文章</span>
        <el-button type="primary" style="float: right" @click="addMd()">
          上传
          <i class="el-icon-upload el-icon--right"></i>
        </el-button>
        <el-button
          type="warning"
          style="float: right; margin: 0 10px"
          @click="resetForm('ref_form_scwz')"
        >
          重置
          <i class="el-icon-refresh-left el-icon--right"></i>
        </el-button>
        <el-button @click="$router.push({name:'wzlb'})" sizi="mini" style="float: right">跳转到"文章列表"页面</el-button>
      </div>
      <el-form ref="ref_form_scwz" :model="formData" label-width="100px">
        <el-form-item label="标题:" prop="title">
          <el-input v-model="formData.title" autocomplete="off" style="width: 200px" clearable></el-input>
        </el-form-item>
        <el-form-item label="内容:" prop="content">
          <editor :init="editConfig" v-model="formData.content" />
        </el-form-item>
      </el-form>
    </el-card>
    <div
      id="tinymce_vue_scwz"
      v-html="formData.content"
      style="position: fixed; visibility: hidden"
    ></div>
    <el-dialog title="上传进度" :visible.sync="dialogTableVisible" :close-on-click-modal="false">
      <el-progress :text-inside="true" :stroke-width="26" :percentage="percentage"></el-progress>
    </el-dialog>
  </div>
</template>

<script>
import { addMd } from "@/api/testmd/index";
import Editor from "@tinymce/tinymce-vue";
import tinymce from "tinymce/tinymce";
import client_alioss from "@/utils/alioss.js";
import { deleteUrls, get_urlImgVideo, arrayDifference } from "@/utils/index.js";

import "tinymce/plugins/image"; // 插入上传图片插件
import "tinymce/plugins/media"; // 插入视频插件
import "tinymce/plugins/table"; // 插入表格插件
import "tinymce/plugins/lists"; // 列表插件
import "tinymce/plugins/wordcount"; // 字数统计插件
import "tinymce/plugins/colorpicker";
import "tinymce/plugins/textcolor";
import "tinymce/plugins/link"; //超链接插件
import "tinymce/plugins/code"; //代码块插件
import "tinymce/plugins/contextmenu"; //右键菜单插件
import "tinymce/plugins/fullscreen"; //全屏
import "tinymce/plugins/help";
import "tinymce/plugins/charmap";
import "tinymce/plugins/paste";
import "tinymce/plugins/print";
import "tinymce/plugins/preview";
import "tinymce/plugins/hr";
import "tinymce/plugins/anchor";
import "tinymce/plugins/pagebreak";
import "tinymce/plugins/spellchecker";
import "tinymce/plugins/searchreplace";
import "tinymce/plugins/visualblocks";
import "tinymce/plugins/visualchars";
import "tinymce/plugins/insertdatetime";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/autosave";
import "tinymce/plugins/fullpage";
import "tinymce/plugins/toc";

import "tinymce/themes/silver";
import "tinymce/icons/default";

export default {
  name: "scwz",
  components: {
    editor: Editor
  },
  data() {
    return {
      tempCheckpoint: "",
      percentage: 0,
      dialogTableVisible: false, //上传进度窗口

      urlImgVideo_start: [], //页面一加载完,就获取编辑器内容中的img和video地址
      urlImgVideo_uploaded: [], //所有上传的img和video的地址。包括 原来已上传的+新上传的地址
      urlImgVideo_end: [], //最后提交编辑内容时,当前的编辑器内容中的img和video地址

      isLoading: false,
      formData: {
        title: "",
        content: ``,
        date: ""
      },
      editConfig: {
        height: "800", //富文本高度
        language_url: "/tinymce/zh_CN.js", //中文包
        language: "zh_CN", //中文

        skin_url: `/tinymce/skins/ui/oxide`,
        content_css: `/tinymce/skins/content/default/content.css`,
        // skin_url: `/tinymce/skins/ui/oxide-dark`, // 暗色系
        // content_css: `/tinymce/skins/content/dark/content.css`, // 暗色系

        browser_spellcheck: true, // 拼写检查
        branding: false, // 去水印
        elementpath: true, //禁用编辑器底部的状态栏
        statusbar: true, // 隐藏编辑器底部的状态栏
        paste_data_images: true, // 是否允许粘贴图像
        menubar: true, // 最上方menu
        fontsize_formats: "14px 16px 18px 20px 24px 26px 28px 30px 32px 36px", //字体大小
        font_formats:
          "微软雅黑=Microsoft YaHei,Helvetica Neue;PingFang SC;sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun;serifsans-serif;Terminal=terminal;monaco;Times New Roman=times new roman;times", //字体
        file_picker_types: "image",
        images_upload_credentials: true,

        plugins: [
          " lists link image charmap print preview anchor",
          "searchreplace visualblocks code fullscreen ",
          "insertdatetime media table paste code help wordcount textcolor wordcount"
        ],
        toolbar:
          "undo redo |formatselect |fontselect fontsizeselect link lineheight forecolor backcolor bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | image media quicklink  outdent indent blockquote table numlist bullist preview fullscreen | removeformat",
        // 图片上传三个参数,图片数据,成功时的回调函数,失败时的回调函数
        images_upload_handler: async (blobInfo, success, failure) => {
          //使用base64在本地加载显示。大图片转为base64后存在数据库会报错。
          // success("data:image/png;base64," + blobInfo.base64());

          //图片上传阿里云oss,并在本地加载显示
          // let filename = blobInfo.filename();
          let { name } = blobInfo.blob();

          let { res } = await client_alioss.multipartUpload(
            name,
            blobInfo.blob()
          );
          if (res.requestUrls) {
            // new URL()解决大图片上传不显示的问题。decodeURIComponent()解决中文乱码的问题
            let { origin, pathname } = new URL(res.requestUrls[0]);
            let decodeImgUrl = origin + decodeURIComponent(pathname);
            success(decodeImgUrl);

            this.urlImgVideo_uploaded.push(decodeImgUrl);
          }
        },
        //上传视频
        file_picker_types: "media", //添加后才会显示出本地上传图标
        // 在tinymce5.0版本中这个钩子函数代替了原来的file_browser_callback,
        // 有三个参数:callback 、value 、meta,其中callback的作用是将所选择的视频的url显示在输入框中。
        file_picker_callback: async (callback, value, meta) => {
          if (meta.filetype == "media") {
            let input = document.createElement("input"); //创建一个隐藏的input
            input.setAttribute("type", "file");
            let that = this;
            //下面这个onchange函数不要用箭头函数!!!,否则报错
            input.onchange = async function() {
              let file = this.files[0]; //选取第一个文件
              let { name, size, type } = file;

              let ext = name.split(".")[1];
              if (ext !== "mp4") {
                that.$message.error({
                  message: "上传资源只能是 mp4 格式的视频!",
                  showClose: true
                });
                return false;
              }

              that.dialogTableVisible = true;

              let result = await client_alioss.multipartUpload(name, file, {
                progress: function(p, checkpoint) {
                  that.percentage = parseFloat((p * 100).toFixed(2));
                  that.tempCheckpoint = checkpoint;
                }
              });
              if (that.percentage == 100) {
                that.dialogTableVisible = false;
                that.$message({
                  showClose: true,
                  message: "上传成功!",
                  type: "success"
                });
              }

              if (result) {
                let { origin, pathname } = new URL(result.res.requestUrls[0]);
                let decodeVideoUrl = origin + decodeURIComponent(pathname);
                callback(decodeVideoUrl);

                that.urlImgVideo_uploaded.push(decodeVideoUrl);
              }
            };
            //触发点击
            input.click();
          }
        }
      }
    };
  },
  computed: {},
  mounted() {},
  methods: {
    //提交编辑内容
    async addMd() {
      this.isLoading = true;
      let { title, content } = this.formData;
      let {
        data: { code }
      } = await addMd({ title, content });

      //删除已上传到服务器中的那些在编辑器内容中没有使用的img和video地址
      //提交编辑器内容,给urlImgVideo_end属性赋值。获取当前富文本编辑器内容中的所有img ,video标签中的src,返回数组。
      this.urlImgVideo_end = [...get_urlImgVideo("tinymce_vue_scwz")];

      if (this.urlImgVideo_uploaded.length !== 0) {
        deleteUrls(
          arrayDifference(this.urlImgVideo_uploaded, this.urlImgVideo_end),
          client_alioss
        );
      }
      setTimeout(() => {
        if (code == 200) {
          this.isLoading = false;

          this.$message.success("上传成功");
          this.$refs["ref_form_scwz"].resetFields(); //注意:上传成功后,这里用这一行代码清空表单和富文本编辑框内容就行了,不要使用resetForm()方法
          //注意:这里不要跳转到文章列表页面,否则上传文章页面会有缓存,清空不了
          this.urlImgVideo_uploaded = [];
        }
      }, 1000);
    },
    //点击重置按钮
    async resetForm(formName) {
      this.$refs[formName].resetFields(); //清空表单

      //上传了img、video,又点击重置按钮,就需要把已上传的img、video地址给删掉
      deleteUrls(this.urlImgVideo_uploaded, client_alioss);
      this.urlImgVideo_uploaded = [];
    }
  }
};
</script>

<style lang="scss" scoped>
.scwz {
  padding: 10px;
  color: black;
}
</style>

view/wzlb/index.vue:

<!--wzlb 文章列表 -->
<template>
  <div class="wzlb">
    <el-card v-show="!isShowEditPage">
      <div slot="header" class="clearfix">
        <span style="font-size: 23px">文章列表</span>
        <el-button @click="getMdList()" sizi="mini" icon="el-icon-refresh" style="float: right">刷新</el-button>
        <el-button @click="$router.push({name:'scwz'})" sizi="mini" style="float: right">跳转到"上传文章"页面</el-button>
      </div>
      <el-table :data="tableData" border>
        <el-table-column label="标题" prop="title" align="center"></el-table-column>
        <el-table-column label="更新日期" prop="date" align="center">
          <template slot-scope="scope">
            <i class="el-icon-time"></i>
            <span style="margin-left: 10px">{{ scope.row.date | formatDate('YYYY-MM-DD HH:mm:ss') }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="300px">
          <template slot-scope="scope">
            <el-button size="mini" @click="showPreviewDialog(scope.row)">
              <i class="el-icon-view" /> 预览
            </el-button>
            <el-button size="mini" @click="showEditPage(scope.row)">
              <i class="el-icon-edit" /> 编辑
            </el-button>
            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">
              <i class="el-icon-delete" /> 删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <!-- 预览文章 -->
    <el-dialog :title="formData.title" width="70%" :visible.sync="isPreviewDialogVisible">
      <div v-html="formData.content"></div>
    </el-dialog>
    <WzlbEdit v-if="isShowEditPage" :formData="formData" @cancelEdit="cancelEdit" @editMd="editMd"></WzlbEdit>
    <div
      id="tinymce_vue_wzlbDelete"
      v-html="htmlContent"
      style="position: fixed; visibility: hidden"
    ></div>
  </div>
</template>

<script>
import { getMdList, deleteMd, editMd } from "@/api/testmd/index";
import WzlbEdit from "./wzlbEdit";
import client_alioss from "@/utils/alioss.js";
import { deleteUrls, get_urlImgVideo } from "@/utils/index.js";

export default {
  name: "",
  components: { WzlbEdit },
  data() {
    return {
      isPreviewDialogVisible: false,
      isShowEditPage: false,
      htmlContent: ``,
      formData: {
        _id: "",
        title: "",
        content: "",
        date: ""
      },
      tableData: [
        {
          title: "afd",
          content: `asdf`,
          date: ""
        }
      ]
    };
  },
  props: {},
  created() {},
  mounted() {
    this.getMdList();
  },
  computed: {},
  methods: {
    //获取md列表
    async getMdList() {
      let { data } = await getMdList();
      this.tableData = data.data; //注意:这里赋值时,有的时候是data,有的时候是data.data,具体的要自己打印出来再赋值
    },
    //打开预览窗口
    showPreviewDialog(row) {
      this.isPreviewDialogVisible = true;
      this.formData = {
        title: row.title,
        content: row.content
      };
    },
    //打开编辑窗口
    showEditPage(row) {
      this.isShowEditPage = true;
      this.formData = {
        _id: row._id,
        title: row.title,
        content: row.content,
        date: row.date //不能少,因为编辑页面是主要监听的这个属性
      };
    },
    //编辑Md
    async editMd() {
      let {
        data: { code }
      } = await editMd(this.formData, this.formData._id);
      if (code == 200) {
        this.isShowEditPage = false;
        this.$notify({
          title: "成功",
          message: "编辑成功!",
          type: "success"
        });
        this.getMdList();
      }
    },
    //取消编辑
    cancelEdit() {
      this.isShowEditPage = false;
    },
    //删除Md
    handleDelete(row) {
      this.$confirm("此操作将永久删除该文章, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(async () => {
          this.$message({
            type: "success",
            message: "删除成功!",
            showClose: true
          });
          await deleteMd(row._id);
          //注意:判断数组是否为空,要用Array.length是否为0来判断,不要直接用if([]){}判断!!!
          //TODO :删除当前文章中所有保存在服务器中的图片和视频
          this.htmlContent = row.content;
          this.$nextTick(() =>
            deleteUrls(
              [...get_urlImgVideo("tinymce_vue_wzlbDelete")],
              client_alioss
            )
          );

          this.getMdList();
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
            showClose: true
          });
        });
    }
  },
  watch: {}
};
</script>

<style lang="scss" scoped>
.wzlb {
  padding: 10px;
}
</style>

view/wzlbEdit.vue:

<!--wzlbEdit   -->
<template>
  <div class="wzlbEdit">
    <el-card>
      <div slot="header" class="clearfix">
        <span style="font-size: 23px">编辑文章</span>
        <el-button
          @click="editMd('ref_form_wzlb')"
          type="primary"
          sizi="mini"
          style="float: right"
          icon="el-icon-edit"
        >确 定</el-button>
        <el-button
          sizi="mini"
          icon="el-icon-refresh-left"
          style="float: right; margin: 0 10px"
          @click="cancelEdit()"
        >取消编辑</el-button>
      </div>

      <el-form ref="ref_form_wzlb" :model="formData" :rules="rules" label-width="100px">
        <el-form-item label="标题" prop="title">
          <el-input v-model="formData.title" autocomplete="off" style="width: 90%"></el-input>
        </el-form-item>
        <el-form-item label="内容" prop="content">
          <editor :init="editConfig" v-model="formData.content" />
        </el-form-item>
      </el-form>
    </el-card>
    <div
      id="tinymce_vue_wzlbEdit"
      v-html="formData.content"
      style="position: fixed; visibility: hidden"
    ></div>
    <el-dialog title="上传进度" :visible.sync="dialogTableVisible" :close-on-click-modal="false">
      <el-progress :text-inside="true" :stroke-width="26" :percentage="percentage"></el-progress>
    </el-dialog>
  </div>
</template>

<script>
import Editor from "@tinymce/tinymce-vue";
import client_alioss from "@/utils/alioss.js";
import tinymce from "tinymce/tinymce";
import { deleteUrls, get_urlImgVideo, arrayDifference } from "@/utils/index.js";

import "tinymce/plugins/image"; // 插入上传图片插件
import "tinymce/plugins/media"; // 插入视频插件
import "tinymce/plugins/table"; // 插入表格插件
import "tinymce/plugins/lists"; // 列表插件
import "tinymce/plugins/wordcount"; // 字数统计插件
import "tinymce/plugins/colorpicker";
import "tinymce/plugins/textcolor";
import "tinymce/plugins/link"; //超链接插件
import "tinymce/plugins/code"; //代码块插件
import "tinymce/plugins/contextmenu"; //右键菜单插件
import "tinymce/plugins/fullscreen"; //全屏
import "tinymce/plugins/help";
import "tinymce/plugins/charmap";
import "tinymce/plugins/paste";
import "tinymce/plugins/print";
import "tinymce/plugins/preview";
import "tinymce/plugins/hr";
import "tinymce/plugins/anchor";
import "tinymce/plugins/pagebreak";
import "tinymce/plugins/spellchecker";
import "tinymce/plugins/searchreplace";
import "tinymce/plugins/visualblocks";
import "tinymce/plugins/visualchars";
import "tinymce/plugins/insertdatetime";
import "tinymce/plugins/nonbreaking";
import "tinymce/plugins/autosave";
import "tinymce/plugins/fullpage";
import "tinymce/plugins/toc";

import "tinymce/themes/silver";
import "tinymce/icons/default";

export default {
  name: "",
  components: { Editor },
  props: ["formData"],
  data() {
    return {
      tempCheckpoint: "",
      percentage: 0,
      dialogTableVisible: false, //上传进度窗口

      urlImgVideo_start: [], //页面一加载完,就获取编辑器内容中的img和video地址
      urlImgVideo_uploaded: [], //所有上传的img和video的地址。包括 原来已上传的+新上传的地址
      urlImgVideo_end: [], //最后提交编辑内容时,当前的编辑器内容中的img和video地址

      rules: {
        title: [{ required: true, message: "请输入标题", trigger: "blur" }]
      },
      editConfig: {
        height: "800", //富文本高度
        language_url: "/tinymce/zh_CN.js", //中文包
        language: "zh_CN", //中文

        skin_url: `/tinymce/skins/ui/oxide`,
        content_css: `/tinymce/skins/content/default/content.css`,
        // skin_url: `/tinymce/skins/ui/oxide-dark`, // 暗色系
        // content_css: `/tinymce/skins/content/dark/content.css`, // 暗色系

        browser_spellcheck: true, // 拼写检查
        branding: false, // 去水印
        elementpath: true, //禁用编辑器底部的状态栏
        statusbar: true, // 隐藏编辑器底部的状态栏
        paste_data_images: true, // 是否允许粘贴图像
        menubar: true, // 隐藏最上方menu
        fontsize_formats: "14px 16px 18px 20px 24px 26px 28px 30px 32px 36px", //字体大小
        font_formats:
          "微软雅黑=Microsoft YaHei,Helvetica Neue;PingFang SC;sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun;serifsans-serif;Terminal=terminal;monaco;Times New Roman=times new roman;times", //字体
        file_picker_types: "image",
        images_upload_credentials: true,
        plugins: [
          " lists link image charmap print preview anchor",
          "searchreplace visualblocks code fullscreen",
          "insertdatetime media table paste code help wordcount"
        ],
        toolbar:
          "undo redo |formatselect |fontselect fontsizeselect link lineheight forecolor backcolor bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | image media quicklink  outdent indent blockquote table numlist bullist preview fullscreen | removeformat",
        // 图片上传三个参数,图片数据,成功时的回调函数,失败时的回调函数
        images_upload_handler: async (blobInfo, success, failure) => {
          //使用base64在本地加载显示
          // success("data:image/jpeg;base64," + blobInfo.base64());

          //上传阿里云oss,并在本地加载显示
          let { name } = blobInfo.blob();
          let {
            res: { requestUrls }
          } = await client_alioss.multipartUpload(name, blobInfo.blob());
          if (requestUrls) {
            //new URL()解决大图片上传不显示的问题。decodeURIComponent()解决中文乱码的问题
            let { origin, pathname } = new URL(requestUrls[0]);
            let decodeImgUrl = origin + decodeURIComponent(pathname);
            success(decodeImgUrl);

            this.urlImgVideo_uploaded.push(decodeImgUrl);
          }
        },

        //上传视频
        file_picker_types: "media", //添加后才会显示出本地上传图标
        // 在tinymce5.0版本中这个钩子函数代替了原来的file_browser_callback,
        // 有三个参数:callback 、value 、meta,其中callback的作用是将所选择的视频的url显示在输入框中。
        file_picker_callback: async (callback, value, meta) => {
          if (meta.filetype == "media") {
            let input = document.createElement("input"); //创建一个隐藏的input
            input.setAttribute("type", "file");
            input.setAttribute("accept", "video/mp4");
            let that = this;
            //下面这个onchange函数不要用箭头函数!!!,否则报错
            input.onchange = async function() {
              let file = this.files[0]; //选取第一个文件
              let { name, size, type } = file;
              let ext = name.split(".")[1];

              if (ext !== "mp4") {
                that.$message.error({
                  message: "上传资源只能是 mp4 格式的视频!",
                  showClose: true
                });
                return false;
              }
              that.dialogTableVisible = true;

              let result = await client_alioss.multipartUpload(name, file, {
                progress: function(p, checkpoint) {
                  that.percentage = parseFloat((p * 100).toFixed(2));
                  that.tempCheckpoint = checkpoint;
                }
              });
              if (that.percentage == 100) {
                that.dialogTableVisible = false;
                that.$message({
                  showClose: true,
                  message: "上传成功!",
                  type: "success"
                });
              }
              if (result) {
                let { origin, pathname } = new URL(result.res.requestUrls[0]);
                let decodeVideoUrl = origin + decodeURIComponent(pathname);
                callback(decodeVideoUrl);

                that.urlImgVideo_uploaded.push(decodeVideoUrl);
              }
            };
            //触发点击
            input.click();
          }
        }
      }
    };
  },
  created() {},
  mounted() {},
  computed: {},
  methods: {
    //提交编辑
    editMd(formName) {
      this.$refs[formName].validate(async valid => {
        if (valid) {
          //删除已上传到服务器中的那些在编辑器内容中没有使用的img和video地址
          // this.get_urlImgVideo_end();
          this.urlImgVideo_end = [...get_urlImgVideo("tinymce_vue_wzlbEdit")];

          if (this.urlImgVideo_uploaded.length !== 0) {
            deleteUrls(
              arrayDifference(this.urlImgVideo_uploaded, this.urlImgVideo_end),
              client_alioss
            );
          }

          //更新数据库
          this.$emit("editMd");
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    },
    //点击取消编辑
    async cancelEdit() {
      //删除已上传到服务器中的那些在编辑器内容中没有使用的img和video地址
      if (this.urlImgVideo_uploaded.length !== 0) {
        deleteUrls(
          arrayDifference(this.urlImgVideo_uploaded, this.urlImgVideo_start),
          client_alioss
        );
      }
      this.$emit("cancelEdit");
    }
  },
  watch: {
    //有类似触发子组件钩子函数的作用
    "formData.date": {
      handler: function(newVal) {
        //由于formData中有date属性,而每次点击时时间都不同,所以每次点击编辑按钮都会触发监听
        this.$nextTick(() => {
          //初始化,给urlImgVideo_start属性赋值。页面一加载就获取当前富文本编辑器内容中的所有img ,video标签中的src,返回数组。
          this.urlImgVideo_start = [...get_urlImgVideo("tinymce_vue_wzlbEdit")];
          this.urlImgVideo_uploaded = [...this.urlImgVideo_start];
        });
      },
      immediate: true
    }
  }
};
</script>
<style lang="scss" scoped>
.wzlbEdit {
  /*去除upload组件过渡效果*/
  ::v-deep .el-upload-list__item {
    transition: none !important;
  }
}
</style>

util/index.js


//通过img和video的url组成的数组来删除服务器中的img,video
export async function deleteUrls(arr_deleteUrl = [], client_alioss = {}) {
    if (arr_deleteUrl.length == 0) {
        return
    }
    let arr_pathname = [];
    for (const item of arr_deleteUrl) {
        let { pathname } = new URL(item);//new URL()解决大图片加载失败问题。因为大图片的url中含有uploadid,导致url不能直接预览
        //decodeURIComponent函数解决中文乱码
        arr_pathname.push(decodeURIComponent(pathname));
    }
    if (arr_pathname.length !== 0) {
        await client_alioss.deleteMulti(arr_pathname);
    }
}

//获取img、video地址数组
export function get_urlImgVideo(elementId) {
    let arrImgVideo = document.getElementById(elementId).querySelectorAll("img,video");
    let arr = [];
    if (arrImgVideo.length !== 0) {
        arrImgVideo.forEach(item => {
            arr.push(decodeURIComponent(item.src));
        });
    }
    return arr;
}

// 两个数组取差集,返回arrA中有,而arrB中没有的。
// 如arrA = ["a", "b", "c", "d"];arrB = ["a", "b", "e"];则返回["c","d"]
export function arrayDifference(arrA, arrB) {
    return arrA.filter(item => !arrB.includes(item))
}

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [{
  path: '/',
  name: 'home',
  component: () => import("@/views/home.vue"),
  redirect: "/scwz",
  children: [{
    path: '/scwz',
    name: 'scwz',
    component: () => import("@/views/scwz/index")
  }, {
    path: '/wzlb',
    name: 'wzlb',
    component: () => import("@/views/wzlb/index")
  }]
}]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

 util/alioss.js

const OSS = require("ali-oss");
let client = new OSS({
    region: "oss-cn-chengdu",//这里改成自己的
    accessKeyId: "xxx",//这里改成自己的
    accessKeySecret: "xxx",//这里改成自己的
    bucket: "bucket-lijiang-test",//这里改成自己的
 
});
 
export default client;

request.js

import axios from 'axios'

const service = axios.create({
    timeout: 5000
})

service.interceptors.request.use(
    config => {
        return config
    },
    error => {
        return Promise.reject(error)
    }
)

service.interceptors.response.use(
    response => {
        return response
    },
    error => {
        return Promise.reject(error)
    }
)

export default service

api/testmd/index.js

import request from '@/utils/request'

//添加md
export function addMd(data) {
    return request({
        url: '/api/testmd/add',
        method: 'post',
        data
    })
}

//获取md列表
export function getMdList() {
    return request({
        url: '/api/testmd/getList',
        method: 'get',
    })
}

//  编辑md
export function editMd(data, _id) {
    return request({
        url: `/api/testmd/edit/${_id}`,
        method: 'post',
        data
    })
}

//  删除md
export function deleteMd(_id) {
    return request({
        url: `/api/testmd/delete/${_id}`,
        method: 'get'
    })
}

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)


import moment from 'moment'
//全局过滤器
Vue.filter('formatDate', function (value) {
  return moment(value).format('YYYY-MM-DD HH:mm')
})

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

vue.config.js

module.exports = {
    lintOnSave: false, //取消eslint
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000', //设置开发环境服务域名和端口号 别忘了加http
                changeOrigin: true, //這裡true表示实现跨域
                pathRewrite: {
                    '^/api': ''//使用时,如:axios.get('/api/login')
                }
            }
        }
    }
}

服务端

models/Testmd.js

const mongoose = require("../db");

const Schema = mongoose.Schema;
const testmdSchema = new Schema({

    title: {
        type: String
    },
    content: {
        type: String
    },

    date: {
        type: Date,
        default: Date.now
    }

})

module.exports = Testmd = mongoose.model("Testmd", testmdSchema,
    "testmd")

routes/testmd.js

const router = require('koa-router')()

const Testmd = require("../models/Testmd"); //引入模块模型

router.prefix('/testmd')

//新增md
router.post('/add', async (ctx, next) => {
    let { title, content } = ctx.request.body;
    await Testmd.create({ title, content })
    ctx.body = { code: 200, message: "新增成功" }

})

//获取md列表
router.get('/getList', async (ctx, next) => {
    let data = await Testmd.find({})
    ctx.body = { code: 200, message: "新增成功", data }

})

//编辑md
router.post('/edit/:_id', async (ctx, next) => {
    let { title, content, date } = ctx.request.body;
    let { _id } = ctx.params
    await Testmd.findByIdAndUpdate(_id, { title, content, date })
    ctx.body = { code: 200, message: "编辑成功" }
})

//删除md
router.get('/delete/:_id', async (ctx, next) => {
    let { _id } = ctx.params;
    await Testmd.findByIdAndDelete(_id)
    ctx.body = { code: 200, message: "删除成功" }
})


module.exports = router

app.js

const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')

const index = require('./routes/index')
const users = require('./routes/users')

const testmd = require('./routes/testmd')


onerror(app)

// middlewares
app.use(bodyparser({
  enableTypes: ['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

app.use(views(__dirname + '/views', {
  extension: 'ejs'
}))

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

app.use(testmd.routes(), testmd.allowedMethods())


// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

db.js

//db.js
let mongoose = require('mongoose')
mongoose.set('useCreateIndex', true)
mongoose.set('useNewUrlParser', true)
mongoose.set('useUnifiedTopology', true)

const DB_NAME = 'test_boke' //数据库名,自己更换
const DB_URL = 'localhost:27017'
mongoose.connect(`mongodb://${DB_URL}/${DB_NAME}`)
mongoose.connection.on('open', err => {
    if (err) {
        console.log(err)
        return
    }
    console.log("数据库连接成功!")
})
module.exports = mongoose;

 

Vue2 Tinymce富文本编辑器是一种用于在Vue2项目中实现富文本编辑功能的插件。您可以按照以下步骤进行安装和使用: 1. 首先,您需要安装依赖。可以通过在终端中运行以下命令来安装依赖: ``` npm install tinymce ``` 2. 接下来,您需要将tinymce的skins文件夹复制到您的项目中。您可以在node_modules/tinymce目录下找到skins文件夹,并将其复制到您的src/assets/tinymce目录下。 3. 然后,您可以创建一个Tinymce.vue组件来封装Tinymce编辑器。可以根据您的需求进行自定义配置,***并将其放置在您的项目中。 5. 在Vue组件中使用Tinymce编辑器时,您可以直接导入Tinymce组件并在template中使用它。您可以根据需要通过props传递参数给Tinymce组件。 6. 最后,在整体的目录结构中,您需要确保Tinymce相关的文件和依赖正确地放置在对应的位置。 在使用Vue2 Tinymce富文本编辑器的过程中,您可能会遇到一些问题,比如路径找不到导致无法引入“tinymce/icons/default”的问题。这时,您可以尝试升级tinymce的版本来解决这个问题。 希望以上信息对您有所帮助!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【vuevue2 中使用 Tinymce 富文本编辑器](https://blog.csdn.net/qq_46123200/article/details/130099360)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值