vue 调用pc端本地摄像头、麦克风实现拍照、录视频、录音 并上传到服务器指定树文件夹

vue 调用pc端本地摄像头、麦克风实现拍照、录视频、录音 并上传

自己写blog只是为了下次方便使用 过程确实很烦 ,自己摸索加各大网站cv查看
可以直接使用

1、调用摄像头拍照 录屏

首先是npm i vue-video-player -D 安装依赖
在main.js里面引入

import VideoPlayer from 'vue-video-player'
require('video.js/dist/video-js.css')
require('vue-video-player/src/custom-theme.css')
Vue.use(VideoPlayer)

此处代码主要是用来覆盖video的播放按钮样式
require(’./styles/video.css’);具体内容如下

 .video-js .vjs-big-play-button {
	  width: 72px;
	  height: 72px;
	  border-radius: 100%;
	  z-index: 100;
	  background-color: #ffffff;
	  border: solid 1px #979797;
	}

<template>
<div class="main ">
    <div class="left">
      <div class="vedio">
        <div class="shexiang">
         
          <!--图片展示-->
          <video
            ref="video"
            width="350px"
            height="265px"
            autoplay
          ></video>
          <div class="btnsBackground">
            <el-popover
              placement="top-start"
              width="160"
              trigger="click"
            >
              <div style="text-align: left; margin: 0">
                <div style="letter-spacing:1px">
                  间隔 <input
                    v-model="everyTime"
                    type="text"
                    id="everyTime"
                    style="width:30px;color:#41CFB1"
                  >
                  秒拍一次;</div>
                <div style="letter-spacing:1px">
                  最长拍照时间<input
                    v-model="allTime"
                    type="text"
                    id="allTime"
                    style="width:30px;color:#41CFB1;margin-top:10px"
                  ></div>
                <el-checkbox
                  v-model="checked"
                  style="margin-top:10px"
                >连拍模式</el-checkbox>
              </div>
              <el-image
                slot="reference"
                class="photo1"
                :src="require('@/assets/images/luxiang/setting.png')"
                fit="cover"
              ></el-image>
            </el-popover>
            <el-image
              class="photo2"
              @click="recordMedia"
              :src="require('@/assets/images/luxiang/video.png')"
              fit="cover"
            ></el-image>
            <el-image
              @click="takephoto"
              class="photo3"
              :src="require('@/assets/images/luxiang/photo.png')"
              fit="fill"
            ></el-image>
          </div>

        </div>
        <span class="greySpan"> 注:1、录制视频文件时,一个视频时长不能超过一分钟;</span>
        <span
          class="greySpan"
          style="margin-left:24px"
        > 2、点击设置按钮,可选择连拍模式</span>
      </div>
    </div>
    <div class="right">
    //通过canvas绘画 
      <canvas
        width="350px"
        height="265px"
        ref="canvas"
        v-show="false"
      ></canvas>
      <div class="right1">
        <div
          class="box"
          v-for="(item, index) in dataList"
          :key="'photo-' + index"
        >
          <div
            v-if="item.type=='vedio'"
            style="width: 100%; height:100%;border-radius: 5px;cursor: pointer;"
            @click="preVideo(item.src)"
          >
            <video
              :src="item.src"
              style="width: 100%; height:100%;object-fit:cover"
            ></video>
            <div class="player">
              <img
                :src="require('@/assets/images/luxiang/player.png')"
                alt=""
              >
            </div>
          </div>
          <el-image
            v-if="item.type=='photo'"
            :preview-src-list="prephotoList"
            :src="item.src"
            fit="cover"
            style="width: 100%;height: 100%;border-radius: 5px;"
          ></el-image>
          <div class="del-icon"> <i
              class="el-icon-delete "
              @click="deletePhoto(index)"
            ></i></div>
        </div>
      </div>
     //手动分页
      <el-pagination
        small
        v-if="pagation.total>18"
        :pager-count="5"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        background
        :current-page.sync="pagation.currentPage"
        :page-size="pagation.pagesize"
        layout="prev, pager, next,total"
        :total="pagation.total"
      >
      </el-pagination>

    </div>

    <div class="text-center margin-b10 subBtn">
      <el-button
        @click="closeContent"
        class="width-80 margin-r20"
      >取 消</el-button>
      <el-button
        type="primary"
        @click="submitContent"
        class="width-80"
      >确 定</el-button>
    </div>
    //点击视频预览
    <el-dialog
      modal
      class="videoShowDialog"
      destroy-on-close
      close-on-press-escape
      close-on-click-modal
      :visible.sync="preVideoShow"
    >
      <video-player
        onPlayerPlay
        class="video-player vjs-custom-skin"
        ref="videoPlayer"
        :playsinline="true"
        style="width:100%;height:100%"
        :options="playerOptions"
        :x5-video-player-fullscreen="true"
      ></video-player>
    </el-dialog>
  </div>
</template>


