rendering

Tinymce

Foreground: Publishing articles requires a good-looking rich text editor, which supports text editing, background editing, uploading pictures and other functions. Here, I typeset the rich text editor according to the function sequence of Word.

Vue article

Download the tinymce

npm i tinymce -S
Copy the code
  • Copy to /static on node_module/tinymce
  • Create the TinyMce folder in your component folder Components

/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, // Hide the logo in the bottom right corner // GLOBAL Language:'zh_CN'// Default height: 500, // Default height: theme'modern', // Default theme 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'], // Need toolbar 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: < img style > 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 requires it to paste_data_images:true// Paste_convert_word_fake_listsfalsePaste_webkit_styles: // Inserting word documents requires this property:'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: 'First indent',
            block: 'p',
            styles: { 'text-indent': '2em' }
          },
          {
            title: 'high line',
            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: 'Microsoft Yahy = Microsoft Yahy; 宋体=宋体; Black body = black body; Imitation song = imitation song; Regular = regular; Li Shu = Li Shu; Small circle = small circle; 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({// default configuration... This. DefaultConfig, // Mount the DOM object selector: '#${this.Id}`,
        file_picker_types: 'file'// Upload 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) // Throw'on-upload-complete'Hook}else {
                    callback()
                    self.$emit('on-upload-complete', res) // Throw'on-upload-complete'Hook}})})}else {
              alert('Please select file upload'}}}, // Images_upload_handler:function (blobInfo, success, failure) {
          console.log('result==>', blobInfo, success, failure)
          if (blobInfo.blob().size > self.maxSize) {
            failure('File size too large')}if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
            uploadPic()
          } else {
            failure('Picture format error')
          }

          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) // Throw'on-upload-complete'Hook}else {
                  failure('Upload failed:')
                  self.$emit('on-upload-complete', res) // Throw'on-upload-complete'Hook}})})}}, // prop passed in config... this.config, setup: (editor) => { self.myEditor = editor editor.on('init', () => {
              self.loading = true
              self.$emit('on-ready') / / throw'on-ready'Event hook Editor.setContent (self.value) self.loading =false}) // throw'input'Event hook to synchronize value data editor.on('input change undo redo', () => {
              self.$emit('input', editor.getContent())})}})}, // Empty the rich text box dataclear () {
      this.myEditor.setContent(' ')}},mounted () {
    this.init()
  },
  beforeDestroy() {// Destroy tinymce this.$emit('on-destroy')
    window.tinymce.remove(`#${this.Id}`)
  }
}
</script>
Copy the code

/src/api/public.js

// Generic API
import axios from 'axios'

// File upload API address
export const ossUpload = (a)= > {
  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
  })
}
Copy the code

Page references this component usage (there is a pit here, code error needs to declare window.tinymce.baseurl)

<template>
    <div class="form-box">
      <div class="title-box">
        <p class="title">The 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">Clear the air</p>
      <p class="btn-sub" @click="submit">To hand in</p>
    </div>
</template>

<script>
import editor from '.. /.. /components/TinyMce/index.vue'
window.tinymce.baseURL = '/static/tinymce' // Add this to the component that needs to call tinymce, otherwise an error will be reported
export default {
  name: 'logForm'.components: {
    editor
  },
  mounted () {
    this.$nextTick((a)= > {
      console.log(this.$refs.edit)
    })
  },
  data: function () {
    return {
      content: ' './ / tinymce configuration information Refer to the official documentation at 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: 'Upload successful'})}else {
        this.$message({
          type: 'error'.message: res.msg
        })
      }
    },
    set () {
      this.$refs.richText.setContent('Set content')
    },
    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>
Copy the code

The Node article

Thread: Node mainly realizes ali’s OSS image uploading function. Node configures ali’s OSS key and key to exchange for signature, and then transmits key, signature, request Ali path and other parameters to the front end. The front end can upload local images to Ali’s OSS platform through this path and parameters.

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'.// Create your own namespace
  region: 'oss-cn-hangzhou'.// You need to fill in the area you selected in Aliyun
  accessKeyId: 'your accessKeyId'.accessKeySecret: 'your accessKeySecret'.expAfter: 300000.// Signature expiration time, milliseconds
  maxSize: 1048576000 // Maximum file size
}
const client = new OSS(config);

// Process cross-domain requests
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin'.'http://localhost:8080'); // A cross-domain address is required
  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 {
    awaitnext(); }});// The child path is ali-oss
let auth = new Router();
auth.get('/ali'.async(ctx)=>{
  console.log('Request ali-OSS service',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, / / signature
    policy,
    url, // the front-end requests ali for the OSS address
    'OSSAccessKeyId': config.accessKeyId, / / your accessKeyId
    'key': expireTime, // File timestamp
    'success_action_status': 201.'imgUrl': client.signatureUrl(expireTime.toString()) // Access the full path of the image
  };
});
// Secondary route
router.use('/auth',auth.routes(),auth.allowedMethods());

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

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

Direct request to http://localhost:3003/auth/ali for the node server sign a good configuration

Note:

There is a question about obtaining the path before uploading the image. I thought it was too young too simple when I asked ali’s upload interface. Ali’s library ali-OSS on the server has already implemented this for us. SignatureUrl (expiretime.toString ())), in this case, the full path to access the image can be directly as a parameter to the front end.