mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-23 01:30:26 +08:00
feat: add resizable
and ColPage
component (#5188)
* feat: add component resizable * feat: component `ColPage` with demo
This commit is contained in:
parent
1853ba1d60
commit
acd87b2250
@ -28,6 +28,7 @@ export {
|
||||
Fullscreen,
|
||||
Github,
|
||||
Grip,
|
||||
GripVertical,
|
||||
Info,
|
||||
InspectionPanel,
|
||||
Languages,
|
||||
|
@ -16,6 +16,7 @@ export * from './pagination';
|
||||
export * from './pin-input';
|
||||
export * from './popover';
|
||||
export * from './radio-group';
|
||||
export * from './resizable';
|
||||
export * from './scroll-area';
|
||||
export * from './select';
|
||||
export * from './separator';
|
||||
|
@ -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';
|
107
packages/effects/common-ui/src/components/col-page/col-page.vue
Normal file
107
packages/effects/common-ui/src/components/col-page/col-page.vue
Normal 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>
|
@ -0,0 +1,2 @@
|
||||
export { default as ColPage } from './col-page.vue';
|
||||
export * from './types';
|
26
packages/effects/common-ui/src/components/col-page/types.ts
Normal file
26
packages/effects/common-ui/src/components/col-page/types.ts
Normal 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;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
export * from './api-component';
|
||||
export * from './captcha';
|
||||
export * from './col-page';
|
||||
export * from './ellipsis-text';
|
||||
export * from './icon-picker';
|
||||
export * from './page';
|
||||
|
@ -1 +1,2 @@
|
||||
export { default as Page } from './page.vue';
|
||||
export * from './types';
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { PageProps } from './types';
|
||||
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
@ -11,23 +13,11 @@ import {
|
||||
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
|
||||
import { cn } from '@vben-core/shared/utils';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
contentClass?: string;
|
||||
/**
|
||||
* 根据content可见高度自适应
|
||||
*/
|
||||
autoContentHeight?: boolean;
|
||||
headerClass?: string;
|
||||
footerClass?: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Page',
|
||||
});
|
||||
|
||||
const { autoContentHeight = false } = defineProps<Props>();
|
||||
const { autoContentHeight = false } = defineProps<PageProps>();
|
||||
|
||||
const headerHeight = ref(0);
|
||||
const footerHeight = ref(0);
|
||||
|
11
packages/effects/common-ui/src/components/page/types.ts
Normal file
11
packages/effects/common-ui/src/components/page/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface PageProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
contentClass?: string;
|
||||
/**
|
||||
* 根据content可见高度自适应
|
||||
*/
|
||||
autoContentHeight?: boolean;
|
||||
headerClass?: string;
|
||||
footerClass?: string;
|
||||
}
|
@ -56,5 +56,11 @@
|
||||
"timestamp": "Timestamp:",
|
||||
"x": "x:",
|
||||
"y": "y:"
|
||||
},
|
||||
"resize": {
|
||||
"title": "Resize"
|
||||
},
|
||||
"layout": {
|
||||
"col-page": "ColPage Layout"
|
||||
}
|
||||
}
|
||||
|
@ -59,5 +59,8 @@
|
||||
"timestamp": "时间戳:",
|
||||
"x": "x:",
|
||||
"y": "y:"
|
||||
},
|
||||
"layout": {
|
||||
"col-page": "双列布局"
|
||||
}
|
||||
}
|
||||
|
@ -237,6 +237,17 @@ const routes: RouteRecordRaw[] = [
|
||||
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'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
106
playground/src/views/examples/layout/col-page.vue
Normal file
106
playground/src/views/examples/layout/col-page.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user