committed by
GitHub
10 changed files with 494 additions and 0 deletions
@ -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> |
|||
@ -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'; |
|||
|
|||
@ -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