normalArea.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <template>
  2. <div>
  3. <article :id="id" :style="fullScreen ? { ...fullPositionStyle, ...styles } : styles" class="article" :class="getClass">
  4. <!-- <textEditor :id="id" :value="inputValue" /> -->
  5. <span v-show="fullScreen" class="changeSizeBtn" @click="changeSize">
  6. <svg-icon
  7. icon-class="icon-sx"
  8. class="icon"
  9. />
  10. </span>
  11. <span v-show="!fullScreen" class="changeSizeBtn" @click="changeSize">
  12. <svg-icon
  13. icon-class="icon-qp"
  14. class="icon"
  15. />
  16. </span>
  17. <editor :id="'tinymce_'+id" ref="editor" v-model="inputValue" :init="init" @input="changeText" />
  18. </article>
  19. </div>
  20. </template>
  21. <script>
  22. import axios from 'axios'
  23. import tinymce from 'tinymce/tinymce'
  24. import Editor from '@tinymce/tinymce-vue'
  25. import 'tinymce/themes/silver/theme'
  26. import 'tinymce/icons/default/icons'
  27. // import { uploadImage } from '@/api/common.js'
  28. // import textEditor from './editor'
  29. export default {
  30. components: {
  31. Editor
  32. // textEditor
  33. },
  34. props: {
  35. id: {
  36. type: String,
  37. default: '',
  38. required: true
  39. },
  40. value: {
  41. type: String,
  42. default: '',
  43. required: false
  44. },
  45. height: {
  46. type: Number,
  47. default: 200,
  48. required: false
  49. },
  50. fullPositionStyle: {
  51. type: Object,
  52. default: () => {
  53. return { }
  54. },
  55. required: false
  56. },
  57. styles: {
  58. type: Object,
  59. default: () => {
  60. return { }
  61. },
  62. required: false
  63. },
  64. bottomMargin: {
  65. type: Boolean,
  66. default: false,
  67. required: false
  68. }
  69. },
  70. data() {
  71. return {
  72. fullScreen: false,
  73. inputValue: '',
  74. edit: false,
  75. init: {
  76. auto_focus: false,
  77. language_url: '/tinymce/langs/zh_CN.js',
  78. language: 'zh_CN',
  79. skin_url: '/tinymce/skins/ui/oxide', // 编辑器需要一个skin才能正常工作,所以要设置一个skin_url指向之前复制出来的skin文件
  80. height: this.height,
  81. browser_spellcheck: true, // 拼写检查
  82. branding: false, // 去水印
  83. elementpath: false, // 禁用编辑器底部的状态栏
  84. statusbar: false, // 隐藏编辑器底部的状态栏
  85. paste_data_images: true, // 允许粘贴图像
  86. menubar: false, // 隐藏最上方menu
  87. fontsize_formats: '14px 16px 18px 20px 24px 26px 28px 30px 32px 36px', // 字体大小
  88. file_picker_types: 'image',
  89. images_upload_credentials: true,
  90. plugins: 'fullscreen lists table textcolor wordcount contextmenu', // 引入插件
  91. toolbar: 'fullscreen bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent table | undo redo | removeformat formatselect',
  92. table_toolbar: 'tableprops | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol | tablemergecells tablesplitcells'
  93. }
  94. }
  95. },
  96. computed: {
  97. getClass() {
  98. let className = ''
  99. if (this.fullScreen) {
  100. className += 'fullScreen'
  101. }
  102. if (this.bottomMargin) {
  103. className += ' bottomMargin'
  104. }
  105. return className
  106. }
  107. },
  108. watch: {
  109. value: {
  110. handler(newV, oldV) {
  111. console.log('new', newV, 'old', oldV)
  112. // resetImgSrc
  113. this.inputValue = this.resetImgSrc(newV)
  114. },
  115. immediate: true
  116. }
  117. },
  118. mounted() {
  119. tinymce.init({ selector: `#tinymce_${this.id}` })
  120. },
  121. methods: {
  122. async resetImgSrc(str) {
  123. let newStr = str
  124. const imgReg = /<img.*?(?:>|\/>)/gi
  125. const srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i
  126. const imgArr = newStr.match(imgReg)
  127. console.log(imgArr)
  128. imgArr && imgArr.map(async t => {
  129. var src = t.match(srcReg)
  130. console.log(src)
  131. if (src[1] && src[1].includes('data:image')) {
  132. // var t = src[0].replace(/src/i, "href");
  133. // alert(t);
  134. // const res = await uploadImage({
  135. // 'imgData': src[1],
  136. // 'innerPublic': true
  137. // })
  138. const newImgUrl = await this.uploadImg(src[1])
  139. newStr = newStr.replace(src[1], newImgUrl)
  140. console.log(newStr)
  141. console.log(src[1])
  142. }
  143. })
  144. console.log('newStr', newStr)
  145. console.log('str', str)
  146. return newStr
  147. },
  148. uploadImg(imgData) {
  149. // var fd = new FormData();
  150. // fd.append('file', this.base64Url2Blob(imgData));
  151. // fd.append('innerPublic',true);
  152. const config = {
  153. headers: {
  154. // 'Content-Type': 'multipart/form-data'
  155. 'Content-Type': 'multipart/json'
  156. },
  157. withCredentials: false
  158. } // 添加请求头
  159. return new Promise((resolve, reject) => {
  160. axios.post(
  161. '//star.xiaojukeji.com/upload/img.node',
  162. // fd,
  163. {
  164. 'imgData': imgData,
  165. 'innerPublic': true
  166. },
  167. config
  168. ).then(res => {
  169. console.log(res)
  170. // newStr = newStr.replace(imgData, res.url)
  171. resolve(res.url)
  172. }).catch(err => {
  173. reject(err)
  174. })
  175. })
  176. },
  177. base64Url2Blob (url) {
  178. // 将base64url通过 , 分割为含有两个元素的数组
  179. const temp = url.split(',')
  180. // 将图片的base64编码数据解码
  181. const bytes = window.atob(temp[1])
  182. // 匹配图片的 mime
  183. const mime = temp[0].match(/:(.*?);/)[1]
  184. // 创建一个类型化数组,该数组的长度与解码后的图片数据长度相同
  185. let ia = new Uint8Array(bytes.length)
  186. // 变量图片数据的每一位,并将每一位的 Unicode 编码存入类型化数组
  187. for (let i = 0; i < bytes.length; i++) {
  188. ia[i] = bytes.charCodeAt(i)
  189. }
  190. // 通过类型化数组创建一个 Blob 对象
  191. return new Blob([ia], {type: mime})
  192. },
  193. changeText(e) { // 富文本内容改变
  194. this.inputValue = e
  195. this.$emit('update:value', this.inputValue)
  196. this.$emit('change', this.inputValue)
  197. },
  198. changeSize() {
  199. this.fullScreen = !this.fullScreen
  200. }
  201. }
  202. }
  203. </script>
  204. <style scoped lang="less">
  205. .article {
  206. position: relative;
  207. .changeSizeBtn {
  208. position: absolute;
  209. top: 3px;
  210. right: 0px;
  211. z-index: 1000;
  212. height: 34px;
  213. width: 24px;
  214. text-align: center;
  215. line-height: 34px;
  216. border-radius: 3px;
  217. .icon {
  218. color: #409eff;
  219. font-weight: 700;
  220. font-size: 18px;
  221. }
  222. &:hover {
  223. background: #c8cbcf;
  224. border: 0;
  225. box-shadow: none;
  226. }
  227. }
  228. }
  229. .fullScreen {
  230. position: fixed;
  231. top: 0px;
  232. bottom: 0;
  233. left: 225px;
  234. right: 0;
  235. z-index: 1000;
  236. height: 100vh!important;
  237. /deep/.tox-tinymce {
  238. height: 100vh!important;
  239. }
  240. &.bottomMargin {
  241. /deep/.tox-tinymce {
  242. height: calc(100vh - 40px)!important;
  243. }
  244. }
  245. }
  246. .editor {
  247. border: 1px solid #666;
  248. }
  249. </style>