在vue 2.x版本上由于switch 开关不支持异步,我便观看了一下element-ui的switch的源码,发现是作者当时没有考虑到这种情况。
想起vant的源码写的好,于是我看了下vant 官网的switch对异步是支持的,便看了下源码,发现vant-ui 中的 switch 对异步支持是依靠$confirm来实现的,本身代码也不支持异步属性事件。
最后我去element-plus,发现相比于老版本的switch 新添加了几个属性,也移除了部分属性。就将plus版本的对于异步支持的BeforeChange属性事件加到了element-ui的switch上面,就此实现了异步,由于切换比较生硬,我便将plus的loading属性加上去了。
这样子我们就能通过实际请求或逻辑来判定是否是成功开启或者关闭状态了。
使用组件
<!--
* Copyright ©
* #
* @author: zw
* @date: 2022-07-15
-->
<template>
<el-row class="mt-20" :gutter="20">
<el-col :span="8" :push="8">
<el-switch v-model="value" :before-change="beforeChange" @change="change" :loading="loading" activeValue="yes" inactiveValue="no" active-color="#13ce66" width="80" active-text="按月付费" inactive-text="按年付费" />
</el-col>
</el-row>
</template>
<script>
export default {
name: 'demo',
data() {
return {
value: 'no',
loading: false,
};
},
methods: {
async beforeChange(currentVal) {
this.loading = true;
return this.$confirm(`此操作将改变为<span style="color: #${!currentVal ? '409EFF' : 'F56C6C'};"> ${!currentVal} </span>状态,是否继续?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', dangerouslyUseHTMLString: true }).catch(() => {
this.loading = false;
return Promise.reject("cancel");
});
},
change(val) {
setTimeout(() => {
this.$message.success('switch success' + val);
this.loading = false;
}, 1500)
}
// End
}
</script>
组件源码 1 (继承)
<!--
* Copyright ©
* #
* @author: zw
* @date: 2022-07-15
-->
<script>
import { Switch } from 'element-ui'
const isFunction = (val) => typeof val === 'function'
const isObject = (val) => val !== null && typeof val === 'object'
const isPromise = (val) => isObject(val) && isFunction(val.then) && isFunction(val.catch)
const isBool = (val) => typeof val === 'boolean'
export default {
extends: Switch,
props: {
width: { type: [String, Number], default: 40 },
beforeChange: Function,
},
methods: {
switchValue() {
if (this.switchDisabled) return
const { beforeChange, checked } = this
if (!beforeChange) {
return this.handleChange()
}
const shouldChange = beforeChange(checked)
const isExpectType = [isPromise(shouldChange), isBool(shouldChange)].some((i) => i)
if (!isExpectType) {
error(this.$options.name, 'beforeChange must return type `Promise<boolean>` or `boolean`')
}
if (isPromise(shouldChange)) {
shouldChange
.then(() => {
this.handleChange()
})
.catch((e) => {
if (process.env.NODE_ENV !== 'production') {
console.warn(this.$options.name, `some error occurred: ${e}`)
}
})
} else if (shouldChange) {
this.handleChange()
}
},
},
}
</script>
<style lang="scss" scoped></style>
组件源码 2 (完整)
<!--
* Copyright ©
* #
* @author: zw
* @date: 2022-07-15
-->
<template>
<div class="el-switch" :class="{ 'is-disabled': switchDisabled, 'is-checked': checked }" role="switch" :aria-checked="checked" :aria-disabled="switchDisabled" @click.prevent="switchValue">
<input class="el-switch__input" type="checkbox" ref="input" :id="id" :name="name" :true-value="activeValue" :false-value="inactiveValue" :disabled="switchDisabled">
<span :class="['el-switch__label', 'el-switch__label--left', !checked && 'is-active']" v-if="inactiveIconClass || inactiveText">
<i :class="[inactiveIconClass]" v-if="inactiveIconClass"></i>
<span v-if="!inactiveIconClass && inactiveText" :aria-hidden="checked">{{ inactiveText }}</span>
</span>
<span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
<div class="el-switch__action">
<i v-if="loading" class="el-icon-loading" />
</div>
</span>
<span :class="['el-switch__label', 'el-switch__label--right', checked && 'is-active']" v-if="activeIconClass || activeText">
<i :class="[activeIconClass]" v-if="activeIconClass"></i>
<span v-if="!activeIconClass && activeText" :aria-hidden="!checked">{{ activeText }}</span>
</span>
</div>
</template>
<script>
const kebabCase = (str, hyphenateRE = /([^-])([A-Z])/g) => str.replace(hyphenateRE, '$1-$2').replace(hyphenateRE, '$1-$2').toLowerCase();
const isFunction = (val) => typeof val === 'function';
const isObject = (val) => val !== null && typeof val === 'object';
const isPromise = (val) => isObject(val) && isFunction(val.then) && isFunction(val.catch);
const isBool = (val) => typeof val === 'boolean';
class ElementPlusError extends Error {
constructor(m) {
super(m);
this.name = 'ElementPlusError';
}
}
var error = (scope, m) => {
throw new ElementPlusError(`[${scope}] ${m}`);
};
function warn(scope, m) {
console.warn(new ElementPlusError(`[${scope}] ${m}`));
}
export default {
name: 'ElSwitch',
data() {
return {
coreWidth: this.width
};
},
props: {
value: { type: [Boolean, String, Number], default: false },
disabled: { type: Boolean, efault: false },
width: { type: [String, Number], default: 40 },
activeIconClass: { type: String, default: '' },
inactiveIconClass: { type: String, default: '' },
activeText: String,
inactiveText: String,
activeColor: { type: String, default: '' },
inactiveColor: { type: String, default: '' },
activeValue: { type: [Boolean, String, Number], default: true },
inactiveValue: { type: [Boolean, String, Number], default: false },
name: { type: String, default: '' },
validateEvent: { type: Boolean, default: true },
id: String,
loading: { type: Boolean, default: false },
beforeChange: Function
},
created() {
if (!~[this.activeValue, this.inactiveValue].indexOf(this.value)) {
this.$emit('input', this.inactiveValue);
}
},
mounted() {
this.checkMigratingConfig();
this.coreWidth = this.width || 40;
if (this.activeColor || this.inactiveColor) {
this.setBackgroundColor();
}
this.$refs.input.checked = this.checked;
},
methods: {
handleChange() {
const val = this.checked ? this.inactiveValue : this.activeValue;
this.$emit('input', val);
this.$emit('change', val);
this.$nextTick(() => {
// set input's checked property
// in case parent refuses to change component's value
if (this.$refs.input) {
this.$refs.input.checked = this.checked;
}
});
},
setBackgroundColor() {
let newColor = this.checked ? this.activeColor : this.inactiveColor;
this.$refs.core.style.borderColor = newColor;
this.$refs.core.style.backgroundColor = newColor;
},
switchValue() {
if (this.switchDisabled) return;
const { beforeChange, checked } = this;
if (!beforeChange) {
return this.handleChange();
}
const shouldChange = beforeChange(checked);
const isExpectType = [isPromise(shouldChange), isBool(shouldChange)].some((i) => i);
if (!isExpectType) {
error(this.$options.name, "beforeChange must return type `Promise<boolean>` or `boolean`");
}
if (isPromise(shouldChange)) {
shouldChange.then(() => {
this.handleChange();
}).catch((e) => {
if (process.env.NODE_ENV !== "production") {
warn(this.$options.name, `some error occurred: ${e}`);
}
});
} else if (shouldChange) {
this.handleChange();
}
},
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
checkMigratingConfig() {
if (process.env.NODE_ENV === 'production') return;
if (!this.$vnode) return;
const { props = {}, events = {} } = this.getMigratingConfig();
const { data, componentOptions } = this.$vnode;
const definedProps = data.attrs || {};
const definedEvents = componentOptions.listeners || {};
for (let propName in definedProps) {
propName = kebabCase(propName); // compatible with camel case
if (props[propName]) {
console.warn(`[Element Migrating][${this.$options.name}][Attribute]: ${props[propName]}`);
}
}
for (let eventName in definedEvents) {
eventName = kebabCase(eventName); // compatible with camel case
if (events[eventName]) {
console.warn(`[Element Migrating][${this.$options.name}][Event]: ${events[eventName]}`);
}
}
},
getMigratingConfig() {
return {
props: {
'on-color': 'on-color is renamed to active-color.',
'off-color': 'off-color is renamed to inactive-color.',
'on-text': 'on-text is renamed to active-text.',
'off-text': 'off-text is renamed to inactive-text.',
'on-value': 'on-value is renamed to active-value.',
'off-value': 'off-value is renamed to inactive-value.',
'on-icon-class': 'on-icon-class is renamed to active-icon-class.',
'off-icon-class': 'off-icon-class is renamed to inactive-icon-class.'
}
};
},
focus() {
this.$refs['input'].focus();
}
},
computed: {
checked() {
return this.value === this.activeValue;
},
switchDisabled() {
return this.disabled || this.loading || (this.elForm || {}).disabled;
}
},
watch: {
checked() {
this.$refs.input.checked = this.checked;
if (this.activeColor || this.inactiveColor) {
this.setBackgroundColor();
}
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [this.value]);
}
}
},
// End
}
</script>
<style lang='css' scoped>
.el-switch {
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
font-size: 14px;
line-height: 20px;
height: 20px;
vertical-align: middle;
}
.el-switch.is-disabled .el-switch__core,
.el-switch.is-disabled .el-switch__label {
cursor: not-allowed;
}
.el-switch__core,
.el-switch__label {
display: inline-block;
cursor: pointer;
vertical-align: middle;
}
.el-switch__label {
-webkit-transition: 0.2s;
transition: 0.2s;
height: 20px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.el-switch__label.is-active {
color: #409eff;
}
.el-switch__label--left {
margin-right: 10px;
}
.el-switch__label--right {
margin-left: 10px;
}
.el-switch__label * {
line-height: 1;
font-size: 14px;
display: inline-block;
}
.el-switch__input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.el-switch__core {
margin: 0;
position: relative;
width: 40px;
height: 20px;
border: 1px solid #dcdfe6;
outline: 0;
border-radius: 10px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
background: #dcdfe6;
-webkit-transition: border-color 0.3s, background-color 0.3s;
transition: border-color 0.3s, background-color 0.3s;
}
.el-switch__core .el-switch__action {
position: absolute;
top: 1px;
left: 1px;
z-index: 9999;
border-radius: 100%;
-webkit-transition: all 0.3s;
transition: all 0.3s;
width: 16px;
height: 16px;
background-color: #fff;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: #303133;
}
.el-switch__core:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
z-index: 9998;
border-radius: 100%;
-webkit-transition: all 0.3s;
transition: all 0.3s;
width: 16px;
height: 16px;
background-color: #fff;
}
.el-switch.is-checked .el-switch__core {
border-color: #409eff;
background-color: #409eff;
}
.el-switch.is-checked .el-switch__core .el-switch__action {
left: 100%;
margin-left: -17px;
color: #409eff;
}
.el-switch.is-checked .el-switch__core::after {
left: 100%;
margin-left: -17px;
}
.el-switch.is-disabled {
opacity: 0.6;
}
.el-switch--wide .el-switch__label.el-switch__label--left span {
left: 10px;
}
.el-switch--wide .el-switch__label.el-switch__label--right span {
right: 10px;
}
.el-switch .label-fade-enter,
.el-switch .label-fade-leave-active {
opacity: 0;
}
</style>
main.js 中需要覆盖掉默认注册的ELSwitch
import ElementUI from "element-ui"
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
import ElSwitch from '@/components/switch.vue';
Vue.component('el-switch', ElSwitch); // 这个步骤同等于对象重新赋值的操作