From 031f748bf239f60b91d34bdaeb7d759c05601232 Mon Sep 17 00:00:00 2001
From: 2betop <2betop.cn@gmail.com>
Date: Fri, 14 Aug 2020 17:45:59 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E7=BB=84=E5=90=88=E5=8D=8A?=
=?UTF-8?q?=E6=88=90=E5=93=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
examples/components/Page/Form.jsx | 32 +++++-
scss/_mixins.scss | 9 ++
scss/components/_condition-builder.scss | 103 +++++++++++++++++
scss/components/_result-box.scss | 1 +
src/components/ListRadios.tsx | 6 +-
src/components/PopOverContainer.tsx | 8 +-
.../condition-builder/ConditionItem.tsx | 64 -----------
.../condition-builder/Expression.tsx | 21 ++++
src/components/condition-builder/Field.tsx | 59 ++++++++++
.../{ConditionGroup.tsx => Group.tsx} | 75 ++++++++-----
.../condition-builder/InputSwitch.tsx | 44 ++++++++
src/components/condition-builder/Item.tsx | 105 ++++++++++++++++++
.../{ConditionBuilder.tsx => index.tsx} | 8 +-
src/components/condition-builder/types.ts | 22 ++--
src/components/icons.tsx | 4 +
src/icons/setting.svg | 9 ++
16 files changed, 457 insertions(+), 113 deletions(-)
delete mode 100644 src/components/condition-builder/ConditionItem.tsx
create mode 100644 src/components/condition-builder/Expression.tsx
create mode 100644 src/components/condition-builder/Field.tsx
rename src/components/condition-builder/{ConditionGroup.tsx => Group.tsx} (60%)
create mode 100644 src/components/condition-builder/InputSwitch.tsx
create mode 100644 src/components/condition-builder/Item.tsx
rename src/components/condition-builder/{ConditionBuilder.tsx => index.tsx} (77%)
create mode 100644 src/icons/setting.svg
diff --git a/examples/components/Page/Form.jsx b/examples/components/Page/Form.jsx
index 2e1041ff6..c146ccf40 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 957da7bb1..504208982 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 7faebb455..22ef45ad2 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 b757d7358..3ec2e0e58 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 fcc802dde..d789d609d 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 e0540dc5c..b7a926c98 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 eef8bf6f7..000000000
--- 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 000000000..3865d206a
--- /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 000000000..3841f90cd
--- /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 9f0e2fcb2..394b3eef2 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 000000000..81e30050f
--- /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 000000000..9238a2a6b
--- /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 9489db8a5..da39918e2 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 f783dfc78..dbc0659a4 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 000000000..61dba6a0b
--- /dev/null
+++ b/src/icons/setting.svg
@@ -0,0 +1,9 @@
+
+