半成品

This commit is contained in:
liaoxuezhi 2020-08-17 00:04:58 +08:00
parent 031f748bf2
commit 8901b57389
11 changed files with 489 additions and 72 deletions

View File

@ -1,17 +1,22 @@
.#{$ns}CBGroup {
font-size: $fontSizeSm;
&-toolbar {
display: flex;
flex-direction: row;
justify-content: space-between;
}
&-field {
&-field,
&-operator {
position: relative;
display: inline-block;
min-width: px2rem(120px);
margin-left: $gap-xs;
}
&-fieldCaret {
&-fieldCaret,
&-operatorCaret {
transition: transform 0.3s ease-out;
margin: 0 $gap-xs;
display: flex;
@ -27,7 +32,8 @@
}
}
&-fieldInput.is-active &-fieldCaret {
&-fieldInput.is-active &-fieldCaret,
&-operatorInput.is-active &-operatorCaret {
transform: rotate(180deg);
}
}
@ -97,7 +103,7 @@
.#{$ns}CBInputSwitch {
position: relative;
display: inline-block;
margin: 0 5px;
margin-left: 5px;
cursor: pointer;
> a {
@include icon-color();
@ -108,3 +114,29 @@
height: px2rem(10px);
}
}
.#{$ns}CBFunc {
display: inline-block;
&-select {
display: inline-block;
position: relative;
}
&-error {
color: $danger;
}
&-args {
display: inline-block;
> span {
display: inline-block;
padding: 0 5px;
color: $info;
}
> div {
display: inline-block;
}
}
}

View File

