mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
chore: 重构 tinymce 组件
This commit is contained in:
@ -1 +0,0 @@
|
||||
export { default as Tinymce } from './src/editor.vue';
|
@ -1,202 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, useAttrs, watch } from 'vue';
|
||||
import { NSpin } from 'naive-ui';
|
||||
import { camelCase } from 'lodash-es';
|
||||
import type { IPropTypes } from '@tinymce/tinymce-vue/lib/cjs/main/ts/components/EditorPropTypes';
|
||||
import type { Editor as EditorType } from 'tinymce/tinymce';
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import type { AxiosProgressEvent } from '@/service/api/system/oss';
|
||||
import { uploadApi } from '@/service/api/system/oss';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { plugins as defaultPlugins, toolbar as defaultToolbar } from '@/components/tinymce/src/tinymce';
|
||||
|
||||
type InitOptions = IPropTypes['init'];
|
||||
|
||||
interface Props {
|
||||
height?: number | string;
|
||||
options?: Partial<InitOptions>;
|
||||
plugins?: string;
|
||||
toolbar?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'TinyMce',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 400,
|
||||
options: () => ({}),
|
||||
plugins: defaultPlugins,
|
||||
toolbar: defaultToolbar,
|
||||
disabled: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
mounted: [];
|
||||
}>();
|
||||
|
||||
/** https://www.jianshu.com/p/59a9c3802443 使用自托管方案(本地)代替cdn 没有key的限制 注意publicPath要以/结尾 */
|
||||
const tinymceScriptSrc = `${import.meta.env.VITE_BASE_URL}tinymce/tinymce.min.js`;
|
||||
|
||||
const content = defineModel<string | null>('modelValue', {
|
||||
default: ''
|
||||
});
|
||||
|
||||
const editorRef = shallowRef<EditorType | null>(null);
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const isDark = computed(() => themeStore.darkMode);
|
||||
const locale = computed(() => appStore.locale);
|
||||
const skinName = computed(() => {
|
||||
return isDark.value ? 'oxide-dark' : 'oxide';
|
||||
});
|
||||
|
||||
const contentCss = computed(() => {
|
||||
return isDark.value ? 'dark' : 'default';
|
||||
});
|
||||
|
||||
/** tinymce支持 en zh_CN */
|
||||
const langName = computed(() => {
|
||||
const lang = locale.value.replace('-', '_');
|
||||
if (lang.includes('en_US')) {
|
||||
return 'en';
|
||||
}
|
||||
return 'zh_CN';
|
||||
});
|
||||
|
||||
/** 通过v-if来挂载/卸载组件来完成主题切换切换 语言切换也需要监听 不监听在切换时候会显示原始<textarea>样式 */
|
||||
const init = ref(true);
|
||||
watch([isDark, locale], async () => {
|
||||
if (!editorRef.value) {
|
||||
return;
|
||||
}
|
||||
// 相当于手动unmounted清理 非常重要
|
||||
editorRef.value.destroy();
|
||||
init.value = false;
|
||||
// 放在下一次tick来切换
|
||||
// 需要先加载组件 也就是v-if为true 然后需要拿到editorRef 必须放在setTimeout(相当于onMounted)
|
||||
await nextTick();
|
||||
init.value = true;
|
||||
});
|
||||
|
||||
// 取消上传
|
||||
const uploadAbortController = new AbortController();
|
||||
onBeforeUnmount(() => {
|
||||
uploadAbortController.abort();
|
||||
});
|
||||
|
||||
// 加载完毕前显示spin
|
||||
const loading = ref(true);
|
||||
const initOptions = computed((): InitOptions => {
|
||||
const { height, options, plugins, toolbar } = props;
|
||||
return {
|
||||
auto_focus: true,
|
||||
branding: false, // 显示右下角的'使用 TinyMCE 构建'
|
||||
content_css: contentCss.value,
|
||||
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
|
||||
contextmenu: 'link image table',
|
||||
default_link_target: '_blank',
|
||||
height,
|
||||
image_advtab: true, // 图片高级选项
|
||||
image_caption: true,
|
||||
importcss_append: true,
|
||||
language: langName.value,
|
||||
link_title: false,
|
||||
menubar: 'file edit view insert format tools table help',
|
||||
noneditable_class: 'mceNonEditable',
|
||||
/** 允许粘贴图片 默认base64格式 images_upload_handler启用时为上传 */
|
||||
paste_data_images: true,
|
||||
images_file_types: 'jpeg,jpg,png,gif,bmp,webp',
|
||||
plugins,
|
||||
quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
|
||||
skin: skinName.value,
|
||||
toolbar,
|
||||
toolbar_mode: 'sliding',
|
||||
...options,
|
||||
/** 覆盖默认的base64行为 */
|
||||
images_upload_handler: (blobInfo, progress) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = blobInfo.blob();
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = e => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
progress(percent);
|
||||
};
|
||||
uploadApi(file, { onUploadProgress: progressEvent, signal: uploadAbortController.signal })
|
||||
.then(response => {
|
||||
const { error, data } = response;
|
||||
// 这里需要手动判断抛出异常 超时会走到then
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (data) {
|
||||
const { url } = data;
|
||||
resolve(url);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject({ message: error.message, remove: true });
|
||||
});
|
||||
});
|
||||
},
|
||||
setup: editor => {
|
||||
editorRef.value = editor;
|
||||
editor.on('init', () => {
|
||||
emit('mounted');
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
/** 获取透传的事件 通过v-on绑定 可绑定的事件 https://www.tiny.cloud/docs/tinymce/latest/vue-ref/#event-binding */
|
||||
const events = computed(() => {
|
||||
const onEvents: Record<string, any> = {};
|
||||
for (const key in attrs) {
|
||||
if (key.startsWith('on')) {
|
||||
const eventKey = camelCase(key.split('on')[1]!);
|
||||
onEvents[eventKey] = attrs[key];
|
||||
}
|
||||
}
|
||||
return onEvents;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-tinymce">
|
||||
<NSpin :show="loading">
|
||||
<Editor
|
||||
v-if="init"
|
||||
v-model="content"
|
||||
:init="initOptions"
|
||||
:tinymce-script-src="tinymceScriptSrc"
|
||||
:disabled="disabled"
|
||||
license-key="gpl"
|
||||
v-on="events"
|
||||
/>
|
||||
</NSpin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.tox.tox-silver-sink.tox-tinymce-aux {
|
||||
/** 该样式默认为1300的zIndex */
|
||||
z-index: 2025;
|
||||
}
|
||||
|
||||
.app-tinymce {
|
||||
/**
|
||||
隐藏右上角upgrade按钮
|
||||
*/
|
||||
.tox-promotion {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,11 +0,0 @@
|
||||
// Any plugins you want to setting has to be imported
|
||||
// Detail plugins list see https://www.tinymce.com/docs/plugins/
|
||||
// Custom builds see https://www.tinymce.com/download/custom-builds/
|
||||
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
|
||||
|
||||
// quickbars 快捷栏
|
||||
export const plugins =
|
||||
'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap emoticons accordion';
|
||||
|
||||
export const toolbar =
|
||||
'undo redo | accordion accordionremove | blocks fontfamily fontsize | bold italic underline strikethrough | align numlist bullist | link image | table media | lineheight outdent indent| forecolor backcolor removeformat | charmap emoticons | code fullscreen preview | save print | pagebreak anchor codesample | ltr rtl';
|
35
src/typings/components.d.ts
vendored
35
src/typings/components.d.ts
vendored
@ -19,21 +19,13 @@ declare module 'vue' {
|
||||
DictRadio: typeof import('./../components/custom/dict-radio.vue')['default']
|
||||
DictSelect: typeof import('./../components/custom/dict-select.vue')['default']
|
||||
DictTag: typeof import('./../components/custom/dict-tag.vue')['default']
|
||||
Editor: typeof import('./../components/tinymce/src/editor.vue')['default']
|
||||
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
|
||||
FileUpload: typeof import('./../components/custom/file-upload.vue')['default']
|
||||
FormTip: typeof import('./../components/custom/form-tip.vue')['default']
|
||||
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
|
||||
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
|
||||
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
|
||||
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
|
||||
IconEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
||||
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
|
||||
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
|
||||
'IconHugeicons:configuration01': typeof import('~icons/hugeicons/configuration01')['default']
|
||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
|
||||
IconLocalBanner: typeof import('~icons/local/banner')['default']
|
||||
'IconMaterialSymbols:add': typeof import('~icons/material-symbols/add')['default']
|
||||
'IconMaterialSymbols:deleteOutline': typeof import('~icons/material-symbols/delete-outline')['default']
|
||||
'IconMaterialSymbols:downloadRounded': typeof import('~icons/material-symbols/download-rounded')['default']
|
||||
@ -45,14 +37,8 @@ declare module 'vue' {
|
||||
IconMaterialSymbolsAddRounded: typeof import('~icons/material-symbols/add-rounded')['default']
|
||||
IconMaterialSymbolsDeleteOutline: typeof import('~icons/material-symbols/delete-outline')['default']
|
||||
IconMaterialSymbolsDriveFileRenameOutlineOutline: typeof import('~icons/material-symbols/drive-file-rename-outline-outline')['default']
|
||||
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
|
||||
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
|
||||
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
|
||||
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
|
||||
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
|
||||
'IconQuill:collapse': typeof import('~icons/quill/collapse')['default']
|
||||
'IconQuill:expand': typeof import('~icons/quill/expand')['default']
|
||||
IconUilSearch: typeof import('~icons/uil/search')['default']
|
||||
JsonPreview: typeof import('./../components/custom/json-preview.vue')['default']
|
||||
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
|
||||
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
|
||||
@ -61,25 +47,17 @@ declare module 'vue' {
|
||||
MenuTreeSelect: typeof import('./../components/custom/menu-tree-select.vue')['default']
|
||||
MonacoEditor: typeof import('./../components/common/monaco-editor.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
@ -95,36 +73,23 @@ declare module 'vue' {
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NTree: typeof import('naive-ui')['NTree']
|
||||
NTreeSelect: typeof import('naive-ui')['NTreeSelect']
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||
NWatermark: typeof import('naive-ui')['NWatermark']
|
||||
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
|
||||
PostSelect: typeof import('./../components/custom/post-select.vue')['default']
|
||||
|
@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { Tinymce } from '@sa/tinymce';
|
||||
import { fetchCreateNotice, fetchUpdateNotice } from '@/service/api/system/notice';
|
||||
import { getToken } from '@/store/modules/auth/shared';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { $t } from '@/locales';
|
||||
import { Tinymce } from '@/components/tinymce';
|
||||
|
||||
defineOptions({
|
||||
name: 'NoticeOperateDrawer'
|
||||
});
|
||||
@ -27,6 +32,17 @@ const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
clientid: import.meta.env.VITE_APP_CLIENT_ID!
|
||||
};
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
@ -106,7 +122,14 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
|
||||
<NDrawer
|
||||
v-model:show="visible"
|
||||
:trap-focus="false"
|
||||
:title="title"
|
||||
display-directive="show"
|
||||
:width="800"
|
||||
class="max-w-90%"
|
||||
>
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem label="公告标题" path="noticeTitle">
|
||||
@ -116,7 +139,13 @@ watch(visible, () => {
|
||||
<DictRadio v-model:value="model.noticeType" dict-code="sys_notice_type" />
|
||||
</NFormItem>
|
||||
<NFormItem label="公告内容" path="noticeContent">
|
||||
<Tinymce v-model="model.noticeContent" />
|
||||
<Tinymce
|
||||
v-model="model.noticeContent"
|
||||
:lang="appStore.locale"
|
||||
:is-dark="themeStore.darkMode"
|
||||
:upload-url="`${baseURL}/resource/oss/upload`"
|
||||
:headers="headers"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="公告状态" path="status">
|
||||
<DictRadio v-model:value="model.status" dict-code="sys_normal_disable" />
|
||||
|
Reference in New Issue
Block a user