elementui源码学习之仿写一个el-switch

语言: CN / TW / HK

本篇文章记录仿写一个 el-switch 组件细节,从而有助于大家更好理解饿了么ui对应组件具体工作细节。本文是elementui源码学习仿写系列的又一篇文章,后续空闲了会不断更新并仿写其他组件。源码在github上,大家可以拉下来,npm start运行跑起来,结合注释有助于更好的理解。github仓库地址如下: https://github.com/shuirongsh...

switch组件思考

组件功能作用

switch组件 一般是表示开关状态或者两种状态之间的切换,如点击开启网站的夜间模式,或关闭夜间模式。如下图vue官网首页就有这样的操作功能:

vue官网链接地址: https://cn.vuejs.org/

组件的结构

switch组件 的结构还是比较简单的,主要分为两部分:

switch组件切换小圆点按钮
switch组件切换容器

组件的实现思路

基本的 switch 切换布局结构

在实现 switch组件 的时候, switch组件切换容器 可以直接画一个 div 去表示,那么 switch组件切换小圆点按钮 我们也需要画一个 div 吗?其实不用的。

  1. 我们可以使用 伪元素 先画出一个 切换小圆点按钮 (结合定位)
  2. 然后需要定义一个 标识布尔值 ,用于更改 切换组件开启关闭状态
  3. 当状态变化的时候,去更改 切换小圆点按钮 在左侧或在右侧的位置(通过class),即实现了切换功能
  4. 再加上过渡效果,这样的话, switch组件 的切换(开启关闭)就会很丝滑了

开启关闭 switch组件 的说明文字功能注意事项

如下图:

  1. 关于开启时候文字在左侧,关闭时候文字在右侧,也开始可以通过弹性盒样式控制 justifyContent:flex-start / flex-end; ,当然动态 padding 也要加上,详情见代码
  2. 若将文字加入切换框内部,那么就需要让切换框背景容器dom的宽度自适应,即根据内容文字的多少来控制,所以要提到 width: fit-content;属性 (使用fit-content属性,让宽度随着内容文字多少自适应)

关于 fit-content 详情见官方文档: https://developer.mozilla.org...

给伪元素加上hover效果的写法

给伪元素加悬浮效果是先hover再::after(不要搞反了)

正确写法: .target:hover::after { background-color: red; }

错误写法!!!: .target::after:hover { background-color: red; }

这里举一个例子代码效果图,复制粘贴即可使用,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            padding: 120px;
        }

        .target {
            display: inline-block;
            width: 60px;
            height: 18px;
            background-color: #c4c4c4;
            border-radius: 10px;
            cursor: pointer;
            transition: all 0.3s;
            position: relative;
        }

        /* 使用伪元素画一个小圆点 */
        .target::after {
            content: "";
            position: absolute;
            top: -4px;
            left: -2px;
            border-radius: 50%;
            width: 24px;
            height: 24px;
            border: 1px solid #e9e9e9;
            box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3);
            background-color: #fff;
            transition: all 0.3s;
        }

        /* 给自己加悬浮效果直接写即可 */
        .target:hover {
            background-color: green;
        }

        /* 给伪元素加悬浮效果是先hover再::after(不要搞反了) */
        .target:hover::after {
            background-color: red;
        }
    </style>
</head>

<body>
    <div class="target"></div>
</body>

</html>

关于封装的 mySwitch组件 的其他的东西,结合笔者的注释,就可以清晰的理解了。这个组件主要还是样式的动态控制。

另,笔者封装的组件暂不搭配 el-form 的校验使用,后续写到表单校验时,会补上并更新github上的代码仓库中

当然部分写法效果,笔者的方案和官方的还是略有不同,毕竟思路略有不同,也建议读者自己尝试仿写封装哦

封装的组件

效果图

笔者的gif录屏软件不太好,道友朋友们有没有不错的gif录制软件推荐一下 ^_^

复制粘贴即可使用哦

使用代码

<template>
  <div>
    <my-divider lineType="dotted" content-position="left">普通使用</my-divider>
    <my-switch @change="change" v-model="flag1"></my-switch>
    <my-switch v-model="flag2"></my-switch>
    <my-divider lineType="dotted" content-position="left"
      >开启关闭文字</my-divider
    >
    <my-switch v-model="flag3" openText="开启啦开启啦" closeText="关闭了"></my-switch>
    <my-switch v-model="flag3" openText="ON" closeText="OFF"></my-switch>
    <my-switch v-model="flag3" openText="✔" closeText="✘"></my-switch>
    <my-divider lineType="dotted" content-position="left"
      >自定义开启关闭背景色</my-divider
    >
    <my-switch
      v-model="flag4"
      active-color="#19be6b"
      inactive-color="#ed4014"
    ></my-switch>
    <my-divider lineType="dotted" content-position="left">禁用</my-divider>
    <my-switch v-model="flag5" disabled></my-switch>
    <my-switch v-model="flag6" disabled></my-switch>
    <br />
    <my-divider lineType="dotted" content-position="left"
      >small切换框</my-divider
    >
    <my-switch
      v-model="flag7"
      active-color="#006CFF"
      inactive-color="#DD6DA6"
      openText="small"
      closeText="switch"
      size="small"
    ></my-switch>
    <my-divider lineType="dotted" content-position="left">big切换框</my-divider>
    <my-switch
      v-model="flag8"
      active-color="#2F2F2F"
      inactive-color="#ddd"
      openText="☾"
      closeText="☼"
      size="big"
    ></my-switch>
  </div>
</template>

