综合办公系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.vue 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. <template>
  2. <div>
  3. <el-upload
  4. :action="uploadUrl"
  5. :before-upload="handleBeforeUpload"
  6. :on-success="handleUploadSuccess"
  7. :on-error="handleUploadError"
  8. name="file"
  9. :show-file-list="false"
  10. :headers="headers"
  11. style="display: none"
  12. ref="upload"
  13. v-if="this.type == 'url'"
  14. >
  15. </el-upload>
  16. <el-upload
  17. :action="uploadUrl"
  18. :before-upload="handleBeforeUploadVideo"
  19. :on-success="handleUploadSuccessVideo"
  20. :on-error="handleUploadErrorVideo"
  21. name="file"
  22. :show-file-list="false"
  23. :headers="headers"
  24. style="display: none"
  25. ref="uploadVideo"
  26. v-if="this.type == 'url'"
  27. >
  28. </el-upload>
  29. <el-upload
  30. :action="uploadUrl"
  31. :before-upload="handleBeforeUploadFile"
  32. :on-success="handleUploadSuccessFile"
  33. :on-error="handleUploadErrorFile"
  34. name="file"
  35. :show-file-list="false"
  36. :headers="headers"
  37. style="display: none"
  38. ref="uploadFile"
  39. v-if="this.type == 'url'"
  40. >
  41. </el-upload>
  42. <div class="editor" ref="editor" :style="styles"></div>
  43. </div>
  44. </template>
  45. <script>
  46. import Quill from "quill";
  47. import "quill/dist/quill.core.css";
  48. import "quill/dist/quill.snow.css";
  49. import "quill/dist/quill.bubble.css";
  50. import { getToken } from "@/utils/auth";
  51. // 源码中是import直接倒入,这里要用Quill.import引入
  52. const Link = Quill.import("formats/link");
  53. // 自定义a链接
  54. class FileBlot extends Link {
  55. // 继承Link Blot
  56. static create (value) {
  57. let node = undefined;
  58. if (value && !value.href) {
  59. // 适应原本的Link Blot
  60. node = super.create(value)
  61. } else {
  62. // 自定义Link Blot
  63. node = super.create(value.href)
  64. node.href = value.href
  65. node.innerText = value.innerText
  66. // node.setAttribute('download', value.innerText); // 左键点击即下载
  67. }
  68. return node;
  69. }
  70. }
  71. FileBlot.blotName = "link" // 这里不用改,如果需要也可以保留原来的,这里用个新的blot
  72. FileBlot.tagName = "A"
  73. Quill.register(FileBlot) // 注册link
  74. export default {
  75. name: "Editor",
  76. props: {
  77. /* 编辑器的内容 */
  78. value: {
  79. type: String,
  80. default: "",
  81. },
  82. /* 高度 */
  83. height: {
  84. type: Number,
  85. default: null,
  86. },
  87. /* 最小高度 */
  88. minHeight: {
  89. type: Number,
  90. default: null,
  91. },
  92. /* 只读 */
  93. readOnly: {
  94. type: Boolean,
  95. default: false,
  96. },
  97. /* 上传文件大小限制(MB) */
  98. fileSize: {
  99. type: Number,
  100. default: 50,
  101. },
  102. /* 上传图片大小限制(MB) */
  103. imageSize: {
  104. type: Number,
  105. default: 5,
  106. },
  107. videoSize: {
  108. type: Number,
  109. default: 500,
  110. },
  111. /* 类型(base64格式、url格式) */
  112. type: {
  113. type: String,
  114. default: "url",
  115. }
  116. },
  117. data() {
  118. return {
  119. uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
  120. headers: {
  121. Authorization: "Bearer " + getToken()
  122. },
  123. Quill: null,
  124. currentValue: "",
  125. options: {
  126. theme: "snow",
  127. bounds: document.body,
  128. debug: "warn",
  129. modules: {
  130. // 工具栏配置
  131. toolbar: [
  132. ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  133. ["blockquote", "code-block"], // 引用 代码块
  134. [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  135. [{ indent: "-1" }, { indent: "+1" }], // 缩进
  136. [{ size: ["20px","14px","16px", "large", "huge"] }], // 字体大小
  137. [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  138. [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  139. [{ align: [] }], // 对齐方式
  140. ["clean"], // 清除文本格式
  141. ["link", "image", "video"] // 链接、图片、视频
  142. ],
  143. },
  144. placeholder: "请输入内容",
  145. readOnly: this.readOnly,
  146. },
  147. };
  148. },
  149. computed: {
  150. styles() {
  151. let style = {};
  152. if (this.minHeight) {
  153. style.minHeight = `${this.minHeight}px`;
  154. }
  155. if (this.height) {
  156. style.height = `${this.height}px`;
  157. }
  158. return style;
  159. },
  160. },
  161. watch: {
  162. value: {
  163. handler(val) {
  164. if (val !== this.currentValue) {
  165. this.currentValue = val === null ? "" : val;
  166. if (this.Quill) {
  167. this.Quill.pasteHTML(this.currentValue);
  168. }
  169. }
  170. },
  171. immediate: true,
  172. },
  173. },
  174. mounted() {
  175. this.init();
  176. },
  177. beforeDestroy() {
  178. this.Quill = null;
  179. },
  180. methods: {
  181. init() {
  182. debugger
  183. const editor = this.$refs.editor;
  184. this.Quill = new Quill(editor, this.options);
  185. var Size = Quill.import("formats/size");
  186. Size.whitelist = ["14px","16px", "large","20px", "huge"];
  187. // 如果设置了上传地址则自定义图片上传事件
  188. if (this.type == 'url') {
  189. let toolbar = this.Quill.getModule("toolbar");
  190. toolbar.addHandler("image", (value) => {
  191. if (value) {
  192. this.$refs.upload.$children[0].$refs.input.click();
  193. } else {
  194. this.quill.format("image", false);
  195. }
  196. });
  197. toolbar.addHandler("video", (value) => {
  198. if (value) {
  199. this.$refs.uploadVideo.$children[0].$refs.input.click();
  200. } else {
  201. this.quill.format("video", false);
  202. }
  203. });
  204. toolbar.addHandler("link", (value) => {
  205. if (value) {
  206. this.$refs.uploadFile.$children[0].$refs.input.click();
  207. } else {
  208. this.quill.format("link", false);
  209. }
  210. });
  211. }
  212. this.Quill.pasteHTML(this.currentValue);
  213. this.Quill.on("text-change", (delta, oldDelta, source) => {
  214. const html = this.$refs.editor.children[0].innerHTML;
  215. const text = this.Quill.getText();
  216. const quill = this.Quill;
  217. this.currentValue = html;
  218. this.$emit("input", html);
  219. this.$emit("on-change", { html, text, quill });
  220. });
  221. this.Quill.on("text-change", (delta, oldDelta, source) => {
  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. },
  231. // 上传前校检格式和大小
  232. handleBeforeUpload(file) {
  233. const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
  234. const isJPG = type.includes(file.type);
  235. // 检验文件格式
  236. if (!isJPG) {
  237. this.$message.error(`图片格式错误!`);
  238. return false;
  239. }
  240. // 校检文件大小
  241. if (this.imageSize) {
  242. const isLt = file.size / 1024 / 1024 < this.imageSize;
  243. if (!isLt) {
  244. this.$message.error(`上传文件大小不能超过 ${this.imageSize} MB!`);
  245. return false;
  246. }
  247. }
  248. return true;
  249. },
  250. handleBeforeUploadVideo(file) {
  251. const type = ["video/mp4"];
  252. const isVideo = type.includes(file.type);
  253. // 检验文件格式
  254. if (!isVideo) {
  255. this.$message.error(`视频格式错误!`);
  256. return false;
  257. }
  258. // 校检文件大小
  259. if (this.videoSize) {
  260. const isLt = file.size / 1024 / 1024 < this.videoSize;
  261. if (!isLt) {
  262. this.$message.error(`上传文件大小不能超过 ${this.videoSize} MB!`);
  263. return false;
  264. }
  265. }
  266. return true;
  267. },
  268. // 上传前校检格式和大小
  269. handleBeforeUploadFile(file) {
  270. // 校检文件大小
  271. if (this.fileSize) {
  272. const isLt = file.size / 1024 / 1024 < this.fileSize;
  273. if (!isLt) {
  274. this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
  275. return false;
  276. }
  277. }
  278. return true;
  279. },
  280. handleUploadSuccess(res, file) {
  281. debugger
  282. // 如果上传成功
  283. if (res.code == 200) {
  284. // 获取富文本组件实例
  285. let quill = this.Quill;
  286. // 获取光标所在位置
  287. let length = quill.getSelection().index;
  288. // 插入图片 res.url为服务器返回的图片地址
  289. quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName);
  290. // 调整光标到最后
  291. quill.setSelection(length + 1);
  292. } else {
  293. this.$message.error("图片插入失败");
  294. }
  295. },
  296. handleUploadSuccessVideo(res, file) {
  297. // 如果上传成功
  298. if (res.code == 200) {
  299. // 获取富文本组件实例
  300. let quill = this.Quill;
  301. // 获取光标所在位置
  302. let length = quill.getSelection().index;
  303. // 插入图片 res.url为服务器返回的图片地址
  304. quill.insertEmbed(length, "video", process.env.VUE_APP_BASE_API + res.fileName);
  305. // 调整光标到最后
  306. quill.setSelection(length + 1);
  307. } else {
  308. this.$message.error("视频插入失败");
  309. }
  310. },
  311. handleUploadSuccessFile(res, file) {
  312. // 如果上传成功
  313. if (res.code == 200) {
  314. // 获取富文本组件实例
  315. let quill = this.Quill;
  316. // 获取光标所在位置
  317. let length = quill.getSelection().index;
  318. // 插入文件 res.url为服务器返回的图片地址
  319. quill.insertEmbed(length, "link",
  320. {
  321. href: process.env.VUE_APP_BASE_API + res.fileName,
  322. innerText: res.originalFilename
  323. },
  324. );
  325. // 调整光标到最后
  326. quill.setSelection(length + 1);
  327. } else {
  328. this.$message.error("文件插入失败");
  329. }
  330. },
  331. handleUploadError() {
  332. this.$message.error("图片插入失败");
  333. },
  334. handleUploadErrorVideo() {
  335. this.$message.error("视频插入失败");
  336. },
  337. handleUploadErrorFile() {
  338. this.$message.error("文件插入失败");
  339. },
  340. },
  341. };
  342. </script>
  343. <style>
  344. .editor, .ql-toolbar {
  345. white-space: pre-wrap !important;
  346. line-height: normal !important;
  347. }
  348. .quill-img {
  349. display: none;
  350. }
  351. .ql-align-right{
  352. text-align: right;
  353. }
  354. .ql-align-center{
  355. text-align: center;
  356. }
  357. .ql-snow .ql-tooltip[data-mode="link"]::before {
  358. content: "请输入链接地址:";
  359. }
  360. .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  361. border-right: 0px;
  362. content: "保存";
  363. padding-right: 0px;
  364. }
  365. .ql-snow .ql-tooltip[data-mode="video"]::before {
  366. content: "请输入视频地址:";
  367. }
  368. .ql-snow .ql-picker.ql-size .ql-picker-label::before,
  369. .ql-snow .ql-picker.ql-size .ql-picker-item::before {
  370. content: "14px";
  371. }
  372. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
  373. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
  374. content: "10px";
  375. }
  376. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
  377. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
  378. content: "14px";
  379. }
  380. .ql-size-14px {
  381. font-size: 14px;
  382. }
  383. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
  384. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
  385. content: "16px";
  386. }
  387. .ql-size-16px {
  388. font-size: 16px;
  389. }
  390. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
  391. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
  392. content: "18px";
  393. }
  394. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
  395. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
  396. content: "20px";
  397. }
  398. .ql-size-20px {
  399. font-size: 20px;
  400. }
  401. .ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
  402. .ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
  403. content: "32px";
  404. }
  405. .ql-snow .ql-picker.ql-header .ql-picker-label::before,
  406. .ql-snow .ql-picker.ql-header .ql-picker-item::before {
  407. content: "文本";
  408. }
  409. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
  410. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  411. content: "标题1";
  412. }
  413. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
  414. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  415. content: "标题2";
  416. }
  417. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
  418. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  419. content: "标题3";
  420. }
  421. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
  422. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  423. content: "标题4";
  424. }
  425. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
  426. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  427. content: "标题5";
  428. }
  429. .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
  430. .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  431. content: "标题6";
  432. }
  433. .ql-snow .ql-picker.ql-font .ql-picker-label::before,
  434. .ql-snow .ql-picker.ql-font .ql-picker-item::before {
  435. content: "标准字体";
  436. }
  437. .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
  438. .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
  439. content: "衬线字体";
  440. }
  441. .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
  442. .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
  443. content: "等宽字体";
  444. }
  445. .ql-video {
  446. width: 100%;
  447. height: 425px;
  448. }
  449. </style>