发布核心框架 (#6)

* api/ui 改名为 server/client

* 微调

* 继续完善 pages

* Fix env file and file mode. (#1)

* Fix: ignore .env file and environment variable names.

* Fix: correct file mode.

* fix: put environment variables together

* fix: separate data and ui resourcer

* feat: collection loader

* feat: redirectTo

* feat: fields & actions & views

* feat: fields & actions

* feat: app & pages & collections...

* feat: collections & pages & permissions...

* Doc: add readme (#2)

* Doc: add README.md.

* Util: add .editorconfig.

* Fix: use glob ignore option instead of additional checking. (#3)

* Fix: typo. (#4)

* feat: permissions

* feat: getCollection & getView actions

* refactor: code cleanup

Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
This commit is contained in:
chenos 2020-11-11 15:23:39 +08:00 committed by GitHub
parent e5d30b30ba
commit dcdb21d398
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
190 changed files with 5588 additions and 398 deletions

19
.editorconfig Normal file
View File

@ -0,0 +1,19 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*]
charset = utf-8
# Indentation override for all JS under lib directory
[*.{js,ts,json}]
indent_style = space
indent_size = 2

1
.env
View File

@ -1 +0,0 @@
DIALECT=postgres

16
.env.example Normal file
View File

@ -0,0 +1,16 @@
DB_DIALECT=postgres
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=nocobase
DB_USER=test
DB_PASSWORD=test
# DB_DIALECT=mysql
# DB_PORT=3306
DB_MYSQL_PORT=3306
DB_POSTGRES_PORT=5432
HTTP_PORT=23000
VERDACCIO_PORT=4873

0
.eslintrc Executable file → Normal file
View File

3
.fatherrc.ts Executable file → Normal file
View File

@ -7,11 +7,12 @@ const headPkgs = [
'database',
'resourcer',
'actions',
'client',
];
const tailPkgs = [];
const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
(pkg) => {
return pkg !== 'father-build' && pkg.charAt(0) !== '.' && !headPkgs.includes(pkg) && !tailPkgs.includes(pkg)
return !['father-build', 'app'].includes(pkg) && pkg.charAt(0) !== '.' && !headPkgs.includes(pkg) && !tailPkgs.includes(pkg)
},
);

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules/
lib/
.env
.DS_Store
package-lock.json
yarn.lock

29
README.md Normal file
View File

@ -0,0 +1,29 @@
Development
----------
### Install Dependencies
~~~shell
# Install dependencies for root project
npm i
# Install dependencies for sub packages via lerna
npm run bootstrap
~~~
### Set Environment Variables
~~~shell
cp .env.example .env
~~~
### Test
~~~
# For all packages
npm test
# For specific package
npm test packages/<name>
~~~

View File

@ -9,9 +9,10 @@ services:
networks:
- node-network
environment:
- VERDACCIO_PORT=4873
- VERDACCIO_PORT=${VERDACCIO_PORT}
restart: always
ports:
- "4873:4873"
- "${VERDACCIO_PORT}:4873"
# volumes:
# - "./verdaccio/storage:/verdaccio/storage"
# - "./verdaccio/config:/verdaccio/conf"
@ -19,20 +20,20 @@ services:
mysql:
image: mysql:5.7
environment:
MYSQL_DATABASE: "test"
MYSQL_USER: "test"
MYSQL_PASSWORD: "test"
MYSQL_ROOT_PASSWORD: "test"
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
restart: always
ports:
- "43306:3306"
- "${DB_MYSQL_PORT}:3306"
postgres:
image: postgres:10
restart: always
ports:
- "45432:5432"
- "${DB_POSTGRES_PORT}:5432"
command: postgres -c wal_level=logical
environment:
POSTGRES_USER: test
POSTGRES_DB: test
POSTGRES_PASSWORD: test
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_PASSWORD: ${DB_PASSWORD}

0
jest.config.js Executable file → Normal file
View File

View File

@ -10,7 +10,7 @@
]
},
"publish": {
"allowBranch": "master",
"allowBranch": ["master", "develop"],
"ignoreChanges": [
"*.md"
]

5
nodemon.json Normal file
View File

@ -0,0 +1,5 @@
{
"watch": ["packages", ".env"],
"ext": "ts",
"exec": "ts-node ./packages/server/example/index.ts"
}

View File

@ -2,6 +2,8 @@
"name": "root",
"private": true,
"scripts": {
"start": "cd packages/app && npm start",
"start-server": "yarn nodemon",
"bootstrap": "lerna bootstrap --no-ci",
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
"build-father-build": "cd packages/father-build && npm run build",
@ -14,11 +16,13 @@
"devDependencies": {
"@koa/router": "^9.3.1",
"@types/jest": "^26.0.4",
"@types/koa": "^2.11.6",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-mount": "^4.0.0",
"@types/koa__router": "^8.0.2",
"@types/lodash": "^4.14.158",
"@types/node": "^14.0.23",
"@types/react": "^16.9.53",
"@types/supertest": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
@ -29,6 +33,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"father-build": "^1.18.5",
"jest": "^26.1.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",

View File

@ -1,15 +0,0 @@
FROM node:stretch
WORKDIR /app
COPY . /app
EXPOSE 23000
# # Install app dependencies
# ENV NPM_CONFIG_LOGLEVEL warn
# RUN yarn install
# # Show current folder structure in logs
# RUN ls -al -R
# CMD [ "npm", "run", "serve" ]

View File

@ -1,41 +0,0 @@
version: "3"
networks:
backend:
driver: bridge
services:
app:
build:
context: ./
volumes:
- ./:/app
ports:
- "${HTTP_PORT}:23000"
command: [ "yarn", "start" ]
env_file:
- ./.env
networks:
- backend
mysql:
image: mysql:5.7
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
restart: always
ports:
- "23306:3306"
networks:
- backend
postgres:
image: postgres:10
restart: always
ports:
- "25432:5432"
networks:
- backend
command: postgres -c wal_level=logical
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}

View File

@ -1,42 +0,0 @@
import Api from '../src';
import dotenv from 'dotenv';
const sync = {
force: true,
alter: {
drop: true,
},
};
dotenv.config();
const api = Api.create({
database: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.platform === 'linux' ? process.env.DB_HOST : 'localhost',
port: process.platform === 'linux' ? parseInt(process.env.DB_PORT) : ( process.env.DB_DIALECT == 'postgres' ? 25432 : 23306 ),
dialect: process.env.DB_DIALECT as any,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
// logging: false,
define: {},
sync,
},
resourcer: {
prefix: '/api',
},
});
api
.plugins([
[require('../../plugin-collections/src/index').default, {}],
])
.then(() => {
api.listen(23001, () => {
console.log('http://localhost:23001/');
});
});

View File

@ -1,6 +0,0 @@
{
"watch": ["src", ".env", ".env.dev", "../plugin-collections/src"],
"ext": "ts",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node ./example/index.ts"
}

View File

@ -1,38 +0,0 @@
{
"name": "@nocobase/api",
"version": "0.3.0-alpha.0",
"main": "lib/index.js",
"license": "MIT",
"scripts": {
"start": "nodemon",
"sync": "ts-node ./src/sync.ts",
"serve": "pm2-runtime start pm2.json",
"build": "tsc --declaration",
"db:sync": "docker-compose run app bash -c 'yarn sync'",
"logs": "docker-compose logs app"
},
"dependencies": {
"@koa/cors": "^3.1.0",
"@koa/router": "^9.4.0",
"@nocobase/actions": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/resourcer": "^0.3.0-alpha.0",
"@types/koa": "^2.11.4",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa__router": "^8.0.2",
"bcrypt": "^5.0.0",
"crypto-random-string": "^3.3.0",
"dotenv": "^8.2.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"mockjs": "^1.1.0",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
"nodemon": "^2.0.4",
"pg": "^8.3.3",
"pg-hstore": "^2.3.3",
"pm2": "^4.4.1",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
}
}

View File

@ -1,14 +0,0 @@
{
"apps": [{
"name": "nocobase-api",
"script": "lib/index.js",
"instances": 0,
"exec_mode": "cluster",
"env": {
"NODE_ENV": "development"
},
"env_production" : {
"NODE_ENV": "production"
}
}]
}

View File

@ -1,53 +0,0 @@
import Koa from 'koa';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import actions from '@nocobase/actions';
export class Application extends Koa {
database: Database;
resourcer: Resourcer;
async plugins(plugins: any[]) {
await Promise.all(plugins.map(async (pluginOption) => {
let plugin: Function;
let options = {};
if (Array.isArray(pluginOption)) {
plugin = pluginOption.shift();
plugin = plugin.bind(this);
options = pluginOption.shift()||{};
} else if (typeof pluginOption === 'function') {
plugin = pluginOption.bind(this);
}
return await plugin(options);
}));
}
}
export default {
create(options: any): Application {
console.log(options);
const app = new Application();
const resourcer = new Resourcer();
const database = new Database(options.database);
app.database = database;
app.resourcer = resourcer;
resourcer.registerHandlers(actions.common);
app.use(async (ctx, next) => {
ctx.db = database;
ctx.database = database;
await next();
});
app.use(resourcer.middleware(options.resourcer || {
prefix: '/api',
}));
return app;
}
}

16
packages/app/.editorconfig Executable file
View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

16
packages/app/.env.example Normal file
View File

@ -0,0 +1,16 @@
DB_DIALECT=postgres
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=nocobase
DB_USER=test
DB_PASSWORD=test
# DB_DIALECT=mysql
# DB_PORT=3306
DB_MYSQL_PORT=3306
DB_POSTGRES_PORT=5432
HTTP_PORT=23000
VERDACCIO_PORT=4873

10
packages/app/.fatherrc.ts Executable file
View File

@ -0,0 +1,10 @@
export default {
entry: 'src/api',
target: 'node',
cjs: { type: 'babel', lazy: true },
include: 'api/*',
disableTypeCheck: true,
// pkgs: [
// 'api',
// ],
};

20
packages/app/.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
/dist
.env
# misc
.DS_Store
# umi
/src/.umi
/src/.umi-production
/src/.umi-test
/.env.local

8
packages/app/.prettierignore Executable file
View File

@ -0,0 +1,8 @@
**/*.md
**/*.svg
**/*.ejs
**/*.html
package.json
.umi
.umi-production
.umi-test

11
packages/app/.prettierrc Executable file
View File

@ -0,0 +1,11 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}

25
packages/app/.umirc.ts Normal file
View File

@ -0,0 +1,25 @@
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
define: {
'process.env.API': `/api`,
// 'process.env.API': `http://localhost:${process.env.HTTP_PORT}/api`,
},
proxy: {
'/api': {
'target': `http://localhost:${process.env.HTTP_PORT}/`,
'changeOrigin': true,
'pathRewrite': { '^/api' : '/api' },
},
},
routes: [
{
exact: false,
path: '/:path(.*)',
component: '@/pages/index',
},
],
});

