【前端】wangeditor源码修改,打包发布到npm,实现上传视频功能,得到视频的第一帧保存为封面,spring-boot+vue,axios实现文件上传,视频图片浏览

一、实现

1、创建git分支,clone下源码

git地址
创建分支
在这里插入图片描述

2、图片上传具有文件选择的功能,所以我完全模仿(抄袭)图片上传

报错不慌,全部改完就不报错了

1)在src/config/index.ts中的export type ConfigType中添加
uploadVideoAccept: string[]
uploadVideoMaxSize: number
customUploadVideo: Function | null
2)src/config/video.ts,添加部分的三个属性
/**
 * @description 视频相关的配置
 * @author hutianhao
 */

import { EMPTY_FN } from '../utils/const'

export default {
    // 插入网络视频前的回调函数
    onlineVideoCheck: (video: string): string | boolean => {
        return true
    },

    // 插入网络视频成功之后的回调函数
    onlineVideoCallback: EMPTY_FN,

    //---------下面三个需要自己添加----------    

    // accept
    uploadVideoAccept: ['mp4'],

    // 上传图片的最大体积,默认 50M
    uploadVideoMaxSize: 50 * 1024 * 1024,

    // 自定义上传
    customUploadImg: null,

}
3)src/menus/video下新建一个upload-video.ts

img中有自动上传的部分,我只留下了自定义上传部分,如果想深入了解,就仔细看一下源码。

/**
 * @description 上传图片
 * @author wangfupeng
 */

import Editor from '../../editor/index'
import {arrForEach} from '../../utils/util'

export type ResType = {
    errno: number | string
    data: string[]
}

class UploadVideo {
    private editor: Editor

    constructor(editor: Editor) {
        this.editor = editor
    }

    /**
     * 往编辑区域插入视频
     * @param src 视频地址
     */
    public insertVideo(src: string): void {
        const editor = this.editor
        const config = editor.config

        const i18nPrefix = 'validate.'
        const t = (text: string, prefix: string = i18nPrefix): string => {
            return editor.i18next.t(prefix + text)
        }

        // 先插入视频,无论是否能成功
        editor.cmd.do('insertHTML', `<iframe src="${src}" style="max-width:100%;" ></iframe>`)
        // 执行回调函数
        // config.linkVideoCallback(src)

        // 加载视频
        let video: any = document.createElement('video')
        video.onload = () => {
            video = null
        }
        video.onerror = () => {
            config.customAlert(
                t('插入视频错误'),
                'error',
                `wangEditor: ${t('插入视频错误')}${t('视频链接')} "${src}",${t('下载链接失败')}`
            )
            video = null
        }
        video.onabort = () => (video = null)
        video.src = src
    }

    /**
     * 上传视频
     * @param files 文件列表
     */
    public uploadVideo(files: FileList | File[]): void {
        if (!files.length) {
            return
        }

        const editor = this.editor
        const config = editor.config

        // ------------------------------ i18next ------------------------------

        const i18nPrefix = 'validate.'
        const t = (text: string): string => {
            return editor.i18next.t(i18nPrefix + text)
        }

        // ------------------------------ 获取配置信息 ------------------------------

        const maxSize = config.uploadVideoMaxSize
        const maxSizeM = maxSize / 1024 / 1024
        // 自定义上传视频
        const customUploadVideo = config.customUploadVideo

        if (!customUploadVideo) {
            // 没有 customUploadImg 的情况下
            return
        }

        // ------------------------------ 验证文件信息 ------------------------------
        const resultFiles: File[] = []
        const errInfos: string[] = []
        arrForEach(files, file => {
            const name = file.name
            const size = file.size

            // chrome 低版本 name === undefined
            if (!name || !size) {
                return
            }

            if (/\.(mp4|jpeg)$/i.test(name) === false) {
                // 后缀名不合法,不是视频
                errInfos.push(`【${name}${t('不是视频')}`)
                return
            }

            if (maxSize < size) {
                // 上传图片过大
                errInfos.push(`【${name}${t('大于')} ${maxSizeM}M`)
                return
            }

            // 验证通过的加入结果列表
            resultFiles.push(file)
        })

        // 抛出验证信息
        if (errInfos.length) {
            config.customAlert(`${t('视频验证未通过')}: \n` + errInfos.join('\n'), 'warning')
            return
        }

        // 如果过滤后文件列表为空直接返回
        if (resultFiles.length === 0) {
            config.customAlert(t('传入的文件不合法'), 'warning')
            return
        }

        // ------------------------------ 自定义上传 ------------------------------
        if (customUploadVideo && typeof customUploadVideo === 'function') {
            customUploadVideo(resultFiles, this.insertVideo.bind(this))
            return
        }

    }
}

