mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 13:07:58 +08:00
feat: 新增 FaCheckboxGrou 组件,并重构 FaCheckbox API
This commit is contained in:
@@ -53,6 +53,14 @@ const routes: RouteRecordRaw = {
|
||||
title: '复选框',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'checkbox_group',
|
||||
name: 'componentExampleCheckboxGroup',
|
||||
component: () => import('@/views/component_example/checkbox_group/index.vue'),
|
||||
meta: {
|
||||
title: '复选框组',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'collapsible',
|
||||
name: 'componentExampleCollapsible',
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ declare module 'vue' {
|
||||
FaButtonGroup: typeof import('@fantastic-admin/components')['FaButtonGroup']
|
||||
FaCard: typeof import('@fantastic-admin/components')['FaCard']
|
||||
FaCheckbox: typeof import('@fantastic-admin/components')['FaCheckbox']
|
||||
FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
@@ -76,6 +77,7 @@ declare global {
|
||||
const FaButtonGroup: typeof import('@fantastic-admin/components')['FaButtonGroup']
|
||||
const FaCard: typeof import('@fantastic-admin/components')['FaCard']
|
||||
const FaCheckbox: typeof import('@fantastic-admin/components')['FaCheckbox']
|
||||
const FaCheckboxGroup: typeof import('@fantastic-admin/components')['FaCheckboxGroup']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
|
||||
Executable → Regular
+1
-1
@@ -5,7 +5,7 @@ const checked = ref(false)
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<FaCheckbox v-model="checked">
|
||||
Checkbox
|
||||
同意协议
|
||||
</FaCheckbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Executable → Regular
+1
-1
@@ -4,6 +4,6 @@ const checked = ref<'indeterminate' | boolean>('indeterminate')
|
||||
|
||||
<template>
|
||||
<FaCheckbox v-model="checked">
|
||||
Checkbox
|
||||
部分选中
|
||||
</FaCheckbox>
|
||||
</template>
|
||||
|
||||
Executable → Regular
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref(['music'])
|
||||
|
||||
const options = [
|
||||
{ label: '阅读', value: 'reading' },
|
||||
{ label: '音乐', value: 'music' },
|
||||
{ label: '运动', value: 'sports' },
|
||||
]
|
||||
|
||||
const currentText = computed(() =>
|
||||
options
|
||||
.filter(option => value.value.includes(option.value))
|
||||
.map(option => option.label)
|
||||
.join('、'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-4 grid">
|
||||
<FaCheckboxGroup v-model="value" :options="options" />
|
||||
<FaCheckboxGroup v-model="value" :options="options" class="flex" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前值:{{ currentText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref(['dashboard', 'report'])
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '看板订阅',
|
||||
value: 'dashboard',
|
||||
description: '每天 8:00 推送核心经营指标。',
|
||||
},
|
||||
{
|
||||
label: '周报摘要',
|
||||
value: 'report',
|
||||
description: '每周一汇总关键数据变化。',
|
||||
},
|
||||
{
|
||||
label: '系统告警',
|
||||
value: 'alarm',
|
||||
description: '异常波动时立即通知处理人。',
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
const currentText = computed(() =>
|
||||
options
|
||||
.filter(option => value.value.includes(option.value))
|
||||
.map(option => option.label)
|
||||
.join('、'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-4 grid">
|
||||
<FaCheckboxGroup v-model="value" :options="options" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前值:{{ currentText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref(['focus', 'balanced'])
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '专注模式',
|
||||
value: 'focus',
|
||||
description: '突出主任务,弱化辅助信息,适合录入和审批场景。',
|
||||
},
|
||||
{
|
||||
label: '平衡模式',
|
||||
value: 'balanced',
|
||||
description: '信息密度与可读性保持平衡,适合作为默认配置。',
|
||||
},
|
||||
{
|
||||
label: '高密度模式',
|
||||
value: 'dense',
|
||||
description: '在大屏中同时承载更多信息,适合运营与监控看板。',
|
||||
},
|
||||
]
|
||||
|
||||
const currentText = computed(() =>
|
||||
options
|
||||
.filter(option => value.value.includes(option.value))
|
||||
.map(option => option.label)
|
||||
.join('、'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup
|
||||
v-model="value"
|
||||
:options="options"
|
||||
class="gap-2 md:grid-cols-3"
|
||||
option-class="rounded-xl border border-transparent px-1 py-1"
|
||||
>
|
||||
<template #option="{ option, checked, disabled }">
|
||||
<div
|
||||
class="px-4 py-3 border rounded-xl flex gap-3 w-full transition-colors items-start justify-between" :class="[
|
||||
checked ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/40',
|
||||
disabled && 'opacity-60',
|
||||
]"
|
||||
>
|
||||
<div class="gap-1 grid min-w-0">
|
||||
<div class="text-sm font-medium truncate">
|
||||
{{ option.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="option.description"
|
||||
class="text-xs text-muted-foreground leading-5"
|
||||
>
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-medium shrink-0" :class="[
|
||||
checked ? 'text-primary' : 'text-muted-foreground',
|
||||
]"
|
||||
>
|
||||
{{ checked ? '已选中' : '可选择' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</FaCheckboxGroup>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前模式:{{ currentText }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref(['dashboard'])
|
||||
|
||||
const options = [
|
||||
{ label: '看板订阅', value: 'dashboard' },
|
||||
{ label: '周报摘要', value: 'report' },
|
||||
{ label: '系统告警', value: 'alarm' },
|
||||
]
|
||||
|
||||
const currentText = computed(() =>
|
||||
options
|
||||
.filter(option => value.value.includes(option.value))
|
||||
.map(option => option.label)
|
||||
.join('、'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-4 grid">
|
||||
<FaCheckboxGroup
|
||||
v-model="value"
|
||||
:options="options"
|
||||
:min="1"
|
||||
:max="2"
|
||||
/>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前值:{{ currentText }}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground leading-5">
|
||||
已选数量小于等于 1 时,已选项会被禁用;已选数量大于等于 2 时,未选项会被禁用。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import Demo1 from './_demo1.vue'
|
||||
import Demo2 from './_demo2.vue'
|
||||
import Demo3 from './_demo3.vue'
|
||||
import Demo4 from './_demo4.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FaPageHeader title="复选框组" description="FaCheckboxGroup" />
|
||||
<FaPageMain main-class="p-4">
|
||||
<Demo1 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="带描述和禁用态" main-class="p-4">
|
||||
<Demo2 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="自定义选项内容" main-class="p-4">
|
||||
<Demo3 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="最少 / 最多选择" main-class="p-4">
|
||||
<Demo4 />
|
||||
</FaPageMain>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,6 +11,7 @@ const options = [
|
||||
<template>
|
||||
<div class="gap-4 grid">
|
||||
<FaRadioGroup v-model="value" :options="options" />
|
||||
<FaRadioGroup v-model="value" :options="options" class="flex" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前值:{{ value }}
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,6 @@ const currentLabel = computed(() =>
|
||||
:options="options"
|
||||
class="gap-4 md:grid-cols-3"
|
||||
option-class="rounded-xl border border-transparent px-1 py-1"
|
||||
item-class="mt-1"
|
||||
>
|
||||
<template #option="{ option, checked, disabled }">
|
||||
<div
|
||||
|
||||
Vendored
+1
@@ -5,6 +5,7 @@ declare module 'vue' {
|
||||
FaButtonGroup: typeof import('./index')['FaButtonGroup']
|
||||
FaCard: typeof import('./index')['FaCard']
|
||||
FaCheckbox: typeof import('./index')['FaCheckbox']
|
||||
FaCheckboxGroup: typeof import('./index')['FaCheckboxGroup']
|
||||
FaCollapsible: typeof import('./index')['FaCollapsible']
|
||||
FaContextMenu: typeof import('./index')['FaContextMenu']
|
||||
FaDivider: typeof import('./index')['FaDivider']
|
||||
|
||||
@@ -2,6 +2,7 @@ export { default as FaAvatar } from './src/avatar/index.vue'
|
||||
export { default as FaButtonGroup } from './src/button-group/index.vue'
|
||||
export { default as FaButton } from './src/button/index.vue'
|
||||
export { default as FaCard } from './src/card/index.vue'
|
||||
export { default as FaCheckboxGroup } from './src/checkbox-group/index.vue'
|
||||
export { default as FaCheckbox } from './src/checkbox/index.vue'
|
||||
export { default as FaCollapsible } from './src/collapsible/index.vue'
|
||||
export { default as FaContextMenu } from './src/context-menu/index.vue'
|
||||
|
||||
@@ -10,6 +10,7 @@ const COMPONENT_NAMES = [
|
||||
'FaButtonGroup',
|
||||
'FaCard',
|
||||
'FaCheckbox',
|
||||
'FaCheckboxGroup',
|
||||
'FaCollapsible',
|
||||
'FaContextMenu',
|
||||
'FaDivider',
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# FaCheckboxGroup 复选框组
|
||||
|
||||
基于选项数组的复选框组组件,支持描述文案、自定义布局样式和选项插槽扩展。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 表单中的多选配置项
|
||||
- 多标签偏好选择
|
||||
- 权限点批量选择
|
||||
- 带说明文案的多选列表
|
||||
- 需要卡片式多选交互的场景
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `options` | `CheckboxGroupOption[]` | **必需** | 复选框组选项数据 |
|
||||
| `disabled` | `boolean` | `false` | 是否整体禁用 |
|
||||
| `min` | `number` | - | 最少选中数量,达到后已选项会被禁用 |
|
||||
| `max` | `number` | - | 最多选中数量,达到后未选项会被禁用 |
|
||||
| `dir` | `'ltr' \| 'rtl'` | - | 文本方向 |
|
||||
| `class` | `HTMLAttributes['class']` | - | 组容器类名 |
|
||||
| `optionClass` | `HTMLAttributes['class']` | - | 每个选项外层容器类名 |
|
||||
| `itemClass` | `HTMLAttributes['class']` | - | 复选框本体类名 |
|
||||
| `labelClass` | `HTMLAttributes['class']` | - | 标签内容类名 |
|
||||
|
||||
### CheckboxGroupOption 接口
|
||||
|
||||
```ts
|
||||
interface CheckboxGroupOption {
|
||||
label: string
|
||||
value: AcceptableValue
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
}
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
|------|------|
|
||||
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }` |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
|--------|------|----------|
|
||||
| `change` | 选中值变化时触发 | `value: AcceptableValue[]` |
|
||||
|
||||
通过 `v-model` 实现双向绑定。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref(['music'])
|
||||
|
||||
const options = [
|
||||
{ label: '阅读', value: 'reading' },
|
||||
{ label: '音乐', value: 'music' },
|
||||
{ label: '运动', value: 'sports' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup v-model="value" :options="options" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 带描述文案
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref(['dashboard', 'report'])
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '看板订阅',
|
||||
value: 'dashboard',
|
||||
description: '每天 8:00 推送核心经营指标。',
|
||||
},
|
||||
{
|
||||
label: '周报摘要',
|
||||
value: 'report',
|
||||
description: '每周一汇总关键数据变化。',
|
||||
},
|
||||
{
|
||||
label: '系统告警',
|
||||
value: 'alarm',
|
||||
description: '异常波动时立即通知处理人。',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup v-model="value" :options="options" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 禁用选项
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref(['basic'])
|
||||
|
||||
const options = [
|
||||
{ label: '基础能力', value: 'basic' },
|
||||
{ label: '审批流', value: 'approval' },
|
||||
{ label: '审计日志', value: 'audit', disabled: true },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup v-model="value" :options="options" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 多列排列
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref(['desktop', 'tablet'])
|
||||
|
||||
const options = [
|
||||
{ label: '桌面端', value: 'desktop' },
|
||||
{ label: '平板端', value: 'tablet' },
|
||||
{ label: '移动端', value: 'mobile' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup
|
||||
v-model="value"
|
||||
:options="options"
|
||||
class="gap-4 md:grid-cols-3"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 最少 / 最多选择
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref(['dashboard'])
|
||||
|
||||
const options = [
|
||||
{ label: '看板订阅', value: 'dashboard' },
|
||||
{ label: '周报摘要', value: 'report' },
|
||||
{ label: '系统告警', value: 'alarm' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup
|
||||
v-model="value"
|
||||
:options="options"
|
||||
:min="1"
|
||||
:max="2"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义选项内容
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref(['focus', 'balanced'])
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '专注模式',
|
||||
value: 'focus',
|
||||
description: '突出主任务,弱化辅助信息。',
|
||||
},
|
||||
{
|
||||
label: '平衡模式',
|
||||
value: 'balanced',
|
||||
description: '信息密度与可读性保持平衡。',
|
||||
},
|
||||
{
|
||||
label: '高密度模式',
|
||||
value: 'dense',
|
||||
description: '适合大屏运营与监控看板。',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckboxGroup
|
||||
v-model="value"
|
||||
:options="options"
|
||||
class="gap-4 md:grid-cols-3"
|
||||
>
|
||||
<template #option="{ option, checked }">
|
||||
<div
|
||||
:class="[
|
||||
'flex w-full items-start justify-between gap-3 rounded-xl border px-4 py-3',
|
||||
checked ? 'border-primary bg-primary/5' : 'border-border',
|
||||
]"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium">{{ option.label }}</div>
|
||||
<div v-if="option.description" class="text-xs text-muted-foreground leading-5">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs" :class="checked ? 'text-primary' : 'text-muted-foreground'">
|
||||
{{ checked ? '已选中' : '可选择' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</FaCheckboxGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据驱动**:组选项必须通过 `options` 传入,不再使用多个 `FaCheckbox` 手工拼接。
|
||||
2. **v-model 类型**:组件通过 `v-model` 管理选中值数组。
|
||||
3. **禁用优先级**:组件整体 `disabled` 为 `true` 时,会覆盖单项可用状态。
|
||||
4. **数量限制**:当选中数量小于等于 `min` 时,已选项会被禁用;当选中数量大于等于 `max` 时,未选项会被禁用。
|
||||
5. **布局控制**:通过 `class` 和 `optionClass` 自定义多列或卡片式排列,不再提供 `orientation`。
|
||||
6. **描述文案布局**:存在 `description` 时,选项会自动切换为顶部对齐布局。
|
||||
7. **自定义插槽**:使用 `option` 插槽后,默认复选框本体会隐藏,需要由插槽内容自行承担完整视觉表现。
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcceptableValue } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '../../utils'
|
||||
import { Checkbox } from '../checkbox/checkbox'
|
||||
import { Label } from '../label/label'
|
||||
|
||||
interface CheckboxGroupOption {
|
||||
label: string
|
||||
value: AcceptableValue
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'FaCheckboxGroup',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
options: CheckboxGroupOption[]
|
||||
disabled?: boolean
|
||||
min?: number
|
||||
max?: number
|
||||
dir?: 'ltr' | 'rtl'
|
||||
class?: HTMLAttributes['class']
|
||||
optionClass?: HTMLAttributes['class']
|
||||
itemClass?: HTMLAttributes['class']
|
||||
labelClass?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: AcceptableValue[]]
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
option?: (props: {
|
||||
option: CheckboxGroupOption
|
||||
checked: boolean
|
||||
disabled: boolean
|
||||
id: string
|
||||
}) => any
|
||||
}>()
|
||||
|
||||
const value = defineModel<AcceptableValue[]>({
|
||||
default: () => [],
|
||||
})
|
||||
|
||||
const baseId = useId()
|
||||
const checkedCount = computed(() => value.value.length)
|
||||
|
||||
watch(value, (newValue) => {
|
||||
emit('change', newValue)
|
||||
})
|
||||
|
||||
function getOptionId(option: CheckboxGroupOption, index: number) {
|
||||
return option.id || `${baseId}-${index}`
|
||||
}
|
||||
|
||||
function getOptionKey(option: CheckboxGroupOption, index: number) {
|
||||
if (option.id) {
|
||||
return option.id
|
||||
}
|
||||
|
||||
return typeof option.value === 'string' || typeof option.value === 'number'
|
||||
? option.value
|
||||
: index
|
||||
}
|
||||
|
||||
function isOptionDisabled(option: CheckboxGroupOption) {
|
||||
if (props.disabled || option.disabled) {
|
||||
return true
|
||||
}
|
||||
|
||||
const checked = isOptionChecked(option)
|
||||
|
||||
if (checked && props.min !== undefined && checkedCount.value <= props.min) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!checked && props.max !== undefined && checkedCount.value >= props.max) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function findOptionIndex(option: CheckboxGroupOption) {
|
||||
return value.value.findIndex(item => Object.is(item, option.value))
|
||||
}
|
||||
|
||||
function isOptionChecked(option: CheckboxGroupOption) {
|
||||
return findOptionIndex(option) > -1
|
||||
}
|
||||
|
||||
function updateOptionChecked(option: CheckboxGroupOption, checked: boolean) {
|
||||
const nextValue = [...value.value]
|
||||
const optionIndex = nextValue.findIndex(item => Object.is(item, option.value))
|
||||
|
||||
if (checked) {
|
||||
if (optionIndex === -1) {
|
||||
nextValue.push(option.value)
|
||||
}
|
||||
}
|
||||
else if (optionIndex > -1) {
|
||||
nextValue.splice(optionIndex, 1)
|
||||
}
|
||||
|
||||
value.value = nextValue
|
||||
}
|
||||
|
||||
function handleOptionModelValueChange(option: CheckboxGroupOption, checked: boolean | 'indeterminate') {
|
||||
if (isOptionDisabled(option)) {
|
||||
return
|
||||
}
|
||||
|
||||
updateOptionChecked(option, checked === true)
|
||||
}
|
||||
|
||||
function handleCustomOptionClick(option: CheckboxGroupOption) {
|
||||
if (!slots.option || isOptionDisabled(option)) {
|
||||
return
|
||||
}
|
||||
|
||||
updateOptionChecked(option, !isOptionChecked(option))
|
||||
}
|
||||
|
||||
function handleCustomOptionKeydown(option: CheckboxGroupOption) {
|
||||
if (!slots.option || isOptionDisabled(option)) {
|
||||
return
|
||||
}
|
||||
|
||||
updateOptionChecked(option, !isOptionChecked(option))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="group"
|
||||
:dir="dir"
|
||||
:class="cn('grid gap-3', props.class)"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in props.options"
|
||||
:key="getOptionKey(option, index)"
|
||||
:tabindex="slots.option && !isOptionDisabled(option) ? 0 : undefined"
|
||||
:role="slots.option ? 'checkbox' : undefined"
|
||||
:aria-checked="slots.option ? `${isOptionChecked(option)}` : undefined"
|
||||
:aria-disabled="slots.option ? `${isOptionDisabled(option)}` : undefined"
|
||||
:class="
|
||||
cn(
|
||||
'flex gap-2',
|
||||
option.description ? 'items-start' : 'items-center',
|
||||
slots.option && 'outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] rounded-xl',
|
||||
props.optionClass,
|
||||
)
|
||||
"
|
||||
@click="handleCustomOptionClick(option)"
|
||||
@keydown.enter.prevent="handleCustomOptionKeydown(option)"
|
||||
@keydown.space.prevent="handleCustomOptionKeydown(option)"
|
||||
>
|
||||
<Checkbox
|
||||
v-show="!slots.option"
|
||||
:id="getOptionId(option, index)"
|
||||
:model-value="isOptionChecked(option)"
|
||||
:disabled="isOptionDisabled(option)"
|
||||
:class="props.itemClass"
|
||||
@update:model-value="handleOptionModelValueChange(option, $event)"
|
||||
/>
|
||||
<Label
|
||||
:for="slots.option ? undefined : getOptionId(option, index)"
|
||||
:class="
|
||||
cn(
|
||||
'min-w-0 flex-1 cursor-pointer gap-0',
|
||||
option.description ? 'items-start' : 'items-center',
|
||||
isOptionDisabled(option) && 'cursor-not-allowed opacity-60',
|
||||
props.labelClass,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot
|
||||
:id="getOptionId(option, index)"
|
||||
name="option"
|
||||
:option="option"
|
||||
:checked="isOptionChecked(option)"
|
||||
:disabled="isOptionDisabled(option)"
|
||||
>
|
||||
<div class="gap-1 grid min-w-0">
|
||||
<span class="truncate">{{ option.label }}</span>
|
||||
<span
|
||||
v-if="option.description"
|
||||
class="text-xs text-muted-foreground leading-5 font-normal"
|
||||
>
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</div>
|
||||
</slot>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,20 +1,23 @@
|
||||
# FaCheckbox 复选框
|
||||
|
||||
简洁的复选框组件,支持双向绑定和标签内容插槽。
|
||||
单个复选框组件,支持普通选中、禁用和半选状态。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 表单中的同意协议选项
|
||||
- 多选过滤条件
|
||||
- 设置选项开关
|
||||
- 列表项批量选择
|
||||
- 单个布尔开关配置
|
||||
- 树形结构中的半选状态展示
|
||||
- 单项功能启用/禁用
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `id` | `string` | 自动生成 | 复选框 id |
|
||||
| `disabled` | `boolean` | `false` | 是否禁用 |
|
||||
| `class` | `HTMLAttributes['class']` | - | 自定义 CSS 类 |
|
||||
| `class` | `HTMLAttributes['class']` | - | 外层容器类名 |
|
||||
| `itemClass` | `HTMLAttributes['class']` | - | 复选框本体类名 |
|
||||
| `labelClass` | `HTMLAttributes['class']` | - | 标签内容类名 |
|
||||
|
||||
## Slots
|
||||
|
||||
@@ -24,7 +27,11 @@
|
||||
|
||||
## Events
|
||||
|
||||
通过 `v-model` 双向绑定,无需单独事件
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
|--------|------|----------|
|
||||
| `change` | 选中值变化时触发 | `value: boolean \| 'indeterminate' \| undefined` |
|
||||
|
||||
通过 `v-model` 实现双向绑定。
|
||||
|
||||
## 示例
|
||||
|
||||
@@ -36,43 +43,41 @@ const checked = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckbox v-model="checked">同意协议</FaCheckbox>
|
||||
<FaCheckbox v-model="checked">
|
||||
我已阅读并同意服务条款
|
||||
</FaCheckbox>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 基础复选框
|
||||
### 半选状态
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const checked = ref(false)
|
||||
const checked = ref<boolean | 'indeterminate'>('indeterminate')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckbox v-model="checked">选项</FaCheckbox>
|
||||
<FaCheckbox v-model="checked">
|
||||
部分选中
|
||||
</FaCheckbox>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 禁用状态
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<FaCheckbox disabled>禁用选项</FaCheckbox>
|
||||
<FaCheckbox v-model="checked" disabled>禁用选中</FaCheckbox>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 复选框组
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const hobbies = ref(['reading', 'music'])
|
||||
const checked = ref(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<FaCheckbox v-model="hobbies" value="reading">阅读</FaCheckbox>
|
||||
<FaCheckbox v-model="hobbies" value="music">音乐</FaCheckbox>
|
||||
<FaCheckbox v-model="hobbies" value="sports">运动</FaCheckbox>
|
||||
<FaCheckbox disabled>
|
||||
禁用未选中
|
||||
</FaCheckbox>
|
||||
<FaCheckbox v-model="checked" disabled>
|
||||
禁用已选中
|
||||
</FaCheckbox>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
@@ -80,6 +85,10 @@ const hobbies = ref(['reading', 'music'])
|
||||
### 无标签复选框
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const checked = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaCheckbox v-model="checked" />
|
||||
</template>
|
||||
@@ -87,48 +96,27 @@ const hobbies = ref(['reading', 'music'])
|
||||
|
||||
### 自定义样式
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<FaCheckbox v-model="checked" class="gap-4">
|
||||
<span class="text-primary font-medium">自定义样式</span>
|
||||
</FaCheckbox>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 表单中的使用
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const form = reactive({
|
||||
agreeTerms: false,
|
||||
subscribe: true,
|
||||
})
|
||||
|
||||
function handleSubmit() {
|
||||
if (!form.agreeTerms) {
|
||||
return
|
||||
}
|
||||
console.log('提交表单')
|
||||
}
|
||||
const checked = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<FaCheckbox v-model="form.agreeTerms">
|
||||
我已阅读并同意服务条款
|
||||
</FaCheckbox>
|
||||
<FaCheckbox v-model="form.subscribe">
|
||||
订阅我们的新闻通讯
|
||||
</FaCheckbox>
|
||||
<FaButton type="submit" :disabled="!form.agreeTerms">提交</FaButton>
|
||||
</form>
|
||||
<FaCheckbox
|
||||
v-model="checked"
|
||||
class="gap-3"
|
||||
item-class="size-5 rounded-md"
|
||||
label-class="text-primary font-medium"
|
||||
>
|
||||
自定义样式
|
||||
</FaCheckbox>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **v-model 绑定**:支持单个 boolean 值和数组(多选)两种模式
|
||||
2. **标签点击**:点击标签文字也能触发复选框状态切换
|
||||
3. **空标签隐藏**:当不提供默认插槽内容时,标签元素会自动隐藏
|
||||
4. **禁用状态**:禁用状态下复选框和标签都无法点击
|
||||
5. **无障碍支持**:原生支持键盘操作(空格键切换状态)
|
||||
1. **单个使用**:`FaCheckbox` 只负责单个复选框,不承担组选项逻辑。
|
||||
2. **v-model 类型**:支持 `boolean` 和 `'indeterminate'` 两种状态值。
|
||||
3. **标签点击**:点击标签文字也能触发复选框状态切换。
|
||||
4. **空标签隐藏**:当不提供默认插槽内容时,标签元素会自动隐藏。
|
||||
5. **禁用状态**:禁用状态下复选框和标签都无法点击。
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
+25
-4
@@ -2,6 +2,7 @@
|
||||
import type { CheckboxRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '../../utils'
|
||||
import { Label } from '../label/label'
|
||||
import { Checkbox } from './checkbox'
|
||||
|
||||
defineOptions({
|
||||
@@ -9,20 +10,40 @@ defineOptions({
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
id?: string
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
itemClass?: HTMLAttributes['class']
|
||||
labelClass?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: CheckboxRootProps['modelValue'] | undefined]
|
||||
}>()
|
||||
|
||||
const value = defineModel<CheckboxRootProps['modelValue']>()
|
||||
const generatedId = useId()
|
||||
const checkboxId = computed(() => props.id || generatedId)
|
||||
|
||||
const id = useId()
|
||||
watch(value, (newValue) => {
|
||||
emit('change', newValue)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex-center-start gap-2', props.class)">
|
||||
<Checkbox :id v-model="value" :disabled />
|
||||
<label :for="id" class="text-sm cursor-pointer empty:hidden">
|
||||
<Checkbox :id="checkboxId" v-model="value" :disabled="disabled" :class="props.itemClass" />
|
||||
<Label
|
||||
:for="checkboxId"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm cursor-pointer empty:hidden',
|
||||
props.disabled && 'cursor-not-allowed opacity-60',
|
||||
props.labelClass,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
</Label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user