15
packages/app/README.md Executable file
View File

@ -0,0 +1,15 @@
# NocoBase Application
## Getting Started
Install dependencies,
```bash
$ yarn install
```
Start the dev server,
```bash
$ yarn start
```

View File

View File

@ -0,0 +1,5 @@
{
"watch": ["src/api", ".env", "../"],
"ext": "ts",
"exec": "ts-node ./src/api/index.ts"
}

45
packages/app/package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "@nocobase/app",
"version": "0.3.0-alpha.0",
"private": true,
"scripts": {
"start": "concurrently \"nodemon\" \"umi dev\"",
"build": "father-build && umi build",
"postinstall": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
},
"dependencies": {
"@ant-design/pro-layout": "^5.0.12",
"@formily/antd-components": "^1.3.6",
"@nocobase/client": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/father-build": "^0.3.0-alpha.0",
"@nocobase/plugin-collections": "^0.3.0-alpha.0",
"@nocobase/plugin-pages": "^0.3.0-alpha.0",
"@nocobase/server": "^0.3.0-alpha.0",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.2.23",
"concurrently": "^5.3.0",
"lint-staged": "^10.0.7",
"nodemon": "^2.0.6",
"prettier": "^1.19.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"styled-components": "^5.2.1",
"umi": "^3.2.23",
"yorkie": "^2.0.0"
}
}

View File

@ -0,0 +1,197 @@
import Api from '../../../server/src';
import dotenv from 'dotenv';
import path from 'path';
import Database, { Model } from '@nocobase/database';
import { get } from 'lodash';
const sync = {
force: true,
alter: {
drop: true,
},
};
dotenv.config();
const api = Api.create({
database: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
logging: false,
define: {},
sync,
},
resourcer: {
prefix: '/api',
},
});
(async () => {
await api
.plugins([
[path.resolve(__dirname, '../../../plugin-collections'), {}],
[path.resolve(__dirname, '../../../plugin-pages'), {}],
[path.resolve(__dirname, '../../../plugin-permissions'), {}],
[path.resolve(__dirname, '../../../plugin-users'), {}],
[path.resolve(__dirname, '../../../plugin-file-manager'), {}],
// [require('../../plugin-collections/src/index').default, {}],
// [require('../../plugin-pages/src/index').default, {}],
]);
const database: Database = api.database;
const [Collection, View, Action, Tab] = database.getModels(['collections', 'views', 'actions', 'tabs']);
const tables = database.getTables([]);
for (let table of tables) {
const options = table.getOptions();
const collection = await Collection.create(options);
// console.log(options);
const associations: any = {};
if (options.fields) {
associations['fields'] = options.fields.map(item => ({
...item,
options: item,
}))
}
if (options.tabs) {
associations['tabs'] = options.tabs.map(item => ({
...item,
options: item,
}))
}
if (options.actions) {
associations['actions'] = options.actions.map(item => ({
...item,
options: item,
}))
}
if (options.views) {
associations['views'] = options.views.map(item => ({
...item,
options: item,
}))
}
await collection.updateAssociations(associations);
}
const actions = await Action.findAll();
for (const action of actions) {
const viewName = action.options.viewName;
console.log({viewName});
if (viewName) {
const view = await View.findOne({
where: {
name: viewName,
collection_name: action.collection_name
},
});
if (view) {
action.options.viewId = view.id;
console.log(action.options);
action.setDataValue('options', action.options);
action.changed('options', true);
await action.save();
}
}
}
const tabs = await Tab.findAll();
for (const tab of tabs) {
const viewName = tab.options.viewName;
if (!viewName) {
continue;
}
let view: any;
if (tab.type === 'association') {
view = await View.findOne({
where: {
name: viewName,
collection_name: tab.options.association,
},
});
} else {
view = await View.findOne({
where: {
name: viewName,
collection_name: tab.collection_name,
},
});
}
if (view) {
tab.options.viewId = view.id;
tab.setDataValue('options', tab.options);
tab.changed('options', true);
await tab.save();
}
}
const views = await View.findAll();
for (const view of views) {
const detailsViewName = view.options.detailsViewName;
if (detailsViewName) {
const v = await View.findOne({
where: {
name: detailsViewName,
collection_name: view.collection_name
},
});
if (v) {
view.options.detailsViewId = v.id;
view.setDataValue('options', view.options);
view.changed('options', true);
await view.save();
}
}
const updateViewName = view.options.updateViewName;
if (updateViewName) {
const v = await View.findOne({
where: {
name: updateViewName,
collection_name: view.collection_name
},
});
if (v) {
view.options.updateViewId = v.id;
view.setDataValue('options', view.options);
view.changed('options', true);
await view.save();
}
}
console.log({detailsViewName, updateViewName});
}
// for (let table of tables) {
// const options = table.getOptions();
// const collection = await Collection.findOne({
// where: {
// name: options.name,
// },
// });
// const tabs = await collection.getTabs() as Model[];
// const actions = await collection.getActions() as Model[];
// const views = await collection.getViews() as Model[];
// for (const tab of tabs) {
// tab.options.viewName;
// }
// }
// const collections = await Collection.findAll();
// await Promise.all(collections.map(async (collection) => {
// return await collection.modelInit();
// }));
api.listen(process.env.HTTP_PORT, () => {
console.log(`http://localhost:${process.env.HTTP_PORT}/`);
});
})();

24
packages/app/src/app.ts Normal file
View File

@ -0,0 +1,24 @@
import { RequestConfig, request as umiRequest, history } from 'umi';
export const request: RequestConfig = {
prefix: process.env.API,
errorConfig: {
adaptor: (resData) => {
return {
...resData,
success: true,
showType: 0,
};
},
},
middlewares: [
async (ctx, next) => {
const { headers } = ctx.req.options as any;
const token = localStorage.getItem('NOCOBASE_TOKEN');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
await next();
}
],
};

View File

@ -0,0 +1,39 @@
import React, { useRef } from 'react';
import { Button } from 'antd';
import ViewFactory from '@/components/views';
export function Create(props) {
console.log(props);
const { title, viewCollectionName, viewName } = props.schema;
const { activeTab = {}, item = {} } = props;
const { association, collection_name } = activeTab;
const params = {};
if (association) {
params['resourceName'] = association;
params['associatedName'] = collection_name;
params['associatedKey'] = item.itemId;
} else {
params['resourceName'] = collection_name;
params['resourceKey'] = item.itemId;
}
const drawerRef = useRef<any>();
return (
<>
<ViewFactory
{...props}
reference={drawerRef}
viewCollectionName={viewCollectionName}
viewName={viewName}
{...params}
/>
<Button type={'primary'} onClick={() => {
drawerRef.current.setVisible(true);
}}>{title}</Button>
</>
)
}
export default Create;

View File

@ -0,0 +1,18 @@
import React, { useRef } from 'react';
import { Button } from 'antd';
import ViewFactory from '@/components/views';
export function Destroy(props) {
console.log(props);
const { title, viewId } = props.schema;
const drawerRef = useRef<any>();
return (
<>
<Button type={'primary'} onClick={() => {
}}>{title}</Button>
</>
)
}
export default Destroy;

View File

@ -0,0 +1,39 @@
import React, { useRef } from 'react';
import { Button } from 'antd';
import ViewFactory from '@/components/views';
export function Update(props) {
console.log(props);
const { title, viewCollectionName, viewName } = props.schema;
const { activeTab = {}, item = {} } = props;
const { association, collection_name } = activeTab;
const params = {};
if (association) {
params['resourceName'] = association;
params['associatedName'] = collection_name;
params['associatedKey'] = item.itemId;
} else {
params['resourceName'] = collection_name;
params['resourceKey'] = item.itemId;
}
const drawerRef = useRef<any>();
return (
<>
<ViewFactory
{...props}
reference={drawerRef}
viewCollectionName={viewCollectionName}
viewName={viewName}
{...params}
/>
<Button type={'primary'} onClick={() => {
drawerRef.current.setVisible(true);
}}>{title}</Button>
</>
)
}
export default Update;

View File

@ -0,0 +1,39 @@
import React from 'react';
import Create from './Create';
import Update from './Update';
import Destroy from './Destroy';
import { Space } from 'antd';
const ACTIONS = new Map<string, any>();
export function registerAction(type: string, Action: any) {
ACTIONS.set(type, Action);
}
registerAction('update', Update);
registerAction('create', Create);
registerAction('destroy', Destroy);
export function getAction(type: string) {
return ACTIONS.get(type);
}
export function Action(props) {
const { schema = {} } = props;
// cnsole.log(schema);
const { type } = schema;
const Action = getAction(type);
return Action && <Action {...props}/>;
}
export function Actions(props) {
const { style, schema, actions = [] } = props;
return actions.length > 0 && (
<Space style={style}>
{actions.map(action => <Action {...props} view={schema} schema={action}/>)}
</Space>
);
}
export default Actions;

