Browse Source

feat: 新增弹幕组件 (#554)

* fix: 弹幕组件 && 适配小程序
Ymm0008 4 years ago
parent
commit
354535835d

+ 11 - 0
src/config.json

@@ -684,6 +684,17 @@
           "sort": 1,
           "show": true,
           "author": "yangxiaolu3"
+        },
+        {
+          "version": "3.0.0",
+          "taro": true,
+          "name": "Barrage",
+          "type": "component",
+          "cName": "弹幕组件",
+          "desc": "用于视频弹幕等展示",
+          "sort": 2,
+          "show": true,
+          "author": "Ymm0008"
         }
       ]
     }

+ 57 - 0
src/packages/__VUE/barrage/demo.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell>
+      <nut-barrage ref="danmu" :danmu="list"></nut-barrage>
+    </nut-cell>
+    <div class="test">
+      <nut-input label="文本" v-model="inputVal" />
+      <nut-button type="primary" @click="addDanmu">添加</nut-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+import { createComponent } from '../../utils/create';
+const { createDemo } = createComponent('barrage');
+export default createDemo({
+  props: {},
+  setup() {
+    const inputVal = ref<any>('');
+    const danmu = ref<any>(null);
+    let list = ref([
+      '画美不看',
+      '不明觉厉',
+      '喜大普奔',
+      '男默女泪',
+      '累觉不爱',
+      '爷青结'
+    ]);
+    function addDanmu() {
+      danmu.value.add(inputVal.value);
+    }
+    return {
+      inputVal,
+      danmu,
+      list,
+      addDanmu
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.nut-cell,
+.nut-barrage {
+  padding: 20px 0;
+  height: 120px;
+}
+.test {
+  .nut-input {
+    width: 80%;
+  }
+  display: flex;
+  justify-content: space-between;
+}
+</style>

+ 63 - 0
src/packages/__VUE/barrage/doc.md

@@ -0,0 +1,63 @@
+# Barrage 组件
+
+### 介绍
+
+用于话语和词组的轮播展示,适用于视频中或其他类似需求中。
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+import { Barrage } from '@nutui/nutui';
+
+const app = createApp();
+app.use(Barrage);
+
+```
+
+## 代码演示
+
+### 基础用法1
+
+`Icon` 的 `name` 属性支持传入图标名称或图片链接。
+
+```html
+<nut-barrage ref="danmu" :danmu="list"></nut-barrage>
+```
+``` javascript
+  setup() {
+    const inputVal = ref<any>('');
+    const danmu = ref<any>(null);
+    let list = ref(["画美不看", "不明觉厉", "喜大普奔", "男默女泪", "累觉不爱", "爷青结"]); 
+    function addDanmu() {
+      danmu.value.add(inputVal.value);
+    }
+    return {
+      inputVal,
+      danmu,
+      list,
+      addDanmu
+    };
+  }
+```
+
+
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| danmu         | 弹幕列表数据               | Array | []              |
+| frequency        | 可视区域内每个弹幕出现的时间间隔                         | Number | 200               |
+| speeds         | 每个弹幕的滚动时间 | Number |  2000               |
+| rows  | 弹幕行数,分几行展示     | Number | 1 |
+| top  | 弹幕垂直距离    | Number | 10 |
+| loop  | 是否循环播放     | Boolean | true |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| add  | 添加数据 | - |

+ 47 - 0
src/packages/__VUE/barrage/index.scss

@@ -0,0 +1,47 @@
+.nut-barrage {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  .dmitem {
+    display: block;
+    position: absolute;
+    right: 0;
+    padding: 5px 25px;
+    border-radius: 50px;
+    font-size: 12px;
+    text-align: center;
+    color: #fff;
+    white-space: pre;
+    transform: translateX(100%);
+    background: linear-gradient(
+      to right,
+      rgba(26, 25, 25, 0.15),
+      rgba(0, 0, 0, 0.2)
+    );
+    &.move {
+      will-change: transform;
+      animation-name: moving;
+      animation-timing-function: linear;
+      animation-play-state: running;
+    }
+  }
+  @keyframes moving {
+    from {
+      transform: translateX(100%);
+    }
+    to {
+      transform: translateX(var(--move-distance));
+    }
+  }
+  @-webkit-keyframes moving {
+    from {
+      -webkit-transform: translateX(100%);
+    }
+    to {
+      -webkit-transform: translateX(var(--move-distance));
+    }
+  }
+}

+ 138 - 0
src/packages/__VUE/barrage/index.taro.vue

@@ -0,0 +1,138 @@
+<template>
+  <view class="dmBody" :class="classes">
+    <view class="dmContainer" id="dmContainer"></view>
+    <!-- <view v-for="(item, index) of danmuList" :key="'danmu'+index" class="dmitem">
+      {{item}}
+    </view> -->
+  </view>
+</template>
+<script lang="ts">
+import {
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  toRefs,
+  watch,
+  nextTick
+} from 'vue';
+import Taro from '@tarojs/taro';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('barrage');
+
+export default create({
+  props: {
+    danmu: {
+      type: Array,
+      default: () => []
+    },
+    frequency: {
+      type: Number,
+      default: 200
+    },
+    speeds: {
+      type: Number,
+      default: 2000
+    },
+    rows: {
+      type: Number,
+      default: 3
+    },
+    top: {
+      type: Number,
+      default: 10
+    },
+    loop: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['click'],
+
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    let timer: number = 0;
+    const danmuList = ref<any[]>(props.danmu);
+    const rows = ref<number>(props.rows);
+    const top = ref<number>(props.top);
+    const index = ref<number>(0);
+    const speeds = props.speeds;
+
+    onMounted(() => {
+      run();
+    });
+
+    watch(
+      () => props.danmu,
+      (newValue, oldVlaue) => {
+        danmuList.value = [...newValue];
+      }
+    );
+
+    const add = (word: string) => {
+      const _index = index.value % danmuList.value.length;
+      danmuList.value.splice(_index, 0, word);
+    };
+
+    const run = () => {
+      clearInterval(timer);
+      timer = 0;
+      timer = setInterval(() => {
+        play();
+        run();
+      }, props.frequency);
+    };
+
+    const play = () => {
+      const query = Taro.createSelectorQuery();
+      let dmContainer: any = document.getElementById('dmContainer');
+      const _index = props.loop
+        ? index.value % danmuList.value.length
+        : index.value;
+      let el: any = document.createElement(`view`);
+      el.innerHTML = danmuList.value[_index] as string;
+      el.classList.add('dmitem');
+      dmContainer.appendChild(el);
+
+      let domList: Array<any> = [];
+      query.selectAll('.dmitem').boundingClientRect((recs: any) => {
+        domList = recs;
+      });
+
+      nextTick(() => {
+        query
+          .select('.dmBody')
+          .boundingClientRect((rec) => {
+            let danmuCWidth = rec.width;
+            // let width = domList[_index]['width'];
+            let height = domList[_index]['height'];
+            el.classList.add('move');
+            el.style.animationDuration = `${speeds}ms`;
+            el.style.top = (_index % rows.value) * (height + top.value) + 'px';
+            // el.style.width = width + 20 + 'px';
+            el.style.width = 'auto';
+            // el.style.left = "-"+(_index % rows.value) + 'px';
+            el.style.setProperty('--move-distance', `-${danmuCWidth}px`);
+            el.dataset.index = `${_index}`;
+            el.addEventListener('animationend', () => {
+              dmContainer.removeChild(el);
+            });
+            index.value++;
+          })
+          .exec();
+      });
+    };
+    return { classes, danmuList, add };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 128 - 0
src/packages/__VUE/barrage/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <view ref="dmBody" :class="classes">
+    <view ref="dmContainer" class="dmContainer"></view>
+    <!-- <view v-for="(item, index) of danmuList" :key="'danmu'+index" class="dmitem">
+      {{item}}
+    </view> -->
+  </view>
+</template>
+<script lang="ts">
+import {
+  computed,
+  onMounted,
+  ref,
+  reactive,
+  toRefs,
+  watch,
+  nextTick
+} from 'vue';
+import { createComponent } from '../../utils/create';
+const { componentName, create } = createComponent('barrage');
+
+export default create({
+  props: {
+    danmu: {
+      type: Array,
+      default: () => []
+    },
+    frequency: {
+      type: Number,
+      default: 200
+    },
+    speeds: {
+      type: Number,
+      default: 2000
+    },
+    rows: {
+      type: Number,
+      default: 3
+    },
+    top: {
+      type: Number,
+      default: 10
+    },
+    loop: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['click'],
+
+  setup(props, { emit }) {
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    let dmBody = ref<HTMLDivElement>(document.createElement('div'));
+    let dmContainer = ref<HTMLDivElement>(document.createElement('div'));
+
+    let timer: number = 0;
+    const danmuList = ref<any[]>(props.danmu);
+    const rows = ref<number>(props.rows);
+    const top = ref<number>(props.top);
+    const index = ref<number>(0);
+    const speeds = props.speeds;
+    const danmuCWidth = ref(0);
+
+    onMounted(() => {
+      danmuCWidth.value = dmBody.value.offsetWidth;
+      run();
+    });
+
+    watch(
+      () => props.danmu,
+      (newValue, oldVlaue) => {
+        danmuList.value = [...newValue];
+      }
+    );
+
+    const add = (word: string) => {
+      const _index = index.value % danmuList.value.length;
+      danmuList.value.splice(_index, 0, word);
+    };
+
+    const run = () => {
+      clearInterval(timer);
+      timer = 0;
+      timer = setInterval(() => {
+        play();
+        run();
+      }, props.frequency);
+    };
+
+    const play = () => {
+      const _index = props.loop
+        ? index.value % danmuList.value.length
+        : index.value;
+      let el = document.createElement(`view`);
+      el.innerHTML = danmuList.value[_index] as string;
+      el.classList.add('dmitem');
+      dmContainer.value.appendChild(el);
+
+      nextTick(() => {
+        const width = el.offsetWidth;
+        const height = el.offsetHeight;
+        el.classList.add('move');
+        el.style.animationDuration = `${speeds}ms`;
+        el.style.top = (_index % rows.value) * (height + top.value) + 'px';
+        el.style.width = width + 20 + 'px';
+        // el.style.left = "-"+(_index % rows.value) + 'px';
+        el.style.setProperty('--move-distance', `-${danmuCWidth.value}px`);
+        el.dataset.index = `${_index}`;
+        el.addEventListener('animationend', () => {
+          dmContainer.value.removeChild(el);
+        });
+        index.value++;
+      });
+    };
+    return { classes, danmuList, dmBody, dmContainer, add };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 23 - 0
src/sites/mobile-taro/vue/project.private.config.json

@@ -0,0 +1,23 @@
+{
+  "condition": {
+    "plugin": {
+      "list": []
+    },
+    "game": {
+      "list": []
+    },
+    "gamePlugin": {
+      "list": []
+    },
+    "miniprogram": {
+      "list": [
+        {
+          "name": "business/pages/barrage/index",
+          "pathName": "business/pages/barrage/index",
+          "query": "",
+          "scene": null
+        }
+      ]
+    }
+  }
+}

+ 1 - 1
src/sites/mobile-taro/vue/src/app.config.ts

@@ -57,7 +57,7 @@ export default {
     },
     {
       root: 'business',
-      pages: ['pages/address/index']
+      pages: ['pages/address/index', 'pages/barrage/index']
     }
   ],
   window: {

+ 3 - 0
src/sites/mobile-taro/vue/src/business/pages/barrage/index.config.js

@@ -0,0 +1,3 @@
+export default {
+  navigationBarTitleText: 'Barrage'
+};

+ 55 - 0
src/sites/mobile-taro/vue/src/business/pages/barrage/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="demo">
+    <h2>基础用法</h2>
+    <nut-cell>
+      <nut-barrage ref="danmu" :danmu="list"></nut-barrage>
+    </nut-cell>
+    <div class="test">
+      <nut-input label="文本" v-model="inputVal" />
+      <nut-button type="primary" @click="addDanmu">添加</nut-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+export default {
+  props: {},
+  setup() {
+    const inputVal = ref<any>('');
+    const danmu = ref<any>(null);
+    let list = ref([
+      '画美不看',
+      '不明觉厉',
+      '喜大普奔',
+      '男默女泪',
+      '累觉不爱',
+      '爷青结'
+    ]);
+    function addDanmu() {
+      danmu.value.add(inputVal.value);
+    }
+    return {
+      inputVal,
+      danmu,
+      list,
+      addDanmu
+    };
+  }
+};
+</script>
+
+<style lang="scss">
+.nut-cell,
+.nut-barrage {
+  padding: 20px 0;
+  height: 120px;
+}
+.test {
+  display: flex;
+  justify-content: space-between;
+  .nut-input {
+    width: 80%;
+  }
+}
+</style>