diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss index 1d9f6ef8..e9fa641a 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/_variables.scss @@ -94,6 +94,10 @@ $scroll-track-color: var(--ao-scroll-track-color); $bg-color-container: var(--ao-bg-color-container); $bg-color-container-hover: var(--ao-bg-color-container-hover); $bg-color-container-active: var(--ao-bg-color-container-active); +$bg-color-container-select: var(--ao-bg-color-container-select); + +$bg-color-component: var(--ao-bg-color-component); +$border-color-component: var(--ao-border-color-component); // 组件边框(Input、Select、Card 等容器的外部边框) // shadow // 基础/下层 投影 hover 使用的组件包括:表格 / diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss index d4217be5..f4c9a9ef 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_dark.scss @@ -72,6 +72,10 @@ --ao-bg-color-container: #242424; --ao-bg-color-container-hover: #2c2c2c; --ao-bg-color-container-active: #4b4b4b; + --ao-bg-color-container-select: #5e5e5e; + + --ao-bg-color-component: #393939; + --ao-border-color-component: #5e5e5e; // shadow阴影 // 基础/下层 投影 hover 使用的组件包括:表格 / diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss index dbce674b..959f0911 100644 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss +++ b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_light.scss @@ -73,6 +73,10 @@ --ao-bg-color-container: #fff; --ao-bg-color-container-hover: #f3f3f3; --ao-bg-color-container-active: #e8e8e8; + --ao-bg-color-container-select: #fff; + + --ao-bg-color-component: #e8e8e8; + --ao-border-color-component: #ddd; // shadow阴影 // 基础/下层 投影 hover 使用的组件包括:表格 / 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/index.scss b/code/base-project/base-vue/packages/ao-design/src/index.scss index 54297fb9..e5218888 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 @@ -21,6 +21,7 @@ [class*='#{variables.$prefix}-'] { -webkit-box-sizing: border-box; box-sizing: border-box; + font-family: Inter, -apple-system, 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 b1c88f8f..28236b3c 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 @@ -5,6 +5,7 @@ export interface RadioGroupContext { value: string | number | boolean disabled: boolean slots: Slots + type?: 'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline' handleChange: (value: string | number | boolean, e: Event) => void } 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 new file mode 100644 index 00000000..79314b03 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/radio/demo.vue @@ -0,0 +1,199 @@ + + + + + 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 cbb3fe6a..743f075a 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 @@ -3,14 +3,13 @@ $radio-prefix: '#{$prefix}-radio'; $radio-group-prefix: '#{$prefix}-radio-group'; -.#{$radio-prefix} { +.#{$radio-prefix}-default { color: $text-color-base; display: inline-flex; - align-items: center; + align-items: flex-start; position: relative; cursor: pointer; user-select: none; - line-height: 18px; &.#{$radio-prefix}-disabled { cursor: not-allowed; opacity: 0.5; @@ -20,13 +19,20 @@ $radio-group-prefix: '#{$prefix}-radio-group'; border-color: $primary-color; } + &.#{$radio-prefix}-checked .#{$radio-prefix}-main .#{$radio-prefix}-inner { + border-color: $primary-color; + &::after { + transform: translate(-50%, -50%) scale(1); + background-color: $primary-color; + } + } + .#{$radio-prefix}-main { cursor: pointer; display: flex; align-items: center; white-space: nowrap; outline: 0; - margin-top: 1px; .#{$radio-prefix}-input { opacity: 0; outline: 0; @@ -66,7 +72,343 @@ $radio-group-prefix: '#{$prefix}-radio-group'; } } } - + + .#{$radio-prefix}-content { + font-size: 12px; + margin-left: 8px; + display: inline-block; + flex-direction: column; + + .#{$radio-prefix}-label { + line-height: 16px; + display: flex; + align-items: center; + color: $text-color-base; + } + + .#{$radio-prefix}-extra { + font-size: 12px; + color: $text-color-secondary; + margin-top: 4px; + } + } +} + +.#{$radio-group-prefix}-filled { + background-color: $bg-color-component; + border-radius: $border-radius; + padding: 1.5px; + line-height: 26px; + + &.#{$radio-group-prefix}-small { + line-height: 18px; + + .#{$radio-prefix}-filled { + font-size: 11px; + line-height: 18px; + } + } + + &.#{$radio-group-prefix}-medium { + line-height: 22px; + .#{$radio-prefix}-filled { + font-size: 12px; + line-height: 22px; + } + } + + &.#{$radio-group-prefix}-large { + line-height: 26px; + .#{$radio-prefix}-filled { + font-size: 13px; + line-height: 26px; + } + } +} + +.#{$radio-prefix}-filled { + margin: 1.5px; + position: relative; + display: inline-block; + color: $text-color-secondary; + font-size: 14px; + line-height: 26px; + background-color: transparent; + border-right: 0; + cursor: pointer; + width: 100%; + + &:first-child { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; + } + + &:last-child { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + } + + &.#{$radio-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + .#{$radio-prefix}-input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + } + + .#{$radio-prefix}-content { + position: relative; + display: block; + padding: 0 12px; + } + + &.#{$radio-prefix}-checked { + color: $text-color-base; + background-color: $bg-color-container-select; + z-index: 1; + } + + &:not(.#{$radio-prefix}-disabled):not(.#{$radio-prefix}-checked):hover { + color: $text-color-base; + } + + &:before { + content: ''; + position: absolute; + left: -2px; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 80%; + background-color: $border-color-component; + transition: opacity 0.2s cubic-bezier(0, 0, 0.15, 1); + } +} + +.#{$radio-group-prefix}-primaryFilled { + background-color: $bg-color-component; + border-radius: $border-radius; + padding: 1.5px; + line-height: 26px; + + &.#{$radio-group-prefix}-small { + line-height: 18px; + + .#{$radio-prefix}-primaryFilled { + font-size: 11px; + line-height: 18px; + } + } + + &.#{$radio-group-prefix}-medium { + line-height: 22px; + .#{$radio-prefix}-primaryFilled { + font-size: 12px; + line-height: 22px; + } + } + + &.#{$radio-group-prefix}-large { + line-height: 26px; + .#{$radio-prefix}-primaryFilled { + font-size: 13px; + line-height: 26px; + } + } +} + +.#{$radio-prefix}-primaryFilled { + margin: 1.5px; + position: relative; + display: inline-block; + color: $text-color-secondary; + font-size: 14px; + line-height: 26px; + background-color: transparent; + border-right: 0; + cursor: pointer; + width: 100%; + + &:first-child { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; + } + + &:last-child { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + } + + &.#{$radio-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + .#{$radio-prefix}-input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + } + + .#{$radio-prefix}-content { + position: relative; + display: block; + padding: 0 12px; + } + + &.#{$radio-prefix}-checked { + color: $text-color-anti; + background-color: $primary-color; + z-index: 1; + } + + &:not(.#{$radio-prefix}-disabled):not(.#{$radio-prefix}-checked):hover { + color: $primary-color; + } + + &:before { + content: ''; + position: absolute; + left: -2px; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 80%; + background-color: $border-color-component; + transition: opacity 0.2s cubic-bezier(0, 0, 0.15, 1); + } +} + +.#{$radio-group-prefix}-outline { + padding: 1.5px; + line-height: 26px; + background-color: transparent; + border-radius: $border-radius; + + &.#{$radio-group-prefix}-small { + line-height: 18px; + + .#{$radio-prefix}-outline { + font-size: 11px; + line-height: 18px; + } + } + + &.#{$radio-group-prefix}-medium { + line-height: 22px; + .#{$radio-prefix}-outline { + font-size: 12px; + line-height: 22px; + } + } + + &.#{$radio-group-prefix}-large { + line-height: 26px; + .#{$radio-prefix}-outline { + font-size: 13px; + line-height: 26px; + } + } + + .#{$radio-prefix}-outline:last-child { + border-right-color: $border-color; + } + .#{$radio-prefix}-outline.#{$radio-prefix}-checked.#{$radio-prefix}-outline:last-child { + border-color: $primary-color; + } + .#{$radio-prefix}-outline.#{$radio-prefix}-checked + .#{$radio-prefix}-outline { + border-left-color: transparent; + } +} + +.#{$radio-prefix}-outline { + position: relative; + display: inline-block; + color: $text-color-secondary; + font-size: 14px; + line-height: 26px; + background-color: transparent; + border: 1px solid $border-color; + border-right-color: transparent; + cursor: pointer; + width: 100%; + + &:first-child { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; + } + + &:last-child { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + } + + &.#{$radio-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + .#{$radio-prefix}-input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + } + .#{$radio-prefix}-content { + position: relative; + display: block; + padding: 0 12px; + } + &:not(.#{$radio-prefix}-disabled):hover { + color: $primary-color; + } + &.#{$radio-prefix}-checked { + color: $primary-color; + border-color: $primary-color; + } +} + +.#{$radio-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: 1px solid transparent; + border-radius: $border-radius; + + &:hover, + &:hover .#{$radio-prefix}-main { + background-color: $bg-color-component; + } + + &.#{$radio-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &.#{$radio-prefix}-checked { + border-color: $primary-color; + background-color: $status-info-bg-color; + } + + &:hover .#{$radio-prefix}-main .#{$radio-prefix}-inner { + border-color: $primary-color; + } + &.#{$radio-prefix}-checked .#{$radio-prefix}-main .#{$radio-prefix}-inner { border-color: $primary-color; &::after { @@ -75,12 +417,118 @@ $radio-group-prefix: '#{$prefix}-radio-group'; } } + .#{$radio-prefix}-main { + cursor: pointer; + display: flex; + align-items: center; + white-space: nowrap; + outline: 0; + + .#{$radio-prefix}-input { + opacity: 0; + outline: 0; + position: absolute; + z-index: -1; + margin: 0; + width: 0; + height: 0; + box-sizing: border-box; + } + + .#{$radio-prefix}-inner { + border: 1px solid $border-color; + border-radius: 100%; + width: 16px; + height: 16px; + background-color: $bg-color-container; + position: relative; + display: inline-block; + cursor: pointer; + box-sizing: border-box; + outline: 0; + &::after { + box-sizing: border-box; + width: 8px; + height: 8px; + border-radius: 999px; + background-color: transparent; + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(0); + transition: + transform 0.1s ease-in, + background-color 0.1s ease-in; + } + } + } + .#{$radio-prefix}-content { font-size: 12px; margin-left: 8px; - vertical-align: middle; + display: inline-block; + flex-direction: column; + + .#{$radio-prefix}-label { + line-height: 16px; + display: flex; + align-items: center; + color: $text-color-base; + font-weight: 600; + } + + .#{$radio-prefix}-extra { + font-size: 12px; + color: $text-color-secondary; + margin-top: 8px; + } } } -.#{$radio-group-prefix} { +.#{$radio-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: 1px solid transparent; + border-radius: $border-radius; + + &:hover { + background-color: $bg-color-component; + } + + &.#{$radio-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &.#{$radio-prefix}-checked { + border-color: $primary-color; + background-color: $status-info-bg-color; + } + + .#{$radio-prefix}-content { + font-size: 12px; + display: inline-block; + flex-direction: column; + + .#{$radio-prefix}-label { + line-height: 16px; + display: flex; + align-items: center; + color: $text-color-base; + font-weight: 600; + } + + .#{$radio-prefix}-extra { + font-size: 12px; + color: $text-color-secondary; + margin-top: 8px; + } + } } 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 e96cac8c..3985f90e 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 @@ -36,6 +36,10 @@ export default defineComponent({ type: String, default: 'value', }, + extraField: { + type: String, + default: 'extra', + }, options: { type: Array as PropType>, default: [], @@ -44,6 +48,14 @@ export default defineComponent({ type: Object as PropType>, default: () => ({}), }, + type: { + type: String as PropType<'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline'>, + default: 'default', + }, + size: { + type: String as PropType<'small' | 'medium' | 'large'>, + default: 'medium', + }, }, emits: ['update:modelValue', 'change'], setup(props, { emit, slots }) { @@ -64,12 +76,13 @@ export default defineComponent({ emit('change', val, e) } const groupDisabled = computed(() => props.disabled) - + const groupType = computed(() => props.type) // 提供 context 给 Radio 使用 provide(radioGroupKey, reactive({ name: 'AoRadioGroup' as const, value: _value, disabled: groupDisabled, + type: groupType, slots, handleChange, })) @@ -88,12 +101,31 @@ export default defineComponent({ ? 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] + + if (slots.radio) { + return ( +
handleChange(opt[props.valueField], e)} // 内部统一处理点击 + > + {slots.radio({ data: opt, checked: _value.value === opt[props.valueField] })} +
+ ) + } + return ( {labelContent} @@ -101,12 +133,30 @@ export default defineComponent({ }) } + const spaceSize + = ['default', 'card', 'pureCard'].includes(groupType.value) + ? [6, 6] + : props.spaceProps.size ?? 0 + + const sizeClass = computed(() => { + if (['filled', 'primaryFilled', 'outline'].includes(groupType.value)) { + return `${prefixCls}-${props.size}` + } + return null + }) + return () => ( -
- - {props.options.length > 0 ? renderOptions() : slots.default?.()} - -
+ + {props.options.length > 0 ? renderOptions() : slots.default?.()} + ) }, }) 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 279ae553..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 @@ -31,6 +31,10 @@ export default defineComponent({ type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), default: '', }, + type: { + type: String as PropType<'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline'>, + default: 'default', + }, }, emits: ['update:modelValue', 'change'], setup(props, { slots, emit }) { @@ -42,11 +46,10 @@ export default defineComponent({ 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 + return props.modelValue ?? _checked.value }) const handleClick = (e: Event) => { @@ -61,17 +64,16 @@ export default defineComponent({ } else { const newValue = props.allowUncheck ? !isChecked.value : true - _checked.value = newValue emit('update:modelValue', newValue) emit('change', props.value, e) } } - return () => ( + const renderDefault = () => ( ) + + const renderFilled = () => ( + + ) + + const renderPrimaryFilled = () => ( + + ) + + const renderCard = () => ( + + ) + + const renderPureCard = () => ( + + ) + + const renderOutline = () => ( + + ) + + return () => { + switch (props.type) { + case 'filled': + return renderFilled() + case 'primaryFilled': + return renderPrimaryFilled() + case 'card': + return renderCard() + case 'pureCard': + return renderPureCard() + case 'outline': + return renderOutline() + default: + return renderDefault() + } + } }, }) 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..5f15cc24 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/tag/tag.tsx @@ -0,0 +1,116 @@ +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 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 ac2a3b22..1ebc6171 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,110 +1,101 @@ - -