mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-07 11:34:10 +08:00
feat: 新增 FaRadioGroup 组件
This commit is contained in:
@@ -222,6 +222,14 @@ const routes: RouteRecordRaw = {
|
||||
title: '进度条',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'radio_group',
|
||||
name: 'componentExampleRadioGroup',
|
||||
component: () => import('@/views/component_example/radio_group/index.vue'),
|
||||
meta: {
|
||||
title: '单选组',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'scroll_area',
|
||||
name: 'componentExampleScrollArea',
|
||||
|
||||
+2
-76
@@ -19,82 +19,45 @@ declare module 'vue' {
|
||||
AppNotAllowed: typeof import('./../components/AppNotAllowed/index.vue')['default']
|
||||
AppNotSupportedMobile: typeof import('./../components/AppNotSupportedMobile/index.vue')['default']
|
||||
AppSystemInfo: typeof import('./../components/AppSystemInfo/index.vue')['default']
|
||||
FaAnimatedBeam: typeof import('@fantastic-admin/components')['FaAnimatedBeam']
|
||||
FaAnimatedCountTo: typeof import('@fantastic-admin/components')['FaAnimatedCountTo']
|
||||
FaAnimatedCountToGroup: typeof import('@fantastic-admin/components')['FaAnimatedCountToGroup']
|
||||
FaAvatar: typeof import('@fantastic-admin/components')['FaAvatar']
|
||||
FaBlurReveal: typeof import('@fantastic-admin/components')['FaBlurReveal']
|
||||
FaBorderBeam: typeof import('@fantastic-admin/components')['FaBorderBeam']
|
||||
FaButton: typeof import('@fantastic-admin/components')['FaButton']
|
||||
FaButtonGroup: typeof import('@fantastic-admin/components')['FaButtonGroup']
|
||||
FaCard: typeof import('@fantastic-admin/components')['FaCard']
|
||||
FaCarousel: typeof import('@fantastic-admin/components')['FaCarousel']
|
||||
FaCascader: typeof import('@fantastic-admin/components')['FaCascader']
|
||||
FaCheckbox: typeof import('@fantastic-admin/components')['FaCheckbox']
|
||||
FaCode: typeof import('@fantastic-admin/components')['FaCode']
|
||||
FaCodePreview: typeof import('@fantastic-admin/components')['FaCodePreview']
|
||||
FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
FaCountTo: typeof import('@fantastic-admin/components')['FaCountTo']
|
||||
FaDigitalCard: typeof import('@fantastic-admin/components')['FaDigitalCard']
|
||||
FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
FaEmpty: typeof import('@fantastic-admin/components')['FaEmpty']
|
||||
FaFileUpload: typeof import('@fantastic-admin/components')['FaFileUpload']
|
||||
FaFixedBar: typeof import('@fantastic-admin/components')['FaFixedBar']
|
||||
FaFlipCard: typeof import('@fantastic-admin/components')['FaFlipCard']
|
||||
FaFlipWords: typeof import('@fantastic-admin/components')['FaFlipWords']
|
||||
FaGlowyCard: typeof import('@fantastic-admin/components')['FaGlowyCard']
|
||||
FaGlowyCardWrapper: typeof import('@fantastic-admin/components')['FaGlowyCardWrapper']
|
||||
FaGradientButton: typeof import('@fantastic-admin/components')['FaGradientButton']
|
||||
FaHoverCard: typeof import('@fantastic-admin/components')['FaHoverCard']
|
||||
FaIcon: typeof import('@fantastic-admin/components')['FaIcon']
|
||||
FaIconPicker: typeof import('@fantastic-admin/components')['FaIconPicker']
|
||||
FaImagePreview: typeof import('@fantastic-admin/components')['FaImagePreview']
|
||||
FaImageUpload: typeof import('@fantastic-admin/components')['FaImageUpload']
|
||||
FaInput: typeof import('@fantastic-admin/components')['FaInput']
|
||||
FaInputOTP: typeof import('@fantastic-admin/components')['FaInputOTP']
|
||||
FaInteractiveButton: typeof import('@fantastic-admin/components')['FaInteractiveButton']
|
||||
FaKbd: typeof import('@fantastic-admin/components')['FaKbd']
|
||||
FaKbdGroup: typeof import('@fantastic-admin/components')['FaKbdGroup']
|
||||
FaLabel: typeof import('@fantastic-admin/components')['FaLabel']
|
||||
FaLayoutContainer: typeof import('@fantastic-admin/components')['FaLayoutContainer']
|
||||
FaLinkPreview: typeof import('@fantastic-admin/components')['FaLinkPreview']
|
||||
FaLoading: typeof import('@fantastic-admin/components')['FaLoading']
|
||||
FaMarquee: typeof import('@fantastic-admin/components')['FaMarquee']
|
||||
FaModal: typeof import('@fantastic-admin/components')['FaModal']
|
||||
FaMultiStepLoader: typeof import('@fantastic-admin/components')['FaMultiStepLoader']
|
||||
FaNumberField: typeof import('@fantastic-admin/components')['FaNumberField']
|
||||
FaPageHeader: typeof import('@fantastic-admin/components')['FaPageHeader']
|
||||
FaPageMain: typeof import('@fantastic-admin/components')['FaPageMain']
|
||||
FaPagination: typeof import('@fantastic-admin/components')['FaPagination']
|
||||
FaParticlesBg: typeof import('@fantastic-admin/components')['FaParticlesBg']
|
||||
FaPasswordStrength: typeof import('@fantastic-admin/components')['FaPasswordStrength']
|
||||
FaPatternBg: typeof import('@fantastic-admin/components')['FaPatternBg']
|
||||
FaPopover: typeof import('@fantastic-admin/components')['FaPopover']
|
||||
FaProgress: typeof import('@fantastic-admin/components')['FaProgress']
|
||||
FaQrcode: typeof import('@fantastic-admin/components')['FaQrcode']
|
||||
FaScratchOff: typeof import('@fantastic-admin/components')['FaScratchOff']
|
||||
FaRadioGroup: typeof import('@fantastic-admin/components')['FaRadioGroup']
|
||||
FaScrollArea: typeof import('@fantastic-admin/components')['FaScrollArea']
|
||||
FaScrollingText: typeof import('@fantastic-admin/components')['FaScrollingText']
|
||||
FaSearchBar: typeof import('@fantastic-admin/components')['FaSearchBar']
|
||||
FaSelect: typeof import('@fantastic-admin/components')['FaSelect']
|
||||
FaSlider: typeof import('@fantastic-admin/components')['FaSlider']
|
||||
FaSmoothSwipe: typeof import('@fantastic-admin/components')['FaSmoothSwipe']
|
||||
FaSparklesText: typeof import('@fantastic-admin/components')['FaSparklesText']
|
||||
FaSparkline: typeof import('@fantastic-admin/components')['FaSparkline']
|
||||
FaSpotlightCard: typeof import('@fantastic-admin/components')['FaSpotlightCard']
|
||||
FaStorageBox: typeof import('@fantastic-admin/components')['FaStorageBox']
|
||||
FaSwitch: typeof import('@fantastic-admin/components')['FaSwitch']
|
||||
FaTabs: typeof import('@fantastic-admin/components')['FaTabs']
|
||||
FaTextarea: typeof import('@fantastic-admin/components')['FaTextarea']
|
||||
FaTextHighlight: typeof import('@fantastic-admin/components')['FaTextHighlight']
|
||||
FaTimeAgo: typeof import('@fantastic-admin/components')['FaTimeAgo']
|
||||
FaTimeline: typeof import('@fantastic-admin/components')['FaTimeline']
|
||||
FaToast: typeof import('@fantastic-admin/components')['FaToast']
|
||||
FaTooltip: typeof import('@fantastic-admin/components')['FaTooltip']
|
||||
FaTree: typeof import('@fantastic-admin/components')['FaTree']
|
||||
FaTrend: typeof import('@fantastic-admin/components')['FaTrend']
|
||||
}
|
||||
}
|
||||
@@ -108,81 +71,44 @@ declare global {
|
||||
const AppNotAllowed: typeof import('./../components/AppNotAllowed/index.vue')['default']
|
||||
const AppNotSupportedMobile: typeof import('./../components/AppNotSupportedMobile/index.vue')['default']
|
||||
const AppSystemInfo: typeof import('./../components/AppSystemInfo/index.vue')['default']
|
||||
const FaAnimatedBeam: typeof import('@fantastic-admin/components')['FaAnimatedBeam']
|
||||
const FaAnimatedCountTo: typeof import('@fantastic-admin/components')['FaAnimatedCountTo']
|
||||
const FaAnimatedCountToGroup: typeof import('@fantastic-admin/components')['FaAnimatedCountToGroup']
|
||||
const FaAvatar: typeof import('@fantastic-admin/components')['FaAvatar']
|
||||
const FaBlurReveal: typeof import('@fantastic-admin/components')['FaBlurReveal']
|
||||
const FaBorderBeam: typeof import('@fantastic-admin/components')['FaBorderBeam']
|
||||
const FaButton: typeof import('@fantastic-admin/components')['FaButton']
|
||||
const FaButtonGroup: typeof import('@fantastic-admin/components')['FaButtonGroup']
|
||||
const FaCard: typeof import('@fantastic-admin/components')['FaCard']
|
||||
const FaCarousel: typeof import('@fantastic-admin/components')['FaCarousel']
|
||||
const FaCascader: typeof import('@fantastic-admin/components')['FaCascader']
|
||||
const FaCheckbox: typeof import('@fantastic-admin/components')['FaCheckbox']
|
||||
const FaCode: typeof import('@fantastic-admin/components')['FaCode']
|
||||
const FaCodePreview: typeof import('@fantastic-admin/components')['FaCodePreview']
|
||||
const FaCollapsible: typeof import('@fantastic-admin/components')['FaCollapsible']
|
||||
const FaContextMenu: typeof import('@fantastic-admin/components')['FaContextMenu']
|
||||
const FaCountTo: typeof import('@fantastic-admin/components')['FaCountTo']
|
||||
const FaDigitalCard: typeof import('@fantastic-admin/components')['FaDigitalCard']
|
||||
const FaDivider: typeof import('@fantastic-admin/components')['FaDivider']
|
||||
const FaDrawer: typeof import('@fantastic-admin/components')['FaDrawer']
|
||||
const FaDropdown: typeof import('@fantastic-admin/components')['FaDropdown']
|
||||
const FaEmpty: typeof import('@fantastic-admin/components')['FaEmpty']
|
||||
const FaFileUpload: typeof import('@fantastic-admin/components')['FaFileUpload']
|
||||
const FaFixedBar: typeof import('@fantastic-admin/components')['FaFixedBar']
|
||||
const FaFlipCard: typeof import('@fantastic-admin/components')['FaFlipCard']
|
||||
const FaFlipWords: typeof import('@fantastic-admin/components')['FaFlipWords']
|
||||
const FaGlowyCard: typeof import('@fantastic-admin/components')['FaGlowyCard']
|
||||
const FaGlowyCardWrapper: typeof import('@fantastic-admin/components')['FaGlowyCardWrapper']
|
||||
const FaGradientButton: typeof import('@fantastic-admin/components')['FaGradientButton']
|
||||
const FaHoverCard: typeof import('@fantastic-admin/components')['FaHoverCard']
|
||||
const FaIcon: typeof import('@fantastic-admin/components')['FaIcon']
|
||||
const FaIconPicker: typeof import('@fantastic-admin/components')['FaIconPicker']
|
||||
const FaImagePreview: typeof import('@fantastic-admin/components')['FaImagePreview']
|
||||
const FaImageUpload: typeof import('@fantastic-admin/components')['FaImageUpload']
|
||||
const FaInput: typeof import('@fantastic-admin/components')['FaInput']
|
||||
const FaInputOTP: typeof import('@fantastic-admin/components')['FaInputOTP']
|
||||
const FaInteractiveButton: typeof import('@fantastic-admin/components')['FaInteractiveButton']
|
||||
const FaKbd: typeof import('@fantastic-admin/components')['FaKbd']
|
||||
const FaKbdGroup: typeof import('@fantastic-admin/components')['FaKbdGroup']
|
||||
const FaLabel: typeof import('@fantastic-admin/components')['FaLabel']
|
||||
const FaLayoutContainer: typeof import('@fantastic-admin/components')['FaLayoutContainer']
|
||||
const FaLinkPreview: typeof import('@fantastic-admin/components')['FaLinkPreview']
|
||||
const FaLoading: typeof import('@fantastic-admin/components')['FaLoading']
|
||||
const FaMarquee: typeof import('@fantastic-admin/components')['FaMarquee']
|
||||
const FaModal: typeof import('@fantastic-admin/components')['FaModal']
|
||||
const FaMultiStepLoader: typeof import('@fantastic-admin/components')['FaMultiStepLoader']
|
||||
const FaNumberField: typeof import('@fantastic-admin/components')['FaNumberField']
|
||||
const FaPageHeader: typeof import('@fantastic-admin/components')['FaPageHeader']
|
||||
const FaPageMain: typeof import('@fantastic-admin/components')['FaPageMain']
|
||||
const FaPagination: typeof import('@fantastic-admin/components')['FaPagination']
|
||||
const FaParticlesBg: typeof import('@fantastic-admin/components')['FaParticlesBg']
|
||||
const FaPasswordStrength: typeof import('@fantastic-admin/components')['FaPasswordStrength']
|
||||
const FaPatternBg: typeof import('@fantastic-admin/components')['FaPatternBg']
|
||||
const FaPopover: typeof import('@fantastic-admin/components')['FaPopover']
|
||||
const FaProgress: typeof import('@fantastic-admin/components')['FaProgress']
|
||||
const FaQrcode: typeof import('@fantastic-admin/components')['FaQrcode']
|
||||
const FaScratchOff: typeof import('@fantastic-admin/components')['FaScratchOff']
|
||||
const FaRadioGroup: typeof import('@fantastic-admin/components')['FaRadioGroup']
|
||||
const FaScrollArea: typeof import('@fantastic-admin/components')['FaScrollArea']
|
||||
const FaScrollingText: typeof import('@fantastic-admin/components')['FaScrollingText']
|
||||
const FaSearchBar: typeof import('@fantastic-admin/components')['FaSearchBar']
|
||||
const FaSelect: typeof import('@fantastic-admin/components')['FaSelect']
|
||||
const FaSlider: typeof import('@fantastic-admin/components')['FaSlider']
|
||||
const FaSmoothSwipe: typeof import('@fantastic-admin/components')['FaSmoothSwipe']
|
||||
const FaSparklesText: typeof import('@fantastic-admin/components')['FaSparklesText']
|
||||
const FaSparkline: typeof import('@fantastic-admin/components')['FaSparkline']
|
||||
const FaSpotlightCard: typeof import('@fantastic-admin/components')['FaSpotlightCard']
|
||||
const FaStorageBox: typeof import('@fantastic-admin/components')['FaStorageBox']
|
||||
const FaSwitch: typeof import('@fantastic-admin/components')['FaSwitch']
|
||||
const FaTabs: typeof import('@fantastic-admin/components')['FaTabs']
|
||||
const FaTextarea: typeof import('@fantastic-admin/components')['FaTextarea']
|
||||
const FaTextHighlight: typeof import('@fantastic-admin/components')['FaTextHighlight']
|
||||
const FaTimeAgo: typeof import('@fantastic-admin/components')['FaTimeAgo']
|
||||
const FaTimeline: typeof import('@fantastic-admin/components')['FaTimeline']
|
||||
const FaToast: typeof import('@fantastic-admin/components')['FaToast']
|
||||
const FaTooltip: typeof import('@fantastic-admin/components')['FaTooltip']
|
||||
const FaTree: typeof import('@fantastic-admin/components')['FaTree']
|
||||
const FaTrend: typeof import('@fantastic-admin/components')['FaTrend']
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref('comfortable')
|
||||
|
||||
const options = [
|
||||
{ label: '默认', value: 'default' },
|
||||
{ label: '舒适', value: 'comfortable' },
|
||||
{ label: '紧凑', value: 'compact' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-4 grid">
|
||||
<FaRadioGroup v-model="value" :options="options" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前值:{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref('growth')
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '创业版',
|
||||
value: 'starter',
|
||||
description: '适合 1-10 人小团队,保留核心能力。',
|
||||
},
|
||||
{
|
||||
label: '成长版',
|
||||
value: 'growth',
|
||||
description: '适合多角色协作,支持审批与审计流程。',
|
||||
},
|
||||
{
|
||||
label: '企业版',
|
||||
value: 'enterprise',
|
||||
description: '高级安全策略与 SSO 即将开放。',
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
const currentLabel = computed(() =>
|
||||
options.find(option => option.value === value.value)?.label ?? '',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-4 grid">
|
||||
<FaRadioGroup v-model="value" :options="options" />
|
||||
<div class="text-sm text-muted-foreground">
|
||||
已选套餐:{{ currentLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
const value = ref('balanced')
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '专注模式',
|
||||
value: 'focus',
|
||||
description: '突出主任务,弱化辅助信息,适合录入和审批场景。',
|
||||
},
|
||||
{
|
||||
label: '平衡模式',
|
||||
value: 'balanced',
|
||||
description: '信息密度与可读性保持平衡,适合作为默认配置。',
|
||||
},
|
||||
{
|
||||
label: '高密度模式',
|
||||
value: 'dense',
|
||||
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"
|
||||
option-class="rounded-xl border border-transparent px-1 py-1"
|
||||
item-class="mt-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>
|
||||
</FaRadioGroup>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
当前模式:{{ currentLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import Demo1 from './_demo1.vue'
|
||||
import Demo2 from './_demo2.vue'
|
||||
import Demo3 from './_demo3.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<FaPageHeader title="单选组" description="FaRadioGroup" />
|
||||
<FaPageMain main-class="p-4">
|
||||
<Demo1 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="带描述和禁用态" main-class="p-4">
|
||||
<Demo2 />
|
||||
</FaPageMain>
|
||||
<FaPageMain title="自定义选项内容" main-class="p-4">
|
||||
<Demo3 />
|
||||
</FaPageMain>
|
||||
</div>
|
||||
</template>
|
||||
Vendored
+1
@@ -28,6 +28,7 @@ declare module 'vue' {
|
||||
FaPasswordStrength: typeof import('./index')['FaPasswordStrength']
|
||||
FaPopover: typeof import('./index')['FaPopover']
|
||||
FaProgress: typeof import('./index')['FaProgress']
|
||||
FaRadioGroup: typeof import('./index')['FaRadioGroup']
|
||||
FaScrollArea: typeof import('./index')['FaScrollArea']
|
||||
FaSearchBar: typeof import('./index')['FaSearchBar']
|
||||
FaSelect: typeof import('./index')['FaSelect']
|
||||
|
||||
@@ -31,6 +31,7 @@ export { default as FaPagination } from './src/pagination/index.vue'
|
||||
export { default as FaPasswordStrength } from './src/password-strength/index.vue'
|
||||
export { default as FaPopover } from './src/popover/index.vue'
|
||||
export { default as FaProgress } from './src/progress/index.vue'
|
||||
export { default as FaRadioGroup } from './src/radio-group/index.vue'
|
||||
export { default as FaScrollArea } from './src/scroll-area/index.vue'
|
||||
export { default as FaSearchBar } from './src/search-bar/index.vue'
|
||||
export { default as FaSelect } from './src/select/index.vue'
|
||||
|
||||
@@ -34,6 +34,7 @@ const COMPONENT_NAMES = [
|
||||
'FaPasswordStrength',
|
||||
'FaPopover',
|
||||
'FaProgress',
|
||||
'FaRadioGroup',
|
||||
'FaScrollArea',
|
||||
'FaSearchBar',
|
||||
'FaSelect',
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
# FaRadioGroup 单选组组件
|
||||
|
||||
基于选项数组的单选组件,支持描述文案、自定义布局样式和选项插槽扩展。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 表单中的单选配置项
|
||||
- 布局模式切换
|
||||
- 套餐、规格、主题方案选择
|
||||
- 带说明文案的单选列表
|
||||
- 需要自定义选项卡片样式的单选交互
|
||||
|
||||
## Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `options` | `RadioGroupOption[]` | **必需** | 单选组选项数据 |
|
||||
| `disabled` | `boolean` | `false` | 是否整体禁用 |
|
||||
| `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | 排列方向 |
|
||||
| `dir` | `'ltr' \| 'rtl'` | - | 文本方向 |
|
||||
| `class` | `HTMLAttributes['class']` | - | 单选组容器类名 |
|
||||
| `optionClass` | `HTMLAttributes['class']` | - | 每个选项外层容器类名 |
|
||||
| `itemClass` | `HTMLAttributes['class']` | - | `RadioGroupItem` 类名 |
|
||||
|
||||
### RadioGroupOption 接口
|
||||
|
||||
```ts
|
||||
interface RadioGroupOption {
|
||||
label: string
|
||||
value: AcceptableValue
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
}
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
|------|------|
|
||||
| `option` | 自定义选项内容,slot props: `{ option, checked, disabled, id }` |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
|--------|------|----------|
|
||||
| `change` | 选中值变化时触发 | `value: AcceptableValue \| undefined` |
|
||||
|
||||
通过 `v-model` 实现双向绑定。
|
||||
|
||||
## 示例
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref('comfortable')
|
||||
|
||||
const options = [
|
||||
{ label: '默认', value: 'default' },
|
||||
{ label: '舒适', value: 'comfortable' },
|
||||
{ label: '紧凑', value: 'compact' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaRadioGroup v-model="value" :options="options" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 带描述文案
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref('growth')
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '创业版',
|
||||
value: 'starter',
|
||||
description: '适合 1-10 人小团队,保留核心能力。',
|
||||
},
|
||||
{
|
||||
label: '成长版',
|
||||
value: 'growth',
|
||||
description: '适合多角色协作,支持审批与审计流程。',
|
||||
},
|
||||
{
|
||||
label: '企业版',
|
||||
value: 'enterprise',
|
||||
description: '高级安全策略与 SSO 即将开放。',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaRadioGroup v-model="value" :options="options" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 禁用状态
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref('growth')
|
||||
|
||||
const options = [
|
||||
{ label: '标准版', value: 'standard' },
|
||||
{ label: '专业版', value: 'pro' },
|
||||
{ label: '企业版', value: 'enterprise', disabled: true },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaRadioGroup v-model="value" :options="options" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 横向排列
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref('balanced')
|
||||
|
||||
const options = [
|
||||
{ label: '专注模式', value: 'focus' },
|
||||
{ label: '平衡模式', value: 'balanced' },
|
||||
{ label: '高密度模式', value: 'dense' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaRadioGroup
|
||||
v-model="value"
|
||||
orientation="horizontal"
|
||||
:options="options"
|
||||
class="gap-4"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义选项内容
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref('balanced')
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '专注模式',
|
||||
value: 'focus',
|
||||
description: '突出主任务,弱化辅助信息。',
|
||||
},
|
||||
{
|
||||
label: '平衡模式',
|
||||
value: 'balanced',
|
||||
description: '信息密度与可读性保持平衡。',
|
||||
},
|
||||
{
|
||||
label: '高密度模式',
|
||||
value: 'dense',
|
||||
description: '适合大屏运营与监控看板。',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaRadioGroup
|
||||
v-model="value"
|
||||
orientation="horizontal"
|
||||
:options="options"
|
||||
class="gap-4"
|
||||
option-class="w-full sm:w-[260px]"
|
||||
>
|
||||
<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>
|
||||
</FaRadioGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 监听变化事件
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const value = ref('default')
|
||||
|
||||
const options = [
|
||||
{ label: '默认', value: 'default' },
|
||||
{ label: '舒适', value: 'comfortable' },
|
||||
{ label: '紧凑', value: 'compact' },
|
||||
]
|
||||
|
||||
function handleChange(value) {
|
||||
console.log('选中值变化:', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FaRadioGroup v-model="value" :options="options" @change="handleChange" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **初始化值**:组件当前通过 `v-model` 管理选中值,不再提供 `defaultValue`。
|
||||
2. **选项结构**:`options` 至少需要 `label` 和 `value` 两个字段。
|
||||
3. **禁用优先级**:组件整体 `disabled` 为 `true` 时,会覆盖单项可用状态。
|
||||
4. **描述文案布局**:存在 `description` 时,选项会自动切换为顶部对齐布局。
|
||||
5. **自定义插槽**:使用 `option` 插槽后,默认 `RadioGroupItem` 会隐藏,需要由插槽内容自行承担完整视觉表现。
|
||||
6. **点击区域**:默认选项内容包裹在 `Label` 中,点击文字区域也能切换选中状态。
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
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 {
|
||||
label: string
|
||||
value: AcceptableValue
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'FaRadioGroup',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
options: RadioGroupOption[]
|
||||
disabled?: RadioGroupRootProps['disabled']
|
||||
orientation?: RadioGroupRootProps['orientation']
|
||||
dir?: RadioGroupRootProps['dir']
|
||||
class?: HTMLAttributes['class']
|
||||
optionClass?: HTMLAttributes['class']
|
||||
itemClass?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
orientation: 'vertical',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: AcceptableValue | undefined]
|
||||
}>()
|
||||
|
||||
const slots = defineSlots<{
|
||||
option?: (props: {
|
||||
option: RadioGroupOption
|
||||
checked: boolean
|
||||
disabled: boolean
|
||||
id: string
|
||||
}) => any
|
||||
}>()
|
||||
|
||||
const value = defineModel<AcceptableValue | undefined>()
|
||||
const baseId = useId()
|
||||
|
||||
watch(value, (newValue) => {
|
||||
emit('change', newValue)
|
||||
})
|
||||
|
||||
function getOptionId(option: RadioGroupOption, index: number) {
|
||||
return option.id || `${baseId}-${index}`
|
||||
}
|
||||
|
||||
function getOptionKey(option: RadioGroupOption, index: number) {
|
||||
if (option.id) {
|
||||
return option.id
|
||||
}
|
||||
|
||||
return typeof option.value === 'string' || typeof option.value === 'number'
|
||||
? option.value
|
||||
: index
|
||||
}
|
||||
|
||||
function isOptionDisabled(option: RadioGroupOption) {
|
||||
return Boolean(props.disabled || option.disabled)
|
||||
}
|
||||
|
||||
function isOptionChecked(option: RadioGroupOption) {
|
||||
return Object.is(value.value, option.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RadioGroup
|
||||
v-model="value"
|
||||
:disabled="disabled"
|
||||
:orientation="orientation"
|
||||
:dir="dir"
|
||||
:class="props.class"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in props.options"
|
||||
:key="getOptionKey(option, index)"
|
||||
:class="
|
||||
cn(
|
||||
'flex gap-3',
|
||||
option.description ? 'items-start' : 'items-center',
|
||||
props.optionClass,
|
||||
)
|
||||
"
|
||||
>
|
||||
<RadioGroupItem
|
||||
v-show="!slots.option"
|
||||
:id="getOptionId(option, index)"
|
||||
:value="option.value"
|
||||
:disabled="isOptionDisabled(option)"
|
||||
:class="props.itemClass"
|
||||
/>
|
||||
<Label
|
||||
:for="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',
|
||||
)
|
||||
"
|
||||
>
|
||||
<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>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { RadioGroupRootEmits, RadioGroupRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { RadioGroupRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<RadioGroupRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<RadioGroupRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RadioGroupRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="radio-group"
|
||||
:class="cn('grid gap-3', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</RadioGroupRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { RadioGroupItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CircleIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
RadioGroupIndicator,
|
||||
RadioGroupItem,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/utils'
|
||||
|
||||
const props = defineProps<RadioGroupItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RadioGroupItem
|
||||
data-slot="radio-group-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<RadioGroupIndicator
|
||||
data-slot="radio-group-indicator"
|
||||
class="flex items-center justify-center relative"
|
||||
>
|
||||
<slot>
|
||||
<CircleIcon class="size-2 left-1/2 top-1/2 absolute fill-primary -translate-x-1/2 -translate-y-1/2" />
|
||||
</slot>
|
||||
</RadioGroupIndicator>
|
||||
</RadioGroupItem>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as RadioGroup } from './RadioGroup.vue'
|
||||
export { default as RadioGroupItem } from './RadioGroupItem.vue'
|
||||
Reference in New Issue
Block a user