Skip to content

Modal 弹窗

说明

项目提供 BasicModal 组件(src/components/Modal/src/BasicModal.vue)和原生 el-dialog 两种弹窗用法。编辑弹窗通过 defineAsyncComponent 懒加载,减少首屏渲染负担。

组件架构

src/components/Modal/
├── src/
│ ├── BasicModal.vue     # 主组件(基于 el-dialog)
│ ├── props.ts           # 属性定义
│ ├── type.ts            # 类型定义(ModalProps、ModalMethods)
│ └── hooks/
│ └── useModalContext.ts # 弹窗上下文
└── index.ts             # 导出 useModal 等

BasicModal 组件

BasicModal 基于 el-dialog 封装,提供全屏切换、自定义头部、提交按钮等能力:

vue
<BasicModal :title="'编辑'" :width="600" @register="registerModal" @ok="handleSubmit">
    <BasicForm @register="registerForm" />
</BasicModal>

核心功能:

  • 自定义头部(标题 + 全屏/关闭按钮)
  • 提交按钮自动 loading(setSubLoading
  • setProps 动态修改弹窗属性
  • openModal / closeModal 控制显隐

实际用法(position 模块)

以岗位管理为例,实际使用原生 el-dialog + 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 (record: Recordable) => {
    positionId.value = record.row.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>
    <el-dialog
        v-model="props.visible"
        :title="props.positionId ? '编辑' : '新增'"
        width="500"
        :close-on-click-modal="false"
        :before-close="dialogClose"
    >
        <el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
            <el-form-item
                label="岗位名称"
                prop="name"
                :rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
            >
                <el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
            </el-form-item>
            <el-form-item label="岗位状态" prop="status">
                <el-radio-group v-model="formData.status">
                    <el-radio :value="1">正常</el-radio>
                    <el-radio :value="2">停用</el-radio>
                </el-radio-group>
            </el-form-item>
            <el-form-item label="排序" prop="sort">
                <el-input-number v-model="formData.sort" />
            </el-form-item>
        </el-form>
        <template #footer>
            <el-button @click="dialogClose">取消</el-button>
            <el-button :loading="subLoading" type="primary" @click="submit">确定</el-button>
        </template>
    </el-dialog>
</template>

<script lang="ts" setup>
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
import { useLockFn } from '@/utils/useLockFn';

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('操作成功');
    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 = record.row.id → editVisible = true

edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单

用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData)

emit('success') → 父组件 reloadTable() → 表格刷新

确认弹窗

删除等危险操作使用 confirm 封装函数(来自 src/utils/auth.ts):

typescript
import { message, confirm } from '@/utils/auth';

async function handleDelete(record: Recordable) {
    let ids = [];
    if (!record) {
        ids = selectionData.value.map(({ id }) => id);
    }

    await confirm('确定要删除?');
    record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
    message('删除成功');
    reloadTable();
}

confirm 内部调用 ElMessageBox.confirm,返回 Promise,用户取消时抛出异常中断后续逻辑。

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>

温馨提示

弹窗组件通过 defineAsyncComponent(() => import('./edit.vue')) 懒加载,首屏不加载弹窗代码。useLockFnisLock 绑定按钮的 :loading 属性,防止重复提交。关闭弹窗时通过 v-model:visible 自动同步父子状态。

总结

编辑弹窗使用 el-dialog + defineAsyncComponent 懒加载,通过 v-model:visible 双向绑定显隐状态,useLockFn 防重复提交。父组件通过 positionId prop 传递编辑行 ID,子组件在 onMounted 中判断并加载详情数据。操作成功后 emit('success') 通知父组件刷新表格。

小蚂蚁云团队 · 提供技术支持