index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <template>
  2. <div>
  3. <div class="editor" ref="editor" :style="styles" @change="onEditorChange($event)"></div>
  4. <!-- 图片上传组件辅助-->
  5. <el-upload v-show="false"
  6. ref="uploadImg"
  7. class="avatar-uploader"
  8. :action="imgUploadUrl"
  9. name="img"
  10. :headers="header"
  11. :data="imgData"
  12. :show-file-list="false"
  13. :on-success="uploadSuccess"
  14. :on-error="uploadError"
  15. :before-upload="beforeUpload">
  16. </el-upload>
  17. <el-upload v-show="false"
  18. ref="uploadVideo"
  19. class="video-uploader"
  20. :action="serverUrl"
  21. :headers="header"
  22. :data="videoData"
  23. :show-file-list="false"
  24. :before-upload="beforeVideoUpload"
  25. :on-success="uploadVideoSuccess"
  26. :on-error="uploadVideoError">
  27. </el-upload>
  28. </div>
  29. </template>
  30. <script>
  31. import Quill from "quill";
  32. import "quill/dist/quill.core.css";
  33. import "quill/dist/quill.snow.css";
  34. import "quill/dist/quill.bubble.css";
  35. import { uploadFile } from "@/api/common/common";
  36. import { getToken } from '@/utils/auth'
  37. export default {
  38. name: "Editor",
  39. props: {
  40. /* 编辑器的内容 */
  41. value: {
  42. type: String,
  43. default: "",
  44. },
  45. /* 高度 */
  46. height: {
  47. type: Number,
  48. default: null,
  49. },
  50. /* 最小高度 */
  51. minHeight: {
  52. type: Number,
  53. default: null,
  54. },
  55. //参数
  56. params: {
  57. type: Object,
  58. default: null,
  59. }
  60. },
  61. data() {
  62. return {
  63. Quill: null,
  64. currentValue: "",
  65. content: null,
  66. serverUrl: process.env.VUE_APP_BASE_API+'/common/upload',
  67. imgUploadUrl: process.env.VUE_APP_BASE_API+'/common/uploadImg',
  68. header:{
  69. 'Authorization': 'Bearer ' + getToken()
  70. },
  71. videoData:{
  72. modName: this.params.modName+'video'
  73. },
  74. imgData:{
  75. modName: this.params.modName+'img'
  76. },
  77. options: {
  78. theme: "snow",
  79. bounds: document.body,
  80. debug: "warn",
  81. modules: {
  82. // 工具栏配置
  83. toolbar: {
  84. container:[
  85. ['sourceEditor'],
  86. ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  87. ["blockquote", "code-block"], // 引用 代码块
  88. [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  89. [{ indent: "-1" }, { indent: "+1" }], // 缩进
  90. [{ size: ["small", false, "large", "huge"] }], // 字体大小
  91. [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  92. [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  93. [{ align: [] }], // 对齐方式
  94. ["clean"], // 清除文本格式
  95. ["link", "image", "video"], // 链接、图片、视频
  96. ],
  97. handlers: {
  98. shadeBox: null,
  99. source: false,
  100. // 添加工具方法
  101. sourceEditor: function () {
  102. this.source = true; //开启源码编辑
  103. console.log("开启源码编辑")
  104. // alert('我新添加的工具方法');
  105. const container = this.container;
  106. const firstChild = container.nextElementSibling.firstChild;
  107. // 在第一次点击源码编辑的时候,会在整个工具条上加一个div,层级比工具条高,再次点击工具条任意位置,就会退出源码编辑。可以在下面cssText里面加个背景颜色看看效果。
  108. if (!this.shadeBox) {
  109. let shadeBox = this.shadeBox = document.createElement('div');
  110. shadeBox.style.cssText = 'position:absolute; top:0; left:0; width:100%; height:100%; cursor:pointer';
  111. container.style.position = 'relative';
  112. container.appendChild(shadeBox);
  113. firstChild.innerText = firstChild.innerHTML;
  114. shadeBox.addEventListener('click', function () {
  115. this.source = false; //关闭源码编辑
  116. console.log("关闭源码编辑")
  117. this.style.display = 'none';
  118. firstChild.innerHTML = firstChild.innerText.trim();
  119. }, false);
  120. } else {
  121. this.shadeBox.style.display = 'block';
  122. firstChild.innerText = firstChild.innerHTML;
  123. }
  124. // this.$emit("isSource",this.source); //父组件触发自定义事件,是否开启源码编辑
  125. },
  126. 'image': function (value) {
  127. if (value) {
  128. // 触发input框选择图片文件
  129. document.querySelector('.avatar-uploader input').click()
  130. } else {
  131. this.quill.format('image', false);
  132. }
  133. },
  134. 'video': function (value) {
  135. if (value) {
  136. // 触发input框选择图片文件
  137. document.querySelector('.video-uploader input').click()
  138. } else {
  139. this.quill.format('video', false);
  140. }
  141. }
  142. }
  143. }
  144. },
  145. placeholder: "请输入内容",
  146. readOnly: false,
  147. initButton: function () {
  148. // 样式随便改
  149. const sourceEditorButton = document.querySelector('.ql-sourceEditor');
  150. sourceEditorButton.style.cssText = 'font-size:18px';
  151. // 加了elementui的icon
  152. sourceEditorButton.classList.add('el-icon-edit-outline');
  153. // 鼠标放上去显示的提示文字
  154. sourceEditorButton.title = '源码编辑';
  155. },
  156. register(q){
  157. //注册标签(因为在富文本编辑器中是没有div,table等标签的,需要自己去注册自己需要的标签)
  158. class div extends q.import('blots/block/embed') {}
  159. class table extends q.import('blots/block/embed') {}
  160. class tr extends q.import('blots/block/embed') {}
  161. class td extends q.import('blots/block/embed') {}
  162. div.blotName =div.tagName='div';
  163. table.blotName =table.tagName='table';
  164. tr.blotName =tr.tagName='tr';
  165. td.blotName =td.tagName='td';
  166. q.register(div);
  167. q.register(table);
  168. q.register(tr);
  169. q.register(td);
  170. },
  171. },
  172. };
  173. },
  174. computed: {
  175. styles() {
  176. let style = {};
  177. if (this.minHeight) {
  178. style.minHeight = `${this.minHeight}px`;
  179. }
  180. if (this.height) {
  181. style.height = `${this.height}px`;
  182. }
  183. return style;
  184. },
  185. },
  186. watch: {
  187. value: {
  188. handler(val) {
  189. if (val !== this.currentValue) {
  190. this.currentValue = val === null ? "" : val;
  191. if (this.Quill) {
  192. this.Quill.pasteHTML(this.value);
  193. }
  194. }
  195. },
  196. immediate: true,
  197. },
  198. },
  199. mounted() {
  200. this.init();
  201. this.options.register(Quill);
  202. this.options.initButton();
  203. },
  204. beforeDestroy() {
  205. this.Quill = null;
  206. },
  207. methods: {
  208. init() {
  209. const editor = this.$refs.editor;
  210. this.Quill = new Quill(editor, this.options);
  211. this.Quill.pasteHTML(this.currentValue);
  212. this.Quill.on("text-change", (delta, oldDelta, source) => {
  213. const html = this.$refs.editor.children[0].innerHTML;
  214. const text = this.Quill.getText();
  215. const quill = this.Quill;
  216. this.currentValue = html;
  217. this.$emit("input", html);
  218. this.$emit("on-change", { html, text, quill });
  219. });
  220. this.Quill.on("text-change", (delta, oldDelta, source) => {
  221. this.$emit("on-text-change", delta, oldDelta, source);
  222. });
  223. this.Quill.on("selection-change", (range, oldRange, source) => {
  224. this.$emit("on-selection-change", range, oldRange, source);
  225. });
  226. this.Quill.on("editor-change", (eventName, ...args) => {
  227. this.$emit("on-editor-change", eventName, ...args);
  228. });
  229. },
  230. onEditorChange({editor, html, text}) {//内容改变事件
  231. console.log("---内容改变事件---");
  232. this.content = html
  233. // console.log(html)
  234. },
  235. // 富文本图片上传前
  236. beforeUpload(file) {
  237. // 显示loading动画
  238. const index = ['image/jpg','image/jpeg','image/gif','image/png'].findIndex(y => Object.is(file.type, y));
  239. const isLt2M = file.size / 1024 / 1024 < 2;
  240. var isJPG = true;
  241. if (index < 0) {
  242. isJPG = false;
  243. this.$message.error('上传头像图片只能是 jpg/gif/png 格式!');
  244. }
  245. if (!isLt2M) {
  246. this.$message.error('上传头像图片大小不能超过 2MB!');
  247. }
  248. return isJPG && isLt2M;
  249. },
  250. uploadSuccess(res, file) {
  251. // res为图片服务器返回的数据
  252. // 获取富文本组件实例
  253. console.log(res);
  254. let quill = this.Quill;
  255. // 如果上传成功
  256. if (res.code == 200 ) {
  257. // 获取光标所在位置
  258. let length = quill.getSelection().index;
  259. // 插入图片 res.url为服务器返回的图片地址
  260. var fileName = res.fileName.replace('//', '/')
  261. quill.insertEmbed(length, 'image',process.env.VUE_APP_BASE_API+ fileName);
  262. // 调整光标到最后
  263. quill.setSelection(length + 1)
  264. } else {
  265. this.$message.error('图片插入失败')
  266. }
  267. },
  268. uploadFile(){
  269. return process.env.VUE_APP_BASE_API+'/common/uploadImg'
  270. },
  271. // 富文本图片上传失败
  272. uploadError() {
  273. this.$message.error('图片插入失败')
  274. },
  275. //--------视频上传-----------
  276. beforeVideoUpload(file){
  277. const size = file.size / 1024 / 1024 < 20;
  278. if (['video/MP4','video/mp4', 'video/ogg', 'video/webm', 'video/WebM'].indexOf(file.type) == -1) {
  279. this.$message.error('视频仅支持 MP4,WebM,Ogg 格式');
  280. return false;
  281. }
  282. if (!size) {
  283. this.$message.error('上传视频大小不能超过 20MB');
  284. return false;
  285. }
  286. },
  287. uploadVideoSuccess(res, file){
  288. // res为服务器返回的数据
  289. // 获取富文本组件实例
  290. console.log(res);
  291. let quill = this.Quill;
  292. // 如果上传成功
  293. if (res.code == 200 ) {
  294. // 获取光标所在位置
  295. let length = quill.getSelection().index;
  296. // 插入图片 res.url为服务器返回的图片地址
  297. var fileName = res.fileName.replace('//', '/')
  298. quill.insertEmbed(length, 'video',process.env.VUE_APP_BASE_API+ fileName);
  299. // 调整光标到最后
  300. quill.setSelection(length + 1)
  301. } else {
  302. this.$message.error('视频插入失败')
  303. }
  304. },
  305. uploadVideoError(){
  306. this.$message.error('视频插入失败')
  307. }
  308. },
  309. };
  310. </script>
  311. <style>
  312. .editor {
  313. white-space: pre-wrap!important;
  314. line-height: normal !important;
  315. }
  316. .quill-img {
  317. display: none;
  318. }
  319. .ql-snow .ql-tooltip[data-mode="link"]::before {
  320. content: "请输入链接地址:";
  321. }
  322. .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  323. border-right: 0px;
  324. content: "保存";
  325. padding-right: 0px;
  326. }
  327. .quill-video {
  328. display: none;
  329. }
  330. /* .ql-snow .ql-tooltip[data-mode="video"]::before {
  331. content: "请输入视频地址:";
  332. } */
  333. .ql-snow .ql-picker.ql-size .ql-picker-label::before,
  334. .ql-snow .ql-picker.ql-size .ql-picker-item::before {
  335. content: "14px";
  336. }
  337. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
  338. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
  339. content: "10px";
  340. }
  341. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
  342. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
  343. content: "18px";
  344. }
  345. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
  346. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
  347. content: "32px";
  348. }
  349. .ql-snow .ql-picker.ql-header .ql-picker-label::before,
  350. .ql-snow .ql-picker.ql-header .ql-picker-item::before {
  351. content: "文本";
  352. }
  353. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
  354. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  355. content: "标题1";
  356. }
  357. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
  358. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  359. content: "标题2";
  360. }
  361. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
  362. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  363. content: "标题3";
  364. }
  365. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
  366. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  367. content: "标题4";
  368. }
  369. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
  370. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  371. content: "标题5";
  372. }
  373. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
  374. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  375. content: "标题6";
  376. }
  377. .ql-snow .ql-picker.ql-font .ql-picker-label::before,
  378. .ql-snow .ql-picker.ql-font .ql-picker-item::before {
  379. content: "标准字体";
  380. }
  381. .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
  382. .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
  383. content: "衬线字体";
  384. }
  385. .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
  386. .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
  387. content: "等宽字体";
  388. }
  389. /* 编辑器内部出现滚动条 */
  390. .ql-container{
  391. overflow-y:auto;
  392. height:8rem!important;
  393. }
  394. /*滚动条整体样式*/
  395. .ql-container ::-webkit-scrollbar{
  396. width: 10px;/*竖向滚动条的宽度*/
  397. height: 10px;/*横向滚动条的高度*/
  398. }
  399. .ql-container ::-webkit-scrollbar-thumb{/*滚动条里面的小方块*/
  400. background: #666666;
  401. border-radius: 5px;
  402. }
  403. .ql-container ::-webkit-scrollbar-track{/*滚动条轨道的样式*/
  404. background: #ccc;
  405. border-radius: 5px;
  406. }
  407. </style>