Skip to content

Commit 4570d5b

Browse files
authored
feat: add VbenButtonGroup and VbenCheckButtonGroup with demo (#5591)
* 添加按钮组、选择按钮组以及相应的Demo
1 parent d49e3e8 commit 4570d5b

File tree

10 files changed

+494
-0
lines changed

10 files changed

+494
-0
lines changed

packages/@core/base/icons/src/lucide.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export {
1414
ChevronRight,
1515
ChevronsLeft,
1616
ChevronsRight,
17+
Circle,
18+
CircleCheckBig,
1719
CircleHelp,
1820
Copy,
1921
CornerDownLeft,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<script lang="ts" setup>
2+
import { cn } from '@vben-core/shared/utils';
3+
4+
defineOptions({ name: 'VbenButtonGroup' });
5+
6+
withDefaults(
7+
defineProps<{
8+
border?: boolean;
9+
gap?: number;
10+
size?: 'large' | 'middle' | 'small';
11+
}>(),
12+
{ border: false, gap: 0, size: 'middle' },
13+
);
14+
</script>
15+
<template>
16+
<div
17+
:class="
18+
cn(
19+
'vben-button-group rounded-md',
20+
`size-${size}`,
21+
gap ? 'with-gap' : 'no-gap',
22+
$attrs.class as string,
23+
)
24+
"
25+
:style="{ gap: gap ? `${gap}px` : '0px' }"
26+
>
27+
<slot></slot>
28+
</div>
29+
</template>
30+
31+
<style lang="scss" scoped>
32+
.vben-button-group {
33+
display: inline-flex;
34+
35+
&.size-large :deep(button) {
36+
height: 2.25rem;
37+
padding: 0.5rem 0.75rem;
38+
font-size: 0.875rem;
39+
line-height: 1.25rem;
40+
41+
.icon-wrapper {
42+
margin-right: 0.4rem;
43+
44+
svg {
45+
width: 1rem;
46+
height: 1rem;
47+
}
48+
}
49+
}
50+
51+
&.size-middle :deep(button) {
52+
height: 2rem;
53+
padding: 0.25rem 0.5rem;
54+
font-size: 0.75rem;
55+
line-height: 1rem;
56+
57+
.icon-wrapper {
58+
margin-right: 0.2rem;
59+
60+
svg {
61+
width: 0.75rem;
62+
height: 0.75rem;
63+
}
64+
}
65+
}
66+
67+
&.size-small :deep(button) {
68+
height: 1.75rem;
69+
padding: 0.2rem 0.4rem;
70+
font-size: 0.65rem;
71+
line-height: 0.75rem;
72+
73+
.icon-wrapper {
74+
margin-right: 0.1rem;
75+
76+
svg {
77+
width: 0.65rem;
78+
height: 0.65rem;
79+
}
80+
}
81+
}
82+
83+
&.no-gap > :deep(button):nth-of-type(1) {
84+
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
85+
}
86+
87+
&.no-gap > :deep(button):last-of-type {
88+
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
89+
}
90+
91+
&.no-gap {
92+
:deep(button + button) {
93+
border-left-width: 0;
94+
border-radius: 0;
95+
}
96+
}
97+
}
98+
</style>

packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,21 @@ export interface VbenButtonProps {
2222
size?: ButtonVariantSize;
2323
variant?: ButtonVariants;
2424
}
25+
26+
export type CustomRenderType = (() => Component | string) | string;
27+
28+
export type ValueType = boolean | number | string;
29+
30+
export interface VbenButtonGroupProps
31+
extends Pick<VbenButtonProps, 'disabled'> {
32+
beforeChange?: (
33+
value: ValueType,
34+
isChecked: boolean,
35+
) => boolean | PromiseLike<boolean | undefined> | undefined;
36+
btnClass?: any;
37+
gap?: number;
38+
multiple?: boolean;
39+
options?: { label: CustomRenderType; value: ValueType }[];
40+
showIcon?: boolean;
41+
size?: 'large' | 'middle' | 'small';
42+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<script lang="ts" setup>
2+
import type { Arrayable } from '@vueuse/core';
3+
4+
import type { ValueType, VbenButtonGroupProps } from './button';
5+
6+
import { computed, ref, watch } from 'vue';
7+
8+
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
9+
import { VbenRenderContent } from '@vben-core/shadcn-ui';
10+
import { cn, isFunction } from '@vben-core/shared/utils';
11+
12+
import { objectOmit } from '@vueuse/core';
13+
14+
import VbenButtonGroup from './button-group.vue';
15+
import Button from './button.vue';
16+
17+
const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
18+
gap: 0,
19+
multiple: false,
20+
showIcon: true,
21+
size: 'middle',
22+
});
23+
24+
const btnDefaultProps = computed(() => {
25+
return {
26+
...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
27+
class: cn(props.btnClass),
28+
};
29+
});
30+
const modelValue = defineModel<Arrayable<ValueType> | undefined>();
31+
32+
const innerValue = ref<Array<ValueType>>([]);
33+
const loadingValues = ref<Array<ValueType>>([]);
34+
watch(
35+
() => props.multiple,
36+
(val) => {
37+
if (val) {
38+
modelValue.value = innerValue.value;
39+
} else {
40+
modelValue.value =
41+
innerValue.value.length > 0 ? innerValue.value[0] : undefined;
42+
}
43+
},
44+
{ immediate: true },
45+
);
46+
47+
watch(
48+
() => modelValue.value,
49+
(val) => {
50+
if (Array.isArray(val)) {
51+
const arrVal = val.filter((v) => v !== undefined);
52+
if (arrVal.length > 0) {
53+
innerValue.value = props.multiple
54+
? [...arrVal]
55+
: [arrVal[0] as ValueType];
56+
} else {
57+
innerValue.value = [];
58+
}
59+
} else {
60+
innerValue.value = val === undefined ? [] : [val as ValueType];
61+
}
62+
},
63+
{ deep: true },
64+
);
65+
66+
async function onBtnClick(value: ValueType) {
67+
if (props.beforeChange && isFunction(props.beforeChange)) {
68+
try {
69+
loadingValues.value.push(value);
70+
const canChange = await props.beforeChange(
71+
value,
72+
!innerValue.value.includes(value),
73+
);
74+
if (canChange === false) {
75+
return;
76+
}
77+
} finally {
78+
loadingValues.value.splice(loadingValues.value.indexOf(value), 1);
79+
}
80+
}
81+
82+
if (props.multiple) {
83+
if (innerValue.value.includes(value)) {
84+
innerValue.value = innerValue.value.filter((item) => item !== value);
85+
} else {
86+
innerValue.value.push(value);
87+
}
88+
modelValue.value = innerValue.value;
89+
} else {
90+
innerValue.value = [value];
91+
modelValue.value = value;
92+
}
93+
}
94+
</script>
95+
<template>
96+
<VbenButtonGroup
97+
:size="props.size"
98+
:gap="props.gap"
99+
class="vben-check-button-group"
100+
>
101+
<Button
102+
v-for="(btn, index) in props.options"
103+
:key="index"
104+
:class="cn('border', props.btnClass)"
105+
:disabled="
106+
props.disabled ||
107+
loadingValues.includes(btn.value) ||
108+
(!props.multiple && loadingValues.length > 0)
109+
"
110+
v-bind="btnDefaultProps"
111+
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
112+
@click="onBtnClick(btn.value)"
113+
>
114+
<div class="icon-wrapper" v-if="props.showIcon">
115+
<LoaderCircle
116+
class="animate-spin"
117+
v-if="loadingValues.includes(btn.value)"
118+
/>
119+
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
120+
<Circle v-else />
121+
</div>
122+
<slot name="option" :label="btn.label" :value="btn.value">
123+
<VbenRenderContent :content="btn.label" />
124+
</slot>
125+
</Button>
126+
</VbenButtonGroup>
127+
</template>
128+
<style lang="scss" scoped>
129+
.vben-check-button-group {
130+
&:deep(.size-large) button {
131+
.icon-wrapper {
132+
margin-right: 0.3rem;
133+
134+
svg {
135+
width: 1rem;
136+
height: 1rem;
137+
}
138+
}
139+
}
140+
141+
&:deep(.size-middle) button {
142+
.icon-wrapper {
143+
margin-right: 0.2rem;
144+
145+
svg {
146+
width: 0.75rem;
147+
height: 0.75rem;
148+
}
149+
}
150+
}
151+
152+
&:deep(.size-small) button {
153+
.icon-wrapper {
154+
margin-right: 0.1rem;
155+
156+
svg {
157+
width: 0.65rem;
158+
height: 0.65rem;
159+
}
160+
}
161+
}
162+
}
163+
</style>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export type * from './button';
2+
export { default as VbenButtonGroup } from './button-group.vue';
23
export { default as VbenButton } from './button.vue';
4+
export { default as VbenCheckButtonGroup } from './check-button-group.vue';
35
export { default as VbenIconButton } from './icon-button.vue';

packages/effects/common-ui/src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export * from '@vben-core/popup-ui';
1515
// 给文档用
1616
export {
1717
VbenButton,
18+
VbenButtonGroup,
19+
VbenCheckButtonGroup,
1820
VbenCountToAnimator,
1921
VbenInputPassword,
2022
VbenLoading,

playground/src/locales/langs/en-US/examples.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,8 @@
6363
},
6464
"layout": {
6565
"col-page": "ColPage Layout"
66+
},
67+
"button-group": {
68+
"title": "Button Group"
6669
}
6770
}

playground/src/locales/langs/zh-CN/examples.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,8 @@
6363
},
6464
"layout": {
6565
"col-page": "双列布局"
66+
},
67+
"button-group": {
68+
"title": "按钮组"
6669
}
6770
}

playground/src/router/routes/modules/examples.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ const routes: RouteRecordRaw[] = [
299299
title: 'Loading',
300300
},
301301
},
302+
{
303+
name: 'ButtonGroup',
304+
path: '/examples/button-group',
305+
component: () => import('#/views/examples/button-group/index.vue'),
306+
meta: {
307+
icon: 'mdi:check-circle',
308+
title: $t('examples.button-group.title'),
309+
},
310+
},
302311
],
303312
},
304313
];

0 commit comments

Comments
 (0)