安装
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-react
# 或者 npm install @wangeditor/editor-for-react --save
引入
import '@wangeditor/editor/dist/css/style.css'; // 引入 css 必须
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
使用
<div style={{ border: '1px solid #ccc', zIndex: 100 }} className={style.container}>
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" style={{ borderBottom: '1px solid #ccc' }} />
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={(editor) => {
setHtml(editor.getHtml());
}}
mode="default"
style={{ height: '500px', overflowY: 'hidden' }}
/>
</div>
配置
工具栏配置
// 工具栏配置
const toolbarConfig = {
insertKeys:{
index: 5, // 插入的位置,基于当前的 toolbarKeys
keys: ['menu-key1', 'menu-key2'],
}
//... 其他
};
菜单配置
此处没有使用自带的上传,而是自定义上传到阿里云
// 编辑器配置
const editorConfig = {
placeholder: '请输入内容',
MENU_CONF: {
uploadImage: {
server: '/apiupload',
allowedFileTypes: ['image/*'],
async customUpload(file, insertFn) {
richerEditorUpload(file, 15, insertFn, 'img');
},
},
uploadVideo: {
server: '/apiupload',
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['video/*'],
async customUpload(file, insertFn) {
richerEditorUpload(file, 16, insertFn, 'video');
},
},
},
};
上传部分的逻辑——封装的ossUploader类
//封装的ossUploader类
import CryptoJS from 'crypto-js';
import Base64 from 'base-64';
import { getCurrentUser } from './authority';
import { getOss } from '@/services/oss';
const OSS = require('ali-oss');
const HOST = '/apiupload';
const LASTHOST = 'oss-cn-hangzhou';
let ACCESSKEYID = 'LTAI5tPCHQJLGnNA3NeHM7jV';
const ACCESSSECRET = 'uQw3w6OKJdcq2DMB71i3XlgHhKp6FJ';
// JavaScript AES解密函数
const customKey = 'CareBay=KxhSpxFWetnTjqMHYFiyBook'; // 必须是16字节
const getLastPoker = () => {
// if (process.env.NODE_ENV === 'development') {
// return "carebay-saas-test"
// } else if (window.location.hostname.includes('saas')) {
// return "carebay-saas-test"
// } else {
// return "carebay-saas"
// }
return localStorage.getItem('bucketName');
};
const BUCKET = getLastPoker();
console.log('BUCKET', BUCKET);
const POLICYTEXT = {
expiration: '2028-01-01T12:00:00.000Z', // 设置该Policy的失效时间,
conditions: [
['content-length-range', 0, 1048576000], // 设置上传文件的大小限制
],
};
const client = new OSS({
// 填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: LASTHOST,
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
accessKeyId: localStorage.getItem('ACCESSKEYID') || ACCESSKEYID,
// accessKeySecret: ACCESSSECRET,
accessKeySecret: localStorage.getItem('secretKey') || ACCESSSECRET,
// 填写存储空间名称。
bucket: BUCKET,
});
export default class ossUpload {
#state = {
todayKey: '',
host: HOST,
target: 1,
cardMap: {
1: '/account/', // 用户头像
2: '/contract/', // 合同
3: '/sales-literature/', // 销售子来哦,房间,套餐等等
4: '/nurse-literature/', // 专户师,房间,套餐等等
5: '/invoice-upload/', // 开票上传
6: '/warm-thanks-upload/', // 温馨答谢上传
7: '/customer-attachments/', // 客户附件上传
8: '/mallManagement-products/', // 商品图片上传
9: '/mallManagement-productDetail/', // 商品详情上传
10: '/mallManagement-ImageUploader/', // 商品详情价格和库存上传
11: '/addNewStore-ImageUploader/', // 新增门店图片上传
12: '/addGroupBuy-ImageUploader/', // 添加团购图片上传
13: '/homeManagement-ImageUploader/', // 首页管理图片上传
14: '/classificationOfProduction-ImageUploader/', // 首页管理图片上传
15: '/contentManagementImageUploader/', // 内容管理管理图片上传
16: '/contentManagementVideoUploader/', // 内容管理管理视频上传
},
// accessKeyId: ACCESSKEYID,
accessKeyId: localStorage.getItem('ACCESSKEYID') || ACCESSKEYID,
// accessSecret: ACCESSSECRET, || ACCESSKEYID
accessSecret: localStorage.getItem('secretKey') || ACCESSSECRET,
policyText: POLICYTEXT,
policy: '',
signature: '',
baseurl: 'tenant/' + getCurrentUser().tenantId,
};
constructor () {
this.#init();
}
#init () {
this.#initPolicy();
this.#initSignature();
}
#initPolicy () {
this.#state.policy = Base64.encode(JSON.stringify(this.#state.policyText));
}
#initSignature () {
this.#state.signature = CryptoJS.HmacSHA1(this.#state.policy, this.#state.accessSecret, { asBytes: true }).toString(CryptoJS.enc.Base64);
}
setName (name, target) {
this.#state.todayKey = name;
this.#state.target = target;
}
getLastUrlByKey (key) {
return client.signatureUrl(this.#state.baseurl + key, { expires: 3600 });
}
getLastUrl () {
if (!this.#state.todayKey) {
console.error('请先执行ossUpload类中,setName方法(在Upload组件中的beforeUpload函数里获取File里的name),用于初始化key');
return {
success: false,
};
}
return {
success: true,
key: this.#state.baseurl + this.#state.cardMap[this.#state.target] + this.#state.todayKey,
value: client.signatureUrl(this.#state.baseurl + this.#state.cardMap[this.#state.target] + this.#state.todayKey, { expires: 3600 }),
};
}
getAccount (name) {
console.log(name, 'name');
if (name) {
let arrr = client.signatureUrl(name, { expires: 3600 });
console.log(arrr);
return arrr;
}
return null;
}
getUpLoad () {
// JavaScript AES解密函数
return {
action: this.#state.host,
headers: {
'X-Requested-With': null,
},
data: {
key: this.#state.baseurl + this.#state.cardMap[this.#state.target] + this.#state.todayKey,
policy: this.#state.policy,
// OSSAccessKeyId: this.#state.accessKeyId,
OSSAccessKeyId: localStorage.getItem('ACCESSKEYID'),
success_action_status: 200,
signature: this.#state.signature,
},
};
}
}
上传部分的逻辑——封装的wangEditor上传逻辑,可以上传视频或图片
// 富文本编辑器的视频或图片上传
export const richerEditorUpload = async (file, targetCode, insertFn, type = 'img') => {
// file 即选中的文件
const uploader = new ossUpload();
uploader.setName(file.name, targetCode);
const { action, data } = uploader.getUpLoad();
const formData = new FormData();
Object.keys(data).forEach((key) => {
formData.append(key, data[key]);
});
formData.append('file', file);
const res = await fetch(action, {
method: 'POST',
body: formData,
});
if (res.status === 200) {
const urlInfo = uploader.getLastUrl();
const url = urlInfo?.value;
const paramsMap = {
img: file.name,
// video: generateVideoCover(url), //前端自己生成视频封面
};
insertFn(url, paramsMap?.[type]);
}
};
图片和视频上传效果预览
视频上传的预览封面(小难点)
正常情况下,经过视频剪辑软件输出的视频文件是自带封面的,但手机直接拍的视频是没有的,上传后无法浏览器预览封面,可以通过如下方法截取视频指定时刻的“帧”生成封面
function generateVideoCover (videoUrl) {
const video = document.createElement('video');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
video.src = videoUrl;
video.crossOrigin = 'anonymous'; // 解决跨域问题(如果视频允许)
video.addEventListener('loadeddata', () => {
// 跳转到第 1 秒(可调整)
video.currentTime = 1;
});
video.addEventListener('seeked', () => {
// 绘制当前帧到 Canvas
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 获取封面 Base64
return canvas.toDataURL('image/jpeg');
});
}