<script>
export default {
  data() {
    return {
      flag1: true,
      flag2: false,
      flag3: true,
      flag4: true,
      flag5: false,
      flag6: true,
      flag7: true,
      flag8: true,
    };
  },
  methods: {
    change(val) {
      console.log("切换后的状态", val);
    },
  },
};
</script>

封装代码

参考注释,建议自己封装适合自己公司业务的 switch组件

<template>
  <div
    class="mySwitchWrap"
    :class="[disabled ? 'disabledSwitch' : '', size]"
    @click="changeStatus"
  >
    <!-- input标签 -->
    <input
      class="switchInput"
      type="checkbox"
      @change="changeStatus"
      ref="input"
      :true-value="activeValue"
      :false-value="inactiveValue"
      :disabled="disabled"
      @keydown.enter="changeStatus"
    />
    <!-- 主要内容 -->
    <span
      :class="[
        'switchCentre',
        'circleDotLeft',
        isOpen ? 'changeCircleDotRight' : '',
      ]"
      :style="{
        background: computedBackground,
        borderColor: computedBackground,
      }"
    >
      <span
        class="text"
        :style="{
          justifyContent: isOpen ? 'flex-start' : 'flex-end',
          padding: isOpen ? '0 28px 0 8px' : '0 8px 0 28px',
        }"
        >{{ isOpen ? openText : closeText }}</span
      >
    </span>
  </div>
</template>

<script>
export default {
  name: "mySwitch",
  props: {
    openText: String,
    closeText: String,
    // v-model搭配value接收数据,this.$emit("input", val)更新数据
    value: {
      type: Boolean,
      default: false, // 默认false
    },
    // 是否禁用,默认不禁用
    disabled: {
      type: Boolean,
      default: false,
    },
    // switch打开时为true
    activeValue: {
      type: Boolean,
      default: true,
    },
    // switch关闭时为false
    inactiveValue: {
      type: Boolean,
      default: false,
    },
    // 自定义switch打开时背景色
    activeColor: {
      type: String,
      default: "",
    },
    // 自定义switch关闭时背景色
    inactiveColor: {
      type: String,
      default: "",
    },
    // switch切换框的大小
    size: {
      type: String,
      default: "",
    },
  },
  computed: {
    // 是否打开切换框取决于外层传递的v-model的值是否为true
    isOpen() {
      return this.value === this.activeValue;
    },
    computedBackground() {
      // 若传递了激活颜色和未激活颜色,就根据是否开启状态使用传递的颜色
      if ((this.activeColor.length > 0) & (this.inactiveColor.length > 0)) {
        return this.isOpen ? this.activeColor : this.inactiveColor;
      }
      // 没传递就根据开启使用默认的背景色
      else {
        return this.isOpen ? "#409EFF" : "#C0CCDA";
      }
    },
  },
  methods: {
    changeStatus() {
      // 禁用情况下,不做状态更改切换
      if (this.disabled) {
        return;
      }
      // 首先看是否开启,若开启,就传递不开启;若不开启,就传递开启(因为状态切换,取反)
      const val = this.isOpen ? this.inactiveValue : this.activeValue;
      this.$emit("input", val); // 更新外层v-model绑定的值
      this.$emit("change", val); // 抛出一个change事件以供用户使用
    },
  },
};
</script>

<style scoped lang="less">
.mySwitchWrap {
  display: inline-block;
  cursor: pointer;
  font-size: 14px;
  margin: 2px;
  /* 将input标签隐藏起来,宽高都为0,透明度也为0,看不到 */
  .switchInput {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
  }
  .switchCentre {
    display: inline-block;
    width: auto;
    height: 20px;
    color: #fff;
    background-color: #c4c4c4;
    border: 1px solid;
    outline: 0;
    border-radius: 10px;
    box-sizing: border-box;
    transition: all 0.3s; // 加上过渡效果
    position: relative;
    .text {
      min-width: 54px; // 设置最小宽度
      width: fit-content; // 使用fit-content属性,让宽度随着内容多少自适应
      height: 100%;
      font-size: 12px;
      display: flex;
      // justify-content: justifyContent; // 注意,这里也是通过:style控制文字靠左还是靠右
      align-items: center;
      transition: all 0.3s; // 加上过渡效果
    }
  }
  // 默认小圆点在左侧的(使用伪元素创建一个小圆点)
  .circleDotLeft::after {
    content: "";
    position: absolute;
    top: -4px;
    left: -2px;
    border-radius: 100%;
    width: 24px;
    height: 24px;
    border: 1px solid #e9e9e9;
    box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3); // 原来小圆点有一点阴影
    background-color: #fff;
    transition: all 0.3s; // 加上过渡效果
  }
  // 当switch打开时,加上类名~通过更改定位left值控制圆点在右侧
  .changeCircleDotRight::after {
    left: 100%;
    margin-left: -24px;
  }
  // 悬浮加重小圆点阴影
  .circleDotLeft:hover::after {
    box-shadow: 0 1px 18px 0 rgba(0, 0, 0, 0.5);
  }
}
// 除了cursor样式的not-allowed还要搭配js判断才禁用到位
.disabledSwitch {
  cursor: not-allowed;
  opacity: 0.48;
}
// 禁用情况下,保持小圆点原有阴影
.disabledSwitch .circleDotLeft:hover::after {
  box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3);
}
// 小型switch组件做一个缩放
.small {
  zoom: 0.8;
}
// 大型switch组件做一个缩放
.big {
  zoom: 1.6;
}
</style>

注意 true-value和false-value 是官方自带的搭配v-model属性,其实这里不用也行,大家参考一下antd的组件便可明了。详见: https://cn.vuejs.org/guide/es...