Browse Source

新增视频组件提交,左滑优化提交

yewenwen 6 years ago
parent
commit
8c86c12499

BIN
src/assets/img/video-icon.png


File diff suppressed because it is too large
+ 539 - 537
src/config.json


+ 4 - 1
src/nutui.js

@@ -102,6 +102,8 @@ import "./packages/tabselect/tabselect.scss";
 import './packages/popup/popup.scss';
 import LuckDraw from "./packages/luckdraw/index.js";
 import "./packages/luckdraw/luckdraw.scss";
+import Video from "./packages/video/index.js";
+import "./packages/video/video.scss";
 
 const packages = {
   Cell,
@@ -153,7 +155,8 @@ const packages = {
   Popup,
   LeftSlip,
   TabSelect: TabSelect,
-  LuckDraw: LuckDraw
+  LuckDraw: LuckDraw,
+  Video: Video
 };
 
 const components = {};

+ 1 - 1
src/packages/leftslip/leftslip.vue

@@ -4,7 +4,7 @@
             <div
                 class="nut-leftslip-item-main"
                 @touchstart="touchStart($event)"
-                @touchmove="touchMove($event)"
+                @touchmove.stop.prevent="touchMove($event)"
                 @touchend="touchEnd($event)"
             >
                 <slot name="slip-main"></slot>

+ 65 - 0
src/packages/video/demo.vue

@@ -0,0 +1,65 @@
+<template>
+    <div>
+        <h4>基本用法</h4>
+        <div class="video-con">
+            <nut-video :sources="sources" :options="options"></nut-video>
+        </div>
+        <h4>自动播放</h4>
+        <div class="video-con">
+            <nut-video :sources="sources" :options="options2"></nut-video>
+        </div>
+        <h4>初始化静音</h4>
+        <div class="video-con">
+            <nut-video :sources="sources" :options="options3"></nut-video>
+        </div>
+        <h4>视频背景图</h4>
+        <p>行内展示,自动播放,循环播放,不可操作,静音</p>
+        <div class="video-con">
+            <nut-video :sources="sources" :options="options4"></nut-video>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    components: {},
+    data() {
+        return {
+            sources: [
+                {
+                    src:
+                        'https://storage.jd.com/about/big-final.mp4?Expires=3730193075&AccessKey=3LoYX1dQWa6ZXzQl&Signature=ViMFjz%2BOkBxS%2FY1rjtUVqbopbJI%3D',
+                    type: 'video/mp4'
+                }
+            ],
+
+            options: {
+                controls: true
+            },
+            options2: {
+                autoplay: true,
+                volume: 0.6,
+                poster: ''
+            },
+            options3: {
+                controls: true,
+                volume: 0.6,
+                poster: '',
+                muted: true
+            },
+            options4: {
+                autoplay: true,
+                volume: 0.6,
+                poster: '',
+                muted: true,
+                disabled: true,
+                playsinline: true,
+                loop: true
+            }
+        };
+    },
+    methods: {}
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 117 - 0
src/packages/video/doc.md

@@ -0,0 +1,117 @@
+# Video 视频
+
+视频播放器
+
+## 基本用法
+
+```html
+<nut-video :sources="sources" :options="options"></nut-video>
+```
+
+```javascript
+export default {
+  methods: {
+    data() {
+        return {
+            sources: [
+                {
+                    src:
+                        'https://storage.jd.com/about/big-final.mp4?Expires=3730193075&AccessKey=3LoYX1dQWa6ZXzQl&Signature=ViMFjz%2BOkBxS%2FY1rjtUVqbopbJI%3D',
+                    type: 'video/mp4'
+                }
+            ],
+            options: {
+                controls: true
+            },
+
+    },
+  }
+};
+```
+
+## 自动播放
+
+```html
+<nut-video :sources="sources" :options="options2"></nut-video>
+```
+
+```javascript
+export default {
+  methods: {
+    data() {
+        return {
+            options2: {
+                autoplay: true,
+                volume: 0.6,
+                poster: ''
+            },
+
+    },
+  }
+};
+```
+
+## 初始化静音
+
+```html
+<nut-video :sources="sources" :options="options3"></nut-video>
+```
+
+```javascript
+export default {
+  methods: {
+    data() {
+        return {
+            options3: {
+                controls: true,
+                volume: 0.6,
+                poster: '',
+                muted: true
+            },
+
+    },
+  }
+};
+```
+
+## 视频背景图
+
+行内展示,自动播放,循环播放,不可操作,静音
+
+```html
+<nut-video :sources="sources" :options="options4"></nut-video>
+```
+
+```javascript
+export default {
+  methods: {
+    data() {
+        return {
+            options4: {
+                autoplay: true,
+                volume: 0.6,
+                poster: '',
+                muted: true,
+                disabled: true,
+                playsinline: true,
+                loop: true
+            },
+
+    },
+  }
+};
+```
+
+## Prop
+
+| 字段                | 说明                                       | 类型    | 默认值   |
+| ------------------- | ------------------------------------------ | ------- | -------- |
+| sources             | 视频地址和类型设置                         | Array   | -        |
+| options             | 控制视频播放属性                           | Object  | required |
+| options.autoplay    | 是否自动播放                               | Boolean | false    |
+| options.poster      | 海报设置                                   | String  | -        |
+| options.loop        | 是否循环播放                               | Boolean | false    |
+| options.controls    | 是否展示操作栏                             | Boolean | true     |
+| options.muted       | 是否静音                                   | Boolean | false    |
+| options.disabled    | 是否自动播放                               | Boolean | false    |
+| options.playsinline | 是否设置为行内播放元素(解决安卓兼容问题) | Boolean | false    |

+ 8 - 0
src/packages/video/index.js

@@ -0,0 +1,8 @@
+import Video from './video.vue';
+import './video.scss';
+
+Video.install = function(Vue) {
+  Vue.component(Video.name, Video);
+};
+
+export default Video

+ 52 - 0
src/packages/video/video.scss

@@ -0,0 +1,52 @@
+.nut-video{
+    width: 100%;
+    height: 100%;
+    position: relative;
+    display: flex;
+    .nut-videoplayer{
+        width: 100%;
+    }
+    .playing-mask{
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        left: 0;
+        top: 0;
+        right: 0;
+        bottom: 0;
+    }
+    .nut-video-play-btn{
+        // display: none;
+        top: 50%;
+        left: 50%;
+        margin-left: -1em;
+        width: 2em;
+        border: 0;
+        background-color: rgba(0,0,0,.45);
+        color: #fff;
+        transition: border-color .4s,outline .4s,background-color .4s;
+        position: absolute;
+        padding: 0;
+        cursor: pointer;
+        opacity: 1;
+        background-color: rgba(0,0,0,.5);
+        font-size: 2.5em;
+        border-radius: 20%;
+        height: 1.4em;
+        line-height: 1.4em;
+        margin-top: -.7em;
+        text-align: center;
+        &:hover{
+            background-color: #cc181e;
+        }
+        &:before{
+            content: '';
+            background: url('../../assets/img/video-icon.png') no-repeat;
+            width: 40px;
+            height: 31px;
+            display: inline-block;
+            background-position: 0 0;
+            background-size: 218px 38px;
+        }
+    }
+}

+ 126 - 0
src/packages/video/video.vue

@@ -0,0 +1,126 @@
+<template>
+    <div class="nut-video">
+        <video
+            ref="video"
+            class="nut-videoplayer"
+            type="video/mp4"
+            :muted="options.muted"
+            :autoplay="options.autoplay"
+            :loop="options.loop"
+            :controls="options.controls"
+        >
+            <source v-for="source in sources" :src="source.src" :type="source.type" :key="source.src" />
+        </video>
+        <div class="playing-mask" @click="play">
+            <div class="nut-video-play-btn" v-show="!state.playing"></div>
+        </div>
+
+        <div class="nut-video-controller"></div>
+    </div>
+</template>
+<script>
+export default {
+    name: 'nut-video',
+    props: {
+        src: '',
+        playsinline: {
+            type: Boolean,
+            default: false
+        },
+
+        sources: Array,
+        options: {
+            type: Object,
+            default() {
+                return {
+                    autoplay: false, //是否自动播放
+                    volume: 0.9,
+                    poster: '',
+                    noScrub: false,
+                    loop: false,
+                    controls: true,
+                    muted: false, //是否静音
+                    disabled: false, //禁止操作
+                    playsinline: false //行内展示
+                };
+            },
+            required: true
+        }
+    },
+    data() {
+        return {
+            video: null,
+            initial: true, //控制封面的显示
+            showToolbox: false, //控制控制器和标题的显示
+            progress: 0, //进度
+            duration: 0, //总时长
+            state: {
+                contrlShow: true,
+                vol: 0.5, //音量
+                currentTime: 0, //当前时间
+                fullScreen: false,
+                playing: false //是否正在播放
+            }
+        };
+    },
+    mounted() {
+        this.init();
+        // this.$refs.video.addEventListener('durationchange', e => {
+        //     this.duration = e.target.duration;
+        // });
+        // this.$refs.video.addEventListener('timeupdate', e => {
+        //     this.currentTime = e.target.currentTime;
+        //     this.progress = (e.target.currentTime / this.duration) * 100;
+        // });
+        // this.$refs.video.addEventListener('playing', e => {
+        //     this.playing = true;
+        // });
+        // this.$refs.video.addEventListener('pause', e => {
+        //     this.playing = false;
+        // });
+        // this.$refs.video.addEventListener('ended', e => {
+        //     this.playing = false;
+        // });
+    },
+    methods: {
+        init() {
+            // this.video = this.$refs.video;
+            // let {autoplay, playsinline} = this.options;
+            if (this.options.autoplay) {
+                this.play();
+            }
+
+            if (this.options.playsinline) {
+                this.$refs.video.setAttribute('playsinline', this.options.playsinline);
+                this.$refs.video.setAttribute('webkit-playsinline', this.options.playsinline);
+                this.$refs.video.setAttribute('x5-playsinline', this.options.playsinline);
+                this.$refs.video.setAttribute('x5-video-player-type', 'h5');
+                // this.$refs.video.setAttribute('x5-video-player-fullscreen', false);
+            }
+        },
+        play() {
+            this.state.playing = !this.state.playing;
+            if (this.options.autoplay && this.options.disabled) {
+                this.state.playing = true;
+                return false;
+            }
+            if (this.$refs.video) {
+                if (this.state.playing) {
+                    this.$refs.video.play();
+                    // this.mouseLeaveVideo();
+                    this.$refs.video.addEventListener('timeupdate', this.timeline);
+                    this.$refs.video.addEventListener('ended', e => {
+                        this.state.playing = false;
+                        // this.video.pos.current = 0;
+                        // this.$refs.video.currentTime = 0;
+                    });
+                } else {
+                    this.$refs.video.pause();
+                }
+            }
+        },
+        togglePlay() {}
+    },
+    beforeDestroy() {}
+};
+</script>

+ 276 - 0
src/packages/video/video2.vue

@@ -0,0 +1,276 @@
+<template>
+    <div
+        class="video"
+        @pointermove.prevent="handleMouseMove($event)"
+        @pointerup.prevent="stopDragging"
+        @pointerleave="handleMouseLeave"
+        @pointerenter="handleMouseEnter"
+        ref="vcontainer"
+    >
+        <video class="video__player" ref="v" @timeupdate="handleTimeUpdate" @ended="handleEnd">
+            <source :src="videoSrc" />
+        </video>
+        <div class="controller" v-show="isControlVisible">
+            <div class="controller__progress-wrapper">
+                <div class="controller__progress" ref="p" @click="handleProgressClick($event)">
+                    <div class="controller__progress controller__progress--passed" :style="{width: videoProgressPercent}"></div>
+                    <div class="controller__dot" :style="{left: videoProgressPercent}" @pointerdown="startDragging($event)">
+                        <div class="controller__inner-dot"></div>
+                    </div>
+                </div>
+            </div>
+            <div class="controller__btn-wrapper">
+                <div class="controller__btn" @click="togglePlaying">
+                    <font-awesome-icon :icon="['fas', 'play']" v-if="isPaused"></font-awesome-icon>
+                    <font-awesome-icon :icon="['fas', 'pause']" v-else></font-awesome-icon>
+                </div>
+                <div class="controller__btn" @click="stopPlaying">
+                    <font-awesome-icon :icon="['fas', 'stop']"></font-awesome-icon>
+                </div>
+                <div class="controller__btn" @click="toggleMute">
+                    <font-awesome-icon :icon="['fas', 'volume-up']" v-if="isMuted"></font-awesome-icon>
+                    <font-awesome-icon :icon="['fas', 'volume-mute']" v-else></font-awesome-icon>
+                </div>
+                <div class="controller__timer">
+                    {{ videoTime }}
+                </div>
+                <div class="controller__btn controller__btn--fullscreen" @click="toggleFullscreen">
+                    <font-awesome-icon :icon="['fas', 'expand']"></font-awesome-icon>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+function secToTimer(originalSec) {
+    const min = Math.floor(originalSec / 60);
+    const sec = Math.floor(originalSec % 60);
+    const minStr = min < 10 ? `0${min}` : String(min);
+    const secStr = sec < 10 ? `0${sec}` : String(sec);
+    return `${minStr}:${secStr}`;
+}
+export default {
+    name: 'MyVideo',
+    props: ['videoSrc'],
+    data() {
+        return {
+            video: null,
+            isPaused: true,
+            isMuted: false,
+            videoTime: '00:00 / 00:00',
+            isDragging: false,
+            isControlVisible: false,
+            hidingEvent: null,
+            videoProgress: 0,
+            draggingStartX: 0,
+            dotOffsetX: 0,
+            progress: null
+        };
+    },
+    computed: {
+        videoProgressPercent() {
+            return `${this.videoProgress * 100}%`;
+        }
+    },
+    methods: {
+        toggleFullscreen() {
+            const isFullscreen = document.webkitIsFullScreen || document.fullscreen;
+            if (isFullscreen) {
+                const exitFunc = document.exitFullscreen || document.webkitExitFullscreen;
+                exitFunc.call(document);
+            } else {
+                const element = this.$refs.vcontainer;
+                const fullscreenFunc = element.requestFullscreen || element.webkitRequestFullScreen;
+                fullscreenFunc.call(element);
+            }
+        },
+        handleTimeUpdate() {
+            this.videoTime = this.refreshTime();
+            this.videoProgress = this.video.currentTime / this.video.duration;
+        },
+        refreshTime() {
+            if (!this.video) {
+                return `${secToTimer(0)} / ${secToTimer(0)}`;
+            }
+            const currTime = this.video.currentTime || 0;
+            const duration = this.video.duration || 0;
+            return `${secToTimer(currTime)} / ${secToTimer(duration)}`;
+        },
+        togglePlaying() {
+            if (this.video.paused) {
+                this.playVideo();
+            } else {
+                this.pauseVideo();
+            }
+        },
+        stopPlaying() {
+            this.video.currentTime = 0;
+            this.pauseVideo();
+        },
+        toggleMute() {
+            this.video.muted = !this.video.muted;
+            this.isMuted = this.video.muted;
+        },
+        handleEnd() {
+            this.pauseVideo();
+        },
+        playVideo() {
+            this.isPaused = false;
+            this.video.play();
+        },
+        pauseVideo() {
+            this.isPaused = true;
+            this.video.pause();
+        },
+        setProgress(x) {
+            const progressRect = this.progress.getBoundingClientRect();
+            let progressPercent = (x - progressRect.left) / progressRect.width;
+            if (progressPercent < 0) {
+                progressPercent = 0;
+            } else if (progressPercent > 1) {
+                progressPercent = 1;
+            }
+            this.video.currentTime = this.video.duration * progressPercent;
+        },
+        hideControlBar() {
+            const isFullscreen = document.webkitIsFullScreen || document.fullscreen;
+            if (isFullscreen) {
+                this.hideCursor();
+            }
+            this.isControlVisible = false;
+        },
+        showControlBar() {
+            this.isControlVisible = true;
+        },
+        hideCursor() {
+            document.body.style.cursor = 'none';
+        },
+        showCursor() {
+            document.body.style.cursor = 'default';
+        },
+        handleProgressClick(event) {
+            const clickX = event.clientX;
+            this.setProgress(clickX);
+        },
+        startDragging(event) {
+            this.pauseVideo();
+            this.isDragging = true;
+            this.draggingStartX = event.clientX;
+        },
+        moveDragging(event) {
+            if (this.isDragging) {
+                const offsetX = event.clientX - this.draggingStartX;
+                this.dotOffsetX = offsetX < 0 ? 0 : offsetX;
+                this.setProgress(event.clientX);
+            }
+        },
+        stopDragging() {
+            this.isDragging = false;
+            this.dotOffsetX = 0;
+        },
+        handleMouseMove(event) {
+            this.showControlBar();
+            this.showCursor();
+            if (this.hidingEvent !== null) {
+                clearInterval(this.hidingEvent);
+            }
+            this.hidingEvent = setInterval(this.hideControlBar, 3000);
+            this.moveDragging(event);
+        },
+        handleMouseLeave() {
+            this.hideControlBar();
+            this.stopDragging();
+        },
+        handleMouseEnter() {
+            this.showControlBar();
+        }
+    },
+    mounted() {
+        this.video = this.$refs.v;
+        this.progress = this.$refs.p;
+    }
+};
+</script>
+
+<style scoped>
+.video {
+    position: relative;
+}
+.video__player {
+    width: 100%;
+    height: 100%;
+    display: flex;
+}
+.controller {
+    flex-direction: column;
+    height: 50px;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: rgba(0, 0, 0, 0.5);
+}
+.controller__btn-wrapper {
+    position: relative;
+    height: calc(100% - 5px);
+    display: flex;
+    align-items: center;
+    color: #fff;
+    padding: 0 18px;
+}
+.controller__btn {
+    cursor: pointer;
+    transition: 0.5s;
+    margin: 0 20px;
+}
+.controller__btn:hover {
+    color: #409eff;
+}
+.controller__timer {
+    margin-left: 15px;
+}
+.controller__btn--fullscreen {
+    position: absolute;
+    right: 15px;
+}
+.controller__progress-wrapper {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+.controller__progress {
+    height: 5px;
+    position: relative;
+    width: calc(100% - 30px);
+    border-radius: 100px;
+    background: #dcdcdc;
+    cursor: pointer;
+}
+.controller__progress--passed {
+    position: absolute;
+    top: 0;
+    left: 0;
+    background: #409eff;
+}
+.controller__dot {
+    position: absolute;
+    z-index: 50;
+    left: 0;
+    top: -5px;
+    width: 15px;
+    height: 15px;
+    border-radius: 50%;
+    background-color: #fff;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+.controller__inner-dot {
+    width: 5px;
+    height: 5px;
+    border-radius: 50%;
+    background-color: #409eff;
+}
+</style>

+ 591 - 0
src/packages/video/video3.vue

@@ -0,0 +1,591 @@
+<style>
+.__cov-video-container {
+    position: relative;
+    width: 100%;
+    background-color: #000;
+}
+.__cov-video {
+    width: 100%;
+    height: 100%;
+    vertical-align: bottom;
+}
+.__cov-contrl-content {
+    position: absolute;
+    display: flex;
+    left: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.41);
+    height: 2rem;
+    width: 100%;
+    z-index: 2147483647;
+}
+.__cov-contrl-play-btn {
+    position: relative;
+    height: 100%;
+    background: none;
+    border: none;
+    height: 2rem;
+    width: 4rem;
+    outline: none;
+    vertical-align: top;
+}
+.__cov-contrl-play-btn:hover {
+    background-color: rgba(255, 255, 255, 0.27);
+}
+.__cov-contrl-play-btn-icon {
+    position: absolute;
+    height: 1rem;
+    width: 1rem;
+    top: 50%;
+    left: 50%;
+    margin-top: -0.5rem;
+    margin-left: -0.5rem;
+}
+.__cov-contrl-vol-btn-icon {
+    position: absolute;
+    height: 1.1rem;
+    width: 1.1rem;
+    top: 50%;
+    left: 50%;
+    margin-top: -0.55rem;
+    margin-left: -0.55rem;
+}
+.__cov-contrl-vol-slider {
+    position: relative;
+    display: inline-block;
+    height: 100%;
+    width: 6rem;
+    height: 2rem;
+    overflow: hidden;
+    transition: all 0.2s ease-in;
+}
+.__cov-contrl-vol-rail {
+    position: absolute;
+    top: 50%;
+    width: 6rem;
+    height: 0.1rem;
+    margin-top: -0.05rem;
+    background: #fff;
+}
+.__cov-contrl-vol-inner {
+    position: absolute;
+    display: inline-block;
+    left: 0;
+    top: 50%;
+    background: #fff;
+    width: 0.5rem;
+    height: 0.5rem;
+    border-radius: 50%;
+    margin-top: -0.25rem;
+    z-index: 2;
+    cursor: pointer;
+}
+.__cov-contrl-vol-box {
+    display: flex;
+}
+.__cov-contrl-video-slider {
+    position: relative;
+    display: inline-block;
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+    margin: 0 0.5rem;
+    transition: all 0.2s ease-in;
+}
+.__cov-contrl-video-slider.no-scrub {
+    pointer-events: none;
+}
+.__cov-contrl-video-rail {
+    position: absolute;
+    top: 50%;
+    width: 100%;
+    height: 0.1rem;
+    margin-top: -0.05rem;
+    background: rgba(255, 255, 255, 0.5);
+    overflow: hidden;
+}
+.__cov-contrl-video-rail-inner {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 0.1rem;
+    background: rgb(255, 255, 255);
+    transition: transform 0.2s;
+}
+.__cov-contrl-video-inner {
+    position: absolute;
+    display: inline-block;
+    left: 0;
+    top: 50%;
+    background: #fff;
+    width: 0.5rem;
+    height: 0.5rem;
+    border-radius: 50%;
+    margin-top: -0.25rem;
+    z-index: 2;
+    cursor: pointer;
+    transition: all 16ms;
+}
+.__cov-contrl-video-time {
+    padding: 0 1rem;
+}
+.__cov-contrl-video-time-text {
+    color: #fff;
+    line-height: 2rem;
+    font-size: 0.8rem;
+}
+::-webkit-media-controls {
+    display: none !important;
+}
+video::-webkit-media-controls {
+    display: none !important;
+}
+video::-webkit-media-controls-enclosure {
+    display: none !important;
+}
+.fade-transition {
+    transition: opacity 0.3s ease;
+}
+.fade-enter {
+    opacity: 1;
+}
+.fade-leave {
+    opacity: 0;
+}
+.hide-cursor {
+    cursor: none;
+}
+@media all and (max-width: 768px) {
+    .__cov-contrl-vol-slider {
+        width: 3rem;
+    }
+    .__cov-contrl-video-time {
+        padding: 0 0.2rem;
+    }
+    .__cov-contrl-vol-box .__cov-contrl-play-btn {
+        width: 2rem;
+    }
+}
+</style>
+<template>
+    <div id="app">
+        <div class="container">
+            <div class="__cov-video-container" @mouseenter="mouseEnterVideo" @mouseleave="mouseLeaveVideo">
+                <video :class="{'hide-cursor': !state.contrlShow}" class="__cov-video" :poster="options.poster">
+                    <source v-for="source in sources" :src="source.src" :type="source.type" :key="source.src" />
+                </video>
+                <div class="__cov-contrl-content" transition="fade" v-show="state.contrlShow">
+                    <button class="__cov-contrl-play-btn" @click="play">
+                        <svg
+                            class="__cov-contrl-play-btn-icon"
+                            v-show="!state.playing"
+                            viewBox="0 0 47 57"
+                            version="1.1"
+                            xmlns="http://www.w3.org/2000/svg"
+                        >
+                            <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+                            <title>Triangle 1</title>
+                            <desc>Created with Sketch.</desc>
+                            <defs></defs>
+                            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+                                <polygon id="Triangle-1" stroke="#FFFFFF" fill="#FFFFFF" points="1 56 1 1 47 28.5"></polygon>
+                            </g>
+                        </svg>
+                        <svg
+                            class="__cov-contrl-play-btn-icon"
+                            v-show="state.playing"
+                            viewBox="0 0 15 22"
+                            version="1.1"
+                            xmlns="http://www.w3.org/2000/svg"
+                        >
+                            <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+                            <title>Combined Shape</title>
+                            <desc>Created with Sketch.</desc>
+                            <defs>
+                                <path
+                                    d="M0,0.979149244 L5,0.979149244 L5,22 L0,22 L0,0.979149244 Z M10,0.979149244 L15,0.979149244 L15,22 L10,22 L10,0.979149244 Z"
+                                    id="path-1"
+                                ></path>
+                                <mask
+                                    id="mask-2"
+                                    maskContentUnits="userSpaceOnUse"
+                                    maskUnits="objectBoundingBox"
+                                    x="0"
+                                    y="0"
+                                    width="15"
+                                    height="21.0208508"
+                                    fill="white"
+                                >
+                                    <use xlink:href="#path-1"></use>
+                                </mask>
+                            </defs>
+                            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+                                <use
+                                    id="Combined-Shape"
+                                    stroke="#FFFFFF"
+                                    mask="url(#mask-2)"
+                                    stroke-width="2"
+                                    fill="#FFFFFF"
+                                    xlink:href="#path-1"
+                                ></use>
+                            </g>
+                        </svg>
+                    </button>
+                    <div
+                        class="__cov-contrl-video-slider"
+                        @click="slideClick"
+                        @mousedown="videoMove"
+                        :class="{'no-scrub': options.noScrub}"
+                    >
+                        <div class="__cov-contrl-video-inner" :style="{transform: `translate3d(${video.pos.current}px, 0, 0)`}"></div>
+                        <div class="__cov-contrl-video-rail">
+                            <div
+                                class="__cov-contrl-video-rail-inner"
+                                :style="{transform: 'translate3d(' + video.loaded + '%, 0, 0)'}"
+                            ></div>
+                        </div>
+                    </div>
+                    <div class="__cov-contrl-video-time">
+                        <span class="__cov-contrl-video-time-text">{{ video.displayTime }}</span>
+                    </div>
+                    <div class="__cov-contrl-vol-box">
+                        <button class="__cov-contrl-play-btn" @click="volMuted">
+                            <svg
+                                class="__cov-contrl-vol-btn-icon"
+                                viewBox="0 0 41 44"
+                                version="1.1"
+                                xmlns="http://www.w3.org/2000/svg"
+                                xmlns:xlink="http://www.w3.org/1999/xlink"
+                            >
+                                <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+                                <title>vol</title>
+                                <desc>Created with Sketch.</desc>
+                                <defs>
+                                    <path
+                                        d="M8.61522369,12 L20,0.615223689 L20,37.3847763 L8.61522369,26 L1.99201702,26 C0.891856397,26 0,25.1029399 0,23.9941413 L0,14.0058587 C0,12.8980535 0.900176167,12 1.99201702,12 L8.61522369,12 L8.61522369,12 Z"
+                                        id="cov-vol"
+                                    ></path>
+                                </defs>
+                                <g id="Page-1" stroke="none" stroke-width="2" fill="none" fill-rule="evenodd">
+                                    <g id="vol" transform="translate(2.000000, 3.000000)">
+                                        <g id="cov-vol-icon">
+                                            <g id="Combined-Shape-Clipped">
+                                                <path
+                                                    v-show="volume.percent > 1 && !volume.muted"
+                                                    d="M25,29.5538997 C28.4589093,27.6757536 31.2629093,23.2984641 31.2629093,19.7769499 C31.2629093,16.2554357 28.4589093,11.8781461 25,10"
+                                                    id="vol-range-2"
+                                                    stroke="#FFFFFF"
+                                                ></path>
+                                                <path
+                                                    v-show="volume.percent > 70 && !volume.muted"
+                                                    d="M28,35.5538997 C33.5816016,32.5231573 38.1063837,25.4595762 38.1063837,19.7769499 C38.1063837,14.0943235 33.5816016,7.03074247 28,4"
+                                                    id="vol-range-2"
+                                                    stroke="#FFFFFF"
+                                                ></path>
+                                                <mask id="mask-2" fill="white">
+                                                    <use xlink:href="#cov-vol"></use>
+                                                </mask>
+                                                <use id="vol-path" stroke="#FFFFFF" stroke-width="3" xlink:href="#cov-vol"></use>
+                                                <g id="Combined-Shape" mask="url(#mask-2)" stroke="#FFFFFF" stroke-width="2" fill="#FFFFFF">
+                                                    <path
+                                                        d="M8.61522369,12 L20,0.615223689 L20,37.3847763 L8.61522369,26 L1.99201702,26 C0.891856397,26 0,25.1029399 0,23.9941413 L0,14.0058587 C0,12.8980535 0.900176167,12 1.99201702,12 L8.61522369,12 L8.61522369,12 Z"
+                                                        id="cov-vol"
+                                                    ></path>
+                                                </g>
+                                            </g>
+                                        </g>
+                                    </g>
+                                </g>
+                            </svg>
+                        </button>
+                        <div class="__cov-contrl-vol-slider" @click="volSlideClick" @mousedown="volMove">
+                            <div class="__cov-contrl-vol-inner" :style="{transform: `translate3d(${volume.pos.current}px, 0, 0)`}"></div>
+                            <div class="__cov-contrl-vol-rail"></div>
+                        </div>
+                    </div>
+                    <button class="__cov-contrl-play-btn" @click="fullScreen">
+                        <svg
+                            class="__cov-contrl-vol-btn-icon"
+                            viewBox="0 0 33 33"
+                            version="1.1"
+                            xmlns="http://www.w3.org/2000/svg"
+                            xmlns:xlink="http://www.w3.org/1999/xlink"
+                        >
+                            <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
+                            <defs></defs>
+                            <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+                                <path
+                                    d="M31.1682064,22 L31.1682064,31.0073537 L22,31.0073537 M22,1 L31.0073537,1 L31.0073537,10.1682064 M1,10.0073537 L1,1 L10.1682064,1 M10.0073537,31.1682064 L1,31.1682064 L1,22"
+                                    id="Combined-Shape"
+                                    stroke="#FFFFFF"
+                                    stroke-width="2"
+                                ></path>
+                            </g>
+                        </svg>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+const getMousePosition = function(e, type = 'x') {
+    if (type === 'x') {
+        return e.pageX;
+    }
+    return e.pageY;
+};
+const pad = val => {
+    val = Math.floor(val);
+    if (val < 10) {
+        return '0' + val;
+    }
+    return val + '';
+};
+const timeParse = sec => {
+    let min = 0;
+    min = Math.floor(sec / 60);
+    sec = sec - min * 60;
+    return pad(min) + ':' + pad(sec);
+};
+export default {
+    props: {
+        sources: Array,
+        options: {
+            type: Object,
+            default() {
+                return {
+                    autoplay: false,
+                    volume: 0.9,
+                    poster: '',
+                    noScrub: false
+                };
+            }
+        }
+    },
+    data() {
+        return {
+            $video: null,
+            video: {
+                $videoSlider: null,
+                len: 0,
+                current: 0,
+                loaded: 0,
+                moving: false,
+                displayTime: '00:00',
+                pos: {
+                    start: 0,
+                    width: 0,
+                    innerWidth: 0,
+                    current: 0
+                }
+            },
+            volume: {
+                $volBox: null,
+                muted: false,
+                percent: 60,
+                moving: false,
+                pos: {
+                    start: 0,
+                    width: 0,
+                    innerWidth: 0,
+                    current: 0
+                }
+            },
+            player: {
+                $player: null,
+                pos: null
+            },
+            tmp: {
+                contrlHideTimer: null
+            },
+            state: {
+                contrlShow: true,
+                vol: 0.5,
+                currentTime: 0,
+                fullScreen: false,
+                playing: false
+            }
+        };
+    },
+    ready() {
+        this.init();
+    },
+    mounted() {
+        this.init();
+    },
+    beforeDestroy() {
+        document.body.removeEventListener('mousemove', this.mouseMoveAction);
+        document.body.removeEventListener('mouseup', this.mouseUpAction);
+    },
+    methods: {
+        init() {
+            this.$video = this.$el.getElementsByTagName('video')[0];
+            this.initCore();
+            if (this.options.autoplay) {
+                this.play();
+            }
+            document.body.addEventListener('mousemove', this.mouseMoveAction, false);
+            document.body.addEventListener('mouseup', this.mouseUpAction, false);
+        },
+        initCore() {
+            this.initVol();
+            this.initVideo();
+            this.initPlayer();
+            const vol = this.options.volume || 0.9;
+            this.setVol(vol);
+        },
+        initPlayer() {
+            const $player = this.$el.getElementsByClassName('__cov-video-container')[0];
+            this.player.pos = $player.getBoundingClientRect();
+            this.player.$player = $player;
+        },
+        initVol() {
+            const $volBox = this.$el.getElementsByClassName('__cov-contrl-vol-slider')[0];
+            const $volInner = $volBox.getElementsByClassName('__cov-contrl-vol-inner')[0];
+            this.volume.$volBox = $volBox;
+            this.volume.pos.innerWidth = $volInner.getBoundingClientRect().width;
+            this.volume.pos.start = $volBox.getBoundingClientRect().left;
+            this.volume.pos.width = $volBox.getBoundingClientRect().width - this.volume.pos.innerWidth;
+        },
+        initVideo() {
+            const $videoSlider = this.$el.getElementsByClassName('__cov-contrl-video-slider')[0];
+            const $videoInner = $videoSlider.getElementsByClassName('__cov-contrl-video-inner')[0];
+            this.$videoSlider = $videoSlider;
+            this.video.pos.start = $videoSlider.getBoundingClientRect().left;
+            this.video.pos.innerWidth = $videoInner.getBoundingClientRect().width;
+            this.video.pos.width = $videoSlider.getBoundingClientRect().width - this.video.pos.innerWidth;
+            this.getTime();
+        },
+        mouseEnterVideo() {
+            if (this.tmp.contrlHideTimer) {
+                clearTimeout(this.tmp.contrlHideTimer);
+                this.tmp.contrlHideTimer = null;
+            }
+            this.state.contrlShow = true;
+        },
+        mouseLeaveVideo(e) {
+            if (this.tmp.contrlHideTimer) {
+                clearTimeout(this.tmp.contrlHideTimer);
+            }
+            this.tmp.contrlHideTimer = setTimeout(() => {
+                this.state.contrlShow = false;
+                this.tmp.contrlHideTimer = null;
+            }, 2000);
+        },
+        toggleContrlShow() {
+            this.state.contrlShow = !this.state.contrlShow;
+        },
+        getTime() {
+            this.$video.addEventListener('durationchange', e => {
+                console.log(e);
+            });
+            this.$video.addEventListener('progress', e => {
+                this.video.loaded = (-1 + this.$video.buffered.end(0) / this.$video.duration) * 100;
+            });
+            this.video.len = this.$video.duration;
+        },
+        setVideoByTime(percent) {
+            this.$video.currentTime = Math.floor(percent * this.video.len);
+        },
+        play() {
+            this.state.playing = !this.state.playing;
+            if (this.$video) {
+                if (this.state.playing) {
+                    this.$video.play();
+                    this.mouseLeaveVideo();
+                    this.$video.addEventListener('timeupdate', this.timeline);
+                    this.$video.addEventListener('ended', e => {
+                        this.state.playing = false;
+                        this.video.pos.current = 0;
+                        this.$video.currentTime = 0;
+                    });
+                } else {
+                    this.$video.pause();
+                }
+            }
+        },
+        timeline() {
+            const percent = this.$video.currentTime / this.$video.duration;
+            this.video.pos.current = (this.video.pos.width * percent).toFixed(3);
+            this.video.displayTime = timeParse(this.$video.duration - this.$video.currentTime);
+            if (percent === 1) {
+                this.$emit('video-ended', true);
+            }
+        },
+        volMove(e) {
+            this.initVol();
+            this.volume.moving = true;
+        },
+        videoMove(e) {
+            this.initVideo();
+            this.video.moving = true;
+        },
+        slideClick(e) {
+            this.videoSlideMove(e);
+        },
+        volSlideClick(e) {
+            this.volSlideMove(e);
+        },
+        volMuted() {
+            this.$video.muted = !this.$video.muted;
+            this.volume.muted = this.$video.muted;
+        },
+        setVol(val) {
+            if (this.$video) {
+                this.volume.pos.current = val * this.volume.pos.width;
+                this.volume.percent = val * 100;
+                this.$video.volume = val;
+            }
+        },
+        fullScreen() {
+            if (!this.state.fullScreen) {
+                this.state.fullScreen = true;
+                this.$video.webkitRequestFullScreen();
+            } else {
+                this.state.fullScreen = false;
+                document.webkitCancelFullScreen();
+            }
+            setTimeout(this.initVideo, 200);
+        },
+        mouseMoveAction(e) {
+            if (this.volume.moving) {
+                this.volSlideMove(e);
+            }
+            if (this.video.moving) {
+                this.videoSlideMove(e);
+            }
+            this.contrlHider(e);
+        },
+        contrlHider(e) {
+            const x = getMousePosition(e, 'x');
+            const y = getMousePosition(e, 'y');
+            if (!this.player.pos) return;
+            if (x > this.player.pos.left && x < this.player.pos.left + this.player.pos.width) {
+                if (y > this.player.pos.top + this.player.pos.height * 0.6 && y < this.player.pos.top + this.player.pos.height) {
+                    return this.mouseEnterVideo();
+                }
+            }
+            return this.mouseLeaveVideo();
+        },
+        volSlideMove(e) {
+            const x = getMousePosition(e) - this.volume.pos.start;
+            if (x > 0 && x < this.volume.pos.width) {
+                this.setVol(x / this.volume.pos.width);
+            }
+        },
+        videoSlideMove(e) {
+            const x = getMousePosition(e) - this.video.pos.start;
+            if (x > 0 && x < this.video.pos.width) {
+                this.video.pos.current = x;
+                this.setVideoByTime(x / this.video.pos.width);
+            }
+        },
+        mouseUpAction(e) {
+            this.volume.moving = false;
+            this.video.moving = false;
+        }
+    }
+};
+</script>

+ 520 - 0
src/packages/video/videotest.vue

@@ -0,0 +1,520 @@
+<template>
+    <div class="video-player">
+        <!-- 播放器界面; 兼容ios  controls-->
+        <video
+            ref="video"
+            v-if="showVideo"
+            webkit-playsinline="true"
+            playsinline="true"
+            x-webkit-airplay="true"
+            x5-video-player-type="h5"
+            x5-video-player-fullscreen="true"
+            x5-video-orientation="portraint"
+            style="object-fit:fill"
+            preload="auto"
+            muted="true"
+            poster="https://photo.mac69.com/180205/18020526/a9yPQozt0g.jpg"
+            :src="src"
+            @waiting="handleWaiting"
+            @canplaythrough="state.isLoading = false"
+            @playing="(state.isLoading = false), (state.controlBtnShow = false), (state.playing = true)"
+            @stalled="state.isLoading = true"
+            @error="handleError"
+            >您的浏览器不支持HTML5</video
+        >
+        <!-- 兼容Android端层级问题, 弹出层被覆盖 -->
+        <img v-show="!showVideo || state.isEnd" class="poster" src="https://photo.mac69.com/180205/18020526/a9yPQozt0g.jpg" alt />
+        <!-- 控制窗口 -->
+        <div class="control" v-show="!state.isError" ref="control" @touchstart="touchEnterVideo" @touchend="touchLeaveVideo">
+            <!-- 播放 || 暂停 || 加载中-->
+            <div class="play" @touchstart.stop="clickPlayBtn" v-show="state.controlBtnShow">
+                <img
+                    v-show="!state.playing && !state.isLoading"
+                    src="https://img10.360buyimg.com/imagetools/jfs/t1/32964/16/6517/1174/5c8f3cabE260d5fc4/036d6af59b6df77c.png"
+                />
+                <img
+                    v-show="state.playing && !state.isLoading"
+                    src="https://img14.360buyimg.com/imagetools/jfs/t1/24379/23/11486/739/5c8f3cabE9e633c19/826703b8fe47deb9.png"
+                />
+                <div class="loader" v-show="state.isLoading">
+                    <div class="loader-inner ball-clip-rotate">
+                        <div></div>
+                    </div>
+                </div>
+            </div>
+            <!-- 控制条 -->
+            <!-- <div class="control-bar" v-show="state.controlBarShow"> -->
+            <div class="control-bar" :style="{visibility: state.controlBarShow ? 'visible' : 'hidden'}">
+                <span class="time">{{ video.displayTime }}</span>
+                <span class="progress" ref="progress">
+                    <img
+                        class="progress-btn ignore"
+                        :style="{transform: `translate3d(${video.progress.current}px, 0, 0)`}"
+                        src="../../assets/img/cursor.gif"
+                    />
+                    <span class="progress-loaded" :style="{width: `${video.loaded}%`}"></span>
+                    <!-- 设置手动移动的进度条 -->
+                    <span
+                        class="progress-move"
+                        @touchmove.stop.prevent="moveIng($event)"
+                        @touchstart.stop="moveStart($event)"
+                        @touchend.stop="moveEnd($event)"
+                    ></span>
+                </span>
+
+                <span class="total-time">{{ video.totalTime }}</span>
+                <span class="full-screen" @click="fullScreen">
+                    <img src="../../assets/img/w-icon.png" alt />
+                </span>
+            </div>
+        </div>
+        <!-- 错误弹窗 -->
+        <div class="error" v-show="state.isError">
+            <p class="lose">视频加载失败</p>
+            <p class="retry" @click="retry">点击重试</p>
+        </div>
+    </div>
+</template>
+<script>
+// import {throttle} from '@/common/js/tool';
+
+export const throttle = (func, wait, type) => {
+    if (type === 1) {
+        var previous = 0;
+    } else if (type === 2) {
+        var timeout;
+    }
+    return function() {
+        let context = this;
+        let args = arguments;
+        if (type === 1) {
+            let now = Date.now();
+
+            if (now - previous > wait) {
+                func.apply(context, args);
+                previous = now;
+            }
+        } else if (type === 2) {
+            if (!timeout) {
+                timeout = setTimeout(() => {
+                    timeout = null;
+                    func.apply(context, args);
+                }, wait);
+            }
+        }
+    };
+};
+
+const pad = val => {
+    val = Math.floor(val);
+    if (val < 10) {
+        return '0' + val;
+    }
+    return val + '';
+};
+const timeParse = sec => {
+    let min = 0;
+    min = Math.floor(sec / 60);
+    sec = sec - min * 60;
+    return pad(min) + ':' + pad(sec);
+};
+export default {
+    name: 'nut-video',
+    props: {
+        src: {
+            type: String,
+            required: true
+        },
+        showVideo: {
+            // 兼容android弹出层
+            type: Boolean,
+            default: true
+        }
+    },
+    data() {
+        return {
+            // video元素
+            $video: null,
+            // 视频容器元素
+            player: {
+                $player: null,
+                pos: null
+            },
+            // progress进度条元素
+            progressBar: {
+                $progress: null, // 进度条DOM对象
+                pos: null
+            },
+            // video控制显示设置
+            video: {
+                loaded: 0, // 缓存长度
+                displayTime: '00:00', // 进度时间
+                totalTime: '00:00', // 总时间
+                progress: {
+                    width: 0, // 进度条长度
+                    current: 0 // 进度条当前位置
+                }
+            },
+            // 定时器
+            hideTimer: null,
+            // 播放状态控制
+            state: {
+                controlBtnShow: true, // 播放按钮
+                controlBarShow: false, // 控制条
+                fullScreen: false,
+                playing: false,
+                isLoading: false,
+                isEnd: false,
+                isError: false
+            },
+            // 首次触摸
+            isFirstTouch: true,
+            // 暂停状态触摸
+            isPauseTouch: false
+        };
+    },
+    methods: {
+        init() {
+            // 初始化video,获取video元素
+            this.$video = this.$el.getElementsByTagName('video')[0];
+            this.initPlayer();
+        },
+        // 初始化播放器容器, 获取video-player元素
+        // getBoundingClientRect()以client可视区的左上角为基点进行位置计算
+        initPlayer() {
+            const $player = this.$el;
+            const $progress = this.$el.getElementsByClassName('progress')[0];
+            // 播放器位置
+            this.player.$player = $player;
+            this.progressBar.$progress = $progress;
+            this.player.pos = $player.getBoundingClientRect();
+            this.progressBar.pos = $progress.getBoundingClientRect();
+            this.video.progress.width = Math.round($progress.getBoundingClientRect().width);
+        },
+        // 点击播放 & 暂停按钮
+        clickPlayBtn() {
+            if (this.state.isLoading) return;
+            this.isFirstTouch = false;
+            this.state.playing = !this.state.playing;
+            this.state.isEnd = false;
+            if (this.$video) {
+                // 播放状态
+                if (this.state.playing) {
+                    try {
+                        this.$video.play();
+                        this.isPauseTouch = false;
+                        // 监听缓存进度
+                        this.$video.addEventListener('progress', e => {
+                            this.getLoadTime();
+                        });
+                        // 监听播放进度
+                        this.$video.addEventListener('timeupdate', throttle(this.getPlayTime, 100, 1));
+                        // 监听结束
+                        this.$video.addEventListener('ended', e => {
+                            // 重置状态
+                            this.state.playing = false;
+                            this.state.isEnd = true;
+                            this.state.controlBtnShow = true;
+                            this.video.displayTime = '00:00';
+                            this.video.progress.current = 0;
+                            this.$video.currentTime = 0;
+                            this.src = this.src;
+                        });
+                    } catch (e) {
+                        // 捕获url异常出现的错误
+                    }
+                }
+                // 停止状态
+                else {
+                    this.isPauseTouch = true;
+                    this.$video.pause();
+                }
+            }
+        },
+        // 触碰播放区
+        touchEnterVideo() {
+            if (this.isFirstTouch) return;
+            if (this.hideTimer) {
+                clearTimeout(this.hideTimer);
+                this.hideTimer = null;
+            }
+            this.state.controlBtnShow = true;
+            this.state.controlBarShow = true;
+        },
+        // 离开播放区
+        touchLeaveVideo() {
+            if (this.isFirstTouch) return;
+            if (this.hideTimer) {
+                clearTimeout(this.hideTimer);
+            }
+            // 暂停触摸, 不隐藏
+            if (this.isPauseTouch) {
+                this.state.controlBtnShow = true;
+                this.state.controlBarShow = true;
+            } else {
+                this.hideTimer = setTimeout(() => {
+                    this.state.controlBarShow = false;
+                    // 加载中只显示loading
+                    if (this.state.isLoading) {
+                        this.state.controlBtnShow = true;
+                    } else {
+                        this.state.controlBtnShow = false;
+                    }
+                    this.hideTimer = null;
+                }, 3000);
+            }
+        },
+        // 等待数据加载
+        handleWaiting() {
+            this.state.controlBtnShow = true;
+            this.state.isLoading = true;
+        },
+        // 数据加载出错
+        handleError() {
+            this.state.isError = true;
+        },
+        // 点击重新加载
+        retry() {
+            this.state.isError = false;
+            this.init();
+        },
+        // 获取播放时间
+        getPlayTime() {
+            const percent = this.$video.currentTime / this.$video.duration;
+            this.video.progress.current = Math.round(this.video.progress.width * percent);
+            // 赋值时长
+            this.video.totalTime = timeParse(this.$video.duration);
+            this.video.displayTime = timeParse(this.$video.currentTime);
+        },
+        // 获取缓存时间
+        getLoadTime() {
+            // console.log('缓存了...',this.$video.buffered.end(0));
+            this.video.loaded = (this.$video.buffered.end(0) / this.$video.duration) * 100;
+        },
+        // 手动调节播放进度
+        moveStart(e) {},
+        moveIng(e) {
+            // console.log("触摸中...");
+            let currentX = e.targetTouches[0].pageX;
+            let offsetX = currentX - this.progressBar.pos.left;
+            // 边界检测
+            if (offsetX <= 0) {
+                offsetX = 0;
+            }
+            if (offsetX >= this.video.progress.width) {
+                offsetX = this.video.progress.width;
+            }
+            this.video.progress.current = offsetX;
+
+            let percent = this.video.progress.current / this.video.progress.width;
+            this.$video.duration && this.setPlayTime(percent, this.$video.duration);
+        },
+        moveEnd(e) {
+            // console.log("触摸结束...");
+            let currentX = e.changedTouches[0].pageX;
+            let offsetX = currentX - this.progressBar.pos.left;
+            this.video.progress.current = offsetX;
+            // 这里的offsetX都是正数
+            let percent = offsetX / this.video.progress.width;
+            this.$video.duration && this.setPlayTime(percent, this.$video.duration);
+        },
+        // 设置手动播放时间
+        setPlayTime(percent, totalTime) {
+            this.$video.currentTime = Math.floor(percent * totalTime);
+        },
+        // 设置全屏
+        fullScreen() {
+            console.log('点击全屏...');
+            if (!this.state.fullScreen) {
+                this.state.fullScreen = true;
+                this.$video.webkitRequestFullScreen();
+            } else {
+                this.state.fullScreen = false;
+                document.webkitCancelFullScreen();
+            }
+        }
+    },
+    mounted() {
+        setTimeout(() => {
+            this.init();
+        }, 20);
+    },
+    beforeDestroy() {}
+};
+</script>
+<style lang="scss" scoped>
+@keyframes rotate {
+    0% {
+        transform: rotate(0deg);
+    }
+    50% {
+        transform: rotate(180deg);
+    }
+    100% {
+        transform: rotate(360deg);
+    }
+}
+.loader {
+    width: 58px;
+    height: 58px;
+    background: rgba(15, 16, 17, 0.3);
+    border-radius: 50%;
+    position: relative;
+    .ball-clip-rotate {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        > div {
+            width: 15px;
+            height: 15px;
+            border-radius: 100%;
+            margin: 2px;
+            animation-fill-mode: both;
+            border: 2px solid #fff;
+            border-bottom-color: transparent;
+            height: 26px;
+            width: 26px;
+            background: transparent !important;
+            display: inline-block;
+            animation: rotate 0.75s 0s linear infinite;
+        }
+    }
+}
+.video-player {
+    width: 100%;
+    height: 188px;
+    overflow: hidden;
+    position: relative;
+    // 封面
+    .poster {
+        position: absolute;
+        z-index: 1;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+    }
+    video {
+        border-radius: 10px 10px 0 0;
+        width: 100%;
+        height: 100%;
+    }
+    // 控制层
+    .control {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        z-index: 2;
+        left: 0;
+        top: 0;
+        background-color: transparent;
+        .play {
+            width: 58px;
+            height: 58px;
+            position: absolute;
+            z-index: 3;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            img {
+                width: 100%;
+                height: 100%;
+            }
+        }
+        &-bar {
+            position: absolute;
+            bottom: 8px;
+            left: 50%;
+            transform: translateX(-50%);
+            height: 26px;
+            display: flex;
+            align-items: center;
+            span {
+                font-size: 12px;
+                color: #fff;
+                font-weight: 500;
+                white-space: nowrap;
+            }
+            .progress {
+                position: relative;
+                margin: 0 8px;
+                width: 203px;
+                height: 2px;
+                background: rgba(255, 255, 255, 0.4);
+                border-radius: 2px;
+                .progress-move {
+                    width: 100%;
+                    height: 26px;
+                    position: absolute;
+                    z-index: 100;
+                    left: 0;
+                    top: 0;
+                    transform: translateY(-50%);
+                    background-color: transparent;
+                }
+                .progress-btn {
+                    position: absolute;
+                    left: -10px;
+                    top: -16px;
+                    transition: all 16ms;
+                }
+                // 不会被转义
+                .ignore {
+                    width: 18px;
+                    height: 18px;
+                }
+                .progress-loaded {
+                    background-color: #fff;
+                    position: absolute;
+                    left: 0;
+                    top: 0;
+                    width: 0;
+                    height: 100%;
+                    border-radius: 2px;
+                    transition: all 16ms;
+                }
+            }
+            .full-screen {
+                width: 26px;
+                height: 26px;
+                margin-left: 8px;
+                img {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+        }
+    }
+    // 错误层
+    .error {
+        width: 100%;
+        height: 100%;
+        background: rgba(0, 0, 0, 0.84);
+        position: absolute;
+        left: 0;
+        top: 0;
+        z-index: 2;
+        .lose {
+            text-align: center;
+            font-size: 15px;
+            font-weight: 500;
+            color: rgba(255, 255, 255, 1);
+            line-height: 23px;
+            margin-top: 63px;
+            margin-bottom: 40px;
+        }
+        .retry {
+            margin: 0 auto;
+            text-align: center;
+            line-height: 38px;
+            width: 101px;
+            height: 38px;
+            background: rgba(255, 255, 255, 1);
+            border-radius: 19px;
+            font-size: 15px;
+            color: #008eff;
+        }
+    }
+}
+</style>

+ 1 - 0
types/nutui.d.ts

@@ -70,3 +70,4 @@ export declare class TabSelect extends UIComponent {}
 export declare class Popup extends UIComponent {}
 
 export declare class LuckDraw extends UIComponent {}
+export declare class Video extends UIComponent {}