index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. <script lang="tsx">
  2. import { defineComponent, watch, computed, ref, reactive, unref, PropType } from 'vue'
  3. import { useI18n } from 'vue-i18n'
  4. // import { t } from 'lance-element-vue/src/locale'
  5. // import InputNumber from 'lance-element-vue/packages/InputNumber'
  6. import InputNumber from '../InputNumber'
  7. // import InputNumberRange from 'lance-element-vue/packages/InputNumberRange'
  8. import InputNumberRange from '../InputNumberRange'
  9. // import CustomRender from 'lance-element-vue/packages/CustomRender'
  10. import CustomRender from '../CustomRender'
  11. // import LeSelect from 'lance-element-vue/packages/Select'
  12. import LeSelect from '../Select'
  13. import { renderOption, getOptions, get_formSlots } from './utils.ts'
  14. import { LeFormItem, ObjectOpts, FormConfigOpts, FormItemSlots, SlotOption } from './formConfig.types'
  15. import { OptionItemProps } from '@/components/Select/select.types.ts'
  16. // import { useFormItems } from './hooks/useForm.tsx'
  17. export const FormConfigProps = {
  18. forms: {
  19. type: Array as PropType<LeFormItem[]>,
  20. required: true
  21. },
  22. formData: {
  23. // 后台传递的初始值 对象 【后期拿操作的表单数据 请使用 submit 的params】
  24. type: Object as PropType<ObjectOpts>,
  25. default: () => ({})
  26. }, // 后台传递过来的 数据
  27. // form的配置项对象 参考 default_formConfig
  28. formConfig: {
  29. // type: Object,
  30. type: Object as PropType<FormConfigOpts>,
  31. default: () => ({})
  32. },
  33. // 是否可以编辑
  34. isEdit: {
  35. type: Boolean,
  36. default: true
  37. }
  38. }
  39. const formConfigEmits = ['cancel', 'submit']
  40. const default_formConfig: FormConfigOpts = {
  41. /**自定义Config*/
  42. // 默认的formItem内容宽度(eg: input/select/radio...)
  43. itemWidth: undefined,
  44. // 默认的formItem 对应的 col 外壳 span 配置
  45. span: 24,
  46. // 是否展示 底部操作集合
  47. showFooter: true,
  48. // footer下的 提交按钮 描述
  49. submitBtnText: 'le.btn.confirm',
  50. // footer下的 提交按钮loading
  51. submitLoading: false,
  52. // footer下的 取消按钮 是否显示
  53. showCancelBtn: false,
  54. // footer下的 取消按钮 text
  55. cancelBtnText: 'le.btn.cancel',
  56. // footer下的 重置按钮 是否显示
  57. showResetBtn: false,
  58. // footer下的 重置按钮 text
  59. resetBtnText: 'le.btn.reset',
  60. /**
  61. * element中的配置
  62. * 更多请参考 [https://element-plus.gitee.io/zh-CN/component/form.html]
  63. */
  64. labelPosition: 'right',
  65. // 表单域标签的后缀
  66. labelSuffix: ':',
  67. // 表单内组件的尺寸
  68. size: 'default'
  69. }
  70. const FormConfig = defineComponent({
  71. name: 'LeFormConfig',
  72. components: {
  73. CustomRender,
  74. InputNumber,
  75. InputNumberRange,
  76. LeSelect
  77. },
  78. emits: formConfigEmits,
  79. props: FormConfigProps,
  80. setup(props, ctx) {
  81. const { t } = useI18n()
  82. const formRef = ref(/*formRef*/)
  83. /*const queryItemTypeKeys = form => {
  84. const { prop, itemType } = form
  85. switch (itemType) {
  86. // 对Number区间进行特殊处理
  87. case 'inputNumberRange':
  88. const propStart = form.propStart || `${prop}Start`
  89. const propEnd = form.propEnd || `${prop}End`
  90. return [propStart, propEnd]
  91. case 'render':
  92. // /!** !!! 暂不对render类型 进行更多标签处理 *!/
  93. // return []
  94. case 'leSelect':
  95. case 'select':
  96. case 'radio':
  97. case 'datePicker':
  98. case 'inputNumber':
  99. case 'input':
  100. default:
  101. return [prop]
  102. }
  103. }*/
  104. const setItemData = (value, defaultValue?) => {
  105. if (typeof value !== 'boolean' && typeof value !== 'number') {
  106. return value || defaultValue
  107. }
  108. return value
  109. }
  110. const changeFormData = (formData, isInit?) => {
  111. const { forms } = props,
  112. // params = reactive({}),
  113. params = {},
  114. bindProps = []
  115. forms.forEach((v, i) => {
  116. const _prop = v.prop,
  117. props = v.props // 绑定的其他数据
  118. let defaultValue: any
  119. if (v.itemType === 'inputNumberRange') defaultValue = []
  120. params[_prop] = setItemData(formData[_prop], defaultValue) // 数据初始化
  121. /*const itemProps = queryItemTypeKeys(v)
  122. itemProps.map(prop => {
  123. params[prop] = setItemData(formData[prop]) // 数据初始化
  124. })*/
  125. /**fix 自定义inputNumberRange*/
  126. /*if(v.itemType === 'inputNumberRange') {
  127. params[_prop] = computed({
  128. get() {
  129. return itemProps.map(key => params[key])
  130. },
  131. set(values) {
  132. itemProps.map((key, i) => {
  133. params[key] = values[i]
  134. })
  135. console.error(values, 'values')
  136. }
  137. })
  138. }*/
  139. // 若该formItem 包含forms列表中未定义的prop 需要从formData取值
  140. if (Array.isArray(props)) {
  141. bindProps.push(...props)
  142. }
  143. })
  144. // 赋值其他被绑的的值
  145. bindProps.map(prop => {
  146. params[prop] = formData[prop] // 被绑定的其他数据初始化
  147. })
  148. if (isInit) {
  149. return {
  150. params,
  151. bindProps
  152. }
  153. } else {
  154. state.params = params
  155. state.bindProps = bindProps
  156. }
  157. }
  158. const cancelHandler = () => {
  159. ctx.emit('cancel')
  160. }
  161. const submitHandler = submitCallback => {
  162. getParams((error, params) => {
  163. if (!error) {
  164. // 若在父级组件使用 自定义的操作按钮, 可调用callback函数 作为提交操作
  165. if (typeof submitCallback === 'function') {
  166. return submitCallback(params)
  167. }
  168. ctx.emit('submit', params)
  169. }
  170. })
  171. }
  172. const getParams = (callback, unValidate = false) => {
  173. const _getParams = () => {
  174. const { forms } = props
  175. const { params, formValueFormats } = state
  176. const formattedForm = {} // 最后提交后台使用的params对象
  177. forms.forEach(form => {
  178. const prop = form.prop
  179. if (prop) {
  180. formattedForm[prop] = params[prop]
  181. // // 对应的form 内部设置有 formValueFormats 函数的值做提交前的最后操作 fn(value, prop)
  182. // formattedForm[prop] = formValueFormats[prop] ? formValueFormats[prop](params, prop) : params[prop]
  183. }
  184. /*queryItemTypeKeys(form).map(prop => {
  185. formattedForm[prop] = params[prop]
  186. })*/
  187. // 对含有 其他prop的数据 赋值
  188. if (Array.isArray(form.props)) {
  189. form.props.map(_key => {
  190. formattedForm[_key] = params[_key]
  191. })
  192. }
  193. })
  194. if (callback) callback(null, formattedForm)
  195. }
  196. if (unValidate) return _getParams()
  197. formRef.value.validate((valid, errObj) => {
  198. if (valid) {
  199. _getParams()
  200. } else {
  201. if (callback) callback(errObj)
  202. }
  203. })
  204. }
  205. const resetHandler = (isAsync = false) => {
  206. // 是否异步 传入的 formData 重置
  207. if (isAsync) {
  208. // 由于 formData 从父级传入后 内部并没有直接使用 固可用做 重置功能
  209. changeFormData(props.formData)
  210. } else {
  211. formRef.value.resetFields()
  212. const formData = props.formData || {}
  213. // 额外 props 通过formData 数据进行重置
  214. state.bindProps.map(prop => {
  215. state.params[prop] = formData[prop]
  216. })
  217. }
  218. }
  219. watch(
  220. () => props.formData,
  221. (newData, oldData) => {
  222. // console.warn(JSON.stringify(newData), JSON.stringify(oldData), 'newFormData, oldFormData... 监听 formData')
  223. changeFormData(newData)
  224. }
  225. )
  226. // 本地数据
  227. const state = reactive({
  228. // { params, bindProps }
  229. ...changeFormData(props.formData, true)
  230. /*params,
  231. // 额外props 集合
  232. bindProps: []*/
  233. /*formValueFormats: props.forms.reduce((res, v) => {
  234. const { prop, formValueFormat } = v
  235. if (formValueFormat) {
  236. // 遍历 集成formValueFormat对象
  237. res[prop] = formValueFormat
  238. }
  239. return res
  240. }, {})*/
  241. })
  242. const local_formConfig = computed(() => {
  243. return {
  244. ...default_formConfig,
  245. ...props.formConfig
  246. }
  247. })
  248. const itemStyle = computed(() => {
  249. const { itemWidth } = props.formConfig
  250. if (itemWidth) {
  251. return `width: ${itemWidth};`
  252. }
  253. return ''
  254. })
  255. /*const { renderForms } = useFormItems('FormConfig', {
  256. props,
  257. params: state.params,
  258. slots: ctx.slots,
  259. })*/
  260. ctx.expose({
  261. formRef,
  262. getParams,
  263. resetHandler,
  264. submitHandler,
  265. cancelHandler
  266. })
  267. const vSlots = ctx.slots
  268. const realForms = computed(() => {
  269. return (props.forms || []).map((form: LeFormItem) => {
  270. return {
  271. ...form,
  272. le_slots: get_formSlots(vSlots, form.slots)
  273. }
  274. })
  275. })
  276. // render
  277. return () => {
  278. const { params } = state
  279. const { isEdit } = props
  280. const {
  281. size,
  282. gutter,
  283. span,
  284. showFooter,
  285. submitBtnText,
  286. cancelBtnText,
  287. resetBtnText,
  288. submitLoading,
  289. showCancelBtn,
  290. showResetBtn,
  291. ...form_config
  292. } = unref(local_formConfig)
  293. const itemRender = form => {
  294. // const propKey = form.prop
  295. const {
  296. prop,
  297. itemType,
  298. itemWidth,
  299. options,
  300. change,
  301. // 申明: onChange 会导致(类input) change后触发两次(组件定义一次,原生change一次) 对组件定义进行过滤,仅留原生触发,组件触发onChange 用change 替代
  302. onChange,
  303. itemStyle: form_itemStyle = '',
  304. size: _size,
  305. placeholder,
  306. t_placeholder,
  307. le_slots,
  308. ...formOthers
  309. } = form
  310. const _options = options || []
  311. const _itemStyle = unref(itemStyle) + form_itemStyle + (itemWidth ? `;width: ${itemWidth}` : '')
  312. const _placeholder = t_placeholder ? t(t_placeholder) : placeholder
  313. let disabled = form.disabled
  314. if (disabled === undefined) {
  315. disabled = isEdit === false
  316. }
  317. // 优化后的 change事件
  318. const formatterChange = async () => {
  319. // console.log(params[prop], `value, params.${prop}, options`, _options)
  320. if (change) {
  321. return change(params[prop], _options, params)
  322. }
  323. }
  324. switch (itemType) {
  325. /* 自定义 le 自定义Select */
  326. case 'leSelect':
  327. // leSelect: 基于 element-plus el-select-v2扩展
  328. const slots_leSelect = {
  329. default: le_slots.option as SlotOption<OptionItemProps>
  330. }
  331. let leStyle = _itemStyle + (/width\:/g.test(_itemStyle) ? '' : ';width: 200px')
  332. return (
  333. <LeSelect
  334. {...formOthers}
  335. options={_options}
  336. v-model={params[prop]}
  337. isPopover={formOthers.isPopover ?? true}
  338. // 通过teleport插入到body (popper-append-to-body popperAppendToBody已弃用)
  339. teleported={formOthers.teleported ?? true}
  340. onChange={formatterChange}
  341. size={_size ?? size}
  342. placeholder={_placeholder}
  343. style={leStyle}
  344. v-slots={slots_leSelect}
  345. />
  346. )
  347. /* 自定义 render */
  348. case 'render':
  349. return <CustomRender form={form} params={params} />
  350. /* 下拉框 */
  351. case 'select':
  352. return (
  353. <el-select
  354. {...formOthers}
  355. v-model={params[prop]}
  356. onChange={formatterChange}
  357. style={_itemStyle}
  358. disabled={disabled}
  359. size={_size ?? size}
  360. placeholder={_placeholder}
  361. >
  362. {getOptions(_options, form).map((option, i) => {
  363. const { label, value, disabled } = option
  364. return (
  365. <el-option key={`${value}_${i}`} value={value} label={label} disabled={disabled} title={label}>
  366. {/*renderOption(ctx.slots, form.slots?.option, option)*/}
  367. {renderOption(le_slots.option, option)}
  368. </el-option>
  369. )
  370. })}
  371. </el-select>
  372. )
  373. /* 单选框 */
  374. case 'radio':
  375. return (
  376. <el-radio-group
  377. {...formOthers}
  378. v-model={params[prop]}
  379. disabled={disabled}
  380. size={_size ?? size}
  381. onChange={formatterChange}
  382. style={_itemStyle}
  383. >
  384. {getOptions(_options, form).map((option, i) => {
  385. const { label, value, disabled } = option
  386. return (
  387. <el-radio key={`${value}_${i}`} label={value} disabled={disabled} title={label}>
  388. {renderOption(le_slots.option, option)}
  389. </el-radio>
  390. )
  391. })}
  392. </el-radio-group>
  393. )
  394. /* 级联 */
  395. case 'cascader':
  396. const slots_cascader = {
  397. default: le_slots.option as SlotOption<{ data: any; node: any }>
  398. }
  399. return (
  400. <el-cascader
  401. {...formOthers}
  402. v-model={params[prop]}
  403. onChange={formatterChange}
  404. style={_itemStyle}
  405. disabled={disabled}
  406. size={_size ?? size}
  407. clearable={form.clearable ?? true}
  408. filterable={form.filterable ?? true}
  409. options={_options}
  410. placeholder={_placeholder}
  411. v-slots={slots_cascader}
  412. />
  413. )
  414. /* 数字 */
  415. case 'inputNumber':
  416. return (
  417. <InputNumber
  418. class="rate100"
  419. {...formOthers}
  420. slots={le_slots}
  421. v-model={params[prop]}
  422. onChange={formatterChange}
  423. style={_itemStyle}
  424. disabled={disabled}
  425. placeholder={_placeholder}
  426. size={_size ?? size}
  427. precision={form.precision || 0}
  428. v-slots={le_slots}
  429. />
  430. )
  431. /* 数字区间 */
  432. case 'inputNumberRange':
  433. const numberChange = (e, propKey) => {
  434. change && change(params[prop], _options, params, propKey)
  435. }
  436. return (
  437. <InputNumberRange
  438. prop={prop}
  439. {...formOthers}
  440. v-model={params[prop]}
  441. isValueArray
  442. onChange={numberChange}
  443. itemStyle={_itemStyle}
  444. disabled={disabled}
  445. placeholder={_placeholder}
  446. size={_size ?? size}
  447. precision={form.precision || 0}
  448. v-slots={formOthers?.slots}
  449. />
  450. )
  451. /* 日期选择(单日期 || 日期区间) */
  452. case 'datePicker':
  453. let dateOpts: any = {}
  454. dateOpts.valueFormat = form.valueFormat || 'MM/DD/YYYY'
  455. dateOpts.format = form.format || dateOpts.valueFormat
  456. // 区间类型
  457. if (/range$/.test(form.type || '')) {
  458. dateOpts = Object.assign(dateOpts, {
  459. startPlaceholder: t(form.startPlaceholder || 'le.filter.startDate'),
  460. endPlaceholder: t(form.endPlaceholder || 'le.filter.endDate'),
  461. unlinkPanels: form.unlinkPanels ?? true // 解除联动
  462. })
  463. } else {
  464. dateOpts.placeholder = _placeholder || t('le.filter.selectDate')
  465. }
  466. return (
  467. <el-date-picker
  468. {...formOthers}
  469. {...dateOpts}
  470. v-model={params[prop]}
  471. onChange={formatterChange}
  472. style={_itemStyle}
  473. disabled={disabled}
  474. size={_size ?? size}
  475. />
  476. )
  477. /* switch */
  478. case 'switch':
  479. return (
  480. <el-switch
  481. {...formOthers}
  482. v-model={params[prop]}
  483. size={_size ?? size}
  484. onChange={formatterChange}
  485. style={_itemStyle}
  486. disabled={disabled}
  487. />
  488. )
  489. case 'input':
  490. default:
  491. return (
  492. <el-input
  493. {...formOthers}
  494. v-model={params[prop]}
  495. size={_size ?? size}
  496. onChange={formatterChange}
  497. disabled={disabled}
  498. placeholder={_placeholder}
  499. style={_itemStyle}
  500. />
  501. )
  502. }
  503. }
  504. const createFooter = () => {
  505. return (
  506. <div class="footer">
  507. <div class="right-actions">
  508. {showCancelBtn && (
  509. <el-button class="cancel-button" size={size} onClick={cancelHandler}>
  510. {t(cancelBtnText)}
  511. </el-button>
  512. )}
  513. {showResetBtn && (
  514. <el-button class="reset-button" plain size={size} onClick={resetHandler.bind(null, undefined)}>
  515. {t(resetBtnText)}
  516. </el-button>
  517. )}
  518. <el-button class="submit-button" type="primary" size={size} loading={submitLoading} onClick={submitHandler}>
  519. {t(submitBtnText)}
  520. </el-button>
  521. </div>
  522. </div>
  523. )
  524. }
  525. return (
  526. <el-form ref={formRef} class={`le-form-config le-form-config--${size}`} {...form_config} size={size} model={params}>
  527. <el-row class="form_wrap" gutter={gutter}>
  528. {/*renderForms({forms: realForms.value, gutter, span})*/}
  529. {realForms.value.map((form, idx) => {
  530. const { span: _span, t_label, label, ...others } = form
  531. const _label = t_label ? t(t_label) : label
  532. const formItemSlots = {
  533. label: form.le_slots.label
  534. }
  535. return (
  536. <el-col v-show={form.visible !== false} key={idx} span={_span ?? span}>
  537. <el-form-item class={form.showLabel === false ? 'hideLabel' : ''} {...others} label={_label} v-slots={formItemSlots}>
  538. {itemRender(form)}
  539. </el-form-item>
  540. </el-col>
  541. )
  542. })}
  543. {/** 额外的插入内容 */}
  544. {ctx.slots.extraContent?.()}
  545. </el-row>
  546. {showFooter && createFooter()}
  547. </el-form>
  548. )
  549. }
  550. }
  551. })
  552. export default FormConfig
  553. /**
  554. * eg: 示例 实例参考 views/form/default.vue
  555. */
  556. </script>