dark
3 months ago
33 changed files with 816 additions and 1260 deletions
-
43.eslintrc.cjs
-
6package.json
-
2src/components/cascader/Cascader.tsx
-
21src/components/crazy-form/context.ts
-
222src/components/crazy-form/index.tsx
-
80src/components/crazy-form/tabs-form/TabForm.tsx
-
375src/components/crazy-form/tabs-form/index.tsx
-
74src/components/crazy-form/typeing.d.ts
-
319src/components/x-form/index.tsx
-
13src/components/x-form/style.ts
-
155src/components/x-form/utils/index.tsx
-
19src/context.ts
-
0src/hooks/useNavigate.ts
-
2src/layout/EmptyLayout.tsx
-
31src/layout/RootLayout.tsx
-
2src/pages/app/package/index.tsx
-
16src/pages/system/menus/index.tsx
-
3src/pages/system/roles/index.tsx
-
20src/pages/x-form/hooks/useApi.tsx
-
298src/pages/x-form/index.tsx
-
8src/pages/x-form/utils/index.tsx
-
1src/request.ts
-
33src/routes.tsx
-
4src/service/base.ts
-
6src/service/x-form/model.ts
-
4src/store/app/package.ts
-
3src/store/system/menu.ts
-
259src/store/x-form/model.ts
-
2src/types/x-form/model.d.ts
-
1src/utils/index.ts
-
13src/utils/tree.ts
-
5tsconfig.json
-
34vite.config.ts
@ -1,24 +1,25 @@ |
|||
module.exports = { |
|||
root: true, |
|||
env: { browser: true, es2020: true }, |
|||
extends: [ |
|||
'eslint:recommended', |
|||
'plugin:@typescript-eslint/recommended', |
|||
'plugin:react-hooks/recommended', |
|||
root: true, |
|||
env: { browser: true, es2020: true }, |
|||
extends: [ |
|||
'eslint:recommended', |
|||
'plugin:@typescript-eslint/recommended', |
|||
'plugin:react-hooks/recommended', |
|||
], |
|||
ignorePatterns: [ 'dist', '.eslintrc.cjs' ], |
|||
parser: '@typescript-eslint/parser', |
|||
plugins: [ 'react-refresh' ], |
|||
rules: { |
|||
'react-refresh/only-export-components': [ |
|||
'warn', |
|||
{ allowConstantExport: true }, |
|||
], |
|||
ignorePatterns: [ 'dist', '.eslintrc.cjs' ], |
|||
parser: '@typescript-eslint/parser', |
|||
plugins: [ 'react-refresh' ], |
|||
rules: { |
|||
'react-refresh/only-export-components': [ |
|||
'warn', |
|||
{ allowConstantExport: true }, |
|||
], |
|||
'@typescript-eslint/ban-ts-comment': [ 'error', { |
|||
'ts-expect-error': 'allow-with-description', |
|||
'ts-ignore': 'allow-with-description', |
|||
'minimumDescriptionLength': 10 |
|||
} ], |
|||
'@typescript-eslint/no-explicit-any': 'off', |
|||
}, |
|||
'@typescript-eslint/ban-ts-comment': [ 'error', { |
|||
'ts-expect-error': 'allow-with-description', |
|||
'ts-ignore': 'allow-with-description', |
|||
'minimumDescriptionLength': 10 |
|||
} ], |
|||
'@typescript-eslint/no-explicit-any': 'off', |
|||
'no-unused-vars': 'off', |
|||
}, |
|||
} |
@ -1,21 +0,0 @@ |
|||
import React from 'react' |
|||
import { FormInstance } from 'antd/lib' |
|||
import { CrazyChildFormProps } from './typeing' |
|||
|
|||
|
|||
export const CrazyFormProvide = React.createContext< |
|||
| { |
|||
regForm: (name: string, props: CrazyChildFormProps<any>) => void; |
|||
unRegForm: (name: string) => void; |
|||
onFormFinish: (name: string, formData: any) => void; |
|||
keyArray: string[]; |
|||
formArrayRef: React.MutableRefObject< |
|||
React.MutableRefObject<FormInstance<any> | undefined>[] |
|||
>; |
|||
loading: boolean; |
|||
setLoading: (loading: boolean) => void; |
|||
formMapRef: React.MutableRefObject<Map<string, CrazyChildFormProps>>; |
|||
} |
|||
| undefined |
|||
>(undefined) |
|||
|
@ -1,222 +0,0 @@ |
|||
import { |
|||
FormSchema, |
|||
ItemType, |
|||
ProFormRenderValueTypeHelpers |
|||
} from '@ant-design/pro-form/es/components/SchemaForm/typing' |
|||
import { StepsForm, Embed } from '@ant-design/pro-form/es/components/SchemaForm/layoutType' |
|||
import { renderValueType } from '@ant-design/pro-form/es/components/SchemaForm/valueType' |
|||
import { |
|||
DrawerForm, FormProps, LabelIconTip, |
|||
LightFilter, |
|||
ModalForm, omitUndefined, |
|||
ProForm, ProFormColumnsType, ProFormInstance, ProFormProps, |
|||
QueryFilter, runFunction, |
|||
StepsForm as ProStepsForm, stringify, useDeepCompareMemo, useLatest, useReactiveRef, useRefFunction |
|||
} from '@ant-design/pro-components' |
|||
import React, { useCallback, useImperativeHandle, useRef, useState } from 'react' |
|||
import { TabsForm } from './tabs-form' |
|||
import { Form } from 'antd' |
|||
|
|||
export type CrazyBateFormProps<T, ValueType> = { |
|||
layoutType: FormSchema<T, ValueType>['layoutType'] | 'TabsForm' |
|||
} & Omit<FormSchema<T, ValueType>, 'layout'> |
|||
|
|||
|
|||
const FormLayoutType = { |
|||
DrawerForm, |
|||
QueryFilter, |
|||
LightFilter, StepForm: ProStepsForm.StepForm, |
|||
|
|||
StepsForm: StepsForm, |
|||
ModalForm, |
|||
Embed, |
|||
Form: ProForm, |
|||
TabsForm, |
|||
} |
|||
|
|||
const CrazyBateForm = <T, ValueType = 'text'>(props: CrazyBateFormProps<T, ValueType>) => { |
|||
|
|||
const { |
|||
columns, |
|||
layoutType = 'TabsForm', |
|||
type = 'form', |
|||
action, |
|||
shouldUpdate = (pre, next) => stringify(pre) !== stringify(next), |
|||
formRef: propsFormRef, |
|||
...restProps |
|||
} = props |
|||
|
|||
const FormRenderComponents = (FormLayoutType[layoutType as 'TabsForm'] || |
|||
ProForm) as React.FC<ProFormProps<T>> |
|||
|
|||
const [ form ] = Form.useForm() |
|||
const formInstance = Form.useFormInstance() |
|||
|
|||
const [ , forceUpdate ] = useState<[]>([]) |
|||
const [ formDomsDeps, updatedFormDoms ] = useState<[]>(() => []) |
|||
|
|||
const formRef = useReactiveRef<ProFormInstance | undefined>( |
|||
props.form || formInstance || form, |
|||
) |
|||
const oldValuesRef = useRef<T>() |
|||
const propsRef = useLatest(props) |
|||
|
|||
/** |
|||
* 生成子项,方便被 table 接入 |
|||
* |
|||
* @param items |
|||
*/ |
|||
const genItems: ProFormRenderValueTypeHelpers<T, ValueType>['genItems'] = |
|||
useRefFunction((items: ProFormColumnsType<T, ValueType>[]) => { |
|||
return items |
|||
.filter((originItem) => { |
|||
return !(originItem.hideInForm && type === 'form') |
|||
}) |
|||
.sort((a, b) => { |
|||
if (b.order || a.order) { |
|||
return (b.order || 0) - (a.order || 0) |
|||
} |
|||
return (b.index || 0) - (a.index || 0) |
|||
}) |
|||
.map((originItem, index) => { |
|||
const title = runFunction( |
|||
originItem.title, |
|||
originItem, |
|||
'form', |
|||
<LabelIconTip |
|||
label={originItem.title as string} |
|||
//@ts-ignore @ts-expect-error
|
|||
tooltip={originItem.tooltip || originItem.tip} |
|||
/>, |
|||
) |
|||
|
|||
const item = omitUndefined({ |
|||
title, |
|||
label: title, |
|||
name: originItem.name, |
|||
valueType: runFunction(originItem.valueType, {}), |
|||
key: originItem.key || originItem.dataIndex || index, |
|||
columns: originItem.columns, |
|||
valueEnum: originItem.valueEnum, |
|||
dataIndex: originItem.dataIndex || originItem.key, |
|||
initialValue: originItem.initialValue, |
|||
width: originItem.width, |
|||
index: originItem.index, |
|||
readonly: originItem.readonly, |
|||
colSize: originItem.colSize, |
|||
colProps: originItem.colProps, |
|||
rowProps: originItem.rowProps, |
|||
className: originItem.className, |
|||
//@ts-ignore @ts-expect-error
|
|||
tooltip: originItem.tooltip || originItem.tip, |
|||
dependencies: originItem.dependencies, |
|||
proFieldProps: originItem.proFieldProps, |
|||
ignoreFormItem: originItem.ignoreFormItem, |
|||
getFieldProps: originItem.fieldProps |
|||
? () => |
|||
runFunction( |
|||
originItem.fieldProps, |
|||
formRef.current, |
|||
originItem, |
|||
) |
|||
: undefined, |
|||
getFormItemProps: originItem.formItemProps |
|||
? () => |
|||
runFunction( |
|||
originItem.formItemProps, |
|||
formRef.current, |
|||
originItem, |
|||
) |
|||
: undefined, |
|||
render: originItem.render, |
|||
renderFormItem: originItem.renderFormItem, |
|||
renderText: originItem.renderText, |
|||
request: originItem.request, |
|||
params: originItem.params, |
|||
transform: originItem.transform, |
|||
convertValue: originItem.convertValue, |
|||
debounceTime: originItem.debounceTime, |
|||
defaultKeyWords: originItem.defaultKeyWords, |
|||
}) as ItemType<any, any> |
|||
|
|||
return renderValueType(item, { |
|||
action, |
|||
type, |
|||
originItem, |
|||
formRef, |
|||
genItems, |
|||
}) |
|||
}) |
|||
.filter((field) => { |
|||
return Boolean(field) |
|||
}) |
|||
}) |
|||
|
|||
const onValuesChange: FormProps<T>['onValuesChange'] = useCallback( |
|||
(changedValues: any, values: T) => { |
|||
const { onValuesChange: propsOnValuesChange } = propsRef.current |
|||
if ( |
|||
shouldUpdate === true || |
|||
(typeof shouldUpdate === 'function' && |
|||
shouldUpdate(values, oldValuesRef.current)) |
|||
) { |
|||
updatedFormDoms([]) |
|||
} |
|||
oldValuesRef.current = values |
|||
propsOnValuesChange?.(changedValues, values) |
|||
}, |
|||
[ propsRef, shouldUpdate ], |
|||
) |
|||
|
|||
const formChildrenDoms = useDeepCompareMemo(() => { |
|||
if (!formRef.current) return |
|||
// like StepsForm's columns but not only for StepsForm
|
|||
if (columns.length && Array.isArray(columns[0])) return |
|||
return genItems(columns as ProFormColumnsType<T, ValueType>[]) |
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
}, [ columns, restProps?.open, action, type, formDomsDeps, !!formRef.current ]) |
|||
|
|||
/** |
|||
* Append layoutType component specific props |
|||
*/ |
|||
const specificProps = useDeepCompareMemo(() => { |
|||
if (layoutType === 'StepsForm') { |
|||
return { |
|||
forceUpdate: forceUpdate, |
|||
columns: columns as ProFormColumnsType<T, ValueType>[][], |
|||
} |
|||
} |
|||
|
|||
return {} |
|||
}, [ columns, layoutType ]) |
|||
|
|||
useImperativeHandle( |
|||
propsFormRef, |
|||
() => { |
|||
return formRef.current |
|||
}, |
|||
[ formRef.current ], |
|||
) |
|||
|
|||
return ( |
|||
<FormRenderComponents |
|||
{...specificProps} |
|||
{...restProps} |
|||
onInit={(_, initForm) => { |
|||
if (propsFormRef) { |
|||
(propsFormRef as React.MutableRefObject<ProFormInstance<T>>).current = |
|||
initForm |
|||
} |
|||
restProps?.onInit?.(_, initForm) |
|||
formRef.current = initForm |
|||
}} |
|||
form={props.form || form} |
|||
formRef={formRef} |
|||
onValuesChange={onValuesChange} |
|||
> |
|||
{formChildrenDoms} |
|||
</FormRenderComponents> |
|||
) |
|||
} |
|||
|
|||
export default CrazyBateForm |
@ -1,80 +0,0 @@ |
|||
import { useContext, useEffect, useImperativeHandle, useRef } from 'react' |
|||
import { TabsProps } from 'antd' |
|||
import { noteOnce } from 'rc-util/lib/warning' |
|||
import { FormInstance } from 'antd/lib' |
|||
import { CrazyFormProvide } from '../context.ts' |
|||
import { TabFormProvide } from './index.tsx' |
|||
import { CrazyChildFormProps } from '../typeing' |
|||
|
|||
export type TabFormProps<T = Record<string, any>> = { |
|||
tab?: string; |
|||
tabProps?: TabsProps; |
|||
} & CrazyChildFormProps<T> |
|||
|
|||
const TabForm = <T = Record<string, any>>(props: TabFormProps<T>) => { |
|||
|
|||
const formRef = useRef<FormInstance | undefined>() |
|||
const context = useContext(CrazyFormProvide) |
|||
const tabContext = useContext(TabFormProvide) |
|||
|
|||
const { |
|||
onFinish, |
|||
tab, |
|||
formRef: propFormRef, |
|||
tabProps, |
|||
...restProps |
|||
} = props |
|||
|
|||
noteOnce(!(restProps as any).submitter, 'TabForm 不包含提交按钮,请在 TabsForm 上') |
|||
|
|||
/** 重置 formRef */ |
|||
useImperativeHandle(propFormRef, () => formRef.current, [ |
|||
propFormRef?.current, |
|||
]) |
|||
|
|||
/** Dom 不存在的时候解除挂载 */ |
|||
useEffect(() => { |
|||
if (!(props.name || props.tab)) return |
|||
const name = (props.name || props.tab)!.toString() |
|||
context?.regForm(name, props) |
|||
return () => { |
|||
context?.unRegForm(name) |
|||
} |
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
}, []) |
|||
|
|||
if (context && context?.formArrayRef) { |
|||
context.formArrayRef.current[tab || 0] = formRef |
|||
} |
|||
|
|||
return ( |
|||
<BaseForm |
|||
formRef={formRef} |
|||
onFinish={async (values) => { |
|||
if (restProps.name) { |
|||
context?.onFormFinish(restProps.name, values) |
|||
} |
|||
if (onFinish) { |
|||
context?.setLoading(true) |
|||
// 如果报错,直接抛出
|
|||
await onFinish?.(values) |
|||
|
|||
context?.setLoading(false) |
|||
return |
|||
} |
|||
|
|||
}} |
|||
onInit={(_, form) => { |
|||
formRef.current = form |
|||
if (context && context?.formArrayRef) { |
|||
context.formArrayRef.current[tab || 0] = formRef |
|||
} |
|||
restProps?.onInit?.(_, form) |
|||
}} |
|||
layout="vertical" |
|||
{...omit(restProps, [ 'layoutType', 'columns' ] as any[])} |
|||
/> |
|||
) |
|||
} |
|||
|
|||
export default TabForm |
@ -1,375 +0,0 @@ |
|||
import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react' |
|||
import TabForm, { TabFormProps } from './TabForm' |
|||
import { CrazyFormProps } from '../typeing' |
|||
import { Button, Col, Form, Row, Space, Tabs, TabsProps } from 'antd' |
|||
import { FormInstance } from 'antd/lib' |
|||
import toArray from 'rc-util/lib/Children/toArray' |
|||
import useMergedState from 'rc-util/lib/hooks/useMergedState' |
|||
import { merge, ProConfigProvider, useRefFunction } from '@ant-design/pro-components' |
|||
import { t } from '@/i18n' |
|||
import classnames from 'classnames' |
|||
import { CrazyFormProvide } from '../context' |
|||
|
|||
type TabsFormProps<T = Record<string, any>> = { |
|||
toggleProps: TabsProps |
|||
direction?: 'horizontal' | 'vertical' |
|||
} & Omit<CrazyFormProps<T>, 'toggleProps'> |
|||
|
|||
export const TabFormProvide = React.createContext<TabFormProps<any> | null>(null) |
|||
|
|||
|
|||
const TabsLayoutStrategy: Record< |
|||
string, |
|||
(dom: LayoutRenderDom) => React.ReactNode |
|||
> = { |
|||
horizontal({ toggleDom, formDom }) { |
|||
return ( |
|||
<> |
|||
<Row gutter={{ xs: 8, sm: 16, md: 24 }}> |
|||
<Col span={24}>{toggleDom}</Col> |
|||
</Row> |
|||
<Row gutter={{ xs: 8, sm: 16, md: 24 }}> |
|||
<Col span={24}>{formDom}</Col> |
|||
</Row> |
|||
</> |
|||
) |
|||
}, |
|||
vertical({ stepsDom, formDom }) { |
|||
return ( |
|||
<Row align="stretch" wrap={true} gutter={{ xs: 8, sm: 16, md: 24 }}> |
|||
<Col xxl={4} xl={6} lg={7} md={8} sm={10} xs={12}> |
|||
{React.cloneElement(stepsDom, { |
|||
style: { |
|||
height: '100%', |
|||
}, |
|||
})} |
|||
</Col> |
|||
<Col> |
|||
<div |
|||
style={{ |
|||
display: 'flex', |
|||
alignItems: 'center', |
|||
width: '100%', |
|||
height: '100%', |
|||
}} |
|||
> |
|||
{formDom} |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
) |
|||
}, |
|||
} |
|||
|
|||
|
|||
const TabsForm = <T = Record<string, any>>( |
|||
props: TabsFormProps<T> & { |
|||
children: React.ReactNode |
|||
} |
|||
) => { |
|||
|
|||
const { |
|||
toggleProps, |
|||
toggleFormRender, |
|||
direction = 'horizontal', |
|||
current: tab, |
|||
onCurrentChange, |
|||
submitter, |
|||
formRender, |
|||
onFinish, |
|||
formProps, |
|||
containerStyle, |
|||
formRef, |
|||
formMapRef: propsFormMapRef, |
|||
layoutRender: propsLayoutRender, |
|||
...rest |
|||
} = props |
|||
|
|||
|
|||
const formDataRef = useRef(new Map<string, Record<string, any>>()) |
|||
const formMapRef = useRef(new Map<string, TabFormProps>()) |
|||
const formArrayRef = useRef< |
|||
React.MutableRefObject<FormInstance<any> | undefined>[] |
|||
>([]) |
|||
const [ formArray, setFormArray ] = useState<string[]>([]) |
|||
const [ loading, setLoading ] = useState<boolean>(false) |
|||
|
|||
/** |
|||
* 受控的方式来操作表单 |
|||
*/ |
|||
const [ tab, setTab ] = useMergedState<number>(0, { |
|||
value: props.current, |
|||
onChange: props.onCurrentChange, |
|||
}) |
|||
|
|||
const layoutRender = useMemo(() => { |
|||
return TabsLayoutStrategy[direction] |
|||
}, [ direction ]) |
|||
|
|||
|
|||
/** |
|||
* 注册一个form进入,方便进行 props 的修改 |
|||
*/ |
|||
const regForm = useCallback( |
|||
(name: string, childrenFormProps: TabFormProps) => { |
|||
if (!formMapRef.current.has(name)) { |
|||
setFormArray((oldFormArray) => [ ...oldFormArray, name ]) |
|||
} |
|||
formMapRef.current.set(name, childrenFormProps) |
|||
}, |
|||
[], |
|||
) |
|||
|
|||
/** |
|||
* 解除挂载掉这个 form,同时步数 -1 |
|||
*/ |
|||
const unRegForm = useCallback((name: string) => { |
|||
setFormArray((oldFormArray) => oldFormArray.filter((n) => n !== name)) |
|||
formMapRef.current.delete(name) |
|||
formDataRef.current.delete(name) |
|||
}, []) |
|||
|
|||
useImperativeHandle(propsFormMapRef, () => formArrayRef.current, [ |
|||
formArrayRef.current, |
|||
]) |
|||
|
|||
useImperativeHandle( |
|||
formRef, |
|||
() => { |
|||
return formArrayRef.current[tab || 0]?.current |
|||
}, |
|||
[ tab, formArrayRef.current ], |
|||
) |
|||
|
|||
/** |
|||
* ProForm处理了一下 from 的数据,在其中做了一些操作 如果使用 Provider 自带的,自带的数据处理就无法生效了 |
|||
*/ |
|||
const onFormFinish = useCallback( |
|||
async (name: string, formData: any) => { |
|||
formDataRef.current.set(name, formData) |
|||
|
|||
|
|||
setLoading(true) |
|||
const values: any = merge( |
|||
{}, |
|||
...Array.from(formDataRef.current.values()), |
|||
) |
|||
try { |
|||
const success = await onFinish(values) |
|||
if (success) { |
|||
formArrayRef.current.forEach((form) => form.current?.resetFields()) |
|||
} |
|||
} catch (error) { |
|||
console.log(error) |
|||
} finally { |
|||
setLoading(false) |
|||
} |
|||
}, |
|||
[ lastStep, onFinish, setLoading, setTab ], |
|||
) |
|||
|
|||
const toggleDoms = useMemo(() => { |
|||
const itemsProps = { |
|||
items: formArray.map((item) => { |
|||
const itemProps = formMapRef.current.get(item) |
|||
return { |
|||
key: item, |
|||
title: itemProps?.title, |
|||
...itemProps?.tabProps, |
|||
} |
|||
}), |
|||
} |
|||
|
|||
return ( |
|||
<div className={`crazy-tabs-container`.trim()} |
|||
|
|||
> |
|||
<Tabs |
|||
{...toggleProps} |
|||
{...itemsProps} |
|||
activeKey={tab} |
|||
onChange={onCurrentChange} |
|||
> |
|||
</Tabs> |
|||
</div> |
|||
) |
|||
}, [ formArray, tab, toggleProps, onCurrentChange ]) |
|||
|
|||
const onSubmit = useRefFunction(() => { |
|||
const from = formArrayRef.current[tab] |
|||
from.current?.submit() |
|||
}) |
|||
|
|||
const submit = useMemo(() => { |
|||
return ( |
|||
submitter !== false && ( |
|||
<Button |
|||
key="submit" |
|||
type="primary" |
|||
loading={loading} |
|||
{...submitter?.submitButtonProps} |
|||
onClick={() => { |
|||
submitter?.onSubmit?.() |
|||
onSubmit() |
|||
}} |
|||
> |
|||
{t('actions.submit', '提交')} |
|||
</Button> |
|||
) |
|||
) |
|||
}, [ loading, onSubmit, submitter ]) |
|||
|
|||
|
|||
const submitterDom = useMemo(() => { |
|||
let buttons: (React.ReactElement | false)[] = [ submit ] |
|||
buttons = buttons.filter(React.isValidElement) |
|||
|
|||
if (submitter && submitter.render) { |
|||
const submitterProps: any = { |
|||
form: formArrayRef.current[tab]?.current, |
|||
onSubmit, |
|||
current: tab, |
|||
} |
|||
|
|||
return submitter.render( |
|||
submitterProps, |
|||
buttons as React.ReactElement[], |
|||
) as React.ReactNode |
|||
} |
|||
if (submitter && submitter?.render === false) { |
|||
return null |
|||
} |
|||
return buttons as React.ReactElement[] |
|||
}, [ formArray.length, onSubmit, tab, submit, submitter ]) |
|||
|
|||
const formDom = useMemo(() => { |
|||
return toArray(props.children).map((item, index) => { |
|||
const itemProps = item.props as TabFormProps |
|||
const name = itemProps.name || `${index}` |
|||
/** 是否是当前的表单 */ |
|||
const isShow = tab === name |
|||
|
|||
const config = isShow |
|||
? { |
|||
contentRender: formRender, |
|||
} |
|||
: {} |
|||
return ( |
|||
<div |
|||
className={classnames(`crazy-tab`, { |
|||
[`crazy-tab-active`]: isShow, |
|||
})} |
|||
key={name} |
|||
> |
|||
<TabFormProvide.Provider |
|||
value={{ |
|||
...config, |
|||
...formProps, |
|||
...itemProps, |
|||
name, |
|||
index, |
|||
tab: name, |
|||
}} |
|||
> |
|||
{item} |
|||
</TabFormProvide.Provider> |
|||
</div> |
|||
) |
|||
}) |
|||
}, [ formProps, props.children, tab, formRender ]) |
|||
|
|||
const finalTabsDom = useMemo(() => { |
|||
if (toggleFormRender) { |
|||
return toggleFormRender( |
|||
formArray.map((item) => ({ |
|||
key: item, |
|||
title: formMapRef.current.get(item)?.title, |
|||
})), |
|||
toggleDoms, |
|||
) as React.ReactElement |
|||
} |
|||
return toggleDoms |
|||
}, [ formArray, toggleDoms, toggleFormRender ]) |
|||
|
|||
const formContainer = useMemo( |
|||
() => ( |
|||
<div |
|||
className={`crazy-container`.trim()} |
|||
style={containerStyle} |
|||
> |
|||
{formDom} |
|||
{toggleFormRender ? null : <Space>{submitterDom}</Space>} |
|||
</div> |
|||
), |
|||
[ containerStyle, formDom, toggleFormRender, submitterDom ], |
|||
) |
|||
|
|||
const tabsFormDom = useMemo(() => { |
|||
const doms = { |
|||
toggleDom: finalTabsDom, |
|||
formDom: formContainer, |
|||
} |
|||
|
|||
if (toggleFormRender) { |
|||
if (propsLayoutRender) { |
|||
return toggleFormRender(propsLayoutRender(doms), submitterDom) |
|||
} else { |
|||
return toggleFormRender(layoutRender(doms), submitterDom) |
|||
} |
|||
} |
|||
|
|||
if (propsLayoutRender) { |
|||
return propsLayoutRender(doms) |
|||
} |
|||
|
|||
return layoutRender(doms) |
|||
}, [ |
|||
finalTabsDom, |
|||
formContainer, |
|||
layoutRender, |
|||
toggleFormRender, |
|||
submitterDom, |
|||
propsLayoutRender, |
|||
]) |
|||
|
|||
|
|||
return ( |
|||
<div> |
|||
<Form.Provider {...rest}> |
|||
<CrazyFormProvide.Provider |
|||
value={{ |
|||
loading, |
|||
setLoading, |
|||
regForm, |
|||
keyArray: formArray, |
|||
formArrayRef, |
|||
formMapRef, |
|||
unRegForm, |
|||
onFormFinish, |
|||
}} |
|||
> |
|||
{tabsFormDom} |
|||
</CrazyFormProvide.Provider> |
|||
</Form.Provider> |
|||
</div> |
|||
) |
|||
|
|||
} |
|||
|
|||
function TabsFormWarp<T = Record<string, any>>( |
|||
props: CrazyFormProps<T> & { |
|||
children: any; |
|||
}, |
|||
) { |
|||
return ( |
|||
<ProConfigProvider needDeps> |
|||
<TabsForm<T> {...props} /> |
|||
</ProConfigProvider> |
|||
) |
|||
} |
|||
|
|||
TabsFormWarp.TabForm = TabForm |
|||
TabsFormWarp.useForm = Form.useForm |
|||
|
|||
export { TabsFormWarp as TabsForm } |
|||
export type { TabFormProps, CrazyFormProps as TabsFormProps } |
@ -1,74 +0,0 @@ |
|||
import { FormInstance } from 'antd/lib' |
|||
import { FormProps, ProFormInstance, ProFormProps, SubmitterProps } from '@ant-design/pro-components' |
|||
import React from 'react' |
|||
import { FormProviderProps } from 'antd/es/form/context' |
|||
import type { CommonFormProps } from '@ant-design/pro-form/es/BaseForm/BaseForm' |
|||
|
|||
export type CrazyFormProps<T = Record<string, any>> = { |
|||
|
|||
onFinish?: (values: T) => Promise<boolean | void>; |
|||
current?: string; |
|||
/**、 |
|||
* 切换区域传透的Props |
|||
*/ |
|||
toggleProps?: Record<any, any>; |
|||
formProps?: ProFormProps<T>; |
|||
onCurrentChange?: (current: string) => void; |
|||
/** 自定义步骤器 */ |
|||
toggleRender?: ( |
|||
items: { |
|||
key: string; |
|||
title?: React.ReactNode; |
|||
[key: string]: any |
|||
}[], |
|||
defaultDom: React.ReactNode, |
|||
) => React.ReactNode; |
|||
/** @name 当前展示表单的 formRef */ |
|||
formRef?: React.MutableRefObject<ProFormInstance<any> | undefined | null>; |
|||
/** @name 所有表单的 formMapRef */ |
|||
formMapRef?: React.MutableRefObject< |
|||
React.MutableRefObject<FormInstance<any> | undefined>[] |
|||
>; |
|||
/** |
|||
* 自定义单个表单 |
|||
* |
|||
* @param form From 的 dom,可以放置到别的位置 |
|||
*/ |
|||
toggleFormRender?: (from: React.ReactNode) => React.ReactNode; |
|||
|
|||
/** |
|||
* 自定义整个表单区域 |
|||
* |
|||
* @param form From 的 dom,可以放置到别的位置 |
|||
* @param submitter 操作按钮 |
|||
*/ |
|||
formRender?: ( |
|||
from: React.ReactNode, |
|||
submitter: React.ReactNode, |
|||
) => React.ReactNode; |
|||
/** 按钮的统一配置,优先级低于分步表单的配置 */ |
|||
submitter?: |
|||
| SubmitterProps<{ |
|||
current: string; //当前激活的toggle
|
|||
form?: FormInstance<any>; |
|||
}> |
|||
| false; |
|||
|
|||
containerStyle?: React.CSSProperties; |
|||
/** |
|||
* 自定義整個佈局。 |
|||
* |
|||
* @param layoutDom toggleDom 和 formDom 元素可以放置在任何地方。 |
|||
*/ |
|||
layoutRender?: (layoutDom: { |
|||
toggleDom: React.ReactElement; |
|||
formDom: React.ReactElement; |
|||
}) => React.ReactNode; |
|||
} & Omit<FormProviderProps, 'children'>; |
|||
|
|||
|
|||
export type CrazyChildFormProps<T = Record<string, any>, U = Record<string, any>> = { |
|||
|
|||
index?: number; |
|||
} & Omit<FormProps<T>, 'onFinish' | 'form'> & |
|||
Omit<CommonFormProps<T, U>, 'submitter' | 'form'>; |
@ -0,0 +1,319 @@ |
|||
import { useStyle } from './style.ts' |
|||
import { Badge, Button, Divider, Form, Popconfirm, Space, Tooltip } from 'antd' |
|||
import { useAtom, useAtomValue, useSetAtom } from 'jotai' |
|||
import { ModelContext, useSpanModel } from '@/store/x-form/model.ts' |
|||
import { ReactNode, useEffect, useState } from 'react' |
|||
import { transformAntdTableProColumns } from './utils' |
|||
import Action from '@/components/action/Action.tsx' |
|||
import { FilterOutlined } from '@ant-design/icons' |
|||
import ListPageLayout from '@/layout/ListPageLayout.tsx' |
|||
import { Table as ProTable } from '@/components/table' |
|||
import { getValueCount, unSetColumnRules } from '@/utils' |
|||
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components' |
|||
import { useApiContext } from '@/context.ts' |
|||
import { useDeepCompareEffect } from 'react-use' |
|||
import { XFormTypes } from '@/types/x-form/model' |
|||
|
|||
|
|||
export interface XFormProps { |
|||
title?: ReactNode |
|||
namespace?: string |
|||
columns?: ProColumns[] //重写columns
|
|||
} |
|||
|
|||
const XForm = ({ namespace, columns: propColumns = [], title }: XFormProps) => { |
|||
|
|||
const { styles, cx } = useStyle() |
|||
const apiCtx = useApiContext() |
|||
const { |
|||
apiAtom, |
|||
deleteModelAtom, |
|||
modelAtom, |
|||
modelCURDAtom, |
|||
modelsAtom, |
|||
modelSearchAtom, |
|||
saveOrUpdateModelAtom |
|||
} = useSpanModel(namespace || apiCtx?.menu?.meta?.name || 'default') as ModelContext |
|||
const [ form ] = Form.useForm() |
|||
const [ filterForm ] = Form.useForm() |
|||
const setApi = useSetAtom(apiAtom) |
|||
const [ model, setModel ] = useAtom<XFormTypes.IModel>(modelAtom) |
|||
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateModelAtom) |
|||
const [ search, setSearch ] = useAtom(modelSearchAtom) |
|||
const { data, isFetching, isLoading, refetch } = useAtomValue(modelsAtom) |
|||
const { mutate: deleteModel, isPending: isDeleting } = useAtomValue(deleteModelAtom) |
|||
const { data: curdModal, isLoading: curdLoading, refetch: reloadCURDModal } = useAtomValue(modelCURDAtom) |
|||
const [ open, setOpen ] = useState(false) |
|||
const [ openFilter, setFilterOpen ] = useState(false) |
|||
const [ searchKey, setSearchKey ] = useState(search?.key) |
|||
const [ columns, setColumns ] = useState<ProColumns[]>([]) |
|||
|
|||
useDeepCompareEffect(() => { |
|||
|
|||
const res = transformAntdTableProColumns(curdModal?.column || [], propColumns) |
|||
const _columns = [ { |
|||
title: 'ID', |
|||
dataIndex: 'id', |
|||
hideInTable: true, |
|||
hideInSearch: true, |
|||
formItemProps: { hidden: true } |
|||
} ].concat(res as any).concat([ |
|||
{ |
|||
title: '操作', |
|||
dataIndex: 'option', |
|||
valueType: 'option', |
|||
fixed: 'right', |
|||
render: (_, record) => [ |
|||
<Action key="edit" |
|||
as={'a'} |
|||
onClick={() => { |
|||
form.setFieldsValue(record) |
|||
setOpen(true) |
|||
}}>{'编辑'}</Action>, |
|||
<Popconfirm |
|||
key={'del_confirm'} |
|||
disabled={isDeleting} |
|||
onConfirm={() => { |
|||
deleteModel([ record.id ]) |
|||
}} |
|||
title={'确定要删除吗?'}> |
|||
<a key="del"> |
|||
删除 |
|||
</a> |
|||
</Popconfirm> |
|||
] |
|||
} as any |
|||
]) |
|||
setColumns(_columns) |
|||
}, [ curdModal?.column, propColumns, deleteModel, form, isDeleting, setOpen, ]) |
|||
|
|||
useEffect(() => { |
|||
if (apiCtx.isApi && apiCtx.api) { |
|||
setApi(apiCtx.api) |
|||
reloadCURDModal() |
|||
} |
|||
}, [ apiCtx.isApi, apiCtx.api ]) |
|||
|
|||
useDeepCompareEffect(() => { |
|||
|
|||
setSearchKey(search?.key) |
|||
|
|||
filterForm.setFieldsValue(search) |
|||
|
|||
}, [ search ]) |
|||
|
|||
useEffect(() => { |
|||
if (isSuccess) { |
|||
setOpen(false) |
|||
} |
|||
}, [ isSuccess ]) |
|||
|
|||
const formProps = curdModal?.dialogType === 'drawer' ? { |
|||
layoutType: 'DrawerForm', |
|||
drawerProps: { |
|||
maskClosable: false, |
|||
} |
|||
} : { |
|||
layoutType: 'ModalForm', |
|||
modalProps: { |
|||
maskClosable: false, |
|||
} |
|||
} |
|||
|
|||
const renderTitle = () => { |
|||
if (title) { |
|||
return title |
|||
} |
|||
|
|||
if (apiCtx.menu) { |
|||
const { menu } = apiCtx |
|||
return menu.title |
|||
} |
|||
return null |
|||
} |
|||
|
|||
const tableTitle = <> |
|||
<Button key={'add'} |
|||
onClick={() => { |
|||
form.resetFields() |
|||
form.setFieldsValue({ |
|||
id: 0, |
|||
}) |
|||
setOpen(true) |
|||
}} |
|||
type={'primary'}>{'添加'}</Button> |
|||
</> |
|||
|
|||
return ( |
|||
<> |
|||
<ListPageLayout |
|||
className={styles.container} |
|||
title={renderTitle()}> |
|||
|
|||
<ProTable |
|||
rowKey="id" |
|||
headerTitle={tableTitle} |
|||
toolbar={{ |
|||
/*search: { |
|||
loading: isFetching && !!search?.key, |
|||
onSearch: (value: string) => { |
|||
setSearch(prev => ({ |
|||
...prev, |
|||
title: value |
|||
})) |
|||
}, |
|||
allowClear: true, |
|||
onChange: (e) => { |
|||
setSearchKey(e.target?.value) |
|||
}, |
|||
value: searchKey, |
|||
placeholder: '输入关键字搜索', |
|||
},*/ |
|||
actions: [ |
|||
<Tooltip key={'filter'} title={'高级查询'}> |
|||
<Badge count={getValueCount(search)}> |
|||
<Button |
|||
onClick={() => { |
|||
setFilterOpen(true) |
|||
}} |
|||
icon={<FilterOutlined/>} shape={'circle'} size={'small'}/> |
|||
</Badge> |
|||
</Tooltip>, |
|||
<Divider type={'vertical'} key={'divider'}/>, |
|||
|
|||
] |
|||
}} |
|||
scroll={{ |
|||
x: (columns?.length || 1) * 100, |
|||
y: 'calc(100vh - 290px)' |
|||
}} |
|||
search={false} |
|||
onRow={(record) => { |
|||
return { |
|||
className: cx({ |
|||
// 'ant-table-row-selected': currentMovie?.id === record.id
|
|||
}), |
|||
onClick: () => { |
|||
setModel(record) |
|||
} |
|||
} |
|||
}} |
|||
dateFormatter="string" |
|||
loading={isLoading || isFetching || curdLoading} |
|||
dataSource={data?.rows ?? []} |
|||
columns={columns} |
|||
options={{ |
|||
reload: () => { |
|||
refetch() |
|||
}, |
|||
}} |
|||
pagination={{ |
|||
total: data?.total, |
|||
pageSize: search.pageSize, |
|||
current: search.page, |
|||
onShowSizeChange: (current: number, size: number) => { |
|||
setSearch({ |
|||
...search, |
|||
pageSize: size, |
|||
page: current |
|||
}) |
|||
}, |
|||
onChange: (current, pageSize) => { |
|||
setSearch(prev => { |
|||
return { |
|||
...prev, |
|||
page: current, |
|||
pageSize: pageSize, |
|||
} |
|||
}) |
|||
}, |
|||
}} |
|||
/> |
|||
|
|||
<BetaSchemaForm |
|||
grid={true} |
|||
shouldUpdate={false} |
|||
width={1000} |
|||
form={form} |
|||
layout={'vertical'} |
|||
scrollToFirstError={true} |
|||
title={model?.id !== 0 ? '编辑' : '添加'} |
|||
{...formProps as any} |
|||
open={open} |
|||
onOpenChange={(open) => { |
|||
setOpen(open) |
|||
}} |
|||
loading={isSubmitting} |
|||
onFinish={async (values) => { |
|||
saveOrUpdate(values as any) |
|||
}} |
|||
columns={columns as ProFormColumnsType[]}/> |
|||
<BetaSchemaForm |
|||
title={'高级查询'} |
|||
grid={true} |
|||
shouldUpdate={false} |
|||
width={500} |
|||
form={filterForm} |
|||
open={openFilter} |
|||
onOpenChange={open => { |
|||
setFilterOpen(open) |
|||
}} |
|||
layout={'vertical'} |
|||
scrollToFirstError={true} |
|||
layoutType={formProps.layoutType as any} |
|||
drawerProps={{ |
|||
...formProps.drawerProps, |
|||
mask: false, |
|||
}} |
|||
modalProps={{ |
|||
...formProps.modalProps, |
|||
mask: false, |
|||
}} |
|||
submitter={{ |
|||
searchConfig: { |
|||
resetText: '清空', |
|||
submitText: '查询', |
|||
}, |
|||
onReset: () => { |
|||
filterForm.resetFields() |
|||
}, |
|||
render: (props,) => { |
|||
return ( |
|||
<div style={{ textAlign: 'right' }}> |
|||
<Space> |
|||
<Button onClick={() => { |
|||
props.reset() |
|||
|
|||
}}>{props.searchConfig?.resetText}</Button> |
|||
<Button type="primary" |
|||
onClick={() => { |
|||
props.submit() |
|||
}} |
|||
>{props.searchConfig?.submitText}</Button> |
|||
</Space> |
|||
</div> |
|||
) |
|||
}, |
|||
|
|||
}} |
|||
|
|||
|
|||
onFinish={async (values) => { |
|||
//处理,变成数组
|
|||
Object.keys(values).forEach(key => { |
|||
if (typeof values[key] === 'string' && values[key].includes(',')) { |
|||
values[key] = values[key].split(',') |
|||
} |
|||
}) |
|||
|
|||
setSearch(values) |
|||
|
|||
}} |
|||
columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/> |
|||
|
|||
</ListPageLayout> |
|||
</> |
|||
) |
|||
} |
|||
|
|||
export default XForm |
@ -0,0 +1,13 @@ |
|||
import { createStyles } from '@/theme' |
|||
|
|||
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
|||
const prefix = `${prefixCls}-${token?.proPrefix}-x-form-component` |
|||
|
|||
const container = css`
|
|||
|
|||
`
|
|||
|
|||
return { |
|||
container: cx(prefix, props?.className, container), |
|||
} |
|||
}) |
@ -0,0 +1,155 @@ |
|||
import { XFormTypes } from '@/types/x-form/model' |
|||
import { ProColumns } from '@ant-design/pro-components' |
|||
import Switch from '@/components/switch' |
|||
import { Checkbox, DatePicker, Input, Radio, Select, TreeSelect } from 'antd' |
|||
import request from '@/request' |
|||
import { convertToBool, genProTableColumnWidthProps } from '@/utils' |
|||
|
|||
|
|||
const getValueType = (column: XFormTypes.IColumn) => { |
|||
switch (column.type) { |
|||
case 'input': |
|||
return 'text' |
|||
case 'select': |
|||
return 'select' |
|||
case 'date': |
|||
return 'date' |
|||
case 'switch': |
|||
return 'switch' |
|||
case 'radio': |
|||
return 'radio' |
|||
case 'checkbox': |
|||
return 'checkbox' |
|||
case 'textarea': |
|||
return 'textarea' |
|||
case 'tree': |
|||
return 'treeSelect' |
|||
default: |
|||
return 'text' |
|||
} |
|||
} |
|||
|
|||
//根据type返回对应的组件
|
|||
const getComponent = (column: XFormTypes.IColumn) => { |
|||
const type = getValueType(column) as any |
|||
switch (type) { |
|||
case 'input': |
|||
return Input |
|||
case 'select': |
|||
return Select |
|||
case 'date': |
|||
return DatePicker |
|||
case 'switch': |
|||
return Switch |
|||
case 'radio': |
|||
return Radio |
|||
case 'checkbox': |
|||
return Checkbox |
|||
case 'textarea': |
|||
return Input.TextArea |
|||
case 'tree': |
|||
case 'treeSelect': |
|||
return TreeSelect |
|||
default: |
|||
return Input |
|||
} |
|||
} |
|||
|
|||
|
|||
export const transformAntdTableProColumns = (columns: XFormTypes.IColumn[], overwriteColumns?: ProColumns[]) => { |
|||
|
|||
const overwriteKeys = [] as string[] |
|||
|
|||
return (columns || []).map(item => { |
|||
const { value, props, multiple, checkStrictly } = item |
|||
|
|||
const { width, fieldProps: _fieldProps } = genProTableColumnWidthProps(item.width) |
|||
const fieldProps: ProColumns['fieldProps'] = { |
|||
dataFiledNames: props, |
|||
...(multiple ? { multiple: true } : {}), |
|||
...(checkStrictly ? { treeCheckStrictly: true } : {}), |
|||
..._fieldProps, |
|||
} |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|||
const formItemProps: ProColumns['formItemProps'] = (_form, _config) => { |
|||
|
|||
return { |
|||
rules: item.rules?.map(i => { |
|||
return { |
|||
required: i.required, |
|||
message: i.message |
|||
} |
|||
}), |
|||
...(value ? { valuePropName: value } : {}) |
|||
} |
|||
} |
|||
|
|||
const rowProps = item.gutter ? { gutter: item.gutter } : { gutter: [ 16, 0 ], } |
|||
const colProps = item.span ? { span: item.span } : {} |
|||
|
|||
const type = getValueType(item) |
|||
|
|||
const overwrite = overwriteColumns?.find(i => i.dataIndex === item.prop) |
|||
if (overwrite) { |
|||
overwriteKeys.push(item.prop) |
|||
} |
|||
|
|||
return { |
|||
title: item.label, |
|||
dataIndex: item.prop, |
|||
key: item.prop, |
|||
width, |
|||
valueType: type, |
|||
hideInSearch: !item.search, |
|||
hideInTable: item.hide, |
|||
fieldProps, |
|||
formItemProps, |
|||
colProps, |
|||
rowProps, |
|||
request: item.dicUrl ? async (params, props) => { |
|||
const { fieldProps: { dataFiledNames } } = props |
|||
const { value, res: resKey, label } = dataFiledNames || {} |
|||
const url = `/${item.dicUrl.replace(/^:/, '/')}` |
|||
return request[item.dicMethod || 'get'](url, params).then(res => { |
|||
return (res.data?.[resKey] || res.data || []).map((i: any) => { |
|||
// console.log(i)
|
|||
const disabled = 'disabled' in i ? i.disabled : |
|||
('status' in i ? !convertToBool(i.status) : false) |
|||
return { |
|||
title: i[label || 'label'], |
|||
label: i[label || 'label'], |
|||
value: i[value || 'id'], |
|||
disabled, |
|||
data: i |
|||
} |
|||
}) |
|||
}) |
|||
} : undefined, |
|||
renderFormItem: (_scheam, config) => { |
|||
const Component = getComponent(item) as any |
|||
const { options, ...props } = config as any |
|||
|
|||
if ([ 'tree', 'treeSelect' ].includes(_scheam.valueType as string)) { |
|||
return <Component {...props} treeData={options}/> |
|||
} |
|||
if (_scheam.valueType as string === 'select') { |
|||
return <Select {...props} options={options}/> |
|||
} |
|||
|
|||
return <Component {...config} /> |
|||
}, |
|||
render: (text: any, record: any) => { |
|||
if (type === 'switch' || type === 'checkbox' || type === 'radio') { |
|||
return <Switch size={'small'} value={record[item.prop]}/> |
|||
} |
|||
if (item.colorFormat) { |
|||
return <span style={{ color: item.colorFormat }}>{text}</span> |
|||
} |
|||
return text |
|||
}, |
|||
...overwrite |
|||
} as ProColumns |
|||
}).concat(overwriteColumns?.filter(i => !overwriteKeys.includes(i.dataIndex)) || []) |
|||
|
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue