dark
4 months ago
19 changed files with 1826 additions and 1 deletions
-
1package.json
-
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
-
194src/components/interact-popup/index.tsx
-
40src/components/interact-popup/style.ts
-
0src/components/log-vewr/index.tsx
-
195src/pages/db/movie/components/Edit.tsx
-
4src/pages/db/movie/components/context.ts
-
21src/pages/db/movie/components/form/PrimaryFacts.tsx
-
70src/pages/db/movie/components/style.ts
-
280src/pages/db/movie/index.tsx
-
29src/pages/db/movie/style.ts
-
111src/service/base.ts
-
9src/service/db/movie.ts
-
90src/store/db/movie.ts
-
11src/types/db/movie.d.ts
@ -0,0 +1,21 @@ |
|||||
|
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) |
||||
|
|
@ -0,0 +1,222 @@ |
|||||
|
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 |
@ -0,0 +1,80 @@ |
|||||
|
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 |
@ -0,0 +1,375 @@ |
|||||
|
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 } |
@ -0,0 +1,74 @@ |
|||||
|
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,194 @@ |
|||||
|
import { useStyle } from './style' |
||||
|
import { Button, Drawer, DrawerProps, Modal, ModalProps, Space } from 'antd' |
||||
|
import { forwardRef, memo, ReactNode, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' |
||||
|
import { t } from '@/i18n' |
||||
|
|
||||
|
export interface IInteractPopupProps { |
||||
|
|
||||
|
value?: any |
||||
|
onChange?: (value: any) => void |
||||
|
|
||||
|
onLoad?: (ref: InteractPopupRef) => Promise<any> |
||||
|
|
||||
|
footerExtends?: ReactNode | JSX.Element | (() => ReactNode | JSX.Element) |
||||
|
|
||||
|
target?: false | ReactNode | JSX.Element | (() => ReactNode | JSX.Element) |
||||
|
type: 'drawer' | 'modal', |
||||
|
open?: boolean |
||||
|
afterOpenChange?: (open: boolean) => void |
||||
|
closeText?: string |
||||
|
okText?: string |
||||
|
onClose?: () => void | boolean |
||||
|
onOk?: (value: any, ref: InteractPopupRef) => Promise<void | boolean> |
||||
|
width?: ModalProps['width'] |
||||
|
title: ModalProps['title'] |
||||
|
styles?: ModalProps['styles'] |
||||
|
|
||||
|
//内核modal或drawer的props传透
|
||||
|
typeProps?: DrawerProps | ModalProps |
||||
|
|
||||
|
children?: ReactNode | JSX.Element | ((value: any) => ReactNode | JSX.Element) |
||||
|
} |
||||
|
|
||||
|
type InteractPopupRef = { |
||||
|
open: () => void |
||||
|
close: () => void |
||||
|
submitting: boolean |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 交互式弹层 |
||||
|
*/ |
||||
|
const InteractPopup = forwardRef<InteractPopupRef, IInteractPopupProps>(( |
||||
|
{ |
||||
|
title, |
||||
|
width, |
||||
|
footerExtends, |
||||
|
target: propTarget, |
||||
|
type = 'drawer', |
||||
|
open: propOpen, |
||||
|
afterOpenChange, |
||||
|
children, |
||||
|
value: propValue, |
||||
|
onChange: propOnChange, |
||||
|
okText, closeText, |
||||
|
onClose, onOk, onLoad, |
||||
|
styles: propStyles, |
||||
|
typeProps, |
||||
|
}: IInteractPopupProps, ref) => { |
||||
|
|
||||
|
const { styles } = useStyle() |
||||
|
const [ value, setValue ] = useState(() => propValue) |
||||
|
const [ open, setOpen ] = useState(() => propOpen || false) |
||||
|
const target = useMemo(() => { |
||||
|
if (propTarget) { |
||||
|
if (typeof propTarget === 'function') { |
||||
|
return propTarget() |
||||
|
} |
||||
|
return !(propTarget as boolean) ? null : t(`actions.open`, '打开') |
||||
|
} |
||||
|
return null |
||||
|
}, [ propTarget ]) |
||||
|
const [ loading, setLoading ] = useState(false) |
||||
|
const [ submitting, setSubmitting ] = useState(false) |
||||
|
|
||||
|
|
||||
|
const refValue = useMemo(() => { |
||||
|
return { |
||||
|
open() { |
||||
|
setOpen(true) |
||||
|
}, |
||||
|
close() { |
||||
|
setOpen(false) |
||||
|
}, |
||||
|
get submitting() { |
||||
|
return submitting |
||||
|
}, |
||||
|
set submitting(val: boolean) { |
||||
|
setSubmitting(val) |
||||
|
} |
||||
|
} |
||||
|
}, [ setOpen, submitting, setSubmitting ]) |
||||
|
|
||||
|
useImperativeHandle(ref, () => { |
||||
|
return refValue |
||||
|
}) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (typeof propOpen === 'boolean') |
||||
|
setOpen(propOpen) |
||||
|
}, [ propOpen ]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
setValue(propValue) |
||||
|
}, [ propValue ]) |
||||
|
|
||||
|
const onChange = useCallback((val: any) => { |
||||
|
propOnChange?.(val) |
||||
|
|
||||
|
}, [ propOnChange ]) |
||||
|
|
||||
|
const Wrap = type === 'drawer' ? Drawer : Modal |
||||
|
|
||||
|
const renderFooter = useCallback(() => { |
||||
|
const extFooter = typeof footerExtends === 'function' ? footerExtends() : footerExtends |
||||
|
return <div className={styles.footer}> |
||||
|
<div className={'extends'}>{extFooter}</div> |
||||
|
<Space align={'end'} className={'actions'}> |
||||
|
<Button type={'default'} |
||||
|
key={'btn-cancel'} |
||||
|
onClick={() => { |
||||
|
if (onClose?.() === false) { |
||||
|
return |
||||
|
} |
||||
|
setOpen(false) |
||||
|
}} |
||||
|
>{closeText || t(`actions.cancel`)}</Button> |
||||
|
<Button type={'primary'} |
||||
|
loading={submitting} |
||||
|
onClick={async () => { |
||||
|
if (onOk) { |
||||
|
setSubmitting(true) |
||||
|
try { |
||||
|
const res = await onOk(value, refValue) |
||||
|
setSubmitting(false) |
||||
|
if (res === false) { |
||||
|
return |
||||
|
} |
||||
|
setValue(value) |
||||
|
onChange(value) |
||||
|
} catch (e) { |
||||
|
setSubmitting(false) |
||||
|
} |
||||
|
} |
||||
|
setOpen(false) |
||||
|
}} |
||||
|
key={'btn-ok'}>{okText || t(`actions.ok`)}</Button> |
||||
|
</Space> |
||||
|
</div> |
||||
|
|
||||
|
}, [ styles, footerExtends, setOpen, submitting, value, onOk, onClose, okText, closeText, refValue ]) |
||||
|
|
||||
|
const renderChildren = () => { |
||||
|
if (typeof children === 'function') { |
||||
|
return children(value) |
||||
|
} |
||||
|
return children |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className={styles.container}> |
||||
|
<span onClick={async () => { |
||||
|
if (onLoad) { |
||||
|
setLoading(true) |
||||
|
const res = await onLoad(refValue) |
||||
|
setLoading(false) |
||||
|
setValue(res) |
||||
|
} |
||||
|
setOpen(true) |
||||
|
}}>{target}</span> |
||||
|
<Wrap |
||||
|
title={title} |
||||
|
footer={renderFooter()} |
||||
|
width={width} |
||||
|
styles={propStyles} |
||||
|
{...typeProps as any} |
||||
|
loading={loading} |
||||
|
open={open} |
||||
|
onClose={() => { |
||||
|
setOpen(false) |
||||
|
}} |
||||
|
onCancel={() => { |
||||
|
setOpen(false) |
||||
|
}} |
||||
|
afterOpenChange={afterOpenChange} |
||||
|
rootClassName={styles.container} |
||||
|
|
||||
|
> |
||||
|
{renderChildren()} |
||||
|
</Wrap> |
||||
|
</div> |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
export default memo(InteractPopup) |
@ -0,0 +1,40 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
import { useScrollStyle } from '@/hooks/useScrollStyle.ts' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-portal-form-component` |
||||
|
const { scrollbarBackground } = useScrollStyle() |
||||
|
|
||||
|
const container = css`
|
||||
|
|
||||
|
|
||||
|
.ant-modal-body{ |
||||
|
${scrollbarBackground} |
||||
|
} |
||||
|
.ant-drawer-body{ |
||||
|
|
||||
|
${scrollbarBackground} |
||||
|
} |
||||
|
|
||||
|
`
|
||||
|
|
||||
|
const footer = css`
|
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
|
||||
|
gap: 10px; |
||||
|
|
||||
|
.extends { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.actions { |
||||
|
|
||||
|
} |
||||
|
`
|
||||
|
return { |
||||
|
container: cx(prefix, props?.className, container), |
||||
|
footer, |
||||
|
} |
||||
|
}) |
@ -0,0 +1,195 @@ |
|||||
|
import { useStyle } from './style' |
||||
|
import { Form, Layout, Menu, Row, Spin } from 'antd' |
||||
|
import { t } from '@/i18n' |
||||
|
import { ReactNode, useCallback, useMemo, useState } from 'react' |
||||
|
import PrimaryFacts from './form/PrimaryFacts.tsx' |
||||
|
import { EditProvide } from './context.ts' |
||||
|
import { FormInstance } from 'antd/lib' |
||||
|
import { BetaSchemaForm, ProFormColumnsType } from '@ant-design/pro-components' |
||||
|
|
||||
|
export interface IEditProps<T = Record<string, any>> { |
||||
|
record: T |
||||
|
form: FormInstance |
||||
|
} |
||||
|
|
||||
|
const i18nPrefix = `db.movie.form` |
||||
|
|
||||
|
const items = [ |
||||
|
{ label: t(`${i18nPrefix}.nav.primary_facts`, '基本信息'), key: 'primary_facts' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.alternative_titles`, '别名'), key: 'alternative_titles' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.cast`, '演员'), key: 'cast' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.crew`, '工作人员'), key: 'crew' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.external_ids`, '外部编号'), key: 'external_ids' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.genres`, '类型'), key: 'genres' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.keywords`, '关键词'), key: 'keywords' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.production_information`, '影片信息'), key: 'production_information' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.release_information`, '发行信息'), key: 'release_information' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.taglines`, '标语'), key: 'taglines' }, |
||||
|
{ label: t(`${i18nPrefix}.nav.videos`, '视频'), key: 'videos' }, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
const Edit = <T = Record<string, any>>(props: IEditProps<T>) => { |
||||
|
|
||||
|
const { styles } = useStyle() |
||||
|
const [ activeKey, setActiveKey ] = useState<string>('primary_facts') |
||||
|
const [ spinning, setSpinning ] = useState(false) |
||||
|
|
||||
|
const columns = useMemo(() => { |
||||
|
|
||||
|
return { |
||||
|
|
||||
|
primary_facts: [ |
||||
|
|
||||
|
{ |
||||
|
dataIndex: 'original_language', |
||||
|
title: t(`${i18nPrefix}.column.original_language`, '原电影语言'), |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'origin_country', |
||||
|
title: t(`${i18nPrefix}.column.origin_country`, '原始国家或地区'), |
||||
|
colProps: { span: 16 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'original_title', |
||||
|
title: t(`${i18nPrefix}.column.original_title`, '原产地片名'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'title', |
||||
|
title: t(`${i18nPrefix}.column.title`, '所选地区语言的片名 (汉语)'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'overview', |
||||
|
title: t(`${i18nPrefix}.column.overview`, '所选地区语言的剧情简介 (汉语)'), |
||||
|
valueType: 'textarea', |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'status', |
||||
|
title: t(`${i18nPrefix}.column.status`, '电影状态'), |
||||
|
valueType: 'select', |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'adult', |
||||
|
title: t(`${i18nPrefix}.column.adult`, '是否为成人电影?'), |
||||
|
valueType: 'select', |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'video', |
||||
|
title: t(`${i18nPrefix}.column.video`, '视频'), |
||||
|
valueType: 'select', |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'runtime', |
||||
|
title: t(`${i18nPrefix}.column.runtime`, '时长'), |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'revenue', |
||||
|
title: t(`${i18nPrefix}.column.revenue`, '票房 (US Dollars)'), |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'budget', |
||||
|
title: t(`${i18nPrefix}.column.budget`, '预算 (US Dollars)'), |
||||
|
colProps: { span: 8 }, |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'homepage', |
||||
|
title: t(`${i18nPrefix}.column.homepage`, '主页'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'spoken_languages', |
||||
|
title: t(`${i18nPrefix}.column.spoken_languages`, '原声对白语言'), |
||||
|
valueType: 'select', |
||||
|
}, |
||||
|
|
||||
|
/*{ |
||||
|
dataIndex: 'backdrop_path', |
||||
|
title: t(`${i18nPrefix}.column.backdrop_path`, '背景图片路径'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'belongs_to_collection', |
||||
|
title: t(`${i18nPrefix}.column.belongs_to_collection`, '所属系列'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'genres', |
||||
|
title: t(`${i18nPrefix}.column.genres`, '类型'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'id', |
||||
|
title: t(`${i18nPrefix}.column.id`, 'ID'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'imdb_id', |
||||
|
title: t(`${i18nPrefix}.column.imdb_id`, 'IMDB ID'), |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
dataIndex: 'popularity', |
||||
|
title: t(`${i18nPrefix}.column.popularity`, '人气'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'poster_path', |
||||
|
title: t(`${i18nPrefix}.column.poster_path`, '海报路径'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'production_companies', |
||||
|
title: t(`${i18nPrefix}.column.production_companies`, '制作公司'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'production_countries', |
||||
|
title: t(`${i18nPrefix}.column.production_countries`, '制作国家'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'release_date', |
||||
|
title: t(`${i18nPrefix}.column.release_date`, '上映日期'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'tagline', |
||||
|
title: t(`${i18nPrefix}.column.tagline`, '标语'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'vote_average', |
||||
|
title: t(`${i18nPrefix}.column.vote_average`, '平均评分'), |
||||
|
}, |
||||
|
{ |
||||
|
dataIndex: 'vote_count', |
||||
|
title: t(`${i18nPrefix}.column.vote_count`, '评分人数'), |
||||
|
},*/ |
||||
|
] |
||||
|
} as Record<string, ProFormColumnsType[]> |
||||
|
|
||||
|
}, []) |
||||
|
|
||||
|
const renderFormColumns = () => { |
||||
|
return columns[activeKey] || [] |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Layout className={styles.container} hasSider={true}> |
||||
|
|
||||
|
<Layout.Sider theme={'light'} className={styles.sider} > |
||||
|
<Menu items={items} |
||||
|
selectedKeys={[ activeKey ]} |
||||
|
onSelect={({ key }) => { |
||||
|
setActiveKey(key) |
||||
|
}}/> |
||||
|
</Layout.Sider> |
||||
|
<Layout.Content className={styles.body}> |
||||
|
<Spin spinning={spinning}> |
||||
|
<BetaSchemaForm |
||||
|
grid={true} |
||||
|
submitter={false} |
||||
|
columns={renderFormColumns()} |
||||
|
/> |
||||
|
</Spin> |
||||
|
</Layout.Content> |
||||
|
</Layout> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Edit |
@ -0,0 +1,4 @@ |
|||||
|
import React from 'react' |
||||
|
|
||||
|
|
||||
|
export const EditProvide = React.createContext<any | null>(null) |
@ -0,0 +1,21 @@ |
|||||
|
import { useContext, useMemo, useRef } from 'react' |
||||
|
import { EditProvide } from '../context.ts' |
||||
|
|
||||
|
const PrimaryFacts = () => { |
||||
|
|
||||
|
const context = useContext(EditProvide) |
||||
|
const formRef = useRef() |
||||
|
|
||||
|
const columns = useMemo(() => { |
||||
|
|
||||
|
return [] |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
|
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default PrimaryFacts |
@ -0,0 +1,70 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
import { useScrollStyle } from '@/hooks/useScrollStyle' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-edit-component` |
||||
|
const { scrollbarBackground } = useScrollStyle() |
||||
|
const container = css`
|
||||
|
|
||||
|
--heaer-height: 57px; |
||||
|
--footer-height: 50px; |
||||
|
|
||||
|
position: relative!important; |
||||
|
|
||||
|
.ant-layout-sider-children{ |
||||
|
//display: flex;
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
.ant-menu{ |
||||
|
|
||||
|
border-inline-end: none!important; |
||||
|
} |
||||
|
|
||||
|
.ant-menu .ant-menu-item, .ant-menu .ant-menu-submenu, .ant-menu .ant-menu-submenu-title { |
||||
|
border-radius: 0; |
||||
|
margin: 0; |
||||
|
width: 100%; |
||||
|
flex: 1; |
||||
|
min-width: 200px; |
||||
|
} |
||||
|
|
||||
|
.ant-drawer-content{ |
||||
|
--heaer-height: 57px; |
||||
|
--footer-height: 50px; |
||||
|
} |
||||
|
|
||||
|
.ant-modal-content{ |
||||
|
--heaer-height: 57px; |
||||
|
--footer-height: 50px; |
||||
|
} |
||||
|
|
||||
|
`
|
||||
|
const sider = css`
|
||||
|
.ant-layout-sider-children { |
||||
|
display: flex; |
||||
|
overflow-y: auto; |
||||
|
overflow-x: hidden; |
||||
|
position: fixed !important; |
||||
|
//top: 0;
|
||||
|
//bottom: 0;
|
||||
|
border-inline-end: 1px solid rgba(5, 5, 5, 0.06); |
||||
|
height: calc(100% - var(--heaer-height, 0) - var(--footer-height, 0)); |
||||
|
|
||||
|
${scrollbarBackground} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
`
|
||||
|
|
||||
|
const body = css`
|
||||
|
padding: 30px; |
||||
|
flex: 1; |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix, props?.className, container), |
||||
|
body, |
||||
|
sider, |
||||
|
} |
||||
|
}) |
@ -0,0 +1,280 @@ |
|||||
|
import { t, useTranslation } from '@/i18n.ts' |
||||
|
import { Button, Form, Popconfirm, Divider, Space, Tooltip, Badge, Layout, Menu, Spin } from 'antd' |
||||
|
import { useAtom, useAtomValue } from 'jotai' |
||||
|
import { |
||||
|
deleteMovieAtom, |
||||
|
saveOrUpdateMovieAtom, movieAtom, moviesAtom, movieSearchAtom, |
||||
|
} from '@/store/db/movie' |
||||
|
import React, { useEffect, useMemo, useState } from 'react' |
||||
|
import Action from '@/components/action/Action.tsx' |
||||
|
import { |
||||
|
BetaSchemaForm, |
||||
|
ProColumns, ProForm, |
||||
|
ProFormColumnsType, |
||||
|
} from '@ant-design/pro-components' |
||||
|
import ListPageLayout from '@/layout/ListPageLayout.tsx' |
||||
|
import { useStyle } from './style' |
||||
|
import { ExportOutlined, FilterOutlined } from '@ant-design/icons' |
||||
|
import { getValueCount } from '@/utils' |
||||
|
import { Table as ProTable } from '@/components/table' |
||||
|
import InteractPopup from '@/components/interact-popup' |
||||
|
import Edit from './components/Edit.tsx' |
||||
|
|
||||
|
const i18nPrefix = 'movies.list' |
||||
|
|
||||
|
|
||||
|
const Movie = () => { |
||||
|
|
||||
|
const { styles, cx } = useStyle() |
||||
|
const { t } = useTranslation() |
||||
|
const [ form ] = Form.useForm() |
||||
|
const [ filterForm ] = Form.useForm() |
||||
|
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateMovieAtom) |
||||
|
const [ search, setSearch ] = useAtom(movieSearchAtom) |
||||
|
const [ currentMovie, setMovie ] = useAtom(movieAtom) |
||||
|
const { data, isFetching, isLoading, refetch } = useAtomValue(moviesAtom) |
||||
|
const { mutate: deleteMovie, isPending: isDeleting } = useAtomValue(deleteMovieAtom) |
||||
|
|
||||
|
const [ open, setOpen ] = useState(false) |
||||
|
const [ openFilter, setFilterOpen ] = useState(false) |
||||
|
const [ searchKey, setSearchKey ] = useState(search?.title) |
||||
|
|
||||
|
const columns = useMemo(() => { |
||||
|
return [ |
||||
|
{ |
||||
|
title: 'ID', |
||||
|
dataIndex: 'id', |
||||
|
hideInTable: true, |
||||
|
hideInSearch: true, |
||||
|
formItemProps: { hidden: true } |
||||
|
}, |
||||
|
{ |
||||
|
title: t(`${i18nPrefix}.columns.name`, 'name'), |
||||
|
dataIndex: 'name', |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
title: t(`${i18nPrefix}.columns.description`, 'description'), |
||||
|
dataIndex: 'description', |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
title: t(`${i18nPrefix}.columns.updated_at`, 'updated_at'), |
||||
|
dataIndex: 'updated_at', |
||||
|
}, |
||||
|
|
||||
|
{ |
||||
|
title: t(`${i18nPrefix}.columns.option`, '操作'), |
||||
|
key: 'option', |
||||
|
valueType: 'option', |
||||
|
fixed: 'right', |
||||
|
render: (_, record) => [ |
||||
|
<Action key="edit" |
||||
|
as={'a'} |
||||
|
onClick={() => { |
||||
|
form.setFieldsValue(record) |
||||
|
setMovie(record) |
||||
|
setOpen(true) |
||||
|
}}>{t('actions.edit')}</Action>, |
||||
|
<Popconfirm |
||||
|
key={'del_confirm'} |
||||
|
disabled={isDeleting} |
||||
|
onConfirm={() => { |
||||
|
deleteMovie([ record.id ]) |
||||
|
}} |
||||
|
title={t('message.deleteConfirm')}> |
||||
|
<a key="del"> |
||||
|
{t('actions.delete', '删除')} |
||||
|
</a> |
||||
|
</Popconfirm> |
||||
|
] |
||||
|
} |
||||
|
] as ProColumns[] |
||||
|
}, [ isDeleting, currentMovie, search ]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
|
||||
|
setSearchKey(search?.title) |
||||
|
filterForm.setFieldsValue(search) |
||||
|
|
||||
|
}, [ search ]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (isSuccess) { |
||||
|
setOpen(false) |
||||
|
} |
||||
|
}, [ isSuccess ]) |
||||
|
|
||||
|
return ( |
||||
|
<ListPageLayout className={styles.container}> |
||||
|
<ProTable |
||||
|
rowKey="id" |
||||
|
headerTitle={t(`${i18nPrefix}.title`, '影视管理')} |
||||
|
toolbar={{ |
||||
|
search: { |
||||
|
loading: isFetching && !!search?.title, |
||||
|
onSearch: (value: string) => { |
||||
|
setSearch(prev => ({ |
||||
|
...prev, |
||||
|
title: value |
||||
|
})) |
||||
|
}, |
||||
|
allowClear: true, |
||||
|
onChange: (e) => { |
||||
|
setSearchKey(e.target?.value) |
||||
|
}, |
||||
|
value: searchKey, |
||||
|
placeholder: t(`${i18nPrefix}.placeholder`, '输入影视管理名称') |
||||
|
}, |
||||
|
actions: [ |
||||
|
<Tooltip key={'filter'} title={t(`${i18nPrefix}.filter.tooltip`, '高级查询')}> |
||||
|
<Badge count={getValueCount(search)}> |
||||
|
<Button |
||||
|
onClick={() => { |
||||
|
setFilterOpen(true) |
||||
|
}} |
||||
|
icon={<FilterOutlined/>} shape={'circle'} size={'small'}/> |
||||
|
</Badge> |
||||
|
</Tooltip>, |
||||
|
<Divider type={'vertical'} key={'divider'}/>, |
||||
|
<Button key={'add'} |
||||
|
onClick={() => { |
||||
|
form.resetFields() |
||||
|
form.setFieldsValue({ |
||||
|
id: 0, |
||||
|
}) |
||||
|
setOpen(true) |
||||
|
}} |
||||
|
type={'primary'}>{t(`${i18nPrefix}.add`, '添加')}</Button> |
||||
|
] |
||||
|
}} |
||||
|
scroll={{ |
||||
|
x: 2500, y: 'calc(100vh - 290px)' |
||||
|
}} |
||||
|
search={false} |
||||
|
onRow={(record) => { |
||||
|
return { |
||||
|
className: cx({ |
||||
|
// 'ant-table-row-selected': currentMovie?.id === record.id
|
||||
|
}), |
||||
|
onClick: () => { |
||||
|
setMovie(record) |
||||
|
} |
||||
|
} |
||||
|
}} |
||||
|
dateFormatter="string" |
||||
|
loading={isLoading || isFetching} |
||||
|
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, |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
}} |
||||
|
/> |
||||
|
|
||||
|
<InteractPopup |
||||
|
title={t(`${i18nPrefix}.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '影视管理编辑' : '影视管理添加')} |
||||
|
open={open} |
||||
|
afterOpenChange={open => { |
||||
|
setOpen(open) |
||||
|
}} |
||||
|
styles={{ |
||||
|
body: { padding: 0} |
||||
|
}} |
||||
|
type={'drawer'} |
||||
|
width={'90%'} |
||||
|
typeProps={{ |
||||
|
maskClosable: false, |
||||
|
}} |
||||
|
> |
||||
|
<Edit record={currentMovie} form={form} /> |
||||
|
|
||||
|
</InteractPopup> |
||||
|
|
||||
|
<BetaSchemaForm |
||||
|
title={t(`${i18nPrefix}.filter.title`, '影视管理高级查询')} |
||||
|
grid={true} |
||||
|
shouldUpdate={false} |
||||
|
width={500} |
||||
|
form={filterForm} |
||||
|
open={openFilter} |
||||
|
onOpenChange={open => { |
||||
|
setFilterOpen(open) |
||||
|
}} |
||||
|
layout={'vertical'} |
||||
|
scrollToFirstError={true} |
||||
|
layoutType={'DrawerForm'} |
||||
|
drawerProps={{ |
||||
|
maskClosable: false, |
||||
|
mask: false, |
||||
|
}} |
||||
|
submitter={{ |
||||
|
searchConfig: { |
||||
|
resetText: t(`${i18nPrefix}.filter.reset`, '清空'), |
||||
|
submitText: t(`${i18nPrefix}.filter.submit`, '查询'), |
||||
|
}, |
||||
|
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> |
||||
|
) |
||||
|
}, |
||||
|
|
||||
|
}} |
||||
|
onValuesChange={(values) => { |
||||
|
|
||||
|
}} |
||||
|
|
||||
|
onFinish={async (values) => { |
||||
|
//处理,变成数组
|
||||
|
Object.keys(values).forEach(key => { |
||||
|
if (typeof values[key] === 'string' && values[key].includes(',')) { |
||||
|
values[key] = values[key].split(',') |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
setSearch(values) |
||||
|
|
||||
|
}} |
||||
|
columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> |
||||
|
</ListPageLayout> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Movie |
@ -0,0 +1,29 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-movie-list-page` |
||||
|
|
||||
|
const container = css`
|
||||
|
.ant-table-cell { |
||||
|
.ant-tag { |
||||
|
padding-inline: 3px; |
||||
|
margin-inline-end: 3px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ant-table-empty { |
||||
|
.ant-table-body { |
||||
|
height: calc(100vh - 350px) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ant-pro-table-highlight { |
||||
|
|
||||
|
} |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix, props?.className, container), |
||||
|
|
||||
|
} |
||||
|
}) |
@ -0,0 +1,9 @@ |
|||||
|
import { createCURD, createMockCURD } from '@/service/base.ts' |
||||
|
import { DB } from '@/types/db/movie' |
||||
|
|
||||
|
const movie = { |
||||
|
...createCURD<any, DB.IMovie>('/db/movie'), |
||||
|
...createMockCURD<any, DB.IMovie>('/db/movie') |
||||
|
} |
||||
|
|
||||
|
export default movie |
@ -0,0 +1,90 @@ |
|||||
|
import { atom } from 'jotai' |
||||
|
import { IApiResult, IPage } from '@/global' |
||||
|
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query' |
||||
|
import { message } from 'antd' |
||||
|
import { t } from 'i18next' |
||||
|
import { DB } from '@/types/db/movie' |
||||
|
import dBServ from '@/service/db/movie' |
||||
|
|
||||
|
type SearchParams = IPage & { |
||||
|
key?: string |
||||
|
|
||||
|
[key: string]: any |
||||
|
} |
||||
|
|
||||
|
export const movieIdAtom = atom(0) |
||||
|
|
||||
|
export const movieIdsAtom = atom<number[]>([]) |
||||
|
|
||||
|
export const movieAtom = atom<DB.IMovie>(undefined as unknown as DB.IMovie ) |
||||
|
|
||||
|
export const movieSearchAtom = atom<SearchParams>({ |
||||
|
key: '', |
||||
|
pageSize: 10, |
||||
|
page: 1, |
||||
|
} as SearchParams) |
||||
|
|
||||
|
export const moviePageAtom = atom<IPage>({ |
||||
|
pageSize: 10, |
||||
|
page: 1, |
||||
|
}) |
||||
|
|
||||
|
export const moviesAtom = atomWithQuery((get) => { |
||||
|
return { |
||||
|
queryKey: [ 'movies', get(movieSearchAtom) ], |
||||
|
queryFn: async ({ queryKey: [ , params ] }) => { |
||||
|
return await dBServ.list(params as SearchParams) |
||||
|
}, |
||||
|
select: res => { |
||||
|
const data = res.data |
||||
|
data.rows = data.rows?.map(row => { |
||||
|
return { |
||||
|
...row, |
||||
|
//status: convertToBool(row.status)
|
||||
|
} |
||||
|
}) |
||||
|
return data |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
//saveOrUpdateAtom
|
||||
|
export const saveOrUpdateMovieAtom = atomWithMutation<IApiResult, DB.IMovie>((get) => { |
||||
|
|
||||
|
return { |
||||
|
mutationKey: [ 'updateMovie' ], |
||||
|
mutationFn: async (data) => { |
||||
|
//data.status = data.status ? '1' : '0'
|
||||
|
if (data.id === 0) { |
||||
|
return await dBServ.add(data) |
||||
|
} |
||||
|
return await dBServ.update(data) |
||||
|
}, |
||||
|
onSuccess: (res) => { |
||||
|
const isAdd = !!res.data?.id |
||||
|
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功')) |
||||
|
|
||||
|
//更新列表
|
||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
// @ts-ignore fix
|
||||
|
get(queryClientAtom).invalidateQueries({ queryKey: [ 'movies', get(movieSearchAtom) ] }) |
||||
|
|
||||
|
return res |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
export const deleteMovieAtom = atomWithMutation((get) => { |
||||
|
return { |
||||
|
mutationKey: [ 'deleteMovie' ], |
||||
|
mutationFn: async (ids: number[]) => { |
||||
|
return await dBServ.batchDelete(ids ?? get(movieIdsAtom)) |
||||
|
}, |
||||
|
onSuccess: (res) => { |
||||
|
message.success('message.deleteSuccess') |
||||
|
//更新列表
|
||||
|
get(queryClientAtom).invalidateQueries({ queryKey: [ 'movies', get(movieSearchAtom) ] }) |
||||
|
return res |
||||
|
} |
||||
|
} |
||||
|
}) |
@ -0,0 +1,11 @@ |
|||||
|
export namespace DB { |
||||
|
export interface IMovie { |
||||
|
id: number; |
||||
|
name: string; |
||||
|
description: string; |
||||
|
created_at: string; |
||||
|
created_by: number; |
||||
|
updated_at: string; |
||||
|
updated_by: number; |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue