Explorar o código

feat: improve `page` component (#5013)

* feat: `page` component support fixed header

* docs: `page`  component documentation

* docs: Improve `props` types of `page`

* docs: improve `fixedHeader` description of `page`

* fix: `page` header border color with fixedHeader

* feat: add `headerClass` for `Page`
Netfan hai 9 meses
pai
achega
17c7ce8f21

+ 10 - 0
docs/.vitepress/config/zh.mts

@@ -148,6 +148,16 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
         },
       ],
     },
+    {
+      collapsed: false,
+      text: '布局组件',
+      items: [
+        {
+          link: 'layout-ui/page',
+          text: 'Page 页面',
+        },
+      ],
+    },
     {
       collapsed: false,
       text: '通用组件',

+ 4 - 0
docs/src/components/introduction.md

@@ -6,6 +6,10 @@
 
 :::
 
+## 布局组件
+
+布局组件一般在页面内容区域用作顶层容器组件,提供一些统一的布局样式和基本功能。
+
 ## 通用组件
 
 通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。

+ 45 - 0
docs/src/components/layout-ui/page.md

@@ -0,0 +1,45 @@
+---
+outline: deep
+---
+
+# Page 常规页面组件
+
+提供一个常规页面布局的组件,包括头部、内容区域、底部三个部分。
+
+::: info 写在前面
+
+本组件是一个基本布局组件。如果有更多的通用页面布局需求(比如双列布局等),可以根据实际需求自行封装。
+
+:::
+
+## 基础用法
+
+将`Page`作为你的业务页面的根组件即可。
+
+### Props
+
+| 属性名 | 描述 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| title | 页面标题 | `string\|slot` | - |
+| description | 页面描述(标题下的内容) | `string\|slot` | - |
+| contentClass | 内容区域的class | `string` | - |
+| headerClass | 头部区域的class | `string` | - |
+| footerClass | 底部区域的class | `string` | - |
+| autoContentHeight | 自动调整内容区域的高度 | `boolean` | `false` |
+| fixedHeader | 固定头部在页面内容区域顶部,在滚动时保持可见 | `boolean` | `false` |
+
+::: tip 注意
+
+如果`title`、`description`、`extra`三者均未提供有效内容(通过`props`或者`slots`均可),则页面头部区域不会渲染。
+
+:::
+
+### Slots
+
+| 插槽名称    | 描述         |
+| ----------- | ------------ |
+| default     | 页面内容     |
+| title       | 页面标题     |
+| description | 页面描述     |
+| extra       | 页面头部右侧 |
+| footer      | 页面底部     |

+ 1 - 1
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -503,7 +503,7 @@ function handleHeaderToggle() {
 
     <div
       ref="contentRef"
-      class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in"
+      class="flex flex-1 flex-col transition-all duration-300 ease-in"
     >
       <div
         :class="[

+ 1 - 0
packages/effects/common-ui/package.json

@@ -22,6 +22,7 @@
   "dependencies": {
     "@vben-core/form-ui": "workspace:*",
     "@vben-core/popup-ui": "workspace:*",
+    "@vben-core/preferences": "workspace:*",
     "@vben-core/shadcn-ui": "workspace:*",
     "@vben-core/shared": "workspace:*",
     "@vben/constants": "workspace:*",

+ 41 - 3
packages/effects/common-ui/src/components/page/page.vue

@@ -1,5 +1,15 @@
 <script setup lang="ts">
-import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
+import {
+  computed,
+  nextTick,
+  onMounted,
+  ref,
+  type StyleValue,
+  useTemplateRef,
+} from 'vue';
+
+import { preferences } from '@vben-core/preferences';
+import { cn } from '@vben-core/shared/utils';
 
 interface Props {
   title?: string;
@@ -9,6 +19,10 @@ interface Props {
    * 根据content可见高度自适应
    */
   autoContentHeight?: boolean;
+  /** 头部固定 */
+  fixedHeader?: boolean;
+  headerClass?: string;
+  footerClass?: string;
 }
 
 defineOptions({
@@ -20,6 +34,7 @@ const {
   description = '',
   autoContentHeight = false,
   title = '',
+  fixedHeader = false,
 } = defineProps<Props>();
 
 const headerHeight = ref(0);
@@ -29,6 +44,17 @@ const shouldAutoHeight = ref(false);
 const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
 const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
 
+const headerStyle = computed<StyleValue>(() => {
+  return fixedHeader
+    ? {
+        position: 'sticky',
+        zIndex: 200,
+        top:
+          preferences.header.mode === 'fixed' ? 'var(--vben-header-height)' : 0,
+      }
+    : undefined;
+});
+
 const contentStyle = computed(() => {
   if (autoContentHeight) {
     return {
@@ -69,7 +95,14 @@ onMounted(() => {
         $slots.extra
       "
       ref="headerRef"
-      class="bg-card relative px-6 py-4"
+      :class="
+        cn(
+          'bg-card relative px-6 py-4',
+          headerClass,
+          fixedHeader ? 'border-border border-b' : '',
+        )
+      "
+      :style="headerStyle"
     >
       <slot name="title">
         <div v-if="title" class="mb-2 flex text-lg font-semibold">
@@ -95,7 +128,12 @@ onMounted(() => {
     <div
       v-if="$slots.footer"
       ref="footerRef"
-      class="bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4"
+      :class="
+        cn(
+          footerClass,
+          'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
+        )
+      "
     >
       <slot name="footer"></slot>
     </div>

+ 20 - 3
playground/src/views/examples/form/basic.vue

@@ -1,13 +1,17 @@
 <script lang="ts" setup>
+import { ref } from 'vue';
+
 import { Page } from '@vben/common-ui';
 
-import { Button, Card, message } from 'ant-design-vue';
+import { Button, Card, message, TabPane, Tabs } from 'ant-design-vue';
 import dayjs from 'dayjs';
 
 import { useVbenForm } from '#/adapter/form';
 
 import DocButton from '../doc-button.vue';
 
+const activeTab = ref('basic');
+
 const [BaseForm, baseFormApi] = useVbenForm({
   // 所有表单项共用,可单独在表单内覆盖
   commonConfig: {
@@ -331,18 +335,31 @@ function handleSetFormValue() {
   <Page
     content-class="flex flex-col gap-4"
     description="表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。"
+    fixed-header
+    header-class="pb-0"
     title="表单组件"
   >
+    <template #description>
+      <div class="text-muted-foreground">
+        <p>
+          表单组件基础示例,请注意,该页面用到的参数代码会添加一些简单注释,方便理解,请仔细查看。
+        </p>
+      </div>
+      <Tabs v-model:active-key="activeTab" :tab-bar-style="{ marginBottom: 0 }">
+        <TabPane key="basic" tab="基础示例" />
+        <TabPane key="layout" tab="自定义布局" />
+      </Tabs>
+    </template>
     <template #extra>
       <DocButton path="/components/common-ui/vben-form" />
     </template>
-    <Card title="基础示例">
+    <Card v-show="activeTab === 'basic'" title="基础示例">
       <template #extra>
         <Button type="primary" @click="handleSetFormValue">设置表单值</Button>
       </template>
       <BaseForm />
     </Card>
-    <Card title="使用tailwind自定义布局">
+    <Card v-show="activeTab === 'layout'" title="使用tailwind自定义布局">
       <CustomLayoutForm />
     </Card>
   </Page>

+ 1 - 0
playground/src/views/examples/modal/index.vue

@@ -77,6 +77,7 @@ function openFormModal() {
 <template>
   <Page
     description="弹窗组件常用于在不离开当前页面的情况下,显示额外的信息、表单或操作提示,更多api请查看组件文档。"
+    fixed-header
     title="弹窗组件示例"
   >
     <template #extra>

+ 3 - 0
pnpm-lock.yaml

@@ -1460,6 +1460,9 @@ importers:
       '@vben-core/popup-ui':
         specifier: workspace:*
         version: link:../../@core/ui-kit/popup-ui
+      '@vben-core/preferences':
+        specifier: workspace:*
+        version: link:../../@core/preferences
       '@vben-core/shadcn-ui':
         specifier: workspace:*
         version: link:../../@core/ui-kit/shadcn-ui