form-driver 0.4.20 → 0.4.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-driver",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "An efficient framework for creating forms.",
5
5
  "license": "MIT",
6
6
  "authors": [
@@ -58,6 +58,9 @@
58
58
  "@ant-design/icons": "^4.3.0",
59
59
  "@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
60
60
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
61
+ "@dnd-kit/core": "^6.3.1",
62
+ "@dnd-kit/sortable": "^10.0.0",
63
+ "@dnd-kit/utilities": "^3.2.2",
61
64
  "@babel/runtime": "^7.9.2",
62
65
  "ali-oss": "^6.17.1",
63
66
  "antd": "4.18.7",
@@ -10,6 +10,8 @@ import {
10
10
  import { JSONSchema6 } from "json-schema";
11
11
  import React from "react";
12
12
  import { SchemaFunc } from "./SchemaFunc";
13
+ import { MDateTimeType } from "../types/MDateTimeType";
14
+ import { getReadableFormat } from "../types/MDateRangeType";
13
15
 
14
16
  export type HideMap = { [fieldName: string]: boolean };
15
17
 
@@ -553,6 +555,149 @@ export let MUtil = {
553
555
  return result;
554
556
  },
555
557
 
558
+ /**
559
+ * 提交时将时间字段转换为可读格式
560
+ * @param schema 表单 schema(type 为 object 的根 schema)
561
+ * @param database 表单数据
562
+ * @returns 深拷贝后转换过的数据
563
+ */
564
+ formatForExport: function (schema: MFieldSchemaAnonymity, database: any): any {
565
+ if (_.isNil(database)) {
566
+ return database;
567
+ }
568
+ const result = _.cloneDeep(database);
569
+ const fields = schema.objectFields;
570
+ if (!fields) {
571
+ return result;
572
+ }
573
+ for (const field of fields) {
574
+ const value = result[field.name];
575
+ if (_.isNil(value)) {
576
+ continue;
577
+ }
578
+ const type = field.type;
579
+ // 时间选择器
580
+ if (type === "year" || type === "yearMonth" || type === "yearMonthDay" || type === "datetime" || type === "date") {
581
+ const antConf = MDateTimeType.antConf(field);
582
+ if (antConf) {
583
+ const m = moment(value, antConf.dataFormat);
584
+ if (m.isValid()) {
585
+ result[field.name] = m.format(antConf.readableFormat);
586
+ }
587
+ }
588
+ }
589
+ // 时间范围选择器
590
+ else if (type === "dateRange") {
591
+ if (_.isArray(value)) {
592
+ const [start, end, tillNow] = value;
593
+ const fmt = getReadableFormat(field.dateRangePrecision, field.dateRange?.showTime);
594
+ const dataFormat = field.dataFormat ?? "x";
595
+ let startStr = "";
596
+ let endStr = "";
597
+ if (!_.isNil(start)) {
598
+ const m = moment(start, dataFormat);
599
+ startStr = m.isValid() ? m.format(fmt) : "";
600
+ }
601
+ if (tillNow) {
602
+ endStr = "至今";
603
+ } else if (!_.isNil(end)) {
604
+ const m = moment(end, dataFormat);
605
+ endStr = m.isValid() ? m.format(fmt) : "";
606
+ }
607
+ result[field.name] = startStr + " - " + endStr;
608
+ }
609
+ }
610
+ // 嵌套 object
611
+ else if (type === "object" && field.objectFields) {
612
+ result[field.name] = MUtil.formatForExport(field, value);
613
+ }
614
+ // 嵌套 array
615
+ else if (type === "array" && field.arrayMember && _.isArray(value)) {
616
+ if (field.arrayMember.objectFields) {
617
+ result[field.name] = value.map((item: any) =>
618
+ MUtil.formatForExport(field.arrayMember!, item)
619
+ );
620
+ }
621
+ }
622
+ }
623
+ return result;
624
+ },
625
+
626
+ /**
627
+ * 回填时将可读格式的时间字段反解析为内部格式
628
+ * @param schema 表单 schema(type 为 object 的根 schema)
629
+ * @param database 可读格式的数据
630
+ * @returns 深拷贝后反解析过的数据
631
+ */
632
+ parseFromExport: function (schema: MFieldSchemaAnonymity, database: any): any {
633
+ if (_.isNil(database)) {
634
+ return database;
635
+ }
636
+ const result = _.cloneDeep(database);
637
+ const fields = schema.objectFields;
638
+ if (!fields) {
639
+ return result;
640
+ }
641
+ for (const field of fields) {
642
+ const value = result[field.name];
643
+ if (_.isNil(value)) {
644
+ continue;
645
+ }
646
+ const type = field.type;
647
+ // 时间选择器
648
+ if (type === "year" || type === "yearMonth" || type === "yearMonthDay" || type === "datetime" || type === "date") {
649
+ const antConf = MDateTimeType.antConf(field);
650
+ if (antConf && _.isString(value)) {
651
+ const m = moment(value, antConf.readableFormat);
652
+ if (m.isValid()) {
653
+ result[field.name] = m.format(antConf.dataFormat);
654
+ }
655
+ }
656
+ }
657
+ // 时间范围选择器
658
+ else if (type === "dateRange") {
659
+ if (_.isString(value)) {
660
+ const fmt = getReadableFormat(field.dateRangePrecision, field.dateRange?.showTime);
661
+ const dataFormat = field.dataFormat ?? "x";
662
+ const parts = value.split(" - ");
663
+ const startStr = parts[0]?.trim();
664
+ const endStr = parts[1]?.trim();
665
+ let start: string | null = null;
666
+ let end: string | null = null;
667
+ let tillNow = false;
668
+ if (startStr) {
669
+ const m = moment(startStr, fmt);
670
+ if (m.isValid()) {
671
+ start = m.format(dataFormat);
672
+ }
673
+ }
674
+ if (endStr === "至今") {
675
+ tillNow = true;
676
+ } else if (endStr) {
677
+ const m = moment(endStr, fmt);
678
+ if (m.isValid()) {
679
+ end = m.format(dataFormat);
680
+ }
681
+ }
682
+ result[field.name] = [start, end, tillNow];
683
+ }
684
+ }
685
+ // 嵌套 object
686
+ else if (type === "object" && field.objectFields) {
687
+ result[field.name] = MUtil.parseFromExport(field, value);
688
+ }
689
+ // 嵌套 array
690
+ else if (type === "array" && field.arrayMember && _.isArray(value)) {
691
+ if (field.arrayMember.objectFields) {
692
+ result[field.name] = value.map((item: any) =>
693
+ MUtil.parseFromExport(field.arrayMember!, item)
694
+ );
695
+ }
696
+ }
697
+ }
698
+ return result;
699
+ },
700
+
556
701
  /** 啥也不干的空回调 */
557
702
  doNothing: function () {},
558
703
 
@@ -6,16 +6,16 @@ import moment from "moment";
6
6
  import _ from "lodash";
7
7
 
8
8
  /** 根据 precision 获取可读格式 */
9
- function getReadableFormat(precision?: string, showTime?: boolean): string {
9
+ export function getReadableFormat(precision?: string, showTime?: boolean): string {
10
10
  switch (precision) {
11
11
  case "year":
12
- return "YYYY";
12
+ return "YYYY";
13
13
  case "month":
14
- return "YYYYMM";
14
+ return "YYYY.MM";
15
15
  case "minute":
16
- return "YYYYMMDD HH:mm";
16
+ return "YYYY.MM.DD HH:mm";
17
17
  default:
18
- return showTime ? "YYYYMMDD HH:mm:ss" : "YYYYMMDD";
18
+ return showTime ? "YYYY.MM.DD HH:mm:ss" : "YYYY.MM.DD";
19
19
  }
20
20
  }
21
21
 
@@ -39,11 +39,11 @@ export function timeRangeExpr(
39
39
  from: number | string,
40
40
  to: number | string,
41
41
  tillNow: boolean,
42
- readableFormat: string = "YYYYMMDD",
42
+ readableFormat: string = "YYYY.MM.DD",
43
43
  ): string {
44
44
  return (
45
45
  timeExpr(assembly, from, false, readableFormat) +
46
- " ~ " +
46
+ " - " +
47
47
  timeExpr(assembly, to, tillNow, readableFormat)
48
48
  );
49
49
  }
@@ -31,17 +31,17 @@ export const MDateTimeType: MType & {
31
31
  case "yearMonth":
32
32
  mode = "month";
33
33
  dataFormat = dataFormat ?? "YYYYMM";
34
- readableFormat = "YYYY-MM";
34
+ readableFormat = "YYYY.MM";
35
35
  break;
36
36
  case "yearMonthDay":
37
37
  mode = "date";
38
38
  dataFormat = dataFormat ?? "YYYYMMDD";
39
- readableFormat = "YYYY-MM-DD";
39
+ readableFormat = "YYYY.MM.DD";
40
40
  break;
41
41
  case "datetime":
42
42
  mode = undefined;
43
43
  dataFormat = dataFormat ?? "x";
44
- readableFormat = "YYYY-MM-DD HH:mm";
44
+ readableFormat = "YYYY.MM.DD HH:mm";
45
45
  showTime = true;
46
46
  break;
47
47
  case "date":
@@ -54,19 +54,19 @@ export const MDateTimeType: MType & {
54
54
  case "month":
55
55
  mode = "month";
56
56
  dataFormat = dataFormat ?? "YYYYMM";
57
- readableFormat = "YYYY-MM";
57
+ readableFormat = "YYYY.MM";
58
58
  break;
59
59
  case "minute":
60
60
  mode = undefined;
61
61
  dataFormat = dataFormat ?? "x";
62
- readableFormat = "YYYY-MM-DD HH:mm";
62
+ readableFormat = "YYYY.MM.DD HH:mm";
63
63
  showTime = true;
64
64
  break;
65
65
  default:
66
66
  // "day" 或未指定
67
67
  mode = "date";
68
68
  dataFormat = dataFormat ?? "YYYYMMDD";
69
- readableFormat = "YYYY-MM-DD";
69
+ readableFormat = "YYYY.MM.DD";
70
70
  break;
71
71
  }
72
72
  break;
@@ -78,7 +78,9 @@ export class ARangePicker extends Viewer<State> {
78
78
  */
79
79
  _data2rangePicker(d: ARangePickerData): AntData {
80
80
  const dataFormat = this.props.schema.dataFormat ?? "x";
81
- return [d[0] ? moment(d[0], dataFormat) : null, d[1] ? moment(d[1], dataFormat) : null];
81
+ // tillNow=true,结束时间传 null,避免 RangePicker 缓存旧结束时间
82
+ const endTime = d[2] === true ? null : (d[1] ? moment(d[1], dataFormat) : null);
83
+ return [d[0] ? moment(d[0], dataFormat) : null, endTime];
82
84
  }
83
85
 
84
86
  element() {
@@ -118,25 +120,38 @@ export class ARangePicker extends Viewer<State> {
118
120
  key={`end_${mobilePrecision}`}
119
121
  visible={this.state.mobileDlg && this.state.mobileStep === 'end'}
120
122
  precision={mobilePrecision}
121
- title="选择结束日期"
122
- tillNow={!this.props.schema.dateRange?.hideTillNow && !this.props.schema.dateRange?.showTime}
123
+ title={
124
+ // 如果允许"至今"且开始时间不在未来,在标题区域展示"至今"按钮
125
+ !this.props.schema.dateRange?.hideTillNow
126
+ && !this.props.schema.dateRange?.showTime
127
+ && !(this.state.mobileStartDate && this.state.mobileStartDate > new Date())
128
+ ? <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
129
+ <span>选择结束日期</span>
130
+ <AntButton
131
+ size="small"
132
+ type="primary"
133
+ onClick={() => {
134
+ const startDate = this.state.mobileStartDate;
135
+ if (startDate) {
136
+ super.changeValueEx(
137
+ this._rangePicker2Data([moment(startDate), moment()], true),
138
+ true, true
139
+ );
140
+ }
141
+ this.setState({ mobileDlg: false });
142
+ }}
143
+ >至今</AntButton>
144
+ </div>
145
+ : "选择结束日期"
146
+ }
123
147
  min={this.state.mobileStartDate || (this.props.schema.min ? new Date(this.props.schema.min) : undefined)}
124
148
  max={this.props.schema.max ? new Date(this.props.schema.max) : undefined}
125
149
  onConfirm={(val: any) => {
126
150
  const startDate = this.state.mobileStartDate;
127
151
  if (startDate) {
128
- const isTillNow = !!(val as any).tillNow;
129
- if (isTillNow) {
130
- // 用户选择了"至今"
131
- super.changeValueEx(
132
- this._rangePicker2Data([moment(startDate), moment()], true),
133
- true, true
134
- );
135
- } else {
136
- // 防御:结束日期不能早于开始日期
137
- const finalEnd = val < startDate ? startDate : val;
138
- super.changeValueEx(this._rangePicker2Data([moment(startDate), moment(finalEnd)], false), true, true);
139
- }
152
+ // 防御:结束日期不能早于开始日期
153
+ const finalEnd = val < startDate ? startDate : val;
154
+ super.changeValueEx(this._rangePicker2Data([moment(startDate), moment(finalEnd)], false), true, true);
140
155
  }
141
156
  this.setState({ mobileDlg: false });
142
157
  }}
@@ -159,7 +174,8 @@ export class ARangePicker extends Viewer<State> {
159
174
  } else if (precision === 'month') {
160
175
  extraProps.picker = 'month';
161
176
  } else if (pcShowTime) {
162
- extraProps.showTime = true;
177
+ // precision 为 minute 时只展示时分,不展示秒
178
+ extraProps.showTime = precision === 'minute' ? { format: 'HH:mm' } : true;
163
179
  }
164
180
 
165
181
  // 构造元素
@@ -168,13 +184,20 @@ export class ARangePicker extends Viewer<State> {
168
184
  key={`${this.state.ctrlVersion}_${this.props.schema.dateRangePrecision ?? 'day'}`}
169
185
  renderExtraFooter={hideFooter
170
186
  ? undefined
171
- : (mode) => <div style={{ textAlign: "right" }}>
172
- <AntButton
173
- size="small" style={{ width: "100px", display: "inline-block", marginTop: "5px" }}
174
- onClick={() => {
175
- super.changeValueEx(this._rangePicker2Data(this._onCalendarChangeValue, true), true, true);
176
- }}>至今</AntButton>
177
- </div>
187
+ : (mode) => {
188
+ // 如果开始时间超过当前时间(未来时间),不展示"至今"按钮
189
+ const startMoment = this._onCalendarChangeValue?.[0];
190
+ if (startMoment && startMoment.isAfter(moment())) {
191
+ return null;
192
+ }
193
+ return <div style={{ textAlign: "right" }}>
194
+ <AntButton
195
+ size="small" style={{ width: "100px", display: "inline-block", marginTop: "5px" }}
196
+ onClick={() => {
197
+ super.changeValueEx(this._rangePicker2Data(this._onCalendarChangeValue, true), true, true);
198
+ }}>至今</AntButton>
199
+ </div>;
200
+ }
178
201
  }
179
202
  bordered={this.props.hideBorder ? false : true}
180
203
  style={{ width: "300px" }}
@@ -184,7 +207,16 @@ export class ARangePicker extends Viewer<State> {
184
207
  this._onCalendarChangeValue = d;
185
208
  }}
186
209
  onChange={(vv) => {
187
- super.changeValueEx(this._rangePicker2Data(vv, false), true, true)
210
+ // 用户清空日期范围时,直接置空
211
+ if (!vv) {
212
+ super.changeValueEx(undefined, true, true);
213
+ return;
214
+ }
215
+ const currentData = super.getValue();
216
+ const isTillNow = _.get(currentData, '[2]') === true;
217
+ // 若当前是"至今"且用户只改了开始时间(结束时间仍为 null),保留 tillNow
218
+ const newTillNow = isTillNow && vv[1] == null;
219
+ super.changeValueEx(this._rangePicker2Data(vv, newTillNow), true, true);
188
220
  }}
189
221
  {...extraProps}
190
222
  />;
@@ -21,6 +21,8 @@
21
21
  border-radius: 6px;
22
22
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
23
23
  transition: all 0.3s ease;
24
+ user-select: none;
25
+ -webkit-user-select: none;
24
26
 
25
27
  // 水平方向布局
26
28
  &.direction-horizontal {
@@ -99,6 +101,31 @@
99
101
  }
100
102
  }
101
103
  }
104
+
105
+ // 移动端拖拽句柄触控区域增大
106
+ .enhanced-dragHandleWrapper {
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ min-width: 44px;
111
+ min-height: 44px;
112
+ cursor: grab;
113
+ margin-left: 4px;
114
+ touch-action: none;
115
+ -webkit-touch-callout: none;
116
+
117
+ &:active {
118
+ .enhanced-dragHandleIcon {
119
+ color: #1890ff;
120
+ }
121
+ }
122
+ }
123
+
124
+ .enhanced-dragHandleIcon {
125
+ font-size: 20px;
126
+ color: #999;
127
+ transition: color 0.2s;
128
+ }
102
129
  }
103
130
 
104
131
  // 拖拽动画
@@ -140,3 +167,37 @@
140
167
  }
141
168
  }
142
169
  }
170
+
171
+ // ========== DragOverlay 预览 — 简洁浮动卡片 ==========
172
+ .enhanced-sortDrag-overlay {
173
+ background: #ffffff;
174
+ border-radius: 8px;
175
+ box-shadow:
176
+ 0 1px 3px rgba(0, 0, 0, 0.08),
177
+ 0 8px 24px rgba(0, 0, 0, 0.12);
178
+ border: 1px solid rgba(0, 0, 0, 0.06);
179
+
180
+ // 普通模式预览
181
+ .enhanced-sortDrag-overlay-content {
182
+ padding: 10px 16px;
183
+
184
+ .enhanced-sortDrag-overlay-body {
185
+ font-size: 14px;
186
+ color: #262626;
187
+ }
188
+ }
189
+
190
+ // 表格行模式预览
191
+ .enhanced-sortDrag-overlay-row {
192
+ display: flex;
193
+ align-items: center;
194
+ padding: 10px 16px;
195
+ gap: 8px;
196
+
197
+ .enhanced-sortDrag-overlay-label {
198
+ font-size: 14px;
199
+ color: #262626;
200
+ font-weight: 500;
201
+ }
202
+ }
203
+ }