mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat: adapt styles for mobile devices (#6393)
* feat(layout): integrate ConfigProvider for theme management in admin layout * feat(layout): add mobile layout context and transform utility for schema * feat(toolbar): disable dragging in mobile layout for BlockSchemaToolbar * test: add unit tests for transformMultiColumnToSingleColumn function * feat: add mobile layout support to form schema transformation * feat: support mobile layout in Tabs component schema transformation * feat: integrate mobile layout support into designable components * feat: refactor mobile layout hooks and integrate MobileComponentsProvider * feat: update mobile layout styles and integrate global theme in ActionDrawer * feat: update Mobile component to use CommonDrawer for popup handling in mobile layout * feat: adjust padding in ActionDrawer style for improved layout on mobile * feat: optimize scope usage in InternalPicker for improved performance * feat: update Action.Page style to improve overflow handling * style: remove minHeight from ActionDrawer style * feat: add mobile layout support and transform schema for multi-column fields * feat: support mobile components * fix(mobile): issue with mobile flash/flicker * feat: hide Plugin manager and Settings center * feat: hide scroll bar in mobile * feat(todos): support to collapse sider menu * feat: enhance mobile layout theme support with dark mode
This commit is contained in:
parent
37f8936781
commit
c2786c1bee
@ -14,6 +14,7 @@ import { useCollection } from '../../data-source/collection/CollectionProvider';
|
||||
import { useCompile } from '../../schema-component';
|
||||
import { SchemaToolbar } from '../../schema-settings/GeneralSchemaDesigner';
|
||||
import { useSchemaTemplate } from '../../schema-templates';
|
||||
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
|
||||
|
||||
export const BlockSchemaToolbar = (props) => {
|
||||
const { t } = useTranslation();
|
||||
@ -22,6 +23,7 @@ export const BlockSchemaToolbar = (props) => {
|
||||
const template = useSchemaTemplate();
|
||||
const { association, collection } = useDataBlockProps() || {};
|
||||
const compile = useCompile();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
if (association) {
|
||||
const [collectionName] = association.split('.');
|
||||
@ -51,7 +53,7 @@ export const BlockSchemaToolbar = (props) => {
|
||||
].filter(Boolean);
|
||||
}, [currentCollectionTitle, currentCollectionName, associationField, associationCollection, compile, templateName]);
|
||||
|
||||
return <SchemaToolbar title={toolbarTitle} {...props} />;
|
||||
return <SchemaToolbar title={toolbarTitle} {...props} draggable={!isMobileLayout} />;
|
||||
};
|
||||
|
||||
export function getCollectionTitle(arg: {
|
||||
|
@ -13,6 +13,7 @@ import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useApp, useNavigateNoUpdate } from '../application';
|
||||
import { useMobileLayout } from '../route-switch/antd/admin-layout';
|
||||
import { useCompile } from '../schema-component';
|
||||
import { useToken } from '../style';
|
||||
|
||||
@ -20,6 +21,12 @@ export const PluginManagerLink = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const { token } = useToken();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
if (isMobileLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={t('Plugin manager')}>
|
||||
<Button
|
||||
@ -62,6 +69,12 @@ export const SettingsCenterDropdown = () => {
|
||||
};
|
||||
}, [app.pluginSettingsManager]);
|
||||
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
if (isMobileLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
@ -75,7 +88,7 @@ export const SettingsCenterDropdown = () => {
|
||||
<Button
|
||||
data-testid="plugin-settings-button"
|
||||
icon={<SettingOutlined style={{ color: token.colorTextHeaderMenu }} />}
|
||||
// title={t('All plugin settings')}
|
||||
// title={t('All plugin settings')}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
|
@ -11,7 +11,7 @@ import { EllipsisOutlined } from '@ant-design/icons';
|
||||
import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layout';
|
||||
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
|
||||
import { css } from '@emotion/css';
|
||||
import { Popover, Tooltip } from 'antd';
|
||||
import { theme as antdTheme, ConfigProvider, Popover, Tooltip } from 'antd';
|
||||
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
@ -29,6 +29,7 @@ import {
|
||||
RemoteSchemaTemplateManagerProvider,
|
||||
SortableItem,
|
||||
useDesignable,
|
||||
useGlobalTheme,
|
||||
useMenuDragEnd,
|
||||
useParseURLAndParams,
|
||||
useRequest,
|
||||
@ -73,7 +74,7 @@ const AllAccessDesktopRoutesContext = createContext<{
|
||||
refresh: () => void;
|
||||
}>({
|
||||
allAccessRoutes: emptyArray,
|
||||
refresh: () => {},
|
||||
refresh: () => { },
|
||||
});
|
||||
AllAccessDesktopRoutesContext.displayName = 'AllAccessDesktopRoutesContext';
|
||||
|
||||
@ -495,22 +496,44 @@ const headerRender = (props: HeaderViewProps, defaultDom: React.ReactNode) => {
|
||||
return <headerContext.Provider value={headerContextValue}>{defaultDom}</headerContext.Provider>;
|
||||
};
|
||||
|
||||
const IsMobileLayoutContext = React.createContext<{
|
||||
isMobileLayout: boolean;
|
||||
setIsMobileLayout: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}>({
|
||||
isMobileLayout: false,
|
||||
setIsMobileLayout: () => { },
|
||||
});
|
||||
|
||||
const MobileLayoutProvider: FC = (props) => {
|
||||
const [isMobileLayout, setIsMobileLayout] = useState(false);
|
||||
const value = useMemo(() => ({ isMobileLayout, setIsMobileLayout }), [isMobileLayout]);
|
||||
|
||||
return <IsMobileLayoutContext.Provider value={value}>{props.children}</IsMobileLayoutContext.Provider>;
|
||||
};
|
||||
|
||||
export const useMobileLayout = () => {
|
||||
const { isMobileLayout, setIsMobileLayout } = useContext(IsMobileLayoutContext);
|
||||
return { isMobileLayout, setIsMobileLayout };
|
||||
};
|
||||
|
||||
export const InternalAdminLayout = () => {
|
||||
const { allAccessRoutes } = useAllAccessDesktopRoutes();
|
||||
const { designable } = useDesignable();
|
||||
const { designable: _designable } = useDesignable();
|
||||
const location = useLocation();
|
||||
const { onDragEnd } = useMenuDragEnd();
|
||||
const { token } = useToken();
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { isMobileLayout, setIsMobileLayout } = useMobileLayout();
|
||||
const [collapsed, setCollapsed] = useState(isMobileLayout);
|
||||
const doNotChangeCollapsedRef = useRef(false);
|
||||
const { t } = useMenuTranslation();
|
||||
const designable = isMobileLayout ? false : _designable;
|
||||
|
||||
const route = useMemo(() => {
|
||||
return {
|
||||
path: '/',
|
||||
children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile, t }),
|
||||
children: convertRoutesToLayout(allAccessRoutes, { designable, isMobile: isMobileLayout, t }),
|
||||
};
|
||||
}, [allAccessRoutes, designable, isMobile, t]);
|
||||
}, [allAccessRoutes, designable, isMobileLayout, t]);
|
||||
const layoutToken = useMemo(() => {
|
||||
return {
|
||||
header: {
|
||||
@ -534,6 +557,21 @@ export const InternalAdminLayout = () => {
|
||||
bgLayout: token.colorBgLayout,
|
||||
};
|
||||
}, [token]);
|
||||
const { theme, isDarkTheme } = useGlobalTheme();
|
||||
const mobileTheme = useMemo(() => {
|
||||
return {
|
||||
...theme,
|
||||
token: {
|
||||
...theme.token,
|
||||
paddingPageHorizontal: 8, // Horizontal page padding
|
||||
paddingPageVertical: 8, // Vertical page padding
|
||||
marginBlock: 12, // Spacing between blocks
|
||||
borderRadiusBlock: 8, // Block border radius
|
||||
fontSize: 14, // Font size
|
||||
},
|
||||
algorithm: isDarkTheme ? [antdTheme.compactAlgorithm, antdTheme.darkAlgorithm] : antdTheme.compactAlgorithm, // Set mobile mode to always use compact algorithm
|
||||
};
|
||||
}, [theme, isDarkTheme]);
|
||||
|
||||
const onCollapse = useCallback((collapsed: boolean) => {
|
||||
if (doNotChangeCollapsedRef.current) {
|
||||
@ -575,11 +613,15 @@ export const InternalAdminLayout = () => {
|
||||
{(value: RouteContextType) => {
|
||||
const { isMobile: _isMobile } = value;
|
||||
|
||||
if (_isMobile !== isMobile) {
|
||||
setIsMobile(_isMobile);
|
||||
if (_isMobile !== isMobileLayout) {
|
||||
setIsMobileLayout(_isMobile);
|
||||
}
|
||||
|
||||
return <LayoutContent />;
|
||||
return (
|
||||
<ConfigProvider theme={_isMobile ? mobileTheme : theme}>
|
||||
<LayoutContent />
|
||||
</ConfigProvider>
|
||||
);
|
||||
}}
|
||||
</RouteContext.Consumer>
|
||||
</ProLayout>
|
||||
@ -696,6 +738,7 @@ export class AdminLayoutPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.schemaSettingsManager.add(userCenterSettings);
|
||||
this.app.addComponents({ AdminLayout, AdminDynamicPage });
|
||||
this.app.use(MobileLayoutProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,36 +760,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => {
|
||||
const { inHeader } = useContext(headerContext);
|
||||
|
||||
return (
|
||||
<RouteContext.Consumer>
|
||||
{(value: RouteContextType) => {
|
||||
const { collapsed } = value;
|
||||
|
||||
if (collapsed && !inHeader) {
|
||||
return props.icon ? (
|
||||
<Icon type={props.icon} />
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{props.title.charAt(0)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return props.icon ? <Icon type={props.icon} /> : null;
|
||||
}}
|
||||
</RouteContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuDesignerButton: FC<{ testId: string }> = (props) => {
|
||||
const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer);
|
||||
|
||||
@ -766,7 +779,7 @@ const MenuTitleWithIcon: FC<{ icon: any; title: string }> = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
return props.title;
|
||||
return <>{props.title}</>;
|
||||
};
|
||||
|
||||
function convertRoutesToLayout(
|
||||
|
@ -20,7 +20,8 @@ export const useActionPageStyle = genStyleHook('nb-action-page', (token) => {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: token.colorBgLayout,
|
||||
overflow: 'auto',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
|
||||
'.ant-tabs-nav': {
|
||||
background: token.colorBgContainer,
|
||||
|
@ -13,6 +13,7 @@ import { differenceBy, unionBy } from 'lodash';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import {
|
||||
FormProvider,
|
||||
PopupSettingsProvider,
|
||||
RecordPickerContext,
|
||||
RecordPickerProvider,
|
||||
SchemaComponentOptions,
|
||||
@ -24,6 +25,7 @@ import {
|
||||
NocoBaseRecursionField,
|
||||
RecordProvider,
|
||||
useCollectionRecordData,
|
||||
useMobileLayout,
|
||||
} from '../../..';
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import {
|
||||
@ -35,6 +37,7 @@ import { ActionContextProvider } from '../action';
|
||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||
import schema from './schema';
|
||||
import { flatData, getLabelFormatValue, useLabelUiSchema } from './util';
|
||||
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||
|
||||
export const useTableSelectorProps = () => {
|
||||
const field: any = useField();
|
||||
@ -117,6 +120,7 @@ export const InternalPicker = observer(
|
||||
collectionField,
|
||||
currentFormCollection: collectionName,
|
||||
};
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
const getValue = () => {
|
||||
if (multiple == null) return null;
|
||||
@ -147,8 +151,16 @@ export const InternalPicker = observer(
|
||||
},
|
||||
};
|
||||
};
|
||||
const scope = useMemo(
|
||||
() => ({
|
||||
usePickActionProps,
|
||||
useTableSelectorProps,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupSettingsProvider enableURL={false}>
|
||||
<Space.Compact style={{ display: 'flex', lineHeight: '32px' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<Select
|
||||
@ -213,16 +225,11 @@ export const InternalPicker = observer(
|
||||
<CollectionProvider_deprecated name={collectionField?.target}>
|
||||
<FormProvider>
|
||||
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
||||
<SchemaComponentOptions
|
||||
scope={{
|
||||
usePickActionProps,
|
||||
useTableSelectorProps,
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions scope={scope}>
|
||||
<NocoBaseRecursionField
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
schema={isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'AssociationField.Selector';
|
||||
}}
|
||||
@ -233,7 +240,7 @@ export const InternalPicker = observer(
|
||||
</CollectionProvider_deprecated>
|
||||
</RecordPickerProvider>
|
||||
</ActionContextProvider>
|
||||
</>
|
||||
</PopupSettingsProvider>
|
||||
);
|
||||
},
|
||||
{ displayName: 'InternalPicker' },
|
||||
|
@ -28,6 +28,8 @@ import { useLocalVariables, useVariables } from '../../../variables';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { useFormBlockHeight } from './hook';
|
||||
import { useApp } from '../../../application';
|
||||
import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
|
||||
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||
|
||||
export interface FormProps extends IFormLayoutProps {
|
||||
form?: FormilyForm;
|
||||
@ -49,6 +51,8 @@ const FormComponent: React.FC<FormProps> = (props) => {
|
||||
labelWidth = 120,
|
||||
labelWrap = true,
|
||||
} = cardItemSchema?.['x-component-props'] || {};
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
return (
|
||||
<FieldContext.Provider value={undefined}>
|
||||
<FormContext.Provider value={form}>
|
||||
@ -76,7 +80,12 @@ const FormComponent: React.FC<FormProps> = (props) => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<NocoBaseRecursionField basePath={f.address} schema={fieldSchema} onlyRenderProperties isUseFormilyField />
|
||||
<NocoBaseRecursionField
|
||||
basePath={f.address}
|
||||
schema={isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema}
|
||||
onlyRenderProperties
|
||||
isUseFormilyField
|
||||
/>
|
||||
</div>
|
||||
</FormLayout>
|
||||
</FormContext.Provider>
|
||||
@ -93,6 +102,8 @@ const FormDecorator: React.FC<FormProps> = (props) => {
|
||||
// TODO: component 里 useField 会与当前 field 存在偏差
|
||||
const f = useAttach(form.createVoidField({ ...field.props, basePath: '' }));
|
||||
const Component = useComponent(fieldSchema['x-component'], Def);
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
return (
|
||||
<FieldContext.Provider value={undefined}>
|
||||
<FormContext.Provider value={form}>
|
||||
@ -101,7 +112,7 @@ const FormDecorator: React.FC<FormProps> = (props) => {
|
||||
<Component {...field.componentProps}>
|
||||
<NocoBaseRecursionField
|
||||
basePath={f.address}
|
||||
schema={fieldSchema}
|
||||
schema={isMobileLayout ? transformMultiColumnToSingleColumn(fieldSchema) : fieldSchema}
|
||||
onlyRenderProperties
|
||||
isUseFormilyField
|
||||
/>
|
||||
@ -250,7 +261,7 @@ export const Form: React.FC<FormProps> & {
|
||||
const theme: any = useMemo(() => {
|
||||
return {
|
||||
token: {
|
||||
// 这里是为了防止区块内部也收到 marginBlock 的影响(marginBlock:区块之间的间距)
|
||||
// 这里是为了防止区块内部也受到 marginBlock 的影响(marginBlock:区块之间的间距)
|
||||
// @ts-ignore
|
||||
marginBlock: token.marginLG,
|
||||
},
|
||||
|
@ -25,6 +25,8 @@ import { ListDesigner } from './List.Designer';
|
||||
import { ListItem } from './List.Item';
|
||||
import useStyles from './List.style';
|
||||
import { useListActionBarProps, useListBlockHeight } from './hooks';
|
||||
import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
|
||||
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||
|
||||
const InternalList = withSkeletonComponent(
|
||||
(props) => {
|
||||
@ -121,6 +123,8 @@ const InternalList = withSkeletonComponent(
|
||||
};
|
||||
};
|
||||
const paginationProps = usePagination();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
return wrapSSR(
|
||||
<SchemaComponentOptions
|
||||
scope={{
|
||||
@ -165,7 +169,9 @@ const InternalList = withSkeletonComponent(
|
||||
key={index}
|
||||
name={index}
|
||||
onlyRenderProperties
|
||||
schema={getSchema(index)}
|
||||
schema={
|
||||
isMobileLayout ? transformMultiColumnToSingleColumn(getSchema(index)) : getSchema(index)
|
||||
}
|
||||
></NocoBaseRecursionField>
|
||||
);
|
||||
})
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
NocoBaseDesktopRouteType,
|
||||
NocoBaseRouteContext,
|
||||
useCurrentRoute,
|
||||
useMobileLayout,
|
||||
} from '../../../route-switch/antd/admin-layout';
|
||||
import { KeepAliveProvider, useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
|
||||
import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer';
|
||||
@ -50,6 +51,7 @@ import { useMenuDragEnd, useNocoBaseRoutes } from '../menu/Menu';
|
||||
import { useStyles } from './Page.style';
|
||||
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
||||
import { PopupRouteContextResetter } from './PopupRouteContextResetter';
|
||||
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||
|
||||
interface PageProps {
|
||||
currentTabUid: string;
|
||||
@ -174,6 +176,7 @@ const displayNone = {
|
||||
const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid: string }) => {
|
||||
const mountedRef = useRef(false);
|
||||
const { active: pageActive } = useKeepAlive();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
if (tabActive && !mountedRef.current) {
|
||||
mountedRef.current = true;
|
||||
@ -186,7 +189,10 @@ const TabPane = React.memo(({ active: tabActive, uid }: { active: boolean; uid:
|
||||
return (
|
||||
<div style={tabActive ? displayBlock : displayNone}>
|
||||
<KeepAliveProvider active={pageActive && tabActive}>
|
||||
<RemoteSchemaComponent uid={uid} />
|
||||
<RemoteSchemaComponent
|
||||
uid={uid}
|
||||
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||
/>
|
||||
</KeepAliveProvider>
|
||||
</div>
|
||||
);
|
||||
@ -204,6 +210,7 @@ const InternalPageContent = (props: PageContentProps) => {
|
||||
const currentRoute = useCurrentRoute();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const location = useLocationNoUpdate();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
const children = currentRoute?.children || [];
|
||||
const noTabs = children.every((tabRoute) => tabRoute.schemaUid !== activeKey && tabRoute.tabSchemaName !== activeKey);
|
||||
@ -244,7 +251,10 @@ const InternalPageContent = (props: PageContentProps) => {
|
||||
return (
|
||||
<div className={className1}>
|
||||
<NocoBaseRouteContext.Provider value={currentRoute?.children?.[0]}>
|
||||
<RemoteSchemaComponent uid={currentRoute?.children?.[0].schemaUid} />
|
||||
<RemoteSchemaComponent
|
||||
uid={currentRoute?.children?.[0].schemaUid}
|
||||
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||
/>
|
||||
</NocoBaseRouteContext.Provider>
|
||||
</div>
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ const PopupSettingsContext = React.createContext({
|
||||
export const PopupSettingsProvider: FC<{
|
||||
/**
|
||||
* @default true
|
||||
* Whether the popup should be controlled by URL
|
||||
*/
|
||||
enableURL?: boolean;
|
||||
}> = (props) => {
|
||||
@ -52,7 +53,7 @@ export const usePopupSettings = () => {
|
||||
}, [enableURL, isInSettingsPage]);
|
||||
|
||||
return {
|
||||
/** 弹窗窗口的显隐是否由 URL 控制 */
|
||||
/** Whether the visibility of the popup window is controlled by URL */
|
||||
isPopupVisibleControlledByURL,
|
||||
};
|
||||
};
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo } from 'react';
|
||||
@ -20,6 +20,8 @@ import { SchemaComponent } from '../../core';
|
||||
import { useDesigner } from '../../hooks/useDesigner';
|
||||
import { useTabsContext } from './context';
|
||||
import { TabsDesigner } from './Tabs.Designer';
|
||||
import { useMobileLayout } from '../../../route-switch/antd/admin-layout';
|
||||
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||
|
||||
const MemoizeRecursionField = React.memo(RecursionField);
|
||||
MemoizeRecursionField.displayName = 'MemoizeRecursionField';
|
||||
@ -32,6 +34,7 @@ export const Tabs: any = React.memo((props: TabsProps) => {
|
||||
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
||||
const contextProps = useTabsContext();
|
||||
const { PaneRoot = React.Fragment as React.FC<any> } = contextProps;
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const result = fieldSchema.mapProperties((schema, key: string) => {
|
||||
@ -40,14 +43,19 @@ export const Tabs: any = React.memo((props: TabsProps) => {
|
||||
label: <MemoizeRecursionField name={key} schema={schema} onlyRenderSelf />,
|
||||
children: (
|
||||
<PaneRoot key={key} {...(PaneRoot !== React.Fragment ? { active: key === contextProps.activeKey } : {})}>
|
||||
<SchemaComponent name={key} schema={schema} onlyRenderProperties distributed />
|
||||
<SchemaComponent
|
||||
name={key}
|
||||
schema={isMobileLayout ? new Schema(transformMultiColumnToSingleColumn(schema)) : schema}
|
||||
onlyRenderProperties
|
||||
distributed
|
||||
/>
|
||||
</PaneRoot>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [fieldSchema]);
|
||||
}, [fieldSchema, isMobileLayout]);
|
||||
|
||||
const tabBarExtraContent = useMemo(
|
||||
() => ({
|
||||
|
@ -14,6 +14,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDesignable } from '..';
|
||||
import { useToken } from '../../style';
|
||||
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
|
||||
|
||||
const designableStyle = {
|
||||
backgroundColor: 'var(--colorSettings) !important',
|
||||
@ -28,10 +29,15 @@ export const DesignableSwitch = () => {
|
||||
const { t } = useTranslation();
|
||||
const { token } = useToken();
|
||||
const style = designable ? designableStyle : unDesignableStyle;
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
// 快捷键切换编辑状态
|
||||
useHotkeys('Ctrl+Shift+U', () => setDesignable(!designable), [designable]);
|
||||
|
||||
if (isMobileLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={t('UI Editor')}>
|
||||
<Button
|
||||
|
@ -25,6 +25,7 @@ import { addAppVersion } from './addAppVersion';
|
||||
|
||||
// @ts-ignore
|
||||
import clientPkg from '../../../package.json';
|
||||
import { useMobileLayout } from '../../route-switch/antd/admin-layout';
|
||||
|
||||
interface CreateDesignableProps {
|
||||
current: Schema;
|
||||
@ -780,9 +781,11 @@ export function useDesignable() {
|
||||
dn.loadAPIClientEvents();
|
||||
}, [dn]);
|
||||
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
return {
|
||||
dn,
|
||||
designable,
|
||||
designable: isMobileLayout ? false : designable,
|
||||
reset,
|
||||
refresh,
|
||||
setDesignable,
|
||||
|
@ -9,9 +9,16 @@
|
||||
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { BlockTemplateProvider, CollectionDeletedPlaceholder, RemoteSchemaComponent, useDesignable } from '..';
|
||||
import {
|
||||
BlockTemplateProvider,
|
||||
CollectionDeletedPlaceholder,
|
||||
RemoteSchemaComponent,
|
||||
useDesignable,
|
||||
useMobileLayout,
|
||||
} from '..';
|
||||
import { useTemplateBlockContext } from '../block-provider/TemplateBlockProvider';
|
||||
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
|
||||
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
|
||||
|
||||
export const BlockTemplate = observer(
|
||||
(props: any) => {
|
||||
@ -22,6 +29,7 @@ export const BlockTemplate = observer(
|
||||
const { dn } = useDesignable();
|
||||
const template = useMemo(() => getTemplateById(templateId), [templateId]);
|
||||
const { onTemplateSuccess } = useTemplateBlockContext();
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
const onSuccess = (data) => {
|
||||
fieldSchema['x-linkage-rules'] = data?.data?.['x-linkage-rules'] || [];
|
||||
@ -30,7 +38,12 @@ export const BlockTemplate = observer(
|
||||
};
|
||||
return template ? (
|
||||
<BlockTemplateProvider {...{ dn, field, fieldSchema, template }}>
|
||||
<RemoteSchemaComponent noForm uid={template?.uid} onSuccess={onSuccess} />
|
||||
<RemoteSchemaComponent
|
||||
noForm
|
||||
uid={template?.uid}
|
||||
onSuccess={onSuccess}
|
||||
schemaTransform={isMobileLayout ? transformMultiColumnToSingleColumn : undefined}
|
||||
/>
|
||||
</BlockTemplateProvider>
|
||||
) : (
|
||||
<CollectionDeletedPlaceholder type="Block template" name={templateId} />
|
||||
|
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { transformMultiColumnToSingleColumn } from '../transformMultiColumnToSingleColumn';
|
||||
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../uid', () => ({
|
||||
uid: vi.fn(() => 'mocked-uid'),
|
||||
}));
|
||||
|
||||
// Mock package.json
|
||||
vi.mock('../../package.json', () => ({
|
||||
default: { version: '1.0.0' },
|
||||
}));
|
||||
|
||||
describe('transformMultiColumnToSingleColumn', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return schema as is if null or undefined', () => {
|
||||
expect(transformMultiColumnToSingleColumn(null)).toBeNull();
|
||||
expect(transformMultiColumnToSingleColumn(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should recursively process non-Grid schemas', () => {
|
||||
const schema = {
|
||||
'x-component': 'Form',
|
||||
properties: {
|
||||
field1: {
|
||||
'x-component': 'Input',
|
||||
},
|
||||
grid1: {
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
row1: {
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
col1: { 'x-component': 'Input' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformMultiColumnToSingleColumn(schema);
|
||||
|
||||
// The non-Grid top level should remain, but child Grid should be transformed
|
||||
expect(result['x-component']).toBe('Form');
|
||||
expect(result.properties.grid1.properties.row1['x-index']).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle Grid with non-Grid.Row components', () => {
|
||||
const schema = {
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
nonRow: {
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformMultiColumnToSingleColumn(schema);
|
||||
|
||||
expect(result.properties.nonRow['x-index']).toBe(1);
|
||||
});
|
||||
|
||||
it('should ignore Grid.Row without properties', () => {
|
||||
const schema = {
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
row1: {
|
||||
'x-component': 'Grid.Row',
|
||||
// No properties
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformMultiColumnToSingleColumn(schema);
|
||||
|
||||
expect(Object.keys(result.properties).length).toBe(0);
|
||||
});
|
||||
|
||||
it('should keep Grid.Row with single column as is', () => {
|
||||
const schema = {
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
row1: {
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
col1: { 'x-component': 'Input' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformMultiColumnToSingleColumn(schema);
|
||||
|
||||
expect(result.properties.row1['x-index']).toBe(1);
|
||||
expect(Object.keys(result.properties.row1.properties).length).toBe(1);
|
||||
expect(result.properties.row1.properties.col1).toBeDefined();
|
||||
});
|
||||
|
||||
it('should transform Grid.Row with multiple columns', () => {
|
||||
const schema = {
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
row1: {
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
col1: { 'x-component': 'Input' },
|
||||
col2: { 'x-component': 'Select' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformMultiColumnToSingleColumn(schema);
|
||||
|
||||
// Original row should keep only the first column
|
||||
expect(result.properties.row1['x-index']).toBe(1);
|
||||
expect(Object.keys(result.properties.row1.properties).length).toBe(1);
|
||||
expect(result.properties.row1.properties.col1).toBeDefined();
|
||||
|
||||
// Second column should be transformed into a new row
|
||||
const newRowKey = 'mocked-uid_col2';
|
||||
expect(result.properties[newRowKey]).toBeDefined();
|
||||
expect(result.properties[newRowKey]['x-component']).toBe('Grid.Row');
|
||||
expect(result.properties[newRowKey]['x-index']).toBe(2);
|
||||
expect(result.properties[newRowKey].properties.col2).toBeDefined();
|
||||
expect(result.properties[newRowKey].properties.col2['x-component-props'].width).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle complex Grid schema with multiple rows and columns', () => {
|
||||
const schema = {
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
row1: {
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
col1: { 'x-component': 'Input' },
|
||||
col2: { 'x-component': 'Select' },
|
||||
col3: { 'x-component': 'Checkbox' },
|
||||
},
|
||||
},
|
||||
row2: {
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
col4: { 'x-component': 'DatePicker' },
|
||||
},
|
||||
},
|
||||
nonRow: {
|
||||
'x-component': 'Divider',
|
||||
},
|
||||
shouldRemove: {
|
||||
'x-component': 'Grid.Row',
|
||||
// No properties
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformMultiColumnToSingleColumn(schema);
|
||||
|
||||
// Check structure
|
||||
expect(Object.keys(result.properties).length).toBe(5); // row1 + 2 new rows from col2,col3 + row2 + nonRow
|
||||
|
||||
// Check row1 (first column stays)
|
||||
expect(result.properties.row1['x-index']).toBe(1);
|
||||
expect(Object.keys(result.properties.row1.properties).length).toBe(1);
|
||||
expect(result.properties.row1.properties.col1).toBeDefined();
|
||||
|
||||
// Check col2 became its own row
|
||||
expect(result.properties['mocked-uid_col2'].properties.col2).toBeDefined();
|
||||
expect(result.properties['mocked-uid_col2'].properties.col2['x-component-props'].width).toBe(100);
|
||||
|
||||
// Check col3 became its own row (second call to uid would return the same mocked value)
|
||||
expect(result.properties['mocked-uid_col3'].properties.col3).toBeDefined();
|
||||
|
||||
// Check row2 stayed intact
|
||||
expect(result.properties.row2['x-index']).toBe(4);
|
||||
expect(Object.keys(result.properties.row2.properties).length).toBe(1);
|
||||
expect(result.properties.row2.properties.col4).toBeDefined();
|
||||
|
||||
// Check nonRow was processed
|
||||
expect(result.properties.nonRow['x-index']).toBe(5);
|
||||
|
||||
// Check shouldRemove was ignored
|
||||
expect(Object.keys(result.properties).includes('shouldRemove')).toBe(false);
|
||||
});
|
||||
});
|
@ -30,4 +30,5 @@ export * from './isPortalInBody';
|
||||
export * from './parseHTML';
|
||||
export * from './uid';
|
||||
export * from './url';
|
||||
export * from './transformMultiColumnToSingleColumn';
|
||||
export { dayjs, lodash };
|
||||
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { uid } from './uid';
|
||||
|
||||
// @ts-ignore
|
||||
import pkg from '../package.json';
|
||||
import _ from 'lodash';
|
||||
import { ISchema, Schema } from '@formily/json-schema';
|
||||
|
||||
/**
|
||||
* 将多列布局转换为单列布局
|
||||
* @param {Object} schema - 输入的 JSON Schema 对象
|
||||
* @returns {Object} - 转换后的 JSON Schema 对象
|
||||
*/
|
||||
export const transformMultiColumnToSingleColumn = (schema: any): any => {
|
||||
if (!schema) return schema;
|
||||
|
||||
if (schema.toJSON) {
|
||||
schema = schema.toJSON();
|
||||
}
|
||||
|
||||
if (schema['x-component'] !== 'Grid') {
|
||||
Object.keys(schema.properties || {}).forEach((key) => {
|
||||
schema.properties[key] = transformMultiColumnToSingleColumn(schema.properties[key]);
|
||||
});
|
||||
return schema;
|
||||
}
|
||||
|
||||
schema = _.cloneDeep(schema);
|
||||
|
||||
const newProperties: any = {};
|
||||
const { properties = {} } = schema;
|
||||
let index = 0;
|
||||
Object.keys(properties).forEach((key, rowIndex) => {
|
||||
const row = properties[key];
|
||||
|
||||
if (row['x-component'] !== 'Grid.Row') {
|
||||
row['x-index'] = ++index;
|
||||
newProperties[key] = row;
|
||||
return;
|
||||
}
|
||||
|
||||
// 忽略没有列的行
|
||||
if (!row.properties) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果一个行只有一列,那么无需展开
|
||||
if (Object.keys(row.properties).length === 1) {
|
||||
row['x-index'] = ++index;
|
||||
newProperties[key] = row;
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果一个行有多列,则保留第一列,其余的列需要放到外面形成新的行(每一行依然保持一列)
|
||||
Object.keys(row.properties).forEach((columnKey, colIndex) => {
|
||||
const column = row.properties[columnKey];
|
||||
_.set(column, 'x-component-props.width', 100);
|
||||
|
||||
if (colIndex === 0) {
|
||||
row['x-index'] = ++index;
|
||||
newProperties[key] = row;
|
||||
return;
|
||||
}
|
||||
|
||||
delete row.properties[columnKey];
|
||||
// 将列转换为行
|
||||
newProperties[`${uid()}_${columnKey}`] = createRow(column, columnKey, ++index);
|
||||
});
|
||||
});
|
||||
|
||||
schema.properties = newProperties;
|
||||
return schema;
|
||||
};
|
||||
|
||||
function createRow(column: any, key: string, index: number) {
|
||||
return {
|
||||
type: 'void',
|
||||
version: '2.0',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': pkg.version,
|
||||
'x-uid': uid(),
|
||||
'x-async': false,
|
||||
'x-index': index,
|
||||
_isJSONSchemaObject: true,
|
||||
properties: {
|
||||
[key]: column,
|
||||
},
|
||||
};
|
||||
}
|
@ -1,9 +1,17 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { ISchema, useFieldSchema } from '@formily/react';
|
||||
import { Action, ActionContextProvider, SchemaComponent, useCompile } from '@nocobase/client';
|
||||
import { Action, ActionContextProvider, PopupSettingsProvider, SchemaComponent, useCompile } from '@nocobase/client';
|
||||
import React, { useState } from 'react';
|
||||
import { NAMESPACE } from './constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
|
||||
const importFormSchema: ISchema = {
|
||||
@ -113,7 +121,6 @@ const importFormSchema: ISchema = {
|
||||
|
||||
export const ImportAction = (props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation(NAMESPACE);
|
||||
const compile = useCompile();
|
||||
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
@ -17,7 +17,6 @@ import {
|
||||
useSchemaInitializerItem,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { NAMESPACE } from './constants';
|
||||
import { useImportTranslation } from './locale';
|
||||
import { useFields } from './useFields';
|
||||
import { Alert } from 'antd';
|
||||
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Action, OpenModeProvider, SchemaComponentOptions, useMobileLayout, usePopupSettings } from '@nocobase/client';
|
||||
import { createGlobalStyle } from 'antd-style';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { ActionDrawerUsedInMobile, useToAdaptActionDrawerToMobile } from './adaptor-of-desktop/ActionDrawer';
|
||||
import { useToAdaptFilterActionToMobile } from './adaptor-of-desktop/FilterAction';
|
||||
import { mobileComponents } from './pages/dynamic-page/MobilePage';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const CommonDrawer: FC = (props) => {
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
|
||||
// 在移动端布局中,只要弹窗是通过 URL 打开的,都需要显示成子页面的样子
|
||||
if (isMobileLayout && isPopupVisibleControlledByURL()) {
|
||||
return <Action.Page {...props} />;
|
||||
}
|
||||
|
||||
return <ActionDrawerUsedInMobile {...props} />;
|
||||
};
|
||||
|
||||
const openModeToComponent = {
|
||||
page: Action.Page,
|
||||
drawer: CommonDrawer,
|
||||
modal: CommonDrawer,
|
||||
};
|
||||
|
||||
const MobileAdapter: FC = (props) => {
|
||||
useToAdaptFilterActionToMobile();
|
||||
useToAdaptActionDrawerToMobile();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.setProperty('--nb-mobile-page-tabs-content-padding', '12px');
|
||||
document.body.style.setProperty('--nb-mobile-page-header-height', '46px');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<OpenModeProvider defaultOpenMode="page" isMobile={true} openModeToComponent={openModeToComponent}>
|
||||
<SchemaComponentOptions components={mobileComponents}>{props.children}</SchemaComponentOptions>
|
||||
</OpenModeProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileComponentsProvider: FC = (props) => {
|
||||
const { isMobileLayout } = useMobileLayout();
|
||||
|
||||
if (!isMobileLayout) {
|
||||
return <>{props.children} </>;
|
||||
}
|
||||
|
||||
return <MobileAdapter>{props.children}</MobileAdapter>;
|
||||
};
|
@ -50,6 +50,23 @@ export const useMobileActionDrawerStyle = genStyleHook('nb-mobile-action-drawer'
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: token.colorBgLayout,
|
||||
|
||||
// 不带 tab 页的半窗
|
||||
'& > .nb-grid-container': {
|
||||
padding: `${token.paddingPageVertical}px ${token.paddingPageHorizontal}px`,
|
||||
},
|
||||
|
||||
// 带有 tab 页的半窗
|
||||
'.ant-tabs-nav': {
|
||||
marginBottom: '0px !important',
|
||||
padding: `0 ${token.paddingPageHorizontal + token.borderRadiusBlock / 2}px`,
|
||||
backgroundColor: token.colorBgContainer,
|
||||
},
|
||||
|
||||
// 带有 tab 页的半窗
|
||||
'.ant-tabs-content-holder': {
|
||||
padding: `${token.paddingPageVertical}px ${token.paddingPageHorizontal}px`,
|
||||
},
|
||||
|
||||
// clear the margin-bottom of the last block
|
||||
'& > .nb-grid-container > .nb-grid > .nb-grid-warp > .nb-grid-row:nth-last-child(2) .noco-card-item': {
|
||||
marginBottom: 0,
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
NocoBaseRecursionField,
|
||||
SchemaComponent,
|
||||
useActionContext,
|
||||
useGlobalTheme,
|
||||
useZIndexContext,
|
||||
zIndexContext,
|
||||
} from '@nocobase/client';
|
||||
@ -31,6 +32,7 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
||||
const { popupContainerRef, visiblePopup } = usePopupContainer(visible);
|
||||
const { componentCls, hashId } = useMobileActionDrawerStyle();
|
||||
const parentZIndex = useZIndexContext();
|
||||
const { theme: globalTheme } = useGlobalTheme();
|
||||
|
||||
// this schema need to add padding in the content area of the popup
|
||||
const isSpecialSchema = isChangePasswordSchema(fieldSchema) || isEditProfileSchema(fieldSchema);
|
||||
@ -55,7 +57,6 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
||||
});
|
||||
|
||||
const title = field.title || '';
|
||||
const marginBlock = 18;
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
setVisible(false);
|
||||
@ -63,14 +64,14 @@ export const ActionDrawerUsedInMobile: any = observer((props: { footerNodeName?:
|
||||
|
||||
const theme = useMemo(() => {
|
||||
return {
|
||||
...globalTheme,
|
||||
token: {
|
||||
marginBlock,
|
||||
borderRadiusBlock: 0,
|
||||
boxShadowTertiary: 'none',
|
||||
...globalTheme.token,
|
||||
marginBlock: 12,
|
||||
zIndexPopupBase: newZIndex,
|
||||
},
|
||||
};
|
||||
}, [newZIndex]);
|
||||
}, [globalTheme, newZIndex]);
|
||||
|
||||
return (
|
||||
<zIndexContext.Provider value={newZIndex}>
|
||||
|
@ -109,12 +109,12 @@ export const useToAdaptFilterActionToMobile = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 之所以不直接在 mobile-container 中设置 transform,是因为会影响到子页面区块的拖拽功能。详见:https://nocobase.height.app/T-4959
|
||||
* 之所以不直接在 mobile-container 中设置 transform,是因为会影响到子页面区块的拖拽功能。
|
||||
* @param visible
|
||||
* @returns
|
||||
*/
|
||||
export const usePopupContainer = (visible: boolean) => {
|
||||
const [mobileContainer] = useState<HTMLElement>(() => document.querySelector('.mobile-container'));
|
||||
const [mobileContainer] = useState<HTMLElement>(() => document.querySelector('.mobile-container') || document.body);
|
||||
const [visiblePopup, setVisiblePopup] = useState(false);
|
||||
const popupContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const parentZIndex = useZIndexContext();
|
||||
|
@ -53,6 +53,7 @@ import { MobileSettingsBlockInitializer } from './mobile-blocks/settings-block/M
|
||||
import { MobileSettingsBlockSchemaSettings } from './mobile-blocks/settings-block/schemaSettings';
|
||||
// @ts-ignore
|
||||
import pkg from './../../package.json';
|
||||
import { MobileComponentsProvider } from './MobileComponentsProvider';
|
||||
|
||||
export * from './desktop-mode';
|
||||
export * from './mobile';
|
||||
@ -104,6 +105,7 @@ export class PluginMobileClient extends Plugin {
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.app.use(MobileComponentsProvider);
|
||||
this.addComponents();
|
||||
this.addAppRoutes();
|
||||
this.addRoutes();
|
||||
|
@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
Action,
|
||||
AdminProvider,
|
||||
AntdAppProvider,
|
||||
AssociationFieldMode,
|
||||
@ -18,9 +17,10 @@ import {
|
||||
OpenModeProvider,
|
||||
useAssociationFieldModeContext,
|
||||
usePlugin,
|
||||
usePopupSettings,
|
||||
zIndexContext,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { isDesktop } from 'react-device-detect';
|
||||
|
||||
import { theme } from 'antd';
|
||||
@ -36,10 +36,21 @@ import { PluginMobileClient } from '../index';
|
||||
import { MobileAppProvider } from './MobileAppContext';
|
||||
import { useStyles } from './styles';
|
||||
|
||||
const CommonDrawer: FC = (props) => {
|
||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
|
||||
// 在移动端布局中,只要弹窗是通过 URL 打开的,都需要显示成子页面的样子。因为这样可以通过左滑返回
|
||||
if (isPopupVisibleControlledByURL()) {
|
||||
return <MobileActionPage {...(props as any)} />;
|
||||
}
|
||||
|
||||
return <ActionDrawerUsedInMobile {...props} />;
|
||||
};
|
||||
|
||||
const openModeToComponent = {
|
||||
page: MobileActionPage,
|
||||
page: CommonDrawer,
|
||||
drawer: ActionDrawerUsedInMobile,
|
||||
modal: Action.Modal,
|
||||
modal: ActionDrawerUsedInMobile,
|
||||
};
|
||||
|
||||
export const Mobile = () => {
|
||||
|
@ -60,7 +60,7 @@ DatePickerMobile.FilterWithPicker = (props) => {
|
||||
};
|
||||
DatePickerMobile.RangePicker = MobileRangePicker;
|
||||
|
||||
const mobileComponents = {
|
||||
export const mobileComponents = {
|
||||
Button: MobileButton,
|
||||
Select: (props) => {
|
||||
const { designable } = useDesignable();
|
||||
|
@ -285,5 +285,10 @@ const MobileTimePicker: ComposedMobileTimePicker = connect(
|
||||
mapReadPretty(NBTimePicker.ReadPretty),
|
||||
);
|
||||
|
||||
MobileDateTimePicker.displayName = 'MobileDateTimePicker';
|
||||
MobileRangePicker.displayName = 'MobileRangePicker';
|
||||
MobileDateFilterWithPicker.displayName = 'MobileDateFilterWithPicker';
|
||||
MobileTimePicker.displayName = 'MobileTimePicker';
|
||||
|
||||
MobileTimePicker.RangePicker = NBTimePicker.RangePicker;
|
||||
export { MobileDateTimePicker, MobileRangePicker, MobileDateFilterWithPicker, MobileTimePicker };
|
||||
|
@ -108,4 +108,6 @@ const MobilePicker = connect(
|
||||
mapProps({ dataSource: 'options' }),
|
||||
);
|
||||
|
||||
MobilePicker.displayName = 'MobilePicker';
|
||||
|
||||
export { MobilePicker };
|
||||
|
@ -6,12 +6,12 @@
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Badge, Button, Layout, Menu, Tabs, Tooltip } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Layout, Menu, Badge, Tooltip, Tabs } from 'antd';
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import {
|
||||
css,
|
||||
@ -36,19 +36,6 @@ const layoutClass = css`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const sideClass = css`
|
||||
overflow: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
|
||||
.ant-layout-sider-children {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const contentClass = css`
|
||||
padding: 24px;
|
||||
min-height: 280px;
|
||||
@ -134,8 +121,8 @@ function StatusTabs() {
|
||||
tabBarExtraContent={
|
||||
ExtraActions
|
||||
? {
|
||||
right: <ExtraActions />,
|
||||
}
|
||||
right: <ExtraActions />,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
@ -196,7 +183,7 @@ export function WorkflowTasks() {
|
||||
|
||||
return (
|
||||
<Layout className={layoutClass}>
|
||||
<Layout.Sider className={sideClass} theme="light">
|
||||
<Layout.Sider theme="light" breakpoint="md" collapsedWidth="0" zeroWidthTriggerStyle={{ top: 38 }}>
|
||||
<Menu mode="inline" selectedKeys={[typeKey]} items={items} style={{ height: '100%' }} />
|
||||
</Layout.Sider>
|
||||
<Layout
|
||||
|
Loading…
x
Reference in New Issue
Block a user