Procházet zdrojové kódy

perf: improve modal and drawer component documentation and fix known problems (#4264)

* feat: improve modal and drawer component documentation and fix known problems

* chore: update ci
Vben před 8 měsíci
rodič
revize
36e7ca19a1
54 změnil soubory, kde provedl 884 přidání a 178 odebrání
  1. 16 0
      .github/labeler.yml
  2. 1 1
      .github/release-drafter.yml
  3. 3 3
      .github/workflows/ci.yml
  4. 21 5
      .github/workflows/deploy.yml
  5. 21 0
      .github/workflows/labeler.yml
  6. 5 1
      docs/.vitepress/config/zh.mts
  7. 108 0
      docs/src/components/common-ui/vben-drawer.md
  8. 95 22
      docs/src/components/common-ui/vben-modal.md
  9. 1 1
      docs/src/components/introduction.md
  10. 45 0
      docs/src/demos/vben-drawer/auto-height/drawer.vue
  11. 21 0
      docs/src/demos/vben-drawer/auto-height/index.vue
  12. 11 0
      docs/src/demos/vben-drawer/basic/index.vue
  13. 26 0
      docs/src/demos/vben-drawer/dynamic/drawer.vue
  14. 30 0
      docs/src/demos/vben-drawer/dynamic/index.vue
  15. 8 0
      docs/src/demos/vben-drawer/extra/drawer.vue
  16. 21 0
      docs/src/demos/vben-drawer/extra/index.vue
  17. 26 0
      docs/src/demos/vben-drawer/shared-data/drawer.vue
  18. 26 0
      docs/src/demos/vben-drawer/shared-data/index.vue
  19. 21 0
      docs/src/demos/vben-modal/auto-height/index.vue
  20. 45 0
      docs/src/demos/vben-modal/auto-height/modal.vue
  21. 2 2
      docs/src/demos/vben-modal/basic/index.vue
  22. 21 0
      docs/src/demos/vben-modal/draggable/index.vue
  23. 10 0
      docs/src/demos/vben-modal/draggable/modal.vue
  24. 30 0
      docs/src/demos/vben-modal/dynamic/index.vue
  25. 38 0
      docs/src/demos/vben-modal/dynamic/modal.vue
  26. 2 3
      docs/src/demos/vben-modal/extra/index.vue
  27. 1 1
      docs/src/demos/vben-modal/extra/modal.vue
  28. 26 0
      docs/src/demos/vben-modal/shared-data/index.vue
  29. 26 0
      docs/src/demos/vben-modal/shared-data/modal.vue
  30. 16 2
      docs/src/guide/introduction/quick-start.md
  31. 8 10
      docs/src/guide/introduction/vben.md
  32. 15 1
      docs/src/guide/introduction/why.md
  33. 0 4
      internal/lint-configs/eslint-config/src/configs/vue.ts
  34. 1 1
      packages/@core/base/design/src/design-tokens/default/index.css
  35. 5 2
      packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
  36. 19 2
      packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
  37. 1 1
      packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
  38. 5 1
      packages/@core/ui-kit/popup-ui/src/modal/modal.ts
  39. 43 42
      packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  40. 1 31
      packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts
  41. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue
  42. 1 1
      packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue
  43. 2 0
      packages/effects/common-ui/src/components/page/page.vue
  44. 1 1
      packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue
  45. 1 1
      packages/stores/shim-pinia.d.ts
  46. 0 1
      packages/stores/src/modules/tabbar.ts
  47. 1 1
      packages/utils/src/helpers/generate-routes-backend.ts
  48. 16 0
      playground/src/views/examples/doc-button.vue
  49. 14 7
      playground/src/views/examples/drawer/auto-height-demo.vue
  50. 0 8
      playground/src/views/examples/drawer/base-demo.vue
  51. 5 2
      playground/src/views/examples/drawer/index.vue
  52. 15 9
      playground/src/views/examples/modal/auto-height-demo.vue
  53. 0 8
      playground/src/views/examples/modal/base-demo.vue
  54. 6 2
      playground/src/views/examples/modal/index.vue

+ 16 - 0
.github/labeler.yml

@@ -0,0 +1,16 @@
+# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name
+feature:
+  - head-branch: ["^feat", "feat"]
+
+bug:
+  - head-branch: ["^fix", "fix"]
+
+chore:
+  - head-branch: ["^chore", "chore"]
+
+perf:
+  - head-branch: ["^perf", "perf"]
+
+documentation:
+  - changed-files:
+      - any-glob-to-any-file: ["**/*.md", "docs/**"]

+ 1 - 1
.github/release-drafter.yml

@@ -13,13 +13,13 @@ categories:
   - title: "🚀 Features"
     labels:
       - "feature"
-      - "enhancement"
   - title: "🐞 Bug Fixes"
     labels:
       - "bug"
   - title: "📈 Performance"
     labels:
       - "perf"
+      - "enhancement"
   - title: 📝 Documentation
     labels:
       - "documentation"

+ 3 - 3
.github/workflows/ci.yml

@@ -23,7 +23,7 @@ jobs:
       matrix:
         os:
           - ubuntu-latest
-          - macos-latest
+          # - macos-latest
           - windows-latest
     timeout-minutes: 20
     steps:
@@ -62,7 +62,7 @@ jobs:
       matrix:
         os:
           - ubuntu-latest
-          - macos-latest
+          # - macos-latest
           - windows-latest
 
     steps:
@@ -85,7 +85,7 @@ jobs:
       matrix:
         os:
           - ubuntu-latest
-          - macos-latest
+          # - macos-latest
           - windows-latest
     steps:
       - name: Checkout code

+ 21 - 5
.github/workflows/deploy.yml

@@ -6,7 +6,7 @@ on:
       - main
 
 jobs:
-  deploy-push-playground-ftp:
+  deploy-playground-ftp:
     name: Deploy Push Playground Ftp
     if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
     runs-on: ubuntu-latest
@@ -27,7 +27,7 @@ jobs:
         uses: ./.github/actions/setup-node
 
       - name: Build
-        run: pnpm build:play && pnpm build:docs
+        run: pnpm build:play
 
       - name: Sync Playground files
         uses: SamKirkland/FTP-Deploy-Action@v4.3.5
@@ -37,6 +37,22 @@ jobs:
           password: ${{ secrets.WEB_PLAYGROUND_FTP_PWSSWORD }}
           local-dir: ./playground/dist/
 
+  deploy-docs-ftp:
+    name: Deploy Push Docs Ftp
+    if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Setup Node
+        uses: ./.github/actions/setup-node
+
+      - name: Build
+        run: pnpm build:docs
+
       - name: Sync Docs files
         uses: SamKirkland/FTP-Deploy-Action@v4.3.5
         with:
@@ -45,7 +61,7 @@ jobs:
           password: ${{ secrets.WEBSITE_FTP_PASSWORD }}
           local-dir: ./docs/.vitepress/dist/
 
-  deploy-push-antd-ftp:
+  deploy-antd-ftp:
     name: Deploy Push Antd Ftp
     if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
     runs-on: ubuntu-latest
@@ -76,7 +92,7 @@ jobs:
           password: ${{ secrets.WEB_ANTD_FTP_PASSWORD }}
           local-dir: ./apps/web-antd/dist/
 
-  deploy-push-ele-ftp:
+  deploy-ele-ftp:
     name: Deploy Push Element Ftp
     if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
     runs-on: ubuntu-latest
@@ -107,7 +123,7 @@ jobs:
           password: ${{ secrets.WEB_ELE_FTP_PASSWORD }}
           local-dir: ./apps/web-ele/dist/
 
-  deploy-push-naive-ftp:
+  deploy-naive-ftp:
     name: Deploy Push Naive Ftp
     if: github.actor != 'dependabot[bot]' && !contains(github.event.head_commit.message, '[skip ci]')
     runs-on: ubuntu-latest

+ 21 - 0
.github/workflows/labeler.yml

@@ -0,0 +1,21 @@
+name: PR Labeler
+
+on:
+  pull_request:
+    types: [opened, edited, synchronize]
+
+jobs:
+  label:
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Label PR based on title or file changes
+        uses: actions/labeler@v5
+        with:
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          configuration-path: .github/labeler.yml

+ 5 - 1
docs/.vitepress/config/zh.mts

@@ -154,7 +154,11 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
       items: [
         {
           link: 'common-ui/vben-modal',
-          text: 'Modal 弹窗',
+          text: 'Vben Modal 模态框',
+        },
+        {
+          link: 'common-ui/vben-drawer',
+          text: 'Vben Drawer 抽屉',
         },
       ],
     },

+ 108 - 0
docs/src/components/common-ui/vben-drawer.md

@@ -0,0 +1,108 @@
+---
+outline: deep
+---
+
+# Vben Drawer 抽屉
+
+框架提供的抽屉组件,支持`自动高度`、`loading`等功能。
+
+## 基础用法
+
+使用 `useVbenDrawer` 创建最基础的模态框。
+
+<DemoPreview dir="demos/vben-drawer/basic" />
+
+## 组件抽离
+
+Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 drawer 内的内容抽离出来,也方便复用。通过 `connectedComponent` 参数,可以将内外组件进行连接,而不用其他任何操作。
+
+<DemoPreview dir="demos/vben-drawer/extra" />
+
+## 自动计算高度
+
+弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合 `loading` 效果以及使用 `prepend-footer` 插槽。
+
+<DemoPreview dir="demos/vben-drawer/auto-height" />
+
+## 使用 Api
+
+通过 `drawerApi` 可以调用 drawer 的方法以及使用 `setState` 更新 drawer 的状态。
+
+<DemoPreview dir="demos/vben-drawer/dynamic" />
+
+## 数据共享
+
+如果你使用了 `connectedComponent` 参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用 `drawerApi` 来获取数据和设置数据,配合 `onOpenChange`,可以满足大部分的需求。
+
+<DemoPreview dir="demos/vben-drawer/shared-data" />
+
+::: info 注意
+
+- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
+- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onComfirm`,那么以内部的 `onComfirm` 为准。`onOpenChange`事件除外,内外都会触发。
+
+:::
+
+## API
+
+```ts
+// Drawer 为弹窗组件
+// drawerApi 为弹窗的方法
+const [Drawer, drawerApi] = useVbenDrawer({
+  // 属性
+  // 事件
+});
+```
+
+### Props
+
+所有属性都可以传入 `useVbenDrawer` 的第一个参数中。
+
+| 属性名             | 描述                | 类型            | 默认值  |
+| ------------------ | ------------------- | --------------- | ------- |
+| title              | 标题                | `string\|slot`  | -       |
+| titleTooltip       | 标题提示信息        | `string\|slot`  | -       |
+| description        | 描述信息            | `string\|slot`  | -       |
+| isOpen             | 弹窗打开状态        | `boolean`       | `false` |
+| loading            | 弹窗加载状态        | `boolean`       | `false` |
+| closable           | 显示关闭按钮        | `boolean`       | `true`  |
+| modal              | 显示遮罩            | `boolean`       | `true`  |
+| header             | 显示header          | `boolean`       | `true`  |
+| footer             | 显示footer          | `boolean\|slot` | `true`  |
+| confirmLoading     | 确认按钮loading状态 | `boolean`       | `false` |
+| closeOnClickModal  | 点击遮罩关闭弹窗    | `boolean`       | `true`  |
+| closeOnPressEscape | esc 关闭弹窗        | `boolean`       | `true`  |
+| confirmText        | 确认按钮文本        | `boolean\|slot` | `确认`  |
+| cancelText         | 取消按钮文本        | `boolean\|slot` | `取消`  |
+
+### Event
+
+以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
+
+| 事件名 | 描述 | 类型 |
+| --- | --- | --- |
+| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
+| onCancel | 点击取消按钮触发 | `()=>void` |
+| onConfirm | 点击确认按钮触发 | `()=>void` |
+| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
+
+### Slots
+
+除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
+
+| 插槽名         | 描述                |
+| -------------- | ------------------- |
+| default        | 默认插槽 - 弹窗内容 |
+| prepend-footer | 取消按钮左侧        |
+| append-footer  | 取消按钮右侧        |
+
+### modalApi
+
+| 事件名 | 描述 | 类型 |
+| --- | --- | --- |
+| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
+| open | 打开弹窗 | `()=>void` |
+| close | 关闭弹窗 | `()=>void` |
+| setData | 设置共享数据 | `<T>(data:T)=>void` |
+| getData | 获取共享数据 | `<T>()=>T` |
+| useStore | 获取可响应式状态 | - |

+ 95 - 22
docs/src/components/common-ui/vben-modal.md

@@ -2,44 +2,117 @@
 outline: deep
 ---
 
-# vben-modal
+# Vben Modal 模态框
 
-::: tip
-
-文档还在完善中,敬请期待。
-
-:::
-
-框架提供的模态框组件,支持`拖拽`、`全屏`、`自定义`等功能。
+框架提供的模态框组件,支持`拖拽`、`全屏`、`自动高度`、`loading`等功能。
 
 ## 基础用法
 
-使用 `useVbenModal` 创建最基的模态框。
+使用 `useVbenModal` 创建最基础的模态框。
 
 <DemoPreview dir="demos/vben-modal/basic" />
 
 ## 组件抽离
 
-modal 内的内容一般业务中,会比较复杂,所以我们可以将 modal 内的内容抽离出来。
+Modal 内的内容一般业务中,会比较复杂,所以我们可以将 modal 内的内容抽离出来,也方便复用。通过 `connectedComponent` 参数,可以将内外组件进行连接,而不用其他任何操作
 
 <DemoPreview dir="demos/vben-modal/extra" />
 
-## API
+## 开启拖拽
+
+通过 `draggable` 参数,可开启拖拽功能。
+
+<DemoPreview dir="demos/vben-modal/draggable" />
+
+## 自动计算高度
+
+弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合 `loading` 效果以及使用 `prepend-footer` 插槽。
+
+<DemoPreview dir="demos/vben-modal/auto-height" />
+
+## 使用 Api
+
+通过 `modalApi` 可以调用 modal 的方法以及使用 `setState` 更新 modal 的状态。
 
-### 属性
+<DemoPreview dir="demos/vben-modal/dynamic" />
 
-| 属性名 | 描述  | 类型     | 默认值 |
-| ------ | ----- | -------- | ------ |
-| title  | 标题. | `string` | —      |
+## 数据共享
 
-### 事件
+如果你使用了 `connectedComponent` 参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用 `modalApi` 来获取数据和设置数据,配合 `onOpenChange`,可以满足大部分的需求。
+
+<DemoPreview dir="demos/vben-modal/shared-data" />
+
+::: info 注意
+
+- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
+- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onComfirm`,那么以内部的 `onComfirm` 为准。`onOpenChange`事件除外,内外都会触发。
+
+:::
+
+## API
+
+```ts
+// Modal 为弹窗组件
+// modalApi 为弹窗的方法
+const [Modal, modalApi] = useVbenModal({
+  // 属性
+  // 事件
+});
+```
+
+### Props
+
+所有属性都可以传入 `useVbenModal` 的第一个参数中。
+
+| 属性名             | 描述                | 类型            | 默认值  |
+| ------------------ | ------------------- | --------------- | ------- |
+| title              | 标题                | `string\|slot`  | -       |
+| titleTooltip       | 标题提示信息        | `string\|slot`  | -       |
+| description        | 描述信息            | `string\|slot`  | -       |
+| isOpen             | 弹窗打开状态        | `boolean`       | `false` |
+| loading            | 弹窗加载状态        | `boolean`       | `false` |
+| fullscreen         | 全屏显示            | `boolean`       | `false` |
+| fullscreenButton   | 显示全屏按钮        | `boolean`       | `true`  |
+| draggable          | 可拖拽              | `boolean`       | `false` |
+| closable           | 显示关闭按钮        | `boolean`       | `true`  |
+| centered           | 居中显示            | `boolean`       | `false` |
+| modal              | 显示遮罩            | `boolean`       | `true`  |
+| header             | 显示header          | `boolean`       | `true`  |
+| footer             | 显示footer          | `boolean\|slot` | `true`  |
+| confirmLoading     | 确认按钮loading状态 | `boolean`       | `false` |
+| closeOnClickModal  | 点击遮罩关闭弹窗    | `boolean`       | `true`  |
+| closeOnPressEscape | esc 关闭弹窗        | `boolean`       | `true`  |
+| confirmText        | 确认按钮文本        | `boolean\|slot` | `确认`  |
+| cancelText         | 取消按钮文本        | `boolean\|slot` | `取消`  |
+
+### Event
+
+以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
 
 | 事件名 | 描述 | 类型 |