<script>
import "video.js/dist/video-js.css";
import "vue-video-player/src/custom-theme.css";
import { videoPlayer } from "vue-video-player";
import {
  dataDetailApi,
  showFloderApi,
  addFolderApi,
} from "@/services/api/apply.js";
import Bus from "@/assets/js/bus";
import UploadBigFile from "./vedioUpload.vue";
import { getUserInfo, UUID } from "@/utils/helpers.js";
import { addDataFileApi } from "@/services/api/apply.js";
import { uploadFileApi, uploadMoreFileApi } from "@/services/api/common.js";
export default {
  components: { UploadBigFile, videoPlayer },
  props: ["dragShow", "name", "datasetId"],
  data() {
    return {
      time: null,
      time1: null,
      time2: null,
      preVideoShow: false,
      pagation: {
        pagesize: 18,
        total: 0,
        currentPage: 1,
      },
      checked: false,
      recordType: 0, //0 未录制 1 录制中
      mediaRecorder: null,
      photoAndVedioList: [],
      photoList: [],
      prephotoList: [],
      everyTime: "",
      allTime: "",
      list: [],
      remotePath: "",
      inputName: "",
      editorNameShow: false,
      addPathData: {},
      id: "",
      addPath: "",
      data: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
      backFloader: [{}],
      firstPath: "",
      nowPath: "",
      lastPath: "",
      fileData: { listDirectory: [], listFile: [], fileServer: "" },
      playerOptions: {
        playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
        autoplay: false, //如果true,浏览器准备好时开始回放。
        muted: false, // 默认情况下将会消除任何音频。
        loop: false, // 导致视频一结束就重新开始。
        preload: "auto", // 建议浏览器在
        language: "zh-CN",
        aspectRatio: "16:9", // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
        fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
        sources: [
          {
            type: "video/mp4", //这里的种类支持很多种:基本视频格式、直播、流媒体等,具体可以参看git网址项目
            src: "", //url地址
          },
        ],
        poster: "", //你的封面地址

        // width: document.documentElement.clientWidth, //播放器宽度
        notSupportedMessage: "此视频暂无法播放,请稍后再试", //允许覆盖Video.js无法播放媒体源时显示的默认信息。
        controlBar: {
          timeDivider: true,
          durationDisplay: true,
          remainingTimeDisplay: false,
          fullscreenToggle: true, //全屏按钮
        },
      },
    };
  },
  methods: {
  //弹窗预览视频
    preVideo(url) {
      this.preVideoShow = true;
      this.playerOptions.sources[0].src = url;
    },
    
    handleSizeChange(size) {
      this.pagation.pagesize = size;
    },
    //切换页码
    handleCurrentChange(currentPage) {
      this.pagation.currentPage = currentPage;
    },
	//视频录制按钮控制
    recordMedia() {
      if (this.recordType == 0) {
        this.startMakeVideo();
      } else {
        this.stopMakeVideo();
      }
    },

  // 调用摄像头
    callCamera() {
      // H5调用电脑摄像头API
      navigator.mediaDevices
        .getUserMedia({
          video: true,
          // audio: true,
        })
        .then((success) => {
          // 摄像头开启成功
          this.$refs["video"].srcObject = success;
          this.mediaRecorder = new MediaRecorder(success, {
			//因为视频和音频分开录制这里直接注释了
            // audioBitsPerSecond: 128000, // 音频码率
            videoBitsPerSecond: 1000000, // 视频码率
            mimeType: "video/webm;codecs=h264", // 编码格式
          });

          // 实时拍照效果
          this.$refs["video"].play();
        })
        .catch((error) => {
          console.error("摄像头开启失败,请检查摄像头是否可用!");
        });
    },
    //录制视频
    startMakeVideo() {
      this.mediaRecorder.start();
      console.log("开始采集");
      this.recordType = 1;
      this.time = window.setTimeout(() => {
        this.stopMakeVideo();
      }, 60 * 1000);
    },
    //停止录制并上传
    stopMakeVideo() {
      this.mediaRecorder.stop();
      // 事件
      let _this = this;
      this.mediaRecorder.ondataavailable = function (e) {
        console.log(e);
        //构建文件blob流
        let blob = new Blob([e.data], { type: "video/mp4" });
        //调用转base64函数
        _this.blobToBase64(blob).then((res) => {
          _this.photoList.push({ src: res, type: "vedio" });
          _this.pagation.total = _this.photoList.length;
          // 转化后的base64
          console.log("base64", res);
        });
        console.log("停止采集视频");
      };

      this.recordType = 0;
      window.clearTimeout(this.time);
    },
  
    //拍照
    takephoto() {
      if (this.checked) {
        if (this.everyTime && this.allTime) {
        //设置参数后定时拍照
          let time1 = window.setInterval(() => {
            this.savePhoto();
          }, this.everyTime * 1000);
          let time2 = setTimeout(() => {
            window.clearInterval(time1);
          }, this.allTime * 1000);
        } else {
          this.$message.error("请设置连拍参数");
        }
      } else {
        this.savePhoto();
      }
    },
    //图片展示在右侧 并保存在数组里
    savePhoto() {
      let ctx = this.$refs["canvas"].getContext("2d");
      // 把当前视频帧内容渲染到canvas上
      ctx.drawImage(this.$refs["video"], 0, 0, 350, 265);
      // 转base64格式、图片格式转换、图片质量压缩
      let imgBase64 = this.$refs["canvas"].toDataURL("image/png", 0.7); // 由字节转换为KB 判断大小
      console.log(this.dataURLtoFile(imgBase64));
      this.photoList.push({ src: imgBase64, type: "photo" });
      this.prephotoList.push(imgBase64);
      this.pagation.total = this.photoList.length;
    },

    //blob转base64
    blobToBase64(blob) {
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = (e) => {
          resolve(e.target.result);
        };
        // readAsDataURL
        fileReader.readAsDataURL(blob);
        fileReader.onerror = () => {
          reject(new Error("blobToBase64 error"));
        };
      });
    },
    //base64转文件
    dataURLtoFile(dataurl) {
      let arr = dataurl.split(","),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], UUID(), { type: mime });
    },
    
    //多文件列表上传
    async uploadMoreFile() {
      let formData = new FormData();
      this.photoList.forEach((it) => {
        formData.append("multipartFiles", this.dataURLtoFile(it.src));
      });
      const userInfo = getUserInfo();
      formData.append("userId", userInfo.userId);
      //这是项目的上传接口
      const res = await this.$request(uploadMoreFileApi, formData);
      if (res && res.status == 200 && res.data) {
        res.data.forEach(({ fileFixedPath, fileType, fileSize, fileName }) => {
          this.photoAndVedioList.push({
            datasetcontentPath: fileFixedPath,
            datasetcontentSuffix: fileType,
            datasetcontentSize: fileSize,
            datasetcontentName: fileName + "." + fileType.split("/").pop(),
          });
        });
      } else {
        this.$message.error("文件上传失败");
      }
    },
    closeContent() {
      this.$emit("close");
    },
    async getDetail() {
      this.treeLoading = true;
      const res = await this.$request(dataDetailApi, { id: this.datasetId });
      if (res && res.status == 200) {
        this.firstPath = this.nowPath = res.data.filesPath || "";
        this.data = [];
      }
    },
   

    deletePhoto(index) {
      this.photoList.splice(index + (this.pagation.currentPage - 1) * 18, 1);
      this.pagation.total = this.photoList.length;
      if (this.dataList.length == 0) {
        if (this.pagation.currentPage > 1)
          this.handleCurrentChange(this.pagation.currentPage - 1);
      }
    },
    async submitContent() {
      if (this.photoList.length == 0) {
        this.$message.error("请拍摄照片或数据集");
        return;
      }
      //调用上传先传至服务器
      this.uploadMoreFile().then(() => {
        const datasetcontentsList = this.photoAndVedioList.map((it) => {
          return { ...it, datasetId: this.datasetId };
        });
        //提交保存在这个项目中
        this.$emit("submit", datasetcontentsList);
        this.$emit("close");
      });
    },
  },
  created() {
    this.callCamera();
    this.getDetail();
  },
  computed: {
    dataList() {
      const dataCodeList = this.photoList;
      const pagesize = this.pagation.pagesize;
      const currentPage = this.pagation.currentPage;
      return dataCodeList.slice(
        (currentPage - 1) * pagesize,
        pagesize * currentPage
      );
    },
  },
};
</script>