View File

@ -0,0 +1,197 @@
import React, { Fragment } from 'react'
import {
ISchemaFieldComponentProps,
SchemaField
} from '@formily/react-schema-renderer'
import { toArr, isFn, FormPath } from '@formily/shared'
import { ArrayList } from '@formily/react-shared-components'
import { CircleButton } from '../circle-button'
import { TextButton } from '../text-button'
import { Card } from 'antd'
import {
PlusOutlined,
DeleteOutlined,
DownOutlined,
UpOutlined
} from '@ant-design/icons'
import styled from 'styled-components'
const ArrayComponents = {
CircleButton,
TextButton,
AdditionIcon: () => <PlusOutlined />,
RemoveIcon: () => <DeleteOutlined />,
MoveDownIcon: () => <DownOutlined />,
MoveUpIcon: () => <UpOutlined />
}
export const ArrayCards: any = styled(
(props: ISchemaFieldComponentProps & { className: string }) => {
const { value, schema, className, editable, path, mutators } = props
const {
renderAddition,
renderRemove,
renderMoveDown,
renderMoveUp,
renderEmpty,
renderExtraOperations,
...componentProps
} = schema.getExtendsComponentProps() || {}
const schemaItems = Array.isArray(schema.items)
? schema.items[schema.items.length - 1]
: schema.items
const onAdd = () => {
if (schemaItems) {
mutators.push(schemaItems.getEmptyValue())
}
}
return (
<div className={className}>
<ArrayList
value={value}
minItems={schema.minItems}
maxItems={schema.maxItems}
editable={editable}
components={ArrayComponents}
renders={{
renderAddition,
renderRemove,
renderMoveDown,
renderMoveUp,
renderEmpty
}}
>
{toArr(value).map((item, index) => {
return (
<Card
{...componentProps}
size="small"
className={`card-list-item`}
key={index}
title={
<span>
{index + 1}<span>.</span> {componentProps.title || schema.title}
</span>
}
extra={
<Fragment>
<ArrayList.Remove
index={index}
onClick={() => mutators.remove(index)}
/>
<ArrayList.MoveDown
index={index}
onClick={() => mutators.moveDown(index)}
/>
<ArrayList.MoveUp
index={index}
onClick={() => mutators.moveUp(index)}
/>
{isFn(renderExtraOperations)
? renderExtraOperations(index)
: renderExtraOperations}
</Fragment>
}
>
{schemaItems && (
<SchemaField
path={FormPath.parse(path).concat(index)}
schema={schemaItems}
/>
)}
</Card>
)
})}
<ArrayList.Empty>
{({ children, allowAddition }) => {
return (
<Card
{...componentProps}
size="small"
className={`card-list-item card-list-empty ${allowAddition ? 'add-pointer' : ''}`}
onClick={allowAddition ? onAdd : undefined}
>
<div className="empty-wrapper">{children}</div>
</Card>
)
}}
</ArrayList.Empty>
<ArrayList.Addition>
{({ children, isEmpty }) => {
if (!isEmpty) {
return (
<div className="array-cards-addition" onClick={onAdd}>
{children}
</div>
)
}
}}
</ArrayList.Addition>
</ArrayList>
</div>
)
}
)<ISchemaFieldComponentProps>`
width: 100%;
.ant-card {
.ant-card {
box-shadow: none;
}
.ant-card-body {
padding: 20px 10px 0 10px;
}
.array-cards-addition {
box-shadow: none;
border: 1px solid #eee;
transition: all 0.35s ease-in-out;
&:hover {
border: 1px solid #ccc;
}
}
.empty-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 10px;
img {
height: 85px;
}
.ant-btn {
color: #888;
}
}
}
.card-list-empty.card-list-item.add-pointer {
cursor: pointer;
}
.array-cards-addition {
margin-top: 10px;
margin-bottom: 3px;
background: #fff;
display: flex;
cursor: pointer;
padding: 5px 0;
justify-content: center;
box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1);
}
.card-list-item {
margin-top: 10px;
border: 1px solid #eee;
}
.card-list-item:first-child {
margin-top: 0 !important;
}
.ant-card-extra {
display: flex;
button {
margin-right: 8px;
}
}
`
ArrayCards.isFieldComponent = true
export default ArrayCards

View File

@ -0,0 +1 @@
import 'antd/lib/card/style/index'

View File

@ -0,0 +1,234 @@
import React, { useContext } from 'react'
import {
ISchemaFieldComponentProps,
SchemaField,
Schema,
complieExpression,
FormExpressionScopeContext
} from '@formily/react-schema-renderer'
import { toArr, isFn, isArr, FormPath } from '@formily/shared'
import { ArrayList, DragListView } from '@formily/react-shared-components'
import { CircleButton } from '../circle-button'
import { TextButton } from '../text-button'
import { Table, Form } from 'antd'
import { FormItemShallowProvider } from '@formily/antd'
import {
PlusOutlined,
DeleteOutlined,
DownOutlined,
UpOutlined
} from '@ant-design/icons'
import styled from 'styled-components'
const ArrayComponents = {
CircleButton,
TextButton,
AdditionIcon: () => <PlusOutlined style={{ fontSize: 20 }} />,
RemoveIcon: () => <DeleteOutlined />,
MoveDownIcon: () => <DownOutlined />,
MoveUpIcon: () => <UpOutlined />
}
const DragHandler = styled.span`
width: 7px;
display: inline-block;
height: 14px;
border: 2px dotted #c5c5c5;
border-top: 0;
border-bottom: 0;
cursor: move;
margin-bottom: 24px;
`
export const ArrayTable: any = styled(
(props: ISchemaFieldComponentProps & { className: string }) => {
const expressionScope = useContext(FormExpressionScopeContext)
const { value, schema, className, editable, path, mutators } = props
const {
renderAddition,
renderRemove,
renderMoveDown,
renderMoveUp,
renderEmpty,
renderExtraOperations,
operationsWidth,
operations,
draggable,
...componentProps
} = schema.getExtendsComponentProps() || {}
const schemaItems = Array.isArray(schema.items)
? schema.items[schema.items.length - 1]
: schema.items
const onAdd = () => {
if (schemaItems) {
mutators.push(schemaItems.getEmptyValue())
}
}
const onMove = (dragIndex, dropIndex) => {
mutators.move(dragIndex, dropIndex)
}
const renderColumns = (items: Schema) => {
return items.mapProperties((props, key) => {
const itemProps = {
...props.getExtendsItemProps(),
...props.getExtendsProps()
}
return {
title: complieExpression(props.title, expressionScope),
...itemProps,
key,
dataIndex: key,
render: (value: any, record: any, index: number) => {
const newPath = FormPath.parse(path).concat(index, key)
return (
<FormItemShallowProvider
key={newPath.toString()}
label={undefined}
labelCol={undefined}
wrapperCol={undefined}
>
<SchemaField path={newPath} schema={props} />
</FormItemShallowProvider>
)
}
}
})
}
// 兼容异步items schema传入
let columns = []
if (schema.items) {
columns = isArr(schema.items)
? schema.items.reduce((buf, items) => {
return buf.concat(renderColumns(items))
}, [])
: renderColumns(schema.items)
}
if (editable && operations !== false) {
columns.push({
...operations,
key: 'operations',
dataIndex: 'operations',
width: operationsWidth || 200,
render: (value: any, record: any, index: number) => {
return (
<Form.Item>
<div className="array-item-operator">
<ArrayList.Remove
index={index}
onClick={() => mutators.remove(index)}
/>
<ArrayList.MoveDown
index={index}
onClick={() => mutators.moveDown(index)}
/>
<ArrayList.MoveUp
index={index}
onClick={() => mutators.moveUp(index)}
/>
{isFn(renderExtraOperations)
? renderExtraOperations(index)
: renderExtraOperations}
</div>
</Form.Item>
)
}
})
}
if (draggable) {
columns.unshift({
width: 20,
key: 'dragHandler',
render: () => {
return <DragHandler className="drag-handler" />
}
})
}
const renderTable = () => {
return (
<Table
{...componentProps}
rowKey={record => {
return toArr(value).indexOf(record)
}}
pagination={false}
columns={columns}
dataSource={toArr(value)}
></Table>
)
}
return (
<div className={className}>
<ArrayList
value={value}
minItems={schema.minItems}
maxItems={schema.maxItems}
editable={editable}
components={ArrayComponents}
renders={{
renderAddition,
renderRemove,
renderMoveDown,
renderMoveUp,
renderEmpty
}}
>
{draggable ? (
<DragListView
onDragEnd={onMove}
nodeSelector="tr.ant-table-row"
ignoreSelector="tr.ant-table-expanded-row"
>
{renderTable()}
</DragListView>
) : (
renderTable()
)}
<ArrayList.Addition>
{({ children }) => {
return (
children && (
<div className="array-table-addition" onClick={onAdd}>
{children}
</div>
)
)
}}
</ArrayList.Addition>
</ArrayList>
</div>
)
}
)`
width: 100%;
margin-bottom: 10px;
table {
margin-bottom: 0 !important;
}
.array-table-addition {
background: #fbfbfb;
cursor: pointer;
margin-top: 3px;
border-radius: 3px;
.next-btn-text {
color: #888;
}
.next-icon:before {
width: 16px !important;
font-size: 16px !important;
margin-right: 5px;
}
}
.ant-btn {
color: #888;
}
.array-item-operator {
display: flex;
button {
margin-right: 8px;
}
}
`
ArrayTable.isFieldComponent = true
export default ArrayTable

View File

