Bootstrap

VUE实现刻度尺进度条

一、如下图所示效果:

运行后入下图所示效果:

实现原理是用div画图并动态改变进度,

二、div源码

<div style="width: 100%;">
    <div class="sdg_title" style="height: 35px;">
      <!--对话组[{{ dialogGroup.index }}]编辑-->
      <div class="sdg_title_btns">
        <a-button size="mini" @click="deleteCurrentBlock">删除</a-button>
        <div class="sdg_split"></div>
        <a-button size="mini" @click="play" v-if="isPlay != '播放中'">运行</a-button>
        <a-button size="mini" @click="pause" v-if="isPlay == '播放中'">暂停</a-button>
        <a-button size="mini" @click="stop" v-if="isPlay != '未播放'">停止</a-button>
        <div class="sdg_split"></div>
        <div style="box-sizing: border-box; display: flex; justify-content: center; align-items: center;">
          <span>最小刻度单位:</span>
          <a-radio-group v-model:value="scaleUnit" @change="init">
            <a-radio-button value="秒">1秒</a-radio-button>
            <a-radio-button value="5秒">5秒</a-radio-button>
            <a-radio-button value="10秒">10秒</a-radio-button>
          </a-radio-group>
        </div>
        <div style="padding-left: 10px;">
          <span>刻度总时长:</span>
          <a-input-number v-model:value="initData.period" placeholder="请输入任务想定名称"
                   style="width: 80px" @change="maxPeriodChange"></a-input-number>
        </div>
      </div>
    </div>
    <div style="position: relative; width: 100%; display: flex; flex-direction: row;">
      <!--title-->
      <div style="width: 150px; height: 250px;" class="scroll">
        <div class="blockTitle">
          刻度(总时长)
        </div>
        <div class="blockTitle2">
          A项
        </div>
        <div class="blockTitle2">
          B项
        </div>
      </div>
      <div style="width: calc(100% - 150px); height: 250px;" class="scroll">
        <!--刻度表-->
        <div class="timeScaleBlock" :style="{ width: maxWidth + 'px' }">
          <div v-for="val in maxWidth / 100" :key="val" class="oneScaleUnit">
            <!--刻度主线-->
            <div class="mainLine"></div>
            <!--长刻度线-->
            <div class="longLine" style="left: -1px"></div>
            <div class="longLine" style="left: 48px">
              <div class="middleScaleValue" v-if="scaleUnit == '5秒'">{{ formatSecond((val * 10 - 5) * 5) }}</div>
              <div class="middleScaleValue" v-else-if="scaleUnit == '10秒'">{{ formatSecond((val * 10 - 5) * 10) }}</div>
              <div class="middleScaleValue" v-else>{{ formatSecond((val * 10 - 5)) }}</div>
            </div>
            <!--短刻度线-->
            <div class="shortLine" style="left: 9px"></div>
            <div class="shortLine" style="left: 19px"></div>
            <div class="shortLine" style="left: 28px"></div>
            <div class="shortLine" style="left: 38px"></div>

            <div class="shortLine" style="left: 58px"></div>
            <div class="shortLine" style="left: 68px"></div>
            <div class="shortLine" style="left: 78px"></div>
            <div class="shortLine" style="left: 88px"></div>
          </div>
          <div class="leftBorder" v-if="clickBlock" :style="{ left: leftBorderValue }">
            ↑
            <div class="borderValue">
              {{ formatSecond(clickBlock.startTime) }}
            </div>
          </div>
          <div class="leftBorder" v-if="clickBlock" :style="{ left: rightBorderVaule }">
            ↑
            <div class="borderValue">
              {{ formatSecond(clickBlock.endTime) }}
            </div>
          </div>
          <!--播放进度三尖角-->
          <div class="triangleDiv" :style="{ left: triangleDivLeft + 'px' }">
            <span class="second">{{ formatSecond(startTime) }}</span>
          </div>
        </div>
        <!--背景信号-->
        <div class="stationBlock" id="backgroundTimeLineDiv" v-if="initData" :style="{ width: maxBlockWidth + 'px' }" @mousemove="bgMousemove" @mouseup="bgMouseup" @mouseleave="bgMouseleave">
          <div :id="item.id"
               class="tae_base_block1"
               v-for="(item, index) in initData.backgroundTimeLine" :key="index"
               @mousedown="dialogueBlockClick(item)"
               :class="{ block_selected1: clickBlock && clickBlock.id == item.id }"
               :style="{ width: item.widthPx, left: item.leftPx }">
            <div class="blockItem">{{ item.name }}</div>
            <div class="blockItem">{{ item.period }}秒</div>
          </div>
          <!-- 拖拽进入的临时模块 -->
          <div v-if="tempBlockData && tempBlockData.blockType == 'signal'" class="tae_base_block1_temp" :style="{ width: tempBlockData.widthPx, left: tempBlockData.leftPx }">
            <div class="blockItem">{{ tempBlockData.name }}</div>
            <div class="blockItem">{{ tempBlockData.period }}秒</div>
          </div>
        </div>
        <!--设备部署-->
        <div class="stationBlock" v-if="initData" :style="{ width: maxBlockWidth + 'px' }" @mousemove="bgMousemove" @mouseup="bgMouseup" @mouseleave="bgMouseleave">
          <div :id="item.id"
               class="tae_base_block2"
               v-for="(item, index) in initData.equipmentTimeLine" :key="index"
               @mousedown="dialogueBlockClick(item)"
               :class="{ block_selected2: clickBlock && clickBlock.id == item.id }"
               :style="{ width: item.widthPx, left: item.leftPx }">
            <div class="blockItem">{{ item.name }}</div>
            <div class="blockItem">{{ item.period }}秒</div>
          </div>
          <!-- 拖拽进入的临时模块 -->
          <div v-if="tempBlockData && tempBlockData.blockType == 'place'" class="tae_base_block2_temp" :style="{ width: tempBlockData.widthPx, left: tempBlockData.leftPx }">
            <div class="blockItem">{{ tempBlockData.name }}</div>
            <div class="blockItem">{{ tempBlockData.period }}秒</div>
          </div>
        </div>
        <!--播放背景布-->
        <div v-if="startTime > 0" class="stationPlayBlock" :style="{ width: triangleDivLeft + 6 + 'px'}"></div>
      </div>
    </div>
  </div>