-| ------ | ---- | ---- |
-| TODO   | TODO | TODO |
+| --- | --- | --- |
+| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
+| onCancel | 点击取消按钮触发 | `()=>void` |
+| onConfirm | 点击确认按钮触发 | `()=>void` |
+| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
+
+### Slots
 
-### 插槽
+除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
 
-| 插槽名  | 描述 |
-| ------- | ---- |
-| default | xx.  |
+| 插槽名         | 描述                |
+| -------------- | ------------------- |
+| default        | 默认插槽 - 弹窗内容 |
+| prepend-footer | 取消按钮左侧        |
+| append-footer  | 取消按钮右侧        |
+
+### modalApi
+
+| 事件名 | 描述 | 类型 |
+| --- | --- | --- |
+| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
+| open | 打开弹窗 | `()=>void` |
+| close | 关闭弹窗 | `()=>void` |
+| setData | 设置共享数据 | `<T>(data:T)=>void` |
+| getData | 获取共享数据 | `<T>()=>T` |
+| useStore | 获取可响应式状态 | - |

+ 1 - 1
docs/src/components/introduction.md

@@ -1,6 +1,6 @@
 # 介绍
 
-::: tip README
+::: info README
 
 该文档介绍的是框架组件的使用方法、属性、事件等。如果你觉得组件封装的不好,或者不符合你的需求,你可以直接使用原生的组件,或者自己封装一个组件,不需要拘泥于框架提供的组件。我们只是提供了一些常用的组件,方便你快速开发。是否使用,取决于你的需求。
 

+ 45 - 0
docs/src/demos/vben-drawer/auto-height/drawer.vue

@@ -0,0 +1,45 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+const list = ref<number[]>([]);
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  onCancel() {
+    drawerApi.close();
+  },
+  onConfirm() {
+    console.log('onConfirm');
+  },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      handleUpdate(10);
+    }
+  },
+});
+
+function handleUpdate(len: number) {
+  drawerApi.setState({ loading: true });
+  setTimeout(() => {
+    list.value = Array.from({ length: len }, (_v, k) => k + 1);
+    drawerApi.setState({ loading: false });
+  }, 2000);
+}
+</script>
+<template>
+  <Drawer title="自动计算高度">
+    <div
+      v-for="item in list"
+      :key="item"
+      class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
+    >
+      {{ item }}
+    </div>
+    <template #prepend-footer>
+      <VbenButton type="link" @click="handleUpdate(6)">
+        点击更新数据
+      </VbenButton>
+    </template>
+  </Drawer>
+</template>

+ 21 - 0
docs/src/demos/vben-drawer/auto-height/index.vue

@@ -0,0 +1,21 @@
+<script lang="ts" setup>
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+import ExtraDrawer from './drawer.vue';
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  // 连接抽离的组件
+  connectedComponent: ExtraDrawer,
+});
+
+function open() {
+  drawerApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Drawer />
+    <VbenButton @click="open">Open</VbenButton>
+  </div>
+</template>

+ 11 - 0
docs/src/demos/vben-drawer/basic/index.vue

@@ -0,0 +1,11 @@
+<script lang="ts" setup>
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+const [Drawer, drawerApi] = useVbenDrawer();
+</script>
+<template>
+  <div>
+    <VbenButton @click="() => drawerApi.open()">Open</VbenButton>
+    <Drawer class="w-[600px]" title="基础示例"> drawer content </Drawer>
+  </div>
+</template>

+ 26 - 0
docs/src/demos/vben-drawer/dynamic/drawer.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  onCancel() {
+    drawerApi.close();
+  },
+  onConfirm() {
+    console.info('onConfirm');
+  },
+  title: '动态修改配置示例',
+});
+
+function handleUpdateTitle() {
+  drawerApi.setState({ title: '内部动态标题' });
+}
+</script>
+<template>
+  <Drawer>
+    <div class="flex-col-center">
+      <VbenButton class="mb-3" type="primary" @click="handleUpdateTitle()">
+        内部动态修改标题
+      </VbenButton>
+    </div>
+  </Drawer>
+</template>

+ 30 - 0
docs/src/demos/vben-drawer/dynamic/index.vue

