用vue3实现一个模拟算盘

295 阅读3分钟

这几天玩保卫萝卜3,对里面的滑杆很感兴趣,在想能不能用vue来实现一个,等我把这个滑杆组件做完,突然想到还可以做成一个算盘

image.png

先来看看效果

ezgif-1-76f8dbadd0.gif

因为还不懂算盘的用法,而且感觉也很难,目前只做了个样子,并不能用来计算,这里主要讲讲这个算盘的这个珠子在横杠上滑动的组件

ezgif-1-08a002e50d.gif

组件有下面几种功能:

  1. 可以自定义珠子的个数
  2. 可以水平或者垂直来滑动
  3. 可以调节珠子的大小、颜色,横杠的粗细、颜色
  4. 在珠子滑动的方向,可以推动剩余的珠子一起滑动,并且会限制滑动的边界

珠子用一个数组来表示

<Drag-Link
  direction="column"
  beads={[100, 120, 140, 160, 180]} // 传入珠子的位置
  bodLength={200}
/>

也可以传入一个数字,用珠子的高度算出数组每一项的位置

    const beads: any = ref([])
    if (Array.isArray(props.beads)) {
      beads.value = props.beads
    } else {
      beads.value = [...Array(props.beads).keys()].map(
        (i) => i * props.beadsHeight
      )
    }

接下来就是用绝对定位,把珠子数组的位置定位在横杠上

    // 设置珠子定位
    const headsMove = (val: number) => {
      const fixed = `${props.direction === 'row' ? 'top' : 'left'}:-${
        (props.beadsWidth - props.bodSize) / 2
      }px`
      const move = `${props.direction === 'row' ? 'left' : 'top'}:${val}px`
      return `${move};${fixed}`
    }
    
    {beads.value.map((item: number, index: number) => {
      return (
        <div
          class="beads"
          style={[headsMove(item)]}
        ></div>
      )
    })}

然后就是珠子在横杠上的滑动,通过设置定位的left或者top值就可以

珠子滑动鼠标事件

const onMousedown = (e: any, index: number) => {
  e.preventDefault()
  const startX = e.clientX
  const startY = e.clientY
  const offset = beads.value[index] // 初始位置
  const onMousemove = (event: any) => {
    const dx = event.clientX - startX
    const dy = event.clientY - startY
  }
}

超出范围判断

这里是判断滑动的距离是不是大于左右两边边界的位置加上左右两边珠子的宽度,比如左边有两个珠子,每个珠子20像素宽,那往左滑40像素就停止了

   const isValidMovement = (
      offset: number,
      delta: number,
      index: number,
      beadsHeight: number,
      bodLength: number
    ) => {
      const minPosition = index * beadsHeight
      const maxPosition = bodLength - (beads.value.length - index) * beadsHeight
      return offset + delta >= minPosition && offset + delta <= maxPosition
    }
    
    const isValid = isValidMovement(
      offset, // 初始位置
      props.direction === 'row' ? dx : dy, // 滑动的距离
      index, // 当前珠子的坐标
      props.beadsHeight, // 珠子沿横杠方向的高度
      props.bodLength // 横杠的长度
    )
    if (!isValid) return

相邻珠子位置推动

首先是判断一下滑动的方向,然后循环一下滑动方向外的珠子,然后把当前滑动的珠子的位置累计在推动的珠子上

    const adjustAdjacentBeads = (
      index: number,
      dx: number,
      dy: number
    ): void => {
      const isAdd = props.direction === 'row' ? dx > 1 : dy > 1
      const len = isAdd ? beads.value.length - index - 1 : index

      for (let i = 0; i < len; i++) {
        const adjacentIndex = isAdd ? index + i : i
        const adjacentBead = beads.value[adjacentIndex]
        if (isAdd) {
          if (
            adjacentBead >
            beads.value[adjacentIndex + 1] - props.beadsHeight
          ) {
            beads.value[adjacentIndex + 1] = adjacentBead + props.beadsHeight
          }
        } else {
          if (
            beads.value[index] <
            adjacentBead + props.beadsHeight * (index - adjacentIndex)
          ) {
            beads.value[adjacentIndex] =
              beads.value[index] - (index - adjacentIndex) * props.beadsHeight
          }
        }
      }
    }
    
    // 相邻珠子位置判断
    adjustAdjacentBeads(index, dx, dy)

完整代码

import { defineComponent, ref, PropType } from 'vue'
import './index.scss'

import { useStyle } from './useStyle'

