feat: add resizable and ColPage component (#5188)

* feat: add component resizable

* feat: component `ColPage` with demo
This commit is contained in:
Netfan 2024-12-19 20:37:42 +08:00 committed by GitHub
parent 1853ba1d60
commit acd87b2250
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 368 additions and 13 deletions

View File

@ -28,6 +28,7 @@ export {
Fullscreen, Fullscreen,
Github, Github,
Grip, Grip,
GripVertical,
Info, Info,
InspectionPanel, InspectionPanel,
Languages, Languages,

View File

@ -16,6 +16,7 @@ export * from './pagination';
export * from './pin-input'; export * from './pin-input';
export * from './popover'; export * from './popover';
export * from './radio-group'; export * from './radio-group';
export * from './resizable';
export * from './scroll-area'; export * from './scroll-area';
export * from './select'; export * from './select';
export * from './separator'; export * from './separator';

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export { default as ColPage } from './col-page.vue';
export * from './types';

View File

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

View File

@ -1,5 +1,6 @@
export * from './api-component'; export * from './api-component';
export * from './captcha'; export * from './captcha';
export * from './col-page';
export * from './ellipsis-text'; export * from './ellipsis-text';
export * from './icon-picker'; export * from './icon-picker';
export * from './page'; export * from './page';

View File

@ -1 +1,2 @@
export { default as Page } from './page.vue'; export { default as Page } from './page.vue';
export * from './types';

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { PageProps } from './types';
import { import {
computed, computed,
nextTick, nextTick,
@ -11,23 +13,11 @@ import {
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants'; import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
import { cn } from '@vben-core/shared/utils'; import { cn } from '@vben-core/shared/utils';
interface Props {
title?: string;
description?: string;
contentClass?: string;
/**
* 根据content可见高度自适应
*/
autoContentHeight?: boolean;
headerClass?: string;
footerClass?: string;
}
defineOptions({ defineOptions({
name: 'Page', name: 'Page',
}); });
const { autoContentHeight = false } = defineProps<Props>(); const { autoContentHeight = false } = defineProps<PageProps>();
const headerHeight = ref(0); const headerHeight = ref(0);
const footerHeight = ref(0); const footerHeight = ref(0);

View File

@ -0,0 +1,11 @@
export interface PageProps {
title?: string;
description?: string;
contentClass?: string;
/**
* content可见高度自适应
*/
autoContentHeight?: boolean;
headerClass?: string;
footerClass?: string;
}

View File

@ -56,5 +56,11 @@
"timestamp": "Timestamp:", "timestamp": "Timestamp:",
"x": "x:", "x": "x:",
"y": "y:" "y": "y:"
},
"resize": {
"title": "Resize"
},
"layout": {
"col-page": "ColPage Layout"
} }
} }

View File

@ -59,5 +59,8 @@
"timestamp": "时间戳:", "timestamp": "时间戳:",
"x": "x", "x": "x",
"y": "y" "y": "y"
},
"layout": {
"col-page": "双列布局"
} }
} }

View File

@ -237,6 +237,17 @@ const routes: RouteRecordRaw[] = [
title: $t('examples.resize.title'), title: $t('examples.resize.title'),
}, },
}, },
{
name: 'ColPageDemo',
path: '/examples/layout/col-page',
component: () => import('#/views/examples/layout/col-page.vue'),
meta: {
badge: 'Alpha',
badgeVariants: 'destructive',
icon: 'material-symbols:horizontal-distribute',
title: $t('examples.layout.col-page'),
},
},
], ],
}, },
]; ];

View File

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