index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { h, ref, toRefs, nextTick } from 'vue'
  2. import { ElForm } from 'element-plus'
  3. import { useFormProvide } from '../hooks/use-form'
  4. import CipFormItem from '../cip-form-item'
  5. import CipFormDirectory from './form-directory'
  6. import CipFormLayout from '../cip-form-layout'
  7. import { DRender } from '../helper/d-render'
  8. import { toUpperFirstCase, getFieldValue } from '@cip/utils/util'
  9. import CipMessage from '../cip-message'
  10. import ElementClass from './element'
  11. import scrollIntoView from 'scroll-into-view-if-needed'
  12. import './index.less'
  13. const dRender = new DRender()
  14. export default {
  15. name: 'CipForm',
  16. props: {
  17. model: Object,
  18. fieldList: Array,
  19. showOnly: Boolean,
  20. modelKey: {
  21. type: [String, Function]
  22. },
  23. grid: { type: [Number, Boolean] }, // 是否开启grid布局
  24. useDirectory: Boolean,
  25. labelPosition: String,
  26. equipment: {
  27. type: String,
  28. default: 'pc',
  29. validate: (val) => ['pc', 'mobile'].includes(val)
  30. },
  31. isScrollError: {
  32. type: Boolean,
  33. default: true
  34. },
  35. border: Boolean, // showOnly + border 将出现边框
  36. enterHandler: Function, // 回车触发回调
  37. options: Object
  38. },
  39. emits: ['update:model', 'submit', 'cancel'],
  40. setup (props, context) {
  41. // 下发属性
  42. const uploadQueue = ref({})
  43. useFormProvide(props, uploadQueue)
  44. const directoryConfig = ref([])
  45. const { model, fieldList } = toRefs(props)
  46. const cipFormRef = ref()
  47. // 修改model的值
  48. const updateModel = (val) => {
  49. context.emit('update:model', val)
  50. }
  51. const generateComponentKey = (key) => {
  52. if (props.modelKey) {
  53. const appendKey = toUpperFirstCase(key)
  54. if (typeof props.modelKey === 'function') {
  55. return `${props.modelKey(props.model)}${appendKey}`
  56. } else {
  57. const value = getFieldValue(props.model, props.modelKey)
  58. return `${value || ''}${appendKey}`
  59. }
  60. } else {
  61. return key
  62. }
  63. }
  64. // 获取layout及item组件需要的props
  65. const getComponentProps = (key, config) => {
  66. const componentKey = generateComponentKey(key)
  67. const componentProps = {
  68. key: componentKey,
  69. componentKey: componentKey,
  70. model,
  71. fieldKey: key,
  72. config,
  73. readonly: props.showOnly,
  74. grid: props.grid,
  75. formLabelPosition: props.labelPosition,
  76. 'onUpdate:model': (val) => {
  77. if (componentKey === generateComponentKey(key)) {
  78. updateModel(val)
  79. }
  80. }
  81. }
  82. if (props.enterHandler) {
  83. componentProps.onKeyup = (e) => {
  84. const { keyCode } = e
  85. if (keyCode === 13) {
  86. props.enterHandler()
  87. }
  88. }
  89. }
  90. return componentProps
  91. }
  92. // 布局字段渲染方式
  93. const getFormLayout = (componentProps) => {
  94. return h(CipFormLayout, {
  95. ...componentProps,
  96. onValidate: (cb) => {
  97. validate(cb)
  98. },
  99. onSubmit: () => {
  100. context.emit('submit')
  101. },
  102. onCancel: () => {
  103. context.emit('cancel')
  104. }
  105. }, {
  106. item: ({ children = [], isShow } = {}) => {
  107. return children.map((v) => getFormDefaultSlot(v, isShow))
  108. }
  109. })
  110. }
  111. // 输入字段渲染方式
  112. const getFormItem = (componentProps) => {
  113. return h(CipFormItem, componentProps)
  114. }
  115. // 渲染单个字段
  116. const getFormDefaultSlot = ({ key, config } = {}, isShow) => {
  117. // 若存在字段key值的插槽覆盖则配置整个ElFormItem
  118. config._isGrid = props.grid
  119. config._isShow = isShow
  120. if (context.slots[key]) {
  121. return context.slots[key]({ key, config })
  122. }
  123. const componentProps = getComponentProps(key, config)
  124. // 若存在字段key值+Input的插槽覆盖则配置ElFormItem内的Input
  125. if (context.slots[`${key}Input`]) {
  126. return h(CipFormItem, {
  127. ...componentProps,
  128. customSlots: context.slots[`${key}Input`]
  129. })
  130. }
  131. // 判断是否为布局类型的字段
  132. if (dRender.isLayoutType(config.type)) {
  133. // layout类型字段
  134. return getFormLayout(componentProps)
  135. } else {
  136. // input类型字段
  137. // 如果需要表单目录导航则添加
  138. if (config.directory) {
  139. directoryConfig.value[key] = { label: config.staticInfo || config.label, level: config.directory }
  140. }
  141. return getFormItem(componentProps)
  142. }
  143. }
  144. // 渲染表单
  145. const getFormDefaultSlots = () => {
  146. if (props.useDirectory) {
  147. return fieldList.value.map((v) => getFormDefaultSlot(v)).concat(
  148. [h(CipFormDirectory, { directory: directoryConfig.value })]
  149. )
  150. } else {
  151. return fieldList.value.map((v) => getFormDefaultSlot(v))
  152. }
  153. }
  154. /** start父组件通过ref调用方法 **/
  155. const validateUpload = () => {
  156. return new Promise((resolve, reject) => {
  157. const keys = Object.keys(uploadQueue.value)
  158. for (let i = 0; i < keys.length; i++) {
  159. const key = keys[i]
  160. if (uploadQueue.value[key]) {
  161. CipMessage.error('请等待文件上传', '提示')
  162. resolve(false)
  163. break
  164. }
  165. }
  166. resolve(true)
  167. })
  168. }
  169. const validateField = (props, cb) => {
  170. return cipFormRef.value.validateField(props, cb)
  171. }
  172. const validate = async (cb = () => {}) => {
  173. const isUpload = await validateUpload()
  174. if (!isUpload) {
  175. // eslint-disable-next-line standard/no-callback-literal
  176. cb(false)
  177. throw new Error('请等待文件上传')
  178. } else {
  179. // const res = await cipFormRef.value.validate() // 此方式返回的res为 true or false
  180. return new Promise((resolve, reject) => {
  181. cipFormRef.value.validate(async (isValid, invalidFields) => {
  182. // 自动定位到error项
  183. props.isScrollError && await scrollToField(null, props.options)
  184. if (typeof cb === 'function') cb(isValid, invalidFields)
  185. if (isValid) {
  186. resolve(isValid)
  187. } else {
  188. reject(isValid)
  189. }
  190. })
  191. })
  192. // console.log(res)
  193. // return await cipFormRef.value.validate(async (isValid, invalidFields) => {
  194. // // 自动定位到error项
  195. // props.isScrollError && await scrollToField(null, props.options)
  196. // if (typeof cb === 'function') {
  197. // cb(isValid, invalidFields)
  198. // }
  199. // })
  200. }
  201. }
  202. const scrollToField = async (name, options = {}) => {
  203. await nextTick(() => {
  204. const elements = new ElementClass(cipFormRef.value.$el)
  205. // 加了name是手动定位,不检查是否验证通过
  206. // 自动定位到第一个未校验同通过项
  207. const target = name ? elements.getItemByName(name) : elements.getFirstErrorItem()
  208. // 原生scrollIntoView会导致整个页面滚动
  209. target && scrollIntoView(target, {
  210. scrollMode: 'if-needed',
  211. block: 'nearest',
  212. behavior: 'smooth',
  213. inline: 'nearest',
  214. ...options
  215. })
  216. })
  217. }
  218. const clearValidate = () => {
  219. return cipFormRef.value?.clearValidate()
  220. }
  221. context.expose({
  222. validateUpload,
  223. validateField,
  224. validate,
  225. clearValidate,
  226. scrollToField
  227. })
  228. /** end父组件通过ref调用方法 **/
  229. return () => h(ElForm, {
  230. ...context.attrs,
  231. ref: cipFormRef,
  232. hideRequiredAsterisk: true,
  233. model: model, // 待进行测试 使用model.value后数据是否正常
  234. class: ['cip-form', `cip-form--${props.equipment}`, { 'cip-form--grid': props.grid, 'cip-form--border': props.border && props.showOnly }],
  235. style: { gridTemplateColumns: `repeat(${typeof props.grid === 'number' ? props.grid : 3},1fr)` },
  236. size: 'small',
  237. labelPosition: props.labelPosition,
  238. onSubmit: ev => { ev.preventDefault() }
  239. }, { default: () => [getFormDefaultSlots(), context.slots.default?.()] })
  240. }
  241. }