270 lines
8.5 KiB
Markdown
270 lines
8.5 KiB
Markdown
# 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')` 通知父组件刷新表格。
|