Browse Source

重构x-form为组件,支持在自动渲染的基础上重写列配置

main
dark 4 weeks ago
parent
commit
7bd0e534e4
  1. 43
      .eslintrc.cjs
  2. 6
      package.json
  3. 2
      src/components/cascader/Cascader.tsx
  4. 21
      src/components/crazy-form/context.ts
  5. 222
      src/components/crazy-form/index.tsx
  6. 80
      src/components/crazy-form/tabs-form/TabForm.tsx
  7. 375
      src/components/crazy-form/tabs-form/index.tsx
  8. 74
      src/components/crazy-form/typeing.d.ts
  9. 319
      src/components/x-form/index.tsx
  10. 13
      src/components/x-form/style.ts
  11. 155
      src/components/x-form/utils/index.tsx
  12. 19
      src/context.ts
  13. 0
      src/hooks/useNavigate.ts
  14. 2
      src/layout/EmptyLayout.tsx
  15. 31
      src/layout/RootLayout.tsx
  16. 2
      src/pages/app/package/index.tsx
  17. 16
      src/pages/system/menus/index.tsx
  18. 3
      src/pages/system/roles/index.tsx
  19. 20
      src/pages/x-form/hooks/useApi.tsx
  20. 298
      src/pages/x-form/index.tsx
  21. 8
      src/pages/x-form/utils/index.tsx
  22. 1
      src/request.ts
  23. 33
      src/routes.tsx
  24. 4
      src/service/base.ts
  25. 6
      src/service/x-form/model.ts
  26. 4
      src/store/app/package.ts
  27. 3
      src/store/system/menu.ts
  28. 259
      src/store/x-form/model.ts
  29. 2
      src/types/x-form/model.d.ts
  30. 3
      src/utils/index.ts
  31. 13
      src/utils/tree.ts
  32. 5
      tsconfig.json
  33. 34
      vite.config.ts

43
.eslintrc.cjs

@ -1,24 +1,25 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: [ 'dist', '.eslintrc.cjs' ],
parser: '@typescript-eslint/parser',
plugins: [ 'react-refresh' ],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
ignorePatterns: [ 'dist', '.eslintrc.cjs' ],
parser: '@typescript-eslint/parser',
plugins: [ 'react-refresh' ],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/ban-ts-comment': [ 'error', {
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
'minimumDescriptionLength': 10
} ],
'@typescript-eslint/no-explicit-any': 'off',
},
'@typescript-eslint/ban-ts-comment': [ 'error', {
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
'minimumDescriptionLength': 10
} ],
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off',
},
}

6
package.json

@ -18,9 +18,9 @@
"@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",
"@tanstack/query-core": "^5.52.0",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-router": "^1.50.0",
"antd": "^5.16.1",
"antd-style": "^3.6.2",
"axios": "^1.6.8",

2
src/components/cascader/Cascader.tsx

