Skip to content

useDrawer - 抽屉管理

useDrawer 是一个抽屉管理 Hook,提供异步组件加载的抽屉调用方法,适用于侧边栏表单、详情展示等场景。

特性

  • 异步组件加载: 支持动态导入组件,按需加载
  • 多方向支持: 支持从上、右、下、左四个方向滑出
  • Promise 支持: 基于 Promise 的异步操作

基础用法

导入

typescript
import { useDrawer } from '@duxweb/dvha-pro'

基本使用

typescript
const drawer = useDrawer()

// 显示抽屉
drawer.show({
  title: '用户详情',
  width: 500,
  placement: 'right',
  component: () => import('./UserDetail.vue'),
  componentProps: {
    userId: 123
  }
}).then((result) => {
  console.log('抽屉返回结果:', result)
}).catch(() => {
  console.log('用户取消了操作')
})

API 参考

UseDrawerResult

字段类型说明
show(props: UseDrawerProps) => Promise<any>显示抽屉

UseDrawerProps

属性名类型默认值说明
titlestring-抽屉标题
widthnumber | string-抽屉宽度
componentAsyncComponentLoader | Component-要显示的组件
componentPropsRecord<string, any>-传递给组件的属性
placement'top' | 'right' | 'bottom' | 'left''right'抽屉滑出方向

使用示例

基础抽屉

vue
<script setup>
import { useDrawer } from '@duxweb/dvha-pro'
import { NButton } from 'naive-ui'

const drawer = useDrawer()

// 显示用户详情抽屉
function showUserDetail(userId) {
  drawer.show({
    title: '用户详情',
    width: 500,
    placement: 'right',
    component: () => import('./components/UserDetail.vue'),
    componentProps: {
      userId
    }
  })
}

// 显示编辑表单抽屉
function showEditForm(data) {
  drawer.show({
    title: '编辑信息',
    width: 600,
    placement: 'right',
    component: () => import('./components/EditForm.vue'),
    componentProps: {
      data
    }
  }).then((result) => {
    console.log('编辑结果:', result)
  })
}
</script>

<template>
  <div class="flex gap-4">
    <NButton @click="showUserDetail(123)">
      查看详情
    </NButton>
    <NButton @click="showEditForm({ id: 1 })">
      编辑信息
    </NButton>
  </div>
</template>

不同方向的抽屉

vue
<script setup>
import { useDrawer } from '@duxweb/dvha-pro'
import { NButton } from 'naive-ui'

const drawer = useDrawer()

function showFromRight() {
  drawer.show({
    title: '右侧抽屉',
    width: 400,
    placement: 'right',
    component: () => import('./RightPanel.vue')
  })
}

function showFromLeft() {
  drawer.show({
    title: '左侧抽屉',
    width: 300,
    placement: 'left',
    component: () => import('./LeftPanel.vue')
  })
}

function showFromTop() {
  drawer.show({
    title: '顶部抽屉',
    width: '100%',
    placement: 'top',
    component: () => import('./TopPanel.vue')
  })
}

function showFromBottom() {
  drawer.show({
    title: '底部抽屉',
    width: '100%',
    placement: 'bottom',
    component: () => import('./BottomPanel.vue')
  })
}
</script>

<template>
  <div class="grid grid-cols-2 gap-4">
    <NButton @click="showFromRight">
      右侧抽屉
    </NButton>
    <NButton @click="showFromLeft">
      左侧抽屉
    </NButton>
    <NButton @click="showFromTop">
      顶部抽屉
    </NButton>
    <NButton @click="showFromBottom">
      底部抽屉
    </NButton>
  </div>
</template>

配置选项

placement - 抽屉位置

支持四个方向的抽屉滑出:

typescript
// 从右侧滑出(默认)
drawer.show({
  placement: 'right',
  component: SidePanel
})

// 从左侧滑出
drawer.show({
  placement: 'left',
  component: SideMenu
})

// 从顶部滑出
drawer.show({
  placement: 'top',
  component: NotificationPanel
})

// 从底部滑出
drawer.show({
  placement: 'bottom',
  component: ActionSheet
})

width - 抽屉宽度

支持固定宽度和响应式宽度:

typescript
// 固定宽度
drawer.show({
  width: 400,
  component: UserForm
})

// 百分比宽度
drawer.show({
  width: '50%',
  component: DataViewer
})

// 响应式宽度
drawer.show({
  width: 'min(600px, 80vw)',
  component: ResponsivePanel
})

完整示例

基础使用示例

vue
<script setup>
import { useDrawer } from '@duxweb/dvha-pro'
import { NButton } from 'naive-ui'

const drawer = useDrawer()

// 显示用户编辑抽屉
function showUserEdit(userId) {
  drawer.show({
    title: '编辑用户',
    width: 500,
    placement: 'right',
    component: () => import('./components/UserEditForm.vue'),
    componentProps: {
      userId
    }
  }).then((result) => {
    console.log('用户编辑结果:', result)
  }).catch(() => {
    console.log('用户取消了编辑')
  })
}