2、调用麦克风录音

1、创建recorder.js

// 兼容
window.URL = window.URL || window.webkitURL
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia

let HZRecorder = function (stream, config) {
  config = config || {}
  config.sampleBits = config.sampleBits || 8 // 采样数位 8, 16
  config.sampleRate = config.sampleRate || (44100 / 6) // 采样率(1/6 44100)
  let context = new (window.webkitAudioContext || window.AudioContext)()
  let audioInput = context.createMediaStreamSource(stream)
  let createScript = context.createScriptProcessor || context.createJavaScriptNode
  let recorder = createScript.apply(context, [4096, 1, 1])
  let audioData = {
    size: 0, // 录音文件长度
    buffer: [], // 录音缓存
    inputSampleRate: context.sampleRate, // 输入采样率
    inputSampleBits: 16, // 输入采样数位 8, 16
    outputSampleRate: config.sampleRate, // 输出采样率
    oututSampleBits: config.sampleBits, // 输出采样数位 8, 16
    input: function (data) {
      this.buffer.push(new Float32Array(data))
      this.size += data.length
    },
    compress: function () { // 合并压缩
      // 合并
      let data = new Float32Array(this.size)
      let offset = 0
      for (let i = 0; i < this.buffer.length; i++) {
        data.set(this.buffer[i], offset)
        offset += this.buffer[i].length
      }
      // 压缩
      let compression = parseInt(this.inputSampleRate / this.outputSampleRate)
      let length = data.length / compression
      let result = new Float32Array(length)
      let index = 0; let j = 0
      while (index < length) {
        result[index] = data[j]
        j += compression
        index++
      }
      return result
    },
    encodeWAV: function () {
      let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate)
      let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits)
      let bytes = this.compress()
      let dataLength = bytes.length * (sampleBits / 8)
      let buffer = new ArrayBuffer(44 + dataLength)
      let data = new DataView(buffer)

      let channelCount = 1// 单声道
      let offset = 0

      let writeString = function (str) {
        for (let i = 0; i < str.length; i++) {
          data.setUint8(offset + i, str.charCodeAt(i))
        }
      }

      // 资源交换文件标识符
      writeString('RIFF'); offset += 4
      // 下个地址开始到文件尾总字节数,即文件大小-8
      data.setUint32(offset, 36 + dataLength, true); offset += 4
      // WAV文件标志
      writeString('WAVE'); offset += 4
      // 波形格式标志
      writeString('fmt '); offset += 4
      // 过滤字节,一般为 0x10 = 16
      data.setUint32(offset, 16, true); offset += 4
      // 格式类别 (PCM形式采样数据)
      data.setUint16(offset, 1, true); offset += 2
      // 通道数
      data.setUint16(offset, channelCount, true); offset += 2
      // 采样率,每秒样本数,表示每个通道的播放速度
      data.setUint32(offset, sampleRate, true); offset += 4
      // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
      data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4
      // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
      data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2
      // 每样本数据位数
      data.setUint16(offset, sampleBits, true); offset += 2
      // 数据标识符
      writeString('data'); offset += 4
      // 采样数据总数,即数据总大小-44
      data.setUint32(offset, dataLength, true); offset += 4
      // 写入采样数据
      if (sampleBits === 8) {
        for (let i = 0; i < bytes.length; i++ , offset++) {
          let s = Math.max(-1, Math.min(1, bytes[i]))
          let val = s < 0 ? s * 0x8000 : s * 0x7FFF
          val = parseInt(255 / (65535 / (val + 32768)))
          data.setInt8(offset, val, true)
        }
      } else {
        for (let i = 0; i < bytes.length; i++ , offset += 2) {
          let s = Math.max(-1, Math.min(1, bytes[i]))
          data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
        }
      }

      return new Blob([data], { type: 'audio/mp3' })
    }
  }

  // 开始录音
  this.start = function () {
    audioInput.connect(recorder)
    recorder.connect(context.destination)
  }

  // 停止
  this.stop = function () {
    recorder.disconnect()
  }

  // 获取音频文件
  this.getBlob = function () {
    this.stop()
    return audioData.encodeWAV()
  }

  // 回放
  this.play = function (audio) {
    let downRec = document.getElementById('downloadRec')
    downRec.href = window.URL.createObjectURL(this.getBlob())
    downRec.download = new Date().toLocaleString() + '.mp3'
    audio.src = window.URL.createObjectURL(this.getBlob())
  }

  // 上传
  this.upload = function (url, callback) {
    let fd = new FormData()
    fd.append('audioData', this.getBlob())
    let xhr = new XMLHttpRequest()
    /* eslint-disable */
    if (callback) {
      xhr.upload.addEventListener('progress', function (e) {
        callback('uploading', e)
      }, false)
      xhr.addEventListener('load', function (e) {
        callback('ok', e)
      }, false)
      xhr.addEventListener('error', function (e) {
        callback('error', e)
      }, false)
      xhr.addEventListener('abort', function (e) {
        callback('cancel', e)
      }, false)
    }
    /* eslint-disable */
    xhr.open('POST', url)
    xhr.send(fd)
  }

  // 音频采集
  recorder.onaudioprocess = function (e) {
    audioData.input(e.inputBuffer.getChannelData(0))
    // record(e.inputBuffer.getChannelData(0));
  }
}
// 抛出异常
HZRecorder.throwError = function (message) {
  alert(message)
  throw new function () { this.toString = function () { return message } }()
}
// 是否支持录音
HZRecorder.canRecording = (navigator.getUserMedia != null)
// 获取录音机
HZRecorder.get = function (callback, config) {
  if (callback) {
    if (navigator.getUserMedia) {
      navigator.getUserMedia(
        { audio: true } // 只启用音频
        , function (stream) {
          let rec = new HZRecorder(stream, config)
          callback(rec,stream)
        }
        , function (error) {
          switch (error.code || error.name) {
            case 'PERMISSION_DENIED':
            case 'PermissionDeniedError':
              HZRecorder.throwError('用户拒绝提供信息。')
              break
            case 'NOT_SUPPORTED_ERROR':
            case 'NotSupportedError':
              HZRecorder.throwError('浏览器不支持硬件设备。')
              break
            case 'MANDATORY_UNSATISFIED_ERROR':
            case 'MandatoryUnsatisfiedError':
              HZRecorder.throwError('无法发现指定的硬件设备。')
              break
            default:
              HZRecorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name))
              break
          }
        })
    } else {
      HZRecorder.throwErr('当前浏览器不支持录音功能。'); return
    }
  }
}
export default HZRecorder

   上面callback的回调参数我加了stream 便于离开页面时自动关闭麦克

