Skip to content

Modal 弹窗 ​

说明

项目使用 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,逻辑全部写在回调中。

温馨提示

弹窗组件通过 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') 通知父组件刷新表格。

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