export default UploadVideo

4)修改src/menus/video/create-panel-conf.ts
/**
 * @description video 菜单 panel tab 配置
 * @author tonghan
 */

import Editor from '../../editor/index'
import {PanelConf, PanelTabConf} from '../menu-constructors/Panel'
import {getRandom} from '../../utils/util'
import $ from '../../utils/dom-core'
import UploadVideo from './upload-video'
import {videoRegex} from '../../utils/const'

export default function (editor: Editor, video: string): PanelConf {
    const config = editor.config
    const uploadVideo = new UploadVideo(editor)
    // panel 中需要用到的id
    const inputIFrameId = getRandom('input-iframe')
    const btnOkId = getRandom('btn-ok')
    const i18nPrefix = 'menus.panelMenus.video.'
    const t = (text: string, prefix: string = i18nPrefix): string => {
        return editor.i18next.t(prefix + text)
    }

    // panel 中需要用到的id
    const upTriggerId = getRandom('up-trigger-id')
    const upFileId = getRandom('up-file-id')

    /**
     * 插入链接
     * @param iframe html标签
     */
    function insertVideo(video: string): void {
        editor.cmd.do('insertHTML', video + '<p><br></p>')

        // video添加后的回调
        editor.config.onlineVideoCallback(video)
    }

    /**
     * 校验在线视频链接
     * @param video 在线视频链接
     */
    function checkOnlineVideo(video: string): boolean {
        // 编辑器进行正常校验,video 合规则使指针为true,不合规为false
        let flag = true
        if (!videoRegex.test(video)) {
            flag = false
        }

        // 查看开发者自定义配置的返回值
        const check = editor.config.onlineVideoCheck(video)
        if (check === undefined) {
            if (flag === false) console.log(t('您刚才插入的视频链接未通过编辑器校验', 'validate.'))
        } else if (check === true) {
            // 用户通过了开发者的校验
            if (flag === false) {
                editor.config.customAlert(
                    `${t('您插入的网络视频无法识别', 'validate.')}${t(
                        '请替换为正确的网络视频格式',
                        'validate.'
                    )}:如<iframe src=...></iframe>`,
                    'warning'
                )
            } else {
                return true
            }
        } else {
            //用户未能通过开发者的校验,开发者希望我们提示这一字符串
            editor.config.customAlert(check, 'error')
        }
        return false
    }

    // tabs 配置 -----------------------------------------
    const accepts: string = config.uploadVideoAccept.map((item: string) => `video/${item}`).join(',')
    //上传视频的菜单
    const tabsConf: PanelTabConf[] = [
        // first tab
        {
            // 标题
            title: t('插入本地视频'),
            // 模板,//不需要多文件上传如果需要的话,在input中加上 multiple
            tpl: `<div class="w-e-up-img-container">
                    <div id="${upTriggerId}" class="w-e-up-btn">
                        <i class="w-e-icon-upload2"></i>
                    </div>
                    <div style="display:none;">
                        <input id="${upFileId}" type="file" accept="${accepts}"/>
                    </div>
                </div>`,
            // 事件绑定
            events: [
                // 触发选择视频
                {
                    selector: '#' + upTriggerId,
                    type: 'click',
                    fn: () => {
                        const $file = $('#' + upFileId)
                        const fileElem = $file.elems[0]
                        if (fileElem) {
                            fileElem.click()
                        } else {
                            // 返回 true 可关闭 panel
                            return true
                        }
                    },
                },
                // 选择图片完毕
                {
                    selector: '#' + upFileId,
                    type: 'change',
                    fn: () => {
                        const $file = $('#' + upFileId)
                        const fileElem = $file.elems[0]
                        if (!fileElem) {
                            // 返回 true 可关闭 panel
                            return true
                        }

                        // 获取选中的 file 对象列表
                        const fileList = (fileElem as any).files
                        if (fileList.length) {
                            uploadVideo.uploadVideo(fileList)
                        }

                        // 返回 true 可关闭 panel
                        return true
                    },
                },
            ],
        }, // first tab end
        // second tab
        {
            // tab 的标题
            title: t('插入网络视频'),
            // title: editor.i18next.t('menus.panelMenus.video.插入视频'),
            // 模板
            tpl: `<div>
                        <input 
                            id="${inputIFrameId}" 
                            type="text" 
                            class="block" 
                            placeholder="${editor.i18next.t('如')}:<iframe src=... ></iframe>"/>
                        </td>
                        <div class="w-e-button-container">
                            <button type="button" id="${btnOkId}" class="right">
                                ${editor.i18next.t('插入')}
                            </button>
                        </div>
                    </div>`,
            // 事件绑定
            events: [
                // 插入视频
                {
                    selector: '#' + btnOkId,
                    type: 'click',
                    fn: () => {
                        // 执行插入视频
                        const $video = $('#' + inputIFrameId)
                        let video = $video.val().trim()

                        // 视频为空,则不插入
                        if (!video) return
                        // 对当前用户插入的内容进行判断,插入为空,或者返回false,都停止插入
                        if (!checkOnlineVideo(video)) return

                        insertVideo(video)

                        // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭
                        return true
                    },
                },
            ],

        }, // second tab end
    ]


    const conf: PanelConf = {
        width: 300,
        height: 0,
        tabs: [],
    }
    // 显示“插入本地视频”
    if (
        window.FileReader && config.customUploadVideo
    ) {
        conf.tabs.push(tabsConf[0])
    }
    // 显示“插入网络视频”
    if (config.showLinkImg) {
        conf.tabs.push(tabsConf[1])
    }
    return conf
}