2、先写个播放预览动态组件
audioplay.vue

<template>
  <div
    class="audio-user"
    @click.stop="playThisAudio($event, url)"
  >
    <div style="display:flex">
      <img
        style="width:20px;height:20px"
        v-if="!isLive"
        :src="require('@/assets/images/luxiang/audio.png')"
        		//自己静态展示图
        alt=""
      >

      <div
        id="run"
        v-if="isLive"
      >
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div></div>
      </div>
    </div>

    <div>00:{{durations}}</div>
  </div>
</template>
 
<script>
export default {
  name: "audioplay",
  components: {},
  props: ["url", "duration", "index"],
  data() {
    return {
      isLive: false,
      audioStatus: "播放",
      durations: this.duration,
      audio: "",
    };
  },
  methods: {
    // 播放语音
    playThisAudio(e, url) {
      this.$emit("play", this.index);
      let that = this;
      if (this.audioStatus === "播放中...") {
        if (this.audio) {
          this.audio.pause();
          that.isLive = false;
          this.audioStatus = "暂停";
          return false;
        }
      }
      if (this.audioStatus === "暂停") {
        if (this.audio) {
          this.audio.play();
          that.isLive = true;
          this.audioStatus = "播放中...";
          return false;
        }
      }
      this.audio = new Audio(url);
      this.audio.autoplay = true;
      this.audio.play();
      that.isLive = true;
      var sumDuration = this.duration;
      var audioI = 0;

      this.audio.addEventListener("timeupdate", function () {
        audioI++;
        if (audioI <= 1) {
          that.isLive = true;
          that.audioStatus = "播放中...";
        }
        let du = parseInt(sumDuration - that.audio.currentTime);
        if (du < 1) {
          that.durations = "00";
        } else if (du < 10) {
          that.durations = "0" + du;
        } else {
          that.durations = du;
        }
      });
      this.audio.addEventListener("ended", function () {
        that.durations = that.duration;
        that.isLive = false;
        that.audioStatus = "播放完成";
        setTimeout(() => {
          this.audio = "";
        }, 150);
      });
    },
  },
  created() {},
  watch: {
    duration(newValue) {
      if (this.duration) {
        this.durations = newValue;
      }
    },
  },
};
</script>
 
<style lang="less" scoped>
#run {
  width: 20px;
  height: 20px;
}
#run div {
  display: inline-block;
  position: absolute;
  bottom: 10px;
  padding: 0;
  margin: 0;
  width: 2px;
  height: 20px;
  background-color: #41cfb1;
  transform-origin: bottom;
  border-radius: 5px 5px 0 0;
}

#run div:nth-child(1) {
  left: 5px;
  animation: musicWave 0.5s infinite linear both alternate;
}
#run div:nth-child(2) {
  left: 10px;
  animation: musicWave 0.2s infinite linear both alternate;
}
#run div:nth-child(3) {
  left: 15px;
  animation: musicWave 0.6s infinite linear both alternate;
}
#run div:nth-child(4) {
  left: 20px;
  animation: musicWave 0.3s infinite linear both alternate;
}
#run div:nth-child(5) {
  left: 25px;
  animation: musicWave 0.3s infinite linear both alternate;
}
@keyframes musicWave {
  0% {
    height: 2px;
  }
  100% {
    height: 15px;
  }
}
.audio-user {
  /* width: 3rem;
  height: 0.5rem; */
  width: 100px;
  height: 40px;
  line-height: 0.5rem;
  cursor: pointer;
  position: relative;
  /* background-color: #eee; */
  /* border-radius: 0.5rem; */
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  align-items: center;
  /* padding: 0 0.2rem; */
  color: green;
}
</style>

3、Cvoice.vue