@ -20,7 +20,7 @@ export const Cascader = ({ value, options = [], fieldNames, ...props }: Cascader
children: fieldNames?.children ?? 'children',
} as any
return (
<AntCascader {...props}
<AntCascader {...props as any}
options={options}
fieldNames={fieldNames}
value={getValue(options, value, f)}>

21
src/components/crazy-form/context.ts

@ -1,21 +0,0 @@
import React from 'react'
import { FormInstance } from 'antd/lib'
import { CrazyChildFormProps } from './typeing'
export const CrazyFormProvide = React.createContext<
| {
regForm: (name: string, props: CrazyChildFormProps<any>) => void;
unRegForm: (name: string) => void;
onFormFinish: (name: string, formData: any) => void;
keyArray: string[];
formArrayRef: React.MutableRefObject<
React.MutableRefObject<FormInstance<any> | undefined>[]
>;
loading: boolean;
setLoading: (loading: boolean) => void;
formMapRef: React.MutableRefObject<Map<string, CrazyChildFormProps>>;
}
| undefined
>(undefined)

222
src/components/crazy-form/index.tsx

@ -1,222 +0,0 @@
import {
FormSchema,
ItemType,
ProFormRenderValueTypeHelpers
} from '@ant-design/pro-form/es/components/SchemaForm/typing'
import { StepsForm, Embed } from '@ant-design/pro-form/es/components/SchemaForm/layoutType'
import { renderValueType } from '@ant-design/pro-form/es/components/SchemaForm/valueType'
import {
DrawerForm, FormProps, LabelIconTip,
LightFilter,
ModalForm, omitUndefined,
ProForm, ProFormColumnsType, ProFormInstance, ProFormProps,
QueryFilter, runFunction,
StepsForm as ProStepsForm, stringify, useDeepCompareMemo, useLatest, useReactiveRef, useRefFunction
} from '@ant-design/pro-components'
import React, { useCallback, useImperativeHandle, useRef, useState } from 'react'
import { TabsForm } from './tabs-form'
import { Form } from 'antd'
export type CrazyBateFormProps<T, ValueType> = {
layoutType: FormSchema<T, ValueType>['layoutType'] | 'TabsForm'
} & Omit<FormSchema<T, ValueType>, 'layout'>
const FormLayoutType = {
DrawerForm,
QueryFilter,
LightFilter, StepForm: ProStepsForm.StepForm,
StepsForm: StepsForm,
ModalForm,
Embed,
Form: ProForm,
TabsForm,
}
const CrazyBateForm = <T, ValueType = 'text'>(props: CrazyBateFormProps<T, ValueType>) => {
const {
columns,
layoutType = 'TabsForm',
type = 'form',
action,
shouldUpdate = (pre, next) => stringify(pre) !== stringify(next),
formRef: propsFormRef,
...restProps
} = props
const FormRenderComponents = (FormLayoutType[layoutType as 'TabsForm'] ||
ProForm) as React.FC<ProFormProps<T>>
const [ form ] = Form.useForm()
const formInstance = Form.useFormInstance()
const [ , forceUpdate ] = useState<[]>([])
const [ formDomsDeps, updatedFormDoms ] = useState<[]>(() => [])
const formRef = useReactiveRef<ProFormInstance | undefined>(
props.form || formInstance || form,
)
const oldValuesRef = useRef<T>()
const propsRef = useLatest(props)
/**
* 便 table
*
* @param items
*/
const genItems: ProFormRenderValueTypeHelpers<T, ValueType>['genItems'] =
useRefFunction((items: ProFormColumnsType<T, ValueType>[]) => {
return items
.filter((originItem) => {
return !(originItem.hideInForm && type === 'form')
})
.sort((a, b) => {
if (b.order || a.order) {
return (b.order || 0) - (a.order || 0)
}
return (b.index || 0) - (a.index || 0)
})
.map((originItem, index) => {
const title = runFunction(
originItem.title,
originItem,
'form',
<LabelIconTip
label={originItem.title as string}
//@ts-ignore @ts-expect-error
tooltip={originItem.tooltip || originItem.tip}
/>,
)
const item = omitUndefined({
title,
label: title,
name: originItem.name,
valueType: runFunction(originItem.valueType, {}),
key: originItem.key || originItem.dataIndex || index,
columns: originItem.columns,
valueEnum: originItem.valueEnum,
dataIndex: originItem.dataIndex || originItem.key,
initialValue: originItem.initialValue,
width: originItem.width,
index: originItem.index,
readonly: originItem.readonly,
colSize: originItem.colSize,
colProps: originItem.colProps,
rowProps: originItem.rowProps,
className: originItem.className,
//@ts-ignore @ts-expect-error
tooltip: originItem.tooltip || originItem.tip,
dependencies: originItem.dependencies,
proFieldProps: originItem.proFieldProps,
ignoreFormItem: originItem.ignoreFormItem,
getFieldProps: originItem.fieldProps
? () =>
runFunction(
originItem.fieldProps,
formRef.current,
originItem,
)
: undefined,
getFormItemProps: originItem.formItemProps
? () =>
runFunction(
originItem.formItemProps,
formRef.current,
originItem,
)
: undefined,
render: originItem.render,
renderFormItem: originItem.renderFormItem,
renderText: originItem.renderText,
request: originItem.request,
params: originItem.params,
transform: originItem.transform,
convertValue: originItem.convertValue,
debounceTime: originItem.debounceTime,
defaultKeyWords: originItem.defaultKeyWords,
}) as ItemType<any, any>
return renderValueType(item, {
action,
type,
originItem,
formRef,
genItems,
})
})
.filter((field) => {
return Boolean(field)
})
})
const onValuesChange: FormProps<T>['onValuesChange'] = useCallback(
(changedValues: any, values: T) => {
const { onValuesChange: propsOnValuesChange } = propsRef.current
if (
shouldUpdate === true ||
(typeof shouldUpdate === 'function' &&
shouldUpdate(values, oldValuesRef.current))
) {
updatedFormDoms([])
}
oldValuesRef.current = values
propsOnValuesChange?.(changedValues, values)
},
[ propsRef, shouldUpdate ],
)
const formChildrenDoms = useDeepCompareMemo(() => {
if (!formRef.current) return
// like StepsForm's columns but not only for StepsForm
if (columns.length && Array.isArray(columns[0])) return
return genItems(columns as ProFormColumnsType<T, ValueType>[])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ columns, restProps?.open, action, type, formDomsDeps, !!formRef.current ])
/**
* Append layoutType component specific props
*/
const specificProps = useDeepCompareMemo(() => {
if (layoutType === 'StepsForm') {
return {
forceUpdate: forceUpdate,
columns: columns as ProFormColumnsType<T, ValueType>[][],
}
}
return {}
}, [ columns, layoutType ])
useImperativeHandle(
propsFormRef,
() => {
return formRef.current
},
[ formRef.current ],
)
return (
<FormRenderComponents
{...specificProps}
{...restProps}
onInit={(_, initForm) => {
if (propsFormRef) {
(propsFormRef as React.MutableRefObject<ProFormInstance<T>>).current =
initForm
}
restProps?.onInit?.(_, initForm)
formRef.current = initForm
}}
form={props.form || form}
formRef={formRef}
onValuesChange={onValuesChange}
>
{formChildrenDoms}
</FormRenderComponents>
)
}
export default CrazyBateForm

80
src/components/crazy-form/tabs-form/TabForm.tsx

@ -1,80 +0,0 @@
import { useContext, useEffect, useImperativeHandle, useRef } from 'react'
import { TabsProps } from 'antd'
import { noteOnce } from 'rc-util/lib/warning'
import { FormInstance } from 'antd/lib'
import { CrazyFormProvide } from '../context.ts'
import { TabFormProvide } from './index.tsx'
import { CrazyChildFormProps } from '../typeing'
export type TabFormProps<T = Record<string, any>> = {
tab?: string;
tabProps?: TabsProps;
} & CrazyChildFormProps<T>
const TabForm = <T = Record<string, any>>(props: TabFormProps<T>) => {
const formRef = useRef<FormInstance | undefined>()
const context = useContext(CrazyFormProvide)
const tabContext = useContext(TabFormProvide)
const {
onFinish,
tab,
formRef: propFormRef,
tabProps,
...restProps
} = props
noteOnce(!(restProps as any).submitter, 'TabForm 不包含提交按钮,请在 TabsForm 上')
/** 重置 formRef */
useImperativeHandle(propFormRef, () => formRef.current, [
propFormRef?.current,
])
/** Dom 不存在的时候解除挂载 */
useEffect(() => {
if (!(props.name || props.tab)) return
const name = (props.name || props.tab)!.toString()
context?.regForm(name, props)
return () => {
context?.unRegForm(name)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
if (context && context?.formArrayRef) {
context.formArrayRef.current[tab || 0] = formRef
}
return (
<BaseForm
formRef={formRef}
onFinish={async (values) => {
if (restProps.name) {
context?.onFormFinish(restProps.name, values)
}
if (onFinish) {
context?.setLoading(true)
// 如果报错,直接抛出
await onFinish?.(values)
context?.setLoading(false)
return
}
}}
onInit={(_, form) => {
formRef.current = form
if (context && context?.formArrayRef) {
context.formArrayRef.current[tab || 0] = formRef
}
restProps?.onInit?.(_, form)
}}
layout="vertical"
{...omit(restProps, [ 'layoutType', 'columns' ] as any[])}
/>
)
}
export default TabForm

375
src/components/crazy-form/tabs-form/index.tsx

@ -1,375 +0,0 @@
import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'
import TabForm, { TabFormProps } from './TabForm'
import { CrazyFormProps } from '../typeing'
import { Button, Col, Form, Row, Space, Tabs, TabsProps } from 'antd'
import { FormInstance } from 'antd/lib'
import toArray from 'rc-util/lib/Children/toArray'
import useMergedState from 'rc-util/lib/hooks/useMergedState'
import { merge, ProConfigProvider, useRefFunction } from '@ant-design/pro-components'
import { t } from '@/i18n'
import classnames from 'classnames'
import { CrazyFormProvide } from '../context'
type TabsFormProps<T = Record<string, any>> = {
toggleProps: TabsProps
direction?: 'horizontal' | 'vertical'
} & Omit<CrazyFormProps<T>, 'toggleProps'>
export const TabFormProvide = React.createContext<TabFormProps<any> | null>(null)
const TabsLayoutStrategy: Record<
string,
(dom: LayoutRenderDom) => React.ReactNode
> = {
horizontal({ toggleDom, formDom }) {
return (
<>
<Row gutter={{ xs: 8, sm: 16, md: 24 }}>
<Col span={24}>{toggleDom}</Col>
</Row>
<Row gutter={{ xs: 8, sm: 16, md: 24 }}>
<Col span={24}>{formDom}</Col>
</Row>
</>
)
},
vertical({ stepsDom, formDom }) {
return (
<Row align="stretch" wrap={true} gutter={{ xs: 8, sm: 16, md: 24 }}>
<Col xxl={4} xl={6} lg={7} md={8} sm={10} xs={12}>
{React.cloneElement(stepsDom, {
style: {
height: '100%',
},
})}
</Col>
<Col>
<div
style={{
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
}}
>
{formDom}
</div>
</Col>
</Row>
)
},
}
const TabsForm = <T = Record<string, any>>(
props: TabsFormProps<T> & {
children: React.ReactNode
}
) => {
const {
toggleProps,
toggleFormRender,
direction = 'horizontal',
current: tab,
onCurrentChange,
submitter,
formRender,
onFinish,
formProps,
containerStyle,
formRef,
formMapRef: propsFormMapRef,
layoutRender: propsLayoutRender,
...rest
} = props
const formDataRef = useRef(new Map<string, Record<string, any>>())
const formMapRef = useRef(new Map<string, TabFormProps>())
const formArrayRef = useRef<
React.MutableRefObject<FormInstance<any> | undefined>[]
>([])
const [ formArray, setFormArray ] = useState<string[]>([])
const [ loading, setLoading ] = useState<boolean>(false)
/**
*
*/
const [ tab, setTab ] = useMergedState<number>(0, {
value: props.current,
onChange: props.onCurrentChange,
})
const layoutRender = useMemo(() => {
return TabsLayoutStrategy[direction]
}, [ direction ])
/**
* form进入便 props
*/
const regForm = useCallback(
(name: string, childrenFormProps: TabFormProps) => {
if (!formMapRef.current.has(name)) {
setFormArray((oldFormArray) => [ ...oldFormArray, name ])
}
formMapRef.current.set(name, childrenFormProps)
},
[],
)
/**
* form -1
*/
const unRegForm = useCallback((name: string) => {
setFormArray((oldFormArray) => oldFormArray.filter((n) => n !== name))
formMapRef.current.delete(name)
formDataRef.current.delete(name)
}, [])
useImperativeHandle(propsFormMapRef, () => formArrayRef.current, [
formArrayRef.current,
])
useImperativeHandle(
formRef,
() => {
return formArrayRef.current[tab || 0]?.current
},
[ tab, formArrayRef.current ],
)
/**
* ProForm处理了一下 from 使 Provider
*/
const onFormFinish = useCallback(
async (name: string, formData: any) => {
formDataRef.current.set(name, formData)
setLoading(true)
const values: any = merge(
{},
...Array.from(formDataRef.current.values()),
)
try {
const success = await onFinish(values)
if (success) {
formArrayRef.current.forEach((form) => form.current?.resetFields())
}
} catch (error) {
console.log(error)
} finally {
setLoading(false)
}
},
[ lastStep, onFinish, setLoading, setTab ],
)
const toggleDoms = useMemo(() => {
const itemsProps = {
items: formArray.map((item) => {
const itemProps = formMapRef.current.get(item)
return {
key: item,
title: itemProps?.title,
...itemProps?.tabProps,
}
}),
}
return (
<div className={`crazy-tabs-container`.trim()}
>
<Tabs
{...toggleProps}
{...itemsProps}
activeKey={tab}
onChange={onCurrentChange}
>
</Tabs>
</div>
)
}, [ formArray, tab, toggleProps, onCurrentChange ])
const onSubmit = useRefFunction(() => {
const from = formArrayRef.current[tab]
from.current?.submit()
})
const submit = useMemo(() => {
return (
submitter !== false && (
<Button
key="submit"
type="primary"
loading={loading}
{...submitter?.submitButtonProps}
onClick={() => {
submitter?.onSubmit?.()
onSubmit()
}}
>
{t('actions.submit', '提交')}
</Button>
)
)
}, [ loading, onSubmit, submitter ])
const submitterDom = useMemo(() => {
let buttons: (React.ReactElement | false)[] = [ submit ]
buttons = buttons.filter(React.isValidElement)
if (submitter && submitter.render) {
const submitterProps: any = {
form: formArrayRef.current[tab]?.current,
onSubmit,
current: tab,
}
return submitter.render(
submitterProps,
buttons as React.ReactElement[],
) as React.ReactNode
}
if (submitter && submitter?.render === false) {
return null
}
return buttons as React.ReactElement[]
}, [ formArray.length, onSubmit, tab, submit, submitter ])
const formDom = useMemo(() => {
return toArray(props.children).map((item, index) => {
const itemProps = item.props as TabFormProps
const name = itemProps.name || `${index}`
/** 是否是当前的表单 */
const isShow = tab === name
const config = isShow
? {
contentRender: formRender,
}
: {}
return (
<div
className={classnames(`crazy-tab`, {
[`crazy-tab-active`]: isShow,
})}
key={name}
>
<TabFormProvide.Provider
value={{
...config,
...formProps,
...itemProps,
name,
index,
tab: name,
}}
>
{item}
</TabFormProvide.Provider>
</div>
)
})
}, [ formProps, props.children, tab, formRender ])
const finalTabsDom = useMemo(() => {
if (toggleFormRender) {
return toggleFormRender(
formArray.map((item) => ({
key: item,
title: formMapRef.current.get(item)?.title,
})),
toggleDoms,
) as React.ReactElement
}
return toggleDoms
}, [ formArray, toggleDoms, toggleFormRender ])
const formContainer = useMemo(
() => (
<div
className={`crazy-container`.trim()}
style={containerStyle}
>
{formDom}
{toggleFormRender ? null : <Space>{submitterDom}</Space>}
</div>
),
[ containerStyle, formDom, toggleFormRender, submitterDom ],
)
const tabsFormDom = useMemo(() => {
const doms = {
toggleDom: finalTabsDom,
formDom: formContainer,
}
if (toggleFormRender) {
if (propsLayoutRender) {
return toggleFormRender(propsLayoutRender(doms), submitterDom)
} else {
return toggleFormRender(layoutRender(doms), submitterDom)
}
}
if (propsLayoutRender) {
return propsLayoutRender(doms)
}
return layoutRender(doms)
}, [
finalTabsDom,
formContainer,
layoutRender,
toggleFormRender,
submitterDom,
propsLayoutRender,
])
return (
<div>
<Form.Provider {...rest}>
<CrazyFormProvide.Provider
value={{
loading,
setLoading,
regForm,
keyArray: formArray,
formArrayRef,
formMapRef,
unRegForm,
onFormFinish,
}}
>
{tabsFormDom}
</CrazyFormProvide.Provider>
</Form.Provider>
</div>
)
}
function TabsFormWarp<T = Record<string, any>>(
props: CrazyFormProps<T> & {
children: any;
},
) {
return (
<ProConfigProvider needDeps>
<TabsForm<T> {...props} />
</ProConfigProvider>
)
}
TabsFormWarp.TabForm = TabForm
TabsFormWarp.useForm = Form.useForm
export { TabsFormWarp as TabsForm }
export type { TabFormProps, CrazyFormProps as TabsFormProps }

74
src/components/crazy-form/typeing.d.ts

@ -1,74 +0,0 @@
import { FormInstance } from 'antd/lib'
import { FormProps, ProFormInstance, ProFormProps, SubmitterProps } from '@ant-design/pro-components'
import React from 'react'
import { FormProviderProps } from 'antd/es/form/context'
import type { CommonFormProps } from '@ant-design/pro-form/es/BaseForm/BaseForm'
export type CrazyFormProps<T = Record<string, any>> = {
onFinish?: (values: T) => Promise<boolean | void>;
current?: string;
/**
* Props
*/
toggleProps?: Record<any, any>;
formProps?: ProFormProps<T>;
onCurrentChange?: (current: string) => void;
/** 自定义步骤器 */
toggleRender?: (
items: {
key: string;
title?: React.ReactNode;
[key: string]: any
}[],
defaultDom: React.ReactNode,
) => React.ReactNode;
/** @name 当前展示表单的 formRef */
formRef?: React.MutableRefObject<ProFormInstance<any> | undefined | null>;
/** @name 所有表单的 formMapRef */
formMapRef?: React.MutableRefObject<
React.MutableRefObject<FormInstance<any> | undefined>[]
>;
/**
*
*
* @param form From dom
*/
toggleFormRender?: (from: React.ReactNode) => React.ReactNode;
/**
*
*
* @param form From dom
* @param submitter
*/
formRender?: (
from: React.ReactNode,
submitter: React.ReactNode,
) => React.ReactNode;
/** 按钮的统一配置,优先级低于分步表单的配置 */
submitter?:
| SubmitterProps<{
current: string; //当前激活的toggle
form?: FormInstance<any>;
}>
| false;
containerStyle?: React.CSSProperties;
/**
*
*
* @param layoutDom toggleDom formDom
*/
layoutRender?: (layoutDom: {
toggleDom: React.ReactElement;
formDom: React.ReactElement;
}) => React.ReactNode;
} & Omit<FormProviderProps, 'children'>;
export type CrazyChildFormProps<T = Record<string, any>, U = Record<string, any>> = {
index?: number;
} & Omit<FormProps<T>, 'onFinish' | 'form'> &
Omit<CommonFormProps<T, U>, 'submitter' | 'form'>;

319
src/components/x-form/index.tsx

@ -0,0 +1,319 @@
import { useStyle } from './style.ts'
import { Badge, Button, Divider, Form, Popconfirm, Space, Tooltip } from 'antd'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { ModelContext, useSpanModel } from '@/store/x-form/model.ts'
import { ReactNode, useEffect, useState } from 'react'
import { transformAntdTableProColumns } from './utils'
import Action from '@/components/action/Action.tsx'
import { FilterOutlined } from '@ant-design/icons'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { Table as ProTable } from '@/components/table'
import { getValueCount, unSetColumnRules } from '@/utils'
import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components'
import { useApiContext } from '@/context.ts'
import { useDeepCompareEffect } from 'react-use'
import { XFormTypes } from '@/types/x-form/model'
export interface XFormProps {
title?: ReactNode
namespace?: string
columns?: ProColumns[] //重写columns
}
const XForm = ({ namespace, columns: propColumns = [], title }: XFormProps) => {
const { styles, cx } = useStyle()
const apiCtx = useApiContext()
const {
apiAtom,
deleteModelAtom,
modelAtom,
modelCURDAtom,
modelsAtom,
modelSearchAtom,
saveOrUpdateModelAtom
} = useSpanModel(namespace || apiCtx?.menu?.meta?.name || 'default') as ModelContext
const [ form ] = Form.useForm()
const [ filterForm ] = Form.useForm()
const setApi = useSetAtom(apiAtom)
const [ model, setModel ] = useAtom<XFormTypes.IModel>(modelAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateModelAtom)
const [ search, setSearch ] = useAtom(modelSearchAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(modelsAtom)
const { mutate: deleteModel, isPending: isDeleting } = useAtomValue(deleteModelAtom)
const { data: curdModal, isLoading: curdLoading, refetch: reloadCURDModal } = useAtomValue(modelCURDAtom)
const [ open, setOpen ] = useState(false)
const [ openFilter, setFilterOpen ] = useState(false)
const [ searchKey, setSearchKey ] = useState(search?.key)
const [ columns, setColumns ] = useState<ProColumns[]>([])
useDeepCompareEffect(() => {
const res = transformAntdTableProColumns(curdModal?.column || [], propColumns)
const _columns = [ {
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: { hidden: true }
} ].concat(res as any).concat([
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
render: (_, record) => [
<Action key="edit"
as={'a'}
onClick={() => {
form.setFieldsValue(record)
setOpen(true)
}}>{'编辑'}</Action>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteModel([ record.id ])
}}
title={'确定要删除吗?'}>
<a key="del">
</a>
</Popconfirm>
]
} as any
])
setColumns(_columns)
}, [ curdModal?.column, propColumns, deleteModel, form, isDeleting, setOpen, ])
useEffect(() => {
if (apiCtx.isApi && apiCtx.api) {
setApi(apiCtx.api)
reloadCURDModal()
}
}, [ apiCtx.isApi, apiCtx.api ])
useDeepCompareEffect(() => {
setSearchKey(search?.key)
filterForm.setFieldsValue(search)
}, [ search ])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
const formProps = curdModal?.dialogType === 'drawer' ? {
layoutType: 'DrawerForm',
drawerProps: {
maskClosable: false,
}
} : {
layoutType: 'ModalForm',
modalProps: {
maskClosable: false,
}
}
const renderTitle = () => {
if (title) {
return title
}
if (apiCtx.menu) {
const { menu } = apiCtx
return menu.title
}
return null
}
const tableTitle = <>
<Button key={'add'}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type={'primary'}>{'添加'}</Button>
</>
return (
<>
<ListPageLayout
className={styles.container}
title={renderTitle()}>
<ProTable
rowKey="id"
headerTitle={tableTitle}
toolbar={{
/*search: {
loading: isFetching && !!search?.key,
onSearch: (value: string) => {
setSearch(prev => ({
...prev,
title: value
}))
},
allowClear: true,
onChange: (e) => {
setSearchKey(e.target?.value)
},
value: searchKey,
placeholder: '输入关键字搜索',
},*/
actions: [
<Tooltip key={'filter'} title={'高级查询'}>
<Badge count={getValueCount(search)}>
<Button
onClick={() => {
setFilterOpen(true)
}}
icon={<FilterOutlined/>} shape={'circle'} size={'small'}/>
</Badge>
</Tooltip>,
<Divider type={'vertical'} key={'divider'}/>,
]
}}
scroll={{
x: (columns?.length || 1) * 100,
y: 'calc(100vh - 290px)'
}}
search={false}
onRow={(record) => {
return {
className: cx({
// 'ant-table-row-selected': currentMovie?.id === record.id
}),
onClick: () => {
setModel(record)
}
}
}}
dateFormatter="string"
loading={isLoading || isFetching || curdLoading}
dataSource={data?.rows ?? []}
columns={columns}
options={{
reload: () => {
refetch()
},
}}
pagination={{
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {
...prev,
page: current,
pageSize: pageSize,
}
})
},
}}
/>
<BetaSchemaForm
grid={true}
shouldUpdate={false}
width={1000}
form={form}
layout={'vertical'}
scrollToFirstError={true}
title={model?.id !== 0 ? '编辑' : '添加'}
{...formProps as any}
open={open}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
saveOrUpdate(values as any)
}}
columns={columns as ProFormColumnsType[]}/>
<BetaSchemaForm
title={'高级查询'}
grid={true}
shouldUpdate={false}
width={500}
form={filterForm}
open={openFilter}
onOpenChange={open => {
setFilterOpen(open)
}}
layout={'vertical'}
scrollToFirstError={true}
layoutType={formProps.layoutType as any}
drawerProps={{
...formProps.drawerProps,
mask: false,
}}
modalProps={{
...formProps.modalProps,
mask: false,
}}
submitter={{
searchConfig: {
resetText: '清空',
submitText: '查询',
},
onReset: () => {
filterForm.resetFields()
},
render: (props,) => {
return (
<div style={{ textAlign: 'right' }}>
<Space>
<Button onClick={() => {
props.reset()
}}>{props.searchConfig?.resetText}</Button>
<Button type="primary"
onClick={() => {
props.submit()
}}
>{props.searchConfig?.submitText}</Button>
</Space>
</div>
)
},
}}
onFinish={async (values) => {
//处理,变成数组
Object.keys(values).forEach(key => {
if (typeof values[key] === 'string' && values[key].includes(',')) {
values[key] = values[key].split(',')
}
})
setSearch(values)
}}
columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/>
</ListPageLayout>
</>
)
}
export default XForm

