textArea.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <template>
  2. <div>
  3. <article
  4. :id="id"
  5. v-loading="loading"
  6. :style="styles"
  7. element-loading-text="数据上传中,请稍后"
  8. element-loading-spinner="el-icon-loading"
  9. >
  10. <div v-show="isEmpty && !edit" class="text-edit">
  11. {{ emptyText }}
  12. <el-button type="text" @click="ImmediateAddition">{{ inputButton }}</el-button>
  13. </div>
  14. <div v-show="edit" class="editArticle" :class="fullScreen ? 'fullScreen' : ''">
  15. <span v-show="fullScreen" class="changeSizeBtn" @click="changeSize">
  16. <svg-icon
  17. icon-class="icon-sx"
  18. class="icon"
  19. />
  20. </span>
  21. <span v-show="!fullScreen" class="changeSizeBtn" @click="changeSize">
  22. <svg-icon
  23. icon-class="icon-qp"
  24. class="icon"
  25. />
  26. </span>
  27. <el-tooltip effect="dark" content="单击‘编辑’" placement="bottom">
  28. <editor :id="'tinymce_'+id" v-model="inputValue" :class="'tinymce_'+id" :init="init" @input="changeText" />
  29. </el-tooltip>
  30. </div>
  31. <div v-show="!isEmpty && !edit">
  32. <pre class="text-pre" @click="ImmediateAddition" v-html="handlerText(value)" />
  33. </div>
  34. <div :id="'inputUpload_'+id" style="display: none" @click.stop="blur_textarea" />
  35. <div v-show="edit" class="control">
  36. <el-button size="small" @click="edit=false">取消</el-button>
  37. <el-button type="primary" size="small" @click.stop="blur_textarea">确定</el-button>
  38. </div>
  39. </article>
  40. </div>
  41. </template>
  42. <script>
  43. import { getContainImgHTMLNode } from '@/utils/handleTinymce'
  44. import { keepLastIndex, uploadImg } from '@/utils/util'
  45. import tinymce from 'tinymce/tinymce'
  46. import Editor from '@tinymce/tinymce-vue'
  47. import 'tinymce/themes/silver/theme'
  48. import 'tinymce/icons/default/icons'
  49. import 'tinymce/plugins/table'
  50. export default {
  51. components: {
  52. Editor
  53. },
  54. props: {
  55. styles: {
  56. type: Object,
  57. default: () => {
  58. return { padding: '0 30px 20px 30px' }
  59. },
  60. required: false
  61. },
  62. id: {
  63. type: String,
  64. default: '',
  65. required: true
  66. },
  67. value: {
  68. type: String,
  69. default: '',
  70. required: false
  71. },
  72. height: {
  73. type: Number,
  74. default: 200,
  75. required: false
  76. },
  77. emptyText: {
  78. type: String,
  79. default: '',
  80. required: false
  81. },
  82. inputButton: {
  83. type: String,
  84. default: '',
  85. required: false
  86. }
  87. },
  88. data() {
  89. return {
  90. fullScreen: false,
  91. loading: false,
  92. inputValue: '',
  93. edit: false,
  94. init: {
  95. auto_focus: true,
  96. language_url: '/tinymce/langs/zh_CN.js',
  97. language: 'zh_CN',
  98. skin_url: '/tinymce/skins/ui/oxide', // 编辑器需要一个skin才能正常工作,所以要设置一个skin_url指向之前复制出来的skin文件
  99. height: this.height,
  100. browser_spellcheck: true, // 拼写检查
  101. branding: false, // 去水印
  102. elementpath: false, // 禁用编辑器底部的状态栏
  103. statusbar: false, // 隐藏编辑器底部的状态栏
  104. paste_data_images: true, // 允许粘贴图像
  105. menubar: false, // 隐藏最上方menu
  106. fontsize_formats: '14px 16px 18px 20px 24px 26px 28px 30px 32px 36px', // 字体大小
  107. file_picker_types: 'image',
  108. images_upload_credentials: true,
  109. plugins: 'lists table textcolor wordcount contextmenu', // 引入插件
  110. toolbar: 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent table | undo redo | removeformat formatselect | fullscreen',
  111. table_toolbar: 'tableprops | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol | tablemergecells tablesplitcells | fullscreen'
  112. }
  113. }
  114. },
  115. computed: {
  116. isEmpty() {
  117. return this.inputValue === null || this.inputValue.replace(/\s+/g, '') === ''
  118. }
  119. },
  120. watch: {
  121. value: {
  122. handler(newV, oldV) {
  123. this.inputValue = newV
  124. },
  125. immediate: true
  126. }
  127. // inputValue: {
  128. // handler(newV, oldV) {
  129. // this.resetImgSrc(newV)
  130. // },
  131. // immediate: true
  132. // }
  133. },
  134. mounted() {
  135. // 失去焦点保存功能
  136. // document.body.addEventListener('click', e => {
  137. // if (!document.getElementById(this.id)) {
  138. // return false
  139. // }
  140. // const isContain = document.getElementById(this.id).contains(e.target)
  141. // if (!isContain) {
  142. // document.getElementById(`inputUpload_${this.id}`).click()
  143. // }
  144. // })
  145. tinymce.init({ selector: `.tinymce_${this.id}` })
  146. },
  147. methods: {
  148. async resetImgSrc(str) {
  149. if (!str) return
  150. let newStr = str
  151. const imgReg = /<img.*?(?:>|\/>)/gi
  152. const srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i
  153. const imgArr = newStr.match(imgReg)
  154. imgArr && imgArr.map(async t => {
  155. const src = t.match(srcReg)
  156. if (src[1] && src[1].includes('data:image')) {
  157. const newImgUrl = await uploadImg(src[1])
  158. newStr = newStr.replace(src[1], newImgUrl)
  159. this.inputValue = newStr
  160. // 光标最后
  161. this.$nextTick(() => {
  162. const ifra = document.getElementById(`tinymce_${this.id}_ifr`)
  163. keepLastIndex(ifra.contentWindow.document.getElementById(`tinymce`), ifra.contentWindow)
  164. })
  165. }
  166. })
  167. },
  168. async resetImgSrcAll(str) {
  169. if (!str) return
  170. let newStr = str
  171. const imgReg = /<img.*?(?:>|\/>)/gi
  172. const srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i
  173. const imgArr = newStr.match(imgReg)
  174. let val = this.inputValue
  175. imgArr && imgArr.map(async t => {
  176. const src = t.match(srcReg)
  177. if (src[1] && src[1].includes('data:image')) {
  178. const newImgUrl = await uploadImg(src[1])
  179. console.log(newImgUrl)
  180. newStr = newStr.replace(src[1], newImgUrl)
  181. val = newStr
  182. // // 光标最后
  183. // this.$nextTick(() => {
  184. // const ifra = document.getElementById(`tinymce_${this.id}_ifr`)
  185. // keepLastIndex(ifra.contentWindow.document.getElementById(`tinymce`), ifra.contentWindow)
  186. // })
  187. }
  188. })
  189. console.log(val)
  190. return val
  191. },
  192. ImmediateAddition() { // 立即添加(编辑)
  193. this.edit = true
  194. },
  195. changeText(e) { // 富文本内容改变
  196. this.inputValue = e
  197. },
  198. async blur_textarea() {
  199. if (this.edit) {
  200. this.loading = true
  201. try {
  202. this.inputValue = await getContainImgHTMLNode(this.inputValue)
  203. } catch (error) {
  204. this.loading = false
  205. throw error
  206. }
  207. this.loading = false
  208. this.edit = false
  209. const val = await this.resetImgSrcAll(this.inputValue)
  210. this.$emit('update:value', val)
  211. this.$emit('change', val)
  212. }
  213. },
  214. handlerText(val) {
  215. if (val) {
  216. const reg = new RegExp(/<\/?p[^>]*>/gi)
  217. return val.replace(reg, '')
  218. }
  219. },
  220. changeSize() {
  221. this.fullScreen = !this.fullScreen
  222. }
  223. }
  224. }
  225. </script>
  226. <style scoped lang="less">
  227. .editArticle {
  228. position: relative;
  229. .changeSizeBtn {
  230. position: absolute;
  231. top: 3px;
  232. right: 0px;
  233. z-index: 1000;
  234. height: 34px;
  235. width: 24px;
  236. text-align: center;
  237. line-height: 34px;
  238. border-radius: 3px;
  239. .icon {
  240. color: #409eff;
  241. font-weight: 700;
  242. font-size: 18px;
  243. }
  244. &:hover {
  245. background: #c8cbcf;
  246. border: 0;
  247. box-shadow: none;
  248. }
  249. }
  250. }
  251. .fullScreen {
  252. position: fixed;
  253. top: 0px;
  254. bottom: 0;
  255. left: 225px;
  256. right: 0;
  257. z-index: 1000;
  258. height: 100vh!important;
  259. /deep/.tox-tinymce {
  260. height: 100vh!important;
  261. }
  262. }
  263. .text-edit {
  264. color: #666666;
  265. font-size: 14px;
  266. display: flex;
  267. align-items: center;
  268. justify-content: flex-start;
  269. }
  270. .text-pre {
  271. white-space:pre-line;
  272. font-size: 14px;
  273. color: #333B4A;
  274. cursor: pointer;
  275. }
  276. /deep/ textarea {
  277. width: calc(100% - 40px);
  278. margin: auto;
  279. }
  280. .control {
  281. display: flex;
  282. justify-content: flex-end;
  283. margin-top: 10px;
  284. }
  285. </style>