parent
a5010cd74c
commit
8a2548f480
7
.changeset/tiny-starfishes-smash.md
Normal file
7
.changeset/tiny-starfishes-smash.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@nocobase/plugin-mobile-client": patch
|
||||||
|
"@hera/plugin-mobile": patch
|
||||||
|
"@nocobase/client": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
collapse
|
@ -1,4 +1,4 @@
|
|||||||
import Icon, { TableOutlined } from '@ant-design/icons';
|
import { TableOutlined } from '@ant-design/icons';
|
||||||
import { Divider, Empty, Input, MenuProps, Spin } from 'antd';
|
import { Divider, Empty, Input, MenuProps, Spin } from 'antd';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -13,6 +13,7 @@ import { useCompile } from '../../schema-component';
|
|||||||
import { useSchemaTemplateManager } from '../../schema-templates';
|
import { useSchemaTemplateManager } from '../../schema-templates';
|
||||||
import { useCollectionDataSourceItems } from '../utils';
|
import { useCollectionDataSourceItems } from '../utils';
|
||||||
import { DataSource } from '../../data-source';
|
import { DataSource } from '../../data-source';
|
||||||
|
import { Icon } from '../../icon';
|
||||||
|
|
||||||
const MENU_ITEM_HEIGHT = 40;
|
const MENU_ITEM_HEIGHT = 40;
|
||||||
const STEP = 15;
|
const STEP = 15;
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
"name": "@hera/plugin-mobile",
|
"name": "@hera/plugin-mobile",
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"main": "dist/server/index.js",
|
"main": "dist/server/index.js",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/icons": "5.x",
|
||||||
"@nocobase/schema": "workspace:*",
|
"@nocobase/schema": "workspace:*",
|
||||||
"antd-mobile": "^5.29.1"
|
"@types/lodash": "^4.17.0",
|
||||||
|
"antd": "^5.12.8",
|
||||||
|
"antd-mobile": "^5.33.1",
|
||||||
|
"antd-mobile-icons": "^0.3.0",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"react-i18next": "^11.15.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@nocobase/client": "workspace:*",
|
"@nocobase/client": "workspace:*",
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '@nocobase/client';
|
||||||
|
|
||||||
|
import NoticeSvg from './notice.svg';
|
||||||
|
import SwiperSvg from './swiper.svg';
|
||||||
|
import TabSearchSvg from './tab-search.svg';
|
||||||
|
|
||||||
|
export const loadBase64Icon = (base64: string) => () => (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: atob(base64.replace('data:image/svg+xml;base64,', '')) }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NoticeIcon = (props: any) => <Icon component={loadBase64Icon(NoticeSvg)} {...props} />;
|
||||||
|
export const SwiperIcon = (props: any) => <Icon component={loadBase64Icon(SwiperSvg)} {...props} />;
|
||||||
|
export const TabSearchIcon = (props: any) => <Icon component={loadBase64Icon(TabSearchSvg)} {...props} />;
|
||||||
|
|
||||||
|
Icon.register({
|
||||||
|
'notice-block': NoticeIcon,
|
||||||
|
'swiper-block': SwiperIcon,
|
||||||
|
'tab-search': TabSearchIcon,
|
||||||
|
});
|
@ -0,0 +1,6 @@
|
|||||||
|
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M338.773333 362.666667H170.666667v298.666666h105.578666l45.013334 0.021334 17.258666 0.426666L682.666667 809.258667V214.890667L338.773333 362.666667z m-17.578666-85.333334L768 85.333333v853.333334l-448-191.978667L85.333333 746.666667V277.333333h235.861334zM170.666667 661.333333v85.333334h85.333333v-85.333334H170.666667zM853.333333 298.666667h85.333334v426.666666h-85.333334z"
|
||||||
|
fill="#000000"
|
||||||
|
></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 512 B |
@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
|
||||||
|
<path d="M0 219.428571h73.142857v585.142858H0V219.428571z m950.857143 0h73.142857v585.142858h-73.142857V219.428571z m-146.285714-146.285714a73.142857 73.142857 0 0 1 73.142857 73.142857v731.428572a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V146.285714a73.142857 73.142857 0 0 1 73.142857-73.142857h585.142858z m0 73.142857H219.428571v731.428572h585.142858V146.285714zM365.714286 694.857143a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 804 B |
@ -0,0 +1,5 @@
|
|||||||
|
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
|
||||||
|
<path d="M1263.243636 908.939636l-156.485818-156.392727c25.6-37.050182 39.842909-79.685818 39.842909-128.093091 0-125.114182-102.4-227.514182-227.607272-227.514182a228.258909 228.258909 0 0 0-227.514182 227.514182c0 125.207273 102.4 227.607273 227.514182 227.607273 48.407273 0 91.042909-14.242909 128-39.842909l156.392727 156.392727a41.239273 41.239273 0 0 0 59.764363 0c19.921455-17.035636 19.921455-45.428364 0-59.671273zM921.786182 766.696727a140.846545 140.846545 0 0 1-142.242909-142.242909c0-79.592727 62.557091-142.149818 142.242909-142.149818 79.592727 0 142.242909 62.557091 142.242909 142.149818 0 79.685818-62.650182 142.242909-142.242909 142.242909z"></path>
|
||||||
|
<path d="M694.272 894.696727H111.150545v-512h583.121455a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.728727H111.150545V98.304h853.364364v156.392727a43.752727 43.752727 0 0 0 42.635636 42.635637 43.752727 43.752727 0 0 0 42.728728-42.635637V69.818182a57.064727 57.064727 0 0 0-56.878546-56.878546H82.757818A57.064727 57.064727 0 0 0 25.879273 69.818182v853.364363c0 31.278545 25.6 56.785455 56.785454 56.785455h611.607273a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.635637z"></path>
|
||||||
|
<path d="M637.393455 155.182545c-22.714182 0-42.635636 21.690182-42.635637 46.545455 0 24.762182 19.921455 46.545455 42.635637 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635637-46.545455 0-24.855273-19.921455-46.545455-42.635637-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485819c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485819z"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@ -1,46 +1,16 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { SwiperBlock, SwiperBlockInitializer } from './schema-initializer/SwiperBlockInitializer';
|
import PluginBlock from './schma-block';
|
||||||
import { TabSearchBlock, TabSearchBlockInitializer } from './schema-initializer/TabSearchBlockInitializer';
|
import PluginTabSearch from './schma-component/tab-search';
|
||||||
import { NoticeBlock, NoticeBlockInitializer } from './schema-initializer/NoticeBlockInitializer';
|
import './assets/svg/index';
|
||||||
|
|
||||||
export class PluginMobileClient extends Plugin {
|
|
||||||
async afterAdd() {}
|
|
||||||
|
|
||||||
|
class PluginMobileClient extends Plugin {
|
||||||
async beforeLoad() {}
|
async beforeLoad() {}
|
||||||
|
|
||||||
async load() {
|
async load() {}
|
||||||
this.app.addComponents({
|
|
||||||
NoticeBlock,
|
|
||||||
NoticeBlockInitializer,
|
|
||||||
SwiperBlock,
|
|
||||||
SwiperBlockInitializer,
|
|
||||||
TabSearchBlock,
|
|
||||||
TabSearchBlockInitializer,
|
|
||||||
});
|
|
||||||
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.swiper', {
|
|
||||||
title: 'swiper',
|
|
||||||
name: 'swiper',
|
|
||||||
type: 'item',
|
|
||||||
Component: 'SwiperBlockInitializer',
|
|
||||||
});
|
|
||||||
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.notice', {
|
|
||||||
title: 'notice',
|
|
||||||
name: 'notice',
|
|
||||||
type: 'item',
|
|
||||||
Component: 'NoticeBlockInitializer',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'filterBlocks', {
|
async afterAdd() {
|
||||||
title: '{{t("Filter blocks")}}',
|
this.pm.add(PluginBlock);
|
||||||
type: 'itemGroup',
|
this.pm.add(PluginTabSearch);
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'tabSearch',
|
|
||||||
title: 'tabSearch',
|
|
||||||
Component: 'TabSearchBlockInitializer',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
packages/plugins/@hera/plugin-mobile/src/client/locale.tsx
Normal file
15
packages/plugins/@hera/plugin-mobile/src/client/locale.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useApp, tval as nTval, i18n } from '@nocobase/client';
|
||||||
|
|
||||||
|
const NAMESPACE = '@hera/plugin-mobile';
|
||||||
|
|
||||||
|
export const useTranslation = (): any => {
|
||||||
|
const { i18n } = useApp();
|
||||||
|
const t = (key: string, props = {}) => i18n.t(key, { ns: NAMESPACE, ...props });
|
||||||
|
return { t };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
|
||||||
|
|
||||||
|
export function lang(key: string) {
|
||||||
|
return i18n.t(key, { ns: NAMESPACE });
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
import { ISchema } from '@nocobase/schema';
|
|
||||||
import {
|
|
||||||
Icon,
|
|
||||||
DataBlockInitializer,
|
|
||||||
useCollectionManager,
|
|
||||||
useSchemaInitializer,
|
|
||||||
useSchemaInitializerItem,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
import { uid } from '@nocobase/utils/client';
|
|
||||||
import { NoticeBar } from 'antd-mobile';
|
|
||||||
|
|
||||||
const NoticeSvg = () => (
|
|
||||||
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
|
|
||||||
<path
|
|
||||||
d="M338.773333 362.666667H170.666667v298.666666h105.578666l45.013334 0.021334 17.258666 0.426666L682.666667 809.258667V214.890667L338.773333 362.666667z m-17.578666-85.333334L768 85.333333v853.333334l-448-191.978667L85.333333 746.666667V277.333333h235.861334zM170.666667 661.333333v85.333334h85.333333v-85.333334H170.666667zM853.333333 298.666667h85.333334v426.666666h-85.333334z"
|
|
||||||
fill="#000000"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const NoticeIcon = (props: any) => <Icon type="" component={NoticeSvg} {...props} />;
|
|
||||||
|
|
||||||
export const NoticeBlock = () => {
|
|
||||||
return <NoticeBar content="月底冲量佣金上调" color="alert" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NoticeBlockInitializer = () => {
|
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const cm = useCollectionManager();
|
|
||||||
const itemConfig = useSchemaInitializerItem();
|
|
||||||
return (
|
|
||||||
<DataBlockInitializer
|
|
||||||
{...itemConfig}
|
|
||||||
icon={<NoticeIcon />}
|
|
||||||
componentType={'Notice'}
|
|
||||||
onCreateBlockSchema={async ({ item }) => {
|
|
||||||
const collection = cm.getCollection(item.name);
|
|
||||||
const schema = createGridCardBlockSchema({
|
|
||||||
collection: item.name,
|
|
||||||
dataSource: item.dataSource,
|
|
||||||
rowKey: collection.filterTargetKey || 'id',
|
|
||||||
settings: 'blockSettings:gridCard',
|
|
||||||
});
|
|
||||||
insert(schema);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createGridCardBlockSchema = (options) => {
|
|
||||||
const {
|
|
||||||
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
|
||||||
actionInitializers = 'GridCardActionInitializers',
|
|
||||||
itemActionInitializers = 'GridCardItemActionInitializers',
|
|
||||||
collection,
|
|
||||||
association,
|
|
||||||
resource,
|
|
||||||
template,
|
|
||||||
dataSource,
|
|
||||||
settings,
|
|
||||||
...others
|
|
||||||
} = options;
|
|
||||||
const resourceName = resource || association || collection;
|
|
||||||
const schema: ISchema = {
|
|
||||||
type: 'void',
|
|
||||||
'x-acl-action': `${resourceName}:view`,
|
|
||||||
'x-decorator': 'GridCard.Decorator',
|
|
||||||
'x-decorator-props': {
|
|
||||||
resource: resourceName,
|
|
||||||
collection,
|
|
||||||
association,
|
|
||||||
dataSource,
|
|
||||||
readPretty: true,
|
|
||||||
action: 'list',
|
|
||||||
params: {
|
|
||||||
pageSize: 12,
|
|
||||||
},
|
|
||||||
runWhenParamsChanged: true,
|
|
||||||
...others,
|
|
||||||
},
|
|
||||||
'x-component': 'BlockItem',
|
|
||||||
'x-component-props': {
|
|
||||||
useProps: '{{ useGridCardBlockItemProps }}',
|
|
||||||
},
|
|
||||||
'x-toolbar': 'BlockSchemaToolbar',
|
|
||||||
'x-settings': settings,
|
|
||||||
properties: {
|
|
||||||
[uid()]: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'NoticeBlock',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return schema;
|
|
||||||
};
|
|
@ -1,120 +0,0 @@
|
|||||||
import { ISchema } from '@nocobase/schema';
|
|
||||||
import {
|
|
||||||
DataBlockInitializer,
|
|
||||||
css,
|
|
||||||
Icon,
|
|
||||||
useCollectionManager,
|
|
||||||
useSchemaInitializer,
|
|
||||||
useSchemaInitializerItem,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React from 'react';
|
|
||||||
import { uid } from '@nocobase/utils/client';
|
|
||||||
import { Swiper, Toast } from 'antd-mobile';
|
|
||||||
|
|
||||||
const SwiperSvg = () => (
|
|
||||||
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
|
|
||||||
<path d="M0 219.428571h73.142857v585.142858H0V219.428571z m950.857143 0h73.142857v585.142858h-73.142857V219.428571z m-146.285714-146.285714a73.142857 73.142857 0 0 1 73.142857 73.142857v731.428572a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V146.285714a73.142857 73.142857 0 0 1 73.142857-73.142857h585.142858z m0 73.142857H219.428571v731.428572h585.142858V146.285714zM365.714286 694.857143a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z"></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const SwiperIcon = (props: any) => <Icon type="" component={SwiperSvg} {...props} />;
|
|
||||||
|
|
||||||
export const SwiperBlock = () => {
|
|
||||||
const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac'];
|
|
||||||
const items = colors.map((color, index) => (
|
|
||||||
<Swiper.Item key={index}>
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
height: 120px;
|
|
||||||
color: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 48px;
|
|
||||||
user-select: none;
|
|
||||||
`}
|
|
||||||
style={{ background: color }}
|
|
||||||
onClick={() => {
|
|
||||||
Toast.show(`你点击了卡片 ${index + 1}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
</Swiper.Item>
|
|
||||||
));
|
|
||||||
return (
|
|
||||||
<Swiper loop autoplay>
|
|
||||||
{items}
|
|
||||||
</Swiper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SwiperBlockInitializer = () => {
|
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const cm = useCollectionManager();
|
|
||||||
const itemConfig = useSchemaInitializerItem();
|
|
||||||
return (
|
|
||||||
<DataBlockInitializer
|
|
||||||
{...itemConfig}
|
|
||||||
icon={<SwiperIcon />}
|
|
||||||
componentType={'Swiper'}
|
|
||||||
onCreateBlockSchema={async ({ item }) => {
|
|
||||||
const collection = cm.getCollection(item.name);
|
|
||||||
const schema = createGridCardBlockSchema({
|
|
||||||
collection: item.name,
|
|
||||||
dataSource: item.dataSource,
|
|
||||||
rowKey: collection.filterTargetKey || 'id',
|
|
||||||
settings: 'blockSettings:gridCard',
|
|
||||||
});
|
|
||||||
insert(schema);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createGridCardBlockSchema = (options) => {
|
|
||||||
const {
|
|
||||||
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
|
||||||
actionInitializers = 'GridCardActionInitializers',
|
|
||||||
itemActionInitializers = 'GridCardItemActionInitializers',
|
|
||||||
collection,
|
|
||||||
association,
|
|
||||||
resource,
|
|
||||||
template,
|
|
||||||
dataSource,
|
|
||||||
settings,
|
|
||||||
...others
|
|
||||||
} = options;
|
|
||||||
const resourceName = resource || association || collection;
|
|
||||||
const schema: ISchema = {
|
|
||||||
type: 'void',
|
|
||||||
'x-acl-action': `${resourceName}:view`,
|
|
||||||
'x-decorator': 'GridCard.Decorator',
|
|
||||||
'x-decorator-props': {
|
|
||||||
resource: resourceName,
|
|
||||||
collection,
|
|
||||||
association,
|
|
||||||
dataSource,
|
|
||||||
readPretty: true,
|
|
||||||
action: 'list',
|
|
||||||
params: {
|
|
||||||
pageSize: 12,
|
|
||||||
},
|
|
||||||
runWhenParamsChanged: true,
|
|
||||||
...others,
|
|
||||||
},
|
|
||||||
'x-component': 'BlockItem',
|
|
||||||
'x-component-props': {
|
|
||||||
useProps: '{{ useGridCardBlockItemProps }}',
|
|
||||||
},
|
|
||||||
'x-toolbar': 'BlockSchemaToolbar',
|
|
||||||
'x-settings': settings,
|
|
||||||
properties: {
|
|
||||||
[uid()]: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'SwiperBlock',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return schema;
|
|
||||||
};
|
|
@ -1,144 +0,0 @@
|
|||||||
import { ISchema } from '@nocobase/schema';
|
|
||||||
import {
|
|
||||||
DataBlockInitializer,
|
|
||||||
Icon,
|
|
||||||
css,
|
|
||||||
useCollectionManager,
|
|
||||||
useSchemaInitializer,
|
|
||||||
useSchemaInitializerItem,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { uid } from '@nocobase/utils/client';
|
|
||||||
import { Button, SearchBar, Tabs, Picker } from 'antd-mobile';
|
|
||||||
|
|
||||||
const TabSearchSvg = () => (
|
|
||||||
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
|
|
||||||
<path d="M1263.243636 908.939636l-156.485818-156.392727c25.6-37.050182 39.842909-79.685818 39.842909-128.093091 0-125.114182-102.4-227.514182-227.607272-227.514182a228.258909 228.258909 0 0 0-227.514182 227.514182c0 125.207273 102.4 227.607273 227.514182 227.607273 48.407273 0 91.042909-14.242909 128-39.842909l156.392727 156.392727a41.239273 41.239273 0 0 0 59.764363 0c19.921455-17.035636 19.921455-45.428364 0-59.671273zM921.786182 766.696727a140.846545 140.846545 0 0 1-142.242909-142.242909c0-79.592727 62.557091-142.149818 142.242909-142.149818 79.592727 0 142.242909 62.557091 142.242909 142.149818 0 79.685818-62.650182 142.242909-142.242909 142.242909z"></path>
|
|
||||||
<path d="M694.272 894.696727H111.150545v-512h583.121455a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.728727H111.150545V98.304h853.364364v156.392727a43.752727 43.752727 0 0 0 42.635636 42.635637 43.752727 43.752727 0 0 0 42.728728-42.635637V69.818182a57.064727 57.064727 0 0 0-56.878546-56.878546H82.757818A57.064727 57.064727 0 0 0 25.879273 69.818182v853.364363c0 31.278545 25.6 56.785455 56.785454 56.785455h611.607273a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.635637z"></path>
|
|
||||||
<path d="M637.393455 155.182545c-22.714182 0-42.635636 21.690182-42.635637 46.545455 0 24.762182 19.921455 46.545455 42.635637 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635637-46.545455 0-24.855273-19.921455-46.545455-42.635637-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485819c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485819z"></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TabSearchIcon = (props: any) => <Icon type="" component={TabSearchSvg} {...props} />;
|
|
||||||
|
|
||||||
export const TabSearchBlock = () => {
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const basicColumns = [
|
|
||||||
[
|
|
||||||
{ label: '福建', value: 'Mon' },
|
|
||||||
{ label: '上海', value: 'Wed' },
|
|
||||||
],
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Tabs
|
|
||||||
className={css`
|
|
||||||
flex: 1;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Tabs.Tab title="水果" key="fruits" />
|
|
||||||
<Tabs.Tab title="蔬菜" key="vegetables" />
|
|
||||||
<Tabs.Tab title="动物" key="animals" />
|
|
||||||
</Tabs>
|
|
||||||
<Button onClick={() => setVisible(true)}>全国</Button>
|
|
||||||
<Picker
|
|
||||||
columns={basicColumns}
|
|
||||||
visible={visible}
|
|
||||||
onClose={() => {
|
|
||||||
setVisible(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css`
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<SearchBar
|
|
||||||
className={css`
|
|
||||||
flex: 1;
|
|
||||||
`}
|
|
||||||
placeholder="请输入内容"
|
|
||||||
/>
|
|
||||||
<Button>搜索</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TabSearchBlockInitializer = () => {
|
|
||||||
const { insert } = useSchemaInitializer();
|
|
||||||
const cm = useCollectionManager();
|
|
||||||
const itemConfig = useSchemaInitializerItem();
|
|
||||||
return (
|
|
||||||
<DataBlockInitializer
|
|
||||||
{...itemConfig}
|
|
||||||
icon={<TabSearchIcon />}
|
|
||||||
componentType={'TabSearch'}
|
|
||||||
onCreateBlockSchema={async ({ item }) => {
|
|
||||||
const collection = cm.getCollection(item.name);
|
|
||||||
const schema = createGridCardBlockSchema({
|
|
||||||
collection: item.name,
|
|
||||||
dataSource: item.dataSource,
|
|
||||||
rowKey: collection.filterTargetKey || 'id',
|
|
||||||
settings: 'blockSettings:gridCard',
|
|
||||||
});
|
|
||||||
insert(schema);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createGridCardBlockSchema = (options) => {
|
|
||||||
const {
|
|
||||||
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
|
||||||
actionInitializers = 'GridCardActionInitializers',
|
|
||||||
itemActionInitializers = 'GridCardItemActionInitializers',
|
|
||||||
collection,
|
|
||||||
association,
|
|
||||||
resource,
|
|
||||||
template,
|
|
||||||
dataSource,
|
|
||||||
settings,
|
|
||||||
...others
|
|
||||||
} = options;
|
|
||||||
const resourceName = resource || association || collection;
|
|
||||||
const schema: ISchema = {
|
|
||||||
type: 'void',
|
|
||||||
'x-acl-action': `${resourceName}:view`,
|
|
||||||
'x-decorator': 'GridCard.Decorator',
|
|
||||||
'x-decorator-props': {
|
|
||||||
resource: resourceName,
|
|
||||||
collection,
|
|
||||||
association,
|
|
||||||
dataSource,
|
|
||||||
readPretty: true,
|
|
||||||
action: 'list',
|
|
||||||
params: {
|
|
||||||
pageSize: 12,
|
|
||||||
},
|
|
||||||
runWhenParamsChanged: true,
|
|
||||||
...others,
|
|
||||||
},
|
|
||||||
'x-component': 'CardItem',
|
|
||||||
'x-component-props': {
|
|
||||||
useProps: '{{ useGridCardBlockItemProps }}',
|
|
||||||
},
|
|
||||||
'x-toolbar': 'BlockSchemaToolbar',
|
|
||||||
'x-settings': settings,
|
|
||||||
properties: {
|
|
||||||
[uid()]: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'TabSearchBlock',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return schema;
|
|
||||||
};
|
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { NoticeBlock, NoticeBlockInitializer } from './schema-initializer/NoticeBlockInitializer';
|
||||||
|
import { SwiperBlock, SwiperBlockInitializer } from './schema-initializer/SwiperBlockInitializer';
|
||||||
|
|
||||||
|
class PluginBlock extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.addComponents({
|
||||||
|
NoticeBlock,
|
||||||
|
NoticeBlockInitializer,
|
||||||
|
SwiperBlock,
|
||||||
|
SwiperBlockInitializer,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.swiper', {
|
||||||
|
title: 'swiper',
|
||||||
|
name: 'swiper',
|
||||||
|
type: 'item',
|
||||||
|
Component: 'SwiperBlockInitializer',
|
||||||
|
});
|
||||||
|
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.notice', {
|
||||||
|
title: 'notice',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'item',
|
||||||
|
Component: 'NoticeBlockInitializer',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginBlock;
|
@ -0,0 +1,49 @@
|
|||||||
|
import { ISchema } from '@nocobase/schema';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
export const createGridCardBlockSchema = (options, componentName) => {
|
||||||
|
const {
|
||||||
|
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
||||||
|
actionInitializers = 'GridCardActionInitializers',
|
||||||
|
itemActionInitializers = 'GridCardItemActionInitializers',
|
||||||
|
collection,
|
||||||
|
association,
|
||||||
|
resource,
|
||||||
|
template,
|
||||||
|
dataSource,
|
||||||
|
settings,
|
||||||
|
...others
|
||||||
|
} = options;
|
||||||
|
const resourceName = resource || association || collection;
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
'x-acl-action': `${resourceName}:view`,
|
||||||
|
'x-decorator': 'GridCard.Decorator',
|
||||||
|
'x-decorator-props': {
|
||||||
|
resource: resourceName,
|
||||||
|
collection,
|
||||||
|
association,
|
||||||
|
dataSource,
|
||||||
|
readPretty: true,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
pageSize: 12,
|
||||||
|
},
|
||||||
|
runWhenParamsChanged: true,
|
||||||
|
...others,
|
||||||
|
},
|
||||||
|
'x-component': 'BlockItem',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useGridCardBlockItemProps }}',
|
||||||
|
},
|
||||||
|
'x-toolbar': 'BlockSchemaToolbar',
|
||||||
|
'x-settings': settings,
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': componentName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return schema;
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
DataBlockInitializer,
|
||||||
|
Icon,
|
||||||
|
useCollectionManager,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { NoticeBar } from 'antd-mobile';
|
||||||
|
import { createGridCardBlockSchema } from '../schema-create/createGridCardBlockSchma';
|
||||||
|
import { NoticeIcon } from '../../assets/svg';
|
||||||
|
|
||||||
|
export const NoticeBlock = () => {
|
||||||
|
return <NoticeBar content="月底冲量佣金上调" color="alert" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoticeBlockInitializer = () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const onCreateBlockSchema = async ({ item }) => {
|
||||||
|
const collection = cm.getCollection(item.name);
|
||||||
|
const schema = createGridCardBlockSchema(
|
||||||
|
{
|
||||||
|
collection: item.name,
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
rowKey: collection.filterTargetKey || 'id',
|
||||||
|
settings: 'blockSettings:gridCard',
|
||||||
|
},
|
||||||
|
'NoticeBlock',
|
||||||
|
);
|
||||||
|
insert(schema);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DataBlockInitializer
|
||||||
|
{...itemConfig}
|
||||||
|
icon={<Icon type="notice-block" />}
|
||||||
|
componentType={'Notice'}
|
||||||
|
onCreateBlockSchema={onCreateBlockSchema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
DataBlockInitializer,
|
||||||
|
Icon,
|
||||||
|
css,
|
||||||
|
useCollectionManager,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { Swiper, Toast } from 'antd-mobile';
|
||||||
|
import { createGridCardBlockSchema } from '../schema-create/createGridCardBlockSchma';
|
||||||
|
import { SwiperIcon } from '../../assets/svg';
|
||||||
|
|
||||||
|
export const SwiperBlock = () => {
|
||||||
|
const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac'];
|
||||||
|
const items = colors.map((color, index) => (
|
||||||
|
<Swiper.Item key={index}>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
height: 120px;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 48px;
|
||||||
|
user-select: none;
|
||||||
|
`}
|
||||||
|
style={{ background: color }}
|
||||||
|
onClick={() => {
|
||||||
|
Toast.show(`你点击了卡片 ${index + 1}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
</Swiper.Item>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<Swiper loop autoplay>
|
||||||
|
{items}
|
||||||
|
</Swiper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SwiperBlockInitializer = () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
|
||||||
|
const onCreateBlockSchema = async ({ item }) => {
|
||||||
|
const collection = cm.getCollection(item.name);
|
||||||
|
const schema = createGridCardBlockSchema(
|
||||||
|
{
|
||||||
|
collection: item.name,
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
rowKey: collection.filterTargetKey || 'id',
|
||||||
|
settings: 'blockSettings:gridCard',
|
||||||
|
},
|
||||||
|
'SwiperBlock',
|
||||||
|
);
|
||||||
|
insert(schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataBlockInitializer
|
||||||
|
{...itemConfig}
|
||||||
|
icon={<Icon type="swiper-block" />}
|
||||||
|
componentType={'Swiper'}
|
||||||
|
onCreateBlockSchema={onCreateBlockSchema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { RecursionField } from '@nocobase/schema';
|
||||||
|
import { useTabSearchAction } from './TabSearchAction';
|
||||||
|
|
||||||
|
export const TabSearch = () => {
|
||||||
|
const { fieldSchema, Designer, render, filterProperties } = useTabSearchAction();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Designer />
|
||||||
|
<RecursionField schema={fieldSchema} onlyRenderProperties filterProperties={filterProperties} />
|
||||||
|
<React.Fragment>{render()}</React.Fragment>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
import { useDesigner, useSchemaInitializerRender } from '@nocobase/client';
|
||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { isTabSearchCollapsibleInputItem } from '../utils';
|
||||||
|
|
||||||
|
export const useTabSearchAction = () => {
|
||||||
|
const Designer = useDesigner();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
|
||||||
|
|
||||||
|
const filterProperties = (s) => {
|
||||||
|
if (!isTabSearchCollapsibleInputItem(s['x-component'])) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 保留第一个,如果进入这个判断一定有值,所以取0不会出错
|
||||||
|
return (
|
||||||
|
s === Object.values(s.parent.properties).filter((s) => isTabSearchCollapsibleInputItem(s['x-component']))[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
fieldSchema,
|
||||||
|
Designer,
|
||||||
|
render,
|
||||||
|
filterProperties,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
|
||||||
|
import { ConfigProvider, Row } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { IButton, IInput, ISelect } from './TabSearchCollapsibleInputItemChild';
|
||||||
|
import { useTabSearchCollapsibleInputItemAction } from './TabSearchCollapsibleInputItemAction';
|
||||||
|
|
||||||
|
export const TabSearchCollapsibleInputItem = withDynamicSchemaProps(
|
||||||
|
(props) => {
|
||||||
|
const { collectionField, Designer, options, value, onSelectChange, onInputChange, onButtonClick, customLabelKey } =
|
||||||
|
useTabSearchCollapsibleInputItemAction(props);
|
||||||
|
|
||||||
|
if (!collectionField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItem>
|
||||||
|
<Designer />
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: { colorBgContainer: '#f9f9f9', colorBorder: 'none' },
|
||||||
|
components: {
|
||||||
|
Input: {
|
||||||
|
colorBorder: '#f9f9f9',
|
||||||
|
activeShadow: 'none',
|
||||||
|
hoverBorderColor: 'none',
|
||||||
|
activeBorderColor: 'none',
|
||||||
|
},
|
||||||
|
Divider: { colorSplit: '#c1c2c5' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row style={{ marginTop: '2vh', backgroundColor: '#f9f9f9' }}>
|
||||||
|
<ISelect options={options} onChange={onSelectChange} customLabelKey={customLabelKey} />
|
||||||
|
<IInput options={options} value={value} onChange={onInputChange} />
|
||||||
|
<IButton onClick={onButtonClick} />
|
||||||
|
</Row>
|
||||||
|
</ConfigProvider>
|
||||||
|
</SortableItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'TabSearchCollapsibleInputItem' },
|
||||||
|
);
|
@ -0,0 +1,46 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { useTranslation } from '../../../../locale';
|
||||||
|
import { useCollection, useDesigner } from '@nocobase/client';
|
||||||
|
import { isTabSearchCollapsibleInputItem } from '../../utils';
|
||||||
|
import { useTabSearchCollapsibleInputItem } from './hooks';
|
||||||
|
|
||||||
|
export const useTabSearchCollapsibleInputItemAction = (props) => {
|
||||||
|
const { onSelected } = useTabSearchCollapsibleInputItem();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const collection = useCollection();
|
||||||
|
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
|
||||||
|
const properties = fieldSchema.parent.properties;
|
||||||
|
const Designer = useDesigner();
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const [customLabelKey, setCustomLabelKey] = useState(fieldSchema['name'] as string);
|
||||||
|
|
||||||
|
const options = Object.values(properties)
|
||||||
|
.map((option) => {
|
||||||
|
if (isTabSearchCollapsibleInputItem(option['x-component'])) {
|
||||||
|
return {
|
||||||
|
label: option['title'],
|
||||||
|
value: option['name'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const onSelect = (value) => {
|
||||||
|
onSelected([value], customLabelKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectChange = (label) => setCustomLabelKey(label as string);
|
||||||
|
const onInputChange = (v) => {
|
||||||
|
const inputValue = v.target?.value || v;
|
||||||
|
setValue(inputValue);
|
||||||
|
if (inputValue === '') {
|
||||||
|
onSelect(inputValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onButtonClick = () => {
|
||||||
|
onSelect(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { collectionField, Designer, options, value, onSelectChange, onInputChange, onButtonClick, customLabelKey };
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Col, Divider, Input, Select } from 'antd';
|
||||||
|
import { useTranslation } from '../../../../locale';
|
||||||
|
|
||||||
|
export const ISelect = (props) => {
|
||||||
|
const { options, onChange } = props;
|
||||||
|
return options.length > 1 ? (
|
||||||
|
<Col flex={1} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
|
<Select style={{ width: '100%' }} options={options} defaultValue={options[0].value} onChange={onChange} />
|
||||||
|
<Divider type="vertical" style={{ height: '70%' }} />
|
||||||
|
</Col>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IInput = (props) => {
|
||||||
|
const { options, value, onChange } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Col flex={options.length > 1 ? 3 : 4}>
|
||||||
|
<Input placeholder={t('Please enter search content')} value={value} onChange={onChange} />
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IButton = (props) => {
|
||||||
|
const { onClick } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col flex={1}>
|
||||||
|
<Button style={{ width: '100%', color: '#2c6eff' }} type="link" onClick={onClick}>
|
||||||
|
{t('Search')}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
|
||||||
|
import { Grid } from 'antd-mobile';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTabSearchCollapsibleInputItemAction } from './TabSearchCollapsibleInputItemAction';
|
||||||
|
import { IButton, IInput, ISelect } from './TabSearchCollapsibleInputMItemChild';
|
||||||
|
|
||||||
|
export const TabSearchCollapsibleInputMItem = withDynamicSchemaProps(
|
||||||
|
(props) => {
|
||||||
|
const { collectionField, Designer, options, value, onSelectChange, onInputChange, onButtonClick, customLabelKey } =
|
||||||
|
useTabSearchCollapsibleInputItemAction(props);
|
||||||
|
|
||||||
|
if (!collectionField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItem>
|
||||||
|
<Designer />
|
||||||
|
<Grid columns={4} style={{ backgroundColor: '#f9f9f9', borderRadius: '5px', margin: '5px 0 0 0' }}>
|
||||||
|
<ISelect options={options} onChange={onSelectChange} customLabelKey={customLabelKey} />
|
||||||
|
<IInput options={options} value={value} onChange={onInputChange} />
|
||||||
|
<IButton onClick={onButtonClick} />
|
||||||
|
</Grid>
|
||||||
|
</SortableItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'TabSearchCollapsibleInputItem' },
|
||||||
|
);
|
@ -0,0 +1,60 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from '../../../../locale';
|
||||||
|
import { Grid, Divider, Picker, Input, Space } from 'antd-mobile';
|
||||||
|
import { DownOutline } from 'antd-mobile-icons';
|
||||||
|
|
||||||
|
export const ISelect = (props) => {
|
||||||
|
const { options, onChange, customLabelKey } = props;
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
return options.length > 1 ? (
|
||||||
|
<Grid.Item
|
||||||
|
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ textAlign: 'center', width: '100%' }}>
|
||||||
|
{options.find((option) => option.value === customLabelKey).label}
|
||||||
|
</span>
|
||||||
|
<DownOutline />
|
||||||
|
<Picker
|
||||||
|
columns={[options]}
|
||||||
|
visible={visible}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
defaultValue={customLabelKey}
|
||||||
|
onConfirm={(v) => {
|
||||||
|
onChange(v[0]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Divider direction="vertical" style={{ height: '70%', margin: '0 0 0 5px' }} />
|
||||||
|
</Grid.Item>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IInput = (props) => {
|
||||||
|
const { options, value, onChange } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Grid.Item span={options.length > 1 ? 2 : 3}>
|
||||||
|
<Input
|
||||||
|
placeholder={t('Please enter search content')}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
style={{ '--font-size': '12px', marginLeft: '5px' }}
|
||||||
|
/>
|
||||||
|
</Grid.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IButton = (props) => {
|
||||||
|
const { onClick } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid.Item onClick={onClick} style={{ color: '#2c6eff', textAlign: 'center', cursor: 'pointer' }}>
|
||||||
|
{t('Search')}
|
||||||
|
</Grid.Item>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
|
||||||
|
import { ConfigProvider, Menu } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction';
|
||||||
|
|
||||||
|
export const TabSearchFieldItem = withDynamicSchemaProps(
|
||||||
|
(props) => {
|
||||||
|
const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props);
|
||||||
|
|
||||||
|
if (!collectionField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItem>
|
||||||
|
<Designer />
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Menu: { itemColor: '#a5a5a5', itemHoverColor: '#1d2ffff18' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu items={items} mode="horizontal" onSelect={onSelect} defaultSelectedKeys={['all']} />
|
||||||
|
</ConfigProvider>
|
||||||
|
</SortableItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'TabSearchFieldItem' },
|
||||||
|
);
|
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MenuProps } from 'antd';
|
||||||
|
import { useCollection, useDesigner } from '@nocobase/client';
|
||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { useTranslation } from '../../../../locale';
|
||||||
|
|
||||||
|
export const useTabSearchFieldItemAction = (props) => {
|
||||||
|
const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const c = useCollection();
|
||||||
|
const collectionField = React.useMemo(() => c?.getField(fieldSchema.name as any), [c, fieldSchema.name]);
|
||||||
|
const Designer = useDesigner();
|
||||||
|
const valueKey = _valueKey || collectionField?.targetKey || 'id';
|
||||||
|
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
|
||||||
|
const onSelect = (itemKey) => {
|
||||||
|
const key = itemKey.keyPath?.[0] || itemKey;
|
||||||
|
onSelected([key], null, filterKey);
|
||||||
|
};
|
||||||
|
const fieldNames = {
|
||||||
|
title: labelKey || valueKey,
|
||||||
|
key: valueKey,
|
||||||
|
};
|
||||||
|
const items: MenuProps['items'] = [];
|
||||||
|
|
||||||
|
if (list?.length) {
|
||||||
|
list.forEach((value) => {
|
||||||
|
items.push({
|
||||||
|
label: value[fieldNames.title],
|
||||||
|
key: value[fieldNames.key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items?.unshift({
|
||||||
|
label: t('all'),
|
||||||
|
key: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectionField,
|
||||||
|
Designer,
|
||||||
|
items,
|
||||||
|
onSelect,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useCollection, useCollectionManager } from '@nocobase/client';
|
||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useTabSearchCollapsibleInputItem } from './hooks';
|
||||||
|
import { canBeOptionalField } from '../../utils';
|
||||||
|
|
||||||
|
export const useTabSearchFieldItemProps = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const collection = useCollection();
|
||||||
|
const optionalFieldList = useOptionalFieldList();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
|
||||||
|
const { onSelected } = useTabSearchCollapsibleInputItem();
|
||||||
|
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
|
||||||
|
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
|
||||||
|
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
|
||||||
|
const fieldInterface = fieldSchema['x-component-props'].interface;
|
||||||
|
if (canBeOptionalField(fieldInterface)) {
|
||||||
|
const field = optionalFieldList.find((field) => field.name === fieldSchema.name);
|
||||||
|
const operatorMap = {
|
||||||
|
select: '$in',
|
||||||
|
multipleSelect: '$anyOf',
|
||||||
|
checkbox: '$in',
|
||||||
|
checkboxGroup: '$anyOf',
|
||||||
|
};
|
||||||
|
const _list = field?.uiSchema?.enum || [];
|
||||||
|
result.valueKey = 'value';
|
||||||
|
result.labelKey = 'label';
|
||||||
|
result.list = _list;
|
||||||
|
result.filterKey = `${field.name}.${operatorMap[field.interface]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: result.list,
|
||||||
|
valueKey: result.valueKey,
|
||||||
|
labelKey: result.labelKey,
|
||||||
|
onSelected,
|
||||||
|
filterKey: result.filterKey,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useOptionalFieldList = () => {
|
||||||
|
const collection = useCollection();
|
||||||
|
const currentFields = collection?.fields ?? [];
|
||||||
|
return currentFields.filter((field) => canBeOptionalField(field.interface) && field.uiSchema.enum);
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction';
|
||||||
|
import { Tabs } from 'antd-mobile';
|
||||||
|
|
||||||
|
export const TabSearchFieldMItem = withDynamicSchemaProps(
|
||||||
|
(props) => {
|
||||||
|
const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props);
|
||||||
|
|
||||||
|
if (!collectionField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableItem>
|
||||||
|
<Designer />
|
||||||
|
<Tabs style={{ '--title-font-size': '12px', color: '#a5a5a5' }} onChange={onSelect}>
|
||||||
|
{items.map((value) => (
|
||||||
|
<Tabs.Tab title={value.label} key={value.key} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</SortableItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'TabSearchFieldItem' },
|
||||||
|
);
|
@ -0,0 +1,75 @@
|
|||||||
|
// 关系字段类型
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
useCollection,
|
||||||
|
useCollectionManager,
|
||||||
|
useDataSourceHeaders,
|
||||||
|
useFilterBlock,
|
||||||
|
useRequest,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useField, useFieldSchema } from '@nocobase/schema';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useTabSearchCollapsibleInputItem } from './hooks';
|
||||||
|
import { canBeRelatedField } from '../../utils';
|
||||||
|
|
||||||
|
export const useTabSearchFieldItemRelatedProps = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const collection = useCollection();
|
||||||
|
const field = useField();
|
||||||
|
// TODO 这里需要替换成获取 source id 的方式,不过我们暂时没有用多数据源,就不着急
|
||||||
|
const blockProps = { dataSource: 'main' };
|
||||||
|
const headers = useDataSourceHeaders(blockProps?.dataSource);
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
|
||||||
|
const collectionFieldName = collectionField?.name;
|
||||||
|
const fieldInterface = fieldSchema['x-component-props'].interface;
|
||||||
|
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
|
||||||
|
const { onSelected } = useTabSearchCollapsibleInputItem();
|
||||||
|
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
|
||||||
|
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
const { data, run } = useRequest<{
|
||||||
|
data: { [key: string]: any }[];
|
||||||
|
}>(
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
resource: collectionField?.target,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
fields: [result.labelKey, result.valueKey],
|
||||||
|
pageSize: 200,
|
||||||
|
page: 1,
|
||||||
|
...field.componentProps?.params,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: true,
|
||||||
|
debounceWait: 300,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (canBeRelatedField(fieldInterface) && collectionField?.target) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
result.labelKey,
|
||||||
|
result.valueKey,
|
||||||
|
JSON.stringify(field.componentProps?.params || {}),
|
||||||
|
canBeRelatedField(fieldInterface),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!collectionField) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
result.filterKey = `${collectionFieldName}.${result.valueKey}.$in`;
|
||||||
|
result.list = data?.data || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: result.list,
|
||||||
|
valueKey: result.valueKey,
|
||||||
|
labelKey: result.labelKey,
|
||||||
|
onSelected,
|
||||||
|
filterKey: result.filterKey,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,76 @@
|
|||||||
|
import { findFilterTargets, mergeFilter, useApp, useCollection, useFilterBlock } from '@nocobase/client';
|
||||||
|
import { useFieldSchema } from '@nocobase/schema';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const useTabSearchCollapsibleInputItem = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const collection = useCollection();
|
||||||
|
const { getDataBlocks } = useFilterBlock();
|
||||||
|
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
|
||||||
|
|
||||||
|
const onSelected = (value, customLabelKey?, filterKey?) => {
|
||||||
|
const { targets, uid } = findFilterTargets(fieldSchema);
|
||||||
|
getDataBlocks().forEach((block) => {
|
||||||
|
const target = targets.find((target) => target.uid === block.uid);
|
||||||
|
if (!target) return;
|
||||||
|
const key = `${uid}${fieldSchema.name}`;
|
||||||
|
const param = block.service.params?.[0] || {};
|
||||||
|
if (!collectionField?.target && customLabelKey) {
|
||||||
|
filterKey = `${customLabelKey}.$includes`;
|
||||||
|
}
|
||||||
|
// 保留原有的 filter
|
||||||
|
let storedFilter = block.service.params?.[1]?.filters || {};
|
||||||
|
if (value.length) {
|
||||||
|
storedFilter[key] = {
|
||||||
|
[filterKey]: value,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (block.dataLoadingMode === 'manual') {
|
||||||
|
return block.clearData();
|
||||||
|
}
|
||||||
|
delete storedFilter[key];
|
||||||
|
}
|
||||||
|
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
|
||||||
|
if (value.length === 1 && value[0] === 'all') {
|
||||||
|
const currentKey = Object.keys(storedFilter[key]);
|
||||||
|
if (mergedFilter['$and']) {
|
||||||
|
currentKey.forEach((currentValue) => {
|
||||||
|
mergedFilter['$and'].forEach((value, index) => {
|
||||||
|
if (value[currentValue]) {
|
||||||
|
mergedFilter['$and'].splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
mergedFilter['$and'] = mergedFilter['$and'].filter(Boolean);
|
||||||
|
} else {
|
||||||
|
delete mergedFilter[currentKey[0]];
|
||||||
|
}
|
||||||
|
storedFilter[key] = {};
|
||||||
|
}
|
||||||
|
storedFilter = _.omitBy(
|
||||||
|
storedFilter,
|
||||||
|
(value) => value === null || value === undefined || (_.isObject(value) && _.isEmpty(value)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return block.doFilter(
|
||||||
|
{
|
||||||
|
...param,
|
||||||
|
page: 1,
|
||||||
|
filter: mergedFilter,
|
||||||
|
},
|
||||||
|
{ filters: storedFilter },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onSelected,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsMobile = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const isMobile = Object.values(fieldSchema.root.properties).find((value) => value['x-component'] === 'MContainer');
|
||||||
|
return isMobile;
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
import { ISchema } from '@nocobase/schema';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
export const createTabSearchBlockSchema = (options: {
|
||||||
|
collectionName: string;
|
||||||
|
dataSource: string;
|
||||||
|
blockType: string;
|
||||||
|
}): ISchema => {
|
||||||
|
const { collectionName, dataSource, blockType } = options;
|
||||||
|
return {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TabSearchProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: collectionName,
|
||||||
|
dataSource,
|
||||||
|
blockType,
|
||||||
|
},
|
||||||
|
// 这个工具栏本来就没什么特别的作用,就是用于显示名称、模板名称,用这个没问题
|
||||||
|
'x-toolbar': 'BlockSchemaToolbar',
|
||||||
|
// FIXME 模板在当前这个 provider 下是不支持的,要么添加相关的代码,要么这个 settings 用自己的
|
||||||
|
'x-settings': 'blockSettings:filterCollapse',
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-filter-targets': [],
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-action': 'tabSearch',
|
||||||
|
'x-initializer': 'tabSearch:configureFields',
|
||||||
|
'x-component': 'TabSearch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
export const createTabSearchItemSchema = (options) => {
|
||||||
|
const { field, itemComponent, label, itemUseProps } = options;
|
||||||
|
return {
|
||||||
|
name: field.key,
|
||||||
|
title: field.uiSchema?.title,
|
||||||
|
Component: 'TabSearchFieldSchemaInitializerGadget',
|
||||||
|
schema: {
|
||||||
|
name: field.name,
|
||||||
|
title: field.uiSchema?.title,
|
||||||
|
type: 'void',
|
||||||
|
'x-toolbar': 'CollapseItemSchemaToolbar',
|
||||||
|
'x-settings': 'fieldSettings:TabSearchItem',
|
||||||
|
'x-component': itemComponent,
|
||||||
|
'x-use-component-props': itemUseProps,
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
interface: field.interface,
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,54 @@
|
|||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { TabSearch } from './components/TabSearch';
|
||||||
|
import { TabSearchProvider } from './provider/TabSearchProvider';
|
||||||
|
import { TabSearchBlockInitializer } from './initializer/TabSearchBlockInitializer';
|
||||||
|
import { TabSearchFieldItem } from './components/field-item/TabSearchFieldItem';
|
||||||
|
import { TabSearchCollapsibleInputItem } from './components/field-item/TabSearchCollapsibleInputItem';
|
||||||
|
import { TabSearchFieldSchemaInitializerGadget } from './initializer/TabSearchFieldSchemaInitializerGadget';
|
||||||
|
import { tval } from '../../locale';
|
||||||
|
import { useTabSearchFieldItemRelatedProps } from './components/field-item/TabSerachFieldItemRelatedProps';
|
||||||
|
import { useTabSearchFieldItemProps } from './components/field-item/TabSearchFieldItemProps';
|
||||||
|
import { TabSearchFieldSchemaInitializer } from './initializer/TabSearchFieldSchemaInitializer';
|
||||||
|
import { TabSearchItemFieldSettings } from './settings/TabSearchItemFieldSettings';
|
||||||
|
import { TabSearchFieldMItem } from './components/field-item/TabSearchFieldMItem';
|
||||||
|
import { TabSearchCollapsibleInputMItem } from './components/field-item/TabSearchCollapsibleInputMItem';
|
||||||
|
|
||||||
|
class PluginTabSearch extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.addScopes({
|
||||||
|
useTabSearchFieldItemProps,
|
||||||
|
useTabSearchFieldItemRelatedProps,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.addComponents({
|
||||||
|
TabSearch,
|
||||||
|
TabSearchProvider,
|
||||||
|
TabSearchBlockInitializer,
|
||||||
|
TabSearchFieldItem,
|
||||||
|
TabSearchFieldMItem,
|
||||||
|
TabSearchCollapsibleInputItem,
|
||||||
|
TabSearchCollapsibleInputMItem,
|
||||||
|
TabSearchFieldSchemaInitializerGadget,
|
||||||
|
});
|
||||||
|
this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
|
||||||
|
this.schemaSettingsManager.add(TabSearchItemFieldSettings);
|
||||||
|
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'filterBlocks', {
|
||||||
|
title: tval('Filter blocks'),
|
||||||
|
type: 'itemGroup',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'tabSearch',
|
||||||
|
title: 'tabSearch',
|
||||||
|
Component: 'TabSearchBlockInitializer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
this.app.schemaInitializerManager.addItem('page:addBlock', 'filterBlocks.tabSearch', {
|
||||||
|
name: 'tabSearch',
|
||||||
|
title: 'tabSearch',
|
||||||
|
Component: 'TabSearchBlockInitializer',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginTabSearch;
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { DataBlockInitializer, Icon, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client';
|
||||||
|
import { createTabSearchBlockSchema } from '../create/createTabSearchBlockSchema';
|
||||||
|
|
||||||
|
export const TabSearchBlockInitializer = (props) => {
|
||||||
|
const { filterCollections, onlyCurrentDataSource, hideChildrenIfSingleCollection } = props;
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
|
||||||
|
const onCreateBlockSchema = async ({ item }) => {
|
||||||
|
const schema = createTabSearchBlockSchema({
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
collectionName: item.collectionName || item.name,
|
||||||
|
blockType: 'filter',
|
||||||
|
});
|
||||||
|
insert(schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataBlockInitializer
|
||||||
|
{...itemConfig}
|
||||||
|
onlyCurrentDataSource={onlyCurrentDataSource}
|
||||||
|
icon="tab-search" // FIXME 这里有 bug,直接写字符串会出错
|
||||||
|
componentType={'TabSearch'}
|
||||||
|
filter={filterCollections}
|
||||||
|
hideChildrenIfSingleCollection={hideChildrenIfSingleCollection}
|
||||||
|
onCreateBlockSchema={onCreateBlockSchema}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,70 @@
|
|||||||
|
import { SchemaInitializer, SchemaInitializerItemType, useCollection, useCollectionManager } from '@nocobase/client';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { tval } from '../../../locale';
|
||||||
|
import { canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
|
||||||
|
import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema';
|
||||||
|
import { useIsMobile } from '../components/field-item/hooks';
|
||||||
|
|
||||||
|
const textFieldsItem: SchemaInitializerItemType<any> = {
|
||||||
|
name: 'textFields',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: tval('Text fields'),
|
||||||
|
useChildren() {
|
||||||
|
const collection = useCollection();
|
||||||
|
const associatedFields = collection.fields;
|
||||||
|
const isMobield = useIsMobile();
|
||||||
|
const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem';
|
||||||
|
const children = associatedFields
|
||||||
|
.map((field) => {
|
||||||
|
if (canBeSearchField(field.interface) && !field['isForeignKey']) {
|
||||||
|
const label = field.name;
|
||||||
|
return createTabSearchItemSchema({ field, itemComponent, label });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return children;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const choicesFieldsItem: SchemaInitializerItemType<any> = {
|
||||||
|
name: 'choicesFields',
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: tval('Choices fields'),
|
||||||
|
useChildren() {
|
||||||
|
const collection = useCollection();
|
||||||
|
const optionalList = collection.fields;
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const isMobield = useIsMobile();
|
||||||
|
const itemComponent = isMobield ? 'TabSearchFieldMItem' : 'TabSearchFieldItem';
|
||||||
|
const optionalChildren = optionalList
|
||||||
|
.map((field) => {
|
||||||
|
const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id';
|
||||||
|
if (canBeOptionalField(field.interface)) {
|
||||||
|
return createTabSearchItemSchema({
|
||||||
|
field,
|
||||||
|
itemComponent,
|
||||||
|
label,
|
||||||
|
itemUseProps: 'useTabSearchFieldItemProps',
|
||||||
|
});
|
||||||
|
} else if (canBeRelatedField(field.interface)) {
|
||||||
|
return createTabSearchItemSchema({
|
||||||
|
field,
|
||||||
|
itemComponent,
|
||||||
|
label,
|
||||||
|
itemUseProps: 'useTabSearchFieldItemRelatedProps',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return optionalChildren;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabSearchFieldSchemaInitializer = new SchemaInitializer({
|
||||||
|
name: 'tabSearch:configureFields',
|
||||||
|
style: { marginTop: 16 },
|
||||||
|
icon: 'SettingOutlined',
|
||||||
|
title: tval('Configure fields'),
|
||||||
|
items: [textFieldsItem, choicesFieldsItem],
|
||||||
|
});
|
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
SchemaInitializerSwitch,
|
||||||
|
useCurrentSchema,
|
||||||
|
useSchemaInitializer,
|
||||||
|
useSchemaInitializerItem,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { merge } from '@nocobase/schema';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const TabSearchFieldSchemaInitializerGadget = () => {
|
||||||
|
const itemConfig = useSchemaInitializerItem();
|
||||||
|
const { schema } = itemConfig;
|
||||||
|
const { exists, remove } = useCurrentSchema(schema.name, 'name', itemConfig.find, itemConfig.remove);
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (exists) {
|
||||||
|
return remove();
|
||||||
|
}
|
||||||
|
const s = merge(schema || {}, itemConfig.schema || {});
|
||||||
|
itemConfig?.schemaInitialize?.(s);
|
||||||
|
insert(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <SchemaInitializerSwitch checked={exists} title={itemConfig.title} onClick={onClick} />;
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { DataBlockProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const TabSearchProvider = (props) => {
|
||||||
|
return <DataBlockProvider collection={props.collection}>{props.children}</DataBlockProvider>;
|
||||||
|
};
|
@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
SchemaSettings,
|
||||||
|
SchemaSettingsDataScope,
|
||||||
|
useCollection,
|
||||||
|
useCollectionManager,
|
||||||
|
useCompile,
|
||||||
|
useDesignable,
|
||||||
|
useFormBlockContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useTranslation } from '../../../locale';
|
||||||
|
import { useField, useFieldSchema } from '@nocobase/schema';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { isTabSearchCollapsibleInputItem } from '../utils';
|
||||||
|
|
||||||
|
export const TabSearchItemFieldSettings = new SchemaSettings({
|
||||||
|
name: 'fieldSettings:TabSearchItem',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'decoratorOptions',
|
||||||
|
type: 'itemGroup',
|
||||||
|
hideIfNoChildren: true,
|
||||||
|
useComponentProps() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return {
|
||||||
|
title: t('Generic properties'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
useChildren() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'setTheDataScope',
|
||||||
|
Component: SchemaSettingsDataScope,
|
||||||
|
useComponentProps() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { form } = useFormBlockContext();
|
||||||
|
const field = useField();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const c = useCollection();
|
||||||
|
const collectionField =
|
||||||
|
c.getField(fieldSchema['name']) || cm.getCollectionField(fieldSchema['x-collection-field']);
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
|
||||||
|
return {
|
||||||
|
collectionName: collectionField?.target,
|
||||||
|
defaultFilter: fieldSchema?.['x-component-props']?.params?.filter || {},
|
||||||
|
form: form,
|
||||||
|
onSubmit: ({ filter }) => {
|
||||||
|
_.set(field.componentProps, 'params', {
|
||||||
|
...field.componentProps?.params,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
fieldSchema['x-component-props']['params'] = field.componentProps.params;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-component-props': fieldSchema['x-component-props'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
useVisible() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return !isTabSearchCollapsibleInputItem(fieldSchema['x-component']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'titleField',
|
||||||
|
type: 'select',
|
||||||
|
useComponentProps() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const cm = useCollectionManager();
|
||||||
|
const c = useCollection();
|
||||||
|
const collectionField =
|
||||||
|
c.getField(fieldSchema['name']) || cm.getCollectionField(fieldSchema['x-collection-field']);
|
||||||
|
const compile = useCompile();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const targetFields = collectionField?.target ? cm.getCollectionFields(collectionField?.target) : [];
|
||||||
|
const options = targetFields
|
||||||
|
.filter((field) => !field?.target && field.type !== 'boolean')
|
||||||
|
.map((field) => ({
|
||||||
|
value: field?.name,
|
||||||
|
label: compile(field?.uiSchema?.title) || field?.name,
|
||||||
|
}));
|
||||||
|
const onTitleFieldChange = (label) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
const fieldNames = {
|
||||||
|
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
|
||||||
|
...fieldSchema['x-component-props']?.['fieldNames'],
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: 'title-field',
|
||||||
|
title: t('Title field'),
|
||||||
|
options: options,
|
||||||
|
value: fieldSchema['x-component-props']?.fieldNames?.label,
|
||||||
|
onChange: onTitleFieldChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
useVisible() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return !isTabSearchCollapsibleInputItem(fieldSchema['x-component']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
type: 'remove',
|
||||||
|
sort: 100,
|
||||||
|
useComponentProps() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
return {
|
||||||
|
removeParentsIfNoChildren: true,
|
||||||
|
confirm: {
|
||||||
|
title: t('Delete field'),
|
||||||
|
},
|
||||||
|
breakRemoveOn: (s) => {
|
||||||
|
if (isTabSearchCollapsibleInputItem(fieldSchema['x-component'])) {
|
||||||
|
Object.values(s.properties).forEach((value) => {
|
||||||
|
if (isTabSearchCollapsibleInputItem(value['x-component']) && value.name !== fieldSchema.name) {
|
||||||
|
delete s.properties[value.name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dn.emit('patch', { schema: s });
|
||||||
|
}
|
||||||
|
return s['x-component'] === 'TabSearch';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,25 @@
|
|||||||
|
const canBeOptionalFields = ['select', 'multipleSelect', 'radioGroup', 'checkbox', 'checkboxGroup'];
|
||||||
|
const canBeRelatedFields = ['oho', 'obo', 'o2m', 'm2o', 'm2m'];
|
||||||
|
|
||||||
|
const canBeSearchFields = [
|
||||||
|
'input',
|
||||||
|
'textarea',
|
||||||
|
'phone',
|
||||||
|
'email',
|
||||||
|
'url',
|
||||||
|
'integer',
|
||||||
|
'number',
|
||||||
|
'percent',
|
||||||
|
'password',
|
||||||
|
'color',
|
||||||
|
'icon',
|
||||||
|
'formula',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const isTabSearchCollapsibleInputItem = (component: string) =>
|
||||||
|
component === 'TabSearchCollapsibleInputItem' || component === 'TabSearchCollapsibleInputMItem';
|
||||||
|
|
||||||
|
export const canBeOptionalField = (_interface: string) => canBeOptionalFields.includes(_interface);
|
||||||
|
export const canBeRelatedField = (_interface: string) => canBeRelatedFields.includes(_interface);
|
||||||
|
|
||||||
|
export const canBeSearchField = (_interface: string) => canBeSearchFields.includes(_interface);
|
@ -1,5 +1,13 @@
|
|||||||
import { RecursionField, useField, useFieldSchema } from '@nocobase/schema';
|
import { RecursionField, useField, useFieldSchema } from '@nocobase/schema';
|
||||||
import { ActionBarProvider, SortableItem, TabsContextProvider, css, cx, useDesigner } from '@nocobase/client';
|
import {
|
||||||
|
ActionBarProvider,
|
||||||
|
FilterBlockProvider,
|
||||||
|
SortableItem,
|
||||||
|
TabsContextProvider,
|
||||||
|
css,
|
||||||
|
cx,
|
||||||
|
useDesigner,
|
||||||
|
} from '@nocobase/client';
|
||||||
import { TabsProps } from 'antd';
|
import { TabsProps } from 'antd';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
@ -131,7 +139,13 @@ const InternalPage: React.FC = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MPage = InternalPage as unknown as typeof InternalPage & {
|
const InternalFilterablePage = (props) => (
|
||||||
|
<FilterBlockProvider>
|
||||||
|
<InternalPage {...props} />
|
||||||
|
</FilterBlockProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MPage = InternalFilterablePage as unknown as typeof InternalPage & {
|
||||||
Designer: typeof PageDesigner;
|
Designer: typeof PageDesigner;
|
||||||
};
|
};
|
||||||
MPage.Designer = PageDesigner;
|
MPage.Designer = PageDesigner;
|
||||||
|
639
pnpm-lock.yaml
639
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user