发布核心框架 (#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