textArea.vue 8.3 KB

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