react-extjs 0.0.1
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/package.json +36 -0
- package/src/components/AbsoluteLayout.tsx +206 -0
- package/src/components/Accordion.tsx +266 -0
- package/src/components/AnchorLayout.tsx +131 -0
- package/src/components/BorderLayout.tsx +512 -0
- package/src/components/Box.tsx +289 -0
- package/src/components/Button.tsx +300 -0
- package/src/components/ButtonGroup.tsx +170 -0
- package/src/components/CalendarPicker.tsx +431 -0
- package/src/components/CardLayout.tsx +122 -0
- package/src/components/CenterLayout.tsx +95 -0
- package/src/components/CheckboxGroup.tsx +200 -0
- package/src/components/ColorMenu.tsx +195 -0
- package/src/components/ColorPalette.tsx +126 -0
- package/src/components/ColumnLayout.tsx +183 -0
- package/src/components/ColumnTree.tsx +383 -0
- package/src/components/CycleButton.tsx +302 -0
- package/src/components/DataView.tsx +244 -0
- package/src/components/DataViewMore.tsx +337 -0
- package/src/components/DataViewTransition.tsx +235 -0
- package/src/components/DateMenu.tsx +163 -0
- package/src/components/DatePicker.tsx +387 -0
- package/src/components/DisplayField.tsx +73 -0
- package/src/components/Editor.tsx +214 -0
- package/src/components/EditorGrid.tsx +361 -0
- package/src/components/FieldLabeler.tsx +81 -0
- package/src/components/FieldReplicator.tsx +135 -0
- package/src/components/FieldSet.tsx +149 -0
- package/src/components/FitLayout.tsx +76 -0
- package/src/components/Form.tsx +3484 -0
- package/src/components/Grid.tsx +2173 -0
- package/src/components/GridFilters.tsx +332 -0
- package/src/components/GroupTab.tsx +274 -0
- package/src/components/HtmlEditor.tsx +391 -0
- package/src/components/ImageOrganizer.tsx +984 -0
- package/src/components/ImageOrganizerService.ts +361 -0
- package/src/components/ListView.tsx +221 -0
- package/src/components/LoadMask.tsx +74 -0
- package/src/components/Menu.tsx +227 -0
- package/src/components/MessageBox.tsx +138 -0
- package/src/components/MultiSlider.tsx +369 -0
- package/src/components/Organizer.tsx +403 -0
- package/src/components/PagingToolbar.tsx +255 -0
- package/src/components/Panel.tsx +451 -0
- package/src/components/ProgressBar.tsx +127 -0
- package/src/components/PropertyGrid.tsx +142 -0
- package/src/components/QuickTip.tsx +174 -0
- package/src/components/Resizable.tsx +241 -0
- package/src/components/RowLayout.tsx +126 -0
- package/src/components/Slider.tsx +353 -0
- package/src/components/SliderField.tsx +206 -0
- package/src/components/SpinnerField.tsx +141 -0
- package/src/components/SplitBar.tsx +215 -0
- package/src/components/Spotlight.tsx +262 -0
- package/src/components/StatusBar.tsx +118 -0
- package/src/components/TabPanel.tsx +540 -0
- package/src/components/TableLayout.tsx +132 -0
- package/src/components/ThemeSwitcher.tsx +167 -0
- package/src/components/ToolTip.tsx +226 -0
- package/src/components/Toolbar.tsx +321 -0
- package/src/components/Tree.tsx +809 -0
- package/src/components/TreeGrid.tsx +418 -0
- package/src/components/Viewport.tsx +218 -0
- package/src/components/Window.tsx +441 -0
- package/src/components/Writer.tsx +303 -0
- package/src/components/XmlTreeLoader.tsx +233 -0
- package/src/components/dnd/DndContext.tsx +328 -0
- package/src/components/dnd/DragSource.tsx +202 -0
- package/src/components/dnd/DragZone.tsx +66 -0
- package/src/components/dnd/DropTarget.tsx +86 -0
- package/src/components/dnd/DropZone.tsx +166 -0
- package/src/components/dnd/PanelProxy.tsx +81 -0
- package/src/components/dnd/PanelResizer.tsx +143 -0
- package/src/components/dnd/Portal.tsx +254 -0
- package/src/components/dnd/Reorderer.tsx +73 -0
- package/src/components/dnd/ToolbarReorderer.tsx +226 -0
- package/src/main.tsx +2649 -0
- package/src/styles/ext-theme.css +889 -0
- package/src/util/theme.ts +95 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { DatePicker } from './DatePicker';
|
|
4
|
+
|
|
5
|
+
export interface DateMenuProps {
|
|
6
|
+
/** 是否展开。受控,由触发器(如按钮)管理。 */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
/** 触发元素(用于定位浮层到其下方,对齐 Ext 菜单定位)。 */
|
|
9
|
+
anchorRef?: React.RefObject<HTMLElement>;
|
|
10
|
+
/** 已选日期(决定 DatePicker 的 x-date-selected 高亮)。 */
|
|
11
|
+
value?: Date | null;
|
|
12
|
+
/** 一周起始日,0=周日(与 Ext startDay 一致)。 */
|
|
13
|
+
startDay?: number;
|
|
14
|
+
/** 是否显示底部“今天”按钮,默认 true。 */
|
|
15
|
+
showToday?: boolean;
|
|
16
|
+
/** 最小可选日期(早于则 x-date-disabled)。 */
|
|
17
|
+
minDate?: Date | null;
|
|
18
|
+
/** 最大可选日期。 */
|
|
19
|
+
maxDate?: Date | null;
|
|
20
|
+
/** 禁用的星期索引数组,如 [0,6]。 */
|
|
21
|
+
disabledDays?: number[];
|
|
22
|
+
/** 选中回调,参数为选中的 Date(对齐 Ext select / handler 事件)。 */
|
|
23
|
+
onSelect?: (date: Date) => void;
|
|
24
|
+
/** 选中后是否关闭,默认 true(对齐 Ext hideOnClick)。 */
|
|
25
|
+
hideOnClick?: boolean;
|
|
26
|
+
/** 关闭回调(外部点击 / Esc / 选中后)。 */
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DateMenuHandle {
|
|
31
|
+
/** 打开浮层 */
|
|
32
|
+
open: () => void;
|
|
33
|
+
/** 关闭浮层 */
|
|
34
|
+
close: () => void;
|
|
35
|
+
/** 是否展开 */
|
|
36
|
+
isOpen: () => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* DateMenu —— 保真复刻 Ext.menu.DateMenu(src/widgets/menu/DateMenu.js,Ext 3.2.1)。
|
|
41
|
+
*
|
|
42
|
+
* 保真契约(DOM/class 严格对齐源码):
|
|
43
|
+
* - 根:x-menu x-menu-floating x-layer x-date-menu x-menu-plain x-menu-nosep
|
|
44
|
+
* (plain:true → x-menu-plain;showSeparator:false → x-menu-nosep;cls:'x-date-menu')
|
|
45
|
+
* - a.x-menu-focus(隐藏聚焦元素)+ ul.x-menu-list(padding 2px)
|
|
46
|
+
* - li.x-menu-list-item(padding 上下 1px)→ 直接子 div.x-date-picker.x-menu-date-item
|
|
47
|
+
* (组件型 item 直接 render 进 li,无 a.x-menu-item 包裹,与 ColorMenu 同构)
|
|
48
|
+
* - 内部复用 DatePicker(含表头/日历网格/月份选择浮层/今天按钮),选中后
|
|
49
|
+
* hideOnClick→onClose,并触发 onSelect(date)(对齐 Ext relayEvents(picker,'select'))。
|
|
50
|
+
* - 浮层用 Portal 渲染到 body(对齐 Ext 菜单浮动到 body),fixed 定位到触发器下方,
|
|
51
|
+
* 整数坐标(对齐 Ext.Layer)。
|
|
52
|
+
*
|
|
53
|
+
* 全部样式来自 ext-all.css,零新增 CSS 契约破坏。
|
|
54
|
+
*/
|
|
55
|
+
export const DateMenu = forwardRef<DateMenuHandle, DateMenuProps>(function DateMenu(
|
|
56
|
+
{
|
|
57
|
+
open,
|
|
58
|
+
anchorRef,
|
|
59
|
+
value,
|
|
60
|
+
startDay,
|
|
61
|
+
showToday,
|
|
62
|
+
minDate,
|
|
63
|
+
maxDate,
|
|
64
|
+
disabledDays,
|
|
65
|
+
onSelect,
|
|
66
|
+
hideOnClick = true,
|
|
67
|
+
onClose,
|
|
68
|
+
},
|
|
69
|
+
ref,
|
|
70
|
+
) {
|
|
71
|
+
const [dynOpen, setDynOpen] = useState<boolean>(open ?? false);
|
|
72
|
+
const [coords, setCoords] = useState<{ top: number; left: number } | null>(null);
|
|
73
|
+
const openRef = useRef(dynOpen);
|
|
74
|
+
|
|
75
|
+
useImperativeHandle(
|
|
76
|
+
ref,
|
|
77
|
+
() => ({
|
|
78
|
+
open: () => setDynOpen(true),
|
|
79
|
+
close: () => setDynOpen(false),
|
|
80
|
+
isOpen: () => openRef.current,
|
|
81
|
+
}),
|
|
82
|
+
[],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
openRef.current = dynOpen;
|
|
87
|
+
}, [dynOpen]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (open !== undefined) setDynOpen(open);
|
|
91
|
+
}, [open]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!dynOpen) {
|
|
95
|
+
setCoords(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const el = anchorRef?.current;
|
|
99
|
+
if (el) {
|
|
100
|
+
const r = el.getBoundingClientRect();
|
|
101
|
+
setCoords({ top: Math.round(r.bottom), left: Math.round(r.left) });
|
|
102
|
+
}
|
|
103
|
+
// 对齐 Ext 菜单:点击浮层外部或缩放窗口时关闭;延迟挂载避免捕获打开当次点击。
|
|
104
|
+
const onDocDown = (e: MouseEvent) => {
|
|
105
|
+
const t = e.target as HTMLElement;
|
|
106
|
+
if (
|
|
107
|
+
!t.closest('.x-date-menu') &&
|
|
108
|
+
!t.closest('.x-date-picker') &&
|
|
109
|
+
!(anchorRef?.current && anchorRef.current.contains(t))
|
|
110
|
+
) {
|
|
111
|
+
onClose?.();
|
|
112
|
+
setDynOpen(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const close = () => {
|
|
116
|
+
onClose?.();
|
|
117
|
+
setDynOpen(false);
|
|
118
|
+
};
|
|
119
|
+
const t = window.setTimeout(() => {
|
|
120
|
+
document.addEventListener('mousedown', onDocDown);
|
|
121
|
+
window.addEventListener('resize', close);
|
|
122
|
+
}, 0);
|
|
123
|
+
return () => {
|
|
124
|
+
window.clearTimeout(t);
|
|
125
|
+
document.removeEventListener('mousedown', onDocDown);
|
|
126
|
+
window.removeEventListener('resize', close);
|
|
127
|
+
};
|
|
128
|
+
}, [dynOpen, anchorRef, onClose]);
|
|
129
|
+
|
|
130
|
+
if (!dynOpen || !coords) return null;
|
|
131
|
+
|
|
132
|
+
return createPortal(
|
|
133
|
+
<div
|
|
134
|
+
className="x-menu x-menu-floating x-layer x-date-menu x-menu-plain x-menu-nosep"
|
|
135
|
+
style={{ position: 'fixed', visibility: 'visible', zIndex: 15000, top: coords.top, left: coords.left }}
|
|
136
|
+
>
|
|
137
|
+
<a href="#" className="x-menu-focus" tabIndex={-1} onClick={(e) => e.preventDefault()} />
|
|
138
|
+
<ul className="x-menu-list">
|
|
139
|
+
<li className="x-menu-list-item">
|
|
140
|
+
<DatePicker
|
|
141
|
+
className="x-menu-date-item"
|
|
142
|
+
value={value}
|
|
143
|
+
startDay={startDay}
|
|
144
|
+
showToday={showToday}
|
|
145
|
+
minDate={minDate}
|
|
146
|
+
maxDate={maxDate}
|
|
147
|
+
disabledDays={disabledDays}
|
|
148
|
+
onSelect={(d) => {
|
|
149
|
+
onSelect?.(d);
|
|
150
|
+
if (hideOnClick) {
|
|
151
|
+
setDynOpen(false);
|
|
152
|
+
onClose?.();
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</li>
|
|
157
|
+
</ul>
|
|
158
|
+
</div>,
|
|
159
|
+
document.body,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
(DateMenu as React.FC).displayName = 'DateMenu';
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatePicker —— 保真复刻 Ext.DatePicker(src/widgets/DatePicker.js onRender 304-327)。
|
|
3
|
+
*
|
|
4
|
+
* 保真契约(DOM/class 严格对齐源码):
|
|
5
|
+
* - 根:div.x-date-picker
|
|
6
|
+
* - 表头 table > tr > td.x-date-left(上一月) / td.x-date-middle(月年,点击开月份选择) / td.x-date-right(下一月)
|
|
7
|
+
* - 日历表 table.x-date-inner > thead(周几,取 dayNames 首字母) + tbody(固定 6 行 × 7 = 42 格)
|
|
8
|
+
* - 每格 td,class 之一/组合:x-date-active / x-date-prevday / x-date-nextday / x-date-today / x-date-selected / x-date-disabled
|
|
9
|
+
* 内 a.x-date-date[ tabIndex=1 hidefocus=on] > em > span(日)
|
|
10
|
+
* - 底部(showToday):tr > td.x-date-bottom(今天按钮)
|
|
11
|
+
* - 月份选择浮层:div.x-date-mp(结构见 createMonthPicker 434-453)
|
|
12
|
+
*
|
|
13
|
+
* 交互(行为契约):
|
|
14
|
+
* - 上一月/下一月箭头:切换显示月份
|
|
15
|
+
* - 点击日格:触发 onSelect(date)(单选模式)
|
|
16
|
+
* - 今天按钮:onSelect(今天)
|
|
17
|
+
* - 中间月年:切换 x-date-mp 月份选择浮层(选月/年后回到日历)
|
|
18
|
+
* - hover 高亮由 CSS .x-date-inner a:hover 提供(与 combo 不同,这里确有 :hover 规则)
|
|
19
|
+
*
|
|
20
|
+
* 范围选择(rangeSelect,对齐 Ext 派生 DateRangeField / RangeMenu 的选段行为,非源码内建):
|
|
21
|
+
* - rangeSelect=true 时,首次点击设起点,再次点击设终点并高亮 [起点,终点] 闭区间(含端点 x-date-selected、
|
|
22
|
+
* 区间内日期走内联浅蓝背景,复用既有视觉而非新增 CSS 类,零契约破坏)。
|
|
23
|
+
* - 选满两段后再点击任一格 -> 重置为新起点。onRangeSelect 回传选中日期数组。
|
|
24
|
+
*/
|
|
25
|
+
import { useState, useMemo, type CSSProperties } from 'react';
|
|
26
|
+
|
|
27
|
+
const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
28
|
+
const MONTH_NAMES = [
|
|
29
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
30
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
31
|
+
];
|
|
32
|
+
const SHORT_MONTH_NAMES = [
|
|
33
|
+
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
34
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export interface DatePickerProps {
|
|
38
|
+
/** 已选日期(决定 x-date-selected 高亮,单选模式) */
|
|
39
|
+
value?: Date | null;
|
|
40
|
+
/** 一周起始日,0=周日(与 Ext startDay 一致) */
|
|
41
|
+
startDay?: number;
|
|
42
|
+
/** 是否显示底部“今天”按钮,默认 true */
|
|
43
|
+
showToday?: boolean;
|
|
44
|
+
todayText?: string;
|
|
45
|
+
/** 最小可选日期(早于则 x-date-disabled) */
|
|
46
|
+
minDate?: Date | null;
|
|
47
|
+
/** 最大可选日期 */
|
|
48
|
+
maxDate?: Date | null;
|
|
49
|
+
/** 禁用的星期索引数组,如 [0,6] */
|
|
50
|
+
disabledDays?: number[];
|
|
51
|
+
/** 点击某日回调(单选模式) */
|
|
52
|
+
onSelect?: (date: Date) => void;
|
|
53
|
+
/** 附加 class(如 DateMenu 中 Ext 会加 ctCls=x-menu-date-item) */
|
|
54
|
+
className?: string;
|
|
55
|
+
|
|
56
|
+
/** 范围选择模式(默认 false,开启后点击两日形成闭区间高亮) */
|
|
57
|
+
rangeSelect?: boolean;
|
|
58
|
+
/** 范围模式初始选中日期数组(端点) */
|
|
59
|
+
selectedDates?: Date[];
|
|
60
|
+
/** 范围模式选中变化回调(回传闭区间端点数组) */
|
|
61
|
+
onRangeSelect?: (dates: Date[]) => void;
|
|
62
|
+
/** 透传根节点内联样式(用于组合场景覆盖原版 .x-date-picker 的默认 margin 等) */
|
|
63
|
+
style?: CSSProperties;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Cell {
|
|
67
|
+
day: number;
|
|
68
|
+
cls: string;
|
|
69
|
+
date: Date;
|
|
70
|
+
title: string;
|
|
71
|
+
disabled: boolean;
|
|
72
|
+
/** 处于选定区间内(不含端点,端点用 x-date-selected 标记) */
|
|
73
|
+
inRange: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function startOfDay(d: Date) {
|
|
77
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildCells(
|
|
81
|
+
view: Date,
|
|
82
|
+
startDay: number,
|
|
83
|
+
selectedTimes: number[],
|
|
84
|
+
rangeSet: Set<number> | null,
|
|
85
|
+
min: Date | null | undefined,
|
|
86
|
+
max: Date | null | undefined,
|
|
87
|
+
disabledDays?: number[],
|
|
88
|
+
): Cell[] {
|
|
89
|
+
const year = view.getFullYear();
|
|
90
|
+
const month = view.getMonth();
|
|
91
|
+
const first = new Date(year, month, 1);
|
|
92
|
+
let startingPos = first.getDay() - startDay;
|
|
93
|
+
if (startingPos < 0) startingPos += 7;
|
|
94
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
95
|
+
const total = daysInMonth + startingPos; // active + leading prevdays
|
|
96
|
+
|
|
97
|
+
const today = startOfDay(new Date());
|
|
98
|
+
const todayT = today.getTime();
|
|
99
|
+
const selSet = new Set(selectedTimes);
|
|
100
|
+
const minT = min ? startOfDay(min).getTime() : Number.NEGATIVE_INFINITY;
|
|
101
|
+
const maxT = max ? startOfDay(max).getTime() : Number.POSITIVE_INFINITY;
|
|
102
|
+
|
|
103
|
+
// 起始游标:上月倒数第 startingPos 天
|
|
104
|
+
let cursor = new Date(year, month - 1, new Date(year, month, 0).getDate() - startingPos);
|
|
105
|
+
const cells: Cell[] = [];
|
|
106
|
+
for (let i = 0; i < 42; i++) {
|
|
107
|
+
cursor = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1);
|
|
108
|
+
const t = startOfDay(cursor);
|
|
109
|
+
const tT = t.getTime();
|
|
110
|
+
let cls = i < startingPos ? 'x-date-prevday' : i < total ? 'x-date-active' : 'x-date-nextday';
|
|
111
|
+
let title = '';
|
|
112
|
+
let disabled = false;
|
|
113
|
+
let inRange = false;
|
|
114
|
+
|
|
115
|
+
if (selSet.has(tT)) cls += ' x-date-selected';
|
|
116
|
+
if (tT === todayT) {
|
|
117
|
+
cls += ' x-date-today';
|
|
118
|
+
title = 'Today';
|
|
119
|
+
}
|
|
120
|
+
if (rangeSet && rangeSet.has(tT)) inRange = true;
|
|
121
|
+
if (tT < minT) {
|
|
122
|
+
cls = ' x-date-disabled';
|
|
123
|
+
title = 'The date is before the minimum date';
|
|
124
|
+
disabled = true;
|
|
125
|
+
} else if (tT > maxT) {
|
|
126
|
+
cls = ' x-date-disabled';
|
|
127
|
+
title = 'The date is after the maximum date';
|
|
128
|
+
disabled = true;
|
|
129
|
+
} else if (disabledDays && disabledDays.indexOf(t.getDay()) !== -1) {
|
|
130
|
+
cls = ' x-date-disabled';
|
|
131
|
+
title = 'Disabled';
|
|
132
|
+
disabled = true;
|
|
133
|
+
}
|
|
134
|
+
cells.push({ day: t.getDate(), cls: cls.trim(), date: t, title, disabled, inRange });
|
|
135
|
+
}
|
|
136
|
+
return cells;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function DatePicker({
|
|
140
|
+
value,
|
|
141
|
+
startDay = 0,
|
|
142
|
+
showToday = true,
|
|
143
|
+
todayText = 'Today',
|
|
144
|
+
minDate,
|
|
145
|
+
maxDate,
|
|
146
|
+
disabledDays,
|
|
147
|
+
onSelect,
|
|
148
|
+
className,
|
|
149
|
+
rangeSelect = false,
|
|
150
|
+
selectedDates,
|
|
151
|
+
onRangeSelect,
|
|
152
|
+
style,
|
|
153
|
+
}: DatePickerProps) {
|
|
154
|
+
// Ext.DatePicker 默认选中今天(value 未提供时)
|
|
155
|
+
const effectiveValue = value ?? startOfDay(new Date());
|
|
156
|
+
const initialSel = selectedDates && selectedDates.length ? selectedDates : value ? [value] : [];
|
|
157
|
+
const [selDates, setSelDates] = useState<Date[]>(initialSel);
|
|
158
|
+
|
|
159
|
+
const rangeMode = rangeSelect;
|
|
160
|
+
const viewInit = selDates.length ? selDates[0] : effectiveValue;
|
|
161
|
+
const initial = new Date(viewInit.getFullYear(), viewInit.getMonth(), 1);
|
|
162
|
+
const [view, setView] = useState<Date>(initial);
|
|
163
|
+
const [mpYear, setMpYear] = useState<number>(view.getFullYear());
|
|
164
|
+
const [showMonthPicker, setShowMonthPicker] = useState(false);
|
|
165
|
+
|
|
166
|
+
const { selectedTimes, rangeSet } = useMemo(() => {
|
|
167
|
+
if (rangeMode) {
|
|
168
|
+
const times = selDates
|
|
169
|
+
.map((d) => startOfDay(d).getTime())
|
|
170
|
+
.sort((a, b) => a - b);
|
|
171
|
+
let rs: Set<number> | null = null;
|
|
172
|
+
if (times.length === 2) {
|
|
173
|
+
rs = new Set<number>();
|
|
174
|
+
const day = 86400000;
|
|
175
|
+
for (let t = times[0] + day; t < times[1]; t += day) rs.add(t);
|
|
176
|
+
}
|
|
177
|
+
return { selectedTimes: times, rangeSet: rs };
|
|
178
|
+
}
|
|
179
|
+
const sel = selDates.length
|
|
180
|
+
? startOfDay(selDates[0]).getTime()
|
|
181
|
+
: effectiveValue
|
|
182
|
+
? startOfDay(effectiveValue).getTime()
|
|
183
|
+
: null;
|
|
184
|
+
return { selectedTimes: sel == null ? [] : [sel], rangeSet: null };
|
|
185
|
+
}, [rangeMode, selDates, effectiveValue]);
|
|
186
|
+
|
|
187
|
+
const cells = useMemo(
|
|
188
|
+
() => buildCells(view, startDay, selectedTimes, rangeSet, minDate, maxDate, disabledDays),
|
|
189
|
+
[view, startDay, selectedTimes, rangeSet, minDate, maxDate, disabledDays],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const weekHeaders = useMemo(() => {
|
|
193
|
+
const arr: string[] = [];
|
|
194
|
+
for (let i = 0; i < 7; i++) {
|
|
195
|
+
const d = (startDay + i) % 7;
|
|
196
|
+
arr.push(DAY_NAMES[d].charAt(0));
|
|
197
|
+
}
|
|
198
|
+
return arr;
|
|
199
|
+
}, [startDay]);
|
|
200
|
+
|
|
201
|
+
const monthLabel = `${MONTH_NAMES[view.getMonth()]} ${view.getFullYear()}`;
|
|
202
|
+
|
|
203
|
+
const moveMonth = (delta: number) => {
|
|
204
|
+
setView((v) => new Date(v.getFullYear(), v.getMonth() + delta, 1));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// 10 年窗口(与 updateMPYear 逻辑对齐):y, y+1, y-1, y+2, y-2, y+3, y-3, y+4, y-4, y+5
|
|
208
|
+
const years = useMemo(() => {
|
|
209
|
+
const list: number[] = [];
|
|
210
|
+
for (let i = 1; i <= 10; i++) {
|
|
211
|
+
list.push(i % 2 === 0 ? mpYear + Math.round(i * 0.5) : mpYear - Math.round(((i - 1) * 0.5)));
|
|
212
|
+
}
|
|
213
|
+
return list;
|
|
214
|
+
}, [mpYear]);
|
|
215
|
+
|
|
216
|
+
const pickMonth = (m: number) => {
|
|
217
|
+
setView((v) => new Date(v.getFullYear(), m, 1));
|
|
218
|
+
setShowMonthPicker(false);
|
|
219
|
+
};
|
|
220
|
+
const pickYear = (y: number) => {
|
|
221
|
+
setView((v) => new Date(y, v.getMonth(), 1));
|
|
222
|
+
setShowMonthPicker(false);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleDayClick = (c: Cell) => {
|
|
226
|
+
if (c.disabled) return;
|
|
227
|
+
if (rangeMode) {
|
|
228
|
+
setSelDates((prev) => {
|
|
229
|
+
let next: Date[];
|
|
230
|
+
if (prev.length >= 2) {
|
|
231
|
+
next = [c.date];
|
|
232
|
+
} else if (prev.length === 1) {
|
|
233
|
+
const a = startOfDay(prev[0]).getTime();
|
|
234
|
+
const b = startOfDay(c.date).getTime();
|
|
235
|
+
next = a <= b ? [prev[0], c.date] : [c.date, prev[0]];
|
|
236
|
+
} else {
|
|
237
|
+
next = [c.date];
|
|
238
|
+
}
|
|
239
|
+
onRangeSelect?.(next);
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
setSelDates([c.date]);
|
|
244
|
+
onSelect?.(c.date);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div className={['x-date-picker', className].filter(Boolean).join(' ')} style={style}>
|
|
250
|
+
<table cellSpacing={0}>
|
|
251
|
+
<tbody>
|
|
252
|
+
<tr>
|
|
253
|
+
<td className="x-date-left">
|
|
254
|
+
<a href="#" title="Previous Month" onClick={(e) => { e.preventDefault(); moveMonth(-1); }}> </a>
|
|
255
|
+
</td>
|
|
256
|
+
<td className="x-date-middle" align="center">
|
|
257
|
+
<table cellSpacing={0} className="x-btn x-btn-noicon" style={{ width: 'auto' }}>
|
|
258
|
+
<tbody className="x-btn-small x-btn-icon-small-left">
|
|
259
|
+
<tr>
|
|
260
|
+
<td className="x-btn-tl"><i> </i></td>
|
|
261
|
+
<td className="x-btn-tc"></td>
|
|
262
|
+
<td className="x-btn-tr"><i> </i></td>
|
|
263
|
+
</tr>
|
|
264
|
+
<tr>
|
|
265
|
+
<td className="x-btn-ml"><i> </i></td>
|
|
266
|
+
<td className="x-btn-mc">
|
|
267
|
+
<em className="x-btn-arrow" unselectable="on">
|
|
268
|
+
<button type="button" className="x-btn-text" onClick={() => setShowMonthPicker((s) => !s)}>
|
|
269
|
+
{monthLabel}
|
|
270
|
+
</button>
|
|
271
|
+
</em>
|
|
272
|
+
</td>
|
|
273
|
+
<td className="x-btn-mr"><i> </i></td>
|
|
274
|
+
</tr>
|
|
275
|
+
<tr>
|
|
276
|
+
<td className="x-btn-bl"><i> </i></td>
|
|
277
|
+
<td className="x-btn-bc"></td>
|
|
278
|
+
<td className="x-btn-br"><i> </i></td>
|
|
279
|
+
</tr>
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
</td>
|
|
283
|
+
<td className="x-date-right">
|
|
284
|
+
<a href="#" title="Next Month" onClick={(e) => { e.preventDefault(); moveMonth(1); }}> </a>
|
|
285
|
+
</td>
|
|
286
|
+
</tr>
|
|
287
|
+
<tr>
|
|
288
|
+
<td colSpan={3}>
|
|
289
|
+
<table className="x-date-inner" cellSpacing={0}>
|
|
290
|
+
<thead>
|
|
291
|
+
<tr>
|
|
292
|
+
{weekHeaders.map((w, i) => (
|
|
293
|
+
<th key={i}><span>{w}</span></th>
|
|
294
|
+
))}
|
|
295
|
+
</tr>
|
|
296
|
+
</thead>
|
|
297
|
+
<tbody>
|
|
298
|
+
{Array.from({ length: 6 }).map((_, r) => (
|
|
299
|
+
<tr key={r}>
|
|
300
|
+
{cells.slice(r * 7, r * 7 + 7).map((c, ci) => (
|
|
301
|
+
<td
|
|
302
|
+
key={ci}
|
|
303
|
+
className={[c.cls, c.inRange ? 'x-date-selected' : ''].filter(Boolean).join(' ')}
|
|
304
|
+
title={c.title}
|
|
305
|
+
>
|
|
306
|
+
<a
|
|
307
|
+
href="#"
|
|
308
|
+
className="x-date-date"
|
|
309
|
+
tabIndex={1}
|
|
310
|
+
onClick={(e) => {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
handleDayClick(c);
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
<em><span>{c.day}</span></em>
|
|
316
|
+
</a>
|
|
317
|
+
</td>
|
|
318
|
+
))}
|
|
319
|
+
</tr>
|
|
320
|
+
))}
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
</td>
|
|
324
|
+
</tr>
|
|
325
|
+
{showToday && (
|
|
326
|
+
<tr>
|
|
327
|
+
<td colSpan={3} className="x-date-bottom" align="center" style={{ paddingTop: 4, paddingBottom: 8 }}>
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
className="x-date-today"
|
|
331
|
+
style={{ border: 0, background: 'none', cursor: 'pointer' }}
|
|
332
|
+
onClick={() => (rangeMode ? handleDayClick({ date: startOfDay(new Date()), disabled: false } as Cell) : onSelect?.(startOfDay(new Date())))}
|
|
333
|
+
>
|
|
334
|
+
{todayText}
|
|
335
|
+
</button>
|
|
336
|
+
</td>
|
|
337
|
+
</tr>
|
|
338
|
+
)}
|
|
339
|
+
</tbody>
|
|
340
|
+
</table>
|
|
341
|
+
|
|
342
|
+
<div className="x-date-mp" style={{ display: showMonthPicker ? 'block' : 'none' }}>
|
|
343
|
+
<table border={0} cellSpacing={0}>
|
|
344
|
+
<tbody>
|
|
345
|
+
{Array.from({ length: 6 }).map((_, r) => (
|
|
346
|
+
<tr key={r}>
|
|
347
|
+
<td className="x-date-mp-month">
|
|
348
|
+
<a href="#" onClick={(e) => { e.preventDefault(); pickMonth(r); }}>{SHORT_MONTH_NAMES[r]}</a>
|
|
349
|
+
</td>
|
|
350
|
+
<td className="x-date-mp-month x-date-mp-sep">
|
|
351
|
+
<a href="#" onClick={(e) => { e.preventDefault(); pickMonth(r + 6); }}>{SHORT_MONTH_NAMES[r + 6]}</a>
|
|
352
|
+
</td>
|
|
353
|
+
{r === 0 ? (
|
|
354
|
+
<>
|
|
355
|
+
<td className="x-date-mp-ybtn" align="center">
|
|
356
|
+
<a className="x-date-mp-prev" onClick={(e) => { e.preventDefault(); setMpYear((y) => y - 1); }}></a>
|
|
357
|
+
</td>
|
|
358
|
+
<td className="x-date-mp-ybtn" align="center">
|
|
359
|
+
<a className="x-date-mp-next" onClick={(e) => { e.preventDefault(); setMpYear((y) => y + 1); }}></a>
|
|
360
|
+
</td>
|
|
361
|
+
</>
|
|
362
|
+
) : (
|
|
363
|
+
<>
|
|
364
|
+
<td className="x-date-mp-year">
|
|
365
|
+
<a href="#" onClick={(e) => { e.preventDefault(); pickYear(years[(r - 1) * 2]); }}>{years[(r - 1) * 2]}</a>
|
|
366
|
+
</td>
|
|
367
|
+
<td className="x-date-mp-year">
|
|
368
|
+
<a href="#" onClick={(e) => { e.preventDefault(); pickYear(years[(r - 1) * 2 + 1]); }}>{years[(r - 1) * 2 + 1]}</a>
|
|
369
|
+
</td>
|
|
370
|
+
</>
|
|
371
|
+
)}
|
|
372
|
+
</tr>
|
|
373
|
+
))}
|
|
374
|
+
<tr className="x-date-mp-btns">
|
|
375
|
+
<td colSpan={4}>
|
|
376
|
+
<button type="button" className="x-date-mp-ok" onClick={() => setShowMonthPicker(false)}>OK</button>
|
|
377
|
+
<button type="button" className="x-date-mp-cancel" onClick={() => setShowMonthPicker(false)}>Cancel</button>
|
|
378
|
+
</td>
|
|
379
|
+
</tr>
|
|
380
|
+
</tbody>
|
|
381
|
+
</table>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export default DatePicker;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ext.form.DisplayField 保真复刻(来源 src/widgets/form/DisplayField.js)
|
|
5
|
+
*
|
|
6
|
+
* 保真契约:
|
|
7
|
+
* - DisplayField.autoCreate(DisplayField.js):tag 'div',cls 'x-form-display-field'
|
|
8
|
+
* - Field.onRender:renderTo 时直接 createChild(autoCreate) → 裸 <div class="x-form-display-field">
|
|
9
|
+
* - 在 FormLayout 中由 FormField 包裹为 x-form-item > label + x-form-element > 本元素
|
|
10
|
+
* (复用 Form.tsx 的 FormField,已产出 x-form-item 结构,DOM 与 golden 的 IN_FORM 完全一致)
|
|
11
|
+
*
|
|
12
|
+
* 行为契约(对齐 Ext DisplayField):
|
|
13
|
+
* - value / setValue:显示文本,支持 HTML(Ext 用 update(value) 直写 innerHTML)
|
|
14
|
+
* - html:若提供则优先于 value(Ext 的 html 配置)
|
|
15
|
+
* - 只读展示,无输入态、无校验
|
|
16
|
+
*
|
|
17
|
+
* React 仅产出与 Ext 同构的 DOM/class,复用 ext-all.css 得到像素级还原(零 CSS 契约破坏)。
|
|
18
|
+
*/
|
|
19
|
+
export interface DisplayFieldProps {
|
|
20
|
+
/** 字段名(落到 div 的 name 属性,对齐 Ext)。 */
|
|
21
|
+
name?: string;
|
|
22
|
+
/** 显示文本;支持纯文本或 HTML 字符串。 */
|
|
23
|
+
value?: string;
|
|
24
|
+
/** HTML 内容(优先级高于 value)。 */
|
|
25
|
+
html?: string;
|
|
26
|
+
/** 透传到根 div 的 id(用于 FormField 的 htmlFor 关联)。 */
|
|
27
|
+
id?: string;
|
|
28
|
+
/** 附加 class(追加到 x-form-display-field 之后)。 */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** 透传测试 id。 */
|
|
31
|
+
testId?: string;
|
|
32
|
+
width?: number | string;
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function DisplayField({
|
|
37
|
+
name,
|
|
38
|
+
value,
|
|
39
|
+
html,
|
|
40
|
+
id,
|
|
41
|
+
className,
|
|
42
|
+
testId,
|
|
43
|
+
width,
|
|
44
|
+
style,
|
|
45
|
+
}: DisplayFieldProps) {
|
|
46
|
+
const cls = ['x-form-display-field', className].filter(Boolean).join(' ');
|
|
47
|
+
const divStyle = { ...(width !== undefined ? { width } : {}), ...style };
|
|
48
|
+
// Ext 用 update() 直写 innerHTML:html 配置优先级高于 value,且直接落到 div 上(无额外包裹),
|
|
49
|
+
// 以与 golden 的 <div class="x-form-display-field">{html}</div> 同构。
|
|
50
|
+
if (html !== undefined) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
id={id}
|
|
54
|
+
{...(name !== undefined ? { name } : {})}
|
|
55
|
+
className={cls}
|
|
56
|
+
data-testid={testId}
|
|
57
|
+
style={divStyle}
|
|
58
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
id={id}
|
|
65
|
+
{...(name !== undefined ? { name } : {})}
|
|
66
|
+
className={cls}
|
|
67
|
+
data-testid={testId}
|
|
68
|
+
style={divStyle}
|
|
69
|
+
>
|
|
70
|
+
{value ?? ''}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|