Procházet zdrojové kódy

feat(custom): dynamicForm

linyuanjie před 5 dny
rodič
revize
cc3047fcb4

+ 21 - 0
src/api/services/common/data.d.ts

@@ -0,0 +1,21 @@
+declare namespace API {
+  type DictItem = {
+    createdBy: string
+    createdTime: string
+    description: string
+    dictId: number
+    dictType: string
+    itemId: number
+    label: string
+    modifiedBy: string
+    modifiedTime: string
+    optFlag: string
+    remarks: string
+    sortOrder: number
+    itemValue: string
+    [key: string]: any
+  }
+
+  /** 字典结果 */
+  type DictResult = DictItem[]
+}

+ 49 - 0
src/api/services/common/index.ts

@@ -0,0 +1,49 @@
+import Request from '../..'
+
+enum Api {
+  Dicts = '/workspace/dict/symbol',
+  DictsNoAuth = '/workspace/dict/symbol-no-auth',
+  PortalConfig = '/workspace/tenant/portal/conf'
+}
+
+// 获取字典
+export function getDictType(data: { type: string }) {
+  return Request.get<API.DictResult>({
+    url: Api.Dicts + `/${data.type}`,
+    params: data
+  })
+}
+
+// 获取字典(不需鉴权)
+export function getDictTypeNoAuth(data: { type: string }) {
+  return Request.get<API.DictResult>({
+    url: Api.DictsNoAuth + `/${data.type}`,
+    params: data
+  })
+}
+
+// 下载文件
+export function exportFile(apiName: string, data: any) {
+  return Request.get({
+    url: apiName,
+    responseType: 'arraybuffer',
+    params: data
+  })
+}
+
+// 获取门户专属配置
+export function getPortalConfig(data: { domain: string }) {
+  return Request.get({
+    url: Api.PortalConfig,
+    params: data
+  })
+}
+
+// 上传文件
+export function uploadFile(uploadUrl: string, headers: { [key: string]: any }, data: any) {
+  return Request.post({
+    url: uploadUrl,
+    data,
+    headers
+  })
+}

+ 201 - 30
src/components/formParse/components/DynamicForm.tsx

@@ -1,5 +1,8 @@
-import { forwardRef, memo, useImperativeHandle } from 'react'
-import { Form, Input, Select } from 'antd'
+import { forwardRef, memo, useEffect, useImperativeHandle } from 'react'
+import { dictMapping, useNoAuthDict } from '@/hooks/useDict'
+import { DatePicker, Form, Input, InputNumber, Select } from 'antd'
+
+const { RangePicker } = DatePicker
 
 export interface DynamicFormRef {
   onSubmit: () => void
@@ -20,53 +23,221 @@ const DynamicForm = memo(
           onSubmit
         }
       })
+
+      const { dictArray: inputUnitTypeOptions, dictRequestFn: getInputUnitTypeOptions } =
+        useNoAuthDict('input_unit_type')
+      useEffect(() => {
+        getInputUnitTypeOptions()
+      }, [])
+      useEffect(() => {
+        console.log(inputUnitTypeOptions)
+      }, [inputUnitTypeOptions])
+
+      const [form] = Form.useForm()
+      const onSubmit = () => {
+        form.submit()
+      }
+      const onFinish = (values: any) => {
+        console.log('Finish:', values)
+        for (const key in values) {
+          if (values[key]) {
+            console.log(values[key].format('YYYY-MM-DD'))
+          }
+        }
+      }
+      const onFinishFailed = (errorInfo: any) => {
+        console.log('Failed:', errorInfo)
+      }
       console.log(formData)
       console.log(formItems)
       console.log(setFormData)
 
