发布核心框架 (#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:
parent
e5d30b30ba
commit
dcdb21d398
19
.editorconfig
Normal file
19
.editorconfig
Normal 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
|
16
.env.example
Normal file
16
.env.example
Normal 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
|
3
.fatherrc.ts
Executable file → Normal file
3
.fatherrc.ts
Executable file → Normal file
@ -7,11 +7,12 @@ const headPkgs = [
|
|||||||
'database',
|
'database',
|
||||||
'resourcer',
|
'resourcer',
|
||||||
'actions',
|
'actions',
|
||||||
|
'client',
|
||||||
];
|
];
|
||||||
const tailPkgs = [];
|
const tailPkgs = [];
|
||||||
const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
|
const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
|
||||||
(pkg) => {
|
(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
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
lib/
|
lib/
|
||||||
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
29
README.md
Normal file
29
README.md
Normal 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>
|
||||||
|
~~~
|
@ -9,9 +9,10 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- node-network
|
- node-network
|
||||||
environment:
|
environment:
|
||||||
- VERDACCIO_PORT=4873
|
- VERDACCIO_PORT=${VERDACCIO_PORT}
|
||||||
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "4873:4873"
|
- "${VERDACCIO_PORT}:4873"
|
||||||
# volumes:
|
# volumes:
|
||||||
# - "./verdaccio/storage:/verdaccio/storage"
|
# - "./verdaccio/storage:/verdaccio/storage"
|
||||||
# - "./verdaccio/config:/verdaccio/conf"
|
# - "./verdaccio/config:/verdaccio/conf"
|
||||||
@ -19,20 +20,20 @@ services:
|
|||||||
mysql:
|
mysql:
|
||||||
image: mysql:5.7
|
image: mysql:5.7
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: "test"
|
MYSQL_DATABASE: ${DB_DATABASE}
|
||||||
MYSQL_USER: "test"
|
MYSQL_USER: ${DB_USER}
|
||||||
MYSQL_PASSWORD: "test"
|
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||||
MYSQL_ROOT_PASSWORD: "test"
|
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "43306:3306"
|
- "${DB_MYSQL_PORT}:3306"
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:10
|
image: postgres:10
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "45432:5432"
|
- "${DB_POSTGRES_PORT}:5432"
|
||||||
command: postgres -c wal_level=logical
|
command: postgres -c wal_level=logical
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: test
|
POSTGRES_USER: ${DB_USER}
|
||||||
POSTGRES_DB: test
|
POSTGRES_DB: ${DB_DATABASE}
|
||||||
POSTGRES_PASSWORD: test
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
0
jest.config.js
Executable file → Normal file
0
jest.config.js
Executable file → Normal file
@ -10,7 +10,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"allowBranch": "master",
|
"allowBranch": ["master", "develop"],
|
||||||
"ignoreChanges": [
|
"ignoreChanges": [
|
||||||
"*.md"
|
"*.md"
|
||||||
]
|
]
|
||||||
|
5
nodemon.json
Normal file
5
nodemon.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"watch": ["packages", ".env"],
|
||||||
|
"ext": "ts",
|
||||||
|
"exec": "ts-node ./packages/server/example/index.ts"
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "cd packages/app && npm start",
|
||||||
|
"start-server": "yarn nodemon",
|
||||||
"bootstrap": "lerna bootstrap --no-ci",
|
"bootstrap": "lerna bootstrap --no-ci",
|
||||||
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
|
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
|
||||||
"build-father-build": "cd packages/father-build && npm run build",
|
"build-father-build": "cd packages/father-build && npm run build",
|
||||||
@ -14,11 +16,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@koa/router": "^9.3.1",
|
"@koa/router": "^9.3.1",
|
||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^26.0.4",
|
||||||
|
"@types/koa": "^2.11.6",
|
||||||
"@types/koa-bodyparser": "^4.3.0",
|
"@types/koa-bodyparser": "^4.3.0",
|
||||||
"@types/koa-mount": "^4.0.0",
|
"@types/koa-mount": "^4.0.0",
|
||||||
"@types/koa__router": "^8.0.2",
|
"@types/koa__router": "^8.0.2",
|
||||||
"@types/lodash": "^4.14.158",
|
"@types/lodash": "^4.14.158",
|
||||||
"@types/node": "^14.0.23",
|
"@types/node": "^14.0.23",
|
||||||
|
"@types/react": "^16.9.53",
|
||||||
"@types/supertest": "^2.0.10",
|
"@types/supertest": "^2.0.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.6.1",
|
"@typescript-eslint/eslint-plugin": "^3.6.1",
|
||||||
"@typescript-eslint/parser": "^3.6.1",
|
"@typescript-eslint/parser": "^3.6.1",
|
||||||
@ -29,6 +33,7 @@
|
|||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
|
"father-build": "^1.18.5",
|
||||||
"jest": "^26.1.0",
|
"jest": "^26.1.0",
|
||||||
"koa": "^2.13.0",
|
"koa": "^2.13.0",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
|
@ -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" ]
|
|
@ -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}
|
|
@ -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/');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"watch": ["src", ".env", ".env.dev", "../plugin-collections/src"],
|
|
||||||
"ext": "ts",
|
|
||||||
"ignore": ["src/**/*.test.ts"],
|
|
||||||
"exec": "ts-node ./example/index.ts"
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
@ -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
16
packages/app/.editorconfig
Executable 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
16
packages/app/.env.example
Normal 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
10
packages/app/.fatherrc.ts
Executable 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
20
packages/app/.gitignore
vendored
Normal 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
8
packages/app/.prettierignore
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
**/*.md
|
||||||
|
**/*.svg
|
||||||
|
**/*.ejs
|
||||||
|
**/*.html
|
||||||
|
package.json
|
||||||
|
.umi
|
||||||
|
.umi-production
|
||||||
|
.umi-test
|
11
packages/app/.prettierrc
Executable file
11
packages/app/.prettierrc
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ".prettierrc",
|
||||||
|
"options": { "parser": "json" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
25
packages/app/.umirc.ts
Normal file
25
packages/app/.umirc.ts
Normal 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
15
packages/app/README.md
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
# NocoBase Application
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Install dependencies,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn start
|
||||||
|
```
|
0
packages/plugin-pages/src/index.ts → packages/app/mock/.gitkeep
Normal file → Executable file
0
packages/plugin-pages/src/index.ts → packages/app/mock/.gitkeep
Normal file → Executable file
5
packages/app/nodemon.json
Normal file
5
packages/app/nodemon.json
Normal 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
45
packages/app/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
197
packages/app/src/api/index.ts
Normal file
197
packages/app/src/api/index.ts
Normal 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
24
packages/app/src/app.ts
Normal 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();
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
39
packages/app/src/components/actions/Create.tsx
Normal file
39
packages/app/src/components/actions/Create.tsx
Normal 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;
|
18
packages/app/src/components/actions/Destroy.tsx
Normal file
18
packages/app/src/components/actions/Destroy.tsx
Normal 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;
|
39
packages/app/src/components/actions/Update.tsx
Normal file
39
packages/app/src/components/actions/Update.tsx
Normal 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;
|
39
packages/app/src/components/actions/index.tsx
Normal file
39
packages/app/src/components/actions/index.tsx
Normal 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;
|
197
packages/app/src/components/form.fields/array-cards/index.tsx
Normal file
197
packages/app/src/components/form.fields/array-cards/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/card/style/index'
|
234
packages/app/src/components/form.fields/array-table/index.tsx
Normal file
234
packages/app/src/components/form.fields/array-table/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/table/style/index'
|
21
packages/app/src/components/form.fields/checkbox/index.tsx
Normal file
21
packages/app/src/components/form.fields/checkbox/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/checkbox/style/index'
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/button/style/index'
|
105
packages/app/src/components/form.fields/date-picker/index.tsx
Normal file
105
packages/app/src/components/form.fields/date-picker/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/date-picker/style/index'
|
24
packages/app/src/components/form.fields/form-block/index.tsx
Normal file
24
packages/app/src/components/form.fields/form-block/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/card/style/index'
|
20
packages/app/src/components/form.fields/form-card/index.tsx
Normal file
20
packages/app/src/components/form.fields/form-card/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/card/style/index'
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/col/style/index'
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/row/style/index'
|
@ -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
|
@ -0,0 +1,2 @@
|
|||||||
|
import 'antd/lib/row/style/index'
|
||||||
|
import 'antd/lib/col/style/index'
|
@ -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
|
@ -0,0 +1,6 @@
|
|||||||
|
import { MegaLayout, FormMegaLayout } from '@formily/antd'
|
||||||
|
|
||||||
|
export {
|
||||||
|
MegaLayout,
|
||||||
|
FormMegaLayout,
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { FormSlot } from '@formily/react-schema-renderer'
|
||||||
|
|
||||||
|
export { FormSlot }
|
||||||
|
|
||||||
|
export default FormSlot
|
157
packages/app/src/components/form.fields/form-step/index.tsx
Normal file
157
packages/app/src/components/form.fields/form-step/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/steps/style/index'
|
178
packages/app/src/components/form.fields/form-tab/index.tsx
Normal file
178
packages/app/src/components/form.fields/form-tab/index.tsx
Normal 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
|
@ -0,0 +1,2 @@
|
|||||||
|
import 'antd/lib/tabs/style/index'
|
||||||
|
import 'antd/lib/badge/style/index'
|
128
packages/app/src/components/form.fields/form-text-box/index.tsx
Normal file
128
packages/app/src/components/form.fields/form-text-box/index.tsx
Normal 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
|
29
packages/app/src/components/form.fields/index.tsx
Normal file
29
packages/app/src/components/form.fields/index.tsx
Normal 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'
|
15
packages/app/src/components/form.fields/input/index.tsx
Normal file
15
packages/app/src/components/form.fields/input/index.tsx
Normal 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
|
1
packages/app/src/components/form.fields/input/style.ts
Normal file
1
packages/app/src/components/form.fields/input/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/input/style/index'
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/input-number/style/index'
|
80
packages/app/src/components/form.fields/password/index.tsx
Normal file
80
packages/app/src/components/form.fields/password/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/input/style/index'
|
19
packages/app/src/components/form.fields/radio/index.tsx
Normal file
19
packages/app/src/components/form.fields/radio/index.tsx
Normal 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
|
1
packages/app/src/components/form.fields/radio/style.ts
Normal file
1
packages/app/src/components/form.fields/radio/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/radio/style/index'
|
60
packages/app/src/components/form.fields/range/index.tsx
Normal file
60
packages/app/src/components/form.fields/range/index.tsx
Normal 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
|
1
packages/app/src/components/form.fields/range/style.ts
Normal file
1
packages/app/src/components/form.fields/range/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/slider/style/index'
|
9
packages/app/src/components/form.fields/rating/index.tsx
Normal file
9
packages/app/src/components/form.fields/rating/index.tsx
Normal 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
|
1
packages/app/src/components/form.fields/rating/style.ts
Normal file
1
packages/app/src/components/form.fields/rating/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/rate/style/index'
|
41
packages/app/src/components/form.fields/registry.ts
Normal file
41
packages/app/src/components/form.fields/registry.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
13
packages/app/src/components/form.fields/select/index.tsx
Normal file
13
packages/app/src/components/form.fields/select/index.tsx
Normal 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
|
1
packages/app/src/components/form.fields/select/style.ts
Normal file
1
packages/app/src/components/form.fields/select/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/select/style/index'
|
126
packages/app/src/components/form.fields/shared.tsx
Normal file
126
packages/app/src/components/form.fields/shared.tsx
Normal 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 }
|
10
packages/app/src/components/form.fields/switch/index.tsx
Normal file
10
packages/app/src/components/form.fields/switch/index.tsx
Normal 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;
|
1
packages/app/src/components/form.fields/switch/style.ts
Normal file
1
packages/app/src/components/form.fields/switch/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/switch/style/index'
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/button/style/index'
|
@ -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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/time-picker/style/index'
|
10
packages/app/src/components/form.fields/transfer/index.tsx
Normal file
10
packages/app/src/components/form.fields/transfer/index.tsx
Normal 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
|
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/transfer/style/index'
|
103
packages/app/src/components/form.fields/types.ts
Normal file
103
packages/app/src/components/form.fields/types.ts
Normal 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[]
|
||||||
|
}
|
292
packages/app/src/components/form.fields/upload/index.tsx
Normal file
292
packages/app/src/components/form.fields/upload/index.tsx
Normal 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
|
1
packages/app/src/components/form.fields/upload/style.ts
Normal file
1
packages/app/src/components/form.fields/upload/style.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'antd/lib/upload/style/index'
|
@ -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;
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
@ -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
Loading…
Reference in New Issue
Block a user