条件组合完善进度 80%

This commit is contained in:
2betop 2020-08-18 20:26:04 +08:00
parent b6afea8162
commit 0e78191ddc
12 changed files with 358 additions and 83 deletions

View File

@ -14,6 +14,18 @@ const fields = [
type: 'number' type: 'number'
}, },
{
label: '出生日期',
name: 'birthday',
type: 'date'
},
{
label: '起床时间',
name: 'wakeupAt',
type: 'time'
},
{ {
label: '入职时间', label: '入职时间',
name: 'ruzhi', name: 'ruzhi',
@ -39,22 +51,22 @@ const fields = [
]; ];
const funcs = [ const funcs = [
{ // {
label: '文本', // label: '',
children: [ // children: [
{ // {
type: 'LOWERCASE', // type: 'LOWERCASE',
label: '转小写', // label: '',
returnType: 'text', // returnType: 'text',
args: [ // args: [
{ // {
type: 'text', // type: 'text',
label: '文本' // label: ''
} // }
] // ]
} // }
] // ]
} // }
]; ];
export default { export default {
@ -64,25 +76,39 @@ export default {
type: 'form', type: 'form',
mode: 'horizontal', mode: 'horizontal',
title: '', title: '',
data: {a: [{b: 1, c: [{d: 2}]}]},
// debug: true,
api: '/api/mock2/form/saveForm', api: '/api/mock2/form/saveForm',
controls: [ controls: [
{ // {
label: 'Name', // label: 'Name',
type: 'text', // type: 'text',
name: 'name' // name: 'name'
}, // },
{ // {
label: 'Email', // label: 'Email',
type: 'email', // type: 'email',
name: 'email' // name: 'email'
}, // },
{ // {
name: 'a', // name: 'a',
type: 'static', // type: 'static',
tpl: '${a|json:2}' // tpl: '${a|json:2}'
}, // },
// {
// name: 'a.0.b',
// type: 'text',
// label: 'B'
// },
// {
// name: 'a.0.c.0.d',
// type: 'number',
// label: 'D'
// },
{ {
name: 'a', name: 'a',

View File

@ -1020,12 +1020,12 @@ $ColorPicker-paddingY: (
)/2 - $ColorPicker-borderWidth !default; )/2 - $ColorPicker-borderWidth !default;
$ColorPicker-placeholderColor: $Form-input-placeholderColor !default; $ColorPicker-placeholderColor: $Form-input-placeholderColor !default;
$ColorPicker-onFocused-borderColor: $Form-input-onFocused-borderColor !default; $ColorPicker-onFocused-borderColor: $Form-input-onFocused-borderColor !default;
$DatePicker-onHover-borderColor: $Form-input-borderColor !default;
// datepicker // datepicker
$DatePicker-color: $Form-input-color !default; $DatePicker-color: $Form-input-color !default;
$DatePicker-bg: $white !default; $DatePicker-bg: $white !default;
$DatePicker-onHover-bg: darken($DatePicker-bg, 5%) !default; $DatePicker-onHover-borderColor: $Form-input-onFocused-borderColor !default;
$DatePicker-onHover-bg: $DatePicker-bg !default;
$DatePicker-borderWidth: $Form-input-borderWidth !default; $DatePicker-borderWidth: $Form-input-borderWidth !default;
$DatePicker-borderColor: $Form-input-borderColor !default; $DatePicker-borderColor: $Form-input-borderColor !default;
$DatePicker-borderRadius: $Form-input-borderRadius !default; $DatePicker-borderRadius: $Form-input-borderRadius !default;

View File

@ -41,14 +41,14 @@
&-operator { &-operator {
position: relative; position: relative;
display: inline-block; display: inline-block;
min-width: px2rem(120px); margin: px2rem(3px);
margin-left: $gap-xs; vertical-align: middle;
} }
&-fieldCaret, &-fieldCaret,
&-operatorCaret { &-operatorCaret {
transition: transform 0.3s ease-out; transition: transform 0.3s ease-out;
margin: 0 $gap-xs; margin: 3px;
display: flex; display: flex;
color: $icon-color; color: $icon-color;
&:hover { &:hover {
@ -66,6 +66,46 @@
&-operatorInput.is-active &-operatorCaret { &-operatorInput.is-active &-operatorCaret {
transform: rotate(180deg); transform: rotate(180deg);
} }
&-placeholder {
color: $text--muted-color;
position: relative;
margin-left: 30px;
padding: 10px;
background: rgba(0, 0, 0, 0.03);
border-radius: 5px;
&: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}CBDelete { .#{$ns}CBDelete {
@ -85,7 +125,7 @@
&-body { &-body {
display: flex; display: flex;
padding: 5px 10px; padding: 2px 7px;
border-radius: 5px; border-radius: 5px;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -125,7 +165,6 @@
} }
.#{$ns}CBGroup { .#{$ns}CBGroup {
margin-left: $gap-xs;
flex-grow: 1; flex-grow: 1;
} }
@ -172,7 +211,8 @@
.#{$ns}CBInputSwitch { .#{$ns}CBInputSwitch {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin-left: 5px; vertical-align: middle;
// margin: px2rem(3px);
cursor: pointer; cursor: pointer;
> a { > a {
@include icon-color(); @include icon-color();
@ -186,7 +226,8 @@
.#{$ns}CBFunc { .#{$ns}CBFunc {
display: inline-block; display: inline-block;
margin-left: $gap-xs; vertical-align: middle;
margin: px2rem(3px);
&-select { &-select {
display: inline-block; display: inline-block;
@ -214,5 +255,13 @@
.#{$ns}CBValue { .#{$ns}CBValue {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin-left: $gap-xs; vertical-align: middle;
margin: px2rem(3px);
}
.#{$ns}CBSeprator {
width: 20px;
text-align: center;
display: inline-block;
user-select: none;
} }

View File

@ -5,11 +5,12 @@ import {
Func, Func,
ExpressionFunc, ExpressionFunc,
Type, Type,
FieldSimple FieldSimple,
FieldGroup
} from './types'; } from './types';
import React from 'react'; import React from 'react';
import ConditionField from './Field'; import ConditionField from './Field';
import {autobind, findTree} from '../../utils/helper'; import {autobind, findTree, filterTree} from '../../utils/helper';
import Value from './Value'; import Value from './Value';
import InputSwitch from './InputSwitch'; import InputSwitch from './InputSwitch';
import ConditionFunc from './Func'; import ConditionFunc from './Func';
@ -132,17 +133,6 @@ export class Expression extends React.Component<ExpressionProps> {
return ( return (
<> <>
{types.length > 1 ? (
<InputSwitch
value={inputType}
onChange={this.handleInputTypeChange}
options={types.map(item => ({
label: fieldMap[item],
value: item
}))}
/>
) : null}
{inputType === 'value' ? ( {inputType === 'value' ? (
<Value <Value
field={valueField!} field={valueField!}
@ -155,7 +145,16 @@ export class Expression extends React.Component<ExpressionProps> {
<ConditionField <ConditionField
value={(value as any)?.field} value={(value as any)?.field}
onChange={this.handleFieldChange} onChange={this.handleFieldChange}
options={fields!} options={
valueField
? filterTree(
fields!,
item =>
(item as any).children ||
(item as FieldSimple).type === valueField.type
)
: fields!
}
/> />
) : null} ) : null}
@ -169,6 +168,17 @@ export class Expression extends React.Component<ExpressionProps> {
allowedTypes={allowedTypes} allowedTypes={allowedTypes}
/> />
) : null} ) : null}
{types.length > 1 ? (
<InputSwitch
value={inputType}
onChange={this.handleInputTypeChange}
options={types.map(item => ({
label: fieldMap[item],
value: item
}))}
/>
) : null}
</> </>
); );
} }

View File

@ -129,6 +129,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
className="m-r-xs" className="m-r-xs"
size="xs" size="xs"
active={value?.not} active={value?.not}
level={value?.not ? 'info' : 'default'}
> >
</Button> </Button>
@ -176,8 +177,8 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
</div> </div>
<div className={cx('CBGroup-body')}> <div className={cx('CBGroup-body')}>
{Array.isArray(value?.children) {Array.isArray(value?.children) && value!.children.length ? (
? value!.children.map((item, index) => ( value!.children.map((item, index) => (
<GroupOrItem <GroupOrItem
draggable={value!.children!.length > 1} draggable={value!.children!.length > 1}
onDragStart={onDragStart} onDragStart={onDragStart}
@ -191,7 +192,9 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
onRemove={this.handleItemRemove} onRemove={this.handleItemRemove}
/> />
)) ))
: null} ) : (
<div className={cx('CBGroup-placeholder')}></div>
)}
</div> </div>
</div> </div>
); );

View File

@ -34,7 +34,7 @@ export function InputSwitch({
{({onClick, isOpened, ref}) => ( {({onClick, isOpened, ref}) => (
<div className={cx('CBInputSwitch', isOpened ? 'is-active' : '')}> <div className={cx('CBInputSwitch', isOpened ? 'is-active' : '')}>
<a onClick={onClick} ref={ref}> <a onClick={onClick} ref={ref}>
<Icon icon="setting" /> <Icon icon="ellipsis-v" />
</a> </a>
</div> </div>
)} )}

View File

@ -9,7 +9,8 @@ import {
Field, Field,
FieldSimple, FieldSimple,
ExpressionField, ExpressionField,
OperatorType OperatorType,
ExpressionComplex
} from './types'; } from './types';
import {ThemeProps, themeable} from '../../theme'; import {ThemeProps, themeable} from '../../theme';
import {Icon} from '../icons'; import {Icon} from '../icons';
@ -56,7 +57,12 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
@autobind @autobind
handleLeftChange(leftValue: any) { handleLeftChange(leftValue: any) {
const value = {...this.props.value, left: leftValue}; const value = {
...this.props.value,
left: leftValue,
op: undefined,
right: undefined
};
const onChange = this.props.onChange; const onChange = this.props.onChange;
onChange(value, this.props.index); onChange(value, this.props.index);
@ -64,7 +70,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
@autobind @autobind
handleOperatorChange(op: OperatorType) { handleOperatorChange(op: OperatorType) {
const value = {...this.props.value, op: op}; const value = {...this.props.value, op: op, right: undefined};
this.props.onChange(value, this.props.index); this.props.onChange(value, this.props.index);
} }
@ -76,6 +82,18 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
onChange(value, this.props.index); onChange(value, this.props.index);
} }
handleRightSubChange(index: number, rightValue: any) {
const origin = Array.isArray(this.props.value?.right)
? this.props.value.right.concat()
: [];
origin[index] = rightValue;
const value = {...this.props.value, right: origin};
const onChange = this.props.onChange;
onChange(value, this.props.index);
}
renderLeft() { renderLeft() {
const {value, fields, funcs} = this.props; const {value, fields, funcs} = this.props;
@ -99,7 +117,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
if ((left as ExpressionFunc)?.type === 'func') { if ((left as ExpressionFunc)?.type === 'func') {
const func: Func = findTree( const func: Func = findTree(
funcs!, funcs!,
(i: Func) => i.type === (left as ExpressionFunc).type (i: Func) => i.type === (left as ExpressionFunc).func
) as Func; ) as Func;
if (func) { if (func) {
@ -173,7 +191,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
if ((left as ExpressionFunc)?.type === 'func') { if ((left as ExpressionFunc)?.type === 'func') {
const func: Func = findTree( const func: Func = findTree(
funcs!, funcs!,
(i: Func) => i.type === (left as ExpressionFunc).type (i: Func) => i.type === (left as ExpressionFunc).func
) as Func; ) as Func;
if (func) { if (func) {
@ -198,7 +216,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
} }
renderRightWidgets(type: string, op: OperatorType) { renderRightWidgets(type: string, op: OperatorType) {
const {funcs, value, fields, config} = this.props; const {funcs, value, fields, config, classnames: cx} = this.props;
let field = { let field = {
...config.types[type], ...config.types[type],
type type
@ -220,6 +238,36 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
if (op === 'is_empty' || op === 'is_not_empty') { if (op === 'is_empty' || op === 'is_not_empty') {
return null; return null;
} else if (op === 'between' || op === 'not_between') {
return (
<>
<Expression
funcs={funcs}
valueField={field}
value={(value.right as Array<ExpressionComplex>)?.[0]}
onChange={this.handleRightSubChange.bind(this, 0)}
fields={fields}
defaultType="value"
allowedTypes={
field?.valueTypes || ['value', 'field', 'func', 'raw']
}
/>
<span className={cx('CBSeprator')}>~</span>
<Expression
funcs={funcs}
valueField={field}
value={(value.right as Array<ExpressionComplex>)?.[1]}
onChange={this.handleRightSubChange.bind(this, 1)}
fields={fields}
defaultType="value"
allowedTypes={
field?.valueTypes || ['value', 'field', 'func', 'raw']
}
/>
</>
);
} }
return ( return (

View File

@ -2,6 +2,8 @@ import React from 'react';
import {FieldSimple} from './types'; import {FieldSimple} from './types';
import {ThemeProps, themeable} from '../../theme'; import {ThemeProps, themeable} from '../../theme';
import InputBox from '../InputBox'; import InputBox from '../InputBox';
import NumberInput from '../NumberInput';
import DatePicker from '../DatePicker';
export interface ValueProps extends ThemeProps { export interface ValueProps extends ThemeProps {
value: any; value: any;
@ -17,11 +19,56 @@ export class Value extends React.Component<ValueProps> {
if (field.type === 'text') { if (field.type === 'text') {
input = ( input = (
<InputBox <InputBox
value={value} value={value ?? field.defaultValue}
onChange={onChange} onChange={onChange}
placeholder={field.placeholder} placeholder={field.placeholder}
/> />
); );
} else if (field.type === 'number') {
input = (
<NumberInput
placeholder={field.placeholder || '请选择日期'}
min={field.minimum}
max={field.maximum}
value={value ?? field.defaultValue}
onChange={onChange}
/>
);
} else if (field.type === 'date') {
input = (
<DatePicker
placeholder={field.placeholder || '请选择日期'}
format={field.format || 'YYYY-MM-DD'}
inputFormat={field.inputFormat || 'YYYY-MM-DD'}
value={value ?? field.defaultValue}
onChange={onChange}
timeFormat=""
/>
);
} else if (field.type === 'time') {
input = (
<DatePicker
viewMode="time"
placeholder={field.placeholder || '请选择时间'}
format={field.format || 'HH:mm'}
inputFormat={field.inputFormat || 'HH:mm'}
value={value ?? field.defaultValue}
onChange={onChange}
dateFormat=""
timeFormat={field.format || 'HH:mm'}
/>
);
} else if (field.type === 'datetime') {
input = (
<DatePicker
placeholder={field.placeholder || '请选择日期时间'}
format={field.format || ''}
inputFormat={field.inputFormat || 'YYYY-MM-DD HH:mm'}
value={value ?? field.defaultValue}
onChange={onChange}
timeFormat={field.timeFormat || 'HH:mm'}
/>
);
} }
return <div className={cx('CBValue')}>{input}</div>; return <div className={cx('CBValue')}>{input}</div>;

View File

@ -16,6 +16,12 @@ export interface Config {
export const OperationMap = { export const OperationMap = {
equal: '等于', equal: '等于',
not_equal: '不等于', not_equal: '不等于',
less: '小于',
less_or_equal: '小于或等于',
greater: '大于',
greater_or_equal: '大于或等于',
between: '属于范围',
not_between: '不属于范围',
is_empty: '为空', is_empty: '为空',
is_not_empty: '不为空', is_not_empty: '不为空',
like: '模糊匹配', like: '模糊匹配',
@ -28,6 +34,7 @@ const defaultConfig: Config = {
types: { types: {
text: { text: {
placeholder: '请输入文本', placeholder: '请输入文本',
defaultOp: 'equal',
operators: [ operators: [
'equal', 'equal',
'not_equal', 'not_equal',
@ -38,6 +45,64 @@ const defaultConfig: Config = {
'starts_with', 'starts_with',
'ends_with' 'ends_with'
] ]
},
number: {
operators: [
'equal',
'not_equal',
'less',
'less_or_equal',
'greater',
'greater_or_equal',
'between',
'not_between',
'is_empty',
'is_not_empty'
]
},
date: {
operators: [
'equal',
'not_equal',
'less',
'less_or_equal',
'greater',
'greater_or_equal',
'between',
'not_between',
'is_empty',
'is_not_empty'
]
},
time: {
operators: [
'equal',
'not_equal',
'less',
'less_or_equal',
'greater',
'greater_or_equal',
'between',
'not_between',
'is_empty',
'is_not_empty'
]
},
datetime: {
operators: [
'equal',
'not_equal',
'less',
'less_or_equal',
'greater',
'greater_or_equal',
'between',
'not_between',
'is_empty',
'is_not_empty'
]
} }
}, },

View File

@ -15,7 +15,13 @@ export type OperatorType =
| 'like' | 'like'
| 'not_like' | 'not_like'
| 'starts_with' | 'starts_with'
| 'ends_with'; | 'ends_with'
| 'less'
| 'less_or_equal'
| 'greater'
| 'greater_or_equal'
| 'between'
| 'not_between';
export type FieldItem = { export type FieldItem = {
type: 'text'; type: 'text';
@ -75,9 +81,10 @@ interface BaseField {
funcs?: Array<string>; funcs?: Array<string>;
defaultValue?: any; defaultValue?: any;
placeholder?: string;
} }
type FieldGroup = { export type FieldGroup = {
label: string; label: string;
children: Array<FieldSimple>; children: Array<FieldSimple>;
}; };
@ -85,7 +92,6 @@ type FieldGroup = {
interface TextField extends BaseField { interface TextField extends BaseField {
name: string; name: string;
type: 'text'; type: 'text';
placeholder?: string;
minLength?: number; minLength?: number;
maxLength?: number; maxLength?: number;
} }
@ -100,6 +106,8 @@ interface NumberField extends BaseField {
interface DateField extends BaseField { interface DateField extends BaseField {
name: string; name: string;
type: 'date'; type: 'date';
format?: string;
inputFormat?: string;
minDate?: any; minDate?: any;
maxDate?: any; maxDate?: any;
} }
@ -109,11 +117,16 @@ interface TimeField extends BaseField {
type: 'time'; type: 'time';
minTime?: any; minTime?: any;
maxTime?: any; maxTime?: any;
format?: string;
inputFormat?: string;
} }
interface DatetimeField extends BaseField { interface DatetimeField extends BaseField {
type: 'datetime'; type: 'datetime';
name: string; name: string;
format?: string;
inputFormat?: string;
timeFormat?: string;
} }
interface SelectField extends BaseField { interface SelectField extends BaseField {
@ -164,6 +177,7 @@ export type Funcs = Array<Func | FuncGroup>;
export type Fields = Array<Field>; export type Fields = Array<Field>;
export type Type = { export type Type = {
operators: Array<string>; defaultOp?: OperatorType;
operators: Array<OperatorType>;
placeholder?: string; placeholder?: string;
}; };

View File

@ -134,6 +134,9 @@ import SettingIcon from '../icons/setting.svg';
// @ts-ignore // @ts-ignore
import PlusCicleIcon from '../icons/plus-cicle.svg'; import PlusCicleIcon from '../icons/plus-cicle.svg';
// @ts-ignore
import EllipsisVIcon from '../icons/ellipsis-v.svg';
// 兼容原来的用法,后续不直接试用。 // 兼容原来的用法,后续不直接试用。
// @ts-ignore // @ts-ignore
export const closeIcon = <CloseIcon />; export const closeIcon = <CloseIcon />;
@ -221,6 +224,7 @@ registerIcon('sort-asc', SortAscIcon);
registerIcon('sort-desc', SortDescIcon); registerIcon('sort-desc', SortDescIcon);
registerIcon('setting', SettingIcon); registerIcon('setting', SettingIcon);
registerIcon('plus-cicle', PlusCicleIcon); registerIcon('plus-cicle', PlusCicleIcon);
registerIcon('ellipsis-v', EllipsisVIcon);
export function Icon({ export function Icon({
icon, icon,

9
src/icons/ellipsis-v.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 26 126" 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 id="ellipsis-vertical" transform="translate(0.500000, 0.500000)" fill="currentColor" fill-rule="nonzero">
<path d="M12.5,0 C5.625,0 0,5.625 0,12.5 C0,19.375 5.625,25 12.5,25 C19.375,25 25,19.375 25,12.5 C25,5.625 19.375,0 12.5,0 Z M12.5,50 C5.625,50 0,55.625 0,62.5 C0,69.375 5.625,75 12.5,75 C19.375,75 25,69.375 25,62.5 C25,55.625 19.375,50 12.5,50 Z M12.5,100 C5.625,100 0,105.625 0,112.5 C0,119.375 5.625,125 12.5,125 C19.375,125 25,119.375 25,112.5 C25,105.625 19.375,100 12.5,100 Z" id="形状">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 767 B