@ -1,21 +1,156 @@
import {ExpressionComplex, Field, Funcs} from './types';
import {ExpressionComplex, Field, Funcs, Func, ExpressionFunc} from './types';
import React from 'react';
import ConditionField from './Field';
import {autobind, findTree} from '../../utils/helper';
import Value from './Value';
import InputSwitch from './InputSwitch';
import ConditionFunc from './Func';
import {ThemeProps, themeable} from '../../theme';
/**
* 4
*
* 1.
* 2.
* 3.
* 4.
*/
export interface ExpressionProps {
export interface ExpressionProps extends ThemeProps {
value: ExpressionComplex;
onChange: (value: ExpressionComplex) => void;
index?: number;
onChange: (value: ExpressionComplex, index?: number) => void;
valueField?: Field;
fields?: Field[];
funcs?: Funcs;
defaultType?: 'value' | 'field' | 'func' | 'raw';
allowedTypes?: Array<'value' | 'field' | 'func' | 'raw'>;
}
export class Expression extends React.Component<ExpressionProps> {}
const fieldMap = {
value: '值',
field: '字段',
func: '函数',
raw: '公式'
};
export class Expression extends React.Component<ExpressionProps> {
@autobind
handleInputTypeChange(type: 'value' | 'field' | 'func' | 'raw') {
let value = this.props.value;
const onChange = this.props.onChange;
if (type === 'value') {
value = '';
} else if (type === 'func') {
value = {
type: 'func',
func: (findTree(this.props.funcs!, item => (item as Func).type) as Func)
?.type,
args: []
};
} else if (type === 'field') {
value = {
type: 'field',
field: ''
};
} else if (type === 'raw') {
value = {
type: 'raw',
value: ''
};
}
onChange(value, this.props.index);
}
@autobind
handleValueChange(data: any) {
this.props.onChange(data, this.props.index);
}
@autobind
handleFieldChange(field: string) {
let value = this.props.value;
const onChange = this.props.onChange;
value = {
type: 'field',
field
};
onChange(value, this.props.index);
}
@autobind
handleFuncChange(func: any) {
let value = this.props.value;
const onChange = this.props.onChange;
value = {
...func,
type: 'func'
};
onChange(value, this.props.index);
}
@autobind
handleRawChange() {}
render() {
const {value, defaultType, allowedTypes, funcs, fields} = this.props;
const inputType =
((value as any)?.type === 'field'
? 'field'
: (value as any)?.type === 'func'
? 'func'
: (value as any)?.type === 'raw'
? 'raw'
: value !== undefined
? 'value'
: undefined) ||
defaultType ||
allowedTypes?.[0] ||
'value';
const types = allowedTypes || ['value', 'field', 'func'];
if ((!Array.isArray(funcs) || !funcs.length) && ~types.indexOf('func')) {
types.splice(types.indexOf('func'), 1);
}
return (
<>
{types.length > 1 ? (
<InputSwitch
value={inputType}
onChange={this.handleInputTypeChange}
options={types.map(item => ({
label: fieldMap[item],
value: item
}))}
/>
) : null}
{inputType === 'value' ? <Value /> : null}
{inputType === 'field' ? (
<ConditionField
value={(value as any)?.field}
onChange={this.handleFieldChange}
options={fields!}
/>
) : null}
{inputType === 'func' ? (
<ConditionFunc
value={value as ExpressionFunc}
onChange={this.handleFuncChange}
funcs={funcs}
fields={fields}
defaultType={defaultType}
allowedTypes={allowedTypes}
/>
) : null}
</>
);
}
}
export default themeable(Expression);

View File

@ -2,21 +2,20 @@ import React from 'react';
import PopOverContainer from '../PopOverContainer';
import ListRadios from '../ListRadios';
import ResultBox from '../ResultBox';
import {ClassNamesFn} from '../../theme';
import {ClassNamesFn, ThemeProps, themeable} from '../../theme';
import {Icon} from '../icons';
import {find} from 'lodash';
import {findTree, noop} from '../../utils/helper';
export interface ConditionFieldProps {
export interface ConditionFieldProps extends ThemeProps {
options: Array<any>;
value: any;
onChange: (value: any) => void;
classnames: ClassNamesFn;
}
const option2value = (item: any) => item.name;
export default function ConditionField({
export function ConditionField({
options,
onChange,
value,
@ -57,3 +56,5 @@ export default function ConditionField({
</PopOverContainer>
);
}
export default themeable(ConditionField);

View File

@ -0,0 +1,125 @@
import React from 'react';
import {Func, ExpressionFunc, Field, Funcs} from './types';
import {ThemeProps, themeable} from '../../theme';
import PopOverContainer from '../PopOverContainer';
import ListRadios from '../ListRadios';
import {autobind, findTree, noop} from '../../utils/helper';
import ResultBox from '../ResultBox';
import {Icon} from '../icons';
import Expression from './Expression';
export interface ConditionFuncProps extends ThemeProps {
value: ExpressionFunc;
onChange: (value: ExpressionFunc) => void;
fields?: Field[];
funcs?: Funcs;
defaultType?: 'value' | 'field' | 'func' | 'raw';
allowedTypes?: Array<'value' | 'field' | 'func' | 'raw'>;
}
const option2value = (item: Func) => item.type;
export class ConditionFunc extends React.Component<ConditionFuncProps> {
@autobind
handleFuncChange(type: string) {
const value = {...this.props.value};
value.func = type;
this.props.onChange(value);
}
@autobind
handleArgChange(arg: any, index: number) {
const value = {...this.props.value};
value.args = Array.isArray(value.args) ? value.args.concat() : [];
value.args.splice(index, 1, arg);
this.props.onChange(value);
}
renderFunc(func: Func) {
const {
classnames: cx,
fields,
value,
funcs,
defaultType,
allowedTypes
} = this.props;
return (
<div className={cx('CBFunc-args')}>
<span>(</span>
{Array.isArray(func.args) && func.args.length ? (
<div>
{func.args.map((item, index) => (
<Expression
key={index}
index={index}
fields={fields}
value={value?.args[index]}
valueField={{type: item.type} as any}
onChange={this.handleArgChange}
funcs={funcs}
defaultType={defaultType}
// allowedTypes={allowedTypes}
/>
))}
</div>
) : null}
<span>)</span>
</div>
);
}
render() {
const {value, classnames: cx, funcs} = this.props;
const func = value
? findTree(funcs!, item => (item as Func).type === value.func)
: null;
return (
<div className={cx('CBFunc')}>
<PopOverContainer
popOverRender={({onClose}) => (
<ListRadios
onClick={onClose}
showRadio={false}
options={funcs!}
value={(func as Func)?.type}
option2value={option2value}
onChange={this.handleFuncChange}
/>
)}
>
{({onClick, ref, isOpened}) => (
<div className={cx('CBFunc-select')}>
<ResultBox
className={cx(
'CBGroup-fieldInput',
isOpened ? 'is-active' : ''
)}
ref={ref}
allowInput={false}
result={func?.label}
onResultChange={noop}
onResultClick={onClick}
placeholder="请选择字段"
>
<span className={cx('CBGroup-fieldCaret')}>
<Icon icon="caret" className="icon" />
</span>
</ResultBox>
</div>
)}
</PopOverContainer>
{func ? (
this.renderFunc(func as Func)
) : (
<span className={cx('CBFunc-error')}></span>
)}
</div>
);
}
}
export default themeable(ConditionFunc);

View File

@ -4,8 +4,10 @@ import {ClassNamesFn} from '../../theme';
import Button from '../Button';
import {ConditionItem} from './Item';
import {autobind, guid} from '../../utils/helper';
import {Config} from './config';
export interface ConditionGroupProps {
config: Config;
value?: ConditionGroupValue;
fields: Fields;
funcs?: Funcs;
@ -85,7 +87,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
}
render() {
const {classnames: cx, value, fields, funcs} = this.props;
const {classnames: cx, value, fields, funcs, config} = this.props;
return (
<div className={cx('CBGroup')}>
@ -125,6 +127,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
? value!.children.map((item, index) =>
(item as ConditionGroupValue).conjunction ? (
<ConditionGroup
config={config}
key={item.id}
fields={fields}
value={item as ConditionGroupValue}
@ -135,6 +138,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
/>
) : (
<ConditionItem
config={config}
key={item.id}
fields={fields}
value={item}

View File

@ -2,18 +2,17 @@ import React from 'react';
import PopOverContainer from '../PopOverContainer';
import {Icon} from '../icons';
import ListRadios from '../ListRadios';
import {ClassNamesFn} from '../../theme';
import {ClassNamesFn, themeable, ThemeProps} from '../../theme';
export interface InputSwitchProps {
export interface InputSwitchProps extends ThemeProps {
options: Array<any>;
value: any;
onChange: (value: any) => void;
classnames: ClassNamesFn;
}
const option2value = (item: any) => item.value;
export default function InputSwitch({
export function InputSwitch({
options,
value,
onChange,
@ -42,3 +41,5 @@ export default function InputSwitch({
</PopOverContainer>
);
}
export default themeable(InputSwitch);

View File

@ -1,17 +1,28 @@
import React from 'react';
import {Fields, ConditionRule, ConditionGroupValue, Funcs} from './types';
import {
Fields,
ConditionRule,
ConditionGroupValue,
Funcs,
ExpressionFunc,
Func,
Field,
FieldSimple,
ExpressionField
} from './types';
import {ClassNamesFn} from '../../theme';
import {Icon} from '../icons';
import Select from '../Select';
import {autobind} from '../../utils/helper';
import {autobind, findTree, noop} from '../../utils/helper';
import Expression from './Expression';
import {Config, OperationMap} from './config';
import PopOverContainer from '../PopOverContainer';
import InputBox from '../InputBox';
import ListRadios from '../ListRadios';
import ResultBox from '../ResultBox';
import ConditionField from './Field';
import InputSwitch from './InputSwitch';
const option2value = (item: any) => item.value;
export interface ConditionItemProps {
config: Config;
fields: Fields;
funcs?: Funcs;
index?: number;
@ -20,17 +31,6 @@ export interface ConditionItemProps {
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) {
@ -54,34 +54,101 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
onChange(value, this.props.index);
}
@autobind
handleLeftChange(leftValue: any) {
const value = {...this.props.value, left: leftValue};
const onChange = this.props.onChange;
onChange(value, this.props.index);
}
@autobind
handleOperatorChange() {}
renderLeft() {
const {value, fields, classnames: cx, funcs} = this.props;
const inputType =
value.left && (value.left as any).type === 'func' ? 'func' : 'field';
const {value, fields, funcs} = this.props;
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}
</>
<Expression
funcs={funcs}
value={value.left}
onChange={this.handleLeftChange}
fields={fields}
defaultType="field"
allowedTypes={['field', 'func']}
/>
);
}
renderOperator() {
const {funcs, config, fields, value, classnames: cx} = this.props;
const left = value?.left;
let operators: Array<string> = [];
if ((left as ExpressionFunc)?.type === 'func') {
const func: Func = findTree(
funcs!,
(i: Func) => i.type === (left as ExpressionFunc).type
) as Func;
if (func) {
operators = config.types[func.returnType]?.operators;
}
} else if ((left as ExpressionField)?.type === 'field') {
const field: FieldSimple = findTree(
fields,
(i: FieldSimple) => i.name === (left as ExpressionField).field
) as FieldSimple;
if (field) {
operators = field.operators || config.types[field.type].operators;
}
}
if (Array.isArray(operators) && operators.length) {
return (
<PopOverContainer
popOverRender={({onClose}) => (
<ListRadios
onClick={onClose}
option2value={option2value}
onChange={this.handleOperatorChange}
options={operators.map(operator => ({
label: OperationMap[operator as keyof typeof OperationMap],
value: operator
}))}
value={value.op}
showRadio={false}
/>
)}
>
{({onClick, isOpened, ref}) => (
<div className={cx('CBGroup-operator')}>
<ResultBox
className={cx(
'CBGroup-operatorInput',
isOpened ? 'is-active' : ''
)}
ref={ref}
allowInput={false}
result={OperationMap[value?.op as keyof typeof OperationMap]}
onResultChange={noop}
onResultClick={onClick}
placeholder="请选择操作"
>
<span className={cx('CBGroup-operatorCaret')}>
<Icon icon="caret" className="icon" />
</span>
</ResultBox>
</div>
)}
</PopOverContainer>
);
}
return null;
}
renderItem() {
return null;
}
@ -97,6 +164,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
<div className={cx('CBItem-itemBody')}>
{this.renderLeft()}
{this.renderOperator()}
{this.renderItem()}
</div>
</div>

View File

@ -0,0 +1,7 @@
import React from 'react';
export default class Value extends React.Component<any> {
render() {
return <p>Value</p>;
}
}

View File

@ -1,4 +1,4 @@
import {FieldTypes, OperatorType, Funcs, Fields} from './types';
import {FieldTypes, OperatorType, Funcs, Fields, Type} from './types';
export interface BaseFieldConfig {
operations: Array<OperatorType>;
@ -8,9 +8,38 @@ export interface Config {
fields: Fields;
funcs?: Funcs;
maxLevel?: number;
types: {
[propName: string]: Type;
};
}
export const OperationMap = {
equal: '等于',
not_equal: '不等于',
is_empty: '为空',
is_not_empty: '不为空',
like: 'LIKE',
not_like: 'NOT LIKE',
starts_with: 'Start With',
ends_with: 'Ends With'
};
const defaultConfig: Config = {
types: {
text: {
operators: [
'equal',
'not_equal',
'is_empty',
'is_not_empty',
'like',
'not_like',
'starts_with',
'ends_with'
]
}
},
fields: [
// {
// type: 'text',

View File

@ -4,6 +4,7 @@ import {LocaleProps, localeable} from '../../locale';
import {uncontrollable} from 'uncontrollable';
import {Fields, ConditionGroupValue, Funcs} from './types';
import {ConditionGroup} from './Group';
import defaultConfig from './config';
export interface QueryBuilderProps extends ThemeProps, LocaleProps {
fields: Fields;
@ -13,11 +14,14 @@ export interface QueryBuilderProps extends ThemeProps, LocaleProps {
}
export class QueryBuilder extends React.Component<QueryBuilderProps> {
config = defaultConfig;
render() {
const {classnames: cx, fields, funcs, onChange, value} = this.props;
return (
<ConditionGroup
config={this.config}
funcs={funcs}
fields={fields}
value={value}

View File

@ -19,25 +19,31 @@ export type FieldItem = {
};
export type ExpressionSimple = string | number | object | undefined;
export type ExpressionComplex =
export type ExpressionValue =
| ExpressionSimple
| {
type: 'value';
value: ExpressionSimple;
}
| {
type: 'func';
func: string;
args: Array<ExpressionComplex>;
}
| {
type: 'field';
field: string;
}
| {
type: 'raw';
field: string;
};
export type ExpressionFunc = {
type: 'func';
func: string;
args: Array<ExpressionComplex>;
};
export type ExpressionField = {
type: 'field';
field: string;
};
export type ExpressionRaw = {
type: 'raw';
value: string;
};
export type ExpressionComplex =
| ExpressionValue
| ExpressionFunc
| ExpressionField
| ExpressionRaw;
export interface ConditionRule {
id: any;
@ -59,6 +65,7 @@ interface BaseField {
type: FieldTypes;
label: string;
valueTypes?: Array<'value' | 'field' | 'func' | 'expression'>;
operators?: Array<string>;
// valueTypes 里面配置 func 才有效。
funcs?: Array<string>;
@ -123,7 +130,7 @@ interface GroupField {
children: Array<FieldSimple>;
}
type FieldSimple =
export type FieldSimple =
| TextField
| NumberField
| DateField
@ -150,3 +157,7 @@ export interface FuncArg extends BaseField {
}
export type Funcs = Array<Func | FuncGroup>;
export type Fields = Array<Field>;
export type Type = {
operators: Array<string>;
};