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 

其中引入使用了以下工具函数:

<script setup lang="ts">
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(defineProps<Props>(), {
  options: () => [],
  disabled: false,
  vertical: false,
  checked: false,
  gap: 8,
  button: false,
  buttonStyle: 'outline',
  buttonSize: 'middle',
  value: undefined
})
const radioChecked = ref<boolean>(false)
const optionsCheckedValue = ref<string | number | boolean>()
const wave = ref<boolean>(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-if="optionsAmount"
    class="m-radio"
    :class="{ 'radio-vertical': !button && vertical }"
    :style="`
      --radio-gap: ${gapValue};
      --radio-primary-color: ${colorPalettes[5]};
    `"
    v-bind="$attrs"
  >
    <template v-if="!button">
      <div
        class="radio-wrap"
        :class="{ 'radio-disabled': checkDisabled(option.disabled) }"
        v-for="(option, index) in options"
        :key="index"
        @click="checkDisabled(option.disabled) ? () => false : onClick(option.value)"
      >
        <span class="radio-handle" :class="{ 'radio-checked': optionsCheckedValue === option.value }">
          <span
            v-if="!checkDisabled(option.disabled)"
            class="radio-wave"
            :class="{ 'wave-active': wave && optionsCheckedValue === option.value }"
            @animationend="onWaveEnd"
          ></span>
        </span>
        <span class="radio-label">
          <slot :option="option" :label="option.label" :index="index">{{ option.label }}</slot>
        </span>
      </div>
    </template>
    <template v-else>
      <div
        tabindex="0"
        class="radio-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"
        :key="index"
        @click="checkDisabled(option.disabled) ? () => false : onClick(option.value)"
      >
        <span class="radio-label">
          <slot :option="option" :label="option.label" :index="index">{{ option.label }}</slot>
        </span>
        <span
          v-if="!checkDisabled(option.disabled)"
          class="radio-wave"
          :class="{ 'wave-active': wave && optionsCheckedValue === option.value }"
          @animationend="onWaveEnd"
        ></span>
      </div>
    </template>
  </div>
  <template v-else>
    <div
      v-if="!button"
      class="radio-wrap"
      :class="{ 'radio-disabled': disabled }"
      :style="`--radio-primary-color: ${colorPalettes[5]};`"
      @click="disabled ? () => false : onChecked()"
      v-bind="$attrs"
    >
      <span class="radio-handle" :class="{ 'radio-checked': radioChecked }">
        <span
          v-if="!disabled"
          class="radio-wave"
          :class="{ 'wave-active': wave && radioChecked }"
          @animationend="onWaveEnd"
        ></span>
      </span>
      <span v-if="slotsExist.default" class="radio-label">
        <slot></slot>
      </span>
    </div>
    <div
      v-else
      tabindex="0"
      class="radio-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]};`"
      @click="disabled ? () => false : onChecked()"
      v-bind="$attrs"
    >
      <span class="radio-label">
        <slot></slot>
      </span>
      <span
        v-if="!disabled"
        class="radio-wave"
        :class="{ 'wave-active': wave && radioChecked }"
        @animationend="onWaveEnd"
      ></span>
    </div>
  </template>
</template>
<style lang="less" scoped>
.m-radio {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--radio-gap);
}
.radio-vertical {
  flex-direction: column;
  flex-wrap: nowrap;
}
.radio-wrap {
  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>

②在要使用的页面引入:

 其中引入使用了以下组件:

<script setup lang="ts">
import Radio from './Radio.vue'
import { ref, watchEffect } from 'vue'
import type { RadioProps, RadioOption } from 'vue-amazing-ui'
const options = ref<RadioOption[]>([
  {
    label: '北京市',
    value: 1
  },
  {
    label: '纽约市',
    value: 2
  },
  {
    label: '布宜诺斯艾利斯',
    value: 3
  },
  {
    label: '伊斯坦布尔',
    value: 4
  },
  {
    label: '拜占庭',
    value: 5
  },
  {
    label: '君士坦丁堡',
    value: 6
  }
])
const optionsDisabled = ref<RadioOption[]>([
  {
    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 = ref<RadioProps['checked']>(false)
const value = ref<RadioProps['value']>(2)
const buttonSize = ref<RadioProps['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 class="mt30 mb10">基本使用</h2>
    <Radio v-model:checked="checked" @change="onChange">Radio</Radio>
    <h2 class="mt30 mb10">选项列表</h2>
    <Radio :options="options" v-model:value="value" @change="onChange" />
    <h2 class="mt30 mb10">按钮样式</h2>
    <Space vertical>
      <Radio v-model:checked="checked" button>Radio Button</Radio>
      <Radio :options="options" v-model:value="value" button />
    </Space>
    <h2 class="mt30 mb10">填底的按钮样式</h2>
    <Space vertical>
      <Radio v-model:checked="checked" button button-style="solid">Radio Button Solid</Radio>
      <Radio :options="options" v-model:value="value" button button-style="solid" />
    </Space>
    <h2 class="mt30 mb10">禁用</h2>
    <Space vertical>
      <Radio v-model:checked="checked" disabled>Radio</Radio>
      <Radio :options="options" v-model:value="value" disabled />
      <Radio :options="options" v-model:value="value" button disabled />
    </Space>
    <h2 class="mt30 mb10">禁用选项</h2>
    <Space vertical>
      <Radio :options="optionsDisabled" v-model:value="value" />
      <Radio :options="optionsDisabled" v-model:value="value" button />
      <Radio :options="optionsDisabled" v-model:value="value" button button-style="solid" />
    </Space>
    <h2 class="mt30 mb10">垂直排列</h2>
    <Radio vertical :options="options" v-model:value="value" />
    <h2 class="mt30 mb10">自定义选项名</h2>
    <Radio :options="options" v-model:value="value">
      <template #default="{ option, label, index }">
        <span v-if="index === 1" style="color: #ff6900">{{ label }}</span>
        <span v-if="index === 3" style="color: #1677ff">{{ option.label }}</span>
      </template>
    </Radio>
    <h2 class="mt30 mb10">自定义间距</h2>
    <Flex vertical>
      <Row :gutter="24">
        <Col :span="12">
          <Flex gap="small" vertical> horizontal gap: <Slider v-model:value="horizontalGap" /> </Flex>
        </Col>
        <Col :span="12">
          <Flex gap="small" vertical> vertical gap: <Slider v-model:value="verticalGap" /> </Flex>
        </Col>
      </Row>
      <Radio :gap="[horizontalGap, verticalGap]" :options="options" v-model:value="value" />
    </Flex>
    <h2 class="mt30 mb10">按钮大小</h2>
    <Space vertical>
      <Radio :options="sizeOptions" v-model:value="buttonSize" />
      <Radio v-model:checked="checked" button :button-size="buttonSize">Radio Button</Radio>
      <Radio :options="options" v-model:value="value" button :button-size="buttonSize" />
      <Radio :options="options" v-model:value="value" button button-style="solid" :button-size="buttonSize" />
    </Space>
  </div>
</template>
要将 Vue 3 中的单选框竖着排列,你可以使用 flexbox 或者 grid 布局来实现。下面是两种方法: 1. 使用 Flexbox 布局: ```html <template> <div class="container"> <label v-for="option in options" :key="option.value" class="option"> <input type="radio" :value="option.value" v-model="selectedOption"> {{ option.label }} </label> </div> </template> <style> .container { display: flex; flex-direction: column; } .option { margin-bottom: 10px; } </style> <script> export default { data() { return { selectedOption: null, options: [ { label: &#39;Option 1&#39;, value: &#39;option1&#39; }, { label: &#39;Option 2&#39;, value: &#39;option2&#39; }, { label: &#39;Option 3&#39;, value: &#39;option3&#39; } ] }; } }; </script> ``` 2. 使用 Grid 布局: ```html <template> <div class="container"> <label v-for="option in options" :key="option.value" class="option"> <input type="radio" :value="option.value" v-model="selectedOption"> {{ option.label }} </label> </div> </template> <style> .container { display: grid; grid-template-columns: 1fr; grid-gap: 10px; } .option { margin-bottom: 10px; } </style> <script> export default { data() { return { selectedOption: null, options: [ { label: &#39;Option 1&#39;, value: &#39;option1&#39; }, { label: &#39;Option 2&#39;, value: &#39;option2&#39; }, { label: &#39;Option 3&#39;, value: &#39;option3&#39; } ] }; } }; </script> ``` 以上两种方法可以实现单选框竖着排列效果,你可以根据自己的具体需求选择其中一种。希望能帮到你!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theMuseCatcher

您的支持是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值