This commit is contained in:
oneao committed 2025-09-17 14:49:15 +08:00
1 parent 104e0a78c6
commit 4d0d2f875e
13 files changed
+802 -226

No files matched your search

@@ -0,0 +1,176 @@
import type { PropType, RenderFunction } from 'vue'
import { computed, defineComponent, provide, reactive, ref, watch } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Space from '../space/index'
import Checkbox from './checkbox'
import { checkboxGroupKey } from './context'
interface CheckboxOption {
label?: string | RenderFunction
value?: string | number
extra?: string | RenderFunction
disabled?: boolean
[key: string]: any
}
export default defineComponent({
name: 'CheckboxGroup',
props: {
modelValue: {
type: Array as PropType<Array<string | number | boolean>>,
default: () => [],
},
defaultValue: {
type: Array as PropType<Array<string | number | boolean>>,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
extraField: {
type: String,
default: 'extra',
},
options: {
type: Array as PropType<Array<string | number | CheckboxOption>>,
default: [],
},
spaceProps: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
type: {
type: String as PropType<'default' | 'card' | 'pureCard'>,
default: 'default',
},
class: {
type: [String, Array, Object],
default: '',
},
style: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue', 'change'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('checkbox-group')
const _value = ref(props.modelValue ?? props.defaultValue ?? [])
watch(() => props.modelValue, (newVal) => {
_value.value = newVal ?? []
})
const groupDisabled = computed(() => props.disabled)
const groupType = computed(() => props.type)
const handleChange = (val: string | number | boolean, e: Event) => {
const current = _value.value ? [..._value.value] : []
const index = current.indexOf(val)
if (index > -1) {
// 已存在 -> 取消选中
current.splice(index, 1)
}
else {
// 不存在 -> 添加
current.push(val)
}
// 非受控模式才更新内部值
if (props.modelValue === undefined) {
_value.value = current
}
// 通知外部
emit('update:modelValue', current)
emit('change', current, e)
}
// 提供 context 给 Radio 使用
provide(checkboxGroupKey, reactive({
name: 'AoCheckboxGroup' as const,
value: _value,
disabled: groupDisabled,
type: groupType,
slots,
handleChange,
}))
const renderOptions = () => {
return props.options.map((option) => {
const opt
= typeof option === 'string' || typeof option === 'number'
? { [props.labelField]: String(option), [props.valueField]: option }
: option
const labelContent = slots.label
? slots.label({ data: opt })
: typeof opt[props.labelField] === 'function'
? opt[props.labelField]()
: opt[props.labelField]
const extraContent = slots.extra
? slots.extra({ data: opt })
: typeof opt[props.extraField] === 'function'
? opt[props.extraField]()
: opt[props.extraField]
const isChecked
= _value.value.includes(opt[props.valueField])
if (slots.radio) {
return (
<div
key={opt[props.valueField]}
onClick={e => handleChange(opt[props.valueField], e)}
>
{slots.radio({ data: opt, checked: _value.value === opt[props.valueField] })}
</div>
)
}
return (
<Checkbox
key={opt[props.valueField]}
value={opt[props.valueField]}
extra={extraContent}
disabled={opt.disabled ?? props.disabled}
checked={isChecked}
type={props.type}
>
{labelContent}
</Checkbox>
)
})
}
const spaceSize
= ['default', 'card', 'pureCard'].includes(groupType.value)
? [6, 6]
: props.spaceProps.size ?? 0
return () => (
<Space
align="start"
size={spaceSize}
{...props.spaceProps}
class={[
`${prefixCls}-${groupType.value}`,
]}
>
{props.options.length > 0 ? renderOptions() : slots.default?.()}
</Space>
)
},
})
@@ -0,0 +1,225 @@
import type { PropType, VNode } from 'vue'
import type { CheckboxGroupContext } from './context'
import { computed, defineComponent, inject, ref, watch } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Icon from '../icon/index'
import { checkboxGroupKey } from './context'
export default defineComponent({
name: 'Checkbox',
props: {
modelValue: {
type: Boolean,
default: undefined,
},
checked: {
type: Boolean,
default: undefined,
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
default: true,
},
indeterminate: {
type: Boolean,
default: false,
},
defaultChecked: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
type: {
type: String as PropType<'default' | 'card' | 'pureCard'>,
default: 'default',
},
extra: {
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
default: '',
},
class: {
type: [String, Array, Object],
default: '',
},
style: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue', 'update:checked', 'change'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('checkbox')
const checkboxGroupCtx = inject<CheckboxGroupContext | undefined>(checkboxGroupKey, undefined)
const inGroup = computed(() => !!checkboxGroupCtx)
const isDisabled = computed(() => props.disabled || checkboxGroupCtx?.disabled)
const _checked = ref(props.defaultChecked)
const mergedType = computed(() =>
props.type || checkboxGroupCtx?.type || 'default',
)
const isChecked = computed(() => {
if (inGroup.value && checkboxGroupCtx) {
const groupValue = checkboxGroupCtx.value
if (Array.isArray(groupValue)) {
return groupValue.includes(props.value)
}
return groupValue === props.value
}
return props.checked ?? props.modelValue ?? _checked.value
})
const handleClick = (event: Event) => {
if (isDisabled.value) {
return
}
if (inGroup.value && checkboxGroupCtx) {
checkboxGroupCtx.handleChange(props.value, event)
}
else {
const newChecked = !isChecked.value
if (props.checked === undefined)
_checked.value = newChecked
emit('update:modelValue', newChecked)
emit('update:checked', newChecked)
}
emit('change', { value: props.value, event })
}
watch(() => props.checked, (val) => {
if (val !== undefined) {
_checked.value = val
}
})
const renderDefault = () => (
<label
class={[
`${prefixCls}-default`,
{ [`${prefixCls}-checked`]: isChecked.value },
{ [`${prefixCls}-disabled`]: isDisabled.value },
props.class,
]}
style={{ ...props.style }}
onClick={handleClick}
>
<span class={`${prefixCls}-main`}>
<input
class={`${prefixCls}-input`}
type="checkbox"
onClick={e => e.stopPropagation()}
/>
<span class={`${prefixCls}-inner`}>
{
props.indeterminate
? (
<Icon icon="garden:dash-fill-12" />
)
: isChecked.value
? (
<Icon icon="garden:check-sm-fill-12" />
)
: null
}
</span>
</span>
<div class={`${prefixCls}-content`}>
<div class={`${prefixCls}-label`}>
{slots.default?.()}
</div>
{props.extra && (
<div class={`${prefixCls}-extra`}>
{typeof props.extra === 'function' ? props.extra() : props.extra}
</div>
)}
</div>
</label>
)
const renderCard = () => (
<label
class={[
`${prefixCls}-card`,
{ [`${prefixCls}-checked`]: isChecked.value },
{ [`${prefixCls}-disabled`]: isDisabled.value },
props.class,
]}
style={{ ...props.style }}
onClick={handleClick}
>
<span class={`${prefixCls}-main`}>
<input
class={`${prefixCls}-input`}
type="checkbox"
onClick={e => e.stopPropagation()}
/>
<span class={`${prefixCls}-inner`}>
{
props.indeterminate
? (
<Icon icon="garden:dash-fill-12" />
)
: isChecked.value
? (
<Icon icon="garden:check-sm-fill-12" />
)
: null
}
</span>
</span>
<div class={`${prefixCls}-content`}>
<div class={`${prefixCls}-label`}>
{slots.default?.()}
</div>
{props.extra && (
<div class={`${prefixCls}-extra`}>
{typeof props.extra === 'function' ? props.extra() : props.extra}
</div>
)}
</div>
</label>
)
const renderPureCard = () => (
<label
class={[
`${prefixCls}-pureCard`,
{ [`${prefixCls}-checked`]: isChecked.value },
{ [`${prefixCls}-disabled`]: isDisabled.value },
props.class,
]}
style={{ ...props.style }}
onClick={handleClick}
>
<div class={`${prefixCls}-content`}>
<div class={`${prefixCls}-label`}>
{slots.default?.()}
</div>
{props.extra && (
<div class={`${prefixCls}-extra`}>
{typeof props.extra === 'function' ? props.extra() : props.extra}
</div>
)}
</div>
</label>
)
return () => {
switch (mergedType.value) {
case 'card':
return renderCard()
case 'pureCard':
return renderPureCard()
default:
return renderDefault()
}
}
},
})
@@ -0,0 +1,13 @@
import type { InjectionKey, Slots } from 'vue'
export interface CheckboxGroupContext {
name: 'AoCheckboxGroup'
value: (string | number | boolean)[]
disabled: boolean
slots: Slots
type?: 'default' | 'card' | 'pureCard'
handleChange: (value: string | number | boolean, e: Event) => void
}
export const checkboxGroupKey: InjectionKey<CheckboxGroupContext>
= Symbol('CheckboxGroup')
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { ref } from 'vue'
/* 单个受控 */
const checked1 = ref(false)
/* 单个非受控(只用 defaultChecked) */
const checked2 = ref(true) // 只是初始值演示,不跟随
/* group 多选 */
const groupValue = ref<Array<string | number>>(['B'])
</script>
<template>
<div style="padding:20px">
<h3>单个 Checkbox(受控 v-model:checked)</h3>
<ACheckbox v-model:checked="checked1" value="single1">
选项 1
</ACheckbox>
<p>当前状态: {{ checked1 }}</p>
<h3>单个 Checkbox(非受控 defaultChecked)</h3>
<ACheckbox value="single2" :default-checked="checked2">
选项 2
</ACheckbox>
<p>初始状态: {{ checked2 }}(点击后组件自己维护,不影响这个 ref)</p>
<h3>CheckboxGroup(多选)</h3>
<ACheckboxGroup v-model="groupValue" :options="['A', 'B', 'C']" />
<p>选中值: {{ groupValue }}</p>
<h3>CheckboxGroup(自定义插槽用法)</h3>
<ACheckboxGroup v-model="groupValue">
<ACheckbox value="X">
自定义 X
</ACheckbox>
<ACheckbox value="Y">
自定义 Y
</ACheckbox>
<ACheckbox value="Z">
自定义 Z
</ACheckbox>
</ACheckboxGroup>
<p>选中值: {{ groupValue }}</p>
</div>
</template>
@@ -0,0 +1,222 @@
@use '../_styles/_variables.scss' as *;
@use 'sass:color';
$checkbox-prefix: '#{$prefix}-checkbox';
.#{$checkbox-prefix}-default {
display: inline-flex;
vertical-align: middle;
color: $text-color-base;
align-items: flex-start;
position: relative;
cursor: pointer;
user-select: none;
&.#{$checkbox-prefix}-checked {
.#{$checkbox-prefix}-main {
.#{$checkbox-prefix}-inner {
border-color: transparent;
background-color: $primary-color;
color: $text-color-anti;
}
}
}
.#{$checkbox-prefix}-main {
cursor: pointer;
display: flex;
align-items: center;
white-space: nowrap;
outline: 0;
.#{$checkbox-prefix}-input {
opacity: 0;
outline: 0;
position: absolute;
z-index: -1;
margin: 0;
width: 0;
height: 0;
box-sizing: border-box;
}
.#{$checkbox-prefix}-inner {
border: 1px solid $border-color;
border-radius: $border-radius;
width: 16px;
height: 16px;
background-color: $bg-color-container;
cursor: pointer;
box-sizing: border-box;
outline: 0;
display: flex;
align-items: center;
justify-content: center;
transition:
background-color 0.1s ease,
border-color 0.1s ease;
}
}
.#{$checkbox-prefix}-content {
margin-left: 8px;
display: inline-block;
flex-direction: column;
.#{$checkbox-prefix}-label {
font-size: 14px;
line-height: 16px;
display: flex;
align-items: center;
color: $text-color-base;
}
.#{$checkbox-prefix}-extra {
font-size: 12px;
color: $text-color-secondary;
margin-top: 4px;
}
}
}
.#{$checkbox-prefix}-card {
color: $text-color-base;
display: inline-flex;
align-items: flex-start;
position: relative;
cursor: pointer;
user-select: none;
padding: 8px 12px;
box-sizing: border-box;
border-radius: $border-radius;
border: 1px solid transparent;
&:hover,
&:hover .#{$checkbox-prefix}-main {
background-color: $bg-color-component;
}
&.#{$checkbox-prefix}-disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.#{$checkbox-prefix}-checked {
border-color: $primary-color;
background-color: $status-info-bg-color;
}
&:hover .#{$checkbox-prefix}-main .#{$checkbox-prefix}-inner {
border-color: $primary-color;
}
&.#{$checkbox-prefix}-checked {
.#{$checkbox-prefix}-main {
.#{$checkbox-prefix}-inner {
border-color: transparent;
background-color: $primary-color;
color: $text-color-anti;
}
}
}
.#{$checkbox-prefix}-main {
cursor: pointer;
display: flex;
align-items: center;
white-space: nowrap;
outline: 0;
.#{$checkbox-prefix}-input {
opacity: 0;
outline: 0;
position: absolute;
z-index: -1;
margin: 0;
width: 0;
height: 0;
box-sizing: border-box;
}
.#{$checkbox-prefix}-inner {
border: 1px solid $border-color;
border-radius: $border-radius;
width: 16px;
height: 16px;
background-color: $bg-color-container;
cursor: pointer;
box-sizing: border-box;
outline: 0;
display: flex;
align-items: center;
justify-content: center;
transition:
background-color 0.1s ease,
border-color 0.1s ease;
}
}
.#{$checkbox-prefix}-content {
margin-left: 8px;
display: inline-block;
flex-direction: column;
.#{$checkbox-prefix}-label {
font-size: 14px;
line-height: 16px;
display: flex;
align-items: center;
color: $text-color-base;
}
.#{$checkbox-prefix}-extra {
font-size: 12px;
color: $text-color-secondary;
margin-top: 4px;
}
}
}
.#{$checkbox-prefix}-pureCard {
color: $text-color-base;
display: inline-flex;
align-items: flex-start;
position: relative;
cursor: pointer;
user-select: none;
padding: 8px 12px;
box-sizing: border-box;
border-radius: $border-radius;
border: 1px solid transparent;
&:hover,
&:hover .#{$checkbox-prefix}-main {
background-color: $bg-color-component;
}
&.#{$checkbox-prefix}-disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.#{$checkbox-prefix}-checked {
border-color: $primary-color;
background-color: $status-info-bg-color;
}
.#{$checkbox-prefix}-content {
display: inline-block;
flex-direction: column;
.#{$checkbox-prefix}-label {
font-size: 14px;
line-height: 16px;
display: flex;
align-items: center;
color: $text-color-base;
}
.#{$checkbox-prefix}-extra {
font-size: 12px;
color: $text-color-secondary;
margin-top: 4px;
}
}
}