diff --git a/code/base-project/base-vue/packages/ao-design/src/index.scss b/code/base-project/base-vue/packages/ao-design/src/index.scss index e5218888..ffac5be0 100644 --- a/code/base-project/base-vue/packages/ao-design/src/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/index.scss @@ -13,7 +13,6 @@ @use './drawer/index.scss' as *; @use './tooltip/index.scss' as *; @use './tag/index.scss' as *; -@use './tag-group/index.scss' as *; @use './space/index.scss' as *; @use './separator/index.scss' as *; @use './radio/index.scss' as *; diff --git a/code/base-project/base-vue/packages/ao-design/src/radio/context.ts b/code/base-project/base-vue/packages/ao-design/src/radio/context.ts index 28236b3c..0e4928c9 100644 --- a/code/base-project/base-vue/packages/ao-design/src/radio/context.ts +++ b/code/base-project/base-vue/packages/ao-design/src/radio/context.ts @@ -2,11 +2,12 @@ import type { InjectionKey, Slots } from 'vue' export interface RadioGroupContext { name: 'AoRadioGroup' - value: string | number | boolean + value: string | number | boolean | undefined disabled: boolean slots: Slots + allowUncheck: boolean type?: 'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline' - handleChange: (value: string | number | boolean, e: Event) => void + handleChange: (value: string | number | boolean | undefined, e: Event) => void } export const radioGroupKey: InjectionKey diff --git a/code/base-project/base-vue/packages/ao-design/src/radio/demo.vue b/code/base-project/base-vue/packages/ao-design/src/radio/demo.vue index 79314b03..dd2dc422 100644 --- a/code/base-project/base-vue/packages/ao-design/src/radio/demo.vue +++ b/code/base-project/base-vue/packages/ao-design/src/radio/demo.vue @@ -54,6 +54,7 @@ function toggleThemeDark() { extra-field="note" type="filled" size="small" + :allow-uncheck="false" @change="handleChange" /> diff --git a/code/base-project/base-vue/packages/ao-design/src/radio/radio-group.tsx b/code/base-project/base-vue/packages/ao-design/src/radio/radio-group.tsx index 3985f90e..89a7de21 100644 --- a/code/base-project/base-vue/packages/ao-design/src/radio/radio-group.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/radio/radio-group.tsx @@ -22,7 +22,7 @@ export default defineComponent({ }, defaultValue: { type: [String, Number, Boolean] as PropType, - default: '', + default: undefined, }, disabled: { type: Boolean, @@ -56,21 +56,23 @@ export default defineComponent({ type: String as PropType<'small' | 'medium' | 'large'>, default: 'medium', }, + allowUncheck: { + type: Boolean, + default: true, + }, }, emits: ['update:modelValue', 'change'], setup(props, { emit, slots }) { const prefixCls = getPrefixCls('radio-group') // 内部值,优先使用 v-model - const _value = ref(props.modelValue ?? props.defaultValue) + const _value = ref(props.modelValue ?? props.defaultValue ?? undefined) watch(() => props.modelValue, (newVal) => { - if (newVal !== undefined) { - _value.value = newVal - } + _value.value = newVal ?? undefined }) - const handleChange = (val: string | number | boolean, e: Event) => { + const handleChange = (val: string | number | boolean | undefined, e: Event) => { _value.value = val emit('update:modelValue', val) emit('change', val, e) @@ -84,6 +86,7 @@ export default defineComponent({ disabled: groupDisabled, type: groupType, slots, + allowUncheck: props.allowUncheck, handleChange, })) @@ -126,6 +129,7 @@ export default defineComponent({ modelValue={_value.value === opt[props.valueField]} extra={extraContent} type={props.type} + allowUncheck={props.allowUncheck} > {labelContent} diff --git a/code/base-project/base-vue/packages/ao-design/src/radio/radio.tsx b/code/base-project/base-vue/packages/ao-design/src/radio/radio.tsx index 08e3d734..5fa5addb 100644 --- a/code/base-project/base-vue/packages/ao-design/src/radio/radio.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/radio/radio.tsx @@ -52,18 +52,20 @@ export default defineComponent({ return props.modelValue ?? _checked.value }) + const isAllowUncheck = computed(() => props.allowUncheck || radioGroupCtx?.allowUncheck) + const handleClick = (e: Event) => { if (isDisabled.value) return - if (isChecked.value && !props.allowUncheck) + if (isChecked.value && !isAllowUncheck.value) return if (inGroup.value) { - const newValue = isChecked.value && props.allowUncheck ? '' : props.value + const newValue = isChecked.value && isAllowUncheck.value ? undefined : props.value radioGroupCtx?.handleChange(newValue, e) } else { - const newValue = props.allowUncheck ? !isChecked.value : true + const newValue = isAllowUncheck.value ? !isChecked.value : true _checked.value = newValue emit('update:modelValue', newValue) emit('change', props.value, e) diff --git a/code/base-project/base-vue/packages/ao-design/src/tag-group/demo.vue b/code/base-project/base-vue/packages/ao-design/src/tag-group/demo.vue deleted file mode 100644 index 260a0dc6..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/tag-group/demo.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - diff --git a/code/base-project/base-vue/packages/ao-design/src/tag-group/index.scss b/code/base-project/base-vue/packages/ao-design/src/tag-group/index.scss deleted file mode 100644 index 41da7890..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/tag-group/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -@use '../_styles/_variables.scss' as *; -@use 'sass:color'; - -$tag-prefix: '#{$prefix}-tag'; - -$tag-group-prefix: '#{$prefix}-tag-group'; - -.#{$tag-group-prefix} { - > .#{$tag-prefix}:not(:last-child) { - margin-right: 8px; - } -} diff --git a/code/base-project/base-vue/packages/ao-design/src/tag-group/index.tsx b/code/base-project/base-vue/packages/ao-design/src/tag-group/index.tsx deleted file mode 100644 index 6fb1c5a7..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/tag-group/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { PropType, VNode } from 'vue' -import { cloneVNode, defineComponent } from 'vue' -import { useSelectable } from '../_hooks/useSelectable' -import { getPrefixCls } from '../_utils/getPrefixCls' -import Tag from '../tag/tag' - -interface TagOption { - label?: string - value: string | number - disabled?: boolean - [key: string]: any -} - -export default defineComponent({ - name: 'ATagGroup', - props: { - modelValue: { type: Array as PropType<(string | number)[]>, default: () => [] }, - multiple: { type: Boolean, default: true }, - options: { type: Array as PropType, default: () => [] }, - labelField: { type: String, default: 'label' }, - valueField: { type: String, default: 'value' }, - checkedProps: { type: Object as PropType>, default: () => ({ type: 'solid', status: 'primary' }) }, - uncheckedProps: { type: Object as PropType>, default: () => ({ type: 'solid', status: 'default' }) }, - class: { type: [String, Array, Object], default: '' }, - style: { type: Object, default: () => ({}) }, - }, - emits: ['update:modelValue', 'change'], - setup(props, { slots, emit }) { - const prefixCls = getPrefixCls('tag-group') - const { selectedValues, updateValue } = useSelectable( - { - modelValue: props.modelValue, - multiple: props.multiple, - options: props.options, - valueField: props.valueField, - }, - (newVal, selectedOptions) => { - emit('update:modelValue', newVal) - emit('change', selectedOptions) - }, - ) - - const renderTag = (option: TagOption, vnode?: VNode) => { - const val = option[props.valueField] - const checked = selectedValues.value.includes(val) - const baseProps = { - 'key': val, - 'value': val, - checked, - 'checkable': true, - 'checkedProps': props.checkedProps, - 'uncheckedProps': props.uncheckedProps, - 'disabled': option.disabled, - 'onUpdate:checked': (valChecked: boolean) => updateValue(option, valChecked), - } - return vnode ? cloneVNode(vnode, baseProps) : {option[props.labelField]} - } - - const renderOptionsTags = () => props.options.map(opt => renderTag(opt)) - - const renderSlotTags = () => { - const slotNodes = slots.default?.() || [] - return slotNodes.map((vnode: VNode) => { - // 判断 vnode.type 是否等于 Tag 组件本身 - if (vnode.type === Tag) { - const vnodeProps = vnode.props || {} - const option = { - value: vnodeProps.value, - label: vnodeProps.label ?? vnodeProps.value, - disabled: vnodeProps.disabled, - } - return renderTag(option, vnode) - } - return vnode - }) - } - - return () => { - const children = props.options.length ? renderOptionsTags() : renderSlotTags() - return
{children}
- } - }, -}) diff --git a/code/base-project/base-vue/packages/ao-design/src/tag/context.ts b/code/base-project/base-vue/packages/ao-design/src/tag/context.ts index 588657d8..724a572a 100644 --- a/code/base-project/base-vue/packages/ao-design/src/tag/context.ts +++ b/code/base-project/base-vue/packages/ao-design/src/tag/context.ts @@ -2,7 +2,7 @@ import type { InjectionKey, Slots } from 'vue' export interface TagGroupContext { name: 'AoTagGroup' - value: Array + value: string | number | boolean | (string | number | boolean)[] | undefined disabled: boolean checkable: boolean slots: Slots diff --git a/code/base-project/base-vue/packages/ao-design/src/tag/demo.vue b/code/base-project/base-vue/packages/ao-design/src/tag/demo.vue index d4d74a44..9818b670 100644 --- a/code/base-project/base-vue/packages/ao-design/src/tag/demo.vue +++ b/code/base-project/base-vue/packages/ao-design/src/tag/demo.vue @@ -204,6 +204,8 @@ + + + + + + +
+

多选 - Options 渲染

+ +

选中值: {{ multiSelected }}

+
+ + +
+

单选 - Options 渲染

+ +

选中值: {{ singleSelected }}

+
+ + +
+

Slot 渲染

+ + + 自定义A + + + 自定义B + + + 自定义C + + +

选中值: {{ slotSelected }}

+
+ + +
+

自定义样式

+ +

选中值: {{ customSelected }}

+
+ + --> diff --git a/code/base-project/base-vue/packages/ao-design/src/tag/index.scss b/code/base-project/base-vue/packages/ao-design/src/tag/index.scss index b4f14683..e53edfec 100644 --- a/code/base-project/base-vue/packages/ao-design/src/tag/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/tag/index.scss @@ -2,6 +2,13 @@ @use 'sass:color'; $tag-prefix: '#{$prefix}-tag'; +$tag-group-prefix: '#{$prefix}-tag-group'; + +.#{$tag-group-prefix} { + > .#{$tag-prefix}:not(:last-child) { + margin-right: 8px; + } +} .#{$tag-prefix} { box-sizing: border-box; diff --git a/code/base-project/base-vue/packages/ao-design/src/tag/tag-group.tsx b/code/base-project/base-vue/packages/ao-design/src/tag/tag-group.tsx index 81846a49..0749bdd7 100644 --- a/code/base-project/base-vue/packages/ao-design/src/tag/tag-group.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/tag/tag-group.tsx @@ -15,72 +15,93 @@ export default defineComponent({ name: 'ATagGroup', props: { modelValue: { - type: Array as PropType>, - default: () => [], + type: [String, Number, Boolean, Array] as PropType< + string | number | boolean | (string | number | boolean)[] + >, + default: undefined, }, defaultValue: { - type: Array as PropType>, - default: () => [], + type: [String, Number, Boolean, Array] as PropType< + string | number | boolean | (string | number | boolean)[] + >, + default: undefined, }, - multiple: { type: Boolean, default: true }, // 单选也用数组,只限制长度为1即可 + multiple: { type: Boolean, default: true }, options: { type: Array as PropType, default: () => [] }, labelField: { type: String, default: 'label' }, valueField: { type: String, default: 'value' }, - checkedProps: { type: Object as PropType>, default: () => ({ type: 'solid', status: 'primary' }) }, - uncheckedProps: { type: Object as PropType>, default: () => ({ type: 'solid', status: 'default' }) }, + checkedProps: { + type: Object as PropType>, + default: () => ({ type: 'solid', status: 'primary' }), + }, + uncheckedProps: { + type: Object as PropType>, + default: () => ({ type: 'solid', status: 'default' }), + }, disabled: { type: Boolean, default: false }, class: { type: [String, Array, Object], default: '' }, style: { type: Object, default: () => ({}) }, }, emits: ['update:modelValue', 'change'], setup(props, { slots, emit }) { - // 统一为数组 - const _value = ref>( - Array.isArray(props.modelValue) ? props.modelValue : props.defaultValue ?? [], - ) + const _value = ref(props.modelValue ?? props.defaultValue ?? (props.multiple ? [] : undefined)) const prefixCls = getPrefixCls('tag-group') const groupDisabled = computed(() => props.disabled) - watch(() => props.modelValue, (newVal) => { - if (Array.isArray(newVal)) { - _value.value = newVal - } - }) + watch( + () => props.modelValue, + (newVal) => { + _value.value = newVal ?? (props.multiple ? [] : undefined) + }, + ) const handleChange = (val: string | number | boolean, e: Event) => { - const arr = [..._value.value] - - const idx = arr.indexOf(val) - if (idx >= 0) { - arr.splice(idx, 1) + // 外部 v-model 类型决定内部 value 的类型 + const isArrayMode = Array.isArray(_value.value) + if (isArrayMode) { + const arr = [...(_value.value as (string | number | boolean)[])] + const idx = arr.indexOf(val) + if (idx >= 0) { + arr.splice(idx, 1) + } + else { + if (!props.multiple) + arr.length = 0 // 单选时只保留最后一个 + arr.push(val) + } + _value.value = arr + emit('update:modelValue', arr) + emit('change', arr, e) } else { - if (!props.multiple) - arr.length = 0 // 单选清空已有值 - arr.push(val) + // 非数组模式:单选或多选时非数组 v-model + const newVal = _value.value === val ? undefined : val + _value.value = newVal + emit('update:modelValue', newVal) + emit('change', newVal, e) } - - _value.value = arr - emit('update:modelValue', _value.value) - emit('change', _value.value, e) } - provide(tagGroupKey, reactive({ - name: 'AoTagGroup' as const, - value: _value, - disabled: groupDisabled, - slots, - checkable: true, - multiple: props.multiple, - handleChange, - })) + provide( + tagGroupKey, + reactive({ + name: 'AoTagGroup' as const, + value: _value, + disabled: groupDisabled, + slots, + checkable: true, + multiple: props.multiple, + handleChange, + }), + ) const renderOptions = () => { return props.options.map((option) => { - const opt = typeof option === 'string' || typeof option === 'number' - ? { [props.labelField]: String(option), [props.valueField]: option } - : option + const opt + = typeof option === 'string' || typeof option === 'number' + ? { [props.labelField]: String(option), [props.valueField]: option } + : option const labelContent = slots.label ? slots.label({ data: opt }) @@ -88,7 +109,10 @@ export default defineComponent({ ? opt[props.labelField]() : opt[props.labelField] - const isChecked = _value.value.includes(opt[props.valueField]) + const isChecked = props.multiple + ? Array.isArray(_value.value) + && _value.value.includes(opt[props.valueField]) + : _value.value === opt[props.valueField] return ( 'solid' | 'light' | 'outline' | 'outline-light', default: 'solid' }, status: { type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'default', default: 'default' }, checkable: { type: Boolean, default: false }, - value: { type: [String, Number, Boolean] as PropType, default: true }, + value: { type: [String, Number, Boolean] as PropType, required: true }, checked: { type: Boolean, default: undefined }, - modelValue: { type: Boolean, default: undefined }, defaultChecked: { type: Boolean, default: false }, checkedProps: { type: Object as PropType, default: () => ({ type: 'solid', status: 'primary' }) }, uncheckedProps: { type: Object as PropType, default: () => ({ type: 'solid', status: 'default' }) }, @@ -25,7 +24,7 @@ export default defineComponent({ class: { type: [String, Array, Object], default: '' }, style: { type: Object, default: () => ({}) }, }, - emits: ['click', 'close', 'update:checked', 'update:modelValue'], + emits: ['click', 'close', 'update:checked'], setup(props, { slots, emit }) { const prefixCls = getPrefixCls('tag') const tagGroupCtx = inject(tagGroupKey, undefined) @@ -34,38 +33,33 @@ export default defineComponent({ const visible = ref(true) const isDisabled = computed(() => props.disabled || tagGroupCtx?.disabled) - const isChecked = computed(() => { - if (inGroup.value) { - const groupValue = tagGroupCtx!.value - return groupValue.includes(props.value) - } - return props.checked ?? props.modelValue ?? _checked.value - }) - const isCheckable = computed(() => props.checkable || tagGroupCtx?.checkable) + const isChecked = computed(() => { + if (inGroup.value && tagGroupCtx) { + const groupValue = tagGroupCtx.value + if (Array.isArray(groupValue)) { + return groupValue.includes(props.value) + } + return groupValue === props.value + } + return props.checked ?? _checked.value + }) + const handleClick = (event: MouseEvent) => { if (isDisabled.value) return - if (inGroup.value) { - // 多选:切换 - if (tagGroupCtx!.multiple) { - tagGroupCtx!.handleChange(props.value, event) - } - else { - // 单选:清空已有值或选中新值 - const newValue = isChecked.value ? '' : props.value - tagGroupCtx!.handleChange(newValue, event) - } + if (inGroup.value && tagGroupCtx) { + tagGroupCtx.handleChange(props.value, event) } else { // 独立 Tag 自己控制 const newChecked = !isChecked.value _checked.value = newChecked - emit('update:modelValue', newChecked) emit('update:checked', newChecked) } + emit('click', { value: props.value, event }) } const handleClose = (event: MouseEvent) => { @@ -75,42 +69,28 @@ export default defineComponent({ } return () => - visible.value && (() => { - const renderType = isCheckable.value - ? isChecked.value - ? props.checkedProps.type - : props.uncheckedProps.type - : props.type - - const renderStatus = isCheckable.value - ? isChecked.value - ? props.checkedProps.status - : props.uncheckedProps.status - : props.status - - return ( -
- {props.icon && } - {slots.default?.()} - {!props.disabled && props.showClose && ( - - )} -
- ) - })() + visible.value && ( +
+ {props.icon && } + {slots.default?.()} + {!props.disabled && props.showClose && ( + + )} +
+ ) }, }) diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue index 1ebc6171..dd2dc422 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,101 +1,200 @@ + +