首先构建Checkbox组件的目录结构:
├── packages
│ ├── components
│ │ ├── checkbox
│ │ │ ├── src # 组件入口目录
│ │ │ │ ├── checkbox.ts # Ts类型和组件属性
│ │ │ │ └── checkbox.vue # 组件代码
│ │ │ └── index.ts # 组件入口文件
Ts类型声明和组件属性定义
checkbox
import { UPDATE_MODEL_EVENT } from '@storm/constants'
import { isBoolean, isNumber, isString } from '@storm/utils'
import { ExtractPropTypes } from 'vue'
export type CheckboxValueType = number | string | boolean
export const checkboxProps = {
modelValue: {
type: [Number, String, Boolean],
default: undefined,
},
label: {
type: [String, Boolean, Number]
},
// 设置半选状态,仅负责样式控制
indeterminate: Boolean,
disabled: Boolean,
checked: Boolean,
name: String
}
export const checkboxEmits = {
[UPDATE_MODEL_EVENT]: (val: CheckboxValueType) => isNumber(val) || isString(val) || isBoolean(val),
change: (val: CheckboxValueType) => isNumber(val) || isString(val) || isBoolean(val)
}
// 提供外部使用
export type CheckboxProps = ExtractPropTypes<typeof checkboxProps>
export type CheckboxEmits = typeof checkboxEmits
checkbox-group
import { ExtractPropTypes, PropType, WritableComputedRef, InjectionKey } from 'vue'
import { CheckboxValueType } from './checkbox'
import { UPDATE_MODEL_EVENT } from '@storm/constants'
import { isArray } from '@storm/utils'
// 排除CheckboxValueType中的boolean类型
export type CheckboxGroupValueType = Exclude<CheckboxValueType, boolean>[]
type CheckboxGroupContext = {
modelValue?: WritableComputedRef<any>,
disabled?: boolean,
changeEvent?: (...args: any) => any
}
export const checkboxGroupProps = {
modelValue: {
type: Array as PropType<CheckboxGroupValueType>,
default: () => []
},
diabled: Boolean
}
export const checkGroupEmits = {
[UPDATE_MODEL_EVENT]: (val: CheckboxGroupValueType) => isArray(val),
change: (val: CheckboxGroupValueType) => isArray(val)
}
// inject传递的一些属性的类型
export const checkboxGroupContextKey: InjectionKey<CheckboxGroupContext> = Symbol('checkboxGroupContextKey')
// 提供外部使用
export type CheckboxGroupProps = ExtractPropTypes<typeof checkboxGroupProps>
export type CheckboxGroupEmits = typeof checkGroupEmits
组件实现
checkbox
<template>
<label :class="[
bem.b(),
bem.is('disabled', isDisabled),
bem.is('checked', isChecked)
]">
<span :class="[
bem.e('input'),
bem.is('disabled', isDisabled),
bem.is('checked', isChecked),
bem.is('indeterminate', indeterminate)
]">
<input
type="checkbox"
:class="bem.e('origin')"
v-model="model"
:value="label"
:disabled="isDisabled"
:name="name"
@change="handleChange"
>
<span :class="bem.e('inner')"></span>
</span>
<span :class="bem.e('label')" v-if="hasOwnLabel">
<slot>
{{ label }}
</slot>
</span>
</label>
</template>
<script setup lang="ts">
import { createNamespace } from '@storm/utils';
import { checkboxEmits, checkboxProps } from './checkbox';
import { useCheckbox } from './use-checkbox';
import { useSlots } from 'vue';
defineOptions({ name: 'SCheckbox' })
const props = defineProps(checkboxProps)
defineEmits(checkboxEmits)
const slots = useSlots()
const bem = createNamespace('checkbox')
const { model, isChecked, isDisabled, hasOwnLabel, handleChange } = useCheckbox(props, slots)
</script>
use-checkbox.ts
import { SetupContext, computed, getCurrentInstance, inject } from "vue";
import { CheckboxProps } from "./checkbox";
import { checkboxGroupContextKey } from "./checkbox-group";
import { isArray, isBoolean } from "@storm/utils";
import { UPDATE_MODEL_EVENT } from "@storm/constants";
export const useCheckbox = (props: CheckboxProps, slots: SetupContext['slots']) => {
const { emit } = getCurrentInstance()!
// 注入checkbox-group的值
const checkboxGroup = inject(checkboxGroupContextKey, undefined)
const isGroup = computed(() => !!checkboxGroup)
const model = computed({
get() {
return isGroup.value ? checkboxGroup?.modelValue?.value : props.modelValue
},
set(val: unknown) {
if (isGroup.value && isArray(val)) {
checkboxGroup?.changeEvent?.(val)
} else {
emit(UPDATE_MODEL_EVENT, val)
}
}
})
// 处理不同绑定值的选中状态
const isChecked = computed(() => {
const value = model.value
if (isBoolean(value)) {
return value
} else if (isArray(value)) {
return value.includes(props.label)
} else {
return !!value
}
})
const isDisabled = computed(() => checkboxGroup?.disabled ? checkboxGroup.disabled : props.disabled)
// 没传label也没有默认插槽
const hasOwnLabel = computed<boolean>(() => {
return !!(slots.default || props.label)
})
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement
emit('change', target.checked)
}
return {
model,
isChecked,
isDisabled,
hasOwnLabel,
handleChange
}
}
checkbox-group
<template>
<div :class="bem.b('group')">
<slot />
</div>
</template>
<script setup lang="ts">
import { createNamespace } from '@storm/utils';
import { checkGroupEmits, checkboxGroupProps, CheckboxGroupValueType, checkboxGroupContextKey } from './checkbox-group';
import { computed, provide } from 'vue';
import { UPDATE_MODEL_EVENT } from '@storm/constants';
defineOptions({ name: 'SCheckboxGroup' })
const props = defineProps(checkboxGroupProps)
const emit = defineEmits(checkGroupEmits)
const bem = createNamespace('checkbox')
const modelValue = computed({
get() {
return props.modelValue
},
set(val: CheckboxGroupValueType) {
changeEvent(val)
}
})
const changeEvent = (value: CheckboxGroupValueType) => {
emit(UPDATE_MODEL_EVENT, value)
emit('change', value)
}
// 提供给checkbox的一些值和方法
provide(checkboxGroupContextKey, {
modelValue,
disabled: props.diabled,
changeEvent
})
</script>
入口文件
import { withInstall } from '@storm/utils'
import _Checkbox from './src/checkbox.vue'
import _CheckboxGroup from './src/checkbox-group.vue'
// 添加install方法 注册checkbox的时候同时注册checkbox-group
export const Checkbox = withInstall(_Checkbox, {
_CheckboxGroup
})
export default Checkbox
export * from './src/checkbox'
export * from './src/checkbox-group'
// 配合volar插件 可以在模版中被解析
declare module 'vue' {
export interface GlobalComponents {
SCheckbox: typeof Checkbox
}
}
样式文件
s-checkbox__inner类加上伪类用来实现勾选框。
@use "./mixins/mixins.scss" as *;
@use "./common/var.scss" as *;
@include b(checkbox) {
display: inline-flex;
align-items: center;
height: 32px;
margin-right: 30px;
font-size: $font-size-base;
font-weight: 500;
color: $color-text;
white-space: nowrap;
user-select: none;
cursor: pointer;
@include e(input) {
display: inline-flex;
vertical-align: middle;
@include when(checked) {
.#{$namespace}-checkbox__inner {
border-color: $color-primary;
background-color: $color-primary;
&:after {
border-color: $color-white;
transform: rotate(45deg) scaleY(1);
}
}
& + .#{$namespace}-checkbox__label {
color: $color-primary;
}
}
@include when(disabled) {
.#{$namespace}-checkbox__inner {
border-color: $color-border;
background-color: $color-bg-disabled;
}
& + span.#{$namespace}-checkbox__label {
color: $color-placeholder-text;
}
&.is-checked {
.#{$namespace}-checkbox__inner {
&::after {
border-color: $color-placeholder-text;
}
}
}
&.is-indeterminate {
.#{$namespace}-checkbox__inner {
border-color: $color-border;
background-color: $color-bg-disabled;
&::before {
background-color: $color-placeholder-text;
}
}
}
}
@include when(indeterminate) {
.#{$namespace}-checkbox__inner {
border-color: $color-primary;
background-color: $color-primary;
&:after {
display: none;
}
&:before {
content: "";
position: absolute;
top: 5px;
left: 0;
right: 0;
height: 2px;
background-color: $color-white;
transform: scale(0.5);
}
}
}
}
@include e(origin) {
position: absolute;
width: 0;
height: 0;
z-index: -1;
opacity: 0;
}
@include e(inner) {
position: relative;
display: inline-block;
width: 14px;
height: 14px;
border-radius: 2px;
border: 1px solid $color-border;
background-color: $color-white;
&:after {
content: "";
position: absolute;
top: 1px;
left: 4px;
width: 3px;
height: 7px;
border: 1px solid transparent;
border-left: 0;
border-top: 0;
transform-origin: center;
transform: rotate(45deg) scaleY(0);
transition: transform 0.15s ease-in 0.05s;
}
}
@include e(label) {
display: inline-block;
vertical-align: middle;
line-height: 1;
padding-left: 8px;
}
@include when(disabled) {
cursor: not-allowed;
}
}
最终效果
基础用法:
多选框组:
中间状态: