From 7bd0e534e4bfdd7cfe0a257872e333e6d3b77de9 Mon Sep 17 00:00:00 2001 From: dark Date: Sun, 25 Aug 2024 02:22:05 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84x-form=E4=B8=BA=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=94=AF=E6=8C=81=E5=9C=A8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E7=9A=84=E5=9F=BA=E7=A1=80=E4=B8=8A=E9=87=8D?= =?UTF-8?q?=E5=86=99=E5=88=97=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.cjs | 43 +-- package.json | 6 +- src/components/cascader/Cascader.tsx | 2 +- src/components/crazy-form/context.ts | 21 -- src/components/crazy-form/index.tsx | 222 -------------- src/components/crazy-form/tabs-form/TabForm.tsx | 80 ----- src/components/crazy-form/tabs-form/index.tsx | 375 ------------------------ src/components/crazy-form/typeing.d.ts | 74 ----- src/components/x-form/index.tsx | 319 ++++++++++++++++++++ src/components/x-form/style.ts | 13 + src/components/x-form/utils/index.tsx | 155 ++++++++++ src/context.ts | 19 ++ src/hooks/useNavigate.ts | 0 src/layout/EmptyLayout.tsx | 2 +- src/layout/RootLayout.tsx | 31 +- src/pages/app/package/index.tsx | 2 +- src/pages/system/menus/index.tsx | 16 +- src/pages/system/roles/index.tsx | 3 +- src/pages/x-form/hooks/useApi.tsx | 20 +- src/pages/x-form/index.tsx | 298 +------------------ src/pages/x-form/utils/index.tsx | 8 +- src/request.ts | 1 - src/routes.tsx | 33 ++- src/service/base.ts | 4 +- src/service/x-form/model.ts | 6 +- src/store/app/package.ts | 4 +- src/store/system/menu.ts | 3 +- src/store/x-form/model.ts | 259 +++++++++------- src/types/x-form/model.d.ts | 2 +- src/utils/index.ts | 3 +- src/utils/tree.ts | 13 + tsconfig.json | 5 +- vite.config.ts | 34 +-- 33 files changed, 816 insertions(+), 1260 deletions(-) delete mode 100644 src/components/crazy-form/context.ts delete mode 100644 src/components/crazy-form/index.tsx delete mode 100644 src/components/crazy-form/tabs-form/TabForm.tsx delete mode 100644 src/components/crazy-form/tabs-form/index.tsx delete mode 100644 src/components/crazy-form/typeing.d.ts create mode 100644 src/components/x-form/index.tsx create mode 100644 src/components/x-form/style.ts create mode 100644 src/components/x-form/utils/index.tsx create mode 100644 src/hooks/useNavigate.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b3d98db..c024deb 100644 --- a/.eslintrc.cjs +++ b/.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', + }, } diff --git a/package.json b/package.json index ba50a0b..c0e2852 100644 --- a/package.json +++ b/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", diff --git a/src/components/cascader/Cascader.tsx b/src/components/cascader/Cascader.tsx index 36b5a4b..4fd3245 100644 --- a/src/components/cascader/Cascader.tsx +++ b/src/components/cascader/Cascader.tsx @@ -20,7 +20,7 @@ export const Cascader = ({ value, options = [], fieldNames, ...props }: Cascader children: fieldNames?.children ?? 'children', } as any return ( - diff --git a/src/components/crazy-form/context.ts b/src/components/crazy-form/context.ts deleted file mode 100644 index 753f485..0000000 --- a/src/components/crazy-form/context.ts +++ /dev/null @@ -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) => void; - unRegForm: (name: string) => void; - onFormFinish: (name: string, formData: any) => void; - keyArray: string[]; - formArrayRef: React.MutableRefObject< - React.MutableRefObject | undefined>[] - >; - loading: boolean; - setLoading: (loading: boolean) => void; - formMapRef: React.MutableRefObject>; -} - | undefined ->(undefined) - diff --git a/src/components/crazy-form/index.tsx b/src/components/crazy-form/index.tsx deleted file mode 100644 index 95b3d99..0000000 --- a/src/components/crazy-form/index.tsx +++ /dev/null @@ -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 = { - layoutType: FormSchema['layoutType'] | 'TabsForm' -} & Omit, 'layout'> - - -const FormLayoutType = { - DrawerForm, - QueryFilter, - LightFilter, StepForm: ProStepsForm.StepForm, - - StepsForm: StepsForm, - ModalForm, - Embed, - Form: ProForm, - TabsForm, -} - -const CrazyBateForm = (props: CrazyBateFormProps) => { - - const { - columns, - layoutType = 'TabsForm', - type = 'form', - action, - shouldUpdate = (pre, next) => stringify(pre) !== stringify(next), - formRef: propsFormRef, - ...restProps - } = props - - const FormRenderComponents = (FormLayoutType[layoutType as 'TabsForm'] || - ProForm) as React.FC> - - const [ form ] = Form.useForm() - const formInstance = Form.useFormInstance() - - const [ , forceUpdate ] = useState<[]>([]) - const [ formDomsDeps, updatedFormDoms ] = useState<[]>(() => []) - - const formRef = useReactiveRef( - props.form || formInstance || form, - ) - const oldValuesRef = useRef() - const propsRef = useLatest(props) - - /** - * 生成子项,方便被 table 接入 - * - * @param items - */ - const genItems: ProFormRenderValueTypeHelpers['genItems'] = - useRefFunction((items: ProFormColumnsType[]) => { - return items - .filter((originItem) => { - return !(originItem.hideInForm && type === 'form') - }) - .sort((a, b) => { - if (b.order || a.order) { - return (b.order || 0) - (a.order || 0) - } - return (b.index || 0) - (a.index || 0) - }) - .map((originItem, index) => { - const title = runFunction( - originItem.title, - originItem, - 'form', - , - ) - - const item = omitUndefined({ - title, - label: title, - name: originItem.name, - valueType: runFunction(originItem.valueType, {}), - key: originItem.key || originItem.dataIndex || index, - columns: originItem.columns, - valueEnum: originItem.valueEnum, - dataIndex: originItem.dataIndex || originItem.key, - initialValue: originItem.initialValue, - width: originItem.width, - index: originItem.index, - readonly: originItem.readonly, - colSize: originItem.colSize, - colProps: originItem.colProps, - rowProps: originItem.rowProps, - className: originItem.className, - //@ts-ignore @ts-expect-error - tooltip: originItem.tooltip || originItem.tip, - dependencies: originItem.dependencies, - proFieldProps: originItem.proFieldProps, - ignoreFormItem: originItem.ignoreFormItem, - getFieldProps: originItem.fieldProps - ? () => - runFunction( - originItem.fieldProps, - formRef.current, - originItem, - ) - : undefined, - getFormItemProps: originItem.formItemProps - ? () => - runFunction( - originItem.formItemProps, - formRef.current, - originItem, - ) - : undefined, - render: originItem.render, - renderFormItem: originItem.renderFormItem, - renderText: originItem.renderText, - request: originItem.request, - params: originItem.params, - transform: originItem.transform, - convertValue: originItem.convertValue, - debounceTime: originItem.debounceTime, - defaultKeyWords: originItem.defaultKeyWords, - }) as ItemType - - return renderValueType(item, { - action, - type, - originItem, - formRef, - genItems, - }) - }) - .filter((field) => { - return Boolean(field) - }) - }) - - const onValuesChange: FormProps['onValuesChange'] = useCallback( - (changedValues: any, values: T) => { - const { onValuesChange: propsOnValuesChange } = propsRef.current - if ( - shouldUpdate === true || - (typeof shouldUpdate === 'function' && - shouldUpdate(values, oldValuesRef.current)) - ) { - updatedFormDoms([]) - } - oldValuesRef.current = values - propsOnValuesChange?.(changedValues, values) - }, - [ propsRef, shouldUpdate ], - ) - - const formChildrenDoms = useDeepCompareMemo(() => { - if (!formRef.current) return - // like StepsForm's columns but not only for StepsForm - if (columns.length && Array.isArray(columns[0])) return - return genItems(columns as ProFormColumnsType[]) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ columns, restProps?.open, action, type, formDomsDeps, !!formRef.current ]) - - /** - * Append layoutType component specific props - */ - const specificProps = useDeepCompareMemo(() => { - if (layoutType === 'StepsForm') { - return { - forceUpdate: forceUpdate, - columns: columns as ProFormColumnsType[][], - } - } - - return {} - }, [ columns, layoutType ]) - - useImperativeHandle( - propsFormRef, - () => { - return formRef.current - }, - [ formRef.current ], - ) - - return ( - { - if (propsFormRef) { - (propsFormRef as React.MutableRefObject>).current = - initForm - } - restProps?.onInit?.(_, initForm) - formRef.current = initForm - }} - form={props.form || form} - formRef={formRef} - onValuesChange={onValuesChange} - > - {formChildrenDoms} - - ) -} - -export default CrazyBateForm \ No newline at end of file diff --git a/src/components/crazy-form/tabs-form/TabForm.tsx b/src/components/crazy-form/tabs-form/TabForm.tsx deleted file mode 100644 index 8367e24..0000000 --- a/src/components/crazy-form/tabs-form/TabForm.tsx +++ /dev/null @@ -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> = { - tab?: string; - tabProps?: TabsProps; -} & CrazyChildFormProps - -const TabForm = >(props: TabFormProps) => { - - const formRef = useRef() - const context = useContext(CrazyFormProvide) - const tabContext = useContext(TabFormProvide) - - const { - onFinish, - tab, - formRef: propFormRef, - tabProps, - ...restProps - } = props - - noteOnce(!(restProps as any).submitter, 'TabForm 不包含提交按钮,请在 TabsForm 上') - - /** 重置 formRef */ - useImperativeHandle(propFormRef, () => formRef.current, [ - propFormRef?.current, - ]) - - /** Dom 不存在的时候解除挂载 */ - useEffect(() => { - if (!(props.name || props.tab)) return - const name = (props.name || props.tab)!.toString() - context?.regForm(name, props) - return () => { - context?.unRegForm(name) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - if (context && context?.formArrayRef) { - context.formArrayRef.current[tab || 0] = formRef - } - - return ( - { - if (restProps.name) { - context?.onFormFinish(restProps.name, values) - } - if (onFinish) { - context?.setLoading(true) - // 如果报错,直接抛出 - await onFinish?.(values) - - context?.setLoading(false) - return - } - - }} - onInit={(_, form) => { - formRef.current = form - if (context && context?.formArrayRef) { - context.formArrayRef.current[tab || 0] = formRef - } - restProps?.onInit?.(_, form) - }} - layout="vertical" - {...omit(restProps, [ 'layoutType', 'columns' ] as any[])} - /> - ) -} - -export default TabForm \ No newline at end of file diff --git a/src/components/crazy-form/tabs-form/index.tsx b/src/components/crazy-form/tabs-form/index.tsx deleted file mode 100644 index c810885..0000000 --- a/src/components/crazy-form/tabs-form/index.tsx +++ /dev/null @@ -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> = { - toggleProps: TabsProps - direction?: 'horizontal' | 'vertical' -} & Omit, 'toggleProps'> - -export const TabFormProvide = React.createContext | null>(null) - - -const TabsLayoutStrategy: Record< - string, - (dom: LayoutRenderDom) => React.ReactNode -> = { - horizontal({ toggleDom, formDom }) { - return ( - <> - - {toggleDom} - - - {formDom} - - - ) - }, - vertical({ stepsDom, formDom }) { - return ( - - - {React.cloneElement(stepsDom, { - style: { - height: '100%', - }, - })} - - -
- {formDom} -
- -
- ) - }, -} - - -const TabsForm = >( - props: TabsFormProps & { - children: React.ReactNode - } -) => { - - const { - toggleProps, - toggleFormRender, - direction = 'horizontal', - current: tab, - onCurrentChange, - submitter, - formRender, - onFinish, - formProps, - containerStyle, - formRef, - formMapRef: propsFormMapRef, - layoutRender: propsLayoutRender, - ...rest - } = props - - - const formDataRef = useRef(new Map>()) - const formMapRef = useRef(new Map()) - const formArrayRef = useRef< - React.MutableRefObject | undefined>[] - >([]) - const [ formArray, setFormArray ] = useState([]) - const [ loading, setLoading ] = useState(false) - - /** - * 受控的方式来操作表单 - */ - const [ tab, setTab ] = useMergedState(0, { - value: props.current, - onChange: props.onCurrentChange, - }) - - const layoutRender = useMemo(() => { - return TabsLayoutStrategy[direction] - }, [ direction ]) - - - /** - * 注册一个form进入,方便进行 props 的修改 - */ - const regForm = useCallback( - (name: string, childrenFormProps: TabFormProps) => { - if (!formMapRef.current.has(name)) { - setFormArray((oldFormArray) => [ ...oldFormArray, name ]) - } - formMapRef.current.set(name, childrenFormProps) - }, - [], - ) - - /** - * 解除挂载掉这个 form,同时步数 -1 - */ - const unRegForm = useCallback((name: string) => { - setFormArray((oldFormArray) => oldFormArray.filter((n) => n !== name)) - formMapRef.current.delete(name) - formDataRef.current.delete(name) - }, []) - - useImperativeHandle(propsFormMapRef, () => formArrayRef.current, [ - formArrayRef.current, - ]) - - useImperativeHandle( - formRef, - () => { - return formArrayRef.current[tab || 0]?.current - }, - [ tab, formArrayRef.current ], - ) - - /** - * ProForm处理了一下 from 的数据,在其中做了一些操作 如果使用 Provider 自带的,自带的数据处理就无法生效了 - */ - const onFormFinish = useCallback( - async (name: string, formData: any) => { - formDataRef.current.set(name, formData) - - - setLoading(true) - const values: any = merge( - {}, - ...Array.from(formDataRef.current.values()), - ) - try { - const success = await onFinish(values) - if (success) { - formArrayRef.current.forEach((form) => form.current?.resetFields()) - } - } catch (error) { - console.log(error) - } finally { - setLoading(false) - } - }, - [ lastStep, onFinish, setLoading, setTab ], - ) - - const toggleDoms = useMemo(() => { - const itemsProps = { - items: formArray.map((item) => { - const itemProps = formMapRef.current.get(item) - return { - key: item, - title: itemProps?.title, - ...itemProps?.tabProps, - } - }), - } - - return ( -
- - -
- ) - }, [ formArray, tab, toggleProps, onCurrentChange ]) - - const onSubmit = useRefFunction(() => { - const from = formArrayRef.current[tab] - from.current?.submit() - }) - - const submit = useMemo(() => { - return ( - submitter !== false && ( - - ) - ) - }, [ loading, onSubmit, submitter ]) - - - const submitterDom = useMemo(() => { - let buttons: (React.ReactElement | false)[] = [ submit ] - buttons = buttons.filter(React.isValidElement) - - if (submitter && submitter.render) { - const submitterProps: any = { - form: formArrayRef.current[tab]?.current, - onSubmit, - current: tab, - } - - return submitter.render( - submitterProps, - buttons as React.ReactElement[], - ) as React.ReactNode - } - if (submitter && submitter?.render === false) { - return null - } - return buttons as React.ReactElement[] - }, [ formArray.length, onSubmit, tab, submit, submitter ]) - - const formDom = useMemo(() => { - return toArray(props.children).map((item, index) => { - const itemProps = item.props as TabFormProps - const name = itemProps.name || `${index}` - /** 是否是当前的表单 */ - const isShow = tab === name - - const config = isShow - ? { - contentRender: formRender, - } - : {} - return ( -
- - {item} - -
- ) - }) - }, [ formProps, props.children, tab, formRender ]) - - const finalTabsDom = useMemo(() => { - if (toggleFormRender) { - return toggleFormRender( - formArray.map((item) => ({ - key: item, - title: formMapRef.current.get(item)?.title, - })), - toggleDoms, - ) as React.ReactElement - } - return toggleDoms - }, [ formArray, toggleDoms, toggleFormRender ]) - - const formContainer = useMemo( - () => ( -
- {formDom} - {toggleFormRender ? null : {submitterDom}} -
- ), - [ containerStyle, formDom, toggleFormRender, submitterDom ], - ) - - const tabsFormDom = useMemo(() => { - const doms = { - toggleDom: finalTabsDom, - formDom: formContainer, - } - - if (toggleFormRender) { - if (propsLayoutRender) { - return toggleFormRender(propsLayoutRender(doms), submitterDom) - } else { - return toggleFormRender(layoutRender(doms), submitterDom) - } - } - - if (propsLayoutRender) { - return propsLayoutRender(doms) - } - - return layoutRender(doms) - }, [ - finalTabsDom, - formContainer, - layoutRender, - toggleFormRender, - submitterDom, - propsLayoutRender, - ]) - - - return ( -
- - - {tabsFormDom} - - -
- ) - -} - -function TabsFormWarp>( - props: CrazyFormProps & { - children: any; - }, -) { - return ( - - {...props} /> - - ) -} - -TabsFormWarp.TabForm = TabForm -TabsFormWarp.useForm = Form.useForm - -export { TabsFormWarp as TabsForm } -export type { TabFormProps, CrazyFormProps as TabsFormProps } \ No newline at end of file diff --git a/src/components/crazy-form/typeing.d.ts b/src/components/crazy-form/typeing.d.ts deleted file mode 100644 index f3dd148..0000000 --- a/src/components/crazy-form/typeing.d.ts +++ /dev/null @@ -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> = { - - onFinish?: (values: T) => Promise; - current?: string; - /**、 - * 切换区域传透的Props - */ - toggleProps?: Record; - formProps?: ProFormProps; - onCurrentChange?: (current: string) => void; - /** 自定义步骤器 */ - toggleRender?: ( - items: { - key: string; - title?: React.ReactNode; - [key: string]: any - }[], - defaultDom: React.ReactNode, - ) => React.ReactNode; - /** @name 当前展示表单的 formRef */ - formRef?: React.MutableRefObject | undefined | null>; - /** @name 所有表单的 formMapRef */ - formMapRef?: React.MutableRefObject< - React.MutableRefObject | undefined>[] - >; - /** - * 自定义单个表单 - * - * @param form From 的 dom,可以放置到别的位置 - */ - toggleFormRender?: (from: React.ReactNode) => React.ReactNode; - - /** - * 自定义整个表单区域 - * - * @param form From 的 dom,可以放置到别的位置 - * @param submitter 操作按钮 - */ - formRender?: ( - from: React.ReactNode, - submitter: React.ReactNode, - ) => React.ReactNode; - /** 按钮的统一配置,优先级低于分步表单的配置 */ - submitter?: - | SubmitterProps<{ - current: string; //当前激活的toggle - form?: FormInstance; - }> - | false; - - containerStyle?: React.CSSProperties; - /** - * 自定義整個佈局。 - * - * @param layoutDom toggleDom 和 formDom 元素可以放置在任何地方。 - */ - layoutRender?: (layoutDom: { - toggleDom: React.ReactElement; - formDom: React.ReactElement; - }) => React.ReactNode; -} & Omit; - - -export type CrazyChildFormProps, U = Record> = { - - index?: number; -} & Omit, 'onFinish' | 'form'> & - Omit, 'submitter' | 'form'>; \ No newline at end of file diff --git a/src/components/x-form/index.tsx b/src/components/x-form/index.tsx new file mode 100644 index 0000000..975a4f8 --- /dev/null +++ b/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(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([]) + + 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) => [ + { + form.setFieldsValue(record) + setOpen(true) + }}>{'编辑'}, + { + deleteModel([ record.id ]) + }} + title={'确定要删除吗?'}> + + 删除 + + + ] + } 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 = <> + + + + return ( + <> + + + { + setSearch(prev => ({ + ...prev, + title: value + })) + }, + allowClear: true, + onChange: (e) => { + setSearchKey(e.target?.value) + }, + value: searchKey, + placeholder: '输入关键字搜索', + },*/ + actions: [ + + + + + + + ) + }, + + }} + + + 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[])}/> + + + + ) +} + +export default XForm \ No newline at end of file diff --git a/src/components/x-form/style.ts b/src/components/x-form/style.ts new file mode 100644 index 0000000..2524a20 --- /dev/null +++ b/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), + } +}) \ No newline at end of file diff --git a/src/components/x-form/utils/index.tsx b/src/components/x-form/utils/index.tsx new file mode 100644 index 0000000..3627864 --- /dev/null +++ b/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 + } + if (_scheam.valueType as string === 'select') { + return - + diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index bb6b5f9..e1baf2b 100644 --- a/src/pages/system/roles/index.tsx +++ b/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 (menuList!, value)}/> + checkedKeys={getTreeCheckedStatus(menuList!, value)}/> } diff --git a/src/pages/x-form/hooks/useApi.tsx b/src/pages/x-form/hooks/useApi.tsx index 76daace..1e6e396 100644 --- a/src/pages/x-form/hooks/useApi.tsx +++ b/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(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 } \ No newline at end of file diff --git a/src/pages/x-form/index.tsx b/src/pages/x-form/index.tsx index 1f5902e..845e4ef 100644 --- a/src/pages/x-form/index.tsx +++ b/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) => [ - { - form.setFieldsValue(record) - setOpen(true) - }}>{'编辑'}, - { - deleteModel([ record.id ]) - }} - title={'确定要删除吗?'}> - - 删除 - - - ] - } - ]) - }, [ 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} - - API - {api} { - updateApi(true) - }}/> - }> - - { - setSearch(prev => ({ - ...prev, - title: value - })) - }, - allowClear: true, - onChange: (e) => { - setSearchKey(e.target?.value) - }, - value: searchKey, - placeholder: '输入关键字搜索', - },*/ - actions: [ - - - - ] - }} - 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, - } - }) - }, - }} - /> - - { - setOpen(open) - }} - loading={isSubmitting} - onFinish={async (values) => { - saveOrUpdate(values) - }} - columns={columns as ProFormColumnsType[]}/> - { - 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 ( -
- - - - -
- ) - }, - - }} - - - 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[])}/> - -
- - ) + return <> + + } type XFormRouteSearch = { @@ -309,4 +23,4 @@ export const Route = createFileRoute('x-form')({ }, }) -export default XForm \ No newline at end of file +export default XFormRender \ No newline at end of file diff --git a/src/pages/x-form/utils/index.tsx b/src/pages/x-form/utils/index.tsx index da6d495..f16e53e 100644 --- a/src/pages/x-form/utils/index.tsx +++ b/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 diff --git a/src/request.ts b/src/request.ts index 595a841..805dc39 100644 --- a/src/request.ts +++ b/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')) { diff --git a/src/routes.tsx b/src/routes.tsx index 6e12e35..75640dd 100644 --- a/src/routes.tsx +++ b/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 @@ -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 }) => // history: hashHistory, context: { queryClient, menuData: [] }, defaultPreload: 'intent', - defaultPendingComponent: () => + defaultPendingComponent: () => , + 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 {children} + } }) diff --git a/src/service/base.ts b/src/service/base.ts index fbd73b1..36ab787 100644 --- a/src/service/base.ts +++ b/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 = (api: string, options?: AxiosRequestConfig) => { @@ -48,7 +48,7 @@ export const createCURD = (api: string, options?: AxiosRequest }, curd: async (params: any, opt?: AxiosRequestConfig) => { const method = opt?.method || 'post' - return request[method](`${api}/ui/curd`, { ...params }, { + return request[method](`${api}/ui/curd`, { ...params }, { ...options, ...opt }) diff --git a/src/service/x-form/model.ts b/src/service/x-form/model.ts index d9ae271..4d9b2c4 100644 --- a/src/service/x-form/model.ts +++ b/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(api), - proxy: async (params?: { + ...createCURD(api), + proxy: async (params?: { path: string, body: any, method?: string diff --git a/src/store/app/package.ts b/src/store/app/package.ts index 7e30a94..6c79018 100644 --- a/src/store/app/package.ts +++ b/src/store/app/package.ts @@ -14,7 +14,7 @@ type SearchParams = IPage & { export const appPackageIdAtom = atom(0) -export const appPackageIdsAtom = atom([]) +export const appPackageIdsAtom = atom(0) export const appPackageAtom = atom(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') diff --git a/src/store/system/menu.ts b/src/store/system/menu.ts index 6da3723..810ef02 100644 --- a/src/store/system/menu.ts +++ b/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') } } }) diff --git a/src/store/x-form/model.ts b/src/store/x-form/model.ts index fea1251..44bf607 100644 --- a/src/store/x-form/model.ts +++ b/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 + modelIdAtom: PrimitiveAtom + modelIdsAtom: PrimitiveAtom + modelAtom: PrimitiveAtom + modelSearchAtom: PrimitiveAtom + modelPageAtom: PrimitiveAtom + modelCURDAtom: Atom> + modelsAtom: Atom>> + saveOrUpdateModelAtom: Atom> + deleteModelAtom: Atom> +} + +export const modelContext = new Map() + +// @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([]) + const modelIdsAtom = atom([]) -export const modelAtom = atom(undefined as unknown as XForm.IModel) + const modelAtom = atom(undefined as unknown as XFormTypes.IModel) -export const modelSearchAtom = atom({ - key: '', - pageSize: 10, - page: 1, -} as SearchParams) + const modelSearchAtom = atom({ + key: '', + pageSize: 10, + page: 1, + } as SearchParams) -export const modelPageAtom = atom({ - pageSize: 10, - page: 1, -}) + const modelPageAtom = atom({ + pageSize: 10, + page: 1, + }) -export const modelCURDAtom = atomWithQuery, any, any, any>((get) => { + const modelCURDAtom = atomWithQuery['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>, any, any, any>((get) => { + const modelsAtom = atomWithQuery>, 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({ - path: '/list', - body: params, + if (api.startsWith('http')) { + return await modelServ.model(api).proxy({ + 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((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((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({ + body: data, + path: '/add', + }) + } + return await modelServ.model(api).add(data) + } if (api.startsWith('http')) { - return await modelServ.model(api).proxy({ + return await modelServ.model(api).proxy({ 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({ - 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({ - 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({ + 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 +} \ No newline at end of file diff --git a/src/types/x-form/model.d.ts b/src/types/x-form/model.d.ts index 1f36df1..fd25ef3 100644 --- a/src/types/x-form/model.d.ts +++ b/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; diff --git a/src/utils/index.ts b/src/utils/index.ts index db9ffb5..3abb93f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -214,4 +214,5 @@ export const genProTableColumnWidthProps = (width: string | number) => { style: { width: '100%' } }, } -} \ No newline at end of file +} + diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 17b2073..4a0c508 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -93,4 +93,17 @@ export function findValuePath(tree: TreeNode[], targetValue: string | numb } return null // 如果未找到目标值,则返回null +} + + +//将tree中指定key的string json转为json +export function treeStringToJson(tree: TreeNode[], key: string): TreeNode[] { + 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, + } + }) } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0ef4938..0e678ea 100644 --- a/tsconfig.json +++ b/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 */ diff --git a/vite.config.ts b/vite.config.ts index d4b1eb9..b3518e7 100644 --- a/vite.config.ts +++ b/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 + +const proxyConfig = Object.keys(proxyMap).reduce((acc, key) => { + acc[key] = { + target: proxyMap[key], + changeOrigin: true, + rewrite: (path: string) => path, + } + return acc +}, {} as Record) + // 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,