@@ -0,0 +1,30 @@
+<script lang="ts" setup>
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+import ExtraDrawer from './drawer.vue';
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  // 连接抽离的组件
+  connectedComponent: ExtraDrawer,
+});
+
+function open() {
+  drawerApi.open();
+}
+
+function handleUpdateTitle() {
+  drawerApi.setState({ title: '外部动态标题' });
+  drawerApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Drawer />
+
+    <VbenButton @click="open">Open</VbenButton>
+    <VbenButton class="ml-2" type="primary" @click="handleUpdateTitle">
+      从外部修改标题并打开
+    </VbenButton>
+  </div>
+</template>

+ 8 - 0
docs/src/demos/vben-drawer/extra/drawer.vue

@@ -0,0 +1,8 @@
+<script lang="ts" setup>
+import { useVbenDrawer } from '@vben/common-ui';
+
+const [Drawer] = useVbenDrawer();
+</script>
+<template>
+  <Drawer title="组件抽离示例"> extra drawer content </Drawer>
+</template>

+ 21 - 0
docs/src/demos/vben-drawer/extra/index.vue

@@ -0,0 +1,21 @@
+<script lang="ts" setup>
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+import ExtraDrawer from './drawer.vue';
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  // 连接抽离的组件
+  connectedComponent: ExtraDrawer,
+});
+
+function open() {
+  drawerApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Drawer />
+    <VbenButton @click="open">Open</VbenButton>
+  </div>
+</template>

+ 26 - 0
docs/src/demos/vben-drawer/shared-data/drawer.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenDrawer } from '@vben/common-ui';
+
+const data = ref();
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  onCancel() {
+    drawerApi.close();
+  },
+  onConfirm() {
+    console.info('onConfirm');
+  },
+  onOpenChange(isOpen: boolean) {
+    if (isOpen) {
+      data.value = drawerApi.getData<Record<string, any>>();
+    }
+  },
+});
+</script>
+<template>
+  <Drawer title="数据共享示例">
+    <div class="flex-col-center">外部传递数据: {{ data }}</div>
+  </Drawer>
+</template>

+ 26 - 0
docs/src/demos/vben-drawer/shared-data/index.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { useVbenDrawer, VbenButton } from '@vben/common-ui';
+
+import ExtraDrawer from './drawer.vue';
+
+const [Drawer, drawerApi] = useVbenDrawer({
+  // 连接抽离的组件
+  connectedComponent: ExtraDrawer,
+});
+
+function open() {
+  drawerApi.setData({
+    content: '外部传递的数据 content',
+    payload: '外部传递的数据 payload',
+  });
+  drawerApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Drawer />
+
+    <VbenButton @click="open">Open</VbenButton>
+  </div>
+</template>

+ 21 - 0
docs/src/demos/vben-modal/auto-height/index.vue

@@ -0,0 +1,21 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+import ExtraModal from './modal.vue';
+
+const [Modal, modalApi] = useVbenModal({
+  // 连接抽离的组件
+  connectedComponent: ExtraModal,
+});
+
+function openModal() {
+  modalApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Modal />
+    <VbenButton @click="openModal">Open</VbenButton>
+  </div>
+</template>

+ 45 - 0
docs/src/demos/vben-modal/auto-height/modal.vue

@@ -0,0 +1,45 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+const list = ref<number[]>([]);
+
+const [Modal, modalApi] = useVbenModal({
+  onCancel() {
+    modalApi.close();
+  },
+  onConfirm() {
+    console.log('onConfirm');
+  },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      handleUpdate(10);
+    }
+  },
+});
+
+function handleUpdate(len: number) {
+  modalApi.setState({ loading: true });
+  setTimeout(() => {
+    list.value = Array.from({ length: len }, (_v, k) => k + 1);
+    modalApi.setState({ loading: false });
+  }, 2000);
+}
+</script>
+<template>
+  <Modal title="自动计算高度">
+    <div
+      v-for="item in list"
+      :key="item"
+      class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
+    >
+      {{ item }}
+    </div>
+    <template #prepend-footer>
+      <VbenButton type="link" @click="handleUpdate(6)">
+        点击更新数据
+      </VbenButton>
+    </template>
+  </Modal>
+</template>

+ 2 - 2
docs/src/demos/vben-modal/basic/index.vue

@@ -5,7 +5,7 @@ const [Modal, modalApi] = useVbenModal();
 </script>
 <template>
   <div>
-    <VbenButton @click="() => modalApi.open()">打开弹窗</VbenButton>
-    <Modal title="基础示例"> modal content </Modal>
+    <VbenButton @click="() => modalApi.open()">Open</VbenButton>
+    <Modal class="w-[600px]" title="基础示例"> modal content </Modal>
   </div>
 </template>

+ 21 - 0
docs/src/demos/vben-modal/draggable/index.vue