<template>
  <div class="main ">
    <div class="left">
      <div class="vedio">
        <div class="shexiang">
          <!--canvas截取流-->
          <!--图片展示-->
          //这边因为要自己写动态展示波浪 能力有限就只能暴力写法 动画也是对应的
          <div class="big">
            <div :class="isLive?'bs':'nomove1'"></div>
            <div :class="isLive?'bs':'nomove2'"></div>
            <div :class="isLive?'bs':'nomove3'"></div>
            <div :class="isLive?'bs':'nomove4'"></div>
            <div :class="isLive?'bs':'nomove5'"></div>
            <div :class="isLive?'bs':'nomove6'"></div>
            <div :class="isLive?'bs':'nomove7'"></div>
            <div :class="isLive?'bs':'nomove8'"></div>
            <div :class="isLive?'bs':'nomove9'"></div>
            <div :class="isLive?'bs':'nomove10'"></div>
            <div :class="isLive?'bs':'nomove11'"></div>
            <div :class="isLive?'bs':'nomove12'"></div>
            <div :class="isLive?'bs':'nomove13'"></div>
            <div :class="isLive?'bs':'nomove14'"></div>
            <div :class="isLive?'bs':'nomove15'"></div>
            <div :class="isLive?'bs':'nomove16'"></div>
            <div :class="isLive?'bs':'nomove17'"></div>
            <div :class="isLive?'bs':'nomove18'"></div>
            <div :class="isLive?'bs':'nomove19'"></div>
            <div :class="isLive?'bs':'nomove20'"></div>
            <div :class="isLive?'bs':'nomove21'"></div>
            <div :class="isLive?'bs':'nomove22'"></div>
            <div :class="isLive?'bs':'nomove23'"></div>
            <div :class="isLive?'bs':'nomove24'"></div>
            <div :class="isLive?'bs':'nomove25'"></div>
            <div :class="isLive?'bs':'nomove26'"></div>
            <div :class="isLive?'bs':'nomove27'"></div>
            <div :class="isLive?'bs':'nomove28'"></div>
            <div :class="isLive?'bs':'nomove29'"></div>
            <div :class="isLive?'bs':'nomove30'"></div>
          </div>
          <!-- <video
            ref="video"
            width="350px"
            height="136px"
            autoplay
          ></video> -->
          <el-button
            class="btn"
            @mousedown.native="mouseStart"
            @mouseleave.native="mouseEnd"
            @mouseup.native="mouseEnd"
            type="primary"
            round
            icon="el-icon-microphone"
          >按住录制声音</el-button>
        </div>
      </div>
      <div class="catagory">
        <span style="padding-left:5px">选择数据存储路径</span>
        <div class="flex-r-wrap margin-t5 padding-0-5">
          <div class=" margin-t5 single-file">
            <el-tree
              :node-key="id"
              :show-checkbox="false"
              ref="tree"
              :data="data"
              :props="defaultProps"
              @node-click="nodeClick"
              @node-expand="handleNodeClick"
            >
              <div
                class="custom-tree-node"
                slot-scope="{ node ,data }"
              >
                <span v-if="data.type=='folder'"> <img :src='listImg("folder")' /></span>
                <span v-else>
                  <img :src="getFileTpe(data.name)" />
                </span>

                <span style="margin-left:14px">{{
                  node.label
                  }}</span>

              </div>
            </el-tree>
            <div
              style="height:100%"
              @click="resetPath"
            ></div>
          </div>
          <el-button
            type="primary"
            class="addBtn"
            @click="addFolder"
          >新建文件夹</el-button>
        </div>
      </div>
    </div>
    <div class="right">
      <div class="right1">
        <div
          class="box"
          v-for="(item, index) in dataList"
          :key="'audio-' + index"
        >
          <audioplay
            :url="item.src"
            :duration="item.duration"
            :ref='"audioPlay"+index'
            :index="index"
            @play="play"
          ></audioplay>
          <i
            class="el-icon-error text-ct3"
            @click="deleteAudio(index)"
          ></i>
        </div>
      </div>
      <el-pagination
        small
        v-if="pagation.total>30"
        :pager-count="5"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        background
        :current-page.sync="pagation.currentPage"
        :page-size="pagation.pagesize"
        layout="prev, pager, next,total"
        :total="pagation.total"
      >
      </el-pagination>
    </div>

    <div class="text-center margin-b10 subBtn">
      <el-button
        @click="closeContent"
        class="width-80 margin-r20"
      >取 消</el-button>
      <el-button
        type="primary"
        @click="submitContent"
        class="width-80"
      >确 定</el-button>
    </div>
  </div>
</template>

