Button.vue
3.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<script setup>
/**
* Button —— 通用按钮,与业务无关,只认 props/slot。
*
* @typedef {'primary'|'secondary'|'ghost'|'danger'} ButtonVariant
* @typedef {'sm'|'md'|'lg'} ButtonSize
* @typedef {'button'|'submit'|'reset'} ButtonType
*/
const props = defineProps({
/** @type {import('vue').PropType<ButtonVariant>} 视觉变体。 */
variant: { type: String, default: 'primary' },
/** @type {import('vue').PropType<ButtonSize>} 尺寸。 */
size: { type: String, default: 'md' },
/** 禁用态:阻断点击且降透明度。 */
disabled: { type: Boolean, default: false },
/** 加载态:显示 spinner、阻断点击。 */
loading: { type: Boolean, default: false },
/** @type {import('vue').PropType<ButtonType>} 原生按钮类型。 */
type: { type: String, default: 'button' },
})
const emit = defineEmits(['click'])
/**
* 转发原生点击;disabled / loading 时吞掉事件。
* @param {MouseEvent} e
*/
function onClick(e) {
if (props.disabled || props.loading) return
emit('click', e)
}
</script>
<template>
<button
class="drama-btn"
:class="[`drama-btn--${variant}`, `drama-btn--${size}`, { 'is-loading': loading }]"
:disabled="disabled || loading"
:aria-busy="loading ? 'true' : 'false'"
:type="type"
@click="onClick"
>
<span
v-if="loading"
class="drama-btn__spinner"
aria-hidden="true"
/>
<span class="drama-btn__label"><slot /></span>
</button>
</template>
<style scoped>
.drama-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--drama-space-sm);
border: 1px solid transparent;
border-radius: var(--drama-radius-md);
font-family: var(--drama-font-family-body);
font-weight: var(--drama-font-weight-medium);
cursor: pointer;
transition: background-color 0.15s ease, opacity 0.15s ease;
}
.drama-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
/* min-height 取 control token:按钮和同尺寸的 Input / Select 等高,横排在一起底边成一条线。
仍留纵向 padding —— 文案折行时不至于贴边。 */
.drama-btn--sm {
min-height: var(--drama-control-height-sm);
padding: var(--drama-space-xs) var(--drama-space-md);
font-size: var(--drama-font-size-sm);
}
.drama-btn--md {
min-height: var(--drama-control-height-md);
padding: var(--drama-space-sm) var(--drama-space-lg);
font-size: var(--drama-font-size-md);
}
.drama-btn--lg {
min-height: var(--drama-control-height-lg);
padding: var(--drama-space-md) var(--drama-space-xl);
font-size: var(--drama-font-size-lg);
}
.drama-btn--primary {
background-color: var(--drama-color-primary);
color: var(--drama-color-on-primary);
}
.drama-btn--primary:hover:not(:disabled) {
background-color: var(--drama-color-primary-hover);
}
.drama-btn--secondary {
background-color: var(--drama-color-surface-alt);
color: var(--drama-color-text);
border-color: var(--drama-color-border);
}
.drama-btn--ghost {
background-color: transparent;
color: var(--drama-color-text);
}
.drama-btn--ghost:hover:not(:disabled) {
background-color: var(--drama-color-surface-alt);
}
.drama-btn--danger {
background-color: var(--drama-color-danger);
color: var(--drama-color-on-primary);
}
.drama-btn--danger:hover:not(:disabled) {
background-color: var(--drama-color-danger-hover);
}
.drama-btn__spinner {
width: var(--drama-space-lg);
height: var(--drama-space-lg);
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: var(--drama-radius-full);
animation: drama-btn-spin 0.6s linear infinite;
}
.drama-btn__label {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--drama-space-sm);
line-height: 1;
}
@keyframes drama-btn-spin {
to {
transform: rotate(360deg);
}
}
</style>