5)改完收工,吃饭,吃完后打包上传!

3、打包上传npm

修改名字和version,把所有关联了wangeditor的git地址的地方,都删掉
在这里插入图片描述

1)打包
npm  run  build
2)在npm官方注册账号

https://www.npmjs.com/

必须邮箱验证通过,才能实现发布!!

3)查看自己当前npm的源地址
npm config get registry

如果是 https://registry.npm.taobao.org/ ,就不得行,需要改为npm官方的源地址

4)修改自己的npm镜像,切回到npmjs源
npm config set registry=http://registry.npmjs.org
5)登录你的npm官方账号(切换源之后,每次都要重新登录)
npm login

登录成功后,点一下下面的链接,相当于是验证登录
在这里插入图片描述

6)发布
npm publish
7)源改回淘宝的

不改的话,你下载自己刚发布的依赖会很慢

npm config set registry=https://registry.npm.taobao.org/

4、使用,开干开干!

1)删掉以前的依赖
npm uninstall wangeditor -S
2)引入自己的依赖
npm i tangeditor --save

源码部分解析:插入本地视频的tab只有在存在自定义方法的时候才会存在。img也是一样的。必须要配置了自定义的上传方法,才会显示插入本地视频这一栏。(我在后面源码部分写了的,不慌)

在这里插入图片描述

我的自定义方法代码:

 /**
 * 监听视频上传
 * @param resultFiles 是选中的视频数组,当然我在源码中设置了只允许选中一个,如果需要修改,自己去看源码的node_modules/tangeditor/src/menus/video/create-panel-conf.ts
 * @param insertVideoFn 通过这个对象来把视频插入文本里面。它参数是视频的地址,通常是上传成功之后,后端返回来的数据,我这里写死。
 */
 this.editor.config.customUploadVideo = function (resultFiles, insertVideoFn) {
     console.log(resultFiles[0])
     console.log(insertVideoFn)
     	insertVideoFn("http://localhost:88/api/common/file/video/view/video/艾斯.mp4")
 }

二、我的全部代码

干货甩你一脸

1、springboot上传接口实现

1)配置跨域

如果是网关转发,则只需要网关配置跨域即可。

但是转发的服务和网关都必须在同一个注册中心注册。

否则网关找不到服务。

package gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class TangxzBootCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();

        //1、配置跨域
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);

        source.registerCorsConfiguration("/**",corsConfiguration);
        return new CorsWebFilter(source);
    }
}
2)允许文件上传

只需要在接口所在服务的配置中加上这一部分即可,网关不需要添加这个。