@@ -0,0 +1,21 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+import ExtraModal from './modal.vue';
+
+const [Modal, modalApi] = useVbenModal({
+  // 连接抽离的组件
+  connectedComponent: ExtraModal,
+});
+
+function openModal() {
+  modalApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Modal />
+    <VbenButton @click="openModal">Open</VbenButton>
+  </div>
+</template>

+ 10 - 0
docs/src/demos/vben-modal/draggable/modal.vue

@@ -0,0 +1,10 @@
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+
+const [Modal] = useVbenModal({
+  draggable: true,
+});
+</script>
+<template>
+  <Modal title="拖拽示例"> modal content </Modal>
+</template>

+ 30 - 0
docs/src/demos/vben-modal/dynamic/index.vue

@@ -0,0 +1,30 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+import ExtraModal from './modal.vue';
+
+const [Modal, modalApi] = useVbenModal({
+  // 连接抽离的组件
+  connectedComponent: ExtraModal,
+});
+
+function openModal() {
+  modalApi.open();
+}
+
+function handleUpdateTitle() {
+  modalApi.setState({ title: '外部动态标题' });
+  modalApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Modal />
+
+    <VbenButton @click="openModal">Open</VbenButton>
+    <VbenButton class="ml-2" type="primary" @click="handleUpdateTitle">
+      从外部修改标题并打开
+    </VbenButton>
+  </div>
+</template>

+ 38 - 0
docs/src/demos/vben-modal/dynamic/modal.vue

@@ -0,0 +1,38 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+const [Modal, modalApi] = useVbenModal({
+  draggable: true,
+  onCancel() {
+    modalApi.close();
+  },
+  onConfirm() {
+    console.info('onConfirm');
+  },
+  title: '动态修改配置示例',
+});
+
+const state = modalApi.useStore();
+
+function handleUpdateTitle() {
+  modalApi.setState({ title: '内部动态标题' });
+}
+
+function handleToggleFullscreen() {
+  modalApi.setState((prev) => {
+    return { ...prev, fullscreen: !prev.fullscreen };
+  });
+}
+</script>
+<template>
+  <Modal>
+    <div class="flex-col-center">
+      <VbenButton class="mb-3" type="primary" @click="handleUpdateTitle()">
+        内部动态修改标题
+      </VbenButton>
+      <VbenButton class="mb-3" @click="handleToggleFullscreen()">
+        {{ state.fullscreen ? '退出全屏' : '打开全屏' }}
+      </VbenButton>
+    </div>
+  </Modal>
+</template>

+ 2 - 3
docs/src/demos/vben-modal/extra/index.vue

@@ -4,7 +4,7 @@ import { useVbenModal, VbenButton } from '@vben/common-ui';
 import ExtraModal from './modal.vue';
 
 const [Modal, modalApi] = useVbenModal({
-  // 接抽离的组件
+  // 接抽离的组件
   connectedComponent: ExtraModal,
 });
 
@@ -16,7 +16,6 @@ function openModal() {
 <template>
   <div>
     <Modal />
-
-    <VbenButton @click="openModal">打开弹窗</VbenButton>
+    <VbenButton @click="openModal">Open</VbenButton>
   </div>
 </template>

+ 1 - 1
docs/src/demos/vben-modal/extra/modal.vue

@@ -4,5 +4,5 @@ import { useVbenModal } from '@vben/common-ui';
 const [Modal] = useVbenModal();
 </script>
 <template>
-  <Modal title="基础示例"> extra modal content </Modal>
+  <Modal title="组件抽离示例"> extra modal content </Modal>
 </template>

+ 26 - 0
docs/src/demos/vben-modal/shared-data/index.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+import ExtraModal from './modal.vue';
+
+const [Modal, modalApi] = useVbenModal({
+  // 连接抽离的组件
+  connectedComponent: ExtraModal,
+});
+
+function openModal() {
+  modalApi.setData({
+    content: '外部传递的数据 content',
+    payload: '外部传递的数据 payload',
+  });
+  modalApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Modal />
+
+    <VbenButton @click="openModal">Open</VbenButton>
+  </div>
+</template>

+ 26 - 0
docs/src/demos/vben-modal/shared-data/modal.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+const data = ref();
+
+const [Modal, modalApi] = useVbenModal({
+  onCancel() {
+    modalApi.close();
+  },
+  onConfirm() {
+    console.info('onConfirm');
+  },
+  onOpenChange(isOpen: boolean) {
+    if (isOpen) {
+      data.value = modalApi.getData<Record<string, any>>();
+    }
+  },
+});
+</script>
+<template>
+  <Modal title="数据共享示例">
+    <div class="flex-col-center">外部传递数据: {{ data }}</div>
+  </Modal>
+</template>

+ 16 - 2
docs/src/guide/introduction/quick-start.md

@@ -72,6 +72,8 @@ pnpm install
 
 ### 运行项目
 
+#### 选择项目
+
 执行以下命运行项目:
 
 ```bash
@@ -84,12 +86,24 @@ pnpm dev
 ```bash
 ◆  Select the app you need to run [dev]:
-│   @vben/web-antd
+│   @vben/web-antd
 │  ○ @vben/web-ele
 │  ○ @vben/web-naive
 │  ○ @vben/docs
-│   @vben/playground
+│   @vben/playground
 ```
 
 现在,你可以在浏览器访问 `http://localhost:5555` 查看项目。
+
+#### 运行指定项目
+
+如果你不想选择项目,可以直接运行以下命令运行你需要的应用:
+
+```bash
+pnpm run dev:antd
+pnpm run dev:ele
+pnpm run dev:naive
+pnpm run dev:docs
+pnpm run dev:play
+```

+ 8 - 10
docs/src/guide/introduction/vben.md

@@ -2,27 +2,27 @@
 
 ::: info 你正在阅读的是 [Vben Admin](https://github.com/vbenjs/vue-vben-admin) `5.0`版本的文档!
 
-- Vben Admin 2.x 目前已经存档,只修复一些严重的问题
+- Vben Admin 2.x 目前已存档,仅进行重大问题修复
 - 新版本与旧版本不兼容,如果你使用的是旧版本(v2、v3),请查看 [Vue Vben Admin 2.x 文档](https://doc.vvbin.cn)
-- 如发现文档有误,欢迎提提交 Issue 帮助我们改进。
-- 如果你只是想体验一下,你可以查看 [快速开始](./quick-start.md)。
+- 如发现文档有误,欢迎提交 [issue](https://github.com/vbenjs/vue-vben-admin/issues) 帮助我们改进。
+- 如果你只是想体验一下,你可以查看[快速开始](./quick-start.md)。
 
 :::
 
-[Vben Admin](https://github.com/vbenjs/vue-vben-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/core)、[Vite](https://github.com/vitejs/vite)、 [TypeScript](https://www.typescriptlang.org/) 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 `vue3`、`vite`、`ts` 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
+[Vben Admin](https://github.com/vbenjs/vue-vben-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/core)、[Vite](https://github.com/vitejs/vite)、 [TypeScript](https://www.typescriptlang.org/) 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 `vue3`、`vite`、`ts` 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
 
 ## 特点
 
 - **最新技术栈**:使用 `Vue3`、`Vite`、`TypeScript` 等前端前沿技术开发。
 - **国际化**:内置完善的国际化方案,支持多语言切换。
 - **权限验证**:完善的权限验证方案,按钮级别权限控制。
-- **多主题**:内置多种主题配置&黑暗模式,满足个性化需求。
+- **多主题**:内置多种主题配置黑暗模式,满足个性化需求。
 - **动态菜单**:支持动态菜单,可以根据权限配置显示菜单。
 - **Mock 数据**:基于 Nitro 的本地高性能 Mock 数据方案。
 - **组件丰富**:提供了丰富的组件,可以满足大部分的业务需求。
 - **规范**:代码规范,使用 `ESLint`、`Prettier`、`Stylelint`、`Publint`、`CSpell` 等工具保证代码质量。
 - **工程化**:使用 `Pnpm Monorepo`、`TurboRepo`、`Changeset` 等工具,提高开发效率。
-- **多UI库支持**:支持 `Ant Design Vue`、`Element Plus`、`Vuetify` 等主流 UI 库,不再限制于特定框架。
+- **多UI库支持**:支持 `Ant Design Vue`、`Element Plus`、`Naive` 等主流 UI 库,不再限制于特定框架。
 
 ## 浏览器支持
 
@@ -32,17 +32,15 @@
 
 | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px"  />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
 | :-: | :-: | :-: | :-: | :-: |
-| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+| 不支持 | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
 
 ## 贡献
 
 - [Vben Admin](https://github.com/vbenjs/vue-vben-admin) 还在持续更新中,本项目欢迎您的参与,共同维护,逐步完善,打造更好的中后台解决方案。
-- 如果你想加入我们,可以多提供一些好的建议或者提交 pr,我们会根据你的活跃度邀请你加入。
+- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,我们会根据你的活跃度邀请你加入。
 
 ::: info 加入我们
 
-如果你想加入我们,你可以从以下几个方面开始,我们会根据你的活跃度邀请你加入:
-
 - 长期提交 `PR`。
 - 提供一些好的建议。
 - 参与讨论,帮助解决一些 `issue`。

+ 15 - 1
docs/src/guide/introduction/why.md

@@ -1,9 +1,23 @@
 # 为什么选择我们?
 
-首先,我们不会去和其他框架做比较。我们认为每个框架都有自己的特点,适合不同的场景。我们的目标是提供一个简单、易用的框架,让开发者可以快速上手,专注于业务逻辑的开发。所以我们只会不断完善和优化我们的框架,提供更好的体验。
+::: info 写在前面
+
+我们不会去和其他框架做比较。我们认为每个框架都有自己的特点,适合不同的场景。我们的目标是提供一个简单、易用的框架,让开发者可以快速上手,专注于业务逻辑的开发。所以我们只会不断完善和优化我们的框架,提供更好的体验。
+
+:::
+
+我们致力于为开发者提供一个高效、现代、易用的前端框架。我们的解决方案基于最新的技术栈,如 Vue3、Vite 和 TypeScript,确保您在构建项目时始终走在技术的前沿。同时,我们注重代码的质量与规范,通过严格的工具链保证代码的一致性和可维护性。无论是初创项目还是企业级应用,我们的框架都能帮助您快速构建、迭代和部署。
 
 ## 框架历程
 
 从 Vue Vben Admin 1.x 版本开始,框架经历了许多迭代和优化。从一开始使用 `Vite 0.x` 版本,没有现成的插件,开发了很多自定义插件来弥合 Webpack 和 Vite 之间的差异。虽然很多现在已经被代替,但是我们的初衷一直没有变,就是提供一个简单、易用的框架。
 
 虽然中间有段时间由社区维护,但我们一直密切关注 Vue Vben Admin 的发展。见证了许多开发者使用 Vben Admin,并提供了许多宝贵的建议和反馈。非常感谢大家的支持和贡献,这些都是我们持续改进 Vben Admin 的动力。新版本中,我们持续收集用户反馈,重新开始,不断优化框架,以提供更好的用户体验。我们的目标是让开发者能够快速上手,专注于业务逻辑的开发。
+
+## 单元测试
+
+单元测试是确保代码质量的基石。在开发过程中编写和执行单元测试,以捕捉潜在的错误并提升代码的可靠性。框架核心逻辑使用 `vitest` 做了单元测试,并在逐步增加覆盖率。通过单元测试,可以放心地进行代码重构,减少回归问题,从而提高整体开发效率。
+
+## 质量与规范
+
+我们始终高度重视代码的质量与规范。通过使用 ESLint、Prettier、Stylelint、Publint、CSpell 等工具来确保代码质量。我们的代码规范基于 Vue3、Vite、TypeScript 等现代前端技术制定,旨在提供一个简洁、易用的框架,使开发者能够快速上手并专注于业务逻辑的开发。

+ 0 - 4
internal/lint-configs/eslint-config/src/configs/vue.ts

@@ -15,10 +15,6 @@ export async function vue(): Promise<Linter.Config[]> {
     {
       files: ['**/*.vue'],
       languageOptions: {
-        globals: {
-          // TODO: 等待插件正式支持后删除
-          defineModel: true,
-        },
         parser: parserVue,
         parserOptions: {
           ecmaFeatures: {

+ 1 - 1
packages/@core/base/design/src/design-tokens/default/index.css

@@ -77,7 +77,7 @@
   /* ============= custom ============= */
 
   /* 遮罩颜色 */
-  --overlay: 0deg 0% 0% / 30%;
+  --overlay: 0 0% 0% / 30%;
 
   /* 基本文字大小 */
   --font-size-base: 16px;

+ 5 - 2
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts

@@ -38,7 +38,6 @@ export class DrawerApi {
       isOpen: false,
       loading: false,
       modal: true,
-      sharedData: {},
       title: '',
     };
 
@@ -93,7 +92,11 @@ export class DrawerApi {
    * 取消操作
    */
   onCancel() {
-    this.api.onCancel?.();
+    if (this.api.onCancel) {
+      this.api.onCancel?.();
+    } else {
+      this.close();
+    }
   }
 
   /**

+ 19 - 2
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@@ -1,6 +1,8 @@
 <script lang="ts" setup>
 import type { DrawerProps, ExtendedDrawerApi } from './drawer';
 
+import { ref, watch } from 'vue';
+
 import { useIsMobile, usePriorityValue } from '@vben-core/composables';
 import { Info, X } from '@vben-core/icons';
 import {
@@ -31,6 +33,8 @@ const props = withDefaults(defineProps<Props>(), {
   drawerApi: undefined,
 });
 
+const wrapperRef = ref<HTMLElement>();
+
 const { isMobile } = useIsMobile();
 const state = props.drawerApi?.useStore?.();
 
@@ -47,6 +51,18 @@ const confirmText = usePriorityValue('confirmText', props, state);
 const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
 const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
 
+watch(
+  () => showLoading.value,
+  (v) => {
+    if (v && wrapperRef.value) {
+      wrapperRef.value.scrollTo({
+        // behavior: 'smooth',
+        top: 0,
+      });
+    }
+  },
+);
+
 function interactOutside(e: Event) {
   if (!closeOnClickModal.value) {
     e.preventDefault();
@@ -129,9 +145,10 @@ function pointerDownOutside(e: Event) {
       </SheetHeader>
 
       <div
+        ref="wrapperRef"
         :class="
-          cn('relative flex-1 p-3', contentClass, {
-            'overflow-y-auto': !showLoading,
+          cn('relative flex-1 overflow-y-auto p-3', contentClass, {
+            'overflow-hidden': showLoading,
           })
         "
       >

+ 1 - 1
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts

@@ -38,10 +38,10 @@ export class ModalApi {
       footer: true,
       fullscreen: false,
       fullscreenButton: true,
+      header: true,
       isOpen: false,
       loading: false,
       modal: true,
-      sharedData: {},
       title: '',
     };
 

+ 5 - 1
packages/@core/ui-kit/popup-ui/src/modal/modal.ts

@@ -60,12 +60,16 @@ export interface ModalProps {
    * @default true
    */
   fullscreenButton?: boolean;
+  /**
+   * 是否显示顶栏
+   * @default true
+   */
+  header?: boolean;
   /**
    * 弹窗是否显示
    * @default false
    */
   loading?: boolean;
-
   /**
    * 是否显示遮罩
    * @default true

+ 43 - 42
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -12,7 +12,6 @@ import {
   DialogFooter,
   DialogHeader,
   DialogTitle,
-  DialogTrigger,
   VbenButton,
   VbenIconButton,
   VbenLoading,
@@ -21,8 +20,6 @@ import {
 } from '@vben-core/shadcn-ui';
 import { cn } from '@vben-core/shared';
 
-// import { useElementSize } from '@vueuse/core';
-
 import { useModalDraggable } from './use-modal-draggable';
 
 interface Props extends ModalProps {
@@ -42,15 +39,15 @@ const props = withDefaults(defineProps<Props>(), {
 });
 
 const contentRef = ref();
+const wrapperRef = ref<HTMLElement>();
 const dialogRef = ref();
 const headerRef = ref();
 const footerRef = ref();
 
 const { isMobile } = useIsMobile();
-// const { height: headerHeight } = useElementSize(headerRef);
-// const { height: footerHeight } = useElementSize(footerRef);
 const state = props.modalApi?.useStore?.();
 
+const header = usePriorityValue('header', props, state);
 const title = usePriorityValue('title', props, state);
 const fullscreen = usePriorityValue('fullscreen', props, state);
 const description = usePriorityValue('description', props, state);
@@ -68,9 +65,12 @@ const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
 const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
 const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
 
-const shouldFullscreen = computed(() => fullscreen.value || isMobile.value);
+const shouldFullscreen = computed(
+  () => (fullscreen.value && header.value) || isMobile.value,
+);
+
 const shouldDraggable = computed(
-  () => draggable.value && !shouldFullscreen.value,
+  () => draggable.value && !shouldFullscreen.value && header.value,
 );
 
 const { dragging, transform } = useModalDraggable(
@@ -79,32 +79,29 @@ const { dragging, transform } = useModalDraggable(
   shouldDraggable,
 );
 
-// const loadingStyle = computed(() => {
-//   // py-5 4px*5*2
-//   const headerPadding = 40;
-//   // p-2 4px*2*2
-//   const footerPadding = 16;
-
-//   return {
-//     bottom: `${footerHeight.value + footerPadding}px`,
-//     height: `calc(100% - ${footerHeight.value + headerHeight.value + headerPadding + footerPadding}px)`,
-//     top: `${headerHeight.value + headerPadding}px`,
-//   };
-// });
-
 watch(
   () => state?.value?.isOpen,
   async (v) => {
     if (v) {
       await nextTick();
-      if (contentRef.value) {
-        const innerContentRef = contentRef.value.getContentRef();
-        dialogRef.value = innerContentRef.$el;
-
-        // reopen modal reassign value
-        const { offsetX, offsetY } = transform;
-        dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
-      }
+      if (!contentRef.value) return;
+      const innerContentRef = contentRef.value.getContentRef();
+      dialogRef.value = innerContentRef.$el;
+      // reopen modal reassign value
+      const { offsetX, offsetY } = transform;
+      dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
+    }
+  },
+);
+
+watch(
+  () => showLoading.value,
+  (v) => {
+    if (v && wrapperRef.value) {
+      wrapperRef.value.scrollTo({
+        // behavior: 'smooth',
+        top: 0,
+      });
     }
   },
 );
@@ -142,10 +139,6 @@ function pointerDownOutside(e: Event) {
     :open="state?.isOpen"
     @update:open="() => modalApi?.close()"
   >
-    <DialogTrigger v-if="$slots.trigger" as-child>
-      <slot name="trigger"> </slot>
-    </DialogTrigger>
-
     <DialogContent
       ref="contentRef"
       :class="
@@ -170,8 +163,9 @@ function pointerDownOutside(e: Event) {
         ref="headerRef"
         :class="
           cn(
-            'border-b px-6 py-5',
+            'border-b px-5 py-4',
             {
+              hidden: !header,
               'cursor-move select-none': shouldDraggable,
             },
             props.headerClass,
@@ -182,12 +176,14 @@ function pointerDownOutside(e: Event) {
           <slot name="title">
             {{ title }}
 
-            <VbenTooltip v-if="titleTooltip" side="right">
-              <template #trigger>
-                <Info class="inline-flex size-5 cursor-pointer pb-1" />
-              </template>
-              {{ titleTooltip }}
-            </VbenTooltip>
+            <slot v-if="titleTooltip" name="titleTooltip">
+              <VbenTooltip side="right">
+                <template #trigger>
+                  <Info class="inline-flex size-5 cursor-pointer pb-1" />
+                </template>
+                {{ titleTooltip }}
+              </VbenTooltip>
+            </slot>
           </slot>
         </DialogTitle>
         <DialogDescription v-if="description">
@@ -201,13 +197,18 @@ function pointerDownOutside(e: Event) {
         </VisuallyHidden>
       </DialogHeader>
       <div
+        ref="wrapperRef"
         :class="
-          cn('relative min-h-40 flex-1 p-3', contentClass, {
-            'overflow-y-auto': !showLoading,
+          cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
+            'overflow-hidden': showLoading,
           })
         "
       >
-        <VbenLoading v-if="showLoading" class="size-full" spinning />
+        <VbenLoading
+          v-if="showLoading"
+          class="size-full h-auto min-h-full"
+          spinning
+        />
         <slot></slot>
       </div>
 

+ 1 - 31
packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts

@@ -20,9 +20,6 @@ export function useModalDraggable(
 
   const dragging = ref(false);
 
-  // let isFirstDrag = true;
-  // let initialX = 0;
-  // let initialY = 0;
   const onMousedown = (e: MouseEvent) => {
     const downX = e.clientX;
     const downY = e.clientY;
@@ -31,12 +28,6 @@ export function useModalDraggable(
       return;
     }
 
-    // if (isFirstDrag) {
-    //   const { x, y } = getInitialTransform(targetRef.value);
-    //   initialX = x;
-    //   initialY = y;
-    // }
-
     const targetRect = targetRef.value.getBoundingClientRect();
 
     const { offsetX, offsetY } = transform;
@@ -56,12 +47,9 @@ export function useModalDraggable(
     const onMousemove = (e: MouseEvent) => {
       let moveX = offsetX + e.clientX - downX;
       let moveY = offsetY + e.clientY - downY;
-      // const x = isFirstDrag ? initialX : 0;
-      // const y = isFirstDrag ? initialY : 0;
+
       moveX = Math.min(Math.max(moveX, minLeft), maxLeft);
-      // + x;
       moveY = Math.min(Math.max(moveY, minTop), maxTop);
-      //  + y;
 
       transform.offsetX = moveX;
       transform.offsetY = moveY;
@@ -73,7 +61,6 @@ export function useModalDraggable(
     };
 
     const onMouseup = () => {
-      // isFirstDrag = false;
       dragging.value = false;
       document.removeEventListener('mousemove', onMousemove);
       document.removeEventListener('mouseup', onMouseup);
@@ -127,20 +114,3 @@ export function useModalDraggable(
     transform,
   };
 }
-
-// function getInitialTransform(target: HTMLElement) {
-//   let x = 0;
-//   let y = 0;
-//   const transformValue = window.getComputedStyle(target)?.transform;
-//   if (transformValue) {
-//     const match = transformValue.match(/matrix\(([^)]+)\)/);
-//     if (match) {
-//       const values = match[1]?.split(', ') ?? [];
-//       // 获取 translateX 值
-//       x = Number.parseFloat(`${values[4]}`);
-//       // 获取 translateY 值
-//       y = Number.parseFloat(`${values[5]}`);
-//     }
-//   }
-//   return { x, y };
-// }

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue

@@ -69,7 +69,7 @@ function onTransitionEnd() {
   <div
     :class="
       cn(
-        'bg-overlay z-100 pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center backdrop-blur-sm transition-all duration-500',
+        'bg-overlay z-100 pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',
         {
           'invisible opacity-0': !showSpinner,
         },

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue

@@ -44,7 +44,7 @@ defineExpose({
 <template>
   <DialogPortal>
     <DialogOverlay
-      class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-overlay fixed inset-0 z-[1000] backdrop-blur-sm"
+      class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-overlay fixed inset-0 z-[1000]"
       data-dismissable-modal="true"
       @click="() => emits('close')"
     />

+ 2 - 0
packages/effects/common-ui/src/components/page/page.vue

@@ -30,6 +30,8 @@ const props = withDefaults(defineProps<Props>(), {
           class="mb-2 flex justify-between text-lg font-semibold"
         >
           {{ title }}
+
+          <slot name="extra"></slot>
         </div>
       </slot>
 

+ 1 - 1
packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue

@@ -47,8 +47,8 @@ watch(
       :close-on-press-escape="false"
       :footer="false"
       :fullscreen-button="false"
+      :header="false"
       class="border-none px-10 py-6 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset]"
-      header-class="hidden"
     >
       <VbenAvatar :src="avatar" class="mx-auto mb-6 size-20" />
       <AuthenticationLogin

+ 1 - 1
packages/stores/shim-pinia.d.ts

@@ -1,4 +1,4 @@
-// TODO: https://github.com/vuejs/pinia/issues/2098
+// https://github.com/vuejs/pinia/issues/2098
 declare module 'pinia' {
   export function acceptHMRUpdate(
     initialUseStore: any | StoreDefinition,

+ 0 - 1
packages/stores/src/modules/tabbar.ts

@@ -66,7 +66,6 @@ export const useTabbarStore = defineStore('core-tabbar', {
      */
     async _goToDefaultTab(router: Router) {
       if (this.getTabs.length <= 0) {
-        // TODO: 跳转首页
         return;
       }
       const firstTab = this.getTabs[0];

+ 1 - 1
packages/utils/src/helpers/generate-routes-backend.ts

@@ -76,7 +76,7 @@ function normalizeViewPath(path: string): string {
     ? normalizedPath
     : `/${normalizedPath}`;
 
-  // TODO: 这里耦合了vben-admin的目录结构
+  // 这里耦合了vben-admin的目录结构
   return viewPath.replace(/^\/views/, '');
 }
 export { generateRoutesByBackend };

+ 16 - 0
playground/src/views/examples/doc-button.vue

@@ -0,0 +1,16 @@
+<script lang="ts" setup>
+import { VBEN_DOC_URL } from '@vben/constants';
+import { openWindow } from '@vben/utils';
+
+import { Button } from 'ant-design-vue';
+
+const props = defineProps<{ path: string }>();
+
+function handleClick() {
+  openWindow(VBEN_DOC_URL + props.path);
+}
+</script>
+
+<template>
+  <Button type="link" @click="handleClick">查看组件文档</Button>
+</template>

+ 14 - 7
playground/src/views/examples/drawer/auto-height-demo.vue

@@ -5,6 +5,8 @@ import { useVbenDrawer } from '@vben/common-ui';
 
 import { Button, message } from 'ant-design-vue';
 
+const list = ref<number[]>([]);
+
 const [Drawer, drawerApi] = useVbenDrawer({
   onCancel() {
     drawerApi.close();
@@ -13,14 +15,19 @@ const [Drawer, drawerApi] = useVbenDrawer({
     message.info('onConfirm');
     // drawerApi.close();
   },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      handleUpdate(10);
+    }
+  },
 });
 
-const list = ref<number[]>([]);
-
-list.value = Array.from({ length: 10 }, (_v, k) => k + 1);
-
-function handleUpdate() {
-  list.value = Array.from({ length: 6 }, (_v, k) => k + 1);
+function handleUpdate(len: number) {
+  drawerApi.setState({ loading: true });
+  setTimeout(() => {
+    list.value = Array.from({ length: len }, (_v, k) => k + 1);
+    drawerApi.setState({ loading: false });
+  }, 2000);
 }
 </script>
 <template>
@@ -34,7 +41,7 @@ function handleUpdate() {
     </div>
 
     <template #prepend-footer>
-      <Button type="link" @click="handleUpdate">点击更新数据</Button>
+      <Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
     </template>
   </Drawer>
 </template>

+ 0 - 8
playground/src/views/examples/drawer/base-demo.vue

@@ -11,14 +11,6 @@ const [Drawer, drawerApi] = useVbenDrawer({
     message.info('onConfirm');
     // drawerApi.close();
   },
-  onOpenChange(isOpen) {
-    if (isOpen) {
-      drawerApi.setState({ loading: true });
-      setTimeout(() => {
-        drawerApi.setState({ loading: false });
-      }, 2000);
-    }
-  },
 });
 </script>
 <template>

+ 5 - 2
playground/src/views/examples/drawer/index.vue

@@ -3,18 +3,18 @@ import { Page, useVbenDrawer } from '@vben/common-ui';
 
 import { Button, Card } from 'ant-design-vue';
 
+import DocButton from '../doc-button.vue';
 import AutoHeightDemo from './auto-height-demo.vue';
 import BaseDemo from './base-demo.vue';
 import DynamicDemo from './dynamic-demo.vue';
 import SharedDataDemo from './shared-data-demo.vue';
 
 const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
-  // 接抽离的组件
+  // 接抽离的组件
   connectedComponent: BaseDemo,
 });
 
 const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
-  // 链接抽离的组件
   connectedComponent: AutoHeightDemo,
 });
 
@@ -57,6 +57,9 @@ function openSharedDrawer() {
     description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
     title="抽屉组件示例"
   >
+    <template #extra>
+      <DocButton path="/components/common-ui/vben-drawer" />
+    </template>
     <BaseDrawer />
     <AutoHeightDrawer />
     <DynamicDrawer />

+ 15 - 9
playground/src/views/examples/modal/auto-height-demo.vue

@@ -5,24 +5,31 @@ import { useVbenModal } from '@vben/common-ui';
 
 import { Button, message } from 'ant-design-vue';
 
+const list = ref<number[]>([]);
+
 const [Modal, modalApi] = useVbenModal({
   onCancel() {
     modalApi.close();
   },
   onConfirm() {
     message.info('onConfirm');
-    // modalApi.close();
+  },
+  onOpenChange(isOpen) {
+    if (isOpen) {
+      handleUpdate(10);
+    }
   },
 });
 
-const list = ref<number[]>([]);
-
-list.value = Array.from({ length: 10 }, (_v, k) => k + 1);
-
-function handleUpdate() {
-  list.value = Array.from({ length: 6 }, (_v, k) => k + 1);
+function handleUpdate(len: number) {
+  modalApi.setState({ loading: true });
+  setTimeout(() => {
+    list.value = Array.from({ length: len }, (_v, k) => k + 1);
+    modalApi.setState({ loading: false });
+  }, 2000);
 }
 </script>
+
 <template>
   <Modal title="自动计算高度">
     <div
@@ -32,9 +39,8 @@ function handleUpdate() {
     >
       {{ item }}
     </div>
-
     <template #prepend-footer>
-      <Button type="link" @click="handleUpdate">点击更新数据</Button>
+      <Button type="link" @click="handleUpdate(6)">点击更新数据</Button>
     </template>
   </Modal>
 </template>

+ 0 - 8
playground/src/views/examples/modal/base-demo.vue

@@ -11,14 +11,6 @@ const [Modal, modalApi] = useVbenModal({
     message.info('onConfirm');
     // modalApi.close();
   },
-  onOpenChange(isOpen) {
-    if (isOpen) {
-      modalApi.setState({ loading: true });
-      setTimeout(() => {
-        modalApi.setState({ loading: false });
-      }, 2000);
-    }
-  },
 });
 </script>
 <template>

+ 6 - 2
playground/src/views/examples/modal/index.vue

@@ -3,6 +3,7 @@ import { Page, useVbenModal } from '@vben/common-ui';
 
 import { Button, Card } from 'ant-design-vue';
 
+import DocButton from '../doc-button.vue';
 import AutoHeightDemo from './auto-height-demo.vue';
 import BaseDemo from './base-demo.vue';
 import DragDemo from './drag-demo.vue';
@@ -10,7 +11,7 @@ import DynamicDemo from './dynamic-demo.vue';
 import SharedDataDemo from './shared-data-demo.vue';
 
 const [BaseModal, baseModalApi] = useVbenModal({
-  // 接抽离的组件
+  // 接抽离的组件
   connectedComponent: BaseDemo,
 });
 
@@ -62,9 +63,12 @@ function handleUpdateTitle() {
 
 <template>
   <Page
-    description="弹窗组件常用于在不离开当前页面的情况下,显示额外的信息、表单或操作提示。"
+    description="弹窗组件常用于在不离开当前页面的情况下,显示额外的信息、表单或操作提示,更多api请查看组件文档。"
     title="弹窗组件示例"
   >
+    <template #extra>
+      <DocButton path="/components/common-ui/vben-modal" />
+    </template>
     <BaseModal />
     <AutoHeightModal />
     <DragModal />