<script>
//有些接口是我自己用的可以根据自己需求改
import audioplay from "@/components/audioplay.vue";
import recording from "./recorder.js";
import {
  dataDetailApi,
  showFloderApi,
  addFolderApi,
} from "@/services/api/apply.js";
import Bus from "@/assets/js/bus";
import UploadBigFile from "./vedioUpload.vue";
import { getUserInfo, UUID } from "@/utils/helpers.js";
import { addDataFileApi } from "@/services/api/apply.js";
import { uploadFileApi, uploadMoreFileApi } from "@/services/api/common.js";
export default {
  components: { UploadBigFile, audioplay },
  props: ["dragShow", "name", "datasetId"],
  data() {
    return {
      MediaStreamTrack: null, //用来关闭媒体流的 
      number: "",
      audio: "",
      isLive: false,
      pagation: {
        pagesize: 30,
        total: 0,
        currentPage: 1,
      },
      audioList: [],
      recorder: null,
      interval: "",
      timeOut: "",
      audioFileList: [], // 上传语音列表
      startTime: "", // 语音开始时间
      endTime: "", // 语音结束
      photoAndVedioList: [],
      list: [],
      remotePath: "",
      inputName: "",
      editorNameShow: false,
      addPathData: {},
      id: "",
      addPath: "",
      data: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
      backFloader: [{}],
      firstPath: "",
      nowPath: "",
      lastPath: "",
      fileData: { listDirectory: [], listFile: [], fileServer: "" },
    };
  },
  methods: {
 	 //点击空白处选择根目录 用于上传
    resetPath() {
      this.addPath = this.firstPath;
      this.addPathData = {};
      console.log(this.addPath);
    },
    //audioplay调用播放 下面对number监听  同时只能播放一个音频
    play(index) {
      this.number = index;
    },

    handleSizeChange(size) {
      this.pagation.pagesize = size;
    },
    //手动分页切换页码
    handleCurrentChange(currentPage) {
      this.pagation.currentPage = currentPage;
    },
    // 清除定时器
    clearTimer() {
      if (this.interval) {
        this.num = 60;
        clearInterval(this.interval);
      }
    },
    // 长按说话
    mouseStart() {
      this.isLive = true;
      this.clearTimer();
      this.startTime = new Date().getTime();
      //调用recorder.js的get() 有回调
      recording.get((rec, stream) => {
        // 当首次按下时,要获取浏览器的麦克风权限,所以这时要做一个判断处理
        if (rec) {
          this.MediaStreamTrack = stream.getTracks();
          // track set 流中所有 MediaStreamTrack  对象的序列 下面关闭麦克用的
          this.recorder = rec;
          this.recorder.start();
        }
      });
      //最长录音时间60s
      this.timeOut = setTimeout(() => {
        this.mouseEnd();
      }, 60 * 1000);
    },
    // 松开时上传语音
    mouseEnd() {
      if (this.timeOut) {
        clearTimeout(this.timeOut);
      }
      this.clearTimer();//清定时器
      this.endTime = new Date().getTime();
      let duration = Math.ceil((this.endTime - this.startTime) / 1000 - 0.1);
      if (duration < 1) {
        this.$message.error("说话时间太短啦!");
        return;
      }
      if (this.recorder) {
      //停止录音
        this.recorder.stop();
        // 获取语音二进制文件
        let bold = this.recorder.getBlob();
        // 将获取的二进制对象转为二进制Base64文件流
        this.blobToBase64(bold).then((res) => {
          this.audioList.push({
            src: res,
            duration: duration < 10 ? "0" + duration : duration,
            type: "voice",
          });
          this.pagation.total = this.audioList.length;
       
        });
      }
      this.recorder = null;//录音器对象置空
      this.isLive = false;
    },
    //关闭麦克设备
    cancalCloseAudio() {
      this.MediaStreamTrack[0].stop();
    },

    //blob转base64
    blobToBase64(blob) {
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = (e) => {
          resolve(e.target.result);
        };
        // readAsDataURL
        fileReader.readAsDataURL(blob);
        fileReader.onerror = () => {
          reject(new Error("blobToBase64 error"));
        };
      });
    },
    //base64转文件
    dataURLtoFile(dataurl) {
      let arr = dataurl.split(","),
        mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]),
        n = bstr.length,
        u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], UUID(), { type: mime });
    },
    //上传多个文件
    async uploadMoreFile() {
      let formData = new FormData();//以formData传文件
      this.audioList.forEach((it) => {
        formData.append("multipartFiles", this.dataURLtoFile(it.src));//base64转文件
      });
      const userInfo = getUserInfo();
      formData.append("userId", userInfo.userId);//可根据自己后台接口需求增加参数
      const res = await this.$request(uploadMoreFileApi, formData);
      if (res && res.status == 200 && res.data) {
        res.data.forEach(({ fileFixedPath, fileType, fileSize, fileName }) => {
          this.photoAndVedioList.push({
            datasetcontentPath: fileFixedPath,
            datasetcontentSuffix: fileType,
            datasetcontentSize: fileSize,
            datasetcontentName: fileName + "." + fileType.split("/").pop(),
          });
        });
      } else {
        this.$message.error("文件上传失败");
      }
    },
    //关闭窗口
    closeContent() {
      this.$emit("close");
    },
    //树形目录展示图标
    getFileTpe(name) {
      console.log(name);
      let type = name && name.split(".").pop();
      return this.listImg(type);
    },
    //新建文件夹
    async submitName() {
      console.log(JSON.stringify(this.addPathData) == "{}");
      if (JSON.stringify(this.addPathData) == "{}") {
        this.addPathData = {
          id: "folder_1",
          localPath: this.addPath,
          name: this.inputName,
          type: "folder",
          children: [{}],
        };
      }
      console.log(this.addPathData);
      const newChild = {
        id:
          this.addPathData.id +
          "_" +
          (this.addPathData.children.filter((item) => {
            return item.type == "folder";
          }).length +
            1),
        name: this.inputName,
        children: [],
        type: "folder",
        localPath: this.addPathData.localPath + "/" + this.inputName,
      };
      console.log(this.addPathData);

      const res = await this.$request(addFolderApi, {
        localPath: this.addPathData.localPath + "/" + this.inputName,
        id: this.datasetId,
      });
      if (res && res.status == 200) {
        if (!this.addPathData.children) {
          this.$set(this.addPathData, "children", []);
        }
        this.addPathData.children.push(newChild);
        this.editorNameShow = false;
        if (this.addPath == this.remotePath) {
          this.getDetail();
        }
        this.$emit("initData");
      } else if (res.status == 611) {
        this.$message.error("文件夹名称名称重复");
      }
    },

    async addFolder() {
      this.editorNameShow = true;
    },
    //
    async getDetail() {
      this.treeLoading = true;
      const res = await this.$request(dataDetailApi, { id: this.datasetId });
      if (res && res.status == 200) {
        this.firstPath = this.nowPath = res.data.filesPath || "";
        this.data = [];
        this.showList([], res.data.filesPath || "");
      }
    },
    //树节点点击
    nodeClick(data) {
      this.addPathData = data;
      this.addPath = data.localPath;
      console.log(this.addPathData);
    },
    //树节点展开
    handleNodeClick(data) {
      this.addPathData = data;
      this.addPath = data.localPath;
      // console.log(JSON.stringify(data.children[0]) == "{}");
      if (data.children && JSON.stringify(data.children[0]) !== "{}") {
        return;
      } else if (data.children && JSON.stringify(data.children[0]) == "{}") {
        this.showList(data, data.localPath);
      }
    },
    //展示树
    async showList(data, path) {
      if (data.length == 0) {
        this.nowPath = path;
        let index = path.lastIndexOf("/");
        if (index == 0) {
          this.lastPath = path.substring(0, index + 1);
        } else {
          this.lastPath = path.substring(0, index);
        }
        this.remotePath = path;
        this.addPath = path;
        const res = await this.$request(showFloderApi, {
          remotePath: path,
        });
        if (res && res.status == 200) {
          this.fileData = res.data;
          if (res.data.listDirectory.length != 0) {
            for (let i = 0; i < res.data.listDirectory.length; i++) {
              this.data.push({
                id: "folder_" + (i + 1),
                ...res.data.listDirectory[i],
                children: [{}],
                type: "folder",
              });
            }
          }

          // for (let i = 0; i < res.data.listFile.length; i++) {
          //   this.data.push({
          //     id: "file_" + (i + 1),
          //     type: "file",
          //     ...res.data.listFile[i],
          //   });
          // }
        }
      } else {
        this.nowPath = path;
        let index = path.lastIndexOf("/");
        if (index == 0) {
          this.lastPath = path.substring(0, index + 1);
        } else {
          this.lastPath = path.substring(0, index);
        }
        const res = await this.$request(showFloderApi, {
          remotePath: path,
        });
        if (res && res.status == 200) {
          this.fileData = res.data;
          if (res.data.listDirectory.length == 0) {
            data.children.pop();
          } else {
            for (let i = 0; i < res.data.listDirectory.length; i++) {
              if (i == 0) {
                data.children.pop();
              }
              data.children.push({
                id: data.id + "_" + (i + 1),
                ...res.data.listDirectory[i],
                children: [{}],
                type: "folder",
              });
            }
          }
        }
      }
    },
    listImg(file) {
      if (file === "vedio" || file === "mp4") {
        return require("@/assets/images/vedio.png");
      } else if (file === "voice" || file === "mp3") {
        return require("@/assets/images/voice.png");
      } else if (file === "ZIP") {
        return require("@/assets/images/zip.png");
      } else if (file === "txt") {
        return require("@/assets/images/txt.png");
      } else if (file === "jpg" || file === "png") {
        return require("@/assets/images/picture.png");
      } else if (file === "doc") {
        return require("@/assets/images/doc.png");
      } else if (file === "folder") {
        return require("@/assets/images/folder.png");
      } else {
        return require("@/assets/images/doc.png");
      }
    },

    // queryFile(path, file) {
    //   console.log(file);
    //   this.list.push({
    //     datasetcontentPath: path,
    //     datasetcontentSuffix: file.fileType,
    //     datasetcontentSize: file.size,
    //     datasetcontentName: file.name,
    //   });
    //   console.log(this.list);
    // },
		//删除音频
    deleteAudio(index) {
      // console.log(index);
      // console.log(
      //   index + (this.pagation.currentPage - 1) * this.pagation.pagesize
      // );
	
		//对应分页位置
      this.audioList.splice(
        index + (this.pagation.currentPage - 1) * this.pagation.pagesize,
        1
      );
      console.log(this.audioList);
      this.pagation.total = this.audioList.length;
      if (this.dataList.length == 0) {
        if (this.pagation.currentPage > 1)
          this.handleCurrentChange(this.pagation.currentPage - 1);
      }
    },
    //提交上传,更新内容
    async submitContent() {
      if (this.audioList.length == 0) {
        this.$message.error("请录制音频");
        return;
      }
      this.uploadMoreFile().then(() => {//上传完的回调
        const datasetcontentsList = this.photoAndVedioList.map((it) => {
          return { ...it, datasetId: this.datasetId };
        });
        this.$emit("submit", datasetcontentsList, this.addPath);//调用了更新接口
        this.$emit("close");
      });
    },
  },
  created() {
    this.getDetail();
  },
  beforeDestroy() {
    this.cancalCloseAudio();
  },
  watch: {
  //监听number 用于展示一个同时只能播放一个音频
    number(newValue, oldValue) {
      if (oldValue || oldValue === 0) {
        if (newValue || newValue === 0) {
          this.$refs["audioPlay" + oldValue][0].audio.currentTime = 0;
          this.$refs["audioPlay" + oldValue][0].audio.pause();
          this.$refs["audioPlay" + oldValue][0].audioStatus = "暂停";//控制播放时间的
          this.$refs["audioPlay" + oldValue][0].isLive = false;//控制动态样式
        }
      }
    },
  },
  computed: {
    dataList() {
      const dataCodeList = this.audioList;
      const pagesize = this.pagation.pagesize;
      const currentPage = this.pagation.currentPage;
      console.log(
        dataCodeList.slice((currentPage - 1) * pagesize, pagesize * currentPage)
      );
      return dataCodeList.slice(
        (currentPage - 1) * pagesize,
        pagesize * currentPage
      );
    },
  },
};
</script>

