效果图
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())
,这时候把访问图片的全路径直接作为参数传到前端即可。