committed by
GitHub
16 changed files with 368 additions and 13 deletions
@ -0,0 +1,48 @@ |
|||
<script setup lang="ts"> |
|||
import { computed, type HTMLAttributes } from 'vue'; |
|||
|
|||
import { GripVertical } from '@vben-core/icons'; |
|||
import { cn } from '@vben-core/shared/utils'; |
|||
|
|||
import { |
|||
SplitterResizeHandle, |
|||
type SplitterResizeHandleEmits, |
|||
type SplitterResizeHandleProps, |
|||
useForwardPropsEmits, |
|||
} from 'radix-vue'; |
|||
|
|||
const props = defineProps< |
|||
{ |
|||
class?: HTMLAttributes['class']; |
|||
withHandle?: boolean; |
|||
} & SplitterResizeHandleProps |
|||
>(); |
|||
const emits = defineEmits<SplitterResizeHandleEmits>(); |
|||
|
|||
const delegatedProps = computed(() => { |
|||
const { class: _, ...delegated } = props; |
|||
return delegated; |
|||
}); |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits); |
|||
</script> |
|||
|
|||
<template> |
|||
<SplitterResizeHandle |
|||
v-bind="forwarded" |
|||
:class=" |
|||
cn( |
|||
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 [&[data-orientation=vertical]>div]:rotate-90 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0', |
|||
props.class, |
|||
) |
|||
" |
|||
> |
|||
<template v-if="props.withHandle"> |
|||
<div |
|||
class="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border" |
|||
> |
|||
<GripVertical class="h-2.5 w-2.5" /> |
|||
</div> |
|||
</template> |
|||
</SplitterResizeHandle> |
|||
</template> |
|||
@ -0,0 +1,38 @@ |
|||
<script setup lang="ts"> |
|||
import { computed, type HTMLAttributes } from 'vue'; |
|||
|
|||
import { cn } from '@vben-core/shared/utils'; |
|||
|
|||
import { |
|||
SplitterGroup, |
|||
type SplitterGroupEmits, |
|||
type SplitterGroupProps, |
|||
useForwardPropsEmits, |
|||
} from 'radix-vue'; |
|||
|
|||
const props = defineProps< |
|||
{ class?: HTMLAttributes['class'] } & SplitterGroupProps |
|||
>(); |
|||
const emits = defineEmits<SplitterGroupEmits>(); |
|||
|
|||
const delegatedProps = computed(() => { |
|||
const { class: _, ...delegated } = props; |
|||
return delegated; |
|||
}); |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits); |
|||
</script> |
|||
|
|||
<template> |
|||
<SplitterGroup |
|||
v-bind="forwarded" |
|||
:class=" |
|||
cn( |
|||
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col', |
|||
props.class, |
|||
) |
|||
" |
|||
> |
|||
<slot></slot> |
|||
</SplitterGroup> |
|||
</template> |
|||
@ -0,0 +1,3 @@ |
|||
export { default as ResizableHandle } from './ResizableHandle.vue'; |
|||
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'; |
|||
export { SplitterPanel as ResizablePanel } from 'radix-vue'; |
|||
@ -0,0 +1,107 @@ |
|||
<script lang="ts" setup> |
|||
import type { ColPageProps } from './types'; |
|||
|
|||
import { computed, ref, useSlots } from 'vue'; |
|||
|
|||
import { |
|||
ResizableHandle, |
|||
ResizablePanel, |
|||
ResizablePanelGroup, |
|||
} from '@vben-core/shadcn-ui'; |
|||
|
|||
import Page from '../page/page.vue'; |
|||
|
|||
defineOptions({ |
|||
name: 'ColPage', |
|||
inheritAttrs: false, |
|||
}); |
|||
|
|||
const props = withDefaults(defineProps<ColPageProps>(), { |
|||
leftWidth: 30, |
|||
rightWidth: 70, |
|||
resizable: true, |
|||
}); |
|||
|
|||
const delegatedProps = computed(() => { |
|||
const { leftWidth: _, ...delegated } = props; |
|||
return delegated; |
|||
}); |
|||
|
|||
const slots = useSlots(); |
|||
|
|||
const delegatedSlots = computed(() => { |
|||
const resultSlots: string[] = []; |
|||
|
|||
for (const key of Object.keys(slots)) { |
|||
if (!['default', 'left'].includes(key)) { |
|||
resultSlots.push(key); |
|||
} |
|||
} |
|||
return resultSlots; |
|||
}); |
|||
|
|||
const leftPanelRef = ref<InstanceType<typeof ResizablePanel>>(); |
|||
|
|||
function expandLeft() { |
|||
leftPanelRef.value?.expand(); |
|||
} |
|||
|
|||
function collapseLeft() { |
|||
leftPanelRef.value?.collapse(); |
|||
} |
|||
|
|||
defineExpose({ |
|||
expandLeft, |
|||
collapseLeft, |
|||
}); |
|||
</script> |
|||
<template> |
|||
<Page v-bind="delegatedProps"> |
|||
<!-- 继承默认的slot --> |
|||
<template |
|||
v-for="slotName in delegatedSlots" |
|||
:key="slotName" |
|||
#[slotName]="slotProps" |
|||
> |
|||
<slot :name="slotName" v-bind="slotProps"></slot> |
|||
</template> |
|||
|
|||
<ResizablePanelGroup class="w-full" direction="horizontal"> |
|||
<ResizablePanel |
|||
ref="leftPanelRef" |
|||
:collapsed-size="leftCollapsedWidth" |
|||
:collapsible="leftCollapsible" |
|||
:default-size="leftWidth" |
|||
:max-size="leftMaxWidth" |
|||
:min-size="leftMinWidth" |
|||
> |
|||
<template #default="slotProps"> |
|||
<slot |
|||
name="left" |
|||
v-bind="{ |
|||
...slotProps, |
|||
expand: expandLeft, |
|||
collapse: collapseLeft, |
|||
}" |
|||
></slot> |
|||
</template> |
|||
</ResizablePanel> |
|||
<ResizableHandle |
|||
v-if="resizable" |
|||
:style="{ backgroundColor: splitLine ? undefined : 'transparent' }" |
|||
:with-handle="splitHandle" |
|||
/> |
|||
<ResizablePanel |
|||
:collapsed-size="rightCollapsedWidth" |
|||
:collapsible="rightCollapsible" |
|||
:default-size="rightWidth" |
|||
:max-size="rightMaxWidth" |
|||
:min-size="rightMinWidth" |
|||
> |
|||
<template #default> |
|||
<slot></slot> |
|||
</template> |
|||
</ResizablePanel> |
|||
</ResizablePanelGroup> |
|||
</Page> |
|||
</template> |
|||
@ -0,0 +1,2 @@ |
|||
export { default as ColPage } from './col-page.vue'; |
|||
export * from './types'; |
|||
@ -0,0 +1,26 @@ |
|||
import type { PageProps } from '../page/types'; |
|||
|
|||
export interface ColPageProps extends PageProps { |
|||
/** |
|||
* 左侧宽度 |
|||
* @default 30 |
|||
*/ |
|||
leftWidth?: number; |
|||
leftMinWidth?: number; |
|||
leftMaxWidth?: number; |
|||
leftCollapsedWidth?: number; |
|||
leftCollapsible?: boolean; |
|||
/** |
|||
* 右侧宽度 |
|||
* @default 70 |
|||
*/ |
|||
rightWidth?: number; |
|||
rightMinWidth?: number; |
|||
rightCollapsedWidth?: number; |
|||
rightMaxWidth?: number; |
|||
rightCollapsible?: boolean; |
|||
|
|||
resizable?: boolean; |
|||
splitLine?: boolean; |
|||
splitHandle?: boolean; |
|||
} |
|||
@ -1 +1,2 @@ |
|||
export { default as Page } from './page.vue'; |
|||
export * from './types'; |
|||
|
|||
@ -0,0 +1,11 @@ |
|||
export interface PageProps { |
|||
title?: string; |
|||
description?: string; |
|||
contentClass?: string; |
|||
/** |
|||
* 根据content可见高度自适应 |
|||
*/ |
|||
autoContentHeight?: boolean; |
|||
headerClass?: string; |
|||
footerClass?: string; |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
<script lang="ts" setup> |
|||
import { reactive, ref } from 'vue'; |
|||
|
|||
import { ColPage } from '@vben/common-ui'; |
|||
import { IconifyIcon } from '@vben/icons'; |
|||
|
|||
import { |
|||
Alert, |
|||
Button, |
|||
Card, |
|||
Checkbox, |
|||
Slider, |
|||
Tag, |
|||
Tooltip, |
|||
} from 'ant-design-vue'; |
|||
|
|||
const props = reactive({ |
|||
leftCollapsedWidth: 5, |
|||
leftCollapsible: true, |
|||
leftMaxWidth: 50, |
|||
leftMinWidth: 20, |
|||
leftWidth: 30, |
|||
resizable: true, |
|||
rightWidth: 70, |
|||
splitHandle: false, |
|||
splitLine: false, |
|||
}); |
|||
const leftMinWidth = ref(props.leftMinWidth || 1); |
|||
const leftMaxWidth = ref(props.leftMaxWidth || 100); |
|||
</script> |
|||
<template> |
|||
<ColPage |
|||
auto-content-height |
|||
description="ColPage 是一个双列布局组件,支持左侧折叠、拖拽调整宽度等功能。" |
|||
v-bind="props" |
|||
title="ColPage 双列布局组件" |
|||
> |
|||
<template #title> |
|||
<span class="mr-2 text-2xl font-bold">ColPage 双列布局组件</span> |
|||
<Tag color="hsl(var(--destructive))">Alpha</Tag> |
|||
</template> |
|||
<template #left="{ isCollapsed, expand }"> |
|||
<div v-if="isCollapsed" @click="expand"> |
|||
<Tooltip title="点击展开左侧"> |
|||
<Button shape="circle" type="primary"> |
|||
<template #icon> |
|||
<IconifyIcon class="text-2xl" icon="bi:arrow-right" /> |
|||
</template> |
|||
</Button> |
|||
</Tooltip> |
|||
</div> |
|||
<div |
|||
v-else |
|||
:style="{ minWidth: '200px' }" |
|||
class="border-border bg-card mr-2 rounded-[var(--radius)] border p-2" |
|||
> |
|||
<p>这里是左侧内容</p> |
|||
<p>这里是左侧内容</p> |
|||
<p>这里是左侧内容</p> |
|||
<p>这里是左侧内容</p> |
|||
<p>这里是左侧内容</p> |
|||
</div> |
|||
</template> |
|||
<Card class="ml-2" title="基本使用"> |
|||
<div class="flex flex-col gap-2"> |
|||
<div class="flex gap-2"> |
|||
<Checkbox v-model:checked="props.resizable">可拖动调整宽度</Checkbox> |
|||
<Checkbox v-model:checked="props.splitLine">显示拖动分隔线</Checkbox> |
|||
<Checkbox v-model:checked="props.splitHandle">显示拖动手柄</Checkbox> |
|||
<Checkbox v-model:checked="props.leftCollapsible"> |
|||
左侧可折叠 |
|||
</Checkbox> |
|||
</div> |
|||
<div class="flex items-center gap-2"> |
|||
<span>左侧最小宽度百分比:</span> |
|||
<Slider |
|||
v-model:value="leftMinWidth" |
|||
:max="props.leftMaxWidth - 1" |
|||
:min="1" |
|||
style="width: 100px" |
|||
@after-change="(value) => (props.leftMinWidth = value as number)" |
|||
/> |
|||
<span>左侧最大宽度百分比:</span> |
|||
<Slider |
|||
v-model:value="props.leftMaxWidth" |
|||
:max="100" |
|||
:min="leftMaxWidth + 1" |
|||
style="width: 100px" |
|||
@after-change="(value) => (props.leftMaxWidth = value as number)" |
|||
/> |
|||
</div> |
|||
<Alert message="实验性的组件" show-icon type="warning"> |
|||
<template #description> |
|||
<p> |
|||
双列布局组件是一个在Page组件上扩展的相对基础的布局组件,支持左侧折叠(当拖拽导致左侧宽度比最小宽度还要小时,还可以进入折叠状态)、拖拽调整宽度等功能。 |
|||
</p> |
|||
<p>以上宽度设置的数值是百分比,最小值为1,最大值为100。</p> |
|||
<p class="font-bold text-red-600"> |
|||
这是一个实验性的组件,用法可能会发生变动,也可能最终不会被采用。在其用法正式出现在文档中之前,不建议在生产环境中使用。 |
|||
</p> |
|||
</template> |
|||
</Alert> |
|||
</div> |
|||
</Card> |
|||
</ColPage> |
|||
</template> |
|||
Loading…
Reference in new issue