export default defineComponent({
  components: {},
  props: {
    // 方向
    direction: {
      type: String as PropType<'row' | 'column'>,
      default: 'column',
      validator: function (value: string) {
        // 自定义验证逻辑
        return ['row', 'column'].includes(value)
      }
    },
    // 珠子个数
    beads: {
      type: Number || Array,
      default: 2
    },
    // 珠子宽度
    beadsWidth: {
      type: Number,
      default: 30
    },
    // 珠子高度
    beadsHeight: {
      type: Number,
      default: 20
    },
    beadsStyle: {
      type: String,
      default: ''
    },
    beadsColor: {
      type: String,
      default: '#666'
    },
    bodColor: {
      type: String,
      default: '#000'
    },
    // 横轴的宽度
    bodSize: {
      type: Number,
      default: 10
    },
    // 横轴的长度
    bodLength: {
      type: Number,
      default: 200
    },
    bodStyle: {
      type: String,
      default: ''
    }
  },
  emits: [''],
  setup(props) {
    const { bodCss, bodWrapCss, beadsCss } = useStyle(props)

    const beads: any = ref([])
    if (Array.isArray(props.beads)) {
      beads.value = props.beads
    } else {
      beads.value = [...Array(props.beads).keys()].map(
        (i) => i * props.beadsHeight
      )
    }

    /**
     * @description: 设置珠子的定位
     * @Date: 2023-11-27 11:23:43
     * @param {number} val
     */
    const headsMove = (val: number) => {
      const fixed = `${props.direction === 'row' ? 'top' : 'left'}:-${
        (props.beadsWidth - props.bodSize) / 2
      }px`
      const move = `${props.direction === 'row' ? 'left' : 'top'}:${val}px`
      return `${move};${fixed}`
    }

    // 判断是否滑动到头
    const isValidMovement = (
      offset: number,
      delta: number,
      index: number,
      beadsHeight: number,
      bodLength: number
    ) => {
      console.log(offset, delta, index, beadsHeight, bodLength)
      const minPosition = index * beadsHeight
      const maxPosition = bodLength - (beads.value.length - index) * beadsHeight
      return offset + delta >= minPosition && offset + delta <= maxPosition
    }

    // 鼠标按下
    const onMousedown = (e: any, index: number) => {
      e.preventDefault()
      const startX = e.clientX
      const startY = e.clientY
      const offset = beads.value[index]
      const onMousemove = (event: any) => {
        const dx = event.clientX - startX
        const dy = event.clientY - startY

        const isValid = isValidMovement(
          offset,
          props.direction === 'row' ? dx : dy,
          index,
          props.beadsHeight,
          props.bodLength
        )
        if (!isValid) return

        beads.value[index] = offset + (props.direction === 'row' ? dx : dy)

        // 相邻珠子位置判断
        adjustAdjacentBeads(index, dx, dy)
      }
      const onMouseup = () => {
        document.removeEventListener('mousemove', onMousemove)
        document.removeEventListener('mouseup', onMouseup)
      }
      document.addEventListener('mousemove', onMousemove)
      document.addEventListener('mouseup', onMouseup)
    }

    /**
     * @description: 相邻珠子位置判断
     * @Date: 2023-11-27 15:25:05
     */
    const adjustAdjacentBeads = (
      index: number,
      dx: number,
      dy: number
    ): void => {
      const isAdd = props.direction === 'row' ? dx > 1 : dy > 1
      const len = isAdd ? beads.value.length - index - 1 : index

      for (let i = 0; i < len; i++) {
        const adjacentIndex = isAdd ? index + i : i
        const adjacentBead = beads.value[adjacentIndex]
        if (isAdd) {
          if (
            adjacentBead >
            beads.value[adjacentIndex + 1] - props.beadsHeight
          ) {
            beads.value[adjacentIndex + 1] = adjacentBead + props.beadsHeight
          }
        } else {
          if (
            beads.value[index] <
            adjacentBead + props.beadsHeight * (index - adjacentIndex)
          ) {
            beads.value[adjacentIndex] =
              beads.value[index] - (index - adjacentIndex) * props.beadsHeight
          }
        }
      }
    }

    return () => {
      return (
        <div class="bodWrap" style={bodWrapCss.value}>
          <div class="bod" style={[bodCss.value, props.bodStyle]}>
            {beads.value.map((item: number, index: number) => {
              return (
                <div
                  class="beads"
                  style={[beadsCss.value, props.beadsStyle, headsMove(item)]}
                  onMousedown={(e: any) => {
                    onMousedown(e, index)
                  }}
                ></div>
              )
            })}
          </div>
        </div>
      )
    }
  }
})