This commit is contained in:
oneao committed 2025-09-17 17:31:00 +08:00
1 parent 4d0d2f875e
commit 27263365ad
13 files changed
+265 -21

No files matched your search

@@ -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);
@@ -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;
@@ -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;
@@ -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 {
@@ -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'
@@ -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;
@@ -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<boolean>>,
},
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 = () => {
@@ -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;
@@ -12,11 +12,22 @@ export default defineComponent({
icon: { type: String as PropType<string>, default: 'mingcute:loading-line' },
size: { type: Number as PropType<number>, default: 16 },
rotate: { type: Number as PropType<number>, default: 360 },
tip: { type: String as PropType<string>, 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,
}}
>
<div class={`${prefixCls}-content`}>
@@ -37,8 +47,9 @@ export default defineComponent({
icon={props.icon}
size={props.size}
spin
class={`${prefixCls}-icon`}
/>
{props.tip && <span class={`${prefixCls}-tip`}>{props.tip}</span>}
{renderTip() && <span class={`${prefixCls}-tip`}>{renderTip()}</span>}
</div>
</div>
)}
@@ -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;
}
}
@@ -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<string | number | boolean>,
default: false,
},
checkedValue: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
default: true,
},
uncheckedValue: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
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 () => (
<button
type="button"
role="switch"
disabled={props.disabled || props.loading}
class={[
prefixCls,
props.class,
{
[`${prefixCls}-checked`]: isChecked.value,
[`${prefixCls}-disabled`]: props.disabled,
[`${prefixCls}-loading`]: props.loading,
},
]}
onClick={toggle}
>
<span class={`${prefixCls}-handle`}>
{props.loading && <Spin loading size={12} />}
</span>
<span class={`${prefixCls}-text-holder`}>{displayContent.value}</span>
<span class={`${prefixCls}-text`}>{displayContent.value}</span>
</button>
)
},
})
+15 -4
View File
@@ -1,13 +1,24 @@
<script setup lang="ts">
import { ref } from 'vue'
const value = ref('')
</script>
<template>
<div>
content
<div class="main">
<ASwitch v-model="value" loading checked-value="1" unchecked-value="2" checked-content="1" unchecked-content="2" />
{{ value }}
</div>
</template>
<style lang="scss" scoped>
</style>
.main {
width: 100%;
height: 800px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 16px;
}
</style>
+1
View File
@@ -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']