Replace TinyMCE with WangEditor and update dependencies

This commit is contained in:
wangqiao
2025-08-28 17:41:36 +08:00
parent 3f1431f972
commit fadab4eacb
6 changed files with 478 additions and 188 deletions

View File

@ -1,18 +1,18 @@
<template>
<KlNavTab></KlNavTab>
<div class="mx-auto w-[1440px]">
<div class="mx-auto w-1440px">
<!-- 使用 el-form 重构表单区域 -->
<el-form ref="formRef" inline :model="formData" label-width="110px" class="custom-form mb-[20px] mt-[20px] border rounded p-[20px]!">
<el-form ref="formRef" inline :model="formData" label-width="110px" class="custom-form mb-20px mt-20px border rounded p-20px!">
<el-form-item label="标题:" prop="postsTitle" :rules="{ required: true, message: '请输入标题', trigger: 'blur' }">
<el-input v-model="formData.postsTitle" placeholder="请输入标题" class="w-[300px]!" minlength="4" maxlength="40"></el-input>
<el-input v-model="formData.postsTitle" placeholder="请输入标题" class="w-300px!" minlength="4" maxlength="40"></el-input>
</el-form-item>
<el-form-item label="分类:" class="mb-[10px]" prop="projectDicId" :rules="{ required: true, message: '请选择分类', trigger: ['blur', 'change'] }">
<el-select v-model="formData.projectDicId" placeholder="请选择分类" class="w-[300px]!">
<el-form-item label="分类:" class="mb-10px" prop="projectDicId" :rules="{ required: true, message: '请选择分类', trigger: ['blur', 'change'] }">
<el-select v-model="formData.projectDicId" placeholder="请选择分类" class="w-300px!">
<el-option v-for="item in projectTypeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="标签:" class="mb-[10px]" prop="postsTags" :rules="{ required: true, message: '请输入标签', trigger: ['blur', 'change'] }">
<el-form-item label="标签:" class="mb-10px" prop="postsTags" :rules="{ required: true, message: '请输入标签', trigger: ['blur', 'change'] }">
<el-select
v-model="formData.postsTags"
:remote-method="remoteMethod"
@ -21,13 +21,13 @@
remote
multiple
placeholder="请输入搜索标签"
class="w-[300px]!"
class="w-300px!"
>
<el-option v-for="(item, index) in labelsList" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="频道列表:" class="mb-[10px]" prop="channelId" :rules="{ required: true, message: '请选择频道', trigger: ['blur', 'change'] }">
<el-select v-model="formData.channelId" placeholder="请选择频道" class="w-[300px]!">
<el-form-item label="频道列表:" class="mb-10px" prop="channelId" :rules="{ required: true, message: '请选择频道', trigger: ['blur', 'change'] }">
<el-select v-model="formData.channelId" placeholder="请选择频道" class="w-300px!">
<el-option v-for="item in channelIdList" :key="item.channelId" :label="item.channelTitle" :value="item.channelId" />
</el-select>
</el-form-item>
@ -35,53 +35,43 @@
<KlUploader v-model:file-list="formData.postsCover" :limit="1" :size="1" tips="上传图片支持jpg/gif/png格式"> </KlUploader>
</el-form-item> -->
</el-form>
<div style="border: 1px solid #ccc; margin-top: 10px">
<WeToolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
<ClientOnly>
<Editor :id="tinymceId" v-model="myValue" :init="init" :disabled="disabled" :placeholder="placeholder" />
</ClientOnly>
<WeEditor
style="height: 300px; overflow-y: hidden"
v-model="valueHtml"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="handleCreated"
@onChange="handleChange"
@onDestroyed="handleDestroyed"
@onFocus="handleFocus"
@onBlur="handleBlur"
@customAlert="customAlert"
@customPaste="customPaste"
/>
</div>
<!-- 按钮区域 -->
<div class="mt-[20px] flex justify-end">
<el-button :loading="post_loading" class="mr-[10px]" @click="previewContent">预览</el-button>
<div class="mt-20px flex justify-end">
<!-- <el-button :loading="post_loading" class="mr-10px" @click="previewContent">预览</el-button> -->
<el-button :loading="post_loading" type="primary" @click="saveContent">发表</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { keywords } from '~/api/upnew/index'
import { keywords } from '@/api/upnew/index'
import { reactive, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
import { create, list } from '~/api/channel/index'
import { parent,parentV2 } from '~/api/upnew/index'
import { upload } from '~/api/common/index' // 自定义上传方法
import Editor from '@tinymce/tinymce-vue'
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver'
import 'tinymce/themes/silver/theme'
import 'tinymce/models/dom'
import 'tinymce/icons/default'
import 'tinymce/icons/default/icons'
// 引入编辑器插件
import 'tinymce/plugins/code' //编辑源码
import 'tinymce/plugins/image' //插入编辑图片
import 'tinymce/plugins/media' //插入视频
import 'tinymce/plugins/link' //超链接
import 'tinymce/plugins/preview' //预览
// import 'tinymce/plugins/template' //模板
import 'tinymce/plugins/table' //表格
import 'tinymce/plugins/pagebreak' //分页
import 'tinymce/plugins/lists' //列
import 'tinymce/plugins/advlist' //列
import 'tinymce/plugins/quickbars' //快速工具条
import 'tinymce/plugins/wordcount' // 字数统计插件
// import 'tinymce/langs/zh-Hans'
// import 'tinymce/skins/ui/oxide/skin.min.css'
// import 'tinymce/skins/ui/oxide/content.min.css'
// import 'tinymce/skins/content/default/content.css'
// import '~/assets/tinymce/langs/zh-Hans.js' //下载后的语言包
// import 'tinymce/skins/content/default/content.css'
import { create, list } from '@/api/channel/index'
import { parent } from '@/api/upnew/index'
import { upload } from '@/api/common/index' // 自定义上传方法
import '@wangeditor/editor/dist/css/style.css' // 引入 css
// import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { onBeforeUnmount, shallowRef } from 'vue'
// 获取从其他地方传过来的参数
const channelId = route.query.channelId as string
@ -122,112 +112,30 @@
default: () => ({}),
},
})
//用于接收外部传递进来的富文本
const myValue = ref(props.value)
const tinymceId = ref('vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + ''))
const init = reactive({
selector: '#' + tinymceId.value, //富文本编辑器的id,
language_url: '/_nuxt/static/tinymce/langs/zh_CN.js', // 语言包的路径具体路径看自己的项目文档后面附上中文js文件
language: 'zh-Hans', //语言
skin_url: '/_nuxt/static/tinymce/skins/ui/oxide', // skin路径具体路径看自己的项目
content_css: '/_nuxt/static/tinymce/skins/content/default/content.css',
menubar: true, //顶部菜单栏显示
statusbar: true, // 底部的状态栏
plugins: props.plugins,
toolbar: props.toolbar,
toolbar_mode: 'sliding',
font_formats: 'Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;', //字体
paste_convert_word_fake_lists: false, // 插入word文档需要该属性
font_size_formats: '12px 14px 16px 18px 22px 24px 36px 72px', //文字大小
height: props.height, //编辑器高度
placeholder: props.placeholder,
branding: false, //是否禁用"Powered by TinyMCE"
promotion: false, //禁用升级按钮
image_dimensions: false, //去除宽高属性
paste_webkit_styles: 'all',
paste_merge_formats: true,
nonbreaking_force_tab: false,
paste_auto_cleanup_on_paste: false,
file_picker_types: 'file',
resize: true,
elementpath: true,
content_style: `img {max-width:100%;} body{background-color: #fff;}`, // 直接自定义可编辑区域的css样式
templates: props.templates,
quickbars_selection_toolbar: 'forecolor backcolor bold italic underline strikethrough link',
quickbars_image_toolbar: 'alignleft aligncenter alignright',
quickbars_insert_toolbar: false,
image_caption: true,
image_advtab: true,
convert_urls: false,
images_upload_url: import.meta.env.VITE_BASE_API,
images_upload_handler: function (blobInfo: any, progress: any) {
console.log(blobInfo, progress)
return new Promise((resolve, reject) => {
const data = new FormData()
data.append('file', blobInfo.blob())
data.append('fieldName', blobInfo.filename())
upload('/prod-api/app-api/infra/file/upload', data)
.then((res) => {
resolve(res.data)
})
.catch(() => {
reject('Image upload failed')
})
})
},
// 添加自定义按钮
setup: function (editor: any) {
if (process.client) {
// 注册一个新的视频上传按钮
editor.ui.registry.addButton('customvideoupload', {
icon: 'embed', // 使用嵌入媒体图标
tooltip: '上传视频',
onAction: function () {
// 创建文件输入元素
const input = document.createElement('input')
input.setAttribute('type', 'file')
input.setAttribute('accept', 'video/*')
// 处理文件选择事件
input.onchange = function () {
if (input.files && input.files[0]) {
const file = input.files[0]
const data = new FormData()
data.append('file', file)
data.append('fieldName', file.name)
// 可以在这里添加上传进度显示
upload('/prod-api/app-api/infra/file/upload', data)
.then((res) => {
// 插入视频到编辑器
editor.insertContent(`
<video controls width="400">
<source src="${res.data}" type="video/${file.name.split('.').pop()}">
您的浏览器不支持视频标签
</video>
`)
})
.catch((error) => {
console.error('视频上传失败:', error)
// 可以添加错误提示
})
}
}
// 触发文件选择
input.click()
},
})
const mode = 'default'
}
},
const isClient = ref(false)
if (import.meta.client) {
isClient.value = true
}
preview_styles: true,
...props.options,
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
// 内容 HTML
const valueHtml = ref('<p></p>')
// 模拟 ajax 异步获取内容
onMounted(() => {
setTimeout(() => {
valueHtml.value = '<p></p>'
}, 1500)
})
const toolbarConfig = {}
const editorConfig = { placeholder: '请输入文章内容...' }
// 表单数据
const formData = ref({
projectDicId: undefined,
@ -241,7 +149,7 @@
const post_loading = ref(false)
const saveContent = () => {
formRef.value.validate().then(() => {
if (!myValue.value) {
if (!valueHtml.value) {
ElMessage.error('请输入帖子内容')
return
}
@ -250,7 +158,7 @@
postsTitle: formData.value.postsTitle,
// postsCover: formData.value.postsCover[0].url,
postsTags: formData.value.postsTags.join(','),
postsContent: myValue.value,
postsContent: valueHtml.value,
projectDicId: formData.value.projectDicId,
channelId: formData.value.channelId,
})
@ -269,23 +177,53 @@
})
})
}
const previewContent = () => {
// 获取编辑器实例
const editor = tinymce.get(tinymceId.value)
// 调用编辑器的预览命令
if (editor) {
editor.execCommand('mcePreview')
}
}
//在onMounted中初始化编辑器
onMounted(async () => {
if (process.client) {
getParent()
getChannelIdList()
tinymce.init({})
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor: any) => {
editorRef.value = editor // 记录 editor 实例,重要!
}
const handleChange = (editor: any) => {
console.log('change:', editor.getHtml())
}
const handleDestroyed = (editor: any) => {
console.log('destroyed', editor)
}
const handleFocus = (editor: any) => {
console.log('focus', editor)
}
const handleBlur = (editor: any) => {
console.log('blur', editor)
}
const customAlert = (info: any, type: any) => {
alert(`【自定义提示】${type} - ${info}`)
}
const customPaste = (editor: any, event: any, callback: any) => {
console.log('ClipboardEvent 粘贴事件对象', event)
// const html = event.clipboardData.getData('text/html') // 获取粘贴的 html
// const text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
// const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
// 自定义插入内容
editor.insertText('xxx')
// 返回 false ,阻止默认粘贴行为
event.preventDefault()
callback(false) // 返回值注意vue 事件的返回值,不能用 return
// 返回 true ,继续默认的粘贴行为
// callback(true)
}
/** 获取频道列表 */
const channelIdList = ref<any>([])
const getChannelIdList = () => {
@ -293,19 +231,19 @@
channelIdList.value = res.data
})
}
getChannelIdList()
const projectTypeList = ref<any>([])
/** 获取分类下拉框 */
const getParent = () => {
parentV2({
parent({
type: 1,
parentId: 0,
}).then((res) => {
projectTypeList.value = res.data
})
}
getParent()
const loading = ref(false)
/** 获取标签 */
@ -377,4 +315,4 @@
.text-12px {
font-size: 12px;
}
</style>
</style>