form-driver 0.4.25 → 0.4.27

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.
@@ -1,17 +1,16 @@
1
1
  import React, { Component, RefObject } from "react";
2
2
  import { DatePicker } from "antd";
3
- import zhCN from 'antd/lib/date-picker/locale/zh_CN';
3
+ import zhCN from "antd/lib/date-picker/locale/zh_CN";
4
4
  import moment from "moment";
5
5
  import { DatePicker as DatePickerM } from "antd-mobile";
6
6
  import { Button as AntButton } from "antd";
7
- import ReactDOM from "react-dom";
8
7
  import { RangePickerProps } from "antd/lib/date-picker";
9
8
  import _ from "lodash";
10
- import { Viewer, ViewerState } from '../../BaseViewer';
11
- import { MUtil } from '../../../framework/MUtil';
9
+ import { Viewer, ViewerState } from "../../BaseViewer";
10
+ import { MUtil } from "../../../framework/MUtil";
12
11
  import { MProp } from "../../../framework/Schema";
13
- import { MDateRangeType } from '../../../types/MDateRangeType';
14
- import { assembly } from '../../../framework/Assembly';
12
+ import { MDateRangeType } from "../../../types/MDateRangeType";
13
+ import { assembly } from "../../../framework/Assembly";
15
14
 
