前言:
博主封装了一个公共弹窗函数接收四个参数,(title:弹窗标题, ContentComponent:弹窗中显示的组件内容, opt:接收弹窗本身的属性和props, beforeSure:点击确定做的操作(请求后端接口))
封装的公共函数:
import { defineComponent, h, ref, getCurrentInstance } from "vue-demi";
import Vue from "vue";
import { isFunction, isUndefined, noop, isPlainObject } from "lodash";
const WRAPPED = "wrapped";
function generateDialogComponent(wrapped, opt, listeners = {}) {
return defineComponent({
setup() {
const loading = ref(false);
const vm = getCurrentInstance();
const visible = opt.visible; // Ref<boolean>
const closeable = opt.closeable; // Ref<boolean>
const showCancelButton = isUndefined(opt.dialog.showCancelButton)
? true
: opt.dialog.showCancelButton;
const showSureBtn =
isUndefined(opt.dialog.showSureBtn) &&
isUndefined(opt.dialog.showSureButton)
? true
: opt.dialog.showSureBtn || opt.dialog.showSureButton;
const showFooter = isUndefined(opt.dialog.showFooter)
? true
: opt.dialog.showFooter;
const confirmButtonText = opt.dialog.confirmButtonText || "确定";
const cancelButtonText = opt.dialog.cancelButtonText || "取消";
return () => {
const sure = listeners.sure || (() => Promise.resolve());
const cancel = listeners.cancel || noop;
const destroy = listeners.destroy || noop;
// footer
const sureBtn = h(
"el-button",
{
props: {
size: "mini",
type: "primary",
loading: loading.value,
},
on: {
click: () => {
loading.value = true;
const wrappedVm = vm.proxy.$refs[WRAPPED];
return sure(wrappedVm)
.then(() => {
visible.value = false;
})
.finally(() => (loading.value = false));
},
},
},
confirmButtonText
);
const cancelBtn = h(
"el-button",
{
props: {
size: "mini",
},
on: {
click: () => {
visible.value = false;
},
},
},
cancelButtonText
);
const footer = h(
"div",
{
slot: "footer",
style: {
display: "flex",
justifyContent: "space-between",
},
},
[
h("div", [opt.dialog?.leftFooter?.()]),
h("div", [
closeable.value && showCancelButton && cancelBtn,
showSureBtn && sureBtn,
]),
]
);
return h(
"el-dialog",
{
props: {
closeOnClickModal: false,
visible: visible.value,
...opt.dialog,
closeOnClickModal: closeable.value,
closeOnPressEscape: closeable.value,
showClose: closeable.value,
},
on: {
"update:visible": (val) => {
visible.value = val;
},
closed: () => {
cancel();
destroy();
},
},
},
[
h("div", { style: { padding: "20px" } }, [
h(wrapped, {
ref: WRAPPED,
attrs: Object.assign({}, opt.props),
on: {
close: sure, // 组件内部可以通过 emit('close') 来关闭弹窗
},
}),
]),
showFooter && footer,
]
);
};
},
});
}
function openDialog(title, ContentComponent, opt, beforeSure) {
const defaultOpt = {
dialog: {},
props: {},
};
// 参数格式化
if (isUndefined(opt)) {
opt = defaultOpt;
}
if (isFunction(opt)) {
opt = defaultOpt;
beforeSure = opt;
}
if (!isFunction(beforeSure)) {
beforeSure = (vm) => vm.submit?.();
}
if (isPlainObject(opt)) {
if (isUndefined(opt.props)) {
opt = {
...opt,
props: opt,
};
}
}
opt.dialog = opt.dialog || opt || {};
opt.dialog.title = title;
const mountComponent = ($vueconfig) => {
const vm = new Vue($vueconfig);
const anchor = document.createElement("div");
document.body.appendChild(anchor);
vm.$mount(anchor);
return () => {
vm.$destroy();
document.body.removeChild(vm.$el);
};
};
// 控制 dialog 显隐
const visible = ref(false);
const closeDialog = () => {
visible.value = false;
};
// 控制是否可以关闭
const closeable = ref(true);
// 不可关闭弹窗
const freeze = () => {
closeable.value = false;
};
// 可关闭弹窗
const unfreeze = () => {
closeable.value = true;
};
const wait = new Promise((resolve, reject) => {
let disposer = null;
const destroy = () => isFunction(disposer) && disposer();
const cancel = () => {
reject(new Error("cancel"));
};
const sure = async (wrappedComp) => {
const promise = await beforeSure(wrappedComp);
resolve(promise);
};
disposer = mountComponent(
generateDialogComponent(
ContentComponent,
{ ...opt, visible, closeable },
{ sure, cancel, destroy }
)
);
// 打开弹窗
setTimeout(() => (visible.value = true), 20);
});
return {
close: closeDialog,
promise: wait,
freeze,
unfreeze,
};
}
export function pickByDialog(...args) {
const { promise } = openDialog(...args);
return promise;
}
/**
* 让 pickByDialog 静默失败, 并且提供一个手动关闭弹窗的函数
* @Returns { close }
*/
export const openByDialog = (...args) => {
const { close, freeze, unfreeze, promise } = openDialog(...args);
promise.catch(() => {
// no throw error
});
return {
close,
freeze,
unfreeze,
};
};
部分代码解释:
generateDialogComponent
函数解释:
generateDialogComponent
函数定义了一个组件,并返回该组件。- 在组件的
setup
函数中,首先定义了一些变量和常量,包括:
-
loading
:一个用于表示加载状态的响应式变量(Ref)。vm
:当前组件实例的引用。visible
:一个响应式变量,表示对话框的可见性。closeable
:一个响应式变量,表示对话框是否可关闭。showCancelButton
:一个布尔值,表示是否显示取消按钮,默认为true
。showSureBtn
:一个布尔值,表示是否显示确定按钮,默认为true
。showFooter
:一个布尔值,表示是否显示底部内容,默认为true
。confirmButtonText
:确定按钮的文本,默认为"确定"。cancelButtonText
:取消按钮的文本,默认为"取消"。
- 返回一个函数,该函数使用Vue 3的Composition API语法,作为组件的渲染函数(render function)。
- 渲染函数返回一个
el-dialog
组件,该组件是一个基于Element UI库的对话框组件。 el-dialog
组件的属性包括:
-
closeOnClickModal
:控制是否在点击模态框时关闭对话框,根据closeable
的值进行设置。visible
:控制对话框的可见性,根据visible
的值进行设置。...opt.dialog
:将opt.dialog
对象中的所有属性都作为el-dialog
组件的属性。closeOnClickModal
:控制是否在点击模态框时关闭对话框,根据closeable
的值进行设置。closeOnPressEscape
:控制是否在按下Esc键时关闭对话框,根据closeable
的值进行设置。showClose
:控制是否显示关闭按钮,根据closeable
的值进行设置。
el-dialog
组件的事件包括:
-
update:visible
:当对话框的可见性发生变化时,更新visible
的值。closed
:当对话框关闭时,触发cancel
和destroy
函数。
el-dialog
组件的插槽包括:
-
- 默认插槽:包含一个具有样式
{ padding: "20px" }
的div
元素,其中包含了wrapped
组件。 showFooter
为true
时,底部插槽:包含一个具有样式{ display: "flex", justifyContent: "space-between" }
的div
元素,其中包含了底部内容。
- 默认插槽:包含一个具有样式
- 返回生成的组件。
openDialog
的函数解释:
openDialog
函数接受四个参数:title
(对话框标题),ContentComponent
(对话框内容组件),opt
(可选配置对象),和beforeSure
(可选的确定按钮点击前的回调函数)。- 定义了一个名为
defaultOpt
的默认配置对象,包含dialog
和props
两个属性。 - 对传入的
opt
和beforeSure
进行格式化处理:
-
- 如果
opt
为undefined
,则将其设置为defaultOpt
的值。 - 如果
opt
为函数,则将其设置为defaultOpt
的值,并将beforeSure
设置为该函数。 - 如果
beforeSure
不是函数,则将其设置为一个默认函数,该函数会调用传入的组件实例的submit
方法(如果存在)。
- 如果
- 对
opt
进行进一步处理:
-
- 如果
opt
是普通对象且没有props
属性,则将opt
的值复制给opt.props
。
- 如果
- 将
opt.dialog
设置为opt
或opt.dialog
的值,并将title
设置为opt.dialog.title
。 - 定义了一个名为
mountComponent
的函数,用于将组件挂载到DOM上,并返回一个销毁函数。
-
- 在函数内部,创建了一个新的Vue实例,并将其挂载到一个新创建的
div
元素上。 - 将该
div
元素添加到document.body
中。 - 返回一个函数,该函数在调用时会销毁Vue实例,并从
document.body
中移除该div
元素。
- 在函数内部,创建了一个新的Vue实例,并将其挂载到一个新创建的
- 创建了一个响应式的变量
visible
,用于控制对话框的显示和隐藏。 - 定义了一个
closeDialog
函数,用于关闭对话框。 - 创建了一个响应式的变量
closeable
,用于控制对话框是否可以关闭。 - 定义了
freeze
函数,将closeable
设置为false
,使对话框不可关闭。 - 定义了
unfreeze
函数,将closeable
设置为true
,使对话框可关闭。 - 创建了一个Promise对象
wait
,用于等待对话框的操作完成。 - 在
wait
的执行函数中,定义了一些内部函数和变量:
disposer
:用于存储销毁函数的引用。destroy
函数:用于执行销毁函数。cancel
函数:用于拒绝Promise并抛出一个取消错误。sure
函数:在确定按钮点击时执行的回调函数,调用beforeSure
函数并传入wrappedComp
作为参数,并将返回的Promise解析为resolve
的值。- 将
disposer
设置为调用mountComponent
函数,并传入generateDialogComponent
函数生成的对话框组件。 - 使用
setTimeout
延迟20毫秒后,将visible
设置为true
,打开对话框。
- 返回一个对象,包含以下属性和方法:
close
:关闭对话框的方法。promise
:返回等待对话框操作完成的Promise对象。freeze
:使对话框不可关闭的方法。unfreeze
:使对话框可关闭的方法。
使用方法例子如下
例子一
const { close } = openByDialog(
"自定义列表",
Setting2,
{
props: menuProps,
dialog: {
width: "980px",
confirmButtonText: "保存",
leftFooter: () =>
h(
"el-button",
{
style: { color: "#2783fe" },
props: {
size: "mini",
type: "text",
loading: reseting.value,
},
on: {
click: () => doReset(),
},
},
"恢复系统默认设置"
),
},
},
async (componentWrapper) => {
const updatedColumns = await componentWrapper?.updateColumnSetting();
this.emitChangeColumns2(updatedColumns);
}
);
例子二
//组件弹窗测试
async doImportLog() {
const [cancel, blob] = await to(
pickByDialog(
"弹窗导出测试",
getLogComp(this), //这个是组件
{
dialog: { width: "35%" },
// props: { value: this.value },
// on: {
// input: (val) => {
// this.value = val
// },
// },
},
async (vm) => {
const [changeDateBt, changeDateEt] = vm.date;
console.log("changeDateBt", changeDateBt);
console.log("changeDateEt", changeDateEt);
}
)
);
if (cancel) {
this.$message.info(this.$tof("cancel"));
return;
}
const curDate = dayjs().format("YYYYMMDD");
console.log("curDate", curDate);
saveAs(blob.data, this.$tof("file_name", { curDate }));
},
function getLogComp(vm) {
return {
data() {
return {
date: [],
};
},
render(h) {
return h("el-row", { style: { margin: "100px 50px" } }, [
h(
"el-col",
{ style: { lineHeight: "36px" }, attrs: { span: 8 } },
"导出范围时间"
),
h("el-col", { attrs: { span: 16 } }, [
h("el-date-picker", {
attrs: {
type: "daterange",
rangeSeparator: "-",
startPlaceholder: "开始日期",
endPlaceholder: "结束日期",
value: this.date,
valueFormat: "yyyy-MM-dd",
},
style: { width: "auto !important" },
on: {
input: (val) => {
this.date = val;
},
},
}),
]),
]);
},
};
}
例子三
//组件弹窗测试222
async doAdd() {
const [cancel] = await to(
pickByDialog(
"新增客户",
GroupCreate, //组件
{
dialog: {
width: "80%",
confirmButtonText: "保存",
leftFooter: () =>
h(
"el-button",
{
style: { color: "#2783fe" },
props: {
size: "mini",
type: "text",
loading: false,
},
on: {
click: () => doReset(),
},
},
"恢复系统默认设置"
),
},
props: {},
},
async (vm) => {
console.log("测试点击确定", vm);
}
)
);
if (cancel) {
this.$message.info("已取消");
return;
}
function doReset() {
console.log("测试左边操作");
}
},
GroupCreate组件:
<template>
<div>
<CollapsePanel :show-header="true">
<template #panel-button>
<el-button
size="small"
type="primary"
:loading="loading"
@click="doInquiry"
>
搜索
</el-button>
</template>
<template #panel-main>
<el-form
ref="filterForm"
:inline="true"
:model="schemaModel"
:max-height="200"
class="list-schema-form"
label-position="top"
>
<el-row
v-for="(row, index) in schemaList"
:key="index"
v-bind="layout"
>
<el-col
v-for="{ rules, label, prop, component, ...attrs } in row"
:key="prop"
:span="12"
>
<el-form-item v-if="prop" v-bind="{ label, rules, prop }">
{{ component }}
<component
:is="`el-${component}`"
v-model="schemaModel[prop]"
:placeholder="component + '_placeholder'"
v-bind="attrs"
>
<el-option
v-for="ops in schemaOptions[prop]"
:key="ops.value"
v-bind="ops"
/>
</component>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
</CollapsePanel>
<CollapsePanel :auto-height="true">
<template #panel-main>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
border
stripe
max-height="550"
>
<el-table-column type="selection" fixed="left" width="40" />
<el-table-column
type="index"
width="60"
fixed="left"
:label="'序号'"
/>
<el-table-column
v-for="{ field, render, ...attrs } in columns"
:key="field"
v-bind="attrs"
>
<template #default="scope">
<field-render :render="render" :scope="scope" :field="field" />
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<pagination
:limit="pageSize"
:page="pageNum"
:total="totalPages"
@pagination="(pageNum = $event.page) && (pageSize = $event.limit)"
/>
</div>
</template>
</CollapsePanel>
</div>
</template>
<script>
// import { customerinfoFindCustByCustNameOrCustCodeToCommodity } from '@/api/commodity/price'
import to from "await-to-js";
import { snakeCase } from "lodash";
export default {
name: "GroupCreate",
components: {
FieldRender: {
functional: true,
props: {
scope: {
type: Object,
default: () => ({}),
},
render: Function,
field: String,
},
render: (h, { props }) => {
const { render, scope, field } = props;
return render
? render(h, { ...scope, field })
: h("span", null, scope.row[field]);
},
},
},
data(vm) {
return {
...getSchemaList(vm),
list: [],
columns: getColumList(vm),
pageNum: 1,
pageSize: 10,
totalPages: 0,
loading: false,
layout: {
gutter: 60,
justify: "start",
},
};
},
watch: {
pageNum: {
handler() {
this.doInquiry();
},
},
pageSize: {
handler() {
this.doInquiry();
},
},
},
async created() {
// const options = await this.$ops({});
// Object.assign(this.schemaOptions, options);
this.doInquiry();
},
methods: {
async doInquiry() {
console.log("测试");
// this.loading = true
// const [err, data] = await to(
// customerinfoFindCustByCustNameOrCustCodeToCommodity(
// Object.assign(this.schemaModel, {
// page: this.pageNum - 1,
// size: this.pageSize,
// })
// )
// ).finally(() => (this.loading = false))
// if (err) {
// return
// }
// const { content, totalElements = 5 } = data
// this.totalPages = +totalElements
// this.list = content.map((i) => ({
// custCode: i.customerCode,
// custName: i.customerName,
// }))
},
doReset() {
this.schemaModel = {};
},
doDelete() {},
doAdd() {},
async doSave() {
const [err] = await to(this.$refs["filterForm"].validate());
if (err) {
return;
}
},
doBack() {
this.$store.dispatch("tabsBar/delVisitedRoute", this.$route.fullPath);
this.$router.back();
},
},
};
function getColumList(vm) {
const COLUM_FIELDS = ["custCode", "custName"];
const FORM_FIELDS_NAME = {
custName: "客户名称",
custCode: "客户编码",
};
const colProperties = {};
return COLUM_FIELDS.map((prop) =>
Object.assign(
{ prop, label: FORM_FIELDS_NAME[prop], field: prop },
colProperties[prop] || {
sortable: true,
}
)
);
}
function getSchemaList(vm) {
const FORM_FIELDS = ["custCode", "custName"];
const FORM_FIELDS_NAME = {
custName: "客户名称",
custCode: "客户编码",
};
let properties = {
custCode: {},
custName: {},
};
const array = FORM_FIELDS.map((prop) => {
const label = FORM_FIELDS_NAME[prop];
const attrs = properties[prop];
return {
prop,
label,
clearable: true,
filterable: true,
component: "input",
...attrs,
};
});
const schemaList = [array];
return {
schemaList,
schemaModel: { custStatus: ["20"], custCode: "", custNameList: [] },
schemaOptions: { status: [] },
};
}
</script>
<style lang="scss" scoped>
::v-deep .el-button--text {
color: #409eff;
font-size: 13px;
}
.total-number {
color: #f55448;
}
</style>
例子四
async doAdd2() {
openByDialog(
"测试表单",
FormTest,
{
props: {
isShowCol: true,
},
},
async (componentWrapper) => {
await componentWrapper?.$children[0].validate();
console.log("componentWrapper的数据", componentWrapper);
componentWrapper.ruleForm.date1 = dayjs(
componentWrapper.ruleForm.date1
).format("YYYYMMDD");
let payload = componentWrapper.ruleForm;
return await this.fetchData(payload);
}
);
},
FormTest组件
<template>
<el-form
:model="ruleForm"
:rules="rules"
ref="ruleFormRef"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="活动时间" required>
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
type="date"
placeholder="选择日期"
v-model="ruleForm.date1"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker
placeholder="选择时间"
v-model="ruleForm.date2"
style="width: 100%"
></el-time-picker>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="即时配送" prop="delivery">
<el-switch v-model="ruleForm.delivery"></el-switch>
</el-form-item>
<!-- <el-form-item label="活动性质" prop="type">
<el-checkbox-group v-model="ruleForm.type">
<el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
<el-checkbox label="地推活动" name="type"></el-checkbox>
<el-checkbox label="线下主题活动" name="type"></el-checkbox>
<el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item> -->
<el-form-item label="特殊资源" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio label="线上品牌商赞助"></el-radio>
<el-radio label="线下场地免费"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="活动形式" prop="desc">
<el-input type="textarea" v-model="ruleForm.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">
立即创建
</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { defineComponent, ref } from "vue-demi";
export default defineComponent({
name: "ruleFormTest",
props: {
isShowCol: {
type: Boolean,
default: false,
},
},
setup(props) {
const ruleFormRef = ref();
let ruleForm = ref({
name: "",
region: "",
date1: "",
date2: "",
delivery: props.isShowCol,
type: [],
resource: "",
desc: "",
});
let rules = ref({
name: [
{ required: true, message: "请输入活动名称", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" },
],
region: [
{ required: true, message: "请选择活动区域", trigger: "change" },
],
date1: [
{
type: "date",
required: true,
message: "请选择日期",
trigger: "change",
},
],
date2: [
{
type: "date",
required: true,
message: "请选择时间",
trigger: "change",
},
],
type: [
{
type: "array",
required: true,
message: "请至少选择一个活动性质",
trigger: "change",
},
],
resource: [
{ required: true, message: "请选择活动资源", trigger: "change" },
],
desc: [{ required: true, message: "请填写活动形式", trigger: "blur" }],
});
const submitForm = async (formEl) => {
console.log("formEl", formEl);
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
console.log("submit!");
} else {
console.log("error submit!", fields);
}
});
};
const resetForm = (formName) => {};
// expose({ submitForm });
return {
ruleForm,
rules,
ruleFormRef,
submitForm,
resetForm,
};
},
});
</script>
模拟一个异步返回
fetchData(params) {
return new Promise((resolve, reject) => {
// 模拟异步请求
setTimeout(() => {
const data = { name: "John", age: 30, ...params };
// 模拟请求成功
resolve(data);
// this.$message.error("请求接口失败");
// 模拟请求失败
// reject('请求失败');
}, 1000);
});
},
填写完必填项发起后端请求,拿到数据