From 4d0d2f875e3dbd6004f154b6a763aa8eaf3b1754 Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Wed, 17 Sep 2025 14:49:15 +0800 Subject: [PATCH] u --- .../ao-design/src/checkbox/checkbox-group.tsx | 176 ++++++++++++++ .../ao-design/src/checkbox/checkbox.tsx | 225 ++++++++++++++++++ .../ao-design/src/checkbox/context.ts | 13 + .../packages/ao-design/src/checkbox/demo.vue | 46 ++++ .../ao-design/src/checkbox/index.scss | 222 +++++++++++++++++ .../packages/ao-design/src/components.ts | 2 + .../packages/ao-design/src/index.scss | 1 + .../packages/ao-design/src/radio/index.scss | 6 +- .../ao-design/src/radio/radio-group.tsx | 6 +- .../packages/ao-design/src/radio/radio.tsx | 35 ++- .../packages/ao-design/src/tag/tag.tsx | 99 ++++++-- code/base-project/base-vue/src/App.vue | 195 +-------------- .../base-project/base-vue/src/components.d.ts | 2 + 13 files changed, 802 insertions(+), 226 deletions(-) create mode 100644 code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox-group.tsx create mode 100644 code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox.tsx create mode 100644 code/base-project/base-vue/packages/ao-design/src/checkbox/context.ts create mode 100644 code/base-project/base-vue/packages/ao-design/src/checkbox/demo.vue create mode 100644 code/base-project/base-vue/packages/ao-design/src/checkbox/index.scss diff --git a/code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox-group.tsx b/code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox-group.tsx new file mode 100644 index 00000000..f7ceeb11 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox-group.tsx @@ -0,0 +1,176 @@ +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 Checkbox from './checkbox' +import { checkboxGroupKey } from './context' + +interface CheckboxOption { + label?: string | RenderFunction + value?: string | number + extra?: string | RenderFunction + disabled?: boolean + [key: string]: any +} + +export default defineComponent({ + name: 'CheckboxGroup', + props: { + modelValue: { + type: Array as PropType>, + default: () => [], + }, + defaultValue: { + type: Array as PropType>, + default: () => [], + }, + disabled: { + type: Boolean, + default: false, + }, + labelField: { + type: String, + default: 'label', + }, + valueField: { + type: String, + default: 'value', + }, + extraField: { + type: String, + default: 'extra', + }, + options: { + type: Array as PropType>, + default: [], + }, + spaceProps: { + type: Object as PropType>, + default: () => ({}), + }, + type: { + type: String as PropType<'default' | 'card' | 'pureCard'>, + default: 'default', + }, + class: { + type: [String, Array, Object], + default: '', + }, + style: { + type: Object, + default: () => ({}), + }, + }, + emits: ['update:modelValue', 'change'], + setup(props, { slots, emit }) { + const prefixCls = getPrefixCls('checkbox-group') + + const _value = ref(props.modelValue ?? props.defaultValue ?? []) + + watch(() => props.modelValue, (newVal) => { + _value.value = newVal ?? [] + }) + + const groupDisabled = computed(() => props.disabled) + const groupType = computed(() => props.type) + + const handleChange = (val: string | number | boolean, e: Event) => { + const current = _value.value ? [..._value.value] : [] + const index = current.indexOf(val) + + if (index > -1) { + // 已存在 -> 取消选中 + current.splice(index, 1) + } + else { + // 不存在 -> 添加 + current.push(val) + } + + // 非受控模式才更新内部值 + if (props.modelValue === undefined) { + _value.value = current + } + + // 通知外部 + emit('update:modelValue', current) + emit('change', current, e) + } + + // 提供 context 给 Radio 使用 + provide(checkboxGroupKey, reactive({ + name: 'AoCheckboxGroup' as const, + value: _value, + disabled: groupDisabled, + type: groupType, + slots, + 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 labelContent = slots.label + ? slots.label({ data: opt }) + : typeof opt[props.labelField] === 'function' + ? opt[props.labelField]() + : opt[props.labelField] + + const extraContent = slots.extra + ? slots.extra({ data: opt }) + : typeof opt[props.extraField] === 'function' + ? opt[props.extraField]() + : opt[props.extraField] + + const isChecked + = _value.value.includes(opt[props.valueField]) + + if (slots.radio) { + return ( +
handleChange(opt[props.valueField], e)} + > + {slots.radio({ data: opt, checked: _value.value === opt[props.valueField] })} +
+ ) + } + + return ( + + {labelContent} + + ) + }) + } + + const spaceSize + = ['default', 'card', 'pureCard'].includes(groupType.value) + ? [6, 6] + : props.spaceProps.size ?? 0 + + return () => ( + + {props.options.length > 0 ? renderOptions() : slots.default?.()} + + ) + }, +}) diff --git a/code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox.tsx b/code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox.tsx new file mode 100644 index 00000000..4269a594 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/checkbox/checkbox.tsx @@ -0,0 +1,225 @@ +import type { PropType, VNode } from 'vue' +import type { CheckboxGroupContext } from './context' +import { computed, defineComponent, inject, ref, watch } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' +import Icon from '../icon/index' +import { checkboxGroupKey } from './context' + +export default defineComponent({ + name: 'Checkbox', + props: { + modelValue: { + type: Boolean, + default: undefined, + }, + checked: { + type: Boolean, + default: undefined, + }, + value: { + type: [String, Number, Boolean] as PropType, + default: true, + }, + indeterminate: { + type: Boolean, + default: false, + }, + + defaultChecked: { + type: Boolean, + default: false, + }, + disabled: { + type: Boolean, + default: false, + }, + type: { + type: String as PropType<'default' | 'card' | 'pureCard'>, + default: 'default', + }, + extra: { + type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), + default: '', + }, + class: { + type: [String, Array, Object], + default: '', + }, + style: { + type: Object, + default: () => ({}), + }, + }, + emits: ['update:modelValue', 'update:checked', 'change'], + setup(props, { slots, emit }) { + const prefixCls = getPrefixCls('checkbox') + + const checkboxGroupCtx = inject(checkboxGroupKey, undefined) + const inGroup = computed(() => !!checkboxGroupCtx) + + const isDisabled = computed(() => props.disabled || checkboxGroupCtx?.disabled) + const _checked = ref(props.defaultChecked) + + const mergedType = computed(() => + props.type || checkboxGroupCtx?.type || 'default', + ) + + const isChecked = computed(() => { + if (inGroup.value && checkboxGroupCtx) { + const groupValue = checkboxGroupCtx.value + if (Array.isArray(groupValue)) { + return groupValue.includes(props.value) + } + return groupValue === props.value + } + return props.checked ?? props.modelValue ?? _checked.value + }) + + const handleClick = (event: Event) => { + if (isDisabled.value) { + return + } + if (inGroup.value && checkboxGroupCtx) { + checkboxGroupCtx.handleChange(props.value, event) + } + else { + const newChecked = !isChecked.value + if (props.checked === undefined) + _checked.value = newChecked + + emit('update:modelValue', newChecked) + emit('update:checked', newChecked) + } + emit('change', { value: props.value, event }) + } + + watch(() => props.checked, (val) => { + if (val !== undefined) { + _checked.value = val + } + }) + + const renderDefault = () => ( + + ) + + const renderCard = () => ( + + ) + + const renderPureCard = () => ( + + ) + + return () => { + switch (mergedType.value) { + case 'card': + return renderCard() + case 'pureCard': + return renderPureCard() + default: + return renderDefault() + } + } + }, +}) diff --git a/code/base-project/base-vue/packages/ao-design/src/checkbox/context.ts b/code/base-project/base-vue/packages/ao-design/src/checkbox/context.ts new file mode 100644 index 00000000..e1d25d73 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/checkbox/context.ts @@ -0,0 +1,13 @@ +import type { InjectionKey, Slots } from 'vue' + +export interface CheckboxGroupContext { + name: 'AoCheckboxGroup' + value: (string | number | boolean)[] + disabled: boolean + slots: Slots + type?: 'default' | 'card' | 'pureCard' + handleChange: (value: string | number | boolean, e: Event) => void +} + +export const checkboxGroupKey: InjectionKey + = Symbol('CheckboxGroup') diff --git a/code/base-project/base-vue/packages/ao-design/src/checkbox/demo.vue b/code/base-project/base-vue/packages/ao-design/src/checkbox/demo.vue new file mode 100644 index 00000000..6470537c --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/checkbox/demo.vue @@ -0,0 +1,46 @@ + + + diff --git a/code/base-project/base-vue/packages/ao-design/src/checkbox/index.scss b/code/base-project/base-vue/packages/ao-design/src/checkbox/index.scss new file mode 100644 index 00000000..b35079d3 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/checkbox/index.scss @@ -0,0 +1,222 @@ +@use '../_styles/_variables.scss' as *; +@use 'sass:color'; + +$checkbox-prefix: '#{$prefix}-checkbox'; + +.#{$checkbox-prefix}-default { + display: inline-flex; + vertical-align: middle; + color: $text-color-base; + align-items: flex-start; + position: relative; + cursor: pointer; + user-select: none; + + &.#{$checkbox-prefix}-checked { + .#{$checkbox-prefix}-main { + .#{$checkbox-prefix}-inner { + border-color: transparent; + background-color: $primary-color; + color: $text-color-anti; + } + } + } + + .#{$checkbox-prefix}-main { + cursor: pointer; + display: flex; + align-items: center; + white-space: nowrap; + outline: 0; + + .#{$checkbox-prefix}-input { + opacity: 0; + outline: 0; + position: absolute; + z-index: -1; + margin: 0; + width: 0; + height: 0; + box-sizing: border-box; + } + .#{$checkbox-prefix}-inner { + border: 1px solid $border-color; + border-radius: $border-radius; + width: 16px; + height: 16px; + background-color: $bg-color-container; + cursor: pointer; + box-sizing: border-box; + outline: 0; + display: flex; + align-items: center; + justify-content: center; + transition: + background-color 0.1s ease, + border-color 0.1s ease; + } + } + + .#{$checkbox-prefix}-content { + margin-left: 8px; + display: inline-block; + flex-direction: column; + + .#{$checkbox-prefix}-label { + font-size: 14px; + line-height: 16px; + display: flex; + align-items: center; + color: $text-color-base; + } + + .#{$checkbox-prefix}-extra { + font-size: 12px; + color: $text-color-secondary; + margin-top: 4px; + } + } +} + +.#{$checkbox-prefix}-card { + color: $text-color-base; + display: inline-flex; + align-items: flex-start; + position: relative; + cursor: pointer; + user-select: none; + padding: 8px 12px; + box-sizing: border-box; + border-radius: $border-radius; + border: 1px solid transparent; + + &:hover, + &:hover .#{$checkbox-prefix}-main { + background-color: $bg-color-component; + } + + &.#{$checkbox-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &.#{$checkbox-prefix}-checked { + border-color: $primary-color; + background-color: $status-info-bg-color; + } + + &:hover .#{$checkbox-prefix}-main .#{$checkbox-prefix}-inner { + border-color: $primary-color; + } + + &.#{$checkbox-prefix}-checked { + .#{$checkbox-prefix}-main { + .#{$checkbox-prefix}-inner { + border-color: transparent; + background-color: $primary-color; + color: $text-color-anti; + } + } + } + + .#{$checkbox-prefix}-main { + cursor: pointer; + display: flex; + align-items: center; + white-space: nowrap; + outline: 0; + .#{$checkbox-prefix}-input { + opacity: 0; + outline: 0; + position: absolute; + z-index: -1; + margin: 0; + width: 0; + height: 0; + box-sizing: border-box; + } + .#{$checkbox-prefix}-inner { + border: 1px solid $border-color; + border-radius: $border-radius; + width: 16px; + height: 16px; + background-color: $bg-color-container; + cursor: pointer; + box-sizing: border-box; + outline: 0; + display: flex; + align-items: center; + justify-content: center; + transition: + background-color 0.1s ease, + border-color 0.1s ease; + } + } + + .#{$checkbox-prefix}-content { + margin-left: 8px; + display: inline-block; + flex-direction: column; + + .#{$checkbox-prefix}-label { + font-size: 14px; + line-height: 16px; + display: flex; + align-items: center; + color: $text-color-base; + } + + .#{$checkbox-prefix}-extra { + font-size: 12px; + color: $text-color-secondary; + margin-top: 4px; + } + } +} + +.#{$checkbox-prefix}-pureCard { + color: $text-color-base; + display: inline-flex; + align-items: flex-start; + position: relative; + cursor: pointer; + user-select: none; + padding: 8px 12px; + box-sizing: border-box; + border-radius: $border-radius; + border: 1px solid transparent; + + &:hover, + &:hover .#{$checkbox-prefix}-main { + background-color: $bg-color-component; + } + + &.#{$checkbox-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &.#{$checkbox-prefix}-checked { + border-color: $primary-color; + background-color: $status-info-bg-color; + } + + .#{$checkbox-prefix}-content { + display: inline-block; + flex-direction: column; + + .#{$checkbox-prefix}-label { + font-size: 14px; + line-height: 16px; + display: flex; + align-items: center; + color: $text-color-base; + } + + .#{$checkbox-prefix}-extra { + font-size: 12px; + color: $text-color-secondary; + margin-top: 4px; + } + } +} diff --git a/code/base-project/base-vue/packages/ao-design/src/components.ts b/code/base-project/base-vue/packages/ao-design/src/components.ts index 947ff529..b2224227 100644 --- a/code/base-project/base-vue/packages/ao-design/src/components.ts +++ b/code/base-project/base-vue/packages/ao-design/src/components.ts @@ -1,5 +1,7 @@ export { default as ConfigProvider } from './_config' export { default as Button } from './button' +export { default as Checkbox } from './checkbox/checkbox' +export { default as CheckboxGroup } from './checkbox/checkbox-group' export { default as Drawer } from './drawer' export { default as Group } from './group' export { default as Icon } from './icon' 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 ffac5be0..3a1f65b3 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 @@ -16,6 +16,7 @@ @use './space/index.scss' as *; @use './separator/index.scss' as *; @use './radio/index.scss' as *; +@use './checkbox/index.scss' as *; [class*='#{variables.$prefix}-'] { -webkit-box-sizing: border-box; diff --git a/code/base-project/base-vue/packages/ao-design/src/radio/index.scss b/code/base-project/base-vue/packages/ao-design/src/radio/index.scss index 743f075a..541717a6 100644 --- a/code/base-project/base-vue/packages/ao-design/src/radio/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/radio/index.scss @@ -74,7 +74,7 @@ $radio-group-prefix: '#{$prefix}-radio-group'; } .#{$radio-prefix}-content { - font-size: 12px; + font-size: 14px; margin-left: 8px; display: inline-block; flex-direction: column; @@ -465,12 +465,12 @@ $radio-group-prefix: '#{$prefix}-radio-group'; } .#{$radio-prefix}-content { - font-size: 12px; margin-left: 8px; display: inline-block; flex-direction: column; .#{$radio-prefix}-label { + font-size: 14px; line-height: 16px; display: flex; align-items: center; @@ -513,11 +513,11 @@ $radio-group-prefix: '#{$prefix}-radio-group'; } .#{$radio-prefix}-content { - font-size: 12px; display: inline-block; flex-direction: column; .#{$radio-prefix}-label { + font-size: 14px; line-height: 16px; display: flex; align-items: center; 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 89a7de21..30aaabed 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 @@ -72,13 +72,15 @@ export default defineComponent({ _value.value = newVal ?? undefined }) + const groupDisabled = computed(() => props.disabled) + const groupType = computed(() => props.type) + const handleChange = (val: string | number | boolean | undefined, e: Event) => { _value.value = val emit('update:modelValue', val) emit('change', val, e) } - const groupDisabled = computed(() => props.disabled) - const groupType = computed(() => props.type) + // 提供 context 给 Radio 使用 provide(radioGroupKey, reactive({ name: 'AoRadioGroup' as const, 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 5fa5addb..35173466 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 @@ -1,6 +1,6 @@ import type { PropType, VNode } from 'vue' import type { RadioGroupContext } from './context' -import { computed, defineComponent, inject, ref } from 'vue' +import { computed, defineComponent, inject, ref, watch } from 'vue' import { getPrefixCls } from '../_utils/getPrefixCls' import { radioGroupKey } from './context' @@ -11,6 +11,14 @@ export default defineComponent({ type: Boolean, default: undefined, }, + checked: { + type: Boolean, + default: undefined, + }, + value: { + type: [String, Number, Boolean] as PropType, + default: undefined, + }, defaultChecked: { type: Boolean, default: false, @@ -19,10 +27,6 @@ export default defineComponent({ type: Boolean, default: false, }, - value: { - type: [String, Number, Boolean] as PropType, - default: true, - }, allowUncheck: { type: Boolean, default: true, @@ -36,7 +40,7 @@ export default defineComponent({ default: 'default', }, }, - emits: ['update:modelValue', 'change'], + emits: ['update:modelValue', 'update:checked', 'change'], setup(props, { slots, emit }) { const prefixCls = getPrefixCls('radio') @@ -46,10 +50,14 @@ export default defineComponent({ const isDisabled = computed(() => props.disabled || radioGroupCtx?.disabled) const _checked = ref(props.defaultChecked) + const mergedType = computed(() => + props.type || radioGroupCtx?.type || 'default', + ) + const isChecked = computed(() => { if (inGroup.value) return props.value === radioGroupCtx!.value - return props.modelValue ?? _checked.value + return props.checked ?? props.modelValue ?? _checked.value }) const isAllowUncheck = computed(() => props.allowUncheck || radioGroupCtx?.allowUncheck) @@ -66,12 +74,21 @@ export default defineComponent({ } else { const newValue = isAllowUncheck.value ? !isChecked.value : true - _checked.value = newValue + if (props.checked === undefined) { + _checked.value = newValue + } + emit('update:modelValue', newValue) emit('change', props.value, e) } } + watch(() => props.checked, (val) => { + if (val !== undefined) { + _checked.value = val + } + }) + const renderDefault = () => (