@ -0,0 +1 @@
import 'antd/lib/table/style/index'

View File

@ -0,0 +1,21 @@
import {
connect
} from '@formily/react-schema-renderer'
import { Checkbox as AntdCheckbox } from 'antd'
import {
transformDataSourceKey,
mapStyledProps,
mapTextComponent
} from '../shared'
export const Checkbox = connect<'Group'>({
valueName: 'checked',
getProps: mapStyledProps
})(AntdCheckbox)
Checkbox.Group = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent
})(transformDataSourceKey(AntdCheckbox.Group, 'options'))
export default Checkbox

View File

@ -0,0 +1 @@
import 'antd/lib/checkbox/style/index'

View File

@ -0,0 +1,16 @@
import React from 'react'
import { Button } from 'antd'
import { ButtonProps } from 'antd/lib/button'
export const CircleButton: React.FC<ButtonProps> = props => {
const hasText = String(props.className || '').indexOf('has-text') > -1
return (
<Button
type={hasText ? 'link' : undefined}
shape={hasText ? undefined : 'circle'}
{...props}
/>
)
}
export default CircleButton

View File

@ -0,0 +1 @@
import 'antd/lib/button/style/index'

View File

@ -0,0 +1,105 @@
import React from 'react'
import { connect } from '@formily/react-schema-renderer'
import moment from 'moment'
import { DatePicker as AntdDatePicker } from 'antd'
import {
mapStyledProps,
mapTextComponent,
compose,
isStr,
isArr
} from '../shared'
class YearPicker extends React.Component {
public render() {
return <AntdDatePicker {...this.props} picker={'year'} />
}
}
const transformMoment = (value, format = 'YYYY-MM-DD HH:mm:ss') => {
if (value === '') return undefined
return value && value.format ? value.format(format) : value
}
const mapMomentValue = (props: any, fieldProps: any) => {
const { value, showTime = false } = props
if (!fieldProps.editable) return props
try {
if (isStr(value) && value) {
props.value = moment(
value,
showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'
)
} else if (isArr(value) && value.length) {
props.value = value.map(
item =>
(item &&
moment(item, showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')) ||
''
)
}
} catch (e) {
throw new Error(e)
}
return props
}
export const DatePicker = connect<
'RangePicker' | 'MonthPicker' | 'YearPicker' | 'WeekPicker'
>({
getValueFromEvent(_, value) {
const props = this.props || {}
return transformMoment(
value,
props.format || (props.showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
)
},
getProps: compose(mapStyledProps, mapMomentValue),
getComponent: mapTextComponent
})(AntdDatePicker)
DatePicker.RangePicker = connect({
getValueFromEvent(_, [startDate, endDate]) {
const props = this.props || {}
const format =
props.format || (props.showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
return [
transformMoment(startDate, format),
transformMoment(endDate, format)
]
},
getProps: compose(mapStyledProps, mapMomentValue),
getComponent: mapTextComponent
})(AntdDatePicker.RangePicker)
DatePicker.MonthPicker = connect({
getValueFromEvent(_, value) {
return transformMoment(value)
},
getProps: compose(mapStyledProps, mapMomentValue),
getComponent: mapTextComponent
})(AntdDatePicker.MonthPicker)
DatePicker.WeekPicker = connect({
getValueFromEvent(_, value) {
return transformMoment(value, 'gggg-wo')
},
getProps: compose(mapStyledProps, props => {
if (isStr(props.value) && props.value) {
const parsed = props.value.match(/\D*(\d+)\D*(\d+)\D*/) || ['', '', '']
props.value = moment(parsed[1], 'YYYY').add(parsed[2] - 1, 'weeks')
}
return props
}),
getComponent: mapTextComponent
})(AntdDatePicker.WeekPicker)
DatePicker.YearPicker = connect({
getValueFromEvent(_, value) {
return transformMoment(value, 'YYYY')
},
getProps: compose(mapStyledProps, mapMomentValue),
getComponent: mapTextComponent
})(YearPicker)
export default DatePicker

View File

@ -0,0 +1 @@
import 'antd/lib/date-picker/style/index'

View File

@ -0,0 +1,24 @@
import React from 'react'
import { createVirtualBox } from '@formily/react-schema-renderer'
import { Card } from 'antd'
import { CardProps } from 'antd/lib/card'
import styled from 'styled-components'
export const FormBlock = createVirtualBox<CardProps>(
'block',
styled(({ children, className, ...props }) => {
return (
<Card className={className} size="small" {...props}>
{children}
</Card>
)
})`
margin-bottom: 10px !important;
&.ant-card {
border: none;
box-shadow: none;
}
`
)
export default FormBlock

View File

@ -0,0 +1 @@
import 'antd/lib/card/style/index'

View File

@ -0,0 +1,20 @@
import React from 'react'
import { createVirtualBox } from '@formily/react-schema-renderer'
import { Card } from 'antd'
import { CardProps } from 'antd/lib/card'
import styled from 'styled-components'
export const FormCard = createVirtualBox<CardProps>(
'card',
styled(({ children, className, ...props }) => {
return (
<Card className={className} size="small" {...props}>
{children}
</Card>
)
})`
margin-bottom: 10px !important;
`
)
export default FormCard

View File

@ -0,0 +1 @@
import 'antd/lib/card/style/index'

View File

@ -0,0 +1,10 @@
import React from 'react'
import { createVirtualBox } from '@formily/react-schema-renderer'
import { Col } from 'antd'
import { ColProps } from 'antd/lib/grid'
export const FormGridCol = createVirtualBox<ColProps>('grid-col', props => {
return <Col {...props}>{props.children}</Col>
})
export default FormGridCol

View File

@ -0,0 +1 @@
import 'antd/lib/col/style/index'

View File

@ -0,0 +1,25 @@
import React from 'react'
import { AntdSchemaFieldAdaptor, pickFormItemProps } from '@formily/antd'
import { createVirtualBox } from '@formily/react-schema-renderer'
import { Row } from 'antd'
import { RowProps } from 'antd/lib/grid'
import { FormItemProps as ItemProps } from 'antd/lib/form'
import { IItemProps } from '../types'
export const FormGridRow = createVirtualBox<RowProps & ItemProps & IItemProps>(
'grid-row',
props => {
const { title, label } = props
const grids = <Row {...props}>{props.children}</Row>
if (title || label) {
return (
<AntdSchemaFieldAdaptor {...pickFormItemProps(props)}>
{grids}
</AntdSchemaFieldAdaptor>
)
}
return grids
}
)
export default FormGridRow

View File

@ -0,0 +1 @@
import 'antd/lib/row/style/index'

View File

@ -0,0 +1,68 @@
import React, { Fragment } from 'react'
import {
AntdSchemaFieldAdaptor,
pickFormItemProps,
pickNotFormItemProps
} from '@formily/antd'
import { createVirtualBox } from '@formily/react-schema-renderer'
import { toArr } from '@formily/shared'
import { Row, Col } from 'antd'
import { FormItemProps as ItemProps } from 'antd/lib/form'
import { IFormItemGridProps, IItemProps } from '../types'
import { normalizeCol } from '../shared'
export const FormItemGrid = createVirtualBox<
React.PropsWithChildren<IFormItemGridProps & ItemProps & IItemProps>
>('grid', props => {
const {
cols: rawCols,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
title,
label
} = props
const formItemProps = pickFormItemProps(props)
const gridProps = pickNotFormItemProps(props)
const children = toArr(props.children)
const cols = toArr(rawCols).map(col => normalizeCol(col))
const childNum = children.length
if (cols.length < childNum) {
let offset: number = childNum - cols.length
let lastSpan: number =
24 -
cols.reduce((buf, col) => {
return (
buf +
Number(col.span ? col.span : 0) +
Number(col.offset ? col.offset : 0)
)
}, 0)
for (let i = 0; i < offset; i++) {
cols.push({ span: Math.floor(lastSpan / offset) })
}
}
const grids = (
<Row {...gridProps}>
{children.reduce((buf, child, key) => {
return child
? buf.concat(
<Col key={key} {...cols[key]}>
{child}
</Col>
)
: buf
}, [])}
</Row>
)
if (title || label) {
return (
<AntdSchemaFieldAdaptor {...formItemProps}>
{grids}
</AntdSchemaFieldAdaptor>
)
}
return <Fragment>{grids}</Fragment>
})
export default FormItemGrid

View File

@ -0,0 +1,2 @@
import 'antd/lib/row/style/index'
import 'antd/lib/col/style/index'

View File

@ -0,0 +1,29 @@
import React from 'react'
import { FormItemDeepProvider, useDeepFormItem } from '@formily/antd'
import { createVirtualBox } from '@formily/react-schema-renderer'
import cls from 'classnames'
import { IFormItemTopProps } from '../types'
export const FormLayout = createVirtualBox<IFormItemTopProps>(
'layout',
props => {
const { inline } = useDeepFormItem()
const isInline = props.inline || inline
const children =
isInline || props.className || props.style ? (
<div
className={cls(props.className, {
'ant-form ant-form-inline': isInline
})}
style={props.style}
>
{props.children}
</div>
) : (
props.children
)
return <FormItemDeepProvider {...props}>{children}</FormItemDeepProvider>
}
)
export default FormLayout

View File

@ -0,0 +1,6 @@
import { MegaLayout, FormMegaLayout } from '@formily/antd'
export {
MegaLayout,
FormMegaLayout,
}

View File

@ -0,0 +1,5 @@
import { FormSlot } from '@formily/react-schema-renderer'
export { FormSlot }
export default FormSlot

View File

@ -0,0 +1,157 @@
import React, { useRef, Fragment, useEffect } from 'react'
import {
createControllerBox,
ISchemaVirtualFieldComponentProps,
createEffectHook,
useFormEffects,
useFieldState,
IVirtualBoxProps
} from '@formily/react-schema-renderer'
import { toArr } from '@formily/shared'
import { Steps } from 'antd'
import { createMatchUpdate } from '../shared'
import { IFormStep } from '../types'
enum StateMap {
ON_FORM_STEP_NEXT = 'onFormStepNext',
ON_FORM_STEP_PREVIOUS = 'onFormStepPrevious',
ON_FORM_STEP_GO_TO = 'onFormStepGoto',
ON_FORM_STEP_CURRENT_CHANGE = 'onFormStepCurrentChange',
ON_FORM_STEP_DATA_SOURCE_CHANGED = 'onFormStepDataSourceChanged'
}
const EffectHooks = {
onStepNext$: createEffectHook<void>(StateMap.ON_FORM_STEP_NEXT),
onStepPrevious$: createEffectHook<void>(StateMap.ON_FORM_STEP_PREVIOUS),
onStepGoto$: createEffectHook<void>(StateMap.ON_FORM_STEP_GO_TO),
onStepCurrentChange$: createEffectHook<{
value: number
preValue: number
}>(StateMap.ON_FORM_STEP_CURRENT_CHANGE)
}
type ExtendsProps = StateMap & typeof EffectHooks
export const FormStep: React.FC<IVirtualBoxProps<IFormStep>> &
ExtendsProps = createControllerBox<IFormStep>(
'step',
({
form,
schema,
path,
name,
children
}: ISchemaVirtualFieldComponentProps) => {
const { dataSource, ...stepProps } = schema.getExtendsComponentProps()
const [{ current }, setFieldState] = useFieldState({
current: stepProps.current || 0
})
const ref = useRef(current)
const itemsRef = useRef([])
itemsRef.current = toArr(dataSource)
const matchUpdate = createMatchUpdate(name, path)
const update = (cur: number) => {
form.notify(StateMap.ON_FORM_STEP_CURRENT_CHANGE, {
path,
name,
value: cur,
preValue: current
})
setFieldState({
current: cur
})
}
useEffect(() => {
form.notify(StateMap.ON_FORM_STEP_DATA_SOURCE_CHANGED, {
path,
name,
value: itemsRef.current
})
}, [itemsRef.current.length])
useFormEffects(($, { setFieldState }) => {
const updateFields = () => {
itemsRef.current.forEach(({ name }, index) => {
setFieldState(name, (state: any) => {
state.display = index === current
})
})
}
updateFields()
$(StateMap.ON_FORM_STEP_DATA_SOURCE_CHANGED).subscribe(
({ name, path }) => {
matchUpdate(name, path, () => {
updateFields()
})
}
)
$(StateMap.ON_FORM_STEP_CURRENT_CHANGE).subscribe(
({ value, name, path }: any = {}) => {
matchUpdate(name, path, () => {
form.hostUpdate(() => {
itemsRef.current.forEach(({ name }, index) => {
if (!name)
throw new Error(
'FormStep dataSource must include `name` property'
)
setFieldState(name, (state: any) => {
state.display = index === value
})
})
})
})
}
)
$(StateMap.ON_FORM_STEP_NEXT).subscribe(({ name, path }: any = {}) => {
matchUpdate(name, path, () => {
form.validate().then(({ errors }) => {
if (errors.length === 0) {
update(
ref.current + 1 > itemsRef.current.length - 1
? ref.current
: ref.current + 1
)
}
})
})
})
$(StateMap.ON_FORM_STEP_PREVIOUS).subscribe(
({ name, path }: any = {}) => {
matchUpdate(name, path, () => {
update(ref.current - 1 < 0 ? ref.current : ref.current - 1)
})
}
)
$(StateMap.ON_FORM_STEP_GO_TO).subscribe(
({ name, path, value }: any = {}) => {
matchUpdate(name, path, () => {
if (!(value < 0 || value > itemsRef.current.length)) {
update(value)
}
})
}
)
})
ref.current = current
return (
<Fragment>
<Steps {...stepProps} current={current}>
{itemsRef.current.map((props, key) => {
return <Steps.Step {...props} key={key} />
})}
</Steps>{' '}
{children}
</Fragment>
)
}
) as any
Object.assign(FormStep, StateMap, EffectHooks)
export default FormStep

View File

@ -0,0 +1 @@
import 'antd/lib/steps/style/index'

View File

@ -0,0 +1,178 @@
import React, { Fragment, useEffect, useRef } from 'react'
import {
createControllerBox,
ISchemaVirtualFieldComponentProps,
createEffectHook,
useFormEffects,
useFieldState,
FormEffectHooks,
SchemaField,
FormPath,
IVirtualBoxProps
} from '@formily/react-schema-renderer'
import { Tabs, Badge } from 'antd'
import { TabPaneProps } from 'antd/lib/tabs'
import { IFormTab } from '../types'
import { createMatchUpdate } from '../shared'
enum StateMap {
ON_FORM_TAB_ACTIVE_KEY_CHANGE = 'onFormTabActiveKeyChange'
}
const { onFormChange$ } = FormEffectHooks
const EffectHooks = {
onTabActiveKeyChange$: createEffectHook<{
name?: string
path?: string
value?: any
}>(StateMap.ON_FORM_TAB_ACTIVE_KEY_CHANGE)
}
const parseTabItems = (items: any, hiddenKeys?: string[]) => {
return items.reduce((buf: any, { schema, key }) => {
if (Array.isArray(hiddenKeys)) {
if (hiddenKeys.includes(key)) {
return buf
}
}
if (schema.getExtendsComponent() === 'tabpane') {
return buf.concat({
props: schema.getExtendsComponentProps(),
schema,
key
})
}
return buf
}, [])
}
const parseDefaultActiveKey = (hiddenKeys: Array<string> = [], items: any, defaultActiveKey) => {
if(!hiddenKeys.includes(defaultActiveKey))return defaultActiveKey
const index = items.findIndex(item => !hiddenKeys.includes(item.key))
return index >= 0 ? items[index].key : ''
}
const parseChildrenErrors = (errors: any, target: string) => {
return errors.filter(({ path }) => {
return FormPath.parse(path).includes(target)
})
}
const addErrorBadge = (
tab: React.ReactNode,
currentPath: FormPath,
childrenErrors: any[]
) => {
const currentErrors = childrenErrors.filter(({ path }) => {
return FormPath.parse(path).includes(currentPath)
})
if (currentErrors.length > 0) {
return (
<Badge offset={[12, 0]} count={currentErrors.length}>
{tab}
</Badge>
)
}
return tab
}
type ExtendsProps = StateMap &
typeof EffectHooks & {
TabPane: React.FC<IVirtualBoxProps<TabPaneProps>>
}
type ExtendsState = {
activeKey?: string
childrenErrors?: any
}
export const FormTab: React.FC<IVirtualBoxProps<IFormTab>> &
ExtendsProps = createControllerBox<IFormTab>(
'tab',
({ form, schema, name, path }: ISchemaVirtualFieldComponentProps) => {
const orderProperties = schema.getOrderProperties()
let { hiddenKeys, defaultActiveKey, ...componentProps } = schema.getExtendsComponentProps()
hiddenKeys = hiddenKeys || []
const [{ activeKey, childrenErrors }, setFieldState] = useFieldState<
ExtendsState
>({
activeKey: parseDefaultActiveKey(hiddenKeys, orderProperties, defaultActiveKey),
childrenErrors: []
})
const itemsRef = useRef([])
itemsRef.current = parseTabItems(orderProperties, hiddenKeys)
const update = (cur: string) => {
form.notify(StateMap.ON_FORM_TAB_ACTIVE_KEY_CHANGE, {
name,
path,
value: cur
})
}
const matchUpdate = createMatchUpdate(name, path)
useEffect(() => {
if (Array.isArray(hiddenKeys)) {
setFieldState({
activeKey: parseDefaultActiveKey(hiddenKeys, orderProperties, defaultActiveKey)
})
}
}, [hiddenKeys.length])
useFormEffects(({ hasChanged }) => {
onFormChange$().subscribe(formState => {
const errorsChanged = hasChanged(formState, 'errors')
if (errorsChanged) {
setFieldState({
childrenErrors: parseChildrenErrors(formState.errors, path)
})
}
})
EffectHooks.onTabActiveKeyChange$().subscribe(
({ value, name, path }: any = {}) => {
if(!itemsRef.current.map(item => item.key).includes(value))return
matchUpdate(name, path, () => {
setFieldState({
activeKey: value
})
})
}
)
})
return (
<Tabs {...componentProps} activeKey={activeKey} onChange={update}>
{itemsRef.current.map(({ props, schema, key }) => {
const currentPath = FormPath.parse(path).concat(key)
return (
<Tabs.TabPane
{...props}
tab={
activeKey === key
? props.tab
: addErrorBadge(props.tab, currentPath, childrenErrors)
}
key={key}
forceRender
>
<SchemaField
path={currentPath}
schema={schema}
onlyRenderProperties
/>
</Tabs.TabPane>
)
})}
</Tabs>
)
}
) as any
FormTab.TabPane = createControllerBox<TabPaneProps>(
'tabpane',
({ children }) => {
return <Fragment>{children}</Fragment>
}
)
Object.assign(FormTab, StateMap, EffectHooks)
export default FormTab

View File

@ -0,0 +1,2 @@
import 'antd/lib/tabs/style/index'
import 'antd/lib/badge/style/index'

View File

@ -0,0 +1,128 @@
import React, { useRef, useLayoutEffect } from 'react'
import { createControllerBox, Schema } from '@formily/react-schema-renderer'
import { IFormTextBox } from '../types'
import { toArr } from '@formily/shared'
import { FormItemProps as ItemProps } from 'antd/lib/form'
import { version } from 'antd'
import { AntdSchemaFieldAdaptor, pickFormItemProps } from '@formily/antd'
import styled from 'styled-components'
const isV4 = /^4\./.test(version)
export const FormTextBox = createControllerBox<IFormTextBox & ItemProps>(
'text-box',
styled(({ props, form, className, children }) => {
const schema = new Schema(props)
const mergeProps = schema.getExtendsComponentProps()
const { title, label, text, gutter, style } = Object.assign(
{
gutter: 5
},
mergeProps
)
const formItemProps = pickFormItemProps(mergeProps)
const ref: React.RefObject<HTMLDivElement> = useRef()
const arrChildren = toArr(children)
const split = text.split('%s')
let index = 0
useLayoutEffect(() => {
if (ref.current) {
const elements = ref.current.querySelectorAll('.text-box-field')
const syncLayouts = Array.prototype.map.call(
elements,
(el: HTMLElement) => {
return [
el,
() => {
const ctrl = el.querySelector('.ant-form-item-children')
setTimeout(() => {
if (ctrl) {
const editable = form.getFormState(state => state.editable)
el.style.width = editable
? ctrl.getBoundingClientRect().width + 'px'
: 'auto'
}
})
}
]
}
)
syncLayouts.forEach(([el, handler]) => {
handler()
el.addEventListener('DOMSubtreeModified', handler)
})
return () => {
syncLayouts.forEach(([el, handler]) => {
el.removeEventListener('DOMSubtreeModified', handler)
})
}
}
}, [])
const newChildren = split.reduce((buf, item, key) => {
return buf.concat(
item ? (
<p
key={index++}
className="text-box-words"
style={{
marginRight: gutter / 2,
marginLeft: gutter / 2,
...style
}}
>
{item}
</p>
) : null,
arrChildren[key] ? (
<div key={index++} className="text-box-field">
{arrChildren[key]}
</div>
) : null
)
}, [])
const textChildren = (
<div
className={`${className} ${mergeProps.className}`}
style={{
marginRight: -gutter / 2,
marginLeft: -gutter / 2
}}
ref={ref}
>
{newChildren}
</div>
)
if (!title && !label) return textChildren
return (
<AntdSchemaFieldAdaptor {...formItemProps}>
{textChildren}
</AntdSchemaFieldAdaptor>
)
})`
display: flex;
.text-box-words:nth-child(1) {
margin-left: 0;
}
.text-box-words {
margin-bottom: 0 !important;
${isV4 ? 'line-height:32px' : ''}
}
.text-box-field {
display: inline-block;
.ant-form-item {
margin-bottom: 0 !important;
}
}
.next-form-item {
margin-bottom: 0 !important;
}
.preview-text {
text-align: center !important;
}
`
)
export default FormTextBox

View File

@ -0,0 +1,29 @@
export * from './text-button'
export * from './time-picker'
export * from './transfer'
export * from './switch'
export * from './array-cards'
export * from './array-table'
export * from './checkbox'
export * from './circle-button'
export * from './date-picker'
export * from './form-block'
export * from './form-card'
export * from './form-tab'
export * from './form-grid-col'
export * from './form-grid-row'
export * from './form-item-grid'
export * from './form-layout'
export * from './form-mega-layout'
export * from './form-step'
export * from './form-text-box'
export * from './form-slot'
export * from './input'
export * from './select'
export * from './number-picker'
export * from './password'
export * from './radio'
export * from './range'
export * from './rating'
export * from './upload'
export * from './registry'

View File

@ -0,0 +1,15 @@
import { connect } from '@formily/react-schema-renderer'
import { Input as AntdInput } from 'antd'
import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared'
export const Input = connect<'TextArea'>({
getProps: mapStyledProps,
getComponent: mapTextComponent
})(acceptEnum(AntdInput))
Input.TextArea = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent
})(acceptEnum(AntdInput.TextArea))
export default Input

View File

@ -0,0 +1 @@
import 'antd/lib/input/style/index'

View File

@ -0,0 +1,10 @@
import { connect } from '@formily/react-schema-renderer'
import { InputNumber } from 'antd'
import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared'
export const NumberPicker = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent
})(acceptEnum(InputNumber))
export default NumberPicker

View File

@ -0,0 +1 @@
import 'antd/lib/input-number/style/index'

View File

@ -0,0 +1,80 @@
import React, { useState } from 'react'
import { connect } from '@formily/react-schema-renderer'
import { Input } from 'antd'
import { PasswordProps } from 'antd/lib/input'
import { PasswordStrength } from '@formily/react-shared-components'
import styled from 'styled-components'
import { mapStyledProps } from '../shared'
export interface IPasswordProps extends PasswordProps {
checkStrength: boolean
}
export const Password = connect({
getProps: mapStyledProps
})(styled((props: IPasswordProps) => {
const { value, className, checkStrength, ...others } = props
return (
<span className={className}>
<Input.Password {...others} value={value} />
{checkStrength && (
<PasswordStrength value={String(value)}>
{score => {
return (
<div className="password-strength-wrapper">
<div className="div-1 div" />
<div className="div-2 div" />
<div className="div-3 div" />
<div className="div-4 div" />
<div
className="password-strength-bar"
style={{
clipPath: `polygon(0 0,${score}% 0,${score}% 100%,0 100%)`
}}
/>
</div>
)
}}
</PasswordStrength>
)}
</span>
)
})`
.password-strength-wrapper {
background: #e0e0e0;
margin-bottom: 3px;
position: relative;
.div {
position: absolute;
z-index: 1;
height: 8px;
top: 0;
background: #fff;
width: 1px;
transform: translate(-50%, 0);
}
.div-1 {
left: 20%;
}
.div-2 {
left: 40%;
}
.div-3 {
left: 60%;
}
.div-4 {
left: 80%;
}
.password-strength-bar {
position: relative;
background-image: -webkit-linear-gradient(left, #ff5500, #ff9300);
transition: all 0.35s ease-in-out;
height: 8px;
width: 100%;
margin-top: 5px;
}
}
`)
export default Password

