feat: add VbenButtonGroup
and VbenCheckButtonGroup
with demo (#5591)
* 添加按钮组、选择按钮组以及相应的Demo
This commit is contained in:
@@ -14,6 +14,8 @@ export {
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
Circle,
|
||||
CircleCheckBig,
|
||||
CircleHelp,
|
||||
Copy,
|
||||
CornerDownLeft,
|
||||
|
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
defineOptions({ name: 'VbenButtonGroup' });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
border?: boolean;
|
||||
gap?: number;
|
||||
size?: 'large' | 'middle' | 'small';
|
||||
}>(),
|
||||
{ border: false, gap: 0, size: 'middle' },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'vben-button-group rounded-md',
|
||||
`size-${size}`,
|
||||
gap ? 'with-gap' : 'no-gap',
|
||||
$attrs.class as string,
|
||||
)
|
||||
"
|
||||
:style="{ gap: gap ? `${gap}px` : '0px' }"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vben-button-group {
|
||||
display: inline-flex;
|
||||
|
||||
&.size-large :deep(button) {
|
||||
height: 2.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-right: 0.4rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.size-middle :deep(button) {
|
||||
height: 2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-right: 0.2rem;
|
||||
|
||||
svg {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.size-small :deep(button) {
|
||||
height: 1.75rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.65rem;
|
||||
line-height: 0.75rem;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-right: 0.1rem;
|
||||
|
||||
svg {
|
||||
width: 0.65rem;
|
||||
height: 0.65rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-gap > :deep(button):nth-of-type(1) {
|
||||
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
|
||||
}
|
||||
|
||||
&.no-gap > :deep(button):last-of-type {
|
||||
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
|
||||
}
|
||||
|
||||
&.no-gap {
|
||||
:deep(button + button) {
|
||||
border-left-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -22,3 +22,21 @@ export interface VbenButtonProps {
|
||||
size?: ButtonVariantSize;
|
||||
variant?: ButtonVariants;
|
||||
}
|
||||
|
||||
export type CustomRenderType = (() => Component | string) | string;
|
||||
|
||||
export type ValueType = boolean | number | string;
|
||||
|
||||
export interface VbenButtonGroupProps
|
||||
extends Pick<VbenButtonProps, 'disabled'> {
|
||||
beforeChange?: (
|
||||
value: ValueType,
|
||||
isChecked: boolean,
|
||||
) => boolean | PromiseLike<boolean | undefined> | undefined;
|
||||
btnClass?: any;
|
||||
gap?: number;
|
||||
multiple?: boolean;
|
||||
options?: { label: CustomRenderType; value: ValueType }[];
|
||||
showIcon?: boolean;
|
||||
size?: 'large' | 'middle' | 'small';
|
||||
}
|
||||
|
@@ -0,0 +1,163 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Arrayable } from '@vueuse/core';
|
||||
|
||||
import type { ValueType, VbenButtonGroupProps } from './button';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
|
||||
import { VbenRenderContent } from '@vben-core/shadcn-ui';
|
||||
import { cn, isFunction } from '@vben-core/shared/utils';
|
||||
|
||||
import { objectOmit } from '@vueuse/core';
|
||||
|
||||
import VbenButtonGroup from './button-group.vue';
|
||||
import Button from './button.vue';
|
||||
|
||||
const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
|
||||
gap: 0,
|
||||
multiple: false,
|
||||
showIcon: true,
|
||||
size: 'middle',
|
||||
});
|
||||
|
||||
const btnDefaultProps = computed(() => {
|
||||
return {
|
||||
...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
|
||||
class: cn(props.btnClass),
|
||||
};
|
||||
});
|
||||
const modelValue = defineModel<Arrayable<ValueType> | undefined>();
|
||||
|
||||
const innerValue = ref<Array<ValueType>>([]);
|
||||
const loadingValues = ref<Array<ValueType>>([]);
|
||||
watch(
|
||||
() => props.multiple,
|
||||
(val) => {
|
||||
if (val) {
|
||||
modelValue.value = innerValue.value;
|
||||
} else {
|
||||
modelValue.value =
|
||||
innerValue.value.length > 0 ? innerValue.value[0] : undefined;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
(val) => {
|
||||
if (Array.isArray(val)) {
|
||||
const arrVal = val.filter((v) => v !== undefined);
|
||||
if (arrVal.length > 0) {
|
||||
innerValue.value = props.multiple
|
||||
? [...arrVal]
|
||||
: [arrVal[0] as ValueType];
|
||||
} else {
|
||||
innerValue.value = [];
|
||||
}
|
||||
} else {
|
||||
innerValue.value = val === undefined ? [] : [val as ValueType];
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
async function onBtnClick(value: ValueType) {
|
||||
if (props.beforeChange && isFunction(props.beforeChange)) {
|
||||
try {
|
||||
loadingValues.value.push(value);
|
||||
const canChange = await props.beforeChange(
|
||||
value,
|
||||
!innerValue.value.includes(value),
|
||||
);
|
||||
if (canChange === false) {
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
loadingValues.value.splice(loadingValues.value.indexOf(value), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.multiple) {
|
||||
if (innerValue.value.includes(value)) {
|
||||
innerValue.value = innerValue.value.filter((item) => item !== value);
|
||||
} else {
|
||||
innerValue.value.push(value);
|
||||
}
|
||||
modelValue.value = innerValue.value;
|
||||
} else {
|
||||
innerValue.value = [value];
|
||||
modelValue.value = value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VbenButtonGroup
|
||||
:size="props.size"
|
||||
:gap="props.gap"
|
||||
class="vben-check-button-group"
|
||||
>
|
||||
<Button
|
||||
v-for="(btn, index) in props.options"
|
||||
:key="index"
|
||||
:class="cn('border', props.btnClass)"
|
||||
:disabled="
|
||||
props.disabled ||
|
||||
loadingValues.includes(btn.value) ||
|
||||
(!props.multiple && loadingValues.length > 0)
|
||||
"
|
||||
v-bind="btnDefaultProps"
|
||||
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
|
||||
@click="onBtnClick(btn.value)"
|
||||
>
|
||||
<div class="icon-wrapper" v-if="props.showIcon">
|
||||
<LoaderCircle
|
||||
class="animate-spin"
|
||||
v-if="loadingValues.includes(btn.value)"
|
||||
/>
|
||||
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
|
||||
<Circle v-else />
|
||||
</div>
|
||||
<slot name="option" :label="btn.label" :value="btn.value">
|
||||
<VbenRenderContent :content="btn.label" />
|
||||
</slot>
|
||||
</Button>
|
||||
</VbenButtonGroup>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.vben-check-button-group {
|
||||
&:deep(.size-large) button {
|
||||
.icon-wrapper {
|
||||
margin-right: 0.3rem;
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:deep(.size-middle) button {
|
||||
.icon-wrapper {
|
||||
margin-right: 0.2rem;
|
||||
|
||||
svg {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:deep(.size-small) button {
|
||||
.icon-wrapper {
|
||||
margin-right: 0.1rem;
|
||||
|
||||
svg {
|
||||
width: 0.65rem;
|
||||
height: 0.65rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,3 +1,5 @@
|
||||
export type * from './button';
|
||||
export { default as VbenButtonGroup } from './button-group.vue';
|
||||
export { default as VbenButton } from './button.vue';
|
||||
export { default as VbenCheckButtonGroup } from './check-button-group.vue';
|
||||
export { default as VbenIconButton } from './icon-button.vue';
|
||||
|
@@ -15,6 +15,8 @@ export * from '@vben-core/popup-ui';
|
||||
// 给文档用
|
||||
export {
|
||||
VbenButton,
|
||||
VbenButtonGroup,
|
||||
VbenCheckButtonGroup,
|
||||
VbenCountToAnimator,
|
||||
VbenInputPassword,
|
||||
VbenLoading,
|
||||
|
Reference in New Issue
Block a user