// 显示设置面板
function showSettings() {
  drawer.show({
    title: '系统设置',
    width: 600,
    placement: 'right',
    component: () => import('./components/SettingsPanel.vue')
  })
}

// 显示详情信息
function showDetails(itemId) {
  drawer.show({
    title: '详细信息',
    width: 800,
    placement: 'right',
    component: () => import('./components/ItemDetails.vue'),
    componentProps: {
      itemId
    }
  })
}
</script>

<template>
  <div class="flex gap-4">
    <NButton @click="showUserEdit(123)">
      编辑用户
    </NButton>
    <NButton @click="showSettings">
      系统设置
    </NButton>
    <NButton @click="showDetails(456)">
      查看详情
    </NButton>
  </div>
</template>

表单编辑抽屉

vue
<!-- UserEditForm.vue -->
<script setup>
import { DuxDrawerPage } from '@duxweb/dvha-pro'
import { NButton, NForm, NFormItem, NInput, NSelect, useMessage } from 'naive-ui'
import { ref } from 'vue'

const props = defineProps<{
  userId?: number
  onConfirm?: (data: any) => void
  onClose?: () => void
}>()

const message = useMessage()

const formData = ref({
  name: '',
  email: '',
  role: 'user',
  status: 'active'
})

const roleOptions = [
  { label: '管理员', value: 'admin' },
  { label: '普通用户', value: 'user' }
]

const statusOptions = [
  { label: '启用', value: 'active' },
  { label: '禁用', value: 'inactive' }
]

function handleSubmit() {
  // 表单验证
  if (!formData.value.name) {
    message.error('请输入用户名')
    return
  }

  if (!formData.value.email) {
    message.error('请输入邮箱')
    return
  }

  // 提交数据
  props.onConfirm?.(formData.value)
  message.success('保存成功')
}

function handleCancel() {
  props.onClose?.()
}
</script>

<template>
  <DuxDrawerPage>
    <template #default>
      <NForm :model="formData" label-placement="top">
        <NFormItem label="用户名" path="name">
          <NInput v-model:value="formData.name" placeholder="请输入用户名" />
        </NFormItem>
        <NFormItem label="邮箱" path="email">
          <NInput v-model:value="formData.email" placeholder="请输入邮箱" />
        </NFormItem>
        <NFormItem label="角色" path="role">
          <NSelect v-model:value="formData.role" :options="roleOptions" />
        </NFormItem>
        <NFormItem label="状态" path="status">
          <NSelect v-model:value="formData.status" :options="statusOptions" />
        </NFormItem>
      </NForm>
    </template>

    <template #action>
      <NButton @click="handleCancel">
        取消
      </NButton>
      <NButton type="primary" @click="handleSubmit">
        保存
      </NButton>
    </template>
  </DuxDrawerPage>
</template>

详情展示抽屉

vue
<!-- ItemDetails.vue -->
<script setup>
import { DuxDrawerPage } from '@duxweb/dvha-pro'
import { NDescriptions, NDescriptionsItem, NTag } from 'naive-ui'
import { ref } from 'vue'

const props = defineProps<{
  itemId?: number
  onClose?: () => void
}>()

const itemData = ref({
  id: 1,
  name: '示例商品',
  category: '电子产品',
  price: 999.99,
  status: 'active',
  description: '这是一个示例商品的详细描述信息...',
  createdAt: '2024-01-01 10:00:00',
  updatedAt: '2024-01-15 15:30:00'
})

function handleClose() {
  props.onClose?.()
}
</script>

<template>
  <DuxDrawerPage>
    <template #default>
      <div class="space-y-6">
        <div>
          <h3 class="text-lg font-semibold mb-4">
            基本信息
          </h3>
          <NDescriptions :column="1" bordered>
            <NDescriptionsItem label="商品ID">
              {{ itemData.id }}
            </NDescriptionsItem>
            <NDescriptionsItem label="商品名称">
              {{ itemData.name }}
            </NDescriptionsItem>
            <NDescriptionsItem label="分类">
              {{ itemData.category }}
            </NDescriptionsItem>
            <NDescriptionsItem label="价格">
              ¥{{ itemData.price }}
            </NDescriptionsItem>
            <NDescriptionsItem label="状态">
              <NTag :type="itemData.status === 'active' ? 'success' : 'error'">
                {{ itemData.status === 'active' ? '上架' : '下架' }}
              </NTag>
            </NDescriptionsItem>
          </NDescriptions>
        </div>

        <div>
          <h3 class="text-lg font-semibold mb-4">
            详细描述
          </h3>
          <div class="p-4 bg-gray-50 rounded">
            {{ itemData.description }}
          </div>
        </div>

        <div>
          <h3 class="text-lg font-semibold mb-4">
            时间信息
          </h3>
          <NDescriptions :column="1" bordered>
            <NDescriptionsItem label="创建时间">
              {{ itemData.createdAt }}
            </NDescriptionsItem>
            <NDescriptionsItem label="更新时间">
              {{ itemData.updatedAt }}
            </NDescriptionsItem>
          </NDescriptions>
        </div>
      </div>
    </template>

    <template #action>
      <NButton @click="handleClose">
        关闭
      </NButton>
    </template>
  </DuxDrawerPage>
</template>

高级用法

多步骤抽屉

vue
<script setup>
import { useDrawer } from '@duxweb/dvha-pro'

const drawer = useDrawer()

function showMultiStepDrawer() {
  drawer.show({
    title: '多步骤向导',
    width: 600,
    component: () => import('./components/MultiStepWizard.vue')
  }).then((result) => {
    console.log('向导完成:', result)
  })
}
</script>

嵌套抽屉

typescript
function showNestedDrawer() {
  // 第一层抽屉
  drawer.show({
    title: '用户列表',
    width: 500,
    component: UserList
  }).then(() => {
    // 第二层抽屉
    return drawer.show({
      title: '用户详情',
      width: 400,
      component: UserDetail
    })
  }).then(() => {
    console.log('嵌套抽屉操作完成')
  })
}

动态标题和宽度

typescript
function showDynamicDrawer(type: string) {
  const configs = {
    edit: {
      title: '编辑信息',
      width: 600,
      component: EditForm
    },
    view: {
      title: '查看详情',
      width: 400,
      component: ViewDetail
    },
    settings: {
      title: '系统设置',
      width: 800,
      component: SettingsPanel
    }
  }

  const config = configs[type]
  if (config) {
    drawer.show(config)
  }
}

最佳实践

1. 合理选择抽屉位置

typescript
// ✅ 推荐:根据内容选择合适的位置
drawer.show({ placement: 'right', ... }) // 表单编辑、详情展示
drawer.show({ placement: 'left', ... })  // 导航菜单、过滤器
drawer.show({ placement: 'top', ... })   // 通知、消息中心
drawer.show({ placement: 'bottom', ... }) // 操作面板、选择器

2. 合理设置抽屉宽度

typescript
// ✅ 推荐:根据内容设置合适的宽度
drawer.show({ width: 400 }) // 简单表单
drawer.show({ width: 600 }) // 复杂表单
drawer.show({ width: 800 }) // 详情展示
drawer.show({ width: '50%' }) // 响应式宽度

3. 正确处理组件通信

typescript
// ✅ 推荐:通过 componentProps 传递数据和回调
drawer.show({
  component: () => import('./EditForm.vue'),
  componentProps: {
    userId: 123,
    onSave: (data) => {
      // 处理保存逻辑
      console.log('保存数据:', data)
    },
    onCancel: () => {
      // 处理取消逻辑
      console.log('用户取消')
    }
  }
})

4. 合理使用异步组件

typescript
// ✅ 推荐:使用动态导入减少初始包大小
drawer.show({
  component: () => import('./components/LargeForm.vue')
})

5. 提供良好的用户体验

typescript
// ✅ 推荐:提供清晰的标题和操作反馈
drawer.show({
  title: '编辑用户信息', // 清晰的标题
  component: () => import('./UserForm.vue'),
  componentProps: {
    onSave: async (data) => {
      try {
        await saveUser(data)
        message.success('保存成功')
        // 关闭抽屉
      }
      catch (error) {
        message.error('保存失败')
      }
    }
  }
})

常见问题

Q: 如何在抽屉中使用表单验证?

A: 可以在抽屉组件内部使用 Naive UI 的表单验证功能:

vue
<script setup>
import { NForm, NFormItem, NInput } from 'naive-ui'
import { ref } from 'vue'

const formRef = ref()
const formData = ref({})

const rules = {
  name: { required: true, message: '请输入姓名' }
}

function handleSubmit() {
  formRef.value?.validate((errors) => {
    if (!errors) {
      // 验证通过,提交表单
    }
  })
}
</script>

<template>
  <NForm ref="formRef" :model="formData" :rules="rules">
    <NFormItem label="姓名" path="name">
      <NInput v-model:value="formData.name" />
    </NFormItem>
  </NForm>
</template>

Q: 如何实现抽屉的自动关闭?

A: 可以在组件中调用 onClose 回调:

typescript
// 在抽屉组件中
props.onClose?.() // 关闭抽屉

Q: 如何处理抽屉的数据传递?

A: 通过 componentProps 传递数据,通过回调函数返回结果:

typescript
drawer.show({
  componentProps: {
    data: inputData,
    onConfirm: (result) => {
      console.log('返回结果:', result)
    }
  }
})

Q: 如何实现抽屉的拖拽调整大小?

A: 抽屉组件内置了拖拽调整大小功能,可以通过 resizable 属性控制。