<style lang="less" scoped>
.main {
  display: flex;
  flex-wrap: nowrap;
  position: relative;
}

.subBtn {
  position: absolute;
  top: 650px;
  left: 350px;
  z-index: 2;
}
.addBtn {
  padding: 3px;
  top: 350px;
  left: 15px;
  position: absolute;
  z-index: 2;
}
.single-file /deep/ .el-tree-node.is-expanded > .el-tree-node__children {
  display: inline;
}
.single-file {
  display: flex;
  flex-direction: column;
  padding-top: 10px;
  width: 350px;
  height: 300px;
  overflow: auto;
  border: 1px solid #e6e6e6;
}
#everyTime {
  width: 20px;
  border: 0px;
  outline: none;
  border-bottom: 1px solid black;
}
#allTime {
  outline: none;
  width: 30px;
  border: 0px;
  border-bottom: 1px solid black;
}
.right {
  margin-top: 10px;
  margin-left: 10px;
  width: 500px;
  height: 600px;
  // background: red;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  overflow: auto;
  .right1 {
    align-self: flex-start;
  }
  .box {
    margin-top: 10px;
    margin-right: 10px;
    // padding: 10px;
    width: 114px;
    height: 46px;
    background: #ffffff;
    box-shadow: 0px 0px 10px 0px rgba(51, 51, 51, 0.06);
    display: inline-block;
    position: relative;
  }
  .text-ct3 {
    position: absolute;
    top: 0px;
    right: 0px;
    color: #ccc;
    font-size: 18px;
    cursor: pointer;
  }
}

