Files
djangoadmin-wiki/DjangoAdmin_Django_NaiveVue/zh/volume4/frontend/modal.md
T
2026-09-13 18:12:07 +08:00

270 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Modal 弹窗
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
:::tip 说明
项目使用 `basicModal` 组件(`src/components/Modal/src/BasicModal.vue`)配合 `useModal` hook 实现弹窗。编辑弹窗通过 `v-if` 懒加载,减少首屏渲染负担。
:::
## 组件架构
```
src/components/Modal/
├── src/
│ ├── BasicModal.vue # 主组件(基于 NaiveUI n-modal)
│ ├── props.ts # 属性定义
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
│ └── hooks/
│ └── useModalContext.ts # 弹窗上下文
└── index.ts # 导出 useModal 等
```
## basicModal 组件
`basicModal` 基于 NaiveUI `n-modal` 封装,通过 `useModal` hook 管理弹窗状态,提供全屏切换、自定义头部、自动 OK/Cancel 按钮等能力:
```vue
<basicModal @register="modalRegister" ref="modalRef" class="basicModal basicFormModal" @on-ok="handleSubmit" @on-close="handleClose">
<template #default>
<n-form ref="formRef" :model="formData" label-placement="left" label-width="85px">
...
</n-form>
</template>
</basicModal>
```
```typescript
const [modalRegister, { openModal, setSubLoading }] = useModal({
title: '编辑',
subBtuText: '确定',
width: 600,
});
```
核心功能:
- `useModal` hook 返回 `modalRegister`(绑定到组件 `@register`)和操作方法
- `openModal` 打开弹窗、`setSubLoading` 控制提交按钮 loading
- 自动渲染 OK/Cancel 底部按钮,无需手动编写 footer 模板
- `@on-ok` 监听确定按钮点击,`@on-close` 监听关闭事件
## 实际用法(position 模块)
以岗位管理为例,使用 `basicModal` + `useModal` hook 管理弹窗状态。
### 父组件(index.vue)
```vue
<!-- src/views/system/position/index.vue -->
<script setup>
import { h, nextTick, reactive, ref } from 'vue';
import { useMessage, useDialog } from 'naive-ui';
import editDialog from './edit.vue';
const message = useMessage();
const dialog = useDialog();
const createModalRef = ref();
const editVisible = ref(false);
const positionId = ref(0);
// 新增
const handleAdd = async () => {
positionId.value = 0;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
// 编辑
const handleEdit = async (record: Recordable) => {
positionId.value = record.id;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
// 删除
async function handleDelete(record) {
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record.id ? await positionDelete(record.id) : await positionBatchDelete(rowKeys.value);
message.success('删除成功');
reloadTable();
},
});
}
</script>
<template>
<editDialog
ref="createModalRef"
:positionId="positionId"
v-if="editVisible"
v-model:visible="editVisible"
@success="reloadTable('noRefresh')"
/>
</template>
```
父组件通过 `ref` 获取子组件实例,调用 `createModalRef.value.openModal()` 打开弹窗。子组件通过 `defineExpose({ openModal })` 暴露该方法。
### 编辑弹窗(edit.vue)
```vue
<!-- src/views/system/position/edit.vue -->
<template>
<basicModal
@register="modalRegister"
ref="modalRef"
class="basicModal basicFormModal"
@on-ok="handleSubmit"
@on-close="handleClose"
>
<template #default>
<n-form
class="ls-form"
ref="formRef"
:model="formData"
label-placement="left"
label-width="85px"
>
<n-form-item
label="岗位名称"
path="name"
:rule="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
>
<n-input v-model:value="formData.name" placeholder="请输入岗位名称" clearable />
</n-form-item>
<n-form-item label="岗位状态" path="code">
<n-radio-group v-model:value="formData.status" name="status">
<n-radio :value="1">正常</n-radio>
<n-radio :value="2">停用</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formData.sort" />
</n-form-item>
</n-form>
</template>
</basicModal>
</template>
<script lang="ts" setup>
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
import { onMounted, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useModal } from '@/components/Modal';
const emit = defineEmits(['success', 'update:visible']);
const formRef = ref();
const message = useMessage();
const formData = reactive({ id: '', name: '', status: 1, sort: 0 });
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
positionId: { type: Number, required: true, default: 0 },
});
// 定义模态框
const [modalRegister, { openModal, setSubLoading }] = useModal({
title: props.positionId ? '编辑岗位' : '添加岗位',
subBtuText: '确定',
width: 600,
});
// 提交处理
const handleSubmit = async () => {
formRef.value
.validate()
.then(async () => {
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message.success('操作成功');
setSubLoading(false);
emit('update:visible', false);
emit('success');
})
.catch((error) => {
setSubLoading(false);
});
};
// 关闭弹窗
const handleClose = () => { emit('update:visible', false); };
// 编辑时加载数据
const setFormData = async () => {
const data = await getPositionDetail(props.positionId);
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
formData[key] = data[key];
}
}
};
onMounted(() => {
if (props.positionId) setFormData();
});
defineExpose({ openModal });
</script>
```
## 弹窗数据流
```
父组件点击编辑 → positionId = record.id → editVisible = true → nextTick → createModalRef.openModal()
↓
basicModal 显示 → useModal 配置标题和按钮
↓
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
↓
用户点击确定 → @on-ok → handleSubmit → formRef.validate() → positionUpdate(formData) → setSubLoading(false)
↓
emit('success') → 父组件 reloadTable() → 表格刷新
```
## 确认弹窗
删除等危险操作使用 NaiveUI 的 `useDialog` 组合式函数:
```typescript
import { useMessage, useDialog } from 'naive-ui';
const message = useMessage();
const dialog = useDialog();
async function handleDelete(record: Recordable) {
let ids = [];
if (!record) {
ids = selectionData.value.map(({ id }) => id);
}
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record ? await positionDelete(record.id) : await positionBatchDelete(ids);
message.success('删除成功');
reloadTable();
},
});
}
```
`dialog.warning` 返回一个对话框实例,用户点击"确定"后执行 `onPositiveClick` 回调。与旧版 `confirm` 不同,不需要 `await` 和 `try/catch`,逻辑全部写在回调中。
:::tip 温馨提示
弹窗组件通过 `v-if` 控制懒加载,首屏不加载弹窗代码。`useModal` 的 `setSubLoading` 控制确定按钮 loading 状态,防止重复提交。`message.success` 和 `dialog.warning` 来自 NaiveUI 的 `useMessage()` 和 `useDialog()` 组合式函数,需要在 `setup` 中调用获取实例。
:::
## 总结
编辑弹窗使用 `basicModal` + `useModal` hook 管理弹窗状态,通过 `defineExpose({ openModal })` 暴露给父组件调用。`useModal` 返回 `modalRegister` 绑定到 `@register`,以及 `openModal`、`setSubLoading` 等操作方法。表单使用 NaiveUI 的 `n-form` 系列组件,绑定方式为 `v-model:value`,校验字段用 `path` 属性。删除确认使用 `useDialog` 的 `dialog.warning` 回调模式。操作成功后通过 `emit('success')` 通知父组件刷新表格。