form-driver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/m3.css +310 -0
- package/dist/m3.js +1 -0
- package/es/m3.css +310 -0
- package/es/m3.js +20919 -0
- package/lib/m3.css +310 -0
- package/lib/m3.js +20959 -0
- package/package.json +132 -0
- package/src/.DS_Store +0 -0
- package/src/framework/Ajax.ts +96 -0
- package/src/framework/Assembly.tsx +165 -0
- package/src/framework/Init.tsx +196 -0
- package/src/framework/M3.tsx +94 -0
- package/src/framework/MContext.ts +15 -0
- package/src/framework/MFieldViewer.tsx +32 -0
- package/src/framework/MUtil.tsx +653 -0
- package/src/framework/MViewer.less +128 -0
- package/src/framework/MViewer.tsx +180 -0
- package/src/framework/MViewerDebug.tsx +95 -0
- package/src/framework/Persistant.ts +90 -0
- package/src/framework/Schema.ts +386 -0
- package/src/framework/SchemaFunc.ts +30 -0
- package/src/framework/Validator.ts +160 -0
- package/src/framework/editorMap.ts +109 -0
- package/src/index.ts +33 -0
- package/src/types/MArrayType.ts +73 -0
- package/src/types/MCascadeType.ts +35 -0
- package/src/types/MCnAddressType.ts +54 -0
- package/src/types/MDateRangeType.ts +52 -0
- package/src/types/MDateTimeType.ts +53 -0
- package/src/types/MDecorationType.ts +6 -0
- package/src/types/MEnumType.ts +65 -0
- package/src/types/MExperienceType.ts +81 -0
- package/src/types/MFloatType.ts +10 -0
- package/src/types/MGB2260Type.ts +56 -0
- package/src/types/MIntDiffType.ts +6 -0
- package/src/types/MIntType.ts +44 -0
- package/src/types/MKvSetType.ts +50 -0
- package/src/types/MMatrixType.ts +52 -0
- package/src/types/MObjectType.ts +89 -0
- package/src/types/MSetType.ts +220 -0
- package/src/types/MStringType.ts +27 -0
- package/src/types/MTelType.ts +14 -0
- package/src/types/MType.ts +77 -0
- package/src/types/MVLPairType.ts +35 -0
- package/src/types/gb2260.json +1 -0
- package/src/ui/BaseViewer.tsx +110 -0
- package/src/ui/editor/.DS_Store +0 -0
- package/src/ui/editor/basic/.DS_Store +0 -0
- package/src/ui/editor/basic/ACascadePicker.tsx +114 -0
- package/src/ui/editor/basic/ACheckBox.tsx +104 -0
- package/src/ui/editor/basic/ADatetimePicker.tsx +76 -0
- package/src/ui/editor/basic/AGB2260.tsx +52 -0
- package/src/ui/editor/basic/AInputBox.tsx +59 -0
- package/src/ui/editor/basic/AIntBox.tsx +39 -0
- package/src/ui/editor/basic/AKvSet.less +9 -0
- package/src/ui/editor/basic/AKvSet.tsx +90 -0
- package/src/ui/editor/basic/ARadio.tsx +86 -0
- package/src/ui/editor/basic/ARangePicker.tsx +129 -0
- package/src/ui/editor/basic/ARate.less +8 -0
- package/src/ui/editor/basic/ARate.tsx +37 -0
- package/src/ui/editor/basic/ARemoteSelector.tsx +116 -0
- package/src/ui/editor/basic/ASelector.tsx +88 -0
- package/src/ui/editor/basic/ASetSelector.tsx +65 -0
- package/src/ui/editor/basic/ASpecInputBox.tsx +20 -0
- package/src/ui/editor/basic/ATreeSelect.tsx +41 -0
- package/src/ui/editor/basic/AUpload.tsx +119 -0
- package/src/ui/editor/basic/NPS.less +21 -0
- package/src/ui/editor/basic/NPS.tsx +47 -0
- package/src/ui/editor/complex/AArray.less +10 -0
- package/src/ui/editor/complex/AArray.tsx +104 -0
- package/src/ui/editor/complex/AArrayGrid.tsx +115 -0
- package/src/ui/editor/complex/ACnAddress.less +15 -0
- package/src/ui/editor/complex/ACnAddress.tsx +61 -0
- package/src/ui/editor/complex/ADialogForm.tsx +45 -0
- package/src/ui/editor/complex/AExperience.tsx +85 -0
- package/src/ui/editor/complex/AForm.less +35 -0
- package/src/ui/editor/complex/AForm.tsx +340 -0
- package/src/ui/editor/complex/AIntDiff.tsx +77 -0
- package/src/ui/editor/complex/AMatrix.less +18 -0
- package/src/ui/editor/complex/AMatrix.tsx +242 -0
- package/src/ui/editor/complex/ATable.less +4 -0
- package/src/ui/editor/complex/ATable.tsx +33 -0
- package/src/ui/editor/complex/JsonEditor.tsx +37 -0
- package/src/ui/readable/A.tsx +33 -0
- package/src/ui/readable/ArrayViewer.tsx +46 -0
- package/src/ui/readable/DecorationViewer.tsx +76 -0
- package/src/ui/readable/DivViewer.tsx +11 -0
- package/src/ui/widget/Collapsible.tsx +156 -0
- package/src/ui/widget/Segment.less +39 -0
- package/src/ui/widget/Segment.tsx +40 -0
- package/src/ui/widget/SegmentEditSwitch.tsx +46 -0
- package/src/ui/widget/SelectBox.tsx +43 -0
- package/src/ui/widget/UnderlineInputBox.less +47 -0
- package/src/ui/widget/UnderlineInputBox.tsx +10 -0
- package/types/framework/Ajax.d.ts +5 -0
- package/types/framework/Assembly.d.ts +59 -0
- package/types/framework/Init.d.ts +4 -0
- package/types/framework/M3.d.ts +6 -0
- package/types/framework/MContext.d.ts +11 -0
- package/types/framework/MFieldViewer.d.ts +8 -0
- package/types/framework/MUtil.d.ts +180 -0
- package/types/framework/MViewer.d.ts +75 -0
- package/types/framework/MViewerDebug.d.ts +11 -0
- package/types/framework/Persistant.d.ts +17 -0
- package/types/framework/Schema.d.ts +306 -0
- package/types/framework/SchemaFunc.d.ts +14 -0
- package/types/framework/Validator.d.ts +53 -0
- package/types/framework/editorMap.d.ts +107 -0
- package/types/index.d.ts +21 -0
- package/types/types/MArrayType.d.ts +2 -0
- package/types/types/MCascadeType.d.ts +11 -0
- package/types/types/MCnAddressType.d.ts +2 -0
- package/types/types/MDateRangeType.d.ts +7 -0
- package/types/types/MDateTimeType.d.ts +11 -0
- package/types/types/MDecorationType.d.ts +7 -0
- package/types/types/MEnumType.d.ts +2 -0
- package/types/types/MExperienceType.d.ts +5 -0
- package/types/types/MFloatType.d.ts +2 -0
- package/types/types/MGB2260Type.d.ts +9 -0
- package/types/types/MIntDiffType.d.ts +2 -0
- package/types/types/MIntType.d.ts +2 -0
- package/types/types/MKvSetType.d.ts +11 -0
- package/types/types/MMatrixType.d.ts +5 -0
- package/types/types/MObjectType.d.ts +11 -0
- package/types/types/MSetType.d.ts +7 -0
- package/types/types/MStringType.d.ts +2 -0
- package/types/types/MTelType.d.ts +4 -0
- package/types/types/MType.d.ts +46 -0
- package/types/types/MVLPairType.d.ts +5 -0
- package/types/ui/BaseViewer.d.ts +45 -0
- package/types/ui/editor/basic/ACascadePicker.d.ts +11 -0
- package/types/ui/editor/basic/ACheckBox.d.ts +17 -0
- package/types/ui/editor/basic/ADatetimePicker.d.ts +15 -0
- package/types/ui/editor/basic/AGB2260.d.ts +10 -0
- package/types/ui/editor/basic/AInputBox.d.ts +8 -0
- package/types/ui/editor/basic/AIntBox.d.ts +9 -0
- package/types/ui/editor/basic/AKvSet.d.ts +19 -0
- package/types/ui/editor/basic/ARadio.d.ts +14 -0
- package/types/ui/editor/basic/ARangePicker.d.ts +30 -0
- package/types/ui/editor/basic/ARate.d.ts +13 -0
- package/types/ui/editor/basic/ARemoteSelector.d.ts +15 -0
- package/types/ui/editor/basic/ASelector.d.ts +14 -0
- package/types/ui/editor/basic/ASetSelector.d.ts +10 -0
- package/types/ui/editor/basic/ASpecInputBox.d.ts +9 -0
- package/types/ui/editor/basic/ATreeSelect.d.ts +18 -0
- package/types/ui/editor/basic/AUpload.d.ts +33 -0
- package/types/ui/editor/basic/NPS.d.ts +13 -0
- package/types/ui/editor/complex/AArray.d.ts +11 -0
- package/types/ui/editor/complex/AArrayGrid.d.ts +13 -0
- package/types/ui/editor/complex/ACnAddress.d.ts +10 -0
- package/types/ui/editor/complex/ADialogForm.d.ts +11 -0
- package/types/ui/editor/complex/AExperience.d.ts +16 -0
- package/types/ui/editor/complex/AForm.d.ts +46 -0
- package/types/ui/editor/complex/AIntDiff.d.ts +14 -0
- package/types/ui/editor/complex/AMatrix.d.ts +48 -0
- package/types/ui/editor/complex/ATable.d.ts +9 -0
- package/types/ui/editor/complex/JsonEditor.d.ts +9 -0
- package/types/ui/readable/A.d.ts +5 -0
- package/types/ui/readable/ArrayViewer.d.ts +5 -0
- package/types/ui/readable/DecorationViewer.d.ts +3 -0
- package/types/ui/readable/DivViewer.d.ts +5 -0
- package/types/ui/widget/Collapsible.d.ts +46 -0
- package/types/ui/widget/Segment.d.ts +18 -0
- package/types/ui/widget/SegmentEditSwitch.d.ts +20 -0
- package/types/ui/widget/SelectBox.d.ts +13 -0
- package/types/ui/widget/UnderlineInputBox.d.ts +6 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
|
|
2
|
+
import React from "react";
|
|
3
|
+
import _ from "lodash";
|
|
4
|
+
import "./AForm.less";
|
|
5
|
+
import { Viewer, ViewerState } from '../../BaseViewer';
|
|
6
|
+
import { M3UISpec, MFieldSchema, MProp } from '../../../framework/Schema';
|
|
7
|
+
import { HideMap, MUtil } from "../../../framework/MUtil";
|
|
8
|
+
import { MFieldViewer } from '../../../framework/MFieldViewer';
|
|
9
|
+
import { Segment } from '../../widget/Segment';
|
|
10
|
+
import { MORPH } from '../../../framework/Assembly';
|
|
11
|
+
import { SegmentEditSwitch } from "../../widget/SegmentEditSwitch";
|
|
12
|
+
import { Modal, Popover } from "antd";
|
|
13
|
+
import { QuestionCircleTwoTone } from "@ant-design/icons";
|
|
14
|
+
import { MContext } from "../../../framework/MContext";
|
|
15
|
+
import { Collapsible } from "../../widget/Collapsible";
|
|
16
|
+
|
|
17
|
+
function ItemLabel(props: { uispec?: M3UISpec, schema: MFieldSchema, labelWidth?: number, morph: MORPH }): JSX.Element {
|
|
18
|
+
// label自动加冒号功能 uispec.comma
|
|
19
|
+
let label = props.schema.label;
|
|
20
|
+
if (props.uispec?.comma) {
|
|
21
|
+
// @ts-ignore trimEnd支持正则的,但lodash的声明写得不对
|
|
22
|
+
label = _.trimEnd(label, new RegExp("::" + props.uispec.comma)) + props.uispec.comma;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 必填label加星号
|
|
26
|
+
const star = (props.schema.required && props.morph === "editor") ? <span style={{ color: "red" }}>*</span> : undefined;
|
|
27
|
+
|
|
28
|
+
if (!props.schema.label) {
|
|
29
|
+
return <></>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let popoverDesc = undefined;
|
|
33
|
+
if (props.schema.popoverDesc) {
|
|
34
|
+
popoverDesc = <Popover key=":popoverDesc" content={props.schema.popoverDesc}>
|
|
35
|
+
<QuestionCircleTwoTone style={{ marginLeft: 5 }} />
|
|
36
|
+
</Popover>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (props.labelWidth) {
|
|
40
|
+
return <span className="ItemLabel" style={{ display: "inline-block", width: props.labelWidth + 20 }}>{star}{label}{popoverDesc}</span>;
|
|
41
|
+
} else {
|
|
42
|
+
return <div className="ItemLabel" key={"字段标题:" + props.schema.name}>{star}{label}{popoverDesc}</div>;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface State extends ViewerState {
|
|
47
|
+
/** 编辑中的分组label,以及编辑前的数据 */
|
|
48
|
+
editing: { [label: string]: any };
|
|
49
|
+
|
|
50
|
+
/** 保存中的分组label */
|
|
51
|
+
saving: { [segmentLabel: string]: boolean };
|
|
52
|
+
|
|
53
|
+
// /** 上一个状态时的hideMap */
|
|
54
|
+
// shouldAnimation: {[n:string]: boolean};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// /**
|
|
58
|
+
// * 判断是否应该使用动画。
|
|
59
|
+
// *
|
|
60
|
+
// * 当相邻的题,状态变化不同向(都是隐藏,或者都是出现)时,不能使用折叠动画,否则会出现抖动
|
|
61
|
+
// * 这个函数判断所有的字段,是否需要折叠动画
|
|
62
|
+
// */
|
|
63
|
+
// function shouldAnimation(prevHideMap:HideMap, hideMap:HideMap, fields: MFieldSchema[]): {[n:string]: boolean}{
|
|
64
|
+
// // 变化方向:隐藏->展现=1 展现->隐藏=-1 状态不变=0
|
|
65
|
+
// const direction = fields.map(f=> {
|
|
66
|
+
// const prev = ! (prevHideMap?.[f.name]);
|
|
67
|
+
// const now = ! (hideMap[f.name]);
|
|
68
|
+
// if(!prev && now) {
|
|
69
|
+
// return 1;
|
|
70
|
+
// } else if(prev && !now) {
|
|
71
|
+
// return -1;
|
|
72
|
+
// } else {
|
|
73
|
+
// return 0;
|
|
74
|
+
// }
|
|
75
|
+
// })
|
|
76
|
+
|
|
77
|
+
// // 监视哨
|
|
78
|
+
// direction[-1] = 0;
|
|
79
|
+
// direction[fields.length] = 0;
|
|
80
|
+
|
|
81
|
+
// // 计算动画是否会抖动
|
|
82
|
+
// let anim = {};
|
|
83
|
+
// fields.map((f,idx)=>{
|
|
84
|
+
// if(direction[idx] + direction[idx - 1] != 0 && direction[idx] + direction[idx + 1] != 0){
|
|
85
|
+
// anim[f.name] = true;
|
|
86
|
+
// } else {
|
|
87
|
+
// anim[f.name] = false;
|
|
88
|
+
// }
|
|
89
|
+
// });
|
|
90
|
+
// console.log(anim)
|
|
91
|
+
// return anim;
|
|
92
|
+
// }
|
|
93
|
+
export class AForm extends Viewer<State> {
|
|
94
|
+
constructor(p: MProp) {
|
|
95
|
+
super(p);
|
|
96
|
+
this.state = {
|
|
97
|
+
//shouldAnimation:{},
|
|
98
|
+
editing: {}, saving: {}, ctrlVersion: 1, noValidate: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 表单项,包括label和viewer
|
|
103
|
+
* @param hideField
|
|
104
|
+
* @param hideMap
|
|
105
|
+
* @param f
|
|
106
|
+
* @param objectFields
|
|
107
|
+
* @param uispec
|
|
108
|
+
* @param morph
|
|
109
|
+
* @param labelWidth
|
|
110
|
+
* @param invalidLayoutMsg
|
|
111
|
+
* @param column
|
|
112
|
+
* @returns
|
|
113
|
+
*/
|
|
114
|
+
private formItem(hideField: boolean, hideMap:HideMap, f: MFieldSchema, objectFields: MFieldSchema[], uispec, morph: MORPH,
|
|
115
|
+
labelWidth: number, invalidLayoutMsg: string, column: number): JSX.Element {
|
|
116
|
+
|
|
117
|
+
const path = MUtil.jsonPath(this.props.path, f.name)
|
|
118
|
+
const wrapperProp = {
|
|
119
|
+
style: {
|
|
120
|
+
display: undefined,
|
|
121
|
+
marginBottom: 15,
|
|
122
|
+
content: f.name // debug用
|
|
123
|
+
},
|
|
124
|
+
open: !hideField,
|
|
125
|
+
//ms: this.state.shouldAnimation?.[f.name] ? 500 : null,
|
|
126
|
+
ms: 500,
|
|
127
|
+
key: f.name,
|
|
128
|
+
id: path,
|
|
129
|
+
name: f.name
|
|
130
|
+
};
|
|
131
|
+
if (column > 1) {
|
|
132
|
+
// 计算每项的宽度(保留2位有效数字)
|
|
133
|
+
Object.assign(wrapperProp.style, {
|
|
134
|
+
width: Math.floor(100 / column * 100) / 100 + '%'
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fieldViewer = <MFieldViewer morph={morph ?? "readable"} key={path} schema={f} database={this.props.database} path={path} afterChange={(p, v: any, blur) => {
|
|
139
|
+
this.props.afterChange?.(path, v, blur);
|
|
140
|
+
|
|
141
|
+
const newHideMap = MUtil.hideMap(MUtil.get(this.props.database, this.props.path), objectFields, uispec);
|
|
142
|
+
if (!_.isEqual(newHideMap, hideMap)) { // 如果有字段依赖导致表单项展示与否变化
|
|
143
|
+
//this.setState({shouldAnimation: shouldAnimation(hideMap, newHideMap, objectFields)});
|
|
144
|
+
this.setState({});
|
|
145
|
+
}
|
|
146
|
+
}} parent={this.props.schema} forceValid={this.props.forceValid} style={{ width: "100%" }} />
|
|
147
|
+
|
|
148
|
+
let ele;
|
|
149
|
+
if(uispec.layout === "vertical") { // label在字段上面的分段布局
|
|
150
|
+
ele = <Collapsible {...wrapperProp}>
|
|
151
|
+
<ItemLabel uispec={uispec} schema={f} morph={morph}/>
|
|
152
|
+
{fieldViewer}
|
|
153
|
+
</Collapsible>
|
|
154
|
+
} else if(uispec.layout === "horizontal") { // label在字段左边的分段布局 TODO
|
|
155
|
+
wrapperProp.style.display = "flex";
|
|
156
|
+
ele = <Collapsible {...wrapperProp}>
|
|
157
|
+
<ItemLabel uispec={uispec} schema={f} labelWidth={labelWidth} morph={morph}/>
|
|
158
|
+
<span style={{flex: 1}}>{fieldViewer}</span>
|
|
159
|
+
</Collapsible>
|
|
160
|
+
} else {
|
|
161
|
+
ele = MUtil.error(invalidLayoutMsg);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return <MContext.Consumer key={f.name}>
|
|
165
|
+
{ctx => {
|
|
166
|
+
if (ctx.rootProps.formItemWrapper) {
|
|
167
|
+
return ctx.rootProps.formItemWrapper(ele, f)
|
|
168
|
+
} else {
|
|
169
|
+
return ele
|
|
170
|
+
}
|
|
171
|
+
}}
|
|
172
|
+
</MContext.Consumer>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 分段表单
|
|
177
|
+
* @param objectFields
|
|
178
|
+
* @param uispec
|
|
179
|
+
*/
|
|
180
|
+
private _segmentForm(objectFields: MFieldSchema[], uispec: M3UISpec, column: number) {
|
|
181
|
+
const objectFieldMap = _.chain(objectFields).keyBy('name').value();
|
|
182
|
+
const hideMap = MUtil.hideMap(MUtil.get(this.props.database, this.props.path), objectFields, uispec);
|
|
183
|
+
if (!uispec.segments) {
|
|
184
|
+
return MUtil.error("分段未定义");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 先计算一遍label的宽度,即使有隐藏的字段,确保展示出来时也不会因为label太长布局变化。
|
|
188
|
+
let labelWidth = 0;
|
|
189
|
+
if (uispec.layout === "horizontal") {
|
|
190
|
+
for (let segment of uispec.segments) {
|
|
191
|
+
for (let fieldName of segment.fields) {
|
|
192
|
+
let f = objectFieldMap[fieldName];
|
|
193
|
+
if (!f) {
|
|
194
|
+
return MUtil.error(`segments中的${fieldName}未定义`);
|
|
195
|
+
}
|
|
196
|
+
let label = f.label;
|
|
197
|
+
labelWidth = Math.max(labelWidth, MUtil.antdTextWidth(label ?? ""))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 然后再把表单表格画出来
|
|
203
|
+
const segments = [];
|
|
204
|
+
let fno = 0;
|
|
205
|
+
for (let segment of uispec.segments) {
|
|
206
|
+
const segHide = hideMap["segment:" + segment.label];
|
|
207
|
+
const items = [];
|
|
208
|
+
const segmentFieldNames: string[] = [];
|
|
209
|
+
|
|
210
|
+
for (let fieldName of segment.fields) {
|
|
211
|
+
let f = objectFieldMap[fieldName];
|
|
212
|
+
if (!f) {
|
|
213
|
+
items.push(MUtil.error(`字段不存在:${fieldName}`));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const hideField = segHide || hideMap[f.name];
|
|
218
|
+
|
|
219
|
+
// segment里的字段名都记下来
|
|
220
|
+
segmentFieldNames.push(fieldName);
|
|
221
|
+
|
|
222
|
+
items.push(this.formItem(hideField, hideMap, f, objectFields, uispec,
|
|
223
|
+
((this.props.morph === "editor" || this.state.editing[segment.label]) ? "editor" : "readable"),
|
|
224
|
+
labelWidth, `${segment.label}的layout值无效:${uispec.layout}`, column))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let topRight: React.ReactNode = undefined;
|
|
228
|
+
if (segment.onSubmit) {
|
|
229
|
+
const onSubmit = segment.onSubmit;
|
|
230
|
+
if (this.state.editing[segment.label]) {
|
|
231
|
+
const saving = this.state.saving[segment.label];
|
|
232
|
+
const cancel = () => {
|
|
233
|
+
const prev = this.state.editing[segment.label];
|
|
234
|
+
_.assign(this.props.database, prev);
|
|
235
|
+
delete this.state.editing[segment.label];
|
|
236
|
+
this.setState({});
|
|
237
|
+
}
|
|
238
|
+
topRight = <SegmentEditSwitch state={saving ? "saving" : "editor"} onClick={(e) => {
|
|
239
|
+
if (e) {
|
|
240
|
+
this.state.saving[segment.label] = true; // 先展示loading动画
|
|
241
|
+
onSubmit(segment, _.pick(this.props.database, segmentFieldNames), () => {
|
|
242
|
+
// 保存完了以后移除loading状态,并且改回readable模式
|
|
243
|
+
this.setState({
|
|
244
|
+
saving: _.omit(this.state.saving, segment.label),
|
|
245
|
+
editing: _.omit(this.state.saving, segment.label),
|
|
246
|
+
});
|
|
247
|
+
})
|
|
248
|
+
} else {
|
|
249
|
+
const prev = this.state.editing[segment.label];
|
|
250
|
+
const current = _.pick(this.props.database, segmentFieldNames);
|
|
251
|
+
if (!_.isEqual(prev, current)) {
|
|
252
|
+
Modal.confirm({
|
|
253
|
+
content: '会丢失刚才的修改,确定吗?',
|
|
254
|
+
okText: "刚才的修改不要了",
|
|
255
|
+
cancelText: "继续编辑",
|
|
256
|
+
onOk: cancel
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
cancel();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}} />
|
|
263
|
+
} else {
|
|
264
|
+
topRight = <SegmentEditSwitch state={"readable"} onClick={(e) => {
|
|
265
|
+
this.setState({
|
|
266
|
+
editing: _.set(this.state.editing, segment.label, _.pick(this.props.database, segmentFieldNames))
|
|
267
|
+
});
|
|
268
|
+
}} />
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
segments.push(<Segment column={column} style={{ ...segment.style, display: segHide ? "none" : undefined }} key={fno++} mainTitle={segment.label} labelName={"segment_" + fno} topRight={topRight}>
|
|
273
|
+
{items}
|
|
274
|
+
</Segment>);
|
|
275
|
+
}
|
|
276
|
+
return <div className="AForm">{segments}</div>
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 不分段的表单
|
|
281
|
+
* @param objectFields
|
|
282
|
+
* @param uispec
|
|
283
|
+
* @param column
|
|
284
|
+
*/
|
|
285
|
+
private _flowForm(objectFields: MFieldSchema[], uispec: M3UISpec, column: number) {
|
|
286
|
+
const hideMap = MUtil.hideMap(MUtil.get(this.props.database, this.props.path), objectFields, uispec);
|
|
287
|
+
|
|
288
|
+
// 先计算一遍label的宽度,即使有隐藏的字段,确保展示出来时也不会因为label太长布局变化。
|
|
289
|
+
// table防止折行是保底的
|
|
290
|
+
let labelWidth = 0;
|
|
291
|
+
if (uispec.layout === "horizontal") {
|
|
292
|
+
for (let f of objectFields) {
|
|
293
|
+
let label = f.label;
|
|
294
|
+
labelWidth = Math.max(labelWidth, MUtil.antdTextWidth(label ?? ""))
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 然后再把表单表格画出来
|
|
299
|
+
const items = [];
|
|
300
|
+
for (let f of objectFields) {
|
|
301
|
+
items.push(this.formItem(hideMap[f.name], hideMap, f, objectFields, uispec,
|
|
302
|
+
this.props.morph, labelWidth, `layout值无效:${uispec.layout}`, column
|
|
303
|
+
))
|
|
304
|
+
}
|
|
305
|
+
if (column > 1) {
|
|
306
|
+
return <div className="AForm" style={{
|
|
307
|
+
display: 'flex',
|
|
308
|
+
flexFlow: 'row wrap',
|
|
309
|
+
alignContent: 'flex-start',
|
|
310
|
+
}}>
|
|
311
|
+
{items}
|
|
312
|
+
</div>
|
|
313
|
+
} else {
|
|
314
|
+
return <div className="AForm">{items}</div>
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
element() {
|
|
319
|
+
const objectFields = this.props.schema.objectFields;
|
|
320
|
+
if (!objectFields) {
|
|
321
|
+
return MUtil.error(`表单是空的`, this.props.schema);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const uispec = this.props.schema.uispec ?? { type: "flowForm", layout: "vertical" };
|
|
325
|
+
const column = this.props.schema.column ?? 1;
|
|
326
|
+
switch (this.props.schema.uispec?.type) {
|
|
327
|
+
case "segmentForm": // 分段的表单
|
|
328
|
+
if (!uispec) {
|
|
329
|
+
return MUtil.error(`uispec未定义`);
|
|
330
|
+
}
|
|
331
|
+
return this._segmentForm(objectFields, uispec, column);
|
|
332
|
+
case "flowForm": // 不分段的表单
|
|
333
|
+
return this._flowForm(objectFields, uispec, column);
|
|
334
|
+
case null:
|
|
335
|
+
case undefined:
|
|
336
|
+
default:
|
|
337
|
+
return this._flowForm(objectFields, uispec, column);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Radio } from 'antd';
|
|
4
|
+
import { BaseViewer } from "../../BaseViewer";
|
|
5
|
+
import { MUtil } from "../../../framework/MUtil";
|
|
6
|
+
import { UnderlineInputBox } from '../../widget/UnderlineInputBox';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 类似这样分正负的数字:
|
|
10
|
+
*
|
|
11
|
+
* 与去年同期(2019 年 1 月初-6 月底)相比,您的收入
|
|
12
|
+
* (1)增长了(具体数字:约___万元) (2)下降了(具体数字:约___万元) (3) 基本没有变化
|
|
13
|
+
*/
|
|
14
|
+
export class AIntDiff extends BaseViewer {
|
|
15
|
+
_posInputBoxValue?: number;
|
|
16
|
+
_negInputBoxValue?: number;
|
|
17
|
+
_radio?: number;
|
|
18
|
+
|
|
19
|
+
element() {
|
|
20
|
+
const value = super.getValue();
|
|
21
|
+
const sign = this._radio ?? MUtil.sign(value);
|
|
22
|
+
const radioStyle = {
|
|
23
|
+
display: 'block',
|
|
24
|
+
marginBottom: 3
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return <Radio.Group key={this.state.ctrlVersion} defaultValue={sign} onChange={(v)=>{
|
|
28
|
+
const sign = v.target.value;
|
|
29
|
+
if(sign < 0){
|
|
30
|
+
super.changeValue(this._negInputBoxValue);
|
|
31
|
+
} else if(sign > 0){
|
|
32
|
+
super.changeValue(this._posInputBoxValue);
|
|
33
|
+
} else {
|
|
34
|
+
super.changeValue(0);
|
|
35
|
+
}
|
|
36
|
+
this._radio = sign;
|
|
37
|
+
}}>
|
|
38
|
+
<Radio style={radioStyle} key={1} value={1}>
|
|
39
|
+
{this.props.schema.intDiff?.incLabel}
|
|
40
|
+
<UnderlineInputBox pattern="\d*" type="number" style={{width:"5em"}} min={1} defaultValue={this._posInputBoxValue || undefined } onChange={(v)=> {
|
|
41
|
+
const nv = parseInt(v.target.value);
|
|
42
|
+
if(nv === 0){
|
|
43
|
+
this._posInputBoxValue = undefined;
|
|
44
|
+
super.changeValueEx(nv, true, false)
|
|
45
|
+
this._radio = 0;
|
|
46
|
+
} else {
|
|
47
|
+
this._posInputBoxValue = nv;
|
|
48
|
+
super.changeValue(nv)
|
|
49
|
+
}
|
|
50
|
+
}} disabled={!sign || sign <= 0} onBlur={()=>{
|
|
51
|
+
super.changeValue(value)
|
|
52
|
+
}}/>
|
|
53
|
+
{this.props.schema.intDiff?.incLabelPostfix}
|
|
54
|
+
</Radio>
|
|
55
|
+
<Radio style={radioStyle} key={-1} value={-1}>
|
|
56
|
+
{this.props.schema.intDiff?.decLabel}
|
|
57
|
+
<UnderlineInputBox pattern="\d*" type="number" style={{width:"5em"}} size={10} min={1} defaultValue={this._negInputBoxValue || undefined} onChange={(v)=> {
|
|
58
|
+
const nv = parseInt(v.target.value);
|
|
59
|
+
if(nv === 0){
|
|
60
|
+
this._negInputBoxValue = undefined;
|
|
61
|
+
super.changeValueEx(nv, true, false)
|
|
62
|
+
this._radio = 0;
|
|
63
|
+
} else {
|
|
64
|
+
this._negInputBoxValue = nv;
|
|
65
|
+
super.changeValue(-nv)
|
|
66
|
+
}
|
|
67
|
+
}} disabled={!sign || sign >= 0} onBlur={()=>{
|
|
68
|
+
super.changeValue(value)
|
|
69
|
+
}}/>
|
|
70
|
+
{this.props.schema.intDiff?.decLabelPostfix}
|
|
71
|
+
</Radio>
|
|
72
|
+
<Radio style={radioStyle} key={0} value={0} onClick={()=>super.changeValue(0)}>
|
|
73
|
+
{this.props.schema.intDiff?.keep}
|
|
74
|
+
</Radio>
|
|
75
|
+
</Radio.Group>
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
|
|
2
|
+
import { Radio } from "antd";
|
|
3
|
+
import React, { CSSProperties } from "react";
|
|
4
|
+
import "./AMatrix.less";
|
|
5
|
+
import _ from "lodash";
|
|
6
|
+
import { BaseViewer } from "../../BaseViewer";
|
|
7
|
+
import { CompactArray, MUtil } from "../../../framework/MUtil";
|
|
8
|
+
import { UnderlineInputBox } from "../../widget/UnderlineInputBox";
|
|
9
|
+
import { SelectBox } from "../../widget/SelectBox";
|
|
10
|
+
import { MEnumField } from "../../../framework/Schema";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 单选矩阵.
|
|
14
|
+
* 暂不支持字符串以外的值。
|
|
15
|
+
* 示例:{label:"3.3 您是否愿意与农村人",name:"countryman", type:"matrix", editor:"ARadioMatrix", matrix: {x:"很愿意 比较愿意 不太愿意 很不愿意 不好说", y:"聊天 一起工作 成为邻居 成为亲密朋友 结成亲家"}},
|
|
16
|
+
* 数据:
|
|
17
|
+
* {"聊天":["很愿意", "比较愿意"], "一起工作":"比较愿意"}
|
|
18
|
+
*/
|
|
19
|
+
export class AMatrix extends BaseViewer {
|
|
20
|
+
private _inputBoxValue?: string;
|
|
21
|
+
/**
|
|
22
|
+
* 当某个值被选中时,将其设置到数据上。
|
|
23
|
+
* 例如:
|
|
24
|
+
* let data = {}
|
|
25
|
+
* _setDataForKey(data, "foo", "bar", ["foo"])
|
|
26
|
+
* // data会变成 {foo:bar}
|
|
27
|
+
*
|
|
28
|
+
* let data = {"foo": "bar1"}
|
|
29
|
+
* _setDataForKey(data, "foo", "bar2", ["foo"])
|
|
30
|
+
* // data会变成 {foo:["bar1", "bar2"]}
|
|
31
|
+
*
|
|
32
|
+
* let data = {"foo": "bar1"}
|
|
33
|
+
* _setDataForKey(data, "foo", "bar2", ["foo"])
|
|
34
|
+
* // data会变成 {foo:["bar1", "bar2"]}
|
|
35
|
+
*
|
|
36
|
+
* let data = {"foo": "bar1", "open":"before"}
|
|
37
|
+
* _setDataForKey(data, nil, "bar2", ["foo"])
|
|
38
|
+
* // 假如此时文本框内是foo3
|
|
39
|
+
* // data会变成 {foo:"bar1", foo3:"bar2"}
|
|
40
|
+
*
|
|
41
|
+
* @param data 这个控件的数据,这个函数会讲key/value存入data,例如:{"聊天":["很愿意", "比较愿意"], "一起工作":"比较愿意", "开放-用户填的":"比较愿意"}
|
|
42
|
+
* @param key 字符串表示 nil表示设置开放选项,此时key不固定,以_inputBoxValue为准
|
|
43
|
+
* @param value
|
|
44
|
+
* @param yyClosed 非开放选项的值,传进来免得再计算
|
|
45
|
+
*/
|
|
46
|
+
private _setDataForKey(data:any, key:string, value:string, yyClosed: (string|boolean|number)[]){
|
|
47
|
+
const maxX = this.props.schema.matrix?.maxX ?? 1;
|
|
48
|
+
const maxY = this.props.schema.matrix?.maxY ?? Number.MAX_VALUE;
|
|
49
|
+
|
|
50
|
+
// 预先处理好开放选项
|
|
51
|
+
let prevValues;
|
|
52
|
+
let actualKey;
|
|
53
|
+
if(!key) { // 设置开放选项
|
|
54
|
+
// 先找到原来的开放选项并删除掉,因为key可能会变的
|
|
55
|
+
const prevOpenKey = _openKey(data, yyClosed);
|
|
56
|
+
if(!_.isNil(prevOpenKey)) {
|
|
57
|
+
prevValues = data[prevOpenKey];
|
|
58
|
+
delete data[prevOpenKey];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
actualKey = this._inputBoxValue ?? "";
|
|
62
|
+
if(prevValues) {
|
|
63
|
+
data[actualKey] = prevValues;
|
|
64
|
+
}
|
|
65
|
+
} else { // 设置备选项
|
|
66
|
+
actualKey = key;
|
|
67
|
+
prevValues = data[key];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 再考虑maxX的情况下,设置数据
|
|
71
|
+
switch(typeof prevValues) {
|
|
72
|
+
case "undefined":
|
|
73
|
+
data[actualKey] = value;
|
|
74
|
+
break;
|
|
75
|
+
case "string"://"..."
|
|
76
|
+
if(maxX === 1) {
|
|
77
|
+
data[actualKey] = value;
|
|
78
|
+
} else {
|
|
79
|
+
data[actualKey] = [prevValues, value];
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case "object":// ["...","..."]
|
|
83
|
+
if(prevValues.length >= maxX) {
|
|
84
|
+
data[actualKey].shift();
|
|
85
|
+
}
|
|
86
|
+
data[actualKey] = [...prevValues, value];
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 再考虑maxY,如果超了,要去掉一个勾
|
|
91
|
+
let n = 0;
|
|
92
|
+
for(let key in data) {
|
|
93
|
+
const row = data[key];
|
|
94
|
+
if(row === value || row?.includes(value)){
|
|
95
|
+
n ++;
|
|
96
|
+
if(n >= maxY && key !== actualKey){
|
|
97
|
+
if(data[key] === value){
|
|
98
|
+
delete data[key];
|
|
99
|
+
} else {
|
|
100
|
+
data[key] = MUtil.arrayRemove(data[key], value);
|
|
101
|
+
}
|
|
102
|
+
n --;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 渲染成矩阵
|
|
110
|
+
*/
|
|
111
|
+
_renderAsMatrix(){
|
|
112
|
+
let data = super.getValue() ?? {};
|
|
113
|
+
|
|
114
|
+
// const minX = this.props.schema.matrix?.minX ?? 1;
|
|
115
|
+
// const minY = this.props.schema.matrix?.minY ?? 1;
|
|
116
|
+
|
|
117
|
+
const style = {};
|
|
118
|
+
const xx = MUtil.standardFields(this.props.schema.matrix?.x);
|
|
119
|
+
const yy = MUtil.standardFields(this.props.schema.matrix?.y);
|
|
120
|
+
const yyClosed = yy.map(e=>e.value);
|
|
121
|
+
|
|
122
|
+
if(this.props.schema.matrix?.open) {
|
|
123
|
+
yy.push({value:"", label:this.props.schema.matrix.open.label});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let trs:JSX.Element[] = [];
|
|
127
|
+
let titleRow = [<td key={"空的格子"}></td>];
|
|
128
|
+
for(let x of xx) {
|
|
129
|
+
titleRow.push(<td key={x.value?.toString()} style={style}>{x.label}</td>);
|
|
130
|
+
}
|
|
131
|
+
trs.push(<tr key={"标题"}>{titleRow}</tr>);
|
|
132
|
+
|
|
133
|
+
const openKey = _openKey(data, yyClosed);
|
|
134
|
+
for(let y of yy) {
|
|
135
|
+
let dataRow:JSX.Element[] = [];
|
|
136
|
+
let key: string|undefined = y.value?.toString();
|
|
137
|
+
|
|
138
|
+
if(y.value) { // 候选项
|
|
139
|
+
dataRow.push(<td key={y.value + ":label"}>{y.label}</td>);
|
|
140
|
+
} else { // 开放选项
|
|
141
|
+
dataRow.push(<td key={"open:label"} className="AMatrixOpenTd">
|
|
142
|
+
{y.label}
|
|
143
|
+
<UnderlineInputBox defaultValue={openKey} disabled={_.isNil(openKey)} onChange={(v)=>{
|
|
144
|
+
const openKeyOnClick = _openKey(data, yyClosed);
|
|
145
|
+
const str = v.target.value;
|
|
146
|
+
const matchEnum = yy.find(e=>e.value === str);
|
|
147
|
+
if(matchEnum){ // 不能让用户输入了某个枚举值
|
|
148
|
+
alert("请不要直接输入,您可以勾选 " + (matchEnum.label ?? matchEnum.value)); // TODO 用校验解决此问题,chrome解析不了反撇号
|
|
149
|
+
} else {
|
|
150
|
+
this._inputBoxValue = str;
|
|
151
|
+
super.changeValue(this._inputBoxValue);
|
|
152
|
+
if(!_.isNil(openKeyOnClick)) {
|
|
153
|
+
const prev = data[openKeyOnClick];
|
|
154
|
+
delete data[openKeyOnClick];
|
|
155
|
+
data[this._inputBoxValue] = prev;
|
|
156
|
+
}
|
|
157
|
+
super.changeValue(data);
|
|
158
|
+
}
|
|
159
|
+
}}/>
|
|
160
|
+
</td>);
|
|
161
|
+
|
|
162
|
+
// 开放选项的key不固定
|
|
163
|
+
key = openKey;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for(let x of xx) {
|
|
167
|
+
const checked = CompactArray.indexOf(MUtil.get(data, key), x.value) >= 0; // _.isNil(key) ? false : data[key] === x.value || data[key]?.includes(x.value);
|
|
168
|
+
dataRow.push(<td key={x.value?.toString()} style={style}><Radio key={x.value?.toString()} checked={checked} onChange={(vv)=>{
|
|
169
|
+
if(vv.target.checked) {
|
|
170
|
+
this._setDataForKey(data, y.value?.toString(), x.value?.toString(), yyClosed);
|
|
171
|
+
}
|
|
172
|
+
super.changeValue(data);
|
|
173
|
+
}}/></td>);
|
|
174
|
+
}
|
|
175
|
+
trs.push(<tr key={y.value?.toString()}>{dataRow}</tr>);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return <table className="M3_table" style={{width:"100%", textAlign:"center"}}><tbody>{trs}</tbody></table>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 渲染成一组下拉框
|
|
183
|
+
*/
|
|
184
|
+
_renderAsDropdownList(){
|
|
185
|
+
let data = super.getValue() ?? {};
|
|
186
|
+
const xx = MUtil.standardFields(this.props.schema.matrix?.x);
|
|
187
|
+
const yy = MUtil.standardFields(this.props.schema.matrix?.y);
|
|
188
|
+
|
|
189
|
+
const getLabelLength = (e?:MEnumField) => e ? (e.label ?? e.value?.toString()).length : 0;
|
|
190
|
+
const maxXLen = _.maxBy(xx, getLabelLength);
|
|
191
|
+
const maxYLen = _.maxBy(yy, getLabelLength);
|
|
192
|
+
|
|
193
|
+
const sameLine = (getLabelLength(maxXLen) + getLabelLength(maxYLen)) < 19; // 超过19个字符就要折行
|
|
194
|
+
const toOption = (x:any) => {return {label: x.label??x.value, value: x.value}};
|
|
195
|
+
|
|
196
|
+
let jsx = [];
|
|
197
|
+
|
|
198
|
+
if(this.props.schema.matrix?.minX === 1) { // 每行只能选一个,可以渲染成一组下拉框
|
|
199
|
+
const options = xx.map(toOption);
|
|
200
|
+
for(let y of yy) {
|
|
201
|
+
jsx.push(
|
|
202
|
+
<div key={y.value?.toString()} className={sameLine ? "AMatrix_DropdownList_sameLine" : "AMatrix_DropdownList_wrap"}>
|
|
203
|
+
<span key="label" style={{width: MUtil.antdTextWidth(maxYLen?.label ?? maxXLen?.value?.toString())}}>{y.label ?? y.value}</span>
|
|
204
|
+
<SelectBox key="selector" data={data[y.value?.toString()]} options={options} onChange={(v)=>{
|
|
205
|
+
data[y.value?.toString()] = v;
|
|
206
|
+
super.changeValue(data);
|
|
207
|
+
}}/>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
} else if(this.props.schema.matrix?.minY ===1){
|
|
212
|
+
const options = yy.map(toOption);
|
|
213
|
+
const x2y:any = _.invert(data);
|
|
214
|
+
for(let x of xx) {
|
|
215
|
+
jsx.push(
|
|
216
|
+
<div key={x.value?.toString()} className={sameLine ? "AMatrix_DropdownList_sameLine" : "AMatrix_DropdownList_wrap"}>
|
|
217
|
+
<span key="label" style={{width: MUtil.antdTextWidth(maxXLen?.label)}}>{x.label ?? x.value}</span>
|
|
218
|
+
<SelectBox key="selector" data={x2y[x.value?.toString()]} options={options} onChange={(v)=>{
|
|
219
|
+
x2y[x.value?.toString()] = v;
|
|
220
|
+
super.changeValue(_.invert(x2y));
|
|
221
|
+
}}/>
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return jsx;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
element() {
|
|
230
|
+
let screenAdaption = this.props.schema.screenAdaption ?? (MUtil.phoneLike() ? "phone" : "big");
|
|
231
|
+
|
|
232
|
+
if(screenAdaption === "phone"){ // 手机空间不足,只能各种想办法转换选项形式
|
|
233
|
+
return this._renderAsDropdownList();
|
|
234
|
+
} else {
|
|
235
|
+
return this._renderAsMatrix();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _openKey(data:any, yyClosed: (string|boolean|number)[]){
|
|
241
|
+
return _.first(_.difference(_.keys(data), yyClosed))?.toString();
|
|
242
|
+
}
|