Skip to content

Instantly share code, notes, and snippets.

@mouyong
Last active May 18, 2025 19:03
Show Gist options
  • Save mouyong/16467a10a52b91cec814e260e1e9c29b to your computer and use it in GitHub Desktop.
Save mouyong/16467a10a52b91cec814e260e1e9c29b to your computer and use it in GitHub Desktop.
vue3 版本的 135 编辑器集成,参考:https://gist.github.com/mouyong/f3838ffdf827021e47506810d4c17ffc
<template>
<VueMarkdownEditor
v-model="newModelValue"
:disabled-menus="[]"
:include-level="[1, 2, 3, 4, 5, 6]"
@upload-image="handleUploadImage"
@change="change"
:height="height + 'px'"
:placeholder="placeholder"
>
</VueMarkdownEditor>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import VueMarkdownEditor from '@kangc/v-md-editor';
import '@kangc/v-md-editor/lib/style/base-editor.css';
import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js';
import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
import Prism from 'prismjs';
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
if (!(window as any).__V_MARKDOWN_EDITOR_THEME_REGISTERED__) {
VueMarkdownEditor.use(vuepressTheme, { Prism });
(window as any).__V_MARKDOWN_EDITOR_THEME_REGISTERED__ = true;
}
interface Props {
modelValue?: string;
placeholder?: string;
height?: number;
type?: string;
usageType?: string;
uploadFile?: Function;
}
const props = withDefaults(defineProps<Props>(), {
type: 'image',
placeholder: '',
usageType: '',
height: 300,
});
const emit = defineEmits(['update:modelValue', 'htmlContent']);
const newModelValue = computed({
get() {
return props.modelValue;
},
set(value: string) {
emit('update:modelValue', value);
},
});
const change = (markdownContent: any, htmlContent: any) => {
emit('update:modelValue', markdownContent);
emit('htmlContent', htmlContent);
};
const handleUploadImage = async (
event: any,
insertImage: (info: { url: string; desc?: string }) => void,
files: File[],
) => {
console.log(files);
const file = files[0];
if (!file) return;
const toBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file); // 读取为 base64
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const toBlobUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const blobUrl = URL.createObjectURL(file);
return blobUrl;
});
};
const data = new FormData();
data.append('file', file, file.name); // 直接用 File
data.append('type', props.type);
data.append('usage_type', props.usageType);
window.file = file as any;
console.log(window.file, '上传文件信息');
let url = '';
try {
let res: any = null;
if (props.uploadFile) {
res = await props.uploadFile(data); // 你自定义的上传接口
}
console.log(res);
if (!res || !res.data?.fileinfo?.url) {
throw new Error('上传失败');
}
url = res.data?.fileinfo?.url;
} catch (err) {
console.error(err, '上传文件失败');
url = await toBase64(file);
}
insertImage({
url,
desc: file.name,
});
};
</script>
<style></style>
<template>
<EditorMD
v-if="content_type === 'markdown'"
v-bind="{ ...$props, content_type: undefined }"
:uploadFile="uploadFile"
@update:modelValue="(value: any) => emit('update:modelValue', value)"
/>
<TinyMCE
v-if="content_type === 'rich_text'"
v-bind="{ ...$props, content_type: undefined }"
:uploadFile="uploadFile"
@update:modelValue="(value: any) => emit('update:modelValue', value)"
/>
</template>
<script setup lang="ts" name="MyEditor">
import { withDefaults } from 'vue';
import { uploadFile } from '../MyUpload/api';
import EditorMD from './EditorMD.vue';
import TinyMCE from './TinyMCE.vue';
interface Props {
modelValue?: string;
placeholder?: string;
height?: number;
type?: string;
usageType?: string;
uploadFile?: Function;
// export const CONTENT_TYPE_OPTIONS = [
// { label: t('pages.memoryRecognitionKnowledge.content_type_enums.rich_text'), value: 'rich_text' },
// { label: t('pages.memoryRecognitionKnowledge.content_type_enums.markdown'), value: 'markdown' },
// ];
// <t-col :span="8">
// <t-form-item :label="$t('pages.memoryRecognitionKnowledge.formBase.content_type')" name="content_type">
// <t-radio-group
// v-model="formData.content_type"
// :default-value="'rich_text'"
// name="mode"
// :options="CONTENT_TYPE_OPTIONS"
// @change="() => {}"
// >
// </t-radio-group>
// </t-form-item>
// </t-col>
content_type?: string; // rich_text, markdown
}
const props = withDefaults(defineProps<Props>(), {
content_type: 'rich_text',
});
const emit = defineEmits(['update:modelValue']);
</script>
tinymce.PluginManager.add('editor135', function (editor, url) {
let activeEditor = null; // 用于保存当前活动的 tinymce 编辑器实例
let iframeSrc = ''
const openDialog = function (editor) {
// iframeSrc = 'https://www.135editor.com/simple_editor.html?callback=true&appkey=' + '&editorId=' + editor.id; // 无免费样式过滤操作
iframeSrc = 'https://www.135editor.com/beautify_editor.html?callback=true&appkey=' + '&editorId=' + editor.id; // 有免费样式过滤操作
activeEditor = editor;
// return window.open(iframeSrc, '_blank'); // 需要注释这行,注释后可避免原有文章内容无法同步的问题
return editor.windowManager.openUrl({
title: '135编辑器',
url: iframeSrc,
size: 'large',
});
};
// 接收135传来的数据
window.addEventListener('message', function (event) {
if (event.origin !== 'https://www.135editor.com') {
return false;
}
if (typeof event.data !== 'string') {
if (event.data.ready) {
console.log('135加载完成');
// 获取135实例
const editor135 = document.querySelector(`iframe[src='${iframeSrc}']`);
if (editor135) {
editor135.contentWindow.postMessage(editor.getContent(), '*');
}
}
return;
};
if (activeEditor) {
// 使用 activeEditor 变量来访问当前活动的 tinymce 编辑器实例
activeEditor.setContent(event.data);
activeEditor.windowManager.close();
activeEditor = null; // 重置 activeEditor 变量以防止后续的错误处理
}
}, false);
editor.ui.registry.getAll().icons.editor135 || editor.ui.registry.addIcon('editor135',
'<svg t="1609988577378" class="icon" viewBox="0 0 1375 1024" width="22" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="811" width="200" height="200"><path d="M1118.988883 1021.363716h-1013.760512l-6.082563-50.688026c32.947217-244.823164 17.740809-490.153208 8.110084-735.990131 57.277469-117.596219-3.548162-136.857669-101.376051-123.678783 8.110084-43.591702-35.988498-112.020537 68.428835-101.376051 128.747585 10.644485 258.508931 0 387.763396 0-101.376051 193.628258 101.376051 113.034297 152.064076 167.777365v216.944749c-80.593961-15.206408-192.107617-118.60998-202.752102 70.456356-25.344013 155.612239 117.596219 121.144381 189.066335 167.270484l10.644486 226.068594c-44.605463-11.658246-86.169644-57.277469-134.830148-22.302731-11.658246 8.110084-12.165126 58.291229-7.603204 59.81187 50.688026 17.740809 91.745326 117.596219 156.119119 20.782091l95.293488 5.068802c83.128362-117.089339 32.440336-246.850685 35.481618-371.543227v-17.740809c-6.589443-124.185663 53.729307-253.440128-43.084822-370.022587l-50.688026-6.082563L561.420602 7.603204c228.096115 0 456.19223 16.727048 683.781465-5.575683 160.681041-16.220168 124.692543 74.004517 127.226944 157.63976a324.910244 324.910244 0 0 1-210.862186 47.139863c0-37.509139 22.809612-84.649003-43.591702-88.704044h-57.277469l-86.169644 44.098582c-90.731566 96.814129-79.5802 197.6833-11.151365 300.579992l13.685767-4.561923c57.277469 91.238446 164.229203 105.937973 245.836924 159.667281 2.534401 50.688026 5.575683 103.910452 8.110084 156.119119-54.743068 62.346271-241.781882-175.380569-206.807145 90.731566-31.426576 90.224686 70.456356 99.85541 94.786608 156.625999z" fill="#F9FAF8" p-id="812"></path><path d="M1161.566825 206.807144a324.910244 324.910244 0 0 0 210.862186-47.139863v405.504204a119.62374 119.62374 0 0 1-46.126103-6.082563c-105.937973-94.786608-286.894225-141.926472-220.492911-346.199214z" fill="#FBFAF6" p-id="813"></path><path d="M99.145808 970.67569l6.082563 50.688026c-50.688026-4.055042-109.993016 27.878414-104.924213-76.032039 11.151366-210.862186 3.548162-422.231253 3.548162-633.60032h50.688026c27.878414 216.94475-53.729307 443.520224 44.605462 658.944333z" fill="#10A4E3" p-id="814"></path><path d="M561.420602 7.603204c32.947217 50.688026 66.401314 97.321009 101.376051 146.488394l-50.688026 21.288971c-50.688026-54.743068-253.440128 25.850893-152.064076-167.777365z" fill="#E6430F" p-id="815"></path><path d="M1326.302908 559.088922a119.62374 119.62374 0 0 0 46.126103 6.082563v50.688026c-74.511398 55.249948-39.02978 136.857669-50.688025 207.314025l-90.731566-50.688026c-2.534401-50.688026-5.575683-103.910452-8.616964-156.119119z" fill="#E75225" p-id="816"></path><path d="M1321.740986 823.173536c11.658246-70.456356-23.823372-152.064077 50.688025-207.314025v354.816179l-50.688025 50.688026h-50.688026a311.224477 311.224477 0 0 0-6.082563-43.591702z" fill="#EC774C" p-id="817"></path><path d="M1264.970397 977.772014a311.224477 311.224477 0 0 1 6.082563 43.591702h-152.064077c-24.330252-56.770589-126.213184-66.401314-94.786608-157.13288z" fill="#008FFE" p-id="818"></path><path d="M54.540346 311.731357l-23.823372 3.548162H6.386721v-202.752102c101.376051-13.178887 160.681041 6.082563 101.376052 123.678782z" fill="#CDC04C" p-id="819"></path><path d="M1321.740986 1021.363716l50.688025-50.688026c9.630725 43.591702-7.096324 60.31875-50.688025 50.688026z" fill="#EEBEAB" p-id="820"></path><path d="M1264.970397 977.772014l-240.768122-113.541178c-34.974738-266.112134 152.064077-28.385294 206.807145-90.731565l90.731566 50.688025c-18.754569 50.688026-38.016019 101.882931-56.770589 153.584718z" fill="#E6BC2A" p-id="821"></path><path d="M1326.302908 559.088922l-105.937973 56.770589c-81.100841-53.729307-188.052575-68.428835-245.330044-159.667281 152.064077-35.988498 73.497637-161.187921 97.321009-247.357565h31.426576c-64.373793 208.327785 116.582459 254.960769 222.520432 350.254257z" fill="#FDB41A" p-id="822"></path><path d="M612.108627 175.380569l50.688026-21.288971 50.688026 6.082563 6.082563 304.128153-95.800369 101.376052-14.699527-57.277469 4.561922-117.089339z" fill="#89AD17" p-id="823"></path><path d="M54.540346 311.731357l55.249948-77.552679c9.630725 245.836924 24.837133 491.166968-8.110084 735.990132-100.869171-214.917229-19.26145-441.492703-47.139864-658.437453z" fill="#008EDD" p-id="824"></path><path d="M1074.383421 210.355306c-23.823372 86.169644 50.688026 211.369067-97.321009 247.357565l-13.685767 4.561922c3.548162-101.376051 7.096324-202.752102 11.151365-300.579991l86.676524-44.098583a160.174161 160.174161 0 0 0 8.616965 41.057301z" fill="#E4500E" p-id="825"></path><path d="M720.074122 919.987665l-95.293488-5.068803-13.685767-58.291229-10.644486-226.068595a59.81187 59.81187 0 0 1 21.288971-19.261449l94.279728 13.178886z" fill="#A8BF10" p-id="826"></path><path d="M612.108627 392.325318l-4.561922 117.089339-198.19018-45.619223c11.658246-190.080096 123.171902-86.676524 202.752102-71.470116z" fill="#FEE404" p-id="827"></path><path d="M409.356525 463.795434l197.17642 46.632984c5.068803 18.754569 9.630725 38.016019 14.699527 57.277469a213.903468 213.903468 0 0 0 0 44.098582 59.81187 59.81187 0 0 0-21.288971 19.26145c-70.963236-47.139864-213.903468-12.672006-190.586976-167.270485z" fill="#E1B007" p-id="828"></path><path d="M719.060361 463.795434l-6.082563-304.128153c96.814129 114.048058 36.495378 245.836924 43.084822 370.022586-12.165126-20.78209-24.837133-42.071061-37.002259-65.894433zM720.074122 919.987665c0-98.33477 0-196.669539-3.548162-295.51119 12.672006-25.344013 25.850893-50.688026 39.02978-76.032038-3.041282 124.185663 47.646744 253.947008-35.481618 371.543228zM974.52801 159.667281c-4.055042 101.376051-7.603204 202.752102-11.151365 300.579991-68.428835-100.869171-79.5802-199.203941 11.151365-300.579991z" fill="#FBFAF6" p-id="829"></path><path d="M612.108627 856.120752l13.685767 58.29123c-64.373793 96.814129-106.444854-3.041282-156.119119-20.782091-4.561922 0-4.055042-50.688026 7.603204-59.81187 47.646744-34.974738 89.210925 10.644485 134.830148 22.302731z" fill="#0899E8" p-id="830"></path><path d="M755.55574 547.937557c-13.178887 25.344013-26.357773 50.688026-39.02978 76.032038l-94.786608-13.178887a213.903468 213.903468 0 0 1 0-44.098582l95.800369-101.376051c12.165126 21.795851 24.837133 43.084822 37.002258 64.373792z" fill="#3A7B00" p-id="831"></path><path d="M1074.383421 210.355306l-4.561922-50.688025 50.688025-40.550421c66.401314 4.055042 45.112343 50.688026 43.591702 88.704045l-55.756828 6.082563h-15.713288z" fill="#8FB01C" p-id="832"></path><path d="M1118.988883 118.1031l-50.688025 40.55042a160.174161 160.174161 0 0 1-8.616965-41.057301z" fill="#7EC21E" p-id="833"></path></svg>'
);
// 注册一个工具栏按钮名称
editor.ui.registry.addButton('editor135', {
// text: '135',
icon: 'editor135',
tooltip: '使用135编辑器',
onAction: function () {
openDialog(editor);
}
});
// 注册一个菜单项名称 menu/menubar
editor.ui.registry.addMenuItem('editor135', {
text: '135编辑器',
icon: 'editor135',
onAction: function () {
openDialog(editor);
}
});
return {
getMetadata: function () {
return {
// 插件名和链接会显示在“帮助”→“插件”→“已安装的插件”中
name: "135编辑器", //插件名称
url: "https://marketplace.plugins-world.cn", //作者网址
};
}
};
});
<template>
<Editor
v-model="formData.table_prefix_post_description"
:content_type="formData.content_type"
usageType="table_prefix_post_description"
:uploadFile="uploadFile"
:height="500"
:placeholder="$t('projectForm.placeholder.table_prefix_post_description')"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// 替换成项目的上传接口
import { uploadFile } from '@/api/global';
const formData = ref({
table_prefix_post_description: ''
});
</script>
<template>
<div ref="editor"></div>
</template>
<script setup lang="ts" name="TinyMCE">
import { ref, watch, onMounted, onUnmounted, withDefaults } from 'vue';
import tinymce from 'tinymce';
interface Props {
modelValue?: string;
placeholder?: string;
height?: number;
type?: string;
usageType?: string;
uploadFile?: Function; // 项目的上传接口
}
const props = withDefaults(defineProps<Props>(), {
type: 'image',
placeholder: '',
usageType: '',
height: 300,
});
const editor = ref<HTMLElement | null>(null);
const editorInstance = ref<tinymce.Editor | null>(null);
const emitModelValue = defineEmits(['update:modelValue']);
onMounted(() => {
tinymce.init({
target: editor.value as HTMLElement,
license_key: 'gpl',
base_url: '/tinymce',
external_plugins: {
editor135: '/tinymce/plugins/editor135/plugin.js',
preview: '/tinymce/plugins/preview/plugin.js',
fullscreen: '/tinymce/plugins/fullscreen/plugin.js',
autosave: '/tinymce/plugins/autosave/plugin.js',
// importword: '/tinymce/plugins/importword/plugin.js', // https://github.com/Five-great/tinymce-plugins/blob/main/importword/plugin.js
// tpImportword: '/tinymce/plugins/tpImportword/plugin.js', // https://github.com/tinymce-plugin/tinymce-plugin/blob/main/tpImportword/plugin.js
// tpImportword: 'https://unpkg.com/[email protected]/plugins/tpImportword/plugin.min.js', // https://github.com/tinymce-plugin/tinymce-plugin/blob/main/tpImportword/plugin.js
link: '/tinymce/plugins/link/plugin.js',
lists: '/tinymce/plugins/lists/plugin.js',
pagebreak: '/tinymce/plugins/pagebreak/plugin.js',
image: '/tinymce/plugins/image/plugin.js',
media: '/tinymce/plugins/media/plugin.js',
table: '/tinymce/plugins/table/plugin.js',
wordcount: '/tinymce/plugins/wordcount/plugin.js',
code: '/tinymce/plugins/code/plugin.js',
codesample: '/tinymce/plugins/codesample/plugin.js',
},
language_url: '/tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: '/tinymce/skins/ui/oxide',
content_css: '/tinymce/skins/content/default/content.css',
menubar: false,
statusbar: true,
plugins: [],
skeletonScreen: true,
toolbar: `editor135 preview print fullscreen restoredraft tpImportword | undo redo | forecolor backcolor bold italic underline strikethrough link | blocks fontfamily fontsize | \
alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | pagebreak | \
image media table wordcount | code codesample selectall`,
autosave_ask_before_unload: true,
autosave_interval: '10s',
toolbar_mode: 'sliding',
font_size_formats: '12px 14px 16px 18px 22px 24px 36px 72px',
height: props.height,
placeholder: props.placeholder || '',
branding: false,
resize: true,
elementpath: true,
content_style: '',
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,
images_upload_handler(blobInfo: any) {
const toBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file); // 读取为 base64
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const toBlobUrl = (file: File): Promise<string> => {
return Promise.resolve(URL.createObjectURL(file));
};
return new Promise(async (resolve, reject) => {
// 声明上传参数
const data = new FormData();
data.append('file', blobInfo.blob(), blobInfo.filename());
data.append('type', props.type);
data.append('usage_type', props.usageType);
window.blobInfo = blobInfo as any;
console.log(window.blobInfo, '上传文件信息');
try {
let res: any = null;
if (props.uploadFile) {
res = await props.uploadFile(data); // 你自定义的上传接口
}
console.log(res);
const url = res?.data?.fileinfo?.url;
if (!url) throw new Error('上传失败');
// TinyMCE 要求返回 string url
resolve(url);
} catch (err) {
console.error('上传失败,尝试使用 base64 显示', err);
try {
const base64 = await toBase64(blobInfo.blob());
resolve(base64);
} catch (e) {
reject(new Error('图片上传失败,且 base64 解析失败'));
}
}
});
},
setup(tinymceEditor: any) {
editorInstance.value = tinymceEditor;
tinymceEditor.on('init', () => {
tinymceEditor.getBody().style.fontSize = '14px';
// 避免 props.modelValue 还没有值就执行了
setTimeout(() => {
tinymceEditor.setContent(props.modelValue || '');
}, 1500);
const container = tinymceEditor.getContainer();
const wordCountButton = container.querySelector('button.tox-statusbar__wordcount');
wordCountButton?.click();
});
tinymceEditor.on('change undo redo', () => {
emitModelValue('update:modelValue', tinymceEditor.getContent());
});
tinymceEditor.on('OpenWindow', (e) => {
// FIX 编辑器在el-drawer中,编辑器的弹框无法获得焦点
const D = document.querySelector('.el-drawer.open');
const E = e.target.editorContainer;
if (D && D.contains(E)) {
const nowDA = document.activeElement;
setTimeout(() => {
document.activeElement.blur();
nowDA.focus();
}, 0);
}
});
},
});
});
watch(
() => props.modelValue,
(newVal: any) => {
if (editorInstance.value && newVal !== editorInstance.value.getContent()) {
editorInstance.value.setContent(newVal || '');
}
},
);
// Cleanup editor instance on component unmount
onUnmounted(() => {
if (editorInstance.value) {
tinymce.remove(editorInstance.value);
}
});
</script>
@mouyong
Copy link
Author

mouyong commented May 4, 2025

icons、plugins、skins、themes 从 node_modules 复制到 public/tinymce/ 目录
langs 从官网下载 https://www.tiny.cloud/get-tiny/language-packages/

yarn add tinymce
cp -r node_modules/tinymce/icons public/tinymce
cp -r node_modules/tinymce/themes public/tinymce
cp -r node_modules/tinymce/models public/tinymce
cp -r node_modules/tinymce/plugins public/tinymce

在 public/tinymce/plugins/editor135 目录下 手动创建 plugin.js 文件,内容从 gist 复制

在全局 components 目录封装 TinyMCE 编辑器。并进行全局注册。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment