Files
djangoadmin-wiki/DjangoAdmin_Flask_NaiveVue/zh/volume4/frontend/modal.md
T
2026-09-13 23:01:19 +08:00

241 lines
7.1 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 弹窗
:::tip 说明
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `basicModal` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
:::
## 组件架构
```
src/components/Modal/
├── src/
│ ├── BasicModal.vue # 主组件(基于 n-modal)
│ ├── props.ts # 属性定义
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
│ └── hooks/
│ └── useModalContext.ts # 弹窗上下文
└── index.ts # 导出 useModal 等
```
## BasicModal 组件
`BasicModal` 基于 NaiveUI 的 `n-modal` 封装,提供全屏切换、自定义头部、提交按钮等能力:
```vue
<basicModal title="编辑" :width="600" @register="registerModal" @on-ok="handleSubmit">
<BasicForm @register="registerForm" />
</basicModal>
```
核心功能:
- 自定义头部(标题 + 全屏/关闭按钮)
- 提交按钮自动 loading(`setSubLoading`)
- `setProps` 动态修改弹窗属性
- `openModal` / `closeModal` 控制显隐
## 实际用法(position 模块)
以岗位管理为例,使用 `basicModal` + `useModal` hook:
### 父组件(index.vue)
```vue
<!-- src/views/system/position/index.vue -->
<script setup>
import { nextTick, ref } from 'vue';
import editDialog from './edit.vue';
const positionId = ref(0);
const editVisible = ref(false);
const createModalRef = ref();
// 新增
const handleAdd = async () => {
positionId.value = 0;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
// 编辑
async function handleEdit(record) {
positionId.value = record.id;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
}
</script>
<template>
<editDialog
ref="createModalRef"
v-if="editVisible"
:positionId="positionId"
v-model:visible="editVisible"
@success="reloadTable('noRefresh')"
/>
</template>
```
### 编辑弹窗(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="status">
<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(() => {
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
↓
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
↓
用户修改 → submit → formRef.validate() → positionUpdate(formData)
↓
emit('success') → 父组件 reloadTable() → 表格刷新
```
## 确认弹窗
删除等危险操作使用 NaiveUI 的 `useDialog`:
```typescript
import { useMessage, useDialog } from 'naive-ui';
const message = useMessage();
const dialog = useDialog();
async function handleDelete(record) {
let ids = [];
if (!record) {
ids = rowKeys.value;
}
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record?.id ? await positionDelete(record.id) : await positionBatchDelete(ids);
message.success('删除成功');
reloadTable();
},
});
}
```
`dialog.warning()` 是 NaiveUI 的命令式对话框,返回 Promise,用户取消时不执行 `onPositiveClick` 回调。
## v-model:visible 双向绑定
弹窗使用 `v-model:visible` 实现父子组件双向绑定:
```vue
<!-- 父组件 -->
<editDialog v-model:visible="editVisible" />
<!-- 子组件 -->
<script setup>
const emit = defineEmits(['update:visible']);
const handleClose = () => { emit('update:visible', false); };
</script>
```
:::tip 温馨提示
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useModal` hook 提供 `openModal`、`closeModal`、`setSubLoading` 等方法。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。消息提示使用 `useMessage()`,确认弹窗使用 `useDialog()`。
:::
## 总结
编辑弹窗使用 `basicModal`(基于 `n-modal`)+ `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。消息提示使用 `useMessage()`,确认对话框使用 `useDialog()`。