This commit is contained in:
oneao committed 2025-09-15 17:31:18 +08:00
1 parent ae0ad2bd63
commit 5a7e96db02
12 files changed
+466 -85

No files matched your search

@@ -6,7 +6,8 @@ export { default as Icon } from './icon'
export { default as Input } from './input'
export { default as Popconfirm } from './popconfirm'
export { default as Popup } from './popup'
export { default as Radio } from './radio'
export { default as Radio } from './radio/radio'
export { default as RadioGroup } from './radio/radio-group'
export { default as Separator } from './separator'
export { default as Space } from './space'
export { default as Spin } from './spin'
@@ -22,7 +22,17 @@
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-family:
'Helvetica', 'Arial', 'PingFangSC-Regular', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', 'Microsoft JhengHei';
Inter,
-apple-system,
BlinkMacSystemFont,
Segoe UI,
PingFang SC,
Hiragino Sans GB,
Microsoft YaHei,
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
}
html,
@@ -1,7 +1,7 @@
import type { InjectionKey, Slots } from 'vue'
export interface RadioGroupContext {
name: 'ArcoRadioGroup'
name: 'AoRadioGroup'
value: string | number | boolean
disabled: boolean
slots: Slots
@@ -1,6 +1,7 @@
@use '../_styles/_variables.scss' as *;
$radio-prefix: '#{$prefix}-radio';
$radio-group-prefix: '#{$prefix}-radio-group';
.#{$radio-prefix} {
color: $text-color-base;
@@ -10,6 +11,11 @@ $radio-prefix: '#{$prefix}-radio';
cursor: pointer;
user-select: none;
&.#{$radio-prefix}-disabled {
cursor: not-allowed;
opacity: 0.5;
}
&:hover .#{$radio-prefix}-main .#{$radio-prefix}-inner {
border-color: $primary-color;
}
@@ -42,8 +48,8 @@ $radio-prefix: '#{$prefix}-radio';
.#{$radio-prefix}-inner {
border: 1px solid $border-color;
border-radius: 100%;
width: 16px;
height: 16px;
width: 14px;
height: 14px;
background-color: $bg-color-container;
position: relative;
display: inline-block;
@@ -52,8 +58,8 @@ $radio-prefix: '#{$prefix}-radio';
outline: 0;
&::after {
box-sizing: border-box;
width: 8px;
height: 8px;
width: 6px;
height: 6px;
border-radius: 999px;
background-color: transparent;
content: '';
@@ -62,8 +68,8 @@ $radio-prefix: '#{$prefix}-radio';
top: 50%;
transform: translate(-50%, -50%) scale(0);
transition:
transform 0.15s ease-in,
background-color 0.15s ease-in;
transform 0.1s ease-in,
background-color 0.1s ease-in;
}
}
}
@@ -72,6 +78,8 @@ $radio-prefix: '#{$prefix}-radio';
font-size: 12px;
margin-left: 8px;
vertical-align: middle;
line-height: 1;
}
}
.#{$radio-group-prefix} {
}
@@ -1,70 +0,0 @@
import { defineComponent, inject } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import { radioGroupKey } from './context'
export default defineComponent({
name: 'Radio',
props: {
checked: {
type: Boolean,
default: false,
},
value: {
type: [String, Number],
required: false,
},
disabled: {
type: Boolean,
default: false,
},
// private
uninjectGroupContext: {
type: Boolean,
default: false,
},
},
emits: ['change', 'update:checked'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('radio')
const radioGroupCtx = !props.uninjectGroupContext
? inject(radioGroupKey, undefined)
: undefined
const handleClick = () => {
if (props.disabled)
return
const newChecked = !props.checked
emit('update:checked', newChecked)
emit('change', {
value: props.value,
checked: newChecked,
})
}
return () => (
<label
class={[
prefixCls,
props.checked ? `${prefixCls}-checked` : '',
props.disabled ? `${prefixCls}-disabled` : '',
]}
onClick={handleClick}
>
<span class={`${prefixCls}-main`}>
<span class={`${prefixCls}-inner`} />
<input
class={`${prefixCls}-input`}
type="radio"
checked={props.checked}
disabled={props.disabled}
onClick={e => e.stopPropagation()}
/>
</span>
<span class={`${prefixCls}-content`}>
{slots.default ? slots.default() : null}
</span>
</label>
)
},
})
@@ -0,0 +1,112 @@
import type { PropType, RenderFunction } from 'vue'
import { computed, defineComponent, provide, reactive, ref, watch } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Space from '../space/index'
import { radioGroupKey } from './context'
import Radio from './radio'
interface RadioOption {
label?: string | RenderFunction
value?: string | number
extra?: string | RenderFunction
disabled?: boolean
[key: string]: any
}
export default defineComponent({
name: 'RadioGroup',
props: {
modelValue: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
default: undefined,
},
defaultValue: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
labelField: {
type: String,
default: 'label',
},
valueField: {
type: String,
default: 'value',
},
options: {
type: Array as PropType<Array<string | number | RadioOption>>,
default: [],
},
spaceProps: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
},
emits: ['update:modelValue', 'change'],
setup(props, { emit, slots }) {
const prefixCls = getPrefixCls('radio-group')
// 内部值,优先使用 v-model
const _value = ref(props.modelValue ?? props.defaultValue)
watch(() => props.modelValue, (newVal) => {
if (newVal !== undefined) {
_value.value = newVal
}
})
const handleChange = (val: string | number | boolean, e: Event) => {
_value.value = val
emit('update:modelValue', val)
emit('change', val, e)
}
const groupDisabled = computed(() => props.disabled)
// 提供 context 给 Radio 使用
provide(radioGroupKey, reactive({
name: 'AoRadioGroup' as const,
value: _value,
disabled: groupDisabled,
slots,
handleChange,
}))
const renderOptions = () => {
return props.options.map((option) => {
// 标准化 option
const opt: RadioOption
= typeof option === 'string' || typeof option === 'number'
? { [props.labelField]: String(option), [props.valueField]: option }
: option
const labelContent = slots.label
? slots.label({ data: opt })
: typeof opt[props.labelField] === 'function'
? opt[props.labelField]()
: opt[props.labelField]
return (
<Radio
key={opt[props.valueField]}
value={opt[props.valueField]}
disabled={opt.disabled ?? props.disabled}
modelValue={_value.value === opt[props.valueField]}
>
{labelContent}
</Radio>
)
})
}
return () => (
<div class={[prefixCls]}>
<Space {...props.spaceProps}>
{props.options.length > 0 ? renderOptions() : slots.default?.()}
</Space>
</div>
)
},
})
@@ -0,0 +1,96 @@
import type { PropType, VNode } from 'vue'
import type { RadioGroupContext } from './context'
import { computed, defineComponent, inject, ref } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import { radioGroupKey } from './context'
export default defineComponent({
name: 'Radio',
props: {
modelValue: {
type: Boolean,
default: undefined,
},
defaultChecked: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
default: true,
},
allowUncheck: {
type: Boolean,
default: true,
},
extra: {
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
default: '',
},
},
emits: ['update:modelValue', 'change'],
setup(props, { slots, emit }) {
const prefixCls = getPrefixCls('radio')
const radioGroupCtx = inject<RadioGroupContext | undefined>(radioGroupKey, undefined)
const inGroup = computed(() => !!radioGroupCtx)
const isDisabled = computed(() => props.disabled || radioGroupCtx?.disabled)
const _checked = ref(props.defaultChecked)
// 判断是否选中
const isChecked = computed(() => {
if (inGroup.value)
return props.value === radioGroupCtx!.value
return _checked.value
})
const handleClick = (e: Event) => {
if (isDisabled.value)
return
if (isChecked.value && !props.allowUncheck)
return
if (inGroup.value) {
const newValue = isChecked.value && props.allowUncheck ? '' : props.value
radioGroupCtx?.handleChange(newValue, e)
}
else {
const newValue = props.allowUncheck ? !isChecked.value : true
_checked.value = newValue
emit('update:modelValue', newValue)
emit('change', props.value, e)
}
}
return () => (
<label
class={[
prefixCls,
isDisabled.value ? `${prefixCls}-disabled` : '',
isChecked.value ? `${prefixCls}-checked` : '',
]}
onClick={handleClick}
>
<span class={`${prefixCls}-main`}>
<span class={`${prefixCls}-inner`} />
<input
class={`${prefixCls}-input`}
type="radio"
disabled={isDisabled.value}
onClick={e => e.stopPropagation()}
/>
</span>
<span class={`${prefixCls}-content`}>
{slots.default?.()}
</span>
</label>
)
},
})