script源码如下

<script>
import { mapState, mapActions } from 'vuex'
import { message } from 'ant-design-vue'
export default {
  name: 'DialogBlockEditor',
  components: {
  },
  props: {
    initDefaultData: {
      type: Object,
      default: () => {
        return null
      }
    },
    dialogGroup: {
      type: Object,
      default: () => {
        return null
      }
    },
  },
  computed: {
    ...mapState(['runningScenario']),
    leftBorderValue() {
      let unit = 1
      if (this.scaleUnit == '5秒') {
        unit = 5
      } else if (this.scaleUnit == '10秒') {
        unit = 10
      }
      return (10 * this.clickBlock.startTime) / unit - 7 + 'px'
    },
    rightBorderVaule() {
      let unit = 1
      if (this.scaleUnit == '5秒') {
        unit = 5
      } else if (this.scaleUnit == '10秒') {
        unit = 10
      }
      return (10 * this.clickBlock.endTime) / unit - 7 + 'px'
    },
    isPlay() {
      if (this.startTime == 0 && this.timer == null) {
        return '未播放'
      } else if (this.startTime > 0 && this.timer == null) {
        return '暂停中'
      } else {
        return '播放中'
      }
    }
  },
  data() {
    return {
      initData: {
        startTime: 0,
        period: 60, // 任务总时长
        backgroundTimeLine: [
          {
            name: '素材1',
            type: 'signal',
            period: 10,
            startTime: 10,
          },
        ], // 时间线
        equipmentTimeLine: [
          {
            name: '素材1',
            type: 'place',
            period: 10,
            startTime: 10,
          },
        ], // 时间线
      },
      loading: false,
      maxWidth: 2000,
      maxBlockWidth: 1000,
      scaleUnit: '秒',
      dialogueBlock: null,
      clickBlock: null,
      dataList: [],
      audioSrc: '',
      triangleDivLeft: -6,
      timer: null,
      startTime: 0,
      tempData: null,
      tempBlockData: null,
      clientLeft: 0,
    }
  },
  mounted() {
    if(this.initDefaultData==null){
      this.initDefaultData=this.initData
    }else{
      this.initData=this.initDefaultData
    }
    this.init()
    setTimeout(() => {
      let backgroundTimeLineDiv = document.getElementById('backgroundTimeLineDiv')
      this.clientLeft = backgroundTimeLineDiv.getBoundingClientRect().left
    }, 2000)
  },
  beforeDestroy() {
    this.stop()
  },
  methods: {
    ...mapActions(['runScenario', 'stopScenario']),
    async init() {
      if (this.scaleUnit == '5秒') {
        this.maxWidth = Math.ceil(this.initData.period / 50) * 100
        this.maxBlockWidth = this.initData.period * 2
      } else if (this.scaleUnit == '10秒') {
        this.maxWidth = Math.ceil(this.initData.period / 100) * 100
        this.maxBlockWidth = this.initData.period
      } else {
        this.maxWidth = Math.ceil(this.initData.period / 10) * 100
        this.maxBlockWidth = this.initData.period * 10
      }
      let sortNumber = 0
      for (let backgroundData of this.initData.backgroundTimeLine) {
        backgroundData.id = Tool.uuid(8, 32)
        backgroundData.leftPx = this.timeToPx(backgroundData.startTime - this.initData.startTime)
        backgroundData.widthPx = this.timeToPx(backgroundData.period)
        backgroundData.endTime = backgroundData.startTime + backgroundData.period
        backgroundData.blockType = backgroundData.type
        backgroundData.sortNumber = sortNumber++
        // rightPx: this.timeToPx(dialogueDetail[0] - this.initData.startTime + dialogue.period)
      }

      sortNumber = 0
      for (let equipmentData of this.initData.equipmentTimeLine) {
        equipmentData.id = Tool.uuid(8, 32)
        equipmentData.leftPx = this.timeToPx(equipmentData.startTime - this.initData.startTime)
        equipmentData.widthPx = this.timeToPx(equipmentData.period)
        equipmentData.endTime = equipmentData.startTime + equipmentData.period
        equipmentData.blockType = equipmentData.type
        equipmentData.sortNumber = sortNumber++
        // rightPx: this.timeToPx(dialogueDetail[0] - this.initData.startTime + dialogue.period)
      }
      this.$nextTick(() => {
        this.initBlockClickEvent()
      })
    },
    /**
     * 增加素材到刻度尺
     * @param data 素材数据
     * @param type 
     */
    addBlock(data, type) {
      if (type == 'signal') {
        this.initData.backgroundTimeLine.push(data)
      } else if (type == 'place') {
        this.initData.equipmentTimeLine.push(data)
      }
      this.init()
    },
    timeToPx(time) {
      switch (this.scaleUnit) {
        case '秒':
          return time * 10 + 'px'
        case '5秒':
          return time * 2 + 'px'
        case '10秒':
          return time + 'px'
      }
    },
    pxToTime(px) {
      switch (this.scaleUnit) {
        case '秒':
          return Math.round(px / 10) + this.initData.startTime
        case '5秒':
          return Math.round(px / 2) + this.initData.startTime
        case '10秒':
          return px + this.initData.startTime
      }
    },
    async dataConfirm() {
      this.$emit('save-scheme-dialog', this.initData)
    },
    dialogueBlockClick(block) {
      this.clickBlock = block
    },
    deleteCurrentBlock() {
      if (this.clickBlock) {
        if (this.clickBlock.blockType == 'signal') { // 删除的
          for (let index in this.initData.backgroundTimeLine) {
            if (this.initData.backgroundTimeLine[index].id == this.clickBlock.id) {
              this.initData.backgroundTimeLine.splice(index, 1)
              break
            }
          }
        } else {
          for (let index in this.initData.equipmentTimeLine) { // 删除的是
            if (this.initData.equipmentTimeLine[index].id == this.clickBlock.id) {
              this.initData.equipmentTimeLine.splice(index, 1)
              break
            }
          }
        }
        this.clickBlock = null
        message.success('删除成功')
        this.init()
      } else {
        message.error('未选择台词块!')
      }
    },
    initBlockClickEvent() {
      for (let dialogueWrapper of this.initData.backgroundTimeLine) {
        let dom = document.getElementById(dialogueWrapper.id)
        dom.onmousedown = (e) => {
          this.onmousedownEvent(e, dialogueWrapper)
        }
      }
      for (let dialogueWrapper of this.initData.equipmentTimeLine) {
        let dom = document.getElementById(dialogueWrapper.id)
        dom.onmousedown = (e) => {
          this.onmousedownEvent(e, dialogueWrapper)
        }
      }
    },
    maxPeriodChange(){
      this.init()
    },
    onmousedownEvent(event, clickBlock) {
      let disX = event.clientX
      let oldLeftPx = Number.parseFloat(clickBlock.leftPx)
      document.onmousemove = (me) => {
        let offsetPx = me.clientX - disX
        switch (this.scaleUnit) {
          case '秒':
            offsetPx = Math.round(offsetPx / 10) * 10
            break
          case '5秒':
            offsetPx = Math.round(offsetPx / 2) * 2
            break
          case '10秒':
            break
        }
        offsetPx += oldLeftPx

        // 边界处理
        let preBlock = null, nextBlock = null
        if (clickBlock.blockType == 'signal') { //
          if (clickBlock.sortNumber > 0) {
            preBlock = this.initData.backgroundTimeLine[clickBlock.sortNumber - 1]
          }
          if (this.initData.backgroundTimeLine[clickBlock.sortNumber + 1]) {
            nextBlock = this.initData.backgroundTimeLine[clickBlock.sortNumber + 1]
          }
        } else { // 
          if (clickBlock.sortNumber > 0) {
            preBlock = this.initData.equipmentTimeLine[clickBlock.sortNumber - 1]
          }
          if (this.initData.equipmentTimeLine[clickBlock.sortNumber + 1]) {
            nextBlock = this.initData.equipmentTimeLine[clickBlock.sortNumber + 1]
          }
        }
        // 左边重合
        if (preBlock) {
          if (offsetPx < Number.parseInt(preBlock.leftPx) + Number.parseInt(preBlock.widthPx)) { // 重合了
            offsetPx = Number.parseInt(preBlock.leftPx) + Number.parseInt(preBlock.widthPx)
          }
        } else {
          if (offsetPx < 0) {
            offsetPx = 0
          }
        }
        // 判断右边重合
        if (nextBlock) {
          if (offsetPx + Number.parseInt(clickBlock.widthPx) > Number.parseInt(nextBlock.leftPx)) {
            offsetPx = Number.parseInt(nextBlock.leftPx) - Number.parseInt(clickBlock.widthPx)
          }
        }

        clickBlock.leftPx = offsetPx + 'px'
        clickBlock.startTime = this.pxToTime(offsetPx)
        clickBlock.endTime = clickBlock.startTime + clickBlock.period
      }
      document.onmouseup = () => {
        document.onmousemove = null
        document.onmouseup = null
      }
    },
    closeDialog() {
      this.$emit('close-dialog')
      return false
    },
    play() {
      this.timer = setInterval(() => {
        this.startTime++
        this.calcTriangleDivLeft()
        for (let block of this.initData.backgroundTimeLine) {
          if (!block.status) { // 还未运行
            if (this.startTime >= block.startTime && this.startTime <= block.endTime) {
              block.status = 'running'
              this.runScenario({
                _this: this,
                scenario: this.initData,
                callback: () => {
                }
              })
              break
            }
          } else if (block.status == 'running') { // 正在运行
            if (this.startTime > block.endTime) {
              block.status = 'ended'
              break
            }
          }
        }
        for (let block of this.initData.equipmentTimeLine) {
          if (!block.status) { // 还未运行
            if (this.startTime >= block.startTime && this.startTime <= block.endTime) {
              block.status = 'running'
              break
            }
          } else if (block.status == 'running') { // 正在运行
            if (this.startTime > block.endTime) {
              block.status = 'ended'
              break
            }
          }
        }
        if (this.startTime >= this.initData.period) { // 播放完成了
          this.stop()
        }
      }, 1000)
      message.success('开始播放')
    },
    pause() {
      if (this.timer) {
        try {
          clearInterval(this.timer)
        } catch (e) {

        }
      }
      this.timer = null
      message.success('暂停')
    },
    stop() {
      if (this.timer) {
        try {
          clearInterval(this.timer)
        } catch (e) {

        }
      }
      this.timer = null
      this.startTime = 0
      this.triangleDivLeft = -6
      for (let block of this.initData.backgroundTimeLine) {
        block.status = null
      }
      for (let block of this.initData.equipmentTimeLine) {
        block.status = null
      }
      message.success('停止')
    },
    calcTriangleDivLeft() {
      switch (this.scaleUnit) {
        case '秒':
          this.triangleDivLeft = this.startTime * 10 - 6
          break
        case '5秒':
          this.triangleDivLeft = this.startTime * 2 - 6
          break
        case '10秒':
          this.triangleDivLeft = this.startTime - 6
          break
        case '分':
          this.triangleDivLeft = this.startTime * 10 / 60 - 6
          break
        case '5分':
          this.triangleDivLeft = this.startTime * 10 / 300 - 6
          break
        case '10分':
          this.triangleDivLeft = this.startTime * 10 / 600 - 6
          break
      }
    },
    formatSecond(value) {
      value = value || 0;
      let second = parseInt(value, 10); // 秒
      let minute = 0; // 分
      let hour = 0; // 小时
      if (second > 60) {
        // 当大于60秒时,才需要做转换
        minute = Math.floor(second / 60);
        second = Math.floor(second % 60);
        if (minute > 60) {
          hour = Math.floor(minute / 60);
          minute = Math.floor(minute % 60);
        }
      } else {
        // 小于60秒时,直接显示,不需要处理
      }
      let result = "" + Tool.PrefixInteger(second, 2) + "";
      // 拼上分钟
      result = "" + Tool.PrefixInteger(minute, 2) + ":" + result;
      // 拼上小时
      result = "" + Tool.PrefixInteger(hour, 2) + ":" + result;
      return result
    },
    bgMousemove(e) {
      if (this.tempData) {
        document.body.style.cursor = 'move'
        let offsetPx = e.clientX - this.clientLeft
        switch (this.scaleUnit) {
          case '秒':
            offsetPx = Math.round(offsetPx / 10) * 10
            break
          case '5秒':
            offsetPx = Math.round(offsetPx / 2) * 2
            break
          case '10秒':
            break
        }
        let startTime = this.pxToTime(offsetPx)
        this.tempBlockData = {
          id: Tool.uuid(8, 32),
          name: this.tempData.name,
          period: this.tempData.period,
          leftPx: offsetPx + 'px',
          widthPx: this.timeToPx(this.tempData.period),
          startTime: startTime,
          endTime: startTime + this.tempData.period,
          blockType: this.tempData.type,
        }
        // 判断是否重合
        if (this.judgeBlockCoincidence(this.tempBlockData)) { // 重合了
          document.body.style.cursor = 'not-allowed'
          this.tempBlockData.success = false
        }
        else { // 没有重合
          document.body.style.cursor = 'move'
          this.tempBlockData.success = true
        }
      }
    },
    bgMouseup(e) {
      document.body.style.cursor = 'auto'
      if (this.tempBlockData && this.tempBlockData.success) {
        let tempData = Tool.deepCopy(this.tempData)
        tempData.startTime = this.tempBlockData.startTime
        this.initData.period+=this.tempBlockData.period
        if (this.tempBlockData.blockType == 'signal') {
          this.initData.backgroundTimeLine.push(tempData)
          this.initData.backgroundTimeLine.sort((a, b) => a.startTime > b.startTime ? 1 : -1)
        }
        else {
          this.initData.equipmentTimeLine.push(tempData)
          this.initData.equipmentTimeLine.sort((a, b) => a.startTime > b.startTime ? 1 : -1)
        }
        //console.log('规划-鼠标松开1',this.tempData.period)
        this.init()
        //console.log('规划-鼠标松开2',this.tempData.period)
      }
      this.tempBlockData = null
      this.tempData = null
    },
    bgMouseleave(e) {
      this.tempBlockData = null
      document.body.style.cursor = 'auto'
    },
    // 判断是否有重合
    judgeBlockCoincidence(blockData) {
      let list = null
      if (blockData.blockType == 'signal') {
        list = this.initData.backgroundTimeLine
      } else {
        list = this.initData.equipmentTimeLine
      }
      for (let block of list) {
        if (blockData.startTime > block.startTime && blockData.startTime < block.endTime) {
          return true
        }
        if (blockData.endTime > block.startTime && blockData.endTime < block.endTime) {
          return true
        }
      }
      return false
    },

    setTempData(tempData) {
      this.tempData = tempData
    },

    updata(tempData){
      this.initData=tempData
      this.init()
    }
  }
}
</script>

运行时如有问题,欢迎讨论

;