哦屋~如此完美的富文本编辑器你值得拥有

效果图

Tinymce

前景:发布文章得有一个好看好用的富文本编辑器,这个文本编辑器得支持文字编辑,背景编辑,上传图片等等功能,这边富文本编辑器我按word的功能顺序排版。

Vue篇

下载tinymce

npm i tinymce -S
复制代码
  • 在node_module/tinymce复制到/static下
  • 在自己的组件文件夹components创建TinyMce文件夹

/src/components/TinyMce/index.vue

<template>
  <div>
    <input type="file" id="photoFileUpload" style="display: none" />
    <textarea :id="Id"></textarea>
  </div>
</template>
<script>
import { ossUpload, uploadImg } from '../../api/public'
import '../../../static/tinymce/tinymce'
export default {
  name: 'mceeditor',
  props: {
    value: {
      default: '',
      type: String
    },
    config: {
      type: Object,
      default: () => {
        return {
          theme: 'modern',
          height: 600
        }
      }
    },
    url: {
      default: '',
      type: String
    },
    accept: {
      default: 'image/jpeg, image/png',
      type: String
    },
    maxSize: {
      default: 2097152,
      type: Number
    }
  },
  data () {
    const Id = Date.now()
    return {
      Id: Id,
      myEditor: null,
      DefaultConfig: {
        branding: false, // 隐藏右下角logo
        // GLOBAL
        language: 'zh_CN', // 汉化
        height: 500, // 默认高度
        theme: 'modern', // 默认主题
        menubar: true,
        toolbar: [
          'undo redo fontselect fontsizeselect removeformat imagetools paste uploadimage',
          'bold italic underline strikethrough backcolor forecolor alignleft aligncenter alignright alignjustify bullist numlist outdent indent blockquote link unlink image code print preview media fullpage fullscreen emoticons'], // 需要的工具栏
        plugins: `
            paste
            importcss
            image
            code
            table
            advlist
            fullscreen
            link
            lists
            textcolor
            colorpicker
            hr
            preview
          `,
        // CONFIG
        forced_root_block: 'p',
        force_p_newlines: true,
        importcss_append: true,
        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
        content_style: `
            *                         { padding:0; margin:0; }
            html, body                { height:100%; }
            img                       { max-width:100%; display:block;height:auto; }
            a                         { text-decoration: none; }
            iframe                    { width: 100%; }
            p                         { line-height:1.6; margin: 0px; }
            table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
            .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
            ul,ol                     { list-style-position:inside; }
          `,
        insert_button_items: 'image link | inserttable',
        // CONFIG: Paste
        paste_retain_style_properties: 'all',
        paste_word_valid_elements: '*[*]', // word需要它
        paste_data_images: true, // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
        paste_convert_word_fake_lists: false, // 插入word文档需要该属性
        paste_webkit_styles: 'all',
        paste_merge_formats: true,
        nonbreaking_force_tab: false,
        paste_auto_cleanup_on_paste: false,
        // CONFIG: Font
        fontsize_formats: '12px 14px 16px 18px 20px 24px',
        // CONFIG: StyleSelect
        style_formats: [
          {
            title: '首行缩进',
            block: 'p',
            styles: { 'text-indent': '2em' }
          },
          {
            title: '行高',
            items: [
              { title: '1', styles: { 'line-height': '1' }, inline: 'span' },
              { title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span' },
              { title: '2', styles: { 'line-height': '2' }, inline: 'span' },
              { title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span' },
              { title: '3', styles: { 'line-height': '3' }, inline: 'span' }
            ]
          }
        ],
        // FontSelect
        font_formats: `
            微软雅黑=微软雅黑;
            宋体=宋体;
            黑体=黑体;
            仿宋=仿宋;
            楷体=楷体;
            隶书=隶书;
            幼圆=幼圆;
            Andale Mono=andale mono,times;
            Arial=arial, helvetica,
            sans-serif;
            Arial Black=arial black, avant garde;
            Book Antiqua=book antiqua,palatino;
            Comic Sans MS=comic sans ms,sans-serif;
            Courier New=courier new,courier;
            Georgia=georgia,palatino;
            Helvetica=helvetica;
            Impact=impact,chicago;
            Symbol=symbol;
            Tahoma=tahoma,arial,helvetica,sans-serif;
            Terminal=terminal,monaco;
            Times New Roman=times new roman,times;
            Trebuchet MS=trebuchet ms,geneva;
            Verdana=verdana,geneva;
            Webdings=webdings;
            Wingdings=wingdings,zapf dingbats`,
        // Tab
        tabfocus_elements: ':prev,:next',
        object_resizing: true,
        // Image
        imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
      }
    }
  },
  methods: {
    setContent (content) {
      this.myEditor.setContent(content)
    },
    getContent () {
      return this.myEditor.getContent()
    },
    init () {
      const self = this
      window.tinymce.init({
        // 默认配置
        ...this.DefaultConfig,
        // 挂载的DOM对象
        selector: `#${this.Id}`,
        file_picker_types: 'file',
        // 上传文件
        file_picker_callback: function (callback, value, meta) {
          let fileUploadControl = document.getElementById('photoFileUpload')
          fileUploadControl.click()
          fileUploadControl.onchange = function () {
            if (fileUploadControl.files.length > 0) {
              let localFile = fileUploadControl.files[0]
              ossUpload({ type: localFile.type }).then(res => {
                uploadImg(res, localFile).then(res => {
                  if (res.code === 0) {
                    callback(res.data.name, { text: localFile.name })
                    self.$emit('on-upload-complete', res) // 抛出 'on-upload-complete' 钩子
                  } else {
                    callback()
                    self.$emit('on-upload-complete', res) // 抛出 'on-upload-complete' 钩子
                  }
                })
              })
            } else {
              alert('请选择文件上传')
            }
          }
        },
        // 图片上传
        images_upload_handler: function (blobInfo, success, failure) {
          console.log('result==>', blobInfo, success, failure)
          if (blobInfo.blob().size > self.maxSize) {
            failure('文件体积过大')
          }
          if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
            uploadPic()
          } else {
            failure('图片格式错误')
          }

          async function uploadPic () {
            ossUpload().then(res => {
              uploadImg(res, blobInfo.blob()).then(res => {
                if (res.code === 0) {
                  success(res.data.name)
                  self.$emit('on-upload-complete', res) // 抛出 'on-upload-complete' 钩子
                } else {
                  failure('上传失败: ')
                  self.$emit('on-upload-complete', res) // 抛出 'on-upload-complete' 钩子
                }
              })
            })
          }
        },
        // prop内传入的的config
        ...this.config,
        setup: (editor) => {
          self.myEditor = editor
          editor.on(
            'init', () => {
              self.loading = true
              self.$emit('on-ready') // 抛出 'on-ready' 事件钩子
              editor.setContent(self.value)
              self.loading = false
            }
          )
          // 抛出 'input' 事件钩子,同步value数据
          editor.on(
            'input change undo redo', () => {
              self.$emit('input', editor.getContent())
            }
          )
        }
      })
    },
    // 清空富文本框数据
    clear () {
      this.myEditor.setContent('')
    }
  },
  mounted () {
    this.init()
  },
  beforeDestroy () {
    // 销毁tinymce
    this.$emit('on-destroy')
    window.tinymce.remove(`#${this.Id}`)
  }
}
</script>
复制代码

/src/api/public.js

// 通用型api
import axios from 'axios'

// 文件上传 api 地址
export const ossUpload = () => {
  return axios.get(`http://localhost:3003/auth/ali`, {})
}

export const uploadImg = (data, file) => {
  let ossConfig = data.data
  let uploadUrl = data.data.url
  let formData = new FormData()
  formData.append('OSSAccessKeyId', ossConfig.OSSAccessKeyId)
  formData.append('Signature', ossConfig.signature)
  formData.append('key', ossConfig.key)
  formData.append('policy', ossConfig.policy)
  formData.append('Content-Type', file.type)
  formData.append('file', file)
  return axios.post(uploadUrl, formData).then(res => {
    let res1 = {
      code: 0,
      data: {name: data.data.imgUrl},
      msg: ''
    }
    return Promise.resolve(res1)
  }).catch(err => {
    return err
  })
}
复制代码

页面引用此组件用法(这边有个坑,代码报错需要声明window.tinymce.baseURL)

<template>
    <div class="form-box">
      <div class="title-box">
        <p class="title">标题</p>
        <input class="input" type="text" />
      </div>
      <!--<editor-->
        <!--class="editor"-->
        <!--ref="edit"-->
        <!--:setting="editorSetting"-->
        <!--@onContent="onContent">-->

      <!--</editor>-->
      <editor
        ref="edit"
        v-model="content"
        @on-upload-complete="onEditorUploadComplete" />
      <p class="btn-sub clear" @click="clear">清 空</p>
      <p class="btn-sub" @click="submit">提 交</p>
    </div>
</template>

<script>
import editor from '../../components/TinyMce/index.vue'
window.tinymce.baseURL = '/static/tinymce' // 需要调用tinymce的组件中得加入这,不然会报错
export default {
  name: 'logForm',
  components: {
    editor
  },
  mounted () {
    this.$nextTick(() => {
      console.log(this.$refs.edit)
    })
  },
  data: function () {
    return {
      content: '',
      // tinymce的配置信息 参考官方文档 https://www.tinymce.com/docs/configure/integration-and-setup/
      editorSetting: {
        height: 600
      }
    }
  },
  methods: {
    submit () {
      console.log(this.content)
    },
    clear () {
      this.$refs.edit.clear()
    },
    onContent (txt) {
      this.content = txt
    },

    onEditorUploadComplete (res) {
      if (res.code === 0) {
        this.$message({
          type: 'success',
          message: '上传成功'
        })
      } else {
        this.$message({
          type: 'error',
          message: res.msg
        })
      }
    },
    set () {
      this.$refs.richText.setContent('设置内容')
    },
    get () {
      console.log(this.$refs.richText.getContent())
    }
  }
}
</script>

<style scoped>
  .title-box{
    display: flex;
    margin-bottom: 20px;
    flex-direction: row;
    align-items: center;
  }
  .title{
    font-size: 20px;
    font-weight: bold;
  }
  .input{
    flex: 1;
    margin-left: 12px;
    border-radius: 4px;
    line-height: 32px;
    outline: none;
    padding: 0px 10px;
    border-style: solid;
    border-width: 1px;
    border-color: #a8a8a8;
  }
  .form-box{
    padding: 60px;
  }
  .btn-sub{
    display: inline-block;
    font-size: 16px;
    padding: 10px 30px;
    background-color: #348eed;
    border-radius: 4px;
    cursor: pointer;
    color: white;
    margin-top: 20px;
    transition: opacity .3s linear;
    opacity: 1;
  }
  .btn-sub:hover{
    opacity: .8;
  }
  .btn-sub:active{
    position: relative;
    left:1px;
    top:1px;
  }
  .btn-sub.clear{
    margin-right: 20px;
    background-color: gainsboro;
  }
</style>
复制代码

Node篇

思路:node主要实现阿里的oss图片上传功能,node通过配置阿里的oss的key和密钥去换取签名,然后将key、签名、请求阿里路径等参数传给前端,前端通过这个路径加参数即可将本地图片上传到阿里oss平台。

const Koa  = require('koa')
const app = new Koa()
const Router = require('koa-router');
const router = new Router();
const OSS = require('ali-oss');

const config = {
  bucket: 'img-o-wu', // 你自己创建的命名空间
  region: 'oss-cn-hangzhou',// 区域,你要填你在阿里云选的
  accessKeyId: '你的accessKeyId',
  accessKeySecret: '你的accessKeySecret',
  expAfter: 300000, // 签名失效时间,毫秒
  maxSize: 1048576000 // 文件最大的 size
}
const client = new OSS(config);

// 处理跨域请求
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', 'http://localhost:8080'); // 需要跨域的地址
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200;
  } else {
    await next();
  }
 });

