├── .gitignore ├── .npmignore ├── docs ├── demo │ ├── static │ │ └── js │ │ │ ├── _vendor_.6c270d91.js │ │ │ └── runtime.35902342.js │ └── index.html ├── static │ └── css │ │ ├── vendor.less │ │ └── modules │ │ ├── color │ │ └── colors.less │ │ └── antd-custom.less ├── app │ ├── index.js │ └── modules │ │ ├── App.js │ │ └── config.js ├── .gitignore ├── README.md ├── .tern-project ├── public │ └── index.html ├── scripts │ ├── config │ │ ├── polyfills.js │ │ ├── tslintrc.json │ │ ├── env.js │ │ ├── checkMissDependencies.js │ │ ├── paths.js │ │ ├── eslintrc.js │ │ ├── helper.js │ │ ├── webpack.config.dev.js │ │ └── webpack.config.prod.js │ ├── start.js │ ├── i18n.js │ ├── cdn.js │ └── build.js ├── .tern-webpack-config.js ├── global.d.ts ├── tsconfig.json └── package.json ├── src ├── index.js ├── FormItem.test.tsx └── FormItem.js ├── dist ├── index.cjs.js ├── index.esm.js ├── index.umd.js ├── react-antd-formutil.esm.production.js ├── react-antd-formutil.cjs.production.js └── react-antd-formutil.umd.production.js ├── npm ├── index.cjs.js ├── index.esm.js └── index.umd.js ├── babel.config.json ├── jest ├── setupTests.ts ├── cssTransform.js ├── fileTransform.js ├── test.js └── jest.config.js ├── tsconfig.json ├── LICENSE ├── index.d.ts ├── eslint.config.js ├── package.json ├── rollup.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | .DS_Store 3 | node_modules 4 | /.git-tsconfig.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | src/ 3 | npm/ 4 | rollup.config.* 5 | babel.config.json 6 | *.un~ 7 | -------------------------------------------------------------------------------- /docs/demo/static/js/_vendor_.6c270d91.js: -------------------------------------------------------------------------------- 1 | /*! @author qiqiboy */ 2 | (this.webpackJsonp=this.webpackJsonp||[]).push([[2],[],[[0,0,1]]]); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | //export react-formutil 2 | export * from 'react-formutil'; 3 | 4 | export { default as FormItem, setErrorLevel } from './FormItem'; 5 | -------------------------------------------------------------------------------- /docs/static/css/vendor.less: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.less'; 2 | 3 | .app { 4 | padding: 20px; 5 | > h2 { 6 | text-align: center; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from 'modules/App'; 5 | 6 | render(, document.getElementById('wrap')); 7 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | .*.swp 3 | .*.swo 4 | *.rdb 5 | .DS_Store 6 | node_modules 7 | bower_components 8 | /npm-debug.log 9 | /dump.rdb 10 | Thumbs.db 11 | 12 | # git pre-commit tsc lint 13 | .git-tsconfig.json -------------------------------------------------------------------------------- /dist/index.cjs.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-antd-formutil.cjs.production.js'); 3 | } else { 4 | module.exports = require('./react-antd-formutil.cjs.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /dist/index.esm.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-antd-formutil.esm.production.js'); 3 | } else { 4 | module.exports = require('./react-antd-formutil.esm.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /npm/index.cjs.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-antd-formutil.cjs.production.js'); 3 | } else { 4 | module.exports = require('./react-antd-formutil.cjs.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /npm/index.esm.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-antd-formutil.esm.production.js'); 3 | } else { 4 | module.exports = require('./react-antd-formutil.esm.development.js'); 5 | } 6 | -------------------------------------------------------------------------------- /dist/index.umd.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-antd-formutil.umd.production.js'); 3 | } else { 4 | module.exports = require('./react-antd-formutil.umd.development.js'); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /npm/index.umd.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./react-antd-formutil.umd.production.js'); 3 | } else { 4 | module.exports = require('./react-antd-formutil.umd.development.js'); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | "react-hot-loader/babel", 5 | [ 6 | "@babel/plugin-proposal-decorators", 7 | { 8 | "legacy": true 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 支持React + ES6的标准开发环境 2 | ================ 3 | 4 | ### 开始开发 5 | $ npm start 6 | 7 | 该命令会在本地通过`webpack-dev-server`创建一个本地开发服务,支持hot reload. 8 | 9 | ### 部署测试 10 | $ npm run build:dev 11 | 12 | ### 上线 13 | $ npm run pack 14 | 15 | -------------------------------------------------------------------------------- /jest/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /docs/.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": [ 3 | "browser", 4 | "jquery", 5 | "ecma5", 6 | "ecma6", 7 | "underscore", 8 | "angular" 9 | ], 10 | "plugins": { 11 | "angular": {}, 12 | "node": {}, 13 | "es_modules": {}, 14 | "commonjs": {}, 15 | "webpack": { 16 | "configPath": "./.tern-webpack-config.js" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/app/modules/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import MyForm from './MyForm'; 3 | 4 | class App extends Component { 5 | render() { 6 | return ( 7 |
8 |

react-antd-formutil

