index.vue 11 KB

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