indexCopy.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. <template>
  2. <div>
  3. <el-upload
  4. :action="uploadUrl"
  5. :on-error="handleUploadError"
  6. name="file"
  7. :show-file-list="false"
  8. :headers="headers"
  9. style="display: none"
  10. ref="upload"
  11. :http-request="imageChange"
  12. >
  13. </el-upload>
  14. <div class="editor" ref="editor" :style="styles"></div>
  15. </div>
  16. </template>
  17. <script>
  18. import Quill from "quill";
  19. import "quill/dist/quill.core.css";
  20. import "quill/dist/quill.snow.css";
  21. import "quill/dist/quill.bubble.css";
  22. // import { getToken } from "@/utils/auth";
  23. import ImageResize from "quill-image-resize-module";
  24. import { BASE_URL } from "@/utils/request.js";
  25. export default {
  26. name: "Editor",
  27. props: {
  28. /* 编辑器的内容 */
  29. value: {
  30. type: String,
  31. default: "",
  32. },
  33. /* 高度 */
  34. height: {
  35. type: Number,
  36. default: null,
  37. },
  38. /* 最小高度 */
  39. minHeight: {
  40. type: Number,
  41. default: null,
  42. },
  43. /* 最大高度 */
  44. maxHeight: {
  45. type: Number,
  46. default: null,
  47. },
  48. /* 只读 */
  49. readOnly: {
  50. type: Boolean,
  51. default: false,
  52. },
  53. },
  54. data() {
  55. return {
  56. uploadUrl: BASE_URL,
  57. headers: {
  58. token: this.$store.state.backstageToken,
  59. },
  60. Quill: null,
  61. currentValue: "",
  62. options: {
  63. theme: "snow",
  64. bounds: document.body,
  65. debug: "warn",
  66. modules: {
  67. clipboard: {
  68. // 粘贴版,处理粘贴时候带图片
  69. matchers: [[Node.ELEMENT_NODE, this.handleCustomMatcher]],
  70. },
  71. // 工具栏配置
  72. toolbar: [
  73. ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  74. ["blockquote", "code-block"], // 引用 代码块
  75. [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  76. [{ indent: "-1" }, { indent: "+1" }], // 缩进
  77. [{ size: ["small", false, "large", "huge"] }], // 字体大小
  78. [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  79. [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  80. [{ align: [] }], // 对齐方式
  81. ["clean"], // 清除文本格式
  82. ["link", "image"], // 链接、图片
  83. ],
  84. imageResize: {
  85. displayStyles: {
  86. backgroundColor: "black",
  87. border: "none",
  88. color: "white",
  89. },
  90. modules: ["Resize", "DisplaySize", "Toolbar"],
  91. },
  92. },
  93. placeholder: "请输入内容",
  94. readOnly: this.readOnly,
  95. },
  96. };
  97. },
  98. computed: {
  99. styles() {
  100. let style = {};
  101. if (this.minHeight) {
  102. style.minHeight = `${this.minHeight}px`;
  103. }
  104. if (this.maxHeight) {
  105. style.maxHeight = `${this.maxHeight}px`;
  106. }
  107. if (this.height) {
  108. style.height = `${this.height}px`;
  109. }
  110. return style;
  111. },
  112. },
  113. watch: {
  114. value: {
  115. handler(val) {
  116. if (val !== this.currentValue) {
  117. this.currentValue = val === null ? "" : val;
  118. if (this.Quill) {
  119. this.Quill.pasteHTML(this.currentValue);
  120. }
  121. }
  122. },
  123. immediate: true,
  124. },
  125. },
  126. mounted() {
  127. Quill.register("modules/imageResize", ImageResize);
  128. this.init();
  129. },
  130. beforeDestroy() {
  131. this.Quill = null;
  132. },
  133. methods: {
  134. handleCustomMatcher(node, Delta) {
  135. let ops = [];
  136. Delta.ops.forEach((op) => {
  137. if (op.insert && typeof op.insert === "string") {
  138. // 如果粘贴了图片,这里会是一个对象,所以可以这样处理
  139. ops.push({
  140. insert: op.insert,
  141. });
  142. } else {
  143. if (op.insert.image.includes("data:image")) {
  144. /**
  145. * 粘贴图片
  146. */
  147. let arr = op.insert.image.split(",");
  148. let mime = arr[0].match(/:(.*?);/)[1];
  149. let bytes = atob(arr[1]);
  150. let n = bytes.length;
  151. let ia = new Uint8Array(n);
  152. while (n--) {
  153. ia[n] = bytes.charCodeAt(n);
  154. }
  155. let arry = new File([ia], "随机名称", { type: mime });
  156. this.imageChange({
  157. file: arry,
  158. });
  159. } else {
  160. ops.push({
  161. insert: op.insert,
  162. });
  163. }
  164. }
  165. });
  166. Delta.ops = ops;
  167. return Delta;
  168. },
  169. base64ToFile(base64, fileName) {
  170. return new Promise((resolve, reject) => {
  171. let arr = base64.split(",");
  172. let mime = arr[0].match(/:(.*?);/)[1];
  173. let bytes = atob(arr[1]);
  174. let n = bytes.length;
  175. let ia = new Uint8Array(n);
  176. while (n--) {
  177. ia[n] = bytes.charCodeAt(n);
  178. }
  179. let arry = new File([ia], fileName, { type: mime });
  180. this.$api.XfSysBussinessUploadFile(arry, 2).then((res) => {
  181. resolve(this.$methods.splitImgHost(res.Data));
  182. });
  183. });
  184. },
  185. init() {
  186. const editor = this.$refs.editor;
  187. this.Quill = new Quill(editor, this.options);
  188. // 如果设置了上传地址则自定义图片上传事件
  189. if (this.uploadUrl) {
  190. let toolbar = this.Quill.getModule("toolbar");
  191. toolbar.addHandler("image", (value) => {
  192. this.uploadType = "image";
  193. if (value) {
  194. // document.querySelector(".avatar-uploader input").click();
  195. this.$refs.upload.$children[0].$refs.input.click();
  196. } else {
  197. this.quill.format("image", false);
  198. }
  199. });
  200. toolbar.addHandler("video", (value) => {
  201. this.uploadType = "video";
  202. if (value) {
  203. this.$refs.upload.$children[0].$refs.input.click();
  204. } else {
  205. this.quill.format("video", false);
  206. }
  207. });
  208. }
  209. this.Quill.enable(false);
  210. this.Quill.pasteHTML(this.currentValue);
  211. // this.$nextTick(function() {
  212. // this.Quill.blur();
  213. // this.Quill.enable(true);
  214. // });
  215. this.Quill.on("text-change", (delta, oldDelta, source) => {
  216. const html = this.$refs.editor.children[0].innerHTML;
  217. const text = this.Quill.getText();
  218. const quill = this.Quill;
  219. this.currentValue = html;
  220. this.$emit("input", html);
  221. this.$emit("on-change", { html, text, quill });
  222. this.$emit("on-text-change", delta, oldDelta, source);
  223. });
  224. this.Quill.on("selection-change", (range, oldRange, source) => {
  225. this.$emit("on-selection-change", range, oldRange, source);
  226. });
  227. this.Quill.on("editor-change", (eventName, ...args) => {
  228. this.$emit("on-editor-change", eventName, ...args);
  229. });
  230. editor.onclick = () => {
  231. this.Quill.enable(true);
  232. this.Quill.focus();
  233. };
  234. },
  235. imageChange(param, type) {
  236. let formData = new FormData();
  237. formData.append("file", param.file);
  238. this.$api
  239. .XfSysBussinessUploadFile(formData)
  240. .then((res) => {
  241. let quill = this.Quill;
  242. // 获取光标所在位置
  243. let length = quill.getSelection().index;
  244. // 插入图片 res.url为服务器返回的图片地址
  245. quill.insertEmbed(
  246. length,
  247. "image",
  248. this.$methods.splitImgHost(res.Data)
  249. );
  250. // 调整光标到最后
  251. quill.setSelection(length + 1);
  252. })
  253. .catch((err) => {
  254. console.log(err);
  255. });
  256. },
  257. handleUploadError() {
  258. this.$message.error("图片插入失败");
  259. },
  260. },
  261. };
  262. </script>
  263. <style>
  264. .editor,
  265. .ql-toolbar {
  266. white-space: pre-wrap !important;
  267. line-height: normal !important;
  268. }
  269. .ql-toolbar.ql-snow {
  270. border-top-left-radius: 8px;
  271. border-top-right-radius: 8px;
  272. }
  273. .ql-container {
  274. border-bottom-left-radius: 8px;
  275. border-bottom-right-radius: 8px;
  276. overflow: auto;
  277. }
  278. .quill-img {
  279. display: none;
  280. }
  281. .ql-snow .ql-tooltip[data-mode="link"]::before {
  282. content: "请输入链接地址:";
  283. }
  284. .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  285. border-right: 0px;
  286. content: "保存";
  287. padding-right: 0px;
  288. }
  289. .ql-snow .ql-tooltip[data-mode="video"]::before {
  290. content: "请输入视频地址:";
  291. }
  292. .ql-snow .ql-picker.ql-size .ql-picker-label::before,
  293. .ql-snow .ql-picker.ql-size .ql-picker-item::before {
  294. content: "14px";
  295. }
  296. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
  297. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
  298. content: "10px";
  299. }
  300. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
  301. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
  302. content: "18px";
  303. }
  304. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
  305. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
  306. content: "32px";
  307. }
  308. .ql-snow .ql-picker.ql-header .ql-picker-label::before,
  309. .ql-snow .ql-picker.ql-header .ql-picker-item::before {
  310. content: "文本";
  311. }
  312. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
  313. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  314. content: "标题1";
  315. }
  316. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
  317. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  318. content: "标题2";
  319. }
  320. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
  321. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  322. content: "标题3";
  323. }
  324. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
  325. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  326. content: "标题4";
  327. }
  328. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
  329. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  330. content: "标题5";
  331. }
  332. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
  333. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  334. content: "标题6";
  335. }
  336. .ql-snow .ql-picker.ql-font .ql-picker-label::before,
  337. .ql-snow .ql-picker.ql-font .ql-picker-item::before {
  338. content: "标准字体";
  339. }
  340. .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
  341. .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
  342. content: "衬线字体";
  343. }
  344. .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
  345. .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
  346. content: "等宽字体";
  347. }
  348. .ql-toolbar {
  349. background-color: #eee !important;
  350. }
  351. </style>