一、安装依赖
yarn add tinymce -S;
yarn add @tinymce/tinymce-vue -S;
-
- 下载中文语言包和静态文件
- 2.1 这一步网上都有,直接去官网下载语言包,将文件放在public/tinymce/langs中,这样打包后静态文件会被复制到打包后的目录中
- 2.2 还可以添加主题UI等样式,public/tinymce/skins,可以在github找一些相关项目复制下来,可选项
二、 封装组件
-
- tinymce 引入功能
import 'tinymce/themes/silver'
import 'tinymce/icons/default'
import 'tinymce/plugins/advlist'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/autoresize'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/code'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/fullpage/index'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/help'
import 'tinymce/plugins/hr'
import 'tinymce/plugins/image'
import 'tinymce/plugins/importcss'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/media'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/quickbars'
import 'tinymce/plugins/save'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/table'
import 'tinymce/plugins/template'
import 'tinymce/plugins/textcolor'
import 'tinymce/plugins/textpattern'
import 'tinymce/plugins/toc'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'
-
- tinymce 配置项
const buttonPlugins = 'emoticons preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave paste';
const toolbar = 'fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor \ table image | alignleft aligncenter alignright alignjustify outdent indent | \ styleselect formatselect fontselect fontsizeselect | bullist numlist | blockquote subscript superscript removeformat | media charmap emoticons hr pagebreak insertdatetime print preview | code selectall searchreplace visualblocks | indent2em lineheight formatpainter axupimgs';
export const init = {
selector: '#js_tinymce_editor',
height: 550,
width: '100%',
cleanup: true,
language_url: './tinymce/langs/zh-Hans.js',
language: 'zh-Hans',
content_css: true,
skin_url: './tinymce/skins/ui/oxide',
plugins: buttonPlugins,
toolbar: toolbar,
body_class: 'panel-body',
object_resizing: false,
fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 28px 32px 36px 48px 56px 72px',
nonbreaking_force_tab: true,
font_formats: '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
lineheight_formats: '0.5 0.8 1 1.2 1.5 1.75 2 2.5 3 4 5',
branding: false,
resize: false,
elementpath: false,
content_style: 'p {margin-block-start: 0; margin-block-end: 0; color: #606D81; font-size: 14px;}; table { border: 1px}',
paste_data_images: true,
images_upload_handler: (blobInfo, success, failure) => {
},
imagetools_toolbar: 'editimage',
}
-
- tinymce 封装组件
<template>
<div class="tinymce-box">
<Editor
v-model="editorContent"
:init="initData"
:disabled="isDisabled"
ref="editorRef"
id="js_tinymce_editor"
></Editor>
</div>
</template>
<script setup lang="ts">
import Editor from '@tinymce/tinymce-vue';
import tinymce from 'tinymce/tinymce';
import { formatTime } from '@/utils/';
import { uploadImage } from '@/api/tinymce';
const props = defineProps({
modelValue: {
type: String,
default: '',
},
});
import './js/importTinymce';
import { init } from './js/config';
import axios from 'axios';
onMounted(() => {
tinymce.init({});
});
const isDisabled = ref(true);
const initData = ref(
Object.assign({}, init, {
init_instance_callback: (editor: any) => {
isDisabled.value = false;
},
images_upload_handler: async (blobInfo: any, success: any, failure: any) => {
var form = new FormData();
form.append('file', blobInfo.blob());
const config = {
headers: {
'Content-Type': 'multipart/form-data',
},
};
axios
.post('/my-api/vueadmin/image/upload', form, config)
.then((res) => {
const data = res.data.data;
if (res.data.code === '00000') {
success(data.imgUrl);
} else {
failure('上传失败!');
}
})
.catch((err) => {
failure('上传失败!');
});
},
})
);
const editorContent = ref(props.modelValue);
const emit = defineEmits(['update:modelValue']);
watch(
() => editorContent.value,
(n) => {
debounce(() => {
emit('update:modelValue', editorContent.value);
});
}
);
const timeout = ref();
const debounce = (fn: any, wait = 400) => {
if (timeout.value) {
clearTimeout(timeout.value);
fn();
}
timeout.value = setTimeout(fn, wait);
};
</script>
<style lang="scss" scoped>
.tinymce-box {
width: 100%;
}
</style>
三、引用组件
-
- 封装好Tinymce组件后,可以开始应用
注意:如果Tinymce引入的页面是弹窗之类的,存在其自身弹窗层级问题,导致其自身层级低于项目弹窗的层级,这里可以通过加添或修改public/tinymce/skins自定义的样式来覆盖
<!-- @format -->
<template>
<el-card>
<el-form
ref="formDataRef"
:model="formData"
:rules="rules"
label-width="120px"
class="demo-formData"
status-icon
>
<el-form-item label="短信内容" prop="region" style="width: 100%">
<Editor
v-if="editDataLoading"
v-model="formData.textarea"
style="width: 100%"
></Editor>
</el-form-item>
</el-form>
<div class="btn-box">
<el-button type="primary" @click="submitForm(formDataRef)"> 保存 </el-button>
<el-button @click="resetForm(formDataRef)">重置</el-button>
</div>
</el-card>
</template>
<script setup lang="ts">
import Editor from '@/components/Editor/index.vue';
import type { FormInstance, FormRules } from 'element-plus';
import { IFormData } from '@/api/sms/types';
import { getSmsInfo, saveSms } from '@/api/sms/';
const propsData = defineProps({
drawerData: {
type: Object,
default: () => {
id: '';
},
},
});
const emit = defineEmits(['closeDrawerHandle']);
const editDataLoading = ref(false);
const initHandle = async () => {
if (propsData.drawerData?.id) {
const { code, data } = await getSmsInfo(propsData.drawerData.id);
if (code === '00000') {
console.log(data);
formData.textarea = data.textarea;
formData.id = data.id;
}
}
nextTick(() => {
editDataLoading.value = true;
});
};
initHandle();
const formDataRef = ref<FormInstance>();
const formData = reactive<IFormData>({
textarea: '',
id: '',
});
const rules = reactive<FormRules<IFormData>>({
textarea: [
{
required: true,
message: '请输入富文本',
trigger: 'blur',
},
],
});
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid, fields) => {
if (valid) {
console.log(formData);
const { code, data } = await saveSms(formData);
if (code === '00000') {
ElMessage.success(data.msg || '保存成功!');
emit('closeDrawerHandle', true);
}
} else {
console.log(fields);
ElMessage.error('保存失败!');
}
});
};
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
</script>
<style scoped></style>
本地虚拟机wamp存储富文本
-
- 在数据库中创建数据表
DROP TABLE IF EXISTS `tinymce_content`;
CREATE TABLE `tinymce_content` (
`id` bigint NOT NULL AUTO_INCREMENT,
`textarea` longtext NOT NULL DEFAULT '' COMMENT '富文本内容',
`create_time` date NOT NULL DEFAULT '' COMMENT '添加时间',
PRIMARY KEY (`id`) USING BTREE,
) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8;
-
- 在数据库中创建图片数据表
DROP TABLE IF EXISTS `tinymce_img`;
CREATE TABLE `tinymce_img` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '图片名称',
`url` varchar(100) NOT NULL DEFAULT '' COMMENT '图片地址',
`type` varchar(50) NOT NULL DEFAULT '' COMMENT '图片类型',
`blob` mediumblob DEFAULT NULL COMMENT '图片内容(不建议将图片存入数据库)',
`create_time` date NOT NULL DEFAULT '' COMMENT '添加时间',
PRIMARY KEY (`id`) USING BTREE,
) ENGINE = InnoDB AUTO_INCREMENT = 0 CHARACTER SET = utf8;
-
- 创建好这两张表后,在富文本编辑器中便可以将上传、截图、图片链接源存在项目中,并将图片链接存在tinymce_img中,整个内容存在tinymce_content中
-
- 后端使用PHP框架Yii2,所以图片存在根目录的/web/static/静态目录中,访问地址:http://myproject.com/static/1705046024_finmart.png,主域名是通过wamp配置的代理host访问
-
- PHP存储代码
<?php
namespace app\controllers;
use Yii;
use app\models\TinymceImg;
use app\controllers\BaseController;
class ImageController extends BaseController
{
public function actionIndex()
{
return $this->render('index');
}
public function actionUpload()
{
$model = new Images();
$fileData = $_FILES['file'];
$tempFile = $fileData['tmp_name'];
$fileType = $fileData['type'];
$size = $fileData['size'];
$name = time() . '_' . $fileData["name"];
if (is_uploaded_file($tempFile)) {
$img = file_get_contents($tempFile);
$saveLocalUrl = Yii::$app->getBasePath() . "/web/static/" . $name;
$imgLink = 'http://' . $_SERVER['HTTP_HOST'] . "/static/$name";
move_uploaded_file($tempFile, $saveLocalUrl);
$data = [
"date" => date("Y-m-d H:i:s"),
"name" => $name,
"url" => $imgLink,
"type" => $fileType,
];
$row = $model->create($data);
if (isset($row)) {
$res = ['imgUrl' => $imgLink];
return $this->response($res);
} else {
$this->code = "00001";
$this->msg = "图片保存失败";
return $this->response();
}
}
}
}