diff --git a/examples/components/Page/Form.jsx b/examples/components/Page/Form.jsx index 2e1041ff..c146ccf4 100644 --- a/examples/components/Page/Form.jsx +++ b/examples/components/Page/Form.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ConditionBuilder from '../../../src/components/condition-builder/ConditionBuilder'; +import ConditionBuilder from '../../../src/components/condition-builder'; const fields = [ { @@ -25,19 +25,38 @@ const fields = [ children: [ { label: '姓名', - name: 'name', + name: 'name2', type: 'text' }, { label: '年龄', - name: 'age', + name: 'age2', type: 'number' } ] } ]; +const funcs = [ + { + label: '文本', + children: [ + { + type: 'LOWERCASE', + label: '转小写', + returnType: 'text', + args: [ + { + type: 'text', + label: '文本' + } + ] + } + ] + } +]; + export default { type: 'page', title: '表单页面', @@ -68,7 +87,12 @@ export default { { name: 'a', component: ({value, onChange}) => ( - + ) } ] diff --git a/scss/_mixins.scss b/scss/_mixins.scss index 957da7bb..50420898 100644 --- a/scss/_mixins.scss +++ b/scss/_mixins.scss @@ -310,6 +310,7 @@ padding: $Form-input-paddingY $Form-input-paddingX; font-size: $Form-input-fontSize; flex-wrap: wrap; + justify-content: space-between; input { flex-basis: px2rem(80px); @@ -404,3 +405,11 @@ } } } + +@mixin icon-color { + color: $icon-color; + + &:hover { + color: $icon-onHover-color; + } +} diff --git a/scss/components/_condition-builder.scss b/scss/components/_condition-builder.scss index 7faebb45..22ef45ad 100644 --- a/scss/components/_condition-builder.scss +++ b/scss/components/_condition-builder.scss @@ -4,4 +4,107 @@ flex-direction: row; justify-content: space-between; } + + &-field { + position: relative; + display: inline-block; + min-width: px2rem(120px); + } + + &-fieldCaret { + transition: transform 0.3s ease-out; + margin: 0 $gap-xs; + display: flex; + color: $icon-color; + &:hover { + color: $icon-onHover-color; + } + + > svg { + width: px2rem(12px); + height: px2rem(12px); + top: 0; + } + } + + &-fieldInput.is-active &-fieldCaret { + transform: rotate(180deg); + } +} + +.#{$ns}CBItem { + display: flex; + margin-top: px2rem(10px); + padding: 5px 10px; + border-radius: 5px; + flex-direction: row; + align-items: center; + margin-left: px2rem(30px); + position: relative; + background: rgba(0, 0, 0, 0.03); + transition: all 0.3s ease-out; + + &-dragbar { + cursor: move; + width: 20px; + margin-left: -5px; + opacity: 0; + text-align: center; + transition: opacity 0.3s ease-out; + @include icon-color(); + } + + &:hover { + background-color: rgba(0, 0, 0, 0.05); + } + + &:hover &-dragbar { + opacity: 1; + } + + &:before { + position: absolute; + content: ''; + top: -10px; + left: -30px; + width: 20px; + border-left: solid 1px $borderColor; + bottom: 0; + } + + &:after { + position: absolute; + content: ''; + top: 50%; + width: 20px; + left: -30px; + border-top: solid 1px $borderColor; + } + + &:last-child { + &:before { + border-bottom-left-radius: 5px; + border-bottom: solid 1px $borderColor; + bottom: 50%; + } + + &:after { + display: none; + } + } +} + +.#{$ns}CBInputSwitch { + position: relative; + display: inline-block; + margin: 0 5px; + cursor: pointer; + > a { + @include icon-color(); + } + + svg { + width: px2rem(10px); + height: px2rem(10px); + } } diff --git a/scss/components/_result-box.scss b/scss/components/_result-box.scss index b757d735..3ec2e0e5 100644 --- a/scss/components/_result-box.scss +++ b/scss/components/_result-box.scss @@ -11,6 +11,7 @@ } &.is-focused, + &.is-active, &:focus { outline: none; border-color: $Form-input-onFocused-borderColor; diff --git a/src/components/ListRadios.tsx b/src/components/ListRadios.tsx index fcc802dd..d789d609 100644 --- a/src/components/ListRadios.tsx +++ b/src/components/ListRadios.tsx @@ -21,6 +21,7 @@ export interface BaseRadiosProps extends ThemeProps, LocaleProps { disabled?: boolean; clearable?: boolean; showRadio?: boolean; + onClick?: (e: React.MouseEvent) => void; } export class BaseRadios< @@ -119,7 +120,8 @@ export class BaseRadios< className, placeholder, classnames: cx, - option2value + option2value, + onClick } = this.props; const __ = this.props.translate; @@ -131,7 +133,7 @@ export class BaseRadios< } return ( -
+
{body && body.length ? ( body ) : ( diff --git a/src/components/PopOverContainer.tsx b/src/components/PopOverContainer.tsx index e0540dc5..b7a926c9 100644 --- a/src/components/PopOverContainer.tsx +++ b/src/components/PopOverContainer.tsx @@ -50,7 +50,7 @@ export class PopOverContainer extends React.Component< @autobind getTarget() { - return findDOMNode(this.target || this) as HTMLElement; + return this.target || (findDOMNode(this) as HTMLElement); } @autobind @@ -82,7 +82,11 @@ export class PopOverContainer extends React.Component< {dropdownRender({onClose: this.close})} diff --git a/src/components/condition-builder/ConditionItem.tsx b/src/components/condition-builder/ConditionItem.tsx deleted file mode 100644 index eef8bf6f..00000000 --- a/src/components/condition-builder/ConditionItem.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import {Fields, ConditionRule, ConditionGroupValue} from './types'; -import {ClassNamesFn} from '../../theme'; -import {Icon} from '../icons'; -import Select from '../Select'; -import {autobind} from '../../utils/helper'; -import PopOverContainer from '../PopOverContainer'; -import InputBox from '../InputBox'; -import ListRadios from '../ListRadios'; -import ResultBox from '../ResultBox'; - -export interface ConditionItemProps { - fields: Fields; - value: ConditionRule; - classnames: ClassNamesFn; - onChange: (value: ConditionRule) => void; -} - -export class ConditionItem extends React.Component { - @autobind - handleLeftSelect() {} - - renderLeft() { - const {value, fields} = this.props; - - return ( - ( - - )} - > - {({onClick, ref}) => ( - - )} - - ); - } - - renderItem() { - return null; - } - - render() { - const {classnames: cx} = this.props; - - return ( -
- - - - -
- {this.renderLeft()} - {this.renderItem()} -
-
- ); - } -} diff --git a/src/components/condition-builder/Expression.tsx b/src/components/condition-builder/Expression.tsx new file mode 100644 index 00000000..3865d206 --- /dev/null +++ b/src/components/condition-builder/Expression.tsx @@ -0,0 +1,21 @@ +import {ExpressionComplex, Field, Funcs} from './types'; +import React from 'react'; + +/** + * 支持4中表达式设置方式 + * 1. 直接就是值,由用户直接填写。 + * 2. 选择字段,让用户选一个字段。 + * 3. 选择一个函数,然后会参数里面的输入情况是个递归。 + * 4. 粗暴点,函数让用户自己书写。 + */ + +export interface ExpressionProps { + value: ExpressionComplex; + onChange: (value: ExpressionComplex) => void; + valueField?: Field; + fields?: Field[]; + funcs?: Funcs; + allowedTypes?: Array<'value' | 'field' | 'func' | 'raw'>; +} + +export class Expression extends React.Component {} diff --git a/src/components/condition-builder/Field.tsx b/src/components/condition-builder/Field.tsx new file mode 100644 index 00000000..3841f90c --- /dev/null +++ b/src/components/condition-builder/Field.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PopOverContainer from '../PopOverContainer'; +import ListRadios from '../ListRadios'; +import ResultBox from '../ResultBox'; +import {ClassNamesFn} from '../../theme'; +import {Icon} from '../icons'; +import {find} from 'lodash'; +import {findTree, noop} from '../../utils/helper'; + +export interface ConditionFieldProps { + options: Array; + value: any; + onChange: (value: any) => void; + classnames: ClassNamesFn; +} + +const option2value = (item: any) => item.name; + +export default function ConditionField({ + options, + onChange, + value, + classnames: cx +}: ConditionFieldProps) { + return ( + ( + + )} + > + {({onClick, ref, isOpened}) => ( +
+ item.name === value)?.label : '' + } + onResultChange={noop} + onResultClick={onClick} + placeholder="请选择字段" + > + + + + +
+ )} +
+ ); +} diff --git a/src/components/condition-builder/ConditionGroup.tsx b/src/components/condition-builder/Group.tsx similarity index 60% rename from src/components/condition-builder/ConditionGroup.tsx rename to src/components/condition-builder/Group.tsx index 9f0e2fcb..394b3eef 100644 --- a/src/components/condition-builder/ConditionGroup.tsx +++ b/src/components/condition-builder/Group.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import {Fields, ConditionGroupValue} from './types'; +import {Fields, ConditionGroupValue, Funcs} from './types'; import {ClassNamesFn} from '../../theme'; import Button from '../Button'; -import {ConditionItem} from './ConditionItem'; +import {ConditionItem} from './Item'; import {autobind, guid} from '../../utils/helper'; export interface ConditionGroupProps { value?: ConditionGroupValue; fields: Fields; - onChange: (value: ConditionGroupValue) => void; + funcs?: Funcs; + index?: number; + onChange: (value: ConditionGroupValue, index?: number) => void; classnames: ClassNamesFn; removeable?: boolean; } @@ -27,7 +29,7 @@ export class ConditionGroup extends React.Component { let value = this.getValue(); value.not = !value.not; - onChange(value); + onChange(value, this.props.index); } @autobind @@ -35,7 +37,7 @@ export class ConditionGroup extends React.Component { const onChange = this.props.onChange; let value = this.getValue(); value.conjunction = value.conjunction === 'and' ? 'or' : 'and'; - onChange(value); + onChange(value, this.props.index); } @autobind @@ -50,7 +52,7 @@ export class ConditionGroup extends React.Component { value.children.push({ id: guid() }); - onChange(value); + onChange(value, this.props.index); } @autobind @@ -66,53 +68,70 @@ export class ConditionGroup extends React.Component { id: guid(), conjunction: 'and' }); - onChange(value); + onChange(value, this.props.index); + } + + @autobind + handleItemChange(item: any, index?: number) { + const onChange = this.props.onChange; + let value = this.getValue(); + + value.children = Array.isArray(value.children) + ? value.children.concat() + : []; + + value.children.splice(index!, 1, item); + onChange(value, this.props.index); } render() { - const {classnames: cx, value, fields, onChange} = this.props; + const {classnames: cx, value, fields, funcs} = this.props; return (
- - +
+ + +
-
{Array.isArray(value?.children) - ? value!.children.map(item => + ? value!.children.map((item, index) => (item as ConditionGroupValue).conjunction ? ( ) : ( { fields={fields} value={item} classnames={cx} - onChange={onChange} + index={index} + onChange={this.handleItemChange} + funcs={funcs} /> ) ) diff --git a/src/components/condition-builder/InputSwitch.tsx b/src/components/condition-builder/InputSwitch.tsx new file mode 100644 index 00000000..81e30050 --- /dev/null +++ b/src/components/condition-builder/InputSwitch.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PopOverContainer from '../PopOverContainer'; +import {Icon} from '../icons'; +import ListRadios from '../ListRadios'; +import {ClassNamesFn} from '../../theme'; + +export interface InputSwitchProps { + options: Array; + value: any; + onChange: (value: any) => void; + classnames: ClassNamesFn; +} + +const option2value = (item: any) => item.value; + +export default function InputSwitch({ + options, + value, + onChange, + classnames: cx +}: InputSwitchProps) { + return ( + ( + + )} + > + {({onClick, isOpened, ref}) => ( +
+ + + +
+ )} +
+ ); +} diff --git a/src/components/condition-builder/Item.tsx b/src/components/condition-builder/Item.tsx new file mode 100644 index 00000000..9238a2a6 --- /dev/null +++ b/src/components/condition-builder/Item.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import {Fields, ConditionRule, ConditionGroupValue, Funcs} from './types'; +import {ClassNamesFn} from '../../theme'; +import {Icon} from '../icons'; +import Select from '../Select'; +import {autobind} from '../../utils/helper'; +import PopOverContainer from '../PopOverContainer'; +import InputBox from '../InputBox'; +import ListRadios from '../ListRadios'; +import ResultBox from '../ResultBox'; +import ConditionField from './Field'; +import InputSwitch from './InputSwitch'; + +export interface ConditionItemProps { + fields: Fields; + funcs?: Funcs; + index?: number; + value: ConditionRule; + classnames: ClassNamesFn; + onChange: (value: ConditionRule, index?: number) => void; +} + +const leftInputOptions = [ + { + label: '字段', + value: 'field' + }, + { + label: '函数', + value: 'func' + } +]; + +export class ConditionItem extends React.Component { + @autobind + handleLeftFieldSelect(field: any) { + const value = {...this.props.value}; + const onChange = this.props.onChange; + value.left = field; + onChange(value, this.props.index); + } + + @autobind + handleLeftInputTypeChange(type: 'func' | 'field') { + const value = {...this.props.value}; + const onChange = this.props.onChange; + + if (type === 'func') { + value.left = {type: 'func'}; + } else { + value.left = ''; + } + + onChange(value, this.props.index); + } + + renderLeft() { + const {value, fields, classnames: cx, funcs} = this.props; + const inputType = + value.left && (value.left as any).type === 'func' ? 'func' : 'field'; + + return ( + <> + {Array.isArray(funcs) ? ( + + ) : null} + + {inputType === 'field' ? ( + + ) : null} + + ); + } + + renderItem() { + return null; + } + + render() { + const {classnames: cx} = this.props; + + return ( +
+ + + + +
+ {this.renderLeft()} + {this.renderItem()} +
+
+ ); + } +} diff --git a/src/components/condition-builder/ConditionBuilder.tsx b/src/components/condition-builder/index.tsx similarity index 77% rename from src/components/condition-builder/ConditionBuilder.tsx rename to src/components/condition-builder/index.tsx index 9489db8a..da39918e 100644 --- a/src/components/condition-builder/ConditionBuilder.tsx +++ b/src/components/condition-builder/index.tsx @@ -2,21 +2,23 @@ import React from 'react'; import {ThemeProps, themeable} from '../../theme'; import {LocaleProps, localeable} from '../../locale'; import {uncontrollable} from 'uncontrollable'; -import {Fields, ConditionGroupValue} from './types'; -import {ConditionGroup} from './ConditionGroup'; +import {Fields, ConditionGroupValue, Funcs} from './types'; +import {ConditionGroup} from './Group'; export interface QueryBuilderProps extends ThemeProps, LocaleProps { fields: Fields; + funcs?: Funcs; value?: ConditionGroupValue; onChange: (value: ConditionGroupValue) => void; } export class QueryBuilder extends React.Component { render() { - const {classnames: cx, fields, onChange, value} = this.props; + const {classnames: cx, fields, funcs, onChange, value} = this.props; return ( ; }; -export type ConditionRightValueLiteral = string | number | object | undefined; -export type ConditionRightValue = - | ConditionRightValueLiteral +export type ExpressionSimple = string | number | object | undefined; +export type ExpressionComplex = + | ExpressionSimple | { - type: 'raw'; - value: ConditionRightValueLiteral; + type: 'value'; + value: ExpressionSimple; } | { type: 'func'; func: string; - args: Array; + args: Array; } | { type: 'field'; field: string; } | { - type: 'expression'; + type: 'raw'; field: string; }; export interface ConditionRule { id: any; - left?: string; + left?: ExpressionComplex; op?: OperatorType; - right?: ConditionRightValue | Array; + right?: ExpressionComplex | Array; } export interface ConditionGroupValue { @@ -58,7 +58,7 @@ export interface ConditionValue extends ConditionGroupValue {} interface BaseField { type: FieldTypes; label: string; - valueTypes?: Array<'raw' | 'field' | 'func' | 'expression'>; + valueTypes?: Array<'value' | 'field' | 'func' | 'expression'>; // valueTypes 里面配置 func 才有效。 funcs?: Array; @@ -132,7 +132,7 @@ type FieldSimple = | SelectField | BooleanField; -type Field = FieldSimple | FieldGroup | GroupField; +export type Field = FieldSimple | FieldGroup | GroupField; interface FuncGroup { label: string; diff --git a/src/components/icons.tsx b/src/components/icons.tsx index f783dfc7..dbc0659a 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -128,6 +128,9 @@ import SortAscIcon from '../icons/sort-asc.svg'; // @ts-ignore import SortDescIcon from '../icons/sort-desc.svg'; +// @ts-ignore +import SettingIcon from '../icons/setting.svg'; + // 兼容原来的用法,后续不直接试用。 // @ts-ignore export const closeIcon = ; @@ -213,6 +216,7 @@ registerIcon('folder', FolderIcon); registerIcon('sort-default', SortDefaultIcon); registerIcon('sort-asc', SortAscIcon); registerIcon('sort-desc', SortDescIcon); +registerIcon('setting', SettingIcon); export function Icon({ icon, diff --git a/src/icons/setting.svg b/src/icons/setting.svg new file mode 100644 index 00000000..61dba6a0 --- /dev/null +++ b/src/icons/setting.svg @@ -0,0 +1,9 @@ + + + + + + + + +