16
15
  export type ARangePickerData = [
17
16
  // 开始时间
@@ -19,25 +18,31 @@ export type ARangePickerData = [
19
18
  // 结束时间
20
19
  string | null | undefined,
21
20
  // 是否至今,如果true,结束时间是无效的
22
- boolean | null | undefined
21
+ boolean | null | undefined,
23
22
  ];
24
23
 
25
24
  type AntData = [moment.Moment | null, moment.Moment | null];
26
25
 
27
26
  interface State extends ViewerState {
28
27
  mobileDlg: boolean;
29
- mobileStep: 'start' | 'end';
28
+ mobileStep: "start" | "end";
30
29
  mobileStartDate?: Date;
31
30
  }
32
31
 
33
32
  export class ARangePicker extends Viewer<State> {
34
- _pickerRef: RefObject<any> = React.createRef();
33
+ _pickerRef: RefObject<HTMLDivElement> = React.createRef();
35
34
  _onCalendarChangeValue?: AntData | null;
35
+ _panelClickedDate?: moment.Moment | null; // showTime 模式下通过面板点击事件捕获的日期
36
36
  _startConfirmed = false; // 标记开始日期是否已确认,用于区分 onClose 是取消还是确认后的自动触发
37
37
 
38
38
  constructor(p: MProp) {
39
39
  super(p);
40
- this.state = { ctrlVersion: 1, noValidate: false, mobileDlg: false, mobileStep: 'start' };
40
+ this.state = {
41
+ ctrlVersion: 1,
42
+ noValidate: false,
43
+ mobileDlg: false,
44
+ mobileStep: "start",
45
+ };
41
46
  }
42
47
 
43
48
  componentDidUpdate() {
@@ -50,21 +55,27 @@ export class ARangePicker extends Viewer<State> {
50
55
 
51
56
  _patchTillnow() {
52
57
  const v = super.getValue();
53
- if (_.get(v, "[2]")) { // tillnow
54
- const dom = ReactDOM.findDOMNode(this._pickerRef.current);
58
+ if (_.get(v, "[2]")) {
59
+ // tillnow
60
+ const dom = this._pickerRef.current;
55
61
  if (dom) {
56
- // @ts-ignore
57
62
  let r = dom.querySelector(":nth-child(3)");
58
- r.innerHTML = "<input readonly disabled size='12' autocomplete='off' value='至今' style='color: black'>";
63
+ if (r) {
64
+ r.innerHTML =
65
+ "<input readonly disabled size='12' autocomplete='off' value='至今' style='color: black'>";
66
+ }
59
67
  }
60
68
  }
61
69
  }
62
70
 
63
71
  /**
64
72
  * RangePicker的数据转换成json上的数据类型
65
- * @param r
73
+ * @param r
66
74
  */
67
- _rangePicker2Data(v: AntData | null | undefined, tillNow: boolean): ARangePickerData | undefined {
75
+ _rangePicker2Data(
76
+ v: AntData | null | undefined,
77
+ tillNow: boolean,
78
+ ): ARangePickerData | undefined {
68
79
  if (!v) {
69
80
  return undefined;
70
81
  }
@@ -74,12 +85,13 @@ export class ARangePicker extends Viewer<State> {
74
85
 
75
86
  /**
76
87
  * json上的数据转换成RangePicker的数据
77
- * @param d
88
+ * @param d
78
89
  */
79
90
  _data2rangePicker(d: ARangePickerData): AntData {
80
91
  const dataFormat = this.props.schema.dataFormat ?? "x";
81
92
  // 若 tillNow=true,结束时间传 null,避免 RangePicker 缓存旧结束时间
82
- const endTime = d[2] === true ? null : (d[1] ? moment(d[1], dataFormat) : null);
93
+ const endTime =
94
+ d[2] === true ? null : d[1] ? moment(d[1], dataFormat) : null;
83
95
  return [d[0] ? moment(d[0], dataFormat) : null, endTime];
84
96
  }
85
97
 
@@ -87,45 +99,76 @@ export class ARangePicker extends Viewer<State> {
87
99
  const p = this.props.schema.props ?? {};
88
100
  let rangePickerData = this._data2rangePicker(this.getValue() ?? []);
89
101
  if (MUtil.phoneLike()) {
90
- let show = MDateRangeType.toReadableN(assembly, this.props.schema, super.getValue());
102
+ let show = MDateRangeType.toReadableN(
103
+ assembly,
104
+ this.props.schema,
105
+ super.getValue(),
106
+ );
91
107
 
92
108
  // 根据 precision 配置确定移动端 DatePicker 精度(使用扁平化属性 dateRangePrecision)
93
- const mobilePrecision = (this.props.schema.dateRangePrecision || 'day') as 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second';
94
-
95
- return <>
96
- <div className="backfill" onClick={() => this.setState({ mobileDlg: true, mobileStep: 'start' })}> {show ?? '请点击选择'} </div>
97
- {/* 移动端:使用两步 DatePickerM 选择开始和结束日期 */}
98
- <DatePickerM
99
- key={`start_${mobilePrecision}`}
100
- visible={this.state.mobileDlg && this.state.mobileStep === 'start'}
101
- precision={mobilePrecision}
102
- title="选择开始日期"
103
- min={this.props.schema.min ? new Date(this.props.schema.min) : undefined}
104
- max={this.props.schema.max ? new Date(this.props.schema.max) : undefined}
105
- onConfirm={(val) => {
106
- this._startConfirmed = true;
107
- this.setState({ mobileStartDate: val, mobileStep: 'end' });
108
- }}
109
- onClose={() => {
110
- // antd-mobile v5 确认时会同时触发 onConfirm 和 onClose
111
- // 用实例变量同步判断:确认后的 onClose 应忽略,仅用户主动取消时才关闭
112
- if (this._startConfirmed) {
113
- this._startConfirmed = false;
114
- return;
109
+ const mobilePrecision = (this.props.schema.dateRangePrecision ||
110
+ "day") as "year" | "month" | "day" | "hour" | "minute" | "second";
111
+
112
+ return (
113
+ <>
114
+ <div
115
+ className="backfill"
116
+ onClick={() =>
117
+ this.setState({ mobileDlg: true, mobileStep: "start" })
118
+ }
119
+ >
120
+ {" "}
121
+ {show ?? "请点击选择"}{" "}
122
+ </div>
123
+ {/* 移动端:使用两步 DatePickerM 选择开始和结束日期 */}
124
+ <DatePickerM
125
+ key={`start_${mobilePrecision}`}
126
+ visible={this.state.mobileDlg && this.state.mobileStep === "start"}
127
+ precision={mobilePrecision}
128
+ title="选择开始日期"
129
+ min={
130
+ this.props.schema.min
131
+ ? new Date(this.props.schema.min)
132
+ : undefined
133
+ }
134
+ max={
135
+ this.props.schema.max
136
+ ? new Date(this.props.schema.max)
137
+ : undefined
115
138
  }
116
- this.setState({ mobileDlg: false });
117
- }}
118
- />
119
- <DatePickerM
120
- key={`end_${mobilePrecision}`}
121
- visible={this.state.mobileDlg && this.state.mobileStep === 'end'}
122
- precision={mobilePrecision}
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 }}>
139
+ onConfirm={(val) => {
140
+ this._startConfirmed = true;
141
+ this.setState({ mobileStartDate: val, mobileStep: "end" });
142
+ }}
143
+ onClose={() => {
144
+ // antd-mobile v5 确认时会同时触发 onConfirm 和 onClose
145
+ // 用实例变量同步判断:确认后的 onClose 应忽略,仅用户主动取消时才关闭
146
+ if (this._startConfirmed) {
147
+ this._startConfirmed = false;
148
+ return;
149
+ }
150
+ this.setState({ mobileDlg: false });
151
+ }}
152
+ />
153
+ <DatePickerM
154
+ key={`end_${mobilePrecision}`}
155
+ visible={this.state.mobileDlg && this.state.mobileStep === "end"}
156
+ precision={mobilePrecision}
157
+ title={
158
+ // 如果允许"至今"且开始时间不在未来,在标题区域展示"至今"按钮
159
+ !this.props.schema.dateRange?.hideTillNow &&
160
+ !(
161
+ this.state.mobileStartDate &&
162
+ this.state.mobileStartDate > new Date()
163
+ ) ? (
164
+ <div
165
+ style={{
166
+ display: "flex",
167
+ alignItems: "center",
168
+ justifyContent: "center",
169
+ gap: 12,
170
+ }}
171
+ >
129
172
  <span>选择结束日期</span>
130
173
  <AntButton
131
174
  size="small"
@@ -134,96 +177,252 @@ export class ARangePicker extends Viewer<State> {
134
177
  const startDate = this.state.mobileStartDate;
135
178
  if (startDate) {
136
179
  super.changeValueEx(
137
- this._rangePicker2Data([moment(startDate), moment()], true),
138
- true, true
180
+ this._rangePicker2Data(
181
+ [moment(startDate), moment()],
182
+ true,
183
+ ),
184
+ true,
185
+ true,
139
186
  );
140
187
  }
141
188
  this.setState({ mobileDlg: false });
142
189
  }}
143
- >至今</AntButton>
190
+ >
191
+ 至今
192
+ </AntButton>
144
193
  </div>
145
- : "选择结束日期"
146
- }
147
- min={this.state.mobileStartDate || (this.props.schema.min ? new Date(this.props.schema.min) : undefined)}
148
- max={this.props.schema.max ? new Date(this.props.schema.max) : undefined}
149
- onConfirm={(val: any) => {
150
- const startDate = this.state.mobileStartDate;
151
- if (startDate) {
152
- // 防御:结束日期不能早于开始日期
153
- const finalEnd = val < startDate ? startDate : val;
154
- super.changeValueEx(this._rangePicker2Data([moment(startDate), moment(finalEnd)], false), true, true);
194
+ ) : (
195
+ "选择结束日期"
196
+ )
155
197
  }
156
- this.setState({ mobileDlg: false });
157
- }}
158
- onClose={() => {
159
- // 回退到第一步,让用户可以重新选择开始日期
160
- this.setState({ mobileStep: 'start' });
161
- }}
162
- />
163
- </>
198
+ min={
199
+ this.state.mobileStartDate ||
200
+ (this.props.schema.min
201
+ ? new Date(this.props.schema.min)
202
+ : undefined)
203
+ }
204
+ max={
205
+ this.props.schema.max
206
+ ? new Date(this.props.schema.max)
207
+ : undefined
208
+ }
209
+ onConfirm={(val: any) => {
210
+ const startDate = this.state.mobileStartDate;
211
+ if (startDate) {
212
+ // 防御:结束日期不能早于开始日期
213
+ const finalEnd = val < startDate ? startDate : val;
214
+ super.changeValueEx(
215
+ this._rangePicker2Data(
216
+ [moment(startDate), moment(finalEnd)],
217
+ false,
218
+ ),
219
+ true,
220
+ true,
221
+ );
222
+ }
223
+ this.setState({ mobileDlg: false });
224
+ }}
225
+ onClose={() => {
226
+ // 回退到第一步,让用户可以重新选择开始日期
227
+ this.setState({ mobileStep: "start" });
228
+ }}
229
+ />
230
+ </>
231
+ );
164
232
  } else {
165
233
  // 根据 precision 配置确定 PC 端 picker 模式和 showTime(使用扁平化属性 dateRangePrecision)
166
234
  const precision = this.props.schema.dateRangePrecision;
167
- const pcShowTime = precision === 'minute' || this.props.schema.dateRange?.showTime;
168
- const hideFooter = this.props.schema.dateRange?.hideTillNow || pcShowTime;
235
+ const pcShowTime =
236
+ precision === "minute" || this.props.schema.dateRange?.showTime;
237
+ const hideFooter = this.props.schema.dateRange?.hideTillNow;
169
238
 
170
239
  // 动态构建额外属性,避免 showTime 和 picker 同时传入导致类型冲突
171
240
  const extraProps: any = {};
172
- if (precision === 'year') {
173
- extraProps.picker = 'year';
174
- } else if (precision === 'month') {
175
- extraProps.picker = 'month';
241
+ if (precision === "year") {
242
+ extraProps.picker = "year";
243
+ } else if (precision === "month") {
244
+ extraProps.picker = "month";
176
245
  } else if (pcShowTime) {
177
246
  // precision 为 minute 时只展示时分,不展示秒
178
- extraProps.showTime = precision === 'minute' ? { format: 'HH:mm' } : true;
247
+ extraProps.showTime =
248
+ precision === "minute" ? { format: "HH:mm" } : true;
179
249
  // 同步设置输入框的显示格式,避免 showTime.format 只影响面板列而输入框仍显示秒
180
- if (precision === 'minute') {
181
- extraProps.format = 'YYYY-MM-DD HH:mm';
250
+ if (precision === "minute") {
251
+ extraProps.format = "YYYY-MM-DD HH:mm";
182
252
  }
183
253
  }
184
254
 
255
+ // "至今"按钮的点击处理函数
256
+ const handleTillNow = () => {
257
+ // 优先级:onCalendarChange 记录的值 > 面板点击捕获的日期 > 已有默认值 > 当前时间
258
+ const fromCalendarChange = this._onCalendarChangeValue?.[0];
259
+ const fromPanelClick = this._panelClickedDate;
260
+ let startMoment =
261
+ fromCalendarChange ??
262
+ fromPanelClick ??
263
+ rangePickerData?.[0] ??
264
+ moment();
265
+
266
+ // 如果开始时间来自面板点击(只有日期没有时间),将当前时刻的时分附加上去
267
+ if (!fromCalendarChange && fromPanelClick) {
268
+ const now = moment();
269
+ startMoment = startMoment
270
+ .clone()
271
+ .hour(now.hour())
272
+ .minute(now.minute())
273
+ .second(0);
274
+ }
275
+
276
+ super.changeValueEx(
277
+ this._rangePicker2Data([startMoment, moment()], true),
278
+ true,
279
+ true,
280
+ );
281
+ };
282
+
283
+ // 判断是否应展示"至今"按钮
284
+ const showTillNow = !hideFooter;
285
+
185
286
  // 构造元素
186
- return <DatePicker.RangePicker
187
- ref={this._pickerRef}
188
- key={`${this.state.ctrlVersion}_${this.props.schema.dateRangePrecision ?? 'day'}`}
189
- renderExtraFooter={hideFooter
190
- ? undefined
191
- : (mode) => {
192
- // 如果开始时间超过当前时间(未来时间),不展示"至今"按钮
193
- const startMoment = this._onCalendarChangeValue?.[0];
194
- if (startMoment && startMoment.isAfter(moment())) {
195
- return null;
287
+ return (
288
+ <div ref={this._pickerRef}>
289
+ <DatePicker.RangePicker
290
+ key={`${this.state.ctrlVersion}_${
291
+ this.props.schema.dateRangePrecision ?? "day"
292
+ }`}
293
+ panelRender={
294
+ pcShowTime && showTillNow
295
+ ? (panelNode) => {
296
+ // showTime 模式下:通过事件委托捕获面板上日期单元格的点击
297
+ // 同时在 footer 的"确定"按钮同一行注入"至今"按钮
298
+ return (
299
+ <div
300
+ onClick={(e) => {
301
+ const target = e.target as HTMLElement;
302
+ const cell = target.closest?.(".ant-picker-cell");
303
+ if (cell) {
304
+ // 排除 disabled 状态的日期单元格
305
+ if (
306
+ cell.classList.contains(
307
+ "ant-picker-cell-disabled",
308
+ )
309
+ )
310
+ return;
311
+ const title = cell.getAttribute("title");
312
+ if (title) {
313
+ const parsed = moment(title, "YYYY-MM-DD");
314
+ // 校验 moment 解析有效性,无效日期不记录
315
+ if (parsed.isValid()) {
316
+ this._panelClickedDate = parsed;
317
+ }
318
+ }
319
+ }
320
+ }}
321
+ ref={(el) => {
322
+ // 面板渲染后,将"至今"按钮注入到 .ant-picker-ok 内部,确定按钮之后
323
+ // 这样两者在同一个 flex item 中,确定在左、至今在右
324
+ if (!el) return;
325
+ const okLi = el.querySelector(".ant-picker-ok");
326
+ if (!okLi) return;
327
+
328
+ // 开始时间在未来时,移除已有的至今按钮并不再注入
329
+ const previewStart =
330
+ this._onCalendarChangeValue?.[0] ??
331
+ this._panelClickedDate ??
332
+ rangePickerData?.[0];
333
+ if (previewStart && previewStart.isAfter(moment())) {
334
+ const existingBtn =
335
+ okLi.querySelector(".till-now-btn");
336
+ if (existingBtn) existingBtn.remove();
337
+ return;
338
+ }
339
+
340
+ if (!okLi.querySelector(".till-now-btn")) {
341
+ const tillNowBtn = document.createElement("button");
342
+ tillNowBtn.className =
343
+ "ant-btn ant-btn-sm till-now-btn";
344
+ tillNowBtn.textContent = "至今";
345
+ tillNowBtn.style.cssText = "margin-left: 8px;";
346
+ // 使用具名函数以便于清理;先移除可能的旧监听再添加,防止重复绑定
347
+ const onTillNowClick = (e: Event) => {
348
+ e.stopPropagation();
349
+ handleTillNow();
350
+ };
351
+ tillNowBtn.addEventListener(
352
+ "click",
353
+ onTillNowClick,
354
+ );
355
+ okLi.appendChild(tillNowBtn);
356
+ }
357
+ }}
358
+ >
359
+ {panelNode}
360
+ </div>
361
+ );
362
+ }
363
+ : undefined
196
364
  }
197
- return <div style={{ textAlign: "right" }}>
198
- <AntButton
199
- size="small" style={{ width: "100px", display: "inline-block", marginTop: "5px" }}
200
- onClick={() => {
201
- super.changeValueEx(this._rangePicker2Data(this._onCalendarChangeValue, true), true, true);
202
- }}>至今</AntButton>
203
- </div>;
204
- }
205
- }
206
- bordered={this.props.hideBorder ? false : true}
207
- style={{ width: "300px" }}
208
- locale={zhCN}
209
- defaultValue={rangePickerData}
210
- onCalendarChange={(d) => {
211
- this._onCalendarChangeValue = d;
212
- }}
213
- onChange={(vv) => {
214
- // 用户清空日期范围时,直接置空
215
- if (!vv) {
216
- super.changeValueEx(undefined, true, true);
217
- return;
218
- }
219
- const currentData = super.getValue();
220
- const isTillNow = _.get(currentData, '[2]') === true;
221
- // 若当前是"至今"且用户只改了开始时间(结束时间仍为 null),保留 tillNow
222
- const newTillNow = isTillNow && vv[1] == null;
223
- super.changeValueEx(this._rangePicker2Data(vv, newTillNow), true, true);
224
- }}
225
- {...extraProps}
226
- />;
365
+ renderExtraFooter={
366
+ !pcShowTime && showTillNow
367
+ ? (mode) => {
368
+ // showTime 模式:使用 renderExtraFooter 展示"至今"按钮
369
+ const previewStart =
370
+ this._onCalendarChangeValue?.[0] ?? rangePickerData?.[0];
371
+ if (previewStart && previewStart.isAfter(moment())) {
372
+ return null;
373
+ }
374
+ return (
375
+ <div style={{ textAlign: "right" }}>
376
+ <AntButton
377
+ size="small"
378
+ style={{
379
+ width: "100px",
380
+ display: "inline-block",
381
+ marginTop: "5px",
382
+ }}
383
+ onClick={handleTillNow}
384
+ >
385
+ 至今
386
+ </AntButton>
387
+ </div>
388
+ );
389
+ }
390
+ : undefined
391
+ }
392
+ bordered={this.props.hideBorder ? false : true}
393
+ style={{ width: "360px" }}
394
+ locale={zhCN}
395
+ defaultValue={rangePickerData}
396
+ onCalendarChange={(d) => {
397
+ this._onCalendarChangeValue = d;
398
+ // 用户开始新一轮选择时清理面板点击缓存,避免上一轮过时数据污染
399
+ this._panelClickedDate = null;
400
+ }}
401
+ onOpenChange={(open) => {
402
+ if (!open) {
403
+ this._panelClickedDate = null;
404
+ }
405
+ }}
406
+ onChange={(vv) => {
407
+ // 用户清空日期范围时,直接置空
408
+ if (!vv) {
409
+ super.changeValueEx(undefined, true, true);
410
+ return;
411
+ }
412
+ const currentData = super.getValue();
413
+ const isTillNow = _.get(currentData, "[2]") === true;
414
+ // 若当前是"至今"且用户只改了开始时间(结束时间仍为 null),保留 tillNow
415
+ const newTillNow = isTillNow && vv[1] == null;
416
+ super.changeValueEx(
417
+ this._rangePicker2Data(vv, newTillNow),
418
+ true,
419
+ true,
420
+ );
421
+ }}
422
+ {...extraProps}
423
+ />
424
+ </div>
425
+ );
227
426
  }
228
427
  }
229
428
  }
@@ -1,6 +1,6 @@
1
1
  import { RefObject } from "react";
2
2
  import moment from "moment";
3
- import { Viewer, ViewerState } from '../../BaseViewer';
3
+ import { Viewer, ViewerState } from "../../BaseViewer";
4
4
  import { MProp } from "../../../framework/Schema";
5
5
  export type ARangePickerData = [
6
6
  string | null | undefined,
@@ -10,12 +10,13 @@ export type ARangePickerData = [
10
10
  type AntData = [moment.Moment | null, moment.Moment | null];
11
11
  interface State extends ViewerState {
12
12
  mobileDlg: boolean;
13
- mobileStep: 'start' | 'end';
13
+ mobileStep: "start" | "end";
14
14
  mobileStartDate?: Date;
15
15
  }
16
16
  export declare class ARangePicker extends Viewer<State> {
17
- _pickerRef: RefObject<any>;
17
+ _pickerRef: RefObject<HTMLDivElement>;
18
18
  _onCalendarChangeValue?: AntData | null;
19
+ _panelClickedDate?: moment.Moment | null;
19
20
  _startConfirmed: boolean;
20
21
  constructor(p: MProp);
21
22
  componentDidUpdate(): void;