View File

@ -0,0 +1 @@
import 'antd/lib/input/style/index'

View File

@ -0,0 +1,19 @@
import { connect } from '@formily/react-schema-renderer'
import { Radio as AntdRadio } from 'antd'
import {
transformDataSourceKey,
mapStyledProps,
mapTextComponent
} from '../shared'
export const Radio = connect<'Group'>({
valueName: 'checked',
getProps: mapStyledProps
})(AntdRadio)
Radio.Group = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent
})(transformDataSourceKey(AntdRadio.Group, 'options'))
export default Radio

View File

@ -0,0 +1 @@
import 'antd/lib/radio/style/index'

View File

@ -0,0 +1,60 @@
import React from 'react'
import { Slider } from 'antd'
import { connect } from '@formily/react-schema-renderer'
import { mapStyledProps } from '../shared'
export interface ISliderMarks {
[key: number]:
| React.ReactNode
| {
style: React.CSSProperties
label: React.ReactNode
}
}
export declare type SliderValue = number | [number, number]
// TODO 并不是方法,最好能引用组件的 typescript 接口定义
export interface ISliderProps {
min?: number
max?: number
marks?: ISliderMarks
value?: SliderValue
defaultValue?: SliderValue
onChange?: (value: SliderValue) => void
}
export const Range = connect({
defaultProps: {
style: {
width: 320
}
},
getProps: mapStyledProps
})(
class Component extends React.Component<ISliderProps> {
public render() {
const { onChange, value, min, max, marks, ...rest } = this.props
let newMarks = {}
if (Array.isArray(marks)) {
marks.forEach(mark => {
newMarks[mark] = mark
})
} else {
newMarks = marks
}
return (
<Slider
{...rest}
onChange={onChange}
value={value}
min={min}
max={max}
marks={newMarks}
/>
)
}
}
)
export default Range

View File

@ -0,0 +1 @@
import 'antd/lib/slider/style/index'

View File

@ -0,0 +1,9 @@
import { connect } from '@formily/react-schema-renderer'
import { Rate } from 'antd'
import { mapStyledProps } from '../shared'
export const Rating = connect({
getProps: mapStyledProps
})(Rate)
export default Rating

View File

@ -0,0 +1 @@
import 'antd/lib/rate/style/index'

View File

@ -0,0 +1,41 @@
import { registerFormFields } from '@formily/antd'
import { TimePicker } from './time-picker'
import { Transfer } from './transfer'
import { Switch } from './switch'
import { ArrayCards } from './array-cards'
import { ArrayTable } from './array-table'
import { Checkbox } from './checkbox'
import { DatePicker } from './date-picker'
import { Input } from './input'
import { NumberPicker } from './number-picker'
import { Password } from './password'
import { Radio } from './radio'
import { Range } from './range'
import { Rating } from './rating'
import { Upload } from './upload'
export const setup = () => {
registerFormFields({
time: TimePicker,
timerange: TimePicker.RangePicker,
transfer: Transfer,
boolean: Switch,
array: ArrayCards,
cards: ArrayCards,
table: ArrayTable,
checkbox: Checkbox.Group,
date: DatePicker,
daterange: DatePicker.RangePicker,
year: DatePicker.YearPicker,
month: DatePicker.MonthPicker,
week: DatePicker.WeekPicker,
string: Input,
textarea: Input.TextArea,
number: NumberPicker,
password: Password,
radio: Radio.Group,
range: Range,
rating: Rating,
upload: Upload
})
}

View File

@ -0,0 +1,13 @@
import { connect } from '@formily/react-schema-renderer'
import {
Select as AntdSelect,
mapStyledProps,
mapTextComponent
} from '../shared'
export const Select = connect({
getProps: mapStyledProps,
getComponent: mapTextComponent,
})(AntdSelect)
export default Select

View File

@ -0,0 +1 @@
import 'antd/lib/select/style/index'

View File

