index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <template>
  2. <div class="editor">
  3. <div id="editorContent"></div>
  4. <!-- <button type="button" class="btn" @click="getEditorData">获取当前内容</button> -->
  5. <!-- <h3>内容预览</h3> -->
  6. <!-- <textarea name="" id="" cols="170" rows="20" readonly v-model="editorData"></textarea> -->
  7. </div>
  8. </template>
  9. <script>
  10. // 引入 wangEditor
  11. import wangEditor from 'wangeditor'
  12. import axios from 'axios'
  13. import qs from 'qs'
  14. import load from '@/utils/loading'
  15. import {
  16. getToken
  17. } from '@/utils/auth'
  18. export default {
  19. data() {
  20. return {
  21. editor: null,
  22. editorData: ''
  23. }
  24. },
  25. props: {
  26. value: { // 组件状态
  27. type: String
  28. },
  29. },
  30. mounted() {
  31. const editor = new wangEditor(`#editorContent`)
  32. // 默认情况下,显示所有菜单
  33. editor.config.menus = [
  34. 'head',
  35. 'bold',
  36. 'fontSize',
  37. 'fontName',
  38. 'italic',
  39. 'underline',
  40. 'strikeThrough',
  41. 'indent',
  42. 'lineHeight',
  43. 'foreColor',
  44. 'backColor',
  45. // 'link',
  46. 'list',
  47. 'justify',
  48. 'quote',
  49. // 'emoticon',
  50. 'image',
  51. // 'video',
  52. 'table',
  53. // 'code',
  54. 'splitLine',
  55. 'undo',
  56. 'redo',
  57. ]
  58. // 最大上传图片数
  59. editor.config.uploadImgMaxLength = 1
  60. editor.config.zIndex = 500
  61. // 限制图片大小
  62. // editor.config.uploadImgMaxSize = 2 * 1024 * 1024
  63. // 配置 onchange 回调函数,将数据同步到 vue 中
  64. editor.config.onchange = (newHtml) => {
  65. this.editorData = newHtml
  66. this.$emit('input', newHtml)
  67. }
  68. // (editor.txt.eventHooks)
  69. // editor.txt.eventHooks.clickEvents.push(() => {
  70. // return false
  71. // })
  72. // editor.txt.eventHooks.imgClickEvents.push(() => {
  73. // })
  74. // editor.config.uploadImgServer = '/api-web/uploadFile'
  75. editor.config.customUploadImg = async (resultFiles, insertImgFn) => {
  76. // resultFiles 是 input 中选中的文件列表
  77. // insertImgFn 是获取图片 url 后,插入到编辑器的方法
  78. const file = resultFiles[0]
  79. const imageType = {
  80. "image/png": true,
  81. "image/jpeg": true
  82. };
  83. const isImage = imageType[file.type];
  84. const isLt2M = file.size / 1024 / 1024 < 2;
  85. if (!isImage) {
  86. this.$message.error("只能上传图片格式!");
  87. }
  88. if (!isLt2M) {
  89. this.$message.error("上传图片大小不能超过 2M!");
  90. }
  91. if(isImage && isLt2M) {
  92. load.startLoading()
  93. let form = new FormData()
  94. form.append('file', file)
  95. await axios({
  96. method: 'post',
  97. headers: {
  98. Authorization: getToken()
  99. },
  100. data: form,
  101. url: '/api-web/uploadFile',
  102. }).then(res => {
  103. const result = res.data
  104. if(result.code == 200) {
  105. // 上传图片,返回结果,将图片插入到编辑器中
  106. insertImgFn(result.data.url)
  107. }
  108. })
  109. load.endLoading()
  110. }
  111. }
  112. // 创建编辑器
  113. editor.create()
  114. this.editor = editor
  115. },
  116. methods: {
  117. getEditorData() {
  118. // 通过代码获取编辑器内容
  119. let data = this.editor.txt.html()
  120. this.$emit('getEditorData', data)
  121. // alert(data)
  122. }
  123. },
  124. watch: {
  125. value(newValue) {
  126. this.editor.txt.html(newValue)
  127. }
  128. },
  129. beforeDestroy() {
  130. // 调用销毁 API 对当前编辑器实例进行销毁
  131. this.editor.destroy()
  132. }
  133. }
  134. </script>
  135. <style lang="scss">
  136. .home {
  137. margin: auto;
  138. position: relative;
  139. .btn {
  140. position: absolute;
  141. right: 0;
  142. top: 0;
  143. padding: 5px 10px;
  144. cursor: pointer;
  145. }
  146. h3 {
  147. margin: 30px 0 15px;
  148. }
  149. }
  150. </style>
  151. <!-- <template>
  152. <div class="editor">
  153. <quill-editor class="ql-editor" v-model="content" ref="myQuillEditor" :options="editorOption" @change="onEditorChange($event)"></quill-editor>
  154. <el-upload class="ivu-upload" :show-upload-list="false" :headers="headers" :on-success="handleSuccess" accept=".jpg, .jpeg, .png"
  155. :max-size="2048" multiple action="/api-web/uploadFile">
  156. <Button icon="ios-cloud-upload-outline"></Button>
  157. </el-upload>
  158. <el-dialog title="插入视频" width="500px" :visible.sync="dialogFormVisible">
  159. <el-form :model="dialogForm" ref="diologForm" :rules="dialogFormRules">
  160. <el-form-item label="封面图地址" label-width="90px">
  161. <el-upload class="avatar-uploader" style="line-height: 0;display: inline-block" action="/api-web/uploadFile"
  162. :headers="headers" :show-file-list="false" v-loading="uploadImgLoading" accept=".jpg, .jpeg, .png"
  163. :on-success="handleImgSuccess" :on-error="handleUploadImgError" :before-upload="beforeImgUpload">
  164. <img v-if="dialogForm.poster" :src="dialogForm.poster" class="avatar" />
  165. <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  166. </el-upload>
  167. </el-form-item>
  168. <el-form-item label="视频类型" label-width="90px">
  169. <el-radio-group v-model="formRadio">
  170. <el-radio :label="1">外部链接</el-radio>
  171. <el-radio :label="2">上传</el-radio>
  172. </el-radio-group>
  173. </el-form-item>
  174. <el-form-item v-if="formRadio == 1" label="视频地址" label-width="90px" prop="url">
  175. <el-input v-model="dialogForm.url" style="width: 100%;" autocomplete="off"></el-input>
  176. </el-form-item>
  177. <el-form-item v-if="formRadio == 2" label="上传视频" label-width="90px" prop="videoUrl">
  178. <el-upload class="upload-demo" style="display: inline-block" v-loading="uploadLoading" action="/api-web/uploadFile"
  179. :before-upload="beforeUpload" :on-success="handleUploadSuccess" :on-error="handleUploadError"
  180. :show-file-list="false" accept=".mp4" :file-list="fileList" :on-exceed="handleExceed">
  181. <video style="width: 120px; height: 120px" v-if="dialogForm.videoUrl" type="video/mp4" preload="auto" :src="dialogForm.videoUrl"></video>
  182. <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  183. </el-upload>
  184. <p class="imageSize">只能上传mp4文件, 且不超过100M</p>
  185. </el-form-item>
  186. </el-form>
  187. <div slot="footer" class="dialog-footer">
  188. <el-button @click="dialogFormVisible = false">取 消</el-button>
  189. <el-button type="primary" @click="onVideoComfirm('diologForm')">确 定</el-button>
  190. </div>
  191. </el-dialog>
  192. </div>
  193. </template>
  194. <script>
  195. import {
  196. getToken
  197. } from "@/utils/auth";
  198. import "quill/dist/quill.core.css";
  199. import "quill/dist/quill.snow.css";
  200. import "quill/dist/quill.bubble.css";
  201. import Quill from "quill";
  202. import {
  203. quillEditor
  204. } from "vue-quill-editor";
  205. // 工具栏配置
  206. const toolbarOptions = [
  207. ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  208. ["blockquote", "code-block"], // 引用 代码块
  209. [{ header: 1 }, { header: 2 }], // 1、2 级标题
  210. [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  211. [{ script: "sub" }, { script: "super" }], // 上标/下标
  212. [{ indent: "-1" }, { indent: "+1" }], // 缩进
  213. // [{'direction': 'rtl'}], // 文本方向
  214. [{ size: ["small", false, "large", "huge"] }], // 字体大小
  215. [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  216. [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  217. [{ font: [] }], // 字体种类
  218. [{ align: [] }], // 对齐方式
  219. ["clean"], // 清除文本格式
  220. ["image", "video"] // 链接、图片、视频
  221. // ["link", "image", "video"] // 链接、图片、视频
  222. ];
  223. // 标题
  224. const titleConfig = {
  225. "ql-bold": "加粗",
  226. "ql-color": "颜色",
  227. "ql-font": "字体",
  228. "ql-code": "插入代码",
  229. "ql-italic": "斜体",
  230. // 'ql-link': '添加链接',
  231. "ql-background": "背景颜色",
  232. "ql-size": "字体大小",
  233. "ql-strike": "删除线",
  234. "ql-script": "上标/下标",
  235. "ql-underline": "下划线",
  236. "ql-blockquote": "引用",
  237. "ql-header": "标题",
  238. "ql-indent": "缩进",
  239. "ql-list": "列表",
  240. "ql-align": "文本对齐",
  241. "ql-direction": "文本方向",
  242. "ql-code-block": "代码块",
  243. "ql-formula": "公式",
  244. "ql-image": "图片",
  245. "ql-video": "视频",
  246. "ql-clean": "清除字体样式",
  247. "ql-upload": "文件"
  248. };
  249. // 这里引入修改过的video模块并注册
  250. import Video from "@/views/quill/video.js";
  251. import dayjs from 'dayjs'
  252. Quill.register(Video, true);
  253. export default {
  254. name: 'editor',
  255. components: { quillEditor },
  256. data() {
  257. return {
  258. content: null,
  259. headers: {
  260. Authorization: getToken()
  261. },
  262. dialogFormVisible: false,
  263. dialogForm: {
  264. poster: null,
  265. url: null,
  266. videoUrl: null
  267. },
  268. uploadLoading: false,
  269. uploadImgLoading: false,
  270. fileList: [],
  271. dialogFormRules: {
  272. url: [{ required: true, message: "请输入视频地址", trigger: "blur" }],
  273. videoUrl: [{ required: true, message: "请上传视频", trigger: 'blur' }]
  274. },
  275. formRadio: 1,
  276. editorOption: {
  277. placeholder: "请输入内容",
  278. modules: {
  279. toolbar: {
  280. container: toolbarOptions,
  281. handlers: {
  282. image: function(value) {
  283. if (value) {
  284. // 调用iview图片上传
  285. document.querySelector(".ivu-upload .el-upload").click();
  286. } else {
  287. this.quill.format("image", false);
  288. }
  289. },
  290. video: function(value) {
  291. if (value) {
  292. that.dialogFormVisible = true;
  293. let editor = that.$refs.myQuillEditor.quill;
  294. // 光标所在位置
  295. that.editorIndex = editor.getSelection().index;
  296. } else {
  297. this.quill.format("image", false);
  298. }
  299. }
  300. }
  301. }
  302. }
  303. },
  304. }
  305. },
  306. methods: {
  307. onEditorChange({
  308. quill,
  309. html,
  310. text
  311. }) {
  312. this.form.content = html;
  313. },
  314. onVideoComfirm (formName) {
  315. this.$refs[formName].validate(valid => {
  316. if (valid) {
  317. let dialogForm = this.dialogForm;
  318. // 获取富文本组件实例
  319. let quill = this.editor;
  320. // 插入图片,res为服务器返回的图片链接地址
  321. const params = {
  322. poster: dialogForm.poster,
  323. url: this.formRadio == 1 ? dialogForm.url : dialogForm.videoUrl,
  324. }
  325. quill.insertEmbed(this.editorIndex, "video", params);
  326. // 调整光标到最后
  327. quill.setSelection(this.editorIndex + 1, { preload: false });
  328. this.dialogFormVisible = false;
  329. this.dialogForm = {
  330. poster: null,
  331. url: null,
  332. videoUrl: null
  333. };
  334. } else {
  335. return false;
  336. }
  337. });
  338. },
  339. handleSuccess (res) {
  340. // 获取富文本组件实例
  341. let quill = this.editor;
  342. // 如果上传成功
  343. if (res.code) {
  344. // 获取光标所在位置
  345. let length = quill.getSelection().index;
  346. // 插入图片,res为服务器返回的图片链接地址
  347. quill.insertEmbed(length, "image", res.data.url);
  348. // 调整光标到最后
  349. quill.setSelection(length + 1);
  350. } else {
  351. // 提示信息,需引入Message
  352. this.$message.error("图片插入失败");
  353. }
  354. },
  355. addQuillTitle () {
  356. const oToolBar = document.querySelector(".ql-toolbar"),
  357. aButton = oToolBar.querySelectorAll("button"),
  358. aSelect = oToolBar.querySelectorAll("select");
  359. aButton.forEach(function (item) {
  360. if (item.className === "ql-script") {
  361. item.value === "sub" ? (item.title = "下标") : (item.title = "上标");
  362. } else if (item.className === "ql-indent") {
  363. item.value === "+1"
  364. ? (item.title = "向右缩进")
  365. : (item.title = "向左缩进");
  366. } else {
  367. item.title = titleConfig[item.classList[0]];
  368. }
  369. });
  370. aSelect.forEach(function (item) {
  371. item.parentNode.title = titleConfig[item.classList[0]];
  372. });
  373. },
  374. handleUploadImgError(file) {
  375. this.uploadImgLoading = false
  376. this.$message.error('上传失败')
  377. },
  378. handleImgSuccess(res, file) {
  379. this.uploadImgLoading = false
  380. this.dialogForm.poster = res.data.url
  381. },
  382. beforeImgUpload(file) {
  383. const imageType = {
  384. "image/png": true,
  385. "image/jpeg": true
  386. };
  387. const isImage = imageType[file.type];
  388. const isLt2M = file.size / 1024 / 1024 < 2;
  389. (isImage, isLt2M)
  390. if (!isImage) {
  391. this.$message.error("只能上传图片格式!");
  392. }
  393. if (!isLt2M) {
  394. this.$message.error("上传图片大小不能超过 2MB!");
  395. }
  396. if (isImage && isLt2M) {
  397. this.uploadImgLoading = true
  398. }
  399. return isImage && isLt2M;
  400. },
  401. handleAvatarSuccess(res, file) {
  402. this.form.coverImage = res.data.url;
  403. },
  404. beforeAvatarUpload(file) {
  405. const imageType = {
  406. "image/png": true,
  407. "image/jpeg": true
  408. };
  409. const isImage = imageType[file.type];
  410. const isLt2M = file.size / 1024 / 1024 < 2;
  411. if (!isImage) {
  412. this.$message.error("只能上传图片格式!");
  413. }
  414. if (!isLt2M) {
  415. this.$message.error("上传图片大小不能超过 2M!");
  416. }
  417. return isImage && isLt2M;
  418. },
  419. beforeUpload(file) {
  420. // const isJPG = file.type === 'image/jpeg';
  421. const isLt2M = file.size / 1024 / 1024 < 100;
  422. // if (!isJPG) {
  423. // this.$message.error('上传头像图片只能是 JPG 格式!');
  424. // }
  425. if (!isLt2M) {
  426. this.$message.error('上传视频大小不能超过 100MB!');
  427. }
  428. this.uploadLoading = true
  429. return isLt2M;
  430. },
  431. handleUploadError(file) {
  432. this.uploadLoading = false
  433. this.$message.error('上传视频失败')
  434. },
  435. handleUploadSuccess(file, fileList) {
  436. this.uploadLoading = false
  437. this.$message.success('上传视频成功')
  438. this.dialogForm.videoUrl = file.data.url;
  439. },
  440. handleExceed(files, fileList) {
  441. this.$message.error('您已上传过视频')
  442. }
  443. },
  444. computed: {
  445. editor() {
  446. return this.$refs.myQuillEditor.quill;
  447. }
  448. }
  449. }
  450. </script>
  451. <style>
  452. /deep/.ql-editor {
  453. min-height: 300px;
  454. padding: 0;
  455. }
  456. /deep/.ql-container .ql-editor {
  457. max-height: 500px;
  458. }
  459. .el-button--primary {
  460. background: #14928a;
  461. border-color: #14928a;
  462. color: #fff;
  463. &:hover,
  464. &:active,
  465. &:focus {
  466. background: #14928a;
  467. border-color: #14928a;
  468. color: #fff;
  469. }
  470. }
  471. </style>
  472. -->