+      const FieldWrapper = (props: { children: React.ReactNode }) => {
+        return (
+          <div className='px-[20px] py-[16px] mb-[16px] rounded-[10px] bg-[#ecf5ff]'>
+            {props.children}
+          </div>
+        )
+      }
       const renderField = (field: API.TmplItem) => {
         const options = field.itemStruct
-        const { type, label, placeholder, visible, valid, expand, widgetId } = options!
-        // if (hidden && hidden(form.getFieldsValue())) return null // 条件渲染
-        console.log(visible)
+        const { type, label, desc, placeholder, visible, valid, expand, widgetId } = options!
+        if (!visible) return null // 条件渲染
         console.log(valid)
 
+        const labelContent = (
+          <div>
+            <div>{label.text}</div>
+            {desc && (
+              <div className='text-text-secondary mt-[4px]' style={{ fontSize: '12px' }}>
+                {desc}
+              </div>
+            )}
+          </div>
+        )
         switch (type) {
-          case 'INPUT':
+          case 'INPUT': {
+            const expandVal = expand as API.FD_COM['INPUT']
+
+            return (
+              <FieldWrapper key={widgetId}>
+                <Form.Item name={widgetId} label={labelContent} style={{ marginBottom: 0 }}>
+                  <Input
+                    placeholder={placeholder}
+                    maxLength={parseInt(expandVal.maxChars)}
+                    prefix={expandVal.prefix}
+                    suffix={expandVal.suffix}
+                    showCount
+                    allowClear
+                  />
+                </Form.Item>
+              </FieldWrapper>
+            )
+          }
+          case 'NUMBER_INPUT': {
+            const expandVal = expand as API.FD_COM['NUMBER_INPUT']
+            const min = typeof expandVal.min === 'number' ? expandVal.min : undefined
+            const max = typeof expandVal.max === 'number' ? expandVal.max : undefined
+            const decimalDigits = expandVal.decimalPlacesNumber || 0
+
             return (
-              <Form.Item name={widgetId} label={label.text} key={widgetId}>
-                <Input
-                  placeholder={placeholder}
-                  maxLength={parseInt((expand as API.FD_COM['INPUT']).maxChars)}
-                  showCount
-                  prefix={(expand as API.FD_COM['INPUT']).prefix}
-                  suffix={(expand as API.FD_COM['INPUT']).suffix}
-                />
-              </Form.Item>
+              <FieldWrapper key={widgetId}>
+                <Form.Item name={widgetId} label={labelContent} style={{ marginBottom: 0 }}>
+                  <InputNumber
+                    className='!w-full'
+                    placeholder={`${min !== undefined ? '最小值:' + min.toFixed(decimalDigits) + ';' : ''}${
+                      max !== undefined ? '最大值:' + max.toFixed(decimalDigits) + ';' : ''
+                    }${'小数位数:' + decimalDigits + ';'}`}
+                    maxLength={parseInt(expandVal.maxChars)}
+                    prefix={expandVal.prefix}
+                    suffix={
+                      (expandVal.suffix ||
+                        (expandVal.featureType === 'NUMBER' && expandVal.unit)) &&
+                      (expandVal.featureType === 'NUMBER' && expandVal.unit !== 'NONE'
+                        ? expandVal.unit
+                        : expandVal.suffix)
+                    }
+                    min={min}
+                    max={max}
+                    precision={decimalDigits}
+                  />
+                </Form.Item>
+              </FieldWrapper>
             )
-          case 'SELECT':
+          }
+          case 'AMOUNT_INPUT': {
+            const expandVal = expand as API.FD_COM['NUMBER_INPUT']
+            const min = typeof expandVal.min === 'number' ? expandVal.min : undefined
+            const max = typeof expandVal.max === 'number' ? expandVal.max : undefined
+            const decimalDigits = expandVal.decimalPlacesNumber || 0
+
+            return (
+              <FieldWrapper key={widgetId}>
+                <Form.Item name={widgetId} label={labelContent} style={{ marginBottom: 0 }}>
+                  <InputNumber
+                    className='!w-full'
+                    placeholder={`${min !== undefined ? '最小值:' + min.toFixed(decimalDigits) + ';' : ''}${
+                      max !== undefined ? '最大值:' + max.toFixed(decimalDigits) + ';' : ''
+                    }${'小数位数:' + decimalDigits + ';'}`}
+                    maxLength={parseInt(expandVal.maxChars)}
+                    prefix={expandVal.prefix}
+                    suffix={
+                      (expandVal.suffix || expandVal.unit) &&
+                      (expandVal.unit
+                        ? dictMapping({ unit: expandVal.unit }, 'unit', inputUnitTypeOptions)
+                        : expandVal.suffix)
+                    }
+                    min={min}
+                    max={max}
+                    precision={decimalDigits}
+                  />
+                </Form.Item>
+              </FieldWrapper>
+            )
+          }
+          case 'SELECT': {
+            const expandVal = expand as API.FD_COM['SELECT']
+
             return (
-              <Form.Item name={widgetId} label={label.text} key={widgetId}>
-                <Select placeholder={placeholder} />
-              </Form.Item>
+              <FieldWrapper key={widgetId}>
+                <Form.Item name={widgetId} label={labelContent} style={{ marginBottom: 0 }}>
+                  <Select
+                    mode={expandVal.multiple ? 'multiple' : undefined}
+                    placeholder={placeholder}
+                    options={expandVal.userInput}
+                    allowClear
+                  />
+                </Form.Item>
+              </FieldWrapper>
             )
+          }
+
+          case 'DATETIME_PICKER': {
+            const expandVal = expand as API.FD_COM['DATETIME_PICKER']
+
+            const datePickerType = expandVal.dateType.toLowerCase()
+            const datePickerTypeMap = {
+              year: { picker: 'year', showTime: false, dateFormat: 'YYYY' },
+              month: { picker: 'month', showTime: false, dateFormat: 'YYYY-MM' },
+              date: { picker: undefined, showTime: false, dateFormat: 'YYYY-MM-DD' },
+              week: { picker: 'week', showTime: false, dateFormat: 'YYYY-WW' },
+              datetime: { picker: undefined, showTime: true, dateFormat: 'YYYY-MM-DD HH:mm:ss' },
+              daterange: { picker: 'range', showTime: false, dateFormat: 'YYYY-MM-DD' },
+              datetimerange: { picker: 'range', showTime: true, dateFormat: 'YYYY-MM-DD HH:mm:ss' }
+            }
+            if (!datePickerType.includes('range')) {
+              return (
+                <FieldWrapper key={widgetId}>
+                  {datePickerType}
+                  {datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap].dateFormat}
+                  <Form.Item name={widgetId} label={labelContent} style={{ marginBottom: 0 }}>
+                    <DatePicker
+                      className='w-full'
+                      picker={
+                        datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap]
+                          .picker as any
+                      }
+                      showTime={
+                        datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap].showTime
+                      }
+                      format={
+                        datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap]
+                          .dateFormat
+                      }
+                    />
+                  </Form.Item>
+                </FieldWrapper>
+              )
+            } else {
+              return (
+                <FieldWrapper key={widgetId}>
+                  {datePickerType}
+                  <Form.Item name={widgetId} label={labelContent} style={{ marginBottom: 0 }}>
+                    <RangePicker
+                      className='w-full'
+                      picker={
+                        datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap]
+                          .picker as any
+                      }
+                      showTime={
+                        datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap].showTime
+                      }
+                      format={
+                        datePickerTypeMap[datePickerType as keyof typeof datePickerTypeMap]
+                          .dateFormat
+                      }
+                    />
+                  </Form.Item>
+                </FieldWrapper>
+              )
+            }
+          }
           // 其他组件类型...
           default:
             return null
         }
       }
 