13
src/components/x-form/style.ts

@ -0,0 +1,13 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-x-form-component`
const container = css`
`
return {
container: cx(prefix, props?.className, container),
}
})

155
src/components/x-form/utils/index.tsx

@ -0,0 +1,155 @@
import { XFormTypes } from '@/types/x-form/model'
import { ProColumns } from '@ant-design/pro-components'
import Switch from '@/components/switch'
import { Checkbox, DatePicker, Input, Radio, Select, TreeSelect } from 'antd'
import request from '@/request'
import { convertToBool, genProTableColumnWidthProps } from '@/utils'
const getValueType = (column: XFormTypes.IColumn) => {
switch (column.type) {
case 'input':
return 'text'
case 'select':
return 'select'
case 'date':
return 'date'
case 'switch':
return 'switch'
case 'radio':
return 'radio'
case 'checkbox':
return 'checkbox'
case 'textarea':
return 'textarea'
case 'tree':
return 'treeSelect'
default:
return 'text'
}
}
//根据type返回对应的组件
const getComponent = (column: XFormTypes.IColumn) => {
const type = getValueType(column) as any
switch (type) {
case 'input':
return Input
case 'select':
return Select
case 'date':
return DatePicker
case 'switch':
return Switch
case 'radio':
return Radio
case 'checkbox':
return Checkbox
case 'textarea':
return Input.TextArea
case 'tree':
case 'treeSelect':
return TreeSelect
default:
return Input
}
}
export const transformAntdTableProColumns = (columns: XFormTypes.IColumn[], overwriteColumns?: ProColumns[]) => {
const overwriteKeys = [] as string[]
return (columns || []).map(item => {
const { value, props, multiple, checkStrictly } = item
const { width, fieldProps: _fieldProps } = genProTableColumnWidthProps(item.width)
const fieldProps: ProColumns['fieldProps'] = {
dataFiledNames: props,
...(multiple ? { multiple: true } : {}),
...(checkStrictly ? { treeCheckStrictly: true } : {}),
..._fieldProps,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const formItemProps: ProColumns['formItemProps'] = (_form, _config) => {
return {
rules: item.rules?.map(i => {
return {
required: i.required,
message: i.message
}
}),
...(value ? { valuePropName: value } : {})
}
}
const rowProps = item.gutter ? { gutter: item.gutter } : { gutter: [ 16, 0 ], }
const colProps = item.span ? { span: item.span } : {}
const type = getValueType(item)
const overwrite = overwriteColumns?.find(i => i.dataIndex === item.prop)
if (overwrite) {
overwriteKeys.push(item.prop)
}
return {
title: item.label,
dataIndex: item.prop,
key: item.prop,
width,
valueType: type,
hideInSearch: !item.search,
hideInTable: item.hide,
fieldProps,
formItemProps,
colProps,
rowProps,
request: item.dicUrl ? async (params, props) => {
const { fieldProps: { dataFiledNames } } = props
const { value, res: resKey, label } = dataFiledNames || {}
const url = `/${item.dicUrl.replace(/^:/, '/')}`
return request[item.dicMethod || 'get'](url, params).then(res => {
return (res.data?.[resKey] || res.data || []).map((i: any) => {
// console.log(i)
const disabled = 'disabled' in i ? i.disabled :
('status' in i ? !convertToBool(i.status) : false)
return {
title: i[label || 'label'],
label: i[label || 'label'],
value: i[value || 'id'],
disabled,
data: i
}
})
})
} : undefined,
renderFormItem: (_scheam, config) => {
const Component = getComponent(item) as any
const { options, ...props } = config as any
if ([ 'tree', 'treeSelect' ].includes(_scheam.valueType as string)) {
return <Component {...props} treeData={options}/>
}
if (_scheam.valueType as string === 'select') {
return <Select {...props} options={options}/>
}
return <Component {...config} />
},
render: (text: any, record: any) => {
if (type === 'switch' || type === 'checkbox' || type === 'radio') {
return <Switch size={'small'} value={record[item.prop]}/>
}
if (item.colorFormat) {
return <span style={{ color: item.colorFormat }}>{text}</span>
}
return text
},
...overwrite
} as ProColumns
}).concat(overwriteColumns?.filter(i => !overwriteKeys.includes(i.dataIndex)) || [])
}

19
src/context.ts

@ -1,6 +1,7 @@
import { IAppData } from '@/global'
import React, { createContext, ProviderProps, useContext } from 'react'
import { t } from 'i18next'
import { IMenu } from '@/types/system/menus'
export interface IAppContextValue {
appData: () => IAppData
@ -23,4 +24,22 @@ export const AppContextProvider = ({ value, children }: ProviderProps<Partial<IA
export const useAppContext = () => {
return useContext<IAppContextValue>(AppContext)
}
type ApiContextValue = {
api?: string
isApi?: boolean
menu?: IMenu
path?: string
}
export const ApiContext = createContext<ApiContextValue>({})
export const ApiContextProvider = ({ value, children }: ProviderProps<Partial<ApiContextValue>>) => {
return React.createElement(ApiContext.Provider, {
value,
}, children)
}
export const useApiContext = () => {
return useContext<ApiContextValue>(ApiContext)
}

0
src/hooks/useNavigate.ts

2
src/layout/EmptyLayout.tsx

@ -5,7 +5,7 @@ const EmptyLayout = () => {
return (
<CatchBoundary
getResetKey={() => 'reset-layout'}
errorComponent={ErrorPage}
errorComponent={ErrorPage as any}
>
<Outlet/>
</CatchBoundary>

31
src/layout/RootLayout.tsx

@ -7,7 +7,7 @@ import { currentStaticUserAtom, userMenuDataAtom } from '@/store/system/user.ts'
import { MenuItem } from '@/global'
import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
import { enUSIntl, zhCNIntl } from '@ant-design/pro-provider/es/intl'
import { CatchBoundary, Link, Outlet, useNavigate } from '@tanstack/react-router'
import { CatchBoundary, Link, Outlet, useNavigate, useRouterState } from '@tanstack/react-router'
import { ConfigProvider } from '@/components/config-provider'
import { useEffect, useRef, useState } from 'react'
import { useAtomValue, useSetAtom } from 'jotai'
@ -43,8 +43,10 @@ const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
return breadcrumbData.reverse()
}
export default () => {
const { location } = useRouterState()
const navigate = useNavigate()
const { styles } = useStyle()
const currentUser = useAtomValue(currentStaticUserAtom)
@ -68,6 +70,7 @@ export default () => {
}
if (item?.meta.affix) {
// affix = true 为 dashboard页面
return [ item.meta.name ]
}
return item ? item.parentName : []
@ -77,7 +80,7 @@ export default () => {
const currentMenu = menuData.find(item => {
return item.key === rootMenuKeys?.[0]
})
// console.log(rootMenuKeys)
childMenuRef.current = currentMenu?.children || []
useEffect(() => {
@ -107,7 +110,7 @@ export default () => {
>
<CatchBoundary
getResetKey={() => 'reset-page'}
errorComponent={ErrorPage}
errorComponent={ErrorPage as any}
>
<ProConfigProvider hashed={false} intl={language === 'zh-CN' ? zhCNIntl : enUSIntl}>
<ConfigProvider
@ -117,16 +120,16 @@ export default () => {
}}
>
<div {
...{
rotate: -31,
content: currentUser?.nickname,
font: {
color: '#00000012',
size: 17,
},
zIndex: 1009,
} as any
} style={{ width: '100vw', height: '100vh' }}>
...{
rotate: -31,
content: currentUser?.nickname,
font: {
color: '#00000012',
size: 17,
},
zIndex: 1009,
} as any
} style={{ width: '100vw', height: '100vh' }}>
<ProLayout
token={{
header: {
@ -175,10 +178,8 @@ export default () => {
pathname,
}
}
menu={{
collapsedShowGroupTitle: true,
}}
menuItemRender={(item: MenuDataItem) => {
return (<span style={{ userSelect: 'none' }} onClick={() => {

2
src/pages/app/package/index.tsx

@ -194,7 +194,7 @@ const AppPackage = () => {
},
render: (_text, record) => {
//0未处理 1队列中 2打包中 3打包成功 4打包失败
return <Badge status={[ 'default', 'processing', 'processing', 'success', 'error' ][record.status]}
return <Badge status={[ 'default', 'processing', 'processing', 'success', 'error' ][record.status] as any}
text={[ '处理', '队列中', '打包中', '打包成功', '打包失败' ][record.status]}/>
}
},

16
src/pages/system/menus/index.tsx

@ -35,7 +35,7 @@ const Menus = () => {
const { data = [] } = useAtomValue(menuDataAtom)
const [ currentMenu, setMenuData ] = useAtom<MenuItem>(selectedMenuAtom) ?? {}
const menuInputRef = useRef<InputRef | undefined>(undefined)
const isApi = Form.useWatch('type', form) === 'api'
useEffect(() => {
@ -99,7 +99,7 @@ const Menus = () => {
</>}
>
<Form form={form}
initialValues={currentMenu!}
// initialValues={currentMenu!}
labelCol={{ flex: '110px' }}
labelAlign="left"
labelWrap
@ -155,6 +155,10 @@ const Menus = () => {
label: t('system.menus.form.typeOptions.button', '按钮'),
value: 'button'
},
{
label: t('system.menus.form.typeOptions.api', 'API'),
value: 'api'
},
]}
optionType="button"
buttonStyle="solid"
@ -179,7 +183,13 @@ const Menus = () => {
<Form.Item label={t('system.menus.form.path', '路由')} name={'path'}>
<Input/>
</Form.Item>
<Form.Item label={t('system.menus.form.active', '菜单高亮')}
<Form.Item
hidden={!isApi}
label={t('system.menus.form.api', 'API地址')} name={['properties','api']}>
<Input/>
</Form.Item>
<Form.Item
label={t('system.menus.form.active', '菜单高亮')}
name={'active'}
help={t('system.menus.form.activeHelp','子节点或详情页需要高亮的上级菜单路由地址')}
>

3
src/pages/system/roles/index.tsx

@ -1,6 +1,5 @@
import Switch from '@/components/switch'
import { IMenu } from '@/types/system/menus'
import {
ActionType,
ProColumns,
@ -44,7 +43,7 @@ const MenuTree = (props: any) => {
return <Tree treeData={menuList}
fieldNames={{ title: 'title', key: 'id' }}
disabled={mode !== 'edit'} checkable={true} onCheck={onCheck}
checkedKeys={getTreeCheckedStatus<IMenu>(menuList!, value)}/>
checkedKeys={getTreeCheckedStatus(menuList!, value)}/>
}

20
src/pages/x-form/hooks/useApi.tsx

@ -1,12 +1,15 @@
import { useNavigate } from '@tanstack/react-router'
import { useNavigate, useRouterState } from '@tanstack/react-router'
import { useAtom } from 'jotai/index'
import { apiAtom } from '@/store/x-form/model.ts'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Input, message, Modal } from 'antd'
import { Route } from '@/pages/x-form'
import { useApiContext } from '@/context.ts'
export const useApi = () => {
const { location } = useRouterState()
const apiCtx = useApiContext()
const nav = useNavigate()
const [ api, setApi ] = useAtom(apiAtom)
const { api: apiParam } = Route.useSearch()
@ -15,12 +18,19 @@ export const useApi = () => {
const [ open, setOpen ] = useState(false)
const apiRef = useRef<string>(apiParam)
useEffect(() => {
if (apiCtx.isApi && apiCtx.api) {
apiRef.current = apiCtx.api
setApi(apiCtx.api)
return
}
if (!apiParam && api) {
apiRef.current = api
nav({
to: '/x-form',
to: location.pathname,
search: {
api
}
@ -41,7 +51,7 @@ export const useApi = () => {
}
}, 2000)
}, [ api, apiParam ])
}, [ api, apiParam, apiCtx ])
const onOK = useCallback(() => {
if (!innerApi) {
@ -53,8 +63,9 @@ export const useApi = () => {
setOpen(false)
setApi(innerApi)
setChange(true)
nav({
to: '/x-form',
to: location.pathname,
search: {
api: innerApi
}
@ -94,6 +105,7 @@ export const useApi = () => {
setApi,
apiChange: isChange,
api,
apiCtx,
} as const
}

298
src/pages/x-form/index.tsx

@ -1,297 +1,11 @@
import { createFileRoute } from '@tanstack/react-router'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { useApi } from './hooks/useApi.tsx'
import { Badge, Button, Divider, Form, Popconfirm, Space, Tag, Tooltip } from 'antd'
import { EditOutlined, FilterOutlined } from '@ant-design/icons'
import { BetaSchemaForm, ProFormColumnsType } from '@ant-design/pro-components'
import { Table as ProTable } from '@/components/table'
import { useEffect, useMemo, useState } from 'react'
import { useAtomValue } from 'jotai'
import {
deleteModelAtom, modelAtom,
modelCURDAtom,
modelsAtom,
modelSearchAtom,
saveOrUpdateModelAtom
} from '@/store/x-form/model.ts'
import { useAtom } from 'jotai/index'
import { getValueCount, unSetColumnRules } from '@/utils'
import { useStyle } from './style.ts'
import Action from '@/components/action/Action.tsx'
import { transformAntdTableProColumns } from './utils'
import XForm from '@/components/x-form'
const XForm = () => {
const XFormRender = () => {
const { styles, cx } = useStyle()
const { holderElement, updateApi, api, apiChange } = useApi()
const [ form ] = Form.useForm()
const [ filterForm ] = Form.useForm()
const [ model, setModel ] = useAtom(modelAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateModelAtom)
const [ search, setSearch ] = useAtom(modelSearchAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(modelsAtom)
const { mutate: deleteModel, isPending: isDeleting } = useAtomValue(deleteModelAtom)
const { data: curdModal, isLoading: curdLoading, refetch: reloadCURDModal } = useAtomValue(modelCURDAtom)
const [ open, setOpen ] = useState(false)
const [ openFilter, setFilterOpen ] = useState(false)
const [ searchKey, setSearchKey ] = useState(search?.key)
const columns = useMemo(() => {
return transformAntdTableProColumns(curdModal?.column || []).concat([
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: { hidden: true }
},
{
title: '操作',
key: 'option',
valueType: 'option',
fixed: 'right',
render: (_, record) => [
<Action key="edit"
as={'a'}
onClick={() => {
form.setFieldsValue(record)
setOpen(true)
}}>{'编辑'}</Action>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteModel([ record.id ])
}}
title={'确定要删除吗?'}>
<a key="del">
</a>
</Popconfirm>
]
}
])
}, [ curdModal?.column, deleteModel, form, isDeleting, setOpen ])
useEffect(() => {
setSearchKey(search?.key)
filterForm.setFieldsValue(search)
}, [ search ])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
useEffect(() => {
console.log('apiChange')
if (apiChange) {
reloadCURDModal()
}
}, [ apiChange ])
const formProps = curdModal?.dialogType === 'drawer' ? {
layoutType: 'DrawerForm',
drawerProps: {
maskClosable: false,
}
} : {
layoutType: 'ModalForm',
modalProps: {
maskClosable: false,
}
}
return (
<>
{holderElement}
<ListPageLayout
className={styles.container}
title={<>
<Tag color={'green'} style={{ marginBlockEnd: 0 }}>API</Tag>
<span>{api} <EditOutlined
style={{ color: '#666', cursor: 'pointer', fontSize: 16 }}
onClick={() => {
updateApi(true)
}}/></span>
</>}>
<ProTable
rowKey="id"
headerTitle={`${api}/list`}
toolbar={{
/*search: {
loading: isFetching && !!search?.key,
onSearch: (value: string) => {
setSearch(prev => ({
...prev,
title: value
}))
},
allowClear: true,
onChange: (e) => {
setSearchKey(e.target?.value)
},
value: searchKey,
placeholder: '输入关键字搜索',
},*/
actions: [
<Tooltip key={'filter'} title={'高级查询'}>
<Badge count={getValueCount(search)}>
<Button
onClick={() => {
setFilterOpen(true)
}}
icon={<FilterOutlined/>} shape={'circle'} size={'small'}/>
</Badge>
</Tooltip>,
<Divider type={'vertical'} key={'divider'}/>,
<Button key={'add'}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type={'primary'}>{'添加'}</Button>
]
}}
scroll={{
x: (columns?.length || 1) * 100,
y: 'calc(100vh - 290px)'
}}
search={false}
onRow={(record) => {
return {
className: cx({
// 'ant-table-row-selected': currentMovie?.id === record.id
}),
onClick: () => {
setModel(record)
}
}
}}
dateFormatter="string"
loading={isLoading || isFetching || curdLoading}
dataSource={data?.rows ?? []}
columns={columns}
options={{
reload: () => {
refetch()
},
}}
pagination={{
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {
...prev,
page: current,
pageSize: pageSize,
}
})
},
}}
/>
<BetaSchemaForm
grid={true}
shouldUpdate={false}
width={1000}
form={form}
layout={'vertical'}
scrollToFirstError={true}
title={form.getFieldValue('id') !== 0 ? '编辑' : '添加'}
{...formProps as any}
open={open}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
saveOrUpdate(values)
}}
columns={columns as ProFormColumnsType[]}/>
<BetaSchemaForm
title={'高级查询'}
grid={true}
shouldUpdate={false}
width={500}
form={filterForm}
open={openFilter}
onOpenChange={open => {
setFilterOpen(open)
}}
layout={'vertical'}
scrollToFirstError={true}
layoutType={formProps.layoutType as any}
drawerProps={{
...formProps.drawerProps,
mask: false,
}}
modalProps={{
...formProps.modalProps,
mask: false,
}}
submitter={{
searchConfig: {
resetText: '清空',
submitText: '查询',
},
onReset: () => {
filterForm.resetFields()
},
render: (props,) => {
return (
<div style={{ textAlign: 'right' }}>
<Space>
<Button onClick={() => {
props.reset()
}}>{props.searchConfig?.resetText}</Button>
<Button type="primary"
onClick={() => {
props.submit()
}}
>{props.searchConfig?.submitText}</Button>
</Space>
</div>
)
},
}}
onFinish={async (values) => {
//处理,变成数组
Object.keys(values).forEach(key => {
if (typeof values[key] === 'string' && values[key].includes(',')) {
values[key] = values[key].split(',')
}
})
setSearch(values)
}}
columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/>
</ListPageLayout>
</>
)
return <>
<XForm/>
</>
}
type XFormRouteSearch = {
@ -309,4 +23,4 @@ export const Route = createFileRoute('x-form')({
},
})
export default XForm
export default XFormRender

8
src/pages/x-form/utils/index.tsx

@ -1,4 +1,4 @@
import { XForm } from '@/types/x-form/model'
import { XFormTypes } from '@/types/x-form/model'
import { ProColumns } from '@ant-design/pro-components'
import Switch from '@/components/switch'
import { Checkbox, DatePicker, Input, Radio, Select, TreeSelect } from 'antd'
@ -6,7 +6,7 @@ import request from '@/request'
import { convertToBool, genProTableColumnWidthProps } from '@/utils'
import { ReactNode } from 'react'
const getValueType = (column: XForm.IColumn) => {
const getValueType = (column: XFormTypes.IColumn) => {
switch (column.type) {
case 'input':
return 'text'
@ -30,7 +30,7 @@ const getValueType = (column: XForm.IColumn) => {
}
//根据type返回对应的组件
const getComponent = (column: XForm.IColumn) => {
const getComponent = (column: XFormTypes.IColumn) => {
const type = getValueType(column) as any
switch (type) {
case 'input':
@ -56,7 +56,7 @@ const getComponent = (column: XForm.IColumn) => {
}
export const transformAntdTableProColumns = (columns: XForm.IColumn[]) => {
export const transformAntdTableProColumns = (columns: XFormTypes.IColumn[]) => {
return (columns || []).map(item => {
const { value, props, multiple, checkStrictly } = item

1
src/request.ts

@ -53,7 +53,6 @@ axiosInstance.interceptors.response.use(
const result = response.data as IApiResult
switch (result.code) {
case 0:
return response
case 200:
//login
if (response.config.url?.includes('/sys/login')) {

33
src/routes.tsx

@ -19,16 +19,17 @@ import {
createRouter, lazyRouteComponent,
Outlet,
redirect,
RouterProvider,
RouterProvider, useRouter, useRouterState,
// createHashHistory,
} from '@tanstack/react-router'
// import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { memo, useEffect, useRef } from 'react'
import { createElement, memo, useEffect, useRef } from 'react'
import RootLayout from './layout/RootLayout'
import { IRootContext, MenuItem } from './global'
// import { DevTools } from 'jotai-devtools'
import { useAtomValue } from 'jotai'
import { userMenuDataAtom } from '@/store/system/user.ts'
import { ApiContextProvider } from '@/context.ts'
const PageRootLayout = () => {
return <PageStoreProvider>
@ -189,6 +190,9 @@ const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
const path = menu.path?.replace(parentRoute.options?.path, '')
const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu'
const isApi = menu.type === 'api'
const api = menu.properties?.startsWith('{') ? JSON.parse(menu.properties)?.api : undefined
if (isLayout && (!menu.path || !menu.component)) {
//没有component的layout,直接返回
return createRoute({
@ -202,6 +206,8 @@ const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
const options = {
getParentRoute: () => parentRoute,
menu,
isApi,
api,
} as any
if (isLayout) {
@ -255,7 +261,6 @@ const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
}
return module().then((d: any) => {
// console.log(d)
if (d.Route) {
d.Route.update({
path: menu.path,
@ -270,7 +275,7 @@ const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => {
// 对menuData递归生成路由,只处理type =1 的菜单
const did = (menus: MenuItem[], parentRoute: AnyRoute) => {
return menus.filter((item) => item.type === 'menu').map((item, index) => {
return menus.filter((item) => [ 'menu', 'iframe', 'api' ].includes(item.type)).map((item, index) => {
// 如果有children则递归生成子路由,同样只处理type =1 的菜单
const route = generateRoutes(item, parentRoute)
@ -349,7 +354,25 @@ export const RootProvider = memo((props: { context: Partial<IRootContext> }) =>
// history: hashHistory,
context: { queryClient, menuData: [] },
defaultPreload: 'intent',
defaultPendingComponent: () => <Loading loading={true} delay={100}/>
defaultPendingComponent: () => <Loading loading={true} delay={100}/>,
InnerWrap: ({ children }) => {
const { matches } = useRouterState()
const { routesById } = useRouter()
//取最后一个match的route
const route = matches[matches.length - 1]
// 排除rootRoute
if (!route || route.id === '__root__') {
return <>{children}</>
}
const options = routesById[route.routeId].options
return <ApiContextProvider value={{
api: options?.api,
menu: options?.menu,
isApi: options?.isApi,
path: options?.path,
}}>{children}</ApiContextProvider>
}
})

4
src/service/base.ts

@ -1,6 +1,6 @@
import { request, AxiosRequestConfig } from '@/request.ts'
import { IApiResult, IPage, IPageResult } from '@/global'
import { XForm } from '@/types/x-form/model'
import { XFormTypes } from '@/types/x-form/model'
export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequestConfig) => {
@ -48,7 +48,7 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
},
curd: async (params: any, opt?: AxiosRequestConfig) => {
const method = opt?.method || 'post'
return request[method]<XForm.IModelCURD>(`${api}/ui/curd`, { ...params }, {
return request[method]<XFormTypes.IModelCURD>(`${api}/ui/curd`, { ...params }, {
...options,
...opt
})

6
src/service/x-form/model.ts

@ -1,11 +1,11 @@
import { createCURD } from '@/service/base.ts'
import { XForm } from '@/types/x-form/model'
import { XFormTypes } from '@/types/x-form/model'
import request from '@/request.ts'
const model = (api: string) => {
return {
...createCURD<any, XForm.IModel>(api),
proxy: async <T = XForm.IModelCURD>(params?: {
...createCURD<any, XFormTypes.IModel>(api),
proxy: async <T = XFormTypes.IModelCURD>(params?: {
path: string,
body: any,
method?: string

4
src/store/app/package.ts

@ -14,7 +14,7 @@ type SearchParams = IPage & {
export const appPackageIdAtom = atom(0)
export const appPackageIdsAtom = atom<number[]>([])
export const appPackageIdsAtom = atom<number>(0)
export const appPackageAtom = atom<APP.IAppPackage>(undefined as unknown as APP.IAppPackage)
@ -78,7 +78,7 @@ export const deleteAppPackageAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteAppPackage' ],
mutationFn: async (ids: number) => {
return await aPPServ.delete({ id: ids } ?? get(appPackageIdsAtom))
return await aPPServ.delete(ids ?? get(appPackageIdsAtom) as number)
},
onSuccess: (res) => {
message.success('message.deleteSuccess')

3
src/store/system/menu.ts

@ -5,6 +5,7 @@ import { atom, createStore } from 'jotai'
import { message } from 'antd'
import { t } from '@/i18n.ts'
import { System } from '@/types'
import { treeStringToJson } from '@/utils/tree.ts'
@ -33,7 +34,7 @@ export const menuDataAtom = atomWithQuery((get) => {
return await systemServ.menus.list(page)
},
select: (res) => {
return res.data.rows ?? []
return treeStringToJson((res.data.rows ?? []), 'properties')
}
}
})

259
src/store/x-form/model.ts

@ -1,11 +1,17 @@
import { atom } from 'jotai'
import { Atom, atom } from 'jotai'
import { IApiResult, IPage, IPageResult } from '@/global'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import {
atomWithMutation,
atomWithQuery,
AtomWithQueryResult,
AtomWithMutationResult,
queryClientAtom
} from 'jotai-tanstack-query'
import { message } from 'antd'
import { t } from 'i18next'
import { XForm } from '@/types/x-form/model'
import { XFormTypes } from '@/types/x-form/model'
import * as modelServ from '@/service/x-form/model'
import { atomWithStorage } from 'jotai/utils'
import { PrimitiveAtom } from 'jotai/vanilla/atom'
type SearchParams = IPage & {
key?: string
@ -13,128 +19,171 @@ type SearchParams = IPage & {
}
export const apiAtom = atomWithStorage('api', '')
export type ModelContext = {
apiAtom: PrimitiveAtom<string>
modelIdAtom: PrimitiveAtom<number>
modelIdsAtom: PrimitiveAtom<number[]>
modelAtom: PrimitiveAtom<XFormTypes.IModel>
modelSearchAtom: PrimitiveAtom<SearchParams>
modelPageAtom: PrimitiveAtom<IPage>
modelCURDAtom: Atom<AtomWithQueryResult<XFormTypes.IModelCURD['data']>>
modelsAtom: Atom<AtomWithQueryResult<IPageResult<any>>>
saveOrUpdateModelAtom: Atom<AtomWithMutationResult<any, any, any, any>>
deleteModelAtom: Atom<AtomWithMutationResult<any, any, any, any>>
}
export const modelContext = new Map<string, ModelContext>()
// @ts-ignore fix debug modelContext
window.__MODELCONTEXT__ = modelContext
export const useSpanModel = (name: string) => {
if (modelContext.has(name)) {
return modelContext.get(name)
}
const apiAtom = atom('')
export const modelIdAtom = atom(0)
const modelIdAtom = atom(0)
export const modelIdsAtom = atom<number[]>([])
const modelIdsAtom = atom<number[]>([])
export const modelAtom = atom<XForm.IModel>(undefined as unknown as XForm.IModel)
const modelAtom = atom<XFormTypes.IModel>(undefined as unknown as XFormTypes.IModel)
export const modelSearchAtom = atom<SearchParams>({
key: '',
pageSize: 10,
page: 1,
} as SearchParams)
const modelSearchAtom = atom<SearchParams>({
key: '',
pageSize: 10,
page: 1,
} as SearchParams)
export const modelPageAtom = atom<IPage>({
pageSize: 10,
page: 1,
})
const modelPageAtom = atom<IPage>({
pageSize: 10,
page: 1,
})
export const modelCURDAtom = atomWithQuery<IApiResult<XForm.IModelCURD>, any, any, any>((get) => {
const modelCURDAtom = atomWithQuery<IApiResult<XFormTypes.IModelCURD>['data'], any, any, any>((get) => {
return {
enabled: !!get(apiAtom),
queryKey: [ 'modelCURD', get(apiAtom) ],
queryFn: async ({ queryKey: [ , api ] }) => {
return await modelServ.model(api).proxy()
},
select: (res) => {
return res.data.data
return {
enabled: !!get(apiAtom),
queryKey: [ 'modelCURD', get(apiAtom) ],
queryFn: async ({ queryKey: [ , api ] }) => {
return await modelServ.model(api).proxy()
},
select: (res) => {
return res.data.data
}
}
}
})
})
export const modelsAtom = atomWithQuery<IApiResult<IPageResult<any>>, any, any, any>((get) => {
const modelsAtom = atomWithQuery<IApiResult<IPageResult<any>>, any, any, any>((get) => {
const curd = get(modelCURDAtom)
return {
enabled: curd.isSuccess && !!get(apiAtom),
queryKey: [ 'models', get(modelSearchAtom), get(apiAtom) ],
queryFn: async ({ queryKey: [ , params, api ] }) => {
const curd = get(modelCURDAtom)
return {
enabled: curd.isSuccess && !!get(apiAtom),
queryKey: [ 'models', get(modelSearchAtom), get(apiAtom) ],
queryFn: async ({ queryKey: [ , params, api ] }) => {
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XForm.IModel>({
path: '/list',
body: params,
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XFormTypes.IModel>({
path: '/list',
body: params,
})
}
return await modelServ.model(api).list(params as SearchParams)
},
select: res => {
const data = res.data
data.rows = data.rows?.map(row => {
return {
...row,
//status: convertToBool(row.status)
}
})
return data
}
return await modelServ.model(api).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 saveOrUpdateModelAtom = atomWithMutation<IApiResult, XForm.IModel>((get) => {
return {
mutationKey: [ 'updateModel', get(apiAtom) ],
mutationFn: async (data) => {
const api = get(apiAtom)
if (!api) {
return Promise.reject('api 不能为空')
}
if (data.id === 0) {
const saveOrUpdateModelAtom = atomWithMutation<IApiResult, XFormTypes.IModel>((get) => {
return {
mutationKey: [ 'updateModel', get(apiAtom) ],
mutationFn: async (data) => {
const api = get(apiAtom)
if (!api) {
return Promise.reject('api 不能为空')
}
if (data.id === 0) {
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XFormTypes.IModel>({
body: data,
path: '/add',
})
}
return await modelServ.model(api).add(data)
}
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XForm.IModel>({
return await modelServ.model(api).proxy<XFormTypes.IModel>({
body: data,
path: '/add',
path: '/edit',
})
}
return await modelServ.model(api).add(data)
return await modelServ.model(api).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: [ 'models', get(modelSearchAtom) ] })
return res
}
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XForm.IModel>({
body: data,
path: '/edit',
})
}
return await modelServ.model(api).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: [ 'models', get(modelSearchAtom) ] })
return res
}
}
})
export const deleteModelAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteModel', get(apiAtom) ],
mutationFn: async (ids: number[]) => {
const api = get(apiAtom)
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XForm.IModel>({
body: ids ?? get(modelIdsAtom),
path: '/deletes',
})
})
const deleteModelAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteModel', get(apiAtom) ],
mutationFn: async (ids: number[]) => {
const api = get(apiAtom)
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<XFormTypes.IModel>({
body: ids ?? get(modelIdsAtom),
path: '/deletes',
})
}
return await modelServ.model(api).batchDelete(ids ?? get(modelIdsAtom))
},
onSuccess: (res) => {
message.success('message.deleteSuccess')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'models', get(modelSearchAtom) ] })
return res
}
return await modelServ.model(api).batchDelete(ids ?? get(modelIdsAtom))
},
onSuccess: (res) => {
message.success('message.deleteSuccess')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'models', get(modelSearchAtom) ] })
return res
}
}
})
})
const value = {
apiAtom,
modelIdAtom,
modelIdsAtom,
modelAtom,
modelSearchAtom,
modelPageAtom,
modelCURDAtom,
modelsAtom,
saveOrUpdateModelAtom,
deleteModelAtom,
} as ModelContext
modelContext.set(name, value)
return value
}

2
src/types/x-form/model.d.ts

@ -1,4 +1,4 @@
export namespace XForm {
export namespace XFormTypes {
export interface IModel {
id: number;
created_at: string;

3
src/utils/index.ts

@ -214,4 +214,5 @@ export const genProTableColumnWidthProps = (width: string | number) => {
style: { width: '100%' }
},
}
}
}

13
src/utils/tree.ts

@ -93,4 +93,17 @@ export function findValuePath<T>(tree: TreeNode<T>[], targetValue: string | numb
}
return null // 如果未找到目标值,则返回null
}
//将tree中指定key的string json转为json
export function treeStringToJson<T>(tree: TreeNode<T>[], key: string): TreeNode<T>[] {
return tree.map(node => {
const children = node.children ? treeStringToJson(node.children, key) : undefined
return {
...node,
[key]: node[key] ? JSON.parse(node[key] as string) : undefined,
children,
}
})
}

5
tsconfig.json

@ -19,8 +19,9 @@
/* Linting */
"noImplicitAny": false,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
/* Type checking */

34
vite.config.ts

@ -5,8 +5,23 @@ import { viteMockServe } from 'vite-plugin-mock'
import jotaiDebugLabel from 'jotai/babel/plugin-debug-label'
import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh'
//import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
const proxyMap = {
'/api/v1/package': 'http://154.88.7.8:45321',
'/api/v1/movie': 'http://47.113.117.106:10000',
} as Record<any, string>
const proxyConfig = Object.keys(proxyMap).reduce((acc, key) => {
acc[key] = {
target: proxyMap[key],
changeOrigin: true,
rewrite: (path: string) => path,
}
return acc
}, {} as Record<any, any>)
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
@ -26,24 +41,7 @@ export default defineConfig(({ mode }) => {
origin: '*'
},
proxy: {
'/api/v1/package': {
target: 'http://154.88.7.8:45321',
changeOrigin: true,
rewrite: (path) => {
//replace /api/v1/package to /package/v1
//http://154.88.7.8:45321/package/v1/list
// path = path.replace('/api/v1/package', '/package/v1')
// console.log(path)
return path
}
},
'/api/v1/movie': {
target: 'http://47.113.117.106:10000',
changeOrigin: true,
rewrite: (path) => {
return path
}
},
...proxyConfig,
'/api': {
target: env.API_URL,
changeOrigin: true,

Loading…
Cancel
Save