ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue3单选框(Radio)

2026/8/29 13:11:39 拓冰建站 浏览量
Vue3单选框(Radio) 可自定义设置以下属性单选框选项数据options类型 Option[]默认 []是否禁用disabled类型boolean默认 false是否垂直排列vertical类型boolean默认 false仅当 button: false 时生效当前是否选中v-model:checked类型boolean默认 false多个单选框之间的间距gap类型number | number[]单位 px默认 8垂直排列时为垂直间距数组间距用于水平排列折行时[水平间距, 垂直间距]仅当 button: false 时生效是否启用按钮样式button类型boolean默认 false按钮样式风格buttonStyle目前有描边和填色两种风格类型outline | solid默认 outline仅当 button: true 时生效按钮大小buttonSize类型small | middle | large默认 middle仅当 button: true 时生效当前选中的值v-model:value类型string | number | boolean默认 undefined效果如下图在线预览①创建单选框组件Radio.vue其中引入使用了以下工具函数监听插槽存在 useSlotsExist获取依赖注入 useInjectscript setup langts import { computed, ref, watchEffect, nextTick } from vue import { useSlotsExist, useInject } from components/utils export interface Option { label: string // 选项名 value: string | number | boolean // 选项值 disabled?: boolean // 是否禁用选项 } export interface Props { options?: Option[] // 单选框选项数据 disabled?: boolean // 是否禁用 vertical?: boolean // 是否垂直排列仅当 button: false 时生效 checked?: boolean // (v-model) 当前是否选中 gap?: number | number[] // 多个单选框之间的间距垂直排列时为垂直间距单位 px数组间距用于水平排列折行时[水平间距, 垂直间距]仅当 button: false 时生效 button?: boolean // 是否启用按钮样式 buttonStyle?: outline | solid // 按钮样式风格 buttonSize?: small | middle | large // 按钮大小仅当 button: true 时生效 value?: string | number | boolean // (v-model) 当前选中的值 } const props withDefaults(definePropsProps(), { options: () [], disabled: false, vertical: false, checked: false, gap: 8, button: false, buttonStyle: outline, buttonSize: middle, value: undefined }) const radioChecked refboolean(false) const optionsCheckedValue refstring | number | boolean() const wave refboolean(false) const { colorPalettes } useInject(Radio) // 主题色注入 const emits defineEmits([update:checked, update:value, change]) const slotsExist useSlotsExist([default]) // 选项总数 const optionsAmount computed(() { return props.options.length }) const gapValue computed(() { if (!props.button) { if (!props.vertical Array.isArray(props.gap)) { return ${props.gap[1]}px ${props.gap[0]}px } return ${props.gap}px } else { return 0 } }) watchEffect(() { radioChecked.value props.checked }) watchEffect(() { optionsCheckedValue.value props.value }) function checkDisabled(disabled: boolean | undefined): boolean { if (disabled undefined) { return props.disabled } else { return disabled } } function onClick(value: string | number | boolean): void { if (value ! optionsCheckedValue.value) { startWave() optionsCheckedValue.value value emits(update:value, value) emits(change, value) } } function onChecked(): void { if (!radioChecked.value) { startWave() radioChecked.value true emits(update:checked, true) emits(change, true) } } function startWave(): void { if (wave.value) { wave.value false nextTick(() { wave.value true }) } else { wave.value true } } function onWaveEnd(): void { wave.value false } /script template div v-ifoptionsAmount classradio-wrap :class{ radio-vertical: !button vertical } :style --radio-gap: ${gapValue}; --radio-primary-color: ${colorPalettes[5]}; v-bind$attrs template v-if!button div classradio-container :class{ radio-disabled: checkDisabled(option.disabled) } v-for(option, index) in options :keyindex clickcheckDisabled(option.disabled) ? () false : onClick(option.value) span classradio-handle :class{ radio-checked: optionsCheckedValue option.value } span v-if!checkDisabled(option.disabled) classradio-wave :class{ wave-active: wave optionsCheckedValue option.value } animationendonWaveEnd /span /span span classradio-label slot :optionoption :labeloption.label :indexindex{{ option.label }}/slot /span /div /template template v-else div tabindex0 classradio-button-wrap :class{ radio-button-checked: optionsCheckedValue option.value, radio-button-disabled: checkDisabled(option.disabled), radio-button-solid: buttonStyle solid, radio-button-small: buttonSize small, radio-button-large: buttonSize large } v-for(option, index) in options :keyindex clickcheckDisabled(option.disabled) ? () false : onClick(option.value) span classradio-label slot :optionoption :labeloption.label :indexindex{{ option.label }}/slot /span span v-if!checkDisabled(option.disabled) classradio-wave :class{ wave-active: wave optionsCheckedValue option.value } animationendonWaveEnd /span /div /template /div template v-else div v-if!button classradio-container :class{ radio-disabled: disabled } :style--radio-primary-color: ${colorPalettes[5]}; clickdisabled ? () false : onChecked() v-bind$attrs span classradio-handle :class{ radio-checked: radioChecked } span v-if!disabled classradio-wave :class{ wave-active: wave radioChecked } animationendonWaveEnd /span /span span v-ifslotsExist.default classradio-label slot/slot /span /div div v-else tabindex0 classradio-button-wrap radio-button-single :class{ radio-button-checked: radioChecked, radio-button-disabled: disabled, radio-button-solid: buttonStyle solid, radio-button-small: buttonSize small, radio-button-large: buttonSize large } :style--radio-primary-color: ${colorPalettes[5]}; clickdisabled ? () false : onChecked() v-bind$attrs span classradio-label slot/slot /span span v-if!disabled classradio-wave :class{ wave-active: wave radioChecked } animationendonWaveEnd /span /div /template /template style langless scoped .radio-wrap { display: inline-flex; flex-wrap: wrap; gap: var(--radio-gap); } .radio-vertical { flex-direction: column; flex-wrap: nowrap; } .radio-container { display: inline-flex; align-items: baseline; cursor: pointer; color: rgba(0, 0, 0, 0.88); font-size: 14px; line-height: 1.5714285714285714; :not(.radio-disabled):hover { .radio-handle { border-color: var(--radio-primary-color); } } .radio-handle { /* 如果所有项目的flex-shrink属性都为1当空间不足时都将等比例缩小 如果一个项目的flex-shrink属性为0其他项目都为1则空间不足时前者不缩小。 */ flex-shrink: 0; // 默认 1.即空间不足时项目将缩小 align-self: center; position: relative; width: 16px; height: 16px; background: transparent; border: 1px solid #d9d9d9; border-radius: 50%; transition: all 0.3s; ::after { box-sizing: border-box; position: absolute; top: 50%; left: 50%; display: block; width: 16px; height: 16px; margin-top: -8px; margin-left: -8px; background-color: #fff; border-top: 0; border-left: 0; border-radius: 16px; transform: scale(0); opacity: 0; transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); content: ; } } .radio-checked { border-color: var(--radio-primary-color); background-color: var(--radio-primary-color); ::after { transform: scale(0.375); opacity: 1; transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86); } } .radio-label { word-break: break-all; padding: 0 8px; line-height: 1.5714285714285714; } } .radio-disabled { color: rgba(0, 0, 0, 0.25); cursor: not-allowed; .radio-handle { background-color: rgba(0, 0, 0, 0.04); border-color: #d9d9d9; cursor: not-allowed; ::after { transform: scale(0.5); background-color: rgba(0, 0, 0, 0.25); } } } .radio-button-wrap { position: relative; height: 32px; padding-inline: 15px; line-height: 30px; background: #ffffff; border: 1px solid #d9d9d9; border-top-width: 1px; border-left-width: 0; border-right-width: 1px; cursor: pointer; outline: none; transition: all 0.2s, box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); :first-child { border-left: 1px solid #d9d9d9; border-start-start-radius: 6px; border-end-start-radius: 6px; } :not(:first-child):not(.radio-button-single)::before { position: absolute; top: -1px; left: -1px; display: block; width: 1px; height: 100%; padding-block: 1px; box-sizing: content-box; background-color: #d9d9d9; transition: background-color 0.3s; content: ; } :last-child { border-start-end-radius: 6px; border-end-end-radius: 6px; } :not(.radio-button-disabled):hover { color: var(--radio-primary-color); } } .radio-button-single { border-left: 1px solid #d9d9d9; border-radius: 6px; } .radio-button-wrap.radio-button-checked:not(.radio-button-disabled) { z-index: 1; color: var(--radio-primary-color); background-color: #ffffff; border-color: var(--radio-primary-color); ::before { background-color: var(--radio-primary-color); } } .radio-button-disabled { color: rgba(0, 0, 0, 0.25); background-color: rgba(0, 0, 0, 0.04); border-color: #d9d9d9; cursor: not-allowed; } .radio-button-disabled.radio-button-checked { background-color: rgba(0, 0, 0, 0.15); } .radio-button-solid.radio-button-checked:not(.radio-button-disabled) { color: #fff; background-color: var(--radio-primary-color); border-color: var(--radio-primary-color); :hover { color: #fff; } } .radio-button-small { .radio-button-wrap { height: 24px; padding-inline: 7px; line-height: 22px; :first-child { border-start-start-radius: 4px; border-end-start-radius: 4px; } :last-child { border-start-end-radius: 4px; border-end-end-radius: 4px; } } .radio-button-single { border-radius: 4px; } } .radio-button-large { .radio-button-wrap { height: 40px; font-size: 16px; line-height: 38px; :first-child { border-start-start-radius: 8px; border-end-start-radius: 8px; } :last-child { border-start-end-radius: 8px; border-end-end-radius: 8px; } } .radio-button-single { border-radius: 8px; } } .radio-wave { position: absolute; pointer-events: none; top: 0; right: 0; bottom: 0; left: 0; animation-iteration-count: 1; animation-duration: 0.6s; animation-timing-function: cubic-bezier(0, 0, 0.2, 1), cubic-bezier(0, 0, 0.2, 1); border-radius: inherit; } .wave-active { z-index: 1; animation-name: waveSpread, waveOpacity; keyframes waveSpread { from { box-shadow: 0 0 0.5px 0 var(--radio-primary-color); } to { box-shadow: 0 0 0.5px 5px var(--radio-primary-color); } } keyframes waveOpacity { from { opacity: 0.6; } to { opacity: 0; } } } /style②在要使用的页面引入其中引入使用了以下组件Vue3间距SpaceVue3弹性布局FlexVue3栅格GridVue3滑动输入条Sliderscript setup langts import Radio from ./Radio.vue import { ref, watchEffect } from vue import type { RadioProps, RadioOption } from vue-amazing-ui const options refRadioOption[]([ { label: 北京市, value: 1 }, { label: 纽约市, value: 2 }, { label: 布宜诺斯艾利斯, value: 3 }, { label: 伊斯坦布尔, value: 4 }, { label: 拜占庭, value: 5 }, { label: 君士坦丁堡, value: 6 } ]) const optionsDisabled refRadioOption[]([ { label: 北京市, value: 1 }, { label: 纽约市, value: 2, disabled: true }, { label: 布宜诺斯艾利斯, value: 3 }, { label: 伊斯坦布尔, value: 4 }, { label: 拜占庭, value: 5 }, { label: 君士坦丁堡, value: 6 } ]) const sizeOptions [ { label: small, value: small }, { label: middle, value: middle }, { label: large, value: large } ] const checked refRadioProps[checked](false) const value refRadioProps[value](2) const buttonSize refRadioProps[buttonSize](middle) watchEffect(() { console.log(checked, checked.value) }) watchEffect(() { console.log(value, value.value) }) const horizontalGap ref(16) const verticalGap ref(8) function onChange(value: string | number | boolean) { console.log(change, value) } /script template div h1{{ $route.name }} {{ $route.meta.title }}/h1 h2 classmt30 mb10基本使用/h2 Radio v-model:checkedchecked changeonChangeRadio/Radio h2 classmt30 mb10选项列表/h2 Radio :optionsoptions v-model:valuevalue changeonChange / h2 classmt30 mb10按钮样式/h2 Space vertical Radio v-model:checkedchecked buttonRadio Button/Radio Radio :optionsoptions v-model:valuevalue button / /Space h2 classmt30 mb10填底的按钮样式/h2 Space vertical Radio v-model:checkedchecked button button-stylesolidRadio Button Solid/Radio Radio :optionsoptions v-model:valuevalue button button-stylesolid / /Space h2 classmt30 mb10禁用/h2 Space vertical Radio v-model:checkedchecked disabledRadio/Radio Radio :optionsoptions v-model:valuevalue disabled / Radio :optionsoptions v-model:valuevalue button disabled / /Space h2 classmt30 mb10禁用选项/h2 Space vertical Radio :optionsoptionsDisabled v-model:valuevalue / Radio :optionsoptionsDisabled v-model:valuevalue button / Radio :optionsoptionsDisabled v-model:valuevalue button button-stylesolid / /Space h2 classmt30 mb10垂直排列/h2 Radio vertical :optionsoptions v-model:valuevalue / h2 classmt30 mb10自定义选项名/h2 Radio :optionsoptions v-model:valuevalue template #default{ option, label, index } span v-ifindex 1 stylecolor: #ff6900{{ label }}/span span v-ifindex 3 stylecolor: #1677ff{{ option.label }}/span /template /Radio h2 classmt30 mb10自定义间距/h2 Flex vertical Row :gutter24 Col :span12 Flex gapsmall vertical horizontal gap: Slider v-model:valuehorizontalGap / /Flex /Col Col :span12 Flex gapsmall vertical vertical gap: Slider v-model:valueverticalGap / /Flex /Col /Row Radio :gap[horizontalGap, verticalGap] :optionsoptions v-model:valuevalue / /Flex h2 classmt30 mb10按钮大小/h2 Space vertical Radio :optionssizeOptions v-model:valuebuttonSize / Radio v-model:checkedchecked button :button-sizebuttonSizeRadio Button/Radio Radio :optionsoptions v-model:valuevalue button :button-sizebuttonSize / Radio :optionsoptions v-model:valuevalue button button-stylesolid :button-sizebuttonSize / /Space /div /template