From 2ee2dbdfabb30af06229bb0c141be6dd5d4acf4f Mon Sep 17 00:00:00 2001 From: dark Date: Sun, 11 Aug 2024 17:50:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BD=B1=E8=A7=86=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/components/crazy-form/context.ts | 21 ++ src/components/crazy-form/index.tsx | 222 ++++++++++++ src/components/crazy-form/tabs-form/TabForm.tsx | 80 +++++ src/components/crazy-form/tabs-form/index.tsx | 375 +++++++++++++++++++++ src/components/crazy-form/typeing.d.ts | 74 ++++ src/components/interact-popup/index.tsx | 194 +++++++++++ src/components/interact-popup/style.ts | 40 +++ src/components/log-vewr/index.tsx | 0 src/pages/db/movie/components/Edit.tsx | 195 +++++++++++ src/pages/db/movie/components/context.ts | 4 + .../db/movie/components/form/PrimaryFacts.tsx | 21 ++ src/pages/db/movie/components/style.ts | 70 ++++ src/pages/db/movie/index.tsx | 280 +++++++++++++++ src/pages/db/movie/style.ts | 29 ++ src/service/base.ts | 111 +++++- src/service/db/movie.ts | 9 + src/store/db/movie.ts | 90 +++++ src/types/db/movie.d.ts | 11 + 19 files changed, 1826 insertions(+), 1 deletion(-) create mode 100644 src/components/crazy-form/context.ts create mode 100644 src/components/crazy-form/index.tsx create mode 100644 src/components/crazy-form/tabs-form/TabForm.tsx create mode 100644 src/components/crazy-form/tabs-form/index.tsx create mode 100644 src/components/crazy-form/typeing.d.ts create mode 100644 src/components/interact-popup/index.tsx create mode 100644 src/components/interact-popup/style.ts create mode 100644 src/components/log-vewr/index.tsx create mode 100644 src/pages/db/movie/components/Edit.tsx create mode 100644 src/pages/db/movie/components/context.ts create mode 100644 src/pages/db/movie/components/form/PrimaryFacts.tsx create mode 100644 src/pages/db/movie/components/style.ts create mode 100644 src/pages/db/movie/index.tsx create mode 100644 src/pages/db/movie/style.ts create mode 100644 src/service/db/movie.ts create mode 100644 src/store/db/movie.ts create mode 100644 src/types/db/movie.d.ts diff --git a/package.json b/package.json index e28df5e..1fed9a6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@formily/core": "^2.3.1", "@formily/react": "^2.3.1", "@icon-park/react": "^1.4.2", + "@melloware/react-logviewer": "^5.2.0", "@tanstack/query-core": "^5.29.0", "@tanstack/react-query": "^5.29.2", "@tanstack/react-router": "^1.26.20", diff --git a/src/components/crazy-form/context.ts b/src/components/crazy-form/context.ts new file mode 100644 index 0000000..753f485 --- /dev/null +++ b/src/components/crazy-form/context.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) => void; + unRegForm: (name: string) => void; + onFormFinish: (name: string, formData: any) => void; + keyArray: string[]; + formArrayRef: React.MutableRefObject< + React.MutableRefObject | undefined>[] + >; + loading: boolean; + setLoading: (loading: boolean) => void; + formMapRef: React.MutableRefObject>; +} + | undefined +>(undefined) + diff --git a/src/components/crazy-form/index.tsx b/src/components/crazy-form/index.tsx new file mode 100644 index 0000000..95b3d99 --- /dev/null +++ b/src/components/crazy-form/index.tsx @@ -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 = { + layoutType: FormSchema['layoutType'] | 'TabsForm' +} & Omit, 'layout'> + + +const FormLayoutType = { + DrawerForm, + QueryFilter, + LightFilter, StepForm: ProStepsForm.StepForm, + + StepsForm: StepsForm, + ModalForm, + Embed, + Form: ProForm, + TabsForm, +} + +const CrazyBateForm = (props: CrazyBateFormProps) => { + + 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> + + const [ form ] = Form.useForm() + const formInstance = Form.useFormInstance() + + const [ , forceUpdate ] = useState<[]>([]) + const [ formDomsDeps, updatedFormDoms ] = useState<[]>(() => []) + + const formRef = useReactiveRef( + props.form || formInstance || form, + ) + const oldValuesRef = useRef() + const propsRef = useLatest(props) + + /** + * 生成子项,方便被 table 接入 + * + * @param items + */ + const genItems: ProFormRenderValueTypeHelpers['genItems'] = + useRefFunction((items: ProFormColumnsType[]) => { + 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', + , + ) + + 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 + + return renderValueType(item, { + action, + type, + originItem, + formRef, + genItems, + }) + }) + .filter((field) => { + return Boolean(field) + }) + }) + + const onValuesChange: FormProps['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[]) + // 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[][], + } + } + + return {} + }, [ columns, layoutType ]) + + useImperativeHandle( + propsFormRef, + () => { + return formRef.current + }, + [ formRef.current ], + ) + + return ( + { + if (propsFormRef) { + (propsFormRef as React.MutableRefObject>).current = + initForm + } + restProps?.onInit?.(_, initForm) + formRef.current = initForm + }} + form={props.form || form} + formRef={formRef} + onValuesChange={onValuesChange} + > + {formChildrenDoms} + + ) +} + +export default CrazyBateForm \ No newline at end of file diff --git a/src/components/crazy-form/tabs-form/TabForm.tsx b/src/components/crazy-form/tabs-form/TabForm.tsx new file mode 100644 index 0000000..8367e24 --- /dev/null +++ b/src/components/crazy-form/tabs-form/TabForm.tsx @@ -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> = { + tab?: string; + tabProps?: TabsProps; +} & CrazyChildFormProps + +const TabForm = >(props: TabFormProps) => { + + const formRef = useRef() + 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 ( + { + 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 \ No newline at end of file diff --git a/src/components/crazy-form/tabs-form/index.tsx b/src/components/crazy-form/tabs-form/index.tsx new file mode 100644 index 0000000..c810885 --- /dev/null +++ b/src/components/crazy-form/tabs-form/index.tsx @@ -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> = { + toggleProps: TabsProps + direction?: 'horizontal' | 'vertical' +} & Omit, 'toggleProps'> + +export const TabFormProvide = React.createContext | null>(null) + + +const TabsLayoutStrategy: Record< + string, + (dom: LayoutRenderDom) => React.ReactNode +> = { + horizontal({ toggleDom, formDom }) { + return ( + <> + + {toggleDom} + + + {formDom} + + + ) + }, + vertical({ stepsDom, formDom }) { + return ( + + + {React.cloneElement(stepsDom, { + style: { + height: '100%', + }, + })} + + +
+ {formDom} +
+ +
+ ) + }, +} + + +const TabsForm = >( + props: TabsFormProps & { + 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>()) + const formMapRef = useRef(new Map()) + const formArrayRef = useRef< + React.MutableRefObject | undefined>[] + >([]) + const [ formArray, setFormArray ] = useState([]) + const [ loading, setLoading ] = useState(false) + + /** + * 受控的方式来操作表单 + */ + const [ tab, setTab ] = useMergedState(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 ( +
+ + +
+ ) + }, [ formArray, tab, toggleProps, onCurrentChange ]) + + const onSubmit = useRefFunction(() => { + const from = formArrayRef.current[tab] + from.current?.submit() + }) + + const submit = useMemo(() => { + return ( + submitter !== false && ( + + ) + ) + }, [ 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 ( +
+ + {item} + +
+ ) + }) + }, [ 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( + () => ( +
+ {formDom} + {toggleFormRender ? null : {submitterDom}} +
+ ), + [ 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 ( +
+ + + {tabsFormDom} + + +
+ ) + +} + +function TabsFormWarp>( + props: CrazyFormProps & { + children: any; + }, +) { + return ( + + {...props} /> + + ) +} + +TabsFormWarp.TabForm = TabForm +TabsFormWarp.useForm = Form.useForm + +export { TabsFormWarp as TabsForm } +export type { TabFormProps, CrazyFormProps as TabsFormProps } \ No newline at end of file diff --git a/src/components/crazy-form/typeing.d.ts b/src/components/crazy-form/typeing.d.ts new file mode 100644 index 0000000..f3dd148 --- /dev/null +++ b/src/components/crazy-form/typeing.d.ts @@ -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> = { + + onFinish?: (values: T) => Promise; + current?: string; + /**、 + * 切换区域传透的Props + */ + toggleProps?: Record; + formProps?: ProFormProps; + onCurrentChange?: (current: string) => void; + /** 自定义步骤器 */ + toggleRender?: ( + items: { + key: string; + title?: React.ReactNode; + [key: string]: any + }[], + defaultDom: React.ReactNode, + ) => React.ReactNode; + /** @name 当前展示表单的 formRef */ + formRef?: React.MutableRefObject | undefined | null>; + /** @name 所有表单的 formMapRef */ + formMapRef?: React.MutableRefObject< + React.MutableRefObject | 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; + }> + | false; + + containerStyle?: React.CSSProperties; + /** + * 自定義整個佈局。 + * + * @param layoutDom toggleDom 和 formDom 元素可以放置在任何地方。 + */ + layoutRender?: (layoutDom: { + toggleDom: React.ReactElement; + formDom: React.ReactElement; + }) => React.ReactNode; +} & Omit; + + +export type CrazyChildFormProps, U = Record> = { + + index?: number; +} & Omit, 'onFinish' | 'form'> & + Omit, 'submitter' | 'form'>; \ No newline at end of file diff --git a/src/components/interact-popup/index.tsx b/src/components/interact-popup/index.tsx new file mode 100644 index 0000000..81167a1 --- /dev/null +++ b/src/components/interact-popup/index.tsx @@ -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 + + 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 + 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(( + { + 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
+
{extFooter}
+ + + + +
+ + }, [ styles, footerExtends, setOpen, submitting, value, onOk, onClose, okText, closeText, refValue ]) + + const renderChildren = () => { + if (typeof children === 'function') { + return children(value) + } + return children + } + + return ( +
+ { + if (onLoad) { + setLoading(true) + const res = await onLoad(refValue) + setLoading(false) + setValue(res) + } + setOpen(true) + }}>{target} + { + setOpen(false) + }} + onCancel={() => { + setOpen(false) + }} + afterOpenChange={afterOpenChange} + rootClassName={styles.container} + + > + {renderChildren()} + +
+ ) +}) + +export default memo(InteractPopup) \ No newline at end of file diff --git a/src/components/interact-popup/style.ts b/src/components/interact-popup/style.ts new file mode 100644 index 0000000..2c8a115 --- /dev/null +++ b/src/components/interact-popup/style.ts @@ -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, + } +}) \ No newline at end of file diff --git a/src/components/log-vewr/index.tsx b/src/components/log-vewr/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/db/movie/components/Edit.tsx b/src/pages/db/movie/components/Edit.tsx new file mode 100644 index 0000000..6b01a61 --- /dev/null +++ b/src/pages/db/movie/components/Edit.tsx @@ -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> { + 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 = >(props: IEditProps) => { + + const { styles } = useStyle() + const [ activeKey, setActiveKey ] = useState('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 + + }, []) + + const renderFormColumns = () => { + return columns[activeKey] || [] + } + + return ( + + + + { + setActiveKey(key) + }}/> + + + + + + + + ) +} + +export default Edit \ No newline at end of file diff --git a/src/pages/db/movie/components/context.ts b/src/pages/db/movie/components/context.ts new file mode 100644 index 0000000..4a1e0d4 --- /dev/null +++ b/src/pages/db/movie/components/context.ts @@ -0,0 +1,4 @@ +import React from 'react' + + +export const EditProvide = React.createContext(null) diff --git a/src/pages/db/movie/components/form/PrimaryFacts.tsx b/src/pages/db/movie/components/form/PrimaryFacts.tsx new file mode 100644 index 0000000..8c7a01b --- /dev/null +++ b/src/pages/db/movie/components/form/PrimaryFacts.tsx @@ -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 \ No newline at end of file diff --git a/src/pages/db/movie/components/style.ts b/src/pages/db/movie/components/style.ts new file mode 100644 index 0000000..367871a --- /dev/null +++ b/src/pages/db/movie/components/style.ts @@ -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, + } +}) \ No newline at end of file diff --git a/src/pages/db/movie/index.tsx b/src/pages/db/movie/index.tsx new file mode 100644 index 0000000..ab827ee --- /dev/null +++ b/src/pages/db/movie/index.tsx @@ -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) => [ + { + form.setFieldsValue(record) + setMovie(record) + setOpen(true) + }}>{t('actions.edit')}, + { + deleteMovie([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + ] + } + ] as ProColumns[] + }, [ isDeleting, currentMovie, search ]) + + useEffect(() => { + + setSearchKey(search?.title) + filterForm.setFieldsValue(search) + + }, [ search ]) + + useEffect(() => { + if (isSuccess) { + setOpen(false) + } + }, [ isSuccess ]) + + return ( + + { + setSearch(prev => ({ + ...prev, + title: value + })) + }, + allowClear: true, + onChange: (e) => { + setSearchKey(e.target?.value) + }, + value: searchKey, + placeholder: t(`${i18nPrefix}.placeholder`, '输入影视管理名称') + }, + actions: [ + + + + ] + }} + 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, + } + }) + }, + }} + /> + + { + setOpen(open) + }} + styles={{ + body: { padding: 0} + }} + type={'drawer'} + width={'90%'} + typeProps={{ + maskClosable: false, + }} + > + + + + + { + 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 ( +
+ + + + +
+ ) + }, + + }} + 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[]}/> +
+ ) +} + +export default Movie \ No newline at end of file diff --git a/src/pages/db/movie/style.ts b/src/pages/db/movie/style.ts new file mode 100644 index 0000000..0b108b1 --- /dev/null +++ b/src/pages/db/movie/style.ts @@ -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), + + } +}) \ No newline at end of file diff --git a/src/service/base.ts b/src/service/base.ts index e25ace4..b374a58 100644 --- a/src/service/base.ts +++ b/src/service/base.ts @@ -1,5 +1,5 @@ import { request, AxiosRequestConfig } from '@/request.ts' -import { IPage, IPageResult } from '@/global' +import { IApiResult, IPage, IPageResult } from '@/global' export const createCURD = (api: string, options?: AxiosRequestConfig) => { @@ -25,4 +25,113 @@ export const createCURD = (api: string, options?: AxiosRequest } } +} + +// 模拟的数据结果 + +export const createMockCURD = (api: string, options?: AxiosRequestConfig) => { + console.log(api, options) + return { + list: (params?: TParams & IPage): Promise>> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + data: { + rows: [ + { + id: 1, + name: 'Item 1', + description: 'Description 1', + } as unknown as TResult, + { + id: 2, + name: 'Item 2', + description: 'Description 2', + } as unknown as TResult, + ], + total: 2, + pageSize: params?.pageSize || 10, + page: params?.page || 1, + }, + message: 'success', + }) + }, 500) // 模拟网络延迟 + }) + }, + + add: (data: TParams): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + data: { + ...data, + id: Math.floor(Math.random() * 1000) + 1, // 随机生成一个ID + } as unknown as TResult, + message: 'Item added successfully', + }) + }, 500) + }) + }, + + update: (data: TParams): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + data: { + ...data, + updatedAt: new Date().toISOString(), // 更新的时间戳 + } as unknown as TResult, + message: 'Item updated successfully', + }) + }, 500) + }) + }, + + delete: (id: number): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + data: { + success: true, + } as unknown as TResult, + message: `Item with id ${id} deleted successfully.`, + }) + }, 500) + }) + }, + + batchDelete: (ids: number[]): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + data: { + success: true, + } as unknown as TResult, + message: `Items with ids ${ids.join(', ')} deleted successfully.`, + }) + }, 500) + }) + }, + + info: (id: number): Promise> => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + data: { + id: id, + name: `Item ${id}`, + description: `Description for item ${id}`, + } as unknown as TResult, + message: 'Item info retrieved successfully', + }) + }, 500) + }) + }, + } } \ No newline at end of file diff --git a/src/service/db/movie.ts b/src/service/db/movie.ts new file mode 100644 index 0000000..d573400 --- /dev/null +++ b/src/service/db/movie.ts @@ -0,0 +1,9 @@ +import { createCURD, createMockCURD } from '@/service/base.ts' +import { DB } from '@/types/db/movie' + +const movie = { + ...createCURD('/db/movie'), + ...createMockCURD('/db/movie') +} + +export default movie \ No newline at end of file diff --git a/src/store/db/movie.ts b/src/store/db/movie.ts new file mode 100644 index 0000000..d86454c --- /dev/null +++ b/src/store/db/movie.ts @@ -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([]) + +export const movieAtom = atom(undefined as unknown as DB.IMovie ) + +export const movieSearchAtom = atom({ + key: '', + pageSize: 10, + page: 1, +} as SearchParams) + +export const moviePageAtom = atom({ + 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((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 + } + } +}) diff --git a/src/types/db/movie.d.ts b/src/types/db/movie.d.ts new file mode 100644 index 0000000..0d89308 --- /dev/null +++ b/src/types/db/movie.d.ts @@ -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; + } +} \ No newline at end of file