Commit b39687e7 authored by 前端-钟卫鹏's avatar 前端-钟卫鹏

fix: NiceForm添加表单字段完成度收集和FormItemCard组件,添加表单顶部锚点组件用于新设计的表单布局

parent be4028e7
.titleAvator {
width:48px;
height:48px;
background:rgba(135,119,217,1);
border-radius:4px;
border:1px solid rgba(223,225,230,1);
line-height: 48px;
text-align: center;
color: #fff;
margin: 0 24px;
}
.titleAvatorText {
margin-left: 8px;
font-size: 16px;
color: #252D37;
font-weight: 500;
}
.titleCompleteProcess {
width: 240px;
margin-left: 8px;
height: 16px;
font-size: 12px;
color: #00A98F;
line-height: 16px;
font-weight: 400;
background: #E4F7EF;
border-radius: 8px;
padding-left: 8px;
}
.detailHeader {
background: #fff;
padding: 16px 18px 0;
:global {
.ant-row {
.ant-col-2 {
.ant-btn {
position: absolute;
top: 0;
right: 0;
}
}
}
.ant-anchor-wrapper {
min-height: auto;
}
.ant-anchor {
display: flex;
}
.ant-anchor-ink{
display: none;
}
.ant-anchor-link {
padding: 0;
text-align: center;
a {
display: inline-block;
font-size: 14px;
color: #909399;
margin: 0 16px;
height: 48px;
line-height: 48px;
}
}
.ant-anchor-link-active {
a{
font-weight: 500;
color: #303133;
border-bottom: 2px solid #00B37A;
}
}
}
}
.detailCol {
display: flex;
margin-top: 20px;
color: #303133;
}
.colLabel {
color: #909399;
margin-right: 16px;
}
// tabs标签锚点
.anchorTitle {
// height: 48px;
ul {
padding-left: 0;
display: flex;
li {
height: 48px;
line-height: 48px;
text-align: center;
list-style: none;
a {
display: inline-block;
height: 48px;
font-size: 14px;
color: #909399;
line-height: 48px;
margin: 0 16px;
}
.current {
font-weight: 500;
color: #303133;
border-bottom: 2px solid #00B37A;
}
}
}
}
.anchorTitleFixed {
position: fixed;
top: 0;
width: 100%;
z-index: 10;
:global {
.ant-row {
.ant-col-2 {
.ant-btn {
position: fixed;
top: 16px;
right: 16px;
}
}
}
}
}
import React, { ReactNode, useContext, useEffect, useRef, useState } from 'react'
import { Row, Col, Skeleton, Anchor } from 'antd'
import { history } from 'umi'
import { ArrowLeftOutlined } from '@ant-design/icons'
import style from './index.less'
import { ISchema } from '@formily/antd'
import { FormDetailContext } from '@/formSchema/context'
const { Link } = Anchor;
export interface FormDetailHeaderProps {
title: string,
/**
* 右侧额外操作
*/
extraRight?: ReactNode,
/**
* 返回操作跳转链接
*/
backLink?: string,
/**
* 表单描述schema
*/
schema: ISchema
}
interface itemProps extends ISchema {
['x-component-props']?: any
}
/**
* NiceForm表单详情的锚点头部
*
*/
const FormDetailHeader: React.FC<FormDetailHeaderProps> = ({
title,
extraRight,
backLink,
schema,
}) => {
const ctx = useContext(FormDetailContext)
const flagRef = useRef({
flag: false,
distanceTop: 0
})
const [current, setCurrent] = useState<number>(0)
const [isFixed, setIsFixed] = useState<boolean>(false)
useEffect(() => {
window.addEventListener("scroll", onScroll)
return (() => {
window.removeEventListener('scroll', onScroll)
})
}, [])
const onScroll = () => {
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
let floors = document.querySelectorAll(".anchorContent>div")
floors.forEach((floor: any, index: any) => {
if (floor.offsetTop - 100 <= scrollTop) {
setCurrent(index)
}
})
// 锚点导航距离顶端距离
let navDom: any = document.getElementById("anchorTitle")
if (navDom) {
let distance = navDom.offsetTop - document.documentElement.scrollTop
if (!flagRef.current.flag) {
flagRef.current.distanceTop = navDom.offsetTop
flagRef.current.flag = true
}
if (distance <= 0) {
setIsFixed(true)
}
if (document.documentElement.scrollTop <= flagRef.current.distanceTop) {
setIsFixed(false)
}
}
}
console.log(ctx)
return (
<div className={isFixed ? [style.detailHeader, style.anchorTitleFixed].join(' ') : style.detailHeader} id="detailHeader">
<Row>
{
<>
<Col span={22}>
<Row align='middle'>
<Col>
<ArrowLeftOutlined size={14} onClick={() => backLink ? history.push(backLink) : history.goBack()} />
</Col>
<Col>
<div className={style.titleAvatorText}>{title}</div>
</Col>
<Col>
<div className={style.titleCompleteProcess}>信息完成度{` ${Number(ctx.formContext.formProcess) * 100}%`}</div>
</Col>
</Row>
<Row>
<Col>
<div className={style.anchorTitle} id="anchorTitle">
<Anchor onClick={(e) => e.preventDefault()} showInkInFixed={false} targetOffset={200}>
{
schema['properties'] && Object.values(schema['properties']).map((item: itemProps, index) => {
const {id, title} = item['x-component-props']
if(id && title) {
return (<Link key={index} href={`#${id}`} title={title} />)
}
})
}
</Anchor>
</div>
</Col>
</Row>
</Col>
<Col span={2}>{extraRight}</Col>
</>
}
</Row>
</div>
)
}
FormDetailHeader.defaultProps = {}
export default FormDetailHeader
import React from 'react'
import style from './index.less'
export interface FormDetailWrapperProps {}
const FormDetailWrapper:React.FC<FormDetailWrapperProps> = (props) => {
return (
<div className={style.wrapper}>{props.children}</div>
)
}
FormDetailWrapper.defaultProps = {}
export default FormDetailWrapper
import React, { useEffect } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import SchemaForm, {
IAntdSchemaFormProps, createVirtualBox, registerVirtualBox, Schema, SchemaField, FormButtonGroup, Reset, createControllerBox, registerValidationRules,
IAntdSchemaFormProps, createVirtualBox, registerVirtualBox, Schema, SchemaField, FormButtonGroup, Reset, createControllerBox, registerValidationRules, ISchemaFormActions, FormEffectHooks, ISchemaFormAsyncActions,
} from '@formily/antd';
import { Button, Space, Row, Col, DatePicker } from 'antd';
import CustomUpload from './components/CustomUpload';
......@@ -45,6 +45,8 @@ import CustomCascader from './components/CustomCascader';
import FixUpload from './components/FixUpload';
import { currentStateType, getCurrentState } from './utils/keepAlive';
import { useRouteMatch } from 'umi';
import FormItemCard from './components/FormItemCard';
import { FormDetailContext } from '@/formSchema/context';
export interface NiceFormProps extends IAntdSchemaFormProps {
loading?: boolean
......@@ -70,6 +72,18 @@ registerValidationRules({
}
});
// 全局注册card布局组件
registerVirtualBox("MellowCard", ({ children, schema }) => {
const props = schema['x-component-props']
return (
<FormItemCard {...props}>
{children}
</FormItemCard>
);
});
// 该组件用于schema中嵌套表单, 不过控制台会出现警告
const schemaLayout = createControllerBox("schemaLayout", (_props) => {
const { schema } = _props;
......@@ -156,6 +170,55 @@ const NiceForm: React.FC<NiceFormProps> = props => {
}
}, [])
/**
* 自定义字段完成度收集逻辑
*
* **/
const { formContext } = useContext(FormDetailContext)
// 总交互字段数
const [amount, setAomunt] = useState<number>(0)
// 输入数
const [inputAmount, setInputAomunt] = useState<number>(0)
// 有效统计字段
const effectFields = []
useEffect(() => {
if(amount > 0) {
formContext && formContext.ctl.setFormProcess(() => (inputAmount/amount).toFixed(2))
}
}, [amount, inputAmount])
const useAttachmentChangeForContext = (ctx: ISchemaFormActions | ISchemaFormAsyncActions) => {
FormEffectHooks.onFormMount$().subscribe(() => {
formContext && formContext.ctl.setFormProcess(0) // 表单初始化至0
const fieldTree = ctx.getFormGraph()
let fieldAmount = 0
for(let item in fieldTree) {
const value = fieldTree[item]
if(value['displayName'] === "FieldState" && value['visible'] && value['display']) {
++fieldAmount
effectFields.push(value['name'])
}
}
setAomunt(fieldAmount)
})
FormEffectHooks.onFormValuesChange$().subscribe(values => {
// @todo 若输入再清除 其实表单值存在 只不过为''或者undefined
// 编辑的时候 初始值可能会有很多 过滤有效字段数
setInputAomunt(() => {
let inputNumber = 0
Object.keys(values.values).forEach(item => {
if(effectFields.includes(item)) {
++inputNumber
}
})
return inputNumber
})
})
}
return (
<div style={{ width: '100%', position: 'relative' }}>
<SchemaForm
......@@ -163,12 +226,16 @@ const NiceForm: React.FC<NiceFormProps> = props => {
components={defineComponents}
style={{ opacity: loading ? 0 : 1, position: loading ? 'absolute' : 'initial' }}
effects={($, ctx) => {
console.log(ctx)
// 自定义联动scope收集器
useLinkComponentProps(expressionScope)
// 组件联动
effects && effects($, ctx)
// 自定义字段完成度收集
useAttachmentChangeForContext(ctx)
}}
expressionScope={expressionScope}
{...reset}
......
import { createContext } from 'react';
// 表单 Context
export const FormDetailContext = createContext<any>({})
import { useEffect, useState } from 'react'
import { usePageStatus } from '@/hooks/usePageStatus'
import { FormEffectHooks, ISchemaFormActions, ISchemaFormAsyncActions } from '@formily/antd'
/**
* 带锚点跳转式schema表单 hook
* @returns
*/
export const useFormDetail = () => {
// 表单数据
const [formData, setFormData] = useState<any>(null)
// 完成度
const [formProcess, setFormProcess] = useState<string>()
// // 总交互字段数
// const [amount, setAomunt] = useState<number>(0)
// // 输入数
// const [inputAmount, setInputAomunt] = useState<number>(0)
// const { id } = usePageStatus()
// useEffect(() => {
// console.log(amount, inputAmount)
// if(amount > 0) {
// setFormProcess(() => (inputAmount/amount).toFixed(2))
// }
// }, [amount, inputAmount])
// /**
// * 获取表单状态树
// * @param ctx 表单action
// */
// const useAttachmentChangeForContext = (ctx: ISchemaFormActions | ISchemaFormAsyncActions) => {
// FormEffectHooks.onFormMount$().subscribe(() => {
// const fieldTree = ctx.getFormGraph()
// let fieldAmount = 0
// for(let item in fieldTree) {
// const value = fieldTree[item]
// if(value['displayName'] === "FieldState" && value['visible'] && value['display']) {
// ++fieldAmount
// }
// }
// setAomunt(fieldAmount)
// })
// FormEffectHooks.onFormValuesChange$().subscribe(values => {
// // @todo 若输入再清除 其实表单值存在 只不过为''或者undefined
// setInputAomunt(() => Object.values(values.values).length)
// })
// }
// 需共享的状态
const formContext = {
data: formData,
formProcess,
ctl: {
setFormData,
setFormProcess
},
// useAttachmentChangeForContext,
}
return {
formContext,
// id,
}
}
......@@ -19,6 +19,10 @@ import MaterialModalTable from './components/materialModalTable'
import DepartmentModalTable from './components/departmentModalTable'
import MemberModalTable from './components/memberModalTable'
import styled from 'styled-components'
import FormDetailHeader from '@/components/FormDetailHeader'
import FormDetailWrapper from '@/components/FormDetailWrapper'
import { FormDetailContext } from '@/formSchema/context'
import { useFormDetail } from '@/formSchema/effects/useFormDetail'
const addSchemaAction = createFormActions()
......@@ -62,6 +66,7 @@ const IncreaseRequisition:React.FC<{}> = () => {
const update = useUpdate()
const { id } = usePageStatus()
const [initFormValue, setInitFormValue] = useState<any>({})
const { formContext } = useFormDetail()
// 请购单物料
const { materialAddButton, materialRef, materialColumns, materialComponents, ...surplusProps } = useMaterialTable(addSchemaAction)
......@@ -137,18 +142,35 @@ const IncreaseRequisition:React.FC<{}> = () => {
const departmentBtn = <div className='connectBtn' onClick={handleDepartment}><LinkOutlined style={{marginRight: 4}}/>选择</div>
return (
<PageHeaderWrapper
const providerValue = {
schemaActions: addSchemaAction,
formContext,
}
return (<div style={{}}>
{/* <PageHeaderWrapper
onBack={() => history.goBack()}
backIcon={<ReutrnEle description="返回"/>}
title={changeRouterTitleByStatus()}
extra={[
<Button key="1" onClick={() => addSchemaAction.submit()} loading={btnLoading} type="primary" icon={<SaveOutlined />}>
保存
保存
</Button>,
]}
>
<Card>
]}
> */}
<FormDetailContext.Provider value={providerValue}>
<FormDetailHeader
title={id ? '编辑请购单' : '新增请购单'}
schema={increaseSchema}
extraRight={[
<Button key="1" onClick={() => addSchemaAction.submit()} loading={formLoading} type="primary" icon={<SaveOutlined />}>
保存
</Button>,
]}
/>
<FormDetailWrapper>
{/* <Card> */}
<NiceForm
loading={formLoading}
previewPlaceholder=' '
......@@ -163,6 +185,9 @@ const IncreaseRequisition:React.FC<{}> = () => {
// 物料信息的改动 渲染总额
useMaterialTableChangeForAmount(ctx, update)
// // 注入表单完成进度
// formContext.useAttachmentChangeForContext(ctx)
}}
expressionScope={{
memberBtn,
......@@ -173,7 +198,10 @@ const IncreaseRequisition:React.FC<{}> = () => {
help,
}}
/>
</Card>
{/* </Card> */}
</FormDetailWrapper>
</FormDetailContext.Provider>
{/* 选择部门 */}
<DepartmentModalTable currentRef={departmentRef} schemaAction={addSchemaAction}/>
......@@ -182,8 +210,8 @@ const IncreaseRequisition:React.FC<{}> = () => {
{/* 选择供应会员 */}
<MemberModalTable currentRef={memberRef} schemaAction={addSchemaAction}/>
</PageHeaderWrapper>
)
{/* </PageHeaderWrapper> */}
</div>)
}
IncreaseRequisition.defaultProps = {}
......
import { ISchema } from '@formily/antd';
import moment from 'moment'
// // 基本信息
// const basicInfo: ISchema = {
// "x-index": 0,
// type: 'object',
// "x-component": 'tabpane',
// "x-component-props": {
// tab: '基本信息',
// className: 'useConnectBtnWrapper'
// },
// properties: {
// NO_SUBMIT_LAYOUT: {
// type: 'object',
// "x-component": 'mega-layout',
// "x-component-props": {
// labelCol: 4,
// labelAlign: 'left',
// wrapperCol: 10
// },
// properties: {
// requisitionNo: {
// type: 'string',
// title: '请购单号',
// "x-component": 'text',
// visible: false
// },
// digest: {
// type: 'string',
// title: '订单摘要',
// "x-rules": [
// {
// required: true,
// message: '请输入订单摘要'
// },
// {
// limitByte: true,
// maxByte: 60
// }
// ]
// },
// deliverTime: {
// type: 'string',
// "x-component": 'date',
// title: '预交日期',
// required: true,
// "x-component-props": {
// // showTime: true,
// format: 'YYYY-MM-DD',
// disabledDate: current => {
// return current && current < moment().startOf('day')
// },
// style: { width: '100%' }
// }
// },
// department: {
// type: 'string',
// title: '请购部门',
// required: true,
// "x-component-props": {
// disabled: true,
// addonAfter: "{{departmentBtn}}"
// },
// },
// departmentId: {
// type: 'string',
// title: '请购部门ID',
// visible: false,
// },
// purpose: {
// type: 'string',
// title: '请购用途',
// "x-rules": [
// {
// required: true,
// message: '请输入请购用途'
// },
// {
// limitByte: true,
// maxByte: 100
// }
// ]
// },
// vendorMemberName: {
// type: 'string',
// title: '供应会员',
// "x-component-props": {
// disabled: true,
// addonAfter: "{{memberBtn}}"
// },
// required: true,
// },
// vendorMemberId: {
// type: 'string',
// display: false
// },
// vendorRoleId: {
// type: 'string',
// display: false
// },
// createTime: {
// type: 'string',
// title: '单据时间',
// visible: false
// },
// interiorStateName: {
// type: 'string',
// title: '内部状态',
// visible: false
// },
// }
// },
// }
// }
// // 请购单物料
// export const material: ISchema = {
// "x-index": 2,
// type: 'object',
// "x-component": 'tabpane',
// "x-component-props": {
// tab: '订单物料'
// },
// properties: {
// products: {
// type: 'array',
// "x-component": 'MultTable',
// "x-component-props": {
// rowKey: 'materialId',
// columns: "{{materialColumns}}",
// components: "{{materialComponents}}",
// prefix: "{{materialAddButton}}",
// },
// },
// NO_SUBMIT_SPY: {
// type: 'object',
// "x-component": "moneyTotalBox"
// }
// }
// }
// // 新增请购单
// export const increaseSchema: ISchema = {
// type: 'object',
// properties: {
// NO_SUBMIT_TABS: {
// type: 'object',
// "x-component": 'tab',
// properties: {
// basicInfo,
// material,
// }
// }
// }
// }
// 基本信息
const basicInfo: ISchema = {
"x-index": 0,
type: 'object',
"x-component": 'tabpane',
"x-component": 'MellowCard',
"x-component-props": {
tab: '基本信息',
className: 'useConnectBtnWrapper'
title: '基本信息',
id: 'basicInfo',
},
properties: {
NO_SUBMIT_LAYOUT: {
......@@ -16,15 +176,19 @@ const basicInfo: ISchema = {
"x-component": 'mega-layout',
"x-component-props": {
labelCol: 4,
labelAlign: 'left',
wrapperCol: 10
wrapperCol: 18,
labelAlign: "left",
grid: true,
full: true,
autoRow: true,
columns: 2,
},
properties: {
requisitionNo: {
type: 'string',
title: '请购单号',
"x-component": 'text',
visible: false
visible: false,
},
digest: {
type: 'string',
......@@ -38,7 +202,10 @@ const basicInfo: ISchema = {
limitByte: true,
maxByte: 60
}
]
],
"x-mega-props": {
span: 1
}
},
deliverTime: {
type: 'string',
......@@ -46,12 +213,13 @@ const basicInfo: ISchema = {
title: '预交日期',
required: true,
"x-component-props": {
// showTime: true,
format: 'YYYY-MM-DD',
disabledDate: current => {
return current && current < moment().startOf('day')
},
style: { width: '100%' }
},
"x-mega-props": {
span: 1
}
},
department: {
......@@ -62,6 +230,9 @@ const basicInfo: ISchema = {
disabled: true,
addonAfter: "{{departmentBtn}}"
},
"x-mega-props": {
span: 1
}
},
departmentId: {
type: 'string',
......@@ -80,7 +251,10 @@ const basicInfo: ISchema = {
limitByte: true,
maxByte: 100
}
]
],
"x-mega-props": {
span: 1
}
},
vendorMemberName: {
type: 'string',
......@@ -93,11 +267,11 @@ const basicInfo: ISchema = {
},
vendorMemberId: {
type: 'string',
display: false
visible: false
},
vendorRoleId: {
type: 'string',
display: false
visible: false
},
createTime: {
type: 'string',
......@@ -116,19 +290,21 @@ const basicInfo: ISchema = {
}
// 请购单物料
export const material: ISchema = {
const material: ISchema = {
"x-index": 2,
type: 'object',
"x-component": 'tabpane',
"x-component": 'MellowCard',
"x-component-props": {
tab: '订单物料'
title: '订单物料',
id: 'orderMaterial',
},
properties: {
products: {
type: 'array',
"x-component": 'MultTable',
required: true,
"x-component-props": {
rowKey: 'materialId',
rowKey: 'id',
columns: "{{materialColumns}}",
components: "{{materialComponents}}",
prefix: "{{materialAddButton}}",
......@@ -142,18 +318,10 @@ export const material: ISchema = {
}
// 新增请购单
export const increaseSchema: ISchema = {
type: 'object',
properties: {
NO_SUBMIT_TABS: {
type: 'object',
"x-component": 'tab',
properties: {
basicInfo,
material,
}
}
basicInfo,
material,
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment