feat: add VbenButtonGroup
and VbenCheckButtonGroup
with demo (#5591)
* 添加按钮组、选择按钮组以及相应的Demo
This commit is contained in:
parent
d49e3e81a4
commit
4570d5b54b
@ -14,6 +14,8 @@ export {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsLeft,
|
ChevronsLeft,
|
||||||
ChevronsRight,
|
ChevronsRight,
|
||||||
|
Circle,
|
||||||
|
CircleCheckBig,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Copy,
|
Copy,
|
||||||
CornerDownLeft,
|
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;
|
size?: ButtonVariantSize;
|
||||||
variant?: ButtonVariants;
|
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 type * from './button';
|
||||||
|
export { default as VbenButtonGroup } from './button-group.vue';
|
||||||
export { default as VbenButton } from './button.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';
|
export { default as VbenIconButton } from './icon-button.vue';
|
||||||
|
@ -15,6 +15,8 @@ export * from '@vben-core/popup-ui';
|
|||||||
// 给文档用
|
// 给文档用
|
||||||
export {
|
export {
|
||||||
VbenButton,
|
VbenButton,
|
||||||
|
VbenButtonGroup,
|
||||||
|
VbenCheckButtonGroup,
|
||||||
VbenCountToAnimator,
|
VbenCountToAnimator,
|
||||||
VbenInputPassword,
|
VbenInputPassword,
|
||||||
VbenLoading,
|
VbenLoading,
|
||||||
|
@ -63,5 +63,8 @@
|
|||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"col-page": "ColPage Layout"
|
"col-page": "ColPage Layout"
|
||||||
|
},
|
||||||
|
"button-group": {
|
||||||
|
"title": "Button Group"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,5 +63,8 @@
|
|||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"col-page": "双列布局"
|
"col-page": "双列布局"
|
||||||
|
},
|
||||||
|
"button-group": {
|
||||||
|
"title": "按钮组"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -299,6 +299,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: 'Loading',
|
title: 'Loading',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ButtonGroup',
|
||||||
|
path: '/examples/button-group',
|
||||||
|
component: () => import('#/views/examples/button-group/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'mdi:check-circle',
|
||||||
|
title: $t('examples.button-group.title'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
194
playground/src/views/examples/button-group/index.vue
Normal file
194
playground/src/views/examples/button-group/index.vue
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
VbenButton,
|
||||||
|
VbenButtonGroup,
|
||||||
|
VbenCheckButtonGroup,
|
||||||
|
} from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
const radioValue = ref<string | undefined>('a');
|
||||||
|
const checkValue = ref(['a', 'b']);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: '选项1', value: 'a' },
|
||||||
|
{ label: '选项2', value: 'b' },
|
||||||
|
{ label: '选项3', value: 'c' },
|
||||||
|
{ label: '选项4', value: 'd' },
|
||||||
|
{ label: '选项5', value: 'e' },
|
||||||
|
{ label: '选项6', value: 'f' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function resetValues() {
|
||||||
|
radioValue.value = undefined;
|
||||||
|
checkValue.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeChange(v: any, isChecked: boolean) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
message.loading({
|
||||||
|
content: `正在设置${v}为${isChecked ? '选中' : '未选中'}...`,
|
||||||
|
duration: 0,
|
||||||
|
key: 'beforeChange',
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
message.success({ content: `${v} 已设置成功`, key: 'beforeChange' });
|
||||||
|
resolve(true);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const compProps = reactive({
|
||||||
|
beforeChange: undefined,
|
||||||
|
disabled: false,
|
||||||
|
gap: 0,
|
||||||
|
showIcon: true,
|
||||||
|
size: 'middle',
|
||||||
|
} as Recordable<any>);
|
||||||
|
|
||||||
|
const [Form] = useVbenForm({
|
||||||
|
handleValuesChange(values) {
|
||||||
|
Object.keys(values).forEach((k) => {
|
||||||
|
if (k === 'beforeChange') {
|
||||||
|
compProps[k] = values[k] ? beforeChange : undefined;
|
||||||
|
} else {
|
||||||
|
compProps[k] = values[k];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '大', value: 'large' },
|
||||||
|
{ label: '中', value: 'middle' },
|
||||||
|
{ label: '小', value: 'small' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: compProps.size,
|
||||||
|
fieldName: 'size',
|
||||||
|
label: '尺寸',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{ label: '无', value: 0 },
|
||||||
|
{ label: '小', value: 5 },
|
||||||
|
{ label: '中', value: 15 },
|
||||||
|
{ label: '大', value: 30 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: compProps.gap,
|
||||||
|
fieldName: 'gap',
|
||||||
|
label: '间距',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
defaultValue: compProps.showIcon,
|
||||||
|
fieldName: 'showIcon',
|
||||||
|
label: '显示图标',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
defaultValue: compProps.disabled,
|
||||||
|
fieldName: 'disabled',
|
||||||
|
label: '禁用',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
defaultValue: false,
|
||||||
|
fieldName: 'beforeChange',
|
||||||
|
label: '前置回调',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
submitOnChange: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onBtnClick(value: any) {
|
||||||
|
const opt = options.find((o) => o.value === value);
|
||||||
|
if (opt) {
|
||||||
|
message.success(`点击了按钮${opt.label},value = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
title="VbenButtonGroup 按钮组"
|
||||||
|
description="VbenButtonGroup是一个按钮容器,用于包裹一组按钮,协调整体样式。VbenCheckButtonGroup则可以作为一个表单组件,提供单选或多选功能"
|
||||||
|
>
|
||||||
|
<Card title="基本用法">
|
||||||
|
<template #extra>
|
||||||
|
<Button type="primary" @click="resetValues">清空值</Button>
|
||||||
|
</template>
|
||||||
|
<p class="mt-4">按钮组:</p>
|
||||||
|
<div class="mt-2 flex flex-col gap-2">
|
||||||
|
<VbenButtonGroup v-bind="compProps" border>
|
||||||
|
<VbenButton
|
||||||
|
v-for="btn in options"
|
||||||
|
:key="btn.value"
|
||||||
|
variant="link"
|
||||||
|
@click="onBtnClick(btn.value)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</VbenButton>
|
||||||
|
</VbenButtonGroup>
|
||||||
|
<VbenButtonGroup v-bind="compProps" border>
|
||||||
|
<VbenButton
|
||||||
|
v-for="btn in options"
|
||||||
|
:key="btn.value"
|
||||||
|
variant="outline"
|
||||||
|
@click="onBtnClick(btn.value)"
|
||||||
|
>
|
||||||
|
{{ btn.label }}
|
||||||
|
</VbenButton>
|
||||||
|
</VbenButtonGroup>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4">单选:{{ radioValue }}</p>
|
||||||
|
<div class="mt-2 flex flex-col gap-2">
|
||||||
|
<VbenCheckButtonGroup
|
||||||
|
v-model="radioValue"
|
||||||
|
:options="options"
|
||||||
|
v-bind="compProps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4">单选插槽:{{ radioValue }}</p>
|
||||||
|
<div class="mt-2 flex flex-col gap-2">
|
||||||
|
<VbenCheckButtonGroup
|
||||||
|
v-model="radioValue"
|
||||||
|
:options="options"
|
||||||
|
v-bind="compProps"
|
||||||
|
>
|
||||||
|
<template #option="{ label, value }">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<span class="ml-2 text-gray-400">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VbenCheckButtonGroup>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4">多选{{ checkValue }}</p>
|
||||||
|
<div class="mt-2 flex flex-col gap-2">
|
||||||
|
<VbenCheckButtonGroup
|
||||||
|
v-model="checkValue"
|
||||||
|
multiple
|
||||||
|
:options="options"
|
||||||
|
v-bind="compProps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="设置" class="mt-4">
|
||||||
|
<Form />
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user