//子路由ali-oss
let auth = new Router();
auth.get('/ali',async(ctx)=>{
  console.log('请求ali-oss服务',ctx);
  const url = `https://${config.bucket}.${config.region}.aliyuncs.com`
  const expireTime = new Date().getTime() + config.expAfter
  const expiration = new Date(expireTime).toISOString()
  const policyString = JSON.stringify({
    expiration,
    conditions: [
      ['content-length-range', 0, config.maxSize]
    ]
  })
  const policy = Buffer.from(policyString).toString('base64')
  const signature = client.signature(policy)
  ctx.body={
    signature, // 签名
    policy,
    url, // 前端请求阿里oss地址
    'OSSAccessKeyId': config.accessKeyId, // 你的accessKeyId
    'key': expireTime, // 文件时间戳
    'success_action_status': 201,
    'imgUrl': client.signatureUrl(expireTime.toString()) // 访问图片全路径
  };
});
// 二级路由
router.use('/auth',auth.routes(),auth.allowedMethods());

app.use(router.routes()).use(router.allowedMethods());

app.listen(3003, () => {
    console.log('myBlog is run')
})

复制代码

直接请求http://localhost:3003/auth/ali获取node服务端签好名的配置

注意:

有一个疑问就是获取上传图片前路径我本来以为是在前端请求阿里的上传接口时就跨域拿到了,too young too simple,别想当然了,服务端的ali-oss这个阿里的库已经帮我们实现了,拿来主义^_^,直接client.signatureUrl(expireTime.toString()),这时候把访问图片的全路径直接作为参数传到前端即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值