Browse Source

增加r-form渲染

main
dark 4 weeks ago
parent
commit
1461b27851
  1. 322
      src/components/r-form/index.tsx
  2. 13
      src/components/r-form/style.ts
  3. 90
      src/components/r-form/utils/index.tsx
  4. 15
      src/components/x-form/utils/index.tsx
  5. 26
      src/pages/r-form/index.tsx
  6. 29
      src/pages/r-form/style.ts
  7. 22
      src/service/r-form/model.ts
  8. 189
      src/store/r-form/model.ts
  9. 25
      src/types/r-form/model.d.ts
  10. 25
      src/utils/tree.ts

322
src/components/r-form/index.tsx

@ -0,0 +1,322 @@
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/r-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 { RFormTypes } from '@/types/r-form/model'
export interface RFormProps {
title?: ReactNode
namespace?: string
columns?: ProColumns[] //重写columns
}
const RForm = ({ namespace, columns: propColumns = [], title }: RFormProps) => {
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<RFormTypes.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?.columns || [], 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?.columns, 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?.form.layoutType === 'DrawerForm' ? {
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
{...curdModal?.table}
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
{...curdModal?.form}
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
{...curdModal?.form}
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 RForm

13
src/components/r-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),
}
})

90
src/components/r-form/utils/index.tsx

@ -0,0 +1,90 @@
import { RFormTypes } from '@/types/r-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 } from '@/utils'
import { has, get } from 'lodash'
import { mapTree } from '@/utils/tree.ts'
const getValueType = (column: RFormTypes.IColumn) => {
return column.valueType
}
//根据type返回对应的组件
const getComponent = (column: RFormTypes.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: RFormTypes.IColumn[], overwriteColumns?: ProColumns[]) => {
const overwriteKeys = [] as string[]
return (columns || []).map(item => {
const type = getValueType(item)
const overwrite = overwriteColumns?.find(i => i.dataIndex === item.dataIndex || item.name)
if (overwrite) {
overwriteKeys.push(item.prop)
}
return {
...item,
request: item.dicUrl ? async (params, props) => {
const { fieldProps: { dataFiledNames } } = props
const { value, res: resKey, label, disabled: disabledKey, children } = dataFiledNames || {}
const url = `/${item.dicUrl.replace(/^:/, '/')}`
return request[item.dicMethod || 'get'](url, params).then(res => {
const data = has(res.data, resKey) ? get(res.data, resKey) : res.data
return mapTree(data || [], (i: any) => {
const disabled = has(i, disabledKey) ? get(i, disabledKey) : ('status' in i ? !convertToBool(i.status) : false)
return {
title: i[label || 'label'],
label: i[label || 'label'],
value: i[value || 'id'],
disabled,
data: i
}
}, { children })
})
} : undefined,
// renderFormItem: (_scheam, config, dom) => {
//
// },
render: (text: any, _record: any) => {
return text
},
...overwrite
} as ProColumns
}).concat(overwriteColumns?.filter(i => !overwriteKeys.includes(i.dataIndex)) || [])
}

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

@ -4,6 +4,8 @@ import Switch from '@/components/switch'
import { Checkbox, DatePicker, Input, Radio, Select, TreeSelect } from 'antd' import { Checkbox, DatePicker, Input, Radio, Select, TreeSelect } from 'antd'
import request from '@/request' import request from '@/request'
import { convertToBool, genProTableColumnWidthProps } from '@/utils' import { convertToBool, genProTableColumnWidthProps } from '@/utils'
import { has, get } from 'lodash'
import { mapTree } from '@/utils/tree.ts'
const getValueType = (column: XFormTypes.IColumn) => { const getValueType = (column: XFormTypes.IColumn) => {
@ -109,13 +111,13 @@ export const transformAntdTableProColumns = (columns: XFormTypes.IColumn[], over
rowProps, rowProps,
request: item.dicUrl ? async (params, props) => { request: item.dicUrl ? async (params, props) => {
const { fieldProps: { dataFiledNames } } = props const { fieldProps: { dataFiledNames } } = props
const { value, res: resKey, label } = dataFiledNames || {}
const { value, res: resKey, label, disabled: disabledKey, children } = dataFiledNames || {}
const url = `/${item.dicUrl.replace(/^:/, '/')}` const url = `/${item.dicUrl.replace(/^:/, '/')}`
return request[item.dicMethod || 'get'](url, params).then(res => { 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)
const data = has(res.data, resKey) ? get(res.data, resKey) : res.data
return mapTree(data || [], (i: any) => {
const disabled = has(i, disabledKey) ? get(i, disabledKey) : ('status' in i ? !convertToBool(i.status) : false)
return { return {
title: i[label || 'label'], title: i[label || 'label'],
label: i[label || 'label'], label: i[label || 'label'],
@ -123,7 +125,8 @@ export const transformAntdTableProColumns = (columns: XFormTypes.IColumn[], over
disabled, disabled,
data: i data: i
} }
})
}, { children })
}) })
} : undefined, } : undefined,
renderFormItem: (_scheam, config) => { renderFormItem: (_scheam, config) => {

26
src/pages/r-form/index.tsx

@ -0,0 +1,26 @@
import { createFileRoute } from '@tanstack/react-router'
import RForm from '@/components/r-form'
const RFormRender = () => {
return <>
<RForm/>
</>
}
type RFormRouteSearch = {
api: string
}
// @ts-ignore fix route id
export const Route = createFileRoute('x-form')({
validateSearch: (search: Record<string, unknown>): RFormRouteSearch => {
// validate and parse the search params into a typed state
// console.log(search.id)
return {
api: (search.api ?? '') as string
} as RFormRouteSearch
},
})
export default RFormRender

29
src/pages/r-form/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}-r-form-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),
}
})

