From 0e8fb0bc319cb1341f9c0889b96f41e161d072ca Mon Sep 17 00:00:00 2001 From: lingting Date: Thu, 14 Oct 2021 15:18:50 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat(Page):=20=E9=A1=B5=E9=9D=A2=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/FullForm.tsx | 10 +- src/components/Form/ModalForm.tsx | 12 +- src/components/Form/index.ts | 9 +- src/components/Page/BasePage.tsx | 175 ++++++++++++++++++++++++++++++ src/components/Page/FullPage.tsx | 92 ++++++---------- src/components/Page/ModalPage.tsx | 92 ++++++---------- src/components/Page/typings.ts | 47 ++++---- src/components/Page/utils.tsx | 141 ------------------------ src/layouts/BasicLayout.tsx | 23 ++-- 9 files changed, 291 insertions(+), 310 deletions(-) create mode 100644 src/components/Page/BasePage.tsx delete mode 100644 src/components/Page/utils.tsx diff --git a/src/components/Form/FullForm.tsx b/src/components/Form/FullForm.tsx index f3d551f..99b31f3 100644 --- a/src/components/Form/FullForm.tsx +++ b/src/components/Form/FullForm.tsx @@ -5,15 +5,9 @@ import ProForm from '@ant-design/pro-form'; import type { R } from '@/typings'; import I18n from '@/utils/I18nUtils'; import type { FullFormProps } from '.'; +import { defautlTitle } from '.'; import { Button, Card } from 'antd'; -export const defautlTitle = { - read: I18n.text('form.read'), - edit: I18n.text('form.edit'), - create: I18n.text('form.create'), - del: I18n.text('form.del'), -}; - const FullForm = (props: FullFormProps) => { const { formRef: currencyRef, @@ -121,10 +115,10 @@ const FullForm = (props: FullFormProps) => { ]; }, }} - {...antProps} layout="horizontal" labelCol={labelCol || { sm: { span: 24 }, md: { span: 4 } }} wrapperCol={wrapperCol} + {...antProps} formRef={formRef} onFinish={async (values) => { switch (status) { diff --git a/src/components/Form/ModalForm.tsx b/src/components/Form/ModalForm.tsx index cdcfd35..64ffd93 100644 --- a/src/components/Form/ModalForm.tsx +++ b/src/components/Form/ModalForm.tsx @@ -4,13 +4,7 @@ import type { ProFormInstance } from '@ant-design/pro-form'; import { ModalForm as AntdModalForm } from '@ant-design/pro-form'; import type { R } from '@/typings'; import I18n from '@/utils/I18nUtils'; - -export const defautlTitle = { - read: I18n.text('form.read'), - edit: I18n.text('form.edit'), - create: I18n.text('form.create'), - del: I18n.text('form.del'), -}; +import { defautlTitle } from '.'; const ModalForm = (props: ModalFormProps) => { const { @@ -95,13 +89,13 @@ const ModalForm = (props: ModalFormProps) => { return ( - {...antProps} width={width} layout="horizontal" labelCol={labelCol || { sm: { span: 24 }, md: { span: 4 } }} wrapperCol={wrapperCol} - formRef={formRef} title={modalTitle} + {...antProps} + formRef={formRef} visible={visible} onVisibleChange={setVisible} onFinish={async (values) => { diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index fc05cd0..340dae4 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -4,6 +4,7 @@ import FormDictSelect from './dict/FormDictSelect'; import FormNumber from './FormNumber'; import FormGroup from './FormGroup'; import FormDictCheckbox from './dict/FormDictCheckbox'; +import I18n from '@/utils/I18nUtils'; export * from './typings'; @@ -18,4 +19,10 @@ const Form = { export default Form; -export { ModalForm, FormDictRadio, FormDictSelect, FormNumber }; +const defautlTitle = { + read: I18n.text('form.read'), + edit: I18n.text('form.edit'), + create: I18n.text('form.create'), + del: I18n.text('form.del'), +}; +export { ModalForm, FormDictRadio, FormDictSelect, FormNumber, defautlTitle }; diff --git a/src/components/Page/BasePage.tsx b/src/components/Page/BasePage.tsx new file mode 100644 index 0000000..95b1ef1 --- /dev/null +++ b/src/components/Page/BasePage.tsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Table from '@/components/Table'; +import type { ActionType, ProColumns } from '@ant-design/pro-table'; +import type { BasePageProps } from '.'; +import Auth from '../Auth'; +import { defautlTitle } from '../Form'; +import I18n from '@/utils/I18nUtils'; + +const BasePage = ({ + title, + rowKey, + query, + columns, + toolBarActions, + children, + operateBar, + operteBarProps, + del, + formData = (data) => data as unknown as E, + tableProps, + tableRef: pTableRef, + formRef, + perStatusChange = () => undefined, +}: BasePageProps) => { + let tableRef = useRef(); + + if (pTableRef) { + tableRef = pTableRef; + } + + const [toolBarActionsList, setToolBarActionsList] = useState([]); + const [tableColumns, setTableColumns] = useState[]>([]); + + // 表格上方工具栏更新 + useEffect(() => { + const list: React.ReactNode[] = []; + if (toolBarActions && toolBarActions.length > 0) { + toolBarActions.forEach((tb) => { + if (!React.isValidElement(tb)) { + list.push( + { + if (perStatusChange('create') === false) { + return; + } + formRef.current?.create(); + }} + />, + ); + } else { + list.push(tb); + } + }); + } + setToolBarActionsList(list); + }, [toolBarActions]); + + // 表格列更新 + useEffect(() => { + const newColumns = columns ? [...columns] : []; + + if (operateBar && operateBar.length > 0) { + newColumns.push({ + title: I18n.text('form.operate'), + width: 160, + fixed: 'right', + ...operteBarProps, + hideInSearch: true, + render: (dom, record) => { + const nodes: React.ReactNode[] = []; + + operateBar.forEach((ob) => { + if (typeof ob === 'function') { + nodes.push(ob(dom, record)); + return; + } + + let obProps; + + if (typeof ob.props === 'function') { + obProps = ob.props(record); + } else { + obProps = ob.props; + } + + if (ob.type === 'read') { + nodes.push( + { + if (perStatusChange('read') === false) { + return; + } + formRef.current?.read(formData(record)); + }} + />, + ); + } else if (ob.type === 'edit') { + nodes.push( + { + if (perStatusChange('edit') === false) { + return; + } + formRef.current?.edit(formData(record)); + }} + />, + ); + } else { + nodes.push( + { + if (del === undefined) { + I18n.error({ key: 'orm.error.request', params: { title: defautlTitle.del } }); + return; + } + del(record).then(() => { + tableRef.current?.reload(); + I18n.success('global.operation.success'); + }); + }} + />, + ); + } + }); + + return {nodes}; + }, + }); + } + + setTableColumns(newColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [columns, operateBar]); + + return ( + <> + + {...tableProps} + rowKey={rowKey} + columns={tableColumns} + request={query} + headerTitle={title} + actionRef={tableRef} + toolbar={{ ...tableProps?.toolbar, actions: toolBarActionsList }} + /> + + {children} + + ); +}; + +export default BasePage; diff --git a/src/components/Page/FullPage.tsx b/src/components/Page/FullPage.tsx index 0173991..c3c9862 100644 --- a/src/components/Page/FullPage.tsx +++ b/src/components/Page/FullPage.tsx @@ -1,11 +1,10 @@ -import React, { useState, useEffect, useRef } from 'react'; -import Table from '@/components/Table'; -import type { ActionType, ProColumns } from '@ant-design/pro-table'; +import React, { useState, useRef } from 'react'; +import type { ActionType } from '@ant-design/pro-table'; import type { FormStatus, FullFormRef } from '@/components/Form'; import I18n from '@/utils/I18nUtils'; -import utils from './utils'; import FullForm from '../Form/FullForm'; import type { FullPageProps } from '.'; +import BasePage from './BasePage'; const ModalPage = ({ title, @@ -43,40 +42,8 @@ const ModalPage = ({ tableRef = pTableRef; } - const [toolBarActionsList, setToolBarActionsList] = useState([]); - const [tableColumns, setTableColumns] = useState[]>([]); const [tableStyle, setTableStyle] = useState({}); - // 表格上方工具栏更新 - useEffect(() => { - setToolBarActionsList( - utils.generateToolBarActionsList(perStatusChange, formRef, toolBarActions), - ); - }, [toolBarActions]); - - // 表格列更新 - useEffect(() => { - const newColumns = columns ? [...columns] : []; - - if (operateBar && operateBar.length > 0) { - newColumns.push( - utils.generateOperateBar( - operateBar, - rowKey, - perStatusChange, - formData, - formRef, - tableRef, - del, - operteBarProps, - ), - ); - } - - setTableColumns(newColumns); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columns, operateBar]); - const formStatusChange = (status: FormStatus) => { if (status) { // 表单状态不为空 @@ -91,33 +58,38 @@ const ModalPage = ({ return ( <> - - {...tableProps} - style={tableStyle} + rowKey={rowKey} - columns={tableColumns} - request={query} - headerTitle={title} - actionRef={tableRef} - toolbar={{ ...tableProps?.toolbar, actions: toolBarActionsList }} - /> - - - {...formProps} + columns={columns} + query={query} + title={title} + toolBarActions={toolBarActions} + operateBar={operateBar} + operteBarProps={operteBarProps} + perStatusChange={perStatusChange} + formData={formData} + del={del} + tableProps={{ ...tableProps, style: tableStyle }} + tableRef={tableRef} formRef={formRef} - onStatusChange={formStatusChange} - handlerData={handlerData} - create={create} - edit={edit} - onFinish={(st, body) => { - tableRef.current?.reload(); - onFinish(st, body); - I18n.success('global.operation.success'); - }} - onError={onError} > - {children} - + + {...formProps} + formRef={formRef} + onStatusChange={formStatusChange} + handlerData={handlerData} + create={create} + edit={edit} + onFinish={(st, body) => { + tableRef.current?.reload(); + onFinish(st, body); + I18n.success('global.operation.success'); + }} + onError={onError} + > + {children} + + ); }; diff --git a/src/components/Page/ModalPage.tsx b/src/components/Page/ModalPage.tsx index 46a638d..c75348b 100644 --- a/src/components/Page/ModalPage.tsx +++ b/src/components/Page/ModalPage.tsx @@ -1,11 +1,10 @@ -import React, { useState, useEffect, useRef } from 'react'; -import Table from '@/components/Table'; -import type { ActionType, ProColumns } from '@ant-design/pro-table'; +import { useRef } from 'react'; +import type { ActionType } from '@ant-design/pro-table'; import type { ModalFormRef } from '@/components/Form'; import ModalForm from '@/components/Form/ModalForm'; import type { ModalPageProps } from './typings'; import I18n from '@/utils/I18nUtils'; -import utils from './utils'; +import BasePage from './BasePage'; const ModalPage = ({ title, @@ -17,6 +16,7 @@ const ModalPage = ({ create, edit, onFinish = () => {}, + onError = () => {}, children, operateBar, operteBarProps, @@ -42,66 +42,40 @@ const ModalPage = ({ tableRef = pTableRef; } - const [toolBarActionsList, setToolBarActionsList] = useState([]); - const [tableColumns, setTableColumns] = useState[]>([]); - - // 表格上方工具栏更新 - useEffect(() => { - setToolBarActionsList( - utils.generateToolBarActionsList(perStatusChange, formRef, toolBarActions), - ); - }, [toolBarActions]); - - // 表格列更新 - useEffect(() => { - const newColumns = columns ? [...columns] : []; - - if (operateBar && operateBar.length > 0) { - newColumns.push( - utils.generateOperateBar( - operateBar, - rowKey, - perStatusChange, - formData, - formRef, - tableRef, - del, - operteBarProps, - ), - ); - } - - setTableColumns(newColumns); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columns, operateBar]); - return ( <> - - {...tableProps} + rowKey={rowKey} - columns={tableColumns} - request={query} - headerTitle={title} - actionRef={tableRef} - toolbar={{ ...tableProps?.toolbar, actions: toolBarActionsList }} - /> - - - {...formProps} + columns={columns} + query={query} + title={title} + toolBarActions={toolBarActions} + operateBar={operateBar} + operteBarProps={operteBarProps} + perStatusChange={perStatusChange} + formData={formData} + del={del} + tableProps={{ ...tableProps }} + tableRef={tableRef} formRef={formRef} - onStatusChange={onStatusChange} - handlerData={handlerData} - create={create} - edit={edit} - onFinish={(st, body) => { - tableRef.current?.reload(); - onFinish(st, body); - I18n.success('global.operation.success'); - }} > - {children} - + + {...formProps} + formRef={formRef} + onStatusChange={onStatusChange} + handlerData={handlerData} + create={create} + edit={edit} + onFinish={(st, body) => { + tableRef.current?.reload(); + onFinish(st, body); + I18n.success('global.operation.success'); + }} + onError={onError} + > + {children} + + ); }; diff --git a/src/components/Page/typings.ts b/src/components/Page/typings.ts index f4451e1..4df2dd3 100644 --- a/src/components/Page/typings.ts +++ b/src/components/Page/typings.ts @@ -1,7 +1,7 @@ import type { PageResult, QueryParam, R } from '@/typings'; import type React from 'react'; import type { ActionType, ProColumns } from '@ant-design/pro-table'; -import type { FormStatus, FullFormProps, FullFormRef, ModalFormRef } from '../Form'; +import type { FormRef, FormStatus, FullFormProps, FullFormRef, ModalFormRef } from '../Form'; import type { AuthNoneOptionalProps } from '../Auth'; import type { TableProps } from '../Table/typings'; import type { ModalFormProps } from '@/components/Form'; @@ -19,25 +19,11 @@ export type PageOperateBar = | PageOperateBarPreset | ((dom: React.ReactNode, data: T) => JSX.Element); -// t : 表格字典 -// u : 表格请求字段 -// e : 表单字段 -// p : 表单请求字段 -export type PageProps = { +export type BasePageProps = { rowKey: string; columns: ProColumns[]; query: (params: QueryParam) => Promise>>; title?: string; - // 创建请求 - create?: (body: P) => Promise>; - // 编辑请求 - edit?: (body: P) => Promise>; - // 删除请求 - del?: (body: T) => Promise>; - // 表单回填数据: 查询返回的数据 转为 表单展示的数据 - formData?: (data: T) => E; - // 请求数据处理, 处理后的数据用来发起 创建, 编辑, 删除请求 - handlerData?: (body: E, status: FormStatus) => P; // 表格上方工具栏 toolBarActions?: PageToolBarActions[]; // 表格右侧操作列 @@ -49,17 +35,36 @@ export type PageProps = { * 变更为新增时, record 为 undefined */ perStatusChange?: (st: FormStatus, record?: T) => boolean | void; + // 表单回填数据: 查询返回的数据 转为 表单展示的数据 + formData?: (data: T) => E; + // 删除请求 + del?: (body: T) => Promise>; + children?: React.ReactNode | React.ReactNode[]; + // rowKey columns 等 无法配置 + tableProps?: TableProps; + tableRef?: React.MutableRefObject; + formRef: React.MutableRefObject | undefined>; +}; + +// t : 表格字典 +// u : 表格请求字段 +// e : 表单字段 +// p : 表单请求字段 +export type PageProps = { + // 创建请求 + create?: (body: P) => Promise>; + // 编辑请求 + edit?: (body: P) => Promise>; + // 请求数据处理, 处理后的数据用来发起 创建, 编辑, 删除请求 + handlerData?: (body: E, status: FormStatus) => P; // 状态变更时执行 onStatusChange?: (st: FormStatus) => void; // 创建, 编辑 请求完成后执行 onFinish?: (status: FormStatus, body: P) => void; // 创建, 编辑 请求出错成后执行 onError?: (e: any) => void; - children?: React.ReactNode | React.ReactNode[]; - // rowKey columns 等 无法配置 - tableProps?: TableProps; - tableRef?: React.MutableRefObject; -}; + formRef?: React.MutableRefObject | undefined>; +} & Omit, 'formRef'>; export type ModalPageProps = { // 部分无法配置 diff --git a/src/components/Page/utils.tsx b/src/components/Page/utils.tsx deleted file mode 100644 index 4f957dd..0000000 --- a/src/components/Page/utils.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import type { PageOperateBar, PageToolBarActions } from '.'; -import React from 'react'; -import type { ActionType, ProColumns } from '@ant-design/pro-table'; -import type { FormStatus, ModalFormRef } from '@/components/Form'; -import Auth from '@/components/Auth'; -import I18n from '@/utils/I18nUtils'; -import { defautlTitle } from '@/components/Form/ModalForm'; -import type { R } from '@/typings'; - -type PSC = (st: FormStatus, record?: T) => boolean | void; - -export default { - generateToolBarActionsList( - perStatusChange: PSC, - formRef: React.MutableRefObject | undefined>, - actions?: PageToolBarActions[], - ) { - if (actions && actions.length > 0) { - const tbList: React.ReactNode[] = []; - actions.forEach((tb) => { - if (!React.isValidElement(tb)) { - tbList.push( - { - if (perStatusChange('create') === false) { - return; - } - formRef.current?.create(); - }} - />, - ); - } else { - tbList.push(tb); - } - }); - return tbList; - } - - return []; - }, - generateOperateBar( - operateBar: PageOperateBar[], - rowKey: string, - perStatusChange: PSC, - formData: (data: T) => E, - formRef: React.MutableRefObject | undefined>, - tableRef: React.MutableRefObject, - del?: (body: T) => Promise>, - operteBarProps?: { title?: string; width?: number; fixed?: 'left' | 'right' | boolean }, - ): ProColumns { - return { - title: I18n.text('form.operate'), - width: 160, - fixed: 'right', - ...operteBarProps, - hideInSearch: true, - render: (dom, record) => { - const nodes: React.ReactNode[] = []; - - operateBar.forEach((ob) => { - if (typeof ob === 'function') { - nodes.push(ob(dom, record)); - return; - } - - let obProps; - - if (typeof ob.props === 'function') { - obProps = ob.props(record); - } else { - obProps = ob.props; - } - - if (ob.type === 'read') { - nodes.push( - { - if (perStatusChange('read') === false) { - return; - } - formRef.current?.read(formData(record)); - }} - />, - ); - } else if (ob.type === 'edit') { - nodes.push( - { - if (perStatusChange('edit') === false) { - return; - } - formRef.current?.edit(formData(record)); - }} - />, - ); - } else { - nodes.push( - { - if (del === undefined) { - I18n.error({ key: 'orm.error.request', params: { title: defautlTitle.del } }); - return; - } - del(record).then(() => { - tableRef.current?.reload(); - I18n.success('global.operation.success'); - }); - }} - />, - ); - } - }); - - return {nodes}; - }, - }; - }, -}; diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 3a30046..b01fa31 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -100,6 +100,12 @@ const BasicLayout: React.FC = (props) => { I18n.setIntl(useIntl()); const { initialState, setInitialState } = useModel('@@initialState'); + let renderDom = children; + + if (reload) { + renderDom = ; + } + useEffect(() => { if (!route.children) { route.children = []; @@ -182,7 +188,6 @@ const BasicLayout: React.FC = (props) => { }} menuItemRender={(menuItemProps) => { const { redirectPath, title, icon } = menuItemProps.meta; - if (!menuItemProps.path || location.pathname === menuItemProps.path) { return renderMenuItem(collapsed, title, false, icon); } @@ -203,16 +208,12 @@ const BasicLayout: React.FC = (props) => { }} rightContentRender={() => } > - {reload ? ( - - ) : ( - - {children} - - )} + + {renderDom} + ); }; From f3dfa775e3b9d8a22ac4d3c9fda984790a69b36b Mon Sep 17 00:00:00 2001 From: lingting Date: Thu, 14 Oct 2021 21:54:39 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(MultiTab):=20=E5=A4=9A=E9=A1=B5?= =?UTF-8?q?=E7=AD=BE=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings.ts | 3 + src/components/MultiTab/index.less | 93 +++++++++++++++ src/components/MultiTab/index.tsx | 177 +++++++++++++++++++++++++++++ src/components/MultiTab/typings.ts | 14 +++ src/layouts/BasicLayout.tsx | 18 ++- 5 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 src/components/MultiTab/index.less create mode 100644 src/components/MultiTab/index.tsx create mode 100644 src/components/MultiTab/typings.ts diff --git a/config/settings.ts b/config/settings.ts index d6f31f4..1e5be70 100644 --- a/config/settings.ts +++ b/config/settings.ts @@ -10,6 +10,8 @@ export type ProjectSetting = LayoutSettings & { defaultLocal: 'zh-CN' | 'en-US'; // 是否展示水印 waterMark: boolean; + // 是否展示顶部多页签 + multiTab: boolean; storageOptions: { // 缓存key 前缀 namespace: string; @@ -33,6 +35,7 @@ const Settings: ProjectSetting = { historyType: 'hash', defaultLocal: 'zh-CN', waterMark: true, + multiTab: true, storageOptions: { namespace: 'ballcat/', storage: 'local', diff --git a/src/components/MultiTab/index.less b/src/components/MultiTab/index.less new file mode 100644 index 0000000..06a5ee3 --- /dev/null +++ b/src/components/MultiTab/index.less @@ -0,0 +1,93 @@ +@import '~antd/es/style/themes/default.less'; + +@multi-tab-prefix-cls: ballcat-multi-tab; + +.@{multi-tab-prefix-cls} { + position: relative; + z-index: 16; + height: 40px; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); + + &-fixed { + top: 48px; + } +} + +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav { + margin: 0; + border-bottom: none; +} + +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav .ant-tabs-nav-wrap .ant-tabs-tab { + height: 40px; + margin: 0; + padding: 0; + line-height: 40px; + background: none; + border: none; + border-radius: 0; + transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); +} + +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav .ant-tabs-nav-wrap .ant-tabs-tab-next, +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav .ant-tabs-nav-wrap .ant-tabs-tab-prev { + width: 40px; + line-height: 1; + opacity: 1; + transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + pointer-events: auto; + + .anticon { + font-size: 14px; + } +} + +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav .ant-tabs-nav-wrap { + height: auto; +} + +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav .ant-tabs-nav-wrap .ant-tabs-tab > div { + padding: 0 0 0 16px; +} + +.@{multi-tab-prefix-cls} .ant-tabs .ant-tabs-nav .ant-tabs-nav-wrap .ant-tabs-tab-active { + background: rgba(24, 144, 255, 0.08); +} + +.@{multi-tab-prefix-cls} + .ant-tabs + .ant-tabs-nav + .ant-tabs-nav-wrap + .ant-tabs-tab + > div.ant-tabs-tab-unclosable { + padding-right: 16px; +} + +.@{multi-tab-prefix-cls} + .ant-tabs + .ant-tabs-nav + .ant-tabs-nav-wrap + .ant-tabs-tab + .ant-tabs-tab-remove { + height: 40px; + margin: -4px 0 0 0; +} + +.@{multi-tab-prefix-cls} .ant-tabs .multi-tab-drop { + width: 40px; + height: 40px; + line-height: 40px; + text-align: center; + cursor: pointer; + + .anticon { + font-size: 14px; + } +} + +.@{multi-tab-prefix-cls} .ant-tabs-nav-operations .ant-tabs-nav-more { + cursor: pointer; +} diff --git a/src/components/MultiTab/index.tsx b/src/components/MultiTab/index.tsx new file mode 100644 index 0000000..3975fef --- /dev/null +++ b/src/components/MultiTab/index.tsx @@ -0,0 +1,177 @@ +import React, { useEffect, useState, useCallback, useImperativeHandle } from 'react'; +import { Tabs, Menu, Dropdown } from 'antd'; +import './index.less'; +import I18n from '@/utils/I18nUtils'; +import { useModel } from 'umi'; + +import type { MultiTabProps } from './typings'; + +export * from './typings'; + +const { TabPane } = Tabs; + +const MultiTab = ({ multiTabRef }: MultiTabProps) => { + const { initialState } = useModel('@@initialState'); + + const [keys, setKeys] = useState([]); + const [panes, setPanes] = useState([]); + const [activeKey, setActiveKey] = useState(''); + const [cache, setCache] = useState>({}); + const update = useCallback( + (key: string, dom: any) => { + const newCache = { ...cache }; + newCache[key] = dom; + setCache(newCache); + }, + [cache], + ); + + const active = useCallback( + (key: string, dom?: any) => { + if (dom) { + update(key, dom); + } + setActiveKey(key); + }, + [update], + ); + + useImperativeHandle(multiTabRef, () => ({ + switch: (key, dom) => { + active(key, dom); + }, + update, + get: (key) => cache[key], + })); + + // 关闭指定 key + const close = useCallback( + (...closeArray: string[]) => { + if (!closeArray || closeArray.length === 0) { + I18n.warning('没有可以被关闭的标签页!'); + return; + } + + if (keys.length === 1) { + I18n.warning('禁止关闭最后一个标签页!'); + return; + } + + let switctKey = false; + const newKeys: string[] = []; + keys.forEach((key) => { + if (closeArray.indexOf(key) === -1) { + // 未删除 + newKeys.push(key); + return; + } + // 删除当前展示key + if (key === activeKey) { + switctKey = true; + } + + // 指定缓存的清理 + }); + + setKeys(newKeys); + if (switctKey) { + active(newKeys[0]); + } + }, + [active, activeKey, keys], + ); + + // 关闭指定key 左侧的所有key + const closeLeft = useCallback( + (key: string) => { + const max = keys.indexOf(key); + const delKeys: string[] = []; + for (let index = 0; index < max; index += 1) { + delKeys.push(keys[index]); + } + close(...delKeys); + }, + [close, keys], + ); + + // 关闭指定key 右侧的所有key + const closeRight = useCallback( + (key: string) => { + const min = keys.indexOf(key); + const delKeys: string[] = []; + for (let index = min + 1; index < keys.length; index += 1) { + delKeys.push(keys[index]); + } + close(...delKeys); + }, + [close, keys], + ); + + // 关闭指定key 除外的其他key + const closeOther = useCallback( + (key: string) => { + const delKeys: string[] = []; + + keys.forEach((otherKey) => { + if (otherKey !== key) { + delKeys.push(otherKey); + } + }); + + close(...delKeys); + }, + [close, keys], + ); + + const overlay = ( + e.preventDefault()}> + close(activeKey)}> + 关闭标签 + + closeLeft(activeKey)}> + 关闭左侧标签 + + closeRight(activeKey)}> + 关闭右侧标签 + + closeOther(activeKey)}> + 关闭其他标签 + + + ); + + useEffect(() => { + const nodes: React.ReactNode[] = []; + + keys.forEach((key) => { + nodes.push(); + }); + + setPanes(nodes); + }, [keys]); + + return ( + +
+ { + if (action === 'remove') { + close(key as string); + } + }} + > + {panes} + +
+
+ ); +}; + +export default MultiTab; diff --git a/src/components/MultiTab/typings.ts b/src/components/MultiTab/typings.ts new file mode 100644 index 0000000..fb68e51 --- /dev/null +++ b/src/components/MultiTab/typings.ts @@ -0,0 +1,14 @@ +export type MultiTabRef = { + switch: (key: string, dom?: any) => void; + update: (key: string, dom: any) => void; + get: (key: string) => any; +}; + +export type MultiTabProps = { + multiTabRef: React.MutableRefObject; +}; + +export type MultiTabPaneProps = { + key: string; + tab: string; +}; diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index b01fa31..9f709c7 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -4,7 +4,7 @@ import type { Settings, } from '@ant-design/pro-layout'; import ProLayout, { WaterMark } from '@ant-design/pro-layout'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import RightContent from '@/components/RightContent'; import { history, Link, useIntl, useModel } from 'umi'; import LoadingComponent from '@ant-design/pro-layout/es/PageLoading'; @@ -16,6 +16,8 @@ import { redirect, goto } from '@/utils/RouteUtils'; import { User, Token } from '@/utils/Ballcat'; import I18n from '@/utils/I18nUtils'; import Icon from '@/components/Icon'; +import type { MultiTabRef } from '@/components/MultiTab'; +import MultiTab from '@/components/MultiTab'; export type BasicLayoutProps = { breadcrumbNameMap: Record; @@ -95,6 +97,8 @@ const BasicLayout: React.FC = (props) => { }, } = props; + const multiTabRef = useRef(); + const [collapsed, setCollapsed] = useState(false); const [reload, setReload] = useState(false); I18n.setIntl(useIntl()); @@ -162,6 +166,18 @@ const BasicLayout: React.FC = (props) => { collapsedButtonRender={false} collapsed={collapsed} onCollapse={setCollapsed} + contentStyle={{ marginTop: settings.multiTab ? '56px' : undefined }} + headerRender={(headerProps, defaultDom) => { + if (settings.multiTab) { + return ( + <> + {defaultDom} + + + ); + } + return defaultDom; + }} headerContentRender={() => { return ( Date: Thu, 14 Oct 2021 21:59:34 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=E6=B8=B2=E6=9F=93=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E9=A1=B5=E9=9D=A2=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/exception/error.tsx | 8 ++++---- src/pages/exception/index.tsx | 4 ++-- src/utils/RouteUtils.ts | 22 ++++++++++++++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/pages/exception/error.tsx b/src/pages/exception/error.tsx index de9554f..92b4290 100644 --- a/src/pages/exception/error.tsx +++ b/src/pages/exception/error.tsx @@ -2,11 +2,11 @@ import { Button, Result } from 'antd'; import React from 'react'; import { history } from 'umi'; -const Error: React.FC = () => ( +const ErrorPage: React.FC = () => ( ( /> ); -export default Error; +export default ErrorPage; diff --git a/src/pages/exception/index.tsx b/src/pages/exception/index.tsx index fae00a8..558d051 100644 --- a/src/pages/exception/index.tsx +++ b/src/pages/exception/index.tsx @@ -1,4 +1,4 @@ import NoFoundPage from './404'; -import Error from './error'; +import ErrorPage from './error'; -export { NoFoundPage, Error }; +export { NoFoundPage, ErrorPage }; diff --git a/src/utils/RouteUtils.ts b/src/utils/RouteUtils.ts index c63253c..c146eb0 100644 --- a/src/utils/RouteUtils.ts +++ b/src/utils/RouteUtils.ts @@ -77,8 +77,17 @@ export function serializationRemoteList(list: GLOBAL.Router[], pId: number, path if (val.targetType === 1) { component = dynamic({ loader: () => { - // TODO 导入模块异常时, 展示异常页面. import是异步.无法捕获. - return import(`@/pages/${val.uri}`); + return new Promise((resolve) => { + import(`@/pages/${val.uri}`) + .then((page) => { + resolve(page); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error('页面加载异常', err); + import(`@/pages/exception/error`).then((errPage) => resolve(errPage)); + }); + }); }, loading: LoadingComponent, }); @@ -117,3 +126,12 @@ export function redirect(arg: string) { export function goto(path: string) { history.push(path); } + +const RouteUtils = { + getMenu, + getRedirectPath, + redirect, + goto, +}; + +export default RouteUtils; From 057576adb70311ddf1d5b69b4660eefa0bb55b4f Mon Sep 17 00:00:00 2001 From: lingting Date: Thu, 14 Oct 2021 22:00:02 +0800 Subject: [PATCH 04/10] =?UTF-8?q?feat(SettingDrawer):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SettingDrawer/index.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/SettingDrawer/index.tsx b/src/components/SettingDrawer/index.tsx index 70593f9..a110a42 100644 --- a/src/components/SettingDrawer/index.tsx +++ b/src/components/SettingDrawer/index.tsx @@ -141,12 +141,10 @@ const SettingDrawer: React.FC = (props) => { if (key === 'layout') { nextState.contentWidth = value === 'top' ? 'Fixed' : 'Fluid'; } - if (key === 'layout' && value !== 'mix') { - nextState.splitMenus = false; - } - if (key === 'layout' && value === 'mix') { - nextState.navTheme = 'light'; + if (key === 'layout') { + nextState.splitMenus = value === 'mix'; } + if (key === 'colorWeak' && value === true) { const dom = document.querySelector('body'); if (dom) { From 6dc671c4a71db9de206b9f97e02e4634aec19aca Mon Sep 17 00:00:00 2001 From: lingting Date: Fri, 15 Oct 2021 10:12:55 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix(Form):=20=E4=BF=AE=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E5=9B=BD=E9=99=85=E5=8C=96=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Form/FullForm.tsx | 2 +- src/components/Form/ModalForm.tsx | 2 +- src/components/Page/BasePage.tsx | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FullForm.tsx b/src/components/Form/FullForm.tsx index 99b31f3..eab1cce 100644 --- a/src/components/Form/FullForm.tsx +++ b/src/components/Form/FullForm.tsx @@ -59,7 +59,7 @@ const FullForm = (props: FullFormProps) => { req?: (body: P) => Promise>, ) => { if (req === undefined) { - I18n.error({ key: 'orm.error.request', params: { title: defautlTitle[st] } }); + I18n.error({ key: 'form.error.request', params: { title: defautlTitle[st] } }); return Promise.resolve(false); } setLoading(true); diff --git a/src/components/Form/ModalForm.tsx b/src/components/Form/ModalForm.tsx index 64ffd93..d595d2f 100644 --- a/src/components/Form/ModalForm.tsx +++ b/src/components/Form/ModalForm.tsx @@ -39,7 +39,7 @@ const ModalForm = (props: ModalFormProps) => { req?: (body: P) => Promise>, ) => { if (req === undefined) { - I18n.error({ key: 'orm.error.request', params: { title: defautlTitle[st] } }); + I18n.error({ key: 'form.error.request', params: { title: defautlTitle[st] } }); return Promise.resolve(false); } diff --git a/src/components/Page/BasePage.tsx b/src/components/Page/BasePage.tsx index 95b1ef1..175d34f 100644 --- a/src/components/Page/BasePage.tsx +++ b/src/components/Page/BasePage.tsx @@ -133,7 +133,10 @@ const BasePage = ({ permission={ob.permission} onClick={() => { if (del === undefined) { - I18n.error({ key: 'orm.error.request', params: { title: defautlTitle.del } }); + I18n.error({ + key: 'form.error.request', + params: { title: defautlTitle.del }, + }); return; } del(record).then(() => { From 6810f6152872334be6cdefadf3b5410fedb3588d Mon Sep 17 00:00:00 2001 From: lingting Date: Fri, 15 Oct 2021 18:30:46 +0800 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E4=BF=9D=E5=AD=98=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/user/Login/index.tsx | 2 +- src/typings.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/user/Login/index.tsx b/src/pages/user/Login/index.tsx index bf625cb..dba0674 100644 --- a/src/pages/user/Login/index.tsx +++ b/src/pages/user/Login/index.tsx @@ -58,7 +58,7 @@ const Login: React.FC = () => { // 解析远程数据 const remoteUser = { ...res, - roles: res.attributes.roles, + roles: res.attributes.roleCodes, permissions: res.attributes.permissions, }; diff --git a/src/typings.d.ts b/src/typings.d.ts index 468a7c7..8dae9dd 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -118,7 +118,7 @@ declare namespace GLOBAL { token_type: 'bearer'; attributes: { permissions: string[]; - roles: string[]; + roleCodes: string[]; }; }; } From fa9d0c98f0fad632678aedd125d49b0b7047ca24 Mon Sep 17 00:00:00 2001 From: lingting Date: Sun, 17 Oct 2021 14:50:30 +0800 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.ts | 1 + package.json | 1 + src/components/Inline/index.less | 6 - src/components/Inline/index.tsx | 8 +- src/components/MultiTab/index.tsx | 177 ++++++++++++++--------------- src/components/MultiTab/typings.ts | 14 --- src/global.less | 8 ++ src/layouts/BasicLayout.tsx | 58 ++++++---- src/utils/RouteUtils.ts | 8 ++ tsconfig.json | 2 +- 10 files changed, 148 insertions(+), 135 deletions(-) delete mode 100644 src/components/Inline/index.less delete mode 100644 src/components/MultiTab/typings.ts diff --git a/config/config.ts b/config/config.ts index 7611109..b5384a6 100644 --- a/config/config.ts +++ b/config/config.ts @@ -47,4 +47,5 @@ export default defineConfig({ }, // Fast Refresh 热更新 fastRefresh: {}, + extraBabelPlugins: ['react-activation/babel'], }); diff --git a/package.json b/package.json index aeed66a..26fbc12 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "moment": "^2.25.3", "omit.js": "^2.0.2", "react": "^17.0.0", + "react-activation": "^0.9.4", "react-color": "2.19.3", "react-cropper": "2.1.8", "react-dev-inspector": "^1.1.1", diff --git a/src/components/Inline/index.less b/src/components/Inline/index.less deleted file mode 100644 index 7d5e079..0000000 --- a/src/components/Inline/index.less +++ /dev/null @@ -1,6 +0,0 @@ -.iframe { - box-sizing: border-box; - width: 100%; - height: 100%; - border: 0; -} diff --git a/src/components/Inline/index.tsx b/src/components/Inline/index.tsx index ca11b6d..238df09 100644 --- a/src/components/Inline/index.tsx +++ b/src/components/Inline/index.tsx @@ -1,5 +1,4 @@ import { Component } from 'react'; -import './index.less'; import type { Route } from '@ant-design/pro-layout/lib/typings'; export type InlineMeta = { @@ -25,7 +24,12 @@ class Inline extends Component { render() { const { meta } = this.state; - return