mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2025-12-19 19:17:07 +00:00
518 lines
10 KiB
TypeScript
518 lines
10 KiB
TypeScript
import { defineComponent, h, inject, nextTick, provide, reactive, ref, watch } from "vue";
|
||
import cloneDeep from "clone-deep";
|
||
import { useAction } from "./helper";
|
||
import { useRefs, useForm } from "../../hooks/core";
|
||
import { deepMerge, isBoolean, isEmpty, isObject, isString } from "../../utils";
|
||
import Parse from "../../utils/parse";
|
||
import { renderNode } from "../../utils/vnode";
|
||
import { Browser, Form } from "../../types";
|
||
import FormHook from "../../hooks/form";
|
||
|
||
export default defineComponent({
|
||
name: "cl-form",
|
||
|
||
props: {
|
||
modelValue: {
|
||
type: Object,
|
||
default: () => {
|
||
return {};
|
||
}
|
||
}
|
||
},
|
||
|
||
emits: ["update:modelValue"],
|
||
|
||
setup(props, { emit }) {
|
||
const { refs, setRefs }: any = useRefs();
|
||
|
||
// 设置表单值
|
||
const { setFormData } = useForm(props);
|
||
|
||
// 表单是否可见
|
||
const visible = ref<boolean>(false);
|
||
|
||
// 表单提交保存状态
|
||
const saving = ref<boolean>(false);
|
||
|
||
// 选项卡
|
||
const tabActive = ref<any>(null);
|
||
|
||
// 表单数据
|
||
const form = setFormData();
|
||
|
||
// 表单配置
|
||
const conf = reactive<Form>({
|
||
title: "自定义表单",
|
||
width: "50%",
|
||
props: {
|
||
size: "small",
|
||
labelWidth: "100px"
|
||
},
|
||
on: {},
|
||
op: {
|
||
hidden: false,
|
||
saveButtonText: "保存",
|
||
closeButtonText: "取消",
|
||
buttons: ["close", "save"]
|
||
},
|
||
dialog: {
|
||
props: {
|
||
fullscreen: false,
|
||
"close-on-click-modal": false,
|
||
"append-to-body": true
|
||
},
|
||
hiddenControls: false,
|
||
controls: ["fullscreen", "close"]
|
||
},
|
||
items: [],
|
||
_data: {}
|
||
});
|
||
|
||
// 表单动作
|
||
const {
|
||
loading,
|
||
showLoading,
|
||
hiddenLoading,
|
||
collapseItem,
|
||
getForm,
|
||
setForm,
|
||
setData,
|
||
setOptions,
|
||
toggleItem,
|
||
hiddenItem,
|
||
showItem,
|
||
resetFields,
|
||
clearValidate,
|
||
validateField,
|
||
validate
|
||
} = useAction({ conf, form, refs });
|
||
|
||
// 更新绑定值
|
||
watch(form, (val: any) => {
|
||
emit("update:modelValue", val);
|
||
});
|
||
|
||
// 提供
|
||
provide("form", form);
|
||
|
||
// 请求表单保存状态
|
||
function done() {
|
||
saving.value = false;
|
||
}
|
||
|
||
// 关闭表单
|
||
function close() {
|
||
visible.value = false;
|
||
done();
|
||
}
|
||
|
||
function onClosed() {
|
||
tabActive.value = null;
|
||
}
|
||
|
||
// 表单关闭前事件
|
||
function beforeClose() {
|
||
if (conf.on?.close) {
|
||
conf.on.close(close);
|
||
} else {
|
||
close();
|
||
}
|
||
}
|
||
|
||
// 清空表单验证
|
||
function clear() {
|
||
for (const i in form) {
|
||
delete form[i];
|
||
}
|
||
|
||
clearValidate();
|
||
}
|
||
|
||
// 表单提交
|
||
function submit(callback?: Function) {
|
||
// 验证表单
|
||
refs.value.form.validate(async (valid: boolean, error: any) => {
|
||
if (valid) {
|
||
saving.value = true;
|
||
|
||
// 拷贝表单值
|
||
const d = cloneDeep(form);
|
||
|
||
// 过滤隐藏的表单项
|
||
conf.items.forEach((e: any) => {
|
||
if (e._hidden) {
|
||
delete d[e.prop];
|
||
}
|
||
|
||
if (e.hook) {
|
||
d[e.prop] = FormHook.submit(d[e.prop], e.hook, d);
|
||
}
|
||
});
|
||
|
||
const submit = callback || conf.on?.submit;
|
||
|
||
// 提交事件
|
||
if (submit) {
|
||
submit(d, {
|
||
done,
|
||
close
|
||
});
|
||
} else {
|
||
console.error("Not found callback function");
|
||
}
|
||
} else {
|
||
// 判断是否使用form-tabs,切换到对应的选项卡
|
||
const keys = Object.keys(error);
|
||
|
||
if (tabActive.value) {
|
||
const item = conf.items.find((e) => e.prop === keys[0]);
|
||
|
||
if (item) {
|
||
tabActive.value = item.group;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 打开表单
|
||
function open(options?: Form) {
|
||
if (!options) {
|
||
options = {
|
||
items: []
|
||
};
|
||
}
|
||
|
||
clear();
|
||
|
||
// 合并配置
|
||
for (const i in conf) {
|
||
switch (i) {
|
||
case "items":
|
||
conf.items = cloneDeep(options.items || []);
|
||
break;
|
||
case "title":
|
||
case "width":
|
||
conf[i] = options[i];
|
||
break;
|
||
case "props":
|
||
case "on":
|
||
case "op":
|
||
case "dialog":
|
||
case "_data":
|
||
conf[i] = deepMerge(conf[i], options[i] || {});
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 显示对话框
|
||
visible.value = true;
|
||
|
||
// 预设表单值
|
||
if (options?.form) {
|
||
for (const i in options.form) {
|
||
form[i] = options.form[i];
|
||
}
|
||
}
|
||
|
||
// 设置表单数据
|
||
conf.items.map((e: any) => {
|
||
if (e.prop) {
|
||
form[e.prop] = FormHook.bind(
|
||
isEmpty(form[e.prop]) ? cloneDeep(e.value) : form[e.prop],
|
||
e.hook,
|
||
form
|
||
);
|
||
}
|
||
});
|
||
|
||
// 打开回调
|
||
nextTick(() => {
|
||
if (conf.on?.open) {
|
||
conf.on.open(form, {
|
||
close,
|
||
submit,
|
||
done
|
||
});
|
||
}
|
||
});
|
||
|
||
return {
|
||
showLoading,
|
||
hiddenLoading,
|
||
collapseItem,
|
||
getForm,
|
||
setForm,
|
||
setData,
|
||
setOptions,
|
||
toggleItem,
|
||
hiddenItem,
|
||
showItem,
|
||
resetFields,
|
||
clearValidate,
|
||
validateField,
|
||
validate
|
||
};
|
||
}
|
||
|
||
// 重新绑定表单数据
|
||
function reBindForm(data: any) {
|
||
for (const i in data) {
|
||
const d: any = conf.items.find((e) => e.prop === i);
|
||
form[i] = d ? FormHook.bind(data[i], d.hook, form) : data[i];
|
||
}
|
||
}
|
||
|
||
return {
|
||
visible,
|
||
saving,
|
||
tabActive,
|
||
form,
|
||
refs,
|
||
setRefs,
|
||
conf,
|
||
loading,
|
||
open,
|
||
beforeClose,
|
||
close,
|
||
onClosed,
|
||
done,
|
||
clear,
|
||
submit,
|
||
reBindForm,
|
||
showLoading,
|
||
hiddenLoading,
|
||
collapseItem,
|
||
getForm,
|
||
setForm,
|
||
setData,
|
||
setOptions,
|
||
toggleItem,
|
||
hiddenItem,
|
||
showItem,
|
||
resetFields,
|
||
clearValidate,
|
||
validateField,
|
||
validate
|
||
};
|
||
},
|
||
|
||
render(ctx: any) {
|
||
const browser = inject("browser") as Browser;
|
||
const { props, op, title, width, dialog, _data } = ctx.conf;
|
||
|
||
// 渲染表单及表单项
|
||
const renderForm = () => {
|
||
// 表单项列表
|
||
const children = ctx.conf.items.map((e: any) => {
|
||
if (e.type == "tabs") {
|
||
return <cl-form-tabs v-model={ctx.tabActive} {...e.props}></cl-form-tabs>;
|
||
}
|
||
|
||
// 隐藏处理
|
||
e._hidden = Parse("hidden", {
|
||
value: e.hidden,
|
||
scope: ctx.form,
|
||
data: _data
|
||
});
|
||
|
||
// 分组
|
||
e._group =
|
||
isEmpty(ctx.tabActive) || isEmpty(e.group) ? true : e.group === ctx.tabActive;
|
||
|
||
// Flex handler
|
||
if (isEmpty(e.flex)) {
|
||
e._flex = true;
|
||
}
|
||
|
||
return (
|
||
!e._hidden && (
|
||
<el-col span={24} {...e}>
|
||
{e.component &&
|
||
h(
|
||
<el-form-item v-show={e._group}></el-form-item>,
|
||
{
|
||
prop: e.prop,
|
||
rules: e.rules,
|
||
...e.props
|
||
},
|
||
{
|
||
label: () => {
|
||
let d: any = {
|
||
text: "",
|
||
tip: "",
|
||
icon: ""
|
||
};
|
||
|
||
if (isString(e.label)) {
|
||
d.text = e.label;
|
||
} else if (isObject(e.label)) {
|
||
d = e.label;
|
||
}
|
||
|
||
return (
|
||
<el-tooltip
|
||
effect="dark"
|
||
placement="top"
|
||
content={d.tip}
|
||
disabled={!d.tip}>
|
||
<span>
|
||
{d.text}
|
||
{d.icon && <i class={d.icon}></i>}
|
||
</span>
|
||
</el-tooltip>
|
||
);
|
||
},
|
||
default: () => {
|
||
return (
|
||
<div>
|
||
{/* Form item */}
|
||
<div class="cl-form-item">
|
||
{["prepend", "component", "append"].map(
|
||
(name) => {
|
||
return (
|
||
e[name] && (
|
||
<div
|
||
v-show={!e.collapse}
|
||
class={[
|
||
`cl-form-item__${name}`,
|
||
{
|
||
"is-flex":
|
||
e._flex
|
||
}
|
||
]}>
|
||
{renderNode(e[name], {
|
||
prop: e.prop,
|
||
scope: ctx.form,
|
||
slots: ctx.$slots
|
||
})}
|
||
</div>
|
||
)
|
||
);
|
||
}
|
||
)}
|
||
</div>
|
||
{/* Collapse button */}
|
||
{isBoolean(e.collapse) && (
|
||
<div
|
||
class="cl-form-item__collapse"
|
||
onClick={() => {
|
||
ctx.collapseItem(e);
|
||
}}>
|
||
<el-divider content-position="center">
|
||
{e.collapse ? (
|
||
<span>
|
||
查看更多
|
||
<i class="el-icon-arrow-down"></i>
|
||
</span>
|
||
) : (
|
||
<span>
|
||
隐藏内容
|
||
<i class="el-icon-arrow-up"></i>
|
||
</span>
|
||
)}
|
||
</el-divider>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
)}
|
||
</el-col>
|
||
)
|
||
);
|
||
});
|
||
|
||
// el-form
|
||
const ElForm = (
|
||
<el-form
|
||
ref={ctx.setRefs("form")}
|
||
label-position={browser.isMini ? "top" : ""}
|
||
size="small"
|
||
label-width="100px"
|
||
disabled={ctx.saving}
|
||
model={ctx.form}></el-form>
|
||
);
|
||
|
||
return h(ElForm, props, {
|
||
default: () => {
|
||
return (
|
||
<el-row gutter={10} v-loading={ctx.loading}>
|
||
{children}
|
||
</el-row>
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 渲染表单按钮
|
||
function renderFooter() {
|
||
const { hidden, buttons, saveButtonText, closeButtonText } = op;
|
||
const { size = "small" } = props;
|
||
|
||
return hidden
|
||
? null
|
||
: buttons.map((vnode: any) => {
|
||
if (vnode == "save") {
|
||
return (
|
||
<el-button
|
||
{...{
|
||
size,
|
||
type: "success",
|
||
disabled: ctx.loading,
|
||
loading: ctx.saving,
|
||
onClick: () => {
|
||
ctx.submit();
|
||
}
|
||
}}>
|
||
{saveButtonText}
|
||
</el-button>
|
||
);
|
||
} else if (vnode == "close") {
|
||
return (
|
||
<el-button
|
||
{...{
|
||
size,
|
||
onClick: () => {
|
||
ctx.beforeClose();
|
||
}
|
||
}}>
|
||
{closeButtonText}
|
||
</el-button>
|
||
);
|
||
} else {
|
||
return renderNode(vnode, {
|
||
scope: ctx.form,
|
||
slots: ctx.$slots
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
return h(
|
||
<cl-dialog v-model={ctx.visible}></cl-dialog>,
|
||
{
|
||
title,
|
||
width,
|
||
...dialog,
|
||
props: {
|
||
...dialog.props,
|
||
"before-close": ctx.beforeClose
|
||
},
|
||
onClosed: ctx.onClosed
|
||
},
|
||
{
|
||
default() {
|
||
return (
|
||
<div class="cl-form">
|
||
<div class="cl-form__container">{renderForm()}</div>
|
||
<div class="cl-form__footer">{renderFooter()}</div>
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
});
|