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