@ -0,0 +1,126 @@
import React from 'react'
import { mapTextComponent, mapStyledProps, normalizeCol } from '@formily/antd'
import { Select as AntSelect } from 'antd'
import { SelectProps as AntSelectProps } from 'antd/lib/select'
import styled from 'styled-components'
import { isArr, FormPath } from '@formily/shared'
export * from '@formily/shared'
export const compose = (...args: any[]) => {
return (payload: any, ...extra: any[]) => {
return args.reduce((buf, fn) => {
return buf !== undefined ? fn(buf, ...extra) : fn(payload, ...extra)
}, payload)
}
}
interface SelectOption {
label: React.ReactText
value: any
[key: string]: any
}
type SelectProps = AntSelectProps & {
dataSource?: SelectOption[]
}
const createEnum = (enums: any) => {
if (isArr(enums)) {
return enums.map(item => {
if (typeof item === 'object') {
return {
...item
}
} else {
return {
label: item,
value: item
}
}
})
}
return []
}
export const Select: React.FC<SelectProps> = styled((props: SelectProps) => {
const { dataSource = [], onChange, ...others } = props
const children = createEnum(dataSource).map(item => {
const { label, value, ...others } = item
return (
<AntSelect.Option
key={value}
{...others}
title={label as string}
value={value}
>
{label}
</AntSelect.Option>
)
})
return (
<AntSelect
className={props.className}
{...others}
onChange={(value: any, options: any) => {
onChange(
value,
isArr(options)
? options.map(item => ({
...item,
props: undefined
}))
: {
...options,
props: undefined //干掉循环引用
}
)
}}
>
{children}
</AntSelect>
)
})`
min-width: 100px;
width: 100%;
`
export const acceptEnum = (component: React.JSXElementConstructor<any>) => {
return ({ dataSource, ...others }) => {
if (dataSource) {
return React.createElement(Select, { dataSource, ...others })
} else {
return React.createElement(component, others)
}
}
}
export const transformDataSourceKey = (component, dataSourceKey) => {
return ({ dataSource, ...others }) => {
return React.createElement(component, {
[dataSourceKey]: dataSource,
...others
})
}
}
export const createMatchUpdate = (name: string, path: string) => (
targetName: string,
targetPath: string,
callback: () => void
) => {
if (targetName || targetPath) {
if (targetName) {
if (FormPath.parse(targetName).matchAliasGroup(name, path)) {
callback()
}
} else if (targetPath) {
if (FormPath.parse(targetPath).matchAliasGroup(name, path)) {
callback()
}
}
} else {
callback()
}
}
export { mapTextComponent, mapStyledProps, normalizeCol }

View File

@ -0,0 +1,10 @@
import { Switch as AntdSwitch } from 'antd'
import { connect } from '@formily/react-schema-renderer'
import { acceptEnum, mapStyledProps } from '../shared'
export const Switch = connect({
valueName: 'checked',
getProps: mapStyledProps
})(acceptEnum(AntdSwitch))
export default Switch;

View File

@ -0,0 +1 @@
import 'antd/lib/switch/style/index'

View File

@ -0,0 +1,9 @@
import React from 'react'
import { Button } from 'antd'
import { ButtonProps } from 'antd/lib/button'
export const TextButton: React.FC<ButtonProps> = props => (
<Button type="link" {...props} />
)
export default TextButton

View File

@ -0,0 +1 @@
import 'antd/lib/button/style/index'

View File

@ -0,0 +1,50 @@
import { connect } from '@formily/react-schema-renderer'
import moment from 'moment'
import { TimePicker as AntdTimePicker } from 'antd'
import {
mapStyledProps,
mapTextComponent,
compose,
isStr,
isArr,
} from '../shared'
const transformMoment = (value) => {
if (value === '') return undefined
return value
}
const mapMomentValue = (props: any, fieldProps: any) => {
const { value } = props
if (!fieldProps.editable) return props
try {
if (isStr(value) && value) {
props.value = moment(value, 'HH:mm:ss')
} else if (isArr(value) && value.length) {
props.value = value.map(
(item) => (item && moment(item, 'HH:mm:ss')) || ''
)
}
} catch (e) {
throw new Error(e)
}
return props
}
export const TimePicker = connect<'RangePicker'>({
getValueFromEvent(_, value) {
return transformMoment(value)
},
getProps: compose(mapStyledProps, mapMomentValue),
getComponent: mapTextComponent,
})(AntdTimePicker)
TimePicker.RangePicker = connect({
getValueFromEvent(_, [startDate, endDate]) {
return [transformMoment(startDate), transformMoment(endDate)]
},
getProps: compose(mapStyledProps, mapMomentValue),
getComponent: mapTextComponent,
})(AntdTimePicker.RangePicker)
export default TimePicker

View File

@ -0,0 +1 @@
import 'antd/lib/time-picker/style/index'

View File

@ -0,0 +1,10 @@
import { connect } from '@formily/react-schema-renderer'
import { Transfer as AntdTransfer } from 'antd'
import { mapStyledProps } from '../shared'
export const Transfer = connect({
getProps: mapStyledProps,
valueName: 'targetKeys'
})(AntdTransfer)
export default Transfer

View File

@ -0,0 +1 @@
import 'antd/lib/transfer/style/index'

View File

@ -0,0 +1,103 @@
import { ButtonProps } from 'antd/lib/button'
import { FormProps, FormItemProps as ItemProps } from 'antd/lib/form'
import {
StepsProps as StepProps,
StepProps as StepItemProps
} from 'antd/lib/steps'
import { TabsProps } from 'antd/lib/tabs'
import {
ISchemaFormProps,
IMarkupSchemaFieldProps,
ISchemaFieldComponentProps,
FormPathPattern
} from '@formily/react-schema-renderer'
import { PreviewTextConfigProps } from '@formily/react-shared-components'
import { StyledComponent } from 'styled-components'
type ColSpanType = number | string
export type IAntdSchemaFormProps = Omit<
FormProps,
'onSubmit' | 'defaultValue'
> &
IFormItemTopProps &
PreviewTextConfigProps &
ISchemaFormProps
export type IAntdSchemaFieldProps = IMarkupSchemaFieldProps
export interface ISubmitProps extends ButtonProps {
onSubmit?: ISchemaFormProps['onSubmit']
showLoading?: boolean
}
export interface IResetProps extends ButtonProps {
forceClear?: boolean
validate?: boolean
}
export type IFormItemTopProps = React.PropsWithChildren<
Exclude<
Pick<ItemProps, 'prefixCls' | 'labelCol' | 'wrapperCol' | 'labelAlign'>,
'labelCol' | 'wrapperCol'
> & {
inline?: boolean
className?: string
style?: React.CSSProperties
labelCol?: number | { span: number; offset?: number }
wrapperCol?: number | { span: number; offset?: number }
}
>
export type ISchemaFieldAdaptorProps = Omit<
ItemProps,
'labelCol' | 'wrapperCol'
> &
Partial<ISchemaFieldComponentProps> & {
labelCol?: number | { span: number; offset?: number }
wrapperCol?: number | { span: number; offset?: number }
}
export type StyledCP<P extends {}> = StyledComponent<
(props: React.PropsWithChildren<P>) => React.ReactElement,
any,
{},
never
>
export type StyledCC<Props, Statics = {}> = StyledCP<Props> & Statics
export interface IFormButtonGroupProps {
sticky?: boolean
style?: React.CSSProperties
itemStyle?: React.CSSProperties
className?: string
align?: 'left' | 'right' | 'start' | 'end' | 'top' | 'bottom' | 'center'
triggerDistance?: number
zIndex?: number
span?: ColSpanType
offset?: ColSpanType
}
export interface IItemProps {
title?: React.ReactText
description?: React.ReactText
}
export interface IFormItemGridProps extends IItemProps {
cols?: Array<number | { span: number; offset: number }>
gutter?: number
}
export interface IFormTextBox extends IItemProps {
text?: string
gutter?: number
}
export interface IFormStep extends StepProps {
dataSource: Array<StepItemProps & { name: FormPathPattern }>
}
export interface IFormTab extends TabsProps {
hiddenKeys?: string[]
}

View File

