Browse Source

增加影视模块

main
dark 1 month ago
parent
commit
2ee2dbdfab
  1. 1
      package.json
  2. 21
      src/components/crazy-form/context.ts
  3. 222
      src/components/crazy-form/index.tsx
  4. 80
      src/components/crazy-form/tabs-form/TabForm.tsx
  5. 375
      src/components/crazy-form/tabs-form/index.tsx
  6. 74
      src/components/crazy-form/typeing.d.ts
  7. 194
      src/components/interact-popup/index.tsx
  8. 40
      src/components/interact-popup/style.ts
  9. 0
      src/components/log-vewr/index.tsx
  10. 195
      src/pages/db/movie/components/Edit.tsx
  11. 4
      src/pages/db/movie/components/context.ts
  12. 21
      src/pages/db/movie/components/form/PrimaryFacts.tsx
  13. 70
      src/pages/db/movie/components/style.ts
  14. 280
      src/pages/db/movie/index.tsx
  15. 29
      src/pages/db/movie/style.ts
  16. 111
      src/service/base.ts
  17. 9
      src/service/db/movie.ts
  18. 90
      src/store/db/movie.ts
  19. 11
      src/types/db/movie.d.ts

1
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",

21
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<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)

222
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<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

80
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<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

375
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<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 }

74
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<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'>;

194
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<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)

40
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,
}
})

0
src/components/log-vewr/index.tsx

195
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<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

4
src/pages/db/movie/components/context.ts

@ -0,0 +1,4 @@
import React from 'react'
export const EditProvide = React.createContext<any | null>(null)

21
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

70
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,
}
})

280
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) => [
<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

29
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),
}
})

111
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 = <TParams, TResult>(api: string, options?: AxiosRequestConfig) => {
@ -25,4 +25,113 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
}
}
}
// 模拟的数据结果
export const createMockCURD = <TParams, TResult>(api: string, options?: AxiosRequestConfig) => {
console.log(api, options)
return {
list: (params?: TParams & IPage): Promise<IApiResult<IPageResult<TResult>>> => {
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<IApiResult<TResult>> => {
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<IApiResult<TResult>> => {
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<IApiResult<TResult>> => {
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<IApiResult<TResult>> => {
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<IApiResult<TResult>> => {
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)
})
},
}
}

9
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<any, DB.IMovie>('/db/movie'),
...createMockCURD<any, DB.IMovie>('/db/movie')
}
export default movie

90
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<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
}
}
})

11
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;
}
}
Loading…
Cancel
Save