.left {
  display: inline-block;
  width: 350px;
  height: 600px;
  margin-left: 20px;
  margin-top: 20px;
  // background: greenyellow;
  .vedio {
    // background-color: yellowgreen;

    box-shadow: 0px 0px 10px 0px rgba(51, 51, 51, 0.06);
    .greySpan {
      color: grey;
      display: inline-block;
      padding-left: 10px;
    }
    .shexiang {
      width: 350px;
      height: 136px;
      background: #ffffff;
      height: 136px;
      position: relative;

      .big {
        width: 310px;
        height: 100px;
        margin: 0px 18px;
        display: flex;
        align-items: center;
        justify-content: space-around;
      }
      .bs {
        // display: inline-block;
        // position: absolute;
        // bottom: 76px;
        padding: 0;
        margin: 0;
        width: 4px;
        height: 28px;
        background: #464a49;
        border-radius: 2px;
        // transform-origin: bottom;
      }
      .bs:nth-child(1) {
        left: 18px;
        animation: musicWave 0.5s infinite linear both alternate;
      }
      .nomove1 {
        width: 4px;
        height: 10px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(2) {
        left: 28px;
        animation: musicWave 0.2s infinite linear both alternate;
      }
      .nomove2 {
        width: 4px;
        height: 20px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(3) {
        left: 38px;
        animation: musicWave 0.6s infinite linear both alternate;
      }
      .nomove3 {
        width: 4px;
        height: 30px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(4) {
        left: 48px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove4 {
        width: 4px;
        height: 24px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(5) {
        left: 58px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove5 {
        width: 4px;
        height: 26px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(6) {
        left: 68px;
        animation: musicWave 0.5s infinite linear both alternate;
      }
      .nomove6 {
        width: 4px;
        height: 20px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(7) {
        left: 78px;
        animation: musicWave 0.2s infinite linear both alternate;
      }
      .nomove7 {
        width: 4px;
        height: 22px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(8) {
        left: 88px;
        animation: musicWave 0.6s infinite linear both alternate;
      }
      .nomove8 {
        width: 4px;
        height: 27px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(9) {
        left: 98px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove9 {
        width: 4px;
        height: 30px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(10) {
        left: 108px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove10 {
        width: 4px;
        height: 36px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(11) {
        left: 118px;
        animation: musicWave 0.5s infinite linear both alternate;
      }
      .nomove11 {
        width: 4px;
        height: 16px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(12) {
        left: 128px;
        animation: musicWave 0.2s infinite linear both alternate;
      }
      .nomove12 {
        width: 4px;
        height: 25px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(13) {
        left: 138px;
        animation: musicWave 0.6s infinite linear both alternate;
      }
      .nomove13 {
        width: 4px;
        height: 19px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(14) {
        left: 148px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove14 {
        width: 4px;
        height: 27px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(15) {
        left: 158px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove15 {
        width: 4px;
        height: 27px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(16) {
        left: 168px;
        animation: musicWave 0.5s infinite linear both alternate;
      }
      .nomove16 {
        width: 4px;
        height: 36px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(17) {
        left: 178px;
        animation: musicWave 0.2s infinite linear both alternate;
      }
      .nomove17 {
        width: 4px;
        height: 28px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(18) {
        left: 188px;
        animation: musicWave 0.6s infinite linear both alternate;
      }
      .nomove18 {
        width: 4px;
        height: 23px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(19) {
        left: 198px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove19 {
        width: 4px;
        height: 25px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(20) {
        left: 208px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove20 {
        width: 4px;
        height: 22px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(21) {
        left: 218px;
        animation: musicWave 0.5s infinite linear both alternate;
      }
      .nomove21 {
        width: 4px;
        height: 19px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(22) {
        left: 228px;
        animation: musicWave 0.2s infinite linear both alternate;
      }
      .nomove22 {
        width: 4px;
        height: 26px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(23) {
        left: 238px;
        animation: musicWave 0.6s infinite linear both alternate;
      }
      .nomove23 {
        width: 4px;
        height: 18px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(24) {
        left: 248px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove24 {
        width: 4px;
        height: 20px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(25) {
        left: 258px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove25 {
        width: 4px;
        height: 25px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(26) {
        left: 268px;
        animation: musicWave 0.5s infinite linear both alternate;
      }
      .nomove26 {
        width: 4px;
        height: 36px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(27) {
        left: 278px;
        animation: musicWave 0.2s infinite linear both alternate;
      }
      .nomove27 {
        width: 4px;
        height: 21px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(28) {
        left: 288px;
        animation: musicWave 0.6s infinite linear both alternate;
      }
      .nomove28 {
        width: 4px;
        height: 19px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(29) {
        left: 298px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove29 {
        width: 4px;
        height: 25px;
        background: #464a49;
        border-radius: 2px;
      }
      .bs:nth-child(30) {
        left: 308px;
        animation: musicWave 0.3s infinite linear both alternate;
      }
      .nomove30 {
        width: 4px;
        height: 22px;
        background: #464a49;
        border-radius: 2px;
      }
      @keyframes musicWave {
        0% {
          height: 2px;
        }
        100% {
          height: 28px;
        }
      }
      .btn {
        cursor: pointer;
        position: absolute;
        bottom: 12px;
        right: 105px;
        z-index: 2;
      }
    }
  }
  .catagory {
    margin-top: 78px;
    position: relative;
  }
}
</style>

你可能感兴趣的:(javascript,vue.js,html5,css3,es6)