173 lines
4.6 KiB
Vue
173 lines
4.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref, useSlots, watch } from 'vue'
|
|
import { Check, Minus } from '@lucide/vue'
|
|
import { useNamespace } from '../_composables/useNamespace'
|
|
import type { CheckboxProps } from './types'
|
|
|
|
/**
|
|
* 复选框 —— 支持选中 / 未选中 / 半选三种状态。
|
|
*
|
|
* 半选(indeterminate)是原生 DOM property,无法通过 attribute 绑定,组件内部手动同步;
|
|
* 勾选态优先,选中时会强制清除半选。
|
|
*/
|
|
defineOptions({ name: 'G3Checkbox' })
|
|
|
|
const props = withDefaults(defineProps<CheckboxProps>(), {
|
|
modelValue: false,
|
|
indeterminate: false,
|
|
disabled: false,
|
|
name: undefined,
|
|
value: undefined,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
change: [value: boolean, ev: Event]
|
|
focus: [ev: FocusEvent]
|
|
blur: [ev: FocusEvent]
|
|
}>()
|
|
|
|
const { b, e, m, is } = useNamespace('checkbox')
|
|
|
|
const slots = useSlots()
|
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
/** 选中态优先显示勾选;仅在未选中且半选时显示减号 */
|
|
const icon = computed(() => {
|
|
if (props.modelValue) return Check
|
|
if (props.indeterminate) return Minus
|
|
return null
|
|
})
|
|
|
|
const classes = computed(() => [
|
|
b(),
|
|
{
|
|
[is('checked')]: props.modelValue,
|
|
[is('indeterminate')]: props.indeterminate,
|
|
[is('disabled')]: props.disabled,
|
|
// 有文案时才需要「勾选框对齐首行」的定位;无文案保持默认居中
|
|
[is('with-label')]: !!slots.default,
|
|
},
|
|
])
|
|
|
|
function onChange(event: Event) {
|
|
const checked = (event.target as HTMLInputElement).checked
|
|
emit('update:modelValue', checked)
|
|
emit('change', checked, event)
|
|
}
|
|
|
|
function syncIndeterminate() {
|
|
if (inputRef.value) {
|
|
inputRef.value.indeterminate = props.indeterminate && !props.modelValue
|
|
}
|
|
}
|
|
|
|
onMounted(syncIndeterminate)
|
|
watch(() => [props.indeterminate, props.modelValue], syncIndeterminate)
|
|
</script>
|
|
|
|
<template>
|
|
<label :class="classes">
|
|
<input
|
|
ref="inputRef"
|
|
:class="e('input')"
|
|
type="checkbox"
|
|
:checked="modelValue"
|
|
:disabled="disabled"
|
|
:name="name"
|
|
:value="value"
|
|
@change="onChange"
|
|
@focus="emit('focus', $event)"
|
|
@blur="emit('blur', $event)"
|
|
/>
|
|
<span :class="e('box')" aria-hidden="true">
|
|
<component :is="icon" v-if="icon" />
|
|
</span>
|
|
<span v-if="$slots.default" :class="e('label')">
|
|
<slot />
|
|
</span>
|
|
</label>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
@use '@g3soft/tokens/scss' as *;
|
|
|
|
.#{b('checkbox')} {
|
|
display: inline-flex;
|
|
vertical-align: middle;
|
|
align-items: center;
|
|
gap: v('space-2', 8px);
|
|
color: v('text-1', rgba(0, 0, 0, 0.88));
|
|
font-size: v('font-size', 14px);
|
|
line-height: v('line-height', 1.5);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
/* 有文案时对齐到首行:root 顶对齐,勾选框下沉半行高差,使其中心落在
|
|
第一行文字中心;单行结果与默认 center 等价,多行富内容则对齐首行而非整块居中 */
|
|
.#{b('checkbox')}.#{is('with-label')} {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.#{b('checkbox')}.#{is('with-label')} .#{e('checkbox', 'box')} {
|
|
margin-top: calc((v('line-height', 1.5) * 1em - v('checkbox-size', 16px)) / 2);
|
|
}
|
|
|
|
.#{e('checkbox', 'input')} {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
margin: 0;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.#{e('checkbox', 'box')} {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
flex-shrink: 0;
|
|
width: v('checkbox-size', 16px);
|
|
height: v('checkbox-size', 16px);
|
|
border: 1px solid v('border-color', #d9d9d9);
|
|
border-radius: v('radius-sm', 4px);
|
|
background: v('bg-container', #fff);
|
|
color: v('text-inverse', #fff);
|
|
transition:
|
|
background-color v('duration', 0.2s) v('ease', ease),
|
|
border-color v('duration', 0.2s) v('ease', ease);
|
|
|
|
svg {
|
|
width: 10px;
|
|
height: 10px;
|
|
}
|
|
}
|
|
|
|
.#{b('checkbox')}:hover:not(.#{is('disabled')}) .#{e('checkbox', 'box')} {
|
|
border-color: v('color-primary', #18181b);
|
|
}
|
|
|
|
.#{b('checkbox')}.#{is('checked')} .#{e('checkbox', 'box')},
|
|
.#{b('checkbox')}.#{is('indeterminate')} .#{e('checkbox', 'box')} {
|
|
border-color: v('color-primary', #18181b);
|
|
background: v('color-primary', #18181b);
|
|
}
|
|
|
|
.#{e('checkbox', 'input')}:focus-visible + .#{e('checkbox', 'box')} {
|
|
@include focus-ring();
|
|
}
|
|
|
|
.#{b('checkbox')}.#{is('disabled')} {
|
|
color: v('text-disabled', rgba(0, 0, 0, 0.25));
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.#{b('checkbox')}.#{is('disabled')} .#{e('checkbox', 'box')} {
|
|
border-color: v('border-color', #d9d9d9);
|
|
background: v('bg-disabled', rgba(0, 0, 0, 0.04));
|
|
color: v('text-disabled', rgba(0, 0, 0, 0.25));
|
|
}
|
|
</style>
|