spring:
  servlet:
    multipart:
      enabled: true
      file-size-threshold: 0
      max-file-size: 30MB
      max-request-size: 30MB
3)视频返回第一帧为Base图片代码及依赖
<!-- 截取视频的某一帧并返回 -->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.4.3</version>
</dependency>

<dependency>
    <groupId>org.bytedeco.javacpp-presets</groupId>
    <artifactId>ffmpeg-platform</artifactId>
    <version>4.0.2-1.4.3</version>
</dependency>

工具类实现:
返回第一帧的图片Base64
建议数据库存储视频的地址,前端需要的时候,直接来后端请求base64图片。
这个时候这个方法就要改为接口

package common_api.utils;

import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.Java2DFrameConverter;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;

/**
 * @author: Tangxz
 * @email: 1171702529@qq.com
 * @cate: 2020/12/20 22:17
 */
public class VideoCover {

    public static String fetchFrame(String videoPath) {
        FFmpegFrameGrabber ff = null;
        byte[] data = null;
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            ff = new FFmpegFrameGrabber(videoPath);
            ff.start();
            int lenght = ff.getLengthInFrames();
            int i = 0;
            Frame f = null;
            while (i < lenght) {
                // 过滤前5帧,避免出现全黑的图片
                f = ff.grabFrame();
                if ((i > 5) && (f.image != null)) {
                    break;
                }
                i++;
            }
            BufferedImage bi =  new Java2DFrameConverter().getBufferedImage(f);
            String rotate = ff.getVideoMetadata("rotate");
            if (rotate != null) {
                bi = rotate(bi, Integer.parseInt(rotate));
            }
            ImageIO.write(bi, "jpg", os);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (ff != null) {
                    ff.stop();
                }
            } catch (FrameGrabber.Exception e) {
                e.printStackTrace();
            }
        }
        BASE64Encoder encoder = new BASE64Encoder();
        return "data:image/jpg;base64,"+encoder.encode(os.toByteArray());
    }

    public static BufferedImage rotate(BufferedImage src, int angel) {
        int src_width = src.getWidth(null);
        int src_height = src.getHeight(null);
        int type = src.getColorModel().getTransparency();
        Rectangle rect_des = calcRotatedSize(new Rectangle(new Dimension(src_width, src_height)), angel);
        BufferedImage bi = new BufferedImage(rect_des.width, rect_des.height, type);
        Graphics2D g2 = bi.createGraphics();
        g2.translate((rect_des.width - src_width) / 2, (rect_des.height - src_height) / 2);
        g2.rotate(Math.toRadians(angel), src_width / 2, src_height / 2);
        g2.drawImage(src, 0, 0, null);
        g2.dispose();
        return bi;
    }

    public static Rectangle calcRotatedSize(Rectangle src, int angel) {
        if (angel >= 90) {
            if(angel / 90 % 2 == 1) {
                int temp = src.height;
                src.height = src.width;
                src.width = temp;
            }
            angel = angel % 90;
        }
        double r = Math.sqrt(src.height * src.height + src.width * src.width) / 2;
        double len = 2 * Math.sin(Math.toRadians(angel) / 2) * r;
        double angel_alpha = (Math.PI - Math.toRadians(angel)) / 2;
        double angel_dalta_width = Math.atan((double) src.height / src.width);
        double angel_dalta_height = Math.atan((double) src.width / src.height);
        int len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));
        int len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));
        int des_width = src.width + len_dalta_width * 2;
        int des_height = src.height + len_dalta_height * 2;
        return new java.awt.Rectangle(new Dimension(des_width, des_height));
    }

}
4)接口代码
package common_api.controller;

import com.tangxz.common.utils.R;
import common_api.utils.VideoCover;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author: Tangxz
 * @email: 1171702529@qq.com
 * @cate: 2020/12/17 13:38
 */
@Slf4j
@RestController
@RequestMapping("/common/file")
public class FileController {
    @GetMapping("/get")
    public String ww() {
        return "123";
    }

    @Value(value = "${tangxz.path.upload}")
    private String uploadpath;
    @Value(value = "${tangxz.path.gateway}")
    private String gateway;


