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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +36 -0
  3. package/src/components/AbsoluteLayout.tsx +206 -0
  4. package/src/components/Accordion.tsx +266 -0
  5. package/src/components/AnchorLayout.tsx +131 -0
  6. package/src/components/BorderLayout.tsx +512 -0
  7. package/src/components/Box.tsx +289 -0
  8. package/src/components/Button.tsx +300 -0
  9. package/src/components/ButtonGroup.tsx +170 -0
  10. package/src/components/CalendarPicker.tsx +431 -0
  11. package/src/components/CardLayout.tsx +122 -0
  12. package/src/components/CenterLayout.tsx +95 -0
  13. package/src/components/CheckboxGroup.tsx +200 -0
  14. package/src/components/ColorMenu.tsx +195 -0
  15. package/src/components/ColorPalette.tsx +126 -0
  16. package/src/components/ColumnLayout.tsx +183 -0
  17. package/src/components/ColumnTree.tsx +383 -0
  18. package/src/components/CycleButton.tsx +302 -0
  19. package/src/components/DataView.tsx +244 -0
  20. package/src/components/DataViewMore.tsx +337 -0
  21. package/src/components/DataViewTransition.tsx +235 -0
  22. package/src/components/DateMenu.tsx +163 -0
  23. package/src/components/DatePicker.tsx +387 -0
  24. package/src/components/DisplayField.tsx +73 -0
  25. package/src/components/Editor.tsx +214 -0
  26. package/src/components/EditorGrid.tsx +361 -0
  27. package/src/components/FieldLabeler.tsx +81 -0
  28. package/src/components/FieldReplicator.tsx +135 -0
  29. package/src/components/FieldSet.tsx +149 -0
  30. package/src/components/FitLayout.tsx +76 -0
  31. package/src/components/Form.tsx +3484 -0
  32. package/src/components/Grid.tsx +2173 -0
  33. package/src/components/GridFilters.tsx +332 -0
  34. package/src/components/GroupTab.tsx +274 -0
  35. package/src/components/HtmlEditor.tsx +391 -0
  36. package/src/components/ImageOrganizer.tsx +984 -0
  37. package/src/components/ImageOrganizerService.ts +361 -0
  38. package/src/components/ListView.tsx +221 -0
  39. package/src/components/LoadMask.tsx +74 -0
  40. package/src/components/Menu.tsx +227 -0
  41. package/src/components/MessageBox.tsx +138 -0
  42. package/src/components/MultiSlider.tsx +369 -0
  43. package/src/components/Organizer.tsx +403 -0
  44. package/src/components/PagingToolbar.tsx +255 -0
  45. package/src/components/Panel.tsx +451 -0
  46. package/src/components/ProgressBar.tsx +127 -0
  47. package/src/components/PropertyGrid.tsx +142 -0
  48. package/src/components/QuickTip.tsx +174 -0
  49. package/src/components/Resizable.tsx +241 -0
  50. package/src/components/RowLayout.tsx +126 -0
  51. package/src/components/Slider.tsx +353 -0
  52. package/src/components/SliderField.tsx +206 -0
  53. package/src/components/SpinnerField.tsx +141 -0
  54. package/src/components/SplitBar.tsx +215 -0
  55. package/src/components/Spotlight.tsx +262 -0
  56. package/src/components/StatusBar.tsx +118 -0
  57. package/src/components/TabPanel.tsx +540 -0
  58. package/src/components/TableLayout.tsx +132 -0
  59. package/src/components/ThemeSwitcher.tsx +167 -0
  60. package/src/components/ToolTip.tsx +226 -0
  61. package/src/components/Toolbar.tsx +321 -0
  62. package/src/components/Tree.tsx +809 -0
  63. package/src/components/TreeGrid.tsx +418 -0
  64. package/src/components/Viewport.tsx +218 -0
  65. package/src/components/Window.tsx +441 -0
  66. package/src/components/Writer.tsx +303 -0
  67. package/src/components/XmlTreeLoader.tsx +233 -0
  68. package/src/components/dnd/DndContext.tsx +328 -0
  69. package/src/components/dnd/DragSource.tsx +202 -0
  70. package/src/components/dnd/DragZone.tsx +66 -0
  71. package/src/components/dnd/DropTarget.tsx +86 -0
  72. package/src/components/dnd/DropZone.tsx +166 -0
  73. package/src/components/dnd/PanelProxy.tsx +81 -0
  74. package/src/components/dnd/PanelResizer.tsx +143 -0
  75. package/src/components/dnd/Portal.tsx +254 -0
  76. package/src/components/dnd/Reorderer.tsx +73 -0
  77. package/src/components/dnd/ToolbarReorderer.tsx +226 -0
  78. package/src/main.tsx +2649 -0
  79. package/src/styles/ext-theme.css +889 -0
  80. package/src/util/theme.ts +95 -0
