refactor: 更新 CheckboxGroupOption 和 RadioGroupOption 接口,支持泛型扩展

This commit is contained in:
Hooray
2026-05-12 21:49:54 +08:00
parent f7789a4791
commit 2302186258
6 changed files with 92 additions and 88 deletions
@@ -5,64 +5,58 @@ const options = [
{
label: '专注模式',
value: 'focus',
summary: '任务优先',
description: '突出主任务,弱化辅助信息,适合录入和审批场景。',
},
{
label: '平衡模式',
value: 'balanced',
summary: '默认体验',
description: '信息密度与可读性保持平衡,适合作为默认配置。',
},
{
label: '高密度模式',
value: 'dense',
summary: '信息看板',
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',
<div class="gap-4 grid">
<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',
]"
>
{{ checked ? '已选中' : '可选择' }}
</span>
</div>
</template>
</FaCheckboxGroup>
<div class="text-sm text-muted-foreground">
当前模式:{{ currentText }}
<div class="gap-1 grid min-w-0">
<div class="flex gap-2 items-center">
<span class="text-sm font-medium truncate">{{ option.label }}</span>
<span class="text-xs text-muted-foreground px-1.5 py-0.5 rounded bg-muted">
{{ option.summary }}
</span>
</div>
<div 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">
当前值:{{ value.join('、') }}
</div>
</div>
</template>
@@ -5,32 +5,30 @@ const options = [
{
label: '专注模式',
value: 'focus',
badge: '录入',
description: '突出主任务,弱化辅助信息,适合录入和审批场景。',
},
{
label: '平衡模式',
value: 'balanced',
badge: '推荐',
description: '信息密度与可读性保持平衡,适合作为默认配置。',
},
{
label: '高密度模式',
value: 'dense',
badge: '大屏',
description: '在大屏中同时承载更多信息,适合运营与监控看板。',
},
]
const currentLabel = computed(() =>
options.find(option => option.value === value.value)?.label ?? '',
)
</script>
<template>
<div class="gap-4 grid">
<FaRadioGroup
v-model="value"
orientation="horizontal"
:options="options"
class="gap-4 md:grid-cols-3"
class="gap-2 md:grid-cols-3"
option-class="rounded-xl border border-transparent px-1 py-1"
>
<template #option="{ option, checked, disabled }">
@@ -41,28 +39,24 @@ const currentLabel = computed(() =>
]"
>
<div class="gap-1 grid min-w-0">
<div class="text-sm font-medium truncate">
{{ option.label }}
<div class="flex gap-2 items-center">
<span class="text-sm font-medium truncate">{{ option.label }}</span>
<span class="text-xs text-muted-foreground px-1.5 py-0.5 rounded bg-muted">
{{ option.badge }}
</span>
</div>
<div
v-if="option.description"
class="text-xs text-muted-foreground leading-5"
>
<div 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',
]"
>
<span class="text-xs font-medium shrink-0" :class="checked ? 'text-primary' : 'text-muted-foreground'">
{{ checked ? '已选中' : '可选择' }}
</span>
</div>
</template>
</FaRadioGroup>
<div class="text-sm text-muted-foreground">
当前模式:{{ currentLabel }}
当前值:{{ value }}
</div>
</div>
</template>
@@ -27,7 +27,7 @@
### CheckboxGroupOption 接口
```ts
interface CheckboxGroupOption {
export interface CheckboxGroupOption {
label: string
value: AcceptableValue
description?: string
@@ -40,7 +40,7 @@ interface CheckboxGroupOption {
| 名称 | 说明 |
|------|------|
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }` |
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }`,`option` 会保留 `options` 中的扩展字段类型 |
## Events
@@ -173,16 +173,19 @@ const options = [
{
label: '专注模式',
value: 'focus',
summary: '任务优先',
description: '突出主任务,弱化辅助信息。',
},
{
label: '平衡模式',
value: 'balanced',
summary: '默认体验',
description: '信息密度与可读性保持平衡。',
},
{
label: '高密度模式',
value: 'dense',
summary: '信息看板',
description: '适合大屏运营与监控看板。',
},
]
@@ -202,8 +205,13 @@ const options = [
]"
>
<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">
<div class="flex items-center gap-2">
<span class="truncate font-medium">{{ option.label }}</span>
<span class="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
{{ option.summary }}
</span>
</div>
<div class="text-xs text-muted-foreground leading-5">
{{ option.description }}
</div>
</div>
+13 -13
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
<script setup lang="ts" generic="TOption extends CheckboxGroupOption">
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 {
export interface CheckboxGroupOption {
label: string
value: AcceptableValue
description?: string
@@ -18,7 +18,7 @@ defineOptions({
})
const props = defineProps<{
options: CheckboxGroupOption[]
options: TOption[]
disabled?: boolean
min?: number
max?: number
@@ -35,7 +35,7 @@ const emit = defineEmits<{
const slots = defineSlots<{
option?: (props: {
option: CheckboxGroupOption
option: TOption
checked: boolean
disabled: boolean
id: string
@@ -53,11 +53,11 @@ watch(value, (newValue) => {
emit('change', newValue)
})
function getOptionId(option: CheckboxGroupOption, index: number) {
function getOptionId(option: TOption, index: number) {
return option.id || `${baseId}-${index}`
}
function getOptionKey(option: CheckboxGroupOption, index: number) {
function getOptionKey(option: TOption, index: number) {
if (option.id) {
return option.id
}
@@ -67,7 +67,7 @@ function getOptionKey(option: CheckboxGroupOption, index: number) {
: index
}
function isOptionDisabled(option: CheckboxGroupOption) {
function isOptionDisabled(option: TOption) {
if (props.disabled || option.disabled) {
return true
}
@@ -85,15 +85,15 @@ function isOptionDisabled(option: CheckboxGroupOption) {
return false
}
function findOptionIndex(option: CheckboxGroupOption) {
function findOptionIndex(option: TOption) {
return value.value.findIndex(item => Object.is(item, option.value))
}
function isOptionChecked(option: CheckboxGroupOption) {
function isOptionChecked(option: TOption) {
return findOptionIndex(option) > -1
}
function updateOptionChecked(option: CheckboxGroupOption, checked: boolean) {
function updateOptionChecked(option: TOption, checked: boolean) {
const nextValue = [...value.value]
const optionIndex = nextValue.findIndex(item => Object.is(item, option.value))
@@ -109,7 +109,7 @@ function updateOptionChecked(option: CheckboxGroupOption, checked: boolean) {
value.value = nextValue
}
function handleOptionModelValueChange(option: CheckboxGroupOption, checked: boolean | 'indeterminate') {
function handleOptionModelValueChange(option: TOption, checked: boolean | 'indeterminate') {
if (isOptionDisabled(option)) {
return
}
@@ -117,7 +117,7 @@ function handleOptionModelValueChange(option: CheckboxGroupOption, checked: bool
updateOptionChecked(option, checked === true)
}
function handleCustomOptionClick(option: CheckboxGroupOption) {
function handleCustomOptionClick(option: TOption) {
if (!slots.option || isOptionDisabled(option)) {
return
}
@@ -125,7 +125,7 @@ function handleCustomOptionClick(option: CheckboxGroupOption) {
updateOptionChecked(option, !isOptionChecked(option))
}
function handleCustomOptionKeydown(option: CheckboxGroupOption) {
function handleCustomOptionKeydown(option: TOption) {
if (!slots.option || isOptionDisabled(option)) {
return
}
+12 -4
View File
@@ -24,7 +24,7 @@
### RadioGroupOption 接口
```ts
interface RadioGroupOption {
export interface RadioGroupOption {
label: string
value: AcceptableValue
description?: string
@@ -37,7 +37,7 @@ interface RadioGroupOption {
| 名称 | 说明 |
|------|------|
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }` |
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }`,`option` 会保留 `options` 中的扩展字段类型 |
## Events
@@ -147,16 +147,19 @@ const options = [
{
label: '专注模式',
value: 'focus',
badge: '录入',
description: '突出主任务,弱化辅助信息。',
},
{
label: '平衡模式',
value: 'balanced',
badge: '推荐',
description: '信息密度与可读性保持平衡。',
},
{
label: '高密度模式',
value: 'dense',
badge: '大屏',
description: '适合大屏运营与监控看板。',
},
]
@@ -176,8 +179,13 @@ const options = [
]"
>
<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">
<div class="flex items-center gap-2">
<span class="truncate font-medium">{{ option.label }}</span>
<span class="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
{{ option.badge }}
</span>
</div>
<div class="text-xs text-muted-foreground leading-5">
{{ option.description }}
</div>
</div>
@@ -1,11 +1,11 @@
<script setup lang="ts">
<script setup lang="ts" generic="TOption extends RadioGroupOption">
import type { AcceptableValue, RadioGroupRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '../../utils'
import { Label } from '../label/label'
import { RadioGroup, RadioGroupItem } from './radio-group'
interface RadioGroupOption {
export interface RadioGroupOption {
label: string
value: AcceptableValue
description?: string
@@ -18,7 +18,7 @@ defineOptions({
})
const props = defineProps<{
options: RadioGroupOption[]
options: TOption[]
disabled?: RadioGroupRootProps['disabled']
dir?: RadioGroupRootProps['dir']
class?: HTMLAttributes['class']
@@ -32,7 +32,7 @@ const emit = defineEmits<{
const slots = defineSlots<{
option?: (props: {
option: RadioGroupOption
option: TOption
checked: boolean
disabled: boolean
id: string
@@ -46,11 +46,11 @@ watch(value, (newValue) => {
emit('change', newValue)
})
function getOptionId(option: RadioGroupOption, index: number) {
function getOptionId(option: TOption, index: number) {
return option.id || `${baseId}-${index}`
}
function getOptionKey(option: RadioGroupOption, index: number) {
function getOptionKey(option: TOption, index: number) {
if (option.id) {
return option.id
}
@@ -60,11 +60,11 @@ function getOptionKey(option: RadioGroupOption, index: number) {
: index
}
function isOptionDisabled(option: RadioGroupOption) {
function isOptionDisabled(option: TOption) {
return Boolean(props.disabled || option.disabled)
}
function isOptionChecked(option: RadioGroupOption) {
function isOptionChecked(option: TOption) {
return Object.is(value.value, option.value)
}
</script>