    /**
     * 图片/视频上传
     *
     * @param file
     * @return
     */
    @PostMapping(value = "/upload")
    public R upload(@RequestParam(name = "file", required = false) MultipartFile file) {
        try {
            String ctxPath = uploadpath;
            String fileName = null;
            String fileType = file.getContentType();
            String nowday = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
            File filePath = new File(ctxPath + File.separator + fileType + File.separator + nowday);
            if (!filePath.exists()) {
                filePath.mkdirs();// 创建文件根目录
            }
            String orgName = file.getOriginalFilename();// 获取文件名
            fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.indexOf("."));
            String savePath = filePath.getPath() + File.separator + fileName;
            File savefile = new File(savePath);
            FileCopyUtils.copy(file.getBytes(), savefile);
            String dbpath = fileType + File.separator + nowday + File.separator + fileName;
            if (dbpath.contains("\\")) {
                dbpath = dbpath.replace("\\", "/");
            }
            assert fileType != null;
            //判断文件类型,如果是视频,需要截取第一帧图片返回。
            //不同文件类型的访问接口不一样。
            if (fileType.contains("image")) {
                return R.ok().put("imgUrl", gateway + "/common/file/image/view/" + dbpath);
            } else if (fileType.contains("video")) {
                //多返回一个,封面地址
                String coverUrl = VideoCover.fetchFrame(savePath);
                return R.ok().put("videoUrl", gateway + "/common/file/video/view/" + dbpath).put("coverUrl",coverUrl);
            } else {
                return R.ok().put("url", gateway + "/common/file/image/view/" + dbpath);
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
            return R.error().put("error", e.getMessage());
        }
    }


    /**
     * 预览图片
     * 请求地址:http://localhost:88/api/common/file/image/view/{上方组合成的地址:dbpath}
     *
     * @param request
     * @param response
     */
    @GetMapping(value = "/image/view/**")
    public void viewImage(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType("image/jpeg;charset=utf-8");
        view(request, response);
    }

    /**
     * 预览视频
     * 请求地址:     http://localhost:88/api/common/file/video/view/{上方组合成的地址:dbpath}
     * <iframe src="http://localhost:88/api/common/file/video/view/video/mp4/2020-12-20/艾斯.mp4"></iframe>
     * 有些视频格式的问题,所以有些请求无法直接判断文件格式。就直接使用两个方法来实现,一个/image/view,一个video/view
     * @param request
     * @param response
     */
    @GetMapping(value = "/video/view/**")
    public void viewVideo(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType("audio/mp4;charset=utf-8");
        view(request, response);
    }

    /**
     * 浏览图片/视频的实现方法,把文件写入response中
     * @param request
     * @param response
     */
    private void view(HttpServletRequest request, HttpServletResponse response) {
        // ISO-8859-1 ==> UTF-8 进行编码转换
        String filePath = extractPathFromPattern(request);
        // 其余处理略
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            filePath = filePath.replace("..", "");
            if (filePath.endsWith(",")) {
                filePath = filePath.substring(0, filePath.length() - 1);
            }
            //    @Value(value = "${tangxz.path.upload}")
            String localPath = uploadpath;
            String videoUrl = localPath + File.separator + filePath;
            inputStream = new BufferedInputStream(new FileInputStream(videoUrl));
            outputStream = response.getOutputStream();
            byte[] buf = new byte[1024];
            int len;
            while ((len = inputStream.read(buf)) > 0) {
                outputStream.write(buf, 0, len);
            }
            response.flushBuffer();
        } catch (IOException e) {
            log.error("预览失败" + e.getMessage());
            // e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * 把指定URL后的字符串全部截断当成参数
     * 这么做是为了防止URL中包含中文或者特殊字符(/等)时,匹配不了的问题
     *
     * @param request
     * @return
     */
    private static String extractPathFromPattern(final HttpServletRequest request) {
        String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
        String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        return new AntPathMatcher().extractPathWithinPattern(bestMatchPattern, path);
    }
}

2、前端部分代码

<template>
  <div>
    <el-form :model="dataForm" :rules="rules" ref="dataForm" label-width="100px" class="demo-dataForm">
      <el-form-item label="文章标题" prop="articleTitle" style="width: 304px">
        <el-input v-model="dataForm.articleTitle" placeholder="请输入文章标题"></el-input>
      </el-form-item>

