diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..5e1194ffce846881a9e01e166021ab39d7d94ec4 --- /dev/null +++ b/.babelrc @@ -0,0 +1,21 @@ +{ + "sourceMaps": true, + "presets": [ + [ + "@babel/preset-typescript", + { + "isTSX": true, + "allExtensions": true + } + ], + "@babel/preset-react", + [ + "@babel/preset-env", + { + "targets": { + "node": "10.13.0" + } + } + ] + ] +} diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..72120c163d77c19d94628abd432559c308d59591 --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1,33 @@ +/** + * @file commitlint 配置 + * commit message: (): (注意冒号后面有空格) + * type 标识commit类别 + * scope 标识commit影响范围 + * subject 本次修改的简单描述 + */ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'init', // 初始提交 + 'feat', // 新功能(feature) + 'perf', // 优化 + 'fix', // 修补bug + 'docs', // 文档 + 'style', // 格式 + 'refactor', // 重构 + 'build', // 编译构建 + 'test', // 增加测试 + 'revert', // 回滚 + 'chore' // 其他改动 + ] + ], + 'type-empty': [2, 'never'], + 'subject-empty': [2, 'never'], + 'subject-full-stop': [0, 'never'], + 'subject-case': [0, 'never'] + } +} diff --git a/.cz-config.js b/.cz-config.js new file mode 100644 index 0000000000000000000000000000000000000000..ac772823ba1388ff11b811199bcb69db40ae4005 --- /dev/null +++ b/.cz-config.js @@ -0,0 +1,30 @@ +'use strict' +module.exports = { + types: [ + { value: 'init', name: '初始化' }, + { value: 'feat', name: '新增: 新功能' }, + { value: 'fix', name: '修复: 修复一个Bug' }, + { value: 'docs', name: '文档: 变更的只有文档' }, + { value: 'style', name: '格式: 空格, 分号等格式修复' }, + { value: 'refactor', name: '重构: 代码重构,注意和特性、修复区分开' }, + { value: 'perf', name: '性能: 提升性能' }, + { value: 'test', name: '测试: 添加一个测试' }, + { value: 'build', name: '工具: 开发工具变动(构建、脚手架工具等)' }, + { value: 'revert', name: '回滚: 代码回退' }, + { value: 'chore', name: '其他' } + ], + scopes: [{ name: 'javascript' }, { name: 'typescript' }, { name: 'React' }, { name: 'node' }], + messages: { + type: '选择一种你的提交类型:', + scope: '选择一个scope (可选):', + customScope: 'Denote the SCOPE of this change:', + subject: '短说明:\n', + body: '长说明,使用"|"换行(可选):\n', + breaking: '非兼容性说明 (可选):\n', + footer: '关联关闭的issue,例如:#31, #34(可选):\n', + confirmCommit: '确定提交说明?(yes/no)' + }, + allowCustomScopes: true, + allowBreakingChanges: ['特性', '修复'], + subjectLimit: 100 +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..c6c8b3621938a4691225a870a59bf382af1883dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000000000000000000000000000000000..cff24b5cdfa882623d223bf0db74816e1315a989 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,83 @@ +{ + "env": { + "browser": true, + "es2021": true, + "jest": true, + "node": true, + "commonjs": true + }, + "extends": [ + "airbnb", + "plugin:compat/recommended", + "plugin:jest/recommended", + "plugin:react/recommended", + "plugin:import/typescript", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": [ + "react", + "babel", + "jest", + "react-hooks", + "@typescript-eslint" + ], + "settings": { + "react": { + "version": "16.13.1" + }, + "polyfills": [ + "Promise" + ] + }, + "rules": { + "react/jsx-props-no-spreading": 0, + "no-use-before-define": 0, + "react/display-name": 0, + "import/extensions": 0, + "import/no-cycle": 0, + "consistent-return": 0, + "no-restricted-syntax": 0, + "import/no-extraneous-dependencies": 1, + "@typescript-eslint/no-empty-function": 1, + "typescript-eslint/no-explicit-any": 0, + "no-underscore-dangle": 0, + "no-unused-expressions": 0, + "no-plusplus": 0, + "no-continue": 0, + "react/no-array-index-key": 1, + "no-return-await": 1, + "import/no-unresolved": 1, + "no-await-in-loop": 1, + "no-nested-ternary": 1, + "react/jsx-no-useless-fragment": 1, + "no-shadow": 1, + "comma-dangle": [ + "error", + "never" + ], + "space-before-function-paren": [ + 0, + "always" + ], + "react/jsx-filename-extension": [ + 2, + { + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + } + ] + } +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 797152f796db98174899d17db4803e0d27be2f8f..0000000000000000000000000000000000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - jest: true - }, - extends: [ - 'plugin:react/recommended', - 'standard' - ], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaFeatures: { - jsx: true - }, - ecmaVersion: 12, - sourceType: 'module' - }, - plugins: [ - 'react', - '@typescript-eslint' - ], - rules: { - 'no-use-before-define': 'off', - 'react/display-name': 0 - } -} diff --git a/.fatherrc.js b/.fatherrc.js deleted file mode 100644 index c9b7c09c6ebff95bac86df8f11d980b40099c024..0000000000000000000000000000000000000000 --- a/.fatherrc.js +++ /dev/null @@ -1,21 +0,0 @@ -export default { - entry: 'src/index.tsx', - esm: "rollup", - cjs: { - type: "babel", - lazy: true - }, - cssModules: true, - injectCSS: true, - lessInBabelMode: true, - doc: { - typescript: true - }, - extraBabelPlugins: [ - ['babel-plugin-import', { - libraryName: 'antd', - libraryDirectory: 'es', - style: true, - }], - ] -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index acebde45350599c8a0d74a61039a8371466b247d..b3f0ce94c36bdca90bd40201e02def42c005b051 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ node_modules dist lib coverage -package-lock.json \ No newline at end of file +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..51fdad212d7d7852c2eeb93a3c0868f354908b71 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "printWidth": 120, + "useTabs": false, + "tabWidth": 2, + "semi": false, + "trailingComma": "none" +} diff --git a/package.json b/package.json index f8968d7c69d9995611df09a7869546add99fec5a..e64766751397c41044d72990fa396627464d2cb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccms", - "version": "1.2.2", + "version": "1.3.0", "description": "ConfigableCMS", "main": "lib/index.js", "module": "dist/index.js", @@ -10,76 +10,114 @@ "dist" ], "scripts": { - "build": "father build", - "dev": "father doc dev", - "lint-init": "eslint --init", - "lint": "eslint src --ext .ts", + "dev": "npm-watch build", + "build": "npm run clear && npm run build:rollup && npm run build:babel", + "build:rollup": "rollup -c", + "build:babel": "babel src -d lib -s -x '.ts,.tsx'", + "clear": "rm -rf dist && rm -rf lib", "test": "jest", - "test:u": "npm test -- -u" + "test:u": "npm test -- -u", + "lint": "eslint src/** --fix", + "prettier": "prettier -c --write **/*", + "commit": "git-cz" }, + "husky": { + "hooks": { + "commit-msg": "commitlint --env HUSKY_GIT_PARAMS", + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix", + "git add" + ], + "*.{json,html,md,markdown}": [ + "prettier --write", + "git add -f" + ] + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-customizable" + } + }, + "keywords": [ + "ccms" + ], "author": "niuweb", "license": "MIT", + "dependencies": { + "@types/react": "^16.9.46", + "@types/react-router-dom": "^5.1.5", + "axios": "^0.20.0", + "immer": "^9.0.7", + "lodash": "^4.17.21", + "marked": "^1.2.5", + "moment": "^2.29.0", + "qiankun": "^2.5.1", + "query-string": "^6.13.8", + "rc-table": "^7.9.10", + "react-loadable": "^5.5.0", + "tslib": "^2.3.1" + }, "devDependencies": { - "@babel/core": "^7.11.4", + "@babel/cli": "^7.16.8", + "@babel/core": "^7.16.12", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-runtime": "^7.11.0", - "@babel/preset-env": "^7.11.0", - "@babel/preset-react": "^7.10.4", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", + "@commitlint/cli": "^9.0.1", + "@commitlint/config-conventional": "^9.0.1", + "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.1.3", "@testing-library/react": "^11.0.4", - "@types/jest": "^26.0.14", - "@types/lodash": "^4.14.172", + "@types/jest": "^27.4.0", + "@types/lodash": "^4.14.178", "@types/marked": "^1.2.0", "@types/node": "^14.11.2", "@types/react-dom": "^16.9.8", "@types/react-loadable": "^5.5.3", "@types/react-test-renderer": "^16.9.3", - "@typescript-eslint/eslint-plugin": "^4.12.0", - "@typescript-eslint/parser": "^4.12.0", + "@typescript-eslint/eslint-plugin": "^5.10.1", + "@typescript-eslint/parser": "^5.10.1", "awesome-typescript-loader": "^5.2.1", "babel-jest": "^26.3.0", - "babel-loader": "^8.1.0", "babel-plugin-import": "^1.13.0", - "css-loader": "^4.3.0", - "eslint": "^7.17.0", - "eslint-config-standard": "^16.0.2", - "eslint-plugin-import": "^2.22.1", + "commitizen": "^4.2.4", + "commitlint-config-cz": "^0.13.2", + "cz-customizable": "^6.3.0", + "eslint": "^7.32.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-babel": "^5.3.1", + "eslint-plugin-compat": "^4.0.1", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-jest": "^26.0.0", + "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-react": "^7.22.0", - "express": "^4.17.1", - "father": "^2.29.11", - "html-webpack-plugin": "^4.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^26.4.2", - "less": "^3.12.2", - "less-loader": "^7.0.1", - "loadsh": "0.0.4", - "react-docgen": "^5.3.1", - "react-scripts": "^4.0.1", - "react-test-renderer": "^16.13.1", - "style-loader": "^1.2.1", - "ts-jest": "^26.4.0", - "typedoc": "^0.19.2", - "typescript": "^4.0.2", - "webpack": "^4.44.2", - "webpack-bundle-analyzer": "^3.9.0", - "webpack-cli": "^3.3.12", - "webpack-dev-server": "^3.11.0", - "webpack-merge": "^5.1.4" - }, - "dependencies": { - "@types/react": "^16.9.46", - "@types/react-router-dom": "^5.1.5", - "axios": "^0.20.0", - "lodash": "^4.17.21", - "marked": "^1.2.5", - "moment": "^2.29.0", - "qiankun": "^2.5.1", - "query-string": "^6.13.8", - "rc-table": "^7.9.10", - "react-loadable": "^5.5.0" + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-promise": "^4.3.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0", + "execa": "^4.1.0", + "husky": "^4.2.5", + "jest": "^27.4.7", + "lint-staged": "^10.2.11", + "npm-watch": "^0.11.0", + "prettier": "^2.5.1", + "rollup": "^2.66.0", + "rollup-plugin-eslint": "^7.0.0", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.31.1", + "ts-jest": "^27.1.3", + "typescript": "^4.5.5" }, "peerDependencies": { "react": "^16.13.1", diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000000000000000000000000000000000000..53ba1f0be5a747c77618518161f8456f29f4946c --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,42 @@ +import path from 'path' +import ts from 'rollup-plugin-typescript2' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import { babel } from '@rollup/plugin-babel' +import commonjs from '@rollup/plugin-commonjs' +import { eslint } from 'rollup-plugin-eslint' +import json from '@rollup/plugin-json' +import { terser } from 'rollup-plugin-terser' + +export default { + input: 'src/index.tsx', + output: [ + { + format: 'esm', + file: path.resolve('dist/index.esm.js') + } + ], + plugins: [ + json(), + // eslint({ + // throwOnError: true, + // exclude: ['node_modules/**', 'es/**', 'dist/**'] + // }), + ts({ + tsconfig: path.resolve(__dirname, 'tsconfig.json') + }), + babel({ + babelHelpers: 'runtime', + exclude: 'node_modules/**' + }), + + commonjs(), + nodeResolve({ + extensions: ['.js', '.ts', '.tsx'] + }), + terser() + ], + external: ['react', 'react-dom', 'marked', 'lodash', 'axios', 'query-string', 'moment', 'qiankun'], + watch: { + include: 'src/**' + } +} diff --git a/src/components/detail/common.tsx b/src/components/detail/common.tsx index 57902c18fa9ace6e57473ea7a8066cf43eff1e77..3c830feeecf449e1e7a4377317f0f1babf5c443e 100644 --- a/src/components/detail/common.tsx +++ b/src/components/detail/common.tsx @@ -3,6 +3,8 @@ import { ColumnsConfig, ParamConfig } from '../../interface' import { DetailFieldConfigs as getFieldConfigs } from './' import ParamHelper from '../../util/param' +import { CCMSConfig, PageListItem } from '../../main' + /** * 详情页表单项基类配置文件格式定义 * - field: 表单项字段名 @@ -24,7 +26,7 @@ export interface DetailFieldConfig { columns?: ColumnsConfig childColumns?: ColumnsConfig display?: 'none' - defaultValue?: string, + defaultValue?: ParamConfig, condition?: DetailFieldConditionConfig layout?: 'horizontal' | 'vertical' styles?: object @@ -74,20 +76,29 @@ export interface DetailFieldProps { value: T, record: { [field: string]: any }, data: any[], - step: number, + step: { [field: string]: any } // formValue挂载 config: C + // 挂载引用 + detail?: React.ReactNode // TODO 待删除 onChange: (value: T) => Promise // 事件:设置值 - onValueSet: (path: string, value: T, validation: true | DetailFieldError[]) => Promise + onValueSet: (path: string, value: T, options?: { noPathCombination?: boolean }) => Promise // // 事件:置空值 - onValueUnset: (path: string, validation: true | DetailFieldError[]) => Promise + onValueUnset: (path: string, options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 追加 - onValueListAppend: (path: string, value: any, validation: true | DetailFieldError[]) => Promise + onValueListAppend: (path: string, value: any, options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 删除 - onValueListSplice: (path: string, index: number, count: number, validation: true | DetailFieldError[]) => Promise + onValueListSplice: (path: string, index: number, count: number, options?: { noPathCombination?: boolean }) => Promise baseRoute: string, loadDomain: (domain: string) => Promise + loadPageConfig: (pageID: any) => Promise + loadPageList: () => Promise> + handlePageRedirect: (path: string) => void + checkPageAuth: (pageID: any) => Promise + onUnmount: (reload?: boolean, data?: any) => Promise + loadPageURL: (pageID: any) => Promise + loadPageFrameURL: (pageID: any) => Promise } /** @@ -132,7 +143,13 @@ export class DetailField extends Reac config } = this.props - return config.defaultValue + if (typeof config.defaultValue === 'string') { + return config.defaultValue + } + if (config.defaultValue !== undefined) { + return ParamHelper(config.defaultValue, { record: this.props.record, data: this.props.data, step: this.props.step }) + } + return undefined } set: (value: T) => Promise = async (value) => { diff --git a/src/components/detail/custom/index.tsx b/src/components/detail/custom/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..19a582f4153ec1445ca969e4a1a5367d8568ae18 --- /dev/null +++ b/src/components/detail/custom/index.tsx @@ -0,0 +1,95 @@ +import React, { RefObject } from 'react' +import { DetailField, DetailFieldConfig, DetailFieldProps, IDetailField } from '../common' +import { loadMicroApp, MicroApp } from 'qiankun' +import moment from 'moment' +import { cloneDeep } from 'lodash' + +export interface CustomDetailConfig extends DetailFieldConfig { + type: 'custom' + entry: string +} + +export default class CustomDtail extends DetailField implements IDetailField { + identifier: string = '' + entry: string = '' + container: RefObject = React.createRef() + customField: MicroApp | null = null + _get: () => Promise = async () => this.props.value + + componentDidMount () { + this.loadCustomField(this.props.config.entry) + } + + getSnapshotBeforeUpdate () { + const snapshot: string[] = [] + if (this.entry !== this.props.config.entry) { + snapshot.push('entry') + } + return snapshot + } + + get = async (): Promise => { + return await this._get() + } + + bindGet = async (get: () => Promise): Promise => { + this._get = get + } + + componentDidUpdate (_: DetailFieldProps, __: {}, snapshot: string[]) { + if (snapshot.includes('entry')) { + this.loadCustomField(this.props.config.entry) + } else { + if (this.customField && this.customField.update) { + this.customField.update({ + value: this.props.value, + record: this.props.record, + data: cloneDeep(this.props.data), + step: this.props.step, + config: this.props.config, + detail: this.props.detail, + onChange: this.props.onChange, + onValueSet: this.props.onValueSet, + onValueUnset: this.props.onValueUnset, + onValueListAppend: this.props.onValueListAppend, + onValueListSplice: this.props.onValueListSplice, + base: this.props.baseRoute, + loadDomain: this.props.loadDomain + }) + } + } + } + + loadCustomField = (entry: string) => { + if (this.container.current && entry) { + this.entry = this.props.config.entry + this.identifier = `custom|${moment().format('x')}|${Math.floor(Math.random() * 1000)}` + this.customField = loadMicroApp({ + name: this.identifier, + entry, + container: this.container.current, + props: { + value: this.props.value, + record: this.props.record, + data: cloneDeep(this.props.data), + step: this.props.step, + config: this.props.config, + detail: this.props.detail, + onChange: this.props.onChange, + onValueSet: this.props.onValueSet, + onValueUnset: this.props.onValueUnset, + onValueListAppend: this.props.onValueListAppend, + onValueListSplice: this.props.onValueListSplice, + base: this.props.baseRoute, + loadDomain: this.props.loadDomain + } + }) + } + } + + render = () => { + return ( +
+ ) + } +} diff --git a/src/components/detail/detailColor/index.tsx b/src/components/detail/detailColor/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f5b3c3e4ba107de0ecc1b7d8fab534492cafccbf --- /dev/null +++ b/src/components/detail/detailColor/index.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { DetailField, DetailFieldConfig, IDetailField } from '../common' + +export interface ColorDetailConfig extends DetailFieldConfig { + type: 'color' +} + +export interface IColorProps { + value: string +} + +export default class InfoDetail extends DetailField implements IDetailField { + reset: () => Promise = async () => { + const defaults = await this.defaultValue() + return (defaults === undefined) ? '' : defaults + } + + state = { + value: '' + } + + renderComponent = (props: IColorProps) => { + return + 您当前使用的UI版本没有实现colorDetail组件。 + + } + + componentDidMount() { + this.getValue() + } + + getValue = async () => { + const { + value, + config: { + defaultValue + } + } = this.props + + if (value && typeof value === 'string') { + return this.setState({ + value + }) + } + if (typeof defaultValue === 'string') { + this.setState({ + value: defaultValue + }) + } else { + this.setState({ + value: await this.reset() + }) + } + } + + render = () => { + const { value } = this.state + + return ( + + {this.renderComponent({ + value + })} + + ) + } +} diff --git a/src/components/detail/detailInfo/index.tsx b/src/components/detail/detailInfo/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..939662f34de8327d79c013c15cc27c3afbaec907 --- /dev/null +++ b/src/components/detail/detailInfo/index.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { DetailField, DetailFieldConfig, DetailFieldError, IDetailField } from '../common' +import StatementHelper, { StatementConfig } from '../../../util/statement' +import marked from 'marked' + +export interface InfoDetailConfig extends DetailFieldConfig { + type: 'detail_info' + description?: { + descType: 'text' | 'tooltip' | 'modal' + label?: StatementConfig + mode: 'plain' | 'markdown' | 'html' + content?: StatementConfig + showIcon: boolean + }, +} + +export interface IInfoProps { + description?: { + descType: 'text' | 'tooltip' | 'modal' + label: string | undefined + content: React.ReactNode + showIcon: boolean + } +} + +export default class InfoDetail extends DetailField implements IDetailField { + renderComponent = (props: IInfoProps) => { + return + 您当前使用的UI版本没有实现InfoDetail组件。 + + } + + render = () => { + const props: IInfoProps = {} + const { + config: { + description + } + } = this.props + if(description) { + if(description.descType === 'text') { + props.description = { + descType: 'text', + label: StatementHelper(description.label, { data: this.props.data, step: this.props.step }), + content: description.content, + showIcon: description.showIcon + } + } else if (description.descType === 'tooltip') { + props.description = { + descType: 'tooltip', + label: StatementHelper(description.label, { data: this.props.data, step: this.props.step }), + content: description.content, + showIcon: description.showIcon + } + } else { + props.description = { + descType: 'modal', + label: StatementHelper(description.label, { data: this.props.data, step: this.props.step }), + content: description.content, + showIcon: description.showIcon + } + } + if(description.content !== undefined) { + const descriptionType = description.mode + switch (descriptionType) { + case 'plain': + props.description && (props.description.content = StatementHelper(description.content, { data: this.props.data, step: this.props.step })) + break + case 'markdown': + props.description && (props.description.content =
) + break + case 'html': + props.description && (props.description.content =
) + break + } + } + } + + return ( + + {this.renderComponent(props)} + + ) + } +} diff --git a/src/components/detail/enum/index.tsx b/src/components/detail/enum/index.tsx index c79b2cf054310511baceae49e1cd7c387808179b..7e5858fe92949f99d206465ce389c8d4963a28c4 100644 --- a/src/components/detail/enum/index.tsx +++ b/src/components/detail/enum/index.tsx @@ -1,11 +1,12 @@ -import { config } from 'process' import React from 'react' -import { DetailField, DetailFieldConfig, DetailFieldError, DetailFieldProps, IDetailField } from '../common' +import { DetailField, DetailFieldProps, DetailFieldConfig, IDetailField } from '../common' +import InterfaceHelper, { InterfaceConfig } from '../../../util/interface' +import { getValue } from '../../../util/value' export interface EnumDetailConfig extends DetailFieldConfig { type: 'detail_enum' multiple: boolean | ArrayMultipleConfig | SplitMultipleConfig - options: ManualOptionsConfig + options: ManualOptionsConfig | InterfaceOptionsConfig } interface ArrayMultipleConfig { @@ -28,14 +29,40 @@ interface ManualOptionsConfig { getValue?: string } +export interface InterfaceOptionsConfig { + from: 'interface' + interface?: InterfaceConfig + format?: InterfaceOptionsListConfig | InterfaceOptionKVConfig +} + +export interface InterfaceOptionKVConfig { + type: 'kv' +} +export interface InterfaceOptionsListConfig { + type: 'list' + keyField: string + labelField: string +} + + export interface IEnumProps { value?: string | string[] } export default class EnumDetail extends DetailField implements IDetailField { + interfaceHelper = new InterfaceHelper() + reset: () => Promise = async () => { const defaults = await this.defaultValue() - return (defaults === undefined) ? '' : defaults + return (defaults === undefined) ? '/' : defaults + } + + state = { + value: '' + } + + constructor(props: DetailFieldProps) { + super(props) } renderComponent = (props: IEnumProps) => { @@ -44,8 +71,11 @@ export default class EnumDetail extends DetailField } - - getValue = () => { + componentDidMount() { + this.getValue() + } + + getValue = async () => { const { value, config: { @@ -55,7 +85,17 @@ export default class EnumDetail extends DetailField option.value === value) - return option ? option.label : value.toString() + this.setState({ + value: option ? option.label : value.toString() + }) } else if (multiple === true || multiple.type) { if (Array.isArray(theValue)) { - return theValue.map((item) => { - const option = options.data.find((option) => { - return option.value === Number(item) - }) - return option ? option.label : item.toString() - }).join(',') + this.setState({ + value: theValue.map((item) => { + const option = options.data.find((option) => { + return option.value === Number(item) + }) + return option ? option.label : item.toString() + }).join(',') + }) } else { - return '-' + this.setState({ + value: '-' + }) } } + } else { + this.setState({ + value: value + }) + } + } else if (options && options.from === 'interface') { + if (options.interface) { + this.interfaceHelper.request( + options.interface, + {}, + { record: this.props.record, data: this.props.data, step: this.props.step }, + { loadDomain: this.props.loadDomain } + ).then((data) => { + + if (options.format) { + type OptionItem = { value: string, label: string } + let tempOptions: Array = [] + if (options.format.type === 'kv') { + tempOptions = Object.keys(data).map((key) => ({ + value: key, + label: data[key] + })) + this.setState({ + value: theValue.map((item: OptionItem) => { + const option = tempOptions.find((option) => { + return option.value === String(item) + }) + return option ? option.label : item.toString() + }).join(',') + }) + } else if (options.format.type === 'list') { + tempOptions = data.map((item: any) => { + if (options.format && options.format.type === 'list') { + return ({ + value: getValue(item, options.format.keyField), + label: getValue(item, options.format.labelField) + }) + } + }) + this.setState({ + value: theValue.map((item: OptionItem) => { + const option = tempOptions.find((option) => { + return option.value === String(item) + }) + return option ? option.label : item.toString() + }).join(',') + }) + } + } + }) } else { return value } - } else { + } + else { return value } } render = () => { - const value = this.getValue() + // const value = this.getValue() + const { value } = this.state return ( {this.renderComponent({ diff --git a/src/components/detail/group/index.tsx b/src/components/detail/group/index.tsx index c7dd58d9c1ad00c9a059da099d6167f32302480b..c80378a3083a861dce1735c5bb9e8cb1a4bb05b7 100644 --- a/src/components/detail/group/index.tsx +++ b/src/components/detail/group/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import { cloneDeep } from 'lodash' import { setValue, getValue } from '../../../util/value' -import { DetailField, DetailFieldConfig, DetailFieldError, DetailFieldProps, IDetailField } from '../common' +import { DetailField, DetailFieldConfig, DetailFieldProps, IDetailField } from '../common' import getALLComponents, { DetailFieldConfigs } from '../' import { IDetailItem } from '../../../steps/detail' import ConditionHelper from '../../../util/condition' @@ -20,7 +20,6 @@ export interface IGroupField { } interface IGroupFieldState { - detailData: { status: 'normal' | 'error' | 'loading', message?: string }[] } export default class GroupField extends DetailField implements IDetailField { @@ -30,12 +29,10 @@ export default class GroupField extends DetailField | null> = [] detailFieldsMounted: Array = [] - constructor (props: DetailFieldProps) { + constructor(props: DetailFieldProps) { super(props) - this.state = { - detailData: [] - } + this.state = {} } get = async () => { @@ -68,22 +65,6 @@ export default class GroupField extends DetailField { - detailData[detailFieldIndex] = { status: 'normal' } - return { detailData: cloneDeep(detailData) } - }) - } else { - await this.setState(({ detailData }) => { - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message } - return { detailData: cloneDeep(detailData) } - }) - } await detailField?.didMount() } } @@ -118,79 +99,35 @@ export default class GroupField extends DetailField { + handleValueSet = async (detailFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` - await this.props.onValueSet(fullPath, value, true) - - const detailData = cloneDeep(this.state.detailData) - if (validation === true) { - detailData[detailFieldIndex] = { status: 'normal' } - } else { - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - detailData - }) + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) + await this.props.onValueSet(fullPath, value) } } - handleValueUnset = async (detailFieldIndex: number, path: string, validation: true | DetailFieldError[]) => { + handleValueUnset = async (detailFieldIndex: number, path: string, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` - await this.props.onValueUnset(fullPath, true) - - const detailData = cloneDeep(this.state.detailData) - if (validation === true) { - detailData[detailFieldIndex] = { status: 'normal' } - } else { - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - detailData - }) + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) + await this.props.onValueUnset(fullPath) } } - handleValueListAppend = async (detailFieldIndex: number, path: string, value: any, validation: true | DetailFieldError[]) => { + handleValueListAppend = async (detailFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` - await this.props.onValueListAppend(fullPath, value, true) - - const detailData = cloneDeep(this.state.detailData) - if (validation === true) { - detailData[detailFieldIndex] = { status: 'normal' } - } else { - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - detailData - }) + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) + await this.props.onValueListAppend(fullPath, value) } } - handleValueListSplice = async (detailFieldIndex: number, path: string, index: number, count: number, validation: true | DetailFieldError[]) => { + handleValueListSplice = async (detailFieldIndex: number, path: string, index: number, count: number, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` - await this.props.onValueListSplice(fullPath, index, count, true) - - const detailData = cloneDeep(this.state.detailData) - if (validation === true) { - detailData[detailFieldIndex] = { status: 'normal' } - } else { - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - detailData - }) + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) + await this.props.onValueListSplice(fullPath, index, count) } } @@ -250,40 +187,48 @@ export default class GroupField extends DetailField | null) => { - if (detailFieldIndex !== null) { - this.detailFields[detailFieldIndex] = detailField - this.handleMount(detailFieldIndex) - } - }} - formLayout={formLayout} - value={getValue(value, detailFieldConfig.field)} - record={record} - data={cloneDeep(data)} - step={step} - config={detailFieldConfig} - onChange={async (value: any) => { await this.handleChange(detailFieldIndex, value) }} - onValueSet={async (path, value, validation) => this.handleValueSet(detailFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => this.handleValueUnset(detailFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => this.handleValueListAppend(detailFieldIndex, path, value, validation)} - onValueListSplice={async (path, index, count, validation) => this.handleValueListSplice(detailFieldIndex, path, index, count, validation)} - baseRoute={this.props.baseRoute} - loadDomain={async (domain: string) => await this.props.loadDomain(domain)} - /> + | null) => { + if (detailFieldIndex !== null) { + this.detailFields[detailFieldIndex] = detailField + this.handleMount(detailFieldIndex) + } + }} + formLayout={formLayout} + value={getValue(value, detailFieldConfig.field)} + record={record} + data={cloneDeep(data)} + step={step} + config={detailFieldConfig} + detail={this.props.detail} + onChange={async (value: any) => { await this.handleChange(detailFieldIndex, value) }} + onValueSet={async (path, value, options) => this.handleValueSet(detailFieldIndex, path, value, options)} + onValueUnset={async (path, options) => this.handleValueUnset(detailFieldIndex, path, options)} + onValueListAppend={async (path, value, options) => this.handleValueListAppend(detailFieldIndex, path, value, options)} + onValueListSplice={async (path, index, count, options) => this.handleValueListSplice(detailFieldIndex, path, index, count, options)} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + /> ) } // 渲染表单项容器 diff --git a/src/components/detail/image/index.tsx b/src/components/detail/image/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2783cb1759f396d1992d32e9efead623b36c814b --- /dev/null +++ b/src/components/detail/image/index.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import { DetailField, DetailFieldConfig, IDetailField } from '../common' + +/** + * 详情项图片组件 格式定义 + * - type: 图片类型 + * - height: 图片高度 + * - width: 图片宽度 + * - preview: 点击预览 + */ +export interface ImageDetailConfig extends DetailFieldConfig { + type: 'image', + height?: string | number + width?: string | number + preview?: boolean +} + +export interface IImageDetail { + value: string + height: string | number + width: string | number + preview?: boolean +} + +export default class ImageDetail extends DetailField implements IDetailField { + renderComponent = (props: IImageDetail) => { + return + 您当前使用的UI版本没有实现Image组件。 +
+
+
+ } + + getValue = () => { + const { + value, + config: { + defaultValue + } + } = this.props + if (value === undefined || value === null || value === '') { + return defaultValue !== undefined ? defaultValue : '' + } + return value + } + + render = () => { + const { + config: { + height, + width, + preview + } + } = this.props + const props: any = { + height, + width, + preview, + value: this.getValue() + } + + return ( + + {this.renderComponent(props)} + + ) + } +} diff --git a/src/components/detail/importSubform/index.tsx b/src/components/detail/importSubform/index.tsx index 1565bc147a81efbf2936fb564154d6420958e923..a4360cdfaefcd7cc96a87d6f105cf78ce2896a49 100644 --- a/src/components/detail/importSubform/index.tsx +++ b/src/components/detail/importSubform/index.tsx @@ -1,24 +1,26 @@ import React from 'react' -import { getValue } from '../../../util/value' +import { cloneDeep, isEqual } from 'lodash' +import { setTimeout } from 'timers' +import { getValue, getChainPath } from '../../../util/value' import { DetailField, DetailFieldConfig, DetailFieldProps, IDetailField } from '../common' import { Display } from '../../formFields/common' import { display as getALLComponents, FieldConfigs } from '../../formFields' import { IDetailItem } from '../../../steps/detail' -import { cloneDeep, isEqual } from 'lodash' import ConditionHelper from '../../../util/condition' import InterfaceHelper, { InterfaceConfig } from '../../../util/interface' import { ColumnsConfig } from '../../../interface' /** * 子表单配置项 - * - withConfig: 拓展配置 - * - * - enable: 是否开启 - * - * - dataField: (序列化)数据 - * - * - configField: (序列化)配置 + * - configFrom: 配置来源(get用途) + * - * - type: 'data' | 'interface' // 来源类型 + * - * - dataField: 值来源字段 // 仅type为data时生效 + * - * - configField: 配置来源字段 // 仅type为data时生效 + * - * - interface: 来源接口配置 // 仅type为interface时生效 */ export interface ImportSubformFieldConfig extends DetailFieldConfig { - type: 'import_subform', + type: 'import_subform' configFrom?: ImportSubformConfigFromData | ImportSubformConfigFromInterface childColumns?: ColumnsConfig } @@ -42,23 +44,28 @@ export interface IImportSubformField { interface IImportSubformFieldState { didMount: boolean fields: FieldConfigs[] - formData: { status: 'normal' | 'error' | 'loading', message?: string }[] + formData: { status: 'normal' | 'error' | 'loading'; message?: string }[] } -export default class ImportSubformField extends DetailField implements IDetailField { +export default class DetailImportSubformField + extends DetailField + implements IDetailField +{ // 各表单项对应的类型所使用的UI组件的类 getALLComponents = (type: any): typeof Display => getALLComponents[type] // 用于请求防频的判断条件 - requestConfig: string = '' - value: string = '' + requestConfig = '' + + value = '' formFields: Array | null> = [] + formFieldsMounted: Array = [] interfaceHelper = new InterfaceHelper() - constructor (props: DetailFieldProps) { + constructor(props: DetailFieldProps) { super(props) this.state = { @@ -68,13 +75,6 @@ export default class ImportSubformField extends DetailField { await this.setState({ didMount: true @@ -90,57 +90,95 @@ export default class ImportSubformField extends DetailField { + handleValueSet = async ( + formFieldIndex: number, + path: string, + value: any, + options?: { noPathCombination?: boolean } + ) => { + const withConfigPath = + this.props.config.configFrom?.type === 'data' && this.props.config.configFrom.dataField + ? `${this.props.config.configFrom.dataField}` + : '' const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) - await this.props.onValueSet(fullPath, value, true) + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) + await this.props.onValueSet(fullPath, value) } } - handleValueUnset = async (formFieldIndex: number, path: string) => { + handleValueUnset = async (formFieldIndex: number, path: string, options?: { noPathCombination?: boolean }) => { + const withConfigPath = + this.props.config.configFrom?.type === 'data' && this.props.config.configFrom.dataField + ? `${this.props.config.configFrom.dataField}` + : '' const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) - await this.props.onValueUnset(fullPath, true) + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) + await this.props.onValueUnset(fullPath) } } - handleValueListAppend = async (formFieldIndex: number, path: string, value: any) => { + handleValueListAppend = async ( + formFieldIndex: number, + path: string, + value: any, + options?: { noPathCombination?: boolean } + ) => { + const withConfigPath = + this.props.config.configFrom?.type === 'data' && this.props.config.configFrom.dataField + ? `${this.props.config.configFrom.dataField}` + : '' const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) - await this.props.onValueListAppend(fullPath, value, true) + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) + await this.props.onValueListAppend(fullPath, value) } } - handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number) => { + handleValueListSplice = async ( + formFieldIndex: number, + path: string, + index: number, + count: number, + options?: { noPathCombination?: boolean } + ) => { + const withConfigPath = + this.props.config.configFrom?.type === 'data' && this.props.config.configFrom.dataField + ? `${this.props.config.configFrom.dataField}` + : '' const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) - await this.props.onValueListSplice(fullPath, index, count, true) + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) + await this.props.onValueListSplice(fullPath, index, count) } } renderComponent = (props: IImportSubformField) => { - return - 您当前使用的UI版本没有实现ImportSubformField组件。 - + return <>您当前使用的UI版本没有实现ImportSubformField组件。 } /** @@ -149,71 +187,90 @@ export default class ImportSubformField extends DetailField { - return - 您当前使用的UI版本没有实现FormItem组件。 - + return <>您当前使用的UI版本没有实现FormItem组件。 } - render = () => { - const { - config, - formLayout, - value, - record, - data, - step - } = this.props + /** + * 处理data 兼容非法json的情况 + * @param {any} data 待处理数据 + * @returns 返回data反序列化形式 + */ + handleDataToUnstringfy = (data: any) => { + let dataToUnstringfy = data + if (Object.prototype.toString.call(data) === '[object String]') { + try { + dataToUnstringfy = JSON.parse(data) + } catch (e) { + console.error('当前动态子表单接口响应数据格式不是合格的json字符串') + dataToUnstringfy = [] + } + } + return dataToUnstringfy + } + getConfigData = () => { + const { config, value } = this.props if (config.configFrom && config.configFrom.type === 'interface' && config.configFrom.interface) { - this.interfaceHelper.request( - config.configFrom.interface, - {}, - { record: this.props.record, data: this.props.data, step: this.props.step }, - { loadDomain: this.props.loadDomain } - ).then((data: any) => { - let dataToUnstringfy = data - let dataToStringfy = JSON.stringify(data) - if (Object.prototype.toString.call(data) === '[object String]') { - try { - dataToStringfy = data - dataToUnstringfy = JSON.parse(data) - } catch (e) { - console.error('当前动态子表单接口响应数据格式不是合格的json字符串') - dataToUnstringfy = [] - dataToStringfy = '[]' + this.interfaceHelper + .request( + config.configFrom.interface, + {}, + { record: this.props.record, data: this.props.data, step: this.props.step }, + { loadDomain: this.props.loadDomain } + ) + .then((data: any) => { + const dataToUnstringfy = this.handleDataToUnstringfy(data) + if (!isEqual(dataToUnstringfy, this.state.fields)) { + this.setState({ + fields: dataToUnstringfy + }) } - } - if (dataToStringfy !== JSON.stringify(this.state.fields)) { - this.setState({ - fields: dataToUnstringfy - }) - } - }) + }) } - let fields = this.state.fields + let { fields } = this.state if (config.configFrom && config.configFrom.type === 'data') { fields = config.configFrom.configField ? getValue(value, config.configFrom.configField) : [] - if (!isEqual(fields, this.state.fields)) { + const dataToUnstringfy = this.handleDataToUnstringfy(fields) + if (!isEqual(dataToUnstringfy, this.state.fields)) { this.setState({ - fields + fields: dataToUnstringfy }) } } + } + + componentDidMount() { + this.getConfigData() + } + + componentDidUpdate() { + this.getConfigData() + } + + render = () => { + const { config, formLayout, value, record, data, step } = this.props + + const { fields } = this.state + if (!fields || !Array.isArray(fields) || fields.length === 0) { - return - } else { - return ( - - {this.renderComponent({ - columns: config?.columns?.enable ? config.columns : undefined, - children: this.state.didMount - ? fields.map((formFieldConfig, formFieldIndex) => { + return <> + } + const withConfigPath = + this.props.config.configFrom?.type === 'data' && this.props.config.configFrom.dataField + ? `${this.props.config.configFrom.dataField}` + : '' + return ( + <> + {this.renderComponent({ + columns: config?.columns?.enable ? config.columns : undefined, + children: this.state.didMount + ? fields.map((formFieldConfig, formFieldIndex) => { if (!ConditionHelper(formFieldConfig.condition, { record: value, data, step })) { this.formFieldsMounted[formFieldIndex] = false return null } - let display: boolean = true + let display = true if (formFieldConfig.type === 'hidden' || formFieldConfig.display === 'none') { display = false @@ -245,15 +302,21 @@ export default class ImportSubformField extends DetailField this.handleValueSet(formFieldIndex, path, value)} - onValueUnset={async (path) => this.handleValueUnset(formFieldIndex, path)} - onValueListAppend={async (path, value) => this.handleValueListAppend(formFieldIndex, path, value)} - onValueListSplice={async (path, index, count) => this.handleValueListSplice(formFieldIndex, path, index, count)} + onValueSet={async (path, value, options) => + this.handleValueSet(formFieldIndex, path, value, options) + } + onValueUnset={async (path, options) => this.handleValueUnset(formFieldIndex, path, options)} + onValueListAppend={async (path, value, options) => + this.handleValueListAppend(formFieldIndex, path, value, options) + } + onValueListSplice={async (path, index, count, options) => + this.handleValueListSplice(formFieldIndex, path, index, count, options) + } baseRoute={this.props.baseRoute} loadDomain={async (domain: string) => await this.props.loadDomain(domain)} /> @@ -262,10 +325,9 @@ export default class ImportSubformField extends DetailField - ) - } + : [] + })} + + ) } } diff --git a/src/components/detail/index.tsx b/src/components/detail/index.tsx index 24ae0574587bd9fa0fb4b21556f333435775814c..c19ed86a48c83c8760c37e8dc478d9ab272fdc9f 100644 --- a/src/components/detail/index.tsx +++ b/src/components/detail/index.tsx @@ -1,32 +1,50 @@ - import TextField, { TextFieldConfig } from './text' import EnumDetail, { EnumDetailConfig } from './enum' import StatementDetail, { StatementDetailConfig } from './statement' - +import ImageDetail, { ImageDetailConfig } from './image' +import CustomDetail, { CustomDetailConfig } from './custom' import GroupField, { GroupFieldConfig } from './group' import ImportSubformField, { ImportSubformFieldConfig } from './importSubform' +import InfoDetail, { InfoDetailConfig } from './detailInfo' +import ColorDetail, { ColorDetailConfig } from './detailColor' +import TableField, { TableFieldConfig } from './table' /** * 详情步骤内详情项配置文件格式定义 - 枚举 */ export type DetailFieldConfigs = - TextFieldConfig | - EnumDetailConfig | - StatementDetailConfig | - GroupFieldConfig | - ImportSubformFieldConfig + TextFieldConfig | + EnumDetailConfig | + StatementDetailConfig | + ImageDetailConfig | + GroupFieldConfig | + ImportSubformFieldConfig | + InfoDetailConfig | + ColorDetailConfig | + TableFieldConfig | + CustomDetailConfig export type componentType = - 'text' | - 'group' | - 'detail_enum' | - 'statement' | - 'import_subform' + 'text' | + 'group' | + 'detail_enum' | + 'statement' | + 'image' | + 'import_subform' | + 'detail_info' | + 'detail_color' | + 'table' | + 'custom' export default { group: GroupField, text: TextField, import_subform: ImportSubformField, detail_enum: EnumDetail, - statement: StatementDetail + image: ImageDetail, + statement: StatementDetail, + detail_info: InfoDetail, + detail_color: ColorDetail, + table: TableField, + custom: CustomDetail } diff --git a/src/components/detail/table/common/columnStyle.tsx b/src/components/detail/table/common/columnStyle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a0d9e222dc0e70ea03af710b6857e0048a556640 --- /dev/null +++ b/src/components/detail/table/common/columnStyle.tsx @@ -0,0 +1,20 @@ +import React, { Component } from 'react' + +type Props = { + style: any + addfix: boolean +}; + +export default class ColumnStyleComponent extends Component { + render () { + const { style, addfix = true } = this.props + const reSetStyle = Object.assign({}, { fontSize: style?.fontSize, color: style?.color }, style?.customStyle) + return addfix + ?
+ {style?.prefix || ''}{this.props.children}{style?.postfix || ''} +
+ :
+ {this.props.children} +
+ } +} diff --git a/src/components/detail/table/index.tsx b/src/components/detail/table/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d8c399a3e1dff976f8944394b769a97dc308b06c --- /dev/null +++ b/src/components/detail/table/index.tsx @@ -0,0 +1,765 @@ +import React from 'react' +import queryString from 'query-string' +import { getParam, getParamText, getValue } from '../../../util/value' +import { getBoolean } from '../../../util/value' +import { DetailField, DetailFieldConfig, DetailFieldError, IDetailField } from '../common' +import getALLComponents, { ColumnConfigs } from '../../tableColumns' +import CCMS, { CCMSConfig } from '../../../main' +import InterfaceHelper, { InterfaceConfig } from '../../../util/interface' +import ConditionHelper, { ConditionConfig } from '../../../util/condition' +import { ParamConfig } from '../../../interface' +import { DetailFieldProps } from '../common' +import { cloneDeep, get, set } from 'lodash' +import ColumnStyleComponent from './common/columnStyle' +import Column from '../../tableColumns/common' + + +export interface TableFieldConfig extends DetailFieldConfig { + type: 'table' + primary: string + tableColumns: ColumnConfigs[] + operations?: { + rowOperations?: Array + } + pagination?: { + mode: 'none' | 'client' | 'server' + current?: string + pageSize?: string + total?: string + } +} + + +/** + * 表格步骤-菜单配置 + */ +export type TableOperationsType = TableOperationConfig | TableOperationGroupConfig | TableOperationDropdownConfig + +/** + * 表格步骤-操作配置文件格式 + */ +export interface TableOperationGroupConfig { + type: 'group' + label?: string + level?: 'normal' | 'primary' | 'danger' + operations: Array +} + +/** + * 表格步骤-操作配置文件下拉菜单 + */ +export interface TableOperationDropdownConfig { + type: 'dropdown' + label?: string + level?: 'normal' | 'primary' | 'danger' + operations: Array +} + +/** + * 表格步骤-操作配置文件格式 + */ +export interface TableOperationConfig { + type: 'button' + label: string + level?: 'normal' | 'primary' | 'danger' + check?: { enable: false } | TableOperationCheckConfig + confirm?: { enable: false } | TableOperationConfirmConfig + handle: TableCCMSOperationConfig | TableLinkOperationConfig + condition?: ConditionConfig +} + +export interface TableCCMSOperationConfig { + type: 'ccms' + page: any + target: 'current' | 'page' | 'open' | 'handle' + targetURL: string + data: { [key: string]: ParamConfig } + params?: { field: string, data: ParamConfig }[] + callback?: boolean + debug?: boolean +} + +export interface TableLinkOperationConfig { + type: 'link' + target: '_blank' | '_self' + targetURL: string + params?: { field: string, data: ParamConfig }[] + callback?: boolean + debug?: boolean +} + +interface TableOperationCheckConfig { + enable: true + interface: InterfaceConfig +} + +interface TableOperationConfirmConfig { + enable: true + titleText: string + titleParams?: Array<{ field: string, data: ParamConfig }> + okText: string + cancelText: string +} + +/** + * 表格步骤组件 - 列 - UI渲染方法 - 入参 + */ +export interface ITableColumn { + field: string + label: string + align: 'left' | 'center' | 'right' + render: (value: any, record: { [type: string]: any }, index: number) => React.ReactNode +} + +/** + * 表格步骤组件 - 操作 - UI渲染方法 - 入参 + */ +export interface ITableDetailRowOperation { + children: (React.ReactNode | undefined)[] +} + +/** + * 表格步骤组件 - 操作 - UI渲染方法 - 入参 + */ +export interface ITableDetailTableOperation { + children: (React.ReactNode | undefined)[] +} + +/** + * 表格步骤组件 - 操作按钮 - UI渲染方法 - 入参 + */ +export interface ITableDetailRowOperationButton { + label: string + level: 'normal' | 'primary' | 'danger' + disabled?: boolean + onClick: () => Promise +} + +/** + * 表格步骤组件 - 操作按钮组 - UI渲染方法 - 入参 + */ +export interface ITableDetailRowOperationGroup { + label?: string + children: React.ReactNode[] +} + +/** + * 表格步骤组件 - 操作按钮组元素 - UI渲染方法 - 入参 + */ +export interface ITableDetailRowOperationGroupItem { + label: string + level: 'normal' | 'primary' | 'danger' + disabled?: boolean + onClick: () => Promise +} + +/** + * 表格步骤组件 - 操作按钮 - UI渲染方法 - 入参 + */ +export interface ITableDetailTableOperationButton { + label: string + level: 'normal' | 'primary' | 'danger' + disabled?: boolean + onClick: () => Promise +} + +/** + * 表格步骤组件 - 操作按钮组 - UI渲染方法 - 入参 + */ +export interface ITableDetailTableOperationGroup { + label?: string + children: React.ReactNode[] +} + +/** + * 表格步骤组件 - 操作按钮组元素 - UI渲染方法 - 入参 + */ +export interface ITableDetailTableOperationGroupItem { + label: string + level: 'normal' | 'primary' | 'danger' + disabled?: boolean + onClick: () => Promise +} + +/** + * 表格步骤组件 - 操作 - 二次确认 - UI渲染方法 - 入参 + */ +export interface ITableDetailOperationConfirm { + title: string + okText: string + cancelText: string + onOk: () => void + onCancel: () => void +} + +export interface ITableDetailOperationModal { + title: string + visible: boolean + width: string + children: React.ReactNode + onClose: () => void +} + +interface TableState { + operation: { + enable: boolean + target: 'current' | 'handle' + title: string + visible: boolean + config: CCMSConfig + data: any + callback?: boolean + } + pageAuth: { [page: string]: boolean } +} + +/** + * 表格步骤组件 - UI渲染方法 - 入参 + * - data: 数据 + */ +export interface ITableField { + title: string | null + primary: string + data: { [field: string]: any }[] + tableColumns: ITableColumn[] + pagination?: { + current: number + pageSize: number + total: number + onChange: (page: number, pageSize: number) => void + } + // tableOperations: React.ReactNode | null + // multirowOperations: React.ReactNode | null, + description?: { + type: 'text' | 'tooltip' | 'modal' + label: string | undefined + content: React.ReactNode + showIcon: boolean + } +} + +export default class TableField extends DetailField implements IDetailField { + CCMS = CCMS + getALLComponents = (type: any): typeof Column => getALLComponents[type] + interfaceHelper = new InterfaceHelper() + /** + * 页面权限获取状态 + * fulfilled |pending + */ + pageAuth: { [page: string]: boolean } = {} + /* 服务端分页情况下页码溢出标识:页码溢出时退回重新请求,此标识符用于防止死循环判断 */ + pageOverflow: boolean = false + + constructor(props: DetailFieldProps) { + super(props) + + this.state = { + operation: { + enable: false, + target: 'current', + title: '', + visible: false, + config: {}, + data: {}, + callback: false + }, + pageAuth: {} + } + } + + /** + * 执行操作 + * @param operation + * @param record + * @returns + */ + handleRowOperation = async (operation: TableOperationConfig, record: { [field: string]: any }) => { + const { + data, + step + } = this.props + if (operation.check && operation.check.enable) { + const checkResult = await this.interfaceHelper.request( + operation.check.interface, + {}, + { record, data, step }, + { loadDomain: this.props.loadDomain } + ) + if (!checkResult) { + return false + } + } + + if (operation.confirm && operation.confirm.enable) { + const title = operation.confirm.titleParams ? (await getParamText(operation.confirm.titleText, operation.confirm.titleParams, { record, data, step })) : operation.confirm.titleText + const showConfirm = () => { + return new Promise((resolve, reject) => { + if (operation.confirm && operation.confirm.enable) { + this.renderOperationConfirm({ + title, + okText: operation.confirm.okText, + cancelText: operation.confirm.cancelText, + onOk: () => { resolve(true) }, + onCancel: () => { reject(new Error('用户取消')) } + }) + } + }) + } + try { + await showConfirm() + } catch (e) { + return false + } + } + + if (operation.handle.type === 'ccms') { + const params = {} + if (operation.handle.params === undefined) { + for (const [field, param] of Object.entries(operation.handle.data || {})) { + const value = getParam(param, { record, data, step }) + set(params, field, value) + } + } else { + for (const { field, data: dataConfig } of operation.handle.params) { + const value = getParam(dataConfig, { record, data, step }) + set(params, field, value) + } + } + if (operation.handle.debug) { + console.log('CCMS debug: operation - params', params) + } + if (operation.handle.target === 'current' || operation.handle.target === 'handle') { + const operationConfig = await this.props.loadPageConfig(operation.handle.page) + let obj = { + operation: { + enable: true, + target: operation.handle.target, + title: operation.label, + visible: true, + config: operationConfig, + data: params, + callback: operation.handle.callback + } + } + this.setState(obj) + } else if (operation.handle.target === 'page') { + const sourceURL = this.props.loadPageURL ? await this.props.loadPageURL(operation.handle.page) : '' + const { url, query } = queryString.parseUrl(sourceURL, { arrayFormat: 'bracket' }) + const targetURL = operation.handle.targetURL || '' + const targetKey = queryString.stringifyUrl({ url, query: { ...query, ...params } }, { arrayFormat: 'bracket' }) || '' + if (this.props.handlePageRedirect) { + this.props.handlePageRedirect(`${targetURL}${targetKey}`) + } else { + window.location.href = `${targetURL}${targetKey}` + } + } else if (operation.handle.target === 'open') { + const sourceURL = this.props.loadPageFrameURL ? await this.props.loadPageFrameURL(operation.handle.page) : '' + const { url, query } = queryString.parseUrl(sourceURL, { arrayFormat: 'bracket' }) + const targetURL = operation.handle.targetURL || '' + const targetKey = queryString.stringifyUrl({ url, query: { ...query, ...params } }, { arrayFormat: 'bracket' }) || '' + window.open(`${targetURL}${targetKey}`) + } + } + + // 当按钮的响应类型是第三方链接时 + if (operation.handle.type === 'link') { + const params = {} + if (operation.handle.params !== undefined) { + for (const { field, data: dataConfig } of operation.handle.params) { + const value = getParam(dataConfig, { record, data, step }) + set(params, field, value) + } + + if (operation.handle.debug) { + console.log('CCMS debug: operation - operation.handle.type === link', params) + } + + const targetURL = operation.handle.targetURL + const { query } = queryString.parseUrl(targetURL, { arrayFormat: 'bracket' }) + const targetKey = queryString.stringifyUrl({ url: '', query: { ...query, ...params } }, { arrayFormat: 'bracket' }) || '' + const jumpUrl = `${targetURL}${targetKey}` + if (operation.handle.target === '_blank') { + window.open(jumpUrl) + } else { + window.location.href = jumpUrl + } + } + } + } + + /** + * 渲染 操作二次确认弹窗 + * @param props + */ + renderOperationConfirm = (props: ITableDetailOperationConfirm) => { + const mask = document.createElement('DIV') + mask.style.position = 'fixed' + mask.style.left = '0px' + mask.style.top = '0px' + mask.style.width = '100%' + mask.style.height = '100%' + mask.style.backgroundColor = 'white' + mask.innerText = '您当前使用的UI版本没有实现Table的OperationConfirm组件。' + mask.onclick = () => { + mask.remove() + props.onOk() + } + + document.body.appendChild(mask) + } + + checkPageAuth = (page: string) => { + if (!this.pageAuth[page]) { + this.pageAuth[page] = true + + this.props.checkPageAuth && this.props.checkPageAuth(page).then((auth) => { + const pageAuth = cloneDeep(this.state.pageAuth) + pageAuth[page] = auth + this.setState({ pageAuth }) + }) + } + } + + + /** + * 渲染 表格 + * @param props + * @returns + */ + renderComponent = (props: ITableField) => { + return + 您当前使用的UI版本没有实现Table组件。 +
+
+
+ } + + renderRowOperationComponent = (props: ITableDetailRowOperation) => { + return + 您当前使用的UI版本没有实现Table组件的OperationButton部分。 + + } + + renderRowOperationButtonComponent = (props: ITableDetailRowOperationButton) => { + return + 您当前使用的UI版本没有实现Table组件的OperationButton部分。 + + } + + renderRowOperationGroupComponent = (props: ITableDetailRowOperationGroup) => { + return + 您当前使用的UI版本没有实现Table组件的OperationGroup部分。 + + } + + renderRowOperationGroupItemComponent = (props: ITableDetailRowOperationGroupItem) => { + return + 您当前使用的UI版本没有实现Table组件的OperationGroupItem部分。 + + } + + renderRowOperationDropdownComponent = (props: ITableDetailRowOperationGroup) => { + return + 您当前使用的UI版本没有实现Table组件的OperationDropdown部分。 + + } + + renderRowOperationDropdownItemComponent = (props: ITableDetailRowOperationGroupItem) => { + return + 您当前使用的UI版本没有实现Table组件的OperationDropdownItem部分。 + + } + + renderOperationModal = (props: ITableDetailOperationModal) => { + const mask = document.createElement('DIV') + mask.style.position = 'fixed' + mask.style.left = '0px' + mask.style.top = '0px' + mask.style.width = props.width || '100%' + mask.style.height = '100%' + mask.style.backgroundColor = 'white' + mask.innerText = '您当前使用的UI版本没有实现Table的OperationModal组件。' + mask.onclick = () => { + mask.remove() + props.onClose() + } + + document.body.appendChild(mask) + } + + + render = () => { + const { + config: { + field, + label, + // width, + primary, + tableColumns, + operations, + pagination, + // description + }, + data, + step, + onUnmount, + value + } = this.props + + const { + operation: { + enable: operationEnable, + target: operationTarget, + title: operationTitle, + visible: operationVisible, + config: operationConfig, + data: operationData, + callback: operationCallback + }, + pageAuth + } = this.state + + let getDate = value && Array.isArray(value) ? value : [] + const props: ITableField = { + title: label, + primary, + data: getDate, + tableColumns: (tableColumns || []).filter((column) => column.field !== undefined && column.field !== '').map((column, index) => { + const field = column.field.split('.')[0] + return { + field, + label: column.label, + align: column.align, + render: (value: any, record: { [field: string]: any }) => { + if (value && Object.prototype.toString.call(value) === '[object Object]') { + value = getValue(value, column.field.replace(field, '').slice(1)) + } + + const Column = this.getALLComponents(column.type) + if (Column) { + const addfix = ['multirowText'].some((val) => val !== column.field) + return + { }} + record={record} + value={value} + data={data} + step={step} + config={column} + table={this} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + /> + + } + } + } + }) + } + if (pagination && pagination.mode === 'server') { + const paginationCurrent = Number((pagination.current === undefined || pagination.current === '') ? step : get(step, pagination.current, 1)) + const paginationPageSize = Number((pagination.pageSize === undefined || pagination.pageSize === '') ? step : get(step, pagination.pageSize, 10)) + const paginationTotal = Number((pagination.total === undefined || pagination.total === '') ? step : get(step, pagination.total, 0)) + + props.pagination = { + current: Number.isNaN(paginationCurrent) ? 1 : paginationCurrent, + pageSize: Number.isNaN(paginationPageSize) ? 10 : paginationPageSize, + total: Number.isNaN(paginationTotal) ? 0 : paginationTotal, + onChange: (page, pageSize) => { + this.props.onUnmount && this.props.onUnmount(true, { + [pagination.current || '']: page, + [pagination.pageSize || '']: pageSize + }) + } + } + + if (!this.pageOverflow && props.pagination.current > 1 && (props.pagination.current - 1) * props.pagination.pageSize >= props.pagination.total) { + this.pageOverflow = true + this.props.onUnmount && this.props.onUnmount(true, { + [pagination.current || '']: 1, + [pagination.pageSize || '']: props.pagination.pageSize + }) + } + } + + if (operations && operations.rowOperations && operations.rowOperations.length > 0) { + props.tableColumns.push({ + field: 'ccms-table-rowOperation', + label: '操作', + align: 'left', + render: (_value: any, record: { [field: string]: any }) => { + if (operations.rowOperations) { + return this.renderRowOperationComponent({ + children: (operations.rowOperations || []).map((operation, index) => { + if (operation.type === 'button') { + if (!ConditionHelper(operation.condition, { record, data, step })) { + return null + } + + let hidden = false + if (operation.handle && operation.handle.type === 'ccms') { + hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] + operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) + } + return ( + + {hidden + ? null + : this.renderRowOperationButtonComponent({ + label: operation.label, + level: operation.level || 'normal', + onClick: async () => { await this.handleRowOperation(operation, record) } + })} + + ) + } else if (operation.type === 'group') { + return ( + + {this.renderRowOperationGroupComponent({ + label: operation.label, + children: (operation.operations || []).map((operation) => { + if (!ConditionHelper(operation.condition, { record, data, step })) { + return null + } + + let hidden = false + if (operation.handle && operation.handle.type === 'ccms') { + hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] + operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) + } + + return hidden + ? null + : this.renderRowOperationGroupItemComponent({ + label: operation.label, + level: operation.level || 'normal', + onClick: async () => { await this.handleRowOperation(operation, record) } + }) + }) + })} + + ) + } else if (operation.type === 'dropdown') { + return ( + + {this.renderRowOperationDropdownComponent({ + label: operation.label, + children: (operation.operations || []).map((operation) => { + if (!ConditionHelper(operation.condition, { record, data, step })) { + return null + } + + let hidden = false + if (operation.handle && operation.handle.type === 'ccms') { + hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] + operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) + } + + return hidden + ? null + : this.renderRowOperationDropdownItemComponent({ + label: operation.label, + level: operation.level || 'normal', + onClick: async () => { await this.handleRowOperation(operation, record) } + }) + }) + })} + + ) + } else { + return + } + }) + }) + } else { + return + } + } + }) + } + + const CCMS = this.CCMS + return ( + + {this.renderComponent(props)} + {operationEnable && ( + operationTarget === 'current' + ? ( + this.renderOperationModal({ + title: operationTitle, + width: '500', + visible: operationVisible, + children: ( + { + const { operation } = this.state + operation.visible = true + this.setState({ operation }) + }} + callback={() => { + const { operation } = this.state + operation.enable = false + operation.visible = false + this.setState({ operation }) + + if ((operationCallback && operationCallback === true) || Boolean(operationCallback)) { + onUnmount && onUnmount(true) + } + }} + /> + ), + onClose: () => { + const { operation } = this.state + operation.enable = false + operation.visible = false + if ((operationCallback && operationCallback === true) || Boolean(operationCallback)) { + onUnmount && onUnmount(true) + } + this.setState({ operation }) + } + }) + ) + : ( + Promise)} + loadPageURL={this.props.loadPageURL as (pageID: any) => Promise} + loadPageFrameURL={this.props.loadPageFrameURL as (pageID: any) => Promise} + loadPageConfig={this.props.loadPageConfig as (pageID: any) => Promise} + loadPageList={this.props.loadPageList} + loadDomain={this.props.loadDomain} + handlePageRedirect={this.props.handlePageRedirect} + onMount={() => { + const { operation } = this.state + operation.visible = true + this.setState({ operation }) + }} + callback={() => { + const { operation } = this.state + operation.enable = false + operation.visible = false + this.setState({ operation }) + + if ((operationCallback && operationCallback === true) || Boolean(operationCallback)) { + onUnmount && onUnmount(true) + } + }} + /> + ) + )} + + ) + } +} diff --git a/src/components/detail/text/index.tsx b/src/components/detail/text/index.tsx index 6338ad6b8fe582c63102b79bb6db9c182b84e93a..a3de7db3e096ea6bbc3839e452698523b6b8a36e 100644 --- a/src/components/detail/text/index.tsx +++ b/src/components/detail/text/index.tsx @@ -1,16 +1,29 @@ import React from 'react' import { getBoolean } from '../../../util/value' -import { DetailField, DetailFieldConfig, DetailFieldError, IDetailField } from '../common' +import { DetailField, DetailFieldProps, DetailFieldConfig, DetailFieldError, IDetailField } from '../common' export interface TextFieldConfig extends DetailFieldConfig { type: 'text' } export interface ITextField { - value: string + value: string | Array } export default class TextField extends DetailField implements IDetailField { + reset: () => Promise = async () => { + const defaults = await this.defaultValue() + return (defaults === undefined) ? '/' : defaults + } + + state = { + value: '' + } + + constructor(props: DetailFieldProps) { + super(props) + } + renderComponent = (props: ITextField) => { return 您当前使用的UI版本没有实现Text组件。 @@ -19,24 +32,45 @@ export default class TextField extends DetailField } - getValue = () => { + componentDidMount() { + this.getValue() + } + + getValue = async () => { const { value, config: { defaultValue } } = this.props - - if (value === undefined || value === null || value === '') { - return defaultValue !== undefined ? defaultValue : '' + if (value) { + if (typeof value !== 'object') { + return this.setState({ + value: String(value) + }) + } else if (Array.isArray(value)) { + return this.setState({ + value: (value as Array).join(',') + }) + } else { + return this.setState({ + value: '/' + }) + } + } + if (typeof defaultValue === 'string') { + this.setState({ + value: defaultValue + }) + } else { + this.setState({ + value: await this.reset() + }) } - return value } render = () => { - const value = this.getValue() - console.log(value, 'text value ') - + const { value } = this.state return ( {this.renderComponent({ diff --git a/src/components/formFields/any/index.tsx b/src/components/formFields/any/index.tsx index e93f34e4982784fcea9768144f713b13afeb0ae9..e7a4a8ae3e5e96ec3ffefb4d91b60dfcef8c9554 100644 --- a/src/components/formFields/any/index.tsx +++ b/src/components/formFields/any/index.tsx @@ -1,6 +1,7 @@ import React, { ReactNode } from 'react' import { Field, FieldConfig, IField, FieldInterface } from '../common' +import { getChainPath } from '../../../util/value' import TextField from '../text' import * as _ from 'lodash' import NumberField from '../number' @@ -29,8 +30,7 @@ export default class AnyField extends Field { const { - value, - onChange + value } = this.props if (type === 'null') { this.props.onValueSet('', null, true) @@ -80,60 +80,70 @@ export default class AnyField extends Field this.handleChangeType(type) }), - valueContent: type === 'string' ? {}} - form={this.props.form} - formLayout={'horizontal'} - value={typeof value === 'string' ? value : ''} - record={record} - data={_.cloneDeep(data)} - step={step} - config={{ type: 'text', field: '', label: '' }} - onChange={async (value: string) => { await onChange(value) }} - onValueSet={this.props.onValueSet} - onValueUnset={this.props.onValueUnset} - onValueListAppend={this.props.onValueListAppend} - onValueListSplice={this.props.onValueListSplice} - onValueListSort={this.props.onValueListSort} - baseRoute={this.props.baseRoute} - loadDomain={this.props.loadDomain} - /> : ( - type === 'number' ? {}} - form={this.props.form} - formLayout={'horizontal'} - record={record} - value={typeof value === 'number' ? value : ''} - data={_.cloneDeep(data)} - step={step} - config={{ type: 'number', field: '', label: '' }} - onChange={async (value) => { await onChange(Number(value)) }} - onValueSet={async (path, value, validation) => await this.props.onValueSet(path, Number(value), validation)} - onValueUnset={this.props.onValueUnset} - onValueListAppend={this.props.onValueListAppend} - onValueListSplice={this.props.onValueListSplice} - onValueListSort={this.props.onValueListSort} - baseRoute={this.props.baseRoute} - loadDomain={this.props.loadDomain} - /> : {}} - form={this.props.form} - formLayout={'horizontal'} - record={record} - value={typeof value === 'boolean' ? value : false} - data={_.cloneDeep(data)} - step={step} - config={{ type: 'switch', field: '', label: '' }} - onChange={async (value) => { await onChange(Boolean(value)) }} - onValueSet={this.props.onValueSet} - onValueUnset={this.props.onValueUnset} - onValueListAppend={this.props.onValueListAppend} - onValueListSplice={this.props.onValueListSplice} - onValueListSort={this.props.onValueListSort} - baseRoute={this.props.baseRoute} - loadDomain={this.props.loadDomain} - /> - ) + valueContent: + type === 'string' + ? {}} + form={this.props.form} + formLayout={'horizontal'} + value={typeof value === 'string' ? value : ''} + record={record} + data={_.cloneDeep(data)} + step={step} + config={{ type: 'text', field: '', label: '' }} + onChange={async (value: string) => { await onChange(value) }} + onValueSet={this.props.onValueSet} + onValueUnset={this.props.onValueUnset} + onValueListAppend={this.props.onValueListAppend} + onValueListSplice={this.props.onValueListSplice} + onValueListSort={this.props.onValueListSort} + baseRoute={this.props.baseRoute} + loadDomain={this.props.loadDomain} + containerPath={getChainPath(this.props.containerPath, '')} + loadPageList={this.props.loadPageList} + /> + : ( + type === 'number' + ? {}} + form={this.props.form} + formLayout={'horizontal'} + record={record} + value={typeof value === 'number' ? value : ''} + data={_.cloneDeep(data)} + step={step} + config={{ type: 'number', field: '', label: '' }} + onChange={async (value) => { await onChange(Number(value)) }} + onValueSet={async (path, value, validation) => await this.props.onValueSet(path, Number(value), validation)} + onValueUnset={this.props.onValueUnset} + onValueListAppend={this.props.onValueListAppend} + onValueListSplice={this.props.onValueListSplice} + onValueListSort={this.props.onValueListSort} + baseRoute={this.props.baseRoute} + loadDomain={this.props.loadDomain} + containerPath={getChainPath(this.props.containerPath, '')} + loadPageList={this.props.loadPageList} + /> + : {}} + form={this.props.form} + formLayout={'horizontal'} + record={record} + value={typeof value === 'boolean' ? value : false} + data={_.cloneDeep(data)} + step={step} + config={{ type: 'switch', field: '', label: '' }} + onChange={async (value) => { await onChange(Boolean(value)) }} + onValueSet={this.props.onValueSet} + onValueUnset={this.props.onValueUnset} + onValueListAppend={this.props.onValueListAppend} + onValueListSplice={this.props.onValueListSplice} + onValueListSort={this.props.onValueListSort} + baseRoute={this.props.baseRoute} + loadDomain={this.props.loadDomain} + containerPath={getChainPath(this.props.containerPath, '')} + loadPageList={this.props.loadPageList} + />) })} ) diff --git a/src/components/formFields/code/index.tsx b/src/components/formFields/code/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..17a0087135cc7d7b73bc935239ec63156b2ee25f --- /dev/null +++ b/src/components/formFields/code/index.tsx @@ -0,0 +1,202 @@ +import React, { KeyboardEvent } from 'react' +import { Field, FieldConfig, FieldError, IField } from '../common' +import { getBoolean } from '../../../util/value' + +/** + * code编辑器配置项 + * - codeType: 语言类型 + * - height: 代码编辑器高度 + * - theme: 编辑器主题风格 + * - fullScreen: 是否支持全屏 + * - maxLength: 最大字符长度 + * - minLength: 最小字符长度 + * - cjkLength: 中文占字符数 + * - regExp: 正则校验配置 + */ +export interface CodeFieldConfig extends FieldConfig { + type: 'code' + codeType: 'xml' | 'json' | 'javascript' | 'java' + height: number + theme: 'white' | 'black' + fullScreen: boolean + maxLength?: number + minLength?: number + cjkLength?: number + regExp?: { expression: string, message?: string } +} + +export interface ICodeField { + codeType: 'xml' | 'json' | 'javascript' | 'java' + fullScreenStatus: boolean + height: number + theme: 'white' | 'black' + value: string + onChange: (value: string) => Promise +} + +/** + * code编辑器配置项 + * - codeType: 语言类型 + * - height: 代码编辑器高度 + * - onResetValue: 编辑器重置为默认值 + * - fullScreen: 是否支持全屏 + * - fullScreenStatus: 编辑器是不是处于全屏状态 + */ +export interface ICodeFieldContainer { + fullScreen: boolean + fullScreenStatus: boolean + theme: 'white' | 'black' + children: React.ReactNode + onResetValue: (value: string) => Promise + keydownCallback: (value: KeyboardEvent) => Promise + enterFull: () => void + exitFull: () => void +} +interface State { + fullScreenStatus: boolean // 编辑器是不是处于全屏状态 +} +export default class CodeField extends Field implements IField { + state:State = { + fullScreenStatus: false + } + + reset: () => Promise = async () => { + const defaults = await this.defaultValue() + return (defaults === undefined) ? '' : defaults + } + + validate = async (value: string): Promise => { + const { + config: { + label, + required, + maxLength, + minLength, + cjkLength, + regExp + } + } = this.props + + const errors: FieldError[] = [] + + if (getBoolean(required)) { + if (value === '' || value === undefined || value === null || String(value).trim() === '') { + errors.push(new FieldError(`输入${label}`)) + return errors + } + } + if (maxLength !== undefined) { + let valueMaxLength = value + if (cjkLength !== undefined) { + let valueMaxCJKLength = '' + for (let valueMaxCJKIndex = 0; valueMaxCJKIndex < cjkLength; valueMaxCJKIndex++) { + valueMaxCJKLength += '*' + } + valueMaxLength = valueMaxLength.replace(/[\u4e00-\u9fa5]/g, valueMaxCJKLength) + } + if (valueMaxLength && maxLength >= 0 && valueMaxLength.length > maxLength) { + errors.push(new FieldError(`最长可输入${maxLength}个字符。`)) + } + } + + if (minLength !== undefined) { + let valueMinLength = value + if (cjkLength !== undefined) { + let valueMinCJKLength = '' + for (let valueMinCJKIndex = 0; valueMinCJKIndex < cjkLength; valueMinCJKIndex++) { + valueMinCJKLength += '*' + } + valueMinLength = valueMinLength.replace(/[\u4e00-\u9fa5]/g, valueMinCJKLength) + } + if (valueMinLength && minLength >= 0 && valueMinLength.length < minLength) { + errors.push(new FieldError(`最短需输入${minLength}个字符。`)) + } + } + + if (regExp !== undefined) { + if (!(new RegExp(regExp.expression)).test(value)) { + if (regExp.message) { + errors.push(new FieldError(regExp.message)) + } else { + errors.push(new FieldError('格式错误')) + } + } + } + + return errors.length ? errors : true + } + + keydownCallback = (e: KeyboardEvent) => { + e.stopPropagation() + const keyCode = e.keyCode || e.which || e.charCode + const ctrlKey = e.ctrlKey || e.metaKey + if (this.props.config.fullScreen) { + if ((e.key === 'j' || keyCode === 74) && ctrlKey) { + this.enterFull() + } else if ((e.key === 'Escape' || keyCode === 27)) { + this.exitFull() + } + } + } + + enterFull = () => { + this.setState({ fullScreenStatus: true }) + } + + exitFull = () => { + this.setState({ fullScreenStatus: false }) + } + + renderContainer = (props: ICodeFieldContainer) => { + return + 您当前使用的UI版本没有实现CodeField的container组件。 +
+ +
+
+ } + + renderComponent = (props: ICodeField) => { + return + 您当前使用的UI版本没有实现CodeField组件。 +
+ +
+
+ } + + render = () => { + const { + value, + config: { + theme, + fullScreen, + height, + codeType + } + } = this.props + const { fullScreenStatus } = this.state + + return ( + + {this.renderContainer({ + fullScreenStatus, + fullScreen, + theme, + onResetValue: async (defaultCodeValue: string) => await this.props.onValueSet('', defaultCodeValue, await this.validate(defaultCodeValue)), + keydownCallback: async (e: KeyboardEvent) => await this.keydownCallback(e), + enterFull: this.enterFull, + exitFull: this.exitFull, + children: this.renderComponent({ + codeType, + fullScreenStatus, + value, + theme, + height, + onChange: async (value: string) => await this.props.onValueSet('', value, await this.validate(value)) + }) + })} + + ) + } +} diff --git a/src/components/formFields/common.tsx b/src/components/formFields/common.tsx index 9539759b1304936677c5c761ee15c365b09314af..7cb767bd07822f7af0775dfd369928852baafb20 100644 --- a/src/components/formFields/common.tsx +++ b/src/components/formFields/common.tsx @@ -1,10 +1,14 @@ import React from 'react' +import marked from 'marked' import { ColumnsConfig, ParamConfig } from '../../interface' import { FieldConfigs as getFieldConfigs } from './' import ParamHelper from '../../util/param' +import { updateCommonPrefixItem } from '../../util/value' import { ConditionConfig } from '../../util/condition' -import { StatementConfig } from '../../util/statement' +import StatementHelper, { StatementConfig} from '../../util/statement' +import { PageListItem } from '../../main' +import { isEqual, get } from 'lodash' /** * 表单项基类配置文件格式定义 @@ -34,6 +38,11 @@ export interface FieldConfig { disabled?: boolean display?: 'none' defaultValue?: ParamConfig, + subLabelConfig?: { + enable: boolean + mode: 'plain' | 'markdown' | 'html' + content: StatementConfig + } condition?: ConditionConfig extra?: StatementConfig columns?: ColumnsConfig @@ -79,22 +88,25 @@ export interface FieldProps { value: T, record: { [field: string]: any }, data: any[], - step: number, config: C // TODO 待删除 onChange: (value: T) => Promise - // 事件:设置值 - onValueSet: (path: string, value: T, validation: true | FieldError[]) => Promise + // 事件:设置值 noPathCombination:为true时不做路径拼接 + onValueSet: (path: string, value: T, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => Promise // 事件:置空值 - onValueUnset: (path: string, validation: true | FieldError[]) => Promise + onValueUnset: (path: string, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 追加 - onValueListAppend: (path: string, value: any, validation: true | FieldError[]) => Promise + onValueListAppend: (path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 删除 - onValueListSplice: (path: string, index: number, count: number, validation: true | FieldError[]) => Promise + onValueListSplice: (path: string, index: number, count: number, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 修改顺序 - onValueListSort: (path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => Promise + onValueListSort: (path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[], options?: { noPathCombination?: boolean }) => Promise baseRoute: string, + containerPath: string, // 容器组件所在路径以字段拼接展示 1.3.0新增 + onReportFields?: (field: string) => Promise // 向父组件上报依赖字段 1.3.0新增 + step: { [field: string]: any } // 传递formValue loadDomain: (domain: string) => Promise + loadPageList: () => Promise> } /** @@ -127,6 +139,7 @@ export interface FieldInterface { * - S: 表单项的扩展状态 */ export class Field extends React.Component, S> implements IField { + dependentFields: string[] = [] // 组件param依赖字段存放数组 1.3.0新增 static defaultProps = { config: {} }; @@ -134,12 +147,12 @@ export class Field extends React.Component< /** * 获取默认值 */ - defaultValue = async () => { + defaultValue : () => Promise = async () => { const { config } = this.props if (config.defaultValue !== undefined) { - return ParamHelper(config.defaultValue, { record: this.props.record, data: this.props.data, step: this.props.step }) + return ParamHelper(config.defaultValue, { record: this.props.record, data: this.props.data, step: this.props.step }, this) } return undefined @@ -167,12 +180,69 @@ export class Field extends React.Component< didMount: () => Promise = async () => { } + /** + * 根据mode不同,处理subLabel内容 + * @param config 子项config + * @returns + */ + + handleSubLabelContent (config) { + if (config?.subLabelConfig?.enable) { + const content = StatementHelper({ statement: config.subLabelConfig?.content?.statement || '', params: config.subLabelConfig?.content?.params || [] }, { data: this.props.data, step: this.props.step }).replace(/(^\s*)|(\s*$)/g, '') + const mode = config.subLabelConfig?.mode + switch (mode) { + case 'markdown': + return
+ case 'html': + return
+ } + return
{content}
+ } + return undefined + } + + /** + * 上报param依赖字段名称 + * @param field + */ + handleReportFields: (field: string) => void = async (field) => { + const update: string[] | boolean = updateCommonPrefixItem(this.dependentFields, field) + if (typeof update === 'boolean') return + this.dependentFields = update + this.props.onReportFields && await this.props.onReportFields(field) + } + renderComponent = (props: E) => { return 当前UI库未实现该表单类型 } + shouldComponentUpdate (nextProps: FieldProps, nextState: S) { + const dependentFieldsArr = this.dependentFields + let dependentIsChange = false + if (dependentFieldsArr && dependentFieldsArr.length) { + for (let i = dependentFieldsArr.length; i >= 0; i--) { + const nextDependentField = get(nextProps.step, dependentFieldsArr[i]) + const currentDependentField = get(this.props.step, dependentFieldsArr[i]) + + if ((nextDependentField || currentDependentField) && nextDependentField !== currentDependentField) { + dependentIsChange = true + break + } + } + } + + /** + * data提交前不变, 去掉这项的比较 + * record也不比较,需要比较的话就在dependentFieldsArr取出record绝对路径 + * */ + if (!dependentIsChange && isEqual(this.state, nextState) && nextProps.value === this.props.value && this.props.config === nextProps.config) { + return false + } + return true + } + render = () => { return ( 当前UI库未实现该表单类型 @@ -184,16 +254,16 @@ export interface DisplayProps { value: T, record: { [field: string]: any }, data: any[], - step: number, + step: { [field: string]: any } config: C, // 事件:设置值 - onValueSet: (path: string, value: T, validation: true | FieldError[]) => Promise + onValueSet: (path: string, value: T, options?: { noPathCombination?: boolean }) => Promise // 事件:置空值 - onValueUnset: (path: string, validation: true | FieldError[]) => Promise + onValueUnset: (path: string, options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 追加 - onValueListAppend: (path: string, value: any, validation: true | FieldError[]) => Promise + onValueListAppend: (path: string, value: any, options?: { noPathCombination?: boolean }) => Promise // 事件:修改值 - 列表 - 删除 - onValueListSplice: (path: string, index: number, count: number, validation: true | FieldError[]) => Promise + onValueListSplice: (path: string, index: number, count: number, options?: { noPathCombination?: boolean }) => Promise baseRoute: string, loadDomain: (domain: string) => Promise } @@ -239,7 +309,7 @@ export class Display extends React.Componen export class FieldError { message: string - constructor (message: string) { + constructor(message: string) { this.message = message } } diff --git a/src/components/formFields/custom/index.tsx b/src/components/formFields/custom/index.tsx index 492ef98aa19ad36950b83d0e562c06bc749f3cda..fcbafa767a9da4f23713bd7b4442b8e723d1ba53 100644 --- a/src/components/formFields/custom/index.tsx +++ b/src/components/formFields/custom/index.tsx @@ -2,7 +2,7 @@ import React, { RefObject } from 'react' import { Field, FieldConfig, IField, FieldInterface, FieldProps, FieldError } from '../common' import { loadMicroApp, MicroApp } from 'qiankun' import moment from 'moment' -import { cloneDeep } from 'lodash' +// import { cloneDeep } from 'lodash' export interface CustomFieldConfig extends FieldConfig, FieldInterface { type: 'custom' @@ -53,7 +53,7 @@ export default class CustomField extends Field imple this.customField.update({ value: this.props.value, record: this.props.record, - data: cloneDeep(this.props.data), + data: this.props.data, form: this.props.form, step: this.props.step, config: this.props.config, @@ -64,9 +64,10 @@ export default class CustomField extends Field imple onValueListSplice: this.props.onValueListSplice, base: this.props.baseRoute, loadDomain: this.props.loadDomain, + loadPageList: this.props.loadPageList, bindValidate: this.bindValidate, bindGet: this.bindGet - }); + }) } } } @@ -82,7 +83,7 @@ export default class CustomField extends Field imple props: { value: this.props.value, record: this.props.record, - data: cloneDeep(this.props.data), + data: this.props.data, form: this.props.form, step: this.props.step, config: this.props.config, @@ -93,6 +94,7 @@ export default class CustomField extends Field imple onValueListSplice: this.props.onValueListSplice, base: this.props.baseRoute, loadDomain: this.props.loadDomain, + loadPageList: this.props.loadPageList, bindValidate: this.bindValidate, bindGet: this.bindGet } diff --git a/src/components/formFields/diffCode/index.tsx b/src/components/formFields/diffCode/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b497fc25f59da7cb4500ba5da725e7d810dbbbfa --- /dev/null +++ b/src/components/formFields/diffCode/index.tsx @@ -0,0 +1,147 @@ +import React, { KeyboardEvent } from 'react' +import { get } from 'lodash' +import { Field, FieldConfig, FieldError, IField } from '../common' + +/** + * diffCode编辑器配置项 + * - codeType: 语言类型 + * - height: 代码编辑器高度 + * - theme: 编辑器主题风格 + * - fullScreen: 是否支持全屏 + * - originalCodeField: 代码原始值入参字段 + * - modifiedCodeField: 代码修改值入参字段 + */ +export interface DiffCodeFieldConfig extends FieldConfig { + type: 'diffcode' + codeType: 'xml' | 'json' | 'javascript' | 'java' + height: number + theme: 'white' | 'black' + fullScreen: boolean + originalCodeField: string + modifiedCodeField: string +} + +export interface IDiffCodeField { + codeType: 'xml' | 'json' | 'javascript' | 'java' + fullScreenStatus: boolean + height: number + theme: 'white' | 'black' + originalCode: string + modifiedCode: string +} + +/** + * diffCode编辑器配置项 + * - codeType: 语言类型 + * - height: 代码编辑器高度 + * - fullScreen: 是否支持全屏 + * - fullScreenStatus: 编辑器是不是处于全屏状态 + */ +export interface IDiffCodeFieldContainer { + fullScreen: boolean + fullScreenStatus: boolean + theme: 'white' | 'black' + children: React.ReactNode + keydownCallback: (value: KeyboardEvent) => Promise + enterFull: () => void + exitFull: () => void +} +interface DiffCodeFieldValue { + [field: string]: any +} +interface State { + fullScreenStatus: boolean // 编辑器是不是处于全屏状态 +} + +export default class DiffCodeField extends Field implements IField { + state:State = { + fullScreenStatus: false + } + + get: () => Promise = async () => { + return {} + } + + reset: () => Promise = async () => { + const defaults = await this.defaultValue() + return (defaults === undefined) ? '' : defaults + } + + validate = async (value: DiffCodeFieldValue): Promise => { + return true + } + + keydownCallback = (e: KeyboardEvent) => { + e.stopPropagation() + const keyCode = e.keyCode || e.which || e.charCode + const ctrlKey = e.ctrlKey || e.metaKey + if (this.props.config.fullScreen) { + if ((e.key === 'j' || keyCode === 74) && ctrlKey) { + this.enterFull() + } else if ((e.key === 'Escape' || keyCode === 27)) { + this.exitFull() + } + } + } + + enterFull = () => { + this.setState({ fullScreenStatus: true }) + } + + exitFull = () => { + this.setState({ fullScreenStatus: false }) + } + + renderContainer = (props: IDiffCodeFieldContainer) => { + return + 您当前使用的UI版本没有实现CodeField的container组件。 +
+
+
+ } + + renderComponent = (props: IDiffCodeField) => { + return + 您当前使用的UI版本没有实现CodeField组件。 +
+
+
+ } + + render = () => { + const { + value, + config: { + theme, + fullScreen, + height, + codeType, + originalCodeField, + modifiedCodeField + } + } = this.props + const { fullScreenStatus } = this.state + const originalCode = get(value, originalCodeField) || '' + const modifiedCode = get(value, modifiedCodeField) || '' + return ( + + {this.renderContainer({ + fullScreenStatus, + fullScreen, + theme, + keydownCallback: async (e: KeyboardEvent) => await this.keydownCallback(e), + enterFull: this.enterFull, + exitFull: this.exitFull, + children: this.renderComponent({ + codeType, + fullScreenStatus, + originalCode, + modifiedCode, + theme, + height + }) + })} + + ) + } +} diff --git a/src/components/formFields/form/display.tsx b/src/components/formFields/form/display.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0999d8081cb4a9d40d21485d7d36c6c524ba69c6 --- /dev/null +++ b/src/components/formFields/form/display.tsx @@ -0,0 +1,282 @@ +import React from 'react' +import { display as getALLComponents } from '../' +import { FormFieldConfig } from '.' +import { Display, FieldConfigs, DisplayProps } from '../common' +import { getValue, setValue } from '../../../util/value' +import { cloneDeep } from 'lodash' +import ConditionHelper from '../../../util/condition' + +export interface IFormField { + canCollapse?: boolean + children: React.ReactNode[] +} + +export interface IFormFieldItem { + index: number + title: string + canCollapse?: boolean + children: React.ReactNode[] +} + +export interface IFormFieldItemField { + index: number + label: string + fieldType: string + children: React.ReactNode +} + +interface FormState { + didMount: boolean + showItem: boolean + showIndex: number +} + +export default class FormField extends Display, FormState> { + getALLComponents = (type: any): typeof Display => getALLComponents[type] + + formFieldsList: Array | null>> = [] + formFieldsMountedList: Array> = [] + + constructor (props: DisplayProps) { + super(props) + + this.state = { + didMount: false, + showItem: false, + showIndex: 0 + } + } + + didMount = async () => { + await this.setState({ + didMount: true + }) + } + + set = async (value: any) => { + if (this.props.config.unstringify && this.props.config.unstringify.length > 0 && Array.isArray(value)) { + for (let index = 0; index < value.length; index++) { + if (value[index]) { + for (const field of this.props.config.unstringify) { + const info = getValue(value[index], field) + try { + value[index] = setValue(value[index], field, JSON.parse(info)) + } catch (e) {} + } + } + } + } + + return value + } + + get = async () => { + const data: any[] = [] + + for (let index = 0; index < this.formFieldsList.length; index++) { + if (this.formFieldsList[index]) { + let item: any = {} + + if (Array.isArray(this.props.config.fields)) { + for (const formFieldIndex in this.props.config.fields) { + const formFieldConfig = this.props.config.fields[formFieldIndex] + if (!ConditionHelper(formFieldConfig.condition, { record: this.props.value[index], data: this.props.data, step: this.props.step })) { + continue + } + const formField = this.formFieldsList[index] && this.formFieldsList[index][formFieldIndex] + if (formField) { + const value = await formField.get() + item = setValue(item, formFieldConfig.field, value) + } + } + } + + if (this.props.config.stringify) { + for (const field of this.props.config.stringify) { + const info = getValue(item, field) + item = setValue(item, field, JSON.stringify(info)) + } + } + + data[index] = item + } + } + + return data + } + + handleMount = async (index: number, formFieldIndex: number) => { + if (!this.formFieldsMountedList[index]) { + this.formFieldsMountedList[index] = [] + } + if (this.formFieldsMountedList[index][formFieldIndex]) { + return true + } + this.formFieldsMountedList[index][formFieldIndex] = true + + if (this.formFieldsList[index] && this.formFieldsList[index][formFieldIndex]) { + const formField = this.formFieldsList[index][formFieldIndex] + if (formField) { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + + let value = getValue(this.props.value[index] === undefined ? {} : this.props.value[index], formFieldConfig.field) + const source = value + if ((formFieldConfig.defaultValue) && value === undefined) { + value = await formField.reset() + } + value = await formField.set(value) + if (source !== value) { + this.props.onValueSet(`[${index}]${formFieldConfig.field}`, value) + } + + await formField.didMount() + } + } + } + + handleValueSet = async (index: number, formFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) + await this.props.onValueSet(`[${index}]${fullPath}`, value) + } + } + + handleValueUnset = async (index: number, formFieldIndex: number, path: string, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) + await this.props.onValueUnset(`[${index}]${fullPath}`) + } + } + + handleValueListAppend = async (index: number, formFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) + await this.props.onValueListAppend(`[${index}]${fullPath}`, value) + } + } + + handleValueListSplice = async (index: number, formFieldIndex: number, path: string, _index: number, count: number, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) + await this.props.onValueListSplice(`[${index}]${fullPath}`, _index, count) + } + } + + /** + * 用于展示子表单组件中的每一子项中的每一个子表单项组件 + * @param props + * @returns + */ + renderItemFieldComponent = (props: IFormFieldItemField) => { + return + 您当前使用的UI版本没有实现FormField组件的renderItemFieldComponent方法。 + + } + + /** + * 用于展示子表单组件中的每一个子项 + * @param props + * @returns + */ + renderItemComponent = (props: IFormFieldItem) => { + return + 您当前使用的UI版本没有实现FormField组件的renderItemComponent方法。 + + } + + /** + * 用于展示子表单组件 + * @param _props + * @returns + */ + renderComponent = (_props: IFormField) => { + return + 您当前使用的UI版本没有实现FormField组件。 + + } + + render = () => { + const { + value = [], + data, + step, + config: { + fields, + primaryField, + canCollapse + } + } = this.props + + return ( + + { + this.renderComponent({ + canCollapse, + children: ( + this.state.didMount + ? (Array.isArray(value) ? value : []).map((itemValue: any, index: number) => { + return + {this.renderItemComponent({ + index, + title: primaryField !== undefined ? getValue(itemValue, primaryField, '').toString() : index.toString(), + canCollapse, + children: (fields || []).map((formFieldConfig, fieldIndex) => { + if (!ConditionHelper(formFieldConfig.condition, { record: itemValue, data: this.props.data, step: this.props.step })) { + if (!this.formFieldsMountedList[index]) this.formFieldsMountedList[index] = [] + this.formFieldsMountedList[index][fieldIndex] = false + return null + } + const FormField = this.getALLComponents(formFieldConfig.type) || Display + + // 渲染表单项容器 + return ( +
+ { + this.renderItemFieldComponent({ + index: fieldIndex, + label: formFieldConfig.label, + fieldType: formFieldConfig.type, + children: ( + | null) => { + if (fieldRef) { + if (!this.formFieldsList[index]) this.formFieldsList[index] = [] + this.formFieldsList[index][fieldIndex] = fieldRef + this.handleMount(index, fieldIndex) + } + }} + value={getValue(value[index], formFieldConfig.field)} + record={value[index]} + data={cloneDeep(data)} + step={step} + config={formFieldConfig} + onValueSet={async (path, value, options) => this.handleValueSet(index, fieldIndex, path, value, options)} + onValueUnset={async (path, options) => this.handleValueUnset(index, fieldIndex, path, options)} + onValueListAppend={async (path, value, options) => this.handleValueListAppend(index, fieldIndex, path, value, options)} + onValueListSplice={async (path, _index, count, options) => this.handleValueListSplice(index, fieldIndex, path, _index, count, options)} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + /> + ) + }) + } +
+ ) + }) + }) + } +
+ } + ) + : [] + ) + }) + } +
+ ) + } +} diff --git a/src/components/formFields/form/index.tsx b/src/components/formFields/form/index.tsx index d95ddeaeedadd3d4e9843f6ea100c67d497c0e56..25a229f95cfb7379351fcae47dc86e23c576c495 100644 --- a/src/components/formFields/form/index.tsx +++ b/src/components/formFields/form/index.tsx @@ -1,8 +1,9 @@ import React from 'react' import { Field, FieldConfig, FieldConfigs, FieldError, FieldProps, IField } from '../common' import getALLComponents from '../' -import { getValue, listItemMove, setValue, getBoolean } from '../../../util/value' -import { cloneDeep } from 'lodash' +// import { cloneDeep } from 'lodash' +import { getValue, getBoolean, getChainPath } from '../../../util/value' +import { set, setValue, sort, splice } from '../../../util/produce' import ConditionHelper from '../../../util/condition' import StatementHelper from '../../../util/statement' @@ -44,6 +45,7 @@ export interface IFormFieldItem { export interface IFormFieldItemField { index: number label: string + subLabel?: React.ReactNode required: boolean status: 'normal' | 'error' | 'loading' description?: string @@ -127,22 +129,27 @@ export default class FormField extends Field = [] - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList for (const formItemsIndex in this.formFieldsList) { if (!formDataList[formItemsIndex]) formDataList[formItemsIndex] = [] const formItems = this.formFieldsList[formItemsIndex] for (const fieldIndex in (this.props.config.fields || [])) { const formItem = formItems[fieldIndex] - if (formItem !== null && formItem !== undefined) { + if (formItem !== null && formItem !== undefined && !formItem.props.config.disabled) { const validation = await formItem.validate(getValue(value[formItemsIndex], (this.props.config.fields || [])[fieldIndex].field)) if (validation === true) { - formDataList[formItemsIndex][fieldIndex] = { status: 'normal' } + formDataList = set(formDataList, `[${formItemsIndex}][${fieldIndex}]`, { status: 'normal' }) } else { childrenError++ - formDataList[formItemsIndex][fieldIndex] = { status: 'error', message: validation[0].message } + formDataList = set(formDataList, `[${formItemsIndex}][${fieldIndex}]`, { status: 'error', message: validation[0].message }) + childrenErrorMsg.push({ + name: this.props.config.fields[fieldIndex].label, + msg: validation[0].message + }) } } } @@ -153,7 +160,8 @@ export default class FormField extends Field 0) { - errors.push(new FieldError(`子项中存在${childrenError}个错误。`)) + const errTips = `${this.props.config.label || ''} ${childrenErrorMsg.map(err => `${err.name}:${err.msg}`).join('; ')}` + errors.push(new FieldError(errTips)) } return errors.length ? errors : true @@ -167,7 +175,7 @@ export default class FormField extends Field { if (!this.formFieldsMountedList[index]) { - this.formFieldsMountedList[index] = [] + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${index}]`, []) } if (this.formFieldsMountedList[index][formFieldIndex]) { return true } - this.formFieldsMountedList[index][formFieldIndex] = true + // this.formFieldsMountedList[index][formFieldIndex] = true + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${index}][${formFieldIndex}]`, true) - const formDataList = cloneDeep(this.state.formDataList) - if (!formDataList[index]) formDataList[index] = [] + let formDataList = this.state.formDataList + if (!formDataList[index]) formDataList = set(formDataList, `[${index}]`, []) if (this.formFieldsList[index] && this.formFieldsList[index][formFieldIndex]) { const formField = this.formFieldsList[index][formFieldIndex] @@ -235,15 +244,16 @@ export default class FormField extends Field { const index = (this.props.value || []).length - const formDataList = cloneDeep(this.state.formDataList) - formDataList[index] = [] + const formDataList = set(this.state.formDataList, `[${index}]`, []) this.setState({ formDataList }) - this.formFieldsList[index] = [] - this.formFieldsMountedList[index] = [] + this.formFieldsList = set(this.formFieldsList, `${index}`, []) + this.formFieldsMountedList = set(this.formFieldsMountedList, `${index}`, []) - await this.props.onValueListAppend('', this.props.config.initialValues === undefined ? {} : cloneDeep(this.props.config.initialValues), true) + await this.props.onValueListAppend('', this.props.config.initialValues === undefined ? {} : this.props.config.initialValues, true) } handleRemove = async (index: number) => { - const formDataList = cloneDeep(this.state.formDataList) - formDataList.splice(index, 1) + const formDataList = splice(this.state.formDataList, '', index, 1) this.setState({ formDataList }) - - this.formFieldsList.splice(index, 1) - this.formFieldsMountedList.splice(index, 1) - + this.formFieldsList = splice(this.formFieldsList, '', index, 1) + this.formFieldsMountedList = splice(this.formFieldsMountedList, '', index, 1) await this.props.onValueListSplice('', index, 1, true) } handleSort = async (index: number, sortType: 'up' | 'down') => { - const formDataList = listItemMove(cloneDeep(this.state.formDataList), index, sortType) + const list = this.state.formDataList + const formDataList = sort(list, '', index, sortType) this.setState({ - formDataList + formDataList: formDataList }) - this.formFieldsList = listItemMove(this.formFieldsList, index, sortType) - this.formFieldsMountedList = listItemMove(this.formFieldsMountedList, index, sortType) + this.formFieldsList = sort(this.formFieldsList, '', index, sortType) + this.formFieldsMountedList = sort(this.formFieldsMountedList, '', index, sortType) await this.props.onValueListSort('', index, sortType, true) } @@ -322,98 +329,69 @@ export default class FormField extends Field { + /** + * 处理set、unset、append、splice、sort后的操作 + */ + handleValueCallback = async (index: number, formFieldIndex: number, validation: true | FieldError[]) => { + let formDataList = this.state.formDataList + if (validation === true) { + formDataList = set(formDataList, `[${index}][${formFieldIndex}]`, { status: 'normal' }) + } else { + formDataList = set(formDataList, `[${index}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) + } + + this.setState({ + formDataList + }) + } + + handleValueSet = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` - await this.props.onValueSet(`[${index}]${fullPath}`, value, true) - - const formDataList = cloneDeep(this.state.formDataList) - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) + await this.props.onValueSet(`[${index}].${fullPath}`, value, true) - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueUnset = async (index: number, formFieldIndex: number, path: string, validation: true | FieldError[]) => { + handleValueUnset = async (index: number, formFieldIndex: number, path: string, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueUnset(`[${index}]${fullPath}`, true) - const formDataList = cloneDeep(this.state.formDataList) - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueListAppend = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueListAppend = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListAppend(`[${index}]${fullPath}`, value, true) - const formDataList = cloneDeep(this.state.formDataList) - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueListSplice = async (index: number, formFieldIndex: number, path: string, _index: number, count: number, validation: true | FieldError[]) => { + handleValueListSplice = async (index: number, formFieldIndex: number, path: string, _index: number, count: number, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListSplice(`[${index}]${fullPath}`, _index, count, true) - const formDataList = cloneDeep(this.state.formDataList) - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueListSort = async (index: number, formFieldIndex: number, path: string, _index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => { + handleValueListSort = async (index: number, formFieldIndex: number, path: string, _index: number, sortType: 'up' | 'down', validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListSort(`[${index}]${fullPath}`, _index, sortType, true) - const formDataList = cloneDeep(this.state.formDataList) - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } @@ -455,7 +433,6 @@ export default class FormField extends Field { return - {this.renderItemComponent({ - index, - isLastIndex: value.length - 1 === index, - title: primaryField !== undefined ? getValue(itemValue, primaryField, '').toString() : index.toString(), - removeText: removeText === undefined - ? `删除 ${label}` - : removeText, - onRemove: canRemove ? async () => await this.handleRemove(index) : undefined, - onSort: canSort ? async (sortType: 'up' | 'down') => await this.handleSort(index, sortType) : undefined, - canCollapse, - children: (fields || []).map((formFieldConfig, fieldIndex) => { - if (!ConditionHelper(formFieldConfig.condition, { record: itemValue, data: this.props.data, step: this.props.step })) { - if (!this.formFieldsMountedList[index]) this.formFieldsMountedList[index] = [] - this.formFieldsMountedList[index][fieldIndex] = false - return null - } - const FormField = this.getALLComponents(formFieldConfig.type) || Field - - let status = ((this.state.formDataList[index] || [])[fieldIndex] || {}).status || 'normal' - - if (['group', 'import_subform', 'object', 'tabs', 'form'].some((type) => type === formFieldConfig.type)) { - status = 'normal' - } - // 渲染表单项容器 - return ( -
- { - this.renderItemFieldComponent({ - index: fieldIndex, - label: formFieldConfig.label, - status, - message: ((this.state.formDataList[index] || [])[fieldIndex] || {}).message || '', - extra: StatementHelper(formFieldConfig.extra, { record: this.props.record, data: this.props.data, step: this.props.step }), - required: getBoolean(formFieldConfig.required), - layout: formLayout, - fieldType: formFieldConfig.type, - children: ( - | null) => { - if (fieldRef) { - if (!this.formFieldsList[index]) this.formFieldsList[index] = [] - this.formFieldsList[index][fieldIndex] = fieldRef - this.handleMount(index, fieldIndex) - } - }} - form={this.props.form} - formLayout={formLayout} - value={getValue(value[index], formFieldConfig.field)} - record={value[index]} - data={cloneDeep(data)} - step={step} - config={formFieldConfig} - onChange={(value: any) => this.handleChange(index, fieldIndex, value)} - onValueSet={async (path, value, validation) => this.handleValueSet(index, fieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => this.handleValueUnset(index, fieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => this.handleValueListAppend(index, fieldIndex, path, value, validation)} - onValueListSplice={async (path, _index, count, validation) => this.handleValueListSplice(index, fieldIndex, path, _index, count, validation)} - onValueListSort={async (path, _index, sortType, validation) => await this.handleValueListSort(index, fieldIndex, path, _index, sortType, validation)} - baseRoute={this.props.baseRoute} - loadDomain={async (domain: string) => await this.props.loadDomain(domain)} - /> - ) - }) - } -
- ) - }) + {this.renderItemComponent({ + index, + isLastIndex: value.length - 1 === index, + title: primaryField !== undefined ? getValue(itemValue, primaryField, '').toString() : index.toString(), + removeText: removeText === undefined + ? `删除 ${label}` + : removeText, + onRemove: canRemove ? async () => await this.handleRemove(index) : undefined, + onSort: canSort ? async (sortType: 'up' | 'down') => await this.handleSort(index, sortType) : undefined, + canCollapse, + children: (fields || []).map((formFieldConfig, fieldIndex) => { + if (!ConditionHelper(formFieldConfig.condition, { record: itemValue, data: this.props.data, step: this.props.step, extraContainerPath: getChainPath(this.props.config.field, index) }, this)) { + if (!this.formFieldsMountedList[index]) this.formFieldsMountedList = set(this.formFieldsMountedList, `${index}`, []) + // this.formFieldsMountedList[index][fieldIndex] = false + this.formFieldsMountedList = set(this.formFieldsMountedList, `${[index]}.${fieldIndex}`, false) + this.formFieldsList[index] && (this.formFieldsList[index][fieldIndex] = null) + return null + } + const FormField = this.getALLComponents(formFieldConfig.type) || Field + + let status = ((this.state.formDataList[index] || [])[fieldIndex] || {}).status || 'normal' + + if (['group', 'import_subform', 'object', 'tabs', 'form'].some((type) => type === formFieldConfig.type)) { + status = 'normal' + } + // 渲染表单项容器 + return ( +
+ { + this.renderItemFieldComponent({ + index: fieldIndex, + label: formFieldConfig.label, + subLabel: this.handleSubLabelContent(formFieldConfig), + status, + message: ((this.state.formDataList[index] || [])[fieldIndex] || {}).message || '', + extra: StatementHelper(formFieldConfig.extra, { record: itemValue, data: this.props.data, step: this.props.step, extraContainerPath: getChainPath(this.props.config.field, index) }, this), + required: getBoolean(formFieldConfig.required), + layout: formLayout, + fieldType: formFieldConfig.type, + children: ( + | null) => { + if (fieldRef) { + if (!this.formFieldsList[index]) this.formFieldsList = set(this.formFieldsList, `[${index}]`, []) + // this.formFieldsList[index][fieldIndex] = fieldRef + this.formFieldsList = set(this.formFieldsList, `[${index}][${fieldIndex}]`, fieldRef) + this.handleMount(index, fieldIndex) + } + }} + form={this.props.form} + formLayout={formLayout} + value={getValue(value[index], formFieldConfig.field)} + record={value[index]} + step={this.props.step} + data={data} + config={formFieldConfig} + onChange={(value: any) => this.handleChange(index, fieldIndex, value)} + onValueSet={async (path, value, validation, options) => this.handleValueSet(index, fieldIndex, path, value, validation, options)} + onValueUnset={async (path, validation, options) => this.handleValueUnset(index, fieldIndex, path, validation, options)} + onValueListAppend={async (path, value, validation, options) => this.handleValueListAppend(index, fieldIndex, path, value, validation, options)} + onValueListSplice={async (path, _index, count, validation, options) => this.handleValueListSplice(index, fieldIndex, path, _index, count, validation, options)} + onValueListSort={async (path, _index, sortType, validation, options) => await this.handleValueListSort(index, fieldIndex, path, _index, sortType, validation, options)} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + containerPath={getChainPath(this.props.containerPath, this.props.config.field, index)} + onReportFields={async (field: string) => await this.handleReportFields(field)} + loadPageList={async () => await this.props.loadPageList()} + /> + ) + }) + } +
+ ) }) - } -
+ }) + } +
} ) : [] diff --git a/src/components/formFields/group/display.tsx b/src/components/formFields/group/display.tsx new file mode 100644 index 0000000000000000000000000000000000000000..48f71a32a903c6b8bcb67d354aa3d7143f3f112d --- /dev/null +++ b/src/components/formFields/group/display.tsx @@ -0,0 +1,207 @@ +import React from 'react' +import { display as getALLComponents, FieldConfigs } from '../' +import { GroupFieldConfig, IGroupField } from '.' +import { setValue, getValue, getBoolean } from '../../../util/value' +import { Display, DisplayProps } from '../common' +import { IFormItem } from '../../../steps/form' +import { cloneDeep } from 'lodash' +import ConditionHelper from '../../../util/condition' +import StatementHelper from '../../../util/statement' + +interface IGroupFieldState { + didMount: boolean +} + +export default class GroupField extends Display { + // 各表单项对应的类型所使用的UI组件的类 + getALLComponents = (type: any): typeof Display => getALLComponents[type] + + formFields: Array | null> = [] + formFieldsMounted: Array = [] + + constructor(props: DisplayProps) { + super(props) + + this.state = { + didMount: false + } + } + + didMount = async () => { + await this.setState({ + didMount: true + }) + } + + get = async () => { + let data: any = {} + + if (Array.isArray(this.props.config.fields)) { + for (let formFieldIndex = 0; formFieldIndex < this.props.config.fields.length; formFieldIndex++) { + const formFieldConfig = this.props.config.fields[formFieldIndex] + if (!ConditionHelper(formFieldConfig.condition, { record: this.props.value, data: this.props.data, step: this.props.step })) { + continue + } + const formField = this.formFields[formFieldIndex] + if (formField) { + const value = await formField.get() + data = setValue(data, formFieldConfig.field, value) + } + } + } + return data + } + + handleMount = async (formFieldIndex: number) => { + if (this.formFieldsMounted[formFieldIndex]) { + return true + } + this.formFieldsMounted[formFieldIndex] = true + + if (this.formFields[formFieldIndex]) { + const formField = this.formFields[formFieldIndex] + if (formField) { + const formFieldConfig = this.props.config.fields[formFieldIndex] + + let value = getValue(this.props.value, formFieldConfig.field) + const source = value + if ((formFieldConfig.defaultValue) && value === undefined) { + value = await formField.reset() + } + value = await formField.set(value) + if (source !== value) { + this.props.onValueSet(formFieldConfig.field, value) + } + + await formField.didMount() + } + } + } + + handleValueSet = async (formFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + await this.props.onValueSet(fullPath, value) + } + } + + handleValueUnset = async (formFieldIndex: number, path: string, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + await this.props.onValueUnset(fullPath) + } + } + + handleValueListAppend = async (formFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + await this.props.onValueListAppend(fullPath, value) + } + } + + handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, options?: { noPathCombination?: boolean }) => { + const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + await this.props.onValueListSplice(fullPath, index, count) + } + } + + renderComponent = (props: IGroupField) => { + return + 您当前使用的UI版本没有实现GroupField组件。 + + } + + /** + * 表单项组件 - UI渲染方法 + * 各UI库需重写该方法 + * @param props + */ + renderItemComponent = (props: IFormItem) => { + return + 您当前使用的UI版本没有实现FormItem组件。 + + } + + render = () => { + const { + value, + record, + data, + step + } = this.props + + return ( + + {this.renderComponent({ + children: this.state.didMount + ? (this.props.config.fields || []).map((formFieldConfig, formFieldIndex) => { + if (!ConditionHelper(formFieldConfig.condition, { record: value, data: this.props.data, step: this.props.step })) { + this.formFieldsMounted[formFieldIndex] = false + return null + } + let hidden: boolean = true + let display: boolean = true + + if (formFieldConfig.type === 'hidden') { + hidden = true + display = false + } + + if (formFieldConfig.display === 'none') { + hidden = true + display = false + } + + const FormField = this.getALLComponents(formFieldConfig.type) || Display + + let status: 'normal' = 'normal' + const renderData = { + key: formFieldIndex, + label: formFieldConfig.label, + status, + layout: 'horizontal' as 'horizontal', + extra: StatementHelper(formFieldConfig.extra, { record: this.props.record, data: this.props.data, step: this.props.step }), + required: getBoolean(formFieldConfig.required), + visitable: display, + fieldType: formFieldConfig.type, + children: ( + | null) => { + if (formField) { + this.formFields[formFieldIndex] = formField + this.handleMount(formFieldIndex) + } + }} + value={getValue(value, formFieldConfig.field)} + record={record} + data={cloneDeep(data)} + step={step} + config={formFieldConfig} + onValueSet={async (path, value, options) => this.handleValueSet(formFieldIndex, path, value, options)} + onValueUnset={async (path, options) => this.handleValueUnset(formFieldIndex, path, options)} + onValueListAppend={async (path, value, options) => this.handleValueListAppend(formFieldIndex, path, value, options)} + onValueListSplice={async (path, index, count, options) => this.handleValueListSplice(formFieldIndex, path, index, count, options)} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + /> + ) + } + // 渲染表单项容器 + return ( + hidden + ? this.renderItemComponent(renderData) + : + ) + }) + : [] + })} + + ) + } +} diff --git a/src/components/formFields/group/index.tsx b/src/components/formFields/group/index.tsx index 22347f70218252b2865cff54c581901b6af4e888..b15e8f5ca98d51f0d9637f76cc63b1401c4e0165 100644 --- a/src/components/formFields/group/index.tsx +++ b/src/components/formFields/group/index.tsx @@ -1,9 +1,10 @@ import React from 'react' -import { setValue, getValue, getBoolean } from '../../../util/value' +import { getValue, getBoolean, getChainPath } from '../../../util/value' import { Field, FieldConfig, FieldError, FieldProps, IField } from '../common' import getALLComponents, { FieldConfigs } from '../' import { IFormItem } from '../../../steps/form' -import { cloneDeep } from 'lodash' +// import { cloneDeep } from 'lodash' +import { set, setValue } from '../../../util/produce' import ConditionHelper from '../../../util/condition' import StatementHelper from '../../../util/statement' import { ColumnsConfig } from '../../../interface' @@ -66,19 +67,25 @@ export default class GroupField extends Field = [] - const formData = cloneDeep(this.state.formData) + let formData = this.state.formData for (const fieldIndex in (this.props.config.fields || [])) { const formItem = this.formFields[fieldIndex] - if (formItem !== null && formItem !== undefined) { + const formConfig = this.props.config.fields?.[fieldIndex] + if (formItem !== null && formItem !== undefined && !formItem.props.config.disabled) { const validation = await formItem.validate(getValue(value, (this.props.config.fields || [])[fieldIndex].field)) if (validation === true || this.formFieldsMounted[fieldIndex] === false) { - formData[fieldIndex] = { status: 'normal' } + formData = set(formData, `[${fieldIndex}]`, { status: 'normal' }) } else { childrenError++ - formData[fieldIndex] = { status: 'error', message: validation[0].message } + formData = set(formData, `[${fieldIndex}]`, { status: 'error', message: validation[0].message }) + childrenErrorMsg.push({ + name: formConfig?.label, + msg: validation[0].message + }) } } } @@ -88,7 +95,8 @@ export default class GroupField extends Field 0) { - errors.push(new FieldError(`子项中存在${childrenError}个错误。`)) + const errTips = `${this.props.config.label || ''}子项中存在错误。\n ${childrenErrorMsg.map(err => `${err.name}:${err.msg}`).join('; ')}。` + errors.push(new FieldError(errTips)) } return errors.length ? errors : true @@ -100,11 +108,11 @@ export default class GroupField extends Field { - formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } - return { formData: cloneDeep(formData) } - }) - } else { - await this.setState(({ formData }) => { - formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } - return { formData: cloneDeep(formData) } - }) - } + this.handleValueCallback(formFieldIndex, validation) } await formField.didMount() } @@ -182,93 +179,64 @@ export default class GroupField extends Field { + /** + * 处理set、unset、append、splice、sort后的操作 + */ + handleValueCallback = async (formFieldIndex: number, validation: true | FieldError[]) => { + let formData = this.state.formData + if (validation === true) { + formData = set(formData, `[${formFieldIndex}]`, { status: 'normal' }) + } else { + formData = set(formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) + } + + await this.setState({ + formData + }) + } + + handleValueSet = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueSet(fullPath, value, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueUnset = async (formFieldIndex: number, path: string, validation: true | FieldError[]) => { + handleValueUnset = async (formFieldIndex: number, path: string, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueUnset(fullPath, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueListAppend = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueListAppend = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListAppend(fullPath, value, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, validation: true | FieldError[]) => { + handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListSplice(fullPath, index, count, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueListSort = async (formFieldIndex: number, path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => { + handleValueListSort = async (formFieldIndex: number, path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListSort(fullPath, index, sortType, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } @@ -294,7 +262,6 @@ export default class GroupField extends Field { - if (!ConditionHelper(formFieldConfig.condition, { record: value, data: this.props.data, step: this.props.step })) { - this.formFieldsMounted[formFieldIndex] = false + if (!ConditionHelper(formFieldConfig.condition, { record: value, data: this.props.data, step: this.props.step, extraContainerPath: this.props.config.field }, this)) { + this.formFieldsMounted = set(this.formFieldsMounted, `[${formFieldIndex}]`, false) + this.formFields && (this.formFields[formFieldIndex] = null) return null } let hidden: boolean = true @@ -333,6 +301,7 @@ export default class GroupField extends Field | null) => { if (formField) { - this.formFields[formFieldIndex] = formField + this.formFields = set(this.formFields, `[${formFieldIndex}]`, formField) this.handleMount(formFieldIndex) } }} form={this.props.form} formLayout={formLayout} value={getValue(value, formFieldConfig.field)} - record={record} - data={cloneDeep(data)} + record={value} + data={data} step={step} config={formFieldConfig} onChange={async (value: any) => { await this.handleChange(formFieldIndex, value) }} - onValueSet={async (path, value, validation) => this.handleValueSet(formFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => this.handleValueUnset(formFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => this.handleValueListAppend(formFieldIndex, path, value, validation)} - onValueListSplice={async (path, index, count, validation) => this.handleValueListSplice(formFieldIndex, path, index, count, validation)} - onValueListSort={async (path, index, sortType, validation) => this.handleValueListSort(formFieldIndex, path, index, sortType, validation)} + onValueSet={async (path, value, validation, options) => this.handleValueSet(formFieldIndex, path, value, validation, options)} + onValueUnset={async (path, validation, options) => this.handleValueUnset(formFieldIndex, path, validation, options)} + onValueListAppend={async (path, value, validation, options) => this.handleValueListAppend(formFieldIndex, path, value, validation, options)} + onValueListSplice={async (path, index, count, validation, options) => this.handleValueListSplice(formFieldIndex, path, index, count, validation, options)} + onValueListSort={async (path, index, sortType, validation, options) => this.handleValueListSort(formFieldIndex, path, index, sortType, validation, options)} baseRoute={this.props.baseRoute} loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + loadPageList={async () => await this.props.loadPageList()} + containerPath={getChainPath(this.props.containerPath, this.props.config.field)} + onReportFields={async (field: string) => await this.handleReportFields(field)} /> ) } diff --git a/src/components/formFields/hidden/display.tsx b/src/components/formFields/hidden/display.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0ff145f9294fcdf6cba8167ebba7dd81cb43abf5 --- /dev/null +++ b/src/components/formFields/hidden/display.tsx @@ -0,0 +1,5 @@ +import { HiddenFieldConfig } from '.' +import { Display } from '../common' + +export default class HiddenField extends Display { +} diff --git a/src/components/formFields/importSubform/display.tsx b/src/components/formFields/importSubform/display.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4e0eae57e22d4e27e332e23ea1b9d33f4993f3cd --- /dev/null +++ b/src/components/formFields/importSubform/display.tsx @@ -0,0 +1,273 @@ +import React from 'react' +import { cloneDeep, isEqual } from 'lodash' +import { getValue } from '../../../util/value' + +import { DetailField, DetailFieldConfig, DetailFieldProps, IDetailField } from '../../detail/common' +import { Display } from '../common' +import { display as getALLComponents, FieldConfigs } from '..' +import { IDetailItem } from '../../../steps/detail' +import ConditionHelper from '../../../util/condition' +import InterfaceHelper, { InterfaceConfig } from '../../../util/interface' +/** + * 子表单配置项 + * - withConfig: 拓展配置 + * - * - enable: 是否开启 + * - * - dataField: (序列化)数据 + * - * - configField: (序列化)配置 + */ +export interface ImportSubformFieldConfig extends DetailFieldConfig { + type: 'import_subform' + configFrom?: ImportSubformConfigFromData | ImportSubformConfigFromInterface +} + +interface ImportSubformConfigFromData { + type: 'data' + dataField?: string + configField?: string +} + +interface ImportSubformConfigFromInterface { + type: 'interface' + interface?: InterfaceConfig +} + +export interface IImportSubformField { + children: React.ReactNode[] +} + +interface IImportSubformFieldState { + didMount: boolean + fields: FieldConfigs[] + formData: { status: 'normal' | 'error' | 'loading'; message?: string }[] +} + +export default class ImportSubformFieldDisplay + extends DetailField + implements IDetailField +{ + // 各表单项对应的类型所使用的UI组件的类 + getALLComponents = (type: any): typeof Display => getALLComponents[type] + + // 用于请求防频的判断条件 + requestConfig = '' + + value = '' + + formFields: Array | null> = [] + + formFieldsMounted: Array = [] + + interfaceHelper = new InterfaceHelper() + + constructor(props: DetailFieldProps) { + super(props) + + this.state = { + didMount: false, + fields: [], + formData: [] + } + } + + getFullpath(field: string, path = '') { + const withConfigPath = + this.props.config.configFrom?.type === 'data' && this.props.config.configFrom.dataField + ? `${this.props.config.configFrom.dataField}` + : '' + const _fullPath = `${withConfigPath}.${field}.${path}.` + const fullPath = _fullPath.replace(/(^\.*)|(\.*$)|(\.){2,}/g, '$3') + return fullPath + } + + didMount = async () => { + await this.setState({ + didMount: true + }) + } + + set: (value: any) => Promise = async (value) => { + return value + } + + handleMount = async (formFieldIndex: number) => { + if (this.formFieldsMounted[formFieldIndex]) { + return true + } + this.formFieldsMounted[formFieldIndex] = true + if (this.formFields[formFieldIndex]) { + const formField = this.formFields[formFieldIndex] + if (formField) { + const formFieldConfig = this.state.fields[formFieldIndex] + + let value = getValue(this.props.value, this.getFullpath(formFieldConfig.field)) + const source = value + if (formFieldConfig.defaultValue && value === undefined) { + value = await formField.reset() + } + value = await formField.set(value) + if (source !== value) { + this.props.onValueSet(this.getFullpath(formFieldConfig.field), value) + } + await formField.didMount() + } + } + } + + getConfigData = () => { + const { config, value } = this.props + if (config.configFrom && config.configFrom.type === 'interface' && config.configFrom.interface) { + this.interfaceHelper + .request( + config.configFrom.interface, + {}, + { record: this.props.record, data: this.props.data, step: this.props.step }, + { loadDomain: this.props.loadDomain } + ) + .then((data: any) => { + let dataToUnstringfy = data + let dataToStringfy = JSON.stringify(data) + if (Object.prototype.toString.call(data) === '[object String]') { + try { + dataToStringfy = data + dataToUnstringfy = JSON.parse(data) + } catch (e) { + console.error('当前动态子表单接口响应数据格式不是合格的json字符串') + dataToUnstringfy = [] + dataToStringfy = '[]' + } + } + if (dataToStringfy !== JSON.stringify(this.state.fields)) { + this.setState({ + fields: dataToUnstringfy + }) + } + }) + } + let { fields } = this.state + if (config.configFrom && config.configFrom.type === 'data') { + fields = config.configFrom.configField ? getValue(value, config.configFrom.configField) : [] + if (!isEqual(fields, this.state.fields)) { + this.setState({ + fields + }) + } + } + } + + componentDidMount() { + this.getConfigData() + } + + componentDidUpdate() { + this.getConfigData() + } + + handleValueSet = async (formFieldIndex: number, path: string, value: any) => { + const formFieldConfig = (this.state.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = this.getFullpath(formFieldConfig.field, path) + await this.props.onValueSet(fullPath, value) + } + } + + handleValueUnset = async (formFieldIndex: number, path: string) => { + const formFieldConfig = (this.state.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = this.getFullpath(formFieldConfig.field, path) + await this.props.onValueUnset(fullPath) + } + } + + handleValueListAppend = async (formFieldIndex: number, path: string, value: any) => { + const formFieldConfig = (this.state.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = this.getFullpath(formFieldConfig.field, path) + await this.props.onValueListAppend(fullPath, value) + } + } + + handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number) => { + const formFieldConfig = (this.state.fields || [])[formFieldIndex] + if (formFieldConfig) { + const fullPath = this.getFullpath(formFieldConfig.field, path) + await this.props.onValueListSplice(fullPath, index, count) + } + } + + renderComponent = (props: IImportSubformField) => { + return <>您当前使用的UI版本没有实现ImportSubformField组件。 + } + + /** + * 表单项组件 - UI渲染方法 + * 各UI库需重写该方法 + * @param props + */ + renderItemComponent = (props: IDetailItem) => { + return <>您当前使用的UI版本没有实现FormItem组件。 + } + + render = () => { + const { config, value, record, data, step } = this.props + const { fields } = this.state + + if (!fields || !Array.isArray(fields) || fields.length === 0) { + return <> + } + return ( + <> + {this.renderComponent({ + children: this.state.didMount + ? fields.map((formFieldConfig, formFieldIndex) => { + if (!ConditionHelper(formFieldConfig.condition, { record: value, data, step })) { + this.formFieldsMounted[formFieldIndex] = false + return null + } + let display = true + + if (formFieldConfig.type === 'hidden' || formFieldConfig.display === 'none') { + display = false + } + + const FormField = this.getALLComponents(formFieldConfig.type) || Display + + const renderData: IDetailItem = { + key: formFieldIndex, + label: formFieldConfig.label, + visitable: display, + fieldType: formFieldConfig.type, + layout: 'horizontal' as const, + children: ( + | null) => { + if (formField) { + this.formFields[formFieldIndex] = formField + this.handleMount(formFieldIndex) + } + }} + value={getValue(value, this.getFullpath(formFieldConfig.field))} + record={record} + data={cloneDeep(data)} + step={step} + config={formFieldConfig} + onValueSet={async (path, value) => this.handleValueSet(formFieldIndex, path, value)} + onValueUnset={async (path) => this.handleValueUnset(formFieldIndex, path)} + onValueListAppend={async (path, value) => this.handleValueListAppend(formFieldIndex, path, value)} + onValueListSplice={async (path, index, count) => + this.handleValueListSplice(formFieldIndex, path, index, count) + } + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + /> + ) + } + // 渲染表单项容器 + return this.renderItemComponent(renderData) + }) + : [] + })} + + ) + } +} diff --git a/src/components/formFields/importSubform/index.tsx b/src/components/formFields/importSubform/index.tsx index 5735eb9ac76be88b0f40b90b7af81d263cb5af57..2b04f05032823151ece429548a51b82f57e1de94 100644 --- a/src/components/formFields/importSubform/index.tsx +++ b/src/components/formFields/importSubform/index.tsx @@ -1,10 +1,11 @@ import React from 'react' -import { setValue, getValue, getBoolean } from '../../../util/value' +import { isEqual } from 'lodash' +import { getValue, getBoolean, getChainPath } from '../../../util/value' import { Field, FieldConfig, FieldError, FieldProps, IField } from '../common' -import getALLComponents, { FieldConfigs } from '../' +import getALLComponents, { FieldConfigs } from '..' import { IFormItem } from '../../../steps/form' -import { cloneDeep } from 'lodash' +import { set, setValue } from '../../../util/produce' import ConditionHelper from '../../../util/condition' import InterfaceHelper, { InterfaceConfig } from '../../../util/interface' import StatementHelper from '../../../util/statement' @@ -12,14 +13,20 @@ import { ColumnsConfig } from '../../../interface' /** * 子表单配置项 - * - withConfig: 拓展配置 + * - configFrom: 配置来源(get用途) + * - * - type: 'data' | 'interface' // 来源类型 + * - * - dataField: 值来源字段 // 仅type为data时生效 + * - * - configField: 配置项来源字段 // 仅type为data时生效 + * - * - interface: 来源接口配置 // 仅type为interface时生效 + * - withConfig: 拓展配置(set用途) * - * - enable: 是否开启 * - * - dataField: (序列化)数据 * - * - configField: (序列化)配置 */ export interface ImportSubformFieldConfig extends FieldConfig { - type: 'import_subform', + type: 'import_subform' interface?: InterfaceConfig + configFrom?: ImportSubformConfigFromData | ImportSubformConfigFromInterface withConfig?: { enable: boolean dataField: string @@ -27,7 +34,16 @@ export interface ImportSubformFieldConfig extends FieldConfig { } childColumns?: ColumnsConfig } +interface ImportSubformConfigFromData { + type: 'data' + dataField?: string + configField?: string +} +interface ImportSubformConfigFromInterface { + type: 'interface' + interface?: InterfaceConfig +} export interface IImportSubformField { columns?: ColumnsConfig children: React.ReactNode[] @@ -36,23 +52,28 @@ export interface IImportSubformField { interface IImportSubformFieldState { didMount: boolean fields: FieldConfigs[] - formData: { status: 'normal' | 'error' | 'loading', message?: string }[] + formData: { status: 'normal' | 'error' | 'loading'; message?: string }[] } -export default class ImportSubformField extends Field implements IField { +export default class ImportSubformField + extends Field + implements IField +{ // 各表单项对应的类型所使用的UI组件的类 getALLComponents = (type: any): typeof Field => getALLComponents[type] // 用于请求防频的判断条件 - requestConfig: string = '' - value: string = '' + requestConfig = '' + + value = '' formFields: Array | null> = [] + formFieldsMounted: Array = [] interfaceHelper = new InterfaceHelper() - constructor (props: FieldProps) { + constructor(props: FieldProps) { super(props) this.state = { @@ -62,13 +83,6 @@ export default class ImportSubformField extends Field { await this.setState({ didMount: true @@ -81,18 +95,37 @@ export default class ImportSubformField extends Field => { const errors: FieldError[] = [] let childrenError = 0 + const childrenErrorMsg: Array<{ name: string; msg: string }> = [] - const formData = cloneDeep(this.state.formData) + let { formData } = this.state - for (const fieldIndex in (this.state.fields || [])) { + for (const fieldIndex in this.state.fields || []) { const formItem = this.formFields[fieldIndex] + const formConfig = this.state.fields?.[fieldIndex] if (formItem !== null && formItem !== undefined) { - const validation = await formItem.validate(getValue(value, this.getFullpath((this.state.fields || [])[fieldIndex].field))) + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + const validation = await formItem.validate( + getValue(value, getChainPath(withConfigPath, (this.state.fields || [])[fieldIndex].field)) + ) if (validation === true || this.formFieldsMounted[fieldIndex] === false) { - formData[fieldIndex] = { status: 'normal' } + formData = set(formData, `[${fieldIndex}]`, { status: 'normal' }) } else { childrenError++ - formData[fieldIndex] = { status: 'error', message: validation[0].message } + formData = set(formData, `[${fieldIndex}]`, { status: 'error', message: validation[0].message }) + childrenErrorMsg.push({ + name: formConfig?.label, + msg: validation[0].message + }) } } } @@ -138,7 +182,10 @@ export default class ImportSubformField extends Field 0) { - errors.push(new FieldError('子项中存在错误')) + const errTips = `${this.props.config.label || ''}子项中错误。\n ${childrenErrorMsg + .map((err) => `${err.name}:${err.msg}`) + .join('; ')}。` + errors.push(new FieldError(errTips)) } return errors.length ? errors : true @@ -149,36 +196,30 @@ export default class ImportSubformField extends Field { - formData[formFieldIndex] = { status: 'normal' } - return { formData: cloneDeep(formData) } - }) - } else { - await this.setState(({ formData }) => { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - return { formData: cloneDeep(formData) } - }) - } + this.handleValueCallback(formFieldIndex, validation) } await formField.didMount() } @@ -188,9 +229,7 @@ export default class ImportSubformField extends Field { // const formField = this.formFields[formFieldIndex] // const formFieldConfig = this.state.fields[formFieldIndex] - // const formData = cloneDeep(this.state.formData) - // if (formField && formFieldConfig) { // if (this.props.onChange) { // if (formFieldConfig.field === '') { @@ -200,119 +239,159 @@ export default class ImportSubformField extends Field { + /** + * 处理set、unset、append、splice、sort后的操作 + */ + handleValueCallback = async (formFieldIndex: number, validation: true | FieldError[]) => { + let { formData } = this.state + if (validation === true) { + formData = set(formData, `[${formFieldIndex}]`, { status: 'normal' }) + } else { + formData = set(formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) + } + await this.setState({ + formData + }) + } + + handleValueSet = async ( + formFieldIndex: number, + path: string, + value: any, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) await this.props.onValueSet(fullPath, value, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueUnset = async (formFieldIndex: number, path: string, validation: true | FieldError[]) => { + handleValueUnset = async ( + formFieldIndex: number, + path: string, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) await this.props.onValueUnset(fullPath, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueListAppend = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueListAppend = async ( + formFieldIndex: number, + path: string, + value: any, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) await this.props.onValueListAppend(fullPath, value, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, validation: true | FieldError[]) => { + handleValueListSplice = async ( + formFieldIndex: number, + path: string, + index: number, + count: number, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) await this.props.onValueListSplice(fullPath, index, count, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formData - }) + this.handleValueCallback(formFieldIndex, validation) } } - handleValueListSort = async (formFieldIndex: number, path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => { + handleValueListSort = async ( + formFieldIndex: number, + path: string, + index: number, + sortType: 'up' | 'down', + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.state.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = this.getFullpath(formFieldConfig.field, path) + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + const fullPath = + options && options.noPathCombination ? path : getChainPath(withConfigPath, formFieldConfig.field, path) await this.props.onValueListSort(fullPath, index, sortType, true) - const formData = cloneDeep(this.state.formData) - if (validation === true) { - formData[formFieldIndex] = { status: 'normal' } - } else { - formData[formFieldIndex] = { status: 'error', message: validation[0].message } - } + this.handleValueCallback(formFieldIndex, validation) + } + } - this.setState({ - formData - }) + /** + * 处理data 兼容非法json的情况 + * @param {any} data 待处理数据 + * @returns 返回data反序列化形式 + */ + handleDataToUnstringfy = (data: any) => { + let dataToUnstringfy = data + if (Object.prototype.toString.call(data) === '[object String]') { + try { + dataToUnstringfy = JSON.parse(data) + } catch (e) { + console.error('当前动态子表单接口响应数据格式不是合格的json字符串') + dataToUnstringfy = [] + } } + return dataToUnstringfy } renderComponent = (props: IImportSubformField) => { - return - 您当前使用的UI版本没有实现ImportSubformField组件。 - + return <>您当前使用的UI版本没有实现ImportSubformField组件。 } /** @@ -320,141 +399,191 @@ export default class ImportSubformField extends Field { - return - 您当前使用的UI版本没有实现FormItem组件。 - - } + renderItemComponent = (props: IFormItem) => { + return <>您当前使用的UI版本没有实现FormItem组件。 + } - render = () => { - const { - config, - formLayout, - value, - record, - data, - step - } = this.props - - if (config.interface) { - this.interfaceHelper.request( - config.interface, - {}, - { record: this.props.record, data: this.props.data, step: this.props.step }, - { loadDomain: this.props.loadDomain } - ).then((data: any) => { - let dataToUnstringfy = data - let dataToStringfy = JSON.stringify(data) - if (Object.prototype.toString.call(data) === '[object String]') { - try { - dataToStringfy = data - dataToUnstringfy = JSON.parse(data) - } catch (e) { - console.error('当前动态子表单接口响应数据格式不是合格的json字符串') - dataToUnstringfy = [] - dataToStringfy = '[]' - } + getConfigData = () => { + const { config, value } = this.props + let { fields } = this.state + let interfaceConfig: InterfaceConfig | undefined + if (config.configFrom) { + if (config.configFrom.type === 'interface') { + if (config.configFrom.interface) { + interfaceConfig = config.configFrom.interface } - (this.props.config.withConfig?.enable && this.props.config.withConfig?.configField) && this.props.onValueSet(this.props.config.withConfig.configField, data, true) - if (dataToStringfy !== JSON.stringify(this.state.fields)) { + } else if (config.configFrom.type === 'data') { + fields = config.configFrom.configField ? getValue(value, config.configFrom.configField) : [] + const dataToUnstringfy = this.handleDataToUnstringfy(fields) + if (!isEqual(dataToUnstringfy, this.state.fields)) { this.setState({ fields: dataToUnstringfy }) } - }) + } + } else if (config.interface) { + interfaceConfig = config.interface } - if (!this.state.fields || this.state.fields.length === 0) { - return - } else { - return ( - - {this.renderComponent({ - columns: config.columns, - children: this.state.didMount - ? (Array.isArray(this.state.fields) ? this.state.fields : []).map((formFieldConfig, formFieldIndex) => { - if (!ConditionHelper(formFieldConfig.condition, { record: value, data, step })) { - this.formFieldsMounted[formFieldIndex] = false - return null - } - let hidden: boolean = true - let display: boolean = true - - if (formFieldConfig.type === 'hidden') { - hidden = true - display = false - } - - if (formFieldConfig.display === 'none') { - hidden = true - display = false - } - - const FormField = this.getALLComponents(formFieldConfig.type) || Field - - let status = (this.state.formData[formFieldIndex] || {}).status || 'normal' - - if (['group', 'import_subform', 'object', 'tabs', 'form'].some((type) => type === formFieldConfig.type)) { - status = 'normal' - } - - const renderData = { - key: formFieldIndex, - label: formFieldConfig.label, - status, - columns: config.columns?.enable - ? { - type: formFieldConfig.columns?.type || config.childColumns?.type || 'span', - value: formFieldConfig.columns?.value || config.childColumns?.value || 1, - wrap: formFieldConfig.columns?.wrap || config.childColumns?.wrap || false, - gap: config.columns?.gap || 0, - rowGap: config.columns?.rowGap || 0 + if (interfaceConfig) { + this.interfaceHelper + .request( + interfaceConfig, + {}, + { record: this.props.record, data: this.props.data, step: this.props.step }, + { loadDomain: this.props.loadDomain }, + this + ) + .then((data: any) => { + const dataToUnstringfy = this.handleDataToUnstringfy(data) + if (this.props.config.withConfig?.enable && this.props.config.withConfig?.configField) + this.props.onValueSet(this.props.config.withConfig.configField, data, true) + if (!isEqual(dataToUnstringfy, this.state.fields)) { + this.setState({ + fields: dataToUnstringfy + }) + } + }) + } + } + + componentDidMount() { + this.getConfigData() + } + + componentDidUpdate() { + this.getConfigData() + } + + render = () => { + const { config, formLayout, value, data, step } = this.props + + const { fields } = this.state + + if (!fields || !Array.isArray(fields) || fields.length === 0) { + return <> + } + const withConfigPath = + this.props.config.withConfig?.enable && this.props.config.withConfig?.dataField + ? `${this.props.config.withConfig.dataField}` + : '' + return ( + <> + {this.renderComponent({ + columns: config.columns, + children: this.state.didMount + ? (Array.isArray(this.state.fields) ? this.state.fields : []).map((formFieldConfig, formFieldIndex) => { + if ( + !ConditionHelper( + formFieldConfig.condition, + { record: value, data, step, extraContainerPath: this.props.config.field }, + this + ) + ) { + this.formFieldsMounted = set(this.formFieldsMounted, `[${formFieldIndex}]`, false) + this.formFields && (this.formFields[formFieldIndex] = null) + return null + } + let hidden = true + let display = true + + if (formFieldConfig.type === 'hidden') { + hidden = true + display = false + } + + if (formFieldConfig.display === 'none') { + hidden = true + display = false + } + + const FormField = this.getALLComponents(formFieldConfig.type) || Field + + let status = (this.state.formData[formFieldIndex] || {}).status || 'normal' + + if ( + ['group', 'import_subform', 'object', 'tabs', 'form'].some((type) => type === formFieldConfig.type) + ) { + status = 'normal' + } + + const renderData = { + key: formFieldIndex, + label: formFieldConfig.label, + subLabel: this.handleSubLabelContent(formFieldConfig), + status, + columns: config.columns?.enable + ? { + type: formFieldConfig.columns?.type || config.childColumns?.type || 'span', + value: formFieldConfig.columns?.value || config.childColumns?.value || 1, + wrap: formFieldConfig.columns?.wrap || config.childColumns?.wrap || false, + gap: config.columns?.gap || 0, + rowGap: config.columns?.rowGap || 0 + } + : undefined, + message: (this.state.formData[formFieldIndex] || {}).message || '', + extra: StatementHelper( + formFieldConfig.extra, + { + record: this.props.value, + data: this.props.data, + step: this.props.step, + extraContainerPath: this.props.config.field + }, + this + ), + required: getBoolean(formFieldConfig.required), + layout: formLayout, + visitable: display, + fieldType: formFieldConfig.type, + children: ( + | null) => { + if (formField) { + this.formFields = set(this.formFields, `[${formFieldIndex}]`, formField) + this.handleMount(formFieldIndex) } - : undefined, - message: (this.state.formData[formFieldIndex] || {}).message || '', - extra: StatementHelper(formFieldConfig.extra, { record: this.props.record, data: this.props.data, step: this.props.step }), - required: getBoolean(formFieldConfig.required), - layout: formLayout, - visitable: display, - fieldType: formFieldConfig.type, - children: ( - | null) => { - if (formField) { - this.formFields[formFieldIndex] = formField - this.handleMount(formFieldIndex) - } - }} - form={this.props.form} - formLayout={formLayout} - value={getValue(value, this.getFullpath(formFieldConfig.field))} - record={record} - data={cloneDeep(data)} - step={step} - config={formFieldConfig} - onChange={async (value: any) => { await this.handleChange(formFieldIndex, value) }} - onValueSet={async (path, value, validation) => this.handleValueSet(formFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => this.handleValueUnset(formFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => this.handleValueListAppend(formFieldIndex, path, value, validation)} - onValueListSplice={async (path, index, count, validation) => this.handleValueListSplice(formFieldIndex, path, index, count, validation)} - onValueListSort={async (path, index, sortType, validation) => this.handleValueListSort(formFieldIndex, path, index, sortType, validation)} - baseRoute={this.props.baseRoute} - loadDomain={async (domain: string) => await this.props.loadDomain(domain)} - /> - ) - } - // 渲染表单项容器 - return ( - hidden - ? this.renderItemComponent(renderData) - : + }} + form={this.props.form} + formLayout={formLayout} + value={getValue(value, getChainPath(withConfigPath, formFieldConfig.field))} + record={value} + step={this.props.step} + data={data} + config={formFieldConfig} + onChange={async (value: any) => { + await this.handleChange(formFieldIndex, value) + }} + onValueSet={async (path, value, validation, options) => + this.handleValueSet(formFieldIndex, path, value, validation, options) + } + onValueUnset={async (path, validation, options) => + this.handleValueUnset(formFieldIndex, path, validation, options) + } + onValueListAppend={async (path, value, validation, options) => + this.handleValueListAppend(formFieldIndex, path, value, validation, options) + } + onValueListSplice={async (path, index, count, validation, options) => + this.handleValueListSplice(formFieldIndex, path, index, count, validation, options) + } + onValueListSort={async (path, index, sortType, validation, options) => + this.handleValueListSort(formFieldIndex, path, index, sortType, validation, options) + } + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + loadPageList={async () => await this.props.loadPageList()} + containerPath={getChainPath(this.props.containerPath, this.props.config.field)} + onReportFields={async (field: string) => await this.handleReportFields(field)} + /> ) - }) - : [] - })} - - ) - } + } + // 渲染表单项容器 + return hidden ? this.renderItemComponent(renderData) : + }) + : [] + })} + + ) } } diff --git a/src/components/formFields/index.tsx b/src/components/formFields/index.tsx index e7735a6dd6c6f12f256e6133b149ea6f00ea69cc..87b4a202a6ddd08a8ea0205aced6c7f9f38836ad 100644 --- a/src/components/formFields/index.tsx +++ b/src/components/formFields/index.tsx @@ -18,13 +18,16 @@ import ImportSubformField, { ImportSubformFieldConfig } from './importSubform' import GroupField, { GroupFieldConfig } from './group' import AnyField, { AnyFieldConfig } from './any' import SwitchField, { SwitchFieldConfig } from './switch' -import ObjectField, { ObjectFieldConfig } from './object' +// import ObjectField, { ObjectFieldConfig } from './object' import HiddenField from './hidden' import TabsField, { TabsFieldConfig } from './tabs' import MultipleTextField, { MultipleTextFieldConfig } from './multipleText' import CustomField, { CustomFieldConfig } from './custom' +import CodeField, { CodeFieldConfig } from './code' +import DiffCodeField, { DiffCodeFieldConfig } from './diffCode' import TextDisplay from './text/display' +import FormDisplay from './form/display' import RadioDisplay from './radio/display' import ColorDisplay from './color/display' import UploadDisplay from './upload/display' @@ -34,8 +37,12 @@ import DatetimeDisplay from './datetime/display' import DatetimeRangeDisplay from './datetimeRange/display' import SelectSingleDisplay from './select/single/display' import SelectMultipleDisplay from './select/multiple/display' +import ImportSubformDisplay from './importSubform/display' +import GroupDisplay from './group/display' import SwitchDisplay from './switch/display' +import TabsDisplay from './tabs/display' import MultipleTextDisplay from './multipleText/display' +import HiddenDisplay from './hidden/display' export interface HiddenFieldConfig extends FieldConfig { type: 'hidden' | 'none' @@ -64,10 +71,12 @@ export type FieldConfigs = ImportSubformFieldConfig | GroupFieldConfig | AnyFieldConfig | - ObjectFieldConfig | + // ObjectFieldConfig | TabsFieldConfig | MultipleTextFieldConfig | - CustomFieldConfig + CustomFieldConfig | + CodeFieldConfig | + DiffCodeFieldConfig export type componentType = 'text' | @@ -90,10 +99,12 @@ export type componentType = 'group' | 'any' | 'switch' | - 'object' | + // 'object' | 'tabs' | 'multiple_text'| - 'custom' + 'custom' | + 'code' | + 'diffcode' export default { text: TextField, @@ -114,24 +125,31 @@ export default { group: GroupField, any: AnyField, switch: SwitchField, - object: ObjectField, + // object: ObjectField, hidden: HiddenField, tabs: TabsField, multiple_text: MultipleTextField, - custom: CustomField + custom: CustomField, + code: CodeField, + diffcode: DiffCodeField } export const display = { text: TextDisplay, longtext: LongtextDisplay, + form: FormDisplay, radio: RadioDisplay, color: ColorDisplay, upload: UploadDisplay, + import_subform: ImportSubformDisplay, + group: GroupDisplay, number: NumberDisplay, datetime: DatetimeDisplay, datetimeRange: DatetimeRangeDisplay, select_single: SelectSingleDisplay, select_multiple: SelectMultipleDisplay, switch: SwitchDisplay, - multiple_text: MultipleTextDisplay + tabs: TabsDisplay, + multiple_text: MultipleTextDisplay, + hidden: HiddenDisplay } diff --git a/src/components/formFields/object/index.tsx b/src/components/formFields/object/index.tsx index b0de063901f60ff11be62e7d7e9855d0ae8abca5..350d728d240c3ea681790d164dad2eebbec8331f 100644 --- a/src/components/formFields/object/index.tsx +++ b/src/components/formFields/object/index.tsx @@ -2,8 +2,9 @@ import { Field, FieldConfig, FieldConfigs, FieldError, FieldProps, IField } from import getALLComponents from '../' import React from 'react' import ConditionHelper from '../../../util/condition' -import { cloneDeep } from 'lodash' -import { getValue, setValue, getBoolean } from '../../../util/value' +// import { cloneDeep } from 'lodash' +import { set, setValue } from '../../../util/produce' +import { getValue, getBoolean, getChainPath, updateCommonPrefixItem } from '../../../util/value' import StatementHelper from '../../../util/statement' export interface ObjectFieldConfig extends FieldConfig { @@ -79,11 +80,11 @@ export default class ObjectField extends Field extends Field extends Field 0) { - errors.push(new FieldError(`子项中存在${childrenError}个错误。`)) + errors.push(new FieldError('子项中存在错误。')) } return errors.length ? errors : true @@ -138,18 +139,18 @@ export default class ObjectField extends Field { if (!this.formFieldsMountedList[key]) { - this.formFieldsMountedList[key] = [] + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${key}]`, []) } if (this.formFieldsMountedList[key][formFieldIndex]) { return true } - this.formFieldsMountedList[key][formFieldIndex] = true + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${key}][${formFieldIndex}]`, true) if (this.formFieldsList[key] && this.formFieldsList[key][formFieldIndex]) { const formField = this.formFieldsList[key][formFieldIndex] if (formField) { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] - + let value = getValue(this.props.value[key], formFieldConfig.field) const source = value if ((formFieldConfig.defaultValue) && value === undefined) { @@ -164,15 +165,15 @@ export default class ObjectField extends Field { - if (!formDataList[key]) formDataList[key] = [] - formDataList[key][formFieldIndex] = { status: 'normal' } - return { formDataList: cloneDeep(formDataList) } + if (!formDataList[key]) formDataList = set(formDataList, `[${key}]`, []) + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) + return { formDataList: formDataList } }) } else { await this.setState(({ formDataList }) => { - if (!formDataList[key]) formDataList[key] = [] - formDataList[key][formFieldIndex] = { status: 'error', message: validation[0].message } - return { formDataList: cloneDeep(formDataList) } + if (!formDataList[key]) formDataList = set(formDataList, `[${key}]`, []) + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) + return { formDataList: formDataList } }) } } @@ -190,11 +191,11 @@ export default class ObjectField extends Field { formDataList[key] = [] - return { formDataList: cloneDeep(formDataList) } + return { formDataList: formDataList } }) this.props.onValueSet(key, {}, true) @@ -203,27 +204,27 @@ export default class ObjectField extends Field { - delete this.formFieldsList[key] - delete this.formFieldsMountedList[key] + this.formFieldsList = set(this.formFieldsList, `${this.formFieldsList[key]}`) + this.formFieldsMountedList = set(this.formFieldsMountedList, `${this.formFieldsMountedList[key]}`) await this.setState(({ formDataList }) => { - delete formDataList[key] - return { formDataList: cloneDeep(formDataList) } + formDataList = set(formDataList, `${formDataList[key]}`) + return { formDataList: formDataList } }) this.props.onValueUnset(key, true) } handleChangeKey = async (prev: string, next: string) => { - this.formFieldsList[next] = this.formFieldsList[prev] - delete this.formFieldsList[prev] + this.formFieldsList = set(this.formFieldsList, `[${next}]`, this.formFieldsList[prev]) + this.formFieldsList = set(this.formFieldsList, `${this.formFieldsList[prev]}`) - this.formFieldsMountedList[next] = this.formFieldsMountedList[prev] - delete this.formFieldsMountedList[prev] + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${next}]`, this.formFieldsMountedList[prev]) + this.formFieldsMountedList = set(this.formFieldsMountedList, `${this.formFieldsMountedList[prev]}`) await this.setState(({ formDataList }) => { - formDataList[next] = formDataList[prev] - delete formDataList[prev] - return { formDataList: cloneDeep(formDataList) } + formDataList = set(formDataList, `[${next}]`, formDataList[prev]) + formDataList = set(formDataList, `${formDataList[prev]}`) + return { formDataList: formDataList } }) this.props.onValueSet(next, this.props.value[prev], true) @@ -242,34 +243,34 @@ export default class ObjectField extends Field { - // if (!formDataList[key]) formDataList[key] = [] - // formDataList[key][formFieldIndex] = { value, status: 'normal' } + // if (!formDataList[key]) formDataList = set(formDataList, `[${key}]`, []) + // formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) // return { formDataList: cloneDeep(formDataList) } // }) // } else { // await this.setState(({ formDataList }) => { - // if (!formDataList[key]) formDataList[key] = [] - // formDataList[key][formFieldIndex] = { value, status: 'error', message: validation[0].message } + // if (!formDataList[key]) formDataList = set(formDataList, `[${key}]`, []) + // formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) // return { formDataList: cloneDeep(formDataList) } // }) // } // } } - handleValueSet = async (key: string, formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueSet = async (key: string, formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueSet(fullPath === '' ? key : `${key}.${fullPath}`, value, true) - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList if (validation === true) { - formDataList[key][formFieldIndex] = { status: 'normal' } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) } else { - formDataList[key][formFieldIndex] = { status: 'error', message: validation[0].message } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } this.setState({ @@ -278,17 +279,17 @@ export default class ObjectField extends Field { + handleValueUnset = async (key: string, formFieldIndex: number, path: string, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueUnset(fullPath === '' ? key : `${key}.${fullPath}`, true) - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList if (validation === true) { - formDataList[key][formFieldIndex] = { status: 'normal' } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) } else { - formDataList[key][formFieldIndex] = { status: 'error', message: validation[0].message } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } this.setState({ @@ -297,17 +298,17 @@ export default class ObjectField extends Field { + handleValueListAppend = async (key: string, formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListAppend(fullPath === '' ? key : `${key}.${fullPath}`, value, true) - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList if (validation === true) { - formDataList[key][formFieldIndex] = { status: 'normal' } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) } else { - formDataList[key][formFieldIndex] = { status: 'error', message: validation[0].message } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } this.setState({ @@ -316,17 +317,17 @@ export default class ObjectField extends Field { + handleValueListSplice = async (key: string, formFieldIndex: number, path: string, _index: number, count: number, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListSplice(fullPath === '' ? key : `${key}.${fullPath}`, _index, count, true) - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList if (validation === true) { - formDataList[key][formFieldIndex] = { status: 'normal' } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) } else { - formDataList[key][formFieldIndex] = { status: 'error', message: validation[0].message } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } this.setState({ @@ -335,17 +336,17 @@ export default class ObjectField extends Field { + handleValueListSort = async (key: string, formFieldIndex: number, path: string, _index: number, sortType: 'up' | 'down', validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) await this.props.onValueListSort(fullPath === '' ? key : `${key}.${fullPath}`, _index, sortType, true) - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList if (validation === true) { - formDataList[key][formFieldIndex] = { status: 'normal' } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'normal' }) } else { - formDataList[key][formFieldIndex] = { status: 'error', message: validation[0].message } + formDataList = set(formDataList, `[${key}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } this.setState({ @@ -354,6 +355,9 @@ export default class ObjectField extends Field extends Field await this.handleChangeKey(key, value), onRemove: async () => await this.handleRemove(key), children: (Array.isArray(this.props.config.fields) ? this.props.config.fields : []).map((formFieldConfig, formFieldIndex) => { - if (!ConditionHelper(formFieldConfig.condition, { record: this.props.record, data: this.props.data, step: this.props.step })) { - if (!this.formFieldsMountedList[key]) this.formFieldsMountedList[key] = [] - this.formFieldsMountedList[key][formFieldIndex] = false + if (!ConditionHelper(formFieldConfig.condition, { record: this.props.record, data: this.props.data, step: this.props.step }, this)) { + if (!this.formFieldsMountedList[key]) this.formFieldsMountedList = set(this.formFieldsMountedList, `[${key}]`, []) + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${key}][${formFieldIndex}]`, false) return null } let hidden: boolean = true @@ -459,8 +463,8 @@ export default class ObjectField extends Field | null) => { if (formField) { - if (!this.formFieldsList[key]) this.formFieldsList[key] = [] - this.formFieldsList[key][formFieldIndex] = formField + if (!this.formFieldsList[key]) this.formFieldsList = set(this.formFieldsList, `[${key}]`, []) + this.formFieldsList = set(this.formFieldsList, `[${key}][${formFieldIndex}]`, formField) this.handleMount(key, formFieldIndex) } }} @@ -468,17 +472,20 @@ export default class ObjectField extends Field this.handleChange(key, formFieldIndex, value)} - onValueSet={async (path, value, validation) => this.handleValueSet(key, formFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => this.handleValueUnset(key, formFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => this.handleValueListAppend(key, formFieldIndex, path, value, validation)} - onValueListSplice={async (path, _index, count, validation) => this.handleValueListSplice(key, formFieldIndex, path, _index, count, validation)} - onValueListSort={async (path, _index, sortType, validation) => this.handleValueListSort(key, formFieldIndex, path, _index, sortType, validation)} + onValueSet={async (path, value, validation, options) => this.handleValueSet(key, formFieldIndex, path, value, validation, options)} + onValueUnset={async (path, validation, options) => this.handleValueUnset(key, formFieldIndex, path, validation, options)} + onValueListAppend={async (path, value, validation, options) => this.handleValueListAppend(key, formFieldIndex, path, value, validation, options)} + onValueListSplice={async (path, _index, count, validation, options) => this.handleValueListSplice(key, formFieldIndex, path, _index, count, validation, options)} + onValueListSort={async (path, _index, sortType, validation, options) => this.handleValueListSort(key, formFieldIndex, path, _index, sortType, validation, options)} baseRoute={this.props.baseRoute} loadDomain={async (domain: string) => this.props.loadDomain(domain)} + loadPageList={async () => await this.props.loadPageList()} + containerPath={getChainPath(this.props.containerPath, this.props.config.field, key)} + onReportFields={async (field: string) => await this.handleReportFields(field)} /> ) }) diff --git a/src/components/formFields/readme.md b/src/components/formFields/readme.md index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d69849c4b2d5951816ade3b962b89adb3b9b1f6f 100644 --- a/src/components/formFields/readme.md +++ b/src/components/formFields/readme.md @@ -0,0 +1,67 @@ +#### immer + +Immer是一个简单易用、体量小巧、设计巧妙的immutable 库,以最小的成本实现了js的不可变数据结构。 + +Js的对象是引用类型的,在使用过程中常常会不经意的修改。为了解决这个问题,除了可以使用深度拷贝的方法(但成本高,影响性能),还可以使用一些不可变数据结构的库,Immer就是其中的佼佼者。 +immer基于copy-on-write机制——在当前的状态数据上复制出一份临时的草稿,然后对这份草稿修改,最后生成新的状态数据。借力于ES6的Proxy,跟响应式的自动跟踪是一样的。 +https://immerjs.github.io/immer/api + +#### immer写法 + +```js +import produce from "immer" + +this.state = { + members: [{name: 'user', age: 18}] +} + +// 一般写法 +this.setState(state=>{ + return produce(state,draft=>{ + draft.members[0].age++; + }) +}) +// 高阶写法 +this.setState(state=>{ + return produce(draft=>{ + draft.members[0].age++; + })(state) +}) +// 简洁写法 +this.setState(produce(draft => { + draft.members[0].age++; +})) +``` +#### immer拓展 +1. 返回值问题 +以高阶函数写法为例 +语法:produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState +recipe 是否有返回值,nextState 的生成过程是不同的: +- recipe 没有返回值时:nextState是根据recipe 函数内的 draftState生成的; +- recipe有返回值时:nextState是根据 recipe 函数的返回值生成的 + +了解了这些,方便在开发中更加灵活地运用immer +2. Auto freezing(自动冻结) +Immer 会自动冻结使用 produce 修改过的状态树,这样可以防止在变更函数外部修改状态树。这个特性会带来性能影响,所以需要在生产环境中关闭。可以使用 setAutoFreeze(true / false) 打开或者关闭。在开发环境中建议打开,可以避免不可预测的状态树更改 +https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze + +#### immer结合shouldComponentUpdate做了哪些事? + SCU的对于引用类型对象的比较,如果通过比较它们的值往往很耗费性能,有了数据结构共享,我们可以通过比较复杂对象的每一个节点的指针指向是否相同,大大节省性能损耗 + +#### 项目中数据依赖字段上报规则及注意record(当前记录)注意事项 +1. 每一个组件在handleMount以后知道它们的this.props.containerPath和自身的field路径,以containerPath字段向下传递给子组件,我们在paramHelper里面对应record里的依赖项的处理是,拿到父级的路径和record依赖的字段(举例为DepField),通过getChainPath(this.props.containerPath, DepField)拿到完整的链式路径,onReportFields上报给父组件有哪些依赖项,参见下: + +containerPath=this.props.containerPath+field ----传递子组件---> containerPath=this.props.containerPath+field +父组件 <----onReportFields--- getChainPath(this.props.containerPath, DepField) + +2. 正常来说,父组件的value就是子组件的record, 特殊的是tabs和form两个组件对应分别多了tab.field和index,这里通过Helper传递record时,取值分别对应下探到tab.field和index。 +示例: + +```js +// import_subform和group用法: +ConditionHelper(formFieldConfig.condition, { record: this.props.value,...}) +ConditionHelper(formFieldConfig.condition, { record: this.props.value,...}) +// tabs和form用法: +ConditionHelper(formFieldConfig.condition, { record: this.props.value[tab.field],...}) +ConditionHelper(formFieldConfig.condition, { record: this.props.value[index],...}) +``` \ No newline at end of file diff --git a/src/components/formFields/select/common.tsx b/src/components/formFields/select/common.tsx index 528b21d335fcb614216e17a0cbb4b14ae9adf729..fe1dd104edc27c12fd607b8ed55228c17c1b4539 100644 --- a/src/components/formFields/select/common.tsx +++ b/src/components/formFields/select/common.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react' -import EnumerationHelper, { EnumerationOptionsConfig } from '../../../util/enumeration' +import EnumerationHelper, { EnumerationOptionsConfig, InterfaceEnumerationOptionsKVConfig, InterfaceEnumerationOptionsListConfig } from '../../../util/enumeration' import InterfaceHelper from '../../../util/interface' import { Field, FieldConfig, FieldProps, IField, Display, DisplayProps } from '../common' @@ -25,7 +25,7 @@ interface SelectSingleFieldState { export default class SelectField extends Field implements IField { interfaceHelper = new InterfaceHelper() - constructor (props: FieldProps) { + constructor(props: FieldProps) { super(props) this.state = { @@ -37,7 +37,11 @@ export default class SelectField extends Fiel config: EnumerationOptionsConfig | undefined ) => { if (config) { - EnumerationHelper.options(config, (config, source) => this.interfaceHelper.request(config, source, { record: this.props.record, data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain })).then((options) => { + EnumerationHelper.options( + config, + (config, source) => this.interfaceHelper.request(config, source, { record: this.props.record, data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain }, this), + { record: this.props.record, data: this.props.data, step: this.props.step } + ).then((options) => { if (JSON.stringify(this.state.options) !== JSON.stringify(options)) { this.setState({ options @@ -54,7 +58,7 @@ export default class SelectField extends Fiel export class SelectDisplay extends Display { interfaceHelper = new InterfaceHelper() - constructor (props: DisplayProps) { + constructor(props: DisplayProps) { super(props) this.state = { @@ -66,7 +70,11 @@ export class SelectDisplay extends Display { if (config) { - EnumerationHelper.options(config, (config, source) => this.interfaceHelper.request(config, source, { record: this.props.record, data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain })).then((options) => { + EnumerationHelper.options( + config, + (config, source) => this.interfaceHelper.request(config, source, { record: this.props.record, data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain }), + { record: this.props.record, data: this.props.data, step: this.props.step } + ).then((options) => { if (JSON.stringify(this.state.options) !== JSON.stringify(options)) { this.setState({ options diff --git a/src/components/formFields/select/multiple/index.tsx b/src/components/formFields/select/multiple/index.tsx index d2dd26346e084f9d3264716aab19da728c22aa94..ad0aca98b6dcf8985bcf908916772c5df40285f8 100644 --- a/src/components/formFields/select/multiple/index.tsx +++ b/src/components/formFields/select/multiple/index.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { getBoolean } from '../../../../util/value' +import { getBoolean, transformValueType } from '../../../../util/value' import { FieldError } from '../../common' import SelectField, { ISelectFieldOption, SelectFieldConfig } from '../common' @@ -17,7 +17,8 @@ interface SelectMultipleArrayConfig { interface SelectMultipleSplitConfig { type: 'split', - split?: string + split?: string, + valueType?: 'string' | 'number' | 'boolean' | undefined } export interface ISelectMultipleField { @@ -114,7 +115,13 @@ export default class SelectMultipleField extends SelectField { await this.props.onValueSet('', value, await this.validate(value)) }, + onChange: async (value: string | Array | undefined) => { + let useV = value + if (Array.isArray(useV) && multiple !== true && multiple?.type === 'split') { + useV = useV.join(multiple.split || ',') + } + return await this.props.onValueSet('', useV, await this.validate(useV)) + }, onClear: this.props.config.canClear ? async () => { await this.props.onValueSet('', undefined, await this.validate(undefined)) } : undefined, disabled: getBoolean(disabled), readonly: getBoolean(readonly), @@ -129,8 +136,8 @@ export default class SelectMultipleField extends SelectField option.value) props.value.filter((v) => { - if (props.options.map((option) => option.value).includes(v.toString())) { + if (values.includes(v)) { return true } else { console.warn(`选择框的当前值中${v}不在选项中。`) diff --git a/src/components/formFields/select/single/index.tsx b/src/components/formFields/select/single/index.tsx index 3eada975ead174c36cb112c8b6774736d9504b0c..dfc0f2398e179b15264c8b388b6f8092570efbb8 100644 --- a/src/components/formFields/select/single/index.tsx +++ b/src/components/formFields/select/single/index.tsx @@ -144,11 +144,9 @@ export default class SelectSingleField extends SelectField { - const value = props.options[defaultSelect === true ? 0 : defaultSelect].value - props.value = value - this.props.onValueSet('', value, await this.validate(value)) - })() + const value = props.options[defaultSelect === true ? 0 : defaultSelect]?.value + props.value = value + this.props.onValueSet('', value, true) } } diff --git a/src/components/formFields/tabs/display.tsx b/src/components/formFields/tabs/display.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0673de1acfe29f474b846265a30214722a1f1bcd --- /dev/null +++ b/src/components/formFields/tabs/display.tsx @@ -0,0 +1,287 @@ +import React from 'react' +import { display as getALLComponents } from '../' +import { TabsFieldConfig } from '.' +import { Display, FieldConfigs, DisplayProps } from '../common' +import ConditionHelper from '../../../util/condition' +import { getValue, setValue, getBoolean } from '../../../util/value' +import { cloneDeep } from 'lodash' + +export interface ITabsField { + children: React.ReactNode[] +} + +export interface ITabsFieldItem { + key: string + label: string + children: React.ReactNode[] +} + +export interface ITabsFieldItemField { + index: number + label: string + required: boolean + status: 'normal' | 'error' | 'loading' + description?: string + message?: string + extra?: string + fieldType: string + children: React.ReactNode +} +export interface TabsFieldState { + didMount: boolean + extra?: S +} + +export default class TabsField extends Display> { + // 各表单项对应的类型所使用的UI组件的类 + getALLComponents = (type: any): typeof Display => getALLComponents[type] + + formFieldsList: Array | null>> = [] + formFieldsMountedList: Array> = [] + + constructor(props: DisplayProps) { + super(props) + + this.state = { + didMount: false + } + } + + didMount = async () => { + await this.setState({ + didMount: true + }) + } + + get = async () => { + let data: any = {} + + for (let index = 0; index < (this.props.config.tabs || []).length; index++) { + const tab = (this.props.config.tabs || [])[index] + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + + for (let formFieldIndex = 0; formFieldIndex < fields.length; formFieldIndex++) { + const formFieldConfig = fields[formFieldIndex] + if (!ConditionHelper(formFieldConfig.condition, { record: getValue(this.props.value, tab.field), data: this.props.data, step: this.props.step })) { + continue + } + const formField = this.formFieldsList[index] && this.formFieldsList[index][formFieldIndex] + if (formField) { + const value = await formField.get() + const fullPath = tab.field === '' || formFieldConfig.field === '' ? `${tab.field}${formFieldConfig.field}` : `${tab.field}.${formFieldConfig.field}` + data = setValue(data, fullPath, value) + } + } + } + + return data + } + + handleMount = async (index: number, formFieldIndex: number) => { + if (!this.formFieldsMountedList[index]) { + this.formFieldsMountedList[index] = [] + } + if (this.formFieldsMountedList[index][formFieldIndex]) { + return true + } + this.formFieldsMountedList[index][formFieldIndex] = true + + const tab = (this.props.config.tabs || [])[index] + + if (this.formFieldsList[index] && this.formFieldsList[index][formFieldIndex]) { + const formField = this.formFieldsList[index][formFieldIndex] + if (formField) { + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + const formFieldConfig = fields[formFieldIndex] + + const fullPath = tab.field === '' || formFieldConfig.field === '' ? `${tab.field}${formFieldConfig.field}` : `${tab.field}.${formFieldConfig.field}` + + let value = getValue(this.props.value, fullPath) + const source = value + if ((formFieldConfig.defaultValue) && value === undefined) { + value = await formField.reset() + } + value = await formField.set(value) + if (source !== value) { + this.props.onValueSet(fullPath, value) + } + + await formField.didMount() + } + } + } + + handleChange = async (index: number, formFieldIndex: number, value: any) => { + } + + handleValueSet = async (index: number, formFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { + const tab = (this.props.config.tabs || [])[index] + + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + const formFieldConfig = fields[formFieldIndex] + if (formFieldConfig) { + const fieldPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` + await this.props.onValueSet(fullPath, value) + } + } + + handleValueUnset = async (index: number, formFieldIndex: number, path: string, options?: { noPathCombination?: boolean }) => { + const tab = (this.props.config.tabs || [])[index] + + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + const formFieldConfig = fields[formFieldIndex] + if (formFieldConfig) { + const fieldPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` + await this.props.onValueUnset(fullPath) + } + } + + handleValueListAppend = async (index: number, formFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { + const tab = (this.props.config.tabs || [])[index] + + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + const formFieldConfig = fields[formFieldIndex] + if (formFieldConfig) { + const fieldPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` + await this.props.onValueListAppend(fullPath, value) + } + } + + handleValueListSplice = async (index: number, formFieldIndex: number, path: string, _index: number, count: number, options?: { noPathCombination?: boolean }) => { + const tab = (this.props.config.tabs || [])[index] + + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + const formFieldConfig = fields[formFieldIndex] + if (formFieldConfig) { + const fieldPath = options && options.noPathCombination ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` + await this.props.onValueListSplice(fullPath, _index, count) + } + } + + /** + * 用于展示子表单组件 + * @param _props + * @returns + */ + renderComponent = (_props: ITabsField) => { + return + 您当前使用的UI版本没有实现FormField组件。 + + } + + /** + * 用于展示子表单组件中的每一个子项 + * @param props + * @returns + */ + renderItemComponent = (props: ITabsFieldItem) => { + return + 您当前使用的UI版本没有实现FormField组件的renderItemComponent方法。 + + } + + /** + * 用于展示子表单组件中的每一子项中的每一个子表单项组件 + * @param props + * @returns + */ + renderItemFieldComponent = (props: ITabsFieldItemField) => { + return + 您当前使用的UI版本没有实现FormField组件的renderItemFieldComponent方法。 + + } + + render = () => { + const { + value = {} + } = this.props + + return ( + + { + this.renderComponent({ + children: ( + this.state.didMount + ? (this.props.config.tabs || []).map((tab: any, index: number) => { + const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) + return ( + + {this.renderItemComponent({ + key: index.toString(), + label: tab.label, + children: fields.map((formFieldConfig, formFieldIndex) => { + if (!ConditionHelper(formFieldConfig.condition, { record: this.props.record, data: this.props.data, step: this.props.step })) { + if (!this.formFieldsMountedList[index]) this.formFieldsMountedList[index] = [] + this.formFieldsMountedList[index][formFieldIndex] = false + return null + } + let hidden: boolean = true + let display: boolean = true + + if (formFieldConfig.type === 'hidden') { + hidden = true + display = false + } + + if (formFieldConfig.display === 'none') { + hidden = true + display = false + } + + const FormField = this.getALLComponents(formFieldConfig.type) || Display + + let status: 'normal' = 'normal' + // 渲染表单项容器 + if (hidden) { + return ( +
+ {this.renderItemFieldComponent({ + index: formFieldIndex, + label: formFieldConfig.label, + status, + required: getBoolean(formFieldConfig.required), + fieldType: formFieldConfig.type, + children: ( + | null) => { + if (!this.formFieldsList[index]) this.formFieldsList[index] = [] + this.formFieldsList[index][formFieldIndex] = formField + this.handleMount(index, formFieldIndex) + }} + value={getValue(getValue(value, tab.field), formFieldConfig.field)} + record={getValue(value, tab.field)} + data={cloneDeep(this.props.data)} + step={this.props.step} + config={formFieldConfig} + onValueSet={async (path, value, validation) => this.handleValueSet(index, formFieldIndex, path, value, validation)} + onValueUnset={async (path, validation) => this.handleValueUnset(index, formFieldIndex, path, validation)} + onValueListAppend={async (path, value, validation) => this.handleValueListAppend(index, formFieldIndex, path, value, validation)} + onValueListSplice={async (path, _index, count, validation) => this.handleValueListSplice(index, formFieldIndex, path, _index, count, validation)} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + /> + ) + })} +
+ ) + } else { + return + } + }) + })} + + ) + }) + : [] + ) + }) + } +
+ ) + } +} diff --git a/src/components/formFields/tabs/index.tsx b/src/components/formFields/tabs/index.tsx index a32b1157b5b184b5c38df4136ce26cb4743a4a6d..bd45eff7613f9002f3f34989d2f10d731d3ec79f 100644 --- a/src/components/formFields/tabs/index.tsx +++ b/src/components/formFields/tabs/index.tsx @@ -2,8 +2,8 @@ import { Field, FieldConfig, FieldConfigs, FieldError, FieldProps, IField } from import getALLComponents from '../' import React from 'react' import ConditionHelper from '../../../util/condition' -import { cloneDeep } from 'lodash' -import { getValue, setValue, getBoolean } from '../../../util/value' +import { set, setValue } from '../../../util/produce' +import { getValue, getBoolean, getChainPath } from '../../../util/value' import StatementHelper from '../../../util/statement' export type TabsFieldConfig = TabsFieldConfig_Same | TabsFieldConfig_Diff @@ -43,6 +43,7 @@ export interface ITabsFieldItem { export interface ITabsFieldItemField { index: number label: string + subLabel?: React.ReactNode required: boolean status: 'normal' | 'error' | 'loading' description?: string @@ -90,11 +91,11 @@ export default class TabsField extends Field extends Field = [] - const formDataList = cloneDeep(this.state.formDataList) + let formDataList = this.state.formDataList for (const formItemsIndex in this.formFieldsList) { if (!formDataList[formItemsIndex]) formDataList[formItemsIndex] = [] @@ -125,14 +127,17 @@ export default class TabsField extends Field extends Field 0) { - errors.push(new FieldError(`子项中存在${childrenError}个错误。`)) + const errTips = `${this.props.config.label || ''}子项中存在错误。\n ${childrenErrorMsg.map(err => `${err.name}:${err.msg}`).join('; ')}` + errors.push(new FieldError(errTips)) } return errors.length ? errors : true @@ -151,12 +156,12 @@ export default class TabsField extends Field { if (!this.formFieldsMountedList[index]) { - this.formFieldsMountedList[index] = [] + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${index}]`, []) } if (this.formFieldsMountedList[index][formFieldIndex]) { return true } - this.formFieldsMountedList[index][formFieldIndex] = true + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${index}][${formFieldIndex}]`, true) const tab = (this.props.config.tabs || [])[index] @@ -180,19 +185,7 @@ export default class TabsField extends Field { - if (!formDataList[index]) formDataList[index] = [] - formDataList[index][formFieldIndex] = { status: 'normal' } - return { formDataList: cloneDeep(formDataList) } - }) - } else { - await this.setState(({ formDataList }) => { - if (!formDataList[index]) formDataList[index] = [] - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - return { formDataList: cloneDeep(formDataList) } - }) - } + this.handleValueCallback(index, formFieldIndex, validation) } await formField.didMount() } @@ -202,123 +195,90 @@ export default class TabsField extends Field { } - handleValueSet = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + /** + * 处理set、unset、append、splice、sort后的操作 + */ + handleValueCallback = async (index: number, formFieldIndex: number, validation: true | FieldError[]) => { + let formDataList = this.state.formDataList + // if (!formDataList[index]) formDataList = set(formDataList, `[${index}]`, []) + if (validation === true) { + formDataList = set(formDataList, `[${index}][${formFieldIndex}]`, { status: 'normal' }) + } else { + formDataList = set(formDataList, `[${index}][${formFieldIndex}]`, { status: 'error', message: validation[0].message }) + } + + this.setState({ + formDataList + }) + } + + handleValueSet = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const tab = (this.props.config.tabs || [])[index] const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) const formFieldConfig = fields[formFieldIndex] if (formFieldConfig) { - const fieldPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fieldPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` await this.props.onValueSet(fullPath, value, true) - const formDataList = cloneDeep(this.state.formDataList) - if (!formDataList[index]) formDataList[index] = [] - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueUnset = async (index: number, formFieldIndex: number, path: string, validation: true | FieldError[]) => { + handleValueUnset = async (index: number, formFieldIndex: number, path: string, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const tab = (this.props.config.tabs || [])[index] const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) const formFieldConfig = fields[formFieldIndex] if (formFieldConfig) { - const fieldPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fieldPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` await this.props.onValueUnset(fullPath, true) - const formDataList = cloneDeep(this.state.formDataList) - if (!formDataList[index]) formDataList[index] = [] - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueListAppend = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueListAppend = async (index: number, formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const tab = (this.props.config.tabs || [])[index] const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) const formFieldConfig = fields[formFieldIndex] if (formFieldConfig) { - const fieldPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fieldPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` await this.props.onValueListAppend(fullPath, value, true) - const formDataList = cloneDeep(this.state.formDataList) - if (!formDataList[index]) formDataList[index] = [] - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueListSplice = async (index: number, formFieldIndex: number, path: string, _index: number, count: number, validation: true | FieldError[]) => { + handleValueListSplice = async (index: number, formFieldIndex: number, path: string, _index: number, count: number, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const tab = (this.props.config.tabs || [])[index] const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) const formFieldConfig = fields[formFieldIndex] if (formFieldConfig) { - const fieldPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fieldPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` await this.props.onValueListSplice(fullPath, _index, count, true) - const formDataList = cloneDeep(this.state.formDataList) - if (!formDataList[index]) formDataList[index] = [] - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } - handleValueListSort = async (index: number, formFieldIndex: number, path: string, _index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => { + handleValueListSort = async (index: number, formFieldIndex: number, path: string, _index: number, sortType: 'up' | 'down', validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const tab = (this.props.config.tabs || [])[index] const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) const formFieldConfig = fields[formFieldIndex] if (formFieldConfig) { - const fieldPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fieldPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) const fullPath = tab.field === '' || fieldPath === '' ? `${tab.field}${fieldPath}` : `${tab.field}.${fieldPath}` await this.props.onValueListSort(fullPath, _index, sortType, true) - const formDataList = cloneDeep(this.state.formDataList) - if (!formDataList[index]) formDataList[index] = [] - if (validation === true) { - formDataList[index][formFieldIndex] = { status: 'normal' } - } else { - formDataList[index][formFieldIndex] = { status: 'error', message: validation[0].message } - } - - this.setState({ - formDataList - }) + this.handleValueCallback(index, formFieldIndex, validation) } } @@ -338,29 +298,26 @@ export default class TabsField extends Field { - return + renderItemComponent = (props: ITabsFieldItem) => { + return 您当前使用的UI版本没有实现FormField组件的renderItemComponent方法。 - } + } - /** - * 用于展示子表单组件中的每一子项中的每一个子表单项组件 - * @param props - * @returns - */ - renderItemFieldComponent = (props: ITabsFieldItemField) => { - return + /** + * 用于展示子表单组件中的每一子项中的每一个子表单项组件 + * @param props + * @returns + */ + renderItemFieldComponent = (props: ITabsFieldItemField) => { + return 您当前使用的UI版本没有实现FormField组件的renderItemFieldComponent方法。 - } + } render = () => { const { - value = {}, - config: { - label - } + value = {} } = this.props return ( @@ -372,84 +329,89 @@ export default class TabsField extends Field { const fields = this.props.config.mode === 'same' ? (this.props.config.fields || []) : (((this.props.config.tabs || [])[index] || {}).fields || []) return ( - - {this.renderItemComponent({ - key: index.toString(), - label: tab.label, - children: fields.map((formFieldConfig, formFieldIndex) => { - if (!ConditionHelper(formFieldConfig.condition, { record: this.props.record, data: this.props.data, step: this.props.step })) { - if (!this.formFieldsMountedList[index]) this.formFieldsMountedList[index] = [] - this.formFieldsMountedList[index][formFieldIndex] = false - return null - } - let hidden: boolean = true - let display: boolean = true - - if (formFieldConfig.type === 'hidden') { - hidden = true - display = false - } - - if (formFieldConfig.display === 'none') { - hidden = true - display = false - } - - const FormField = this.getALLComponents(formFieldConfig.type) || Field - - let status = ((this.state.formDataList[index] || [])[formFieldIndex] || {}).status || 'normal' - - if (['group', 'import_subform', 'object', 'tabs', 'form'].some((type) => type === formFieldConfig.type)) { - status = 'normal' - } - - // 渲染表单项容器 - if (hidden) { - return ( -
- {this.renderItemFieldComponent({ - index: formFieldIndex, - label: formFieldConfig.label, - status, - message: ((this.state.formDataList[index] || [])[formFieldIndex] || {}).message || '', - required: getBoolean(formFieldConfig.required), - extra: StatementHelper(formFieldConfig.extra, { record: this.props.record, data: this.props.data, step: this.props.step }), - layout: this.props.formLayout, - fieldType: formFieldConfig.type, - children: ( - | null) => { - if (!this.formFieldsList[index]) this.formFieldsList[index] = [] - this.formFieldsList[index][formFieldIndex] = formField - this.handleMount(index, formFieldIndex) - }} - form={this.props.form} - formLayout={this.props.formLayout} - value={getValue(getValue(value, tab.field), formFieldConfig.field)} - record={getValue(value, tab.field)} - data={cloneDeep(this.props.data)} - step={this.props.step} - config={formFieldConfig} - onChange={(value: any) => this.handleChange(index, formFieldIndex, value)} - onValueSet={async (path, value, validation) => this.handleValueSet(index, formFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => this.handleValueUnset(index, formFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => this.handleValueListAppend(index, formFieldIndex, path, value, validation)} - onValueListSplice={async (path, _index, count, validation) => this.handleValueListSplice(index, formFieldIndex, path, _index, count, validation)} - onValueListSort={async (path, _index, sortType, validation) => this.handleValueListSort(index, formFieldIndex, path, _index, sortType, validation)} - baseRoute={this.props.baseRoute} - loadDomain={async (domain: string) => await this.props.loadDomain(domain)} - /> - ) - })} -
- ) - } else { - return - } - }) - })} - + + {this.renderItemComponent({ + key: index.toString(), + label: tab.label, + children: fields.map((formFieldConfig, formFieldIndex) => { + if (!ConditionHelper(formFieldConfig.condition, { record: getValue(value, tab.field), data: this.props.data, step: this.props.step, extraContainerPath: getChainPath(this.props.config.field, tab.field) }, this)) { + if (!this.formFieldsMountedList[index]) this.formFieldsMountedList = set(this.formFieldsMountedList, `[${index}]`, []) + this.formFieldsMountedList = set(this.formFieldsMountedList, `[${index}][${formFieldIndex}]`, false) + this.formFieldsList[index] && (this.formFieldsList[index][formFieldIndex] = null) + return null + } + let hidden: boolean = true + let display: boolean = true + + if (formFieldConfig.type === 'hidden') { + hidden = true + display = false + } + + if (formFieldConfig.display === 'none') { + hidden = true + display = false + } + + const FormField = this.getALLComponents(formFieldConfig.type) || Field + + let status = ((this.state.formDataList[index] || [])[formFieldIndex] || {}).status || 'normal' + + if (['group', 'import_subform', 'object', 'tabs', 'form'].some((type) => type === formFieldConfig.type)) { + status = 'normal' + } + + // 渲染表单项容器 + if (hidden) { + return ( +
+ {this.renderItemFieldComponent({ + index: formFieldIndex, + label: formFieldConfig.label, + subLabel: this.handleSubLabelContent(formFieldConfig), + status, + message: ((this.state.formDataList[index] || [])[formFieldIndex] || {}).message || '', + required: getBoolean(formFieldConfig.required), + extra: StatementHelper(formFieldConfig.extra, { record: getValue(value, tab.field), data: this.props.data, step: this.props.step, extraContainerPath: getChainPath(this.props.config.field, tab.field) }, this), + layout: this.props.formLayout, + fieldType: formFieldConfig.type, + children: ( + | null) => { + if (!this.formFieldsList[index]) this.formFieldsList = set(this.formFieldsList, `[${index}]`, []) + this.formFieldsList = set(this.formFieldsList, `[${index}][${formFieldIndex}]`, formField) + this.handleMount(index, formFieldIndex) + }} + form={this.props.form} + formLayout={this.props.formLayout} + value={getValue(value, getChainPath(tab.field, formFieldConfig.field))} + record={getValue(value, tab.field)} + data={this.props.data} + step={this.props.step} + config={formFieldConfig} + onChange={(value: any) => this.handleChange(index, formFieldIndex, value)} + onValueSet={async (path, value, validation, options) => this.handleValueSet(index, formFieldIndex, path, value, validation, options)} + onValueUnset={async (path, validation, options) => this.handleValueUnset(index, formFieldIndex, path, validation, options)} + onValueListAppend={async (path, value, validation, options) => this.handleValueListAppend(index, formFieldIndex, path, value, validation, options)} + onValueListSplice={async (path, _index, count, validation, options) => this.handleValueListSplice(index, formFieldIndex, path, _index, count, validation, options)} + onValueListSort={async (path, _index, sortType, validation, options) => this.handleValueListSort(index, formFieldIndex, path, _index, sortType, validation, options)} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + containerPath={getChainPath(this.props.containerPath, this.props.config.field, tab.field)} + onReportFields={async (field: string) => await this.handleReportFields(field)} + loadPageList={async () => await this.props.loadPageList()} + /> + ) + })} +
+ ) + } else { + return + } + }) + })} + ) }) : [] diff --git a/src/components/formFields/text/index.test.tsx b/src/components/formFields/text/index.test.tsx index 1633d3f9fd0b2d5af27c8f2de189a4cae175b263..61e58445e074855ee87b29aea8f6978b770c68e9 100644 --- a/src/components/formFields/text/index.test.tsx +++ b/src/components/formFields/text/index.test.tsx @@ -89,7 +89,7 @@ test('文本框默认值 -数据值 query', () => { cleanup() resolve(true) }} - config={{ field: 'jest', label: 'jest', type: 'text', defaultValue: { source: 'query', filed: 'default' } }} + config={{ field: 'jest', label: 'jest', type: 'text', defaultValue: { source: 'query', field: 'default' } }} /> ) }) @@ -108,7 +108,7 @@ test('文本框默认值 -数据值 hash', () => { cleanup() resolve(true) }} - config={{ field: 'jest', label: 'jest', type: 'text', defaultValue: { source: 'hash', filed: 'default' } }} + config={{ field: 'jest', label: 'jest', type: 'text', defaultValue: { source: 'hash', field: 'default' } }} /> ) }) diff --git a/src/components/formFields/text/index.tsx b/src/components/formFields/text/index.tsx index e3d80c9fe58a3038833654c6d78aa01a76769783..b423a19bca0a992a5c6e327d3b10c9b8128cdf6d 100644 --- a/src/components/formFields/text/index.tsx +++ b/src/components/formFields/text/index.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { isEqual, get } from 'lodash' import { getBoolean } from '../../../util/value' import { Field, FieldConfig, FieldError, IField } from '../common' diff --git a/src/components/formFields/treeSelect/index.tsx b/src/components/formFields/treeSelect/index.tsx index 6d3973ad1f9f559c9ada26bc685d458ec4a8406d..4284a2d8bd2b01f90eb635493378efd480830c73 100644 --- a/src/components/formFields/treeSelect/index.tsx +++ b/src/components/formFields/treeSelect/index.tsx @@ -2,22 +2,45 @@ import React, { ReactNode } from 'react' import { get } from 'lodash' import { Field, FieldConfig, IField, FieldError, FieldProps } from '../common' import InterfaceHelper, { InterfaceConfig } from '../../../util/interface' +import ParamHelper from '../../../util/param' +import { RecordParamConfig, DataParamConfig, StepParamConfig, SourceParamConfig } from '../../../interface' +import { transformValueType } from '../../../util/value' +type OptionsConfigDefaultValue = RecordParamConfig | DataParamConfig | StepParamConfig | SourceParamConfig export interface TreeSelectFieldConfig extends FieldConfig { type: 'tree_select' - treeData?: ManualOptionsConfig | InterfaceOptionsConfig + mode?: 'tree' | 'table' | 'treeSelect' + multiple?: true | TreeSelectMultipleArrayConfig | TreeSelectMultipleSplitConfig, + titleColumn?: string, + treeData?: ManualOptionsConfig | InterfaceOptionsConfig | DataOptionsConfig +} + +interface TreeSelectMultipleArrayConfig { + type: 'array' +} + +interface TreeSelectMultipleSplitConfig { + type: 'split', + split?: string, + valueType?: 'string' | 'number' | 'boolean' | undefined +} +export interface DataOptionsConfig { + from: 'data' + sourceConfig?: OptionsConfigDefaultValue, + format?: InterfaceOptionsListConfig } export interface ManualOptionsConfig { from: 'manual' defaultIndex?: string | number - data?: Array<{ - value: string | number - title: string - [extra: string]: any - }> + data?: TreeSelectFieldOption[] +} +export interface TreeSelectFieldOption { + key: string | number + value: string | number + title: string + children?: Array } - export interface InterfaceOptionsConfig { from: 'interface' interface?: InterfaceConfig @@ -35,37 +58,29 @@ export interface InterfaceOptionsListConfig { childrenField?: string } -export interface ISelectFieldOption { - value: string | number, - title: ReactNode, - children?: Array -} - -interface treeData { - value: any, - title: string, - children?: treeData[] +interface TreeSelectFieldState { + interfaceOptionsData: TreeSelectFieldOption[] } -interface SelectSingleFieldState { - interfaceOptionsData: treeData[] -} +type TreeSelectValueType = string | Array | undefined export interface ITreeSelectField { - value?: string, - treeData: Array - onChange: (value: string) => Promise + multiple?: boolean + value: TreeSelectValueType, + treeData: Array + titleColumn?: string + onChange: (value: TreeSelectValueType) => Promise } -export default class TreeSelectField extends Field implements IField { +export default class TreeSelectField extends Field { interfaceHelper = new InterfaceHelper() interfaceOptionsConfig: string = '' - state: SelectSingleFieldState = { + state: TreeSelectFieldState = { interfaceOptionsData: [] } - constructor (props: FieldProps) { + constructor(props: FieldProps) { super(props) this.state = { @@ -73,17 +88,26 @@ export default class TreeSelectField extends Field { - const rsMenu: treeData[] = [] + optionsData = (sourceConfig: OptionsConfigDefaultValue) => { + if (sourceConfig !== undefined) { + return ParamHelper(sourceConfig, { record: this.props.record, data: this.props.data, step: this.props.step }) + } + return undefined + } + + formatTree = (treeList: TreeSelectFieldOption[], value: string, title: string, children: string) => { + const rsMenu: TreeSelectFieldOption[] = [] - treeList.forEach((val: any) => { - const theMenu: treeData = { + treeList.forEach((val: TreeSelectFieldOption) => { + const theMenu: TreeSelectFieldOption = { title: '', - value: null + value: '', + key: '' } theMenu.title = get(val, title) theMenu.value = get(val, value) + theMenu.key = get(val, value) if (get(val, children)) { theMenu.children = this.formatTree(get(val, children), value, title, children) @@ -95,15 +119,27 @@ export default class TreeSelectField extends Field { if (config) { - if (config.from === 'manual') { + if (config.from === 'data') { + if (config.sourceConfig && config.sourceConfig.source && config.sourceConfig.field) { + const data = this.optionsData(config.sourceConfig) + if (Array.isArray(data)) { + return this.formatTree( + data, + config.format?.keyField || 'value', + config.format?.titleField || 'title', + config.format?.childrenField || 'children' + ) + } + } + } else if (config.from === 'manual') { if (config.data) { return this.formatTree(config.data, 'value', 'title', 'children') } @@ -113,7 +149,8 @@ export default class TreeSelectField extends Field { this.setState({ interfaceOptionsData: this.formatTree( @@ -151,7 +188,7 @@ export default class TreeSelectField extends Field => { + validate = async (_value: TreeSelectValueType): Promise => { const { config: { required @@ -171,35 +208,90 @@ export default class TreeSelectField extends Field { return - 您当前使用的UI版本没有实现TreeSelectSingleField组件的SelectSingle模式。 + 您当前使用的UI版本没有实现TreeSelectField组件的treeSelect模式。
} + renderTreeComponent = (props: ITreeSelectField) => { + return + 您当前使用的UI版本没有实现TreeSelectField组件的tree模式。 +
+ +
+
+ } + + renderTableComponent = (props: ITreeSelectField) => { + return + 您当前使用的UI版本没有实现TreeSelectField组件的table模式。 +
+ +
+
+ } + render = () => { const { value, config: { + multiple, + mode, + titleColumn, treeData: optionsConfig }, - onChange, record, data, step } = this.props - this.options(optionsConfig, { record, data, step }) - - return ( - - {this.renderComponent({ - value, - treeData: this.state.interfaceOptionsData, - onChange: async (value: string) => await this.props.onValueSet('', value, await this.validate(value)) - })} - - ) + const temp = this.options(optionsConfig, { record, data, step }) + const props: ITreeSelectField = { + value: undefined, + treeData: this.state.interfaceOptionsData, + onChange: async (value: TreeSelectValueType) => { + let useV = value + if (Array.isArray(useV) && multiple !== true && multiple?.type === 'split') { + useV = useV.join(multiple.split || ',') + } + return await this.props.onValueSet('', useV, await this.validate(useV)) + } + } + if (optionsConfig && (optionsConfig.from === 'manual' || optionsConfig.from === 'data')) { + props.treeData = temp + } + if (multiple === true || multiple?.type === 'array') { + if (Array.isArray(value)) { + props.value = (value as Array) + } else if (value !== undefined) { + props.value = undefined + console.warn('数组类型的树形选框的值需要是字符串或数值的数组。') + } + } else if (multiple?.type === 'split') { + if (typeof value === 'string' && value !== '') { + props.value = transformValueType(String(value).split(multiple.split || ','), multiple?.valueType) + } else if (value !== undefined) { + props.value = undefined + console.warn('字符串分隔类型的树形选框的值需要是字符串。') + } + } else { + props.value = Array.isArray(value) ? value : (mode === 'treeSelect' ? value : undefined) + } + + if (mode === 'table') { + props.titleColumn = titleColumn + return this.renderTableComponent(props) + } else if (mode === 'tree') { + return this.renderTreeComponent(props) + } else { + props.multiple = multiple === true || multiple?.type === 'array' + return ( + + {this.renderComponent(props)} + + ) + } } } diff --git a/src/components/formFields/upload/index.tsx b/src/components/formFields/upload/index.tsx index 5b8fc83ecb6a6ce2d1a7483372cbf60e3a772e7d..c9e515740b82bdb3d76554fa175c671478cd7447 100644 --- a/src/components/formFields/upload/index.tsx +++ b/src/components/formFields/upload/index.tsx @@ -11,6 +11,7 @@ export interface UploadFieldConfigBasic extends FieldConfig { interface: InterfaceConfig requireField: string responseField: string + extraResponseField: Array<{from: string, to: string}> } export interface UploadFieldConfigImage extends UploadFieldConfigBasic { @@ -150,7 +151,6 @@ export default class UploadField extends Field { record: { [field: string]: any } value: V data: any[], - step: number, - config: T + step: { [field: string]: any }, + config: T, + // 挂载引用 + table?: React.ReactNode + baseRoute: string + loadDomain: (domain: string) => Promise } interface ColumnState { } export default class Column extends React.Component, ColumnState> implements IColumn { - constructor (props: ColumnProps) { + constructor(props: ColumnProps) { super(props) this.state = {} } diff --git a/src/components/tableColumns/custom/index.tsx b/src/components/tableColumns/custom/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dbfff5180809b04d0a25fac866ec9fb305f05d8b --- /dev/null +++ b/src/components/tableColumns/custom/index.tsx @@ -0,0 +1,76 @@ +import React, { RefObject } from 'react' +import Column, { ColumnConfig, ColumnProps, IColumn } from '../common' +import { loadMicroApp, MicroApp } from 'qiankun' +import moment from 'moment' +import { cloneDeep } from 'lodash' + +export interface CustomColumnConfig extends ColumnConfig { + type: 'custom' + entry: string +} + +export default class CustomColumn extends Column implements IColumn { + identifier: string = '' + entry: string = '' + container: RefObject = React.createRef() + customColumn: MicroApp | null = null + + componentDidMount () { + this.loadCustomColumn(this.props.config.entry) + } + + getSnapshotBeforeUpdate () { + const snapshot: string[] = [] + if (this.entry !== this.props.config.entry) { + snapshot.push('entry') + } + return snapshot + } + + componentDidUpdate (_: ColumnProps, __: {}, snapshot: string[]) { + if (snapshot.includes('entry')) { + this.loadCustomColumn(this.props.config.entry) + } else { + if (this.customColumn && this.customColumn.update) { + this.customColumn.update({ + value: this.props.value, + record: this.props.record, + data: cloneDeep(this.props.data), + step: this.props.step, + config: this.props.config, + table: this.props.table, + base: this.props.baseRoute, + loadDomain: this.props.loadDomain + }) + } + } + } + + loadCustomColumn = (entry: string) => { + if (this.container.current && entry) { + this.entry = this.props.config.entry + this.identifier = `custom|${moment().format('x')}|${Math.floor(Math.random() * 1000)}` + this.customColumn = loadMicroApp({ + name: this.identifier, + entry, + container: this.container.current, + props: { + value: this.props.value, + record: this.props.record, + data: cloneDeep(this.props.data), + step: this.props.step, + config: this.props.config, + table: this.props.table, + base: this.props.baseRoute, + loadDomain: this.props.loadDomain + } + }) + } + } + + render = () => { + return ( +
+ ) + } +} diff --git a/src/components/tableColumns/enum/index.test.tsx b/src/components/tableColumns/enum/index.test.tsx index 1560a5a22e1a69210ef84ea0ff70e0d457786c2b..b0e744cb8a25005ba47c9bc639bd03c46da80ef3 100644 --- a/src/components/tableColumns/enum/index.test.tsx +++ b/src/components/tableColumns/enum/index.test.tsx @@ -16,12 +16,13 @@ const defaultProps: ColumnProps = { label: 'test', valueType: 'string', multiple: true, + align: 'left', options: { from: 'manual', data: [{ extra: 'a', // todo label: '1', - key: 'filed' + key: 'field' }] }, defaultValue: '' diff --git a/src/components/tableColumns/index.tsx b/src/components/tableColumns/index.tsx index aa8159bee59ff9bb5b32d4cb6efe4c8e28f99d6d..ff52f9443bb24c2fdad21024a94272d152b7e250 100644 --- a/src/components/tableColumns/index.tsx +++ b/src/components/tableColumns/index.tsx @@ -7,6 +7,7 @@ import DatetimeColumn, { DatetimeColumnConfig } from './datetime' import DatetimeRangeColumn, { DatetimeRangeColumnConfig } from './datetimeRange' import MultirowColumn, { MultirowColumnConfig } from './multirowText' import ImageColumn, { ImageColumnConfig } from './image' +import CustomColumn, { CustomColumnConfig } from './custom' export interface componentType { type: 'text' @@ -17,6 +18,7 @@ export interface componentType { | 'Aenum' | 'multirowText' | 'image' + | 'custom' } export type ColumnConfigs = TextColumnConfig @@ -27,6 +29,7 @@ export type ColumnConfigs = TextColumnConfig | NumberColumnConfig | NumberRangeColumnConfig | ImageColumnConfig + | CustomColumnConfig export default { text: TextColumn, @@ -36,5 +39,6 @@ export default { Aenum: EnumColumn, number: NumberColumn, numberRange: NumberRangeColumn, - image: ImageColumn + image: ImageColumn, + custom: CustomColumn } diff --git a/src/components/tableColumns/text/index.tsx b/src/components/tableColumns/text/index.tsx index 639f6fc28ee52a0074f2d29c49b72766231b4608..4a2f8bf4cdc99126e92af1c7956e8410697f043a 100644 --- a/src/components/tableColumns/text/index.tsx +++ b/src/components/tableColumns/text/index.tsx @@ -1,12 +1,26 @@ import React from 'react' import Column, { ColumnConfig } from '../common' +/** + * 表格文本配置项 + * - type: 文本类型 + * - linkUrl: 可跳转文本超链接,临时方案,后续优化。 + * - showLines: 显示行数多行省略 + * - showMore: 查看更多 showLines 大于1时显示 + */ export interface TextColumnConfig extends ColumnConfig { type: 'text' + // 临时方案 后续优化 + linkUrl: boolean + showLines?: number + showMore?: boolean } export interface ITextColumn { value: string + linkUrl: boolean + showLines?: number + showMore?: boolean } export default class TextColumn extends Column { @@ -31,11 +45,19 @@ export default class TextColumn extends Column { } render = () => { + const { + config: { + linkUrl, + showLines, + showMore + } + } = this.props + const value = this.getValue() return ( - {this.renderComponent({ value })} + {this.renderComponent({ value, linkUrl, showLines, showMore })} ) } diff --git a/src/index.tsx b/src/index.tsx index b193a0536d64a8ad4a2c1726d89d8c170a8c9941..c25627c0a2d3701babd80ffb2cf5c4a04f2806d8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,8 +26,11 @@ export { default as HiddenField } from './components/formFields/hidden' export { default as TabsField } from './components/formFields/tabs' export { default as MultipleTextField } from './components/formFields/multipleText' export { default as CustomField } from './components/formFields/custom' +export { default as CodeField } from './components/formFields/code' +export { default as DiffCodeField } from './components/formFields/diffCode' export { default as TextDisplay } from './components/formFields/text/display' +export { default as FormDisplay } from './components/formFields/form/display' export { default as LongTextDisplay } from './components/formFields/longtext/display' export { default as RadioDisplay } from './components/formFields/radio/display' export { default as ColorDisplay } from './components/formFields/color/display' @@ -38,7 +41,11 @@ export { default as DatetimeDisplay } from './components/formFields/datetime/dis export { default as DatetimeRangeDisplay } from './components/formFields/datetimeRange/display' export { default as SelectSingleDisplay } from './components/formFields/select/single/display' export { default as SelectMultipleDisplay } from './components/formFields/select/multiple/display' +export { default as ImportSubformDisplay } from './components/formFields/importSubform/display' +export { default as GroupDisplay } from './components/formFields/group/display' +export { default as TabsDisplay } from './components/formFields/tabs/display' export { default as MultipleTextDisplay } from './components/formFields/multipleText/display' +export { default as HiddenDisplay } from './components/formFields/hidden/display' export { default as TableStep } from './steps/table' export { default as TextColumn } from './components/tableColumns/text' @@ -50,14 +57,20 @@ export { default as NumberRangeColumn } from './components/tableColumns/numberRa export { default as MultirowTextColumn } from './components/tableColumns/multirowText' export { default as DatetimeRangeColumn } from './components/tableColumns/datetimeRange' export { default as ImageColumn } from './components/tableColumns/image' +export { default as CustomColumn } from './components/tableColumns/custom' export { default as FetchStep } from './steps/fetch' export { default as DetailStep } from './steps/detail' export { default as DetailGroupField } from './components/detail/group' export { default as DetailEunmField } from './components/detail/enum' export { default as DetailStatementField } from './components/detail/statement' +export { default as DetailImageField } from './components/detail/image' export { default as DetailTextField } from './components/detail/text' export { default as DetailImportSubformField } from './components/detail/importSubform' +export { default as DetailInfoField } from './components/detail/detailInfo' +export { default as DetailColorField } from './components/detail/detailColor' +export { default as DetailTableField } from './components/detail/table' +export { default as CustomDetail } from './components/detail/custom' export { default as HeaderStep } from './steps/header' diff --git a/src/interface.ts b/src/interface.ts index 1f559664d9d0d57597e44744d185fff77e557cf3..60a20c5318db17d7005a4f6cb61eee4c5cdc2917 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -7,75 +7,84 @@ * - content: 内容 */ export interface RichStringConfig { - type: 'plain' | 'markdown' | 'html' - content: string + type: 'plain' | 'markdown' | 'html'; + content: string; } -export type ParamConfig = RecordParamConfig | DataParamConfig | StepParamConfig | SourceParamConfig | URLParamConfig | QueryParamConfig | HashParamConfig | InterfaceParamConfig | StaticParamConfig +export type ParamConfig = + | RecordParamConfig + | DataParamConfig + | StepParamConfig + | SourceParamConfig + | URLParamConfig + | QueryParamConfig + | HashParamConfig + | InterfaceParamConfig + | StaticParamConfig; -interface RecordParamConfig { - source: 'record' - field: string +export interface RecordParamConfig { + source: 'record'; + field: string; } -interface DataParamConfig { - source: 'data' - field: string +export interface DataParamConfig { + source: 'data'; + field: string; } -interface StepParamConfig { - source: 'step' - step: number - field: string +export interface StepParamConfig { + source: 'step'; + step: number; + field: string; } -interface SourceParamConfig { - source: 'source', - field: string +export interface SourceParamConfig { + source: 'source'; + field: string; } interface URLParamConfig { - source: 'url', - field: string + source: 'url'; + field: string; } interface QueryParamConfig { - source: 'query', - filed: any + source: 'query'; + filed: any; } interface HashParamConfig { - source: 'hash', - filed: any + source: 'hash'; + filed: any; } interface InterfaceParamConfig { - source: 'interface', + source: 'interface'; // api: { // url: string, // method: 'POST', // contentType: 'json', // withCredentials: true // }, - api: object, - apiResponse: string + api: object; + apiResponse: string; } interface StaticParamConfig { - source: 'static', - value: any + source: 'static'; + value: any; } /** * 表单/详情分栏配置定义 -* - * type: 分栏类型 -* - * - * span: 固定分栏 -* - * - * width: 宽度分栏 -* - * value: 分栏相关配置值 -* - * wrap: 分栏后是否换行 -* - * gap: 分栏边距 -*/ + * - * type: 分栏类型 + * - * - * span: 固定分栏 + * - * - * width: 宽度分栏 + * - * value: 分栏相关配置值 + * - * wrap: 分栏后是否换行 + * - * gap: 分栏边距 + */ export interface ColumnsConfig { - enable?: boolean - type?: 'span' | 'width' - value?: number | string, - wrap?: boolean - gap?: number | string - rowGap?: number | string + enable?: boolean; + type?: 'span' | 'width'; + value?: number | string; + wrap?: boolean; + gap?: number | string; + rowGap?: number | string; } diff --git a/src/main.tsx b/src/main.tsx index d580507d2e1a2b24e2458002ae7a33ad6c2f49d0..2aadfb63031b4097292ea51c7dd6cafb0a18e3b9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { forwardRef } from 'react' import marked from 'marked' import Step, { StepProps } from './steps/common' import StepComponents, { StepConfigs } from './steps' @@ -38,7 +38,7 @@ export interface ICCMS { * - config: 页面配置文件 * - sourceData: 传入数据 */ -export interface CCMSProps { +export interface CCMSProps { config: CCMSConfig sourceData: any baseRoute: string @@ -46,10 +46,12 @@ export interface CCMSProps { loadPageURL: (pageID: any) => Promise loadPageFrameURL: (pageID: any) => Promise loadPageConfig: (pageID: any) => Promise + loadPageList: () => Promise> loadDomain: (domain: string) => Promise - handlePageRedirect?: (path: string) => void + handlePageRedirect?: (path: string, replaceHistory: boolean) => void callback: (success: boolean) => void onMount?: () => void + handleFormValue?: (payload: object) => object } /** @@ -58,12 +60,26 @@ export interface CCMSProps { * - viewStep: 界面当前所在步骤 * - data: 各步骤数据 */ -interface CCMSState { +export interface CCMSState { realStep: number viewStep: number[] data: any[] } +/** + * 页面列表项 + * - key: 此项必须设置(其值在整个树范围内唯一) + * - value: 默认根据此属性值进行筛选(其值在整个树范围内唯一) + * - title: 树节点显示的内容 + * - children: 子节点 + */ + export interface PageListItem { + key: string | number + value: string | number + title: string + children?: Array +} + /** * 页面组件 */ @@ -233,10 +249,11 @@ export default class CCMS extends React.Component { loadPageURL, loadPageFrameURL, loadPageConfig, + loadPageList, loadDomain, handlePageRedirect } = this.props - + const handleFormValue = this.props.handleFormValue ? this.props.handleFormValue : (payload: object) => ({}) const { realStep, viewStep, @@ -265,7 +282,7 @@ export default class CCMS extends React.Component { const props: StepProps = { ref: (e) => { this.steps[index] = e }, data, - step: index, + step: data[index], onSubmit: (data: any, unmountView: boolean = true) => this.handleSubmit(index, data, unmountView), onMount: () => this.handleMount(index), onUnmount: (reload: boolean = false, data?: any) => this.handleUnmount(index, reload, data), @@ -275,13 +292,15 @@ export default class CCMS extends React.Component { loadPageURL, loadPageFrameURL, loadPageConfig, + loadPageList, loadDomain, - handlePageRedirect + handlePageRedirect, + handleFormValue } - + const StepComponent = this.getStepComponent(currentStep.type) const children = ( - StepComponent ? : 您当前使用的UI版本没有实现{currentStep.type}步骤组件。 + StepComponent ? : 您当前使用的UI版本没有实现{currentStep.type}步骤组件。 ) return (
{children}
diff --git a/src/steps/common.tsx b/src/steps/common.tsx index 88da0fd7de79b099718f3c48cb43adaee7c70647..de4853a81f4c424840a8353c4a918d14bfae88bc 100644 --- a/src/steps/common.tsx +++ b/src/steps/common.tsx @@ -1,5 +1,7 @@ import React from 'react' -import { CCMSConfig } from '../main' +import marked from 'marked' +import { CCMSConfig, PageListItem } from '../main' +import StatementHelper from '../util/statement' /** * 页面流转步骤基类配置定义 @@ -20,7 +22,7 @@ export interface StepConfig { export interface StepProps { ref: (instance: Step | null) => void data: any[] - step: number + step: {[field: string]: any} config: C onChange?: (data: any) => Promise onSubmit: (data: any, unmountView?: boolean) => Promise @@ -30,9 +32,11 @@ export interface StepProps { loadPageURL: (pageID: any) => Promise loadPageFrameURL: (pageID: any) => Promise loadPageConfig: (pageID: any) => Promise + loadPageList: () => Promise> baseRoute: string loadDomain: (domain: string) => Promise - handlePageRedirect?: (path: string) => void + handlePageRedirect?: (path: string, replaceHistory: boolean) => void + handleFormValue?: (payload: object) => object } /** @@ -44,6 +48,28 @@ export default class Step extends React.Component< } }; + +/** + * 步骤 根据mode不同,处理subLabel内容\ + * @param config 子项config + * @returns + */ + + handleSubLabelContent (config) { + if (config?.subLabelConfig?.enable) { + const content = StatementHelper({ statement: config.subLabelConfig?.content?.statement || '', params: config.subLabelConfig?.content?.params || [] }, { data: this.props.data, step: this.props.step }).replace(/(^\s*)|(\s*$)/g, '') + const mode = config.subLabelConfig?.mode + switch (mode) { + case 'markdown': + return
+ case 'html': + return
+ } + return
{content}
+ } + return undefined + } + stepPush = () => { this.props.onMount() } diff --git a/src/steps/detail/index.tsx b/src/steps/detail/index.tsx index e877424d9c34008e7ba8d9f80766cba711642d87..232a28ef83caf7f5a329d3213817107a31fb89e8 100644 --- a/src/steps/detail/index.tsx +++ b/src/steps/detail/index.tsx @@ -98,7 +98,6 @@ export interface IDetailItem { interface DetailState { ready: boolean detailValue: { [field: string]: any } - detailData: { status: 'normal' | 'error' | 'loading', message?: string, name: string }[] } /** @@ -123,8 +122,7 @@ export default class DetailStep extends Step { super(props) this.state = { ready: false, - detailValue: {}, - detailData: [] + detailValue: {} } } @@ -142,8 +140,6 @@ export default class DetailStep extends Step { onMount } = this.props - const detailData = cloneDeep(this.state.detailData) - if (this.props.config.defaultValue) { let detailDefault = ParamHelper(this.props.config.defaultValue, { data, step }) if (this.props.config.unstringify) { @@ -160,14 +156,12 @@ export default class DetailStep extends Step { const detailFieldConfig = detailFieldsConfig[detailFieldIndex] const value = getValue(detailDefault, detailFieldConfig.field) this.detailValue = setValue(this.detailValue, detailFieldConfig.field, value) - detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label } } } await this.setState({ ready: true, - detailValue: this.detailValue, - detailData: cloneDeep(detailData) + detailValue: this.detailValue }) // 表单初始化结束,展示表单界面。 @@ -180,8 +174,6 @@ export default class DetailStep extends Step { } this.detailFieldsMounted[detailFieldIndex] = true - const detailData = cloneDeep(this.state.detailData) - if (this.detailFields[detailFieldIndex]) { const detailField = this.detailFields[detailFieldIndex] if (detailField) { @@ -190,20 +182,12 @@ export default class DetailStep extends Step { const value = getValue(this.detailValue, detailFieldConfig.field) this.detailValue = setValue(this.detailValue, detailFieldConfig.field, value) - const validation = await detailField.validate(value) - if (validation === true) { - detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label } - } else { - // 首次进入错误提示; - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message, name: detailFieldConfig.label } - } await detailField.didMount() } } await this.setState({ - detailValue: this.detailValue, - detailData: cloneDeep(detailData) + detailValue: this.detailValue }) } @@ -224,23 +208,13 @@ export default class DetailStep extends Step { * @param value 目标值 */ handleChange = async (detailFieldIndex: number, value: any) => { - const detailData = cloneDeep(this.state.detailData) - const detailField = this.detailFields[detailFieldIndex] const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailField && detailFieldConfig) { this.detailValue = setValue(this.detailValue, detailFieldConfig.field, value) - const validation = await detailField.validate(value) - if (validation === true) { - detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label } - } else { - detailData[detailFieldIndex] = { status: 'error', message: validation[0].message, name: detailFieldConfig.label } - } - await this.setState({ - detailValue: this.detailValue, - detailData + detailValue: this.detailValue }) if (this.props.onChange) { this.props.onChange(this.detailValue) @@ -248,10 +222,10 @@ export default class DetailStep extends Step { } } - handleValueSet = async (detailFieldIndex: number, path: string, value: any, validation: true | DetailFieldError[]) => { + handleValueSet = async (detailFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) set(this.detailValue, fullPath, value) this.setState({ @@ -260,22 +234,13 @@ export default class DetailStep extends Step { if (this.props.onChange) { this.props.onChange(this.detailValue) } - - if (validation === true) { - this.detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label, hidden: false } - } else { - this.detailData[detailFieldIndex] = { status: 'error', message: validation[0].message, name: detailFieldConfig.label, hidden: false } - } - await this.setState({ - detailData: this.detailData - }) } } - handleValueUnset = async (detailFieldIndex: number, path: string, validation: true | DetailFieldError[]) => { + handleValueUnset = async (detailFieldIndex: number, path: string, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) unset(this.detailValue, fullPath) this.setState({ @@ -284,23 +249,13 @@ export default class DetailStep extends Step { if (this.props.onChange) { this.props.onChange(this.detailValue) } - - if (validation === true) { - this.detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label, hidden: false } - } else { - this.detailData[detailFieldIndex] = { status: 'error', message: validation[0].message, name: detailFieldConfig.label, hidden: false } - } - - await this.setState({ - detailData: this.detailData - }) } } - handleValueListAppend = async (detailFieldIndex: number, path: string, value: any, validation: true | DetailFieldError[]) => { + handleValueListAppend = async (detailFieldIndex: number, path: string, value: any, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) const list = get(this.detailValue, fullPath, []) list.push(value) @@ -311,23 +266,13 @@ export default class DetailStep extends Step { if (this.props.onChange) { this.props.onChange(this.detailValue) } - - if (validation === true) { - this.detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label, hidden: false } - } else { - this.detailData[detailFieldIndex] = { status: 'error', message: validation[0].message, name: detailFieldConfig.label, hidden: false } - } - - await this.setState({ - detailData: this.detailData - }) } } - handleValueListSplice = async (detailFieldIndex: number, path: string, index: number, count: number, validation: true | DetailFieldError[]) => { + handleValueListSplice = async (detailFieldIndex: number, path: string, index: number, count: number, options?: { noPathCombination?: boolean }) => { const detailFieldConfig = (this.props.config.fields || [])[detailFieldIndex] if (detailFieldConfig) { - const fullPath = detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (detailFieldConfig.field === '' || path === '' ? `${detailFieldConfig.field}${path}` : `${detailFieldConfig.field}.${path}`) const list = get(this.detailValue, fullPath, []) list.splice(index, count) @@ -338,16 +283,6 @@ export default class DetailStep extends Step { if (this.props.onChange) { this.props.onChange(this.detailValue) } - - if (validation === true) { - this.detailData[detailFieldIndex] = { status: 'normal', name: detailFieldConfig.label, hidden: false } - } else { - this.detailData[detailFieldIndex] = { status: 'error', message: validation[0].message, name: detailFieldConfig.label, hidden: false } - } - - await this.setState({ - detailData: this.detailData - }) } } @@ -389,8 +324,7 @@ export default class DetailStep extends Step { const { ready, - detailValue, - detailData + detailValue } = this.state if (ready) { @@ -434,12 +368,12 @@ export default class DetailStep extends Step { // message: detailFieldConfig.field !== undefined ? getValue(detailData, detailFieldConfig.field, {}).message || '' : '', columns: config.columns?.enable ? { - type: detailFieldConfig.columns?.type || config.columns?.type || 'span', - value: detailFieldConfig.columns?.value || config.columns?.value || 1, - wrap: detailFieldConfig.columns?.wrap || config.columns?.wrap || false, - gap: detailFieldConfig.columns?.gap || config.columns?.gap || 0, - rowGap: detailFieldConfig.columns?.rowGap || config.columns?.rowGap || 0 - } + type: detailFieldConfig.columns?.type || config.columns?.type || 'span', + value: detailFieldConfig.columns?.value || config.columns?.value || 1, + wrap: detailFieldConfig.columns?.wrap || config.columns?.wrap || false, + gap: detailFieldConfig.columns?.gap || config.columns?.gap || 0, + rowGap: detailFieldConfig.columns?.rowGap || config.columns?.rowGap || 0 + } : undefined, layout, styles: detailFieldConfig.styles || {}, @@ -447,6 +381,13 @@ export default class DetailStep extends Step { fieldType: detailFieldConfig.type, children: ( this.props.handlePageRedirect} key={detailFieldIndex} ref={(detailField: DetailField | null) => { if (detailFieldIndex !== null) { @@ -457,14 +398,15 @@ export default class DetailStep extends Step { formLayout={layout} value={detailFieldConfig.field !== undefined ? getValue(detailValue, detailFieldConfig.field) || detailFieldConfig.defaultValue : undefined} record={detailValue} + step={cloneDeep(detailValue)} data={cloneDeep(data)} - step={step} + detail={this} config={detailFieldConfig} onChange={async (value: any) => { await this.handleChange(detailFieldIndex, value) }} - onValueSet={async (path, value, validation) => await this.handleValueSet(detailFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => await this.handleValueUnset(detailFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => await this.handleValueListAppend(detailFieldIndex, path, value, validation)} - onValueListSplice={async (path, index, count, validation) => await this.handleValueListSplice(detailFieldIndex, path, index, count, validation)} + onValueSet={async (path, value) => await this.handleValueSet(detailFieldIndex, path, value)} + onValueUnset={async (path) => await this.handleValueUnset(detailFieldIndex, path)} + onValueListAppend={async (path, value) => await this.handleValueListAppend(detailFieldIndex, path, value)} + onValueListSplice={async (path, index, count) => await this.handleValueListSplice(detailFieldIndex, path, index, count)} baseRoute={this.props.baseRoute} loadDomain={async (domain: string) => await this.props.loadDomain(domain)} /> diff --git a/src/steps/fetch/index.tsx b/src/steps/fetch/index.tsx index 19f631ef62e86074f73d93dd3b2886d42223bdb2..954526360d3a568401cc340244b97a8760ea9d14 100644 --- a/src/steps/fetch/index.tsx +++ b/src/steps/fetch/index.tsx @@ -40,7 +40,7 @@ export default class FetchStep extends Step { try { const content = await this.interfaceHelper.request( merge(config.interface, { cache: { disabled: true } }), - {...(this.popData || {}), ...(init_data || {}), ...(this.props.data[this.props.step] || {})}, + {...(this.popData || {}), ...(init_data || {}), ...(this.props.step || {})}, { data: this.props.data, step: this.props.step diff --git a/src/steps/filter/index.tsx b/src/steps/filter/index.tsx index 3f1bd371a4ba70596afeb6743f0f862c83b924db..6d4c8bc83fce7aea4dd96dfca77cd1712def3f99 100644 --- a/src/steps/filter/index.tsx +++ b/src/steps/filter/index.tsx @@ -4,9 +4,10 @@ import Step, { StepConfig, StepProps } from '../common' import getALLComponents from '../../components/formFields' import { ParamConfig } from '../../interface' import ParamHelper from '../../util/param' -import { cloneDeep, get, set, unset } from 'lodash' +import { get } from 'lodash' +import { push, splice, sort, set, setValue } from '../../util/produce' import ConditionHelper from '../../util/condition' -import { getValue, setValue, listItemMove } from '../../util/value' +import { getValue } from '../../util/value' /** * 表单步骤配置文件格式定义 @@ -88,6 +89,7 @@ export default class FilterStep extends Step { // 各表单项所使用的UI组件的实例 formFields: Array | null> = [] formFieldsMounted: Array = [] + dependentFields_: string[] = [] formValue: { [field: string]: any } = {} formData: { status: 'normal' | 'error' | 'loading', message?: string }[] = [] @@ -118,13 +120,13 @@ export default class FilterStep extends Step { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] const value = (formFieldConfig.field === undefined || formFieldConfig.field === '') ? formDefault : get(formDefault, formFieldConfig.field) this.formValue = setValue(this.formValue, formFieldConfig.field, value) - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } } await this.setState({ formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) // 表单初始化结束,展示表单界面。 @@ -141,7 +143,7 @@ export default class FilterStep extends Step { return true } - this.formFieldsMounted[formFieldIndex] = true + this.formFieldsMounted = set(this.formFieldsMounted, `[${formFieldIndex}]`, true) if (this.formFields[formFieldIndex]) { const formField = this.formFields[formFieldIndex] @@ -157,16 +159,16 @@ export default class FilterStep extends Step { if (value !== undefined) { const validation = await formField.validate(value) if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } } } } await this.setState({ formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) } @@ -198,7 +200,7 @@ export default class FilterStep extends Step { console.info('表单校验通过', data) await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) if (canSubmit && this.props.onSubmit) { @@ -235,16 +237,16 @@ export default class FilterStep extends Step { const validation = await formField.validate(value) if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } } } await this.setState({ formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) this.handleSubmit() @@ -263,14 +265,14 @@ export default class FilterStep extends Step { const validation = await formField.validate(value) if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } await this.setState({ formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) if (this.props.onChange) { this.props.onChange(this.formValue) @@ -278,12 +280,12 @@ export default class FilterStep extends Step { } } - handleValueSet = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueSet = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) - set(this.formValue, fullPath, value) + this.formValue = set(this.formValue, fullPath, value) this.setState({ formValue: this.formValue }) @@ -292,23 +294,24 @@ export default class FilterStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueUnset = async (formFieldIndex: number, path: string, validation: true | FieldError[]) => { + handleValueUnset = async (formFieldIndex: number, path: string, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) - unset(this.formValue, fullPath) + // unset(this.formValue, fullPath) + this.formValue = set(this.formValue, fullPath) this.setState({ formValue: this.formValue }) @@ -317,25 +320,23 @@ export default class FilterStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueListAppend = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueListAppend = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) - const list = get(this.formValue, fullPath, []) - list.push(value) - set(this.formValue, fullPath, list) + this.formValue = push(this.formValue, fullPath, value) this.setState({ formValue: this.formValue }) @@ -344,25 +345,23 @@ export default class FilterStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, validation: true | FieldError[]) => { + handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) - const list = get(this.formValue, fullPath, []) - list.splice(index, count) - set(this.formValue, fullPath, list) + this.formValue = splice(this.formValue, fullPath, index, count) this.setState({ formValue: this.formValue }) @@ -371,23 +370,23 @@ export default class FilterStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueListSort = async (formFieldIndex: number, path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => { + + handleValueListSort = async (formFieldIndex: number, path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[], options?: { noPathCombination?: boolean }) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` + const fullPath = options && options.noPathCombination ? path : (formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}`) - const list = listItemMove(get(this.formValue, fullPath, []), index, sortType) - set(this.formValue, fullPath, list) + this.formValue = sort(this.formValue, fullPath, index, sortType) this.setState({ formValue: this.formValue }) @@ -396,13 +395,13 @@ export default class FilterStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal' } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal' }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'error', message: validation[0].message }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } @@ -449,11 +448,11 @@ export default class FilterStep extends Step { {this.renderComponent({ onSubmit: this.props.config?.hiddenSubmit ? undefined : async () => this.handleSubmit(), onReset: this.props.config?.hiddenReset ? undefined : async () => this.handleReset(), - submitText: this.props.config?.submitText?.replace(/(^\s*)|(\s*$)/g, ""), - resetText: this.props.config?.resetText?.replace(/(^\s*)|(\s*$)/g, ""), + submitText: this.props.config?.submitText?.replace(/(^\s*)|(\s*$)/g, ''), + resetText: this.props.config?.resetText?.replace(/(^\s*)|(\s*$)/g, ''), children: fields.map((formFieldConfig, formFieldIndex) => { if (!ConditionHelper(formFieldConfig.condition, { record: formValue, data, step })) { - this.formFieldsMounted[formFieldIndex] = false + this.formFieldsMounted = set(this.formFieldsMounted, `[${formFieldIndex}]`, false) return null } let hidden: boolean = true @@ -488,7 +487,7 @@ export default class FilterStep extends Step { key={formFieldIndex} ref={(formField: Field | null) => { if (formFieldIndex !== null) { - this.formFields[formFieldIndex] = formField + this.formFields = set(this.formFields, `[${formFieldIndex}]`, formField) this.handleFormFieldMount(formFieldIndex) } }} @@ -496,17 +495,19 @@ export default class FilterStep extends Step { formLayout={'inline'} value={formFieldConfig.field !== undefined ? get(formValue, formFieldConfig.field) : undefined} record={formValue} - data={cloneDeep(data)} - step={step} + step={formValue} + data={data} config={formFieldConfig} onChange={async (value: any) => { await this.handleChange(formFieldIndex, value) }} - onValueSet={async (path, value, validation) => await this.handleValueSet(formFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => await this.handleValueUnset(formFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => await this.handleValueListAppend(formFieldIndex, path, value, validation)} - onValueListSplice={async (path, index, count, validation) => await this.handleValueListSplice(formFieldIndex, path, index, count, validation)} - onValueListSort={async (path, index, sortType, validation) => await this.handleValueListSort(formFieldIndex, path, index, sortType, validation)} + onValueSet={async (path, value, validation, options) => await this.handleValueSet(formFieldIndex, path, value, validation, options)} + onValueUnset={async (path, validation, options) => await this.handleValueUnset(formFieldIndex, path, validation, options)} + onValueListAppend={async (path, value, validation, options) => await this.handleValueListAppend(formFieldIndex, path, value, validation, options)} + onValueListSplice={async (path, index, count, validation, options) => await this.handleValueListSplice(formFieldIndex, path, index, count, validation, options)} + onValueListSort={async (path, index, sortType, validation, options) => await this.handleValueListSort(formFieldIndex, path, index, sortType, validation, options)} baseRoute={this.props.baseRoute} loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + loadPageList={async () => await this.props.loadPageList()} + containerPath={''} /> ) } diff --git a/src/steps/form/index.tsx b/src/steps/form/index.tsx index 5be24f068f51c1210dbba4e48eaa5d87f2f7fb52..9e04c4bb632b4d28e4e6da4059015b673678dcc7 100644 --- a/src/steps/form/index.tsx +++ b/src/steps/form/index.tsx @@ -2,10 +2,10 @@ import React from 'react' import { Field, FieldConfigs, FieldError } from '../../components/formFields/common' import Step, { StepConfig, StepProps } from '../common' import getALLComponents from '../../components/formFields' -import { getValue, setValue, listItemMove, getBoolean } from '../../util/value' import { ColumnsConfig, ParamConfig } from '../../interface' +import { getValue, getBoolean } from '../../util/value' import ParamHelper from '../../util/param' -import { cloneDeep, get, set, unset } from 'lodash' +import { push, splice, sort, set, setValue } from '../../util/produce' import ConditionHelper, { ConditionConfig } from '../../util/condition' import StatementHelper, { StatementConfig } from '../../util/statement' import OperationHelper, { OperationConfig } from '../../util/operation' @@ -42,13 +42,14 @@ export interface FormConfig extends StepConfig { * 表单组件配置文件格式定义 * 参照其它组件定义 */ - fields?: FieldConfigs[], - defaultValue?: ParamConfig, + fields?: FieldConfigs[] + defaultValue?: ParamConfig validations?: Array<{ condition?: ConditionConfig message?: StatementConfig }> actions: Array | [] + rightTopActions: Array | [] stringify?: string[] // 序列化字段 unstringify?: string[] // 反序列化字段 hiddenSubmit?: boolean // 是否隐藏提交按钮 TODO 待删除 @@ -76,9 +77,10 @@ export interface FormConfig extends StepConfig { * - * - * - cancel: 取消表单 */ export interface ActionConfig { - type: 'submit' | 'cancel' | 'ccms', - label: string, - mode: 'normal' | 'primary' | 'link', + type: 'submit' | 'cancel' | 'ccms' + label: string + mode: 'normal' | 'primary' | 'link' + submitValidate: boolean condition?: ConditionConfig handle?: OperationConfig callback?: { @@ -115,6 +117,7 @@ export interface IForm { layout: 'horizontal' | 'vertical' | 'inline' columns?: ColumnsConfig actions?: React.ReactNode[] + rightTopActions?: React.ReactNode[] children: React.ReactNode[] onSubmit?: () => Promise onCancel?: () => Promise @@ -151,8 +154,9 @@ export interface IButtonProps { * - children: 表单项内容 */ export interface IFormItem { - key: string | number, - label: string, + key: string | number + label: string + subLabel?: React.ReactNode status: 'normal' | 'error' | 'loading' required: boolean description?: string @@ -172,31 +176,39 @@ export interface IFormItem { interface FormState { ready: boolean formValue: { [field: string]: any } - formData: { status: 'normal' | 'error' | 'loading', message?: string, name: string }[] + formData: { status: 'normal' | 'error' | 'loading'; message?: string; name: string }[] } /** * 表单步骤组件 */ export default class FormStep extends Step { + // ts对class的声明文件报错,临时解决 // 各表单项对应的类型所使用的UI组件的类 getALLComponents = (type: any): typeof Field => getALLComponents[type] + OperationHelper = OperationHelper // 各表单项所使用的UI组件的实例 - formFields: Array | null> = [] + formFields: Array, any> | null> = [] + formFieldsMounted: Array = [] + dependentFields_: string[] = [] + formValue: { [field: string]: any } = {} - formData: { status: 'normal' | 'error' | 'loading', message?: string, name: string }[] = [] - canSubmit: boolean = false + + formData: { status: 'normal' | 'error' | 'loading'; message?: string; name: string }[] = [] + + canSubmit = false + submitData: object = {} /** * 初始化表单的值 * @param props */ - constructor (props: StepProps) { + constructor(props: StepProps) { super(props) this.state = { ready: false, @@ -211,9 +223,7 @@ export default class FormStep extends Step { stepPush = async () => { // 处理表单步骤配置文件的默认值 const { - config: { - fields: formFieldsConfig = [] - }, + config: { fields: formFieldsConfig = [] }, data, step, onMount @@ -236,18 +246,19 @@ export default class FormStep extends Step { } } - for (const formFieldIndex in formFieldsConfig) { + for (let formFieldIndex = 0; formFieldIndex < formFieldsConfig.length; formFieldIndex++) { const formFieldConfig = formFieldsConfig[formFieldIndex] const value = getValue(formDefault, formFieldConfig.field) + this.formValue = setValue(this.formValue, formFieldConfig.field, value) - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } } await this.setState({ ready: true, formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) // 表单初始化结束,展示表单界面。 @@ -258,15 +269,14 @@ export default class FormStep extends Step { if (this.formFieldsMounted[formFieldIndex]) { return true } - this.formFieldsMounted[formFieldIndex] = true + this.formFieldsMounted = set(this.formFieldsMounted, `[${formFieldIndex}]`, true) if (this.formFields[formFieldIndex]) { const formField = this.formFields[formFieldIndex] if (formField) { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] - let value = getValue(this.formValue, formFieldConfig.field) - if ((formFieldConfig.defaultValue) && value === undefined) { + if (formFieldConfig.defaultValue && value === undefined) { value = await formField.reset() } value = await formField.set(value) @@ -275,9 +285,13 @@ export default class FormStep extends Step { if (value !== undefined) { const validation = await formField.validate(value) if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } } await formField.didMount() @@ -286,7 +300,7 @@ export default class FormStep extends Step { await this.setState({ formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) } @@ -298,9 +312,20 @@ export default class FormStep extends Step { this.submitData = {} if (this.props.config.validations) { for (const validation of this.props.config.validations) { - if (!ConditionHelper(validation.condition, { record: this.state.formValue, data: this.props.data, step: this.props.step })) { + if ( + !ConditionHelper(validation.condition, { + record: this.state.formValue, + data: this.props.data, + step: this.formValue + }) + ) { this.canSubmit = false - const message = StatementHelper(validation.message, { record: this.state.formValue, data: this.props.data, step: this.props.step }) || '未填写失败文案或失败文案配置异常' + const message = + StatementHelper(validation.message, { + record: this.state.formValue, + data: this.props.data, + step: this.formValue + }) || '未填写失败文案或失败文案配置异常' this.renderModalComponent({ message }) return } @@ -308,17 +333,20 @@ export default class FormStep extends Step { if (!this.canSubmit) return } - for (const formFieldIndex in (this.props.config.fields || [])) { + for (const formFieldIndex in this.props.config.fields || []) { if (this.formFields[formFieldIndex]) { const formField = this.formFields[formFieldIndex] const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] - if (formField && formFieldConfig) { + if (formField && formFieldConfig && !formFieldConfig.disabled) { const value = await formField.get() const validation = await formField.validate(value) - if (validation !== true) { console.warn('表单项中存在问题', value, formFieldConfig) - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) this.canSubmit = false } this.submitData = setValue(this.submitData, formFieldConfig.field, value) @@ -334,7 +362,7 @@ export default class FormStep extends Step { } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } @@ -355,9 +383,7 @@ export default class FormStep extends Step { * 处理表单返回事件 */ handleCancel = async () => { - const { - onUnmount - } = this.props + const { onUnmount } = this.props onUnmount() } @@ -375,14 +401,18 @@ export default class FormStep extends Step { const validation = await formField.validate(value) if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } await this.setState({ formValue: this.formValue, - formData: cloneDeep(this.formData) + formData: this.formData }) if (this.props.onChange) { this.props.onChange(this.formValue) @@ -390,39 +420,66 @@ export default class FormStep extends Step { } } - handleValueSet = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueSet = async ( + formFieldIndex: number, + path: string, + value: any, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` - - set(this.formValue, fullPath, value) - this.setState({ + const fullPath = + options && options.noPathCombination + ? path + : formFieldConfig.field === '' || path === '' + ? `${formFieldConfig.field}${path}` + : `${formFieldConfig.field}.${path}` + + this.formValue = set(this.formValue, fullPath, value) + this.setState(({ formValue }) => ({ formValue: this.formValue - }) + })) + if (this.props.onChange) { this.props.onChange(this.formValue) } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } console.log('form set data', this.formData) await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueUnset = async (formFieldIndex: number, path: string, validation: true | FieldError[]) => { + handleValueUnset = async ( + formFieldIndex: number, + path: string, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` - - unset(this.formValue, fullPath) + const fullPath = + options && options.noPathCombination + ? path + : formFieldConfig.field === '' || path === '' + ? `${formFieldConfig.field}${path}` + : `${formFieldConfig.field}.${path}` + + // unset(this.formValue, fullPath) + this.formValue = set(this.formValue, fullPath) this.setState({ formValue: this.formValue }) @@ -431,26 +488,38 @@ export default class FormStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueListAppend = async (formFieldIndex: number, path: string, value: any, validation: true | FieldError[]) => { + handleValueListAppend = async ( + formFieldIndex: number, + path: string, + value: any, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` - - let list = get(this.formValue, fullPath, []) - if (!Array.isArray(list)) list = [] - list.push(value) - set(this.formValue, fullPath, list) + const fullPath = + options && options.noPathCombination + ? path + : formFieldConfig.field === '' || path === '' + ? `${formFieldConfig.field}${path}` + : `${formFieldConfig.field}.${path}` + + this.formValue = push(this.formValue, fullPath, value) // 向this.formValue的fullPath下的值添加value this.setState({ formValue: this.formValue }) @@ -459,25 +528,39 @@ export default class FormStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueListSplice = async (formFieldIndex: number, path: string, index: number, count: number, validation: true | FieldError[]) => { + handleValueListSplice = async ( + formFieldIndex: number, + path: string, + index: number, + count: number, + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` - - const list = get(this.formValue, fullPath, []) - list.splice(index, count) - set(this.formValue, fullPath, list) + const fullPath = + options && options.noPathCombination + ? path + : formFieldConfig.field === '' || path === '' + ? `${formFieldConfig.field}${path}` + : `${formFieldConfig.field}.${path}` + + this.formValue = splice(this.formValue, fullPath, index, count) this.setState({ formValue: this.formValue }) @@ -486,24 +569,39 @@ export default class FormStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } - handleValueListSort = async (formFieldIndex: number, path: string, index: number, sortType: 'up' | 'down', validation: true | FieldError[]) => { + handleValueListSort = async ( + formFieldIndex: number, + path: string, + index: number, + sortType: 'up' | 'down', + validation: true | FieldError[], + options?: { noPathCombination?: boolean } + ) => { const formFieldConfig = (this.props.config.fields || [])[formFieldIndex] if (formFieldConfig) { - const fullPath = formFieldConfig.field === '' || path === '' ? `${formFieldConfig.field}${path}` : `${formFieldConfig.field}.${path}` - - const list = listItemMove(get(this.formValue, fullPath, []), index, sortType) - set(this.formValue, fullPath, list) + const fullPath = + options && options.noPathCombination + ? path + : formFieldConfig.field === '' || path === '' + ? `${formFieldConfig.field}${path}` + : `${formFieldConfig.field}.${path}` + + this.formValue = sort(this.formValue, fullPath, index, sortType) this.setState({ formValue: this.formValue }) @@ -512,13 +610,17 @@ export default class FormStep extends Step { } if (validation === true) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { status: 'normal', name: formFieldConfig.label }) } else { - this.formData[formFieldIndex] = { status: 'error', message: validation[0].message, name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'error', + message: validation[0].message, + name: formFieldConfig.label + }) } await this.setState({ - formData: cloneDeep(this.formData) + formData: this.formData }) } } @@ -531,7 +633,11 @@ export default class FormStep extends Step { if (success) { const callbackType = action.callback?.type if (callbackType) { - if (callbackType === 'submit') { this.handleSubmit() } else if (callbackType === 'cancel') { this.handleCancel() } + if (callbackType === 'submit') { + this.handleSubmit() + } else if (callbackType === 'cancel') { + this.handleCancel() + } } } } @@ -542,9 +648,7 @@ export default class FormStep extends Step { * @param props */ renderComponent = (props: IForm) => { - return - 您当前使用的UI版本没有实现Form组件。 - + return <>您当前使用的UI版本没有实现Form组件。 } /** @@ -552,9 +656,7 @@ export default class FormStep extends Step { * @param props */ renderButtonComponent = (props: IButtonProps) => { - return - 您当前使用的UI版本没有实现FormButton组件。 - + return <>您当前使用的UI版本没有实现FormButton组件。 } /** @@ -563,9 +665,7 @@ export default class FormStep extends Step { * @param props */ renderItemComponent = (props: IFormItem) => { - return - 您当前使用的UI版本没有实现FormItem组件。 - + return <>您当前使用的UI版本没有实现FormItem组件。 } /** @@ -579,92 +679,115 @@ export default class FormStep extends Step { }) } - render () { - const { - data, - step, - config: { - columns, - // layout = 'horizontal', - // fields = [] - actions - } - } = this.props - - const layout = this.props.config?.layout || 'horizontal' - const fields = this.props.config?.fields || [] - - const { - ready, - formValue, - formData - } = this.state - + getActios = (actions: Array | [], formValue: { [field: string]: any }, data: any[]) => { let actions_ if (Object.prototype.toString.call(actions) === '[object Array]') { actions_ = [] for (let index = 0, len = actions.length; index < len; index++) { - if (!ConditionHelper(actions[index].condition, { record: formValue, data, step })) { + if (!ConditionHelper(actions[index].condition, { record: formValue, data, step: formValue })) { continue } if (actions[index].type === 'submit') { - actions_.push(this.renderButtonComponent({ - label: actions[index].label || '', - mode: actions[index].mode, - onClick: () => this.handleSubmit() - })) + actions_.push( + this.renderButtonComponent({ + label: actions[index].label || '', + mode: actions[index].mode, + onClick: () => this.handleSubmit() + }) + ) } else if (actions[index].type === 'cancel') { - actions_.push(this.renderButtonComponent({ - label: actions[index].label || '', - mode: actions[index].mode, - onClick: () => this.handleCancel() - })) + actions_.push( + this.renderButtonComponent({ + label: actions[index].label || '', + mode: actions[index].mode, + onClick: () => this.handleCancel() + }) + ) } else { - const OperationHelperWrapper = { await this.handleCallback(actions[index], success) }} - > - {(onClick) => ( - this.renderButtonComponent({ - label: actions[index].label || '', - mode: actions[index].mode, - onClick - }) - )} - + const { submitValidate } = actions[index] + const OperationHelperWrapper = ( + { + await this.handleCallback(actions[index], success) + }} + > + {(onClick) => + this.renderButtonComponent({ + label: actions[index].label || '', + mode: actions[index].mode, + onClick: submitValidate + ? async () => { + await this.handleValidations() + console.info('表单参数信息', this.submitData, this.state.formValue, this.formData) + if (this.canSubmit) { + onClick() + } + } + : onClick + }) + } + + ) actions_.push(OperationHelperWrapper) } } } - + return actions_ + } + + render() { + const { + data, + config: { + columns, + // layout = 'horizontal', + // fields = [] + actions, + rightTopActions + } + } = this.props + + const layout = this.props.config?.layout || 'horizontal' + const fields = this.props.config?.fields || [] + + const { ready, formValue, formData } = this.state + + const actions_ = this.getActios(actions, formValue, data) + + const rightTopActions_ = this.getActios(rightTopActions, formValue, data) + if (ready) { return ( - + <> {/* 渲染表单 */} {this.renderComponent({ layout, columns: columns?.enable ? columns : undefined, actions: actions_, + rightTopActions: rightTopActions_, onSubmit: this.props.config.hiddenSubmit ? undefined : async () => this.handleSubmit(), // TODO 待删除 onCancel: this.props.config.hiddenCancel ? undefined : async () => this.handleCancel(), // TODO 待删除 submitText: this.props.config?.submitText?.replace(/(^\s*)|(\s*$)/g, ''), // TODO 待删除 cancelText: this.props.config?.cancelText?.replace(/(^\s*)|(\s*$)/g, ''), // TODO 待删除 children: fields.map((formFieldConfig, formFieldIndex) => { - if (!ConditionHelper(formFieldConfig.condition, { record: formValue, data, step })) { - this.formFieldsMounted[formFieldIndex] = false + if (!ConditionHelper(formFieldConfig.condition, { record: formValue, data, step: formValue })) { + this.formFieldsMounted = set(this.formFieldsMounted, `[${formFieldIndex}]`, false) + this.formFields && (this.formFields[formFieldIndex] = null) return null } - let hidden: boolean = true - let display: boolean = true + let hidden = true + let display = true if (formFieldConfig.type === 'hidden') { hidden = true @@ -678,7 +801,10 @@ export default class FormStep extends Step { // 隐藏项同时打标录入数据并清空填写项 if (!hidden) { - this.formData[formFieldIndex] = { status: 'normal', name: formFieldConfig.label } + this.formData = set(this.formData, `[${formFieldIndex}]`, { + status: 'normal', + name: formFieldConfig.label + }) } const FormField = this.getALLComponents(formFieldConfig.type) || Field @@ -692,6 +818,7 @@ export default class FormStep extends Step { const renderData = { key: formFieldIndex, label: formFieldConfig.label, + subLabel: this.handleSubLabelContent(formFieldConfig), columns: columns?.enable ? { type: formFieldConfig.columns?.type || columns?.type || 'span', @@ -703,7 +830,7 @@ export default class FormStep extends Step { : undefined, status, message: formData[formFieldIndex]?.message || '', - extra: StatementHelper(formFieldConfig.extra, { data: this.props.data, step: this.props.step }), + extra: StatementHelper(formFieldConfig.extra, { data: this.props.data, step: formValue }), required: getBoolean(formFieldConfig.required), layout, visitable: display, @@ -713,7 +840,7 @@ export default class FormStep extends Step { key={formFieldIndex} ref={(formField: Field | null) => { if (formField !== null) { - this.formFields[formFieldIndex] = formField + this.formFields = set(this.formFields, `[${formFieldIndex}]`, formField) this.handleFormFieldMount(formFieldIndex) } }} @@ -721,32 +848,41 @@ export default class FormStep extends Step { value={formFieldConfig.field !== undefined ? getValue(formValue, formFieldConfig.field) : undefined} record={formValue} form={this} - data={cloneDeep(data)} - step={step} + data={data} + step={formValue} config={formFieldConfig} - onChange={async (value: any) => { await this.handleChange(formFieldIndex, value) }} - onValueSet={async (path, value, validation) => await this.handleValueSet(formFieldIndex, path, value, validation)} - onValueUnset={async (path, validation) => await this.handleValueUnset(formFieldIndex, path, validation)} - onValueListAppend={async (path, value, validation) => await this.handleValueListAppend(formFieldIndex, path, value, validation)} - onValueListSplice={async (path, index, count, validation) => await this.handleValueListSplice(formFieldIndex, path, index, count, validation)} - onValueListSort={async (path, index, sortType, validation) => await this.handleValueListSort(formFieldIndex, path, index, sortType, validation)} + onChange={async (value: any) => { + await this.handleChange(formFieldIndex, value) + }} + onValueSet={async (path, value, validation, options) => + await this.handleValueSet(formFieldIndex, path, value, validation, options) + } + onValueUnset={async (path, validation, options) => + await this.handleValueUnset(formFieldIndex, path, validation, options) + } + onValueListAppend={async (path, value, validation, options) => + await this.handleValueListAppend(formFieldIndex, path, value, validation, options) + } + onValueListSplice={async (path, index, count, validation, options) => + await this.handleValueListSplice(formFieldIndex, path, index, count, validation, options) + } + onValueListSort={async (path, index, sortType, validation, options) => + await this.handleValueListSort(formFieldIndex, path, index, sortType, validation, options) + } baseRoute={this.props.baseRoute} loadDomain={async (domain: string) => await this.props.loadDomain(domain)} + loadPageList={async () => await this.props.loadPageList()} + containerPath="" /> ) } // 渲染表单项容器 - return ( - hidden - ? this.renderItemComponent(renderData) - : - ) + return hidden ? this.renderItemComponent(renderData) : }) })} - + ) - } else { - return <> } + return <> } } diff --git a/src/steps/header/index.tsx b/src/steps/header/index.tsx index a7ec2d747c76be2b6eee07b26283931e10e340cf..78cd9c5c278f884ef3898802557bf9b44f29f595 100644 --- a/src/steps/header/index.tsx +++ b/src/steps/header/index.tsx @@ -75,7 +75,6 @@ export interface statisticContentConfig extends basicContentConfig { statistics?: (valueStatisticConfig | enumerationStatisticConfig)[] } - interface basicStatisticConfig { label?: string value?: ParamConfig @@ -226,15 +225,16 @@ export default class HeaderStep extends Step { ref={(e: Step | null) => { e && e.stepPush() }} data={this.props.data} step={this.props.step} - onSubmit={async (data, unmountView) => {}} - onMount={async () => {}} - onUnmount={async (reload = false, data) => {}} + onSubmit={async (data, unmountView) => { }} + onMount={async () => { }} + onUnmount={async (reload = false, data) => { }} config={merge(config, defaultConfig)} baseRoute={this.props.baseRoute} checkPageAuth={this.props.checkPageAuth} loadPageURL={this.props.loadPageURL} loadPageFrameURL={this.props.loadPageFrameURL} loadPageConfig={this.props.loadPageConfig} + loadPageList={this.props.loadPageList} loadDomain={this.props.loadDomain} handlePageRedirect={this.props.handlePageRedirect} /> @@ -252,7 +252,11 @@ export default class HeaderStep extends Step { }) case 'enumeration': if (statistic.options) { - EnumerationHelper.options(statistic.options, (config, source) => this.interfaceHelper.request(config, source, { data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain })).then((options) => { + EnumerationHelper.options( + statistic.options, + (config, source) => this.interfaceHelper.request(config, source, { data: this.props.data, step: this.props.step }, { loadDomain: this.props.loadDomain }), + { data: this.props.data, step: this.props.step } + ).then((options) => { if (!this.state || JSON.stringify(this.state[`statistic_options_${_position}_${index}`]) !== JSON.stringify(options)) { this.setState({ [`statistic_options_${_position}_${index}`]: options @@ -289,7 +293,7 @@ export default class HeaderStep extends Step { } } - render () { + render() { const props: IHeaderProps = {} if (this.props.config.breadcrumb && this.props.config.breadcrumb.enable) { @@ -303,6 +307,7 @@ export default class HeaderStep extends Step { loadPageURL={this.props.loadPageURL} loadPageFrameURL={this.props.loadPageFrameURL} loadPageConfig={this.props.loadPageConfig} + loadPageList={this.props.loadPageList} baseRoute={this.props.baseRoute} loadDomain={this.props.loadDomain} handlePageRedirect={this.props.handlePageRedirect} @@ -337,20 +342,19 @@ export default class HeaderStep extends Step { switch (mainContent.type) { case 'plain': props.mainContent = this.handlePlainContent(mainContent, 'main') - break; + break case 'markdown': props.mainContent = this.handleMarkdownContent(mainContent, 'main') - break; + break case 'html': props.mainContent = this.handleHTMLContent(mainContent, 'main') - break; + break case 'detail': props.mainContent = this.handleDetailContent(mainContent, 'main') - break; + break case 'statistic': props.mainContent = this.handleStatisticContent(mainContent, 'main') - default: - break; + break } } @@ -359,12 +363,12 @@ export default class HeaderStep extends Step { switch (extraContent.type) { case 'statistic': props.extraContent = this.handleStatisticContent(extraContent, 'extra') - break; + break case 'image': props.extraContent = this.handleImageContent(extraContent, 'extra') - break; + break default: - break; + break } } diff --git a/src/steps/skip/index.tsx b/src/steps/skip/index.tsx index 955b5490c57882bd45a760a31783d34d193f4687..1579c48e33ff1b29ae53faff24d0796bad9805b2 100644 --- a/src/steps/skip/index.tsx +++ b/src/steps/skip/index.tsx @@ -52,11 +52,11 @@ export default class SkipStep extends Step { } break case 'data': - if (data && data[step]) { + if (data && step) { if (defaultField) { - formDefault = getValue(data[step], defaultField) + formDefault = getValue(step, defaultField) } else { - formDefault = data[step] + formDefault = step } } break diff --git a/src/steps/table/index.tsx b/src/steps/table/index.tsx index 72111dcd7afab0aa26d9ed5b7bffabf80a36ac0c..b47104ff94c23fe5359d1c485812bd2d161ae3db 100644 --- a/src/steps/table/index.tsx +++ b/src/steps/table/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import queryString from 'query-string' import { getParam, getParamText, getValue } from '../../util/value' import getALLComponents, { ColumnConfigs } from '../../components/tableColumns' +import Column from '../../components/tableColumns/common' import Step, { StepConfig, StepProps } from '../common' import { ParamConfig } from '../../interface' import ColumnStyleComponent from './common/columnStyle' @@ -9,7 +10,8 @@ import CCMS, { CCMSConfig } from '../../main' import { cloneDeep, get, set } from 'lodash' import InterfaceHelper, { InterfaceConfig } from '../../util/interface' import ConditionHelper, { ConditionConfig } from '../../util/condition' - +import StatementHelper, { StatementConfig } from '../../util/statement' +import marked from 'marked' /** * 表格步骤配置文件格式定义 * - field: 表格列表数据来源字段 @@ -22,19 +24,33 @@ export interface TableConfig extends StepConfig { label: string primary: string columns: ColumnConfigs[] + rowOperationsPosition: 'left' operations?: { - tableOperations?: Array - rowOperations?: Array - multirowOperations?: Array + tableOperations?: Array + leftTableOperations?: Array + rowOperations?: Array + multirowOperations?: Array } pagination?: { mode: 'none' | 'client' | 'server' current?: string pageSize?: string total?: string + }, + description?: { + type: 'text' | 'tooltip' | 'modal' + label?: StatementConfig + mode: 'plain' | 'markdown' | 'html' + content?: StatementConfig + showIcon: boolean } } +/** + * 表格步骤-菜单配置 + */ +export type TableOperationsType = TableOperationConfig | TableOperationGroupConfig | TableOperationDropdownConfig + /** * 表格步骤-操作配置文件格式 */ @@ -45,6 +61,26 @@ export interface TableOperationGroupConfig { operations: Array } +/** + * 表格步骤-操作配置文件下拉菜单 + */ +export interface TableOperationDropdownConfig { + type: 'dropdown' + label?: string + level?: 'normal' | 'primary' | 'danger' + operations: Array +} + +/** + * 表格步骤-操作配置文件下拉菜单 + */ +export interface TableOperationDropdownConfig { + type: 'dropdown' + label?: string + level?: 'normal' | 'primary' | 'danger' + operations: Array +} + /** * 表格步骤-操作配置文件格式 */ @@ -54,18 +90,32 @@ export interface TableOperationConfig { level?: 'normal' | 'primary' | 'danger' check?: { enable: false } | TableOperationCheckConfig confirm?: { enable: false } | TableOperationConfirmConfig - handle: TableCCMSOperationConfig + handle: TableCCMSOperationConfig | TableLinkOperationConfig condition?: ConditionConfig + modalWidthMode?: 'none' | 'percentage' | 'pixel' + modalWidthValue?: string | number } export interface TableCCMSOperationConfig { type: 'ccms' page: any target: 'current' | 'page' | 'open' | 'handle' + replaceHistory?: boolean targetURL: string + width: string data: { [key: string]: ParamConfig } params?: { field: string, data: ParamConfig }[] callback?: boolean + closeCallback?: boolean + debug?: boolean +} + +export interface TableLinkOperationConfig { + type: 'link' + target: '_blank' | '_self' + targetURL: string + params?: { field: string, data: ParamConfig }[] + callback?: boolean debug?: boolean } @@ -82,6 +132,12 @@ interface TableOperationConfirmConfig { cancelText: string } +export interface DescriptionConfig { + type: 'text' | 'tooltip' | 'modal' + label: string | undefined + content: React.ReactNode + showIcon: boolean +} /** * 表格步骤组件 - UI渲染方法 - 入参 * - data: 数据 @@ -89,6 +145,7 @@ interface TableOperationConfirmConfig { export interface ITable { title: string | null primary: string + width: string data: { [field: string]: any }[] columns: ITableColumn[] pagination?: { @@ -98,7 +155,9 @@ export interface ITable { onChange: (page: number, pageSize: number) => void } tableOperations: React.ReactNode | null - multirowOperations: React.ReactNode | null + leftTableOperations: React.ReactNode | null + multirowOperations: React.ReactNode | null, + description?: DescriptionConfig } /** @@ -198,17 +257,24 @@ export interface ITableStepOperationModal { width: string children: React.ReactNode onClose: () => void + modalWidthMode?: 'none' | 'percentage' | 'pixel' + modalWidthValue?: string | number } interface TableState { operation: { enable: boolean target: 'current' | 'handle' + width: string title: string visible: boolean + modalWidthMode?: 'none' | 'percentage' | 'pixel' + modalWidthValue?: string | number config: CCMSConfig data: any callback?: boolean + closeCallback?: boolean + } pageAuth: { [page: string]: boolean } } @@ -218,7 +284,7 @@ interface TableState { */ export default class TableStep extends Step { CCMS = CCMS - getALLComponents = (type: any) => getALLComponents[type] + getALLComponents = (type: any): typeof Column => getALLComponents[type] interfaceHelper = new InterfaceHelper() /** * 页面权限获取状态 @@ -236,10 +302,14 @@ export default class TableStep extends Step { enable: false, target: 'current', title: '', + width: '400px', visible: false, config: {}, data: {}, - callback: false + callback: false, + modalWidthMode: 'none', + modalWidthValue: '', + closeCallback: false }, pageAuth: {} } @@ -313,11 +383,15 @@ export default class TableStep extends Step { operation: { enable: true, target: operation.handle.target, + width: operation.handle.width, title: operation.label, visible: true, + modalWidthMode: operation.modalWidthMode, + modalWidthValue: operation.modalWidthValue, config: operationConfig, data: params, - callback: operation.handle.callback + callback: operation.handle.callback, + closeCallback: operation.handle.closeCallback } }) } else if (operation.handle.target === 'page') { @@ -326,9 +400,13 @@ export default class TableStep extends Step { const targetURL = operation.handle.targetURL || '' const targetKey = queryString.stringifyUrl({ url, query: { ...query, ...params } }, { arrayFormat: 'bracket' }) || '' if (this.props.handlePageRedirect) { - this.props.handlePageRedirect(`${targetURL}${targetKey}`) + this.props.handlePageRedirect(`${targetURL}${targetKey}`, operation.handle?.replaceHistory || false) } else { - window.location.href = `${targetURL}${targetKey}` + if (operation.handle.replaceHistory) { + window.location.replace(`${targetURL}${targetKey}`) + } else { + window.location.href = `${targetURL}${targetKey}` + } } } else if (operation.handle.target === 'open') { const sourceURL = await this.props.loadPageFrameURL(operation.handle.page) @@ -338,6 +416,31 @@ export default class TableStep extends Step { window.open(`${targetURL}${targetKey}`) } } + + // 当按钮的响应类型是第三方链接时 + if (operation.handle.type === 'link') { + const params = {} + if (operation.handle.params !== undefined) { + for (const { field, data: dataConfig } of operation.handle.params) { + const value = getParam(dataConfig, { record, data, step }) + set(params, field, value) + } + + if (operation.handle.debug) { + console.log('CCMS debug: operation - operation.handle.type === link', params) + } + + const targetURL = operation.handle.targetURL + const { query } = queryString.parseUrl(targetURL, { arrayFormat: 'bracket' }) + const targetKey = queryString.stringifyUrl({ url: '', query: { ...query, ...params } }, { arrayFormat: 'bracket' }) || '' + const jumpUrl = `${targetURL}${targetKey}` + if (operation.handle.target === '_blank') { + window.open(jumpUrl) + } else { + window.location.href = jumpUrl + } + } + } } /** @@ -407,6 +510,18 @@ export default class TableStep extends Step { } + renderRowOperationDropdownComponent = (props: ITableStepRowOperationGroup) => { + return + 您当前使用的UI版本没有实现Table组件的OperationDropdown部分。 + + } + + renderRowOperationDropdownItemComponent = (props: ITableStepRowOperationGroupItem) => { + return + 您当前使用的UI版本没有实现Table组件的OperationDropdownItem部分。 + + } + renderTableOperationComponent = (props: ITableStepTableOperation) => { return 您当前使用的UI版本没有实现Table组件的OperationButton部分。 @@ -431,6 +546,18 @@ export default class TableStep extends Step { } + renderTableOperationDropdownComponent = (props: ITableStepTableOperationGroup) => { + return + 您当前使用的UI版本没有实现Table组件的OperationDropdown部分。 + + } + + renderTableOperationDropdownItemComponent = (props: ITableStepTableOperationGroupItem) => { + return + 您当前使用的UI版本没有实现Table组件的OperationDropdownItem部分。 + + } + renderOperationModal = (props: ITableStepOperationModal) => { const mask = document.createElement('DIV') mask.style.position = 'fixed' @@ -448,16 +575,93 @@ export default class TableStep extends Step { document.body.appendChild(mask) } + tableOperations = (tableOperationsList: Array, getDate: Array<{}>) => { + const { + pageAuth + } = this.state + return tableOperationsList.length > 0 + ? this.renderTableOperationComponent({ + children: tableOperationsList.map((operation: TableOperationsType, index: number) => { + if (operation.type === 'button') { + let hidden = false + if (operation.handle && operation.handle.type === 'ccms') { + hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] + operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) + } + + return + { + hidden + ? null + : this.renderTableOperationButtonComponent({ + label: operation.label, + level: operation.level || 'normal', + onClick: async () => { + await this.handleRowOperation(operation, getDate) + } + }) + } + + } else if (operation.type === 'group') { + return + {this.renderTableOperationGroupComponent({ + label: operation.label, + children: (operation.operations || []).map((operation) => { + let hidden = false + if (operation.handle && operation.handle.type === 'ccms') { + hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] + operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) + } + return hidden + ? null + : this.renderTableOperationGroupItemComponent({ + label: operation.label, + level: operation.level || 'normal', + onClick: async () => { await this.handleRowOperation(operation, getDate) } + }) + }) + })} + + } else if (operation.type === 'dropdown') { + return + {this.renderTableOperationDropdownComponent({ + label: operation.label, + children: (operation.operations || []).map((operation) => { + let hidden = false + if (operation.handle && operation.handle.type === 'ccms') { + hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] + operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) + } + return hidden + ? null + : this.renderTableOperationDropdownItemComponent({ + label: operation.label, + level: operation.level || 'normal', + onClick: async () => { await this.handleRowOperation(operation, getDate) } + }) + }) + })} + + } else { + return + } + }) + }) + : null + } + render() { const { config: { field, label, + rowOperationsPosition, width, primary, columns, operations, - pagination + pagination, + description }, data, step, @@ -468,27 +672,30 @@ export default class TableStep extends Step { operation: { enable: operationEnable, target: operationTarget, + width: operationWidth, title: operationTitle, visible: operationVisible, config: operationConfig, data: operationData, - callback: operationCallback + callback: operationCallback, + closeCallback: operationcloseCallback, + modalWidthMode: operationModalWidthMode, + modalWidthValue: operationModalWidthValue }, pageAuth } = this.state - let getDate = field ? getValue(data[step], field) : data[step] + let getDate = field ? getValue(step, field) : step if (Object.prototype.toString.call(getDate) !== '[object Array]') { getDate = [] } - const props: ITable = { title: label, + width, primary, data: getDate, columns: (columns || []).filter((column) => column.field !== undefined && column.field !== '').map((column, index) => { const field = column.field.split('.')[0] - return { field, label: column.label, @@ -503,71 +710,67 @@ export default class TableStep extends Step { const addfix = ['multirowText'].some((val) => val !== column.field) return { }} record={record} value={value} data={data} step={step} config={column} + table={this} + baseRoute={this.props.baseRoute} + loadDomain={async (domain: string) => await this.props.loadDomain(domain)} /> } } } }), - tableOperations: operations && operations.tableOperations - ? this.renderTableOperationComponent({ - children: operations.tableOperations.map((operation, index) => { - if (operation.type === 'button') { - let hidden = false - if (operation.handle && operation.handle.type === 'ccms') { - hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] - operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) - } - - return hidden - ? - : - {this.renderTableOperationButtonComponent({ - label: operation.label, - level: operation.level || 'normal', - onClick: async () => { - await this.handleRowOperation(operation, getDate) - } - })} - - } else if (operation.type === 'group') { - return - {this.renderTableOperationGroupComponent({ - label: operation.label, - children: (operation.operations || []).map((operation) => { - let hidden = false - if (operation.handle && operation.handle.type === 'ccms') { - hidden = operation.handle.page === undefined || !pageAuth[operation.handle.page.toString()] - operation.handle.page !== undefined && this.checkPageAuth(operation.handle.page.toString()) - } - return hidden - ? null - : this.renderTableOperationGroupItemComponent({ - label: operation.label, - level: operation.level || 'normal', - onClick: async () => { await this.handleRowOperation(operation, getDate) } - }) - }) - })} - - } else { - return - } - }) - }) - : null, + tableOperations: this.tableOperations(operations?.tableOperations || [], getDate), + leftTableOperations: this.tableOperations(operations?.leftTableOperations || [], getDate), multirowOperations: null } - + if (description) { + if (description.type === 'text') { + props.description = { + type: 'text', + label: StatementHelper(description.label, { data: this.props.data, step: this.props.step }), + content: description.content, + showIcon: description.showIcon + } + } else if (description.type === 'tooltip') { + props.description = { + type: 'tooltip', + label: StatementHelper(description.label, { data: this.props.data, step: this.props.step }), + content: description.content, + showIcon: description.showIcon + } + } else { + props.description = { + type: 'modal', + label: StatementHelper(description.label, { data: this.props.data, step: this.props.step }), + content: description.content, + showIcon: description.showIcon + } + } + if (description.content !== undefined) { + const descriptionType = description.mode + switch (descriptionType) { + case 'plain': + props.description && (props.description.content = StatementHelper(description.content, { data: this.props.data, step: this.props.step })) + break + case 'markdown': + props.description && (props.description.content =
) + break + case 'html': + props.description && (props.description.content =
) + break + } + } + } if (pagination && pagination.mode === 'server') { - const paginationCurrent = Number((pagination.current === undefined || pagination.current === '') ? data[step] : get(data[step], pagination.current, 1)) - const paginationPageSize = Number((pagination.pageSize === undefined || pagination.pageSize === '') ? data[step] : get(data[step], pagination.pageSize, 10)) - const paginationTotal = Number((pagination.total === undefined || pagination.total === '') ? data[step] : get(data[step], pagination.total, 0)) + const paginationCurrent = Number((pagination.current === undefined || pagination.current === '') ? step : get(step, pagination.current, 1)) + const paginationPageSize = Number((pagination.pageSize === undefined || pagination.pageSize === '') ? step : get(step, pagination.pageSize, 10)) + const paginationTotal = Number((pagination.total === undefined || pagination.total === '') ? step : get(step, pagination.total, 0)) props.pagination = { current: Number.isNaN(paginationCurrent) ? 1 : paginationCurrent, @@ -591,7 +794,7 @@ export default class TableStep extends Step { } if (operations && operations.rowOperations && operations.rowOperations.length > 0) { - props.columns.push({ + const rowOperationData: ITableColumn = { field: 'ccms-table-rowOperation', label: '操作', align: 'left', @@ -621,10 +824,10 @@ export default class TableStep extends Step { })}
) - } else if (operation.type === 'group') { + } else if (operation.type === 'group' || operation.type === 'dropdown') { return ( - {this.renderRowOperationGroupComponent({ + {this.renderRowOperationDropdownComponent({ label: operation.label, children: (operation.operations || []).map((operation) => { if (!ConditionHelper(operation.condition, { record, data, step })) { @@ -639,7 +842,7 @@ export default class TableStep extends Step { return hidden ? null - : this.renderRowOperationGroupItemComponent({ + : this.renderRowOperationDropdownItemComponent({ label: operation.label, level: operation.level || 'normal', onClick: async () => { await this.handleRowOperation(operation, record) } @@ -657,7 +860,13 @@ export default class TableStep extends Step { return } } - }) + } + + if (rowOperationsPosition === 'left') { + props.columns.unshift(rowOperationData) + } else { + props.columns.push(rowOperationData) + } } const CCMS = this.CCMS @@ -668,11 +877,13 @@ export default class TableStep extends Step { {operationEnable && ( operationTarget === 'current' ? ( - this.renderOperationModal({ - title: operationTitle, - width, - visible: operationVisible, - children: ( + this.renderOperationModal({ + title: operationTitle, + width: operationWidth, + visible: operationVisible, + modalWidthMode: operationModalWidthMode, + modalWidthValue: operationModalWidthValue, + children: ( { loadPageURL={this.props.loadPageURL} loadPageFrameURL={this.props.loadPageFrameURL} loadPageConfig={this.props.loadPageConfig} + loadPageList={this.props.loadPageList} loadDomain={this.props.loadDomain} handlePageRedirect={this.props.handlePageRedirect} onMount={() => { @@ -704,6 +916,9 @@ export default class TableStep extends Step { const { operation } = this.state operation.enable = false operation.visible = false + if ((operationcloseCallback && operationcloseCallback === true) || Boolean(operationcloseCallback)) { + onUnmount(true) + } this.setState({ operation }) } }) @@ -717,6 +932,7 @@ export default class TableStep extends Step { loadPageURL={this.props.loadPageURL} loadPageFrameURL={this.props.loadPageFrameURL} loadPageConfig={this.props.loadPageConfig} + loadPageList={this.props.loadPageList} loadDomain={this.props.loadDomain} handlePageRedirect={this.props.handlePageRedirect} onMount={() => { diff --git a/src/util/condition.ts b/src/util/condition.ts index 7d7542bb753a6db9bfba42cc9ee22feaea8e4ee0..4d04bf033181ca4c9ed5637d5a4e6793b77a6aaa 100644 --- a/src/util/condition.ts +++ b/src/util/condition.ts @@ -1,6 +1,8 @@ -import { set, cloneDeep, template } from "lodash" -import { ParamConfig } from "../interface"; -import ParamHelper from "./param"; +import { template } from 'lodash' +import { set } from './produce' +import { ParamConfig } from '../interface' +import ParamHelper from './param' +import { Field } from '../components/formFields/common' export interface ConditionConfig { /** @@ -22,106 +24,100 @@ export interface ConditionConfig { debug?: boolean } -export default function ConditionHelper(condition: ConditionConfig | undefined, datas: { record?: object, data: object[], step: number }): boolean { - if (condition === undefined || ((condition.statement === undefined || condition.statement === '') && (condition.template === undefined || condition.template === ''))) { +export default function ConditionHelper( + condition: ConditionConfig | undefined, + datas: { record?: object; data: object[]; step: { [field: string]: any }; extraContainerPath?: string }, + _this?: Field +): boolean { + // 条件语句模版 + let conditionTemplate = '' + // 条件语句模版入参 + let statementParams = {} + if ( + condition === undefined || + ((condition.statement === undefined || condition.statement === '') && + (condition.template === undefined || condition.template === '')) + ) { return true - } else { - if (condition.template) { - const statementTemplate = template(condition.template) - let statementParams = {} - if (condition.params) { - condition.params.forEach((param) => { - if (param.field !== undefined && param.data !== undefined) { - const value = ParamHelper(param.data, cloneDeep(datas)) - if (param.field === '') { - statementParams = value === undefined ? 'undefined' : JSON.stringify(value) - } else { - set(statementParams, param.field, value === undefined ? 'undefined' : JSON.stringify(value)) - } - } - }) - } - - try { - const statement = statementTemplate(statementParams) - try { - const result = eval(statement) - if (result) { - if (condition.debug) { - console.info('CCMS debug: condition - `' + statement + '` => true') - } - return true + } + if (condition.template) { + conditionTemplate = condition.template + if (condition.params) { + condition.params.forEach((param) => { + if (param.field !== undefined && param.data !== undefined) { + const value = ParamHelper(param.data, datas, _this) + if (param.field === '') { + statementParams = value === undefined ? 'undefined' : JSON.stringify(value) } else { - if (condition.debug) { - console.info('CCMS debug: condition - `' + statement + '` => false') - } - return false - } - } catch (e) { - if (condition.debug) { - console.info('CCMS debug: condition - `' + condition.template + '` => `' + statement + '` => error') + statementParams = set( + statementParams, + param.field, + value === undefined ? 'undefined' : JSON.stringify(value) + ) } - console.error('表单项展示条件语句执行错误。', condition.template, statement) - return false - } - } catch (e) { - if (condition.debug) { - console.info('CCMS debug: condition - `' + condition.template + '` => error') } - console.error('表单项展示条件语句执行错误。', condition.template) - return false - } - } else { - // 用于兼容旧版本中的通配符 - // V2新增逻辑段 - 开始 - // const statementTemplate = template(condition.statement) - // V2新增逻辑段 - 结束 - // V2移除逻辑段 - 开始 - const statementPolyfill = condition.statement?.replace(/([^\$])\{/g, '$1${') - const statementTemplate = template(statementPolyfill) - // V2移除逻辑段 - 结束 - let statementParams = {} - if (condition.params) { - condition.params.forEach((param) => { - if (param.field !== undefined && param.data !== undefined) { - if (param.field === '') { - statementParams = ParamHelper(param.data, cloneDeep(datas)) - } else { - set(statementParams, param.field, ParamHelper(param.data, cloneDeep(datas))) - } - } - }) - } - - try { - const statement = statementTemplate(statementParams) - try { - const result = eval(statement) - if (result) { - if (condition.debug) { - console.info('CCMS debug: condition - `' + statement + '` => true') - } - return true + }) + } + } else { + // 用于兼容旧版本中的通配符 + // V2新增逻辑段 - 开始 + // const statementTemplate = template(condition.statement) + // V2新增逻辑段 - 结束 + // V2移除逻辑段 - 开始 + conditionTemplate = condition.statement?.replace(/([^$])\{/g, '$1${') || '' + // V2移除逻辑段 - 结束 + + if (condition.params) { + condition.params.forEach((param) => { + if (param.field !== undefined && param.data !== undefined) { + if (param.field === '') { + statementParams = ParamHelper(param.data, datas, _this) } else { - if (condition.debug) { - console.info('CCMS debug: condition - `' + statement + '` => false') - } - return false - } - } catch (e) { - if (condition.debug) { - console.info('CCMS debug: condition - `' + condition.statement + '` => `' + statement + '` => error') + statementParams = set(statementParams, param.field, ParamHelper(param.data, datas, _this)) } - console.error('表单项展示条件语句执行错误。', condition.statement, statement) - return false } - } catch (e) { - if (condition.debug) { - console.info('CCMS debug: condition - `' + condition.statement + '` => error') - } - console.error('表单项展示条件语句执行错误。', condition.statement) - return false + }) + } + } + + return execConditionHandler(condition, conditionTemplate, statementParams) +} + +const evil = (fn) => { + const Fn = Function // 一个变量指向Function,防止有些前端编译工具报错 + return new Fn(`return ${fn}`)() +} + +// 执行条件语句,返回结果 +const execConditionHandler = ( + condition: ConditionConfig | undefined, + conditionTemplate: string, + statementParams: object +): boolean => { + try { + if (Object.values(statementParams).some((param) => param === undefined)) { + if (condition?.debug) { + console.info(`CCMS debug: condition ${conditionTemplate} => false`) + } + return false + } + const statement = template(conditionTemplate)(statementParams) + + try { + const result = evil(statement) + if (condition?.debug) { + console.info(`CCMS debug: condition ${statement} => ${result}`) } + return result + } catch (e) { + console.error('表单项展示条件语句执行错误。', conditionTemplate, statement) + return false + } + } catch (e) { + if (condition?.debug) { + console.info(`CCMS debug: condition - \`${conditionTemplate}\` => error`) } + console.error('表单项展示条件语句执行错误。', conditionTemplate) + return false } -} \ No newline at end of file +} diff --git a/src/util/enumeration.ts b/src/util/enumeration.ts index 0b3c41d09c39b7e18f7678360c86e0a0251de4a7..c73045f0f543b4005c709024ad7b1c42de177b65 100644 --- a/src/util/enumeration.ts +++ b/src/util/enumeration.ts @@ -1,7 +1,9 @@ -import { InterfaceConfig } from "./interface"; -import { getValue } from "./value"; +import { InterfaceConfig } from './interface' +import { getValue } from './value' +import { ParamConfig } from '../interface' +import ParamHelper from './param' -export type EnumerationOptionsConfig = ManualEnumerationOptionsConfig | InterfaceEnumerationOptionsConfig +export type EnumerationOptionsConfig = ManualEnumerationOptionsConfig | InterfaceEnumerationOptionsConfig | DataEnumerationOptionsConfig interface ManualEnumerationOptionsConfig { from: 'manual' @@ -18,11 +20,19 @@ interface InterfaceEnumerationOptionsConfig { format?: InterfaceEnumerationOptionsKVConfig | InterfaceEnumerationOptionsListConfig } -interface InterfaceEnumerationOptionsKVConfig { +interface DataEnumerationOptionsConfig { + from: 'data'; + sourceConfig?: ParamConfig; + format?: + | InterfaceEnumerationOptionsKVConfig + | InterfaceEnumerationOptionsListConfig; +} + +export interface InterfaceEnumerationOptionsKVConfig { type: 'kv' } -interface InterfaceEnumerationOptionsListConfig { +export interface InterfaceEnumerationOptionsListConfig { type: 'list' keyField: string labelField: string @@ -31,7 +41,18 @@ interface InterfaceEnumerationOptionsListConfig { export default class EnumerationHelper { static _instance: EnumerationHelper - public async options (config: EnumerationOptionsConfig, interfaceRequire: (config: InterfaceConfig, source: any) => Promise) { + optionsDataValue = (sourceConfig: ParamConfig, datas: { record?: object, data: object[], step: { [field: string]: any } }) => { + if (sourceConfig !== undefined) { + return ParamHelper(sourceConfig, datas) + } + return undefined + } + + public async options( + config: EnumerationOptionsConfig, + interfaceRequire: (config: InterfaceConfig, source: any) => Promise, + datas: { record?: object, data: object[], step: { [field: string]: any } } + ) { if (config) { if (config.from === 'manual') { if (config.data) { @@ -63,15 +84,41 @@ export default class EnumerationHelper { } } } + } else if (config.from === 'data') { + if (config.sourceConfig && config.sourceConfig.source) { + const data = ParamHelper(config.sourceConfig, datas) + if (config.format) { + if (config.format.type === 'kv') { + return Object.keys(data).map((key) => ({ + value: key, + label: data[key] + })) + } else if (config.format.type === 'list') { + if (Array.isArray(data)) { + return data.map((item: any) => { + return { + value: getValue(item, (config.format as InterfaceEnumerationOptionsListConfig).keyField), + label: getValue(item, (config.format as InterfaceEnumerationOptionsListConfig).labelField) + } + }) + } + } + } + } + return [] } } return [] } - static async options (config: EnumerationOptionsConfig, interfaceRequire: (config: InterfaceConfig, source: any) => Promise) { + static async options( + config: EnumerationOptionsConfig, + interfaceRequire: (config: InterfaceConfig, source: any) => Promise, + datas: { record?: object, data: object[], step: { [field: string]: any } } + ) { if (!EnumerationHelper._instance) { EnumerationHelper._instance = new EnumerationHelper() } - return await EnumerationHelper._instance.options(config, interfaceRequire) + return await EnumerationHelper._instance.options(config, interfaceRequire, datas) } -} \ No newline at end of file +} diff --git a/src/util/interface.ts b/src/util/interface.ts index 5e4783bec28d6ae8dca0560c7a3a7038a9e45f41..a394d17b194fa017e796f13f97b270c5050a5606 100644 --- a/src/util/interface.ts +++ b/src/util/interface.ts @@ -1,8 +1,11 @@ -import { isEqual, cloneDeep, template, get, set, merge } from "lodash" +// import { isEqual, cloneDeep, template, get, set, merge } from "lodash" +import { isEqual, template, get, merge } from "lodash" +import { set } from '../util/produce' import axios, { AxiosRequestConfig } from 'axios' -import { ParamConfig } from "../interface"; -import ParamHelper from "./param"; -import { getValue } from "./value"; +import { ParamConfig } from '../interface' +import ParamHelper from './param' +import { getValue } from './value' +import { Field } from '../components/formFields/common' export interface InterfaceConfig { domain?: string @@ -20,12 +23,12 @@ export interface InterfaceConfig { enable?: boolean, field?: string, value?: any, - success?: { type: 'none' } | - { type: 'modal', content?: { type: 'static', content?: string } | - { type: 'field', field?: string }}, - fail?: { type: 'none' } | + success?: { type: 'none' } | + { type: 'modal', content?: { type: 'static', content?: string } | + { type: 'field', field?: string }}, + fail?: { type: 'none' } | { type: 'modal', content?: {type: 'static', content?: string } | - {type: 'field', field?: string }} + {type: 'field', field?: string }} } response?: { @@ -96,11 +99,12 @@ export default class InterfaceHelper { public request ( config: InterfaceConfig, source: any, - datas: { record?: object, data: object[], step: number }, + datas: { record?: object, data: object[], step: { [field: string]: any }, extraContainerPath?: string }, option?: { loadDomain?: (domain: string) => Promise extra_data?: { params?: any, data?: any } - } + }, + _this?: Field ): Promise { return new Promise(async (resolve, reject) => { // 处理URL @@ -110,9 +114,9 @@ export default class InterfaceHelper { config.urlParams.forEach((param) => { if (param.field !== undefined && param.data !== undefined) { if (param.field === '') { - urlParams = ParamHelper(param.data, datas) + urlParams = ParamHelper(param.data, datas, _this) } else { - set(urlParams, param.field, ParamHelper(param.data, datas)) + urlParams = set(urlParams, param.field, ParamHelper(param.data, datas, _this)) } } }) @@ -143,9 +147,9 @@ export default class InterfaceHelper { config.params.forEach((param) => { if (param.field !== undefined && param.data !== undefined) { if (param.field === '') { - params = ParamHelper(param.data, datas) + params = ParamHelper(param.data, datas, _this) } else { - set(params, param.field, ParamHelper(param.data, datas)) + params = set(params, param.field, ParamHelper(param.data, datas, _this)) } } }) @@ -157,7 +161,7 @@ export default class InterfaceHelper { if (config.data) { config.data.forEach((param) => { if (param.field !== undefined && param.data !== undefined) { - (data as FormData).append(param.field, ParamHelper(param.data, datas)) + (data as FormData).append(param.field, ParamHelper(param.data, datas, _this)) } }) } @@ -171,9 +175,9 @@ export default class InterfaceHelper { config.data.forEach((param) => { if (param.field !== undefined && param.data !== undefined) { if (param.field === '') { - data = ParamHelper(param.data, datas) + data = ParamHelper(param.data, datas, _this) } else { - set(data, param.field, ParamHelper(param.data, datas)) + data = set(data, param.field, ParamHelper(param.data, datas, _this)) } } }) @@ -182,12 +186,12 @@ export default class InterfaceHelper { merge(data, option.extra_data.data) } } - + // 缓存判断 if (config.cache && config.cache.global && Object.keys(InterfaceHelper.cache).includes(config.cache.global)) { resolve(InterfaceHelper.cache[config.cache.global]) } else if ( - (!config.cache || !config.cache.disabled) && + (!config.cache || !config.cache.disabled) && isEqual(this._config, config) && isEqual(this._url, url) && isEqual(this._params, params) && @@ -195,11 +199,11 @@ export default class InterfaceHelper { ) { return this._response } else { - this._config = cloneDeep(config) + this._config = config this._url = url - this._params = cloneDeep(params) - this._data = cloneDeep(data) - + this._params = params + this._data = data + const request: AxiosRequestConfig = { url, method: config.method || 'GET', @@ -210,10 +214,10 @@ export default class InterfaceHelper { if (config.method === 'POST') { request.data = data } - + try { const response = await axios(request).then((response) => response.data) - + if (config.condition && config.condition.enable) { if (get(response, config.condition.field || '') === config.condition.value) { if (config.condition.success) { @@ -243,7 +247,7 @@ export default class InterfaceHelper { return } } - + if (config.response) { if (Array.isArray(config.response)) { let content = {} @@ -252,7 +256,7 @@ export default class InterfaceHelper { if (field === undefined || field === '') { content = value } else { - set(content, field, value) + content = set(content, field, value) } } this._response = content @@ -284,4 +288,4 @@ export default class InterfaceHelper { } }) } -} \ No newline at end of file +} diff --git a/src/util/operation.tsx b/src/util/operation.tsx index 3f9f3b907252ecd5b5a3e3dc3cf9e8599a8bc7cf..d7e7018d25be7bf04793b4702675996dd55000b3 100644 --- a/src/util/operation.tsx +++ b/src/util/operation.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import queryString from 'query-string'; -import { set } from "lodash"; -import { ParamConfig } from "../interface"; -import { CCMSConfig, CCMSProps } from "../main"; -import { getParam } from "./value"; +import React from 'react' +import queryString from 'query-string' +import { set } from '../util/produce' +import { ParamConfig } from '../interface' +import { CCMSConfig, CCMSProps, PageListItem } from '../main' +import { getParam } from './value' export type OperationConfig = CCMSOperationConfig @@ -50,15 +50,16 @@ interface CCMSInvisibleOperationConfig extends _CCMSOperationConfig { type CCMSOperationConfig = CCMSPopupOperationConfig | CCMSRedirectOperationConfig | CCMSWindowOperationConfig | CCMSInvisibleOperationConfig interface OperationHelperProps { - config?: OperationConfig, - datas: { record?: object, data: object[], step: number }, + config?: OperationConfig, + datas: { record?: object, data: object[], step: { [field: string]: any } }, checkPageAuth: (pageID: any) => Promise, loadPageURL: (pageID: any) => Promise, loadPageFrameURL: (pageID: any) => Promise, loadPageConfig: (pageID: any) => Promise, + loadPageList: () => Promise>, baseRoute: string, loadDomain: (domain: string) => Promise - handlePageRedirect?: (path: string) => void + handlePageRedirect?: (path: string, replaceHistory: boolean) => void children?: (handleOperation: () => void) => React.ReactNode callback?: (success: boolean) => void @@ -77,7 +78,7 @@ export default class OperationHelper extends React.Component 您当前使用的UI版本没有实现OpertionHelper组件。 @@ -96,20 +97,21 @@ export default class OperationHelper extends React.Component { if (config.type === 'ccms') { - const sourceData = {} + let sourceData = {} if (config.params === undefined) { for (const [field, param] of Object.entries(config.data || {})) { const value = getParam(param, datas) - set(sourceData, field, value) + sourceData = set(sourceData, field, value) } } else { - for (const {field, data} of config.params) { + for (const { field, data } of config.params) { const value = getParam(data, datas) - set(sourceData, field, value) + sourceData = set(sourceData, field, value) } } if (config.mode === 'popup' || config.mode === 'invisible') { @@ -122,7 +124,7 @@ export default class OperationHelper extends React.Component { @@ -180,6 +182,7 @@ export default class OperationHelper extends React.Component { @@ -201,4 +204,4 @@ export default class OperationHelper extends React.Component) { // 1.3.0新增 step由索引变为formValue switch (config.source) { case 'record': if (datas.record) { + if (_this) { + const fullPath = datas.extraContainerPath ? getChainPath(_this.props.containerPath, datas.extraContainerPath, config.field) : getChainPath(_this.props.containerPath, config.field) + _this.handleReportFields && _this.handleReportFields(getChainPath(fullPath)) + } if (config.field === '') { return datas.record } else { @@ -14,11 +20,12 @@ export default function ParamHelper ( config: ParamConfig, datas: { record?: obj } break case 'data': - if (datas.data[datas.step]) { + if (datas.step) { + _this && _this.handleReportFields && _this.handleReportFields(`${config.field}`) if (config.field === '') { - return datas.data[datas.step] + return datas.step } else { - return get(datas.data[datas.step], config.field) + return get(datas.step, config.field) } } break @@ -33,6 +40,7 @@ export default function ParamHelper ( config: ParamConfig, datas: { record?: obj break case 'step': if (datas.data[config.step]) { + _this && _this.handleReportFields && _this.handleReportFields(`${config.field}`) if (config.field === '') { return datas.data[config.step] } else { @@ -46,8 +54,10 @@ export default function ParamHelper ( config: ParamConfig, datas: { record?: obj } else { return get(qs.parse(window.location.search, { arrayFormat: 'bracket' }), config.field) } + break case 'static': return config.value + break } return undefined -} \ No newline at end of file +} diff --git a/src/util/produce.tsx b/src/util/produce.tsx new file mode 100644 index 0000000000000000000000000000000000000000..60b9ff1e6faf60b6359e0ea61d9867cbde1a70b4 --- /dev/null +++ b/src/util/produce.tsx @@ -0,0 +1,129 @@ +import produce, { setAutoFreeze } from 'immer' +import lodash from 'lodash' +import { listItemMove } from './value' + +/** + * setAutoFreeze + * 默认为true, 防止外部修改,维护数据不可变 + * 为false 可以修改数据源 + * 开发环境打开,生产环境关闭 + */ +setAutoFreeze(false) + +/** + * 对应loadsh 的set + * @param current + * @param path + * @param value + * @returns + */ +export function set(current: any, path?: string, value?: any) { + const target = produce(current, (draft: any) => { + if (path) { + if (arguments.length === 2) { + // 移除对象路径的属性 参数改动时同步修改这块 + lodash.unset(draft, path) + } else { + return lodash.set(draft, path, value) + } + } + return draft + }) + return target +} +/** + * current指定路径下的数组添加元素 + * @param current + * @param path + * @param value + * @returns + */ +export const push = (current: any, path = '', value?: any) => { + const target = produce(current, (draft: any) => { + const list = lodash.get(draft, path) + if (!Array.isArray(list)) { + // 如果指定路径下不是数组类型 + const tempArr: any[] = [] + tempArr.push(value) + lodash.set(draft, path, tempArr) + } else { + list.push(value) + } + }) + return target +} + +/** + * current指定路径下的数组删除元素 + * @param current + * @param path + * @param index + * @param count + * @returns + */ +export const splice = (current: any, path = '', index: number, count: number) => { + const target = produce(current, (draft: any) => { + const list = lodash.get(draft, path, []) + list.splice(index, count) + }) + return target +} + +/** + * current指定路径下数组排序 + * @param current + * @param path + * @param index + * @param sortType + * @returns + */ +export const sort = (current: any, path = '', index: number, sortType: 'up' | 'down') => { + const target = produce(current, (draft: any) => { + const list = lodash.get(draft, path, []) + listItemMove(list, index, sortType) + }) + return target +} + +/** + * lodash 递归合并来源对象的自身和继承的可枚举属性到目标对象 + * @param a 目标对象 + * @param b 来源对象 + * @returns + */ +const merge = (a: any, b: any): any => { + return lodash.assignInWith(a, b, (a, b) => { + if (lodash.isUndefined(a) && lodash.isArray(b)) { + a = [] + } + if (lodash.isObject(b)) { + if (lodash.isArray(a)) { + return merge(a, b).filter((i: any) => i !== undefined) + } + return merge(a, b) + } + }) +} + +export const setValue = (obj: any, path = '', value: any) => { + const target = produce(obj, (draft: any) => { + if (path === '') { + if (Object.prototype.toString.call(value) === '[object Object]') { + draft = merge(draft, value) + } else if (value !== undefined) { + draft = value + } + } else { + const source = lodash.get(draft, path) + if ( + Object.prototype.toString.call(value) === '[object Object]' && + Object.prototype.toString.call(source) === '[object Object]' + ) { + lodash.set(draft, path, merge(source, value)) + } else { + lodash.set(draft, path, value) + } + } + }) + return target +} diff --git a/src/util/statement.ts b/src/util/statement.ts index 81ea1e5820eaebf01fc0b0877b0a04948cb4f65e..b3e737b3b5ec382d481496ba8711201c87ac1655 100644 --- a/src/util/statement.ts +++ b/src/util/statement.ts @@ -1,13 +1,15 @@ -import { set, cloneDeep, template } from "lodash" -import { ParamConfig } from "../interface"; -import ParamHelper from "./param"; +import { template } from 'lodash' +import { set } from '../util/produce' +import { ParamConfig } from '../interface' +import ParamHelper from './param' +import { Field } from '../components/formFields/common' export interface StatementConfig { statement: string params: { field: string, data: ParamConfig }[] } -export default function StatementHelper(config: StatementConfig | undefined, datas: { record?: object, data: object[], step: number }): string { +export default function StatementHelper (config: StatementConfig | undefined, datas: { record?: object, data: object[], step: { [field: string]: any }, extraContainerPath?: string }, _this?: Field): string { if (config === undefined || config.statement === undefined || config.statement === '') { return '' } else { @@ -17,14 +19,14 @@ export default function StatementHelper(config: StatementConfig | undefined, dat config.params.forEach((param) => { if (param.field !== undefined && param.data !== undefined) { if (param.field === '') { - statementParams = ParamHelper(param.data, cloneDeep(datas)) + statementParams = ParamHelper(param.data, datas, _this) } else { - set(statementParams, param.field, ParamHelper(param.data, cloneDeep(datas))) + statementParams = set(statementParams, param.field, ParamHelper(param.data, datas, _this)) } } }) } - + try { const statement = statementTemplate(statementParams) return statement @@ -33,4 +35,4 @@ export default function StatementHelper(config: StatementConfig | undefined, dat return '' } } -} \ No newline at end of file +} diff --git a/src/util/value.ts b/src/util/value.ts index 35a8463c108798c0d5b2015bbddbd7714fa1833a..6cf1f158fa24065737235ce7d57985742bb8aa85 100644 --- a/src/util/value.ts +++ b/src/util/value.ts @@ -51,14 +51,14 @@ export const getParam = ( datas: { record?: object data: object[] - step: number + step: { [field: string]: any } } ) => { switch (config.source) { case 'record': return getValue(datas.record || {}, config.field) case 'data': - return getValue(datas.data[datas.step], config.field) + return getValue(datas.step, config.field) case 'source': return getValue(datas.data[0] || {}, config.field) case 'step': @@ -79,7 +79,7 @@ export const getParamText = ( datas: { record?: object data: object[] - step: number + step: { [field: string]: any } } ) => { for (const { field, data } of params) { @@ -114,10 +114,106 @@ export const listItemMove = (list: any[], currentIndex: number, sortType: 'up' | switch (sortType) { case 'up': currentIndex !== 0 && (list[currentIndex] = list.splice(currentIndex - 1, 1, list[currentIndex])[0]) - break; + break case 'down': currentIndex < list.length - 1 && (list[currentIndex] = list.splice(currentIndex + 1, 1, list[currentIndex])[0]) - break; + break } return list } + +// 参数转化为链式路径 +export const getChainPath = (...arg: any[]) => { + const _fullPath = arg.join('.') + const fullPath = _fullPath.replace(/(^\.*)|(\.*$)|(\.){2,}/g, '$3') + return fullPath +} + +/** + * @param source 来源字符串 + * @param find 目标字符串 + * @returns 返回目标字符串出现在来源字符串中所有索引 + */ +function indexes(source: string, find: string) { + const result: number[] = [] + for (let i = 0; i < source.length; ++i) { + if (source.substring(i, i + find.length) === find) { + result.push(i) + } + } + return result +} + +/** + * 获取一个数组中最长共同前缀 + *@param arr 入参数组的元素是二级及以上路径,为节省性能,该方法没有适配处理多个一级路径的共同前缀,一级路径没有共同前缀 + * + * eg: 根据项目使用场景,如['81.mode', '81.me.1', '81.my.1', '81.m']的共同前缀为81,不可以是81.m + */ +export const getLongestCommonPrefix = (arr: string[]) => { + if (arr.length === 0 || arr[0].length === 0) { return '' } + for (let i = 0, len1 = arr[0].length; i < len1; i++) { + const c = arr[0].charAt(i) + for (let j = 1, len2 = arr.length; j < len2; j++) { + if (i === arr[j].length || arr[j].charAt(i) !== c || (i === len1 - 1 && arr[j].length > len1 && arr[j].charAt(len1) !== '.')) { + const _indexes = indexes(arr[0], '.') + const res = arr[0].substring(0, i).replace(/\.+$/, '') // 去掉尾部'.' + for (let n = 0; n < _indexes.length; n++) { + if (res.length === _indexes[n]) { + return res + } + } + return res.replace(/\.+[^\\.]+$/, '') + } + } + } + return arr[0] +} + +/** + * @param arr 目标数组 + * @param sourceField 来源字段 + * @returns 与来源字段比较共同前缀后更新的数组 | 是否更新并上报 + * + * eg: ['a.0', 'b']不会插入'a.0.c',会插入'a'替换'a.0',原数组改变为['a', 'b'] + */ +export const updateCommonPrefixItem = (arr: string[], sourceField: string): string[] | boolean => { + const reg = /[^\\.]+(?=\.?)/ + const sourceFieldPrefix = sourceField.match(reg)?.[0] + const commonPrefixItemS = [sourceField] + for (let i = arr.length - 1; i >= 0; i--) { + const arrItem = arr[i] + if (sourceField === arrItem) { + return false + } + const arrItemPrefix = arrItem.match(reg)?.[0] + if (arrItemPrefix && arrItemPrefix === sourceFieldPrefix) { + arr.splice(i, 1) + commonPrefixItemS.push(arrItem) + } + } + + arr.push(getLongestCommonPrefix(commonPrefixItemS)) + return arr +} +/** + * 转化value数组中的值类型 + * @param list value数组 + * @param type 值类型 + * @returns value数组 + */ +export const transformValueType = (list: any[], type: 'string' | 'number' | 'boolean' | undefined) => { + switch (type) { + case 'string': + return list.map(v => String(v)) + + case 'number': + return list.map(v => +v) + + case 'boolean': + return list.map(v => Boolean(v)) + + default: + return list + } +} diff --git a/tsconfig.json b/tsconfig.json index 95deb7c62079b44b4486d3f93e7bcde5b9509741..823189508981ca46123d85443ef09047ac7fb540 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,26 @@ { "compilerOptions": { - "target": "es6", - "module": "esnext", - "jsx": "react", + "target": "ES6", + "module": "ESNext", + "allowJs": true, "sourceMap": true, "outDir": "./lib/", - "strict": true, + "moduleResolution": "node", "esModuleInterop": true, - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, + "declaration": true, + "jsx": "react", + "strict": true, + "lib": ["dom", "dom.iterable", "esnext"], "skipLibCheck": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "noImplicitAny": true, + "noImplicitAny": false, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, - "types": [ - "node", - "jest" - ], + "types": ["node", "jest"], "experimentalDecorators": true } -} \ No newline at end of file +}