index.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <template>
  2. <div>
  3. <Editor
  4. v-model="myValue"
  5. :init="init"
  6. :disabled="disabled"
  7. :placeholder="placeholder"
  8. :id="tinymceId"
  9. />
  10. </div>
  11. </template>
  12. <script setup lang="ts">
  13. import { reactive, ref, onMounted, watch } from 'vue'
  14. import Editor from '@tinymce/tinymce-vue'
  15. import tinymce from 'tinymce/tinymce'
  16. import 'tinymce/themes/silver'
  17. import 'tinymce/themes/silver/theme'
  18. import 'tinymce/models/dom'
  19. import 'tinymce/icons/default'
  20. import 'tinymce/icons/default/icons'
  21. // 引入编辑器插件 - 移除不存在的插件
  22. import 'tinymce/plugins/code'
  23. import 'tinymce/plugins/image'
  24. import 'tinymce/plugins/media'
  25. import 'tinymce/plugins/link'
  26. import 'tinymce/plugins/preview'
  27. import 'tinymce/plugins/table'
  28. import 'tinymce/plugins/pagebreak'
  29. import 'tinymce/plugins/lists'
  30. import 'tinymce/plugins/advlist'
  31. import 'tinymce/plugins/quickbars'
  32. import 'tinymce/plugins/wordcount'
  33. import '/public/langs/zh_CN'
  34. import 'tinymce/skins/content/default/content.css'
  35. import { getRefreshToken } from '@/utils/auth'
  36. // 定义接口
  37. interface Props {
  38. value?: string
  39. placeholder?: string
  40. height?: number
  41. disabled?: boolean
  42. plugins?: string | string[]
  43. toolbar?: string | string[]
  44. templates?: any[]
  45. options?: Record<string, any>
  46. }
  47. interface Emits {
  48. (e: 'update:value', value: string): void
  49. }
  50. // 定义props
  51. const props = withDefaults(defineProps<Props>(), {
  52. value: '',
  53. placeholder: '',
  54. height: 500,
  55. disabled: false,
  56. // 移除template插件
  57. plugins: 'code image media link preview table quickbars pagebreak lists advlist',
  58. // 移除template相关工具栏
  59. toolbar: 'undo redo codesample bold italic underline strikethrough link alignleft aligncenter alignright alignjustify bullist numlist outdent indent removeformat forecolor backcolor |formatselect fontselect fontsizeselect | blocks fontfamily fontsize pagebreak lists image media table preview | code selectall',
  60. templates: () => [],
  61. options: () => ({})
  62. })
  63. // 定义emits
  64. const emit = defineEmits<Emits>()
  65. // 响应式数据
  66. const myValue = ref<string>(props.value)
  67. const tinymceId = ref<string>(`vue-tinymce-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`)
  68. // 上传相关配置
  69. const HEADERS = { Authorization: 'Bearer ' + getRefreshToken() }
  70. const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent'
  71. // 文件上传处理函数
  72. const handleImageUpload = (blobInfo: any, progress: any): Promise<string> => {
  73. return new Promise((resolve, reject) => {
  74. const formData = new FormData()
  75. formData.append('file', blobInfo.blob(), blobInfo.filename())
  76. // 使用fetch进行上传
  77. fetch(UPLOAD_URL, {
  78. method: 'POST',
  79. headers: HEADERS,
  80. body: formData
  81. })
  82. .then(response => response.json())
  83. .then(res => {
  84. if (res.code === 0) {
  85. resolve(res.data.src || res.data.url)
  86. } else {
  87. reject(new Error(res.msg || '上传失败'))
  88. }
  89. })
  90. .catch(error => {
  91. console.error('上传错误:', error)
  92. reject(new Error('上传失败'))
  93. })
  94. })
  95. }
  96. // 文件选择回调
  97. const handleFilePicker = (callback: Function, value: string, meta: any) => {
  98. // 这里可以实现自定义的文件选择器
  99. if (meta.filetype === 'file') {
  100. callback('mypage.html', { text: 'My text' })
  101. }
  102. if (meta.filetype === 'image') {
  103. callback('myimage.jpg', { alt: 'My alt text' })
  104. }
  105. if (meta.filetype === 'media') {
  106. callback('movie.mp4', { source2: 'alt.ogg', poster: 'image.jpg' })
  107. }
  108. }
  109. // TinyMCE配置
  110. const init = reactive({
  111. selector: `#${tinymceId.value}`,
  112. language_url: '/tinymce/langs/zh-Hans.js',
  113. language: 'zh-Hans',
  114. skin_url: '/tinymce/skins/ui/oxide',
  115. content_css: '/tinymce/skins/content/default/content.css',
  116. menubar: true,
  117. statusbar: true,
  118. plugins: props.plugins,
  119. toolbar: props.toolbar,
  120. toolbar_mode: 'sliding',
  121. font_formats: 'Arial=arial,helvetica,sans-serif; 宋体=SimSun; 微软雅黑=Microsoft Yahei; Impact=impact,chicago;',
  122. paste_convert_word_fake_lists: false,
  123. font_size_formats: '12px 14px 16px 18px 22px 24px 36px 72px',
  124. height: props.height,
  125. placeholder: props.placeholder,
  126. branding: false,
  127. image_dimensions: false,
  128. paste_webkit_styles: 'all',
  129. paste_merge_formats: true,
  130. nonbreaking_force_tab: false,
  131. paste_auto_cleanup_on_paste: false,
  132. file_picker_types: 'file',
  133. resize: true,
  134. elementpath: true,
  135. content_style: '',
  136. // 移除templates配置
  137. quickbars_selection_toolbar: 'forecolor backcolor bold italic underline strikethrough link',
  138. quickbars_image_toolbar: 'alignleft aligncenter alignright',
  139. quickbars_insert_toolbar: false,
  140. image_caption: true,
  141. image_advtab: true,
  142. convert_urls: false,
  143. // 图片上传处理
  144. images_upload_handler: handleImageUpload,
  145. // 文件选择处理
  146. file_picker_callback: handleFilePicker,
  147. // 编辑器设置
  148. setup: (editor: any) => {
  149. editor.on('init', () => {
  150. editor.getBody().style.fontSize = '14px'
  151. })
  152. editor.on('OpenWindow', (e: any) => {
  153. // 修复编辑器在el-drawer中的焦点问题
  154. const drawer = document.querySelector('.el-drawer.open')
  155. const editorContainer = e.target.editorContainer
  156. if (drawer && drawer.contains(editorContainer)) {
  157. const nowActiveElement = document.activeElement
  158. setTimeout(() => {
  159. if (document.activeElement) {
  160. document.activeElement.blur()
  161. }
  162. if (nowActiveElement) {
  163. nowActiveElement.focus()
  164. }
  165. }, 0)
  166. }
  167. })
  168. },
  169. // 合并自定义选项
  170. ...props.options
  171. })
  172. // 监听外部数据变化
  173. watch(
  174. () => props.value,
  175. (newValue) => {
  176. myValue.value = newValue
  177. emit('update:value', myValue.value)
  178. }
  179. )
  180. // 监听内部数据变化
  181. watch(
  182. () => myValue.value,
  183. (newValue) => {
  184. emit('update:value', newValue)
  185. }
  186. )
  187. // 组件挂载时初始化
  188. onMounted(() => {
  189. tinymce.init({})
  190. })
  191. </script>
  192. <style scoped>
  193. /* 可以添加一些自定义样式 */
  194. </style>