      <el-form-item label="文章分类" prop="tabName">
        <el-radio-group v-model="dataForm.tabName">
          <el-radio v-for="tab in tabs" :label="tab.tabName"></el-radio>
        </el-radio-group>
      </el-form-item>

      <el-form-item label="文章标签" prop="articleLabel">
        <el-checkbox-group v-model="dataForm.articleLabel" style="display: flex;width: 400px;flex-wrap: wrap;">
          <el-checkbox v-for="label in labels" :style="{color:label.labelColor}" :label="label.labelId"
                       name="article_label">{{label.labelName}}
          </el-checkbox>
        </el-checkbox-group>
      </el-form-item>
      <el-form-item label="文章内容" style="">
        <div id="writing" style="width: 100%;height: 200%;z-index: inherit">
        </div>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm('dataForm')">立即创建</el-button>
      </el-form-item>
    </el-form>
    <div v-html="dataForm.articleContent">

    </div>
    <img :src="cover" alt="封面">
  </div>
</template>

<script>
  import axios from 'axios'
  // 创建一个富文本编辑框
  import E from 'tangeditor'

  export default {
    name: 'writing',
    data () {
      return {
        editor: null,
        tabs: [],
        labels: [],
        // 需要发送三个请求
        // 上传文件、上传文章全部的基础信息、上传文章标签关系。
        // 需要提交的内容
        dataForm: {
          articleTitle: '',
          tabName: '',
          articleContent: '',
          articleLabel: [],
          haveCover: false,
          articleCoverType: '',
          articleCoverImg: '',
        },
        rules: {
          articleTitle: [
            {required: true, message: '请输入文章标题', trigger: 'blur'}
            // {min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur'}
          ],
          tabName: [
            {required: true, message: '请选择文章分类', trigger: 'change'}
          ],
          articleLabel: [
            {type: 'array', required: true, message: '请至少选择一个标签', trigger: 'change'}
          ]
        }
      }
    },
    created () {
      this.getTabList()
      this.getLabelList()
    },
    mounted () {
      this.editor = new E('#writing')
      // 配置 onchange 回调函数,将数据同步到 vue 中
      this.editor.config.onchange = (newHtml) => {
        this.dataForm.articleContent = newHtml
      }
      // 创建编辑器
      this.editor.create()
      const _self = this
      /**
       * 监听视频上传
       * @param resultFiles 是 input 中选中的文件列表
       * @param insertImgFn 是获取图片 url 后,插入到编辑器的方法
       */
      this.editor.config.customUploadVideo = function (resultFiles, insertVideoFn) {
        console.log(resultFiles[0])
        console.log(insertVideoFn)
        let formData = new window.FormData()
        formData.append('file', resultFiles[0])
        axios({
          method: 'post',
          url: _self.$http.adornUrl(`/common/file/upload`),
          headers: {
            'Content-Type': 'multipart/form-data'
          },
          data: formData
        }).then((res) => {
          console.log(res.data.videoUrl)
          console.log(res.data.coverUrl)
          insertVideoFn(res.data.videoUrl)
          if (!_self.dataForm.haveCover) {
            _self.dataForm.articleCoverImg = res.data.coverUrl
            _self.dataForm.haveCover = true
            _self.dataForm.articleCoverType = 'video'
          }
        }).catch((err) => {
          console.log('难受', err)
        })
      }
      /**
       * 监听图片上传
       * @param resultFiles 是 input 中选中的文件列表
       * @param insertImgFn 是获取图片 url 后,插入到编辑器的方法
       */
      this.editor.config.customUploadImg = function (resultFiles, insertImgFn) {
        console.log(resultFiles[0])
        let formData = new window.FormData()
        formData.append('file', resultFiles[0])
        axios({
          method: 'post',
          url: _self.$http.adornUrl(`/common/file/upload`),
          headers: {
            'Content-Type': 'multipart/form-data'
          },
          data: formData
        }).then((res) => {
          console.log(res.data.imgUrl)
          insertImgFn(res.data.imgUrl)
          if (!_self.dataForm.haveCover) {
            _self.dataForm.articleCoverImg = res.data.imgUrl
            _self.dataForm.haveCover = true
            _self.dataForm.articleCoverType = 'image'
          }
        }).catch((err) => {
          console.log('难受', err)
        })
      }

    },
    methods: {
      getTabList () {
        this.$http({
          url: this.$http.adornUrl('/home/tab/list'),
          method: 'get'
        }).then(({data}) => {
          if (data && data.code === 0) {
            this.tabs = data.page.list
            console.log('tabs', this.tabs)
          } else {
            this.tabs = []
          }
        })
      },
      getLabelList () {
        this.$http({
          url: this.$http.adornUrl('/home/label/list'),
          method: 'get'
        }).then(({data}) => {
          if (data && data.code === 0) {
            this.labels = data.page.list
            console.log('labels', this.labels)
          } else {
            this.labels = []
          }
        })
      },
      /**
       * 点击提交按钮
       * @param formName
       */
      submitForm (formName) {
        this.$refs[formName].validate((valid) => {
          if (valid) {
            console.log(this.dataForm)
            console.log(this.dataForm.articleLabel.toString())

            // 允许创建

            // 保存图片、视频

            // 存储文章内容和文章、标签关系
            this.saveArticle()
          } else {
            console.log('error submit!!')
            return false
          }
        })
      },
      saveArticle () {
        this.$http({
          url: this.$http.adornUrl(`/home/article/saveArticle`),
          method: 'post',
          data: this.$http.adornData({
            'articleId': this.dataForm.articleId || undefined,
            'articleTitle': this.dataForm.articleTitle,
            'tabName': this.dataForm.tabName,
            'articleLabels': this.dataForm.articleLabel.toString(),
            'articleCoverType': this.dataForm.articleCoverType,
            'articleCoverImg': this.dataForm.articleCoverImg,
            'articleContent': this.dataForm.articleContent,
            // 'articleWriterId': this.dataForm.articleWriterId,
            // 'articleWriterName': this.dataForm.articleWriterName,
            'createTime': new Date(),
            // 'editTime': this.dataForm.editTime,
          })
        }).then(({data}) => {
          if (data && data.code === 0) {
            this.$message({
              message: '操作成功',
              type: 'success',
              duration: 1500,
              onClose: () => {
                this.visible = false
                this.$emit('refreshDataList')
                this.dataForm = {
                  articleTitle: '',
                  tabName: '',
                  articleContent: '',
                  articleLabel: [],
                  haveCover: false,
                  articleCoverType: '',
                  articleCoverImg: '',
                }
              }
            })
          } else {
            this.$message.error(data.msg)
          }
        })
      },
    }
  }
</script>

<style scoped>
  .el-checkbox {
    margin: 0;
    margin-right: 30px;
  }