@ -0,0 +1,292 @@
import React from 'react'
import { connect } from '@formily/react-schema-renderer'
import { Button, Upload as AntdUpload } from 'antd'
import styled from 'styled-components'
import { toArr, isArr, isEqual, mapStyledProps } from '../shared'
import {
LoadingOutlined,
PlusOutlined,
UploadOutlined,
InboxOutlined
} from '@ant-design/icons'
const { Dragger: UploadDragger } = AntdUpload
const exts = [
{
ext: /\.docx?$/i,
icon: '//img.alicdn.com/tfs/TB1n8jfr1uSBuNjy1XcXXcYjFXa-200-200.png'
},
{
ext: /\.pptx?$/i,
icon: '//img.alicdn.com/tfs/TB1ItgWr_tYBeNjy1XdXXXXyVXa-200-200.png'
},
{
ext: /\.jpe?g$/i,
icon: '//img.alicdn.com/tfs/TB1wrT5r9BYBeNjy0FeXXbnmFXa-200-200.png'
},
{
ext: /\.pdf$/i,
icon: '//img.alicdn.com/tfs/TB1GwD8r9BYBeNjy0FeXXbnmFXa-200-200.png'
},
{
ext: /\.png$/i,
icon: '//img.alicdn.com/tfs/TB1BHT5r9BYBeNjy0FeXXbnmFXa-200-200.png'
},
{
ext: /\.eps$/i,
icon: '//img.alicdn.com/tfs/TB1G_iGrVOWBuNjy0FiXXXFxVXa-200-200.png'
},
{
ext: /\.ai$/i,
icon: '//img.alicdn.com/tfs/TB1B2cVr_tYBeNjy1XdXXXXyVXa-200-200.png'
},
{
ext: /\.gif$/i,
icon: '//img.alicdn.com/tfs/TB1DTiGrVOWBuNjy0FiXXXFxVXa-200-200.png'
},
{
ext: /\.svg$/i,
icon: '//img.alicdn.com/tfs/TB1uUm9rY9YBuNjy0FgXXcxcXXa-200-200.png'
},
{
ext: /\.xlsx?$/i,
icon: '//img.alicdn.com/tfs/TB1any1r1OSBuNjy0FdXXbDnVXa-200-200.png'
},
{
ext: /\.psd?$/i,
icon: '//img.alicdn.com/tfs/TB1_nu1r1OSBuNjy0FdXXbDnVXa-200-200.png'
},
{
ext: /\.(wav|aif|aiff|au|mp1|mp2|mp3|ra|rm|ram|mid|rmi)$/i,
icon: '//img.alicdn.com/tfs/TB1jPvwr49YBuNjy0FfXXXIsVXa-200-200.png'
},
{
ext: /\.(avi|wmv|mpg|mpeg|vob|dat|3gp|mp4|mkv|rm|rmvb|mov|flv)$/i,
icon: '//img.alicdn.com/tfs/TB1FrT5r9BYBeNjy0FeXXbnmFXa-200-200.png'
},
{
ext: /\.(zip|rar|arj|z|gz|iso|jar|ace|tar|uue|dmg|pkg|lzh|cab)$/i,
icon: '//img.alicdn.com/tfs/TB10jmfr29TBuNjy0FcXXbeiFXa-200-200.png'
},
{
ext: /\.[^.]+$/i,
icon: '//img.alicdn.com/tfs/TB10.R4r3mTBuNjy1XbXXaMrVXa-200-200.png'
}
]
const UploadPlaceholder = styled(props => (
<div>
{props.loading ? <LoadingOutlined /> : <PlusOutlined />}
<div className={'ant-upload-text'}></div>
</div>
))``
const testOpts = (ext, options) => {
if (options && isArr(options.include)) {
return options.include.some(url => ext.test(url))
}
if (options && isArr(options.exclude)) {
return !options.exclude.some(url => ext.test(url))
}
return true
}
const getImageByUrl = (url, options) => {
for (let i = 0; i < exts.length; i++) {
if (exts[i].ext.test(url) && testOpts(exts[i].ext, options)) {
return exts[i].icon || url
}
}
return url
}
const normalizeFileList = fileList => {
if (fileList && fileList.length) {
return fileList.map(file => {
return {
uid: file.uid,
status: file.status,
name: file.name,
url: file.downloadURL || file.imgURL || file.url,
...file.response,
thumbUrl: file.imgURL || getImageByUrl(file.downloadURL || file.url, {
exclude: ['.png', '.jpg', '.jpeg', '.gif']
})
}
})
}
return []
}
const shallowClone = val => {
let result = isArr(val)
? [...val]
: typeof val === 'object'
? { ...val }
: val
if (isArr(result)) {
result = result.map(item => ({
...item,
// 必须要有一个不重复的uid
uid:
item.uid ||
Math.random()
.toFixed(16)
.slice(2, 10)
}))
}
return result
}
export interface IUploaderState {
value: any[]
}
// TODO 能不能直接引用 antd 里面的接口定义呢 ?
export declare type UploadListType = 'text' | 'picture' | 'picture-card'
export interface IUploaderProps {
onChange: (value: any[]) => void
locale: { [name: string]: any }
value: any[]
listType?: UploadListType
}
export const Upload = connect({
getProps: mapStyledProps
})(
class Uploader extends React.Component<IUploaderProps, IUploaderState> {
public static defaultProps = {
action:
'https://www.easy-mock.com/mock/5b713974309d0d7d107a74a3/alifd/upload',
listType: 'text',
multiple: true,
className: 'antd-uploader'
}
readonly state: IUploaderState
constructor(props) {
super(props)
this.state = {
value: shallowClone(toArr(props.value))
}
}
public onRemoveHandler = file => {
const { value } = this.state
const fileList = []
value.forEach(item => {
if (item.uid !== file.uid) {
fileList.push(item)
}
})
this.props.onChange(fileList)
}
public onChangeHandler = ({ fileList }) => {
const { onChange } = this.props
fileList = toArr(fileList)
if (
fileList.every(file => {
if (
file.status === 'done' ||
file.imgURL ||
file.downloadURL ||
file.url ||
file.thumbUrl
)
return true
if (file.response) {
if (
file.response.imgURL ||
file.response.downloadURL ||
file.response.url ||
file.response.thumbUrl
)
return true
}
return false
}) &&
fileList.length
) {
fileList = normalizeFileList(fileList)
this.setState(
{
value: fileList
},
() => {
onChange(fileList.length > 0 ? fileList : undefined)
}
)
} else {
this.setState({
value: fileList
})
}
}
public componentDidUpdate(preProps) {
if (this.props.value && !isEqual(this.props.value, preProps.value)) {
this.setState({
value: shallowClone(this.props.value)
})
}
}
public render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { listType, locale, onChange, value, ...others } = this.props
if (listType.indexOf('card') > -1) {
return (
<AntdUpload
{...others}
fileList={this.state.value}
onChange={this.onChangeHandler}
onRemove={this.onRemoveHandler}
listType={'picture-card'}
>
<UploadPlaceholder />
</AntdUpload>
)
}
if (listType.indexOf('dragger') > -1) {
return (
<UploadDragger
{...others}
fileList={this.state.value}
onChange={this.onChangeHandler}
onRemove={this.onRemoveHandler}
// TODO image 类型是跟 picture 一样 ?
listType={listType.indexOf('image') > -1 ? 'picture' : 'text'}
>
<p className={'ant-upload-drag-icon'}>
<InboxOutlined />
</p>
<p className={'ant-upload-text'}></p>
</UploadDragger>
)
}
return (
<AntdUpload
{...others}
fileList={this.state.value}
onChange={this.onChangeHandler}
onRemove={this.onRemoveHandler}
listType={listType}
>
<Button style={{ margin: '0 0 10px' }}>
<UploadOutlined />
{(locale && locale.uploadText) || '上传文件'}
</Button>
</AntdUpload>
)
}
}
)
export default Upload

View File

@ -0,0 +1 @@
import 'antd/lib/upload/style/index'

View File

@ -0,0 +1,17 @@
import React from 'react';
import { PageHeader, Tabs, Button, Statistic, Descriptions } from 'antd';
import './style.less';
export function Breadcrumb(props) {
return (
<div className={'breadcrumb'}>{props.children}</div>
);
}
Breadcrumb.Item = (props) => {
return (
<div className={'breadcrumb-item'}></div>
);
}
export default Breadcrumb;

View File

@ -0,0 +1,28 @@
.collection-item {
.breadcrumb {
display: flex;
position: absolute;
width: 100%;
padding: 12px 14px 0;
z-index: 4;
&-item {
vertical-align: middle;
color: rgba(0,0,0,.45);
&::after {
content: "/";
color: rgba(0,0,0,.45);
display: inline-block;
margin: 0 8px;
}
&:last-child::after {
display: none;
}
a {
color: rgba(0,0,0,.45);
}
span {
color: rgba(0,0,0,.45);
}
}
}
}

View File

@ -0,0 +1,42 @@
import React from 'react';
import ViewFactory from '@/components/views';
import { PageHeader, Tabs, Button, Statistic, Descriptions } from 'antd';
import { useRequest, request, Spin } from '@nocobase/client';
import { getPathName } from './utils';
export function CollectionIndex(props) {
const { viewName, collection } = props.match.params;
const { title, defaultViewName } = props.collection;
return (
<div>
<PageHeader
ghost={false}
title={title}
// subTitle="This is a subtitle"
extra={[
// <Button key="3">Operation</Button>,
// <Button key="2">Operation</Button>,
// <Button key="1" type="primary">
// Primary
// </Button>,
]}
// footer={
// <Tabs size={'small'} defaultActiveKey="1">
// <Tabs.TabPane tab="已发布" key="1" />
// <Tabs.TabPane tab="草稿" key="2" />
// </Tabs>
// }
/>
<div className={'collection-content'}>
<ViewFactory {...props}
viewCollectionName={collection}
viewName={viewName||defaultViewName}
resourceName={collection}
/>
</div>
</div>
);
}
export default CollectionIndex;

View File

@ -0,0 +1,56 @@
import React from 'react';
import { PageHeader, Tabs, Button, Statistic, Descriptions } from 'antd';
import { CollectionTabPane } from './CollectionTabPane';
import { getPathName, redirectTo } from './utils';
export function CollectionSingle(props) {
console.log(props);
const { item = {} } = props;
const { tabs = [] } = props.collection;
const activeTab = tabs.find(tab => tab.name == item.tabName)||{};
if (!activeTab) {
return null;
}
return (
<div>
<PageHeader
ghost={false}
onBack={() => {
redirectTo({
...props.match.params,
removeLastItem: true,
});
}}
title={'企业信息库'}
// subTitle="This is a subtitle"
extra={[
// <Button key="3">Operation</Button>,
// <Button key="2">Operation</Button>,
// <Button key="1" type="primary">
// Primary
// </Button>,
]}
footer={
<Tabs size={'small'}
defaultActiveKey={`${activeTab.name}`}
onTabClick={(activeKey) => {
redirectTo({
...props.match.params,
lastItem: {
tabName: activeKey,
},
});
}}
>
{tabs.map(tab => <Tabs.TabPane tab={tab.title} key={`${tab.name}`} />)}
</Tabs>
}
/>
<div className={'collection-content'}>
<CollectionTabPane {...props} activeTab={activeTab}/>
</div>
</div>
);
}
export default CollectionSingle;

Some files were not shown because too many files have changed in this diff Show More