feat(plugin-mobile-client): support mobile-side client (#1879)
* feat: init mobile client * feat: add plugin * feat: ready to develop * feat: update pm styels * feat: add mobile center * feat: router ready * feat: support menu block, then menu ready * fix: incorrect path * feat: support TabBar * feat: tabbar, menu support dragging * feat: support page and header * feat: mobile view * fix: optional schema * feat: improve styles * fix: user-scalable * feat: support pc component show in mobile * feat: hidden divider * fix: improve drawer props * feat: support list block * feat: rename to details list * feat: page support tabs * feat: improve designer css * feat: complete enable/disabled header of page * feat: some improve * feat: improve empty data * fix: header info cannot displayed * chore: update deps * fix: incorrect spacing * fix: menu designer * refactor: re implement * feat: support page template * feat: clean code * feat: support i18n * chore: update lock * feat: support GirdCard in mobile * fix: build failed * feat: only render one column in mobile interface * fix: back button should not display in container * fix: switch to padding * fix: fixedBlockDesignRItem shouldn't display in dosen't support block * fix: update font family * fix: remove gridcard title * fix: dragging scope is too wide * fix: add menu cannot direct display * refactor: improve tabbar schema usage * refactor: improve menu schema * feat: should to use simple pagination * feat: the tag should pre-wrap * feat: improve the configuration button * feat: improve name * fix: clear data when modal is closed * fix: the tag is too long * fix: i18n * fix: font incorrect * feat: add map block * fix: some maps error * feat: support global action in page * feat: improve border color * feat: improve performance, the count stop early * style: improve * fix: incorrect font * fix: style conflict * chore: update version * chore: missing dep * feat: support setting block * feat: improve settings block and improve * feat: support onBackPressed * fix: ts error * feat: improve cannot find tab should navigate to mobile * docs: update * chore: update deps * fix: showTitle state is incorrect * feat: improve jsbridge apis * fix: navigate to admin after signout * chore: remove mgrid block * fix: ts error * fix: switch role will reload to root page * fix: update deps * fix: upgrade formily to 2.2.24 --------- Co-authored-by: dream2023 <1098626505@qq.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
b401c54442
commit
9c165db0f7
@ -2,12 +2,17 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, shrink-to-fit=no">
|
||||
<title>Loading...</title>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/favicon/site.webmanifest">
|
||||
<style>
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* width */
|
||||
.win ::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@ -41,4 +46,4 @@
|
||||
window.routerBase = match ? match[0] : "/";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
1
packages/app/client/src/plugins/mobile-client.ts
Normal file
1
packages/app/client/src/plugins/mobile-client.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from '@nocobase/plugin-mobile-client/client';
|
@ -53,9 +53,7 @@ const App = React.memo((props: any) => {
|
||||
const C = compose(...props.providers)(() => {
|
||||
const routes = useRoutes();
|
||||
return (
|
||||
<div>
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
<RouteSwitch routes={routes} />
|
||||
);
|
||||
});
|
||||
return <C />;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { Layout, Menu, PageHeader, Result, Spin, Tabs } from 'antd';
|
||||
import { sortBy } from 'lodash';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
@ -262,8 +262,21 @@ const SettingsCenter = (props) => {
|
||||
};
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<Layout>
|
||||
<div
|
||||
className={cx(
|
||||
'nb-setting-center',
|
||||
css`
|
||||
&.nb-setting-center {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Layout
|
||||
className={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
@ -302,7 +315,12 @@ const SettingsCenter = (props) => {
|
||||
items={menuItems as any}
|
||||
/>
|
||||
</Layout.Sider>
|
||||
<Layout.Content>
|
||||
<Layout.Content
|
||||
className={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
{aclPluginTabCheck && (
|
||||
<PageHeader
|
||||
ghost={false}
|
||||
@ -321,7 +339,7 @@ const SettingsCenter = (props) => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className={'m24'} style={{ margin: 24 }}>
|
||||
<div className={'m24'} style={{ margin: 24, flex: 1 }}>
|
||||
{aclPluginTabCheck ? (
|
||||
component && React.createElement(component)
|
||||
) : (
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Redirect, Route, Switch, useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { RouteContext } from './context';
|
||||
import { useRouteComponent } from './hooks';
|
||||
import { useRoute, useRouteComponent } from './hooks';
|
||||
import { RouteSwitchProps } from './types';
|
||||
|
||||
export function RouteSwitch(props: RouteSwitchProps) {
|
||||
const { routes = [] } = props;
|
||||
const { url: base, path } = useRouteMatch();
|
||||
|
||||
if (!routes.length) {
|
||||
return null;
|
||||
}
|
||||
@ -28,21 +30,20 @@ export function RouteSwitch(props: RouteSwitchProps) {
|
||||
if (!route.path && Array.isArray(route.routes)) {
|
||||
route.path = route.routes.map((r) => r.path) as any;
|
||||
}
|
||||
let nextPath = route.path;
|
||||
if (route.path && typeof route.path === 'string' && !route.path.startsWith('/')) {
|
||||
nextPath = `${base.endsWith('/') ? base : base + '/'}${route.path}`;
|
||||
}
|
||||
return (
|
||||
<Route
|
||||
key={index}
|
||||
path={route.path}
|
||||
key={[nextPath].flat().join(',') as string}
|
||||
path={nextPath}
|
||||
exact={route.exact}
|
||||
strict={route.strict}
|
||||
sensitive={route.sensitive}
|
||||
render={(props) => {
|
||||
return (
|
||||
<RouteContext.Provider value={route}>
|
||||
<ComponentRenderer {...props} route={route} />
|
||||
</RouteContext.Provider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<ComponentRenderer route={route} />
|
||||
</Route>
|
||||
);
|
||||
})}
|
||||
</Switch>
|
||||
@ -50,10 +51,16 @@ export function RouteSwitch(props: RouteSwitchProps) {
|
||||
}
|
||||
|
||||
function ComponentRenderer(props) {
|
||||
const Component = useRouteComponent(props?.route?.component);
|
||||
const { route } = props;
|
||||
const Component = useRouteComponent(route?.component);
|
||||
if (React.isValidElement(Component)) {
|
||||
return React.cloneElement(Component, props, <RouteSwitch routes={route.routes} />);
|
||||
}
|
||||
return (
|
||||
<Component {...props}>
|
||||
<RouteSwitch routes={props.route.routes} />
|
||||
</Component>
|
||||
<RouteContext.Provider value={route}>
|
||||
<Component {...props}>
|
||||
<RouteSwitch routes={route?.routes} />
|
||||
</Component>
|
||||
</RouteContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -286,19 +286,25 @@ export const InternalAdminLayout = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AdminProvider = (props) => {
|
||||
return <CurrentAppInfoProvider>
|
||||
<CurrentUserProvider>
|
||||
<RemoteSchemaTemplateManagerProvider>
|
||||
<RemoteCollectionManagerProvider>
|
||||
<ACLRolesCheckProvider>
|
||||
{props.children}
|
||||
</ACLRolesCheckProvider>
|
||||
</RemoteCollectionManagerProvider>
|
||||
</RemoteSchemaTemplateManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</CurrentAppInfoProvider>
|
||||
}
|
||||
|
||||
export const AdminLayout = (props) => {
|
||||
return (
|
||||
<CurrentAppInfoProvider>
|
||||
<CurrentUserProvider>
|
||||
<RemoteSchemaTemplateManagerProvider>
|
||||
<RemoteCollectionManagerProvider>
|
||||
<ACLRolesCheckProvider>
|
||||
<InternalAdminLayout {...props} />
|
||||
</ACLRolesCheckProvider>
|
||||
</RemoteCollectionManagerProvider>
|
||||
</RemoteSchemaTemplateManagerProvider>
|
||||
</CurrentUserProvider>
|
||||
</CurrentAppInfoProvider>
|
||||
<AdminProvider>
|
||||
<InternalAdminLayout {...props} />
|
||||
</AdminProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { RouteProps } from './types';
|
||||
|
||||
export const RouteSwitchContext = createContext({
|
||||
@ -7,3 +7,5 @@ export const RouteSwitchContext = createContext({
|
||||
});
|
||||
|
||||
export const RouteContext = createContext<RouteProps>(null);
|
||||
|
||||
export const useRouteSwitchContext = () => useContext(RouteSwitchContext);
|
||||
|
@ -31,4 +31,5 @@ export type RouteRedirectProps = RedirectProps | RouteProps;
|
||||
export interface RouteSwitchProps {
|
||||
routes?: RouteRedirectProps[];
|
||||
components?: any;
|
||||
base?: string;
|
||||
}
|
||||
|
@ -3,11 +3,10 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
||||
import { Drawer } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { OpenSize } from './';
|
||||
import { useActionContext } from './hooks';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
import { OpenSize } from './';
|
||||
|
||||
const openSizeWidthMap = new Map<OpenSize, string>([
|
||||
['small', '30%'],
|
||||
@ -18,7 +17,7 @@ export const ActionDrawer: ComposedActionDrawer = observer(
|
||||
(props) => {
|
||||
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
|
||||
const { t } = useTranslation();
|
||||
const { visible, setVisible, openSize = 'middle' } = useActionContext();
|
||||
const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext();
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
|
||||
@ -29,90 +28,86 @@ export const ActionDrawer: ComposedActionDrawer = observer(
|
||||
}
|
||||
return buf;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Drawer
|
||||
width={openSizeWidthMap.get(finalOpenSize)}
|
||||
title={field.title}
|
||||
{...others}
|
||||
destroyOnClose
|
||||
open={visible}
|
||||
onClose={() => setVisible(false, true)}
|
||||
className={classNames(
|
||||
others.className,
|
||||
css`
|
||||
&.nb-action-popup {
|
||||
.ant-drawer-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.ant-drawer-content {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
}
|
||||
|
||||
&.nb-record-picker-selector {
|
||||
.nb-block-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.general-schema-designer {
|
||||
top: -8px;
|
||||
bottom: -8px;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
footer={
|
||||
footerSchema && (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
.ant-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
<Drawer
|
||||
width={openSizeWidthMap.get(openSize)}
|
||||
title={field.title}
|
||||
{...others}
|
||||
{...drawerProps}
|
||||
style={{
|
||||
...drawerProps?.style,
|
||||
...others?.style,
|
||||
}}
|
||||
destroyOnClose
|
||||
open={visible}
|
||||
onClose={() => setVisible(false, true)}
|
||||
className={classNames(
|
||||
drawerProps?.className,
|
||||
others.className,
|
||||
css`
|
||||
&.nb-action-popup {
|
||||
.ant-drawer-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.ant-drawer-content {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
}
|
||||
|
||||
&.nb-record-picker-selector {
|
||||
.nb-block-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.general-schema-designer {
|
||||
top: -8px;
|
||||
bottom: -8px;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
footer={
|
||||
footerSchema && (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
.ant-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== footerNodeName;
|
||||
return s['x-component'] === footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
},
|
||||
{ displayName: 'ActionDrawer' },
|
||||
|
@ -3,7 +3,6 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
||||
import { Modal, ModalProps } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { OpenSize, useActionContext } from '.';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
|
||||
@ -15,7 +14,7 @@ const openSizeWidthMap = new Map<OpenSize, string>([
|
||||
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
||||
(props) => {
|
||||
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
|
||||
const { visible, setVisible, openSize = 'large' } = useActionContext();
|
||||
const { visible, setVisible, openSize = 'large', modalProps } = useActionContext();
|
||||
const actualWidth = width ?? openSizeWidthMap.get(openSize);
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
@ -26,84 +25,79 @@ export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
|
||||
return buf;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{createPortal(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Modal
|
||||
width={actualWidth}
|
||||
title={field.title}
|
||||
{...(others as ModalProps)}
|
||||
destroyOnClose
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(false, true)}
|
||||
className={classNames(
|
||||
others.className,
|
||||
css`
|
||||
&.nb-action-popup {
|
||||
.ant-modal-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
footer={
|
||||
footerSchema ? (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
.ant-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
<Modal
|
||||
width={actualWidth}
|
||||
title={field.title}
|
||||
{...(others as ModalProps)}
|
||||
{...modalProps}
|
||||
style={{
|
||||
...modalProps?.style,
|
||||
...others?.style,
|
||||
}}
|
||||
destroyOnClose
|
||||
open={visible}
|
||||
onCancel={() => setVisible(false, true)}
|
||||
className={classNames(
|
||||
others.className,
|
||||
modalProps?.className,
|
||||
css`
|
||||
&.nb-action-popup {
|
||||
.ant-modal-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
footer={
|
||||
footerSchema ? (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
|
||||
.ant-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== footerNodeName;
|
||||
return s['x-component'] === footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
false
|
||||
)
|
||||
}
|
||||
>
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
schema={schema}
|
||||
onlyRenderProperties
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== footerNodeName;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
{ displayName: 'ActionModal' },
|
||||
|
@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { Button, Modal, Popover } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useActionContext } from '../..';
|
||||
import { useDesignable } from '../../';
|
||||
import { Icon } from '../../../icon';
|
||||
@ -16,7 +16,7 @@ import { ActionDrawer } from './Action.Drawer';
|
||||
import { ActionLink } from './Action.Link';
|
||||
import { ActionModal } from './Action.Modal';
|
||||
import { ActionPage } from './Action.Page';
|
||||
import { ActionContext } from './context';
|
||||
import { ActionContextProvider } from './context';
|
||||
import { useA } from './hooks';
|
||||
import { ComposedAction } from './types';
|
||||
import { linkageAction } from './utils';
|
||||
@ -95,16 +95,6 @@ export const Action: ComposedAction = observer(
|
||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const { designable } = useDesignable();
|
||||
const tarComponent = useComponent(component) || component;
|
||||
const renderTitle = useMemo(() => {
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
if (fieldSchema.title) {
|
||||
return compile(fieldSchema.title);
|
||||
}
|
||||
return null;
|
||||
}, [compile, fieldSchema.title, title]);
|
||||
|
||||
useEffect(() => {
|
||||
field.linkageProperty = {};
|
||||
linkageRules
|
||||
@ -127,6 +117,7 @@ export const Action: ComposedAction = observer(
|
||||
icon={<Icon type={icon} />}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
...others.style,
|
||||
opacity: designable && field?.data?.hidden && 0.1,
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
@ -151,30 +142,28 @@ export const Action: ComposedAction = observer(
|
||||
component={tarComponent || Button}
|
||||
className={classnames(className, actionDesignerCss)}
|
||||
>
|
||||
{renderTitle}
|
||||
{title || compile(fieldSchema.title)}
|
||||
<Designer {...designerProps} />
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionContext.Provider
|
||||
value={{
|
||||
button: renderButton(),
|
||||
visible,
|
||||
setVisible,
|
||||
formValueChanged,
|
||||
setFormValueChanged,
|
||||
openMode,
|
||||
openSize,
|
||||
containerRefKey,
|
||||
fieldSchema,
|
||||
}}
|
||||
<ActionContextProvider
|
||||
button={renderButton()}
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
formValueChanged={formValueChanged}
|
||||
setFormValueChanged={setFormValueChanged}
|
||||
openMode={openMode}
|
||||
openSize={openSize}
|
||||
containerRefKey={containerRefKey}
|
||||
fieldSchema={fieldSchema}
|
||||
>
|
||||
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
||||
{!popover && renderButton()}
|
||||
{!popover && props.children}
|
||||
</ActionContext.Provider>
|
||||
</ActionContextProvider>
|
||||
);
|
||||
},
|
||||
{ displayName: 'Action' },
|
||||
|
@ -1,37 +1,81 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { cx } from '@emotion/css';
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { CSSProperties, useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useSchemaInitializer } from '../../../schema-initializer';
|
||||
import { DndContext } from '../../common';
|
||||
import { useDesignable, useProps } from '../../hooks';
|
||||
|
||||
interface ActionBarContextForceProps {
|
||||
layout?: 'one-column' | 'tow-columns';
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ActionBarContextValue {
|
||||
container?: Element | DocumentFragment;
|
||||
/**
|
||||
* override props
|
||||
*/
|
||||
forceProps?: ActionBarContextForceProps;
|
||||
parentComponents?: string[];
|
||||
}
|
||||
|
||||
const ActionBarContext = React.createContext<ActionBarContextValue>({
|
||||
container: null,
|
||||
});
|
||||
|
||||
export const ActionBarProvider: React.FC<ActionBarContextValue> = ({ children, ...props }) => {
|
||||
return <ActionBarContext.Provider value={props}>{children}</ActionBarContext.Provider>;
|
||||
};
|
||||
|
||||
export const useActionBarContext = () => {
|
||||
return useContext(ActionBarContext);
|
||||
};
|
||||
|
||||
const Portal: React.FC = (props) => {
|
||||
const filedSchema = useFieldSchema();
|
||||
const { container, parentComponents = ['BlockItem', 'CardItem'] } = useActionBarContext();
|
||||
return (
|
||||
<>
|
||||
{container && parentComponents.includes(filedSchema.parent['x-component'])
|
||||
? createPortal(props.children, container)
|
||||
: props.children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActionBar = observer(
|
||||
(props: any) => {
|
||||
const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
|
||||
const { forceProps = {} } = useActionBarContext();
|
||||
const { layout = 'tow-columns', style, spaceProps, ...others } = { ...useProps(props), ...forceProps } as any;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const { designable } = useDesignable();
|
||||
|
||||
if (layout === 'one-column') {
|
||||
return (
|
||||
<DndContext>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', ...style }}
|
||||
{...others}
|
||||
className={cx(others.className, 'nb-action-bar')}
|
||||
>
|
||||
{props.children && (
|
||||
<div style={{ marginRight: 8 }}>
|
||||
<Space {...spaceProps}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
return <RecursionField key={key} name={key} schema={schema} />;
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<InitializerComponent />
|
||||
</div>
|
||||
</DndContext>
|
||||
<Portal>
|
||||
<DndContext>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, ...style }}
|
||||
{...others}
|
||||
className={cx(others.className, 'nb-action-bar')}
|
||||
>
|
||||
{props.children && (
|
||||
<div>
|
||||
<Space {...spaceProps}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
return <RecursionField key={key} name={key} schema={schema} />;
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<InitializerComponent style={{ margin: '0 !important' }} />
|
||||
</div>
|
||||
</DndContext>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
|
||||
@ -41,25 +85,18 @@ export const ActionBar = observer(
|
||||
!designable && !hasActions
|
||||
? undefined
|
||||
: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
overflowX: 'auto',
|
||||
flexShrink: 0,
|
||||
...style,
|
||||
}
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
overflowX: 'auto',
|
||||
flexShrink: 0,
|
||||
...style,
|
||||
}
|
||||
}
|
||||
{...others}
|
||||
className={cx(others.className, 'nb-action-bar')}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
.ant-space:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`}
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<DndContext>
|
||||
<Space {...spaceProps}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
|
@ -1,8 +1,19 @@
|
||||
import { Schema } from '@formily/react';
|
||||
import { createContext } from 'react';
|
||||
import { DrawerProps, ModalProps } from 'antd';
|
||||
import React, { createContext } from 'react';
|
||||
import { useActionContext } from './hooks';
|
||||
|
||||
export const ActionContext = createContext<ActionContextProps>({});
|
||||
|
||||
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
|
||||
const contextProps = useActionContext();
|
||||
return (
|
||||
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value }}>
|
||||
{props.children}
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type OpenSize = 'small' | 'middle' | 'large';
|
||||
export interface ActionContextProps {
|
||||
button?: any;
|
||||
@ -15,4 +26,6 @@ export interface ActionContextProps {
|
||||
formValueChanged?: boolean;
|
||||
setFormValueChanged?: (v: boolean) => void;
|
||||
fieldSchema?: Schema;
|
||||
drawerProps?: DrawerProps;
|
||||
modalProps?: ModalProps;
|
||||
}
|
||||
|
@ -2,15 +2,16 @@ import { css } from '@emotion/css';
|
||||
import cls from 'classnames';
|
||||
import React from 'react';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useDesigner } from '../../hooks';
|
||||
import { useDesigner, useProps } from '../../hooks';
|
||||
|
||||
export const BlockItem: React.FC<any> = (props) => {
|
||||
const { className, children } = useProps(props);
|
||||
const Designer = useDesigner();
|
||||
return (
|
||||
<SortableItem
|
||||
className={cls(
|
||||
'nb-block-item',
|
||||
props.className,
|
||||
className,
|
||||
css`
|
||||
position: relative;
|
||||
&:hover {
|
||||
@ -57,7 +58,7 @@ export const BlockItem: React.FC<any> = (props) => {
|
||||
)}
|
||||
>
|
||||
<Designer />
|
||||
{props.children}
|
||||
{children}
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ export const CardItem: React.FC = (props) => {
|
||||
|
||||
return templateKey && !template ? null : (
|
||||
<BlockItem className={'noco-card-item'}>
|
||||
<Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>
|
||||
<Card style={{ marginBottom: 'var(--nb-spacing)' }} bordered={false} {...restProps}>
|
||||
{props.children}
|
||||
</Card>
|
||||
</BlockItem>
|
||||
|
@ -74,7 +74,6 @@ export const GridCardDesigner = () => {
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaComponentOptions components={{ Slider }}>
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the count of columns displayed in a row')}
|
||||
initialValues={columnCount}
|
||||
@ -155,7 +154,7 @@ export const GridCardDesigner = () => {
|
||||
field: {
|
||||
type: 'string',
|
||||
enum: sortFields,
|
||||
required:true,
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
|
@ -5,7 +5,7 @@ import { List as AntdList, PaginationProps, Col } from 'antd';
|
||||
import { useGridCardActionBarProps } from './hooks';
|
||||
import { SortableItem } from '../../common';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { useDesigner } from '../../hooks';
|
||||
import { useDesigner, useProps } from '../../hooks';
|
||||
import { GridCardItem } from './GridCard.Item';
|
||||
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
|
||||
import { GridCardDesigner } from './GridCard.Designer';
|
||||
@ -55,7 +55,9 @@ const designerCss = css`
|
||||
`;
|
||||
|
||||
const InternalGridCard = (props) => {
|
||||
const { service, columnCount = defaultColumnCount } = useGridCardBlockContext();
|
||||
const { columnCount: columnCountProp, pagination } = useProps(props);
|
||||
const { service, _columnCount = defaultColumnCount } = useGridCardBlockContext();
|
||||
const columnCount = columnCountProp || _columnCount;
|
||||
const { run, params } = service;
|
||||
const meta = service?.data?.meta;
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -70,7 +72,9 @@ const InternalGridCard = (props) => {
|
||||
new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
[key]: fieldSchema.properties['item'],
|
||||
[key]: {
|
||||
...fieldSchema.properties['item'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@ -104,6 +108,7 @@ const InternalGridCard = (props) => {
|
||||
!meta || meta.count <= meta.pageSize
|
||||
? false
|
||||
: {
|
||||
...pagination,
|
||||
onChange: onPaginationChange,
|
||||
total: meta?.count || 0,
|
||||
pageSize: meta?.pageSize || 10,
|
||||
|
@ -332,6 +332,7 @@ export const useGridRowContext = () => {
|
||||
|
||||
export const Grid: any = observer(
|
||||
(props: any) => {
|
||||
const { showDivider = true } = props;
|
||||
const gridRef = useRef(null);
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -346,36 +347,40 @@ export const Grid: any = observer(
|
||||
|
||||
return (
|
||||
<GridContext.Provider
|
||||
value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}
|
||||
value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent, showDivider }}
|
||||
>
|
||||
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
|
||||
<DndWrapper dndContext={props.dndContext}>
|
||||
<RowDivider
|
||||
rows={rows}
|
||||
first
|
||||
id={`${addr}_0`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnGrid,
|
||||
wrapSchema: wrapRowSchema,
|
||||
insertAdjacent: 'afterBegin',
|
||||
schema: fieldSchema,
|
||||
}}
|
||||
/>
|
||||
{showDivider ? (
|
||||
<RowDivider
|
||||
rows={rows}
|
||||
first
|
||||
id={`${addr}_0`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnGrid,
|
||||
wrapSchema: wrapRowSchema,
|
||||
insertAdjacent: 'afterBegin',
|
||||
schema: fieldSchema,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{rows.map((schema, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<RecursionField name={schema.name} schema={schema} />
|
||||
<RowDivider
|
||||
rows={rows}
|
||||
index={index}
|
||||
id={`${addr}_${index + 1}`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnGrid,
|
||||
wrapSchema: wrapRowSchema,
|
||||
insertAdjacent: 'afterEnd',
|
||||
schema,
|
||||
}}
|
||||
/>
|
||||
{showDivider ? (
|
||||
<RowDivider
|
||||
rows={rows}
|
||||
index={index}
|
||||
id={`${addr}_${index + 1}`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnGrid,
|
||||
wrapSchema: wrapRowSchema,
|
||||
insertAdjacent: 'afterEnd',
|
||||
schema,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
@ -394,6 +399,7 @@ Grid.Row = observer(
|
||||
const fieldSchema = useFieldSchema();
|
||||
const addr = field.address.toString();
|
||||
const cols = useColProperties();
|
||||
const { showDivider } = useGridContext();
|
||||
|
||||
return (
|
||||
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
|
||||
@ -402,40 +408,46 @@ Grid.Row = observer(
|
||||
'nb-grid-row',
|
||||
css`
|
||||
overflow-x: hidden;
|
||||
margin: 0 calc(-1 * var(--nb-spacing));
|
||||
display: flex;
|
||||
position: relative;
|
||||
/* z-index: 0; */
|
||||
`,
|
||||
)}
|
||||
style={{
|
||||
margin: showDivider ? '0 calc(-1 * var(--nb-spacing))' : null,
|
||||
}}
|
||||
>
|
||||
<ColDivider
|
||||
cols={cols}
|
||||
first
|
||||
id={`${addr}_0`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnRow,
|
||||
wrapSchema: wrapColSchema,
|
||||
insertAdjacent: 'afterBegin',
|
||||
schema: fieldSchema,
|
||||
}}
|
||||
/>
|
||||
{showDivider && (
|
||||
<ColDivider
|
||||
cols={cols}
|
||||
first
|
||||
id={`${addr}_0`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnRow,
|
||||
wrapSchema: wrapColSchema,
|
||||
insertAdjacent: 'afterBegin',
|
||||
schema: fieldSchema,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{cols.map((schema, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<RecursionField name={schema.name} schema={schema} />
|
||||
<ColDivider
|
||||
cols={cols}
|
||||
index={index}
|
||||
last={index === cols.length - 1}
|
||||
id={`${addr}_${index + 1}`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnRow,
|
||||
wrapSchema: wrapColSchema,
|
||||
insertAdjacent: 'afterEnd',
|
||||
schema,
|
||||
}}
|
||||
/>
|
||||
{showDivider && (
|
||||
<ColDivider
|
||||
cols={cols}
|
||||
index={index}
|
||||
last={index === cols.length - 1}
|
||||
id={`${addr}_${index + 1}`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnRow,
|
||||
wrapSchema: wrapColSchema,
|
||||
insertAdjacent: 'afterEnd',
|
||||
schema,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
@ -449,6 +461,7 @@ Grid.Row = observer(
|
||||
Grid.Col = observer(
|
||||
(props: any) => {
|
||||
const { cols = [] } = useContext(GridRowContext);
|
||||
const { showDivider } = useGridContext();
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
|
||||
@ -456,7 +469,7 @@ Grid.Col = observer(
|
||||
let width = '';
|
||||
if (cols?.length) {
|
||||
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
|
||||
width = `calc(${w}% - var(--nb-spacing) * ${(cols.length + 1) / cols.length})`;
|
||||
width = `calc(${w}% - var(--nb-spacing) * ${(showDivider ? cols.length + 1 : 0) / cols.length})`;
|
||||
}
|
||||
return width;
|
||||
}, [cols?.length, schema?.['x-component-props']?.['width']]);
|
||||
@ -477,5 +490,5 @@ Grid.Col = observer(
|
||||
</GridColContext.Provider>
|
||||
);
|
||||
},
|
||||
{ displayName: 'Grid.Col' },
|
||||
{ displayName: 'Grid.Row' },
|
||||
);
|
||||
|
@ -21,12 +21,22 @@
|
||||
--nb-designer-offset: -10px;
|
||||
}
|
||||
.theme-compact {
|
||||
--nb-spacing: 16px;
|
||||
--nb-spacing: 12px;
|
||||
--nb-designer-offset: -6px;
|
||||
.ant-formily-item {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.ant-card {
|
||||
margin-bottom: 16px !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-formily-item-control-content-component .ant-tag {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
html body {
|
||||
--adm-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB,
|
||||
Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji,
|
||||
Segoe UI Symbol;
|
||||
}
|
||||
|
@ -11,11 +11,13 @@ const FixedBlockContext = React.createContext<{
|
||||
height: number | string;
|
||||
fixedBlockUID: boolean | string;
|
||||
fixedBlockUIDRef: React.MutableRefObject<boolean | string>;
|
||||
inFixedBlock: boolean;
|
||||
}>({
|
||||
setFixedBlock: () => {},
|
||||
height: 0,
|
||||
fixedBlockUID: false,
|
||||
fixedBlockUIDRef: { current: false },
|
||||
inFixedBlock: false,
|
||||
});
|
||||
|
||||
export const useFixedSchema = () => {
|
||||
@ -69,8 +71,9 @@ export const FixedBlockDesignerItem = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const record = useRecord();
|
||||
const { inFixedBlock } = useFixedBlock();
|
||||
|
||||
if (Object.keys(record).length) {
|
||||
if (Object.keys(record).length || !inFixedBlock) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@ -127,7 +130,7 @@ const FixedBlock: React.FC<FixedBlockProps> = (props) => {
|
||||
_setFixedBlock(v);
|
||||
};
|
||||
return (
|
||||
<FixedBlockContext.Provider value={{ height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
|
||||
<FixedBlockContext.Provider value={{ inFixedBlock: true, height, setFixedBlock, fixedBlockUID, fixedBlockUIDRef }}>
|
||||
<div
|
||||
className={fixedBlockUID ? fixedBlockCss : ''}
|
||||
style={{
|
||||
|
@ -245,7 +245,7 @@ export const Page = (props) => {
|
||||
<Tabs.TabPane
|
||||
tab={
|
||||
<SortableItem
|
||||
id={schema.name}
|
||||
id={schema.name as string}
|
||||
schema={schema}
|
||||
className={classNames('nb-action-link', designerCss, props.className)}
|
||||
>
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { CollectionProvider, useCollection } from '../../../collection-manager';
|
||||
import { FormProvider, SchemaComponentOptions } from '../../core';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContext, useActionContext } from '../action';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { FileSelector } from '../preview';
|
||||
import { useFieldNames } from './useFieldNames';
|
||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||
@ -269,7 +269,7 @@ const Drawer: React.FunctionComponent<{
|
||||
return (
|
||||
<RecordPickerProvider {...recordPickerProps}>
|
||||
<CollectionProvider allowNull name={collectionField?.target}>
|
||||
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
|
||||
<ActionContextProvider openMode="drawer" visible={visible} setVisible={setVisible}>
|
||||
<FormProvider>
|
||||
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
|
||||
<SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
|
||||
@ -283,7 +283,7 @@ const Drawer: React.FunctionComponent<{
|
||||
</SchemaComponentOptions>
|
||||
</TableSelectorParamsProvider>
|
||||
</FormProvider>
|
||||
</ActionContext.Provider>
|
||||
</ActionContextProvider>
|
||||
</CollectionProvider>
|
||||
</RecordPickerProvider>
|
||||
);
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
import { WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { BlockAssociationContext } from '../../../block-provider/BlockProvider';
|
||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||
import { FormProvider } from '../../core';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContext, useActionContext } from '../action';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||
import { Preview } from '../preview';
|
||||
import { isShowFilePicker } from './InputRecordPicker';
|
||||
@ -56,8 +55,7 @@ export const ReadPrettyRecordPicker: React.FC = observer(
|
||||
const text = getLabelFormatValue(labelUiSchema, val, true);
|
||||
return (
|
||||
<Fragment key={`${record.id}_${index}`}>
|
||||
{/* test-record-picker-read-pretty-item 用于在单元测试中方便选中元素 */}
|
||||
<span className="test-record-picker-read-pretty-item">
|
||||
<span>
|
||||
{snapshot || isTagsMode ? (
|
||||
text
|
||||
) : (
|
||||
@ -112,11 +110,11 @@ export const ReadPrettyRecordPicker: React.FC = observer(
|
||||
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
<ActionContext.Provider
|
||||
<ActionContextProvider
|
||||
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
|
||||
>
|
||||
{renderRecordProvider()}
|
||||
</ActionContext.Provider>
|
||||
</ActionContextProvider>
|
||||
</CollectionProvider>
|
||||
</BlockAssociationContext.Provider>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import { TableSelector } from './TableSelector';
|
||||
export * from './TableBlockDesigner';
|
||||
export * from './TableField';
|
||||
export * from './TableSelectorDesigner';
|
||||
export * from './FilterDynamicComponent';
|
||||
|
||||
export const TableV2 = Table;
|
||||
|
||||
|
@ -7,15 +7,18 @@ import { Icon } from '../../../icon';
|
||||
import { useSchemaInitializer } from '../../../schema-initializer';
|
||||
import { DndContext, SortableItem } from '../../common';
|
||||
import { useDesigner } from '../../hooks/useDesigner';
|
||||
import { useTabsContext } from './context';
|
||||
import { TabsDesigner } from './Tabs.Designer';
|
||||
|
||||
export const Tabs: any = observer(
|
||||
(props: TabsProps) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const contextProps = useTabsContext();
|
||||
return (
|
||||
<DndContext>
|
||||
<AntdTabs
|
||||
{...contextProps}
|
||||
style={props.style}
|
||||
tabBarExtraContent={{
|
||||
right: render(),
|
||||
@ -93,7 +96,7 @@ Tabs.TabPane = observer(
|
||||
</SortableItem>
|
||||
);
|
||||
},
|
||||
{ displayName: 'TabPane' },
|
||||
{ displayName: 'Tabs.TabPane' },
|
||||
);
|
||||
|
||||
Tabs.Designer = TabsDesigner;
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const TabsContext = React.createContext<TabsProps>({});
|
||||
|
||||
export const TabsContextProvider: React.FC<TabsProps> = ({ children, ...props }) => {
|
||||
return <TabsContext.Provider value={props}>{children}</TabsContext.Provider>;
|
||||
};
|
||||
|
||||
export const useTabsContext = () => {
|
||||
return React.useContext(TabsContext);
|
||||
};
|
@ -1 +1,2 @@
|
||||
export * from './Tabs';
|
||||
export * from './context';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { cx } from '@emotion/css';
|
||||
import { observer, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, HTMLAttributes, useContext } from 'react';
|
||||
|
||||
export const DraggableContext = createContext(null);
|
||||
export const SortableContext = createContext(null);
|
||||
@ -19,18 +20,21 @@ export const SortableProvider = (props) => {
|
||||
};
|
||||
|
||||
export const Sortable = (props: any) => {
|
||||
const { component, style, children, openMode, ...others } = props;
|
||||
const { droppable } = useContext(SortableContext);
|
||||
const { component, overStyle, style, children, openMode, ...others } = props;
|
||||
const { draggable, droppable } = useContext(SortableContext);
|
||||
const { isOver, setNodeRef } = droppable;
|
||||
const droppableStyle = { ...style };
|
||||
|
||||
if (isOver) {
|
||||
droppableStyle['color'] = 'rgba(241, 139, 98, .1)';
|
||||
if (isOver && draggable?.active?.id !== droppable?.over?.id) {
|
||||
droppableStyle[component === 'a' ? 'color' : 'background'] = 'rgba(241, 139, 98, .15)';
|
||||
Object.assign(droppableStyle, overStyle);
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
component || 'div',
|
||||
{
|
||||
...others,
|
||||
className: cx('nb-sortable-designer', props.className),
|
||||
ref: setNodeRef,
|
||||
style: droppableStyle,
|
||||
},
|
||||
@ -55,9 +59,15 @@ const useSortableItemId = (props) => {
|
||||
return field.address.toString();
|
||||
};
|
||||
|
||||
export const SortableItem: React.FC<any> = observer(
|
||||
interface SortableItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
eid?: string;
|
||||
schema?: Schema;
|
||||
removeParentsIfNoChildren?: boolean;
|
||||
}
|
||||
|
||||
export const SortableItem: React.FC<SortableItemProps> = observer(
|
||||
(props) => {
|
||||
const { schema, id, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
|
||||
const { schema, id, eid, removeParentsIfNoChildren, ...others } = useSortableItemProps(props);
|
||||
return (
|
||||
<SortableProvider
|
||||
id={id}
|
||||
@ -67,7 +77,9 @@ export const SortableItem: React.FC<any> = observer(
|
||||
removeParentsIfNoChildren: removeParentsIfNoChildren ?? true,
|
||||
}}
|
||||
>
|
||||
<Sortable {...others}>{props.children}</Sortable>
|
||||
<Sortable id={eid} {...others}>
|
||||
{props.children}
|
||||
</Sortable>
|
||||
</SortableProvider>
|
||||
);
|
||||
},
|
||||
|
@ -144,7 +144,7 @@ export class Designable {
|
||||
if (!current['x-uid']) {
|
||||
return;
|
||||
}
|
||||
await api.request({
|
||||
const res = await api.request({
|
||||
url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
@ -165,7 +165,7 @@ export class Designable {
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
onSuccess?.();
|
||||
onSuccess?.(res?.data?.data);
|
||||
message.success(t('Saved successfully'), 0.2);
|
||||
});
|
||||
this.on('patch', async ({ schema }) => {
|
||||
@ -321,7 +321,7 @@ export class Designable {
|
||||
removed = parent;
|
||||
}
|
||||
}
|
||||
this.emit('remove', { removed });
|
||||
return this.emit('remove', { removed });
|
||||
}
|
||||
|
||||
removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) {
|
||||
@ -501,7 +501,7 @@ export class Designable {
|
||||
const s = this.current.addProperty(wrapped.name || uid(), wrapped);
|
||||
s.parent = this.current;
|
||||
const [schema1, schema2] = splitWrapSchema(s, schema);
|
||||
this.emit('insertAdjacent', {
|
||||
return this.emit('insertAdjacent', {
|
||||
position: 'beforeEnd',
|
||||
schema: schema2,
|
||||
wrap: schema1,
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { ISchema, observer } from '@formily/react';
|
||||
import { ISchema, observer, useForm } from '@formily/react';
|
||||
import { Button, Dropdown, Menu, Switch } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Icon } from '../icon';
|
||||
import { SchemaComponent, useActionContext } from '../schema-component';
|
||||
import { useCompile, useDesignable } from '../schema-component/hooks';
|
||||
import './style.less';
|
||||
import {
|
||||
@ -106,6 +107,24 @@ SchemaInitializer.Button = observer(
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buttonDom = (
|
||||
<Button
|
||||
type={'dashed'}
|
||||
style={{
|
||||
borderColor: '#f18b62',
|
||||
color: '#f18b62',
|
||||
...style,
|
||||
}}
|
||||
{...others}
|
||||
icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon}
|
||||
>
|
||||
{compile(props.children || props.title)}
|
||||
</Button>
|
||||
);
|
||||
if (!items.length) {
|
||||
return buttonDom;
|
||||
}
|
||||
const menu = <Menu style={{ maxHeight: '60vh', overflowY: 'auto' }}>{renderItems(items)}</Menu>;
|
||||
if (!designable && props.designable !== true) {
|
||||
return null;
|
||||
@ -131,22 +150,7 @@ SchemaInitializer.Button = observer(
|
||||
{...dropdown}
|
||||
overlay={menu}
|
||||
>
|
||||
{component ? (
|
||||
component
|
||||
) : (
|
||||
<Button
|
||||
type={'dashed'}
|
||||
style={{
|
||||
borderColor: '#f18b62',
|
||||
color: '#f18b62',
|
||||
...style,
|
||||
}}
|
||||
{...others}
|
||||
icon={<Icon type={icon as string} />}
|
||||
>
|
||||
{compile(props.children || props.title)}
|
||||
</Button>
|
||||
)}
|
||||
{component ? component : buttonDom}
|
||||
</Dropdown>
|
||||
</SchemaInitializerButtonContext.Provider>
|
||||
);
|
||||
@ -242,6 +246,102 @@ SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
|
||||
return component;
|
||||
};
|
||||
|
||||
interface SchemaInitializerActionModalProps {
|
||||
title: string;
|
||||
schema: any;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (values: any) => void;
|
||||
buttonText?: any;
|
||||
}
|
||||
SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => {
|
||||
const { title, schema, buttonText, onCancel, onSubmit } = props;
|
||||
|
||||
const useCancelAction = useCallback(() => {
|
||||
const form = useForm();
|
||||
const ctx = useActionContext();
|
||||
return {
|
||||
async run() {
|
||||
await onCancel?.();
|
||||
ctx.setVisible(false);
|
||||
form.reset();
|
||||
},
|
||||
};
|
||||
}, [onCancel]);
|
||||
|
||||
const useSubmitAction = useCallback(() => {
|
||||
const form = useForm();
|
||||
const ctx = useActionContext();
|
||||
return {
|
||||
async run() {
|
||||
await form.validate();
|
||||
await onSubmit?.(form.values);
|
||||
ctx.setVisible(false);
|
||||
form.reset();
|
||||
},
|
||||
};
|
||||
}, [onSubmit]);
|
||||
const defaultSchema = useMemo(() => {
|
||||
return {
|
||||
type: 'void',
|
||||
properties: {
|
||||
action1: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
icon: 'PlusOutlined',
|
||||
style: {
|
||||
borderColor: 'rgb(241, 139, 98)',
|
||||
color: 'rgb(241, 139, 98)',
|
||||
},
|
||||
title: buttonText,
|
||||
type: 'dashed',
|
||||
},
|
||||
properties: {
|
||||
drawer1: {
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Action.Modal',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
maxWidth: '520px',
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
type: 'void',
|
||||
title,
|
||||
properties: {
|
||||
...(schema?.properties || schema),
|
||||
footer: {
|
||||
'x-component': 'Action.Modal.Footer',
|
||||
type: 'void',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: useCancelAction,
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: useSubmitAction,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [buttonText, schema, title, useCancelAction, useSubmitAction]);
|
||||
|
||||
return <SchemaComponent schema={defaultSchema as any} />;
|
||||
};
|
||||
|
||||
SchemaInitializer.SwitchItem = (props) => {
|
||||
return (
|
||||
<SchemaInitializer.Item onClick={props.onClick}>
|
||||
|
@ -4,9 +4,7 @@ import { SchemaComponent, useActionContext, useDesignable, useRecordIndex } from
|
||||
|
||||
export const TabPaneInitializers = (props?: any) => {
|
||||
const { designable, insertBeforeEnd } = useDesignable();
|
||||
if (!designable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const useSubmitAction = () => {
|
||||
const form = useForm();
|
||||
const ctx = useActionContext();
|
||||
@ -118,6 +116,11 @@ export const TabPaneInitializers = (props?: any) => {
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!designable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SchemaComponent schema={schema} />;
|
||||
};
|
||||
|
||||
|
@ -1064,6 +1064,9 @@ export const createGridCardBlockSchema = (options) => {
|
||||
...others,
|
||||
},
|
||||
'x-component': 'BlockItem',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGridCardBlockItemProps }}',
|
||||
},
|
||||
'x-designer': 'GridCard.Designer',
|
||||
properties: {
|
||||
actionBar: {
|
||||
@ -1081,7 +1084,7 @@ export const createGridCardBlockSchema = (options) => {
|
||||
type: 'array',
|
||||
'x-component': 'GridCard',
|
||||
'x-component-props': {
|
||||
props: '{{ useGridCardBlockProps }}',
|
||||
useProps: '{{ useGridCardBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
item: {
|
||||
|
@ -56,7 +56,7 @@ export const GeneralSchemaItems: React.FC<{
|
||||
/>
|
||||
)}
|
||||
<SchemaSettings.SwitchItem
|
||||
checked={field.decoratorProps.showTitle ?? true}
|
||||
checked={fieldSchema['x-decorator-props']?.['showTitle'] ?? true}
|
||||
title={t('Display title')}
|
||||
onChange={(checked) => {
|
||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||
|
@ -31,6 +31,11 @@ const titleCss = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const overrideAntdCSS = css`
|
||||
& .ant-space-item .anticon {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
export const GeneralSchemaDesigner = (props: any) => {
|
||||
const { disableInitializer, title, template, draggable = true } = props;
|
||||
const { dn, designable } = useDesignable();
|
||||
@ -62,7 +67,7 @@ export const GeneralSchemaDesigner = (props: any) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'general-schema-designer'}>
|
||||
<div className={classNames('general-schema-designer', overrideAntdCSS)}>
|
||||
{title && (
|
||||
<div className={classNames('general-schema-designer-title', titleCss)}>
|
||||
<Space size={2}>
|
||||
|
@ -148,7 +148,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
|
||||
};
|
||||
|
||||
SchemaSettings.Template = function Template(props) {
|
||||
const { componentName, collectionName, resourceName } = props;
|
||||
const { componentName, collectionName, resourceName, needRender } = props;
|
||||
const { t } = useTranslation();
|
||||
const { getCollection } = useCollectionManager();
|
||||
const { dn, setVisible, template, fieldSchema } = useSchemaSettings();
|
||||
@ -156,7 +156,7 @@ SchemaSettings.Template = function Template(props) {
|
||||
const api = useAPIClient();
|
||||
const { dn: tdn } = useBlockTemplateContext();
|
||||
const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager();
|
||||
if (!collectionName) {
|
||||
if (!collectionName && !needRender) {
|
||||
return null;
|
||||
}
|
||||
if (template) {
|
||||
@ -182,7 +182,7 @@ SchemaSettings.Template = function Template(props) {
|
||||
<SchemaSettings.Item
|
||||
onClick={async () => {
|
||||
setVisible(false);
|
||||
const { title } = getCollection(collectionName);
|
||||
const { title } = collectionName ? getCollection(collectionName) : { title: '' };
|
||||
const values = await FormDialog(t('Save as template'), () => {
|
||||
return (
|
||||
<FormLayout layout={'vertical'}>
|
||||
@ -194,7 +194,7 @@ SchemaSettings.Template = function Template(props) {
|
||||
name: {
|
||||
title: t('Template name'),
|
||||
required: true,
|
||||
default: `${compile(title)}_${t(componentName)}`,
|
||||
default: title ? `${compile(title)}_${t(componentName)}` : t(componentName),
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
|
@ -123,6 +123,10 @@ export const useSchemaTemplateManager = () => {
|
||||
const items = templates?.filter?.((template) => template.collectionName === collectionName);
|
||||
return items || [];
|
||||
},
|
||||
getTemplatesByComponentName(componentName: string): Array<any> {
|
||||
const items = templates?.filter?.((template) => template.componentName === componentName);
|
||||
return items || [];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { uid } from '@formily/shared';
|
||||
import { Menu } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContext, SchemaComponent, useActionContext } from '../';
|
||||
import { ActionContextProvider, SchemaComponent, useActionContext } from '../';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { DropdownVisibleContext } from './CurrentUser';
|
||||
|
||||
@ -119,7 +119,7 @@ export const ChangePassword = () => {
|
||||
const { t } = useTranslation();
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Menu.Item
|
||||
key="password"
|
||||
eventKey={'ChangePassword'}
|
||||
@ -131,6 +131,6 @@ export const ChangePassword = () => {
|
||||
{t('Change password')}
|
||||
</Menu.Item>
|
||||
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
</ActionContext.Provider>
|
||||
</ActionContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,6 @@ import { EditProfile } from './EditProfile';
|
||||
import { LanguageSettings } from './LanguageSettings';
|
||||
import { SwitchRole } from './SwitchRole';
|
||||
import { ThemeSettings } from './ThemeSettings';
|
||||
|
||||
const ApplicationVersion = () => {
|
||||
const data = useCurrentAppInfo();
|
||||
return (
|
||||
@ -20,16 +19,18 @@ const ApplicationVersion = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DropdownVisibleContext = createContext(null);
|
||||
|
||||
export const CurrentUser = () => {
|
||||
/**
|
||||
* @note If you want to change here, Note the Setting block on the mobile side
|
||||
*/
|
||||
export const SettingsMenu: React.FC<{
|
||||
redirectUrl?: string;
|
||||
}> = (props) => {
|
||||
const { redirectUrl = '' } = props;
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
const appAllowed = allowAll || snippets?.includes('app');
|
||||
const history = useHistory();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { data } = useCurrentUserContext();
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
const appAllowed = allowAll || snippets?.includes('app');
|
||||
const silenceApi = useAPIClient();
|
||||
const check = async () => {
|
||||
return await new Promise((resolve) => {
|
||||
@ -52,6 +53,69 @@ export const CurrentUser = () => {
|
||||
}, 3000);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Menu>
|
||||
<ApplicationVersion />
|
||||
<Menu.Divider />
|
||||
<EditProfile />
|
||||
<ChangePassword />
|
||||
<Menu.Divider />
|
||||
<SwitchRole />
|
||||
<LanguageSettings />
|
||||
<ThemeSettings />
|
||||
<Menu.Divider />
|
||||
{appAllowed && (
|
||||
<>
|
||||
<Menu.Item
|
||||
key="cache"
|
||||
onClick={async () => {
|
||||
await api.resource('app').clearCache();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t('Clear cache')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="reboot"
|
||||
onClick={async () => {
|
||||
Modal.confirm({
|
||||
title: t('Reboot application'),
|
||||
content: t(
|
||||
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
|
||||
),
|
||||
okText: t('Reboot'),
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await api.resource('app').reboot();
|
||||
await check();
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Reboot application')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<Menu.Item
|
||||
key="signout"
|
||||
onClick={async () => {
|
||||
await api.auth.signOut();
|
||||
history.push(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
|
||||
}}
|
||||
>
|
||||
{t('Sign out')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
export const DropdownVisibleContext = createContext(null);
|
||||
export const CurrentUser = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { data } = useCurrentUserContext();
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', verticalAlign: 'top' }}>
|
||||
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||
@ -60,64 +124,7 @@ export const CurrentUser = () => {
|
||||
onOpenChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
overlay={
|
||||
<Menu>
|
||||
<ApplicationVersion />
|
||||
<Menu.Divider />
|
||||
<EditProfile />
|
||||
<ChangePassword />
|
||||
<Menu.Divider />
|
||||
<SwitchRole />
|
||||
<LanguageSettings />
|
||||
<ThemeSettings />
|
||||
<Menu.Divider />
|
||||
{appAllowed && (
|
||||
<>
|
||||
<Menu.Item
|
||||
key="cache"
|
||||
onClick={async () => {
|
||||
await api.resource('app').clearCache();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t('Clear cache')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="reboot"
|
||||
onClick={async () => {
|
||||
Modal.confirm({
|
||||
title: t('Reboot application'),
|
||||
content: t(
|
||||
'The will interrupt service, it may take a few seconds to restart. Are you sure to continue?',
|
||||
),
|
||||
okText: t('Reboot'),
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await api.resource('app').reboot();
|
||||
await check();
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Reboot application')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
<Menu.Item
|
||||
key="signout"
|
||||
onClick={async () => {
|
||||
await api.auth.signOut();
|
||||
history.push('/signin');
|
||||
}}
|
||||
>
|
||||
{t('Sign out')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
overlay={<SettingsMenu />}
|
||||
>
|
||||
<span
|
||||
className={css`
|
||||
|
@ -4,7 +4,7 @@ import { Menu } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActionContext,
|
||||
ActionContextProvider,
|
||||
DropdownVisibleContext,
|
||||
SchemaComponent,
|
||||
useActionContext,
|
||||
@ -119,18 +119,18 @@ export const EditProfile = () => {
|
||||
const { t } = useTranslation();
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<ActionContextProvider value={{ visible, setVisible }}>
|
||||
<Menu.Item
|
||||
key="profile"
|
||||
eventKey={'EditProfile'}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
ctx.setVisible(false);
|
||||
ctx?.setVisible(false);
|
||||
}}
|
||||
>
|
||||
{t('Edit profile')}
|
||||
</Menu.Item>
|
||||
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
</ActionContext.Provider>
|
||||
</ActionContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -54,7 +54,7 @@ export function useRedirect(next = '/admin') {
|
||||
const history = useHistory();
|
||||
const redirect = location?.['query']?.redirect;
|
||||
return useCallback(() => {
|
||||
history.push(redirect || '/admin');
|
||||
history.replace(redirect || '/admin');
|
||||
}, [redirect]);
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,6 @@ export const SwitchRole = () => {
|
||||
const api = useAPIClient();
|
||||
const roles = useCurrentRoles();
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
@ -57,7 +56,7 @@ export const SwitchRole = () => {
|
||||
onChange={async (roleName) => {
|
||||
api.auth.setRole(roleName);
|
||||
await api.resource('users').setDefaultRole({ values: { roleName } });
|
||||
history.push('/');
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
|
@ -5,7 +5,7 @@ import { css } from '@emotion/css';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useCollection } from '@nocobase/client';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Alert, Button, Modal } from 'antd';
|
||||
import { Alert, Button, Modal, Spin } from 'antd';
|
||||
import React, { useEffect, useCallback, useRef, useState, useMemo, useImperativeHandle } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useMapConfiguration } from '../hooks';
|
||||
@ -102,7 +102,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
||||
const overlay = useRef<AMap.Polygon>();
|
||||
const editor = useRef(null);
|
||||
const history = useHistory();
|
||||
const id = useRef(`nocobase-map-${type}-${Date.now().toString(32)}`);
|
||||
const id = useRef(`nocobase-map-${type || ''}-${Date.now().toString(32)}`);
|
||||
|
||||
const [commonOptions] = useState<AMap.PolylineOptions & AMap.PolygonOptions>({
|
||||
strokeWeight: 5,
|
||||
@ -316,7 +316,7 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
||||
plugins: ['AMap.MouseTool', 'AMap.PolygonEditor', 'AMap.PolylineEditor', 'AMap.CircleEditor'],
|
||||
})
|
||||
.then((amap) => {
|
||||
requestIdleCallback(() => {
|
||||
return requestIdleCallback(() => {
|
||||
map.current = new amap.Map(id.current, {
|
||||
resizeEnable: true,
|
||||
zoom,
|
||||
@ -327,10 +327,14 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.includes('多个不一致的 key')) {
|
||||
setErrMessage(t('The AccessKey is incorrect, please check it'));
|
||||
} else {
|
||||
setErrMessage(err);
|
||||
if (typeof err === 'string') {
|
||||
if (err.includes('多个不一致的 key')) {
|
||||
setErrMessage(t('The AccessKey is incorrect, please check it'));
|
||||
} else {
|
||||
setErrMessage(err);
|
||||
}
|
||||
} else if (err?.type === 'error') {
|
||||
setErrMessage('Something went wrong, please refresh the page and try again');
|
||||
}
|
||||
});
|
||||
|
||||
@ -379,6 +383,19 @@ const AMapComponent = React.forwardRef<AMapForwardedRefProps, AMapComponentProps
|
||||
id={id.current}
|
||||
style={props?.style}
|
||||
>
|
||||
{!aMap.current && (
|
||||
<div
|
||||
className={css`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
{!disabled ? (
|
||||
<>
|
||||
<Search toCenter={toCenter} aMap={aMap.current} />
|
||||
|
4
packages/plugins/mobile-client/client.d.ts
vendored
Executable file
4
packages/plugins/mobile-client/client.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/client';
|
||||
export { default } from './lib/client';
|
||||
|
30
packages/plugins/mobile-client/client.js
Executable file
30
packages/plugins/mobile-client/client.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/client"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
7
packages/plugins/mobile-client/docs/en-US/index.md
Normal file
7
packages/plugins/mobile-client/docs/en-US/index.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Mobile-client overview
|
||||
|
||||
Provides a mobile client for the desktop client.
|
||||
|
||||
## How to visit mobile client?
|
||||
|
||||
When your desktop client is started, you can visit the mobile client in the browser by entering `http://localhost:3000/mobile`. Note that `/mobile` is the route of our mobile client.
|
13
packages/plugins/mobile-client/docs/en-US/installation.md
Normal file
13
packages/plugins/mobile-client/docs/en-US/installation.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Mobile-client Installation
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
yarn add @nocobase/plugin-mobile-client
|
||||
```
|
||||
|
||||
## Activate the plugin
|
||||
|
||||
```bash
|
||||
yarn pm enable mobile-client
|
||||
```
|
14
packages/plugins/mobile-client/docs/en-US/tabs.json
Normal file
14
packages/plugins/mobile-client/docs/en-US/tabs.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"title": "Introduction",
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"title": "Installation",
|
||||
"path": "installation"
|
||||
},
|
||||
{
|
||||
"title": "Usage",
|
||||
"path": "usage"
|
||||
}
|
||||
]
|
1
packages/plugins/mobile-client/docs/en-US/usage.md
Normal file
1
packages/plugins/mobile-client/docs/en-US/usage.md
Normal file
@ -0,0 +1 @@
|
||||
# Mobile-client Usage
|
7
packages/plugins/mobile-client/docs/zh-CN/index.md
Normal file
7
packages/plugins/mobile-client/docs/zh-CN/index.md
Normal file
@ -0,0 +1,7 @@
|
||||
# 移动端
|
||||
|
||||
提供移动端应用,独立于桌面端。
|
||||
|
||||
## 如何访问?
|
||||
|
||||
当你的桌面端启动后,可以在浏览器中输入 `http://localhost:3000/mobile` 访问移动端。注意,`/mobile` 就是我们移动端的路由。
|
13
packages/plugins/mobile-client/docs/zh-CN/installation.md
Normal file
13
packages/plugins/mobile-client/docs/zh-CN/installation.md
Normal file
@ -0,0 +1,13 @@
|
||||
# 移动端安装方法
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
yarn add @nocobase/plugin-mobile-client
|
||||
```
|
||||
|
||||
## 激活插件
|
||||
|
||||
```bash
|
||||
yarn pm enable mobile-client
|
||||
```
|
14
packages/plugins/mobile-client/docs/zh-CN/tabs.json
Normal file
14
packages/plugins/mobile-client/docs/zh-CN/tabs.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"title": "介绍",
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"title": "安装",
|
||||
"path": "installation"
|
||||
},
|
||||
{
|
||||
"title": "用法",
|
||||
"path": "usage"
|
||||
}
|
||||
]
|
2
packages/plugins/mobile-client/docs/zh-CN/usage.md
Normal file
2
packages/plugins/mobile-client/docs/zh-CN/usage.md
Normal file
@ -0,0 +1,2 @@
|
||||
# 移动端用法
|
||||
|
27
packages/plugins/mobile-client/package.json
Normal file
27
packages/plugins/mobile-client/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-mobile-client",
|
||||
"version": "0.9.4-alpha.2",
|
||||
"main": "lib/server/index.js",
|
||||
"displayName": "Mobile-client",
|
||||
"displayName.zh-CN": "移动端",
|
||||
"description": "Provide mobile client access",
|
||||
"description.zh-CN": "提供移动端访问",
|
||||
"devDependencies": {
|
||||
"@emotion/css": "^11.7.1",
|
||||
"@formily/react": "2.2.24",
|
||||
"@formily/shared": "2.2.24",
|
||||
"@formily/antd": "2.2.24",
|
||||
"@nocobase/server": "0.9.4-alpha.2",
|
||||
"@nocobase/test": "0.9.4-alpha.2",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"react": "17.x",
|
||||
"react-dom": "17.x",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"antd": "^4.24.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd-mobile": "^5.29.1"
|
||||
}
|
||||
}
|
4
packages/plugins/mobile-client/server.d.ts
vendored
Executable file
4
packages/plugins/mobile-client/server.d.ts
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './lib/server';
|
||||
export { default } from './lib/server';
|
||||
|
30
packages/plugins/mobile-client/server.js
Executable file
30
packages/plugins/mobile-client/server.js
Executable file
@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
|
||||
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||
|
||||
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||
|
||||
var _index = _interopRequireWildcard(require("./lib/server"));
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
var _exportNames = {};
|
||||
Object.defineProperty(exports, "default", {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index.default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(_index).forEach(function (key) {
|
||||
if (key === "default" || key === "__esModule") return;
|
||||
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||
if (key in exports && exports[key] === _index[key]) return;
|
||||
Object.defineProperty(exports, key, {
|
||||
enumerable: true,
|
||||
get: function get() {
|
||||
return _index[key];
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export const AppConfiguration = () => {
|
||||
return <>App Configuration</>;
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { InterfaceRouter } from '../router';
|
||||
import { MobileDevice } from '../devices';
|
||||
|
||||
export const InterfaceConfiguration = () => {
|
||||
return (
|
||||
<MobileDevice>
|
||||
<InterfaceRouter></InterfaceRouter>
|
||||
</MobileDevice>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './App';
|
||||
export * from './Interface';
|
@ -0,0 +1 @@
|
||||
import './native-call';
|
@ -0,0 +1,12 @@
|
||||
interface InvokeFunction {
|
||||
(params: { action: 'scan' }, cb: (data: { url: string }) => void): void;
|
||||
(params: { action: 'moveTaskToBack' }, cb?: () => void): void;
|
||||
}
|
||||
|
||||
const jsBridge = (window as any).jsBridge as {
|
||||
invoke: InvokeFunction;
|
||||
};
|
||||
|
||||
export const invoke: InvokeFunction = (params, cb) => {
|
||||
jsBridge.invoke(params, cb);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import { invoke } from './injects';
|
||||
|
||||
/**
|
||||
* App 右滑返回
|
||||
* @param cb 回调函数,返回 true 表示 web 自己消费,false表示 app 消费
|
||||
*/
|
||||
|
||||
const JSBridgeFunction = {
|
||||
/**
|
||||
* @description JSBridge injects
|
||||
*/
|
||||
onBackPressed: () => {
|
||||
if (history.length === 1) {
|
||||
invoke({ action: 'moveTaskToBack' });
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(JSBridgeFunction).forEach((key) => {
|
||||
window[key] = JSBridgeFunction[key];
|
||||
});
|
45
packages/plugins/mobile-client/src/client/core/index.tsx
Normal file
45
packages/plugins/mobile-client/src/client/core/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { SchemaComponentOptions, SchemaInitializerProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import {
|
||||
MBlockInitializers,
|
||||
MMenuBlockInitializer,
|
||||
MMenu,
|
||||
MContainer,
|
||||
MTabBar,
|
||||
MPage,
|
||||
MHeader,
|
||||
MSettingsBlockInitializer,
|
||||
MSettings,
|
||||
useGridCardBlockItemProps,
|
||||
useGridCardBlockProps,
|
||||
} from './schema';
|
||||
import './bridge';
|
||||
|
||||
export const MobileCore: React.FC = (props) => {
|
||||
return (
|
||||
<SchemaInitializerProvider
|
||||
initializers={{
|
||||
MBlockInitializers,
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions
|
||||
components={{
|
||||
MMenuBlockInitializer,
|
||||
MSettingsBlockInitializer,
|
||||
MContainer,
|
||||
MMenu,
|
||||
MTabBar,
|
||||
MPage,
|
||||
MHeader,
|
||||
MSettings,
|
||||
}}
|
||||
scope={{
|
||||
useGridCardBlockItemProps,
|
||||
useGridCardBlockProps,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
</SchemaInitializerProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,85 @@
|
||||
import { SchemaSettings, useDesignable } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { findSchema } from '../../helpers';
|
||||
import { Button } from 'antd';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ContainerDesigner = () => {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const tabBarSchema = fieldSchema.reduceProperties(
|
||||
(schema, next) => schema || (next['x-component'] === 'MTabBar' && next),
|
||||
) as Schema;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const field = useField();
|
||||
const schemaSettingsProps = {
|
||||
dn,
|
||||
field,
|
||||
fieldSchema,
|
||||
};
|
||||
return (
|
||||
<SchemaSettings
|
||||
title={
|
||||
<Button
|
||||
style={{
|
||||
borderColor: 'rgb(241, 139, 98)',
|
||||
color: 'rgb(241, 139, 98)',
|
||||
}}
|
||||
icon={<MenuOutlined />}
|
||||
type="dashed"
|
||||
>
|
||||
{t('App level Configuration')}
|
||||
</Button>
|
||||
}
|
||||
{...schemaSettingsProps}
|
||||
>
|
||||
<SchemaSettings.SwitchItem
|
||||
checked={!!tabBarSchema}
|
||||
title={t('Enable TabBar')}
|
||||
onChange={async (v) => {
|
||||
if (v) {
|
||||
const pageSchema = findSchema(fieldSchema, 'MPage');
|
||||
if (!pageSchema) return;
|
||||
await dn.remove(pageSchema);
|
||||
await dn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
'x-component': 'MTabBar',
|
||||
'x-component-props': {},
|
||||
name: 'tabBar',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'MTabBar.Item',
|
||||
'x-designer': 'MTabBar.Item.Designer',
|
||||
'x-component-props': {
|
||||
icon: 'HomeOutlined',
|
||||
title: t('Untitled'),
|
||||
},
|
||||
properties: {
|
||||
page: pageSchema.toJSON(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const pageSchema = findSchema(tabBarSchema.properties[Object.keys(tabBarSchema.properties)[0]], 'MPage');
|
||||
if (!pageSchema) return;
|
||||
await dn.remove(tabBarSchema);
|
||||
await dn.insertBeforeEnd(pageSchema, {
|
||||
onSuccess() {
|
||||
history.push('../');
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SchemaSettings>
|
||||
);
|
||||
};
|
@ -0,0 +1,130 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { ContainerDesigner } from './Container.Designer';
|
||||
import { RouteSwitch, SchemaComponent, SortableItem, useDesigner } from '@nocobase/client';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { Redirect, RouteProps, useParams, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
const findGrid = (schema, uid) => {
|
||||
return schema.reduceProperties((final, next) => {
|
||||
if (final) return final;
|
||||
if (next['x-component'] === 'MTabBar') {
|
||||
return findGrid(next, uid);
|
||||
}
|
||||
if (next['x-component'] === 'MTabBar.Item' && uid === next['x-uid']) {
|
||||
return next;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const TabContentComponent = () => {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
if (!name) return <></>;
|
||||
const gridSchema = findGrid(fieldSchema.properties['tabBar'], name.replace('tab_', ''));
|
||||
if (!gridSchema) {
|
||||
return <Redirect to="../" />;
|
||||
}
|
||||
return <SchemaComponent schema={gridSchema} />;
|
||||
};
|
||||
|
||||
const InternalContainer: React.FC = (props) => {
|
||||
const Designer = useDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const params = useParams<{ name: string }>();
|
||||
const match = useRouteMatch();
|
||||
const tabBarSchema = fieldSchema?.properties?.['tabBar'];
|
||||
const redirectToUid = tabBarSchema?.properties[Object.keys(tabBarSchema.properties)[0]]['x-uid'];
|
||||
|
||||
const tabRoutes = useMemo<RouteProps[]>(() => {
|
||||
if (!redirectToUid) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
!params.name
|
||||
? {
|
||||
type: 'redirect',
|
||||
to: `${match.url}/tab_${redirectToUid}`,
|
||||
from: match.url,
|
||||
exact: true,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
type: 'route',
|
||||
path: match.path,
|
||||
component: TabContentComponent,
|
||||
},
|
||||
].filter(Boolean) as any[];
|
||||
}, [redirectToUid, params.name, match.url, match.path]);
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
eid="nb-mobile-scroll-wrapper"
|
||||
className={cx(
|
||||
'nb-mobile-container',
|
||||
css`
|
||||
& > .general-schema-designer > .general-schema-designer-icons {
|
||||
right: unset;
|
||||
left: 2px;
|
||||
}
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
position: initial !important;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Designer></Designer>
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: tabRoutes.length ? '50px' : '0px',
|
||||
}}
|
||||
className={cx('nb-mobile-container-content')}
|
||||
>
|
||||
{tabRoutes.length ? (
|
||||
<RouteSwitch routes={tabRoutes as any} />
|
||||
) : (
|
||||
<SchemaComponent
|
||||
filterProperties={(schema) => {
|
||||
return schema['x-component'] !== 'MTabBar';
|
||||
}}
|
||||
schema={fieldSchema}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
'nb-mobile-container-tab-bar',
|
||||
css`
|
||||
& > .general-schema-designer {
|
||||
--nb-designer-top: 20px;
|
||||
}
|
||||
position: absolute;
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<SchemaComponent
|
||||
onlyRenderProperties
|
||||
filterProperties={(schema) => {
|
||||
return schema['x-component'] === 'MTabBar';
|
||||
}}
|
||||
schema={fieldSchema}
|
||||
></SchemaComponent>
|
||||
</div>
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MContainer = InternalContainer as unknown as typeof InternalContainer & {
|
||||
Designer: typeof ContainerDesigner;
|
||||
};
|
||||
MContainer.Designer = ContainerDesigner;
|
||||
MContainer.displayName = 'MContainer';
|
@ -0,0 +1 @@
|
||||
export * from './Container';
|
@ -0,0 +1,39 @@
|
||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { Switch } from '@formily/antd';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { useSchemaPatch } from '../../hooks';
|
||||
|
||||
export const HeaderDesigner = () => {
|
||||
const field = useField();
|
||||
const { onUpdateComponentProps } = useSchemaPatch();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<GeneralSchemaDesigner draggable={false}>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Edit info')}
|
||||
components={{ Switch }}
|
||||
initialValues={field.componentProps}
|
||||
schema={{
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: t('Title'),
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
showBack: {
|
||||
type: 'boolean',
|
||||
title: t('Display back button'),
|
||||
'x-component': 'Switch',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onSubmit={onUpdateComponentProps}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { SortableItem, useCompile, useDesigner } from '@nocobase/client';
|
||||
import { NavBar, NavBarProps } from 'antd-mobile';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { HeaderDesigner } from './Header.Designer';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
|
||||
export interface HeaderProps extends NavBarProps {
|
||||
title?: string;
|
||||
showBack?: boolean;
|
||||
}
|
||||
const InternalHeader = (props: HeaderProps) => {
|
||||
const field = useField();
|
||||
const { title = '{{ t("Untitled") }}', showBack = false } = { ...props, ...field?.componentProps };
|
||||
const Designer = useDesigner();
|
||||
const compile = useCompile();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<SortableItem
|
||||
className={cx(
|
||||
'nb-mobile-header',
|
||||
css`
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<NavBar backArrow={showBack} onBack={history.goBack}>
|
||||
{compile(title)}
|
||||
</NavBar>
|
||||
<Designer />
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MHeader = InternalHeader as unknown as typeof InternalHeader & {
|
||||
Designer: typeof HeaderDesigner;
|
||||
};
|
||||
|
||||
MHeader.Designer = HeaderDesigner;
|
@ -0,0 +1 @@
|
||||
export * from './Header';
|
@ -0,0 +1,6 @@
|
||||
export * from './menu';
|
||||
export * from './container';
|
||||
export * from './tab-bar';
|
||||
export * from './page';
|
||||
export * from './header';
|
||||
export * from './settings';
|
@ -0,0 +1,46 @@
|
||||
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { Button } from 'antd';
|
||||
import { useFieldSchema, useField } from '@formily/react';
|
||||
|
||||
export const MenuDesigner: React.FC = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const field = useField();
|
||||
const schemaSettingsProps = {
|
||||
dn,
|
||||
field,
|
||||
fieldSchema,
|
||||
};
|
||||
return (
|
||||
<SchemaSettings
|
||||
title={
|
||||
<Button
|
||||
style={{
|
||||
borderColor: 'rgb(241, 139, 98)',
|
||||
color: 'rgb(241, 139, 98)',
|
||||
}}
|
||||
icon={<MenuOutlined />}
|
||||
type="dashed"
|
||||
>
|
||||
{t('Menu configuration')}
|
||||
</Button>
|
||||
}
|
||||
{...schemaSettingsProps}
|
||||
>
|
||||
<SchemaSettings.Remove
|
||||
key="remove"
|
||||
removeParentsIfNoChildren
|
||||
confirm={{
|
||||
title: t('Delete menu block'),
|
||||
}}
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</SchemaSettings>
|
||||
);
|
||||
};
|
@ -0,0 +1,104 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
Icon,
|
||||
SchemaSettings,
|
||||
SortableItem,
|
||||
useCompile,
|
||||
useDesignable,
|
||||
useDesigner,
|
||||
} from '@nocobase/client';
|
||||
import { List, ListItemProps } from 'antd-mobile';
|
||||
import React from 'react';
|
||||
import { useHistory, useParams, useRouteMatch } from 'react-router-dom';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { useSchemaPatch } from '../../hooks';
|
||||
|
||||
interface MMenuItemProps extends ListItemProps {
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const InternalMenuItem: React.FC<MMenuItemProps> = (props) => {
|
||||
const { icon, name } = props;
|
||||
const Designer = useDesigner();
|
||||
const history = useHistory();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
const match = useRouteMatch();
|
||||
const params = useParams<{ name: string }>();
|
||||
|
||||
const onToPage = () => {
|
||||
history.push(params.name ? fieldSchema['x-uid'] : `${match.url}/${fieldSchema['x-uid']}`);
|
||||
};
|
||||
return (
|
||||
<SortableItem
|
||||
className={cx(
|
||||
'nb-mobile-menu-item',
|
||||
css`
|
||||
width: 100%;
|
||||
background: var(--adm-color-background);
|
||||
> .adm-list-item {
|
||||
background: inherit;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<List.Item arrow clickable {...props} prefix={<Icon type={icon} />} onClick={onToPage}>
|
||||
{compile(name)}
|
||||
</List.Item>
|
||||
<Designer></Designer>
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItemDesigner: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { onUpdateComponentProps } = useSchemaPatch();
|
||||
const field = useField();
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Edit menu info')}
|
||||
initialValues={field.componentProps}
|
||||
schema={{
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: t('Menu name'),
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
icon: {
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'IconPicker',
|
||||
title: t('Icon'),
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
}}
|
||||
onSubmit={onUpdateComponentProps}
|
||||
/>
|
||||
<SchemaSettings.Remove
|
||||
key="remove"
|
||||
removeParentsIfNoChildren
|
||||
confirm={{
|
||||
title: t('Delete menu item?'),
|
||||
}}
|
||||
breakRemoveOn={{
|
||||
'x-component': 'MMenu',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
||||
|
||||
export const MenuItem = InternalMenuItem as typeof InternalMenuItem as unknown as {
|
||||
Designer: typeof MenuItemDesigner;
|
||||
};
|
||||
|
||||
MenuItem.Designer = MenuItemDesigner;
|
@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from './Menu.Item';
|
||||
import {
|
||||
DndContext,
|
||||
SchemaComponent,
|
||||
SchemaInitializer,
|
||||
SortableItem,
|
||||
useDesignable,
|
||||
useDesigner,
|
||||
} from '@nocobase/client';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { MenuDesigner } from './Menu.Designer';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { List } from 'antd-mobile';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { menuItemSchema } from './schema';
|
||||
|
||||
const InternalMenu: React.FC = (props) => {
|
||||
const Designer = useDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { insertBeforeEnd, designable } = useDesignable();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onAddMenuItem = (values: any) => {
|
||||
const properties = {
|
||||
page: {
|
||||
type: 'void',
|
||||
'x-component': 'MPage',
|
||||
'x-designer': 'MPage.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
header: {
|
||||
type: 'void',
|
||||
'x-component': 'MHeader',
|
||||
'x-designer': 'MHeader.Designer',
|
||||
'x-component-props': {
|
||||
title: values.name,
|
||||
showBack: true,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
showDivider: false,
|
||||
},
|
||||
'x-initializer': 'MBlockInitializers',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return insertBeforeEnd({
|
||||
type: 'void',
|
||||
title: values.name,
|
||||
'x-component': 'MMenu.Item',
|
||||
'x-component-props': values,
|
||||
'x-designer': 'MMenu.Item.Designer',
|
||||
properties,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
className={cx(
|
||||
'nb-mobile-menu',
|
||||
css`
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: var(--nb-spacing);
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<List>
|
||||
{designable && (
|
||||
<List.Item>
|
||||
<Designer />
|
||||
</List.Item>
|
||||
)}
|
||||
<DndContext>
|
||||
<SchemaComponent onlyRenderProperties schema={fieldSchema}></SchemaComponent>
|
||||
</DndContext>
|
||||
{designable ? (
|
||||
<List.Item>
|
||||
<SchemaInitializer.ActionModal
|
||||
buttonText={t('Add menu item')}
|
||||
title={t('Add menu item')}
|
||||
schema={menuItemSchema}
|
||||
onSubmit={onAddMenuItem}
|
||||
/>
|
||||
</List.Item>
|
||||
) : null}
|
||||
</List>
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MMenu = InternalMenu as unknown as typeof InternalMenu & {
|
||||
Item: typeof MenuItem;
|
||||
Designer: typeof MenuDesigner;
|
||||
};
|
||||
|
||||
MMenu.Item = MenuItem;
|
||||
MMenu.Designer = MenuDesigner;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { SchemaInitializer } from '@nocobase/client';
|
||||
|
||||
export const MMenuBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
icon={<MenuOutlined />}
|
||||
onClick={async () => {
|
||||
insert({
|
||||
type: 'void',
|
||||
'x-component': 'MMenu',
|
||||
'x-designer': 'MMenu.Designer',
|
||||
'x-component-props': {},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './MenuBlockInitializer';
|
||||
export * from './Menu';
|
@ -0,0 +1,17 @@
|
||||
export const menuItemSchema = {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: `{{t('Menu name')}}`,
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
icon: {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'IconPicker',
|
||||
title: `{{t('Icon')}}`,
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,106 @@
|
||||
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { findGridSchema } from '../../helpers';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button } from 'antd';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
|
||||
export const PageDesigner = (props) => {
|
||||
const { showBack } = props;
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const headerSchema = fieldSchema?.properties?.['header'];
|
||||
const tabsSchema = fieldSchema?.properties?.['tabs'];
|
||||
const field = useField();
|
||||
const schemaSettingsProps = {
|
||||
dn,
|
||||
field,
|
||||
fieldSchema,
|
||||
};
|
||||
|
||||
return (
|
||||
<SchemaSettings
|
||||
title={
|
||||
<Button
|
||||
style={{
|
||||
borderColor: 'rgb(241, 139, 98)',
|
||||
color: 'rgb(241, 139, 98)',
|
||||
}}
|
||||
icon={<MenuOutlined />}
|
||||
type="dashed"
|
||||
>
|
||||
{t('Page configuration')}
|
||||
</Button>
|
||||
}
|
||||
{...schemaSettingsProps}
|
||||
>
|
||||
<SchemaSettings.SwitchItem
|
||||
checked={!!headerSchema}
|
||||
title={t('Enable Header')}
|
||||
onChange={async (v) => {
|
||||
if (v) {
|
||||
await dn.insertAfterBegin({
|
||||
type: 'void',
|
||||
name: 'header',
|
||||
'x-component': 'MHeader',
|
||||
'x-designer': 'MHeader.Designer',
|
||||
'x-component-props': {
|
||||
title: fieldSchema.parent['x-component-props']?.name,
|
||||
showBack,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await dn.remove(headerSchema);
|
||||
}
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.SwitchItem
|
||||
checked={!!tabsSchema}
|
||||
title={t('Enable Tabs')}
|
||||
onChange={async (v) => {
|
||||
if (v) {
|
||||
const gridSchema = findGridSchema(fieldSchema);
|
||||
if (gridSchema) {
|
||||
return dn.remove(gridSchema).then(() => {
|
||||
return dn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
name: 'tabs',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
'x-initializer-props': {
|
||||
gridInitializer: 'MBlockInitializers',
|
||||
},
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Untitled")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
...gridSchema,
|
||||
'x-uid': uid(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const gridSchema = findGridSchema(tabsSchema.properties[Object.keys(tabsSchema.properties)[0]]);
|
||||
if (gridSchema) {
|
||||
return dn.remove(tabsSchema).then(() => dn.insertBeforeEnd(gridSchema, {}));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SchemaSettings>
|
||||
);
|
||||
};
|
@ -0,0 +1,140 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { PageDesigner } from './Page.Designer';
|
||||
import { ActionBarProvider, SortableItem, TabsContextProvider, useDesigner } from '@nocobase/client';
|
||||
import { RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { countGridCol } from '../../helpers';
|
||||
import { TabsProps } from 'antd';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
const globalActionCSS = css`
|
||||
#nb-position-container > & {
|
||||
height: 49px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
margin-bottom: 0px !important;
|
||||
padding: 0 var(--nb-spacing);
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
background: #ffffff;
|
||||
z-index: 100;
|
||||
}
|
||||
`;
|
||||
|
||||
const InternalPage: React.FC = (props) => {
|
||||
const Designer = useDesigner();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const query = new URLSearchParams(location.search);
|
||||
const tabsSchema = fieldSchema.properties?.['tabs'];
|
||||
// Only support globalActions in page
|
||||
const onlyInPage = fieldSchema.root === fieldSchema.parent;
|
||||
let hasGlobalActions = false;
|
||||
if (!tabsSchema) {
|
||||
hasGlobalActions = countGridCol(fieldSchema.properties['grid'], 2) === 1;
|
||||
} else if (query.has('tab') && tabsSchema.properties[query.get('tab')]) {
|
||||
hasGlobalActions = countGridCol(tabsSchema.properties[query.get('tab')]?.properties?.['grid'], 2) === 1;
|
||||
} else {
|
||||
const schema = Object.values(tabsSchema.properties).sort((t1, t2) => t1['x-index'] - t2['x-index'])[0];
|
||||
history.replace({
|
||||
pathname: location.pathname,
|
||||
search: new URLSearchParams({
|
||||
tab: schema.name.toString(),
|
||||
}).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
const onTabsChange = useCallback<TabsProps['onChange']>(
|
||||
(key) => {
|
||||
history.replace({
|
||||
pathname: history.location.pathname,
|
||||
search: new URLSearchParams({
|
||||
tab: key,
|
||||
}).toString(),
|
||||
});
|
||||
},
|
||||
[history],
|
||||
);
|
||||
|
||||
const GlobalActionProvider = useMemo(() => {
|
||||
if (hasGlobalActions) {
|
||||
return ActionBarProvider;
|
||||
}
|
||||
return React.Fragment;
|
||||
}, [hasGlobalActions]);
|
||||
|
||||
return (
|
||||
<GlobalActionProvider
|
||||
container={hasGlobalActions && onlyInPage ? document.getElementById('nb-position-container') : null}
|
||||
forceProps={{
|
||||
layout: 'one-column',
|
||||
className: globalActionCSS,
|
||||
}}
|
||||
>
|
||||
<SortableItem
|
||||
eid="nb-mobile-scroll-wrapper"
|
||||
className={cx(
|
||||
'nb-mobile-page',
|
||||
css`
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-bottom: var(--nb-spacing);
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Designer {...fieldSchema?.['x-designer-props']}></Designer>
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: tabsSchema ? null : 'var(--nb-spacing)',
|
||||
}}
|
||||
className={cx(
|
||||
'nb-mobile-page-header',
|
||||
css`
|
||||
& > .ant-tabs > .ant-tabs-nav {
|
||||
background: #fff;
|
||||
padding: 0 var(--nb-spacing);
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return 'MHeader' === s['x-component'];
|
||||
}}
|
||||
></RecursionField>
|
||||
<TabsContextProvider activeKey={query.get('tab')} onChange={onTabsChange}>
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return 'Tabs' === s['x-component'];
|
||||
}}
|
||||
></RecursionField>
|
||||
</TabsContextProvider>
|
||||
</div>
|
||||
|
||||
{!tabsSchema && (
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] !== 'MHeader';
|
||||
}}
|
||||
></RecursionField>
|
||||
)}
|
||||
</SortableItem>
|
||||
</GlobalActionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const MPage = InternalPage as unknown as typeof InternalPage & {
|
||||
Designer: typeof PageDesigner;
|
||||
};
|
||||
MPage.Designer = PageDesigner;
|
||||
MPage.displayName = 'MPage';
|
@ -0,0 +1 @@
|
||||
export * from './Page';
|
@ -0,0 +1,22 @@
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
|
||||
export const SettingsDesigner = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettings.Remove
|
||||
key="remove"
|
||||
removeParentsIfNoChildren
|
||||
confirm={{
|
||||
title: t('Delete settings block'),
|
||||
}}
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { SettingsMenu, SortableItem, useDesigner } from '@nocobase/client';
|
||||
import { SettingsDesigner } from './Settings.Designer';
|
||||
import { css, cx } from '@emotion/css';
|
||||
export const InternalSettings = () => {
|
||||
const Designer = useDesigner();
|
||||
return (
|
||||
<SortableItem
|
||||
className={cx(
|
||||
'nb-mobile-setting',
|
||||
css`
|
||||
margin-bottom: var(--nb-spacing);
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Designer />
|
||||
<SettingsMenu redirectUrl="/mobile" />
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
export const MSettings = InternalSettings as unknown as typeof InternalSettings & {
|
||||
Designer: typeof SettingsDesigner;
|
||||
};
|
||||
|
||||
MSettings.Designer = SettingsDesigner;
|
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { SchemaInitializer } from '@nocobase/client';
|
||||
|
||||
export const MSettingsBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
icon={<SettingOutlined />}
|
||||
onClick={async () => {
|
||||
insert({
|
||||
type: 'void',
|
||||
'x-component': 'MSettings',
|
||||
'x-designer': 'MSettings.Designer',
|
||||
'x-component-props': {},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './Settings';
|
||||
export * from './SettingsBlockInitializer';
|
@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { TabBar, TabBarItemProps } from 'antd-mobile';
|
||||
import { GeneralSchemaDesigner, SchemaSettings, SortableItem, useDesigner } from '@nocobase/client';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { useSchemaPatch } from '../../hooks';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { tabItemSchema } from './schema';
|
||||
|
||||
const InternalItem: React.FC<TabBarItemProps> = () => {
|
||||
// NOTE: nothing to do
|
||||
// return <TabBar.Item {...props}></TabBar.Item>;
|
||||
const Designer = useDesigner();
|
||||
return (
|
||||
<SortableItem
|
||||
className={cx(
|
||||
'nb-mobile-tab-bar-item',
|
||||
css`
|
||||
position: absolute !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Designer />
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const Designer = () => {
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { onUpdateComponentProps } = useSchemaPatch();
|
||||
const field = useField();
|
||||
const tabItems = Object.keys(fieldSchema.parent.properties).length;
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Edit info')}
|
||||
initialValues={field.componentProps}
|
||||
schema={tabItemSchema}
|
||||
onSubmit={onUpdateComponentProps}
|
||||
></SchemaSettings.ModalItem>
|
||||
{tabItems > 1 ? (
|
||||
<SchemaSettings.Remove
|
||||
key="remove"
|
||||
removeParentsIfNoChildren
|
||||
confirm={{
|
||||
title: t('Delete tab item?'),
|
||||
}}
|
||||
breakRemoveOn={{
|
||||
'x-component': 'MTabBar',
|
||||
}}
|
||||
></SchemaSettings.Remove>
|
||||
) : null}
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
||||
|
||||
export const TabBarItem = InternalItem as unknown as typeof InternalItem & {
|
||||
Designer: typeof Designer;
|
||||
};
|
||||
|
||||
TabBarItem.Designer = Designer;
|
@ -0,0 +1,113 @@
|
||||
import { TabBar } from 'antd-mobile';
|
||||
import { TabBarItem } from './TabBar.Item';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
||||
import { DndContext, Icon, SchemaComponent, SchemaInitializer, SortableItem, useDesignable } from '@nocobase/client';
|
||||
import { useTranslation } from '../../../../locale';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { tabItemSchema } from './schema';
|
||||
|
||||
export const InternalTabBar: React.FC = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { designable } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { insertBeforeEnd, dn } = useDesignable();
|
||||
const history = useHistory();
|
||||
const params = useParams<{ name: string }>();
|
||||
|
||||
const onAddTab = useCallback((values: any) => {
|
||||
return insertBeforeEnd({
|
||||
type: 'void',
|
||||
'x-component': 'MTabBar.Item',
|
||||
'x-component-props': values,
|
||||
'x-designer': 'MTabBar.Item.Designer',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'MPage',
|
||||
'x-designer': 'MPage.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'MBlockInitializers',
|
||||
'x-component-props': {
|
||||
showDivider: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<SortableItem
|
||||
className={cx(
|
||||
'nb-mobile-tab-bar',
|
||||
css`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<DndContext>
|
||||
<TabBar
|
||||
activeKey={params.name}
|
||||
onChange={(key) => {
|
||||
if (key === 'add-tab') {
|
||||
return;
|
||||
}
|
||||
history.push(key);
|
||||
}}
|
||||
safeArea
|
||||
className={cx(
|
||||
css`
|
||||
width: 100%;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
{fieldSchema.mapProperties((schema, name) => {
|
||||
const cp = schema['x-component-props'];
|
||||
return (
|
||||
<TabBar.Item
|
||||
{...cp}
|
||||
key={`tab_${schema['x-uid']}`}
|
||||
title={
|
||||
<>
|
||||
{cp.title}
|
||||
<SchemaComponent schema={schema} name={name} />
|
||||
</>
|
||||
}
|
||||
icon={cp.icon ? <Icon type={cp.icon} /> : undefined}
|
||||
></TabBar.Item>
|
||||
);
|
||||
})}
|
||||
{designable && Object.keys(fieldSchema.properties).length < 5 ? (
|
||||
<TabBar.Item
|
||||
className={css`
|
||||
.adm-tab-bar-item-icon {
|
||||
height: auto;
|
||||
}
|
||||
`}
|
||||
icon={<SchemaInitializer.ActionModal title={t('Add tab')} onSubmit={onAddTab} schema={tabItemSchema} />}
|
||||
key="add-tab"
|
||||
></TabBar.Item>
|
||||
) : null}
|
||||
</TabBar>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MTabBar = InternalTabBar as unknown as typeof InternalTabBar & {
|
||||
Item: typeof TabBarItem;
|
||||
};
|
||||
|
||||
MTabBar.Item = TabBarItem;
|
||||
MTabBar.displayName = 'MTabBar';
|
@ -0,0 +1 @@
|
||||
export * from './TabBar';
|
@ -0,0 +1,18 @@
|
||||
export const tabItemSchema = {
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: `{{ t('Title') }}`,
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
icon: {
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'IconPicker',
|
||||
title: `{{ t('Icon') }}`,
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { ISchema, Schema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
export const gridItemWrap = (schema: ISchema) => {
|
||||
return {
|
||||
type: 'void',
|
||||
'x-component': 'MGrid.Item',
|
||||
properties: {
|
||||
[schema.name || uid()]: schema,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const findSchema = (schema: Schema, component: string) => {
|
||||
const gridSchema = schema.reduceProperties(
|
||||
(schema, next) => schema || (next['x-component'] === component && next),
|
||||
) as Schema;
|
||||
return gridSchema;
|
||||
};
|
||||
export const findGridSchema = (schema: Schema) => {
|
||||
return findSchema(schema, 'Grid');
|
||||
};
|
||||
|
||||
const allowComponents = ['Grid', 'Grid.Row'];
|
||||
const plusComponent = ['Grid.Col'];
|
||||
export const countGridCol = (schema: Schema, countToStop?: number) => {
|
||||
if (!schema) return 0;
|
||||
let count = 0;
|
||||
if (plusComponent.includes(schema['x-component'])) {
|
||||
count += 1;
|
||||
}
|
||||
if (typeof countToStop === 'number' && count >= countToStop) return count;
|
||||
if (allowComponents.includes(schema['x-component'])) {
|
||||
schema.mapProperties((schema) => {
|
||||
count += countGridCol(schema, countToStop);
|
||||
});
|
||||
}
|
||||
return count;
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './useSchemaPatch';
|
@ -0,0 +1,24 @@
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { useDesignable } from '@nocobase/client';
|
||||
import _ from 'lodash';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useSchemaPatch = () => {
|
||||
const { dn } = useDesignable();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
|
||||
const onUpdateComponentProps = useCallback((data) => {
|
||||
_.set(fieldSchema, 'x-component-props', data);
|
||||
field.componentProps = { ...field.componentProps, ...data };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-component-props': fieldSchema['x-component-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}, []);
|
||||
|
||||
return { onUpdateComponentProps };
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from './initializers';
|
||||
export * from './components';
|
||||
export * from './scopes';
|
@ -0,0 +1,81 @@
|
||||
import { gridRowColWrap } from '@nocobase/client';
|
||||
import { generateNTemplate } from '../../../locale';
|
||||
|
||||
// 页面里添加区块
|
||||
export const MBlockInitializers = {
|
||||
title: '{{t("Add block")}}',
|
||||
icon: 'PlusOutlined',
|
||||
wrap: gridRowColWrap,
|
||||
items: [
|
||||
{
|
||||
key: 'dataBlocks',
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Data blocks")}}',
|
||||
children: [
|
||||
{
|
||||
key: 'GridCard',
|
||||
type: 'item',
|
||||
title: '{{t("Grid Card")}}',
|
||||
component: 'GridCardBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'table',
|
||||
type: 'item',
|
||||
title: '{{t("Table")}}',
|
||||
component: 'TableBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'form',
|
||||
type: 'item',
|
||||
title: '{{t("Form")}}',
|
||||
component: 'FormBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'details',
|
||||
type: 'item',
|
||||
title: '{{t("Details")}}',
|
||||
component: 'DetailsBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'calendar',
|
||||
type: 'item',
|
||||
title: '{{t("Calendar")}}',
|
||||
component: 'CalendarBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'mapBlock',
|
||||
type: 'item',
|
||||
title: generateNTemplate('Map'),
|
||||
component: 'MapBlockInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'otherBlocks',
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Other blocks")}}',
|
||||
children: [
|
||||
{
|
||||
key: 'menu',
|
||||
type: 'item',
|
||||
title: generateNTemplate('Menu'),
|
||||
component: 'MMenuBlockInitializer',
|
||||
sort: 100,
|
||||
},
|
||||
{
|
||||
key: 'markdown',
|
||||
type: 'item',
|
||||
title: '{{t("Markdown")}}',
|
||||
component: 'MarkdownBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
type: 'item',
|
||||
title: generateNTemplate('Settings'),
|
||||
component: 'MSettingsBlockInitializer',
|
||||
sort: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './BlockInitializers';
|
@ -0,0 +1,35 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useInterfaceContext } from '../../../router/InterfaceProvider';
|
||||
import { PaginationProps } from 'antd';
|
||||
|
||||
const listCss = css`
|
||||
padding: 0 var(--nb-spacing);
|
||||
& > .nb-action-bar {
|
||||
padding: unset !important;
|
||||
background: unset !important;
|
||||
}
|
||||
`;
|
||||
export const useGridCardBlockItemProps = () => {
|
||||
return {
|
||||
className: listCss,
|
||||
};
|
||||
};
|
||||
|
||||
const columnCountConfig = {
|
||||
xs: 1,
|
||||
sm: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 1,
|
||||
xxl: 1,
|
||||
};
|
||||
|
||||
export const useGridCardBlockProps = () => {
|
||||
const isInterface = useInterfaceContext();
|
||||
return {
|
||||
columnCount: isInterface ? columnCountConfig : null,
|
||||
pagination: {
|
||||
simple: true,
|
||||
} as PaginationProps,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './grid-card';
|
24
packages/plugins/mobile-client/src/client/devices/iOS6.tsx
Normal file
24
packages/plugins/mobile-client/src/client/devices/iOS6.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
const iOS6: React.FC<{
|
||||
className: string;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'nb-mobile-device-ios6',
|
||||
css(`
|
||||
display: flex;
|
||||
width: 375px;
|
||||
height: 667px;
|
||||
`),
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default iOS6;
|
27
packages/plugins/mobile-client/src/client/devices/index.tsx
Normal file
27
packages/plugins/mobile-client/src/client/devices/index.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import Device from './iOS6';
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
export const MobileDevice: React.FC = (props) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'nb-mobile-device-wrapper',
|
||||
css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Device
|
||||
className={css`
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
|
||||
`}
|
||||
{...props}
|
||||
></Device>
|
||||
</div>
|
||||
);
|
||||
};
|
33
packages/plugins/mobile-client/src/client/index.tsx
Normal file
33
packages/plugins/mobile-client/src/client/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { SettingsCenterProvider } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useTranslation } from './locale';
|
||||
import { AppConfiguration, InterfaceConfiguration } from './configuration';
|
||||
import { RouterSwitchProvider } from './router';
|
||||
import { MobileCore } from './core';
|
||||
|
||||
export default React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsCenterProvider
|
||||
settings={{
|
||||
['mobile-client']: {
|
||||
title: t('Mobile Client-side'),
|
||||
icon: 'MobileOutlined',
|
||||
tabs: {
|
||||
interface: {
|
||||
title: t('Interface Configuration'),
|
||||
component: InterfaceConfiguration,
|
||||
},
|
||||
app: {
|
||||
title: t('App Configuration'),
|
||||
component: AppConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RouterSwitchProvider>{props.children}</RouterSwitchProvider>
|
||||
</SettingsCenterProvider>
|
||||
);
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
const locale = {
|
||||
|
||||
}
|
||||
|
||||
export default locale;
|
23
packages/plugins/mobile-client/src/client/locale/index.ts
Normal file
23
packages/plugins/mobile-client/src/client/locale/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { i18n } from '@nocobase/client';
|
||||
import { useTranslation as useT } from 'react-i18next';
|
||||
import enUS from './en-US';
|
||||
import zhCN from './zh-CN';
|
||||
|
||||
export const NAMESPACE = 'mobile-client';
|
||||
|
||||
i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||
i18n.addResources('en-US', NAMESPACE, enUS);
|
||||
|
||||
export function lang(key: string) {
|
||||
return i18n.t(key, { ns: NAMESPACE });
|
||||
}
|
||||
|
||||
export function generateNTemplate(key: string) {
|
||||
return `{{t('${key}', { ns: '${NAMESPACE}' })}}`;
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
return useT([NAMESPACE, 'client'], {
|
||||
nsMode: 'fallback',
|
||||
});
|
||||
}
|
31
packages/plugins/mobile-client/src/client/locale/zh-CN.ts
Normal file
31
packages/plugins/mobile-client/src/client/locale/zh-CN.ts
Normal file
@ -0,0 +1,31 @@
|
||||
const locale = {
|
||||
'Mobile Client-side': '移动端',
|
||||
'Interface Configuration': '界面配置',
|
||||
'App Configuration': 'App配置',
|
||||
'Enable TabBar': '启用底部标签栏',
|
||||
Untitled: '未设置标题',
|
||||
'Edit info': '编辑信息',
|
||||
Title: '标题',
|
||||
'Display back button': '展示退后按钮',
|
||||
'Delete menu': '删除菜单',
|
||||
'Edit menu info': '编辑菜单信息',
|
||||
Menu: '菜单',
|
||||
'Menu name': '菜单名',
|
||||
Icon: '图标',
|
||||
'Delete menu item?': '删除菜单项',
|
||||
'Add menu item': '添加菜单项',
|
||||
'Page template': '页面模板',
|
||||
'Template mode': '模板模式',
|
||||
'Enable Header': '启用头部栏',
|
||||
'Enable Tabs': '启用标签栏',
|
||||
'Delete tab item?': '是否删除标签项?',
|
||||
'Add tab': '添加标签页',
|
||||
'App level Configuration': '应用级别配置',
|
||||
'Menu configuration': '菜单配置',
|
||||
'Page configuration': '页面配置',
|
||||
Settings: '设置',
|
||||
'Delete settings block': '删除设置区块',
|
||||
'Delete menu block': '删除菜单区块',
|
||||
};
|
||||
|
||||
export default locale;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user