base.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /**
  2. * 表单项基础类, 所有输入组件都继承Base
  3. * @module $ui/components/my-form/src/Base
  4. */
  5. import { FormItem } from "element-ui";
  6. import { setStyle } from "element-ui/lib/utils/dom";
  7. import { addResizeListener, removeResizeListener } from "element-ui/lib/utils/resize-event";
  8. const _get = require("lodash/get");
  9. const _set = require("lodash/set");
  10. const _isEqual = require("lodash/isEqual");
  11. const _cloneDeep = require("lodash/cloneDeep");
  12. /**
  13. * 深拷贝
  14. * @param {*} value 要深拷贝的值
  15. * @return {*} 返回拷贝后的值
  16. */
  17. export function cloneDeep(value) {
  18. return _cloneDeep(value);
  19. }
  20. /**
  21. * 判断两个对象是否相等
  22. * @param {*} object 对象1
  23. * @param {*} other 对象2
  24. * @return {boolean}
  25. */
  26. export function isEqual(object, other) {
  27. return _isEqual(object, other);
  28. }
  29. /**
  30. * 插槽
  31. * @member slots
  32. * @property {string} before 输入组件前面的内容,仅当父组件是MyForm有效
  33. * @property {string} after 输入组件后面的内容,仅当父组件是MyForm有效
  34. * @property {string} label 定义字段的label内容,仅当父组件是MyForm有效
  35. * @property {string} error 作用域插槽,定义验证错误提示内容,仅当父组件是MyForm有效
  36. */
  37. export default {
  38. inject: {
  39. myForm: {
  40. default: null
  41. }
  42. },
  43. components: {
  44. FormItem
  45. },
  46. /**
  47. * 属性参数
  48. * @member props
  49. * @property {string} [name] 表单域 model 字段名称, 等价于 el-form-item 的 prop 参数
  50. * @property {string} [width] 宽度,css属性,支持像素,百分比和表达式,也可以在MyForm中统一设置itemWidth
  51. * @property {object} [props] 输入组件参数对象,即 element 组件的参数
  52. * @property {Array} [options] 选项数据,数据优先顺序,options > loader > form.dictMap > form.loader
  53. * @property {Object} [keyMap] 选项数据对象属性名称映射, 默认:{id, parentId, label, value}
  54. * @property {boolean} [collapsible] 可收起
  55. * @property {boolean} [stopEnterEvent] 阻止回车事件冒泡
  56. * @property {string} [depend] 依赖字段名称
  57. * @property {*} [dependValue] 依赖字段的值,即依赖字段的值等于该值才会显示
  58. * @property {string} [cascade] 级联的上级字段名称,需要与loader配合加载数据
  59. * @property {Function} [loader] 加载数据函数,必须返回Promise
  60. * @property {string} [dict] 字典名称,只是标识,需要与loader配合 或 表单的dictMap加载数据
  61. * @property {boolean} [disabled] 禁用
  62. * @property {boolean} [readonly] 只读
  63. * @property {string} [placeholder] 占位文本
  64. *
  65. */
  66. props: {
  67. // 表单域 model 字段名称
  68. name: String,
  69. // 宽度,支持像素,百分比和表达式
  70. width: String,
  71. // 输入组件参数对象
  72. props: Object,
  73. // 选项数据,数据优先顺序,options > loader > form.dictMap > form.loader
  74. options: Array,
  75. // 选项数据对象属性名称映射
  76. keyMap: {
  77. type: Object,
  78. default() {
  79. return {
  80. id: "id",
  81. label: "label",
  82. value: "value",
  83. disabled: "disabled",
  84. parentId: "parentId"
  85. };
  86. }
  87. },
  88. // 可折叠
  89. collapsible: Boolean,
  90. // 阻止回车事件冒泡
  91. stopEnterEvent: Boolean,
  92. // 依赖字段名称
  93. depend: String,
  94. // 依赖字段的值,即依赖字段的值等于该值才会显示
  95. dependValue: [String, Number, Boolean, Object, Array, Function],
  96. // 级联的上级字段名称,需要与loader配合加载数据
  97. cascade: String,
  98. // 加载数据函数,必须返回Promise
  99. loader: Function,
  100. // 字典名称,只是标识,需要与loader配合 或 表单的dictMap加载数据
  101. dict: String,
  102. // 禁用
  103. disabled: Boolean,
  104. // 只读
  105. readonly: Boolean,
  106. // 占位文本
  107. placeholder: String,
  108. // 尺寸
  109. size: String
  110. },
  111. data() {
  112. return {
  113. // 级联的值缓存
  114. cascadeValue: null,
  115. // 当前选项数据
  116. currentOptions: [],
  117. // 正在调用loader
  118. loading: false
  119. };
  120. },
  121. computed: {
  122. // 如果有name参数,并且是MyForm的子组件,即与MyForm的currentModel作双向绑定
  123. // 否则与组件自身的value作双向绑定
  124. fieldValue: {
  125. get() {
  126. if (this.name && this.myForm) {
  127. const { currentModel } = this.myForm;
  128. return _get(currentModel, this.name, this.getDefaultValue());
  129. } else {
  130. return this.value || this.getDefaultValue();
  131. }
  132. },
  133. set(val) {
  134. if (this.name && this.myForm) {
  135. const { currentModel } = this.myForm;
  136. const model = cloneDeep(currentModel);
  137. _set(model, this.name, val);
  138. if (!isEqual(currentModel, model)) {
  139. this.myForm.currentModel[this.name] = model[this.name];
  140. this.myForm.currentModel = model;
  141. }
  142. } else {
  143. this.$emit("input", val);
  144. }
  145. }
  146. },
  147. // 字段域的宽度
  148. itemWidth() {
  149. // 优先取自身设置的宽度,没有就取父组件设置的公共设置宽度
  150. return (
  151. this.width || (this.myForm && this.myForm.itemWidth ? this.myForm.itemWidth : null)
  152. );
  153. },
  154. // 字段域样式
  155. itemStyle() {
  156. return {
  157. width: this.itemWidth
  158. };
  159. },
  160. // 输入框组件参数
  161. innerProps() {
  162. return {
  163. disabled: this.disabled,
  164. readonly: this.readonly,
  165. placeholder: this.placeholder,
  166. size: this.size,
  167. ...this.props
  168. };
  169. }
  170. },
  171. watch: {
  172. itemWidth: {
  173. immediate: true,
  174. handler() {
  175. this.$nextTick(() => {
  176. this.setContentWidth();
  177. });
  178. }
  179. },
  180. "myForm.currentCollapsed"(val) {
  181. const { resetCollapsed, model } = this.myForm;
  182. // 收起时重置表单项值
  183. if (val && resetCollapsed && model && this.collapsible) {
  184. this.$nextTick(() => {
  185. // this.fieldValue = this.myForm.model[this.name]
  186. this.fieldValue = _get(this.myForm.model, this.name, this.getDefaultValue());
  187. });
  188. }
  189. // 开启了折叠功能
  190. if (this.collapsible) {
  191. // 折叠时先要清除事件句柄,因为原先的dom即将发生改变
  192. if (val) {
  193. removeResizeListener(this.$el, this.setContentWidth);
  194. } else {
  195. // 如果没有加载过选项数据,触发加载函数
  196. if (!this.currentOptions || this.currentOptions.length === 0) {
  197. this.loadOptions(this.myForm.currentModel, this);
  198. }
  199. // 展开时,待DOM生成后,重新注册事件句柄
  200. this.$nextTick(() => {
  201. addResizeListener(this.$el, this.setContentWidth);
  202. this.setContentWidth();
  203. });
  204. }
  205. }
  206. },
  207. // options 为了提高性能,不设置deep
  208. options: {
  209. immediate: true,
  210. handler(val) {
  211. this.currentOptions = cloneDeep(val) || [];
  212. // options改变后,会触发表单验证,这里需要清楚验证错误信息
  213. this.$nextTick(() => {
  214. this.clearValidate();
  215. });
  216. }
  217. }
  218. },
  219. methods: {
  220. // 获取表单项的默认值,不同组件有不同的默认值,可在具体的组件重写这个函数
  221. getDefaultValue() {
  222. return "";
  223. },
  224. // 重置字段
  225. resetField() {
  226. this.$refs.elItem && this.$refs.elItem.resetField();
  227. },
  228. // 清除验证错误信息
  229. clearValidate() {
  230. this.$refs.elItem && this.$refs.elItem.clearValidate();
  231. },
  232. isCollapsed() {
  233. if (!this.myForm) return false;
  234. const { collapsible, currentCollapsed } = this.myForm;
  235. // 是否已收起
  236. return collapsible && currentCollapsed && this.collapsible;
  237. },
  238. isMatchDepend() {
  239. // 没有设置依赖,即忽略,当已匹配处理
  240. if (!this.depend || !this.myForm) return true;
  241. const model = this.myForm.currentModel;
  242. // 依赖不支持 按路径查找
  243. const value = model[this.depend];
  244. let isMatch = true;
  245. // 如果 dependValue 是函数,执行回调函数返回布尔值
  246. if (typeof this.dependValue === "function") {
  247. isMatch = this.dependValue(value, model, this);
  248. } else {
  249. // 以上都不符合,即检验 dependValue 与 currentModel中的依赖属性是否一致
  250. isMatch = isEqual(this.dependValue, value);
  251. }
  252. // 清除依赖不符合字段的值
  253. if (!isMatch && this.name && model[this.name]) {
  254. this.fieldValue = this.getDefaultValue();
  255. delete model[this.name];
  256. }
  257. return isMatch;
  258. },
  259. // 传递给输入组件的插槽
  260. createSlots(slots = []) {
  261. return slots.map(name => {
  262. return <template slot={name}>{this.$slots[name]}</template>;
  263. });
  264. },
  265. // 渲染输入组件
  266. renderComponent(vnode) {
  267. // 如果组件不是MyForm的子组件,不需要包裹Item组件
  268. if (!this.myForm) {
  269. return vnode;
  270. }
  271. // el-form-item 作用域插槽
  272. const scopedSlots = this.$scopedSlots.error
  273. ? {
  274. error: props => (
  275. <div class="el-form-item__error my-from__custom-error">
  276. {this.$scopedSlots.error(props)}
  277. </div>
  278. )
  279. }
  280. : null;
  281. // 是否已收起
  282. const collapsed = this.isCollapsed();
  283. // 是否符合依赖项
  284. const isMatched = this.isMatchDepend();
  285. return (
  286. <transition name={this.myForm.collapseEffect}>
  287. {!collapsed && isMatched ? (
  288. <FormItem
  289. ref="elItem"
  290. class="my-form-item"
  291. {...{
  292. props: this.$attrs,
  293. scopedSlots: scopedSlots,
  294. style: this.itemStyle
  295. }}
  296. // 停止回车键事件冒泡
  297. nativeOnKeyup={this.stopEvent}
  298. // el-form-item 的prop用name代替
  299. prop={this.name}>
  300. {// label 插槽
  301. this.$slots.label ? (
  302. <template slot="label">{this.$slots.label}</template>
  303. ) : null}
  304. {this.$slots.before}
  305. {vnode}
  306. {this.$slots.after}
  307. </FormItem>
  308. ) : (
  309. // Vue组件必须要有一个根DOM,创建一个隐藏占位元素
  310. <div style={{ display: "none" }}>{this.name}</div>
  311. )}
  312. </transition>
  313. );
  314. },
  315. // 继承输入组件暴露的方法
  316. extendMethods(ref, names = []) {
  317. if (!ref) return;
  318. names.forEach(name => {
  319. // 子组件的方法加到实例
  320. this[name] = (...args) => {
  321. ref[name].apply(ref, args);
  322. };
  323. });
  324. },
  325. // 设置el-form-item内部的内容区宽度
  326. setContentWidth() {
  327. // 字段域没有设置宽度,默认自适应,不需要处理
  328. if (!this.itemWidth || !this.$el) return;
  329. const content = this.$el.querySelector(".el-form-item__content");
  330. const label = this.$el.querySelector(".el-form-item__label");
  331. if (content) {
  332. const rect = label ? label.getBoundingClientRect() : { width: 0 };
  333. const itemWidth = this.$el.getBoundingClientRect().width;
  334. const contentWidth = itemWidth - rect.width;
  335. setStyle(content, { width: `${contentWidth}px` });
  336. }
  337. },
  338. // 阻止回车事件冒泡
  339. stopEvent(e) {
  340. if (this.stopEnterEvent) {
  341. e.stopPropagation();
  342. }
  343. },
  344. // 加载选项数据
  345. loadOptions(model) {
  346. // 已收起的,不需要处理
  347. if (this.isCollapsed()) return;
  348. // 如果不符合依赖,不处理
  349. if (!this.isMatchDepend()) return;
  350. // 数据优先顺序,options > loader > form.dictMap > form.loader
  351. if (this.options) {
  352. this.currentOptions = cloneDeep(this.options);
  353. return;
  354. }
  355. if (this.loader) {
  356. this.loading = true;
  357. this.loader(model, this)
  358. .then(res => {
  359. this.currentOptions = cloneDeep(res);
  360. })
  361. .finally(() => {
  362. this.loading = false;
  363. });
  364. return;
  365. }
  366. // 无form容器,终止
  367. if (!this.myForm) return;
  368. if (this.dict) {
  369. const { dictMap } = this.myForm;
  370. const options = (dictMap || {})[this.dict];
  371. // 建立与表单的字典数据引用
  372. if (options) {
  373. this.currentOptions = options;
  374. return;
  375. }
  376. }
  377. if (this.myForm.loader) {
  378. this.loading = true;
  379. this.myForm
  380. .loader(model, this)
  381. .then(res => {
  382. this.currentOptions = cloneDeep(res);
  383. })
  384. .finally(() => {
  385. this.loading = false;
  386. });
  387. }
  388. },
  389. // 响应currentModel改变处理级联加载数据
  390. handleWatch(model) {
  391. // 级联上级的值
  392. const val = model[this.cascade];
  393. // 与上次的值不一致即重新获取数据
  394. if (!isEqual(this.cascadeValue, val)) {
  395. this.fieldValue = this.getDefaultValue();
  396. this.cascadeValue = val;
  397. this.loadOptions(model);
  398. }
  399. },
  400. // 绑定级联
  401. bindCascade() {
  402. if (this.cascade && this.myForm) {
  403. const model = this.myForm.currentModel;
  404. this.cascadeValue = model[this.cascade];
  405. this.unwatch = this.$watch("myForm.currentModel", this.handleWatch, { deep: true });
  406. }
  407. },
  408. // 销毁级联事件句柄
  409. unbindCascade() {
  410. this.unwatch && this.unwatch();
  411. }
  412. },
  413. mounted() {
  414. addResizeListener(this.$el, this.setContentWidth);
  415. },
  416. created() {
  417. let model = null;
  418. if (this.myForm) {
  419. this.myForm.addItem(this);
  420. model = this.myForm.currentModel;
  421. }
  422. this.loadOptions(model, this);
  423. this.bindCascade();
  424. },
  425. beforeDestroy() {
  426. removeResizeListener(this.$el, this.setContentWidth);
  427. this.unbindCascade();
  428. if (this.myForm) {
  429. this.myForm.removeItem(this);
  430. }
  431. }
  432. };