哦屋~如此完美的富文本編輯器你值得擁有

效果圖

Tinymce

前景:發佈文章得有一個好看好用的富文本編輯器,這個文本編輯器得支持文字編輯,背景編輯,上傳圖片等等功能,這邊富文本編輯器我按word的功能順序排版。javascript

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平臺。css

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服務端簽好名的配置html

注意:

有一個疑問就是獲取上傳圖片前路徑我原本覺得是在前端請求阿里的上傳接口時就跨域拿到了,too young too simple,別想固然了,服務端的ali-oss這個阿里的庫已經幫咱們實現了,拿來主義^_^,直接client.signatureUrl(expireTime.toString()),這時候把訪問圖片的全路徑直接做爲參數傳到前端便可。前端

相關文章
相關標籤/搜索