| | |
| | | <div :class="prefixCls" :style="{ width: containerWidth }"> |
| | | <textarea |
| | | :id="tinymceId" |
| | | ref="elRef" |
| | | ref="elPwRef" |
| | | :style="{ visibility: 'hidden' }" |
| | | v-if="!initOptions.inline" |
| | | ></textarea> |
| | | <slot v-else></slot> |
| | | <!-- <div class="p-2 tox-statusbar"> |
| | | <a-upload |
| | | v-if="isElse" |
| | | v-model:file-list="fileListTemp" |
| | | action="https://www.mocky.io/v2/5cc8019d300000980a055e76" |
| | | list-type="picture" |
| | | class="upload-list-inline" |
| | | > |
| | | <a-button type="text" size="small" style="margin: 0 auto"> |
| | | <upload-outlined></upload-outlined> |
| | | 附件 |
| | | </a-button> |
| | | <template #iconRender><PaperClipOutlined /></template> |
| | | <template #itemRender="{ file, fileList, actions }"> |
| | | <a-space class="ant-upload-list-picture-card"> |
| | | <span style="display: flex; flex-wrap: wrap"> |
| | | <PaperClipOutlined style="margin-right: 4px" /> |
| | | <span v-if="!file.editor" :style="file.status === 'error' ? 'color: red' : ''">{{ |
| | | file.name |
| | | }}</span> |
| | | <span v-else> |
| | | <a-input size="small" v-model:value="file.tempName"></a-input> |
| | | </span> |
| | | </span> |
| | | <span v-if="!file.editor"> |
| | | <a href="javascript:;" @click="actions.preview">预览</a> |
| | | <a href="javascript:;" @click="fnRename(file, fileList)">重命名</a> |
| | | <a href="javascript:;" @click="actions.remove">删除</a> |
| | | </span> |
| | | <span v-else> |
| | | <a href="javascript:;" @click="fnSaveRename(file, fileList)">保存</a> |
| | | <a href="javascript:;" @click="fnOffRename(file, fileList)">取消</a> |
| | | </span> |
| | | </a-space> |
| | | </template> |
| | | </a-upload> |
| | | <div :class="fileListTemp.length > 0 ? 'my-upload-list' : ''"> |
| | | <div style="display: flex"> |
| | | <ImgUpload |
| | | :fullscreen="fullscreen" |
| | | @uploading="handleImageUploading" |
| | | @done="handleDone" |
| | | v-if="isImg" |
| | | v-show="editorRef" |
| | | :title="'图片'" |
| | | :disabled="disabled" |
| | | :accept="'.jpg,.jpeg,.gif,.png,.webp'" |
| | | /> |
| | | <a-button v-if="isText" type="text" size="small"> |
| | | <SnippetsOutlined /> |
| | | 快速文本</a-button |
| | | > |
| | | </div> |
| | | </div> |
| | | </div> --> |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts" setup> |
| | | import type { Editor, RawEditorSettings } from 'tinymce'; |
| | | import { PaperClipOutlined, UploadOutlined, SnippetsOutlined } from '@ant-design/icons-vue'; |
| | | import tinymce from 'tinymce/tinymce'; |
| | | import 'tinymce/themes/silver'; |
| | | import 'tinymce/icons/default/icons'; |
| | |
| | | import 'tinymce/plugins/image'; |
| | | import 'tinymce/plugins/table'; |
| | | import 'tinymce/plugins/charmap'; |
| | | import 'tinymce/plugins/imagetools'; |
| | | // import 'tinymce/plugins/imagetools'; |
| | | import 'tinymce/plugins/help'; |
| | | import 'tinymce/plugins/emoticons'; |
| | | import 'tinymce/plugins/emoticons/js/emojis'; |
| | |
| | | // import 'tinymce/plugins/tinyddrive'; |
| | | // import 'tinymce/plugins/advcode'; |
| | | // import 'tinymce/plugins/mediaembed'; |
| | | import 'tinymce/plugins/toc'; |
| | | // import 'tinymce/plugins/toc'; |
| | | // import 'tinymce/plugins/checklist'; |
| | | // import 'tinymce/plugins/tinycespellchecker'; |
| | | // import 'tinymce/plugins/a11ychecker'; |
| | |
| | | PropType, |
| | | useAttrs, |
| | | } from 'vue'; |
| | | import ImgUpload from './ImgUpload.vue'; |
| | | import { |
| | | plugins as defaultPlugins, |
| | | toolbar as defaultToolbar, |
| | | toolbar_groups as defaultStyleFormats, |
| | | } from './tinymce'; |
| | | import { buildShortUUID } from '@/utils/uuid'; |
| | | import { bindHandlers } from './helper'; |
| | | import { onMountedOrActivated } from '@vben/hooks'; |
| | | import { useDesign } from '@/hooks/web/useDesign'; |
| | | import { isNumber } from '@/utils/is'; |
| | |
| | | height: { |
| | | type: [Number, String] as PropType<string | number>, |
| | | required: false, |
| | | default: 200, |
| | | default: 400, |
| | | }, |
| | | width: { |
| | | type: [Number, String] as PropType<string | number>, |
| | |
| | | fontsize: { |
| | | type: String, |
| | | }, |
| | | isElse: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | isText: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | isImg: { |
| | | type: Boolean, |
| | | default: true, |
| | | }, |
| | | }); |
| | | console.log('Editor', props); |
| | | |
| | | const emit = defineEmits(['change', 'update:modelValue', 'inited', 'init-error']); |
| | | |
| | | const attrs = useAttrs(); |
| | | const editorRef = ref<Editor | null>(null); |
| | | const tinymceId = ref<string>(buildShortUUID('tiny-vue')); |
| | | const elRef = ref<HTMLElement | null>(null); |
| | | const fullscreen = ref(false); |
| | | const tinymceId = ref<string>(buildShortUUID('tiny-vue-pw')); |
| | | const elPwRef = ref<HTMLElement | null>(null); |
| | | |
| | | const { prefixCls } = useDesign('tinymce-container'); |
| | | |
| | |
| | | fontsize_formats: '10px 11px 12px 14px 16px 18px 24px 36px 48px 48px 56px 72px', |
| | | image_advtab: true, |
| | | importcss_append: true, // 允许样式生效 |
| | | toolbar:[], |
| | | toolbar_sticky: true, // 粘性工具栏(或停靠工具栏),在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部 |
| | | toolbar_mode: 'floating', //默认wrap不收缩工具栏,取值为floating或sliding时,将第一行放不下的工具栏按钮缩进抽屉(3个点的图标)里,scrolling则采用移动端的横线滚动方式。 |
| | | // style_formats_autohide: true, |
| | | menubar: false, |
| | | branding: false, |
| | | elementpath: false, |
| | | toolbar: [], |
| | | // quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', |
| | | plugins, |
| | | language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js', |
| | |
| | | skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value, |
| | | content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css', |
| | | ...options, |
| | | readonly: true, |
| | | setup: (editor: Editor) => { |
| | | editorRef.value = editor; |
| | | editor.on('init', (e) => initSetup(e)); |
| | | }, |
| | | // 只读模式 |
| | | readonly: true, |
| | | }; |
| | | }); |
| | | |
| | | const disabled = computed(() => { |
| | | const { options } = props; |
| | | const getdDisabled = options && Reflect.get(options, 'readonly'); |
| | | const editor = unref(editorRef); |
| | | if (editor) { |
| | | editor.setMode(getdDisabled ? 'readonly' : 'design'); |
| | | } |
| | | return getdDisabled ?? false; |
| | | }); |
| | | |
| | | watch( |
| | |
| | | } |
| | | |
| | | function initEditor() { |
| | | const el = unref(elRef); |
| | | const el = unref(elPwRef); |
| | | if (el) { |
| | | el.style.visibility = ''; |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | function setValue(editor: Record<string, any>, val?: string, prevVal?: string) { |
| | | function initSetup(e) { |
| | | const editor = unref(editorRef); |
| | | if (!editor) { |
| | | return; |
| | | } |
| | | const value = props.modelValue || ''; |
| | | |
| | | editor.setContent(value); |
| | | bindModelHandlers(editor); |
| | | bindHandlers(e, attrs, unref(editorRef)); |
| | | } |
| | | |
| | | function setValue(editor: Record<string, any>, val?: string, prevVal?: string) { |
| | | if (!val) { |
| | | editor.setContent(''); |
| | | } |
| | | if ( |
| | | editor && |
| | | typeof val === 'string' && |
| | |
| | | ) { |
| | | editor.setContent(val); |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | function bindModelHandlers(editor: any) { |
| | | const modelEvents = attrs.modelEvents ? attrs.modelEvents : null; |
| | | const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents; |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | (val, prevVal) => { |
| | | if(val){ |
| | | setValue(editor, val, prevVal); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.value, |
| | | (val, prevVal) => { |
| | | setValue(editor, val, prevVal); |
| | | }, |
| | | { |
| | | immediate: true, |
| | | }, |
| | | ); |
| | | |
| | | editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => { |
| | | const content = editor.getContent({ format: attrs.outputFormat }); |
| | | emit('update:modelValue', content); |
| | | const data = { |
| | | content, |
| | | fileUNID: fileListTemp.value, |
| | | }; |
| | | emit('change', data); |
| | | }); |
| | | |
| | | editor.on('FullscreenStateChanged', (e) => { |
| | | fullscreen.value = e.state; |
| | | }); |
| | | } |
| | | |
| | | function handleImageUploading(name: string) { |
| | | const editor = unref(editorRef); |
| | | if (!editor) { |
| | | return; |
| | | } |
| | | editor.execCommand('mceInsertContent', false, getUploadingImgName(name)); |
| | | const content = editor?.getContent() ?? ''; |
| | | setValue(editor, content); |
| | | } |
| | | |
| | | function handleDone(name: string, url: string) { |
| | | const editor = unref(editorRef); |
| | | if (!editor) { |
| | | return; |
| | | } |
| | | const content = editor?.getContent() ?? ''; |
| | | const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? ''; |
| | | setValue(editor, val); |
| | | } |
| | | |
| | | function getUploadingImgName(name: string) { |
| | | return `[uploading:${name}]`; |
| | | } |
| | | |
| | | // 附件 |
| | | const fileListTemp = ref<UploadProps['fileList']>([ |
| | | // { |
| | | // uid: '-1', |
| | | // name: 'xxx.png', |
| | | // tempName: 'xxx', |
| | | // status: 'done', |
| | | // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', |
| | | // thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', |
| | | // editor: false, |
| | | // }, |
| | | // { |
| | | // uid: '-2', |
| | | // name: 'yyy.png', |
| | | // tempName: 'yyy', |
| | | // status: 'done', |
| | | // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', |
| | | // thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', |
| | | // editor: false, |
| | | // }, |
| | | ]); |
| | | function fnRename(file, fileList) { |
| | | console.log(file, fileList); |
| | | fileListTemp.value = fileList.map((item) => { |
| | | // item.tempName = item.name.split('.').slice(0,-1) |
| | | if (file.uid == item.uid) { |
| | | item.editor = true; |
| | | } else { |
| | | item.editor = false; |
| | | } |
| | | return item; |
| | | }); |
| | | } |
| | | function fnSaveRename(file, fileList) { |
| | | fileListTemp.value = fileList.map((item) => { |
| | | if (file.uid == item.uid) { |
| | | item.name = item.tempName; |
| | | item.editor = false; |
| | | } |
| | | return item; |
| | | }); |
| | | } |
| | | function fnOffRename(file, fileList) { |
| | | fileListTemp.value = fileList.map((item) => { |
| | | if (file.uid == item.uid) { |
| | | item.editor = false; |
| | | } |
| | | return item; |
| | | }); |
| | | } |
| | | </script> |
| | | <style lang="less" scope> |
| | | @prefix-cls: ~'@{namespace}-tinymce-container'; |
| | |
| | | } |
| | | } |
| | | |
| | | .my-upload-list { |
| | | // 过渡 |
| | | position: absolute; |
| | | left: 78px; |
| | | transition: all 0.3s; |
| | | } |
| | | |
| | | .tox-statusbar { |
| | | display: flex; |
| | | // position: absolute; |
| | | min-height: 40px; |
| | | border-bottom-right-radius: 8px; |
| | | border-bottom-left-radius: 8px; |
| | | background: #f0f2f5; |