diff --git a/code/base-project/base-vue/packages/ao-design/src/_hooks/useSelectable.ts b/code/base-project/base-vue/packages/ao-design/src/_hooks/useSelectable.ts new file mode 100644 index 00000000..a5e2b830 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/_hooks/useSelectable.ts @@ -0,0 +1,47 @@ +import type { Ref } from 'vue' +import { ref } from 'vue' + +export interface UseSelectableProps { + modelValue: T[] + multiple?: boolean + options?: Record[] + valueField?: string +} + +export function useSelectable( + props: UseSelectableProps, + onChange?: (selectedValues: T[], selectedOptions?: Record[]) => void, +) { + const valueField = props.valueField || 'value' + + // 修复:使用泛型参数明确指定 ref 的类型 + const selectedValues = ref(props.modelValue) as Ref + + const updateValue = (option: Record, checked: boolean) => { + const val = option[valueField] as T + + let newValues: T[] + if (props.multiple) { + newValues = checked + ? selectedValues.value.includes(val) + ? [...selectedValues.value] + : [...selectedValues.value, val] + : selectedValues.value.filter(v => v !== val) + } + else { + newValues = checked ? [val] : [] + } + + selectedValues.value = newValues + + // 如果有 options,返回完整对象数组 + const selectedOptions = props.options?.filter(opt => newValues.includes(opt[valueField])) + + onChange?.(newValues, selectedOptions) + } + + return { + selectedValues, + updateValue, + } +} 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 13379ffb..1d9f6ef8 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 @@ -66,7 +66,7 @@ $text-color-anti: var(--ao-text-color-anti); // 色彩-文字-反色 $text-color-link: var(--ao-text-color-link); // 色彩-文字-链接 $border-radius: 2px; -$stroke-color: var(--ao-stroke-color); // 分割线(内容区域之间的分隔,如列表、面板内部) +$separator-color: var(--ao-separator-color); // 分割线(内容区域之间的分隔,如列表、面板内部) $border-color: var(--ao-border-color); // 组件边框(Input、Select、Card 等容器的外部边框) $bg-color-control: var( 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 703ca4ba..d4217be5 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 @@ -56,7 +56,7 @@ --ao-text-color-link: #4582e6; /* 链接文字 */ --ao-text-color-watermark: rgba(255, 255, 255, 10%); /* 水印 / 背景文字 */ - --ao-stroke-color: #393939; // 分割线 + --ao-separator-color: #393939; // 分割线 --ao-border-color: #5e5e5e; // 边框 --ao-bg-color-control: transparent; // 特殊组件背景色,目前只用于 button、input 组件多主题场景,浅色主题下固定为白色,深色主题下为 transparent 适配背景颜色 diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_font.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_font.scss deleted file mode 100644 index df20508f..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_font.scss +++ /dev/null @@ -1,70 +0,0 @@ -// 字体相关token -:root { - // 字体family token - --td-font-family: PingFang SC, Microsoft YaHei, Arial Regular; - --td-font-family-medium: PingFang SC, Microsoft YaHei, Arial Medium; - - // 字体大小 token - --td-font-size-link-small: 12px; - --td-font-size-link-medium: 14px; - --td-font-size-link-large: 16px; - --td-font-size-mark-small: 12px; - --td-font-size-mark-medium: 14px; - --td-font-size-body-small: 12px; - --td-font-size-body-medium: 13px; - --td-font-size-body-large: 1px; - --td-font-size-title-small: 14px; - --td-font-size-title-medium: 16px; - --td-font-size-title-large: 18px; - --td-font-size-title-extraLarge: 20px; - --td-font-size-headline-small: 24px; - --td-font-size-headline-medium: 28px; - --td-font-size-headline-large: 36px; - --td-font-size-display-medium: 48px; - --td-font-size-display-large: 64px; - - // 字体行高 token - --td-line-height-link-small: 20px; - --td-line-height-link-medium: 22px; - --td-line-height-link-large: 24px; - --td-line-height-mark-small: 20px; - --td-line-height-mark-medium: 22px; - --td-line-height-body-small: 20px; - --td-line-height-body-medium: 22px; - --td-line-height-body-large: 24px; - --td-line-height-title-small: 22px; - --td-line-height-title-medium: 24px; - --td-line-height-title-large: 26px; - --td-line-height-title-extraLarge: 28px; - --td-line-height-headline-small: 32px; - --td-line-height-headline-medium: 36px; - --td-line-height-headline-large: 44px; - --td-line-height-display-medium: 56px; - --td-line-height-display-large: 72px; - - // font token - --td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family); - --td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family); - --td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family); - --td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family); - --td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family); - --td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family); - --td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family); - --td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family); - --td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family); - --td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) - var(--td-font-family); - --td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family); - --td-font-title-extraLarge: 600 var(--td-font-size-title-extraLarge) / var(--td-line-height-title-extraLarge) - var(--td-font-family); - --td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) - var(--td-font-family); - --td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) - var(--td-font-family); - --td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) - var(--td-font-family); - --td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) - var(--td-font-family); - --td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) - var(--td-font-family); -} 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 da7caf2a..dbce674b 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 @@ -57,7 +57,7 @@ --ao-text-color-link: #003cab; /* 链接文字 */ --ao-text-color-watermark: rgba(0, 0, 0, 10%); /* 水印 / 背景文字 */ - --ao-stroke-color: #e8e8e8; // 分割线 + --ao-separator-color: #e8e8e8; // 分割线 --ao-border-color: #ddd; // 边框 --ao-bg-color-control: #fff; // 特殊组件背景色,目前只用于 button、input 组件多主题场景,浅色主题下固定为白色,深色主题下为 transparent 适配背景颜色 diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_radius.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_radius.scss deleted file mode 100644 index ae55f19f..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_radius.scss +++ /dev/null @@ -1,10 +0,0 @@ -// border-radius相关 token -:root { - --td-radius-small: 2px; - --td-radius-default: 3px; - --td-radius-medium: 6px; - --td-radius-large: 9px; - --td-radius-extraLarge: 12px; - --td-radius-round: 999px; - --td-radius-circle: 50%; -} diff --git a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_size.scss b/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_size.scss deleted file mode 100644 index 0246566c..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/_styles/theme/_size.scss +++ /dev/null @@ -1,19 +0,0 @@ -// size/margin/padding相关 token -:root { - --td-size-2: 2px; - --td-size-4: 4px; - --td-size-6: 6px; - --td-size-8: 8px; - --td-size-12: 12px; - --td-size-16: 16px; - --td-size-20: 20px; - --td-size-24: 24px; - --td-size-28: 28px; - --td-size-32: 32px; - --td-size-36: 36px; - --td-size-40: 40px; - --td-size-48: 48px; - --td-size-56: 56px; - --td-size-64: 64px; - --td-size-72: 72px; -} 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 92d4099b..a83dfd71 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 @@ -6,6 +6,9 @@ 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 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' 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 cf5ba8e3..45d54ab6 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,3 +13,6 @@ @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/popconfirm/index.tsx b/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx index d2167942..b06235e2 100644 --- a/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/popconfirm/index.tsx @@ -1,5 +1,5 @@ import type { PropType, VNode } from 'vue' -import { defineComponent, ref, watch } from 'vue' +import { defineComponent, ref } from 'vue' import { getPrefixCls } from '../_utils/getPrefixCls' import Button from '../button/index' import Icon from '../icon/index' 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 new file mode 100644 index 00000000..098a9a25 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/radio/index.scss @@ -0,0 +1,82 @@ +@use '../_styles/_variables.scss' as *; + +$radio-prefix: '#{$prefix}-radio'; + +.#{$radio-prefix} { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; + user-select: none; + + &-wrapper { + position: relative; + width: 16px; + height: 16px; + + input[type='radio'] { + position: absolute; + opacity: 0; + width: 100%; + height: 100%; + margin: 0; + cursor: pointer; + z-index: 2; + } + + & .#{$radio-prefix}-circle { + position: absolute; + top: 0; + left: 0; + width: 16px; + height: 16px; + border: 1px solid #d9d9d9; + border-radius: 50%; + background-color: #fff; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + } + + // 勾选圆点 + input[type='radio']:checked + .#{$radio-prefix}-circle::after { + content: ''; + width: 8px; + height: 8px; + background-color: #1890ff; + border-radius: 50%; + display: block; + animation: scaleIn 0.2s ease forwards; + box-shadow: 0 0 6px #1890ff33; // #33 = 20% 透明度 + } + } + + &-content { + display: flex; + flex-direction: column; + } + + &-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &-clicked { + .#{$radio-prefix}-circle { + transform: scale(0.9); + transition: transform 0.15s ease; + } + } +} + +@keyframes scaleIn { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} diff --git a/code/base-project/base-vue/packages/ao-design/src/radio/index.tsx b/code/base-project/base-vue/packages/ao-design/src/radio/index.tsx new file mode 100644 index 00000000..be1246e4 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/radio/index.tsx @@ -0,0 +1,71 @@ +import { computed, defineComponent, ref } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' + +export default defineComponent({ + name: 'AnimatedRadio', + props: { + modelValue: { + type: [String, Number, Boolean], + default: null, + }, + value: { + type: [String, Number, Boolean], + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, + class: { + type: [String, Array, Object], + default: '', + }, + style: { + type: Object, + default: () => ({}), + }, + }, + emits: ['update:modelValue'], + setup(props, { slots, emit }) { + const prefixCls = getPrefixCls('radio') + const isClicked = ref(false) + + const checked = computed(() => props.modelValue === props.value) + + const handleChange = () => { + if (!props.disabled) { + emit('update:modelValue', props.value) + // 播放点击动画 + isClicked.value = true + setTimeout(() => (isClicked.value = false), 300) + } + } + + return () => ( + + ) + }, +}) diff --git a/code/base-project/base-vue/packages/ao-design/src/separator/demo.vue b/code/base-project/base-vue/packages/ao-design/src/separator/demo.vue new file mode 100644 index 00000000..c9c1a1bf --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/separator/demo.vue @@ -0,0 +1,55 @@ + + + diff --git a/code/base-project/base-vue/packages/ao-design/src/separator/index.scss b/code/base-project/base-vue/packages/ao-design/src/separator/index.scss new file mode 100644 index 00000000..e51354b4 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/separator/index.scss @@ -0,0 +1,78 @@ +@use '../_styles/_variables.scss' as *; +@use 'sass:color'; + +$separator-prefix: '#{$prefix}-separator'; + +.#{$separator-prefix} { + margin: 0; + padding: 0; + list-style: none; + font-size: 14px; + font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: $text-color-base; + box-sizing: border-box; + &.#{$separator-prefix}-layout-horizontal { + width: 100%; + display: flex; + border-top: 1px solid $separator-color; + margin: 6px 0; + align-self: center; + } + + &.#{$separator-prefix}-layout-vertical { + border-top: 0; + display: inline-block; + height: 0.9em; + vertical-align: middle; + border-left: 1px solid $separator-color; + margin: 0 4px; + } + + &.#{$separator-prefix}-dashed { + // 水平虚线 + &.#{$separator-prefix}-layout-horizontal { + border-top-style: dashed; + } + + // 垂直虚线 + &.#{$separator-prefix}-layout-vertical { + border-left-style: dashed; + } + } + + &.#{$separator-prefix}-text { + display: flex; + white-space: nowrap; + align-items: center; + border-top: 0; + &::before, + &::after { + content: ''; + width: 50%; + border-top: 1px solid $separator-color; + } + } + + & .#{$separator-prefix}-text-inner:not(:empty) { + padding: 0 1em; + display: inline-block; + } + + &.#{$separator-prefix}-text-left { + &::before { + width: 10%; + } + &::after { + width: 90%; + } + } + + &.#{$separator-prefix}-text-right { + &::before { + width: 90%; + } + &::after { + width: 10%; + } + } +} diff --git a/code/base-project/base-vue/packages/ao-design/src/separator/index.tsx b/code/base-project/base-vue/packages/ao-design/src/separator/index.tsx new file mode 100644 index 00000000..0265882c --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/separator/index.tsx @@ -0,0 +1,86 @@ +import type { PropType, VNode } from 'vue' +import { defineComponent } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' + +export default defineComponent({ + name: 'Separator', + props: { + layout: { + type: String as PropType<'horizontal' | 'vertical'>, + default: 'horizontal', + }, + dashed: { + type: Boolean, + default: false, + }, + align: { + type: String as PropType<'left' | 'right' | 'center'>, + default: 'center', + }, + margin: { + type: [Number, Array] as any, + default: null, + }, + content: { + type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), + default: '', + }, + class: { + type: [String, Array, Object], + default: '', + }, + style: { + type: Object, + default: () => ({}), + }, + }, + emits: [], + setup(props, { slots }) { + const prefixCls = getPrefixCls('separator') + + // 根据 props.margin 生成样式 + const marginStyle = () => { + if (props.margin == null) + return {} + if (typeof props.margin === 'number') { + return props.layout === 'horizontal' + ? { margin: `${props.margin}px 0` } + : { margin: `0 ${props.margin}px` } + } + if (Array.isArray(props.margin)) { + return { margin: props.margin.map(v => `${v}px`).join(' ') } + } + return {} + } + + // 渲染文字或默认插槽 + const renderContent = (content: string | VNode | (() => VNode)) => { + if (slots.default) + return slots.default() + if (typeof content === 'function') + return content() + if (typeof content === 'string') + return {content} + return content + } + + const isHorizontal = props.layout === 'horizontal' + const hasContent = !!(slots.default?.().length || props.content) + + return () => ( +
+ {isHorizontal ? {renderContent(props.content)} : null} +
+ ) + }, +}) diff --git a/code/base-project/base-vue/packages/ao-design/src/space/demo.vue b/code/base-project/base-vue/packages/ao-design/src/space/demo.vue new file mode 100644 index 00000000..355320ad --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/space/demo.vue @@ -0,0 +1,52 @@ + + + diff --git a/code/base-project/base-vue/packages/ao-design/src/space/index.scss b/code/base-project/base-vue/packages/ao-design/src/space/index.scss new file mode 100644 index 00000000..db4eef53 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/space/index.scss @@ -0,0 +1,33 @@ +@use '../_styles/_variables.scss' as *; +@use 'sass:color'; + +$space-prefix: '#{$prefix}-space'; + +.#{$space-prefix} { + display: inline-flex; + + &.#{$space-prefix}-direction-horizontal { + flex-direction: row; + } + + &.#{$space-prefix}-direction-vertical { + flex-direction: column; + } + + &.#{$space-prefix}-align-start { + align-items: flex-start; + } + &.#{$space-prefix}-align-end { + align-items: flex-end; + } + &.#{$space-prefix}-align-center { + align-items: center; + } + &.#{$space-prefix}-align-baseline { + align-items: baseline; + } + + &.#{$space-prefix}-wrap { + flex-wrap: wrap; + } +} diff --git a/code/base-project/base-vue/packages/ao-design/src/space/index.tsx b/code/base-project/base-vue/packages/ao-design/src/space/index.tsx new file mode 100644 index 00000000..17194f0a --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/space/index.tsx @@ -0,0 +1,75 @@ +import type { PropType, VNode } from 'vue' +import { defineComponent } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' +import Separator from '../separator/index' + +const sizeMap = { + small: 8, + middle: 16, + large: 24, +} + +export default defineComponent({ + name: 'ASpace', + props: { + size: { + type: [String, Number, Array] as PropType<'small' | 'middle' | 'large' | number | [number, number]>, + default: 'middle', + }, + direction: { + type: String as () => 'horizontal' | 'vertical', + default: 'horizontal', + }, + align: { + type: String as () => 'start' | 'center' | 'end' | 'baseline', + default: 'center', + }, + wrap: { type: Boolean, default: false }, + separator: { type: Boolean, default: false }, + class: { type: [String, Array, Object], default: '' }, + style: { type: Object, default: () => ({}) }, + }, + setup(props, { slots }) { + const prefixCls = getPrefixCls('space') + + const getGap = (): [number, number] => { + if (Array.isArray(props.size)) + return props.size + if (typeof props.size === 'string' && sizeMap[props.size as keyof typeof sizeMap]) { + const val = sizeMap[props.size as keyof typeof sizeMap] + return [val, val] + } + return [props.size as number, props.size as number] + } + + return () => { + const [rowGap, colGap] = getGap() + const isHorizontal = props.direction === 'horizontal' + + const children = slots.default?.() || [] + const items: VNode[] = [] + children.forEach((child, index) => { + items.push(child) + if (props.separator && index < children.length - 1) { + items.push( + , + ) + } + }) + + return ( +
+ {items} +
+ ) + } + }, +}) 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 new file mode 100644 index 00000000..260a0dc6 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/tag-group/demo.vue @@ -0,0 +1,101 @@ + + + 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 909980ec..38590f07 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 @@ -1,113 +1,83 @@ import type { PropType, VNode } from 'vue' -import { cloneVNode, defineComponent, ref, watch } from 'vue' +import { cloneVNode, defineComponent } from 'vue' +import { useSelectable } from '../_hooks/useSelectable' import { getPrefixCls } from '../_utils/getPrefixCls' import Tag from '../tag/index' +interface TagOption { + label?: string + value: string | number + disabled?: boolean + [key: string]: any +} + export default defineComponent({ - name: 'TagGroup', + name: 'ATagGroup', props: { modelValue: { type: Array as PropType<(string | number)[]>, default: () => [] }, multiple: { type: Boolean, default: true }, - options: { - type: Array as PropType>, - default: () => [], - }, - 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', - }), - }, + 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 = ref<(string | number)[]>([...props.modelValue]) - - // 外部 v-model 更新时同步内部值 - watch( - () => props.modelValue, - (val) => { - selectedValues.value = [...val] + 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 updateValue = (value: string | number, checked: boolean) => { - let newValues = [...selectedValues.value] - - if (props.multiple) { - if (checked) { - if (!newValues.includes(value)) - newValues.push(value) - } - else { - newValues = newValues.filter(v => v !== value) - } - } - else { - // 单选:checked 为 true → 只保留当前值 - newValues = checked ? [value] : [] - } - - selectedValues.value = newValues - emit('update:modelValue', newValues) - emit('change', newValues) - } - - const renderTag = (value: string | number, label?: string, vnode?: VNode) => { - const checked = selectedValues.value.includes(value) + const renderTag = (option: TagOption, vnode?: VNode) => { + const val = option[props.valueField] + const checked = selectedValues.value.includes(val) const baseProps = { - 'key': value, - value, + 'key': val, + 'value': val, checked, 'checkable': true, 'checkedProps': props.checkedProps, 'uncheckedProps': props.uncheckedProps, - 'onUpdate:checked': (val: boolean) => updateValue(value, val), - } - - if (vnode) { - return cloneVNode(vnode, baseProps) - } - else { - return ( - - {label} - - ) + 'disabled': option.disabled, + 'onUpdate:checked': (valChecked: boolean) => updateValue(option, valChecked), } + return vnode ? cloneVNode(vnode, baseProps) : {option[props.labelField]} } - // slot 渲染 + const renderOptionsTags = () => props.options.map(opt => renderTag(opt)) + const renderSlotTags = () => { const slotNodes = slots.default?.() || [] return slotNodes.map((vnode: VNode) => { - if ((vnode.type as any)?.name === 'Tag') { - const value = (vnode.props as any)?.value - return renderTag(value, undefined, 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 }) } - // options 渲染 - const renderOptionTags = () => - props.options.map(opt => renderTag(opt.value, opt.label)) - - return () => ( -
- {props.options.length ? renderOptionTags() : renderSlotTags()} -
- ) + return () => { + const children = props.options.length ? renderOptionsTags() : renderSlotTags() + return
{children}
+ } }, }) 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 5ade7899..d4d74a44 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 @@ -4,6 +4,7 @@ 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 7f5b9f5c..9a241e1b 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 @@ -16,75 +16,57 @@ $tag-prefix: '#{$prefix}-tag'; border-radius: $border-color; font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; - &-size-large { + &.#{$tag-prefix}-size-large { padding: 0 8px; height: 24px; font-size: 13px; } - &-size-medium { + &.#{$tag-prefix}-size-medium { padding: 0 6px; height: 22px; font-size: 12px; } - &-size-small { + &.#{$tag-prefix}-size-small { padding: 0 4px; height: 20px; font-size: 12px; } - &-shape-round { + + &.#{$tag-prefix}-shape-round { border-radius: 12px; } - &-shape-mark { - border-radius: 0px 12px 12px 0; + &.#{$tag-prefix}-shape-mark { + border-radius: 0 12px 12px 0; } - &-icon { + + &.#{$tag-prefix}-icon { margin-right: 4px; } - &-close { + &.#{$tag-prefix}-close { margin-left: 4px; cursor: pointer; transition: filter 0.2s cubic-bezier(0.38, 0, 0.24, 1); color: currentColor; - filter: brightness(1.2); /* 比父色亮 20% */ + filter: brightness(1.2); &:hover { - filter: brightness(0.8); /* 比父色暗 20% */ + filter: brightness(0.8); } } - &-check { + &.#{$tag-prefix}-check { cursor: pointer; user-select: none; transition: background-color 0.2s cubic-bezier(0.38, 0, 0.24, 1); } - &-disabled { - cursor: default; + &.#{$tag-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; } - &-check.#{$tag-prefix}-disabled.#{$tag-prefix}-type-solid.#{$tag-prefix}-status-default { - background-color: $default-color-disabled; - color: $text-color-disabled; - } - - &-checked.#{$tag-prefix}-disabled.#{$tag-prefix}-type-solid.#{$tag-prefix}-status-primary { - background-color: $primary-color-disabled; - } - - &-checked.#{$tag-prefix}-disabled.#{$tag-prefix}-type-solid.#{$tag-prefix}-status-danger { - background-color: $danger-color-disabled; - } - - &-checked.#{$tag-prefix}-disabled.#{$tag-prefix}-type-solid.#{$tag-prefix}-status-warning { - background-color: $warning-color-disabled; - } - - &-checked.#{$tag-prefix}-disabled.#{$tag-prefix}-type-solid.#{$tag-prefix}-status-success { - background-color: $success-color-disabled; - } - - &-type-solid { + &.#{$tag-prefix}-type-solid { &.#{$tag-prefix}-status-default { color: $text-color-base; background-color: $default-color; @@ -107,7 +89,8 @@ $tag-prefix: '#{$prefix}-tag'; } } - &-type-light { + // light 类型 + &.#{$tag-prefix}-type-light { &.#{$tag-prefix}-status-default { color: $text-color-base; background-color: $default-color-light; @@ -130,35 +113,34 @@ $tag-prefix: '#{$prefix}-tag'; } } - &-type-outline { + // outline 类型 + &.#{$tag-prefix}-type-outline { + background-color: transparent; + &.#{$tag-prefix}-status-default { color: $text-color-base; - background-color: transparent; border-color: $default-color; } &.#{$tag-prefix}-status-primary { color: $primary-color; - background-color: transparent; border-color: $primary-color; } &.#{$tag-prefix}-status-success { color: $success-color; - background-color: transparent; border-color: $success-color; } &.#{$tag-prefix}-status-warning { color: $warning-color; - background-color: transparent; border-color: $warning-color; } &.#{$tag-prefix}-status-danger { color: $danger-color; - background-color: transparent; border-color: $danger-color; } } - &-type-outline-light { + // outline-light 类型 + &.#{$tag-prefix}-type-outline-light { &.#{$tag-prefix}-status-default { color: $text-color-base; background-color: $default-color-light; diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue index b38fa8e3..22a091cc 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,52 +1,10 @@ - - diff --git a/code/base-project/base-vue/src/components.d.ts b/code/base-project/base-vue/src/components.d.ts index 7b3a7e8b..9bcb48f7 100644 --- a/code/base-project/base-vue/src/components.d.ts +++ b/code/base-project/base-vue/src/components.d.ts @@ -18,6 +18,10 @@ declare module 'vue' { AMessage: typeof import('@ao/ao-design')['Message'] APopconfirm: typeof import('@ao/ao-design')['Popconfirm'] APopup: typeof import('@ao/ao-design')['Popup'] + ARadio: typeof import('@ao/ao-design')['Radio'] + ARdio: typeof import('@ao/ao-design')['Rdio'] + ASeparator: typeof import('@ao/ao-design')['Separator'] + ASpace: typeof import('@ao/ao-design')['Space'] ASpin: typeof import('@ao/ao-design')['Spin'] ATag: typeof import('@ao/ao-design')['Tag'] ATagGroup: typeof import('@ao/ao-design')['TagGroup'] diff --git a/code/base-project/base-vue/test.html b/code/base-project/base-vue/test.html index 5d97917b..784b7862 100644 --- a/code/base-project/base-vue/test.html +++ b/code/base-project/base-vue/test.html @@ -1,278 +1,64 @@ - - - - - - 12方向Tooltip - - - -
Shipper: CHANNEL INTERNATIONAL FREIGHT CO.,LTD 24 FLOOR, YINYI GLOBAL CENTER,B BLOCK,NO.42 YAOHANG STREET HAISHUDISTRICT,NINGBO,CHINA TEL:0086-574-87337359 FAX:0086-574-87135310TO:宁波坤洲国际物流有限公司 ATTN: TEL: FROM:宁波驰诺/裘珊珊 TEL:0574-87237893 QQ:1597608137 业务编号:CNNBYS230201 开航日期:2023-02-0400:00:00.0 船公司:现代商船 柜量:2X2OTKFCL运费条款:FREIGHTPREPAID 运输条款:CY-CY
Consignee: NSINTERNATIONAL(THAILAND)CO.LTD 75/49 RICHMOND BLDG.,15 FL SUKHUMVIT SOI 26 KLONGTOEY,BANGKOK1011O THAILAND ATTN:IMPORT@NSINTER.COM TEL: +66-2-665-6710 FAX:+66-2-665-6494
Notify Party: NS INTERNATIONAL (THAILAND) CO.,LTD 75/49 RICHMOND BLDG., 15 FL SUKHUMVIT SOI 26 KLONGTOEY,BANGKOK1011O THAILAND ATTN:IMPORT@NSINTER.COM TEL:+66-2-665-6710 FAX: +66-2-665-6494
起运港(PortOf Loading):中转港(Port of Dischargc):目的港(Final Destination):
NINGBO,CHINALEAM CHA BANG
标志和号码件数和包装种类货名毛重(公斤)体积(立方米)
SMAU9992165 SMAU99921702TANKSBULK LIQUID ARGON CLASS:2.2 UN:195144000.000KGS50.000CBM
- - - - +联系人:裘珊珊电话:0574-87237893QQ:1597608137传真:0574-87135310宁波市海曙区药行街42号银亿环球中心B座24楼