fdialogsmz
v1.0.14
Published
函数式弹窗、抽屉,无内容版
Downloads
104
Readme
概述
这是一个基于 Vue 3 + TypeScript + Element Plus 构建的函数式弹窗组件,支持动态传入任意Vue组件作为内容,提供丰富的配置选项和灵活的API。
1.0.6: 优化
| 维度 | 优化前 | 优化后 |
| ----- | ------------------ | ----------------- |
| 多实例 | 后者冲掉前者 | 支持任意数量共存 |
| 内存泄漏 | DOM/事件未清理 | 统一 unmount & 删除节点 |
| 类型安全 | 大量 any | 全部严格类型推导 |
| 态竞 | 全局 buttonLoading | 事件级 Set |
安装和引入
- 引入组件
import FDialog from 'fdialog'
import 'fdialog/dist/main.css'
app.use(FDialog);- 引入弹窗组件
import { createModal, ModalHelper } from 'fdialog'使用
- 创建简单弹窗
const BasicComponent = {
setup() {
return () => (
<div style="padding: 20px;">
<p>这是一个简单的弹窗内容</p>
</div>
)
}
}
const showModal = async () => {
const result = await createModal({
component: BasicComponent,
modalProps: {
title: '基础弹窗',
width: '500px'
}
})
console.log('弹窗结果:', result)
}- 使用ModalHelper工具类
// 确认对话框
const result = await ModalHelper.confirm('确定要执行此操作吗?')
// 成功提示
await ModalHelper.success('操作成功完成!')
// 警告提示
await ModalHelper.warning('请注意操作风险')
// 错误提示
await ModalHelper.error('系统发生错误')配置选项
| 参数 | 类型 | 默认值 | 说明 |
|------|------|------------|-----------|
|visible| boolean| false | 是否显示弹窗 |
|title| string| '提示' | 弹窗标题 |
|titleIcon| IconType| - | 标题图标类型 |
|customIconUrl| string| - | 自定义图标URL |
|width| string| '50%' | 弹窗宽度 |
|showFooter| boolean| true | 是否显示底部按钮 |
|footerButtons| ModalButton[]| 取消/确定 | 底部按钮配置 |
|footerPosition| FooterPosition | 'center' | 按钮位置 |
|closeOnClickModal| boolean | true | 点击遮罩关闭 |
|showClose| boolean| true | 显示关闭按钮 |
|headerBackground| string | '#f2f2f2' | 头部背景色 |
|headerIconSize| string | '24px' | 图标大小 |
|headerSlotPosition| HeaderPosition | 'left' | 头部插槽位置 |
IconType 类型
type IconType = 'add' | 'dialog' | 'success' | 'warning' | 'error' | 'info' | 'custom' | stringFooterPosition 类型
type FooterPosition = 'left' | 'center' | 'right' | 'stretch'HeaderPosition 类型
type HeaderPosition = 'left' | 'center' | 'right' | 'stretch'高级用法
- 自定义内容组件
<template>
<div class="user-form">
<h3>用户信息</h3>
<el-input v-model="name" placeholder="请输入姓名" />
<el-input v-model="email" placeholder="请输入邮箱" />
<div class="actions">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
onEvent?: (event: string, data?: any) => void
onConfirm?: (data: any) => void
onCancel?: () => void
}>()
const name = ref('')
const email = ref('')
const handleSubmit = () => {
if (!name.value) {
props.onEvent?.('validation-error', { message: '姓名不能为空' })
return
}
props.onConfirm?.({
name: name.value,
email: email.value
})
}
const handleCancel = () => {
props.onCancel?.()
}
</script>- 复杂弹窗配置
const headerButton = {
template: `<el-button type="primary" size="small">操作</el-button>`
}
const showComplexModal = async () => {
const result = await createModal({
component: UserForm,
componentProps: {
initialData: { name: '张三', email: '' }
},
modalProps: {
title: '用户信息编辑',
titleIcon: 'user',
width: '600px',
headerBackground: '#f0f9eb',
headerIconSize: '28px',
footerPosition: 'right',
footerButtons: [
{ text: '取消', type: '', event: 'cancel', icon: 'info' },
{ text: '保存草稿', type: 'info', event: 'draft', icon: 'info' },
{ text: '确认提交', type: 'primary', event: 'submit', icon: 'success' }
]
},
headerSlot: headerButton,
onComponentEvent: (event, data) => {
console.log('收到事件:', event, data)
// 处理自定义事件
}
})
}- 事件处理
// 在内容组件中发射事件
props.onEvent?.('custom-event', { data: '自定义数据' })
// 在外部监听事件
onComponentEvent: (event, data) => {
switch (event) {
case 'custom-event':
console.log('收到自定义事件:', data)
break
case 'validation-error':
ElMessage.warning(data.message)
break
}
}- 内容组件事件传递
const props = defineProps<{
onEvent?: (event: string, data?: any) => void;
emit?: (event: string, data?: any) => void;
onClose?: (result?: any) => void;
onConfirm?: (data?: any) => void;
onCancel?: () => void;
}>();
// 方法1:使用props传递的回调
const handleSave = () => {
props.onEvent?.("validation-error", { field: "name", message: "姓名不能为空" });
return;
};
// 方法2:发送自定义事件
const handleCustomEvent = () => {
props.onEvent?.("custom-action", {
action: "refresh",
timestamp: new Date().toISOString()
});
};
// 方法3:使用emit别名(更符合Vue习惯)
const handleEmitExample = () => {
props.emit?.("data-updated", "这是emit事件传递参数");
};
// 暴露给弹窗组件的方法
const getData = async () => {
if (!formRef.value) {
throw new Error('表单引用未初始化')
}
// 验证表单
const isValid = await formRef.value.validate()
if (!isValid) {
throw new Error('表单验证失败')
}
return {
...form.value,
}
}
// 获取原始数据(不验证)
const getRawData = () => {
return form.value
}
// 暴露方法给弹窗组件调用
defineExpose({
getData,
validate,
getRawData,
formData: form.value
})- 表单验证
- 内容组件:
<script setup lang="ts">
import { reactive, ref } from "vue";
import type { FormInstance, FormRules } from "element-plus";
interface RuleForm {
name: string;
region: string;
count: string;
date1: string;
date2: string;
delivery: boolean;
location: string;
type: string[];
resource: string;
desc: string;
}
const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive<RuleForm>({
name: "Hello",
region: "",
count: "",
date1: "",
date2: "",
delivery: false,
location: "",
type: [],
resource: "",
desc: ""
});
const locationOptions = ["Home", "Company", "School"];
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: "Please input Activity name", trigger: "blur" },
{ min: 3, max: 5, message: "Length should be 3 to 5", trigger: "blur" }
],
region: [
{
required: true,
message: "Please select Activity zone",
trigger: "change"
}
],
count: [
{
required: true,
message: "Please select Activity count",
trigger: "change"
}
],
date1: [
{
type: "date",
required: true,
message: "Please pick a date",
trigger: "change"
}
],
date2: [
{
type: "date",
required: true,
message: "Please pick a time",
trigger: "change"
}
],
location: [
{
required: true,
message: "Please select a location",
trigger: "change"
}
],
type: [
{
type: "array",
required: true,
message: "Please select at least one activity type",
trigger: "change"
}
],
resource: [
{
required: true,
message: "Please select activity resource",
trigger: "change"
}
],
desc: [{ required: true, message: "Please input activity form", trigger: "blur" }]
});
const options = Array.from({ length: 10000 }).map((_, idx) => ({
value: `${idx + 1}`,
label: `${idx + 1}`
}));
// 暴露验证方法
const validate = async () => {
await ruleFormRef.value?.validate();
};
defineExpose({
validate
});
</script>
<template>
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="handleCustomEvent">发送自定义事件</el-button>
<el-button @click="handleEmitExample">发送别名事件</el-button>
<el-form ref="ruleFormRef" style="max-width: 600px" :model="ruleForm" :rules="rules" label-width="auto">
<el-form-item label="Activity name" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="Activity zone" prop="region">
<el-select v-model="ruleForm.region" placeholder="Activity zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity count" prop="count">
<el-select-v2 v-model="ruleForm.count" placeholder="Activity count" :options="options" />
</el-form-item>
<el-form-item label="Activity time" required>
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
v-model="ruleForm.date1"
type="date"
aria-label="Pick a date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col class="text-center" :span="2">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker v-model="ruleForm.date2" aria-label="Pick a time" placeholder="Pick a time" style="width: 100%" />
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="Instant delivery" prop="delivery">
<el-switch v-model="ruleForm.delivery" />
</el-form-item>
<el-form-item label="Activity location" prop="location">
<el-segmented v-model="ruleForm.location" :options="locationOptions" />
</el-form-item>
<el-form-item label="Activity type" prop="type">
<el-checkbox-group v-model="ruleForm.type">
<el-checkbox value="Online activities" name="type"> Online activities </el-checkbox>
<el-checkbox value="Promotion activities" name="type"> Promotion activities </el-checkbox>
<el-checkbox value="Offline activities" name="type"> Offline activities </el-checkbox>
<el-checkbox value="Simple brand exposure" name="type"> Simple brand exposure </el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="Resources" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio value="Sponsorship">Sponsorship</el-radio>
<el-radio value="Venue">Venue</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form" prop="desc">
<el-input v-model="ruleForm.desc" type="textarea" />
</el-form-item>
</el-form>
</template>
<style scoped>
.read-the-docs {
color: #888888;
}
</style>- 弹窗组件:
<script setup lang="ts">
import { ref } from "vue";
import Example from "./components/example.vue";
const lastResult = ref<any>(null);
const headerButton = {
template: `<el-button type="primary" size="small">操作按钮</el-button>`
};
const showBasicModal = async () => {
const result = await createModal({
component: Example,
componentProps: {
/* 传递给组件的props */
},
modalProps: {
title: "弹窗标题",
// titleIcon: "gdsTitle", // 使用内部图标
width: "900px",
footerButtons: [
{ text: "取消", event: "cancel" },
{ text: "确定", event: "confirm", type: "primary" }
]
},
headerSlot: headerButton,
onComponentEvent: (event, data) => {
// 处理特定事件
switch (event) {
case "confirm":
// 自动调用验证 ”confirm“
}
}
});
lastResult.value = result;
};
</script>
<template>
<div>
<div>
<el-card>
<el-button type="primary" @click="showBasicModal">高度自定义弹窗</el-button>
</el-card>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.el-card__body) {
.md {
height: 650px;
overflow-y: auto;
}
}
</style>
API 参考
createModal 函数
function createModal(options: ModalOptions): Promise<any>参数:
options.component: Vue组件
options.componentProps: 传递给组件的props
options.modalProps: 弹窗配置
options.onComponentEvent: 事件回调函数
返回值: Promise,解析为弹窗关闭时的结果
ModalHelper 工具类
// 确认对话框
ModalHelper.confirm(message: string, title?: string): Promise<any>
// 成功提示
ModalHelper.success(message: string, title?: string): Promise<any>
// 警告提示
ModalHelper.warning(message: string, title?: string): Promise<any>
// 错误提示
ModalHelper.error(message: string, title?: string): Promise<any>
// 信息提示
ModalHelper.info(message: string, title?: string): Promise<any>
// 关闭弹窗
ModalHelper.closeModal(result?: any);
// 获取当前弹窗实例
ModalHelper.getCurrentModal();
// 更新当前模态框属性
ModalHelper.updateModalProps(modalProps: ModalProps);样式定制
- 使用内置样式类
// 弹窗尺寸
.el-dialog-small // 392px
.el-dialog-default // 850px
.el-dialog-large // 1168px
.el-dialog-largest // 1500px- 自定义样式
<template>
<ElDialog class="custom-dialog my-custom-style">
<!-- 内容 -->
</ElDialog>
</template>
<style scoped>
.my-custom-style {
:deep(.el-dialog__header) {
background: linear-gradient(90deg, #409EFF, #64b5ff);
.el-dialog__title {
color: white;
&::before {
filter: brightness(0) invert(1);
}
}
}
}
</style>最佳实践
- 组件设计原则
// 好的实践:组件只关注UI,事件通过props传递
const GoodComponent = {
props: ['onEvent', 'onConfirm'],
setup(props) {
const handleAction = () => {
props.onEvent?.('action-taken', { data: 'example' })
}
return { handleAction }
}
}
// 不好的实践:组件直接操作外部状态
const BadComponent = {
setup() {
const store = useStore() // 避免直接使用store
// ...
}
}- 错误处理
const showModalSafely = async () => {
try {
const result = await createModal({
component: MyComponent,
modalProps: { title: '弹窗' }
})
// 处理结果
} catch (error) {
console.error('弹窗打开失败:', error)
ElMessage.error('弹窗打开失败')
}
}- 性能优化
// 懒加载组件
const LazyComponent = defineAsyncComponent(() =>
import('./LazyComponent.vue')
)
const showLazyModal = async () => {
await createModal({
component: LazyComponent,
modalProps: { title: '懒加载弹窗' }
})
}常见问题
- 类型错误处理
// 确保组件有正确的props定义
defineProps<{
onEvent?: (event: string, data?: any) => void
onConfirm?: (data: any) => void
onCancel?: () => void
}>()