22
src/service/r-form/model.ts

@ -0,0 +1,22 @@
import { createCURD } from '@/service/base.ts'
import { RFormTypes } from '@/types/r-form/model'
import request from '@/request.ts'
const model = (api: string) => {
return {
...createCURD<any, RFormTypes.IModel>(api),
proxy: async <T = RFormTypes.IModelCURD>(params?: {
path: string,
body: any,
method?: string
}) => {
return request.post<T>(`/x_form/proxy`, {
api: `${api}/ui/curd`,
method: 'post',
...params
})
}
}
}
export { model }

189
src/store/r-form/model.ts

@ -0,0 +1,189 @@
import { Atom, atom } from 'jotai'
import { IApiResult, IPage, IPageResult } from '@/global'
import {
atomWithMutation,
atomWithQuery,
AtomWithQueryResult,
AtomWithMutationResult,
queryClientAtom
} from 'jotai-tanstack-query'
import { message } from 'antd'
import { t } from 'i18next'
import { RFormTypes } from '@/types/r-form/model'
import * as modelServ from '@/service/r-form/model'
import { PrimitiveAtom } from 'jotai/vanilla/atom'
type SearchParams = IPage & {
key?: string
api: string
}
export type ModelContext = {
apiAtom: PrimitiveAtom<string>
modelIdAtom: PrimitiveAtom<number>
modelIdsAtom: PrimitiveAtom<number[]>
modelAtom: PrimitiveAtom<RFormTypes.IModel>
modelSearchAtom: PrimitiveAtom<SearchParams>
modelPageAtom: PrimitiveAtom<IPage>
modelCURDAtom: Atom<AtomWithQueryResult<RFormTypes.IModelCURD['page']>>
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('')
const modelIdAtom = atom(0)
const modelIdsAtom = atom<number[]>([])
const modelAtom = atom<RFormTypes.IModel>(undefined as unknown as RFormTypes.IModel)
const modelSearchAtom = atom<SearchParams>({
key: '',
pageSize: 10,
page: 1,
} as SearchParams)
const modelPageAtom = atom<IPage>({
pageSize: 10,
page: 1,
})
const modelCURDAtom = atomWithQuery<IApiResult<RFormTypes.IModelCURD>, 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.page
}
}
})
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 ] }) => {
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<RFormTypes.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
}
}
})
//saveOrUpdateAtom
const saveOrUpdateModelAtom = atomWithMutation<IApiResult, RFormTypes.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<RFormTypes.IModel>({
body: data,
path: '/add',
})
}
return await modelServ.model(api).add(data)
}
if (api.startsWith('http')) {
return await modelServ.model(api).proxy<RFormTypes.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
}
}
})
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<RFormTypes.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
}
}
})
const value = {
apiAtom,
modelIdAtom,
modelIdsAtom,
modelAtom,
modelSearchAtom,
modelPageAtom,
modelCURDAtom,
modelsAtom,
saveOrUpdateModelAtom,
deleteModelAtom,
} as ModelContext
modelContext.set(name, value)
return value
}

25
src/types/r-form/model.d.ts

@ -0,0 +1,25 @@
import { ProColumns, ProTableProps, } from '@ant-design/pro-components'
import { FormSchema } from '@ant-design/pro-form/es/components/SchemaForm/typing'
export namespace RFormTypes {
export interface IModel {
id: number;
created_at: string;
updated_at: string;
[key: string]: any;
}
export interface IModelCURD {
page: {
columns: ProColumns[];
config: any;
table: ProTableProps<any, any>
form: FormSchema
}
}
}

25
src/utils/tree.ts

@ -107,3 +107,28 @@ export function treeStringToJson<T>(tree: TreeNode<T>[], key: string): TreeNode<
} }
}) })
} }
//遍历tree, 对每个节点执行fn
export function traverseTree<T>(tree: TreeNode<T>[], fn: (node: TreeNode<T>) => void, filedNames?: FiledNames): void {
const { children = 'children' } = filedNames ?? { children: 'children' }
const did = (node: TreeNode<T>) => {
fn(node)
if (node[children]) {
node[children].forEach(did)
}
}
tree.forEach(did)
}
//遍历tree, 对每个节点执行fn, 返回新的tree
export function mapTree<T, R>(tree: TreeNode<T>[], fn: (node: TreeNode<T>) => R, filedNames?: FiledNames): TreeNode<R>[] {
const { children = 'children' } = filedNames ?? { children: 'children' }
const did = (node: TreeNode<T>): TreeNode<R> => {
const newNode = fn(node)
if (node[children]) {
newNode[children] = node[children].map(did)
}
return newNode as TreeNode<R>
}
return tree.map(did)
}
Loading…
Cancel
Save