editor.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <template>
  2. <div class="my-editor">
  3. <div
  4. class="quill-editor"
  5. :content="content"
  6. :placeholder="placeholder || ''"
  7. @change="handleChange"
  8. v-quill:[quillName]="editorOption"
  9. ></div>
  10. <input type="file" accept="image/png, image/jpeg" :id="'imgInput'+quillName" @change="handleContentFileChange($event)"
  11. style="display: none;" />
  12. <el-dialog title="提示" :visible.sync="linkDialog" :before-close="handleLinkClose">
  13. <el-form :model="link">
  14. <el-form-item label="标题" :label-width="formLabelWidth">
  15. <el-input v-model="link.title" autocomplete="off"></el-input>
  16. </el-form-item>
  17. <el-form-item label="链接" :label-width="formLabelWidth">
  18. <el-input v-model="link.url" autocomplete="off"></el-input>
  19. </el-form-item>
  20. </el-form>
  21. <span slot="footer" class="dialog-footer">
  22. <el-button @click="handleLinkClose">取 消</el-button>
  23. <el-button type="primary" @click="handleLinkOk">确 定</el-button>
  24. </span>
  25. </el-dialog>
  26. </div>
  27. </template>
  28. <script>
  29. import Vue from "vue";
  30. import "quill/dist/quill.core.css";
  31. import "quill/dist/quill.snow.css";
  32. // import 'quill/dist/quill.bubble.css'
  33. let handerLink = null
  34. if (process.browser) {
  35. const VueQuillEditor = require("vue-quill-editor/dist/ssr");
  36. Vue.use(VueQuillEditor);
  37. handerLink = VueQuillEditor.Quill.import("formats/link")
  38. }
  39. import hljs from "hljs";
  40. export default {
  41. props: ["content", "hideImage", "placeholder", "haveVideo"],
  42. components: {
  43. },
  44. data() {
  45. const extra = ["link"];
  46. if (!this.hideImage) {
  47. extra.push("image");
  48. }
  49. // if (this.haveVideo) {
  50. // extra.push("video");
  51. // }
  52. let placeholder = this.placeholder
  53. return {
  54. editorOption: {
  55. name: Math.random(),
  56. theme: "snow",
  57. placeholder: placeholder || "请输入正文...",
  58. modules: {
  59. toolbar: [
  60. ["bold", "italic", "underline", "strike"],
  61. ["blockquote"],
  62. [{ list: "ordered" }, { list: "bullet" }],
  63. [{ indent: "-1" }, { indent: "+1" }],
  64. [{ size: [false, "small", "large", "huge"] }],
  65. [{ header: [false, 1, 2, 3, 4, 5, 6] }],
  66. [{ font: [] }],
  67. [{ color: [] }, { background: [] }],
  68. [{ align: [] }],
  69. extra
  70. ],
  71. syntax: {
  72. highlight: text => hljs.highlightAuto(text).value
  73. }
  74. }
  75. },
  76. formLabelWidth: "60px",
  77. linkDialog: false,
  78. link: {
  79. title: "",
  80. url: ""
  81. },
  82. quillName: 'myQuillEditor' + Math.floor(Math.random() * 1000000000)
  83. };
  84. },
  85. computed: {},
  86. mounted() {
  87. console.log('this.quillName', this.quillName)
  88. // 为图片ICON绑定事件 getModule 为编辑器的内部属性
  89. this[this.quillName]
  90. .getModule("toolbar").addHandler("image", this.imgHandler);
  91. this.changeParseEvent()
  92. this.addLinkEvent()
  93. },
  94. methods: {
  95. handleChange(e) {
  96. if (typeof e.html === "undefined") {
  97. return;
  98. }
  99. this.$emit("change", e.html);
  100. this.$emit("changeText", e.text);
  101. },
  102. // 点击图片ICON触发事件
  103. imgHandler(state) {
  104. this.addRange = this[this.quillName].getSelection();
  105. if (state) {
  106. let fileInput = document.getElementById("imgInput"+this.quillName);
  107. fileInput.click(); // 加一个触发事件
  108. }
  109. this.uploadType = "image";
  110. },
  111. //change parse
  112. changeParseEvent() {
  113. // 自定义粘贴图片功能
  114. this[this.quillName].root.addEventListener('paste', evt => {
  115. console.log("evt", evt)
  116. if (evt.clipboardData && evt.clipboardData.files && evt.clipboardData.files.length) {
  117. evt.preventDefault();
  118. [].forEach.call(evt.clipboardData.files, file => {
  119. if (!file.type.match(/^image\/(gif|jpe?g|a?png|bmp)/i)) {
  120. return;
  121. }
  122. this.handleContentFileChange({target: {files:[file]}})
  123. });
  124. }
  125. }, false);
  126. },
  127. addLinkEvent() {
  128. let dom = document.getElementsByClassName("ql-link")
  129. console.log("dom", dom)
  130. if (dom.length === 0) {
  131. setTimeout(()=>{
  132. this.addLinkEvent()
  133. }, 2000)
  134. } else {
  135. dom[0].addEventListener("click", this.linkHandler.bind(this), false)
  136. }
  137. },
  138. // 点击link触发事件
  139. linkHandler() {
  140. const selectedRange = this[this.quillName].getSelection();
  141. if (selectedRange && !selectedRange.length ) {
  142. this.$message.warning("请先选择需要插入超链接的内容")
  143. }
  144. },
  145. handleLinkClose() {
  146. this.linkDialog = false;
  147. this.link = {
  148. title: "",
  149. url: ""
  150. };
  151. },
  152. handleLinkOk() {
  153. const link = this.link;
  154. this[this.quillName].deleteText(link.index, link.length);
  155. this[this.quillName].insertEmbed(link.index, "link", {
  156. href: link.url,
  157. innerText: link.title
  158. });
  159. // this[this.quillName].insertText(
  160. // link.index,
  161. // `<a href="${this.link.url}">${this.link.title}</a>`
  162. // );
  163. this.handleLinkClose();
  164. },
  165. // 点击视频ICON触发事件
  166. videoHandler(state) {
  167. this.addRange = this[this.quillName].getSelection();
  168. if (state) {
  169. let fileInput = document.getElementById("imgInput"+this.quillName);
  170. fileInput.click(); // 加一个触发事件
  171. }
  172. // this.uploadType = "video";
  173. },
  174. handleContentFileChange(e) {
  175. const file = e.target.files[0];
  176. if (file.size / 1024/1024 > 2) {
  177. this.$message.error("图片大小不得超过2M,请重新选择");
  178. return false;
  179. }
  180. const formData = new FormData();
  181. formData.append("file", file);
  182. formData.append("original_filename", file.name);
  183. this.$axios
  184. .$post(`/upload_image`, formData, {
  185. headers: { "Content-Type": "multipart/form-data" }
  186. })
  187. .then(res => {
  188. const index = this[this.quillName].selection.savedRange.index;
  189. this[this.quillName].insertEmbed(index, "image", res.filename);
  190. });
  191. }
  192. }
  193. };
  194. </script>
  195. <style>
  196. /* editor: custom style & custom content */
  197. /* font-size */
  198. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
  199. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
  200. content: "S" !important;
  201. }
  202. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
  203. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
  204. content: "L" !important;
  205. }
  206. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
  207. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
  208. content: "H" !important;
  209. }
  210. /* header */
  211. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
  212. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  213. content: "H1" !important;
  214. }
  215. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
  216. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  217. content: "H2" !important;
  218. }
  219. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
  220. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  221. content: "H3" !important;
  222. }
  223. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
  224. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  225. content: "H4" !important;
  226. }
  227. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
  228. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  229. content: "H5" !important;
  230. }
  231. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
  232. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  233. content: "H6" !important;
  234. }
  235. </style>
  236. <style lang="scss">
  237. .my-editor {
  238. position: relative;
  239. width: 100%;
  240. height: 100%;
  241. min-height: 244px;
  242. background: #fff;
  243. .ql-toolbar {
  244. border-width: 0 !important;
  245. }
  246. .ql-editor,
  247. .quill-editor {
  248. min-height: 244px;
  249. border: 0 !important;
  250. font-size: 14px;
  251. line-height: 25px;
  252. }
  253. .ql-snow.ql-toolbar::after {
  254. display: inline-block;
  255. }
  256. }
  257. strong {
  258. font-weight: bold !important;
  259. }
  260. em {
  261. font-style: italic !important;
  262. }
  263. </style>