条件组合半成品

This commit is contained in:
2betop 2020-08-14 17:45:59 +08:00
parent 0f0f50358f
commit 0ba867b2fc
16 changed files with 457 additions and 113 deletions

View File

@ -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}) => (
<ConditionBuilder value={value} onChange={onChange} fields={fields} />
<ConditionBuilder
value={value}
onChange={onChange}
fields={fields}
funcs={funcs}
/>
)
}
]

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -11,6 +11,7 @@
}
&.is-focused,
&.is-active,
&:focus {
outline: none;
border-color: $Form-input-onFocused-borderColor;

View File

@ -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 (
<div className={cx('ListRadios', className)}>
<div className={cx('ListRadios', className)} onClick={onClick}>
{body && body.length ? (
body
) : (

View File

@ -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<
<PopOver
overlay
className={popOverClassName}
style={{minWidth: this.target ? this.target.offsetWidth : 'auto'}}
style={{
minWidth: this.target
? Math.max(this.target.offsetWidth, 100)
: 'auto'
}}
onHide={this.close}
>
{dropdownRender({onClose: this.close})}

View File

@ -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<ConditionItemProps> {
@autobind
handleLeftSelect() {}
renderLeft() {
const {value, fields} = this.props;
return (
<PopOverContainer
popOverRender={({onClose}) => (
<ListRadios showRadio={false} options={fields} onChange={onClose} />
)}
>
{({onClick, ref}) => (
<ResultBox
ref={ref}
allowInput={false}
onResultClick={onClick}
placeholder="请选择"
/>
)}
</PopOverContainer>
);
}
renderItem() {
return null;
}
render() {
const {classnames: cx} = this.props;
return (
<div className={cx('CBGroup-item')}>
<a>
<Icon icon="drag-bar" className="icon" />
</a>
<div className={cx('CBGroup-itemBody')}>
{this.renderLeft()}
{this.renderItem()}
</div>
</div>
);
}
}

View File

@ -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<ExpressionProps> {}

View File

@ -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<any>;
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 (
<PopOverContainer
popOverRender={({onClose}) => (
<ListRadios
onClick={onClose}
showRadio={false}
options={options}
value={value}
option2value={option2value}
onChange={onChange}
/>
)}
>
{({onClick, ref, isOpened}) => (
<div className={cx('CBGroup-field')}>
<ResultBox
className={cx('CBGroup-fieldInput', isOpened ? 'is-active' : '')}
ref={ref}
allowInput={false}
result={
value ? findTree(options, item => item.name === value)?.label : ''
}
onResultChange={noop}
onResultClick={onClick}
placeholder="请选择字段"
>
<span className={cx('CBGroup-fieldCaret')}>
<Icon icon="caret" className="icon" />
</span>
</ResultBox>
</div>
)}
</PopOverContainer>
);
}

View File

@ -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<ConditionGroupProps> {
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<ConditionGroupProps> {
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<ConditionGroupProps> {
value.children.push({
id: guid()
});
onChange(value);
onChange(value, this.props.index);
}
@autobind
@ -66,53 +68,70 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
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 (
<div className={cx('CBGroup')}>
<div className={cx('CBGroup-toolbar')}>
<div className={cx('CBGroup-toolbarLeft')}>
<Button onClick={this.handleNotClick} size="sm" active={value?.not}>
</Button>
<Button
size="sm"
onClick={this.handleConjunctionClick}
active={value?.conjunction !== 'or'}
>
</Button>
<Button
size="sm"
onClick={this.handleConjunctionClick}
active={value?.conjunction === 'or'}
>
</Button>
<div className={cx('ButtonGroup m-l-xs')}>
<Button
size="sm"
onClick={this.handleConjunctionClick}
active={value?.conjunction !== 'or'}
>
</Button>
<Button
size="sm"
onClick={this.handleConjunctionClick}
active={value?.conjunction === 'or'}
>
</Button>
</div>
</div>
<div className={cx('CBGroup-toolbarRight')}>
<Button onClick={this.handleAdd} size="sm">
</Button>
<Button onClick={this.handleAddGroup} size="sm">
<Button onClick={this.handleAddGroup} size="sm" className="m-l-xs">
</Button>
</div>
</div>
{Array.isArray(value?.children)
? value!.children.map(item =>
? value!.children.map((item, index) =>
(item as ConditionGroupValue).conjunction ? (
<ConditionGroup
key={item.id}
fields={fields}
value={item as ConditionGroupValue}
classnames={cx}
onChange={onChange}
index={index}
onChange={this.handleItemChange}
funcs={funcs}
/>
) : (
<ConditionItem
@ -120,7 +139,9 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
fields={fields}
value={item}
classnames={cx}
onChange={onChange}
index={index}
onChange={this.handleItemChange}
funcs={funcs}
/>
)
)

View File

@ -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<any>;
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 (
<PopOverContainer
popOverRender={({onClose}) => (
<ListRadios
onClick={onClose}
option2value={option2value}
onChange={onChange}
options={options}
value={value}
showRadio={false}
/>
)}
>
{({onClick, isOpened, ref}) => (
<div className={cx('CBInputSwitch', isOpened ? 'is-active' : '')}>
<a onClick={onClick} ref={ref}>
<Icon icon="setting" />
</a>
</div>
)}
</PopOverContainer>
);
}

View File

@ -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<ConditionItemProps> {
@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) ? (
<InputSwitch
classnames={cx}
onChange={this.handleLeftInputTypeChange}
options={leftInputOptions}
value={inputType}
/>
) : null}
{inputType === 'field' ? (
<ConditionField
classnames={cx}
options={fields}
value={value.left}
onChange={this.handleLeftFieldSelect}
/>
) : null}
</>
);
}
renderItem() {
return null;
}
render() {
const {classnames: cx} = this.props;
return (
<div className={cx('CBItem')}>
<a className={cx('CBItem-dragbar')}>
<Icon icon="drag-bar" className="icon" />
</a>
<div className={cx('CBItem-itemBody')}>
{this.renderLeft()}
{this.renderItem()}
</div>
</div>
);
}
}

View File

@ -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<QueryBuilderProps> {
render() {
const {classnames: cx, fields, onChange, value} = this.props;
const {classnames: cx, fields, funcs, onChange, value} = this.props;
return (
<ConditionGroup
funcs={funcs}
fields={fields}
value={value}
onChange={onChange}

View File

@ -18,32 +18,32 @@ export type FieldItem = {
operators: Array<OperatorType>;
};
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<ConditionRightValue>;
args: Array<ExpressionComplex>;
}
| {
type: 'field';
field: string;
}
| {
type: 'expression';
type: 'raw';
field: string;
};
export interface ConditionRule {
id: any;
left?: string;
left?: ExpressionComplex;
op?: OperatorType;
right?: ConditionRightValue | Array<ConditionRightValue>;
right?: ExpressionComplex | Array<ExpressionComplex>;
}
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<string>;
@ -132,7 +132,7 @@ type FieldSimple =
| SelectField
| BooleanField;
type Field = FieldSimple | FieldGroup | GroupField;
export type Field = FieldSimple | FieldGroup | GroupField;
interface FuncGroup {
label: string;

View File

@ -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 = <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,

9
src/icons/setting.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 196 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(0.343750, 0.000000)" fill="currentColor" fill-rule="nonzero">
<path d="M170.546281,170.220341 C167.664808,169.153129 165.637105,168.512801 163.716123,167.765753 C157.846456,165.524607 152.190232,162.963298 146.213844,160.935595 C144.506304,160.401989 142.051716,160.615431 140.450898,161.469201 C136.28877,163.496904 132.233364,165.844771 128.3914,168.512802 C127.110746,169.366571 126.043534,171.287553 125.72337,172.888371 C124.442715,179.931972 123.482224,187.082293 122.628455,194.232615 C122.094849,198.288021 120.173867,200.102282 116.011739,199.995561 C103.738799,199.888839 91.4658594,199.888839 79.1929195,199.995561 C75.1375133,199.995561 73.0030889,198.394742 72.4694828,194.339336 C71.5089918,187.189015 70.3350584,180.145414 69.4812887,172.995093 C69.161125,170.540505 68.3073553,169.046408 65.9594885,167.872474 C62.0108035,165.951493 58.4890033,163.283462 54.5403184,161.36248 C53.1529426,160.615432 51.0185182,160.295268 49.5244211,160.828874 C42.587542,163.283462 35.8641053,166.058214 29.0339475,168.726244 C24.1247715,170.647226 22.7373957,170.220341 19.9626439,165.524608 C13.8795346,155.17265 7.90314648,144.820692 1.9267582,134.468734 C-0.847993359,129.666279 -0.741272266,128.492346 3.63429766,125.077267 C9.39724336,120.488254 15.2669104,116.112685 20.9231348,111.416951 C21.9903469,110.563181 22.7373955,108.642199 22.7373955,107.254823 C22.9508379,103.306138 22.0970682,99.1440109 22.7373955,95.3020471 C23.4844439,91.1399195 21.7769045,89.0054953 18.7887105,86.7643498 C13.4526496,82.9223859 8.54347363,78.6535373 3.31413418,74.8115734 C0.219218945,72.4637066 -0.527829688,70.0091187 1.49987344,66.5940396 C8.00986758,55.6017543 14.4131406,44.5027479 20.8164137,33.4037412 C22.6306744,30.308826 24.9785412,29.6684986 28.2868988,30.9491533 C35.3304992,33.7239049 42.3740994,36.6053777 49.5244209,39.1666871 C51.018518,39.7002932 53.3663848,39.3801295 54.8604818,38.6330811 C58.9158881,36.6053779 62.5444094,33.9373475 66.5998156,31.9096443 C68.73424,30.8424322 69.4812885,29.5617775 69.6947309,27.4273533 C70.6552219,20.1703105 71.8291553,12.9132678 72.7896461,5.656225 C73.1098098,1.49409707 75.3509555,0 79.2996404,0 C91.5725803,0.106721289 103.84552,0.106721289 116.11846,0 C119.960424,0 122.094848,1.60081816 122.521733,5.44278203 C123.482224,12.5931035 124.656158,19.6367039 125.509927,26.7870254 C125.830091,29.4550559 126.790582,31.055874 129.351891,32.2298074 C133.087134,33.9373469 136.608934,36.0717713 140.024013,38.4196381 C142.265158,40.0204563 144.18614,40.0204563 146.640728,38.9532441 C153.257444,36.1784926 159.98088,33.7239045 166.704317,30.9491529 C170.43956,29.4550559 173.000869,30.2021045 175.028572,33.8306258 C181.111681,44.6094688 187.514954,55.2815904 193.704785,65.9537121 C196.159373,70.2225607 195.839209,71.6099365 191.890524,74.704852 C186.2343,79.0804219 180.578075,83.5627129 174.815129,87.9382828 C172.894148,89.4323799 172.360542,90.8197557 172.573984,93.2743437 C173.000869,97.7566348 172.467263,102.345647 172.680705,106.827938 C172.787427,108.428756 173.534475,110.349738 174.708408,111.310229 C180.364633,116.005963 186.2343,120.381533 191.997246,124.863824 C195.94593,127.958739 196.159373,129.346115 193.704785,133.614964 C187.408233,144.500528 181.111681,155.279371 174.708408,166.058214 C173.641196,167.979195 171.826935,169.046407 170.546281,170.220341 Z M97.335526,135.216323 C116.652066,135.322503 132.660249,119.634484 132.767515,100.531386 C132.873691,80.8946824 117.292394,64.9932211 97.869132,64.8859622 C78.4458705,64.7797785 62.4371504,80.5745187 62.4371504,99.9977801 C62.330967,119.207599 78.125707,135.109061 97.335526,135.216323 Z">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB