From 27263365ad6cc0fb380bbe000848a8c8a931162d Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Wed, 17 Sep 2025 17:31:00 +0800 Subject: [PATCH] u --- .../ao-design/src/_styles/_variables.scss | 5 + .../ao-design/src/_styles/theme/_dark.scss | 4 + .../ao-design/src/_styles/theme/_light.scss | 5 +- .../ao-design/src/checkbox/index.scss | 5 + .../packages/ao-design/src/components.ts | 1 + .../packages/ao-design/src/index.scss | 2 + .../ao-design/src/popconfirm/index.tsx | 24 ++-- .../packages/ao-design/src/spin/index.scss | 4 + .../packages/ao-design/src/spin/index.tsx | 17 ++- .../packages/ao-design/src/switch/index.scss | 87 ++++++++++++++ .../packages/ao-design/src/switch/index.tsx | 112 ++++++++++++++++++ code/base-project/base-vue/src/App.vue | 19 ++- .../base-project/base-vue/src/components.d.ts | 1 + 13 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 code/base-project/base-vue/packages/ao-design/src/switch/index.scss create mode 100644 code/base-project/base-vue/packages/ao-design/src/switch/index.tsx 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 e9fa641a..a0e1bfb3 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 @@ -99,6 +99,9 @@ $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 等容器的外部边框) +$bg-color-secondarycomponent: var(--ao-bg-color-secondarycomponent); +$bg-color-secondarycomponent-hover: var(--ao-bg-color-secondarycomponent-hover); + // shadow // 基础/下层 投影 hover 使用的组件包括:表格 / $shadow-surface: var(--ao-shadow-surface); @@ -106,6 +109,8 @@ $shadow-surface: var(--ao-shadow-surface); $shadow-popup: var(--ao-shadow-popup); // 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知 $shadow-overlay: var(--ao-shadow-overlay); +// switch 投影 +$shadow-switch: var(--ao-shadow-switch); // 内投影 $shadow-inset-top: var(--ao-shadow-inset-top); 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 f4c9a9ef..741eb6f7 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 @@ -76,6 +76,8 @@ --ao-bg-color-component: #393939; --ao-border-color-component: #5e5e5e; + --ao-bg-color-secondarycomponent: #4b4b4b; + --ao-bg-color-secondarycomponent-hover: #5e5e5e; // shadow阴影 // 基础/下层 投影 hover 使用的组件包括:表格 / @@ -84,6 +86,8 @@ --ao-shadow-popup: 0 0 4px rgba(0, 0, 0, 0.04), 0 0 8px rgba(0, 0, 0, 0.03); // 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知 --ao-shadow-overlay: inset 0 0 0 1px hsla(0, 0%, 100%, 0.1), 0 4px 14px rgba(0, 0, 0, 0.25); + // 开关投影 + --ao-shadow-switch: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12); // 内投影 --ao-shadow-inset-top: inset 0 0.5px 0 #5e5e5e; --ao-shadow-inset-right: inset 0.5px 0 0 #5e5e5e; 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 959f0911..f428449a 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 @@ -77,15 +77,18 @@ --ao-bg-color-component: #e8e8e8; --ao-border-color-component: #ddd; + --ao-bg-color-secondarycomponent: #ddd; + --ao-bg-color-secondarycomponent-hover: #c6c6c6; // shadow阴影 // 基础/下层 投影 hover 使用的组件包括:表格 / --ao-shadow-surface: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%); // 中层投影 下拉 使用的组件包括:下拉菜单 / 气泡确认框 / 选择器 / --ao-shadow-popup: 0 0 4px rgba(0, 0, 0, 0.04), 0 0 8px rgba(0, 0, 0, 0.03); - // 上层投影(警示/弹窗)使用的组件包括:全局提示 / 消息通知 --ao-shadow-overlay: 0 0 1px rgba(0, 0, 0, 0.3), 0 4px 14px rgba(0, 0, 0, 0.1); + // 开关投影 + --ao-shadow-switch: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12); --ao-shadow-inset-top: inset 0 0.5px 0 #dcdcdc; --ao-shadow-inset-right: inset 0.5px 0 0 #dcdcdc; --ao-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc; 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 index b35079d3..b03f7dd2 100644 --- 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 @@ -12,6 +12,11 @@ $checkbox-prefix: '#{$prefix}-checkbox'; cursor: pointer; user-select: none; + &.#{$checkbox-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + &.#{$checkbox-prefix}-checked { .#{$checkbox-prefix}-main { .#{$checkbox-prefix}-inner { 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 b2224227..ae7f8470 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 @@ -13,6 +13,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 Switch } from './switch' export { default as Tag } from './tag/tag' export { default as TagGroup } from './tag/tag-group' export { default as Textarea } from './textarea' 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 3a1f65b3..55978e97 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 @@ -17,6 +17,8 @@ @use './separator/index.scss' as *; @use './radio/index.scss' as *; @use './checkbox/index.scss' as *; +@use './switch/index.scss' as *; +@use './spin/index.scss' as *; [class*='#{variables.$prefix}-'] { -webkit-box-sizing: border-box; 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 b06235e2..00bba24e 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 @@ -20,9 +20,8 @@ export default defineComponent({ type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), required: false, }, - asyncConfirm: { - type: Boolean, - default: false, // 默认同步关闭,不显示 loading + beforeConfirm: { + type: Function as PropType<() => boolean | Promise>, }, confirmBtn: { type: null, @@ -68,21 +67,20 @@ export default defineComponent({ return val } - const handleConfirm = () => { - if (props.asyncConfirm) { - confirmLoading.value = true + const handleConfirm = async () => { + confirmLoading.value = true + let allow = true - emit('confirm', () => { // resolve - popVisible.value = false - confirmLoading.value = false - }, () => { // reject - confirmLoading.value = false - }) + if (props.beforeConfirm) { + allow = await Promise.resolve(props.beforeConfirm()) } - else { + + if (allow) { popVisible.value = false emit('confirm') } + + confirmLoading.value = false } const handleCancel = () => { diff --git a/code/base-project/base-vue/packages/ao-design/src/spin/index.scss b/code/base-project/base-vue/packages/ao-design/src/spin/index.scss index bb4f1a85..745202fb 100644 --- a/code/base-project/base-vue/packages/ao-design/src/spin/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/spin/index.scss @@ -5,6 +5,7 @@ $spin-prefix: #{$prefix}-spin; .#{$spin-prefix}-wrapper { position: relative; display: inline-block; + width: 100%; } .#{$spin-prefix} { @@ -23,6 +24,9 @@ $spin-prefix: #{$prefix}-spin; color: $text-color-base; } + &-icon { + color: $primary-color; + } &-tip { margin-top: 4px; font-size: 12px; diff --git a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx index eaafbdb5..00dc4281 100644 --- a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx @@ -12,11 +12,22 @@ export default defineComponent({ icon: { type: String as PropType, default: 'mingcute:loading-line' }, size: { type: Number as PropType, default: 16 }, rotate: { type: Number as PropType, default: 360 }, - tip: { type: String as PropType, default: '' }, + tip: { + type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), + default: '', + }, }, setup(props, { slots }) { const prefixCls = getPrefixCls('spin') + const renderTip = () => { + if (slots.tip) + return slots.tip() + if (typeof props.tip === 'function') + return props.tip() + return props.tip + } + return () => { if (!props.loading && !slots.default) return null @@ -29,7 +40,6 @@ export default defineComponent({ class={`${prefixCls}`} style={{ background: slots.default ? `rgba(255,255,255,${props.opacity})` : 'transparent', - borderRadius: props.rounded ? '4px' : 0, }} >
@@ -37,8 +47,9 @@ export default defineComponent({ icon={props.icon} size={props.size} spin + class={`${prefixCls}-icon`} /> - {props.tip && {props.tip}} + {renderTip() && {renderTip()}}
)} diff --git a/code/base-project/base-vue/packages/ao-design/src/switch/index.scss b/code/base-project/base-vue/packages/ao-design/src/switch/index.scss new file mode 100644 index 00000000..202cbbb4 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/switch/index.scss @@ -0,0 +1,87 @@ +@use '../_styles/_variables.scss' as *; +@use 'sass:color'; + +$switch-prefix: '#{$prefix}-switch'; + +.#{$switch-prefix} { + position: relative; + box-sizing: border-box; + min-width: 44px; + height: 22px; + padding: 0; + overflow: hidden; + line-height: 22px; + vertical-align: middle; + background-color: $bg-color-secondarycomponent; + border: none; + border-radius: 12px; + outline: none; + cursor: pointer; + transition: background-color 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); + + // 普通 hover,只在非 disabled 下生效 + &:not(.#{$switch-prefix}-disabled):not(.#{$switch-prefix}-loading):hover { + background-color: $bg-color-secondarycomponent-hover; + } + + // checked 状态 hover,只在非 disabled 下生效 + &.#{$switch-prefix}-checked:not(.#{$switch-prefix}-disabled):not(.#{$switch-prefix}-loading):hover { + background-color: $primary-color-hover; + } + + &.#{$switch-prefix}-checked { + background-color: $primary-color; + + .#{$switch-prefix}-handle { + left: calc(100% - 4px - 14px); // 100% - 右边距 - handle宽度 + } + + .#{$switch-prefix}-text { + right: auto; + left: calc(4px + 4px); // left + left + } + .#{$switch-prefix}-text-holder { + margin: 0 24px 0 8px; + } + } + + &.#{$switch-prefix}-loading { + cursor: not-allowed; + } + + &.#{$switch-prefix}-disabled { + cursor: not-allowed; + opacity: 0.5; + } + + .#{$switch-prefix}-handle { + position: absolute; + top: 50%; + left: 4px; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + font-size: 12px; + background-color: #fff; + border-radius: 50%; + transition: all 0.2s cubic-bezier(0.34, 0.69, 0.1, 1); + box-shadow: $shadow-switch; + } + + .#{$switch-prefix}-text-holder { + margin: 0 8px 0 24px; // 8 = 4 + 4 22 = 14 + 4 + 4 + 2 + font-size: 12px; + opacity: 0; + } + + .#{$switch-prefix}-text { + position: absolute; + top: 0; + right: calc(4px + 4px); + color: #fff; + font-size: 12px; + } +} diff --git a/code/base-project/base-vue/packages/ao-design/src/switch/index.tsx b/code/base-project/base-vue/packages/ao-design/src/switch/index.tsx new file mode 100644 index 00000000..3a8d31ea --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/switch/index.tsx @@ -0,0 +1,112 @@ +import type { PropType, VNode } from 'vue' +import { computed, defineComponent, ref, watch } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' +import Icon from '../icon/index' +import Spin from '../spin/index' + +export default defineComponent({ + name: 'Switch', + props: { + modelValue: { + type: [String, Number, Boolean] as PropType, + default: false, + }, + checkedValue: { + type: [String, Number, Boolean] as PropType, + default: true, + }, + uncheckedValue: { + type: [String, Number, Boolean] as PropType, + default: false, + }, + checkedContent: { + type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), + default: '', + }, + uncheckedContent: { + type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode), + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + loading: { + type: Boolean, + default: false, + }, + class: { + type: [String, Array, Object], + default: '', + }, + style: { + type: Object, + default: () => ({}), + }, + }, + emits: ['update:modelValue', 'change'], + setup(props, { emit, slots }) { + const prefixCls = getPrefixCls('switch') + + const _checked = ref(props.modelValue) + + watch( + () => props.modelValue, + (val) => { + _checked.value = val + }, + ) + const isChecked = computed(() => _checked.value === props.checkedValue) + + const toggle = (e: Event) => { + const newValue = isChecked.value ? props.uncheckedValue : props.checkedValue + _checked.value = newValue + + emit('update:modelValue', newValue) + emit('change', newValue, e) + } + + const displayContent = computed(() => { + // 优先使用插槽 + if (isChecked.value && slots.checkedContent) { + return slots.checkedContent() + } + else if (!isChecked.value && slots.uncheckedContent) { + return slots.uncheckedContent() + } + + // fallback 到 props + return isChecked.value + ? typeof props.checkedContent === 'function' + ? props.checkedContent() + : props.checkedContent + : typeof props.uncheckedContent === 'function' + ? props.uncheckedContent() + : props.uncheckedContent + }) + + return () => ( + + ) + }, +}) diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue index d886b548..8063e42d 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,13 +1,24 @@ \ No newline at end of file +.main { + width: 100%; + height: 800px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 16px; +} + diff --git a/code/base-project/base-vue/src/components.d.ts b/code/base-project/base-vue/src/components.d.ts index 6f6a2271..012e7f17 100644 --- a/code/base-project/base-vue/src/components.d.ts +++ b/code/base-project/base-vue/src/components.d.ts @@ -28,6 +28,7 @@ declare module 'vue' { ASeparator: typeof import('@ao/ao-design')['Separator'] ASpace: typeof import('@ao/ao-design')['Space'] ASpin: typeof import('@ao/ao-design')['Spin'] + ASwitch: typeof import('@ao/ao-design')['Switch'] ATag: typeof import('@ao/ao-design')['Tag'] ATagGroup: typeof import('@ao/ao-design')['TagGroup'] ATextarea: typeof import('@ao/ao-design')['Textarea']