新增:开发者中心功能、md编辑器等。

This commit is contained in:
2025-02-17 15:25:24 +08:00
parent 9081e41188
commit d61e683d41
40 changed files with 5036 additions and 591 deletions

View File

@@ -0,0 +1,242 @@
<!-- 富文本编辑器 -->
<template>
<component v-if="inlineEditor" :is="tagName" :id="elementId" />
<textarea v-else :id="elementId"></textarea>
</template>
<script lang="ts" setup>
import {
watch,
onMounted,
onBeforeUnmount,
onActivated,
onDeactivated,
nextTick,
useAttrs
} from 'vue';
import tinymce from 'tinymce/tinymce';
import type {
Editor as TinyMCEEditor,
EditorEvent,
RawEditorSettings
} from 'tinymce';
import 'tinymce/themes/silver';
import 'tinymce/icons/default';
import 'tinymce/plugins/code';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/save';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/link';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/image';
import 'tinymce/plugins/media';
import 'tinymce/plugins/table';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/hr';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/emoticons';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/quickbars';
import 'tinymce/plugins/nonbreaking';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
import 'tinymce/plugins/emoticons/js/emojis';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import {
DEFAULT_CONFIG,
DARK_CONFIG,
uuid,
bindHandlers,
openAlert
} from './util';
import type { AlertOption } from './util';
const props = withDefaults(
defineProps<{
// 编辑器唯一 id
id?: string;
// v-model
value?: string;
// 编辑器配置
init?: RawEditorSettings;
// 是否内联模式
inline?: boolean;
// model events
modelEvents?: string;
// 内联模式标签名
tagName?: string;
// 是否禁用
disabled?: boolean;
// 是否跟随框架主题
autoTheme?: boolean;
// 不跟随框架主题时是否使用暗黑主题
darkTheme?: boolean;
}>(),
{
inline: false,
modelEvents: 'change input undo redo',
tagName: 'div',
autoTheme: true
}
);
const emit = defineEmits<{
(e: 'update:value', value: string): void;
}>();
const attrs = useAttrs();
const themeStore = useThemeStore();
const { darkMode } = storeToRefs(themeStore);
// 编辑器唯一 id
const elementId: string = props.id || uuid('tiny-vue');
// 编辑器实例
let editorIns: TinyMCEEditor | null = null;
// 是否内联模式
const inlineEditor: boolean = props.init?.inline || props.inline;
/* 更新 value */
const updateValue = (value: string) => {
emit('update:value', value);
};
/* 修改内容 */
const setContent = (value?: string) => {
if (
editorIns &&
typeof value === 'string' &&
value !== editorIns.getContent()
) {
editorIns.setContent(value);
}
};
/* 渲染编辑器 */
const render = () => {
const isDark = props.autoTheme ? darkMode.value : props.darkTheme;
tinymce.init({
...DEFAULT_CONFIG,
...(isDark ? DARK_CONFIG : {}),
...props.init,
selector: `#${elementId}`,
readonly: props.disabled,
inline: inlineEditor,
setup: (editor: TinyMCEEditor) => {
editorIns = editor;
editor.on('init', (e: EditorEvent<any>) => {
// 回显初始值
if (props.value) {
setContent(props.value);
}
// v-model
editor.on(props.modelEvents, () => {
updateValue(editor.getContent());
});
// valid events
bindHandlers(e, attrs, editor);
});
if (typeof props.init?.setup === 'function') {
props.init.setup(editor);
}
}
});
};
/* 销毁编辑器 */
const destory = () => {
if (tinymce != null && editorIns != null) {
tinymce.remove(editorIns as any);
editorIns = null;
}
};
/* 弹出提示框 */
const alert = (option?: AlertOption) => {
openAlert(editorIns, option);
};
defineExpose({ editorIns, alert });
watch(
() => props.value,
(val: string, prevVal: string) => {
if (val !== prevVal) {
setContent(val);
}
}
);
watch(
() => props.disabled,
(disable) => {
if (editorIns !== null) {
if (typeof editorIns.mode?.set === 'function') {
editorIns.mode.set(disable ? 'readonly' : 'design');
} else {
editorIns.setMode(disable ? 'readonly' : 'design');
}
}
}
);
watch(
() => props.tagName,
() => {
destory();
nextTick(() => {
render();
});
}
);
watch(darkMode, () => {
if (props.autoTheme) {
destory();
nextTick(() => {
render();
});
}
});
onMounted(() => {
render();
});
onBeforeUnmount(() => {
destory();
});
onActivated(() => {
render();
});
onDeactivated(() => {
destory();
});
</script>
<style>
body .tox-tinymce-aux {
z-index: 19990000;
}
textarea[id^='tiny-vue'] {
width: 0;
height: 0;
margin: 0;
padding: 0;
opacity: 0;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,248 @@
import type {
Editor as TinyMCEEditor,
EditorEvent,
RawEditorSettings
} from 'tinymce';
const BASE_URL = import.meta.env.BASE_URL;
// 默认加载插件
const PLUGINS: string = [
'code',
'preview',
'fullscreen',
'paste',
'searchreplace',
'save',
'autosave',
'link',
'autolink',
'image',
'media',
'table',
'codesample',
'lists',
'advlist',
'hr',
'charmap',
'emoticons',
'anchor',
'directionality',
'pagebreak',
'quickbars',
'nonbreaking',
'visualblocks',
'visualchars',
'wordcount'
].join(' ');
// 默认工具栏布局
const TOOLBAR: string = [
'fullscreen',
'preview',
'code',
'codesample',
'emoticons',
'image',
'media',
'|',
'undo',
'redo',
'|',
'forecolor',
'backcolor',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'|',
'alignleft',
'aligncenter',
'alignright',
'alignjustify',
'|',
'outdent',
'indent',
'|',
'numlist',
'bullist',
'|',
'formatselect',
'fontselect',
'fontsizeselect',
'|',
'link',
'charmap',
'anchor',
'pagebreak',
'|',
'ltr',
'rtl'
].join(' ');
// 默认配置
export const DEFAULT_CONFIG: RawEditorSettings = {
height: 300,
branding: false,
skin_url: BASE_URL + 'tinymce/skins/ui/oxide',
content_css: BASE_URL + 'tinymce/skins/content/default/content.min.css',
language_url: BASE_URL + 'tinymce/langs/zh_CN.js',
language: 'zh_CN',
plugins: PLUGINS,
toolbar: TOOLBAR,
draggable_modal: true,
toolbar_mode: 'sliding',
quickbars_insert_toolbar: '',
images_upload_handler: (blobInfo: any, success: any, error: any) => {
if (blobInfo.blob().size / 1024 > 400) {
error('大小不能超过 400KB');
return;
}
success('data:image/jpeg;base64,' + blobInfo.base64());
},
file_picker_types: 'media',
file_picker_callback: () => {}
};
// 暗黑主题配置
export const DARK_CONFIG: RawEditorSettings = {
skin_url: BASE_URL + 'tinymce/skins/ui/oxide-dark',
content_css: BASE_URL + 'tinymce/skins/content/dark/content.min.css'
};
// 支持监听的事件
export const VALID_EVENTS = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid'
];
let unique = 0;
/**
* 生成编辑器 id
*/
export function uuid(prefix: string): string {
const time = Date.now();
const random = Math.floor(Math.random() * 1000000000);
unique++;
return prefix + '_' + random + unique + String(time);
}
/**
* 绑定事件
*/
export function bindHandlers(
initEvent: EditorEvent<any>,
listeners: Record<string, any>,
editor: TinyMCEEditor
): void {
const validEvents = VALID_EVENTS.map((event) => event.toLowerCase());
Object.keys(listeners)
.filter((key: string) => validEvents.includes(key.toLowerCase()))
.forEach((key: string) => {
const handler = listeners[key];
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor);
} else {
editor.on(key.substring(2), (e: EditorEvent<any>) =>
handler(e, editor)
);
}
}
});
}
/**
* 弹出提示框
*/
export function openAlert(
editor: TinyMCEEditor | null,
option: AlertOption = {}
) {
editor?.windowManager?.open({
title: option.title ?? '提示',
body: {
type: 'panel',
items: [
{
type: 'htmlpanel',
html: `<p>${option.content ?? ''}</p>`
}
]
},
buttons: [
{
type: 'cancel',
name: 'closeButton',
text: '确定',
primary: true
}
]
});
}
export interface AlertOption {
title?: string;
content?: string;
}