9 | 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-antd-formutil 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/scripts/config/polyfills.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | // classList 3 | require('classlist-polyfill'); 4 | 5 | // requestAnimationFrame 6 | require('raf-dom').polyfill(); 7 | 8 | // ECMAScript 9 | require('core-js/features/object'); 10 | require('core-js/features/promise'); 11 | require('core-js/features/map'); 12 | require('core-js/features/set'); 13 | require('core-js/features/array'); 14 | require('core-js/features/string'); 15 | require('core-js/features/number'); 16 | require('core-js/features/symbol'); 17 | 18 | // require('core-js/features/url'); 19 | // require('core-js/features/url-search-params'); 20 | -------------------------------------------------------------------------------- /docs/scripts/config/tslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "trailing-comma": false, 4 | "quotemark": false, 5 | "no-console": false, 6 | "semicolon": false, 7 | "no-namespace": [true, "allow-declarations"], 8 | "max-classes-per-file": false, 9 | "ordered-imports": false, 10 | "member-ordering": false, 11 | "object-literal-sort-keys": false, 12 | "member-access": false, 13 | "arrow-parens": [true, "ban-single-arg-parens"], 14 | "variable-name": false, 15 | "prefer-const": false, 16 | "jsx-boolean-value": false, 17 | "only-arrow-functions": false, 18 | "jsx-no-lambda": false, 19 | "interface-name": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/.tern-webpack-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin'); 3 | 4 | module.exports = { 5 | resolve: { 6 | modules: [path.resolve(__dirname, 'node_modules'), path.resolve(__dirname)], 7 | alias: { 8 | utils: path.resolve(__dirname, 'app/utils'), 9 | components: path.resolve(__dirname, 'app/components'), 10 | modules: path.resolve(__dirname, 'app/modules') 11 | }, 12 | plugins: [ 13 | new DirectoryNamedWebpackPlugin({ 14 | honorIndex: true, 15 | exclude: /node_modules|libs/ 16 | }) 17 | ] 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /docs/global.d.ts: -------------------------------------------------------------------------------- 1 | // 声明资源文件类型 2 | declare module '*.svg'; 3 | declare module '*.png'; 4 | declare module '*.jpg'; 5 | declare module '*.jpeg'; 6 | declare module '*.gif'; 7 | declare module '*.bmp'; 8 | declare module '*.tiff'; 9 | declare module '*.html'; 10 | declare module '*.txt'; 11 | declare module '*.htm'; 12 | 13 | interface Window { 14 | __: I18nFunc; 15 | } 16 | 17 | /** 18 | * create HOC(Higher Order Component) 19 | * 20 | */ 21 | type HOC = ( 22 | Component: React.ComponentType 23 | ) => React.ComponentType>; 24 | 25 | /** 26 | * i18n 27 | */ 28 | type I18nFunc = (key: string) => string; 29 | 30 | declare const __: I18nFunc; 31 | 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM"], 6 | "jsx": "react", 7 | "resolveJsonModule": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "allowUnreachableCode": false, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": false, 17 | "importHelpers": true, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "skipLibCheck": true, 21 | "noUnusedLocals": true, 22 | "importHelpers": true, 23 | "declaration": true, 24 | "outDir": "dist" 25 | }, 26 | "include": ["*.d.ts", "src"], 27 | "exclude": ["node_modules", "dist", "build"] 28 | } 29 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "allowJs": true, 5 | "checkJs": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "jsx": "react", 9 | "resolveJsonModule": true, 10 | "experimentalDecorators": true, 11 | "allowSyntheticDefaultImports": true, 12 | "allowUnreachableCode": false, 13 | "moduleResolution": "node", 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "skipLibCheck": true, 17 | "noEmit": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": false, 21 | "importHelpers": true, 22 | "strictNullChecks": true, 23 | "suppressImplicitAnyIndexErrors": true, 24 | "noUnusedLocals": false, 25 | "baseUrl": ".", 26 | "paths": { 27 | "*": ["*", "app/*"] 28 | } 29 | }, 30 | "exclude": ["scripts", "node_modules", "dist", "build", "buildDev"] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 qiqiboy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-antd-formutil@>=1.0.0 2 | // Project: react-antd-formutil 3 | // Definitions by: qiqiboy 4 | 5 | import { FormItemProps } from 'antd/es/form/FormItem'; 6 | // If the next line occur error, please install rc-field-form or add 'skipLibCheck' to tsconfig.json for disable t.ds check. 7 | import { FieldProps } from 'rc-field-form/es/Field'; 8 | import React from 'react'; 9 | import { BaseEasyFieldComponentProps, $FieldHandler, Omit, OtherKeys } from 'react-formutil'; 10 | 11 | export * from 'react-formutil'; 12 | 13 | export type ErrorLevel = 0 | 1 | 2 | 'off'; 14 | 15 | // Compatible with antd@3 & antd@4 16 | type FilterPropNames = any extends FieldProps ? '__NO__' : keyof FieldProps; 17 | 18 | export interface FormItemComponentProps 19 | extends BaseEasyFieldComponentProps, 20 | Omit { 21 | itemProps?: Omit; 22 | errorLevel?: ErrorLevel; 23 | noStyle?: boolean; 24 | children: React.ReactElement | (($fieldHandler: Partial<$FieldHandler> & OtherKeys) => React.ReactNode); 25 | } 26 | 27 | export class FormItem extends React.Component< 28 | FormItemComponentProps & OtherKeys 29 | > {} 30 | 31 | export function setErrorLevel(errorLevel: ErrorLevel): void; 32 | -------------------------------------------------------------------------------- /docs/demo/static/js/runtime.35902342.js: -------------------------------------------------------------------------------- 1 | /*! @author qiqiboy */!function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c { 10 | throw err; 11 | }); 12 | 13 | const jest = require('jest'); 14 | const execSync = require('child_process').execSync; 15 | let argv = process.argv.slice(2); 16 | 17 | argv.push('-c', 'jest/jest.config.js'); 18 | 19 | function isInGitRepository() { 20 | try { 21 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 22 | 23 | return true; 24 | } catch (e) { 25 | return false; 26 | } 27 | } 28 | 29 | function isInMercurialRepository() { 30 | try { 31 | execSync('hg --cwd . root', { stdio: 'ignore' }); 32 | 33 | return true; 34 | } catch (e) { 35 | return false; 36 | } 37 | } 38 | 39 | // Watch unless on CI or explicitly running all tests 40 | if (!process.env.CI && argv.indexOf('--watchAll') === -1 && argv.indexOf('--watchAll=false') === -1) { 41 | // https://github.com/facebook/create-react-app/issues/5210 42 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 43 | 44 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 45 | } 46 | 47 | jest.run(argv); 48 | -------------------------------------------------------------------------------- /docs/demo/index.html: -------------------------------------------------------------------------------- 1 | react-antd-formutil
-------------------------------------------------------------------------------- /docs/scripts/config/env.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const paths = require('./paths'); 5 | const pkg = require(paths.appPackageJson); 6 | 7 | delete require.cache[require.resolve('./paths')]; 8 | 9 | const NODE_ENV = process.env.NODE_ENV; 10 | 11 | let dotenvFiles = [ 12 | `${paths.dotenv}.${NODE_ENV}.local`, 13 | `${paths.dotenv}.${NODE_ENV}`, 14 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 15 | paths.dotenv 16 | ].filter(Boolean); 17 | 18 | dotenvFiles.forEach(dotenvFile => { 19 | if (fs.existsSync(dotenvFile)) { 20 | require('dotenv-expand')( 21 | require('dotenv').config({ 22 | path: dotenvFile 23 | }) 24 | ); 25 | } 26 | }); 27 | 28 | const appDirectory = fs.realpathSync(process.cwd()); 29 | 30 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 31 | .split(path.delimiter) 32 | .filter(folder => folder && !path.isAbsolute(folder)) 33 | .map(folder => path.resolve(appDirectory, folder)) 34 | .join(path.delimiter); 35 | 36 | if (!('BASE_NAME' in process.env) && 'basename' in pkg) { 37 | process.env.BASE_NAME = pkg.basename; 38 | } 39 | 40 | const REACT_APP = /^(REACT_APP_|TIGER_)/i; 41 | const whitelists = ['BASE_NAME']; 42 | 43 | function getClientEnvironment(publicUrl) { 44 | const raw = Object.keys(process.env) 45 | .filter(key => REACT_APP.test(key) || whitelists.includes(key)) 46 | .reduce( 47 | (env, key) => { 48 | env[key] = process.env[key]; 49 | return env; 50 | }, 51 | { 52 | NODE_ENV: process.env.NODE_ENV || 'development', 53 | PUBLIC_URL: publicUrl 54 | } 55 | ); 56 | // Stringify all values so we can feed into Webpack DefinePlugin 57 | const stringified = { 58 | 'process.env': Object.keys(raw).reduce((env, key) => { 59 | env[key] = JSON.stringify(raw[key]); 60 | return env; 61 | }, {}) 62 | }; 63 | 64 | return { raw, stringified }; 65 | } 66 | 67 | module.exports = getClientEnvironment; 68 | -------------------------------------------------------------------------------- /docs/scripts/config/checkMissDependencies.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const checkDependencies = require('check-dependencies'); 3 | const inquirer = require('react-dev-utils/inquirer'); 4 | const spawn = require('cross-spawn'); 5 | const chalk = require('chalk'); 6 | const paths = require('./paths'); 7 | 8 | async function checkMissDeps(spinner) { 9 | const result = await checkDependencies({ 10 | packageDir: paths.root 11 | }); 12 | 13 | if (result.status !== 0) { 14 | spinner.stop(); 15 | 16 | // 输出错误信息 17 | result.error.forEach(function(err) { 18 | console.log(err); 19 | }); 20 | 21 | console.log(); 22 | 23 | const { reInstall } = await inquirer.prompt([ 24 | { 25 | name: 'reInstall', 26 | type: 'confirm', 27 | message: 28 | '你当前安装的依赖版本和要求的不一致,是否要重新安装所有依赖?\n' + 29 | chalk.dim('重新运行 npm install 安装所有依赖项.'), 30 | default: true 31 | } 32 | ]); 33 | 34 | console.log(); 35 | 36 | if (reInstall) { 37 | await new Promise((resolve, reject) => { 38 | install(function(code, command, args) { 39 | if (code !== 0) { 40 | spinner.fail('`' + command + ' ' + args.join(' ') + '` 运行失败'); 41 | 42 | reject(); 43 | } else { 44 | resolve(); 45 | } 46 | }); 47 | }); 48 | 49 | spinner.succeed(chalk.green('项目依赖已更新')); 50 | } else { 51 | spinner.warn(chalk.yellow('你需要按照下面命令操作后才能继续:')); 52 | console.log(); 53 | 54 | console.log(chalk.green(' ' + paths.npmCommander + ' install')); 55 | 56 | return Promise.reject(); 57 | } 58 | } 59 | } 60 | 61 | function install(callback) { 62 | let command = paths.npmCommander; 63 | let args = ['install']; 64 | 65 | var child = spawn(command, args, { 66 | stdio: 'inherit' 67 | }); 68 | 69 | child.on('close', function(code) { 70 | callback(code, command, args); 71 | }); 72 | } 73 | 74 | module.exports = checkMissDeps; 75 | -------------------------------------------------------------------------------- /jest/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | const fs = require('fs-extra'); 4 | 5 | module.exports = { 6 | projects: [ 7 | { 8 | displayName: 'lint', 9 | runner: 'eslint', 10 | rootDir: process.cwd(), 11 | roots: ['/src', fs.existsSync(process.cwd() + '/tests') && '/tests'].filter(Boolean), 12 | testMatch: [ 13 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 14 | '/{src,tests}/**/*.{spec,test}.{js,jsx,ts,tsx}' 15 | ] 16 | }, 17 | { 18 | displayName: 'test', 19 | rootDir: process.cwd(), 20 | roots: ['/src', fs.existsSync(process.cwd() + '/tests') && '/tests'].filter(Boolean), 21 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], 22 | setupFiles: [], 23 | setupFilesAfterEnv: ['/jest/setupTests.ts'], 24 | testMatch: [ 25 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 26 | '/{src,tests}/**/*.{spec,test}.{js,jsx,ts,tsx}' 27 | ], 28 | testEnvironment: 'jest-environment-jsdom-fourteen', 29 | transform: { 30 | '^.+\\.(js|jsx|ts|tsx)$': '/node_modules/babel-jest', 31 | '^.+\\.(css|less|sass|scss$)': '/jest/cssTransform.js', 32 | '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|json)$)': '/jest/fileTransform.js' 33 | }, 34 | transformIgnorePatterns: [ 35 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', 36 | '^.+\\.module\\.(css|sass|scss|less)$' 37 | ], 38 | modulePaths: [], 39 | moduleNameMapper: { 40 | '^react-native$': 'react-native-web', 41 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy' 42 | }, 43 | moduleFileExtensions: ['web.js', 'js', 'web.ts', 'ts', 'web.tsx', 'tsx', 'json', 'web.jsx', 'jsx', 'node'], 44 | verbose: true, 45 | // resetMocks: true, 46 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'] 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /docs/app/modules/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select } from 'antd'; 3 | 4 | export const formItemLayout = { 5 | labelCol: { 6 | xs: { span: 24 }, 7 | sm: { span: 8 } 8 | }, 9 | wrapperCol: { 10 | xs: { span: 24 }, 11 | sm: { span: 16 } 12 | } 13 | }; 14 | 15 | export const hobbiesOptions = [ 16 | { label: 'Apple', value: 'Apple' }, 17 | { label: 'Pear', value: 'Pear' }, 18 | { label: 'Orange', value: 'Orange' } 19 | ]; 20 | 21 | export const selectOption = Array(36) 22 | .fill('') 23 | .map((n, i) => {i.toString(36) + i}) 24 | .slice(10); 25 | 26 | export const sliderMarks = { 27 | 0: '0°C', 28 | 26: '26°C', 29 | 37: '37°C', 30 | 100: { 31 | style: { 32 | color: '#f50' 33 | }, 34 | label: 100°C 35 | } 36 | }; 37 | 38 | export const cascaderOptions = [ 39 | { 40 | value: 'zhejiang', 41 | label: 'Zhejiang', 42 | children: [ 43 | { 44 | value: 'hangzhou', 45 | label: 'Hangzhou', 46 | children: [ 47 | { 48 | value: 'xihu', 49 | label: 'West Lake' 50 | } 51 | ] 52 | } 53 | ] 54 | }, 55 | { 56 | value: 'jiangsu', 57 | label: 'Jiangsu', 58 | children: [ 59 | { 60 | value: 'nanjing', 61 | label: 'Nanjing', 62 | children: [ 63 | { 64 | value: 'zhonghuamen', 65 | label: 'Zhong Hua Men' 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | ]; 72 | 73 | export const mentionOptions = ['afc163', 'benjycui', 'yiminghe', 'RaoHai', '中文', 'にほんご']; 74 | 75 | export const uplodConfig = { 76 | name: 'file', 77 | action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', 78 | headers: { 79 | authorization: 'authorization-text' 80 | } 81 | }; 82 | 83 | export const transferData = []; 84 | 85 | for (let i = 0; i < 20; i++) { 86 | transferData.push({ 87 | key: i.toString(), 88 | title: `content${i + 1}`, 89 | description: `description of content${i + 1}` 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /docs/scripts/config/paths.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const glob = require('glob'); 5 | const execSync = require('child_process').execSync; 6 | const isDev = process.env.NODE_ENV === 'development'; 7 | const lodash = require('lodash'); 8 | 9 | // Make sure any symlinks in the project folder are resolved: 10 | // https://github.com/facebookincubator/create-react-app/issues/637 11 | const appDirectory = fs.realpathSync(process.cwd()); 12 | const pkg = require(resolveApp('package.json')); 13 | 14 | function resolveApp(relativePath) { 15 | return path.resolve(appDirectory, relativePath); 16 | } 17 | 18 | const nodePaths = (process.env.NODE_PATH || '') 19 | .split(path.delimiter) 20 | .filter(Boolean) 21 | .map(resolveApp); 22 | 23 | const entries = {}; 24 | 25 | glob.sync(resolveApp('app/!(_)*.{j,t}s?(x)')).forEach(function(file) { 26 | const basename = path.basename(file).replace(/\.[jt]sx?$/, ''); 27 | 28 | entries[basename] = file; 29 | }); 30 | 31 | const alias = Object.assign( 32 | { 33 | components: resolveApp('app/components'), 34 | modules: resolveApp('app/modules'), 35 | utils: resolveApp('app/utils'), 36 | stores: resolveApp('app/stores'), 37 | types: resolveApp('app/types'), 38 | hooks: resolveApp('app/hooks') 39 | }, 40 | lodash.mapValues(pkg.alias, function(relativePath) { 41 | if (fs.pathExistsSync(resolveApp(relativePath))) { 42 | return resolveApp(relativePath); 43 | } 44 | 45 | return relativePath; 46 | }) 47 | ); 48 | 49 | // config after eject: we're in ./config/ 50 | module.exports = { 51 | dotenv: resolveApp('.env'), 52 | root: resolveApp(''), 53 | appBuild: resolveApp(process.env.BUILD_DIR || 'demo'), 54 | appPublic: resolveApp('public'), 55 | appHtml: resolveApp('public/index.html'), 56 | appIndexJs: Object.values(entries)[0] || resolveApp('app/index.js'), 57 | appPackageJson: resolveApp('package.json'), 58 | appSrc: resolveApp('app'), 59 | formutilSrc: resolveApp('app/../../src'), 60 | appTsConfig: resolveApp('tsconfig.json'), 61 | staticSrc: resolveApp('static'), 62 | locals: resolveApp('locals'), 63 | proxySetup: resolveApp('setupProxy.js'), 64 | appNodeModules: resolveApp('node_modules'), 65 | ownNodeModules: resolveApp('node_modules'), 66 | nodePaths: nodePaths, 67 | alias: alias, 68 | entries: entries, 69 | pageEntries: glob.sync(resolveApp('public/!(_)*.html')).map(function(file) { 70 | return path.basename(file, '.html'); 71 | }), 72 | moduleFileExtensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx'], 73 | // 一些命令检测 74 | serve: hasInstall('serve'), 75 | npmCommander: ['tnpm', 'cnpm', 'npm'].find(hasInstall) 76 | }; 77 | 78 | function hasInstall(command) { 79 | try { 80 | execSync(command + ' --version', { 81 | stdio: 'ignore' 82 | }); 83 | 84 | return true; 85 | } catch (e) { 86 | return false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | /** 4 | * 0: off 5 | * 1: warn 6 | * 2: error 7 | */ 8 | module.exports = { 9 | overrides: [ 10 | { 11 | files: ['**/__tests__/**/*', '**/*.{spec,test}.*'], 12 | rules: { 13 | 'jest/consistent-test-it': [1, { fn: 'test' }], 14 | 'jest/expect-expect': 1, 15 | 'jest/no-deprecated-functions': 2 16 | } 17 | } 18 | ], 19 | settings: { 20 | 'import/core-modules': [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})] 21 | }, 22 | rules: { 23 | 'react/react-in-jsx-scope': 2, 24 | 'react/no-unsafe': [2, { checkAliases: true }], 25 | 'react/no-deprecated': 2, 26 | 'import/no-anonymous-default-export': [ 27 | 2, 28 | { 29 | allowArray: true, 30 | allowArrowFunction: false, 31 | allowAnonymousClass: false, 32 | allowAnonymousFunction: false, 33 | allowCallExpression: true, // The true value here is for backward compatibility 34 | allowLiteral: true, 35 | allowObject: true 36 | } 37 | ], 38 | 'import/no-duplicates': 1, 39 | 'import/order': [ 40 | 1, 41 | { 42 | groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'unknown'] 43 | } 44 | ], 45 | 'import/no-useless-path-segments': [ 46 | 1, 47 | { 48 | noUselessIndex: true 49 | } 50 | ], 51 | 'lines-between-class-members': [1, 'always', { exceptAfterSingleLine: true }], 52 | 'padding-line-between-statements': [ 53 | 1, 54 | { 55 | blankLine: 'always', 56 | prev: [ 57 | 'multiline-block-like', 58 | 'multiline-expression', 59 | 'const', 60 | 'let', 61 | 'var', 62 | 'cjs-import', 63 | 'import', 64 | 'export', 65 | 'cjs-export', 66 | 'class', 67 | 'throw', 68 | 'directive' 69 | ], 70 | next: '*' 71 | }, 72 | { 73 | blankLine: 'always', 74 | prev: '*', 75 | next: [ 76 | 'multiline-block-like', 77 | 'multiline-expression', 78 | 'const', 79 | 'let', 80 | 'var', 81 | 'cjs-import', 82 | 'import', 83 | 'export', 84 | 'cjs-export', 85 | 'class', 86 | 'throw', 87 | 'return' 88 | ] 89 | }, 90 | { blankLine: 'any', prev: ['cjs-import', 'import'], next: ['cjs-import', 'import'] }, 91 | { blankLine: 'any', prev: ['export', 'cjs-export'], next: ['export', 'cjs-export'] }, 92 | { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] } 93 | ] 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /docs/scripts/start.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | process.env.BABEL_ENV = 'development'; 3 | process.env.NODE_ENV = 'development'; 4 | 5 | process.on('unhandledRejection', err => { 6 | throw err; 7 | }); 8 | 9 | require('./config/env'); 10 | 11 | const chalk = require('chalk'); 12 | const webpack = require('webpack'); 13 | const ora = require('ora'); 14 | const WebpackDevServer = require('webpack-dev-server'); 15 | const { choosePort, prepareProxy, createCompiler, createDevServerConfig } = require('./config/helper'); 16 | const { prepareUrls } = require('react-dev-utils/WebpackDevServerUtils'); 17 | const openBrowser = require('react-dev-utils/openBrowser'); 18 | const clearConsole = require('react-dev-utils/clearConsole'); 19 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 20 | const checkMissDependencies = require('./config/checkMissDependencies'); 21 | const config = require('./config/webpack.config.dev'); 22 | const paths = require('./config/paths'); 23 | const { ensureLocals } = require('./i18n'); 24 | const pkg = require(paths.appPackageJson); 25 | 26 | const spinner = ora('webpack启动中...').start(); 27 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 28 | const HOST = process.env.HOST || '0.0.0.0'; 29 | const isInteractive = process.stdout.isTTY; 30 | 31 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 32 | console.log(); 33 | process.exit(1); 34 | } 35 | 36 | ensureLocals(); 37 | 38 | checkMissDependencies(spinner) 39 | .then(() => { 40 | return choosePort(HOST, DEFAULT_PORT, spinner).then(port => { 41 | if (port === null) { 42 | console.log(); 43 | 44 | spinner.fail( 45 | '请关闭占用 ' + 46 | chalk.bold(chalk.yellow(DEFAULT_PORT)) + 47 | ' 端口的程序后再运行;或者指定一个新的端口:' + 48 | chalk.bold(chalk.yellow('PORT=4000 npm start')) 49 | ); 50 | 51 | console.log(); 52 | process.exit(0); 53 | } else { 54 | spinner.start(); 55 | 56 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 57 | const appName = pkg.name; 58 | const urls = prepareUrls(protocol, HOST, port); 59 | const devSocket = { 60 | warnings: warnings => devServer.sockWrite(devServer.sockets, 'warnings', warnings), 61 | errors: errors => devServer.sockWrite(devServer.sockets, 'errors', errors) 62 | }; 63 | const compiler = createCompiler(webpack, config, appName, urls, devSocket, spinner); 64 | const proxyConfig = prepareProxy(process.env.PROXY || pkg.proxy, paths.appPublic); 65 | const serverConfig = createDevServerConfig(proxyConfig, urls.lanUrlForConfig); 66 | const devServer = new WebpackDevServer(compiler, serverConfig); 67 | 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | 74 | if (isInteractive) { 75 | clearConsole(); 76 | } 77 | 78 | spinner.text = chalk.cyan('正在启动测试服务器...'); 79 | openBrowser(urls.localUrlForBrowser); 80 | }); 81 | 82 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 83 | process.on(sig, function() { 84 | devServer.close(); 85 | spinner.stop(); 86 | process.exit(); 87 | }); 88 | }); 89 | } 90 | }); 91 | }) 92 | .catch(function(err) { 93 | if (err) { 94 | console.log(err.message || err); 95 | console.log(); 96 | } 97 | 98 | process.kill(process.pid, 'SIGINT'); 99 | }); 100 | -------------------------------------------------------------------------------- /docs/scripts/i18n.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs-extra'); 3 | const glob = require('glob'); 4 | const Parser = require('i18next-scanner').Parser; 5 | const xlsx = require('node-xlsx'); 6 | const paths = require('./config/paths'); 7 | const path = require('path'); 8 | const chalk = require('chalk'); 9 | const ora = require('ora'); 10 | const lodash = require('lodash'); 11 | const pkg = require(paths.appPackageJson); 12 | 13 | const spinner = ora(); 14 | 15 | const xlsxOptions = { 16 | '!cols': [{ wch: 50 }, { wch: 50 }] 17 | }; 18 | 19 | const terminalArg = process.argv[2]; 20 | 21 | if (terminalArg === '--scan') { 22 | ensureLocalsConfig(); 23 | scanner(); 24 | } else if (terminalArg === '--read') { 25 | ensureLocalsConfig(); 26 | reader(); 27 | } 28 | 29 | function ensureLocalsConfig() { 30 | if (Array.isArray(pkg.locals) === false) { 31 | spinner.fail(chalk.red('未在 package.json 中找到相关语言包配置!')); 32 | 33 | spinner.warn( 34 | chalk.yellow('需要 package.json 中添加 { "locals": ["zh_CN", "en_US"] } 配置后,才能运行该命令!') 35 | ); 36 | 37 | process.exit(0); 38 | } 39 | } 40 | 41 | /** 42 | * @description 43 | * 扫描源代码文件,匹配需要翻译的文案,并输出excel文件待翻译 44 | */ 45 | function scanner() { 46 | const i18nParser = new Parser({ 47 | lngs: pkg.locals, 48 | nsSeparator: false, 49 | keySeparator: false, 50 | pluralSeparator: false, 51 | contextSeparator: false 52 | }); 53 | 54 | fs.ensureDirSync(path.join(paths.locals, 'xlsx')); 55 | 56 | glob.sync(paths.appSrc + '/**/*.{js,jsx,ts,tsx}').forEach(file => { 57 | const content = fs.readFileSync(file); 58 | 59 | i18nParser.parseFuncFromString(content, { list: ['__', 'i18n.__', 'window.__'] }, key => { 60 | if (key) { 61 | i18nParser.set(key, key); 62 | } 63 | }); 64 | }); 65 | 66 | const i18nJson = i18nParser.get(); 67 | 68 | Object.keys(i18nJson).forEach(key => { 69 | const jsonDestination = path.join(paths.locals, key + '.json'); 70 | const excelDestination = path.join(paths.locals, 'xlsx', key + '.xlsx'); 71 | 72 | const translation = i18nJson[key].translation; 73 | const existConfig = fs.existsSync(jsonDestination) ? JSON.parse(fs.readFileSync(jsonDestination)) : {}; 74 | const newConfig = lodash.pickBy(existConfig, (value, key) => key in translation); 75 | 76 | lodash.each(translation, (value, key) => { 77 | if (!(key in newConfig)) { 78 | newConfig[key] = value; 79 | } 80 | }); 81 | 82 | fs.outputFile(path.join(paths.locals, key + '.json'), JSON.stringify(newConfig, '\n', 2)); 83 | 84 | convertJson2Excel(newConfig, key, path.join(excelDestination)); 85 | 86 | spinner.succeed('输出 ' + chalk.bold(chalk.green(key)) + ' 到 ' + chalk.cyan(excelDestination)); 87 | }); 88 | 89 | console.log(); 90 | spinner.warn(chalk.yellow('你可以将生成的excel文件进行翻译后,放回原处。然后运行:')); 91 | console.log(chalk.green(' npm run i18n-read')); 92 | } 93 | 94 | /** 95 | * @description 96 | * 读取excel文件,并转换为json语言包 97 | */ 98 | function reader() { 99 | glob.sync(path.join(paths.locals, 'xlsx', '!(~$)*.xlsx')).forEach(file => { 100 | const lang = path.basename(file, '.xlsx'); 101 | const jsonDestination = path.join(paths.locals, lang + '.json'); 102 | 103 | convertExcel2Json(file, lang, jsonDestination); 104 | 105 | spinner.succeed('输出 ' + chalk.bold(chalk.green(lang)) + ' 到 ' + chalk.cyan(jsonDestination)); 106 | }); 107 | 108 | console.log(); 109 | spinner.succeed(chalk.green('语言包转换成功!')); 110 | } 111 | 112 | function convertJson2Excel(jsonContent, lang, destination) { 113 | const sheets = [[pkg.name + ' v' + pkg.version, lang], ['原始文案(禁止修改)', '翻译文案'], []]; 114 | 115 | Object.keys(jsonContent).forEach(key => { 116 | const text = jsonContent[key]; 117 | 118 | sheets.push([key, text]); 119 | }); 120 | 121 | const buffer = xlsx.build([{ name: 'locals', data: sheets }], xlsxOptions); 122 | 123 | fs.writeFileSync(destination, buffer); 124 | } 125 | 126 | function convertExcel2Json(file, lang, destination) { 127 | const sheets = xlsx.parse(fs.readFileSync(file)); 128 | 129 | const jsonData = require(destination) || {}; 130 | 131 | sheets[0].data.slice(2).forEach(item => { 132 | if (item.length) { 133 | jsonData[item[0]] = item[1]; 134 | } 135 | }); 136 | 137 | fs.outputFileSync(destination, JSON.stringify(jsonData, '\n', 2)); 138 | } 139 | 140 | exports.ensureLocals = function() { 141 | fs.ensureDirSync(path.join(paths.locals)); 142 | 143 | if (Array.isArray(pkg.locals)) { 144 | pkg.locals.forEach(lang => { 145 | const file = path.join(paths.locals, lang + '.json'); 146 | 147 | if (!fs.existsSync(file)) { 148 | fs.outputJSONSync(file, {}); 149 | } 150 | }); 151 | } 152 | }; 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-antd-formutil", 3 | "version": "1.1.13", 4 | "description": "Happy to use react-formutil in the project based on ant-design ^_^", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "directories": { 8 | "doc": "docs" 9 | }, 10 | "scripts": { 11 | "start": "cd docs && npm start", 12 | "build": "rimraf dist && rollup -c", 13 | "test": "node jest/test.js", 14 | "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --noEmit --checkJs false" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/qiqiboy/react-antd-formutil.git" 19 | }, 20 | "entryFile": "src/index.js", 21 | "keywords": [ 22 | "react", 23 | "ant-design", 24 | "react-component", 25 | "react-form", 26 | "react-formutil", 27 | "ant-design-form" 28 | ], 29 | "author": "qiqiboy", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/qiqiboy/react-antd-formutil/issues" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest", 38 | "./eslint.config.js" 39 | ] 40 | }, 41 | "prettier": { 42 | "printWidth": 120, 43 | "tabWidth": 4, 44 | "trailingComma": "none", 45 | "jsxBracketSameLine": true, 46 | "semi": true, 47 | "singleQuote": true, 48 | "arrowParens": "avoid", 49 | "overrides": [ 50 | { 51 | "files": [ 52 | "*.json" 53 | ], 54 | "options": { 55 | "tabWidth": 2 56 | } 57 | } 58 | ] 59 | }, 60 | "homepage": "https://github.com/qiqiboy/react-antd-formutil#readme", 61 | "types": "./index.d.ts", 62 | "peerDependencies": { 63 | "react": "^16.3.0 || ^17.0.0", 64 | "prop-types": ">15.0.0", 65 | "antd": "^3.0.0 || ^4.0.0", 66 | "react-formutil": "^1.0.11" 67 | }, 68 | "devDependencies": { 69 | "@babel/cli": "7.12.1", 70 | "@babel/core": "7.12.3", 71 | "@commitlint/cli": "11.0.0", 72 | "@commitlint/config-conventional": "11.0.0", 73 | "@rollup/plugin-babel": "5.2.1", 74 | "@rollup/plugin-commonjs": "11.1.0", 75 | "@rollup/plugin-eslint": "8.0.0", 76 | "@rollup/plugin-node-resolve": "10.0.0", 77 | "@rollup/plugin-replace": "2.3.4", 78 | "@testing-library/jest-dom": "5.11.5", 79 | "@testing-library/react": "11.1.2", 80 | "@testing-library/user-event": "12.2.2", 81 | "@types/jest": "26.0.15", 82 | "@types/node": "14.14.7", 83 | "@types/prop-types": "15.7.3", 84 | "@types/react": "16.9.23", 85 | "@types/react-dom": "16.9.5", 86 | "@types/react-is": "16.7.1", 87 | "@typescript-eslint/eslint-plugin": "4.7.0", 88 | "@typescript-eslint/parser": "4.7.0", 89 | "antd": "4.9.2", 90 | "babel-eslint": "10.1.0", 91 | "babel-jest": "26.6.3", 92 | "babel-preset-react-app": "10.0.0", 93 | "eslint": "7.13.0", 94 | "eslint-config-react-app": "6.0.0", 95 | "eslint-plugin-flowtype": "5.2.0", 96 | "eslint-plugin-import": "2.22.1", 97 | "eslint-plugin-jest": "24.1.3", 98 | "eslint-plugin-jsx-a11y": "6.4.1", 99 | "eslint-plugin-react": "7.21.5", 100 | "eslint-plugin-react-hooks": "4.2.0", 101 | "eslint-plugin-testing-library": "3.10.0", 102 | "husky": "3.1.0", 103 | "jest": "26.6.3", 104 | "jest-environment-jsdom-fourteen": "1.0.1", 105 | "jest-resolve": "26.6.2", 106 | "jest-runner-eslint": "0.10.0", 107 | "jest-watch-typeahead": "0.4.2", 108 | "lint-staged": "10.5.1", 109 | "memo-render": "^0.0.2", 110 | "prettier": "2.1.2", 111 | "prop-types": "15.7.2", 112 | "react": "17.0.1", 113 | "react-dom": "17.0.1", 114 | "react-formutil": "^1.1.3", 115 | "react-hot-loader": "^4.12.19", 116 | "rimraf": "3.0.2", 117 | "rollup": "2.33.1", 118 | "rollup-plugin-babel": "4.4.0", 119 | "rollup-plugin-clear": "^2.0.7", 120 | "rollup-plugin-copy": "3.3.0", 121 | "rollup-plugin-filesize": "9.0.2", 122 | "rollup-plugin-sass": "1.2.2", 123 | "rollup-plugin-terser": "7.0.2", 124 | "typescript": "4.0.5" 125 | }, 126 | "engines": { 127 | "node": ">=10.13.0", 128 | "tiger-new": "6.2.7" 129 | }, 130 | "husky": { 131 | "hooks": { 132 | "commit-msg": "node_modules/.bin/commitlint --edit $HUSKY_GIT_PARAMS", 133 | "pre-commit": "lint-staged && export StagedFiles=$(git diff --diff-filter AM --name-only --relative --staged | grep -E '^src/.*\\.m?[jt]sx?$') && if [ -n \"$StagedFiles\" ]; then npm run tsc ; fi" 134 | } 135 | }, 136 | "commitlint": { 137 | "extends": [ 138 | "@commitlint/config-conventional" 139 | ], 140 | "rules": { 141 | "subject-case": [ 142 | 0 143 | ], 144 | "scope-case": [ 145 | 0 146 | ] 147 | } 148 | }, 149 | "lint-staged": { 150 | "src/**/*.{js,jsx,mjs,ts,tsx}": [ 151 | "node_modules/.bin/prettier --write", 152 | "node_modules/.bin/eslint --fix" 153 | ], 154 | "src/**/*.{css,scss,less,json,html,md}": [ 155 | "node_modules/.bin/prettier --write" 156 | ] 157 | }, 158 | "stylelint": { 159 | "extends": "stylelint-config-recommended" 160 | }, 161 | "browserslist": [ 162 | ">0.2%", 163 | "not dead", 164 | "not op_mini all" 165 | ], 166 | "config": { 167 | "commitizen": { 168 | "path": "cz-conventional-changelog" 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "author": "qiqiboy", 4 | "version": "1.0.2", 5 | "private": true, 6 | "vendor": [ 7 | "react", 8 | "react-dom", 9 | "antd", 10 | "react-formutil", 11 | "./static/css/vendor.less" 12 | ], 13 | "noRewrite": true, 14 | "proxy": null, 15 | "scripts": { 16 | "start": "node scripts/start.js", 17 | "build": "node scripts/build.js", 18 | "build:dev": "node scripts/build.js --dev", 19 | "pack": "npm run build", 20 | "count": "node scripts/count.js", 21 | "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts', 'app/utils/i18n/*'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --checkJs false" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "./scripts/config/eslintrc.js" 27 | ] 28 | }, 29 | "prettier": { 30 | "printWidth": 120, 31 | "tabWidth": 4, 32 | "parser": "babylon", 33 | "trailingComma": "none", 34 | "jsxBracketSameLine": true, 35 | "semi": true, 36 | "singleQuote": true, 37 | "overrides": [ 38 | { 39 | "files": "*.json", 40 | "options": { 41 | "tabWidth": 2 42 | } 43 | } 44 | ] 45 | }, 46 | "lint-staged": { 47 | "{app,static}/**/*.{js,jsx,mjs,css,scss,less,json,ts}": [ 48 | "node_modules/.bin/prettier --write", 49 | "git add" 50 | ] 51 | }, 52 | "dependencies": { 53 | "axios": "0.17.0", 54 | "lodash": "^4.17.10", 55 | "moment": "^2.22.2", 56 | "react-animated-router": "0.1.11", 57 | "react-awesome-snippets": "0.0.22", 58 | "react-router-dom": "4.2.2", 59 | "react-transition-group": "2.4.0" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "7.8.6", 63 | "@babel/plugin-proposal-decorators": "7.8.3", 64 | "@commitlint/cli": "7.6.1", 65 | "@commitlint/config-conventional": "7.6.0", 66 | "@svgr/webpack": "4.3.3", 67 | "@types/node": "12.12.29", 68 | "@types/react-is": "16.7.1", 69 | "@types/react-router-dom": "5.1.3", 70 | "@types/react-transition-group": "4.2.4", 71 | "@typescript-eslint/eslint-plugin": "2.21.0", 72 | "@typescript-eslint/parser": "2.21.0", 73 | "address": "1.0.2", 74 | "ali-oss": "6.5.1", 75 | "babel-loader": "8.0.6", 76 | "babel-plugin-named-asset-import": "0.3.6", 77 | "case-sensitive-paths-webpack-plugin": "2.2.0", 78 | "chalk": "3.0.0", 79 | "check-dependencies": "1.1.0", 80 | "classlist-polyfill": "1.2.0", 81 | "core-js": "3.6.4", 82 | "css-loader": "3.4.2", 83 | "cz-conventional-changelog": "2.1.0", 84 | "directory-named-webpack-plugin": "4.0.1", 85 | "dotenv": "8.2.0", 86 | "dotenv-expand": "5.1.0", 87 | "eslint": "7.14.0", 88 | "eslint-config-react-app": "6.0.0", 89 | "eslint-loader": "4.0.2", 90 | "eslint-plugin-flowtype": "4.6.0", 91 | "eslint-plugin-import": "2.22.1", 92 | "eslint-plugin-jsx-a11y": "6.4.1", 93 | "eslint-plugin-react": "7.21.5", 94 | "eslint-plugin-react-hooks": "2.5.0", 95 | "file-loader": "4.3.0", 96 | "fork-ts-checker-webpack-plugin-alt": "0.4.14", 97 | "fs-extra": "8.1.0", 98 | "glob": "7.1.6", 99 | "html-loader": "1.0.0-alpha.0", 100 | "html-webpack-plugin": "4.0.0-beta.11", 101 | "husky": "3.1.0", 102 | "i18next-scanner": "2.10.3", 103 | "imagemin-webpack-plugin": "2.4.2", 104 | "less": "3.11.1", 105 | "less-loader": "5.0.0", 106 | "lint-staged": "9.5.0", 107 | "mini-css-extract-plugin": "0.9.0", 108 | "node-xlsx": "0.15.0", 109 | "optimize-css-assets-webpack-plugin": "5.0.3", 110 | "ora": "4.0.3", 111 | "postcss-flexbugs-fixes": "4.2.0", 112 | "postcss-loader": "3.0.0", 113 | "postcss-preset-env": "6.7.0", 114 | "postcss-safe-parser": "4.0.2", 115 | "prettier": "1.19.1", 116 | "raf-dom": "1.1.0", 117 | "raw-loader": "3.1.0", 118 | "react-dev-utils": "10.1.0", 119 | "rsync": "0.6.1", 120 | "sass": "1.26.2", 121 | "sass-loader": "8.0.2", 122 | "style-loader": "1.1.3", 123 | "stylelint": "12.0.1", 124 | "stylelint-config-recommended": "3.0.0", 125 | "sw-precache-webpack-plugin": "0.11.5", 126 | "terser-webpack-plugin": "2.3.5", 127 | "typescript": "3.7.5", 128 | "webpack": "4.41.6", 129 | "webpack-bundle-analyzer": "^3.6.0", 130 | "webpack-dev-server": "3.10.3" 131 | }, 132 | "husky": { 133 | "hooks": { 134 | "pre-commit": "lint-staged && export StagedFiles=$(git diff --name-only --diff-filter AM --relative --staged | grep -E '.tsx?$') && if [ -n \"$StagedFiles\" ]; then npm run tsc; fi", 135 | "commit-msg": "node_modules/.bin/commitlint --edit $HUSKY_GIT_PARAMS", 136 | "pre-push": "CF=$(git diff --diff-filter AM --name-only @{u}..) || CF=$(git diff --diff-filter AM --name-only origin/master...HEAD); FILES=$(echo \"$CF\" | grep -E '^app/.*\\.m?[jt]sx?$'); if [ -n \"$FILES\" ]; then node_modules/.bin/eslint $FILES --max-warnings 0; fi" 137 | } 138 | }, 139 | "commitlint": { 140 | "extends": [ 141 | "@commitlint/config-conventional" 142 | ], 143 | "rules": { 144 | "subject-case": [ 145 | 0 146 | ], 147 | "scope-case": [ 148 | 0 149 | ] 150 | } 151 | }, 152 | "stylelint": { 153 | "extends": "stylelint-config-recommended" 154 | }, 155 | "browserslist": [ 156 | ">0.2%", 157 | "not dead", 158 | "not op_mini all", 159 | "ie > 10" 160 | ], 161 | "config": { 162 | "commitizen": { 163 | "path": "cz-conventional-changelog" 164 | } 165 | }, 166 | "engines": { 167 | "node": ">=8.10.0", 168 | "tiger-new": "4.3.10" 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/FormItem.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Checkbox, Switch, Radio } from 'antd'; 3 | import { render } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import FormItem from './FormItem'; 6 | import { Form, $Formutil } from '..'; 7 | 8 | if (!window.matchMedia) { 9 | Object.defineProperty(window, 'matchMedia', { 10 | value: jest.fn((query) => ({ 11 | matches: query.includes('max-width'), 12 | addListener: () => {}, 13 | removeListener: () => {} 14 | })) 15 | }); 16 | } 17 | 18 | function renderForm(content: React.ReactNode) { 19 | let $formutil: $Formutil; 20 | 21 | const wrapper = render( 22 |
{ 24 | $formutil = props; 25 | 26 | return content; 27 | }} 28 | /> 29 | ); 30 | 31 | return { 32 | ...wrapper, 33 | getFormutil: () => $formutil 34 | }; 35 | } 36 | 37 | describe('Form.Item props', () => { 38 | test('should pass [label] to Form.Item', () => { 39 | const { getByText } = renderForm( 40 | 41 | 42 | 43 | ); 44 | 45 | expect(getByText('Input')).toBeInTheDocument(); 46 | }); 47 | 48 | test('should pass [colon] to Form.Item', () => { 49 | const { getByText } = renderForm( 50 | 51 | 52 | 53 | ); 54 | 55 | expect(getByText('Input').className).toContain('ant-form-item-no-colon'); 56 | }); 57 | 58 | test('should pass [help] to Form.Item', () => { 59 | const { getByText } = renderForm( 60 | 61 | 62 | 63 | ); 64 | 65 | expect(getByText('help here')).toBeInTheDocument(); 66 | }); 67 | 68 | test('should pass [required] to Form.Item', () => { 69 | renderForm( 70 | 71 | 72 | 73 | ); 74 | 75 | expect(document.querySelector('.ant-form-item-required')).toBeInTheDocument(); 76 | }); 77 | 78 | test('should pass [itemProps] to Form.Item', () => { 79 | const { getByText } = renderForm( 80 | 87 | 88 | 89 | ); 90 | 91 | expect(document.querySelector('.ant-form-item-required')).toBeInTheDocument(); 92 | expect(getByText('Input').className).toContain('ant-form-item-no-colon'); 93 | }); 94 | 95 | test('[itemProps] should have high priority', () => { 96 | const { queryByText } = renderForm( 97 | 103 | 104 | 105 | ); 106 | 107 | expect(queryByText('Input 1')).toBe(null); 108 | expect(queryByText('Input 2')).toBeInTheDocument(); 109 | }); 110 | }); 111 | 112 | test('should handle value from Input', () => { 113 | const { getByPlaceholderText, getFormutil } = renderForm( 114 | 115 | 116 | 117 | ); 118 | 119 | userEvent.type(getByPlaceholderText('input'), 'yes'); 120 | 121 | expect(getFormutil().$params.input).toBe('yes'); 122 | }); 123 | 124 | test('should handle value from Checkbox/Switch/Radio', () => { 125 | const { getFormutil } = renderForm( 126 | <> 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | ); 138 | 139 | expect(getFormutil().$params).toEqual({ 140 | radio: false, 141 | checkbox: false, 142 | switch: 'no' 143 | }); 144 | 145 | userEvent.click(document.querySelector('.ant-radio-wrapper')!); 146 | userEvent.click(document.querySelector('.ant-checkbox-wrapper')!); 147 | userEvent.click(document.querySelector('.ant-switch')!); 148 | 149 | expect(getFormutil().$params).toEqual({ 150 | radio: true, 151 | checkbox: 'yes', 152 | switch: true 153 | }); 154 | }); 155 | 156 | test('[noStyle] should runing', () => { 157 | const { getFormutil } = renderForm( 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | ); 167 | 168 | 169 | expect(getFormutil().$params).toEqual({ 170 | a: '', 171 | b: '' 172 | }); 173 | 174 | expect(getFormutil().$errors).toEqual({ 175 | a: { 176 | required: 'Error input: required' 177 | }, 178 | b: { 179 | required: 'Error input: required' 180 | } 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /docs/scripts/cdn.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const Rsync = require('rsync'); 5 | const OSS = require('ali-oss'); 6 | const chalk = require('chalk'); 7 | const lodash = require('lodash'); 8 | const glob = require('glob'); 9 | const paths = require('./config/paths'); 10 | const ora = require('ora'); 11 | const pkg = require(paths.appPackageJson); 12 | 13 | const staticFileName = 'static.config.json'; 14 | const staticConfigFile = path.resolve(paths.root, staticFileName); 15 | const oldStaticConfig = lodash.invert(getStaticConfig(staticConfigFile)); 16 | const newStaticConfig = {}; 17 | 18 | const spinner = ora(); 19 | let throttleDelay = 0; 20 | 21 | if (process.env.SKIP_CDN === 'true') { 22 | spinner.info(chalk.cyan('本次构建忽略CDN任务')); 23 | } else if (pkg.cdn) { 24 | if ('server' in pkg.cdn || 'ali-oss' in pkg.cdn) { 25 | runCDN(); 26 | } else { 27 | spinner.fail(chalk.red(`未发现CDN服务连接配置信息!`)); 28 | spinner.fail(chalk.red(`如果不需要CDN服务,您可以移除 ${chalk.cyan('package.json')} 中的 cdn 字段;`)); 29 | spinner.fail(chalk.red(`或者,请根据下述信息在 ${chalk.cyan('package.json')} 中补充相关配置:`)); 30 | 31 | console.log( 32 | chalk.grey(` 33 | 支持两种cdn配置方式,分别需要在 ${chalk.cyan('package.json')} 中配置相关的cdn字段: 34 | 35 | { 36 | "name": "${pkg.name}", 37 | "version": "${pkg.version}", 38 | "cdn": { 39 | "host": "${pkg.cdn.host || 'https://xxx.com'}", 40 | "path": "${pkg.cdn.path || '/xxx'}", 41 | ${chalk.cyan(`"server": "host:path", 42 | "ali-oss": { 43 | ... 44 | }`)} 45 | }, 46 | ... 47 | } 48 | 49 | server和ali-oss字段必选其一配置,别对对应下述两种cdn配置方式: 50 | 51 | 1. 阿里云的OSS存储服务,对应ali-oss配置(具体需要配置的内容可以参考阿里云文档) 52 | 2. 通过ssh的rsync命令传到源服务器上,对应server字段配置,即rsync命令的目标服务器与路径,例如:BEIJING_HOST:/data0/webservice/static 53 | `) // ` 54 | ); 55 | } 56 | } else { 57 | spinner.info(chalk.cyan('未发现CDN配置信息,已跳过')); 58 | } 59 | 60 | function getStaticConfig(path) { 61 | try { 62 | return require(path) || {}; 63 | } catch (e) { 64 | return {}; 65 | } 66 | } 67 | 68 | function removeFileNameHash(fileName) { 69 | const pipes = fileName.split('.'); 70 | 71 | pipes.splice(-2, 1); 72 | return pipes.join('.'); 73 | } 74 | 75 | function runCDN() { 76 | throttleDelay = 0; 77 | 78 | spinner.start('开始上传'); 79 | 80 | let exitsNum = 0; 81 | const useOSS = !!pkg.cdn['ali-oss']; 82 | const allFiles = glob.sync(path.join(paths.appBuild, 'static/**/!(*.map)')); 83 | const allSyncPromises = allFiles 84 | .filter(function(file) { 85 | const relative = path.relative(paths.appBuild, file); 86 | 87 | // 文件夹不处理 88 | if (fs.statSync(file).isDirectory()) { 89 | return false; 90 | } 91 | 92 | newStaticConfig[/js$|css$/.test(relative) ? removeFileNameHash(relative) : relative] = relative; 93 | 94 | // 已经存在 95 | if (oldStaticConfig[relative]) { 96 | spinner.succeed(chalk.green('已存在:' + relative)); 97 | exitsNum++; 98 | return false; 99 | } 100 | 101 | return true; 102 | }) 103 | .map(useOSS ? createOSS : createRsync); 104 | 105 | Promise.all(allSyncPromises).then(function(rets) { 106 | let uploadNum = rets.filter(Boolean).length; 107 | let failNum = rets.length - uploadNum; 108 | 109 | console.log(); 110 | 111 | console.log( 112 | chalk[failNum ? 'red' : 'cyan']( 113 | '+++++++++++++++++++++++++++++++\n 文件上传完毕(' + 114 | chalk.blue(pkg.cdn.path) + 115 | ') \n ' + 116 | chalk.magenta('成功: ' + uploadNum) + 117 | ' \n ' + 118 | chalk.red('失败: ' + failNum) + 119 | ' \n ' + 120 | chalk.green('重复: ' + exitsNum) + 121 | '\n+++++++++++++++++++++++++++++++' 122 | ) 123 | ); 124 | 125 | if (!failNum) { 126 | fs.outputFileSync(staticConfigFile, JSON.stringify(newStaticConfig, '\n', 2)); 127 | console.log(chalk.blue('配置文件已经更新: ' + staticConfigFile)); 128 | console.log(); 129 | console.log(chalk.green('项目已经成功编译,运行以下命令可即时预览:')); 130 | 131 | if (!paths.serve) { 132 | console.log(chalk.cyan('npm') + ' install -g serve'); 133 | } 134 | 135 | console.log(chalk.cyan('serve') + ' -s ' + path.relative('.', paths.appBuild)); 136 | } else { 137 | console.log(chalk.red('文件未全部上传,请单独运行') + chalk.green(' npm run cdn ') + chalk.red('命令!')); 138 | process.exit(1); 139 | } 140 | 141 | console.log(); 142 | }); 143 | } 144 | 145 | function createRsync(file) { 146 | return new Promise(resolve => { 147 | setTimeout(() => { 148 | const rsync = new Rsync(); 149 | const relative = path.relative(paths.appBuild, file); 150 | 151 | rsync.cwd(paths.appBuild); 152 | 153 | rsync 154 | .flags('Rz') // 相对路径上传、压缩 155 | .source(relative) 156 | .destination(path.join(pkg.cdn.server || 'static:/data0/webservice/static', pkg.cdn.path)) 157 | .execute(function(error, code, cmd) { 158 | if (error) { 159 | resolve(false); 160 | spinner.fail(chalk.red('上传失败(' + error + '):' + relative)); 161 | } else { 162 | resolve(true); 163 | spinner.warn(chalk.yellow('已上传:' + relative)); 164 | } 165 | }); 166 | }, 200 * throttleDelay++); 167 | }); 168 | } 169 | 170 | function createOSS(file) { 171 | return new Promise(resolve => { 172 | setTimeout(() => { 173 | const client = new OSS(pkg.cdn['ali-oss']); 174 | const objectName = path.relative(paths.appBuild, file); 175 | 176 | client 177 | .put(path.join(pkg.cdn.path, objectName), file) 178 | .then(() => { 179 | resolve(true); 180 | spinner.warn(chalk.yellow('已上传:' + objectName)); 181 | }) 182 | .catch(error => { 183 | resolve(false); 184 | spinner.fail(chalk.red('上传失败(' + error + '):' + objectName)); 185 | }); 186 | }, 200 * throttleDelay++); 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const commonjs = require('@rollup/plugin-commonjs'); 6 | const replace = require('@rollup/plugin-replace'); 7 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 8 | const babel = require('@rollup/plugin-babel').default; 9 | const filesize = require('rollup-plugin-filesize'); 10 | const copy = require('rollup-plugin-copy'); 11 | const sass = require('rollup-plugin-sass'); 12 | const { terser } = require('rollup-plugin-terser'); 13 | const eslint = require('@rollup/plugin-eslint'); 14 | const pkg = require('./package.json'); 15 | 16 | /** 17 | * 如果希望将某些模块代码直接构建进输出文件,可以再这里指定这些模块名称 18 | */ 19 | const externalExclude = [ 20 | '@babel/runtime', 'regenerator-runtime' 21 | ]; 22 | 23 | const exportName = pkg.exportName || pkg.name.split('/').slice(-1)[0]; 24 | /** 25 | * 如果你希望编译后的代码里依然自动包含进去编译后的css,那么这里可以设置为 true 26 | */ 27 | const shouldPreserveCss = false; 28 | 29 | function createConfig(env, module) { 30 | const isProd = env === 'production'; 31 | // for umd globals 32 | const globals = { 33 | react: 'React', 34 | 'react-dom': 'ReactDOM', 35 | 'prop-types': 'PropTypes', 36 | 'react-formutil': 'ReactFormutil', 37 | 'antd': 'antd' 38 | }; 39 | 40 | return { 41 | /** 42 | * 入口文件位置,如果你更改了entryFile,别忘了同时修改 npm/index.cjs.js 和 npm/index.esm.js 里的文件引用名称 43 | */ 44 | input: pkg.entryFile || 'src/index.ts', 45 | external: 46 | module === 'umd' 47 | ? Object.keys(globals) 48 | : id => 49 | !externalExclude.some(name => id.startsWith(name)) && !id.startsWith('.') && !path.isAbsolute(id), 50 | output: { 51 | name: 'ReactAntdFormutil', 52 | file: `dist/${exportName}.${module}.${env}.js`, 53 | format: module, 54 | exports: 'named', 55 | sourcemap: false, 56 | intro: 57 | module !== 'umd' && shouldPreserveCss 58 | ? module === 'cjs' 59 | ? `require('./${exportName}.css');` 60 | : `import('./${exportName}.css');` 61 | : undefined, 62 | globals 63 | }, 64 | treeshake: { 65 | /** 66 | * 如果你有引入一些有副作用的代码模块,或者构建后的代码运行异常,可以尝试将该项设置为 true 67 | */ 68 | moduleSideEffects: false 69 | }, 70 | plugins: [ 71 | eslint({ 72 | fix: true, 73 | throwOnError: true, 74 | throwOnWarning: true 75 | }), 76 | nodeResolve({ 77 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 78 | }), 79 | commonjs({ 80 | include: /node_modules/ 81 | }), 82 | replace({ 83 | 'process.env.NODE_ENV': JSON.stringify(env) 84 | }), 85 | babel({ 86 | exclude: 'node_modules/**', 87 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 88 | babelHelpers: 'runtime', 89 | babelrc: false, 90 | configFile: false, 91 | presets: [ 92 | [ 93 | '@babel/preset-env', 94 | { 95 | useBuiltIns: 'entry', 96 | corejs: 3, 97 | modules: false, 98 | exclude: ['transform-typeof-symbol'] 99 | } 100 | ], 101 | [ 102 | '@babel/preset-react', 103 | { 104 | development: false, 105 | useBuiltIns: true, 106 | runtime: 'classic' 107 | } 108 | ], 109 | ['@babel/preset-typescript'] 110 | ], 111 | plugins: [ 112 | 'babel-plugin-macros', 113 | ['@babel/plugin-proposal-decorators', { legacy: true }], 114 | [ 115 | '@babel/plugin-proposal-class-properties', 116 | { 117 | loose: true 118 | } 119 | ], 120 | [ 121 | '@babel/plugin-transform-runtime', 122 | { 123 | version: require('@babel/helpers/package.json').version, 124 | corejs: false, 125 | helpers: true, 126 | regenerator: true, 127 | useESModules: module === 'esm', 128 | absoluteRuntime: false 129 | } 130 | ], 131 | isProd && [ 132 | // Remove PropTypes from production build 133 | 'babel-plugin-transform-react-remove-prop-types', 134 | { 135 | removeImport: true 136 | } 137 | ], 138 | require('@babel/plugin-proposal-optional-chaining').default, 139 | require('@babel/plugin-proposal-nullish-coalescing-operator').default, 140 | // Adds Numeric Separators 141 | require('@babel/plugin-proposal-numeric-separator').default 142 | ].filter(Boolean) 143 | }), 144 | module !== 'umd' && 145 | sass({ 146 | output: `dist/${exportName}.css` 147 | }), 148 | isProd && 149 | terser({ 150 | output: { comments: false }, 151 | compress: false, 152 | warnings: false, 153 | ecma: 5, 154 | ie8: false, 155 | toplevel: true 156 | }), 157 | filesize(), 158 | copy({ 159 | targets: [ 160 | { 161 | src: `npm/index.${module}.js`, 162 | dest: 'dist' 163 | } 164 | ], 165 | verbose: false 166 | }) 167 | ].filter(Boolean) 168 | }; 169 | } 170 | 171 | module.exports = ['cjs', 'esm', 'umd'].reduce((configQueue, module) => { 172 | return fs.existsSync(`./npm/index.${module}.js`) 173 | ? configQueue.concat(createConfig('development', module), createConfig('production', module)) 174 | : configQueue; 175 | }, []); 176 | -------------------------------------------------------------------------------- /docs/static/css/modules/color/colors.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/color/colorPalette'; 2 | 3 | // color palettes 4 | @blue-1: color(~`colorPalette('@{blue-6}', 1)`); 5 | @blue-2: color(~`colorPalette('@{blue-6}', 2)`); 6 | @blue-3: color(~`colorPalette('@{blue-6}', 3)`); 7 | @blue-4: color(~`colorPalette('@{blue-6}', 4)`); 8 | @blue-5: color(~`colorPalette('@{blue-6}', 5)`); 9 | @blue-6: #1890ff; 10 | @blue-7: color(~`colorPalette('@{blue-6}', 7)`); 11 | @blue-8: color(~`colorPalette('@{blue-6}', 8)`); 12 | @blue-9: color(~`colorPalette('@{blue-6}', 9)`); 13 | @blue-10: color(~`colorPalette('@{blue-6}', 10)`); 14 | 15 | @purple-1: color(~`colorPalette('@{purple-6}', 1)`); 16 | @purple-2: color(~`colorPalette('@{purple-6}', 2)`); 17 | @purple-3: color(~`colorPalette('@{purple-6}', 3)`); 18 | @purple-4: color(~`colorPalette('@{purple-6}', 4)`); 19 | @purple-5: color(~`colorPalette('@{purple-6}', 5)`); 20 | @purple-6: #722ed1; 21 | @purple-7: color(~`colorPalette('@{purple-6}', 7)`); 22 | @purple-8: color(~`colorPalette('@{purple-6}', 8)`); 23 | @purple-9: color(~`colorPalette('@{purple-6}', 9)`); 24 | @purple-10: color(~`colorPalette('@{purple-6}', 10)`); 25 | 26 | @cyan-1: color(~`colorPalette('@{cyan-6}', 1)`); 27 | @cyan-2: color(~`colorPalette('@{cyan-6}', 2)`); 28 | @cyan-3: color(~`colorPalette('@{cyan-6}', 3)`); 29 | @cyan-4: color(~`colorPalette('@{cyan-6}', 4)`); 30 | @cyan-5: color(~`colorPalette('@{cyan-6}', 5)`); 31 | @cyan-6: #13c2c2; 32 | @cyan-7: color(~`colorPalette('@{cyan-6}', 7)`); 33 | @cyan-8: color(~`colorPalette('@{cyan-6}', 8)`); 34 | @cyan-9: color(~`colorPalette('@{cyan-6}', 9)`); 35 | @cyan-10: color(~`colorPalette('@{cyan-6}', 10)`); 36 | 37 | @green-1: color(~`colorPalette('@{green-6}', 1)`); 38 | @green-2: color(~`colorPalette('@{green-6}', 2)`); 39 | @green-3: color(~`colorPalette('@{green-6}', 3)`); 40 | @green-4: color(~`colorPalette('@{green-6}', 4)`); 41 | @green-5: color(~`colorPalette('@{green-6}', 5)`); 42 | @green-6: #52c41a; 43 | @green-7: color(~`colorPalette('@{green-6}', 7)`); 44 | @green-8: color(~`colorPalette('@{green-6}', 8)`); 45 | @green-9: color(~`colorPalette('@{green-6}', 9)`); 46 | @green-10: color(~`colorPalette('@{green-6}', 10)`); 47 | 48 | @magenta-1: color(~`colorPalette('@{magenta-6}', 1)`); 49 | @magenta-2: color(~`colorPalette('@{magenta-6}', 2)`); 50 | @magenta-3: color(~`colorPalette('@{magenta-6}', 3)`); 51 | @magenta-4: color(~`colorPalette('@{magenta-6}', 4)`); 52 | @magenta-5: color(~`colorPalette('@{magenta-6}', 5)`); 53 | @magenta-6: #eb2f96; 54 | @magenta-7: color(~`colorPalette('@{magenta-6}', 7)`); 55 | @magenta-8: color(~`colorPalette('@{magenta-6}', 8)`); 56 | @magenta-9: color(~`colorPalette('@{magenta-6}', 9)`); 57 | @magenta-10: color(~`colorPalette('@{magenta-6}', 10)`); 58 | 59 | // alias of magenta 60 | @pink-1: color(~`colorPalette('@{pink-6}', 1)`); 61 | @pink-2: color(~`colorPalette('@{pink-6}', 2)`); 62 | @pink-3: color(~`colorPalette('@{pink-6}', 3)`); 63 | @pink-4: color(~`colorPalette('@{pink-6}', 4)`); 64 | @pink-5: color(~`colorPalette('@{pink-6}', 5)`); 65 | @pink-6: #eb2f96; 66 | @pink-7: color(~`colorPalette('@{pink-6}', 7)`); 67 | @pink-8: color(~`colorPalette('@{pink-6}', 8)`); 68 | @pink-9: color(~`colorPalette('@{pink-6}', 9)`); 69 | @pink-10: color(~`colorPalette('@{pink-6}', 10)`); 70 | 71 | @red-1: color(~`colorPalette('@{red-6}', 1)`); 72 | @red-2: color(~`colorPalette('@{red-6}', 2)`); 73 | @red-3: color(~`colorPalette('@{red-6}', 3)`); 74 | @red-4: color(~`colorPalette('@{red-6}', 4)`); 75 | @red-5: color(~`colorPalette('@{red-6}', 5)`); 76 | @red-6: #f5222d; 77 | @red-7: color(~`colorPalette('@{red-6}', 7)`); 78 | @red-8: color(~`colorPalette('@{red-6}', 8)`); 79 | @red-9: color(~`colorPalette('@{red-6}', 9)`); 80 | @red-10: color(~`colorPalette('@{red-6}', 10)`); 81 | 82 | @orange-1: color(~`colorPalette('@{orange-6}', 1)`); 83 | @orange-2: color(~`colorPalette('@{orange-6}', 2)`); 84 | @orange-3: color(~`colorPalette('@{orange-6}', 3)`); 85 | @orange-4: color(~`colorPalette('@{orange-6}', 4)`); 86 | @orange-5: color(~`colorPalette('@{orange-6}', 5)`); 87 | @orange-6: #fa8c16; 88 | @orange-7: color(~`colorPalette('@{orange-6}', 7)`); 89 | @orange-8: color(~`colorPalette('@{orange-6}', 8)`); 90 | @orange-9: color(~`colorPalette('@{orange-6}', 9)`); 91 | @orange-10: color(~`colorPalette('@{orange-6}', 10)`); 92 | 93 | @yellow-1: color(~`colorPalette('@{yellow-6}', 1)`); 94 | @yellow-2: color(~`colorPalette('@{yellow-6}', 2)`); 95 | @yellow-3: color(~`colorPalette('@{yellow-6}', 3)`); 96 | @yellow-4: color(~`colorPalette('@{yellow-6}', 4)`); 97 | @yellow-5: color(~`colorPalette('@{yellow-6}', 5)`); 98 | @yellow-6: #fadb14; 99 | @yellow-7: color(~`colorPalette('@{yellow-6}', 7)`); 100 | @yellow-8: color(~`colorPalette('@{yellow-6}', 8)`); 101 | @yellow-9: color(~`colorPalette('@{yellow-6}', 9)`); 102 | @yellow-10: color(~`colorPalette('@{yellow-6}', 10)`); 103 | 104 | @volcano-1: color(~`colorPalette('@{volcano-6}', 1)`); 105 | @volcano-2: color(~`colorPalette('@{volcano-6}', 2)`); 106 | @volcano-3: color(~`colorPalette('@{volcano-6}', 3)`); 107 | @volcano-4: color(~`colorPalette('@{volcano-6}', 4)`); 108 | @volcano-5: color(~`colorPalette('@{volcano-6}', 5)`); 109 | @volcano-6: #fa541c; 110 | @volcano-7: color(~`colorPalette('@{volcano-6}', 7)`); 111 | @volcano-8: color(~`colorPalette('@{volcano-6}', 8)`); 112 | @volcano-9: color(~`colorPalette('@{volcano-6}', 9)`); 113 | @volcano-10: color(~`colorPalette('@{volcano-6}', 10)`); 114 | 115 | @geekblue-1: color(~`colorPalette('@{geekblue-6}', 1)`); 116 | @geekblue-2: color(~`colorPalette('@{geekblue-6}', 2)`); 117 | @geekblue-3: color(~`colorPalette('@{geekblue-6}', 3)`); 118 | @geekblue-4: color(~`colorPalette('@{geekblue-6}', 4)`); 119 | @geekblue-5: color(~`colorPalette('@{geekblue-6}', 5)`); 120 | @geekblue-6: #2f54eb; 121 | @geekblue-7: color(~`colorPalette('@{geekblue-6}', 7)`); 122 | @geekblue-8: color(~`colorPalette('@{geekblue-6}', 8)`); 123 | @geekblue-9: color(~`colorPalette('@{geekblue-6}', 9)`); 124 | @geekblue-10: color(~`colorPalette('@{geekblue-6}', 10)`); 125 | 126 | @lime-1: color(~`colorPalette('@{lime-6}', 1)`); 127 | @lime-2: color(~`colorPalette('@{lime-6}', 2)`); 128 | @lime-3: color(~`colorPalette('@{lime-6}', 3)`); 129 | @lime-4: color(~`colorPalette('@{lime-6}', 4)`); 130 | @lime-5: color(~`colorPalette('@{lime-6}', 5)`); 131 | @lime-6: #a0d911; 132 | @lime-7: color(~`colorPalette('@{lime-6}', 7)`); 133 | @lime-8: color(~`colorPalette('@{lime-6}', 8)`); 134 | @lime-9: color(~`colorPalette('@{lime-6}', 9)`); 135 | @lime-10: color(~`colorPalette('@{lime-6}', 10)`); 136 | 137 | @gold-1: color(~`colorPalette('@{gold-6}', 1)`); 138 | @gold-2: color(~`colorPalette('@{gold-6}', 2)`); 139 | @gold-3: color(~`colorPalette('@{gold-6}', 3)`); 140 | @gold-4: color(~`colorPalette('@{gold-6}', 4)`); 141 | @gold-5: color(~`colorPalette('@{gold-6}', 5)`); 142 | @gold-6: #faad14; 143 | @gold-7: color(~`colorPalette('@{gold-6}', 7)`); 144 | @gold-8: color(~`colorPalette('@{gold-6}', 8)`); 145 | @gold-9: color(~`colorPalette('@{gold-6}', 9)`); 146 | @gold-10: color(~`colorPalette('@{gold-6}', 10)`); 147 | -------------------------------------------------------------------------------- /docs/scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | 3 | const useDevConfig = process.argv[2] === '--dev'; 4 | 5 | process.env.BABEL_ENV = useDevConfig ? 'development' : 'production'; 6 | process.env.NODE_ENV = useDevConfig ? 'development' : 'production'; 7 | 8 | process.env.WEBPACK_BUILDING = 'true'; 9 | 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | require('./config/env'); 15 | 16 | const chalk = require('chalk'); 17 | const fs = require('fs-extra'); 18 | const path = require('path'); 19 | const webpack = require('webpack'); 20 | const config = require(useDevConfig ? './config/webpack.config.dev' : './config/webpack.config.prod'); 21 | const paths = require('./config/paths'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 24 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 25 | const clearConsole = require('react-dev-utils/clearConsole'); 26 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 27 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 28 | const checkMissDependencies = require('./config/checkMissDependencies'); 29 | const { ensureLocals } = require('./i18n'); 30 | const ora = require('ora'); 31 | 32 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 33 | console.log(); 34 | process.exit(1); 35 | } 36 | 37 | ensureLocals(); 38 | 39 | const spinner = ora('webpack启动中...').start(); 40 | // These sizes are pretty large. We'll warn for bundles exceeding them. 41 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 42 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 43 | 44 | checkMissDependencies(spinner) 45 | .then(() => { 46 | return measureFileSizesBeforeBuild(paths.appBuild); 47 | }) 48 | .then(previousFileSizes => { 49 | // Remove all content but keep the directory 50 | fs.emptyDirSync(paths.appBuild); 51 | // Merge with the public folder 52 | copyPublicFolder(); 53 | // Start the webpack build 54 | return build(previousFileSizes); 55 | }) 56 | .then(({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | spinner.warn(chalk.yellow('编译有警告产生:')); 59 | console.log(); 60 | console.log(warnings.join('\n\n')); 61 | console.log(); 62 | // Teach some ESLint tricks. 63 | console.log('\n搜索相关' + chalk.underline(chalk.yellow('关键词')) + '以了解更多关于警告产生的原因.'); 64 | 65 | console.log( 66 | '如果要忽略警告, 可以将 ' + chalk.cyan('// eslint-disable-next-line') + ' 添加到产生警告的代码行上方\n' 67 | ); 68 | 69 | console.log(); 70 | console.log(); 71 | 72 | if (!useDevConfig) { 73 | spinner.fail(chalk.red('请处理所有的错误和警告后再build代码!')); 74 | 75 | console.log(); 76 | console.log(); 77 | process.exit(1); 78 | } 79 | } else { 80 | spinner.succeed(chalk.green('编译通过!!')); 81 | console.log(); 82 | } 83 | 84 | spinner.succeed('gzip后的文件大小:'); 85 | console.log(); 86 | 87 | printFileSizesAfterBuild( 88 | stats, 89 | previousFileSizes, 90 | paths.appBuild, 91 | WARN_AFTER_BUNDLE_GZIP_SIZE, 92 | WARN_AFTER_CHUNK_GZIP_SIZE 93 | ); 94 | 95 | console.log(); 96 | 97 | if (/^http/.test(config.output.publicPath) === false) { 98 | spinner.succeed(chalk.green('项目打包完成,运行以下命令可即时预览:')); 99 | console.log(); 100 | 101 | if (!paths.serve) { 102 | console.log(chalk.cyan('npm') + ' install -g serve'); 103 | } 104 | 105 | console.log(chalk.cyan('serve') + ' -s ' + path.relative('.', paths.appBuild)); 106 | } else { 107 | const publicPath = config.output.publicPath; 108 | 109 | spinner.succeed('项目打包完成,请确保资源已上传到:' + chalk.green(publicPath) + '.'); 110 | } 111 | 112 | console.log(); 113 | }) 114 | .catch(err => { 115 | if (err) { 116 | spinner.fail(chalk.red('编译失败!!')); 117 | console.log(); 118 | console.log(err.message || err); 119 | } 120 | 121 | console.log(); 122 | console.log(); 123 | process.exit(1); 124 | }); 125 | 126 | // Create the production build and print the deployment instructions. 127 | function build(previousFileSizes) { 128 | let packText = useDevConfig ? '启动测试环境打包编译...' : '启动生产环境打包压缩...'; 129 | let startTime = Date.now(); 130 | let timer; 131 | let logProgress = function(stop) { 132 | var text = packText + '已耗时:' + ((Date.now() - startTime) / 1000).toFixed(3) + 's'; 133 | 134 | if (stop) { 135 | clearTimeout(timer); 136 | spinner.succeed(chalk.green(text)); 137 | } else { 138 | spinner.text = chalk.cyan(text); 139 | 140 | timer = setTimeout(logProgress, 100); 141 | } 142 | }; 143 | 144 | clearConsole(); 145 | logProgress(); 146 | 147 | let compiler = webpack(config); 148 | 149 | return new Promise((resolve, reject) => { 150 | compiler.run((err, stats) => { 151 | let messages; 152 | 153 | logProgress(true); // 停止 154 | console.log(); 155 | 156 | if (err) { 157 | if (!err.message) { 158 | return reject(err); 159 | } 160 | 161 | messages = formatWebpackMessages({ 162 | errors: [err.message], 163 | warnings: [] 164 | }); 165 | } else { 166 | messages = formatWebpackMessages(stats.toJson({ all: false, warnings: true, errors: true })); 167 | } 168 | 169 | if (messages.errors.length) { 170 | if (messages.errors.length > 1) { 171 | messages.errors.length = 1; 172 | } 173 | 174 | return reject(new Error(messages.errors.join('\n\n'))); 175 | } 176 | 177 | const resolveArgs = { 178 | stats, 179 | previousFileSizes, 180 | warnings: messages.warnings 181 | }; 182 | 183 | return resolve(resolveArgs); 184 | }); 185 | }); 186 | } 187 | 188 | function copyPublicFolder() { 189 | fs.copySync(paths.appPublic, paths.appBuild, { 190 | dereference: true, 191 | filter: file => { 192 | const relative = path.relative(paths.appPublic, file); 193 | const basename = path.basename(file); 194 | const isDirectory = fs.statSync(file).isDirectory(); 195 | 196 | return isDirectory 197 | ? basename !== 'layout' // layout目录不复制 198 | : !paths.pageEntries.find(name => name + '.html' === relative); 199 | } 200 | }); 201 | } 202 | -------------------------------------------------------------------------------- /dist/react-antd-formutil.esm.production.js: -------------------------------------------------------------------------------- 1 | import{EasyField as e}from"react-formutil";export*from"react-formutil";import t,{Children as r,cloneElement as n,Component as o,createContext as i}from"react";import{Form as a,Switch as u,Checkbox as c,Radio as l,Transfer as s,Pagination as f,Upload as p,Select as d}from"antd";import v from"react-is";import m from"react-fast-compare";function y(e,t,r){if(t in e){Object.defineProperty(e,t,{value:r,enumerable:true,configurable:true,writable:true})}else{e[t]=r}return e}function h(e){"@babel/helpers - typeof";if(typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"){h=function e(t){return typeof t}}else{h=function e(t){return t&&typeof Symbol==="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t}}return h(e)}function b(e,t){if(h(e)!=="object"||e===null)return e;var r=e[Symbol.toPrimitive];if(r!==undefined){var n=r.call(e,t||"default");if(h(n)!=="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function g(e){var t=b(e,"string");return h(t)==="symbol"?t:String(t)}function P(e,t){if(e==null)return{};var r={};var n=Object.keys(e);var o,i;for(i=0;i=0)continue;r[o]=e[o]}return r}function k(e,t){if(e==null)return{};var r=P(e,t);var n,o;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(o=0;o=0)continue;if(!Object.prototype.propertyIsEnumerable.call(e,n))continue;r[n]=e[n]}}return r}function O(e,t){if(!(e instanceof t)){throw new TypeError("Cannot call a class as a function")}}function _(e,t){for(var r=0;r0;var i=r.some((function(e){return e.$dirty}));var a=r.some((function(e){return e.$touched}));var u=r.some((function(e){return e.$focused}));var c=n.map((function(e){return e.$getFirstError()}));return e.getValidationProps(t,o,i,a,u,c)};e.getValidationProps=function(t,r,n,o,i,a){var u=e.checkHasError(t,r,n,o,i);var c={className:[e.props.className,u&&"has-error",r?"is-invalid":"is-valid",n?"is-dirty":"is-pristine",o?"is-touched":"is-untouched",i?"is-focused":"is-unfocused"].filter(Boolean).join(" ")};if(u){Object.assign(c,{validateStatus:"error",help:a})}return c};return e}j(u,[{key:"componentDidMount",value:function e(){var t;(t=this.registerAncestorField)===null||t===void 0?void 0:t.call(this,this.props.name,this.$fieldutil)}},{key:"componentWillUnmount",value:function e(){var t;(t=this.registerAncestorField)===null||t===void 0?void 0:t.call(this,this.props.name,null)}},{key:"render",value:function o(){var i=this;var u=this.props;var c=u.children,l=u.itemProps,s=u.errorLevel,f=s===void 0?I:s,p=u.noStyle,d=k(u,["children","itemProps","errorLevel","noStyle"]);if(!u.name){var v=this.latestValidationProps=this.fetchCurrentValidationProps(f);Promise.resolve().then((function(){if(!m(i.latestValidationProps,i.fetchCurrentValidationProps(f))){i.forceUpdate()}}));return t.createElement(A,{value:this.registerField},t.createElement(a.Item,Object.assign({},d,v),typeof c==="function"?c():c))}if(d.$memo===true){d.__DIFF__={childList:c,compositionValue:this.compositionValue}}else if(Array.isArray(d.$memo)){d.$memo=d.$memo.concat(this.compositionValue)}var h=typeof c==="function"?c:r.only(c);var b=M(h);switch(b){case U:case q:case D:d.__TYPE__="checked";break;case B:if(!("$defaultValue"in d)){d.$defaultValue=1}break;case"checked":case"array":case"object":case"number":case"empty":d.__TYPE__=b;break;default:d.__TYPE__="empty";break}return t.createElement(e,Object.assign({},d,{passUtil:"$fieldutil",render:function e(r){var o,c,s;var v=u.valuePropName,m=v===void 0?"value":v,P=u.changePropName,O=P===void 0?"onChange":P,_=u.focusPropName,j=_===void 0?"onFocus":_,$=u.blurPropName,S=$===void 0?"onBlur":$;var w=r.$fieldutil,C=r[O],E=r[j],V=r[S],F=r[m],T=k(r,["$fieldutil",O,j,S,m].map(g));var N=w.$invalid,A=w.$dirty,I=w.$touched,R=w.$focused,L=w.$getFirstError;var M;switch(b){case U:case q:case D:case"checked":var W=u.checked,z=W===void 0?true:W,G=u.unchecked,J=G===void 0?false:G;M={checked:F===z,onChange:function e(t){var r=t&&t.target?t.target.checked:t;C(r?z:J,t)}};break;case Y:M={targetKeys:F,onChange:C};break;case B:M={current:F,onChange:C};break;case H:M={fileList:(o=F===null||F===void 0?void 0:F.fileList)!==null&&o!==void 0?o:F,onChange:C};break;default:M=(c={onCompositionEnd:function e(t){i.isComposing=false;delete i.compositionValue;C(t)},onCompositionStart:function e(){return i.isComposing=true}},y(c,O,(function(e){if(i.isComposing){var t,r;i.compositionValue=(t=(r=e.target)===null||r===void 0?void 0:r[m])!==null&&t!==void 0?t:e;i.forceUpdate()}else{for(var n=arguments.length,o=new Array(n>1?n-1:0),a=1;a=0)continue;t[o]=e[o]}return t}var b=h;function g(e,r){if(e==null)return{};var t=b(e,r);var n,o;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(o=0;o=0)continue;if(!Object.prototype.propertyIsEnumerable.call(e,n))continue;t[n]=e[n]}}return t}var P=g;function O(e,r){if(!(e instanceof r)){throw new TypeError("Cannot call a class as a function")}}var k=O;function j(e,r){for(var t=0;t0;var i=t.some((function(e){return e.$dirty}));var a=t.some((function(e){return e.$touched}));var u=t.some((function(e){return e.$focused}));var c=n.map((function(e){return e.$getFirstError()}));return e.getValidationProps(r,o,i,a,u,c)};e.getValidationProps=function(r,t,n,o,i,a){var u=e.checkHasError(r,t,n,o,i);var c={className:[e.props.className,u&&"has-error",t?"is-invalid":"is-valid",n?"is-dirty":"is-pristine",o?"is-touched":"is-untouched",i?"is-focused":"is-unfocused"].filter(Boolean).join(" ")};if(u){Object.assign(c,{validateStatus:"error",help:a})}return c};return e}C(i,[{key:"componentDidMount",value:function e(){var r;(r=this.registerAncestorField)===null||r===void 0?void 0:r.call(this,this.props.name,this.$fieldutil)}},{key:"componentWillUnmount",value:function e(){var r;(r=this.registerAncestorField)===null||r===void 0?void 0:r.call(this,this.props.name,null)}},{key:"render",value:function n(){var o=this;var i=this.props;var u=i.children,l=i.itemProps,f=i.errorLevel,p=f===void 0?Y:f,v=i.noStyle,d=P(i,["children","itemProps","errorLevel","noStyle"]);if(!i.name){var m=this.latestValidationProps=this.fetchCurrentValidationProps(p);Promise.resolve().then((function(){if(!c["default"](o.latestValidationProps,o.fetchCurrentValidationProps(p))){o.forceUpdate()}}));return a["default"].createElement(D,{value:this.registerField},a["default"].createElement(t.Form.Item,Object.assign({},d,m),typeof u==="function"?u():u))}if(d.$memo===true){d.__DIFF__={childList:u,compositionValue:this.compositionValue}}else if(Array.isArray(d.$memo)){d.$memo=d.$memo.concat(this.compositionValue)}var h=typeof u==="function"?u:r.Children.only(u);var b=X(h);switch(b){case M:case K:case W:d.__TYPE__="checked";break;case G:if(!("$defaultValue"in d)){d.$defaultValue=1}break;case"checked":case"array":case"object":case"number":case"empty":d.__TYPE__=b;break;default:d.__TYPE__="empty";break}return a["default"].createElement(e.EasyField,Object.assign({},d,{passUtil:"$fieldutil",render:function e(n){var u,c,f;var m=i.valuePropName,g=m===void 0?"value":m,O=i.changePropName,k=O===void 0?"onChange":O,j=i.focusPropName,_=j===void 0?"onFocus":j,C=i.blurPropName,E=C===void 0?"onBlur":C;var S=n.$fieldutil,$=n[k],w=n[_],x=n[E],V=n[g],F=P(n,["$fieldutil",k,_,E,g].map(y));var T=S.$invalid,q=S.$dirty,I=S.$touched,N=S.$focused,R=S.$getFirstError;var A;switch(b){case M:case K:case W:case"checked":var L=i.checked,D=L===void 0?true:L,Y=i.unchecked,B=Y===void 0?false:Y;A={checked:V===D,onChange:function e(r){var t=r&&r.target?r.target.checked:r;$(t?D:B,r)}};break;case z:A={targetKeys:V,onChange:$};break;case G:A={current:V,onChange:$};break;case J:A={fileList:(u=V===null||V===void 0?void 0:V.fileList)!==null&&u!==void 0?u:V,onChange:$};break;default:A=(c={onCompositionEnd:function e(r){o.isComposing=false;delete o.compositionValue;$(r)},onCompositionStart:function e(){return o.isComposing=true}},s(c,k,(function(e){if(o.isComposing){var r,t;o.compositionValue=(r=(t=e.target)===null||t===void 0?void 0:t[g])!==null&&r!==void 0?r:e;o.forceUpdate()}else{for(var n=arguments.length,i=new Array(n>1?n-1:0),a=1;a=0)continue;t[o]=e[o]}return t}var y=d;function m(e,r){if(e==null)return{};var t=y(e,r);var n,o;if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(o=0;o=0)continue;if(!Object.prototype.propertyIsEnumerable.call(e,n))continue;t[n]=e[n]}}return t}var h=m;function b(e,r){if(!(e instanceof r)){throw new TypeError("Cannot call a class as a function")}}var g=b;function $(e,r){for(var t=0;t0;var i=t.some((function(e){return e.$dirty}));var a=t.some((function(e){return e.$touched}));var u=t.some((function(e){return e.$focused}));var f=n.map((function(e){return e.$getFirstError()}));return e.getValidationProps(r,o,i,a,u,f)};e.getValidationProps=function(r,t,n,o,i,a){var u=e.checkHasError(r,t,n,o,i);var f={className:[e.props.className,u&&"has-error",t?"is-invalid":"is-valid",n?"is-dirty":"is-pristine",o?"is-touched":"is-untouched",i?"is-focused":"is-unfocused"].filter(Boolean).join(" ")};if(u){Object.assign(f,{validateStatus:"error",help:a})}return f};return e}O(a,[{key:"componentDidMount",value:function e(){var r;(r=this.registerAncestorField)===null||r===void 0?void 0:r.call(this,this.props.name,this.$fieldutil)}},{key:"componentWillUnmount",value:function e(){var r;(r=this.registerAncestorField)===null||r===void 0?void 0:r.call(this,this.props.name,null)}},{key:"render",value:function e(){var o=this;var a=this.props;var f=a.children,c=a.itemProps,s=a.errorLevel,l=s===void 0?We:s,p=a.noStyle,d=h(a,["children","itemProps","errorLevel","noStyle"]);if(!a.name){var y=this.latestValidationProps=this.fetchCurrentValidationProps(l);Promise.resolve().then((function(){if(!Ue(o.latestValidationProps,o.fetchCurrentValidationProps(l))){o.forceUpdate()}}));return i["default"].createElement(Ke,{value:this.registerField},i["default"].createElement(n.Form.Item,Object.assign({},d,y),typeof f==="function"?f():f))}if(d.$memo===true){d.__DIFF__={childList:f,compositionValue:this.compositionValue}}else if(Array.isArray(d.$memo)){d.$memo=d.$memo.concat(this.compositionValue)}var m=typeof f==="function"?f:t.Children.only(f);var b=or(m);switch(b){case Qe:case Xe:case Ze:d.__TYPE__="checked";break;case rr:if(!("$defaultValue"in d)){d.$defaultValue=1}break;case"checked":case"array":case"object":case"number":case"empty":d.__TYPE__=b;break;default:d.__TYPE__="empty";break}return i["default"].createElement(r.EasyField,Object.assign({},d,{passUtil:"$fieldutil",render:function e(r){var f,s,y;var g=a.valuePropName,$=g===void 0?"value":g,P=a.changePropName,O=P===void 0?"onChange":P,j=a.focusPropName,w=j===void 0?"onFocus":j,S=a.blurPropName,_=S===void 0?"onBlur":S;var k=r.$fieldutil,C=r[O],x=r[w],E=r[_],V=r[$],F=h(r,["$fieldutil",O,w,_,$].map(v));var A=k.$invalid,R=k.$dirty,T=k.$touched,M=k.$focused,I=k.$getFirstError;var L;switch(b){case Qe:case Xe:case Ze:case"checked":var N=a.checked,q=N===void 0?true:N,z=a.unchecked,B=z===void 0?false:z;L={checked:V===q,onChange:function e(r){var t=r&&r.target?r.target.checked:r;C(t?q:B,r)}};break;case er:L={targetKeys:V,onChange:C};break;case rr:L={current:V,onChange:C};break;case tr:L={fileList:(f=V===null||V===void 0?void 0:V.fileList)!==null&&f!==void 0?f:V,onChange:C};break;default:L=(s={onCompositionEnd:function e(r){o.isComposing=false;delete o.compositionValue;C(r)},onCompositionStart:function e(){return o.isComposing=true}},u(s,O,(function(e){if(o.isComposing){var r,t;o.compositionValue=(r=(t=e.target)===null||t===void 0?void 0:t[$])!==null&&r!==void 0?r:e;o.forceUpdate()}else{for(var n=arguments.length,i=new Array(n>1?n-1:0),a=1;a 25 | new Promise(resolve => { 26 | if (port === defaultPort) { 27 | return resolve(port); 28 | } 29 | 30 | spinner.stop(); 31 | clearConsole(); 32 | 33 | const existingProcess = getProcessForPort(defaultPort); 34 | const question = { 35 | type: 'confirm', 36 | name: 'shouldChangePort', 37 | message: 38 | '端口(' + 39 | chalk.yellow(defaultPort) + 40 | ')被占用,可能的程序是: \n ' + 41 | existingProcess + 42 | '\n' + 43 | ' 要换一个端口运行本程序吗?', 44 | default: true 45 | }; 46 | 47 | inquirer.prompt(question).then(answer => { 48 | if (answer.shouldChangePort) { 49 | resolve(port); 50 | } else { 51 | resolve(null); 52 | } 53 | }); 54 | }), 55 | err => { 56 | throw new Error( 57 | chalk.red(`无法为 ${chalk.bold(host)} 找到可用的端口.`) + 58 | '\n' + 59 | ('错误信息: ' + err.message || err) + 60 | '\n' 61 | ); 62 | } 63 | ); 64 | } 65 | 66 | function createCompiler(webpack, config, appName, urls, devSocket, spinner) { 67 | let compiler; 68 | let stime = Date.now(); 69 | 70 | try { 71 | compiler = webpack(config); 72 | } catch (err) { 73 | console.log(chalk.red('启动编译失败!')); 74 | console.log(); 75 | console.log(err.message || err); 76 | console.log(); 77 | process.exit(1); 78 | } 79 | 80 | compiler.hooks.invalid.tap('invalid', () => { 81 | if (isInteractive) { 82 | clearConsole(); 83 | } 84 | 85 | stime = Date.now(); 86 | spinner.text = chalk.cyan('重新编译...'); 87 | }); 88 | 89 | let isFirstCompile = true; 90 | let tsMessagesPromise; 91 | let tsMessagesResolver; 92 | 93 | compiler.hooks.beforeCompile.tap('beforeCompile', () => { 94 | tsMessagesPromise = new Promise(resolve => { 95 | tsMessagesResolver = msgs => resolve(msgs); 96 | }); 97 | }); 98 | 99 | forkTsCheckerWebpackPlugin.getCompilerHooks(compiler).receive.tap('afterTypeScriptCheck', (diagnostics, lints) => { 100 | const allMsgs = [...diagnostics, ...lints]; 101 | const format = message => `${message.file}\n${typescriptFormatter(message, true)}`; 102 | 103 | tsMessagesResolver({ 104 | errors: allMsgs.filter(msg => msg.severity === 'error').map(format), 105 | warnings: allMsgs.filter(msg => msg.severity === 'warning').map(format) 106 | }); 107 | }); 108 | 109 | // "done" event fires when Webpack has finished recompiling the bundle. 110 | // Whether or not you have warnings or errors, you will get this event. 111 | compiler.hooks.done.tap('done', async stats => { 112 | if (isInteractive) { 113 | clearConsole(); 114 | } 115 | 116 | const useTimer = (isTotal = false) => 117 | chalk.grey(`(编译${isTotal ? '总' : '已'}耗时: ${(Date.now() - stime) / 1000}s)`); 118 | 119 | const statsData = stats.toJson({ 120 | all: false, 121 | warnings: true, 122 | errors: true 123 | }); 124 | 125 | if (statsData.errors.length === 0) { 126 | const delayedMsg = setTimeout(() => { 127 | spinner.text = chalk.cyan('文件已编译,正在TSC检查...') + useTimer(); 128 | }, 100); 129 | 130 | const messages = await tsMessagesPromise; 131 | 132 | clearTimeout(delayedMsg); 133 | statsData.errors.push(...messages.errors); 134 | statsData.warnings.push(...messages.warnings); 135 | 136 | stats.compilation.errors.push(...messages.errors); 137 | stats.compilation.warnings.push(...messages.warnings); 138 | 139 | if (messages.errors.length > 0) { 140 | devSocket.errors(messages.errors); 141 | } else if (messages.warnings.length > 0) { 142 | devSocket.warnings(messages.warnings); 143 | } 144 | 145 | if (isInteractive) { 146 | clearConsole(); 147 | } 148 | } 149 | 150 | const messages = formatWebpackMessages(statsData); 151 | const isSuccessful = !messages.errors.length && !messages.warnings.length; 152 | 153 | if (isSuccessful && (isInteractive || isFirstCompile)) { 154 | spinner.succeed(chalk.green('编译通过!' + useTimer(true))); 155 | console.log(); 156 | spinner.succeed(chalk.green('应用(' + appName + ')已启动:')); 157 | console.log(); 158 | 159 | if (urls.lanUrlForTerminal) { 160 | console.log(` ${chalk.bold('本地:')} ${chalk.cyan(urls.localUrlForTerminal)}`); 161 | console.log(` ${chalk.bold('远程:')} ${chalk.cyan(urls.lanUrlForTerminal)}`); 162 | } else { 163 | console.log(` ${urls.localUrlForTerminal}`); 164 | } 165 | } 166 | 167 | isFirstCompile = false; 168 | 169 | // If errors exist, only show errors. 170 | if (messages.errors.length) { 171 | if (messages.errors.length > 1) { 172 | messages.errors.length = 1; 173 | } 174 | 175 | spinner.fail(chalk.red('编译失败!!' + useTimer(true))); 176 | console.log(); 177 | console.log(messages.errors.join('\n\n')); 178 | console.log(); 179 | } 180 | 181 | // Show warnings if no errors were found. 182 | if (messages.warnings.length) { 183 | spinner.warn(chalk.yellow('编译有警告产生:' + useTimer(true))); 184 | console.log(); 185 | console.log(messages.warnings.join('\n\n')); 186 | console.log(); 187 | 188 | // Teach some ESLint tricks. 189 | console.log('\n搜索相关' + chalk.underline(chalk.yellow('关键词')) + '以了解更多关于警告产生的原因.'); 190 | 191 | console.log( 192 | '如果要忽略警告, 可以将 ' + chalk.cyan('// eslint-disable-next-line') + ' 添加到产生警告的代码行上方\n' 193 | ); 194 | } 195 | 196 | console.log(); 197 | spinner.text = chalk.cyan('webpack运行中...'); 198 | spinner.render().start(); 199 | }); 200 | 201 | return compiler; 202 | } 203 | 204 | function createDevServerConfig(proxy, allowedHost) { 205 | return { 206 | headers: { 207 | 'Access-Control-Allow-Origin': '*', 208 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, HEAD, DELETE, FETCH' 209 | }, 210 | disableHostCheck: !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true', 211 | // Enable gzip 212 | compress: true, 213 | clientLogLevel: 'none', 214 | contentBase: paths.appPublic, 215 | watchContentBase: true, 216 | hot: true, 217 | transportMode: 'ws', 218 | injectClient: false, 219 | publicPath: config.output.publicPath, 220 | quiet: true, 221 | watchOptions: { 222 | ignored: ignoredFiles(paths.appSrc) 223 | }, 224 | https: process.env.HTTPS === 'true', 225 | host: process.env.HOST || '0.0.0.0', 226 | overlay: false, 227 | historyApiFallback: pkg.noRewrite 228 | ? false 229 | : { 230 | disableDotRule: true 231 | }, 232 | public: allowedHost, 233 | proxy, 234 | before(app, server) { 235 | if (fs.existsSync(paths.proxySetup)) { 236 | require(paths.proxySetup)(app); 237 | } 238 | 239 | app.use(evalSourceMapMiddleware(server)); 240 | app.use(errorOverlayMiddleware()); 241 | app.use(noopServiceWorkerMiddleware()); 242 | } 243 | }; 244 | } 245 | 246 | function onProxyError(proxy) { 247 | return function(err, req, res) { 248 | var host = req.headers && req.headers.host; 249 | 250 | console.log( 251 | chalk.red('代理错误:') + 252 | '无法将 ' + 253 | chalk.cyan(req.url) + 254 | ' 的请求从 ' + 255 | chalk.cyan(host) + 256 | ' 转发到 ' + 257 | chalk.cyan(proxy) + 258 | '.' 259 | ); 260 | 261 | console.log( 262 | '点击 https://nodejs.org/api/errors.html#errors_common_system_errors 查看更多信息 (' + 263 | chalk.cyan(err.code) + 264 | ').' 265 | ); 266 | 267 | console.log(); 268 | 269 | if (res.writeHead && !res.headersSent) { 270 | res.writeHead(500); 271 | } 272 | 273 | res.end('代理错误: 无法将 ' + req.url + ' 的请求从 ' + host + ' 转发到 ' + proxy + ' (' + err.code + ').'); 274 | }; 275 | } 276 | 277 | function mayProxy(pathname) { 278 | const maybePublicPath = path.resolve(paths.appPublic, pathname.slice(1)); 279 | 280 | return !fs.existsSync(maybePublicPath); 281 | } 282 | 283 | function prepareProxy(proxy) { 284 | if (!proxy) { 285 | return undefined; 286 | } 287 | 288 | if (typeof proxy === 'object') { 289 | return Object.keys(proxy).map(function(path) { 290 | const opt = 291 | typeof proxy[path] === 'object' 292 | ? proxy[path] 293 | : { 294 | target: proxy[path] 295 | }; 296 | const target = opt.target; 297 | 298 | return Object.assign({}, opt, { 299 | context: function(pathname) { 300 | return mayProxy(pathname) && pathname.match(path); 301 | }, 302 | onProxyReq: proxyReq => { 303 | if (proxyReq.getHeader('origin')) { 304 | proxyReq.setHeader('origin', target); 305 | } 306 | }, 307 | onError: onProxyError(target) 308 | }); 309 | }); 310 | } 311 | 312 | if (!/^http(s)?:\/\//.test(proxy)) { 313 | console.log(chalk.red('proxy 只能是一个 http:// 或者 https:// 开头的字符串或者一个object配置')); 314 | console.log(chalk.red('当前 proxy 的类型是 "' + typeof proxy + '"。')); 315 | console.log(chalk.red('你可以从 package.json 中移除它,或者设置一个字符串地址(目标服务器)')); 316 | process.exit(1); 317 | } 318 | 319 | return [ 320 | { 321 | target: proxy, 322 | logLevel: 'silent', 323 | context: function(pathname, req) { 324 | return ( 325 | req.method !== 'GET' || 326 | (mayProxy(pathname) && req.headers.accept && req.headers.accept.indexOf('text/html') === -1) 327 | ); 328 | }, 329 | onProxyReq: proxyReq => { 330 | if (proxyReq.getHeader('origin')) { 331 | proxyReq.setHeader('origin', proxy); 332 | } 333 | }, 334 | onError: onProxyError(proxy), 335 | secure: false, 336 | changeOrigin: true, 337 | ws: true, 338 | xfwd: true 339 | } 340 | ]; 341 | } 342 | 343 | module.exports = { 344 | choosePort, 345 | prepareProxy, 346 | createCompiler, 347 | createDevServerConfig 348 | }; 349 | -------------------------------------------------------------------------------- /docs/scripts/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const resolve = require('resolve'); 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 8 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 9 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 10 | const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); 11 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin'); 12 | const getClientEnvironment = require('./env'); 13 | const paths = require('./paths'); 14 | const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); 15 | const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin'); 16 | const pkg = require(paths.appPackageJson); 17 | const isBuilding = process.env.WEBPACK_BUILDING === 'true'; 18 | 19 | const publicPath = isBuilding ? path.join(pkg.noRewrite ? '.' : process.env.BASE_NAME || '', '/') : '/'; 20 | const publicUrl = publicPath.slice(0, -1); 21 | const env = getClientEnvironment(publicUrl); 22 | const injects = []; 23 | 24 | // eslint-disable-next-line 25 | const matchScriptStylePattern = /<\!--\s*script:\s*([\w]+)(?:\.[jt]sx?)?\s*-->/g; 26 | const hotDev = require.resolve('react-dev-utils/webpackHotDevClient'); 27 | const babelOption = { 28 | babelrc: false, 29 | configFile: false, 30 | compact: false, 31 | presets: [[require.resolve('babel-preset-react-app/dependencies'), { helpers: true }]], 32 | cacheDirectory: true, 33 | cacheCompression: false, 34 | sourceMaps: false 35 | }; 36 | 37 | paths.pageEntries.forEach(function(name) { 38 | var chunks = ['_vendor_']; 39 | var file = path.resolve(paths.appPublic, name + '.html'); 40 | 41 | if (paths.entries[name]) { 42 | chunks.push(name); 43 | } 44 | 45 | var contents = fs.readFileSync(file); 46 | var matches; 47 | 48 | while ((matches = matchScriptStylePattern.exec(contents))) { 49 | chunks.push(matches[1]); 50 | } 51 | 52 | injects.push( 53 | new HtmlWebpackPlugin({ 54 | chunks: chunks, 55 | filename: name + '.html', 56 | template: file, 57 | inject: true, 58 | chunksSortMode: 'manual' 59 | }) 60 | ); 61 | }); 62 | 63 | module.exports = { 64 | mode: 'development', 65 | devtool: 'cheap-module-source-map', 66 | entry: Object.assign( 67 | { 68 | _vendor_: [require.resolve('./polyfills'), !isBuilding && hotDev].filter(Boolean).concat(pkg.vendor || []) 69 | }, 70 | paths.entries 71 | ), 72 | output: { 73 | path: paths.appBuild, 74 | pathinfo: true, 75 | filename: 'static/js/[name].[hash:8].js', 76 | chunkFilename: 'static/js/[name].[hash:8].js', 77 | publicPath: publicPath, 78 | crossOriginLoading: 'anonymous', 79 | devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), 80 | globalObject: 'this' 81 | }, 82 | optimization: { 83 | runtimeChunk: 'single', 84 | splitChunks: { 85 | chunks: 'async', 86 | name: false, 87 | cacheGroups: { 88 | vendors: { 89 | chunks: 'all', 90 | test: '_vendor_', 91 | name: 'vendor' 92 | }, 93 | i18n: { 94 | chunks: 'all', 95 | test: /utils\/i18n|locals\/\w+\.json/, 96 | enforce: true, 97 | name: 'i18n' 98 | } 99 | } 100 | } 101 | }, 102 | resolve: { 103 | modules: ['node_modules', paths.appNodeModules, paths.root].concat(paths.nodePaths), 104 | extensions: paths.moduleFileExtensions, 105 | alias: Object.assign( 106 | { 107 | 'react-native': 'react-native-web' 108 | }, 109 | paths.alias 110 | ), 111 | plugins: [ 112 | new DirectoryNamedWebpackPlugin({ 113 | honorIndex: true, 114 | exclude: /node_modules|libs/ 115 | }) 116 | ] 117 | }, 118 | 119 | module: { 120 | strictExportPresence: true, 121 | rules: [ 122 | { parser: { requireEnsure: false } }, 123 | 124 | { 125 | test: /\.(js|mjs|jsx|ts|tsx)$/, 126 | enforce: 'pre', 127 | use: [ 128 | { 129 | options: { 130 | cache: true, 131 | formatter: require.resolve('react-dev-utils/eslintFormatter'), 132 | eslintPath: require.resolve('eslint'), 133 | resolvePluginsRelativeTo: __dirname 134 | }, 135 | loader: require.resolve('eslint-loader') 136 | } 137 | ], 138 | include: [paths.formutilSrc, paths.appSrc] 139 | }, 140 | { 141 | oneOf: [ 142 | { 143 | test: /\.html$/, 144 | use: [ 145 | { 146 | loader: require.resolve('babel-loader'), 147 | options: babelOption 148 | }, 149 | { 150 | loader: require.resolve('html-loader'), 151 | options: { 152 | url(url) { 153 | return !/\.(webp|png|jpeg|jpg|gif|svg|mp3|wmv|mp4|ogg|webm|s[ac]ss|css|less|m?[tj]sx?)$/.test( 154 | url 155 | ); 156 | }, 157 | import: true 158 | } 159 | } 160 | ] 161 | }, 162 | { 163 | test: /\.(js|mjs|jsx|ts|tsx)$/, 164 | include: [paths.formutilSrc, paths.appSrc], 165 | loader: require.resolve('babel-loader'), 166 | options: { 167 | customize: require.resolve('babel-preset-react-app/webpack-overrides'), 168 | plugins: [ 169 | [ 170 | require.resolve('babel-plugin-named-asset-import'), 171 | { 172 | loaderMap: { 173 | svg: { 174 | ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]' 175 | } 176 | } 177 | } 178 | ] 179 | ], 180 | cacheDirectory: true, 181 | cacheCompression: false, 182 | rootMode: 'upward' 183 | } 184 | }, 185 | { 186 | test: /\.(js|mjs)$/, 187 | exclude: /@babel(?:\/|\\{1,2})runtime/, 188 | loader: require.resolve('babel-loader'), 189 | options: babelOption 190 | }, 191 | { 192 | test: /\.css$/, 193 | exclude: /\.module\.css$/, 194 | use: getStyleLoaders({ 195 | importLoaders: 1 196 | }) 197 | }, 198 | // Adds support for CSS Modules (https://github.com/css-modules/css-modules) 199 | // using the extension .module.css 200 | { 201 | test: /\.module\.css$/, 202 | use: getStyleLoaders({ 203 | importLoaders: 1, 204 | modules: { 205 | getLocalIdent: getCSSModuleLocalIdent 206 | } 207 | }) 208 | }, 209 | { 210 | test: /\.s[ac]ss$/, 211 | exclude: /\.module\.s[ac]ss$/, 212 | use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader') 213 | }, 214 | { 215 | test: /\.module\.s[ac]ss$/, 216 | use: getStyleLoaders( 217 | { 218 | importLoaders: 2, 219 | modules: { 220 | getLocalIdent: getCSSModuleLocalIdent 221 | } 222 | }, 223 | 'sass-loader' 224 | ) 225 | }, 226 | { 227 | test: /\.less$/, 228 | exclude: /\.module\.less$/, 229 | use: getStyleLoaders({ importLoaders: 2 }, 'less-loader') 230 | }, 231 | { 232 | test: /\.module\.less$/, 233 | use: getStyleLoaders( 234 | { 235 | importLoaders: 2, 236 | modules: { 237 | getLocalIdent: getCSSModuleLocalIdent 238 | } 239 | }, 240 | 'less-loader' 241 | ) 242 | }, 243 | { 244 | test: /\.(txt|htm)$/, 245 | loader: require.resolve('raw-loader') 246 | }, 247 | { 248 | test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)$/, 249 | loader: require.resolve('file-loader'), 250 | options: { 251 | name: 'static/media/[name].[hash:8].[ext]' 252 | } 253 | }, 254 | { 255 | exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.(txt|htm)$/], 256 | loader: require.resolve('file-loader'), 257 | options: { 258 | name: 'static/images/[name].[hash:8].[ext]' 259 | } 260 | } 261 | ] 262 | } 263 | ] 264 | }, 265 | plugins: injects 266 | .concat([ 267 | isBuilding && 268 | new MiniCssExtractPlugin({ 269 | filename: 'static/css/[name].[hash:8].css', 270 | ignoreOrder: !!pkg.ignoreCssOrderWarnings || process.env.IGNORE_CSS_ORDER_WARNINGS === 'true' 271 | }), 272 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), 273 | new ModuleNotFoundPlugin(paths.root), 274 | new webpack.EnvironmentPlugin(env.raw), 275 | new CaseSensitivePathsPlugin(), 276 | new webpack.IgnorePlugin({ 277 | resourceRegExp: /^\.\/locale$/, 278 | contextRegExp: /moment$/ 279 | }), 280 | new ForkTsCheckerWebpackPlugin({ 281 | typescript: resolve.sync('typescript', { 282 | basedir: paths.appNodeModules 283 | }), 284 | async: true, 285 | useTypescriptIncrementalApi: true, 286 | checkSyntacticErrors: true, 287 | tsconfig: paths.appTsConfig, 288 | compilerOptions: { 289 | jsx: 'preserve', 290 | checkJs: false 291 | }, 292 | reportFiles: ['**/*.(ts|tsx)', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], 293 | silent: true 294 | }), 295 | new webpack.BannerPlugin({ 296 | banner: '@author ' + pkg.author, 297 | entryOnly: true 298 | }) 299 | ]) 300 | .filter(Boolean), 301 | 302 | node: { 303 | dgram: 'empty', 304 | fs: 'empty', 305 | net: 'empty', 306 | tls: 'empty', 307 | child_process: 'empty' 308 | }, 309 | 310 | performance: false 311 | }; 312 | 313 | function getStyleLoaders(cssOptions, preProcessor) { 314 | const loaders = [ 315 | isBuilding 316 | ? { 317 | loader: MiniCssExtractPlugin.loader, 318 | options: { publicPath: '../../', sourceMap: true, esModule: true } 319 | } 320 | : require.resolve('style-loader'), 321 | { 322 | loader: require.resolve('css-loader'), 323 | options: Object.assign({ sourceMap: true }, cssOptions) 324 | }, 325 | { 326 | loader: require.resolve('postcss-loader'), 327 | options: { 328 | ident: 'postcss', 329 | plugins: () => [ 330 | require('postcss-flexbugs-fixes'), 331 | require('postcss-preset-env')({ 332 | autoprefixer: { 333 | flexbox: 'no-2009' 334 | }, 335 | stage: 3 336 | }) 337 | ], 338 | sourceMap: true 339 | } 340 | } 341 | ]; 342 | 343 | if (preProcessor) { 344 | loaders.push({ 345 | loader: require.resolve(preProcessor), 346 | options: Object.assign( 347 | {}, 348 | { sourceMap: true }, 349 | preProcessor === 'less-loader' 350 | ? { 351 | javascriptEnabled: true 352 | } 353 | : { 354 | implementation: require('sass') 355 | } 356 | ) 357 | }); 358 | } 359 | 360 | return loaders; 361 | } 362 | -------------------------------------------------------------------------------- /src/FormItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, cloneElement, Children, createContext } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, Switch, Checkbox, Radio, Transfer, Pagination, Upload, Select } from 'antd'; 4 | import { EasyField } from 'react-formutil'; 5 | import reactIs from 'react-is'; 6 | import isEqual from 'react-fast-compare'; 7 | 8 | const { isValidElementType } = reactIs; 9 | const { Consumer, Provider } = createContext(); 10 | 11 | let errorLevelGlobal = 1; 12 | 13 | /** 14 | * 0 dirty & invalid & touched 15 | * 1 dirty & invalid 16 | * 2 invalid 17 | */ 18 | export const setErrorLevel = function (level) { 19 | errorLevelGlobal = level; 20 | }; 21 | 22 | const isUglify = Form.Item.name !== 'FormItem'; 23 | 24 | const _Switch = isUglify ? Switch : 'Switch'; 25 | const _Checkbox = isUglify ? Checkbox : 'Checkbox'; 26 | const _Radio = isUglify ? Radio : 'Radio'; 27 | const _Transfer = isUglify ? Transfer : 'Transfer'; 28 | const _Pagination = isUglify ? Pagination : 'Pagination'; 29 | const _Upload = isUglify ? Upload : 'Upload'; 30 | const _Select = isUglify ? Select : 'Select'; 31 | 32 | /* // just for debug 33 | * console.log( 34 | * Switch.name || Switch.displayName, 35 | * Checkbox.name || Checkbox.displayName, 36 | * Radio.name || Radio.displayName, 37 | * Transfer.name || Transfer.displayName, 38 | * Select.name || Select.displayName, 39 | * Upload.name || Upload.displayName, 40 | * Pagination.name || Pagination.displayName 41 | * ); */ 42 | 43 | function getChildComponent(children) { 44 | if (children) { 45 | const childrenType = children.type; 46 | 47 | if (isValidElementType(childrenType)) { 48 | // SomeComponent.formutiType = xx 49 | if (childrenType.formutilType) { 50 | return childrenType.formutilType; 51 | } 52 | 53 | // 54 | if (typeof childrenType === 'string' && children.props.type) { 55 | return children.props.type; 56 | } 57 | 58 | if (!isUglify) { 59 | const name = childrenType.displayName || childrenType.name; 60 | 61 | if (name) { 62 | return name; 63 | } 64 | 65 | if (childrenType.render?.name === 'InternalSelect') { 66 | return 'Select'; 67 | } 68 | 69 | return childrenType; 70 | } 71 | } 72 | 73 | return childrenType || children; 74 | } 75 | } 76 | 77 | class FormItem extends Component { 78 | static propTypes = { 79 | children(props, ...args) { 80 | if ('name' in props) { 81 | return PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired(props, ...args); 82 | } 83 | 84 | return PropTypes.node.isRequired(props, ...args); 85 | }, 86 | itemProps: PropTypes.object, //传递给antd的Form.Item的属性 87 | errorLevel: PropTypes.oneOf([0, 1, 2, 'off']), 88 | noStyle: PropTypes.bool 89 | //$parser $formatter checked unchecked $validators validMessage等传递给 EasyField 组件的额外参数 90 | }; 91 | 92 | fields = {}; 93 | registerField = (name, $fieldutil) => ($fieldutil ? (this.fields[name] = $fieldutil) : delete this.fields[name]); 94 | latestValidationProps = null; 95 | checkHasError = (errorLevel, $invalid, $dirty, $touched, $focused) => { 96 | switch (errorLevel) { 97 | case 0: 98 | return $invalid && $dirty && $touched; 99 | case 1: 100 | return $invalid && $dirty; 101 | case 2: 102 | return $invalid; 103 | default: 104 | return false; 105 | } 106 | }; 107 | 108 | fetchCurrentValidationProps = errorLevel => { 109 | const allFieldutils = Object.keys(this.fields).map(name => this.fields[name].$new()); 110 | const errFieldutils = allFieldutils.filter($fieldutil => $fieldutil.$invalid); 111 | 112 | const $invalid = errFieldutils.length > 0; 113 | const $dirty = allFieldutils.some($fieldutil => $fieldutil.$dirty); 114 | const $touched = allFieldutils.some($fieldutil => $fieldutil.$touched); 115 | const $focused = allFieldutils.some($fieldutil => $fieldutil.$focused); 116 | const $errors = errFieldutils.map($fieldutil => $fieldutil.$getFirstError()); 117 | 118 | return this.getValidationProps(errorLevel, $invalid, $dirty, $touched, $focused, $errors); 119 | }; 120 | 121 | getValidationProps = (errorLevel, $invalid, $dirty, $touched, $focused, $errors) => { 122 | const hasError = this.checkHasError(errorLevel, $invalid, $dirty, $touched, $focused); 123 | 124 | const validationProps = { 125 | className: [ 126 | this.props.className, 127 | hasError && 'has-error', 128 | $invalid ? 'is-invalid' : 'is-valid', 129 | $dirty ? 'is-dirty' : 'is-pristine', 130 | $touched ? 'is-touched' : 'is-untouched', 131 | $focused ? 'is-focused' : 'is-unfocused' 132 | ] 133 | .filter(Boolean) 134 | .join(' ') 135 | }; 136 | 137 | if (hasError) { 138 | Object.assign(validationProps, { 139 | validateStatus: 'error', 140 | help: $errors 141 | }); 142 | } 143 | 144 | return validationProps; 145 | }; 146 | 147 | componentDidMount() { 148 | // eslint-disable-next-line 149 | this.registerAncestorField?.(this.props.name, this.$fieldutil); 150 | } 151 | 152 | componentWillUnmount() { 153 | // eslint-disable-next-line 154 | this.registerAncestorField?.(this.props.name, null); 155 | } 156 | 157 | render() { 158 | const props = this.props; 159 | const { children: childList, itemProps, errorLevel = errorLevelGlobal, noStyle, ...fieldProps } = props; 160 | 161 | if (!props.name) { 162 | const validationProps = (this.latestValidationProps = this.fetchCurrentValidationProps(errorLevel)); 163 | 164 | /** 165 | * 检查下最新的校验状态和当前是否一致,不一致的话需要强制刷新下 166 | */ 167 | Promise.resolve().then(() => { 168 | if (!isEqual(this.latestValidationProps, this.fetchCurrentValidationProps(errorLevel))) { 169 | this.forceUpdate(); 170 | } 171 | }); 172 | 173 | return ( 174 | 175 | 176 | {typeof childList === 'function' ? childList() : childList} 177 | 178 | 179 | ); 180 | } 181 | 182 | // If $memo is true, pass the children to Field for SCU diffing. 183 | if (fieldProps.$memo === true) { 184 | fieldProps.__DIFF__ = { 185 | childList, 186 | compositionValue: this.compositionValue 187 | }; 188 | } else if (Array.isArray(fieldProps.$memo)) { 189 | fieldProps.$memo = fieldProps.$memo.concat(this.compositionValue); 190 | } 191 | 192 | const children = typeof childList === 'function' ? childList : Children.only(childList); 193 | let component = getChildComponent(children); 194 | 195 | switch (component) { 196 | case _Switch: 197 | case _Checkbox: 198 | case _Radio: 199 | fieldProps.__TYPE__ = 'checked'; 200 | break; 201 | 202 | case _Pagination: 203 | if (!('$defaultValue' in fieldProps)) { 204 | fieldProps.$defaultValue = 1; 205 | } 206 | 207 | break; 208 | 209 | case 'checked': 210 | case 'array': 211 | case 'object': 212 | case 'number': 213 | case 'empty': 214 | fieldProps.__TYPE__ = component; 215 | break; 216 | 217 | default: 218 | fieldProps.__TYPE__ = 'empty'; 219 | break; 220 | } 221 | 222 | return ( 223 | { 227 | const { 228 | valuePropName = 'value', 229 | changePropName = 'onChange', 230 | focusPropName = 'onFocus', 231 | blurPropName = 'onBlur' 232 | } = props; 233 | const { 234 | $fieldutil, 235 | 236 | [changePropName]: onChange, 237 | [focusPropName]: onFocus, 238 | [blurPropName]: onBlur, 239 | [valuePropName]: value, 240 | 241 | ...restProps 242 | } = $handleProps; 243 | const { $invalid, $dirty, $touched, $focused, $getFirstError } = $fieldutil; 244 | 245 | let childProps; 246 | 247 | switch (component) { 248 | case _Switch: 249 | case _Checkbox: 250 | case _Radio: 251 | case 'checked': 252 | const { checked = true, unchecked = false } = props; 253 | 254 | childProps = { 255 | checked: value === checked, 256 | onChange: ev => { 257 | const newValue = ev && ev.target ? ev.target.checked : ev; 258 | 259 | onChange(newValue ? checked : unchecked, ev); 260 | } 261 | }; 262 | 263 | break; 264 | 265 | case _Transfer: 266 | childProps = { 267 | targetKeys: value, 268 | onChange 269 | }; 270 | 271 | break; 272 | 273 | case _Pagination: 274 | childProps = { 275 | current: value, 276 | onChange 277 | }; 278 | 279 | break; 280 | 281 | case _Upload: 282 | childProps = { 283 | fileList: value?.fileList ?? value, 284 | onChange 285 | }; 286 | 287 | break; 288 | 289 | default: 290 | childProps = { 291 | onCompositionEnd: ev => { 292 | this.isComposing = false; 293 | delete this.compositionValue; 294 | onChange(ev); 295 | }, 296 | onCompositionStart: () => (this.isComposing = true), 297 | [changePropName]: (ev, ...rest) => { 298 | if (this.isComposing) { 299 | this.compositionValue = ev.target?.[valuePropName] ?? ev; 300 | this.forceUpdate(); 301 | } else { 302 | onChange(ev, ...rest); 303 | } 304 | }, 305 | [valuePropName]: 'compositionValue' in this ? this.compositionValue : value, 306 | [blurPropName]: (...args) => { 307 | if (this.isComposing) { 308 | this.isComposing = false; 309 | delete this.compositionValue; 310 | onChange(...args); 311 | } 312 | 313 | return onBlur(...args); 314 | } 315 | }; 316 | 317 | break; 318 | } 319 | 320 | /** 321 | * Select组件移除composition相关事件 322 | */ 323 | if (component === _Select) { 324 | delete childProps.onCompositionStart; 325 | delete childProps.onCompositionEnd; 326 | } 327 | 328 | childProps = Object.assign( 329 | { 330 | [focusPropName]: onFocus, 331 | [blurPropName]: onBlur 332 | }, 333 | childProps 334 | ); 335 | 336 | // ansure 'required' could pass to Form.Item 337 | if (!restProps.required && fieldProps.required && (!itemProps || !('required' in itemProps))) { 338 | restProps.required = true; 339 | } 340 | 341 | const fieldInstance = 342 | typeof children === 'function' ? children(childProps) : cloneElement(children, childProps); 343 | 344 | return ( 345 | 346 | {registerField => { 347 | if (noStyle) { 348 | this.$fieldutil = $fieldutil; 349 | this.registerAncestorField = registerField; 350 | 351 | return fieldInstance; 352 | } 353 | 354 | const validationProps = this.getValidationProps( 355 | errorLevel, 356 | $invalid, 357 | $dirty, 358 | $touched, 359 | $focused, 360 | $getFirstError() 361 | ); 362 | 363 | return ( 364 | 365 | {fieldInstance} 366 | 367 | ); 368 | }} 369 | 370 | ); 371 | }} 372 | /> 373 | ); 374 | } 375 | } 376 | 377 | export default FormItem; 378 | -------------------------------------------------------------------------------- /docs/static/css/modules/antd-custom.less: -------------------------------------------------------------------------------- 1 | /* stylelint-disable at-rule-empty-line-before,at-rule-name-space-after,at-rule-no-unknown */ 2 | @import 'color/colors'; 3 | 4 | // The prefix to use on all css classes from ant. 5 | @ant-prefix : ant; 6 | 7 | // -------- Colors ----------- 8 | @primary-color : @blue-6; 9 | @info-color : @blue-6; 10 | @success-color : @green-6; 11 | @processing-color : @blue-6; 12 | @error-color : @red-6; 13 | @highlight-color : @red-6; 14 | @warning-color : @gold-6; 15 | @normal-color : #d9d9d9; 16 | 17 | // Color used by default to control hover and active backgrounds and for 18 | // alert info backgrounds. 19 | @primary-1: color(~`colorPalette('@{primary-color}', 1)`); // replace tint(@primary-color, 90%) 20 | @primary-2: color(~`colorPalette('@{primary-color}', 2)`); // replace tint(@primary-color, 80%) 21 | @primary-3: color(~`colorPalette('@{primary-color}', 3)`); // unused 22 | @primary-4: color(~`colorPalette('@{primary-color}', 4)`); // unused 23 | @primary-5: color( 24 | ~`colorPalette('@{primary-color}', 5)` 25 | ); // color used to control the text color in many active and hover states, replace tint(@primary-color, 20%) 26 | @primary-6: @primary-color; // color used to control the text color of active buttons, don't use, use @primary-color 27 | @primary-7: color(~`colorPalette('@{primary-color}', 7)`); // replace shade(@primary-color, 5%) 28 | @primary-8: color(~`colorPalette('@{primary-color}', 8)`); // unused 29 | @primary-9: color(~`colorPalette('@{primary-color}', 9)`); // unused 30 | @primary-10: color(~`colorPalette('@{primary-color}', 10)`); // unused 31 | 32 | // Base Scaffolding Variables 33 | // --- 34 | 35 | // Background color for `` 36 | @body-background : #fff; 37 | // Base background color for most components 38 | @component-background : #fff; 39 | @font-family-no-number : "Chinese Quote", 40 | -apple-system, 41 | BlinkMacSystemFont, 42 | "Segoe UI", 43 | Roboto, 44 | "PingFang SC", 45 | "Hiragino Sans GB", 46 | "Microsoft YaHei", 47 | "Helvetica Neue", 48 | Helvetica, 49 | Arial, 50 | sans-serif; 51 | @font-family : "Monospaced Number", @font-family-no-number; 52 | @code-family : Consolas, Menlo, Courier, monospace; 53 | @heading-color : fade(#000, 85%); 54 | @text-color : fade(#000, 65%); 55 | @text-color-secondary : fade(#000, 45%); 56 | @heading-color-dark : fade(#fff, 100%); 57 | @text-color-dark : fade(#fff,85%); 58 | @text-color-secondary-dark: fade(#fff, 65%); 59 | @font-size-base : 14px; 60 | @font-size-lg : @font-size-base + 2px; 61 | @font-size-sm : 12px; 62 | @line-height-base : 1.5; 63 | @border-radius-base : 4px; 64 | @border-radius-sm : 2px; 65 | 66 | // vertical paddings 67 | @padding-lg : 24px; // containers 68 | @padding-md : 16px; // small containers and buttons 69 | @padding-sm : 12px; // Form controls and items 70 | @padding-xs : 8px; // small items 71 | 72 | // vertical padding for all form controls 73 | @control-padding-horizontal: @padding-sm; 74 | @control-padding-horizontal-sm: @padding-xs; 75 | 76 | // The background colors for active and hover states for things like 77 | // list items or table cells. 78 | @item-active-bg : @primary-1; 79 | @item-hover-bg : @primary-1; 80 | 81 | // ICONFONT 82 | @iconfont-css-prefix : anticon; 83 | @icon-url : 'https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i'; 84 | 85 | // LINK 86 | @link-color : @primary-color; 87 | @link-hover-color : color(~`colorPalette('@{link-color}', 5)`); 88 | @link-active-color : color(~`colorPalette('@{link-color}', 7)`); 89 | @link-decoration : none; 90 | @link-hover-decoration : none; 91 | 92 | // Animation 93 | @ease-out : cubic-bezier(0.215, 0.61, 0.355, 1); 94 | @ease-in : cubic-bezier(0.55, 0.055, 0.675, 0.19); 95 | @ease-in-out : cubic-bezier(0.645, 0.045, 0.355, 1); 96 | @ease-out-back : cubic-bezier(0.12, 0.4, 0.29, 1.46); 97 | @ease-in-back : cubic-bezier(0.71, -0.46, 0.88, 0.6); 98 | @ease-in-out-back : cubic-bezier(0.71, -0.46, 0.29, 1.46); 99 | @ease-out-circ : cubic-bezier(0.08, 0.82, 0.17, 1); 100 | @ease-in-circ : cubic-bezier(0.6, 0.04, 0.98, 0.34); 101 | @ease-in-out-circ : cubic-bezier(0.78, 0.14, 0.15, 0.86); 102 | @ease-out-quint : cubic-bezier(0.23, 1, 0.32, 1); 103 | @ease-in-quint : cubic-bezier(0.755, 0.05, 0.855, 0.06); 104 | @ease-in-out-quint : cubic-bezier(0.86, 0, 0.07, 1); 105 | 106 | // Border color 107 | @border-color-base : hsv(0, 0, 85%); // base border outline a component 108 | @border-color-split : hsv(0, 0, 91%); // split border inside a component 109 | @border-width-base : 1px; // width of the border for a component 110 | @border-style-base : solid; // style of a components border 111 | 112 | // Outline 113 | @outline-blur-size : 0; 114 | @outline-width : 2px; 115 | @outline-color : @primary-color; 116 | 117 | @background-color-light : hsv(0, 0, 98%); // background of header and selected item 118 | @background-color-base : hsv(0, 0, 96%); // Default grey background color 119 | 120 | // Disabled states 121 | @disabled-color : fade(#000, 25%); 122 | @disabled-bg : @background-color-base; 123 | @disabled-color-dark : fade(#fff, 35%); 124 | 125 | // Shadow 126 | @shadow-color : rgba(0, 0, 0, .15); 127 | @box-shadow-base : @shadow-1-down; 128 | @shadow-1-up : 0 2px 8px @shadow-color; 129 | @shadow-1-down : 0 2px 8px @shadow-color; 130 | @shadow-1-left : -2px 0 8px @shadow-color; 131 | @shadow-1-right : 2px 0 8px @shadow-color; 132 | @shadow-2 : 0 4px 12px @shadow-color; 133 | 134 | // Buttons 135 | @btn-font-weight : 400; 136 | @btn-border-radius-base : @border-radius-base; 137 | @btn-border-radius-sm : @border-radius-base; 138 | 139 | @btn-primary-color : #fff; 140 | @btn-primary-bg : @primary-color; 141 | 142 | @btn-default-color : @text-color; 143 | @btn-default-bg : #fff; 144 | @btn-default-border : @border-color-base; 145 | 146 | @btn-danger-color : @error-color; 147 | @btn-danger-bg : @background-color-base; 148 | @btn-danger-border : @border-color-base; 149 | 150 | @btn-disable-color : @disabled-color; 151 | @btn-disable-bg : @disabled-bg; 152 | @btn-disable-border : @border-color-base; 153 | 154 | @btn-padding-base : 0 @padding-md - 1px; 155 | @btn-font-size-lg : @font-size-lg; 156 | @btn-font-size-sm : @font-size-base; 157 | @btn-padding-lg : @btn-padding-base; 158 | @btn-padding-sm : 0 @padding-xs - 1px; 159 | 160 | @btn-height-base : 32px; 161 | @btn-height-lg : 40px; 162 | @btn-height-sm : 24px; 163 | 164 | @btn-circle-size : @btn-height-base; 165 | @btn-circle-size-lg : @btn-height-lg; 166 | @btn-circle-size-sm : @btn-height-sm; 167 | 168 | @btn-group-border : @primary-5; 169 | 170 | // Checkbox 171 | @checkbox-size : 16px; 172 | @checkbox-color : @primary-color; 173 | 174 | // Radio 175 | @radio-size : 16px; 176 | @radio-dot-color : @primary-color; 177 | 178 | // Radio buttons 179 | @radio-button-bg : @btn-default-bg; 180 | @radio-button-color : @btn-default-color; 181 | @radio-button-hover-color : @primary-5; 182 | @radio-button-active-color : @primary-7; 183 | 184 | // Media queries breakpoints 185 | // Extra small screen / phone 186 | @screen-xs : 480px; 187 | @screen-xs-min : @screen-xs; 188 | 189 | // Small screen / tablet 190 | @screen-sm : 576px; 191 | @screen-sm-min : @screen-sm; 192 | 193 | // Medium screen / desktop 194 | @screen-md : 768px; 195 | @screen-md-min : @screen-md; 196 | 197 | // Large screen / wide desktop 198 | @screen-lg : 992px; 199 | @screen-lg-min : @screen-lg; 200 | 201 | // Extra large screen / full hd 202 | @screen-xl : 1200px; 203 | @screen-xl-min : @screen-xl; 204 | 205 | // Extra extra large screen / large descktop 206 | @screen-xxl : 1600px; 207 | @screen-xxl-min : @screen-xxl; 208 | 209 | // provide a maximum 210 | @screen-xs-max : (@screen-sm-min - 1px); 211 | @screen-sm-max : (@screen-md-min - 1px); 212 | @screen-md-max : (@screen-lg-min - 1px); 213 | @screen-lg-max : (@screen-xl-min - 1px); 214 | @screen-xl-max : (@screen-xxl-min - 1px); 215 | 216 | // Grid system 217 | @grid-columns : 24; 218 | @grid-gutter-width : 0; 219 | 220 | // Layout 221 | @layout-body-background : #f0f2f5; 222 | @layout-header-background : #001529; 223 | @layout-footer-background : @layout-body-background; 224 | @layout-header-height : 64px; 225 | @layout-header-padding : 0 50px; 226 | @layout-footer-padding : 24px 50px; 227 | @layout-sider-background : @layout-header-background; 228 | @layout-trigger-height : 48px; 229 | @layout-trigger-background : #002140; 230 | @layout-trigger-color : #fff; 231 | @layout-zero-trigger-width : 36px; 232 | @layout-zero-trigger-height : 42px; 233 | // Layout light theme 234 | @layout-sider-background-light : #fff; 235 | @layout-trigger-background-light: #fff; 236 | @layout-trigger-color-light : @text-color; 237 | 238 | // z-index list 239 | @zindex-affix : 10; 240 | @zindex-back-top : 10; 241 | @zindex-modal-mask : 1000; 242 | @zindex-modal : 1000; 243 | @zindex-notification : 1010; 244 | @zindex-message : 1010; 245 | @zindex-popover : 1030; 246 | @zindex-picker : 1050; 247 | @zindex-dropdown : 1050; 248 | @zindex-tooltip : 1060; 249 | 250 | // Animation 251 | @animation-duration-slow: 0.3s; // Modal 252 | @animation-duration-base: 0.2s; 253 | @animation-duration-fast: 0.1s; // Tooltip 254 | 255 | // Form 256 | // --- 257 | @label-required-color : @highlight-color; 258 | @label-color : @heading-color; 259 | @form-item-margin-bottom : 24px; 260 | @form-item-trailing-colon : true; 261 | @form-vertical-label-padding : 0 0 8px; 262 | @form-vertical-label-margin : 0; 263 | 264 | // Input 265 | // --- 266 | @input-height-base : 32px; 267 | @input-height-lg : 40px; 268 | @input-height-sm : 24px; 269 | @input-padding-horizontal : @control-padding-horizontal - 1px; 270 | @input-padding-horizontal-base: @input-padding-horizontal; 271 | @input-padding-horizontal-sm : @control-padding-horizontal-sm - 1px; 272 | @input-padding-horizontal-lg : @input-padding-horizontal; 273 | @input-padding-vertical-base : 4px; 274 | @input-padding-vertical-sm : 1px; 275 | @input-padding-vertical-lg : 6px; 276 | @input-placeholder-color : hsv(0, 0, 75%); 277 | @input-color : @text-color; 278 | @input-border-color : @border-color-base; 279 | @input-bg : #fff; 280 | @input-addon-bg : @background-color-light; 281 | @input-hover-border-color : @primary-color; 282 | @input-disabled-bg : @disabled-bg; 283 | 284 | // Tooltip 285 | // --- 286 | //* Tooltip max width 287 | @tooltip-max-width: 250px; 288 | //** Tooltip text color 289 | @tooltip-color: #fff; 290 | //** Tooltip background color 291 | @tooltip-bg: rgba(0, 0, 0, 0.75); 292 | //** Tooltip arrow width 293 | @tooltip-arrow-width: 5px; 294 | //** Tooltip distance with trigger 295 | @tooltip-distance: @tooltip-arrow-width - 1px + 4px; 296 | //** Tooltip arrow color 297 | @tooltip-arrow-color: @tooltip-bg; 298 | 299 | // Popover 300 | // --- 301 | //** Popover body background color 302 | @popover-bg: #fff; 303 | //** Popover text color 304 | @popover-color: @text-color; 305 | //** Popover maximum width 306 | @popover-min-width: 177px; 307 | //** Popover arrow width 308 | @popover-arrow-width: 6px; 309 | //** Popover arrow color 310 | @popover-arrow-color: @popover-bg; 311 | //** Popover outer arrow width 312 | //** Popover outer arrow color 313 | @popover-arrow-outer-color: @popover-bg; 314 | //** Popover distance with trigger 315 | @popover-distance: @popover-arrow-width + 4px; 316 | 317 | // Modal 318 | // -- 319 | @modal-mask-bg: rgba(0, 0, 0, 0.65); 320 | 321 | // Progress 322 | // -- 323 | @progress-default-color: @processing-color; 324 | @progress-remaining-color: @background-color-base; 325 | 326 | // Menu 327 | // --- 328 | @menu-inline-toplevel-item-height: 40px; 329 | @menu-item-height: 40px; 330 | @menu-collapsed-width: 80px; 331 | @menu-bg: @component-background; 332 | @menu-item-color: @text-color; 333 | @menu-highlight-color: @primary-color; 334 | @menu-item-active-bg: @item-active-bg; 335 | @menu-item-group-title-color: @text-color-secondary; 336 | // dark theme 337 | @menu-dark-color: @text-color-secondary-dark; 338 | @menu-dark-bg: @layout-header-background; 339 | @menu-dark-arrow-color: #fff; 340 | @menu-dark-submenu-bg: #000c17; 341 | @menu-dark-highlight-color: #fff; 342 | @menu-dark-item-active-bg: @primary-color; 343 | 344 | // Spin 345 | // --- 346 | @spin-dot-size-sm: 14px; 347 | @spin-dot-size: 20px; 348 | @spin-dot-size-lg: 32px; 349 | 350 | // Table 351 | // -- 352 | @table-header-bg: @background-color-light; 353 | @table-header-sort-bg: @background-color-base; 354 | @table-row-hover-bg: @primary-1; 355 | @table-selected-row-bg: #fafafa; 356 | @table-expanded-row-bg: #fbfbfb; 357 | @table-padding-vertical: 16px; 358 | @table-padding-horizontal: 16px; 359 | 360 | // Tag 361 | // -- 362 | @tag-default-bg: @background-color-light; 363 | @tag-default-color: @text-color; 364 | @tag-font-size: @font-size-sm; 365 | 366 | // TimePicker 367 | // --- 368 | @time-picker-panel-column-width: 56px; 369 | @time-picker-panel-width: @time-picker-panel-column-width * 3; 370 | @time-picker-selected-bg: @background-color-base; 371 | 372 | // Carousel 373 | // --- 374 | @carousel-dot-width: 16px; 375 | @carousel-dot-height: 3px; 376 | @carousel-dot-active-width: 24px; 377 | 378 | // Badge 379 | // --- 380 | @badge-height: 20px; 381 | @badge-dot-size: 6px; 382 | @badge-font-size: @font-size-sm; 383 | @badge-font-weight: normal; 384 | @badge-status-size: 6px; 385 | 386 | // Rate 387 | // --- 388 | @rate-star-color: @yellow-6; 389 | @rate-star-bg: @border-color-split; 390 | 391 | // Card 392 | // --- 393 | @card-head-color: @heading-color; 394 | @card-head-background: @component-background; 395 | @card-head-padding: 16px; 396 | @card-inner-head-padding: 12px; 397 | @card-padding-base: 24px; 398 | @card-padding-wider: 32px; 399 | @card-actions-background: @background-color-light; 400 | @card-shadow: 0 2px 8px rgba(0, 0, 0, 0.09); 401 | 402 | // Tabs 403 | // --- 404 | @tabs-card-head-background: @background-color-light; 405 | @tabs-card-height: 40px; 406 | @tabs-card-active-color: @primary-color; 407 | @tabs-title-font-size: @font-size-base; 408 | @tabs-title-font-size-lg: @font-size-lg; 409 | @tabs-title-font-size-sm: @font-size-base; 410 | @tabs-ink-bar-color: @primary-color; 411 | @tabs-bar-margin: 0 0 16px 0; 412 | @tabs-horizontal-margin: 0 32px 0 0; 413 | @tabs-horizontal-padding: 12px 16px; 414 | @tabs-vertical-padding: 8px 24px; 415 | @tabs-vertical-margin: 0 0 16px 0; 416 | @tabs-scrolling-size: 32px; 417 | @tabs-highlight-color: @primary-color; 418 | @tabs-hover-color: @primary-5; 419 | @tabs-active-color: @primary-7; 420 | 421 | // BackTop 422 | // --- 423 | @back-top-color: #fff; 424 | @back-top-bg: @text-color-secondary; 425 | @back-top-hover-bg: @text-color; 426 | 427 | // Avatar 428 | // --- 429 | @avatar-size-base: 32px; 430 | @avatar-size-lg: 40px; 431 | @avatar-size-sm: 24px; 432 | @avatar-font-size-base: 18px; 433 | @avatar-font-size-lg: 24px; 434 | @avatar-font-size-sm: 14px; 435 | @avatar-bg: #ccc; 436 | @avatar-color: #fff; 437 | @avatar-border-radius: @border-radius-base; 438 | 439 | // Switch 440 | // --- 441 | @switch-height: 22px; 442 | @switch-sm-height: 16px; 443 | @switch-sm-checked-margin-left: -(@switch-sm-height - 3px); 444 | @switch-disabled-opacity: 0.4; 445 | @switch-color: @primary-color; 446 | 447 | // Pagination 448 | // --- 449 | @pagination-item-size: 32px; 450 | @pagination-item-size-sm: 24px; 451 | @pagination-font-family: Arial; 452 | @pagination-font-weight-active: 500; 453 | 454 | // Breadcrumb 455 | // --- 456 | @breadcrumb-base-color: @text-color-secondary; 457 | @breadcrumb-last-item-color: @text-color; 458 | @breadcrumb-font-size: @font-size-base; 459 | @breadcrumb-icon-font-size: @font-size-sm; 460 | @breadcrumb-link-color: @text-color-secondary; 461 | @breadcrumb-link-color-hover: @primary-5; 462 | @breadcrumb-separator-color: @text-color-secondary; 463 | @breadcrumb-separator-margin: 0 @padding-xs; 464 | 465 | // Slider 466 | // --- 467 | @slider-margin: 14px 6px 10px; 468 | @slider-rail-background-color: @background-color-base; 469 | @slider-rail-background-color-hover: #e1e1e1; 470 | @slider-track-background-color: @primary-3; 471 | @slider-track-background-color-hover: @primary-4; 472 | @slider-handle-color: @primary-3; 473 | @slider-handle-color-hover: @primary-4; 474 | @slider-handle-color-focus: tint(@primary-color, 20%); 475 | @slider-handle-color-focus-shadow: tint(@primary-color, 50%); 476 | @slider-handle-color-tooltip-open: @primary-color; 477 | @slider-dot-border-color: @border-color-split; 478 | @slider-dot-border-color-active: tint(@primary-color, 50%); 479 | @slider-disabled-color: @disabled-color; 480 | @slider-disabled-background-color: @component-background; 481 | 482 | // Collapse 483 | // --- 484 | @collapse-header-padding: 12px 0 12px 40px; 485 | @collapse-header-bg: @background-color-light; 486 | @collapse-content-padding: @padding-md; 487 | @collapse-content-bg: @component-background; 488 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-antd-formutil 2 | 3 | [![npm](https://img.shields.io/npm/v/react-antd-formutil.svg?style=flat)](https://npm.im/react-antd-formutil) 4 | [![peerDependencies](https://img.shields.io/npm/dependency-version/react-antd-formutil/peer/react.svg?color=yellowgreen)](https://reactjs.org) 5 | [![definitionTypes](https://img.shields.io/npm/types/react-antd-formutil.svg)](https://github.com/qiqiboy/react-antd-formutil/blob/master/index.d.ts) 6 | [![gzip](https://img.shields.io/bundlephobia/minzip/react-antd-formutil.svg)](https://npm.im/react-antd-formutil) 7 | [![download](https://img.shields.io/npm/dm/react-antd-formutil.svg)](https://npm.im/react-antd-formutil) 8 | [![issues](https://img.shields.io/github/issues/qiqiboy/react-antd-formutil.svg)](https://github.com/qiqiboy/react-antd-formutil/issues) 9 | [![license](https://img.shields.io/github/license/qiqiboy/react-antd-formutil.svg)](https://github.com/qiqiboy/react-antd-formutil/blob/master/LICENSE) 10 | [![github](https://img.shields.io/github/last-commit/qiqiboy/react-antd-formutil.svg)](https://github.com/qiqiboy/react-antd-formutil) 11 | [![github](https://img.shields.io/github/release-date/qiqiboy/react-antd-formutil.svg)](https://github.com/qiqiboy/react-antd-formutil/releases) 12 | [![github](https://img.shields.io/github/commit-activity/m/qiqiboy/react-antd-formutil.svg)](https://github.com/qiqiboy/react-antd-formutil/commits/master) 13 | [![github](https://img.shields.io/github/stars/qiqiboy/react-antd-formutil.svg?style=social)](https://github.com/qiqiboy/react-antd-formutil) 14 | 15 | [![react-antd-formutil](https://nodei.co/npm/react-antd-formutil.png?compact=true)](https://npm.im/react-antd-formutil) 16 | 17 | Happy to use react-formutil in the project based on ant-design@`3`&`4` ^\_^ 18 | 19 | 在 [ant-design](https://github.com/ant-design/ant-design) 项目,结合 [react-formutil](https://github.com/qiqiboy/react-formutil) 来快速构建表单。**支持所有的`ant-design`输入型(`data-entry`)组件。** 20 | 21 | > **如果你在使用其他 react 组件库,可以查阅:** 22 | > 23 | > 1. react-bootstrap [`react-bootstrap-formutil`](https://github.com/qiqiboy/react-bootstrap-formutil) [![npm](https://img.shields.io/npm/v/react-bootstrap-formutil.svg?style=flat)](https://npm.im/react-bootstrap-formutil) 24 | > 1. react-md [`react-md-formutil`](https://github.com/qiqiboy/react-md-formutil) [![npm](https://img.shields.io/npm/v/react-md-formutil.svg?style=flat)](https://npm.im/react-md-formutil) 25 | > 1. Material-UI [`react-material-formutil`](https://github.com/qiqiboy/react-material-formutil) [![npm](https://img.shields.io/npm/v/react-material-formutil.svg?style=flat)](https://npm.im/react-material-formutil) 26 | 27 | 28 | 29 | - [安装 Installation](#安装-installation) 30 | - [使用 Usage](#使用-usage) 31 | + [``](#formitem-) 32 | * [`name`](#name) 33 | * [`$defaultValue`](#defaultvalue) 34 | * [`$validators`](#validators) 35 | * [`itemProps`](#itemprops) 36 | * [`$parser`](#parser) 37 | * [`$formatter`](#formatter) 38 | * [`checked` `unchecked`](#checked-unchecked) 39 | * [`$validateLazy`](#validatelazy) 40 | * [`$memo`](#memo) 41 | * [`validMessage`](#validmessage) 42 | * [`valuePropName` `changePropName` `focusPropName` `blurPropName`](#valuepropname-changepropname-focuspropname-blurpropname) 43 | * [`getValueFromEvent`](#getvaluefromevent) 44 | * [`noStyle`](#nostyle) 45 | * [`errorLevel`](#errorlevel) 46 | + [`setErrorLevel(level)`](#seterrorlevellevel) 47 | + [`支持的组件`](#支持的组件) 48 | * [`AutoComplete`](#autocomplete) 49 | * [`Checkbox`](#checkbox) 50 | * [`Cascader`](#cascader) 51 | * [`DatePicker`](#datepicker) 52 | * [`InputNumber`](#inputnumber) 53 | * [`Input`](#input) 54 | * [`Mentions`](#mentions) 55 | * [`Pagination`](#pagination) 56 | * [`Rate`](#rate) 57 | * [`Radio`](#radio) 58 | * [`Switch`](#switch) 59 | * [`Slider`](#slider) 60 | * [`Select`](#select) 61 | * [`TreeSelect`](#treeselect) 62 | * [`Transfer`](#transfer) 63 | * [`TimePicker`](#timepicker) 64 | * [`Upload`](#upload) 65 | + [`动态className`](#动态classname) 66 | - [FAQ](#faq) 67 | + [`给组件设置的onChange、onFocus等方法无效、不执行`](#给组件设置的onchangeonfocus等方法无效不执行) 68 | + [`RangePicker 在safari下假死?`](#rangepicker-在safari下假死) 69 | + [`在生产环境(NODE_ENV==='production')部分组件调用有异常?`](#在生产环境node_envproduction部分组件调用有异常) 70 | + [`如何正确的使用FormItem嵌套渲染多个节点元素?`](#如何正确的使用formitem嵌套渲染多个节点元素) 71 | 72 | 73 | 74 | ### 安装 Installation 75 | 76 | [![react-antd-formutil](https://nodei.co/npm/react-antd-formutil.png?compact=true)](https://npm.im/react-antd-formutil) 77 | 78 | **`react-antd-formutil`从`1.0.0`版本开始,同时支持 Ant Design `3.x`和`4.x`版本** 79 | 80 | ```bash 81 | # npm 82 | npm install react-antd-formutil --save 83 | 84 | # yarn 85 | yarn install react-antd-formutil 86 | ``` 87 | 88 | ### 使用 Usage 89 | 90 | > `react-antd-formutil` 整合了 `react-formutil` 的组件,所以可以直接从`react-antd-formutil`中导出所需要的 `react-formutil` 组件。不用单独从 react-formutil 中导出。 91 | 92 | 先看一个使用示例(点击查看在线完整示例: [react-antd-formutil on codesandbox.io](https://codesandbox.io/s/84y6w5oox2)): 93 | 94 | ```javascript 95 | import React, { Component } from 'react'; 96 | import { withForm, FormItem } from 'react-antd-formutil'; 97 | import { Input, Form } from 'antd'; // 导入antd的Input组件 98 | 99 | @withForm 100 | class MyForm extends Component { 101 | submit = () => { 102 | const { $invalid, $getFirstError, $params } = this.props.$formutil; 103 | 104 | if ($invalid) { 105 | alert($getFistError()); 106 | } else { 107 | // submit your data 108 | } 109 | }; 110 | 111 | render() { 112 | return ( 113 | 114 | 119 | 120 | 121 | 122 | ); 123 | } 124 | } 125 | ``` 126 | 127 | `FormItem`是 `react-antd-formuitl` 新增加的组件,`withForm`是`react-formutil`的组件(没错,你可以直接从`react-antd-formutil`中导出`react-formutil`的组件啦)。 128 | 129 | 只需要将`ant-design`的交互组件,嵌套在`FormItem`下,即可实现自动的表单状态同步。 130 | 131 | #### `` 132 | 133 | 要实现将`ant-design`的交互组件的值能同步到 react-formutil 的状态中,需要通过 FormItem 这个组件来实现中间态绑定。 134 | 135 | 它的作用有些类似 antd 中的`getFieldDecorator`方法,但是用法比`getFieldDecorator`更优雅,更 JSX 语法。`FormItem`完全是标签声明式用法,它是对 antd 的`Form.Item`组件的再次封装。 136 | 137 | 所以`FormItem`会完整实现`Form.Item`所可以显示的校验状态、错误暂时等 UI 变化。 138 | 139 | > 如果给 `FormItem` 传递了多个子节点,可能会出现无法非预期的异常情况。你可以了解[`如何正确的使用FormItem嵌套渲染多个节点元素?`](#如何正确的使用formitem嵌套渲染多个节点元素) 140 | 141 | **支持传递的属性** 142 | 143 | `FormItem`可以接收所有`antd`中的`Form.Item`组件所接收的所有属性,另外还新增以下属性支持: 144 | 145 | ##### `name` 146 | 147 | 设置输入项的 name 值,表单项将会以 name 作为 key 收集到 formutil 的状态中。支持嵌套语法 _(同`react-formutil`的`Field`同名参数,可以参考 [name](https://github.com/qiqiboy/react-formutil#name))_ 148 | 149 | ##### `$defaultValue` 150 | 151 | 设置该表单项的默认值 _(同`react-formutil`的`Field`同名参数,可以参考[\$defaultvalue](https://github.com/qiqiboy/react-formutil#defaultvalue))_ 152 | 153 | ##### `$validators` 154 | 155 | 设置校验方法 _(同`react-formutil`的`Field`同名参数, 可以参考 [\$validators](https://github.com/qiqiboy/react-formutil#validators))_ 156 | 157 | > 同 react-formutil 的 EasyField,FormItem 也内置了同样的校验规则: 158 | 159 | > - `required` 必填 `required` 160 | > - `maxLength` 。最大输入长度,有效输入时才会校验 `maxLength="100"` 161 | > - `minLength` 最小输入长度,有效输入时才会校验 `minLength="10"` 162 | > - `max` 最大输入数值,仅支持 Number 比较。有效输入时才会校验 `max="100"` 163 | > - `min` 最小输入数值,仅支持 Number 比较。有效输入时才会校验 `min="10"` 164 | > - `pattern` 正则匹配。有效输入时才会校验 `pattern={/^\d+$/}` 165 | > - `enum` 枚举值检测。有效输入时才会校验 `enum={[1,2,3]}` 166 | > - `checker` 自定义校验函数。`checker={value => value > 10 && value < 100 || '输入比如大于10小与100'}` 167 | 168 | 注:校验属性的值为 `null` 时表示不进行该校验 169 | 170 | 内置的校验规则无需再次声明,除非规则不符合预期,需要替换,则可以通过`$validators` 传递同名校验方法即可替换默认的。另外,内置的校验规则,如果校验不通过,会尝试去 `validMessage` 匹配错误信息。 171 | 172 | ##### `itemProps` 173 | 174 | 该属性为要传递给`Form.Item`组件的配置项: 175 | 176 | ```javascript 177 | 182 | 183 | 184 | ``` 185 | 186 | ##### `$parser` 187 | 188 | 请参考`react-formutil`中[`$parser`](https://github.com/qiqiboy/react-formutil#parser)介绍。 189 | 190 | ##### `$formatter` 191 | 192 | 请参考`react-formutil`中[`$formatter`](https://github.com/qiqiboy/react-formutil#formatter)介绍。 193 | 194 | ##### `checked` `unchecked` 195 | 196 | 对于 `` `` `` 这三种组件,其值默认是 checked 属性,为布尔值。可以通过`checked` `unchecked`来设置 checked 状态时所要映射的值: 197 | 198 | ```javascript 199 | 200 | 201 | 202 | ``` 203 | 204 | 该示例中, 当 Switch 为开时,获取的值将为 yes。 205 | 206 | ##### `$validateLazy` 207 | 208 | 可以用来优化表单的校验速度,请参考: [`$validateLazy`](https://github.com/qiqiboy/react-formutil#validatelazy) 209 | 210 | ##### `$memo` 211 | 212 | 可以用来优化当前表单项的性能,避免过多的重复渲染。如果你遇到了表单性能问题,可以尝试该属性来改善。 213 | 214 | 详细解释和使用、注意事项请参考: [`$memo`](https://github.com/qiqiboy/react-formutil#memo) 215 | 216 | ##### `validMessage` 217 | 218 | 设置校验结果的错误信息。 219 | 220 | ```javascript 221 | 227 | 228 | 229 | ``` 230 | 231 | ##### `valuePropName` `changePropName` `focusPropName` `blurPropName` 232 | 233 | 该四个参数可以用来设置绑定到组件上的值或者值变动、是否聚焦等事件回调。该项一般不需要设置,`FormItem` 已经针对 `antd` 中的所有 `data-entry` 型组件做了兼容处理。 234 | 235 | 对于一些特殊场景,例如不需要同步 `focus`、`blur`,则可以通过将该值设为`{null}`来禁用: 236 | 237 | ```javascript 238 | //禁用focus、blur状态同步 239 | 240 | 241 | 242 | ``` 243 | 244 | ##### `getValueFromEvent` 245 | 246 | 请参考 [`getValueFromEvent()`](https://github.com/qiqiboy/react-formutil/blob/master/README.md#getvaluefromevent) 247 | 248 | ##### `noStyle` 249 | 250 | > 该属性从 `v1.1.0` 起可用 251 | > 252 | > 该属性同时兼容`antd@3.x` 和`antd@4.x`,都可以使用! 253 | 254 | `noStyle`与`AntDesign v4.0`中新版本的`Form.Item`的`noStyle`类似,可以用来控制是否输出`Form.Item`的额外的样式元素。缺省情况下默认值为`false`。 255 | 256 | 当`noStyle`为`true`时,将会只渲染字段节点本身,但是其表单状态依然会被处理收集。此时,如果其存在父级嵌套的`FormItem`,那么其表达校验状态将会传递给父级的`FormItem`来展现。 257 | 258 | 这对于连续的紧凑型表单元素将非常有用!可以避免校验错误描述信息都堆叠在一起! **但是没有额外的样式显示,包括表单校验状态都无法显示了。此时可以在其外层包裹一层不带`name`的`FormItem`,这些`noStyle`的表单项就会把他们自身的状态向上进行注册显示了!** 259 | 260 | 但是有以下几点需要注意: 261 | 262 | 1. 最外层的`FormItem`不能设置`name`属性,否则将不会被当作子级的校验状态容器 263 | 2. 内层的`FormItem`需要添加相应的`name`值(向表单控制器注册自身)以及`noStyle`属性(不渲染额外的样式,避免和上层冲突) 264 | 265 | ```typescript 266 | // 这里不能设置name 267 | 268 | 269 | {/* 与普通的FormItem用法一致,只是多了个noStyle */} 270 | 271 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | ``` 283 | 284 | 以上运行示例请参考 [示例](http://github.boy.im/react-antd-formutil/demo/) 285 | 286 | ##### `errorLevel` 287 | 288 | 用来覆盖全局的 errorLevel 设置。参考[`setErrorLevel(level)`](#seterrorlevellevel) 289 | 290 | #### `setErrorLevel(level)` 291 | 292 | `setErrorLevel` 该方法可以用来全局设置错误信息何时出现,有三个级别可以设置: 293 | 294 | - `0` 当`$dirty` `$touched` `$invalid` 都为 true 时 295 | - `1` 当`$dirty` `$invalid` 都为 true 时 296 | - `2` 当`$invalid` 为 true 时 297 | - `off` 关闭错误显示 298 | 299 | 默认值为 `1` 300 | 301 | > 注意,该方法影响全局,如果只是希望单独对某个表单项进行设置,可以通过`errorLevel`属性进行设置:参考[`errorLevel`](#errorlevel) 302 | 303 | ```javascript 304 | import { setErrorLevel } from 'react-antd-formutil'; 305 | 306 | setErrorLevel(0); 307 | 308 | // 当关闭错误显示时,errorLevel='off',你可以手动自行设置错误展示方式: 309 | 出错啦 : null 315 | }}> 316 | 317 | ; 318 | ``` 319 | 320 | #### `支持的组件` 321 | 322 | ##### [`AutoComplete`](https://ant.design/components/auto-complete-cn/) 323 | 324 | ##### [`Checkbox`](https://ant.design/components/checkbox-cn/) 325 | 326 | 支持`Checkbox.Group`。 327 | 328 | ##### [`Cascader`](https://ant.design/components/cascader-cn/) 329 | 330 | ##### [`DatePicker`](https://ant.design/components/date-picker-cn/) 331 | 332 | `DatePicker` `TimePicker` `DatePicker.WeekPicker` `DatePicker.MonthPicker` `DatePicker.RangePicker` 等几个日期类组件,都是深度结合了`moment`使用的。如果希望收集到表单中的值是格式化好的时间字符串,可以通过`$parser` `$formatter`实现: 333 | 334 | ```javascript 335 | moment.format('YYYY-MM-DD')} $formatter={date => moment(date)}> 336 | 337 | 338 | ``` 339 | 340 | 对于`DatePicker.RangePicker`,由于其值是一个数组,所以需要这样处理: 341 | 342 | ```javascript 343 | moments.map(moment => moment.format('YYYY-MM-DD'))} 346 | $formatter={dates => dates.map(date => moment(date))}> 347 | 348 | 349 | ``` 350 | 351 | ##### [`InputNumber`](https://ant.design/components/input-number-cn/) 352 | 353 | ##### [`Input`](https://ant.design/components/input-cn/) 354 | 355 | ##### [`Mentions`](https://ant.design/components/mentions-cn/) 356 | 357 | ##### [`Pagination`](https://ant.design/components/pagination-cn/) 358 | 359 | `Pagination` 并非`antd`所归纳的`data entry`组件,但是其接口设计也可以支持`FormItem`: 360 | 361 | ```javascript 362 | 363 | 364 | 365 | ``` 366 | 367 | ##### [`Rate`](https://ant.design/components/rate-cn/) 368 | 369 | ##### [`Radio`](https://ant.design/components/radio-cn/) 370 | 371 | 支持`Radio.Group`。 372 | 373 | ##### [`Switch`](https://ant.design/components/switch-cn/) 374 | 375 | `Switch` `Checkbox`(不包括`Checkbox.Group`) `Radio`(不包括`Radio.Group`)三个组件,可以通过给`FormItem`传递`checked` `unchecked`属性来改变被勾选时所映射到表单状态中的值: 376 | 377 | ```javascript 378 | 379 | 380 | 381 | ``` 382 | 383 | ##### [`Slider`](https://ant.design/components/slider-cn/) 384 | 385 | ##### [`Select`](https://ant.design/components/select-cn/) 386 | 387 | ##### [`TreeSelect`](https://ant.design/components/tree-select-cn/) 388 | 389 | ##### [`Transfer`](https://ant.design/components/transfer-cn/) 390 | 391 | `Transfer`收集到表单状态中的是`targetKeys`。 392 | 393 | ##### [`TimePicker`](https://ant.design/components/time-picker-cn/) 394 | 395 | 参考 [`DatePicker`](#datepicker) 396 | 397 | ##### [`Upload`](https://ant.design/components/upload-cn/) 398 | 399 | `Upload`组件非常特殊,其接受`fileList`对象作为整个组件的状态。而实际业务中,往往只需要获取上传文件的返回的地址,或者一组文件的地址。可以通过`$parser`控制如何获取上传结果的值,并且可以通过`$parser`的第二个回调方法`$setViewValue`来控制`fileList`对象,实现对文件上传数量的控制。 400 | 401 | **单个文件上传,获取单个文件上传地址** 402 | 403 | ```javascript 404 | 407 | url && [ 408 | { 409 | url, 410 | uid: url, 411 | status: 'done', 412 | name: url.split('/').slice(-1)[0] 413 | } 414 | ] 415 | } 416 | $parser={(info, $setViewValue) => { 417 | // 必不可少,限制只能上传一个文件 418 | $setViewValue(info.fileList.slice(-1)); 419 | 420 | if (info.file.status === 'done') { 421 | return info.file.response.url; 422 | } 423 | }} 424 | itemProps={{ ...formItemLayout, label: 'Upload' }} 425 | required> 426 | 427 | 430 | 431 | 432 | ``` 433 | 434 | **多文件列表上传,获取多个文件上传地址数组** 435 | 436 | ```javascript 437 | 440 | urls && 441 | urls.map(url => ({ 442 | url, 443 | uid: url, 444 | status: 'done', 445 | name: url.split('/').slice(-1)[0] 446 | })) 447 | } 448 | $parser={(info, $setViewValue) => { 449 | // 限制最大上传文件数量为3,如果不需要限制,可以移除该行,或者修改该值 450 | $setViewValue(info.fileList.slice(-3)); 451 | 452 | return info.fileList.filter(file => file.status === 'done').map(file => file.url || file.response.url); 453 | }} 454 | itemProps={{ ...formItemLayout, label: 'Upload' }} 455 | required> 456 | 457 | 460 | 461 | 462 | ``` 463 | 464 | #### `动态className` 465 | 466 | `FormGroup`会自动给表单节点增加与该表单项校验状态相关的 className: 467 | 468 | - `has-error` 469 | - `is-invalid` 470 | - `is-valid` 471 | - `is-touched` 472 | - `is-untouched` 473 | - `is-focused` 474 | - `is-unfocused` 475 | - `is-dirty` 476 | - `is-pristine` 477 | 478 | ### FAQ 479 | 480 | #### `给组件设置的onChange、onFocus等方法无效、不执行` 481 | 482 | `FormItem`会覆盖掉直接添加到 antd 组件上的`onFocus` `onBlur` `onChange`方法,所以如果需要这三个事件方法,需要添加到 `FormItem`上: 483 | 484 | ```javascript 485 | console.log('change', ev)} onFocus={ev => console.log('focus', ev)}> 486 | 487 | 488 | ``` 489 | 490 | #### `RangePicker 在safari下假死?` 491 | 492 | 经过 debug,在`3.8.x`版本上,依然存在对`RangePicker`设置`onFocus` `onBlur`会异常频繁触发(比如在光标经过日期选择面板中每个数字时)的问题。可以禁用`onFocus` `onBlur`状态同步: 493 | 494 | ```javascript 495 | 496 | 497 | 498 | ``` 499 | 500 | #### `在生产环境(NODE_ENV==='production')部分组件调用有异常?` 501 | 502 | 如果在生产环境,发现例如`Checkbox` `Radio` `Switch`等组件无法正确捕获用户输入的值,这种情况一般是由于项目中使用了`babel-plugin-import`插件。 503 | 504 | `react-antd-formutil`中是使用 `import { Switch } from 'antd'` 这种写法来调用 `Switch` 组件的,而`babel-plugin-import`插件会将项目源代码中的类似语句,替换成`import Switch from 'antd/lib/switch'`。这两种写法获取到的`Switch`其实并不是严格意义上的相等,前者是对后者的又一层导出封装。 505 | 506 | 而由于`babel-plugin-import`一般仅仅会配置成仅仅对项目代码进行处理,所以处于项目`node_modules`目录中的`react-antd-formutil`中的语句不会被处理。我们需要通过修改项目 webpack 配置的方式,来使`babel-plugin-import`插件能处理`react-antd-formutil`的代码。 507 | 508 | 可以编辑项目的 webpack 配置(只需要配置生产环境的构建配置即可),在`rules`模块下添加以下的代码: 509 | 510 | ```javascript 511 | { 512 | test: /\.(js|mjs)$/, 513 | include: /react-antd-formutil/, // 仅仅处理react-antd-formutil即可 514 | loader: require.resolve('babel-loader'), 515 | options: { 516 | babelrc: false, 517 | plugins: [[ 518 | "import", 519 | { 520 | "libraryName": "antd" 521 | }, 522 | "antd" 523 | ]] 524 | } 525 | } 526 | ``` 527 | 528 | #### `如何正确的使用FormItem嵌套渲染多个节点元素?` 529 | 530 | 你可以通过给给`children`属性传递`render props`函数,来自由定义要渲染出的节点。但是请注意,当传递一个`render props`函数时,需要手动绑定相关绑定事件和 value 属性! 531 | 532 | 该`children`函数接受一个`$fieldHandler`的对象,默认情况下其包含`value` `onChange` `onFocus` `onBlur`四个属性,但是如果你给`FormItem`传递了`valuePropName`等属性的话,这个值将会变为你通过`valuePropName`所定义的名字。 533 | 534 | 更具体解释可以参考 [**react-formutil.\$fieldHandler**](https://github.com/qiqiboy/react-formutil#fieldhandler) 535 | 536 | ```typescript 537 | 538 | {$fieldHandler => ( 539 | <> 540 | 541 |
其它节点内容
542 | 543 | )} 544 |
545 | ``` 546 | -------------------------------------------------------------------------------- /docs/scripts/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-var-requires: 0 */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const resolve = require('resolve'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin'); 8 | const TerserPlugin = require('terser-webpack-plugin'); 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 10 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 11 | const safePostCssParser = require('postcss-safe-parser'); 12 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 13 | const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); 14 | const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); 15 | const DirectoryNamedWebpackPlugin = require('directory-named-webpack-plugin'); 16 | const paths = require('./paths'); 17 | const getClientEnvironment = require('./env'); 18 | const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); 19 | const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin'); 20 | const typescriptFormatter = require('react-dev-utils/typescriptFormatter'); 21 | const ImageminPlugin = require('imagemin-webpack-plugin').default; 22 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 23 | const pkg = require(paths.appPackageJson); 24 | 25 | const relativeRoot = path.join(pkg.noRewrite ? '.' : process.env.BASE_NAME || '/'); 26 | const cdnUrl = process.env.SKIP_CDN !== 'true' && pkg.cdn ? pkg.cdn.host + pkg.cdn.path : relativeRoot; 27 | const publicPath = ensureSlash(cdnUrl, true); 28 | const publicUrl = ensureSlash(cdnUrl, false); 29 | const env = getClientEnvironment(publicUrl); 30 | 31 | const shouldUseRelativeAssetPaths = process.env.SKIP_CDN === 'true' || !pkg.cdn; 32 | const shouldUseSourceMap = false; 33 | const shouldInlineRuntimeChunk = true; 34 | const shouldUseSW = !!pkg.pwa; 35 | 36 | // Assert this just to be safe. 37 | // Development builds of React are slow and not intended for production. 38 | if (env.stringified['process.env'].NODE_ENV !== '"production"') { 39 | throw new Error('Production builds must have NODE_ENV=production.'); 40 | } 41 | 42 | // eslint-disable-next-line 43 | const matchScriptStylePattern = /<\!--\s*script:\s*([\w]+)(?:\.[jt]sx?)?\s*-->/g; 44 | const injects = []; 45 | const babelOption = { 46 | babelrc: false, 47 | configFile: false, 48 | compact: false, 49 | presets: [[require.resolve('babel-preset-react-app/dependencies'), { helpers: true }]], 50 | cacheDirectory: true, 51 | cacheCompression: false, 52 | sourceMaps: false 53 | }; 54 | 55 | paths.pageEntries.forEach(function(name) { 56 | var chunks = ['_vendor_']; 57 | var file = path.resolve(paths.appPublic, name + '.html'); 58 | 59 | if (paths.entries[name]) { 60 | chunks.push(name); 61 | } 62 | 63 | var contents = fs.readFileSync(file); 64 | var matches; 65 | 66 | while ((matches = matchScriptStylePattern.exec(contents))) { 67 | chunks.push(matches[1]); 68 | } 69 | 70 | injects.push( 71 | new HtmlWebpackPlugin({ 72 | chunks: chunks, 73 | filename: name + '.html', 74 | template: file, 75 | inject: true, 76 | chunksSortMode: 'manual', 77 | minify: { 78 | removeComments: true, 79 | collapseWhitespace: true, 80 | removeRedundantAttributes: true, 81 | useShortDoctype: true, 82 | removeEmptyAttributes: true, 83 | removeStyleLinkTypeAttributes: true, 84 | keepClosingSlash: true, 85 | minifyJS: true, 86 | minifyCSS: true, 87 | minifyURLs: true 88 | } 89 | }) 90 | ); 91 | }); 92 | 93 | module.exports = { 94 | mode: 'production', 95 | bail: true, 96 | devtool: shouldUseSourceMap ? 'hidden-source-map' : false, 97 | entry: Object.assign( 98 | { 99 | _vendor_: [require.resolve('./polyfills')].concat(pkg.vendor || []) 100 | }, 101 | paths.entries 102 | ), 103 | output: { 104 | path: paths.appBuild, 105 | filename: 'static/js/[name].[chunkhash:8].js', 106 | chunkFilename: 'static/js/[name].[chunkhash:8].js', 107 | publicPath: publicPath, 108 | crossOriginLoading: 'anonymous', 109 | devtoolModuleFilenameTemplate: info => 110 | path.relative(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, '/'), 111 | globalObject: 'this' 112 | }, 113 | optimization: { 114 | minimizer: [ 115 | new TerserPlugin({ 116 | extractComments: false, 117 | terserOptions: { 118 | parse: { 119 | ecma: 8 120 | }, 121 | compress: { 122 | ecma: 5, 123 | warnings: false, 124 | comparisons: false, 125 | inline: 2 126 | }, 127 | mangle: { 128 | safari10: true 129 | }, 130 | output: { 131 | ecma: 5, 132 | comments: /@(license|author)/i, 133 | ascii_only: true 134 | } 135 | }, 136 | parallel: true, 137 | cache: true, 138 | sourceMap: shouldUseSourceMap 139 | }), 140 | new OptimizeCSSAssetsPlugin({ 141 | cssProcessorOptions: { 142 | parser: safePostCssParser, 143 | map: shouldUseSourceMap 144 | ? { 145 | inline: false, 146 | annotation: true 147 | } 148 | : false 149 | }, 150 | cssProcessorPluginOptions: { 151 | preset: ['default', { minifyFontValues: { removeQuotes: false } }] 152 | } 153 | }) 154 | ], 155 | splitChunks: { 156 | chunks: 'async', 157 | name: false, 158 | cacheGroups: { 159 | vendors: { 160 | chunks: 'all', 161 | test: '_vendor_', 162 | name: 'vendor' 163 | }, 164 | i18n: { 165 | chunks: 'all', 166 | test: /utils\/i18n|locals\/\w+\.json/, 167 | enforce: true, 168 | name: 'i18n' 169 | } 170 | } 171 | }, 172 | runtimeChunk: 'single' 173 | }, 174 | resolve: { 175 | modules: ['node_modules', paths.appNodeModules, paths.root].concat(paths.nodePaths), 176 | extensions: paths.moduleFileExtensions, 177 | alias: Object.assign( 178 | { 179 | 'react-native': 'react-native-web' 180 | }, 181 | paths.alias 182 | ), 183 | plugins: [ 184 | new DirectoryNamedWebpackPlugin({ 185 | honorIndex: true, 186 | exclude: /node_modules|libs/ 187 | }) 188 | ] 189 | }, 190 | 191 | module: { 192 | strictExportPresence: true, 193 | rules: [ 194 | { parser: { requireEnsure: false } }, 195 | 196 | { 197 | test: /\.(js|mjs|jsx|ts|tsx)$/, 198 | enforce: 'pre', 199 | use: [ 200 | { 201 | options: { 202 | cache: true, 203 | formatter: require.resolve('react-dev-utils/eslintFormatter'), 204 | eslintPath: require.resolve('eslint'), 205 | resolvePluginsRelativeTo: __dirname 206 | }, 207 | loader: require.resolve('eslint-loader') 208 | } 209 | ], 210 | include: [paths.formutilSrc, paths.appSrc] 211 | }, 212 | { 213 | oneOf: [ 214 | { 215 | test: /\.html$/, 216 | use: [ 217 | { 218 | loader: require.resolve('babel-loader'), 219 | options: babelOption 220 | }, 221 | { 222 | loader: require.resolve('html-loader'), 223 | options: { 224 | url(url) { 225 | return !/\.(webp|png|jpeg|jpg|gif|svg|mp3|wmv|mp4|ogg|webm|s[ac]ss|css|less|m?[tj]sx?)$/.test( 226 | url 227 | ); 228 | }, 229 | import: true 230 | } 231 | } 232 | ] 233 | }, 234 | { 235 | test: /\.(js|mjs|jsx|ts|tsx)$/, 236 | include: [paths.formutilSrc, paths.appSrc], 237 | loader: require.resolve('babel-loader'), 238 | options: { 239 | customize: require.resolve('babel-preset-react-app/webpack-overrides'), 240 | plugins: [ 241 | ['react-hot-loader/babel', false], // ensure react-hot-loader is disabled 242 | [ 243 | require.resolve('babel-plugin-named-asset-import'), 244 | { 245 | loaderMap: { 246 | svg: { 247 | ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]' 248 | } 249 | } 250 | } 251 | ] 252 | ], 253 | cacheDirectory: true, 254 | cacheCompression: false, 255 | compact: true, 256 | rootMode: 'upward' 257 | } 258 | }, 259 | { 260 | test: /\.(js|mjs)$/, 261 | exclude: /@babel(?:\/|\\{1,2})runtime/, 262 | loader: require.resolve('babel-loader'), 263 | options: babelOption 264 | }, 265 | { 266 | test: /\.css$/, 267 | exclude: /\.module\.css$/, 268 | loader: getStyleLoaders({ 269 | importLoaders: 1 270 | }), 271 | sideEffects: true 272 | }, 273 | { 274 | test: /\.module\.css$/, 275 | loader: getStyleLoaders({ 276 | importLoaders: 1, 277 | modules: { 278 | getLocalIdent: getCSSModuleLocalIdent 279 | } 280 | }) 281 | }, 282 | { 283 | test: /\.s[ac]ss$/, 284 | exclude: /\.module\.s[ac]ss$/, 285 | loader: getStyleLoaders( 286 | { 287 | importLoaders: 2 288 | }, 289 | 'sass-loader' 290 | ), 291 | sideEffects: true 292 | }, 293 | { 294 | test: /\.module\.s[ac]ss$/, 295 | loader: getStyleLoaders( 296 | { 297 | importLoaders: 2, 298 | modules: { 299 | getLocalIdent: getCSSModuleLocalIdent 300 | } 301 | }, 302 | 'sass-loader' 303 | ) 304 | }, 305 | { 306 | test: /\.less$/, 307 | exclude: /\.module\.less$/, 308 | loader: getStyleLoaders( 309 | { 310 | importLoaders: 2 311 | }, 312 | 'less-loader' 313 | ), 314 | sideEffects: true 315 | }, 316 | { 317 | test: /\.module\.less$/, 318 | loader: getStyleLoaders( 319 | { 320 | importLoaders: 2, 321 | modules: { 322 | getLocalIdent: getCSSModuleLocalIdent 323 | } 324 | }, 325 | 'less-loader' 326 | ) 327 | }, 328 | { 329 | test: /\.(txt|htm)$/, 330 | loader: require.resolve('raw-loader') 331 | }, 332 | { 333 | test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)$/, 334 | loader: require.resolve('file-loader'), 335 | options: { 336 | name: 'static/media/[name].[hash:8].[ext]' 337 | } 338 | }, 339 | { 340 | exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.(txt|htm)$/], 341 | loader: require.resolve('file-loader'), 342 | options: { 343 | name: 'static/images/[name].[hash:8].[ext]' 344 | } 345 | } 346 | ] 347 | } 348 | ] 349 | }, 350 | plugins: injects 351 | .concat([ 352 | shouldInlineRuntimeChunk && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime\.\w+[.]js/]), 353 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), 354 | new ModuleNotFoundPlugin(paths.root), 355 | new webpack.EnvironmentPlugin(env.raw), 356 | new ImageminPlugin({ 357 | cacheFolder: path.resolve(paths.appNodeModules, '.cache/imagemin'), 358 | pngquant: { 359 | // quality: '95-100' 360 | } 361 | }), 362 | new MiniCssExtractPlugin({ 363 | filename: 'static/css/[name].[contenthash:8].css', 364 | ignoreOrder: !!pkg.ignoreCssOrderWarnings || process.env.IGNORE_CSS_ORDER_WARNINGS === 'true' 365 | }), 366 | new webpack.HashedModuleIdsPlugin(), 367 | new webpack.IgnorePlugin({ 368 | resourceRegExp: /^\.\/locale$/, 369 | contextRegExp: /moment$/ 370 | }), 371 | new BundleAnalyzerPlugin(), 372 | shouldUseSW && 373 | new SWPrecacheWebpackPlugin({ 374 | cacheId: pkg.name, 375 | dontCacheBustUrlsMatching: /\.\w{8}\./, 376 | filename: 'service-worker.js', 377 | logger(message) { 378 | if (message.indexOf('Total precache size is') === 0) { 379 | // This message occurs for every build and is a bit too noisy. 380 | return; 381 | } 382 | 383 | if (message.indexOf('Skipping static resource') === 0) { 384 | // This message obscures real errors so we ignore it. 385 | // https://github.com/facebookincubator/create-react-app/issues/2612 386 | return; 387 | } 388 | 389 | console.log(message); 390 | }, 391 | minify: true, 392 | 393 | mergeStaticsConfig: true, 394 | staticFileGlobs: 'build/*.html', 395 | stripPrefix: 'build/', 396 | 397 | // For unknown URLs, fallback to the index page 398 | navigateFallback: path.join(relativeRoot, '/index.html'), 399 | // Ignores URLs starting from /__ (useful for Firebase): 400 | // https://github.com/facebookincubator/create-react-app/issues/2237#issuecomment-302693219 401 | navigateFallbackWhitelist: [/^(?!\/__).*/], 402 | // Don't precache sourcemaps (they're large) and build asset manifest: 403 | // /^\/.*\.html$/ 去掉webpack编译阶段由html-webpack-plugin带入的入口html文件 404 | // 因为这种文件是绝对路径,以 / 开头的 405 | staticFileGlobsIgnorePatterns: [/\.map$/, /manifest\.json$/, /^\/.*\.html$/] 406 | }), 407 | new ForkTsCheckerWebpackPlugin({ 408 | typescript: resolve.sync('typescript', { 409 | basedir: paths.appNodeModules 410 | }), 411 | async: false, 412 | useTypescriptIncrementalApi: true, 413 | checkSyntacticErrors: true, 414 | tsconfig: paths.appTsConfig, 415 | compilerOptions: { 416 | jsx: 'preserve', 417 | checkJs: false 418 | }, 419 | reportFiles: ['**/*.(ts|tsx)', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], 420 | silent: true, 421 | formatter: typescriptFormatter 422 | }), 423 | new webpack.BannerPlugin({ 424 | banner: '@author ' + pkg.author, 425 | entryOnly: true 426 | }) 427 | ]) 428 | .filter(Boolean), 429 | node: { 430 | dgram: 'empty', 431 | fs: 'empty', 432 | net: 'empty', 433 | tls: 'empty', 434 | child_process: 'empty' 435 | }, 436 | performance: false 437 | }; 438 | 439 | function getStyleLoaders(cssOptions, preProcessor) { 440 | const loaders = [ 441 | { 442 | loader: MiniCssExtractPlugin.loader, 443 | options: Object.assign( 444 | { 445 | esModule: true 446 | }, 447 | shouldUseRelativeAssetPaths ? { publicPath: '../../' } : undefined 448 | ) 449 | }, 450 | { 451 | loader: require.resolve('css-loader'), 452 | options: Object.assign( 453 | { 454 | sourceMap: shouldUseSourceMap 455 | }, 456 | cssOptions 457 | ) 458 | }, 459 | { 460 | loader: require.resolve('postcss-loader'), 461 | options: { 462 | ident: 'postcss', 463 | plugins: () => [ 464 | require('postcss-flexbugs-fixes'), 465 | require('postcss-preset-env')({ 466 | autoprefixer: { 467 | flexbox: 'no-2009' 468 | }, 469 | stage: 3 470 | }) 471 | ], 472 | sourceMap: shouldUseSourceMap 473 | } 474 | } 475 | ]; 476 | 477 | if (preProcessor) { 478 | loaders.push({ 479 | loader: require.resolve(preProcessor), 480 | options: Object.assign( 481 | {}, 482 | { sourceMap: shouldUseSourceMap }, 483 | preProcessor === 'less-loader' 484 | ? { 485 | javascriptEnabled: true 486 | } 487 | : { 488 | implementation: require('sass') 489 | } 490 | ) 491 | }); 492 | } 493 | 494 | return loaders; 495 | } 496 | 497 | function ensureSlash(path, needsSlash) { 498 | var hasSlash = path.endsWith('/'); 499 | 500 | if (hasSlash && !needsSlash) { 501 | return path.substr(path, path.length - 1); 502 | } else if (!hasSlash && needsSlash) { 503 | return path + '/'; 504 | } 505 | 506 | return path; 507 | } 508 | --------------------------------------------------------------------------------