TEditor.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. <template>
  2. <div>
  3. <div class="teditor-box" :class="[spinShow?'teditor-loadstyle':'teditor-loadedstyle']">
  4. <textarea ref="myTextarea" :id="id">{{ content }}</textarea>
  5. <Spin fix v-if="spinShow">
  6. <Icon type="ios-loading" size=18 class="upload-control-spin-icon-load"></Icon>
  7. <div>{{$L('加载组件中...')}}</div>
  8. </Spin>
  9. <ImgUpload
  10. ref="myUpload"
  11. class="upload-control"
  12. type="callback"
  13. :uploadIng.sync="uploadIng"
  14. @on-callback="editorImage"
  15. num="50"/>
  16. <Upload
  17. name="files"
  18. ref="fileUpload"
  19. class="upload-control"
  20. :action="actionUrl"
  21. :data="params"
  22. multiple
  23. :format="uploadFormat"
  24. :show-upload-list="false"
  25. :max-size="maxSize"
  26. :on-progress="handleProgress"
  27. :on-success="handleSuccess"
  28. :on-error="handleError"
  29. :on-format-error="handleFormatError"
  30. :on-exceeded-size="handleMaxSize"
  31. :before-upload="handleBeforeUpload"/>
  32. </div>
  33. <Spin fix v-if="uploadIng > 0">
  34. <Icon type="ios-loading" class="upload-control-spin-icon-load"></Icon>
  35. <div>{{$L('正在上传文件...')}}</div>
  36. </Spin>
  37. <Modal v-model="transfer" class="teditor-transfer" @on-visible-change="transferChange" footer-hide fullscreen transfer>
  38. <div slot="close">
  39. <Button type="primary" size="small">{{$L('完成')}}</Button>
  40. </div>
  41. <div class="teditor-transfer-body">
  42. <textarea :id="'T_' + id">{{ content }}</textarea>
  43. </div>
  44. <Spin fix v-if="uploadIng > 0">
  45. <Icon type="ios-loading" class="upload-control-spin-icon-load"></Icon>
  46. <div>{{$L('正在上传文件...')}}</div>
  47. </Spin>
  48. </Modal>
  49. </div>
  50. </template>
  51. <style lang="scss">
  52. .teditor-box {
  53. textarea {
  54. opacity: 0;
  55. }
  56. .tox-tinymce {
  57. box-shadow: none;
  58. box-sizing: border-box;
  59. border-color: #dddee1;
  60. border-radius: 4px;
  61. overflow: hidden;
  62. .tox-statusbar {
  63. span.tox-statusbar__branding {
  64. a {
  65. display: none;
  66. }
  67. }
  68. }
  69. }
  70. }
  71. .teditor-transfer {
  72. background-color: #ffffff;
  73. .tox-toolbar {
  74. > div:last-child {
  75. > button:last-child {
  76. margin-right: 64px;
  77. }
  78. }
  79. }
  80. .ivu-modal-header {
  81. display: none;
  82. }
  83. .ivu-modal-close {
  84. top: 7px;
  85. z-index: 2;
  86. }
  87. .teditor-transfer-body {
  88. position: absolute;
  89. top: 0;
  90. left: 0;
  91. width: 100%;
  92. height: 100%;
  93. padding: 0;
  94. margin: 0;
  95. textarea {
  96. opacity: 0;
  97. }
  98. .tox-tinymce {
  99. border: 0;
  100. .tox-statusbar {
  101. span.tox-statusbar__branding {
  102. a {
  103. display: none;
  104. }
  105. }
  106. }
  107. }
  108. }
  109. }
  110. .tox {
  111. &.tox-silver-sink {
  112. z-index: 13000;
  113. }
  114. }
  115. </style>
  116. <style lang="scss" scoped>
  117. .teditor-loadstyle {
  118. width: 100%;
  119. height: 180px;
  120. overflow: hidden;
  121. position: relative;
  122. }
  123. .teditor-loadedstyle {
  124. width: 100%;
  125. max-height: inherit;
  126. overflow: inherit;
  127. position: relative;
  128. }
  129. .upload-control {
  130. display: none;
  131. width: 0;
  132. height: 0;
  133. overflow: hidden;
  134. }
  135. </style>
  136. <script>
  137. import tinymce from 'tinymce/tinymce';
  138. import ImgUpload from "./ImgUpload";
  139. export default {
  140. name: 'TEditor',
  141. components: {ImgUpload},
  142. props: {
  143. id: {
  144. type: String,
  145. default: () => {
  146. return "tinymce_" + Math.round(Math.random() * 10000);
  147. }
  148. },
  149. value: {
  150. default: ''
  151. },
  152. height: {
  153. default: 360,
  154. },
  155. htmlClass: {
  156. default: '',
  157. type: String
  158. },
  159. plugins: {
  160. type: Array,
  161. default: () => {
  162. return [
  163. 'advlist autolink lists link image charmap print preview hr anchor pagebreak imagetools',
  164. 'searchreplace visualblocks visualchars code',
  165. 'insertdatetime media nonbreaking save table contextmenu directionality',
  166. 'emoticons paste textcolor colorpicker imagetools codesample'
  167. ];
  168. }
  169. },
  170. toolbar: {
  171. type: String,
  172. default: ' undo redo | styleselect | uploadImages | uploadFiles | bold italic underline forecolor backcolor | alignleft aligncenter alignright | bullist numlist outdent indent | link image emoticons media codesample | preview screenload',
  173. },
  174. other_options: {
  175. type: Object,
  176. default: () => {
  177. return {};
  178. }
  179. },
  180. readonly: {
  181. type: Boolean,
  182. default: false
  183. }
  184. },
  185. data() {
  186. return {
  187. content: '',
  188. editor: null,
  189. editorT: null,
  190. cTinyMce: null,
  191. checkerTimeout: null,
  192. isTyping: false,
  193. spinShow: true,
  194. transfer: false,
  195. uploadIng: 0,
  196. uploadFormat: ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'],
  197. actionUrl: $A.apiUrl('system/fileupload'),
  198. params: { token: $A.getToken() },
  199. maxSize: 204800
  200. };
  201. },
  202. mounted() {
  203. this.content = this.value;
  204. this.init();
  205. },
  206. activated() {
  207. this.content = this.value;
  208. this.init();
  209. },
  210. deactivated() {
  211. if (this.editor !== null) {
  212. this.editor.destroy();
  213. }
  214. this.spinShow = true;
  215. $A(this.$refs.myTextarea).show();
  216. },
  217. watch: {
  218. value(newValue) {
  219. if (newValue == null) {
  220. newValue = "";
  221. }
  222. if (!this.isTyping) {
  223. if (this.getEditor() !== null) {
  224. this.getEditor().setContent(newValue);
  225. } else{
  226. this.content = newValue;
  227. }
  228. }
  229. },
  230. readonly(value) {
  231. if (this.editor !== null) {
  232. if (value) {
  233. this.editor.setMode('readonly');
  234. } else {
  235. this.editor.setMode('design');
  236. }
  237. }
  238. }
  239. },
  240. methods: {
  241. init() {
  242. this.$nextTick(() => {
  243. tinymce.init(this.concatAssciativeArrays(this.options(false), this.other_options));
  244. });
  245. },
  246. initTransfer() {
  247. this.$nextTick(() => {
  248. tinymce.init(this.concatAssciativeArrays(this.options(true), this.other_options));
  249. });
  250. },
  251. options(isFull) {
  252. return {
  253. selector: (isFull ? '#T_' : '#') + this.id,
  254. base_url: $A.serverUrl('js/build'),
  255. language: "zh_CN",
  256. toolbar: this.toolbar,
  257. plugins: this.plugins,
  258. save_onsavecallback: (e) => {
  259. this.$emit('editorSave', e);
  260. },
  261. paste_data_images: true,
  262. menu: {
  263. view: {
  264. title: 'View',
  265. items: 'code | visualaid visualchars visualblocks | spellchecker | preview fullscreen screenload | showcomments'
  266. },
  267. insert: {
  268. title: "Insert",
  269. items: "image link media addcomment pageembed template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor toc | insertdatetime | uploadImages browseImages | uploadFiles"
  270. }
  271. },
  272. codesample_languages: [
  273. {text:"HTML/VUE/XML",value:"markup"},
  274. {text:"JavaScript",value:"javascript"},
  275. {text:"CSS",value:"css"},
  276. {text:"PHP",value:"php"},
  277. {text:"Ruby",value:"ruby"},
  278. {text:"Python",value:"python"},
  279. {text:"Java",value:"java"},
  280. {text:"C",value:"c"},
  281. {text:"C#",value:"csharp"},
  282. {text:"C++",value:"cpp"}
  283. ],
  284. height: isFull ? '100%' : ($A.rightExists(this.height, '%') ? this.height : ($A.runNum(this.height) || 360)),
  285. resize: !isFull,
  286. convert_urls:false,
  287. toolbar_mode: 'sliding',
  288. toolbar_drawer: 'floating',
  289. setup: (editor) => {
  290. editor.ui.registry.addMenuButton('uploadImages', {
  291. text: this.$L('图片'),
  292. tooltip: this.$L('上传/浏览 图片'),
  293. fetch: (callback) => {
  294. let items = [{
  295. type: 'menuitem',
  296. text: this.$L('上传图片'),
  297. onAction: () => {
  298. this.$refs.myUpload.handleClick();
  299. }
  300. }, {
  301. type: 'menuitem',
  302. text: this.$L('浏览图片'),
  303. onAction: () => {
  304. this.$refs.myUpload.browsePicture();
  305. }
  306. }];
  307. callback(items);
  308. }
  309. });
  310. editor.ui.registry.addMenuItem('uploadImages', {
  311. text: this.$L('上传图片'),
  312. onAction: () => {
  313. this.$refs.myUpload.handleClick();
  314. }
  315. });
  316. editor.ui.registry.addMenuItem('browseImages', {
  317. text: this.$L('浏览图片'),
  318. onAction: () => {
  319. this.$refs.myUpload.browsePicture();
  320. }
  321. });
  322. editor.ui.registry.addButton('uploadFiles', {
  323. text: this.$L('文件'),
  324. tooltip: this.$L('上传文件'),
  325. onAction: () => {
  326. if (this.handleBeforeUpload()) {
  327. this.$refs.fileUpload.handleClick();
  328. }
  329. }
  330. });
  331. editor.ui.registry.addMenuItem('uploadFiles', {
  332. text: this.$L('上传文件'),
  333. onAction: () => {
  334. if (this.handleBeforeUpload()) {
  335. this.$refs.fileUpload.handleClick();
  336. }
  337. }
  338. });
  339. if (isFull) {
  340. editor.ui.registry.addButton('screenload', {
  341. icon: 'fullscreen',
  342. tooltip: this.$L('退出全屏'),
  343. onAction: () => {
  344. this.closeFull();
  345. }
  346. });
  347. editor.ui.registry.addMenuItem('screenload', {
  348. text: this.$L('退出全屏'),
  349. onAction: () => {
  350. this.closeFull();
  351. }
  352. });
  353. editor.on('Init', (e) => {
  354. this.editorT = editor;
  355. this.editorT.setContent(this.content);
  356. if (this.readonly) {
  357. this.editorT.setMode('readonly');
  358. } else {
  359. this.editorT.setMode('design');
  360. }
  361. });
  362. }else{
  363. editor.ui.registry.addButton('screenload', {
  364. icon: 'fullscreen',
  365. tooltip: this.$L('全屏'),
  366. onAction: () => {
  367. this.content = editor.getContent();
  368. this.transfer = true;
  369. this.initTransfer();
  370. }
  371. });
  372. editor.ui.registry.addMenuItem('screenload', {
  373. text: this.$L('全屏'),
  374. onAction: () => {
  375. this.content = editor.getContent();
  376. this.transfer = true;
  377. this.initTransfer();
  378. }
  379. });
  380. editor.on('Init', (e) => {
  381. this.spinShow = false;
  382. this.editor = editor;
  383. this.editor.setContent(this.content);
  384. if (this.readonly) {
  385. this.editor.setMode('readonly');
  386. } else {
  387. this.editor.setMode('design');
  388. }
  389. this.$emit('editorInit', this.editor);
  390. });
  391. editor.on('KeyUp', (e) => {
  392. if (this.editor !== null) {
  393. this.submitNewContent();
  394. }
  395. });
  396. editor.on('Change', (e) => {
  397. if (this.editor !== null) {
  398. if (this.getContent() !== this.value) {
  399. this.submitNewContent();
  400. }
  401. this.$emit('editorChange', e);
  402. }
  403. });
  404. }
  405. },
  406. };
  407. },
  408. closeFull() {
  409. this.content = this.getContent();
  410. this.$emit('input', this.content);
  411. this.transfer = false;
  412. if (this.editorT != null) {
  413. this.editorT.destroy();
  414. this.editorT = null;
  415. }
  416. },
  417. transferChange(visible) {
  418. if (!visible && this.editorT != null) {
  419. this.content = this.editorT.getContent();
  420. this.$emit('input', this.content);
  421. this.editorT.destroy();
  422. this.editorT = null;
  423. }
  424. },
  425. getEditor() {
  426. return this.transfer ? this.editorT : this.editor;
  427. },
  428. concatAssciativeArrays(array1, array2) {
  429. if (array2.length === 0) return array1;
  430. if (array1.length === 0) return array2;
  431. let dest = [];
  432. for (let key in array1) {
  433. if (array1.hasOwnProperty(key)) {
  434. dest[key] = array1[key];
  435. }
  436. }
  437. for (let key in array2) {
  438. if (array2.hasOwnProperty(key)) {
  439. dest[key] = array2[key];
  440. }
  441. }
  442. return dest;
  443. },
  444. submitNewContent() {
  445. this.isTyping = true;
  446. if (this.checkerTimeout !== null) {
  447. clearTimeout(this.checkerTimeout);
  448. }
  449. this.checkerTimeout = setTimeout(() => {
  450. this.isTyping = false;
  451. }, 300);
  452. this.$emit('input', this.getContent());
  453. },
  454. insertContent(content) {
  455. if (this.getEditor() !== null) {
  456. this.getEditor().insertContent(content);
  457. }else{
  458. this.content+= content;
  459. }
  460. },
  461. getContent() {
  462. if (this.getEditor() === null) {
  463. return "";
  464. }
  465. return this.getEditor().getContent();
  466. },
  467. insertImage(src) {
  468. this.insertContent('<img src="' + src + '">');
  469. },
  470. editorImage(lists) {
  471. for (let i = 0; i < lists.length; i++) {
  472. let item = lists[i];
  473. if (typeof item === 'object' && typeof item.url === "string") {
  474. this.insertImage(item.url);
  475. }
  476. }
  477. },
  478. /********************文件上传部分************************/
  479. handleProgress() {
  480. //开始上传
  481. this.uploadIng++;
  482. },
  483. handleSuccess(res, file) {
  484. //上传完成
  485. this.uploadIng--;
  486. if (res.ret === 1) {
  487. this.insertContent(`<a href="${res.data.url}" target="_blank">${res.data.name} (${$A.bytesToSize(res.data.size * 1024)})</a>`);
  488. } else {
  489. this.$Modal.warning({
  490. title: this.$L('上传失败'),
  491. content: this.$L('文件 % 上传失败,%', file.name, res.msg)
  492. });
  493. }
  494. },
  495. handleError() {
  496. //上传错误
  497. this.uploadIng--;
  498. },
  499. handleFormatError(file) {
  500. //上传类型错误
  501. this.$Modal.warning({
  502. title: this.$L('文件格式不正确'),
  503. content: this.$L('文件 % 格式不正确,仅支持上传:%', file.name, this.uploadFormat.join(','))
  504. });
  505. },
  506. handleMaxSize(file) {
  507. //上传大小错误
  508. this.$Modal.warning({
  509. title: this.$L('超出文件大小限制'),
  510. content: this.$L('文件 % 太大,不能超过%。', file.name, $A.bytesToSize(this.maxSize * 1024))
  511. });
  512. },
  513. handleBeforeUpload() {
  514. //上传前判断
  515. this.params = {
  516. token: $A.getToken(),
  517. };
  518. return true;
  519. },
  520. }
  521. }
  522. </script>