This commit is contained in:
oneao committed 2025-09-01 17:30:41 +08:00
1 parent 8bd73b3786
commit 554fe2945f
15 files changed
+531 -159

No files matched your search

@@ -1,17 +1,7 @@
$prefix: 'ao' !default;
// 主色系
$primary-color: #1677ff !default;
$primary-hover-color: #0958d9 !default;
// 功能色
$success-color: #52c41a !default;
$danger-color: #ff4d4f !default;
$warning-color: #faad14 !default;
$info-color: #13c2c2 !default;
// 中性色
$background-color: #f5f7fa !default;
$border-color: #d9d9d9 !default;
$text-color: #000000de !default;
$text-secondary-color: #595959 !default;
$default-color: #0064fa !default;
$primary-color: $default-color !default;
$danger-color: #f93920 !default;
$warning-color: #fc8800 !default;
$success-color: #22c55e !default;
@@ -1,29 +1,198 @@
@import '../_styles/theme/index.scss';
@use 'sass:color';
@use '../_styles/theme/index' as *;
// 按钮基础样式示例
.#{$prefix}-btn {
padding: 6px 16px;
border-radius: 4px;
$btn-prefix: #{$prefix}-btn;
$btn-hover-bgcolor: #f4f5f5;
$btn-active-bgcolor: #eceded;
/* ====== 通用颜色交互函数 ====== */
@function color-hover($color, $amount: 8%) {
$l: color.lightness($color);
@if $l > 50% {
@return color.adjust($color, $lightness: -$amount);
} @else {
@return color.adjust($color, $lightness: $amount);
}
}
@function color-active($color, $amount: 12%) {
$l: color.lightness($color);
@if $l > 50% {
@return color.adjust($color, $lightness: -$amount);
} @else {
@return color.adjust($color, $lightness: $amount);
}
}
.#{$btn-prefix} {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
box-sizing: border-box;
user-select: none;
border-radius: 3px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
&-default {
background: #fff;
border-color: $border-color;
color: $text-secondary-color;
&-block {
width: 100%;
}
&-primary {
background: $primary-color;
color: #fff;
&-icon-left {
margin-right: 4px;
display: inline-flex;
align-items: center;
}
&-icon-right {
margin-left: 4px;
display: inline-flex;
align-items: center;
}
&:hover {
background: $primary-hover-color;
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* ===== Solid 按钮 ===== */
&-solid {
@each $status,
$color
in (
normal: #d9d9d9,
primary: $primary-color,
danger: $danger-color,
warning: $warning-color,
success: $success-color
)
{
&.#{$btn-prefix}-status-#{$status} {
@if $status == normal {
background: #f4f5f5;
color: #000;
&:hover:enabled {
background: #eceded;
}
&:active:enabled {
background: #e4e4e5;
}
} @else {
background: $color;
color: #fff;
border: 1px solid transparent;
&:hover:enabled {
background: color-hover($color, 5%);
}
&:active:enabled {
background: color-active($color, -5%);
}
}
}
}
}
&-danger {
background: $danger-color;
color: #fff;
/* ===== Outline 按钮 ===== */
&-outline {
font-weight: 500;
background: transparent;
@each $status,
$color
in (
normal: $primary-color,
primary: $primary-color,
danger: $danger-color,
warning: $warning-color,
success: $success-color
)
{
&.#{$btn-prefix}-status-#{$status} {
color: if($status == normal, #000, $color);
border-color: if($status == normal, #d9d9d9, $color);
&:hover:enabled {
background-color: $btn-hover-bgcolor;
}
&:active:enabled {
background-color: $btn-active-bgcolor;
}
}
}
}
/* ===== Dashed 按钮 ===== */
&-dashed {
font-weight: 500;
background: transparent;
border: 1px dashed currentColor;
@each $status,
$color
in (
normal: #000,
primary: $primary-color,
danger: $danger-color,
warning: $warning-color,
success: $success-color
)
{
&.#{$btn-prefix}-status-#{$status} {
color: $color;
border-color: if($status == normal, #d9d9d9, $color);
&:hover:enabled {
background-color: $btn-hover-bgcolor;
}
&:active:enabled {
background-color: $btn-active-bgcolor;
}
}
}
}
/* ===== Text 按钮 ===== */
&-text {
font-weight: 500;
background: transparent;
border: none;
@each $status,
$color
in (
normal: #000,
primary: $primary-color,
danger: $danger-color,
warning: $warning-color,
success: $success-color
)
{
&.#{$btn-prefix}-status-#{$status} {
color: $color;
&:hover:enabled {
background-color: $btn-hover-bgcolor;
}
&:active:enabled {
background-color: $btn-active-bgcolor;
}
}
}
}
/* ===== 尺寸 ===== */
&-small {
height: 24px;
padding: 0 12px;
font-size: 12px;
}
&-medium {
height: 28px;
padding: 0 14px;
font-size: 14px;
}
&-large {
height: 32px;
padding: 0 16px;
font-size: 14px;
}
}
@@ -1,14 +1,70 @@
import { computed, defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Icon from '../icon'
import './index.scss'
export default defineComponent({
name: 'AButton',
name: 'Button',
props: {
type: { type: String as () => 'primary' | 'danger' | 'default', default: 'default' },
type: {
type: String as () => 'solid' | 'outline' | 'text' | 'dashed',
default: 'solid',
},
status: {
type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'normal',
default: 'normal',
},
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
icon: { type: String, default: '' },
iconPosition: { type: String as () => 'left' | 'right', default: 'left' },
size: { type: String as () => 'small' | 'medium' | 'large', default: 'medium' },
block: { type: Boolean, default: false },
},
setup(props, { slots }) {
const prefixCls = getPrefixCls('btn')
const cls = computed(() => [prefixCls, `${prefixCls}-${props.type}`])
return () => <button class={cls.value}>{slots.default?.()}</button>
const cls = computed(() => [
prefixCls,
`${prefixCls}-${props.type}`,
`${prefixCls}-status-${props.status}`,
`${prefixCls}-${props.size}`,
props.block && `${prefixCls}-block`,
])
const iconSize = computed(() => {
const map = { small: 14, medium: 16, large: 16 }
return map[props.size]
})
return () => (
<button class={cls.value} disabled={props.disabled || props.loading}>
{/* 左侧图标 */}
{props.iconPosition === 'left' && (props.loading || props.icon) && (
<Icon
icon={props.loading ? 'mingcute:loading-line' : props.icon}
class={`${prefixCls}-icon-left`}
spin={props.loading}
width={iconSize.value}
height={iconSize.value}
/>
)}
{/* 插槽文本 */}
<span>
{slots.default?.()}
</span>
{/* 右侧图标 */}
{props.iconPosition === 'right' && props.icon && (
<Icon
icon={props.icon}
class={`${prefixCls}-icon-right`}
width={iconSize.value}
height={iconSize.value}
/>
)}
</button>
)
},
})
@@ -2,3 +2,4 @@ export { default as ConfigProvider } from './_config'
export { default as Button } from './button'
export { default as Icon } from './icon'
export { default as Input } from './input'
export { default as Spin } from './spin'
@@ -0,0 +1,12 @@
@use '../_styles/theme/index' as *;
.#{$prefix}-icon-spin {
animation: spin 1s linear infinite;
--rotate-angle: 360deg;
}
@keyframes spin {
to {
transform: rotate(var(--rotate-angle));
}
}
@@ -1,22 +1,32 @@
import type { PropType } from 'vue'
import { Icon } from '@iconify/vue'
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import './index.scss'
export default defineComponent({
name: 'AIcon',
name: 'Icon',
props: {
icon: { type: String as PropType<string>, required: true },
width: { type: String, default: '24px' },
height: { type: String, default: '24px' },
color: { type: String, default: 'currentColor' }, // 默认颜色使用 currentColor,支持继承父元素的颜色
width: { type: Number, default: 16 },
height: { type: Number, default: 16 },
color: { type: String, default: 'currentColor' },
spin: { type: Boolean, default: false },
rotate: { type: Number, default: 360 },
},
setup(props) {
const prefixCls = getPrefixCls('icon')
return () => (
<Icon
icon={props.icon}
width={props.width}
height={props.height}
style={{ color: props.color, verticalAlign: 'middle' }}
style={{
'color': props.color,
'verticalAlign': 'middle',
'--rotate-angle': `${props.rotate}deg`,
}}
class={props.spin ? `${prefixCls}-spin` : ''}
/>
)
},
@@ -1 +1,2 @@
@import './button/index.scss';
@use './button/index.scss' as *;
@use './input/index.scss' as *;
@@ -0,0 +1,7 @@
@use '../_styles/theme/index' as *;
$input-prefix: #{$prefix}-input;
.#{$input-prefix} {
border-color: #d9d9d9;
}
@@ -1,63 +1,23 @@
import type { PropType } from 'vue'
import { computed, defineComponent, ref, toRefs } from 'vue'
import { computed, defineComponent, ref } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
export default defineComponent({
name: 'Input',
props: {
modelValue: { type: String, default: '' },
placeholder: { type: String, default: '' },
type: { type: String as PropType<'text' | 'password' | 'number'>, default: 'text' },
disabled: { type: Boolean, default: false },
clearable: { type: Boolean, default: false },
},
emits: ['update:modelValue', 'change', 'focus', 'blur'],
setup(props, { emit }) {
name: 'AInput',
setup() {
const inputRef = ref<HTMLInputElement | null>(null)
const handleInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value
emit('update:modelValue', value)
emit('change', value)
}
const handleClear = () => {
emit('update:modelValue', '')
inputRef.value?.focus()
emit('change', '')
}
const showClear = computed(() => props.clearable && !!props.modelValue && !props.disabled)
// 使用统一前缀
const prefixCls = getPrefixCls('input')
const cls = computed(() => [prefixCls])
return () => (
<div class="a-input-wrapper" style={{ position: 'relative', display: 'inline-block' }}>
<div class={`${prefixCls}-wrapper`} style={{ display: 'inline-block' }}>
<input
ref={inputRef}
class="a-input"
type={props.type}
placeholder={props.placeholder}
value={props.modelValue}
disabled={props.disabled}
onInput={handleInput}
onFocus={e => emit('focus', e)}
onBlur={e => emit('blur', e)}
style={{ paddingRight: showClear.value ? '24px' : '8px' }}
class={cls.value}
type="text"
placeholder="请输入"
/>
{showClear.value && (
<span
class="a-input-clear"
onClick={handleClear}
style={{
position: 'absolute',
right: '4px',
top: '50%',
transform: 'translateY(-50%)',
cursor: 'pointer',
fontSize: '12px',
}}
>
×
</span>
)}
</div>
)
},
@@ -0,0 +1,35 @@
@use '../_styles/theme/index' as *;
$spin-prefix: #{$prefix}-spin;
.#{$spin-prefix}-wrapper {
position: relative;
display: inline-block;
}
.#{$spin-prefix} {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
&-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: $primary-color;
}
&-tip {
margin-top: 4px;
font-size: 12px;
color: #000;
}
&.rounded {
border-radius: inherit;
}
}
@@ -0,0 +1,52 @@
import type { PropType } from 'vue'
import { defineComponent } from 'vue'
import { getPrefixCls } from '../_utils/getPrefixCls'
import Icon from '../icon'
import './index.scss'
export default defineComponent({
name: 'Spin',
props: {
loading: { type: Boolean, default: false },
opacity: { type: Number as PropType<number>, default: 0.6 },
rounded: { type: Boolean, default: true },
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: '' },
},
setup(props, { slots }) {
const prefixCls = getPrefixCls('spin')
return () => {
if (!props.loading && !slots.default)
return null
return (
<div class={`${prefixCls}-wrapper`}>
{slots.default?.()}
{props.loading && (
<div
class={`${prefixCls}`}
style={{
background: slots.default ? `rgba(255,255,255,${props.opacity})` : 'transparent',
borderRadius: props.rounded ? '4px' : 0,
}}
>
<div class={`${prefixCls}-content`}>
<Icon
icon={props.icon}
width={props.size}
height={props.size}
spin
rotate={props.rotate}
/>
{props.tip && <span class={`${prefixCls}-tip`}>{props.tip}</span>}
</div>
</div>
)}
</div>
)
}
},
})