-      const [form] = Form.useForm()
-      const onSubmit = () => {
-        form.submit()
-      }
-      const onFinish = (values: any) => {
-        console.log('Finish:', values)
-      }
-      const onFinishFailed = (errorInfo: any) => {
-        console.log('Failed:', errorInfo)
-      }
-
       return (
         <Form
           form={form}

+ 11 - 4
src/components/formParse/index.tsx

@@ -4,7 +4,9 @@ import { getTenantApply, ITenantApplyInfo } from '@/api/services/riskcontrol/dec
 import { orderStateMap } from './constants/orderState'
 import DynamicForm, { DynamicFormRef } from './components/DynamicForm'
 
-export interface FormParseRef {}
+export interface FormParseRef {
+  dynamicFormSubmit: () => void
+}
 
 interface FormParseProps {
   declFormData: Nullable<API.OpenDeclFormResult>
@@ -46,9 +48,16 @@ const FormParse = memo(
       console.log(disabled)
       // 对外暴露方法
       useImperativeHandle(ref, () => {
-        return {}
+        return {
+          dynamicFormSubmit
+        }
       })
 
+      const DynamicFormInstanceRef = useRef<DynamicFormRef>(null)
+      const dynamicFormSubmit = () => {
+        DynamicFormInstanceRef.current?.onSubmit()
+      }
+
       const formItemsCopy = cloneDeep(declFormData?.formItems)
       console.log(formItemsCopy)
 
@@ -331,8 +340,6 @@ const FormParse = memo(
         initFn()
       }, [])
 
-      const DynamicFormInstanceRef = useRef<DynamicFormRef>(null)
-
       return (
         <div>
           {state && (

+ 66 - 0
src/hooks/useDict.ts

@@ -0,0 +1,66 @@
+import { useState } from 'react'
+import { getDictType, getDictTypeNoAuth } from '@/api/services/common'
+
+/**
+ * 使用方式:
+ *   1. import useDict from '@/hooks/useDict'
+ *   2. const { dictArray: contractTypeDict, dictRequestFn: getContractTypeDict } = useDict('contract_type')
+ * @param dictType 字典接口入参type
+ * @return {{dictRequestFn: dictRequestFn, dictArray: ToRef<*[]>}} 返回: 1.字典数组 2.获取字典的请求函数
+ */
+export default function useDict(dictType: string) {
+  // 定义字典数组
+  const [dictArray, setDictArray] = useState<API.DictItem[]>([])
+  // 获取字典
+  const dictRequestFn = async () => {
+    const res = await getDictType({ type: dictType })
+    if (res?.code === '2000') {
+      setDictArray(res.data)
+    }
+  }
+  return {
+    dictArray,
+    dictRequestFn
+  }
+}
+
+// 不需要鉴权的字典, 使用方式 import { useNoAuthDict } from '@/hooks/useDict' 同上
+export function useNoAuthDict(dictType: string) {
+  // 定义字典数组
+  const [dictArray, setDictArray] = useState<any[]>([])
+  // 获取字典
+  const dictRequestFn = async () => {
+    const res = await getDictTypeNoAuth({ type: dictType })
+    if (res?.code === '2000') {
+      setDictArray(res.data)
+    }
+  }
+  return {
+    dictArray,
+    dictRequestFn
+  }
+}
+
+/**
+ * 映射字典
+ * @param dict 字典内容
+ * @param row 映射的对象
+ * @param attr 映射的对象属性
+ * @return 映射后的值
+ */
+export function dictMapping<T extends Record<string, any>, K extends keyof T>(
+  row: T,
+  attr: K,
+  dict: any[],
+  label: string = 'label',
+  value: string = 'itemValue'
+) {
+  let val
+  for (let i = 0; i < dict.length; i++) {
+    if (dict[i][value] === row[attr]) {
+      val = dict[i][label]
+      break
+    }
+  }
+  return val
+}

+ 2 - 1
src/pages/decl/index.tsx

@@ -79,11 +79,12 @@ function Decl() {
   const submitFn = async (type: ISubmitType, isAfterPayment = false) => {
     console.log(type)
     console.log(isAfterPayment)
+    FormParseRef.current?.dynamicFormSubmit()
   }
 
   return (
     <Layout>
-      <div className='decl-page relative !h-screen bg-gradient-to-b from-[#F2F7FE] to-[#FAFBFC]'>
+      <div className='decl-page relative !min-h-screen bg-gradient-to-b from-[#F2F7FE] to-[#FAFBFC]'>
         {pageState === 'loading' && (
           <div className='loading-mask absolute-content-center text-primary text-[20px]'>
             <LoadingOutlined />