index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. import { defineComponent, h, nextTick } from "vue";
  2. import { cloneDeep, isBoolean } from "lodash-es";
  3. import { useAction, useForm, usePlugins, useTabs } from "./helper";
  4. import { useBrowser, useConfig, useElApi, useRefs } from "../../hooks";
  5. import { getValue, merge } from "../../utils";
  6. import formHook from "../../utils/form-hook";
  7. import { renderNode } from "../../utils/vnode";
  8. import { parseFormHidden } from "../../utils/parse";
  9. export default defineComponent({
  10. name: "cl-form",
  11. props: {
  12. inner: Boolean,
  13. inline: Boolean
  14. },
  15. setup(props, { expose, slots }) {
  16. const { refs, setRefs } = useRefs();
  17. const { style, dict } = useConfig();
  18. const browser = useBrowser();
  19. const { Form, config, form, visible, saving, loading, disabled } = useForm();
  20. // 关闭的操作类型
  21. let closeAction: ClForm.CloseAction = "close";
  22. // 旧表单数据
  23. let defForm: obj | undefined;
  24. // 选项卡
  25. const Tabs = useTabs({ config, Form });
  26. // 操作
  27. const Action = useAction({ config, form, Form });
  28. // 方法
  29. const ElFormApi = useElApi(
  30. ["validate", "validateField", "resetFields", "scrollToField", "clearValidate"],
  31. Form
  32. );
  33. // 插件
  34. const plugin = usePlugins({ visible });
  35. // 显示加载中
  36. function showLoading() {
  37. loading.value = true;
  38. }
  39. // 隐藏加载
  40. function hideLoading() {
  41. loading.value = false;
  42. }
  43. // 设置是否禁用
  44. function setDisabled(val: boolean = true) {
  45. disabled.value = val;
  46. }
  47. // 请求表单保存状态
  48. function done() {
  49. saving.value = false;
  50. }
  51. // 关闭表单
  52. function close(action?: ClForm.CloseAction) {
  53. if (action) {
  54. closeAction = action;
  55. }
  56. beforeClose(() => {
  57. visible.value = false;
  58. done();
  59. });
  60. }
  61. // 关闭前
  62. function beforeClose(done: fn) {
  63. if (config.on?.close) {
  64. config.on.close(closeAction, done);
  65. } else {
  66. done();
  67. }
  68. }
  69. // 关闭后
  70. function onClosed() {
  71. Tabs.clear();
  72. Form.value?.clearValidate();
  73. }
  74. // 清空表单验证
  75. function clear() {
  76. for (const i in form) {
  77. delete form[i];
  78. }
  79. setTimeout(() => {
  80. Form.value?.clearValidate();
  81. }, 0);
  82. }
  83. // 重置
  84. function reset() {
  85. if (defForm) {
  86. for (const i in defForm) {
  87. form[i] = cloneDeep(defForm[i]);
  88. }
  89. }
  90. }
  91. // 表单提交
  92. function submit(callback?: fn) {
  93. // 验证表单
  94. Form.value.validate(async (valid: boolean, error: any) => {
  95. if (valid) {
  96. saving.value = true;
  97. // 拷贝表单值
  98. const d = cloneDeep(form);
  99. config.items.forEach((e) => {
  100. function deep(e: ClForm.Item) {
  101. if (e.prop) {
  102. // 过滤隐藏的表单项
  103. if (e._hidden) {
  104. if (e.prop) {
  105. delete d[e.prop];
  106. }
  107. }
  108. // hook 提交处理
  109. if (e.hook) {
  110. formHook.submit({
  111. ...e,
  112. value: e.prop ? d[e.prop] : undefined,
  113. form: d
  114. });
  115. }
  116. }
  117. if (e.children) {
  118. e.children.forEach(deep);
  119. }
  120. }
  121. deep(e);
  122. });
  123. // 处理 "-" 多层级
  124. for (const i in d) {
  125. if (i.includes("-")) {
  126. // 结构参数
  127. const [a, ...arr] = i.split("-");
  128. // 关键值的key
  129. const k: string = arr.pop() || "";
  130. if (!d[a]) {
  131. d[a] = {};
  132. }
  133. let f: any = d[a];
  134. // 设置默认值
  135. arr.forEach((e) => {
  136. if (!f[e]) {
  137. f[e] = {};
  138. }
  139. f = f[e];
  140. });
  141. // 设置关键值
  142. f[k] = d[i];
  143. delete d[i];
  144. }
  145. }
  146. const submit = callback || config.on?.submit;
  147. // 提交事件
  148. if (submit) {
  149. submit(await plugin.submit(d), {
  150. close() {
  151. close("save");
  152. },
  153. done
  154. });
  155. } else {
  156. done();
  157. }
  158. } else {
  159. // 切换到对应的选项卡
  160. Tabs.toGroup({
  161. refs,
  162. config,
  163. prop: Object.keys(error)[0]
  164. });
  165. }
  166. });
  167. }
  168. // 打开表单
  169. function open(options?: ClForm.Options, plugins?: ClForm.Plugin[]) {
  170. if (!options) {
  171. return console.error("Options is not null");
  172. }
  173. // 清空
  174. if (options.isReset !== false) {
  175. clear();
  176. }
  177. // 显示对话框
  178. visible.value = true;
  179. // 默认关闭方式
  180. closeAction = "close";
  181. // 合并配置
  182. for (const i in config) {
  183. switch (i) {
  184. // 表单项
  185. case "items":
  186. function deep(arr: any[]): any[] {
  187. return arr.map((e) => {
  188. const d = getValue(e);
  189. return {
  190. ...d,
  191. children: d?.children ? deep(d.children) : undefined
  192. };
  193. });
  194. }
  195. config.items = deep(options.items || []);
  196. break;
  197. // 事件、参数、操作
  198. case "on":
  199. case "op":
  200. case "props":
  201. case "dialog":
  202. case "_data":
  203. merge(config[i], options[i] || {});
  204. break;
  205. // 其他
  206. default:
  207. config[i] = options[i];
  208. break;
  209. }
  210. }
  211. // 预设表单值
  212. if (options?.form) {
  213. for (const i in options.form) {
  214. form[i] = options.form[i];
  215. }
  216. }
  217. // 设置表单数据
  218. config.items.forEach((e) => {
  219. function deep(e: ClForm.Item) {
  220. if (e.prop) {
  221. // 解析 prop
  222. if (e.prop.includes(".")) {
  223. e.prop = e.prop.replace(/\./g, "-");
  224. }
  225. // prop 合并
  226. Tabs.mergeProp(e);
  227. // hook 绑定值
  228. formHook.bind({
  229. ...e,
  230. value: form[e.prop] !== undefined ? form[e.prop] : cloneDeep(e.value),
  231. form
  232. });
  233. // 表单验证
  234. if (e.required) {
  235. e.rules = {
  236. required: true,
  237. message: `${e.label}${dict.label.nonEmpty}`
  238. };
  239. }
  240. }
  241. // 设置 tabs 默认值
  242. if (e.type == "tabs") {
  243. Tabs.set(e.value);
  244. }
  245. // 子集
  246. if (e.children) {
  247. e.children.forEach(deep);
  248. }
  249. }
  250. deep(e);
  251. });
  252. // 设置默认值
  253. if (!defForm) {
  254. defForm = cloneDeep(form);
  255. }
  256. // 创建插件
  257. plugin.create(plugins);
  258. // 打开回调
  259. nextTick(() => {
  260. setTimeout(() => {
  261. // 打开事件
  262. if (config.on?.open) {
  263. config.on.open(form);
  264. }
  265. }, 10);
  266. });
  267. }
  268. // 绑定表单数据
  269. function bindForm(data: any) {
  270. config.items.forEach((e) => {
  271. function deep(e: ClForm.Item) {
  272. formHook.bind({
  273. ...e,
  274. value: e.prop ? data[e.prop] : undefined,
  275. form: data
  276. });
  277. if (e.children) {
  278. e.children.forEach(deep);
  279. }
  280. }
  281. deep(e);
  282. });
  283. Object.assign(form, data);
  284. }
  285. // 渲染表单项
  286. function renderFormItem(e: ClForm.Item) {
  287. const { isDisabled } = config._data;
  288. if (e.type == "tabs") {
  289. return (
  290. <cl-form-tabs v-model={Tabs.active.value} {...e.props} onChange={Tabs.onLoad} />
  291. );
  292. }
  293. // 是否隐藏
  294. e._hidden = parseFormHidden(e.hidden, {
  295. scope: form
  296. });
  297. // 分组显示
  298. const inGroup = e.group ? e.group === Tabs.active.value : true;
  299. // 是否已加载完成
  300. const isLoaded = e.component && Tabs.isLoaded(e.group);
  301. // 表单项
  302. const FormItem = h(
  303. <el-form-item
  304. class={{
  305. "no-label": !(e.renderLabel || e.label),
  306. "has-children": !!e.children
  307. }}
  308. key={e.prop}
  309. data-group={e.group || "-"}
  310. data-prop={e.prop || "-"}
  311. label-width={props.inline ? "auto" : ""}
  312. label={e.label}
  313. prop={e.prop}
  314. rules={isDisabled ? null : e.rules}
  315. required={e._hidden ? false : e.required}
  316. v-show={inGroup && !e._hidden}
  317. />,
  318. e.props,
  319. {
  320. label() {
  321. return e.renderLabel
  322. ? renderNode(e.renderLabel, {
  323. scope: form,
  324. render: "slot",
  325. slots
  326. })
  327. : e.label;
  328. },
  329. default() {
  330. return (
  331. <div>
  332. <div class="cl-form-item">
  333. {["prepend", "component", "append"]
  334. .filter((k) => e[k])
  335. .map((name) => {
  336. const children = e.children && (
  337. <div class="cl-form-item__children">
  338. <el-row gutter={10}>
  339. {e.children.map(renderFormItem)}
  340. </el-row>
  341. </div>
  342. );
  343. const Item = renderNode(e[name], {
  344. item: e,
  345. prop: e.prop,
  346. scope: form,
  347. slots,
  348. children,
  349. _data: {
  350. isDisabled
  351. }
  352. });
  353. return (
  354. <div
  355. v-show={!e.collapse}
  356. class={[
  357. `cl-form-item__${name}`,
  358. {
  359. flex1: e.flex !== false
  360. }
  361. ]}
  362. style={e[name].style}>
  363. {Item}
  364. </div>
  365. );
  366. })}
  367. </div>
  368. {isBoolean(e.collapse) && (
  369. <div
  370. class="cl-form-item__collapse"
  371. onClick={() => {
  372. Action.collapseItem(e);
  373. }}>
  374. <el-divider content-position="center">
  375. {e.collapse
  376. ? dict.label.seeMore
  377. : dict.label.hideContent}
  378. </el-divider>
  379. </div>
  380. )}
  381. </div>
  382. );
  383. }
  384. }
  385. );
  386. let span = e.span || style.form.span;
  387. if (browser.isMini) {
  388. span = 24;
  389. }
  390. // 是否行内
  391. const Item = props.inline ? (
  392. FormItem
  393. ) : (
  394. <el-col span={span} {...e.col} v-show={inGroup && !e._hidden}>
  395. {FormItem}
  396. </el-col>
  397. );
  398. return isLoaded ? Item : null;
  399. }
  400. // 渲染表单
  401. function renderContainer() {
  402. // 表单项列表
  403. const children = config.items.map(renderFormItem);
  404. // 表单标签位置
  405. const labelPosition =
  406. browser.isMini && !props.inline
  407. ? "top"
  408. : config.props.labelPosition || style.form.labelPosition;
  409. return (
  410. <div class="cl-form__container" ref={setRefs("form")}>
  411. {h(
  412. <el-form
  413. ref={Form}
  414. size={style.size}
  415. label-width={style.form.labelWidth}
  416. inline={props.inline}
  417. disabled={saving.value}
  418. scroll-to-error
  419. model={form}
  420. onSubmit={(e: Event) => {
  421. submit();
  422. e.preventDefault();
  423. }}
  424. />,
  425. {
  426. ...config.props,
  427. labelPosition
  428. },
  429. {
  430. default: () => {
  431. const items = [
  432. slots.prepend && slots.prepend({ scope: form }),
  433. children,
  434. slots.append && slots.append({ scope: form })
  435. ];
  436. return (
  437. <div class="cl-form__items" v-loading={loading.value}>
  438. {props.inline ? (
  439. items
  440. ) : (
  441. <el-row gutter={10}>{items}</el-row>
  442. )}
  443. </div>
  444. );
  445. }
  446. }
  447. )}
  448. </div>
  449. );
  450. }
  451. // 渲染表单按钮
  452. function renderFooter() {
  453. const { hidden, buttons, saveButtonText, closeButtonText, justify } = config.op;
  454. if (hidden) {
  455. return null;
  456. }
  457. const Btns = buttons?.map((e: any) => {
  458. switch (e) {
  459. case "save":
  460. return (
  461. <el-button
  462. type="success"
  463. size={style.size}
  464. disabled={loading.value}
  465. loading={saving.value}
  466. onClick={() => {
  467. submit();
  468. }}>
  469. {saveButtonText}
  470. </el-button>
  471. );
  472. case "close":
  473. return (
  474. <el-button
  475. size={style.size}
  476. onClick={() => {
  477. close("close");
  478. }}>
  479. {closeButtonText}
  480. </el-button>
  481. );
  482. default:
  483. return renderNode(e, {
  484. scope: form,
  485. slots,
  486. custom() {
  487. return (
  488. <el-button
  489. text
  490. type={e.type}
  491. bg
  492. {...e.props}
  493. onClick={() => {
  494. e.onClick({ scope: form });
  495. }}>
  496. {e.label}
  497. </el-button>
  498. );
  499. }
  500. });
  501. }
  502. });
  503. return (
  504. <div
  505. class="cl-form__footer"
  506. style={{
  507. justifyContent: justify || "flex-end"
  508. }}>
  509. {Btns}
  510. </div>
  511. );
  512. }
  513. expose({
  514. refs,
  515. Form,
  516. visible,
  517. saving,
  518. form,
  519. config,
  520. loading,
  521. disabled,
  522. open,
  523. close,
  524. done,
  525. clear,
  526. reset,
  527. submit,
  528. bindForm,
  529. showLoading,
  530. hideLoading,
  531. setDisabled,
  532. Tabs,
  533. ...Action,
  534. ...ElFormApi
  535. });
  536. return () => {
  537. if (props.inner) {
  538. return (
  539. visible.value && (
  540. <div class="cl-form">
  541. {renderContainer()}
  542. {renderFooter()}
  543. </div>
  544. )
  545. );
  546. } else {
  547. return h(
  548. <cl-dialog v-model={visible.value} class="cl-form" />,
  549. {
  550. title: config.title,
  551. height: config.height,
  552. width: config.width,
  553. ...config.dialog,
  554. beforeClose,
  555. onClosed,
  556. keepAlive: false
  557. },
  558. {
  559. default() {
  560. return renderContainer();
  561. },
  562. footer() {
  563. return renderFooter();
  564. }
  565. }
  566. );
  567. }
  568. };
  569. }
  570. });