import { useState, useMemo, useEffect } from 'react' import { Form, Button, Toast, Result } from 'antd-mobile' import { factory, valueFactory } from './fields' import styled from 'styled-components' import { showError, submitDataTransform, reverseFillDataTransform, formatDisplayConfig, getExpression, COMPONENT_MAP_NAME, isAlipayIDE, isNull } from '@/utils' import { FormControl } from './form' import { flowApi, useFlowSubmitMutation } from '@/services/flow' import { useAppDispatch } from '@/store/storeHooks' import { CHILD_FIELD_VALUE } from './constant' import type { FormDataType, FormValuesType, BasicSelectValueType, BasicRadioValueType, FieldDataType, FieldValueType, TriggerEventParamsType, IBuyerNickInputProps, ValidRepeatEventParamsType, ErrorType, ShopItemType } from '@/types' type FormPropsType = { shopId: string buyerOpenUid: string buyerNick: string companyKey: string platform?: 'tb' | 'fxg' | 'ks' | 'pdd' | 'jd' | 'xiaozhi' | 'pc' | 'tbmp' formData: FormDataType shopUniqueKey: string workOrderId?: string uniqueKey: string } export default function From(props: FormPropsType) { const { formData, uniqueKey, shopId, shopUniqueKey, buyerOpenUid, buyerNick, companyKey } = props const [isSubmitted, setIsSubmitted] = useState(false) const [form] = Form.useForm<{ [key: string]: FieldValueType }>() const initialValues: { [key: string]: FieldValueType | undefined } = {} const [replaceValue, setReplaceValue] = useState<{ [key: string]: number }>( {} ) //字段重复校验 const [shopList, setShopList] = useState([]) let isSubmitting = false const storeDispatch = useAppDispatch() const mappingConfig = useMemo(() => { const readonlyComponents: { [key: string]: { auth: 'single' | 'all' } } = {} const hideComponents: string[] = [] const config: { [triggerComponentKey: string]: { [triggerValue: string]: { [targetComponentKey: string]: | string | { value: string; other: string } } } } = {} ;( formData.currentNodeDetail.workOrderTemplateInfo.componentDtoList || [] ).forEach(componentDto => { const mapping = componentDto.componentConfig.mapping if (mapping?.rules?.length && mapping.triggerComponentKey) { readonlyComponents[componentDto.uniqueKey] = { auth: mapping.auth } if (mapping.auth === 'single') { hideComponents.push(componentDto.uniqueKey) } // 只需要第一个节点,没有前置节点 // if (componentDto._isPreItem) { // return // } const rules: { [triggerValue: string]: { [targetComponentKey: string]: | string | { value: string; other: string } } } = {} mapping.rules.forEach( (rule: { triggerValue: string; currentValue: string }) => { if (!rules[rule.triggerValue]) { rules[rule.triggerValue] = {} } rules[rule.triggerValue][componentDto.uniqueKey] = componentDto.workOrderComponentType === 'RADIO' ? { value: rule.currentValue, other: '' } : rule.currentValue } ) config[mapping.triggerComponentKey] = rules } }) return { config, readonlyComponents, hideComponents } }, [formData.currentNodeDetail.workOrderTemplateInfo.componentDtoList]) const shopIdByUniqueKey: string = useMemo(() => { return ( shopId || shopList.find(t => t.uniqueKey === shopUniqueKey)?.shopId || '' ) }, [shopUniqueKey, shopList, shopId]) const getDisplayAndDisabledConfig = (config: number | undefined) => { if (isNull(config)) { return { display: true, disabled: false } } const display = Math.floor(Number(config) / 2) === 1 // 取整 const disabled = Number(config) % 2 === 0 return { display, disabled } } const componentDtoList = ( formData.currentNodeDetail.workOrderTemplateInfo.componentDtoList || [] ).map(fieldData => { const hasMapping = ['RADIO', 'SELECT'].includes(fieldData.workOrderComponentType) && !!fieldData.componentConfig.mapping let newData = fieldData // 组件配置有选组件状态分为3可见,可以录入;2可见,不能录入;0不可见,不能录入;status为空对象时则默认可见可以录入 const hasControlFieldStatus = ['INPUT'].includes(fieldData.workOrderComponentType) && fieldData.componentConfig?.status?.created !== undefined if (hasMapping) { newData = JSON.parse(JSON.stringify(fieldData)) as FieldDataType if (mappingConfig.readonlyComponents[newData.uniqueKey]?.auth === 'all') { newData.componentConfig[ newData.workOrderComponentType === 'SELECT' ? 'readonly' : 'disabled' ] = true } else if ( mappingConfig.readonlyComponents[newData.uniqueKey]?.auth === 'single' ) { newData.componentConfig.disabled = true // 隐藏未选中组件 newData.componentConfig.hideUnselectedItem = true } } if (hasControlFieldStatus) { newData = JSON.parse(JSON.stringify(fieldData)) as FieldDataType const { disabled: componentCanEdit, display: componentCanShow } = getDisplayAndDisabledConfig(newData?.componentConfig?.status?.created) newData.componentConfig['disabled'] = componentCanEdit // 不显示组件 但是点击提交时需要带上值 newData.componentConfig['hidden'] = !componentCanShow } return newData }) if (buyerOpenUid || buyerNick) { // 如果有传入的昵称或者id,自动填入组件或者插入一个买家昵称组件 const buyerFieldIndex = componentDtoList.findIndex( fieldData => fieldData.workOrderComponentType === 'BUYER_NICK_INPUT' ) if (buyerFieldIndex < 0) { // 如果没有买家昵称 // 补充一个隐藏的买家昵称组件 componentDtoList.unshift({ componentConfig: { hidden: true, readonly: true, submitDisabled: true, required: false, replaceWarn: false }, name: '买家昵称', uniqueKey: 'auto_add_buyer_nick_id', workOrderComponentGroup: 'E_COMPONENT', workOrderComponentType: 'BUYER_NICK_INPUT' }) } else { // 如果已存在,则直接将该组件改成只读 const newBuyer = { ...componentDtoList[buyerFieldIndex], componentConfig: { ...componentDtoList[buyerFieldIndex].componentConfig, readonly: true } } componentDtoList.splice( buyerFieldIndex, 1, newBuyer as IBuyerNickInputProps ) } } const showHeader = !!Object.keys(replaceValue).length componentDtoList.forEach(fieldData => { initialValues[fieldData.uniqueKey] = valueFactory(fieldData, { buyerOpenUid, buyerNick: decodeURIComponent(buyerNick || ''), shopId: shopIdByUniqueKey }) }) const fields = new Map>() const formControl = new FormControl(form, { formData }) const diaplayRules = formData.currentNodeDetail.workOrderTemplateInfo.flowTemplateConfig ?.showHideRule?.rules || [] const idConditionListJSON = formatDisplayConfig(diaplayRules) const idDependenciesFieldsJSON: { [key: string]: Set } = {} for (const fieldId in idConditionListJSON) { const currentIdDependenceOtherFields = idConditionListJSON[fieldId] || [] // 当前组件显隐依赖组件id列表 const dependenciesFields = currentIdDependenceOtherFields.reduce( (cur: Set, nxt) => { nxt.rules.map(i => cur.add(i.leftValue)) return cur }, new Set() ) idDependenciesFieldsJSON[fieldId] = dependenciesFields } const getFieldDisplay = ( fieldId: string, values?: { [fieldId: string]: FieldValueType | undefined } ) => { const dependenciesFields = idDependenciesFieldsJSON[fieldId] const currentIdDependenceOtherFields = idConditionListJSON[fieldId] // 1. 显影隐藏 const show = !dependenciesFields || dependenciesFields.size === 0 || currentIdDependenceOtherFields.reduce((cur: boolean, nxt) => { // 如果有一项满足展示条件则展示 if (cur) return cur const conditionList = nxt.rules const linkCondition = nxt.linkCondition // 满足所有 if (['every', 'some'].includes(linkCondition)) { const res = conditionList[ linkCondition === 'every' ? 'every' : 'some' ](item => { //现支持子组件控制显隐,格式为${uniqueKey}_${type}与普通组件做区分 const isChildField = item.leftValue.includes('_') // prefixId 获取的是组件uniqueKey const prefixId = isChildField ? item.leftValue.split('_')?.[0] : item.leftValue let leftValue = values?.[prefixId] ? values[prefixId] : form.getFieldValue(prefixId) // 复合组件 需要将对应子组件的值取出来, CHILD_FIELD_VALUE是子组件type与value中字段的映射 if (isChildField) { const childFieldType = item.leftValue.split('_')?.[1] leftValue = childFieldType && CHILD_FIELD_VALUE?.[childFieldType] ? (leftValue as Record)?.[ CHILD_FIELD_VALUE[childFieldType] ] : '' } const dependenceField = fields.get(item.leftValue) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment if ( dependenceField && 'formDataTransform' in dependenceField && typeof dependenceField.formDataTransform === 'function' ) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument leftValue = dependenceField.formDataTransform(leftValue) } // 店铺名称显隐 if (item.leftValue === 'userId') { leftValue = shopUniqueKey } return getExpression( item.compareSymbol?.toUpperCase(), leftValue, item.rightValue ) }) return res } return false }, false) // 2. 映射隐藏 const field = componentDtoList.find( componentDto => componentDto.uniqueKey === fieldId ) let mappingShow = true if (field?.componentConfig.hideUnselectedItem) { // 如果隐藏未选中选项 const fieldValue = ( values ? values?.[fieldId] : form.getFieldValue(fieldId) ) as BasicRadioValueType | BasicSelectValueType if (typeof fieldValue === 'string' ? !fieldValue : !fieldValue?.value) { mappingShow = false } } return show && mappingShow } const displayJson: { [fieldId: string]: boolean } = {} componentDtoList.forEach(props => { displayJson[props.uniqueKey] = getFieldDisplay( props.uniqueKey, initialValues ) }) const fieldsDisplay = { ...displayJson } const fieldsDisplayUpdate: { [key: string]: boolean } = {} const [flowSubmit] = useFlowSubmitMutation() const onSubmit = async () => { if (isSubmitting) return isSubmitting = true const values = await form .validateFields() .catch((err: { errorFields: { name: string; errors: string[] }[] }) => { console.warn('[validate error]', err) // 表单校验失败错误时,将错误第一项滚动到可视区域 if (err?.errorFields?.length) { setTimeout(() => { const errorField = document.querySelector( '.adm-form-item-has-error' ) errorField?.scrollIntoView({ block: 'center', behavior: 'smooth' }) }, 0) } }) if (!values) { isSubmitting = false return } const submitFieldData = submitDataTransform(componentDtoList, values) const source = { tbmp: '淘宝小程序', tb: '淘宝', fxg: '抖音', ks: '快手', pdd: '拼多多', jd: '京东', xiaozhi: '小智', pc: '客服工作台' }[props.platform || 'tbmp'] const submitData = { isAdd: true, operateParams: { nodeId: formData.currentNodeDetail.nodeInfo?.id || '', flowTemplateId: formData.flowTemplateId, flowTemplateKey: uniqueKey, flowVersion: formData.flowVersion, flowWorkOrderId: '' }, workOrder: { needTransForm: false, shopUniqueKey: shopUniqueKey || (values.shopUniqueKey as string), companyKey, source, templateId: formData.currentNodeDetail.workOrderTemplateInfo.uniqueKey, ...submitFieldData } } const res = await flowSubmit(submitData) if ('error' in res) { showError(res.error) } else { if (window?.my && !isAlipayIDE && !/debug/.test(location.href)) { my.redirectTo({ url: '/pages/index-page/index-page' }) } else { setIsSubmitted(true) } } isSubmitting = false } const formItemRepeatCheck = async (params: ValidRepeatEventParamsType) => { const templateId = formData.currentNodeDetail.nodeInfo?.relationEvent.workOrderTemplateId let isRepeat = false let error: ErrorType = '' if (!templateId) { Toast.show('templateId缺失') } else { const res = await storeDispatch( flowApi.endpoints.inputRepeatCheck.initiate( { templateId, ...params }, { forceRefetch: true } ) ) if ('error' in res && res.error) { showError(res.error) error = res.error } if (res.data && res.data.resultList?.[0]?.repeatCount > 0) { isRepeat = true } } return Promise.resolve({ isRepeat, error }) } const componentEvent = (params: TriggerEventParamsType) => { const { type, fieldData, value } = params if (type === 'REPEAT_CHECK') { const componentName = params.componentName // if (!value.order) return // 没有物流单号,不查询重复 const templateId = formData.currentNodeDetail.nodeInfo?.relationEvent.workOrderTemplateId if (!templateId) { Toast.show('templateId缺失') return } else if (!value) { // 值不存在的情况下 if (!replaceValue[componentName]) { // 本身就没有提醒 return } // 如果有提醒则去除提醒 setReplaceValue({ ...replaceValue, [componentName]: 0 }) } else { // 值存在,远程获取对应的数据 storeDispatch( flowApi.endpoints.repeatCheck.initiate({ needCheckComponentList: [ { componentKey: `${fieldData.uniqueKey}_${componentName}`, componentValue: value } ], templateId }) ) .then(res => { if ('error' in res && res.error) { showError(res.error) } else if (res.data) { // 设置重复 if ( res.data.resultList.some(item => { return ( (replaceValue[componentName] && item.repeatCount && replaceValue[componentName] != item.repeatCount) || // 都存在不相等,需要更新 (replaceValue[componentName] && !item.repeatCount) || // 取消重复提醒 (!replaceValue[componentName] && item.repeatCount) // 新增重复提示 ) }) ) { const newReplaceValue = res.data.resultList.reduce( (prv, next) => { prv[componentName] = next.repeatCount return prv }, {} as { [key: string]: number } ) setReplaceValue({ ...replaceValue, ...newReplaceValue }) } } }) .catch(showError) } } else if (type === 'ORDER_BACK_REPLACE') { storeDispatch( flowApi.endpoints.orderDetail.initiate({ companyKey, orderNo: value, shopId: shopIdByUniqueKey, templateId: formData.currentNodeDetail.workOrderTemplateInfo.uniqueKey }) ) .then(res => { if ('error' in res) { console.warn(res.error) // showError(res.error) } else if (res.data) { const reverseValues = reverseFillDataTransform(formData, res.data) form.setFieldsValue(reverseValues) } }) .catch(showError) } } const onValuesChange = ( changedValues: FormValuesType // allValues: FormValuesType ) => { for (const key in changedValues) { const changedField = componentDtoList.find( field => field.uniqueKey === key ) if ( changedField && ['RADIO', 'SELECT'].includes(changedField.workOrderComponentType) ) { fillByMapping( key, changedValues[key] as | BasicSelectValueType | BasicRadioValueType ) } } } const checkFillMapping = ( show: boolean, fieldData: FieldDataType, fieldValue: FieldValueType ) => { if (['RADIO', 'SELECT'].includes(fieldData.workOrderComponentType)) { setTimeout(() => { fillByMapping( fieldData.uniqueKey, show ? (fieldValue as BasicSelectValueType | BasicRadioValueType) : '' ) }, 0) } } // 组件映射 const fillByMapping = ( triggerUniqueKey: string, value: BasicSelectValueType | BasicRadioValueType ) => { const triggerValue = typeof value === 'object' ? value?.value : value // 如果有映射关系,则修改对应组件的值 const mapping = mappingConfig.config?.[triggerUniqueKey] || {} let changeValues: { [uniqueKey: string]: FieldValueType } | null = null if (mapping?.[triggerValue]) { changeValues = mapping?.[triggerValue] } else { const resetMap: { [uniqueKey: string]: FieldValueType } = {} for (const key in mapping) { for (const uniqueKey in mapping[key]) { const value = initialValues[uniqueKey] // PS: 目前只有单选和下拉单选支持 resetMap[uniqueKey] = typeof value === 'string' ? '' : { value: '', other: '' } } } changeValues = resetMap } changeValues && form.setFieldsValue(changeValues) } const FromItemWrap = ({ isComposite = false, required, rules, title, name, component, hidden }: { isComposite: boolean required: boolean rules: any[] title: string | JSX.Element name: string component: JSX.Element | null hidden?: boolean }) => { const className = [ required && 'is-required', isComposite && 'is-composite', hidden ? 'is-hidden' : 'no-hidden' ] .filter(n => n) .join(' ') return ( {component} ) } const createField = (fieldData: FieldDataType, form: FormControl) => { const instance = factory(fieldData, form, formItemRepeatCheck) fields.set(instance.uniqueKey, instance) // console.log('in create', fieldData.name) return ( { const show = getFieldDisplay(instance.uniqueKey) fieldsDisplayUpdate[instance.uniqueKey] = fieldsDisplayUpdate[instance.uniqueKey] || fieldsDisplay[instance.uniqueKey] !== show if (fieldsDisplay[instance.uniqueKey] !== show) { fieldsDisplay[instance.uniqueKey] = show } return fieldsDisplayUpdate[instance.uniqueKey] }} > {({ getFieldValue }) => { fieldsDisplayUpdate[instance.uniqueKey] = false checkFillMapping( fieldsDisplay[instance.uniqueKey], fieldData, getFieldValue(instance.uniqueKey) as FieldValueType ) return ( fieldsDisplay[instance.uniqueKey] && ( ) } const finalFormItems = componentDtoList.map(props => createField(props, formControl) ) const getShopList = () => { storeDispatch( flowApi.endpoints.shopList.initiate({ apiName: 'shopList' }) ) .then(res => { const { data = [] } = res setShopList(data) }) .catch(showError) } const handleReSubmit = () => { form.resetFields() setIsSubmitted(false) } const SuccessPage = () => ( ) useEffect(() => { getShopList() }, []) useEffect(() => { shopUniqueKey && fillByMapping('SHOP_SELECT_MAPPING', shopUniqueKey) return }, [shopUniqueKey]) // useEffect(() => { // console.log('formRef', formRef) // }, [formRef]) return !isSubmitted ? (
提交 } > {showHeader && (
{Object.keys(replaceValue).map( key => replaceValue[key] > 0 && (
提示:该{COMPONENT_MAP_NAME[key]}针对此模板已创建过 {replaceValue[key]}条工单!
) )}
)} {[...finalFormItems]}
) : ( ) } const SuccessPageWrap = styled.div` width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; .adm-result { background: transparent; } `