mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-29 18:58:26 +08:00
发布核心框架 (#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',
|
||||
'resourcer',
|
||||
'actions',
|
||||
'client',
|
||||
];
|
||||
const tailPkgs = [];
|
||||
const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
|
||||
(pkg) => {
|
||||
return pkg !== 'father-build' && pkg.charAt(0) !== '.' && !headPkgs.includes(pkg) && !tailPkgs.includes(pkg)
|
||||
return !['father-build', 'app'].includes(pkg) && pkg.charAt(0) !== '.' && !headPkgs.includes(pkg) && !tailPkgs.includes(pkg)
|
||||
},
|
||||
);
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
node_modules/
|
||||
lib/
|
||||
.env
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
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:
|
||||
- node-network
|
||||
environment:
|
||||
- VERDACCIO_PORT=4873
|
||||
- VERDACCIO_PORT=${VERDACCIO_PORT}
|
||||
restart: always
|
||||
ports:
|
||||
- "4873:4873"
|
||||
- "${VERDACCIO_PORT}:4873"
|
||||
# volumes:
|
||||
# - "./verdaccio/storage:/verdaccio/storage"
|
||||
# - "./verdaccio/config:/verdaccio/conf"
|
||||
@ -19,20 +20,20 @@ services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
environment:
|
||||
MYSQL_DATABASE: "test"
|
||||
MYSQL_USER: "test"
|
||||
MYSQL_PASSWORD: "test"
|
||||
MYSQL_ROOT_PASSWORD: "test"
|
||||
MYSQL_DATABASE: ${DB_DATABASE}
|
||||
MYSQL_USER: ${DB_USER}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
|
||||
restart: always
|
||||
ports:
|
||||
- "43306:3306"
|
||||
- "${DB_MYSQL_PORT}:3306"
|
||||
postgres:
|
||||
image: postgres:10
|
||||
restart: always
|
||||
ports:
|
||||
- "45432:5432"
|
||||
- "${DB_POSTGRES_PORT}:5432"
|
||||
command: postgres -c wal_level=logical
|
||||
environment:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: test
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_DB: ${DB_DATABASE}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
0
jest.config.js
Executable file → Normal file
0
jest.config.js
Executable file → Normal file
@ -10,7 +10,7 @@
|
||||
]
|
||||
},
|
||||
"publish": {
|
||||
"allowBranch": "master",
|
||||
"allowBranch": ["master", "develop"],
|
||||
"ignoreChanges": [
|
||||
"*.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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "cd packages/app && npm start",
|
||||
"start-server": "yarn nodemon",
|
||||
"bootstrap": "lerna bootstrap --no-ci",
|
||||
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
|
||||
"build-father-build": "cd packages/father-build && npm run build",
|
||||
@ -14,11 +16,13 @@
|
||||
"devDependencies": {
|
||||
"@koa/router": "^9.3.1",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/koa": "^2.11.6",
|
||||
"@types/koa-bodyparser": "^4.3.0",
|
||||
"@types/koa-mount": "^4.0.0",
|
||||
"@types/koa__router": "^8.0.2",
|
||||
"@types/lodash": "^4.14.158",
|
||||
"@types/node": "^14.0.23",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^3.6.1",
|
||||
"@typescript-eslint/parser": "^3.6.1",
|
||||
@ -29,6 +33,7 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"father-build": "^1.18.5",
|
||||
"jest": "^26.1.0",
|
||||
"koa": "^2.13.0",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
|
@ -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