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 4e62c80a..947ff529 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 @@ -11,7 +11,7 @@ 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' -export { default as Tag } from './tag' -export { default as TagGroup } from './tag-group' +export { default as Tag } from './tag/tag' +export { default as TagGroup } from './tag/tag-group' export { default as Textarea } from './textarea' export { default as Tooltip } from './tooltip' 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 65c2cd89..08e3d734 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 @@ -34,7 +34,7 @@ export default defineComponent({ type: { type: String as PropType<'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline'>, default: 'default', - } + }, }, emits: ['update:modelValue', 'change'], setup(props, { slots, emit }) { @@ -49,7 +49,7 @@ export default defineComponent({ const isChecked = computed(() => { if (inGroup.value) return props.value === radioGroupCtx!.value - return _checked.value + return props.modelValue ?? _checked.value }) const handleClick = (e: Event) => { 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 index 38590f07..6fb1c5a7 100644 --- 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 @@ -2,7 +2,7 @@ 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/index' +import Tag from '../tag/tag' interface TagOption { label?: string 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 new file mode 100644 index 00000000..588657d8 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/tag/context.ts @@ -0,0 +1,13 @@ +import type { InjectionKey, Slots } from 'vue' + +export interface TagGroupContext { + name: 'AoTagGroup' + value: Array + disabled: boolean + checkable: boolean + slots: Slots + multiple: boolean + handleChange: (value: string | number | boolean, e: Event) => void +} + +export const tagGroupKey: InjectionKey = Symbol('TagGroup') diff --git a/code/base-project/base-vue/packages/ao-design/src/tag/index.tsx b/code/base-project/base-vue/packages/ao-design/src/tag/index.tsx deleted file mode 100644 index 93b26593..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/tag/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import type { PropType } from 'vue' -import { defineComponent, ref } from 'vue' -import { getPrefixCls } from '../_utils/getPrefixCls' -import Icon from '../icon/index' - -export default defineComponent({ - name: 'Tag', - props: { - shape: { - type: String as PropType<'square' | 'round' | 'mark'>, - default: 'square', - }, - size: { - type: String as PropType<'small' | 'medium' | 'large'>, - default: 'medium', - }, - type: { - type: String as () => 'solid' | 'light' | 'outline' | 'outline-light', - default: 'solid', - }, - status: { - type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'default', - default: 'default', - }, - checkable: { type: Boolean, default: false }, - checked: { type: Boolean, default: false }, - value: { type: [String, Number], required: false }, - checkedProps: { - type: Object as PropType<{ type?: 'solid' | 'light' | 'outline' | 'outline-light', status?: 'primary' | 'danger' | 'warning' | 'success' | 'default' }>, - default: () => ({ - type: 'solid', - status: 'primary', - }), - }, - uncheckedProps: { - type: Object as PropType<{ type?: 'solid' | 'light' | 'outline' | 'outline-light', status?: 'primary' | 'danger' | 'warning' | 'success' | 'default' }>, - default: () => ({ - type: 'solid', - status: 'default', - }), - }, - showClose: { type: Boolean, default: false }, - icon: { type: String, default: '' }, - disabled: { type: Boolean, default: false }, - class: { type: [String, Array, Object], default: '' }, - style: { type: Object, default: () => ({}) }, - }, - emits: ['click', 'close', 'update:checked'], - setup(props, { slots, emit }) { - const prefixCls = getPrefixCls('tag') - const visible = ref(true) - - const handleClick = (event: MouseEvent) => { - if (props.disabled) - return - - // 如果 checkable,触发 update:checked - if (props.checkable) { - emit('update:checked', !props.checked) - } - - emit('click', { value: props.value, checked: props.checked, event }) - } - - const handleClose = (event: MouseEvent) => { - event.stopPropagation() - visible.value = false - emit('close', { value: props.value, event }) - } - - return () => - visible.value && (() => { - const renderType = props.checkable - ? props.checked - ? props.checkedProps.type - : props.uncheckedProps.type - : props.type - - const renderStatus = props.checkable - ? props.checked - ? props.checkedProps.status - : props.uncheckedProps.status - : props.status - - return ( -
- {props.icon && } - {slots.default?.()} - {!props.disabled && props.showClose && ( - - )} -
- ) - })() - }, -}) 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 new file mode 100644 index 00000000..81846a49 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/tag/tag-group.tsx @@ -0,0 +1,115 @@ +import type { PropType, RenderFunction } from 'vue' +import { computed, defineComponent, provide, reactive, ref, watch } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' +import Tag from '../tag/tag' +import { tagGroupKey } from './context' + +interface TagOption { + label?: string | RenderFunction + value?: string | boolean | number + disabled?: boolean + [key: string]: any +} + +export default defineComponent({ + name: 'ATagGroup', + props: { + modelValue: { + type: Array as PropType>, + default: () => [], + }, + defaultValue: { + type: Array as PropType>, + default: () => [], + }, + multiple: { type: Boolean, default: true }, // 单选也用数组,只限制长度为1即可 + 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' }) }, + 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 prefixCls = getPrefixCls('tag-group') + const groupDisabled = computed(() => props.disabled) + + watch(() => props.modelValue, (newVal) => { + if (Array.isArray(newVal)) { + _value.value = newVal + } + }) + + const handleChange = (val: string | number | boolean, e: Event) => { + const arr = [..._value.value] + + 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', _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, + })) + + 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 isChecked = _value.value.includes(opt[props.valueField]) + + return ( + + {labelContent} + + ) + }) + } + + return () => ( +
+ {props.options.length > 0 ? renderOptions() : slots.default?.()} +
+ ) + }, +}) diff --git a/code/base-project/base-vue/packages/ao-design/src/tag/tag.tsx b/code/base-project/base-vue/packages/ao-design/src/tag/tag.tsx new file mode 100644 index 00000000..2e55ecbe --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/tag/tag.tsx @@ -0,0 +1,117 @@ +import type { PropType } from 'vue' +import type { TagGroupContext } from './context' +import { computed, defineComponent, inject, ref } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' +import Icon from '../icon/index' +import { tagGroupKey } from './context' + +export default defineComponent({ + name: 'Tag', + props: { + shape: { type: String as PropType<'square' | 'round' | 'mark'>, default: 'square' }, + size: { type: String as PropType<'small' | 'medium' | 'large'>, default: 'medium' }, + type: { type: String as () => '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 }, + 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' }) }, + showClose: { type: Boolean, default: false }, + icon: { type: String, default: '' }, + disabled: { type: Boolean, default: false }, + class: { type: [String, Array, Object], default: '' }, + style: { type: Object, default: () => ({}) }, + }, + emits: ['click', 'close', 'update:checked', 'update:modelValue'], + setup(props, { slots, emit }) { + const prefixCls = getPrefixCls('tag') + const tagGroupCtx = inject(tagGroupKey, undefined) + const inGroup = computed(() => !!tagGroupCtx) + const _checked = ref(props.defaultChecked) + const visible = ref(true) + + const isMultiple = computed(() => tagGroupCtx?.multiple ?? false) + 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 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) + } + } + else { + // 独立 Tag 自己控制 + const newChecked = !isChecked.value + _checked.value = newChecked + emit('update:modelValue', newChecked) + emit('update:checked', newChecked) + } + } + + const handleClose = (event: MouseEvent) => { + event.stopPropagation() + visible.value = false + emit('close', { value: props.value, event }) + } + + 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 && ( + + )} +
+ ) + })() + }, +}) diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue index e69319a2..1ebc6171 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,13 +1,101 @@ - -