@@ -0,0 +1,451 @@
1
+ import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
2
+ import { Toolbar } from './Toolbar';
3
+
4
+ export interface PanelTool {
5
+ /** 工具图标 id,对应 CSS 类 x-tool-{id}(toggle/close/minimize/maximize/gear 等) */
6
+ id: string;
7
+ handler?: (e?: React.MouseEvent) => void;
8
+ /** 是否隐藏该工具(对齐 Ext tool.visible,默认 false 显示) */
9
+ hidden?: boolean;
10
+ /** 悬浮提示(对齐 Ext tool.tooltip) */
11
+ tooltip?: string;
12
+ /** 点击时是否阻止事件冒泡到 header(默认 true,避免 header 折叠被误触发) */
13
+ stopEvent?: boolean;
14
+ }
15
+
16
+ /** 命令式句柄:对齐 Ext.Panel 的公开方法(collapse/expand/toggleCollapse/setTitle/setIconClass 等) */
17
+ export interface PanelHandle {
18
+ /** 折叠面板 */
19
+ collapse: () => void;
20
+ /** 展开面板 */
21
+ expand: () => void;
22
+ /** 切换折叠/展开 */
23
+ toggleCollapse: () => void;
24
+ /** 运行时修改标题(对齐 setTitle) */
25
+ setTitle: (title: string) => void;
26
+ /** 运行时修改标题前图标 class(对齐 setIconClass) */
27
+ setIconClass: (cls?: string) => void;
28
+ /** 读取当前折叠态 */
29
+ getCollapsed: () => boolean;
30
+ }
31
+
32
+ export interface PanelProps {
33
+ title?: string;
34
+ /** true=9 宫格圆角边框;false=1px 方角(默认 false) */
35
+ frame?: boolean;
36
+ /** 标题前的图标 class */
37
+ iconCls?: string;
38
+ /** 附加在根节点上的 class */
39
+ cls?: string;
40
+ /** 是否显示头部(有 title 或 tools 时默认 true) */
41
+ header?: boolean;
42
+ /** 折叠态(渲染 x-panel-collapsed) */
43
+ collapsed?: boolean;
44
+ collapsible?: boolean;
45
+ /** collapsible 时不渲染 toggle 工具(默认 false 会渲染 x-tool-toggle) */
46
+ hideCollapseTool?: boolean;
47
+ /** 是否显示边框(默认 true;false 加 -noborder 链) */
48
+ border?: boolean;
49
+ /** body 内边距 */
50
+ bodyPadding?: number | string;
51
+ width?: number | string;
52
+ height?: number | string;
53
+ /** 追加到 header class(如 x-accordion-hd) */
54
+ headerClassName?: string;
55
+ /** 点击 header 或 toggle 工具回调,用于标题折叠交互 */
56
+ onHeaderClick?: (e?: React.MouseEvent) => void;
57
+ /** 透传到 .x-panel-body 的 inline style(对齐 Ext bodyStyle) */
58
+ bodyStyle?: React.CSSProperties;
59
+ /**
60
+ * 区域面板保真模式(BorderLayout 使用):当提供 height 时,面板锁定为固定高度,
61
+ * 内部以 flex 纵向排列(header 固定、body flex:1 填满剩余),无论 header 是否因
62
+ * 长标题换行而变高,面板总尺寸恒等于 height、body 自动收缩,绝不溢出覆盖相邻区域
63
+ * (对齐 ExtJS region 面板被布局固定为 box.h 的行为)。
64
+ */
65
+ bodyFlex?: boolean;
66
+ /** 追加在 .x-panel-body 上的 class(如 x-table-layout-ct),不影响既有保真 */
67
+ bodyClassName?: string;
68
+ tools?: PanelTool[];
69
+ /** 顶部工具条内容(通常是 Button/Toolbar) */
70
+ tbar?: React.ReactNode;
71
+ /** 底部工具条内容 */
72
+ bbar?: React.ReactNode;
73
+ /** 底部按钮区(渲染 x-panel-footer x-panel-btns) */
74
+ footer?: React.ReactNode;
75
+ /** 底部工具条(对齐 Ext fbar:渲染 x-panel-footer x-panel-fbar,与 footer 按钮互斥场景可单独使用) */
76
+ fbar?: React.ReactNode;
77
+ children?: React.ReactNode;
78
+ style?: React.CSSProperties;
79
+ /** 根节点基础 class 前缀(默认 'x-panel';unstyled 时强制为 'x-plain') */
80
+ baseCls?: string;
81
+ /** true 时无样式模式(对齐 Ext unstyled:baseCls 改为 'x-plain',仅保留布局必需样式) */
82
+ unstyled?: boolean;
83
+ /** true 时点击标题栏(任意位置)即可折叠/展开(对齐 Ext titleCollapse,默认 false 仅 toggle 工具可折叠) */
84
+ titleCollapse?: boolean;
85
+ /** 受控折叠:抑制组件内部自切换,折叠态完全由 collapsed 属性驱动(Accordion 子面板使用,避免父子双轨冲突) */
86
+ collapseControlled?: boolean;
87
+ /** toggle 工具相对自定义 tools 的顺序:true=前置(Ext 默认),false=后置 */
88
+ collapseFirst?: boolean;
89
+ /** footer 按钮最小宽度(px,对齐 Ext minButtonWidth,默认 75) */
90
+ minButtonWidth?: number;
91
+ /** footer/fbar 底部按钮水平对齐(对齐 Ext buttonAlign,默认 'right')。'left'|'center'|'right' */
92
+ buttonAlign?: 'left' | 'center' | 'right';
93
+ /** footer 水平内边距(px)。消掉「frame 边框内缩 ~6px + footer 内边距」两层叠加导致按钮离左右边框过远;
94
+ * 底部固定保留 5px(对齐 Ext .x-panel-btns{padding:5px} 的 bottom),避免按钮贴圆角底边框。
95
+ * undefined = 保持 Ext 标准 5px(四方向)。 */
96
+ footerPadding?: number | string;
97
+ /** 是否禁用面板(对齐 Ext disabled:加 x-item-disabled 并屏蔽交互) */
98
+ disabled?: boolean;
99
+ }
100
+
101
+ // 与原版 Ext.BLANK_IMAGE_URL 等价:1x1 透明 gif,避免网络请求
102
+ const BLANK =
103
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
104
+
105
+ /**
106
+ * 保真映射(来源 src/widgets/Panel.js):
107
+ * - 模板结构:onRender(frame / 非 frame 两套 DOM),class 名与 Ext JS 3.2.1 完全一致。
108
+ * - 头部:x-panel-header > span.x-panel-header-text + tools(x-tool x-tool-{id})
109
+ * - 图标:header 加 x-panel-icon,并插入 img.x-panel-inline-icon {iconCls}
110
+ * - 状态类:x-panel-collapsed / x-panel-noborder(el+body+header+footer+tbar+bbar)/ x-panel-body-noheader
111
+ * - unstyled:baseCls 改为 x-plain(x-plain-header/body/tbar/...),仅保留布局必需样式
112
+ * - fbar:x-panel-footer > x-panel-fbar 底部工具条容器
113
+ * - 命令式 API:collapse/expand/toggleCollapse/setTitle/setIconClass/getCollapsed
114
+ * React 仅重写行为与状态,DOM 结构与 class 名与 Ext JS 3.2.1 完全一致。
115
+ */
116
+ export const Panel = forwardRef<PanelHandle, PanelProps>(function Panel(
117
+ {
118
+ title,
119
+ frame = false,
120
+ iconCls,
121
+ cls = '',
122
+ header,
123
+ collapsed = false,
124
+ collapsible = false,
125
+ hideCollapseTool = false,
126
+ collapseControlled = false,
127
+ border = true,
128
+ bodyPadding,
129
+ width,
130
+ height,
131
+ headerClassName,
132
+ onHeaderClick,
133
+ bodyStyle,
134
+ bodyFlex,
135
+ bodyClassName,
136
+ tools,
137
+ tbar,
138
+ bbar,
139
+ footer,
140
+ fbar,
141
+ children,
142
+ style,
143
+ baseCls,
144
+ unstyled = false,
145
+ titleCollapse = false,
146
+ collapseFirst = true,
147
+ minButtonWidth,
148
+ buttonAlign = 'right',
149
+ footerPadding,
150
+ disabled = false,
151
+ },
152
+ ref,
153
+ ) {
154
+ const b = baseCls ?? (unstyled ? 'x-plain' : 'x-panel');
155
+ const showHeader =
156
+ header !== false && (!!title || !!tools?.length || collapsible || !!iconCls);
157
+ const noborder = border === false;
158
+
159
+ // 折叠态:受控(collapsed prop) + 非受控(内部点击切换)双轨。
160
+ // 原生 .x-panel-collapsed 仅调整边框/图标,body 显隐由 JS 控制(对齐 Ext 行为)。
161
+ const [dynCollapsed, setDynCollapsed] = useState(collapsed);
162
+ useEffect(() => {
163
+ setDynCollapsed(collapsed);
164
+ }, [collapsed]);
165
+ // 受控折叠:直接以 collapsed 属性为准,避免依赖 effect 同步产生闪烁
166
+ const effectiveCollapsed = collapseControlled ? collapsed : dynCollapsed;
167
+
168
+ // 运行时标题/图标(对齐 Ext setTitle / setIconClass)
169
+ const [dynTitle, setDynTitle] = useState(title);
170
+ const [dynIconCls, setDynIconCls] = useState(iconCls);
171
+ useEffect(() => {
172
+ setDynTitle(title);
173
+ }, [title]);
174
+ useEffect(() => {
175
+ setDynIconCls(iconCls);
176
+ }, [iconCls]);
177
+
178
+ // 命令式句柄(对齐 Ext.Panel 公开方法)
179
+ useImperativeHandle(ref, () => ({
180
+ collapse: () => setDynCollapsed(true),
181
+ expand: () => setDynCollapsed(false),
182
+ toggleCollapse: () => setDynCollapsed((c) => !c),
183
+ setTitle: (t: string) => setDynTitle(t),
184
+ setIconClass: (c?: string) => setDynIconCls(c),
185
+ getCollapsed: () => dynCollapsed,
186
+ }));
187
+
188
+ const handleHeaderClick = (e?: React.MouseEvent) => {
189
+ onHeaderClick?.(e);
190
+ if (collapsible && titleCollapse && !collapseControlled) setDynCollapsed((c) => !c);
191
+ };
192
+
193
+ const toggleHandler = (e?: React.MouseEvent) => {
194
+ onHeaderClick?.(e);
195
+ if (collapsible && !collapseControlled) setDynCollapsed((c) => !c);
196
+ };
197
+
198
+ // collapsible 时注入 toggle 工具;collapseFirst 决定其相对自定义 tools 的顺序
199
+ const toggleTool: PanelTool = { id: 'toggle', handler: toggleHandler };
200
+ const effectiveTools: PanelTool[] = collapsible && !hideCollapseTool
201
+ ? collapseFirst
202
+ ? [toggleTool, ...(tools ?? [])]
203
+ : [...(tools ?? []), toggleTool]
204
+ : (tools ?? []);
205
+
206
+ const headerEl = showHeader ? (
207
+ <div
208
+ className={[
209
+ `${b}-header`,
210
+ dynIconCls ? `${b}-icon` : '',
211
+ noborder ? `${b}-header-noborder` : '',
212
+ headerClassName ?? '',
213
+ ]
214
+ .filter(Boolean)
215
+ .join(' ')}
216
+ unselectable="on"
217
+ onClick={handleHeaderClick}
218
+ >
219
+ {dynIconCls && (
220
+ <img className={`${b}-inline-icon ${dynIconCls}`} src={BLANK} alt="" />
221
+ )}
222
+ {dynTitle && <span className={`${b}-header-text`}>{dynTitle}</span>}
223
+ {effectiveTools
224
+ .filter((t) => !t.hidden)
225
+ .map((t) => (
226
+ <div
227
+ key={t.id}
228
+ className={`x-tool x-tool-${t.id}`}
229
+ title={t.tooltip}
230
+ unselectable="on"
231
+ onClick={(e) => {
232
+ // 工具点击默认不冒泡到 header,避免 toggle 被触发两次
233
+ if (t.stopEvent !== false) e.stopPropagation();
234
+ t.handler?.(e);
235
+ }}
236
+ >
237
+ &#160;
238
+ </div>
239
+ ))}
240
+ </div>
241
+ ) : null;
242
+
243
+ const bodyClass = [
244
+ `${b}-body`,
245
+ !showHeader ? `${b}-body-noheader` : '',
246
+ noborder ? `${b}-body-noborder` : '',
247
+ bodyClassName ?? '',
248
+ ]
249
+ .filter(Boolean)
250
+ .join(' ');
251
+
252
+ // 区域面板保真模式(bodyFlex):height 给定时,面板固定高度,内部 flex 纵向排列,
253
+ // header 不伸缩、body flex:1 填满剩余,header 因长标题换行变高时 body 自动收缩,不溢出。
254
+ const useFlex = !!bodyFlex && height != null;
255
+ const bwrapStyle: React.CSSProperties | undefined = useFlex
256
+ ? { flex: '1 1 0px', minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }
257
+ : undefined;
258
+ const headerNode = useFlex && headerEl
259
+ ? React.cloneElement(headerEl, { style: { ...(headerEl.props as any).style, flex: '0 0 auto' } })
260
+ : headerEl;
261
+
262
+ const rootClass = [
263
+ b,
264
+ cls,
265
+ effectiveCollapsed ? `${b}-collapsed` : '',
266
+ noborder ? `${b}-noborder` : '',
267
+ disabled ? 'x-item-disabled' : '',
268
+ ]
269
+ .filter(Boolean)
270
+ .join(' ');
271
+
272
+ const rootStyle: React.CSSProperties = {
273
+ width,
274
+ height,
275
+ ...(useFlex ? { display: 'flex', flexDirection: 'column', overflow: 'hidden' } : {}),
276
+ ...(bodyPadding !== undefined
277
+ ? {
278
+ '--x-panel-body-padding':
279
+ typeof bodyPadding === 'number' ? `${bodyPadding}px` : bodyPadding,
280
+ }
281
+ : {}),
282
+ ...style,
283
+ };
284
+
285
+ // 保真注意:Ext JS 的 frame 面板不将 bodyPadding 作为 inline style 落在
286
+ // .x-panel-body 上(实测 golden 的 .x-panel-body padding 为 0px)。
287
+ // frame 面板的内间距由 9 宫格 .x-panel-mc 的 CSS padding 承接;
288
+ // 非 frame 面板同样不在此处注入,由外部/主题自然决定。
289
+ // 但 bodyStyle(如 accordion 子面板 padding:15px)由调用方显式传入。
290
+ const bodyEl = (
291
+ <div
292
+ className={bodyClass}
293
+ style={{
294
+ ...bodyStyle,
295
+ ...(useFlex ? { flex: '1 1 auto', minHeight: 0, overflow: 'auto' } : {}),
296
+ display: effectiveCollapsed ? 'none' : undefined,
297
+ ...(disabled ? { pointerEvents: 'none' } : {}),
298
+ }}
299
+ >
300
+ {children}
301
+ </div>
302
+ );
303
+
304
+ // footer(x-panel-footer x-panel-btns)与 fbar(x-panel-footer > x-panel-fbar)对齐 Ext,
305
+ // 二者可共存;折叠时整体隐藏(保留 DOM 节点,兼容 Accordion 等内部复用)。
306
+ const footerNodes: React.ReactNode[] = [];
307
+ if (footer) {
308
+ footerNodes.push(
309
+ <div
310
+ key="btns"
311
+ className={`${b}-footer ${b}-btns`}
312
+ style={{
313
+ ...footerStyle(minButtonWidth),
314
+ ...footerFlowReset,
315
+ // footerPadding 控制「水平」内边距(消掉 frame 边框 6px + footer 内边距叠加的观感),
316
+ // 底部保留 5px(对齐 Ext .x-panel-btns{padding:5px} 的 bottom),避免按钮贴圆角底边框。
317
+ ...(footerPadding !== undefined
318
+ ? { padding: `${footerPadding}px ${footerPadding}px 5px ${footerPadding}px` }
319
+ : {}),
320
+ display: 'flex',
321
+ flexWrap: 'wrap',
322
+ gap: 3,
323
+ // 对齐 Ext buttonAlign(默认 'right'):footer 按钮水平对齐方式
324
+ justifyContent:
325
+ buttonAlign === 'center'
326
+ ? 'center'
327
+ : buttonAlign === 'right'
328
+ ? 'flex-end'
329
+ : 'flex-start',
330
+ }}
331
+ >
332
+ {footer}
333
+ </div>,
334
+ );
335
+ }
336
+ if (fbar) {
337
+ // 对齐 Ext fbar:原生 Toolbar 根 div 同时带 x-toolbar 与 toolbarCls(x-panel-fbar)
338
+ // (源码 toolbarCls 是「追加」到 x-toolbar 之上,而非替换)。.x-panel-fbar 仅覆盖
339
+ // white-space/font,不重设背景/边框,故必须保留 x-toolbar 类,否则 .x-toolbar 下的
340
+ // 布局表宽(.x-toolbar .x-toolbar-ct{width:100%})与 white-space:nowrap 等规则全部失效,
341
+ // fbar 内工具栏会因缺少 x-toolbar 样式而溢出面板宽度、按钮换行/错位。
342
+ // 若调用方传入的是 <Toolbar>,自动注入 footer 风格根类(保留布局/字体辅助类)。
343
+ // 注:原生 .x-panel-footer 用 position:absolute(依赖 JS 同步 top/width),本 React 实现
344
+ // 以正常流渲染(tbar/bbar 同理),故以内联 position:static/width:100% 覆盖其 absolute 定位,
345
+ // 使 footer 自然落在 body 底部一行、宽度充满 Panel(内联优先级最高,胜出原生 .x-panel-bc .x-panel-footer)。
346
+ const fbarContent =
347
+ React.isValidElement(fbar) && (fbar as React.ReactElement).type === Toolbar
348
+ ? React.cloneElement(
349
+ fbar as React.ReactElement<{ rootClass?: string; buttonAlign?: 'left' | 'center' | 'right' }>,
350
+ {
351
+ rootClass: `x-toolbar ${b}-fbar x-toolbar-layout-ct x-small-editor`,
352
+ buttonAlign,
353
+ },
354
+ )
355
+ : fbar;
356
+ footerNodes.push(
357
+ <div key="fbar" className={`${b}-footer`} style={{ ...footerFlowReset }}>
358
+ {fbarContent}
359
+ </div>,
360
+ );
361
+ }
362
+ const footerEl = footerNodes.length ? (
363
+ <div style={effectiveCollapsed ? { display: 'none' } : undefined}>{footerNodes}</div>
364
+ ) : null;
365
+
366
+ if (!frame) {
367
+ return (
368
+ <div className={rootClass} style={rootStyle}>
369
+ {headerNode}
370
+ <div className={`${b}-bwrap`} style={bwrapStyle}>
371
+ {tbar && (
372
+ <div className={`${b}-tbar`} style={effectiveCollapsed ? { display: 'none' } : undefined}>
373
+ {tbar}
374
+ </div>
375
+ )}
376
+ {bodyEl}
377
+ {bbar && (
378
+ <div className={`${b}-bbar`} style={effectiveCollapsed ? { display: 'none' } : undefined}>
379
+ {bbar}
380
+ </div>
381
+ )}
382
+ {footerEl}
383
+ </div>
384
+ </div>
385
+ );
386
+ }
387
+
388
+ // frame = true:9 宫格圆角结构(与 onRender 实际产出一致)
389
+ return (
390
+ <div className={rootClass} style={rootStyle}>
391
+ <div className={`${b}-tl`}>
392
+ <div className={`${b}-tr`}>
393
+ <div className={`${b}-tc`}>{headerNode}</div>
394
+ </div>
395
+ </div>
396
+ <div className={`${b}-bwrap`} style={bwrapStyle}>
397
+ <div className={`${b}-ml`}>
398
+ <div className={`${b}-mr`}>
399
+ <div className={`${b}-mc`}>
400
+ {tbar && (
401
+ <div className={`${b}-tbar`} style={effectiveCollapsed ? { display: 'none' } : undefined}>
402
+ {tbar}
403
+ </div>
404
+ )}
405
+ {bodyEl}
406
+ {bbar && (
407
+ <div className={`${b}-bbar`} style={effectiveCollapsed ? { display: 'none' } : undefined}>
408
+ {bbar}
409
+ </div>
410
+ )}
411
+ </div>
412
+ </div>
413
+ </div>
414
+ <div
415
+ className={`${b}-bl${footerNodes.length ? '' : ` ${b}-nofooter`}`}
416
+ >
417
+ <div className={`${b}-br`}>
418
+ <div
419
+ className={`${b}-bc`}
420
+ style={{ height: footerNodes.length ? 'auto' : 6, fontSize: 0, lineHeight: 0 }}
421
+ >
422
+ {footerEl}
423
+ </div>
424
+ </div>
425
+ </div>
426
+ </div>
427
+ </div>
428
+ );
429
+ });
430
+
431
+ // 工具函数(必须在组件外定义,避免每次渲染重建)
432
+ function footerStyle(min?: number): React.CSSProperties {
433
+ return min !== undefined
434
+ ? ({ ['--x-panel-minbtn']: `${min}px` } as React.CSSProperties)
435
+ : {};
436
+ }
437
+
438
+ // 复位 ext-theme.css 中 `.x-panel-footer { position:absolute; height:9px; overflow:hidden;
439
+ // font-size:1px; line-height:9px }` 的冲突规则(该规则系主题层为底部 resize 手柄保留,
440
+ // 与本项目「footer 以正常流渲染」的约定相悖)。以更高优先级的内联样式完整复位,
441
+ // DOM/class 名保持不变,符合「不新增/不修改 CSS 文件」约束。
442
+ const footerFlowReset: React.CSSProperties = {
443
+ position: 'static',
444
+ width: '100%',
445
+ height: 'auto',
446
+ overflow: 'visible',
447
+ fontSize: '12px',
448
+ lineHeight: 'normal',
449
+ };
450
+
451
+ export default Panel;
@@ -0,0 +1,127 @@
1
+ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useState, useEffect } from 'react';
2
+
3
+ export interface ProgressBarProps {
4
+ /** 进度值 0~1,默认 0 */
5
+ value?: number;
6
+ /** 进度条文字,默认空 */
7
+ text?: string;
8
+ /** 组件总宽(含边框),默认自适应 */
9
+ width?: number;
10
+ /** 额外 class,如 'x-progress-red' */
11
+ cls?: string;
12
+ /** 根元素 id */
13
+ id?: string;
14
+ /** 点击事件(等价 Ext 给 progress bar 元素绑 'click'),回调携带原始 MouseEvent 便于计算点击位置 */
15
+ onClick?: (e: React.MouseEvent) => void;
16
+ }
17
+
18
+ /**
19
+ * 运行时句柄(对齐 Ext.ProgressBar 公共 API):
20
+ * - setProgress(value, text?) / updateProgress(value, text?):同时更新进度与文字(等价 Ext.updateProgress)
21
+ * - setText(text):仅更新文字(Ext 无独立 setText,但 text 随 updateProgress 走)
22
+ * - setWidth(width):调整总宽(Ext.setWidth)
23
+ * - reset():归零并隐藏白字层(Ext.reset)
24
+ * - getValue()/getText()/getWidth():读当前状态
25
+ */
26
+ export interface ProgressBarHandle {
27
+ setProgress: (value: number, text?: string) => void;
28
+ updateProgress: (value: number, text?: string) => void;
29
+ setText: (text: string) => void;
30
+ setWidth: (width: number) => void;
31
+ reset: () => void;
32
+ getValue: () => number;
33
+ getText: () => string;
34
+ getWidth: () => number | undefined;
35
+ }
36
+
37
+ // 保真映射(来源 Ext.ProgressBar.onRender + afterRender + updateProgress):
38
+ // - 根 div class = 'x-progress-wrap'(Ext 不会把 baseCls 'x-progress' 作为额外 class 落到 el 上,
39
+ // 仅模板根节点 '{cls}-wrap' = 'x-progress-wrap')。
40
+ // - 结构:x-progress-wrap > x-progress-inner >
41
+ // (x-progress-bar > x-progress-text, x-progress-text.x-progress-text-back)
42
+ // - 根 style.width = 传入 width - 2(1px 边框×2),使视觉总宽=width(golden 实测 200→198)。
43
+ // - bar 像素宽 = floor(value × innerContentW),与 golden 的 Math.floor(value*firstChild.offsetWidth) 一致。
44
+ // - bar 高度:本项目全局 box-sizing:border-box,故 bar inline height=18 即「含 1px 上下边框的总高 18px」,
45
+ // 与 .x-progress-inner 的 height:18px 完全对齐(若写 16 会在 inner 底部空出 2px 间隙)。
46
+ // - 顶层 text 容器 width = barW - 10(Ext setWidth 折算 padding 1+5+5=10),z-index:99;文字落在其内层 div(width=innerW)。
47
+ // - 文字内层 div 固定 width=innerW、height=18,使文字跨整条进度条居中;顶层白色文字被 bar 宽度裁切,
48
+ // 形成"已填充蓝条内白字 / 未填充灰底深蓝字"的经典分层效果。
49
+ export const ProgressBar = forwardRef<ProgressBarHandle, ProgressBarProps>(
50
+ ({ value = 0, text = '', width, cls = '', id, onClick }: ProgressBarProps, ref) => {
51
+ // 双轨:prop 初始化 + 句柄运行时改写 + useEffect 同步 prop 变化
52
+ const [dynValue, setDynValue] = useState(value);
53
+ const [dynText, setDynText] = useState(text);
54
+ const [dynWidth, setDynWidth] = useState(width);
55
+ const [dynCls, setDynCls] = useState(cls);
56
+
57
+ useImperativeHandle(
58
+ ref,
59
+ () => ({
60
+ setProgress: (v: number, t?: string) => {
61
+ setDynValue(v);
62
+ if (t !== undefined) setDynText(t);
63
+ },
64
+ updateProgress: (v: number, t?: string) => {
65
+ setDynValue(v);
66
+ if (t !== undefined) setDynText(t);
67
+ },
68
+ setText: (t: string) => setDynText(t),
69
+ setWidth: (w: number) => setDynWidth(w),
70
+ reset: () => {
71
+ setDynValue(0);
72
+ setDynText('');
73
+ },
74
+ getValue: () => dynValue,
75
+ getText: () => dynText,
76
+ getWidth: () => dynWidth,
77
+ }),
78
+ [dynValue, dynText, dynWidth],
79
+ );
80
+
81
+ useEffect(() => setDynValue(value), [value]);
82
+ useEffect(() => setDynText(text), [text]);
83
+ useEffect(() => setDynWidth(width), [width]);
84
+ useEffect(() => setDynCls(cls), [cls]);
85
+
86
+ const innerRef = useRef<HTMLDivElement>(null);
87
+ const [measuredW, setMeasuredW] = useState(0);
88
+
89
+ useLayoutEffect(() => {
90
+ if (innerRef.current) {
91
+ setMeasuredW(innerRef.current.offsetWidth);
92
+ }
93
+ });
94
+
95
+ // 固定 width 时直接推导 innerW(= width-2,对齐 golden 实测),避免首帧 innerW=0 闪现;
96
+ // 自适应(width 未传)时回退到运行时测量。
97
+ const innerW = dynWidth != null ? dynWidth - 2 : measuredW;
98
+
99
+ const barW = Math.floor(dynValue * innerW);
100
+ const frontTextW = Math.max(0, barW - 10);
101
+ // golden 行为:updateProgress(value>0) 时 textTopEl.removeClass('x-hidden');
102
+ // reset(value=0) 时 textTopEl.addClass('x-hidden')(白字层隐藏,仅剩灰底深蓝 back 文字)。
103
+ const frontTextCls = ['x-progress-text', dynValue === 0 ? 'x-hidden' : ''].filter(Boolean).join(' ');
104
+
105
+ return (
106
+ <div
107
+ className={['x-progress-wrap', dynCls].filter(Boolean).join(' ')}
108
+ id={id}
109
+ onClick={onClick}
110
+ style={{ cursor: onClick ? 'pointer' : undefined, ...(dynWidth != null ? { width: dynWidth - 2 } : undefined) }}
111
+ >
112
+ <div className="x-progress-inner" ref={innerRef}>
113
+ <div className="x-progress-bar" style={{ width: barW, height: 18, transition: 'width 0.3s ease-out' }}>
114
+ <div className={frontTextCls} style={{ zIndex: 99, width: frontTextW }}>
115
+ <div style={{ width: innerW, height: 18 }}>{dynText}</div>
116
+ </div>
117
+ </div>
118
+ <div className="x-progress-text x-progress-text-back">
119
+ <div style={{ width: innerW, height: 18 }}>{dynText}</div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ },
125
+ );
126
+
127
+ ProgressBar.displayName = 'ProgressBar';