231 lines
7.1 KiB
Markdown
231 lines
7.1 KiB
Markdown
# Modal 弹窗
|
||
|
||
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
|
||
|
||
:::tip 说明
|
||
项目提供 `BasicModal` 组件(`src/components/Modal/src/basicModal.vue`)和原生 `a-modal` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
|
||
:::
|
||
|
||
## 组件架构
|
||
|
||
```
|
||
src/components/Modal/
|
||
├── src/
|
||
│ ├── basicModal.vue # 主组件(基于 a-modal)
|
||
│ ├── props.ts # 属性定义
|
||
│ ├── type/index.ts # 类型定义(ModalProps、ModalMethods)
|
||
│ ├── hooks/
|
||
│ │ ├── useModal.ts # 弹窗 Hook
|
||
│ │ └── useModalDrag.ts # 弹窗拖拽
|
||
│ └── components/
|
||
│ ├── Modal.tsx # 弹窗渲染组件
|
||
│ └── ModalClose.vue # 关闭按钮组件
|
||
└── index.ts # 导出 useModal 等
|
||
```
|
||
|
||
## BasicModal 组件
|
||
|
||
`BasicModal` 基于 `a-modal` 封装,提供全屏切换、自定义头部、提交按钮等能力:
|
||
|
||
```vue
|
||
<BasicModal title="编辑" :width="600" @register="registerModal" @ok="handleSubmit">
|
||
<BasicForm @register="registerForm" />
|
||
</BasicModal>
|
||
```
|
||
|
||
核心功能:
|
||
|
||
- 自定义头部(标题 + 全屏/关闭按钮)
|
||
- 提交按钮自动 loading(`setSubLoading`)
|
||
- `setProps` 动态修改弹窗属性
|
||
- `openModal` / `closeModal` 控制显隐
|
||
|
||
## 实际用法(position 模块)
|
||
|
||
以岗位管理为例,实际使用原生 `a-modal` + `defineAsyncComponent` 懒加载:
|
||
|
||
### 父组件(index.vue)
|
||
|
||
```vue
|
||
<!-- src/views/system/position/index.vue -->
|
||
<script setup>
|
||
import { defineAsyncComponent } from 'vue';
|
||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||
|
||
const positionId = ref(0);
|
||
const editVisible = ref(false);
|
||
|
||
// 新增
|
||
const handleAdd = async () => {
|
||
positionId.value = 0;
|
||
await nextTick();
|
||
editVisible.value = true;
|
||
};
|
||
|
||
// 编辑
|
||
const handleEdit = async (id: number) => {
|
||
positionId.value = id;
|
||
await nextTick();
|
||
editVisible.value = true;
|
||
};
|
||
</script>
|
||
|
||
<template>
|
||
<editDialog
|
||
v-if="editVisible"
|
||
:positionId="positionId"
|
||
v-model:visible="editVisible"
|
||
@success="reloadTable('noRefresh')"
|
||
/>
|
||
</template>
|
||
```
|
||
|
||
`defineAsyncComponent` 实现懒加载:首屏不加载弹窗组件,首次打开时才加载,加载完成后缓存。
|
||
|
||
### 编辑弹窗(edit.vue)
|
||
|
||
```vue
|
||
<!-- src/views/system/position/edit.vue -->
|
||
<template>
|
||
<a-modal
|
||
v-model:visible="props.visible"
|
||
:title="props.positionId ? '编辑' : '新增'"
|
||
width="500px"
|
||
@cancel="dialogClose"
|
||
>
|
||
<a-form class="ls-form" ref="formRef" :model="formData" :label-col="{ style: { width: '85px' } }">
|
||
<a-form-item
|
||
label="岗位名称"
|
||
name="name"
|
||
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
|
||
>
|
||
<a-input v-model:value="formData.name" placeholder="请输入岗位名称" allow-clear />
|
||
</a-form-item>
|
||
<a-form-item label="岗位状态" name="status">
|
||
<a-radio-group v-model:value="formData.status" name="status">
|
||
<a-radio :value="1">正常</a-radio>
|
||
<a-radio :value="2">停用</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
<a-form-item label="排序" name="sort">
|
||
<a-input-number v-model:value="formData.sort" />
|
||
</a-form-item>
|
||
</a-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<a-button @click="dialogClose">取消</a-button>
|
||
<a-button :loading="subLoading" type="primary" @click="submit">确定</a-button>
|
||
</span>
|
||
</template>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import type { FormInstance } from 'ant-design-vue';
|
||
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
|
||
import { useLockFn } from '@/utils/useLockFn';
|
||
import { message } from 'ant-design-vue';
|
||
|
||
const emit = defineEmits(['success', 'update:visible']);
|
||
const formRef = shallowRef<FormInstance>();
|
||
|
||
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 handleSubmit = async () => {
|
||
await formRef.value?.validate();
|
||
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
|
||
message.success('操作成功');
|
||
emit('update:visible', false);
|
||
emit('success');
|
||
};
|
||
|
||
// 关闭弹窗
|
||
const dialogClose = () => { emit('update:visible', false); };
|
||
|
||
// useLockFn 防重复提交,isLock 绑定按钮 loading
|
||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||
|
||
// 编辑时加载数据
|
||
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();
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 弹窗数据流
|
||
|
||
```
|
||
父组件点击编辑 → positionId = id → editVisible = true
|
||
↓
|
||
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
|
||
↓
|
||
用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData)
|
||
↓
|
||
emit('success') → 父组件 reloadTable() → 表格刷新
|
||
```
|
||
|
||
## 确认弹窗
|
||
|
||
删除等危险操作使用 Ant Design Vue 的 `Modal.confirm`:
|
||
|
||
```typescript
|
||
import { Modal, message } from 'ant-design-vue';
|
||
|
||
async function handleDelete(id) {
|
||
let ids = [];
|
||
if (!id) {
|
||
ids = selectionData.value.map(({ id }) => id);
|
||
}
|
||
|
||
Modal.confirm({
|
||
title: '提示',
|
||
content: '确定要删除?',
|
||
onOk: async () => {
|
||
id ? await positionDelete(id) : await positionBatchDelete(ids);
|
||
message.success('删除成功');
|
||
reloadTable();
|
||
},
|
||
});
|
||
}
|
||
```
|
||
|
||
`Modal.confirm` 返回一个引用,用户点击取消时不会执行 `onOk` 回调。
|
||
|
||
## v-model:visible 双向绑定
|
||
|
||
弹窗使用 `v-model:visible` 实现父子组件双向绑定:
|
||
|
||
```vue
|
||
<!-- 父组件 -->
|
||
<editDialog v-model:visible="editVisible" />
|
||
|
||
<!-- 子组件 -->
|
||
<script setup>
|
||
const emit = defineEmits(['update:visible']);
|
||
const dialogClose = () => { emit('update:visible', false); };
|
||
</script>
|
||
```
|
||
|
||
:::tip 温馨提示
|
||
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useLockFn` 将 `isLock` 绑定按钮的 `:loading` 属性,防止重复提交。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。
|
||
:::
|
||
|
||
## 总结
|
||
|
||
编辑弹窗使用 `a-modal` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。确认弹窗使用 Ant Design Vue 的 `Modal.confirm`。
|