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 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'; |
||||
|
|||||
@ -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