优化更新

This commit is contained in:
2026-09-13 18:12:07 +08:00
parent 7f758cc6e9
commit d7a081e8b7
92 changed files with 2429 additions and 1117 deletions
@@ -3,7 +3,7 @@
<!-- Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 -->
:::tip 说明
项目提供 `BasicModal` 组件(`src/components/Modal/src/BasicModal.vue`)和原生 `el-dialog` 两种弹窗用法。编辑弹窗通过 `defineAsyncComponent` 懒加载,减少首屏渲染负担。
项目使用 `basicModal` 组件(`src/components/Modal/src/BasicModal.vue`)配合 `useModal` hook 实现弹窗。编辑弹窗通过 `v-if` 懒加载,减少首屏渲染负担。
:::
## 组件架构
@@ -11,7 +11,7 @@
```
src/components/Modal/
├── src/
│ ├── BasicModal.vue # 主组件(基于 el-dialog)
│ ├── BasicModal.vue # 主组件(基于 NaiveUI n-modal)
│ ├── props.ts # 属性定义
│ ├── type.ts # 类型定义(ModalProps、ModalMethods)
│ └── hooks/
@@ -19,108 +19,149 @@ src/components/Modal/
└── index.ts # 导出 useModal 等
```
## BasicModal 组件
## basicModal 组件
`BasicModal` 基于 `el-dialog` 封装,提供全屏切换、自定义头部、提交按钮等能力:
`basicModal` 基于 NaiveUI `n-modal` 封装,通过 `useModal` hook 管理弹窗状态,提供全屏切换、自定义头部、自动 OK/Cancel 按钮等能力:
```vue
<BasicModal :title="'编辑'" :width="600" @register="registerModal" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
<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,
});
```
核心功能:
- 自定义头部(标题 + 全屏/关闭按钮)
- 提交按钮自动 loading(`setSubLoading`)
- `setProps` 动态修改弹窗属性
- `openModal` / `closeModal` 控制显隐
- `useModal` hook 返回 `modalRegister`(绑定到组件 `@register`)和操作方法
- `openModal` 打开弹窗、`setSubLoading` 控制提交按钮 loading
- 自动渲染 OK/Cancel 底部按钮,无需手动编写 footer 模板
- `@on-ok` 监听确定按钮点击,`@on-close` 监听关闭事件
## 实际用法(position 模块)
以岗位管理为例,实际使用原生 `el-dialog` + `defineAsyncComponent` 懒加载:
以岗位管理为例,使用 `basicModal` + `useModal` hook 管理弹窗状态。
### 父组件(index.vue)
```vue
<!-- src/views/system/position/index.vue -->
<script setup>
import { defineAsyncComponent } from 'vue';
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
import { h, nextTick, reactive, ref } from 'vue';
import { useMessage, useDialog } from 'naive-ui';
import editDialog from './edit.vue';
const positionId = ref(0);
const message = useMessage();
const dialog = useDialog();
const createModalRef = ref();
const editVisible = ref(false);
const positionId = ref(0);
// 新增
const handleAdd = async () => {
positionId.value = 0;
await nextTick();
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
// 编辑
const handleEdit = async (record: Recordable) => {
positionId.value = record.row.id;
await nextTick();
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
v-if="editVisible"
ref="createModalRef"
:positionId="positionId"
v-if="editVisible"
v-model:visible="editVisible"
@success="reloadTable('noRefresh')"
/>
</template>
```
`defineAsyncComponent` 实现懒加载:首屏不加载弹窗组件,首次打开时才加载,加载完成后缓存。
父组件通过 `ref` 获取子组件实例,调用 `createModalRef.value.openModal()` 打开弹窗。子组件通过 `defineExpose({ openModal })` 暴露该方法。
### 编辑弹窗(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"
<basicModal
@register="modalRegister"
ref="modalRef"
class="basicModal basicFormModal"
@on-ok="handleSubmit"
@on-close="handleClose"
>
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
<el-form-item
label="岗位名称"
prop="name"
:rules="{ required: true, message: '请输入岗位名称', trigger: 'blur' }"
<template #default>
<n-form
class="ls-form"
ref="formRef"
:model="formData"
label-placement="left"
label-width="85px"
>
<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>
<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>
</el-dialog>
</basicModal>
</template>
<script lang="ts" setup>
import { getPositionDetail, positionAdd, positionUpdate } from '@/api/system/position';
import { useLockFn } from '@/utils/useLockFn';
import { onMounted, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useModal } from '@/components/Modal';
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
const formRef = ref();
const message = useMessage();
const formData = reactive({ id: '', name: '', status: 1, sort: 0 });
@@ -129,20 +170,31 @@ const props = defineProps({
positionId: { type: Number, required: true, default: 0 },
});
// 定义模态框
const [modalRegister, { openModal, setSubLoading }] = useModal({
title: props.positionId ? '编辑岗位' : '添加岗位',
subBtuText: '确定',
width: 600,
});
// 提交处理
const handleSubmit = async () => {
await formRef.value?.validate();
props.positionId ? await positionUpdate(formData) : await positionAdd(formData);
message('操作成功');
emit('update:visible', false);
emit('success');
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 dialogClose = () => { emit('update:visible', false); };
// useLockFn 防重复提交,isLock 绑定按钮 loading
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
const handleClose = () => { emit('update:visible', false); };
// 编辑时加载数据
const setFormData = async () => {
@@ -157,27 +209,34 @@ const setFormData = async () => {
onMounted(() => {
if (props.positionId) setFormData();
});
defineExpose({ openModal });
</script>
```
## 弹窗数据流
```
父组件点击编辑 → positionId = record.row.id → editVisible = true
父组件点击编辑 → positionId = record.id → editVisible = true → nextTick → createModalRef.openModal()
↓
basicModal 显示 → useModal 配置标题和按钮
↓
edit.vue onMounted → positionId != 0 → setFormData() → 获取详情填充表单
↓
用户修改 → submit → useLockFn(防重复) → formRef.validate() → positionUpdate(formData)
用户点击确定 → @on-ok → handleSubmit → formRef.validate() → positionUpdate(formData) → setSubLoading(false)
↓
emit('success') → 父组件 reloadTable() → 表格刷新
```
## 确认弹窗
删除等危险操作使用 `confirm` 封装函数(来自 `src/utils/auth.ts`):
删除等危险操作使用 NaiveUI 的 `useDialog` 组合式函数:
```typescript
import { message, confirm } from '@/utils/auth';
import { useMessage, useDialog } from 'naive-ui';
const message = useMessage();
const dialog = useDialog();
async function handleDelete(record: Recordable) {
let ids = [];
@@ -185,34 +244,26 @@ async function handleDelete(record: Recordable) {
ids = selectionData.value.map(({ id }) => id);
}
await confirm('确定要删除?');
record ? await positionDelete(record.row.id) : await positionBatchDelete(ids);
message('删除成功');
reloadTable();
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
record ? await positionDelete(record.id) : await positionBatchDelete(ids);
message.success('删除成功');
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>
```
`dialog.warning` 返回一个对话框实例,用户点击"确定"后执行 `onPositiveClick` 回调。与旧版 `confirm` 不同,不需要 `await` 和 `try/catch`,逻辑全部写在回调中。
:::tip 温馨提示
弹窗组件通过 `defineAsyncComponent(() => import('./edit.vue'))` 懒加载,首屏不加载弹窗代码。`useLockFn` 将 `isLock` 绑定按钮的 `:loading` 属性,防止重复提交。关闭弹窗时通过 `v-model:visible` 自动同步父子状态。
弹窗组件通过 `v-if` 控制懒加载,首屏不加载弹窗代码。`useModal` 的 `setSubLoading` 控制确定按钮 loading 状态,防止重复提交。`message.success` 和 `dialog.warning` 来自 NaiveUI 的 `useMessage()` 和 `useDialog()` 组合式函数,需要在 `setup` 中调用获取实例。
:::
## 总结
编辑弹窗使用 `el-dialog` + `defineAsyncComponent` 懒加载,通过 `v-model:visible` 双向绑定显隐状态,`useLockFn` 防重复提交。父组件通过 `positionId` prop 传递编辑行 ID,子组件在 `onMounted` 中判断并加载详情数据。操作成功后 `emit('success')` 通知父组件刷新表格。
编辑弹窗使用 `basicModal` + `useModal` hook 管理弹窗状态,通过 `defineExpose({ openModal })` 暴露给父组件调用。`useModal` 返回 `modalRegister` 绑定到 `@register`,以及 `openModal`、`setSubLoading` 等操作方法。表单使用 NaiveUI 的 `n-form` 系列组件,绑定方式为 `v-model:value`,校验字段用 `path` 属性。删除确认使用 `useDialog` 的 `dialog.warning` 回调模式。操作成功后通过 `emit('success')` 通知父组件刷新表格。