  /* table 样式 */
  table {
    border-top: 1px solid #ccc;
    border-left: 1px solid #ccc;
  }

  table td,
  table th {
    border-bottom: 1px solid #ccc;
    border-right: 1px solid #ccc;
    padding: 3px 5px;
  }

  table th {
    border-bottom: 2px solid #ccc;
    text-align: center;
  }

  /* blockquote 样式 */
  blockquote {
    display: block;
    border-left: 8px solid #d0e5f2;
    padding: 5px 10px;
    margin: 10px 0;
    line-height: 1.4;
    font-size: 100%;
    background-color: #f1f1f1;
  }

  /* code 样式 */
  code {
    display: inline-block;
    *display: inline;
    *zoom: 1;
    background-color: #f1f1f1;
    border-radius: 3px;
    padding: 3px 5px;
    margin: 0 3px;
  }

  pre code {
    display: block;
  }

  /* ul ol 样式 */
  ul, ol {
    margin: 10px 0 10px 20px;
  }
</style>

三、wangeditor小体验

1、选择图片后自动就上传了,但是如果不要图片或者视频,删除之后不会删除,就很尴尬。以后我应该会写一个方法,来删除没被引用的视频或者图片,写好之后发布出来供大家参考。

2、可以通过 this.editor.config来控制属性,挺人性化的,源码也很简单,非常轻量级。

3、今天一个同学来跟我聊着玩,我就疯狂安利wangeditor,又简单,又好改,最后同学介绍了我一个富文本编辑器,ckeditor。我用了一下,嗯,我真的是孤陋寡闻,之后写个人博客网站的时候,如果这个插件是免费的,我就再试试这个富文本编辑器,虽然一个网站用两个富文本编辑器有点傻逼,但是我就是想用用。你也可以看看ckeditor,挺漂亮的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值