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
package/src/main.tsx ADDED
@@ -0,0 +1,2649 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import './styles/ext-theme.css';
3
+ import { ensureDefaultTheme } from './util/theme';
4
+ ensureDefaultTheme();
5
+ import { useState, useRef, useEffect } from 'react';
6
+ import { Button } from './components/Button';
7
+ import { CycleButton, type CycleButtonHandle } from './components/CycleButton';
8
+ import { DataView, type DataViewHandle } from './components/DataView';
9
+ import { ListView, ListViewHandle } from './components/ListView';
10
+ import { Editor, type EditorHandle } from './components/Editor';
11
+ import { Panel, type PanelHandle } from './components/Panel';
12
+ import { TabPanel, type TabItem, type TabPanelHandle } from './components/TabPanel';
13
+ import { Menu, type MenuHandle } from './components/Menu';
14
+ import {
15
+ FormPanel,
16
+ FormField,
17
+ TextField,
18
+ TextArea,
19
+ NumberField,
20
+ Checkbox,
21
+ Radio,
22
+ RadioGroup,
23
+ Hidden,
24
+ ComboBox,
25
+ TimeField,
26
+ DateField,
27
+ type TextFieldHandle,
28
+ type ComboBoxHandle,
29
+ type DateFieldHandle,
30
+ computeError,
31
+ } from './components/Form';
32
+ import { DisplayField } from './components/DisplayField';
33
+ import { CheckboxGroup, type CheckboxGroupHandle } from './components/CheckboxGroup';
34
+ import { SpinnerField } from './components/SpinnerField';
35
+ import { QuickTip, type QuickTipHandle } from './components/QuickTip';
36
+ import { Slider } from './components/Slider';
37
+ import { ProgressBar, type ProgressBarHandle } from './components/ProgressBar';
38
+ import { DatePicker } from './components/DatePicker';
39
+ import { TreePanel, type TreeNodeData, type TreePanelHandle } from './components/Tree';
40
+ import { Toolbar, type ToolbarHandle } from './components/Toolbar';
41
+ import { Grid, type GridHandle } from './components/Grid';
42
+ import { Window, type WindowHandle } from './components/Window';
43
+ import { PagingToolbar, type PagingToolbarHandle } from './components/PagingToolbar';
44
+ import { EditorGrid, type EditorGridHandle } from './components/EditorGrid';
45
+ import { TreeGrid, type TreeGridNode, type TreeGridHandle } from './components/TreeGrid';
46
+ import { FieldSet, type FieldSetHandle } from './components/FieldSet';
47
+ import { MessageBox, type MessageBoxHandle } from './components/MessageBox';
48
+ import { ToolTip, type ToolTipHandle } from './components/ToolTip';
49
+ import { Accordion, type AccordionHandle } from './components/Accordion';
50
+ import { ColorPalette, type ColorPaletteHandle } from './components/ColorPalette';
51
+ import { ColorMenu, type ColorMenuHandle } from './components/ColorMenu';
52
+ import { Resizable } from './components/Resizable';
53
+ import { Box } from './components/Box';
54
+ import { SplitBar } from './components/SplitBar';
55
+ import { LoadMask } from './components/LoadMask';
56
+ import { ButtonGroup, type ButtonGroupHandle } from './components/ButtonGroup';
57
+ import { StatusBar, type StatusBarHandle } from './components/StatusBar';
58
+ import { PropertyGrid, type PropertyGridHandle } from './components/PropertyGrid';
59
+ import { BorderLayout } from './components/BorderLayout';
60
+ import { Viewport, type ViewportHandle } from './components/Viewport';
61
+ import { CardLayout } from './components/CardLayout';
62
+ import { ColumnLayout } from './components/ColumnLayout';
63
+ import { AnchorLayout } from './components/AnchorLayout';
64
+ import { FitLayout } from './components/FitLayout';
65
+ import { TableLayout } from './components/TableLayout';
66
+ import { HtmlEditor, type HtmlEditorHandle } from './components/HtmlEditor';
67
+
68
+ const tabs: TabItem[] = [
69
+ { title: 'Tab 1', content: <div style={{ padding: 10 }}>内容一</div> },
70
+ { title: 'Tab 2', iconCls: 'x-tab-tab-icon', content: <div style={{ padding: 10 }}>内容二(带图标)</div> },
71
+ { title: 'Tab 3(可关闭)', closable: true, content: <div style={{ padding: 10 }}>内容三</div> },
72
+ { title: 'Tab 4(禁用)', disabled: true, content: <div style={{ padding: 10 }}>内容四</div> },
73
+ { title: 'Tab 5', tabCls: 'x-tab-styled', content: <div style={{ padding: 10 }}>内容五(自定义 tabCls)</div> },
74
+ ];
75
+
76
+ const treeData: TreeNodeData[] = [
77
+ {
78
+ id: 'n1', text: '系统管理', expanded: true, children: [
79
+ { id: 'n1-1', text: '用户管理', children: [
80
+ { id: 'n1-1-1', text: '新增用户' },
81
+ { id: 'n1-1-2', text: '用户列表', checked: true },
82
+ ] },
83
+ { id: 'n1-2', text: '角色管理', children: [
84
+ { id: 'n1-2-1', text: '角色分配' },
85
+ ] },
86
+ { id: 'n1-3', text: '权限配置', disabled: true },
87
+ ],
88
+ },
89
+ {
90
+ id: 'n2', text: '业务中心', expanded: true, children: [
91
+ { id: 'n2-1', text: '订单管理', children: [
92
+ { id: 'n2-1-1', text: '待发货' },
93
+ { id: 'n2-1-2', text: '已完成' },
94
+ ] },
95
+ { id: 'n2-2', text: '报表中心' },
96
+ ],
97
+ },
98
+ { id: 'n3', text: '帮助文档', expandable: true },
99
+ ];
100
+
101
+ // TreeGrid 演示数据(与 reference/golden.js #treegrid 完全同构:3 列 Task/Duration/Assigned To,
102
+ // t1/t2 初始展开,t3 expandable 折叠)。
103
+ const treegridData: TreeGridNode[] = [
104
+ {
105
+ id: 't1', task: '项目规划', duration: 6, user: 'Alice', expanded: true, children: [
106
+ { id: 't1-1', task: '需求调研', duration: 3, user: 'Bob' },
107
+ { id: 't1-2', task: '方案设计', duration: 3, user: 'Carol' },
108
+ ],
109
+ },
110
+ {
111
+ id: 't2', task: '开发', duration: 12, user: 'Dave', expanded: true, children: [
112
+ { id: 't2-1', task: '前端', duration: 6, user: 'Eve' },
113
+ { id: 't2-2', task: '后端', duration: 6, user: 'Frank' },
114
+ ],
115
+ },
116
+ { id: 't3', task: '测试', duration: 4, user: 'Grace', expandable: true },
117
+ ];
118
+
119
+ // 表单校验演示(行为测试专用,不参与保真循环——无效态由 golden 静态 markInvalid 对应)。
120
+ // 用外部「校验」按钮触发两字段校验,复刻 Ext FormPanel.getForm().isValid() 的客户端校验。
121
+ const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
122
+ function ValidateDemo() {
123
+ const nameRef = useRef<TextFieldHandle>(null);
124
+ const emailRef = useRef<TextFieldHandle>(null);
125
+ const validateAll = () => {
126
+ nameRef.current?.validate();
127
+ emailRef.current?.validate();
128
+ };
129
+ return (
130
+ <div data-testid="form-validate" style={{ width: 300 }}>
131
+ <FormPanel title="表单校验" width={300}>
132
+ <FormField fieldLabel="姓名">
133
+ <TextField
134
+ ref={nameRef}
135
+ name="name"
136
+ allowBlank={false}
137
+ invalidText="姓名不能为空"
138
+ width={180}
139
+ />
140
+ </FormField>
141
+ <FormField fieldLabel="邮箱">
142
+ <TextField
143
+ ref={emailRef}
144
+ name="email"
145
+ width={180}
146
+ validator={(v) => (emailRe.test(v) ? true : '邮箱格式不正确')}
147
+ />
148
+ </FormField>
149
+ </FormPanel>
150
+ <div style={{ marginTop: 8 }}>
151
+ <Button text="校验" onClick={validateAll} />
152
+ </div>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ // 表单校验联动演示(跨字段校验 + 提交按钮随有效态联动,不参与保真循环)。
158
+ // 确认密码字段的 validator 读取密码当前值(联动);密码变更时若确认框已交互过则即时
159
+ // 重校验确认字段,使其错误态随密码值刷新。等价于 Ext FormPanel 的跨字段校验与 monitorValid。
160
+ function FormLinkageDemo() {
161
+ const [pwd, setPwd] = useState('');
162
+ const [confirm, setConfirm] = useState('');
163
+ const [touched, setTouched] = useState({ pwd: false, confirm: false });
164
+ const pwdRef = useRef<TextFieldHandle>(null);
165
+ const confirmRef = useRef<TextFieldHandle>(null);
166
+
167
+ const optsPwd = { allowBlank: false, invalidText: '请输入密码' } as const;
168
+ const optsConfirm = {
169
+ allowBlank: false,
170
+ invalidText: '请再次输入密码',
171
+ validator: (v: string) => (v === pwd ? true : '两次输入的密码不一致'),
172
+ } as const;
173
+
174
+ // 全部通过才允许提交(等价 Ext monitorValid),用纯函数计算,避免初始即标红。
175
+ const allValid =
176
+ computeError(pwd, optsPwd) === null && computeError(confirm, optsConfirm) === null;
177
+
178
+ // 联动:密码变化时(且确认框已交互过)即时重校验确认字段,错误态随密码刷新。
179
+ useEffect(() => {
180
+ if (touched.confirm) confirmRef.current?.validate();
181
+ }, [pwd, touched.confirm]);
182
+
183
+ return (
184
+ <div data-testid="form-linkage" style={{ width: 320 }}>
185
+ <FormPanel title="账户注册" width={320}>
186
+ <FormField fieldLabel="密码">
187
+ <TextField
188
+ ref={pwdRef}
189
+ name="pwd"
190
+ inputType="password"
191
+ allowBlank={false}
192
+ invalidText="请输入密码"
193
+ width={200}
194
+ value={pwd}
195
+ onChange={(v) => setPwd(v)}
196
+ onBlur={() => setTouched((t) => ({ ...t, pwd: true }))}
197
+ />
198
+ </FormField>
199
+ <FormField fieldLabel="确认密码">
200
+ <TextField
201
+ ref={confirmRef}
202
+ name="confirm"
203
+ inputType="password"
204
+ allowBlank={false}
205
+ invalidText="请再次输入密码"
206
+ width={200}
207
+ value={confirm}
208
+ validator={(v) => (v === pwd ? true : '两次输入的密码不一致')}
209
+ onChange={(v) => {
210
+ setConfirm(v);
211
+ setTouched((t) => ({ ...t, confirm: true }));
212
+ }}
213
+ onBlur={() => setTouched((t) => ({ ...t, confirm: true }))}
214
+ />
215
+ </FormField>
216
+ </FormPanel>
217
+ <div style={{ marginTop: 8 }}>
218
+ <Button
219
+ text="提交"
220
+ disabled={!allValid}
221
+ onClick={() => {
222
+ pwdRef.current?.validate();
223
+ confirmRef.current?.validate();
224
+ }}
225
+ />
226
+ </div>
227
+ </div>
228
+ );
229
+ }
230
+
231
+ // 生成 TimeField 时间点数据源('g:i A' 格式,对齐 Form.tsx 的 fmtTime),默认范围 12:00 AM–11:30 PM。
232
+ function genTimes(increment: number, format = 'g:i A') {
233
+ const fmt = (h: number, m: number) => {
234
+ if (format === 'H:i') return `${h < 10 ? '0' + h : h}:${m < 10 ? '0' + m : m}`;
235
+ const period = h < 12 ? 'AM' : 'PM';
236
+ const hh = h % 12 === 0 ? 12 : h % 12;
237
+ return `${hh}:${m < 10 ? '0' + m : m} ${period}`;
238
+ };
239
+ const out: { value: string; text: string }[] = [];
240
+ for (let t = 0; t <= 1410; t += increment) {
241
+ const label = fmt(Math.floor(t / 60), t % 60);
242
+ out.push({ value: label, text: label });
243
+ }
244
+ return out;
245
+ }
246
+
247
+ // Panel 新能力夹具(对齐 Ext.Panel):unstyled/fbar/disabled + 命令式 ref 句柄。
248
+ // 无 golden 对应,仅入行为测试,不进保真像素循环。
249
+ function PanelFeaturesDemo() {
250
+ const ref = useRef<PanelHandle>(null);
251
+ return (
252
+ <div data-testid="panel-features" style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'flex-start' }}>
253
+ <div data-testid="panel-unstyled">
254
+ <Panel unstyled title="无样式" width={260}>
255
+ <p>plain 容器</p>
256
+ </Panel>
257
+ </div>
258
+ <div data-testid="panel-fbar">
259
+ <Panel title="带 fbar" frame width={260} fbar={<Toolbar items={[{ text: '左' }, { text: '右' }]} />}>
260
+ <p>正文</p>
261
+ </Panel>
262
+ </div>
263
+ <div data-testid="panel-disabled">
264
+ <Panel title="禁用" disabled width={260}>
265
+ <p>内容</p>
266
+ </Panel>
267
+ </div>
268
+ <div data-testid="panel-cmd">
269
+ <div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
270
+ <Button text="折叠" onClick={() => ref.current?.collapse()} />
271
+ <Button text="展开" onClick={() => ref.current?.expand()} />
272
+ <Button text="改标题" onClick={() => ref.current?.setTitle('运行中标题')} />
273
+ </div>
274
+ <Panel ref={ref} title="命令式面板" collapsible titleCollapse width={260}>
275
+ <p>用上方按钮控制</p>
276
+ </Panel>
277
+ </div>
278
+ <div data-testid="panel-tc-default">
279
+ <Panel title="仅工具折叠" collapsible width={260}>
280
+ <p>点标题栏不折叠,仅 toggle 工具折叠</p>
281
+ </Panel>
282
+ </div>
283
+ </div>
284
+ );
285
+ }
286
+
287
+ // 演示:RowEditor 行编辑器(对齐 Ext.ux.grid.RowEditor)——双击行进入编辑,保存写回数据、取消丢弃,
288
+ // 样式复用 ExtJS 官方 examples/ux/css/RowEditor.css,零新增手写 CSS。
289
+ function RowEditorDemo() {
290
+ const [rows, setRows] = useState<Array<Record<string, unknown>>>([
291
+ { id: 1, name: '张三', email: 'zhangsan@example.com', age: 28, salary: 12000 },
292
+ { id: 2, name: '李四', email: 'lisi@example.com', age: 32, salary: 15000 },
293
+ { id: 3, name: '王五', email: 'wangwu@example.com', age: 45, salary: 9000 },
294
+ { id: 4, name: '赵六', email: 'zhaoliu@example.com', age: 23, salary: 11000 },
295
+ ]);
296
+ return (
297
+ <div data-testid="roweditor" style={{ width: 500 }}>
298
+ <Grid
299
+ title="行编辑器示例"
300
+ width={500}
301
+ stripeRows
302
+ rowEditor={{
303
+ clicksToEdit: 2,
304
+ saveText: '保存',
305
+ cancelText: '取消',
306
+ onCommit: (origIdx, newRow) => {
307
+ setRows((prev) => {
308
+ const next = prev.slice();
309
+ next[origIdx] = { ...next[origIdx], ...newRow };
310
+ return next;
311
+ });
312
+ },
313
+ }}
314
+ columns={[
315
+ { header: '姓名', dataIndex: 'name', width: 110, editor: 'text' },
316
+ { header: '邮箱', dataIndex: 'email', width: 200, editor: 'text' },
317
+ { header: '年龄', dataIndex: 'age', width: 70, align: 'right', editor: 'number' },
318
+ { header: '薪资', dataIndex: 'salary', width: 100, align: 'right', editor: 'number' },
319
+ ]}
320
+ data={rows}
321
+ />
322
+ </div>
323
+ );
324
+ }
325
+
326
+ // 演示:列分组表头(对齐 Ext.ux.grid.ColumnHeaderGroup):表头顶部叠加一层跨列分组标题。
327
+ function ColumnHeaderGroupDemo() {
328
+ return (
329
+ <div data-testid="colheader-group" style={{ width: 480 }}>
330
+ <Grid
331
+ title="列分组表头示例"
332
+ width={480}
333
+ stripeRows
334
+ headerGroups={[
335
+ [
336
+ { header: '客户信息', colspan: 2, align: 'center' },
337
+ { header: '订单信息', colspan: 3, align: 'center' },
338
+ ],
339
+ ]}
340
+ columns={[
341
+ { header: '姓名', dataIndex: 'name', width: 110 },
342
+ { header: '城市', dataIndex: 'city', width: 90 },
343
+ { header: '商品', dataIndex: 'product', width: 120 },
344
+ { header: '数量', dataIndex: 'qty', width: 80, align: 'right' },
345
+ { header: '金额', dataIndex: 'amount', width: 80, align: 'right' },
346
+ ]}
347
+ data={[
348
+ { name: '张三', city: '上海', product: '键盘', qty: 2, amount: 200 },
349
+ { name: '李四', city: '北京', product: '鼠标', qty: 5, amount: 250 },
350
+ { name: '王五', city: '广州', product: '显示器', qty: 1, amount: 1200 },
351
+ ]}
352
+ />
353
+ </div>
354
+ );
355
+ }
356
+
357
+ // 演示:Ext.ux.grid.LockingGridView —— 前两列锁定(姓名/城市),右侧解锁列在窄容器内横向滚动,
358
+ // 纵向滚动两面板同步(样式复用 examples/ux/css/LockingGridView.css)。
359
+ function LockingViewDemo() {
360
+ return (
361
+ <div data-testid="locking-view" style={{ width: 500 }}>
362
+ <Grid
363
+ title="锁定列视图示例"
364
+ width={500}
365
+ height={150}
366
+ stripeRows
367
+ columns={[
368
+ { header: '姓名', dataIndex: 'name', width: 110, locked: true },
369
+ { header: '城市', dataIndex: 'city', width: 90, locked: true, align: 'center' },
370
+ { header: '商品', dataIndex: 'product', width: 160 },
371
+ { header: '数量', dataIndex: 'qty', width: 80, align: 'right' },
372
+ { header: '金额', dataIndex: 'amount', width: 110, align: 'right' },
373
+ { header: '下单日期', dataIndex: 'date', width: 130 },
374
+ ]}
375
+ data={[
376
+ { name: '张三', city: '上海', product: '机械键盘', qty: 2, amount: 200, date: '2026-01-12' },
377
+ { name: '李四', city: '北京', product: '无线鼠标', qty: 5, amount: 250, date: '2026-02-03' },
378
+ { name: '王五', city: '广州', product: '27寸显示器', qty: 1, amount: 1200, date: '2026-03-21' },
379
+ { name: '赵六', city: '深圳', product: 'USB-C 扩展坞', qty: 3, amount: 540, date: '2026-04-09' },
380
+ { name: '钱七', city: '杭州', product: '降噪耳机', qty: 4, amount: 880, date: '2026-05-18' },
381
+ { name: '孙八', city: '成都', product: '人体工学椅', qty: 1, amount: 1600, date: '2026-06-27' },
382
+ { name: '周九', city: '武汉', product: '4K 摄像头', qty: 2, amount: 760, date: '2026-07-14' },
383
+ { name: '吴十', city: '南京', product: '固态硬盘', qty: 6, amount: 420, date: '2026-08-02' },
384
+ { name: '郑一', city: '西安', product: '机械臂', qty: 1, amount: 2300, date: '2026-09-11' },
385
+ { name: '冯二', city: '重庆', product: '网络交换机', qty: 2, amount: 980, date: '2026-10-23' },
386
+ ]}
387
+ />
388
+ </div>
389
+ );
390
+ }
391
+
392
+ function GridFiltersDemo() {
393
+ const data = [
394
+ { name: '张三', city: '上海', product: '机械键盘', qty: 2, amount: 200, date: '2026-01-12', visible: true },
395
+ { name: '李四', city: '北京', product: '无线鼠标', qty: 5, amount: 250, date: '2026-02-03', visible: false },
396
+ { name: '王五', city: '广州', product: '27寸显示器', qty: 1, amount: 1200, date: '2026-03-21', visible: true },
397
+ { name: '赵六', city: '深圳', product: 'USB-C 扩展坞', qty: 3, amount: 540, date: '2026-04-09', visible: true },
398
+ { name: '钱七', city: '杭州', product: '降噪耳机', qty: 4, amount: 880, date: '2026-05-18', visible: false },
399
+ { name: '孙八', city: '成都', product: '人体工学椅', qty: 1, amount: 1600, date: '2026-06-27', visible: true },
400
+ { name: '周九', city: '武汉', product: '4K 摄像头', qty: 2, amount: 760, date: '2026-07-14', visible: true },
401
+ { name: '吴十', city: '南京', product: '固态硬盘', qty: 6, amount: 420, date: '2026-08-02', visible: false },
402
+ { name: '郑一', city: '西安', product: '机械臂', qty: 1, amount: 2300, date: '2026-09-11', visible: true },
403
+ { name: '冯二', city: '重庆', product: '网络交换机', qty: 2, amount: 980, date: '2026-10-23', visible: true },
404
+ ];
405
+ return (
406
+ <div data-testid="gridfilters" style={{ width: 560 }}>
407
+ <Grid
408
+ title="GridFilters 列过滤示例"
409
+ width={560}
410
+ height={260}
411
+ stripeRows
412
+ columns={[
413
+ { header: '姓名', dataIndex: 'name', width: 90, filter: { type: 'string' } },
414
+ {
415
+ header: '城市',
416
+ dataIndex: 'city',
417
+ width: 90,
418
+ filter: { type: 'list', options: ['上海', '北京', '广州', '深圳', '杭州', '成都'] },
419
+ },
420
+ { header: '商品', dataIndex: 'product', width: 150, filter: { type: 'string' } },
421
+ { header: '数量', dataIndex: 'qty', width: 70, align: 'right', filter: { type: 'numeric' } },
422
+ { header: '金额', dataIndex: 'amount', width: 90, align: 'right', filter: { type: 'numeric' } },
423
+ { header: '下单日期', dataIndex: 'date', width: 120, filter: { type: 'date' } },
424
+ { header: '可见', dataIndex: 'visible', width: 60, align: 'center', filter: { type: 'boolean' } },
425
+ ]}
426
+ data={data}
427
+ />
428
+ </div>
429
+ );
430
+ }
431
+
432
+ // 演示:保真复现 examples/grid-filtering/grid-filter-local(Ext.ux.grid.GridFilters 经典官方示例)。
433
+ // 列:Id(numeric) / Company(string) / Price(numeric) / Size(list) / Date(date) / Visible(boolean),
434
+ // 数据 27 行(来自 grid-filter.json),工具栏 Clear Filters / Show Filter Data 对齐原示例底部按钮。
435
+ function GridFilterLocalDemo() {
436
+ const gridRef = useRef<GridHandle>(null);
437
+ const data = [
438
+ { id: 1, company: '3m Co', price: 71.72, date: '2007-09-01 00:00:00', size: 'large', visible: true },
439
+ { id: 2, company: 'Aloca Inc', price: 29.01, date: '2007-08-01 00:00:00', size: 'medium', visible: false },
440
+ { id: 3, company: 'Altria Group Inc', price: 83.81, date: '2007-08-03 00:00:00', size: 'large', visible: false },
441
+ { id: 4, company: 'American Express Company', price: 52.55, date: '2008-01-04 00:00:00', size: 'extra large', visible: true },
442
+ { id: 5, company: 'American International Group Inc.', price: 64.13, date: '2008-03-04 00:00:00', size: 'small', visible: true },
443
+ { id: 6, company: 'AT&T Inc.', price: 31.61, date: '2008-02-01 00:00:00', size: 'extra large', visible: false },
444
+ { id: 7, company: 'Boeing Co.', price: 75.43, date: '2008-01-01 00:00:00', size: 'large', visible: true },
445
+ { id: 8, company: 'Caterpillar Inc.', price: 67.27, date: '2007-12-03 00:00:00', size: 'medium', visible: true },
446
+ { id: 9, company: 'Citigroup, Inc.', price: 49.37, date: '2007-11-24 00:00:00', size: 'large', visible: true },
447
+ { id: 10, company: 'E.I. du Pont de Nemours and Company', price: 40.48, date: '2007-05-09 00:00:00', size: 'extra large', visible: false },
448
+ { id: 11, company: 'Exxon Mobile Corp', price: 68.1, date: '2007-12-12 00:00:00', size: 'large', visible: true },
449
+ { id: 12, company: 'General Electric Company', price: 34.14, date: '2008-06-16 00:00:00', size: 'extra large', visible: true },
450
+ { id: 13, company: 'General Motors Corporation', price: 30.27, date: '2006-12-07 00:00:00', size: 'medium', visible: true },
451
+ { id: 14, company: 'Hewlett-Packard Co.', price: 36.53, date: '2007-05-13 00:00:00', size: 'large', visible: true },
452
+ { id: 15, company: 'Honweywell Intl Inc', price: 38.77, date: '2006-11-07 00:00:00', size: 'medium', visible: false },
453
+ { id: 16, company: 'Intel Corporation', price: 19.88, date: '2007-01-09 00:00:00', size: 'small', visible: true },
454
+ { id: 17, company: 'International Business Machines', price: 81.41, date: '2005-01-21 00:00:00', size: 'extra large', visible: true },
455
+ { id: 18, company: 'Johnson & Johnson', price: 64.72, date: '2008-01-10 00:00:00', size: 'extra large', visible: true },
456
+ { id: 19, company: 'JP Morgan & Chase & Co', price: 45.73, date: '2008-02-20 00:00:00', size: 'large', visible: false },
457
+ { id: 20, company: "McDonald's Corporation", price: 36.76, date: '2007-06-12 00:00:00', size: 'large', visible: true },
458
+ { id: 21, company: 'Pfizer Inc', price: 27.96, date: '2007-12-30 00:00:00', size: 'small', visible: false },
459
+ { id: 22, company: 'The Coca-Cola Company', price: 45.07, date: '2007-01-30 00:00:00', size: 'medium', visible: false },
460
+ { id: 23, company: 'The Home Depot, Inc', price: 34.64, date: '2006-12-31 00:00:00', size: 'small', visible: true },
461
+ { id: 24, company: 'The Procter & Gamble Company', price: 61.91, date: '2007-04-08 00:00:00', size: 'extra large', visible: true },
462
+ { id: 25, company: 'United Technologies Corporation', price: 63.26, date: '2006-06-04 00:00:00', size: 'medium', visible: true },
463
+ { id: 26, company: 'Verizon Communications', price: 35.57, date: '2005-07-09 00:00:00', size: 'small', visible: false },
464
+ { id: 27, company: 'Wal-Mart Stores, Inc', price: 45.45, date: '2006-09-09 00:00:00', size: 'large', visible: true },
465
+ ];
466
+ const fmtDate = (v: string) => {
467
+ const d = new Date(v.replace(' ', 'T'));
468
+ if (isNaN(d.getTime())) return v;
469
+ const p = (n: number) => String(n).padStart(2, '0');
470
+ return `${p(d.getMonth() + 1)}/${p(d.getDate())}/${d.getFullYear()}`;
471
+ };
472
+ return (
473
+ <div data-testid="grid-filter-local" style={{ width: 700 }}>
474
+ <Grid
475
+ ref={gridRef}
476
+ title="Grid Filters Example"
477
+ width={700}
478
+ height={400}
479
+ stripeRows
480
+ autoExpandColumn="company"
481
+ columns={[
482
+ { header: 'Id', dataIndex: 'id', width: 45, filter: { type: 'numeric' } },
483
+ {
484
+ id: 'company',
485
+ header: 'Company',
486
+ dataIndex: 'company',
487
+ width: 200,
488
+ filter: { type: 'string' },
489
+ },
490
+ { header: 'Price', dataIndex: 'price', width: 70, align: 'right', filter: { type: 'numeric' } },
491
+ {
492
+ header: 'Size',
493
+ dataIndex: 'size',
494
+ width: 90,
495
+ filter: {
496
+ type: 'list',
497
+ options: ['small', 'medium', 'large', 'extra large'],
498
+ },
499
+ },
500
+ {
501
+ header: 'Date',
502
+ dataIndex: 'date',
503
+ width: 100,
504
+ renderer: (v: unknown) => fmtDate(String(v)),
505
+ filter: { type: 'date' },
506
+ },
507
+ { header: 'Visible', dataIndex: 'visible', width: 70, align: 'center', filter: { type: 'boolean' } },
508
+ ]}
509
+ data={data}
510
+ />
511
+ <Toolbar
512
+ style={{ marginTop: 4 }}
513
+ items={[
514
+ {
515
+ text: 'Clear Filter Data',
516
+ handler: () => gridRef.current?.clearFilters(),
517
+ },
518
+ {
519
+ text: 'All Filter Data',
520
+ handler: () =>
521
+ window.alert(JSON.stringify(gridRef.current?.getFilterData() ?? [])),
522
+ },
523
+ ]}
524
+ />
525
+ </div>
526
+ );
527
+ }
528
+
529
+ // 演示:TableGrid 保真(对齐 Ext.ux.grid.TableGrid / examples/grid/from-markup):
530
+ // 点击「Create grid」后,由一段 HTML <table> 标记生成 Grid(thead th → 列、tbody tr/td → 数据行),原版会移除源表格。
531
+ const TABLEGRID_MARKUP = `<table cellspacing="0" id="the-table">
532
+ <thead>
533
+ <tr style="background:#eeeeee;">
534
+ <th>Name</th>
535
+ <th style="width: 40px;">Age</th>
536
+ <th>Sex</th>
537
+ </tr>
538
+ </thead>
539
+ <tbody>
540
+ <tr><td>Barney Rubble</td><td>32</td><td>Male</td></tr>
541
+ <tr><td>Fred Flintstone</td><td>33</td><td>Male</td></tr>
542
+ <tr><td>Betty Rubble</td><td>32</td><td>Female</td></tr>
543
+ <tr><td>Pebbles</td><td>1</td><td>Female</td></tr>
544
+ <tr><td>Bamm Bamm</td><td>2</td><td>Male</td></tr>
545
+ </tbody>
546
+ </table>`;
547
+ function TableGridDemo() {
548
+ const [created, setCreated] = useState(false);
549
+ return (
550
+ <div data-testid="tablegrid" style={{ width: 320 }}>
551
+ {!created ? (
552
+ <>
553
+ <Button text="Create grid" onClick={() => setCreated(true)} />
554
+ <div style={{ marginTop: 12 }} dangerouslySetInnerHTML={{ __html: TABLEGRID_MARKUP }} />
555
+ </>
556
+ ) : (
557
+ <Grid fromMarkup={TABLEGRID_MARKUP} stripeRows />
558
+ )}
559
+ </div>
560
+ );
561
+ }
562
+
563
+ // 演示:与 reference/golden.html 的布局/内容一致,供 Playwright 像素对比。
564
+ function App() {
565
+ const [comboValue, setComboValue] = useState('sh');
566
+ const [filterValue, setFilterValue] = useState('bj');
567
+ const [sliderH, setSliderH] = useState(50);
568
+ const [sliderV, setSliderV] = useState(30);
569
+ const comboDynRef = useRef<ComboBoxHandle>(null);
570
+ const tfDynRef = useRef<ComboBoxHandle>(null);
571
+ const [tfDynValue, setTfDynValue] = useState('12:00 AM');
572
+ const dfDynRef = useRef<DateFieldHandle>(null);
573
+ const winDynRef = useRef<WindowHandle>(null);
574
+ const winMinRef = useRef<WindowHandle>(null);
575
+ const treeDynRef = useRef<TreePanelHandle>(null);
576
+ const propDynRef = useRef<PropertyGridHandle>(null);
577
+ const dvRef = useRef<DataViewHandle>(null);
578
+ const ptRef = useRef<PagingToolbarHandle>(null);
579
+ const nextDv = useRef(1);
580
+ const [comboDynValue, setComboDynValue] = useState('sh');
581
+ const [progressVal, setProgressVal] = useState(0.5);
582
+ const [progressTxt, setProgressTxt] = useState('加载中 50%');
583
+ const pbDynRef = useRef<ProgressBarHandle>(null);
584
+ const accDynRef = useRef<AccordionHandle>(null);
585
+ const [activeControlled, setActiveControlled] = useState<number | null>(0);
586
+ const [accEvent, setAccEvent] = useState('');
587
+ const cgDynRef = useRef<CheckboxGroupHandle>(null);
588
+ const bgDynRef = useRef<ButtonGroupHandle>(null);
589
+ const cycDynRef = useRef<CycleButtonHandle>(null);
590
+ const palDynRef = useRef<ColorPaletteHandle>(null);
591
+ const [showMessageBox, setShowMessageBox] = useState(true);
592
+ const [hboxPack, setHboxPack] = useState<'start' | 'center'>('start');
593
+ return (
594
+ <div style={{ padding: 24, display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'flex-start' }}>
595
+ <div data-testid="panel-frame">
596
+ <Panel frame title="Frame Panel" width={300} bodyPadding={10}>
597
+ <p>Frame panel body content.</p>
598
+ </Panel>
599
+ </div>
600
+ <div data-testid="panel-plain">
601
+ <Panel title="Plain Panel" width={300} bodyPadding={10}>
602
+ <p>Plain panel body content.</p>
603
+ </Panel>
604
+ </div>
605
+ <PanelFeaturesDemo />
606
+ <div data-testid="buttons" style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
607
+ <Button text="普通按钮" />
608
+ <Button text="切换" enableToggle pressed />
609
+ <Button text="禁用" disabled />
610
+ <Button
611
+ text="带菜单"
612
+ arrowAlign="bottom"
613
+ menu={[
614
+ { text: '新建' },
615
+ { text: '打开' },
616
+ { separator: true },
617
+ { text: '保存' },
618
+ { text: '删除', disabled: true },
619
+ ]}
620
+ />
621
+ </div>
622
+ <div data-testid="tabpanel" style={{ width: 420 }}>
623
+ <TabPanel items={tabs} width={420} height={180} />
624
+ </div>
625
+ {/* TabPanel 动态增删演示:种子 2 个普通 tab,按钮「添加页」追加可自删的 tab;无 golden 对应,不入保真循环。 */}
626
+ <div data-testid="tabpanel-dynamic">
627
+ <DynamicTabDemo />
628
+ </div>
629
+ {/* TabPanel 溢出滚动(enableTabScroll):窄容器 300 + 多 tabs 触发左右箭头,与 golden #tabscroll 同结构同尺寸。 */}
630
+ <div data-testid="tabscroll" style={{ width: 300 }}>
631
+ <TabPanel
632
+ enableTabScroll
633
+ width={300}
634
+ height={160}
635
+ activeTab={0}
636
+ items={[
637
+ { title: '首页', content: <div style={{ padding: 10 }}>首页内容</div> },
638
+ { title: '产品中心', content: <div style={{ padding: 10 }}>产品内容</div> },
639
+ { title: '解决方案', content: <div style={{ padding: 10 }}>方案内容</div> },
640
+ { title: '客户案例', content: <div style={{ padding: 10 }}>案例内容</div> },
641
+ { title: '服务支持', content: <div style={{ padding: 10 }}>服务内容</div> },
642
+ { title: '关于我们', content: <div style={{ padding: 10 }}>关于内容</div> },
643
+ { title: '联系我们', content: <div style={{ padding: 10 }}>联系内容</div> },
644
+ ]}
645
+ />
646
+ </div>
647
+ {/* Menu 动态增删演示(forwardRef + add/remove,对齐 Ext.menu.Menu.add/remove)。
648
+ 种子 复制/粘贴/分隔/删除;「添加菜单项」追加一个可自删项;无 golden 对应。 */}
649
+ <div data-testid="menu-dynamic" style={{ position: 'relative' }}>
650
+ <DynamicMenuDemo />
651
+ </div>
652
+ {/* Menu 级联子菜单动态增删演示(getSubmenu 对「文件」子菜单动态增删)。无 golden 对应。 */}
653
+ <div data-testid="menu-cascade" style={{ position: 'relative' }}>
654
+ <CascadeMenuDemo />
655
+ </div>
656
+ {/* Toolbar 展示(按钮 + 文本 + 分隔符 + 右对齐填充) */}
657
+ <div data-testid="toolbar" style={{ width: 520 }}>
658
+ <Toolbar
659
+ width={520}
660
+ items={[
661
+ { xtype: 'tbtext', text: '用户管理' },
662
+ { xtype: 'button', text: '新增' },
663
+ { xtype: 'tbseparator' },
664
+ { xtype: 'button', text: '编辑' },
665
+ '->',
666
+ { xtype: 'button', text: '保存' },
667
+ { xtype: 'button', text: '删除', disabled: true },
668
+ ]}
669
+ />
670
+ </div>
671
+ {/* Toolbar 动态增删演示(forwardRef + add/remove,对齐 Ext.Toolbar.add/remove)。
672
+ 种子 2 个按钮;「添加按钮」追加一个可自删的动态按钮;无 golden 对应,不入保真循环。 */}
673
+ <div data-testid="toolbar-dynamic" style={{ width: 600 }}>
674
+ <DynamicToolbarDemo />
675
+ </div>
676
+ {/* Toolbar 溢出(x-toolbar-more):窄容器 + enableOverflow,尾部按钮收进 more 菜单。
677
+ 与 golden #toolbar-overflow 同款:前 4 项可见,Format/Tools/Help 收进 more。 */}
678
+ <div data-testid="toolbar-overflow" style={{ width: 280 }}>
679
+ <Toolbar
680
+ width={280}
681
+ enableOverflow
682
+ items={[
683
+ { xtype: 'button', text: 'File' },
684
+ { xtype: 'button', text: 'Edit' },
685
+ { xtype: 'button', text: 'View' },
686
+ { xtype: 'button', text: 'Insert' },
687
+ ]}
688
+ overflowItems={[
689
+ { xtype: 'button', text: 'Format' },
690
+ { xtype: 'button', text: 'Tools' },
691
+ { xtype: 'button', text: 'Help' },
692
+ ]}
693
+ />
694
+ </div>
695
+ {/* PropertyGrid 演示(Ext.grid.PropertyGrid):两列 Name/Value,根加 x-props-grid 类,
696
+ 与 golden #propertygrid 同结构、同宽(300)、同 source。 */}
697
+ <div data-testid="propertygrid" style={{ width: 300 }}>
698
+ <PropertyGrid
699
+ width={300}
700
+ source={{
701
+ name: '演示窗口',
702
+ grouping: false,
703
+ autoFit: true,
704
+ borderWidth: 5,
705
+ created: new Date(2024, 5, 15), // 6/15/2024
706
+ ratio: 0.75,
707
+ enabled: true,
708
+ }}
709
+ propertyNames={{
710
+ name: '名称',
711
+ grouping: '分组',
712
+ autoFit: '自动适配',
713
+ borderWidth: '边框宽度',
714
+ created: '创建时间',
715
+ ratio: '比例',
716
+ enabled: '启用',
717
+ }}
718
+ />
719
+ </div>
720
+ {/* PropertyGrid 动态(命令式句柄:setSource / setProperty / removeProperty / addProperty)。 */}
721
+ <div data-testid="propertygrid-dynamic" style={{ width: 300, marginTop: 12 }}>
722
+ <PropertyGrid
723
+ ref={propDynRef}
724
+ width={300}
725
+ source={{ Name: '演示项目', Price: 100, Enabled: true }}
726
+ />
727
+ <div style={{ marginTop: 8 }}>
728
+ <Button text="添加属性" onClick={() => propDynRef.current?.addProperty('Version', '1.0')} />
729
+ <Button text="删除 Price" onClick={() => propDynRef.current?.removeProperty('Price')} style={{ marginLeft: 4 }} />
730
+ <Button text="修改 Name" onClick={() => propDynRef.current?.setProperty('Name', '改名为X')} style={{ marginLeft: 4 }} />
731
+ <Button text="整体替换" onClick={() => propDynRef.current?.setSource({ A: '甲', B: '乙' })} style={{ marginLeft: 4 }} />
732
+ </div>
733
+ </div>
734
+ {/* BorderLayout 演示(Ext.layout.BorderLayout):五区,north/south 高 60,east/west 宽 120。
735
+ 容器 600x400,与 golden #borderlayout 同结构、同尺寸。 */}
736
+ <BorderLayout
737
+ testId="borderlayout"
738
+ width={600}
739
+ height={400}
740
+ regions={[
741
+ { region: 'north', height: 60, title: 'North', html: 'North' },
742
+ { region: 'south', height: 60, title: 'South', html: 'South' },
743
+ { region: 'west', width: 120, title: 'West', html: 'West' },
744
+ { region: 'east', width: 120, title: 'East', html: 'East' },
745
+ { region: 'center', title: 'Center', html: 'Center' },
746
+ ]}
747
+ />
748
+ <BorderLayout
749
+ testId="borderlayout-collapsible"
750
+ width={600}
751
+ height={400}
752
+ regions={[
753
+ { region: 'north', height: 60, title: 'North', html: 'North', split: true, minSize: 40, maxSize: 200 },
754
+ { region: 'south', height: 60, title: 'South', html: 'South', collapsible: true, collapsed: true },
755
+ { region: 'west', width: 120, title: 'West', html: 'West', collapsible: true },
756
+ { region: 'east', width: 120, title: 'East', html: 'East' },
757
+ { region: 'center', title: 'Center', html: 'Center' },
758
+ ]}
759
+ />
760
+ <BorderLayout
761
+ testId="borderlayout-split"
762
+ width={600}
763
+ height={400}
764
+ regions={[
765
+ { region: 'north', height: 60, title: 'North', html: 'North' },
766
+ { region: 'south', height: 60, title: 'South', html: 'South', split: true, minSize: 40, maxSize: 200 },
767
+ { region: 'west', width: 120, title: 'West', html: 'West', split: true, minSize: 60, maxSize: 300 },
768
+ { region: 'east', width: 120, title: 'East', html: 'East' },
769
+ { region: 'center', title: 'Center', html: 'Center' },
770
+ ]}
771
+ />
772
+ <BorderLayout
773
+ testId="borderlayout-floatable"
774
+ width={600}
775
+ height={400}
776
+ regions={[
777
+ { region: 'north', height: 60, title: 'North', html: 'North' },
778
+ { region: 'south', height: 60, title: 'South', html: 'South' },
779
+ { region: 'west', width: 120, title: 'West', html: 'West', collapsible: true, floatable: true },
780
+ { region: 'east', width: 120, title: 'East', html: 'East', collapsible: true, floatable: true },
781
+ { region: 'center', title: 'Center', html: 'Center' },
782
+ ]}
783
+ />
784
+ {/* Viewport 演示(Ext.Viewport):全屏 border 容器,复刻真实 Ext.Viewport 的 DOM
785
+ (div.x-viewport > .x-border-layout-ct > .x-border-panel 区域,区域 borderless)。
786
+ 容器 600x400,与 golden #viewport 同结构、同尺寸,供像素对比。 */}
787
+ <Viewport
788
+ testId="viewport"
789
+ width={600}
790
+ height={400}
791
+ regions={[
792
+ { region: 'north', height: 60, title: 'North', html: 'North' },
793
+ { region: 'south', height: 60, title: 'South', html: 'South' },
794
+ { region: 'west', width: 120, title: 'West', html: 'West' },
795
+ { region: 'east', width: 120, title: 'East', html: 'East' },
796
+ { region: 'center', title: 'Center', html: 'Center' },
797
+ ]}
798
+ />
799
+ {/* Viewport 动态演示:通过 ref 调用句柄运行时增删 region / 改尺寸。 */}
800
+ <ViewportDynamicDemo />
801
+ {/* CardLayout 演示(Ext.layout.CardLayout):一组卡片,同一时刻仅激活卡片可见,其余 display:none。
802
+ 容器 300x200,与 golden #cardlayout 同结构同尺寸,供像素对比。 */}
803
+ <div data-testid="cardlayout" style={{ width: 300, height: 200 }}>
804
+ <CardLayout
805
+ width={300}
806
+ height={200}
807
+ items={[
808
+ { title: 'Card 1', html: '<p>Card 1 content</p>' },
809
+ { title: 'Card 2', html: '<p>Card 2 content</p>' },
810
+ { title: 'Card 3', html: '<p>Card 3 content</p>' },
811
+ ]}
812
+ />
813
+ </div>
814
+ {/* AnchorLayout 演示(Ext.layout.AnchorLayout):子面板按 anchor 字符串(百分比/偏移)定位。
815
+ 容器 400x300,与 golden #anchorlayout 同结构同尺寸,供像素对比。 */}
816
+ <AnchorLayout
817
+ testId="anchorlayout"
818
+ width={400}
819
+ height={300}
820
+ items={[
821
+ { title: 'A', html: 'A', anchor: '100% 40%' },
822
+ { title: 'B', html: 'B', anchor: '100% 40%' },
823
+ { title: 'C', html: 'C', anchor: '50% 20%' },
824
+ ]}
825
+ />
826
+ {/* FitLayout 演示(Ext.layout.FitLayout):唯一子面板铺满容器,容器 400x300,
827
+ 与 golden #fitlayout 同结构同尺寸,供像素对比。 */}
828
+ <FitLayout
829
+ testId="fitlayout"
830
+ width={400}
831
+ height={300}
832
+ item={{ title: 'Fit Panel', html: '<p>Fit content</p>' }}
833
+ />
834
+ {/* ColumnLayout 演示(Ext.layout.ColumnLayout):固定列 + 比例列混排,容器宽 600,与 golden #columnlayout 同值。 */}
835
+ <ColumnLayout
836
+ testId="columnlayout"
837
+ width={600}
838
+ items={[
839
+ { title: '固定 150', html: 'A', width: 150 },
840
+ { title: '比例 0.3', html: 'B', columnWidth: 0.3 },
841
+ { title: '比例 0.4', html: 'C', columnWidth: 0.4 },
842
+ { title: '固定 100', html: 'D', width: 100 },
843
+ ]}
844
+ />
845
+ {/* Grid 展示(表头 + 数据行 + 斑马纹 + 选中行)。autoHeight,与 golden 对齐。 */}
846
+ <div data-testid="grid" style={{ width: 500 }}>
847
+ <Grid
848
+ title="用户列表"
849
+ width={500}
850
+ columns={[
851
+ { header: '姓名', dataIndex: 'name', width: 100 },
852
+ { header: '邮箱', dataIndex: 'email', width: 180 },
853
+ { header: '电话', dataIndex: 'phone', width: 140 },
854
+ { header: '年龄', dataIndex: 'age', width: 80, align: 'right' },
855
+ ]}
856
+ data={[
857
+ { name: '张三', email: 'zhangsan@example.com', phone: '13800138000', age: 28 },
858
+ { name: '李四', email: 'lisi@example.com', phone: '13900139000', age: 32 },
859
+ { name: '王五', email: 'wangwu@example.com', phone: '13700137000', age: 45 },
860
+ { name: '赵六', email: 'zhaoliu@example.com', phone: '13600136000', age: 23 },
861
+ { name: '钱七', email: 'qianqi@example.com', phone: '13500135000', age: 37 },
862
+ { name: '孙八', email: 'sunba@example.com', phone: '13400134000', age: 29 },
863
+ ]}
864
+ selectedRow={2}
865
+ />
866
+ </div>
867
+ {/* Grid 列菜单隐藏列演示(enableColumnHide 默认:邮箱列 hidden:true 初始隐藏,表头 hd-btn 点击打开列显示菜单)。与 golden #grid-colmenu 同结构供像素对比。 */}
868
+ <div data-testid="grid-colmenu" style={{ width: 500 }}>
869
+ <Grid
870
+ title="列可隐藏的列表"
871
+ width={500}
872
+ columns={[
873
+ { header: '姓名', dataIndex: 'name', width: 100 },
874
+ { header: '邮箱', dataIndex: 'email', width: 180, hidden: true },
875
+ { header: '电话', dataIndex: 'phone', width: 140 },
876
+ { header: '年龄', dataIndex: 'age', width: 80, align: 'right' },
877
+ ]}
878
+ data={[
879
+ { name: '张三', email: 'zhangsan@example.com', phone: '13800138000', age: 28 },
880
+ { name: '李四', email: 'lisi@example.com', phone: '13900139000', age: 32 },
881
+ { name: '王五', email: 'wangwu@example.com', phone: '13700137000', age: 45 },
882
+ ]}
883
+ />
884
+ </div>
885
+ {/* Grid 行选择展示(checkbox 模式:勾选列 + 行点击单选 + 表头全选)。默认无选中,与 golden #grid-select 同结构供像素对比。 */}
886
+ <div data-testid="grid-select" style={{ width: 390 }}>
887
+ <Grid
888
+ title="可勾选用户列表"
889
+ width={390}
890
+ selectionMode="checkbox"
891
+ columns={[
892
+ { header: '姓名', dataIndex: 'name', width: 100 },
893
+ { header: '邮箱', dataIndex: 'email', width: 150 },
894
+ { header: '电话', dataIndex: 'phone', width: 120 },
895
+ ]}
896
+ data={[
897
+ { name: '张三', email: 'zhangsan@example.com', phone: '13800138000' },
898
+ { name: '李四', email: 'lisi@example.com', phone: '13900139000' },
899
+ { name: '王五', email: 'wangwu@example.com', phone: '13700137000' },
900
+ { name: '赵六', email: 'zhaoliu@example.com', phone: '13600136000' },
901
+ ]}
902
+ />
903
+ </div>
904
+ {/* Grid 列拖拽重排演示(enableColumnReorder:拖拽表头整列移动,隐藏列保留在尾部)。 */}
905
+ <div data-testid="grid-reorder" style={{ width: 480 }}>
906
+ <Grid
907
+ title="可重排列"
908
+ width={480}
909
+ enableColumnReorder
910
+ columns={[
911
+ { header: '姓名', dataIndex: 'name', width: 120 },
912
+ { header: '邮箱', dataIndex: 'email', width: 160 },
913
+ { header: '电话', dataIndex: 'phone', width: 140 },
914
+ { header: '部门', dataIndex: 'dept', width: 100 },
915
+ ]}
916
+ data={[
917
+ { name: '张三', email: 'zhangsan@example.com', phone: '13800138000', dept: '研发' },
918
+ { name: '李四', email: 'lisi@example.com', phone: '13900139000', dept: '市场' },
919
+ { name: '王五', email: 'wangwu@example.com', phone: '13700137000', dept: '研发' },
920
+ ]}
921
+ />
922
+ </div>
923
+ {/* Grid 行拖拽重排演示(enableRowReorder:拖动数据行改变底层顺序,插入线指示落点)。 */}
924
+ <div data-testid="grid-rowreorder" style={{ width: 480 }}>
925
+ <Grid
926
+ title="可重排行"
927
+ width={480}
928
+ enableRowReorder
929
+ columns={[
930
+ { header: '姓名', dataIndex: 'name', width: 120 },
931
+ { header: '邮箱', dataIndex: 'email', width: 200 },
932
+ { header: '部门', dataIndex: 'dept', width: 160 },
933
+ ]}
934
+ data={[
935
+ { name: '张三', email: 'zhangsan@example.com', dept: '研发' },
936
+ { name: '李四', email: 'lisi@example.com', dept: '市场' },
937
+ { name: '王五', email: 'wangwu@example.com', dept: '研发' },
938
+ { name: '赵六', email: 'zhaoliu@example.com', dept: '财务' },
939
+ { name: '钱七', email: 'qianqi@example.com', dept: '市场' },
940
+ ]}
941
+ />
942
+ </div>
943
+ {/* Grid 列动态增删(addColumn/removeColumn 等价 Ext ColumnModel 运行时增删;沿用双轨,不破坏任何保真基线)。无 golden 对应,不入保真循环。 */}
944
+ <div data-testid="grid-coldynamic" style={{ width: 480 }}>
945
+ <GridColDynamicDemo />
946
+ </div>
947
+ {/* Grid 分组展示(GroupingView:按部门分组,分组头栏 + 组内行。列宽和=面板宽 480,与 golden #grouping 同结构供像素对比。 */}
948
+ <div data-testid="grouping" style={{ width: 480 }}>
949
+ <Grid
950
+ title="分组示例"
951
+ width={480}
952
+ groupField="dept"
953
+ columns={[
954
+ { header: '姓名', dataIndex: 'name', width: 120 },
955
+ { header: '部门', dataIndex: 'dept', width: 200 },
956
+ { header: '年龄', dataIndex: 'age', width: 160, align: 'right' },
957
+ ]}
958
+ data={[
959
+ { name: '张三', dept: '研发', age: 28 },
960
+ { name: '李四', dept: '研发', age: 32 },
961
+ { name: '王五', dept: '市场', age: 45 },
962
+ { name: '赵六', dept: '市场', age: 23 },
963
+ { name: '钱七', dept: '市场', age: 37 },
964
+ { name: '孙八', dept: '财务', age: 29 },
965
+ ]}
966
+ />
967
+ </div>
968
+ {/* Grid 分组汇总展示(GroupSummary:在分组基础上给列声明 summaryType 渲染每组底部汇总行;
969
+ 样式复用 ExtJS 官方 examples/ux/css/GroupSummary.css,零新增手写 CSS) */}
970
+ <div data-testid="groupsummary" style={{ width: 480 }}>
971
+ <Grid
972
+ title="分组汇总示例"
973
+ width={480}
974
+ groupField="dept"
975
+ columns={[
976
+ { header: '姓名', dataIndex: 'name', width: 120 },
977
+ { header: '部门', dataIndex: 'dept', width: 200 },
978
+ { header: '薪资', dataIndex: 'salary', width: 90, align: 'right', summaryType: 'sum', summaryRenderer: (v) => '合计 ' + v },
979
+ { header: '年龄', dataIndex: 'age', width: 70, align: 'right', summaryType: 'average' },
980
+ ]}
981
+ data={[
982
+ { name: '张三', dept: '研发', salary: 12000, age: 28 },
983
+ { name: '李四', dept: '研发', salary: 15000, age: 32 },
984
+ { name: '王五', dept: '市场', salary: 9000, age: 45 },
985
+ { name: '赵六', dept: '市场', salary: 11000, age: 23 },
986
+ { name: '钱七', dept: '市场', salary: 8000, age: 37 },
987
+ { name: '孙八', dept: '财务', salary: 13000, age: 29 },
988
+ ]}
989
+ />
990
+ </div>
991
+ {/* RowEditor 行编辑器展示(双击行编辑,保存写回数据;样式复用 ExtJS 官方 RowEditor.css) */}
992
+ <RowEditorDemo />
993
+ {/* ColumnHeaderGroup 列分组表头展示(表头顶部跨列分组;样式复用 ExtJS 官方 ColumnHeaderGroup.css) */}
994
+ <ColumnHeaderGroupDemo />
995
+ {/* LockingGridView 锁定列视图展示(前两列锁定,右侧横向滚动,纵向同步;样式复用 ExtJS 官方 LockingGridView.css) */}
996
+ <LockingViewDemo />
997
+ {/* GridFilters 列过滤展示(表头菜单内 Filters 子菜单:字符串/数字区间/日期/列表/布尔;本地过滤) */}
998
+ <GridFiltersDemo />
999
+ {/* GridFilters 经典官方示例复现(examples/grid-filtering/grid-filter-local:六列+工具栏 Clear/Show) */}
1000
+ <GridFilterLocalDemo />
1001
+ {/* TableGrid 保真(对齐 Ext.ux.grid.TableGrid / examples/grid/from-markup:由 HTML 表格标记生成 Grid) */}
1002
+ <TableGridDemo />
1003
+ {/* EditorGrid 展示(可编辑 Grid:姓名/年龄单击编辑,与 golden 同款) */}
1004
+ <div data-testid="editorgrid" style={{ width: 500 }}>
1005
+ <EditorGrid
1006
+ title="可编辑用户列表"
1007
+ width={500}
1008
+ columns={[
1009
+ { header: '姓名', dataIndex: 'name', width: 100, editor: true },
1010
+ { header: '邮箱', dataIndex: 'email', width: 180 },
1011
+ { header: '电话', dataIndex: 'phone', width: 140 },
1012
+ { header: '年龄', dataIndex: 'age', width: 80, align: 'right', editor: { type: 'number' } },
1013
+ ]}
1014
+ data={[
1015
+ { name: '张三', email: 'zhangsan@example.com', phone: '13800138000', age: 28 },
1016
+ { name: '李四', email: 'lisi@example.com', phone: '13900139000', age: 32 },
1017
+ { name: '王五', email: 'wangwu@example.com', phone: '13700137000', age: 45 },
1018
+ ]}
1019
+ />
1020
+ </div>
1021
+
1022
+ {/* EditorGrid 动态演示(Ext.EditorGrid):通过 ref 调用句柄运行时增删改数据源 / 按坐标启动编辑。 */}
1023
+ <EditorGridDynamicDemo />
1024
+ {/* Window 展示(对话框:标题 + 关闭按钮 + 内容 + 底部按钮)。position:absolute 落入文档流供截图。 */}
1025
+ <div data-testid="window" style={{ position: 'relative', width: 420, height: 260 }}>
1026
+ <Window
1027
+ title="用户详情"
1028
+ width={400}
1029
+ height={220}
1030
+ buttons={[
1031
+ { text: '确定' },
1032
+ { text: '取消' },
1033
+ ]}
1034
+ >
1035
+ <div style={{ padding: 10 }}>这是对话框的内容区域。</div>
1036
+ </Window>
1037
+ </div>
1038
+ {/* Window 周边:maximizable + resizable(头部 maximize 工具 + 透明 SE/N 等缩放手柄) */}
1039
+ <div data-testid="window-max" style={{ position: 'relative', width: 420, height: 260 }}>
1040
+ <Window
1041
+ title="窗口设置"
1042
+ width={400}
1043
+ height={220}
1044
+ resizable
1045
+ maximizable
1046
+ buttons={[
1047
+ { text: '确定' },
1048
+ { text: '取消' },
1049
+ ]}
1050
+ >
1051
+ <div style={{ padding: 10 }}>可缩放、可最大化的窗口。</div>
1052
+ </Window>
1053
+ </div>
1054
+ {/* Window 周边:modal(.ext-el-mask 遮罩覆盖容器,容器 overflow:hidden 裁剪) */}
1055
+ <div data-testid="window-modal" style={{ position: 'relative', width: 420, height: 320, overflow: 'hidden' }}>
1056
+ <Window
1057
+ title="确认操作"
1058
+ width={360}
1059
+ height={200}
1060
+ left={30}
1061
+ top={30}
1062
+ modal
1063
+ buttons={[
1064
+ { text: '确认' },
1065
+ { text: '取消' },
1066
+ ]}
1067
+ >
1068
+ <div style={{ padding: 10 }}>此操作不可撤销。</div>
1069
+ </Window>
1070
+ </div>
1071
+ {/* Window 最小化(minimizable 工具 + 运行时 minimize/expand 句柄)。
1072
+ 对齐 Ext 默认 minimize → fireEvent onMinimize(不整窗消失),这里折叠收起为仅标题栏,
1073
+ 标题栏 restore 展开工具或句柄 expand 可恢复。 */}
1074
+ <div data-testid="window-min" style={{ width: 420 }}>
1075
+ <div style={{ position: 'relative', height: 240 }}>
1076
+ <Window
1077
+ ref={winMinRef}
1078
+ title="最小化演示"
1079
+ width={400}
1080
+ height={220}
1081
+ minimizable
1082
+ buttons={[{ text: '确定' }, { text: '取消' }]}
1083
+ >
1084
+ <div style={{ padding: 10 }}>可最小化的窗口。点击标题栏最小化工具,窗口折叠为仅标题栏;点标题栏上的展开工具恢复。</div>
1085
+ </Window>
1086
+ </div>
1087
+ <div style={{ marginTop: 8 }}>
1088
+ <Button text="展开(句柄)" onClick={() => winMinRef.current?.expand()} />
1089
+ </div>
1090
+ </div>
1091
+ {/* Window 动态演示(对齐 Ext.Window 的运行时 show/hide/setTitle/maximize/restore 与内容替换):
1092
+ 通过 WindowHandle 在运行时控制可见性、标题、最大化、内容。初始不可见。 */}
1093
+ <div data-testid="window-dynamic" style={{ position: 'relative', width: 420, marginTop: 12 }}>
1094
+ {/* 固定高度 relative 包裹盒:Window 为 absolute,需占位盒让下方控制按钮正常流入其下方(避免遮挡) */}
1095
+ <div style={{ position: 'relative', height: 220 }}>
1096
+ <Window
1097
+ ref={winDynRef}
1098
+ title="初始标题"
1099
+ width={400}
1100
+ height={220}
1101
+ maximizable
1102
+ visible={false}
1103
+ buttons={[{ text: '确定' }, { text: '取消' }]}
1104
+ >
1105
+ <div style={{ padding: 10 }}>窗口初始内容</div>
1106
+ </Window>
1107
+ </div>
1108
+ <div style={{ marginTop: 8 }}>
1109
+ <Button text="打开窗口" onClick={() => winDynRef.current?.open()} />
1110
+ <Button text="关闭窗口" onClick={() => winDynRef.current?.close()} style={{ marginLeft: 4 }} />
1111
+ <Button text="改标题" onClick={() => winDynRef.current?.setTitle('动态标题')} style={{ marginLeft: 4 }} />
1112
+ <Button text="最大化" onClick={() => winDynRef.current?.maximize()} style={{ marginLeft: 4 }} />
1113
+ <Button text="还原" onClick={() => winDynRef.current?.restore()} style={{ marginLeft: 4 }} />
1114
+ <Button
1115
+ text="换内容"
1116
+ onClick={() => winDynRef.current?.setContent(<div style={{ padding: 10 }}>已更新内容</div>)}
1117
+ style={{ marginLeft: 4 }}
1118
+ />
1119
+ </div>
1120
+ </div>
1121
+ {/* PagingToolbar 展示(按钮 + 页码框 + displayInfo,与 golden 同款 50 条/页 10) */}
1122
+ <div data-testid="paging" style={{ width: 520 }}>
1123
+ <PagingToolbar total={50} pageSize={10} displayInfo activePage={1} />
1124
+ </div>
1125
+ {/* PagingToolbar 动态数据源(运行时 setTotal 重载 + 程序化翻页)。 */}
1126
+ <div data-testid="paging-dynamic" style={{ width: 520, marginTop: 12 }}>
1127
+ <PagingToolbar
1128
+ ref={ptRef}
1129
+ total={50}
1130
+ pageSize={10}
1131
+ displayInfo
1132
+ activePage={1}
1133
+ />
1134
+ <div style={{ marginTop: 8 }}>
1135
+ <Button text="重载 23 条" onClick={() => ptRef.current?.setTotal(23)} />
1136
+ <Button text="重载 0 条" onClick={() => ptRef.current?.setTotal(0)} style={{ marginLeft: 4 }} />
1137
+ <Button text="跳页 5" onClick={() => ptRef.current?.setPage(5)} style={{ marginLeft: 4 }} />
1138
+ <Button text="下一页" onClick={() => ptRef.current?.next()} style={{ marginLeft: 4 }} />
1139
+ <Button text="首页" onClick={() => ptRef.current?.first()} style={{ marginLeft: 4 }} />
1140
+ </div>
1141
+ </div>
1142
+ {/* Menu 独立展示位(提前开发,单独验证保真)。Menu 是 position:absolute 浮层,
1143
+ 用 position:relative 容器包裹使其落入文档流供截图。 */}
1144
+ <div data-testid="menu-standalone" style={{ position: 'relative', width: 160, height: 150 }}>
1145
+ <Menu
1146
+ items={[
1147
+ { text: '新建' },
1148
+ { text: '打开' },
1149
+ { separator: true },
1150
+ { text: '保存' },
1151
+ { text: '删除', disabled: true },
1152
+ ]}
1153
+ />
1154
+ </div>
1155
+ {/* Menu 子菜单展示位(行为测试专用,不参与保真循环) */}
1156
+ <div data-testid="menu-submenu" style={{ position: 'relative', width: 160, height: 150 }}>
1157
+ <Menu
1158
+ items={[
1159
+ { text: '文件' },
1160
+ { text: '编辑' },
1161
+ { separator: true },
1162
+ { text: '更多', menu: [{ text: '导出' }, { text: '导入' }] },
1163
+ { text: '帮助', disabled: true },
1164
+ ]}
1165
+ onItemClick={(it) =>
1166
+ document
1167
+ .querySelector('[data-testid="menu-submenu"]')
1168
+ ?.setAttribute('data-clicked', it.text ?? '')
1169
+ }
1170
+ />
1171
+ </div>
1172
+ {/* Form 展示(FormLayout + 多种字段)。labelWidth 与 Ext 默认一致。 */}
1173
+ <div data-testid="form" style={{ width: 360 }}>
1174
+ <FormPanel title="用户信息" width={360}>
1175
+ <FormField id="f-name" fieldLabel="姓名">
1176
+ <TextField width={200} emptyText="请输入姓名" />
1177
+ </FormField>
1178
+ <FormField id="f-email" fieldLabel="邮箱">
1179
+ <TextField width={200} value="" />
1180
+ </FormField>
1181
+ <FormField id="f-age" fieldLabel="年龄">
1182
+ <NumberField width={200} />
1183
+ </FormField>
1184
+ <FormField id="f-city" fieldLabel="城市">
1185
+ <ComboBox width={200} data={[
1186
+ { value: 'bj', text: '北京' },
1187
+ { value: 'sh', text: '上海' },
1188
+ { value: 'gz', text: '广州' },
1189
+ ]} />
1190
+ </FormField>
1191
+ <FormField id="f-memo" fieldLabel="备注">
1192
+ <TextArea width={320} rows={3} emptyText="多行备注" />
1193
+ </FormField>
1194
+ <FormField id="f-sub" fieldLabel="订阅" hideLabel>
1195
+ <Checkbox boxLabel="接收邮件通知" defaultChecked />
1196
+ </FormField>
1197
+ <RadioGroup fieldLabel="性别" name="sex">
1198
+ <Radio name="sex" boxLabel="男" inputValue="m" defaultChecked />
1199
+ <Radio name="sex" boxLabel="女" inputValue="f" />
1200
+ </RadioGroup>
1201
+ <FormField id="f-birth" fieldLabel="出生日期">
1202
+ <DateField width={200} />
1203
+ </FormField>
1204
+ <FormField id="f-time" fieldLabel="时间">
1205
+ <TimeField width={200} />
1206
+ </FormField>
1207
+ {/* Hidden 无可见 UI,仅用于表单提交携带值 */}
1208
+ <Hidden name="token" value="abc123" />
1209
+ </FormPanel>
1210
+ </div>
1211
+ {/* DatePicker 独立常开展示位(供像素对比日历保真)。 */}
1212
+ <div data-testid="datepicker" style={{ position: 'relative' }}>
1213
+ <DatePicker />
1214
+ </div>
1215
+ {/* DatePicker 范围选择演示(rangeSelect:点击两日形成闭区间高亮,端点 x-date-selected + 区间内浅蓝背景)。
1216
+ 无 golden 对应,不入保真循环。 */}
1217
+ <div data-testid="datepicker-range" style={{ position: 'relative', marginTop: 12 }}>
1218
+ <DateRangeDemo />
1219
+ </div>
1220
+ {/* Form 校验演示(姓名必填 + 邮箱格式校验 + 外部校验按钮)。golden 静态 markInvalid 两字段对应无效态像素对比。 */}
1221
+ <ValidateDemo />
1222
+ {/* Form 校验联动演示(密码/确认密码跨字段校验 + 提交按钮随有效态联动)。无对应 golden,不入保真循环。 */}
1223
+ <FormLinkageDemo />
1224
+ {/* Tree 展示(TreePanel + 嵌套节点)。rootVisible:false 直接渲染子节点,与 golden 对齐(无外层 Panel)。 */}
1225
+ <div data-testid="tree" style={{ width: 260 }}>
1226
+ <TreePanel
1227
+ rootVisible={false}
1228
+ nodes={treeData}
1229
+ />
1230
+ </div>
1231
+ {/* Tree 异步加载演示(loader 在展开无子节点节点时拉取子节点,等价于 AsyncTreeNode / TreeLoader)。 */}
1232
+ <div data-testid="tree-async" style={{ width: 260 }}>
1233
+ <TreePanel
1234
+ rootVisible={false}
1235
+ loader={(node) =>
1236
+ new Promise<TreeNodeData[]>((resolve) => {
1237
+ setTimeout(() => {
1238
+ if (node.id === 'dept') {
1239
+ resolve([
1240
+ { id: 'dept-rd', text: '研发部' },
1241
+ { id: 'dept-mk', text: '市场部' },
1242
+ ]);
1243
+ } else if (node.id === 'proj') {
1244
+ resolve([{ id: 'proj-1', text: '上线计划' }]);
1245
+ } else {
1246
+ resolve([{ id: `${node.id}-leaf`, text: `${node.text} 详情` }]);
1247
+ }
1248
+ }, 150);
1249
+ })
1250
+ }
1251
+ nodes={[
1252
+ { id: 'dept', text: '部门', expandable: true },
1253
+ { id: 'proj', text: '项目', expandable: true, children: [{ id: 'proj-seed', text: '种子任务' }] },
1254
+ ]}
1255
+ />
1256
+ </div>
1257
+ {/* Tree 节点拖拽重排演示(enableNodeReorder:同级 above/below 移动 + 拖入文件夹 append)。 */}
1258
+ <div data-testid="tree-dd" style={{ width: 260 }}>
1259
+ <TreePanel
1260
+ rootVisible={false}
1261
+ enableNodeReorder
1262
+ nodes={[
1263
+ { id: 'a', text: 'Alpha' },
1264
+ { id: 'b', text: 'Beta' },
1265
+ { id: 'c', text: 'Gamma' },
1266
+ {
1267
+ id: 'f', text: 'Folder', expanded: true, children: [
1268
+ { id: 'f1', text: 'Item 1' },
1269
+ { id: 'f2', text: 'Item 2' },
1270
+ ],
1271
+ },
1272
+ ]}
1273
+ />
1274
+ </div>
1275
+ {/* Tree 复选框级联演示(父节点 checked 控制子节点;子节点全选回写父节点)。 */}
1276
+ <div data-testid="tree-check" style={{ width: 260 }}>
1277
+ <TreePanel
1278
+ rootVisible={false}
1279
+ nodes={[
1280
+ {
1281
+ id: 'p1', text: '全选组', checked: true, expanded: true, children: [
1282
+ { id: 'p1-a', text: '子项 A', checked: true },
1283
+ { id: 'p1-b', text: '子项 B', checked: true },
1284
+ ],
1285
+ },
1286
+ {
1287
+ id: 'p2', text: '半选组', checked: false, expanded: true, children: [
1288
+ { id: 'p2-a', text: '子项 C', checked: true },
1289
+ { id: 'p2-b', text: '子项 D', checked: false },
1290
+ ],
1291
+ },
1292
+ ]}
1293
+ />
1294
+ </div>
1295
+ {/* Tree 动态增删(命令式句柄:appendChild / removeNode / updateNode / insertBefore)。 */}
1296
+ <div data-testid="tree-dynamic" style={{ position: 'relative', width: 260, marginTop: 12 }}>
1297
+ <TreePanel
1298
+ ref={treeDynRef}
1299
+ rootVisible={false}
1300
+ nodes={[
1301
+ {
1302
+ id: 't1', text: '节点 1', expanded: true, children: [
1303
+ { id: 't1a', text: '子节点 A' },
1304
+ { id: 't1b', text: '子节点 B' },
1305
+ ],
1306
+ },
1307
+ { id: 't2', text: '节点 2' },
1308
+ ]}
1309
+ />
1310
+ <div style={{ marginTop: 8 }}>
1311
+ <Button text="添加子节点" onClick={() => treeDynRef.current?.appendChild('t1', { id: 't' + Date.now(), text: '新子节点' })} />
1312
+ <Button text="删除节点2" onClick={() => treeDynRef.current?.removeNode('t2')} style={{ marginLeft: 4 }} />
1313
+ <Button text="重命名节点1" onClick={() => treeDynRef.current?.updateNode('t1', { text: '节点一(改)' })} style={{ marginLeft: 4 }} />
1314
+ <Button text="插入到节点2前" onClick={() => treeDynRef.current?.insertBefore('t2', { id: 't' + Date.now(), text: '插入节点' })} style={{ marginLeft: 4 }} />
1315
+ <Button text="添加根节点" onClick={() => treeDynRef.current?.appendChild(null, { id: 't' + Date.now(), text: '新根节点' })} style={{ marginLeft: 4 }} />
1316
+ </div>
1317
+ </div>
1318
+ {/* ComboBox 独立展示(预选 'sh',与 golden 的 #combo 同款;下拉展开态供像素对比)。 */}
1319
+ <div data-testid="combo" style={{ width: 200 }}>
1320
+ <ComboBox
1321
+ width={200}
1322
+ value={comboValue}
1323
+ onChange={setComboValue}
1324
+ data={[
1325
+ { value: 'bj', text: '北京' },
1326
+ { value: 'sh', text: '上海' },
1327
+ { value: 'gz', text: '广州' },
1328
+ ]}
1329
+ />
1330
+ </div>
1331
+ {/* ComboBox 过滤演示:键入即按 text/value 过滤下拉项并展开,选中回填 display。 */}
1332
+ <div data-testid="combo-filter" style={{ width: 220 }}>
1333
+ <ComboBox
1334
+ width={220}
1335
+ value={filterValue}
1336
+ onChange={setFilterValue}
1337
+ data={[
1338
+ { value: 'bj', text: '北京' },
1339
+ { value: 'sh', text: '上海' },
1340
+ { value: 'gz', text: '广州' },
1341
+ { value: 'sz', text: '深圳' },
1342
+ { value: 'hz', text: '杭州' },
1343
+ { value: 'cd', text: '成都' },
1344
+ { value: 'wh', text: '武汉' },
1345
+ { value: 'xa', text: '西安' },
1346
+ ]}
1347
+ />
1348
+ </div>
1349
+ {/* ComboBox typeAhead 演示:键入前缀唯一匹配则自动补全剩余文本并高亮补全部分。 */}
1350
+ <div data-testid="combo-typeahead" style={{ width: 220 }}>
1351
+ <ComboBox
1352
+ width={220}
1353
+ typeAhead
1354
+ data={[
1355
+ { value: 'apple', text: 'Apple' },
1356
+ { value: 'apricot', text: 'Apricot' },
1357
+ { value: 'banana', text: 'Banana' },
1358
+ { value: 'cherry', text: 'Cherry' },
1359
+ { value: 'grape', text: 'Grape' },
1360
+ ]}
1361
+ />
1362
+ </div>
1363
+ {/* ComboBox 动态数据源演示(对齐 Ext combo.getStore().loadData / add / remove / setValue)。 */}
1364
+ <div data-testid="combo-dynamic" style={{ width: 220, marginTop: 12 }}>
1365
+ <ComboBox
1366
+ ref={comboDynRef}
1367
+ width={220}
1368
+ value={comboDynValue}
1369
+ onChange={setComboDynValue}
1370
+ data={[
1371
+ { value: 'bj', text: '北京' },
1372
+ { value: 'sh', text: '上海' },
1373
+ { value: 'gz', text: '广州' },
1374
+ ]}
1375
+ />
1376
+ <div style={{ marginTop: 8, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
1377
+ <Button text="添加 深圳" onClick={() => comboDynRef.current?.addItem({ value: 'sz', text: '深圳' })} />
1378
+ <Button text="移除 上海" onClick={() => comboDynRef.current?.removeItem('sh')} />
1379
+ <Button text="换源(新地市)" onClick={() => comboDynRef.current?.setData([
1380
+ { value: 'nj', text: '南京' },
1381
+ { value: 'hz', text: '杭州' },
1382
+ { value: 'su', text: '苏州' },
1383
+ ])} />
1384
+ </div>
1385
+ </div>
1386
+ {/* Slider 滑块演示(水平 value=50 width=200 + 垂直 value=30 height=200),与 golden 的 #slider 同参数,供像素对比。 */}
1387
+ <div data-testid="slider" style={{ display: 'flex', gap: 40, alignItems: 'flex-start' }}>
1388
+ <div>
1389
+ <div style={{ marginBottom: 4 }}>水平:<span data-testid="slider-h-val">{sliderH}</span></div>
1390
+ <Slider
1391
+ width={200}
1392
+ defaultValue={50}
1393
+ onChange={setSliderH}
1394
+ onChangeComplete={setSliderH}
1395
+ />
1396
+ </div>
1397
+ <div>
1398
+ <div style={{ marginBottom: 4 }}>垂直:<span data-testid="slider-v-val">{sliderV}</span></div>
1399
+ <Slider
1400
+ height={200}
1401
+ defaultValue={30}
1402
+ vertical
1403
+ onChange={setSliderV}
1404
+ onChangeComplete={setSliderV}
1405
+ />
1406
+ </div>
1407
+ </div>
1408
+
1409
+ {/* ProgressBar 演示(有文字 value=0.5 + 无文字 value=0.3,width=200),与 golden 的 #progress 同参数,供像素对比。 */}
1410
+ <div data-testid="progress" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
1411
+ <ProgressBar id="progress-fill" width={200} value={progressVal} text={progressTxt} />
1412
+ <ProgressBar id="progress-nofill" width={200} value={0.3} />
1413
+ </div>
1414
+ <Button text="进度 +30%" onClick={() => { setProgressVal(0.8); setProgressTxt('加载中 80%'); }} />
1415
+ <Button text="进度重置" onClick={() => { setProgressVal(0); setProgressTxt(''); }} />
1416
+
1417
+ {/* ProgressBar 动态:通过句柄运行时改写进度/文字/宽度/reset,验证 forwardRef API 等效 Ext.ProgressBar。 */}
1418
+ <div data-testid="progressbar-dynamic" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1419
+ <ProgressBar id="pb-dyn" ref={pbDynRef} width={220} value={0.2} text="上传中 20%" />
1420
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
1421
+ <Button text="句柄 +40%" onClick={() => pbDynRef.current?.setProgress((pbDynRef.current.getValue() + 0.4), `上传中 ${Math.round((pbDynRef.current!.getValue() + 0.4) * 100)}%`)} />
1422
+ <Button text="句柄改宽" onClick={() => pbDynRef.current?.setWidth(320)} />
1423
+ <Button text="句柄 reset" onClick={() => pbDynRef.current?.reset()} />
1424
+ </div>
1425
+ </div>
1426
+
1427
+
1428
+ {/* ColorPalette 演示(42 色,初始选中 #993300),与 golden #colorpalette 同参数,供像素对比。 */}
1429
+ <div data-testid="colorpalette" style={{ position: 'relative', width: 150 }}>
1430
+ <ColorPalette value="#993300" />
1431
+ </div>
1432
+
1433
+ {/* ColorPalette 动态:句柄运行时增删色块、替换调色板、选中切换 */}
1434
+ <div data-testid="colorpalette-dynamic" style={{ display: 'flex', gap: 12, alignItems: 'flex-start', marginTop: 12 }}>
1435
+ <ColorPalette ref={palDynRef} value="#FF0000" />
1436
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
1437
+ <Button text="句柄加色" onClick={() => palDynRef.current?.addColor('123456')} />
1438
+ <Button text="句柄删红" onClick={() => palDynRef.current?.removeColor('#FF0000')} />
1439
+ <Button text="句柄替换" onClick={() => palDynRef.current?.setColors(['FFFFFF', '000000', '00FF00'])} />
1440
+ <Button text="句柄选绿" onClick={() => palDynRef.current?.select('00FF00')} />
1441
+ </div>
1442
+ </div>
1443
+
1444
+ {/* Resizable 演示(pinned 全句柄,200×120 蓝底),与 golden #resizable 同参数,供像素对比。
1445
+ wrapper 仅留白避免相邻演示压迫 6px 句柄条;el 自身即 #resizable。 */}
1446
+ <div data-testid="resizable-wrap" style={{ position: 'relative', width: 240, height: 160 }}>
1447
+ <Resizable
1448
+ id="resizable"
1449
+ testId="resizable"
1450
+ width={200}
1451
+ height={120}
1452
+ pinned
1453
+ handles="all"
1454
+ minWidth={20}
1455
+ minHeight={20}
1456
+ style={{ background: '#c3d9ff' }}
1457
+ />
1458
+ </div>
1459
+
1460
+ {/* Box 布局演示(HBox / VBox,纯布局容器,复用 ext-all.css 的 .x-box* 规则零契约破坏)。
1461
+ hbox 与 golden #box-hbox 同参数;vbox 与 golden #box-vbox 同参数。 */}
1462
+ <Box
1463
+ testId="box-hbox"
1464
+ layout="hbox"
1465
+ width={400}
1466
+ height={200}
1467
+ pack={hboxPack}
1468
+ items={[
1469
+ { width: 80, height: 80, style: { background: '#f0c0c0' } },
1470
+ { width: 120, height: 60, style: { background: '#c0f0c0' } },
1471
+ { width: 100, height: 100, style: { background: '#c0c0f0' } },
1472
+ ]}
1473
+ />
1474
+ <Box
1475
+ testId="box-vbox"
1476
+ layout="vbox"
1477
+ width={400}
1478
+ height={200}
1479
+ items={[
1480
+ { width: 300, height: 50, style: { background: '#f0c0c0' } },
1481
+ { width: 200, height: 70, style: { background: '#c0f0c0' } },
1482
+ { width: 250, height: 60, style: { background: '#c0c0f0' } },
1483
+ ]}
1484
+ />
1485
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
1486
+ <Button text="HBox pack 切换" onClick={() => setHboxPack((p) => (p === 'start' ? 'center' : 'start'))} />
1487
+ </div>
1488
+
1489
+ {/* SplitButton 演示(Ext.SplitButton:主体走 handler、箭头展开菜单),与 golden #splitbutton 同参数。 */}
1490
+ <div data-testid="splitbutton" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
1491
+ <Button
1492
+ split
1493
+ text="Options"
1494
+ handler={() => {}}
1495
+ menu={[
1496
+ { text: 'Item 1', handler: () => {} },
1497
+ { text: 'Item 2', handler: () => {} },
1498
+ { text: 'Item 3', handler: () => {} },
1499
+ ]}
1500
+ />
1501
+ </div>
1502
+
1503
+ {/* CycleButton 演示(Ext.CycleButton:主体点击循环切换、箭头展开菜单),与 golden #cyclebutton 同参数。 */}
1504
+ <div data-testid="cyclebutton" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
1505
+ <CycleButton
1506
+ showText
1507
+ prependText="View as "
1508
+ items={[
1509
+ { text: 'text only', checked: true },
1510
+ { text: 'HTML' },
1511
+ { text: 'XML' },
1512
+ ]}
1513
+ />
1514
+ </div>
1515
+
1516
+ {/* CycleButton 动态:句柄运行时增删改选项、切换激活项 */}
1517
+ <div data-testid="cyclebutton-dynamic" style={{ display: 'flex', gap: 8, alignItems: 'center', marginTop: 12 }}>
1518
+ <CycleButton
1519
+ ref={cycDynRef}
1520
+ showText
1521
+ prependText="View as "
1522
+ items={[
1523
+ { text: 'text only', checked: true },
1524
+ { text: 'HTML' },
1525
+ { text: 'XML' },
1526
+ ]}
1527
+ />
1528
+ <Button text="句柄加项" onClick={() => cycDynRef.current?.addItem({ text: 'JSON' })} />
1529
+ <Button text="句柄删首个" onClick={() => cycDynRef.current?.removeItem(0)} />
1530
+ <Button text="句柄改标签" onClick={() => cycDynRef.current?.updateItem(0, { text: 'plain text' })} />
1531
+ <Button text="句柄切第二项" onClick={() => cycDynRef.current?.setActive(1)} />
1532
+ </div>
1533
+
1534
+ {/* DataView 演示(Ext.DataView:容器 x-data-view + 按 tpl 渲染 x-view-item,点击/拖拽框选),
1535
+ 与 golden #dataview 同参数(6 项、预选 index 1)。 */}
1536
+ <div data-testid="dataview" style={{ width: 220 }}>
1537
+ <DataView
1538
+ multiSelect
1539
+ selected={1}
1540
+ data={[
1541
+ { text: 'Item 1', value: '1' },
1542
+ { text: 'Item 2', value: '2' },
1543
+ { text: 'Item 3', value: '3' },
1544
+ { text: 'Item 4', value: '4' },
1545
+ { text: 'Item 5', value: '5' },
1546
+ { text: 'Item 6', value: '6' },
1547
+ ]}
1548
+ />
1549
+ </div>
1550
+
1551
+ {/* DataView 空数据演示(emptyText:data 为空时渲染提示节点,无 golden 对应,不入保真循环)。 */}
1552
+ <div data-testid="dataview-empty" style={{ width: 220 }}>
1553
+ <DataView multiSelect emptyText="没有数据" data={[]} />
1554
+ </div>
1555
+
1556
+ {/* DataView 拖拽框选演示(multiSelect + 固定高度留出底部空白起拖区,供 rubber-band select 行为测试;无 golden 对应)。 */}
1557
+ <div data-testid="dataview-drag" style={{ width: 220 }}>
1558
+ <DataView
1559
+ multiSelect
1560
+ selected={[]}
1561
+ height={260}
1562
+ data={[
1563
+ { text: 'Item 1', value: '1' },
1564
+ { text: 'Item 2', value: '2' },
1565
+ { text: 'Item 3', value: '3' },
1566
+ { text: 'Item 4', value: '4' },
1567
+ ]}
1568
+ />
1569
+ </div>
1570
+
1571
+ {/* DataView 动态数据源(addItem/removeByIndex/getItems/setSelected):种子 4 项,按钮运行时增减;无 golden 对应。 */}
1572
+ <div data-testid="dataview-dynamic" style={{ width: 220 }}>
1573
+ <DataView
1574
+ ref={dvRef}
1575
+ multiSelect
1576
+ selected={[0]}
1577
+ data={[
1578
+ { text: 'A', value: 'a' },
1579
+ { text: 'B', value: 'b' },
1580
+ { text: 'C', value: 'c' },
1581
+ { text: 'D', value: 'd' },
1582
+ ]}
1583
+ />
1584
+ <div style={{ marginTop: 8 }}>
1585
+ <Button text="添加项" onClick={() => dvRef.current?.addItem({ text: `X${nextDv.current++}`, value: 'x' })} />
1586
+ <Button text="删除首项" onClick={() => dvRef.current?.removeByIndex(0)} style={{ marginLeft: 4 }} />
1587
+ <Button text="清空" onClick={() => dvRef.current?.clear()} style={{ marginLeft: 4 }} />
1588
+ <Button text="选中全部" onClick={() => {
1589
+ const n = dvRef.current?.getItems().length ?? 0;
1590
+ dvRef.current?.setSelected(Array.from({ length: n }, (_, i) => i));
1591
+ }} style={{ marginLeft: 4 }} />
1592
+ </div>
1593
+ </div>
1594
+
1595
+ {/* ListView 演示(Ext.list.ListView:列头 + 数据行,点击行选中、悬停高亮),与 golden #listview 同参数。 */}
1596
+ <div data-testid="listview" style={{ width: 320 }}>
1597
+ <ListView
1598
+ width={320}
1599
+ height={200}
1600
+ multiSelect
1601
+ value={[1]}
1602
+ columns={[
1603
+ { header: 'Name', dataIndex: 'name', width: 0.4 },
1604
+ { header: 'Size', dataIndex: 'size', width: 0.2, align: 'right' },
1605
+ { header: 'Modified', dataIndex: 'modified', width: 0.4 },
1606
+ ]}
1607
+ data={[
1608
+ { name: 'report.pdf', size: '2067', modified: '08-12 09:30' },
1609
+ { name: 'budget.xls', size: '1530', modified: '08-13 14:05' },
1610
+ { name: 'notes.txt', size: '412', modified: '08-14 11:20' },
1611
+ { name: 'photo.jpg', size: '8810', modified: '08-15 18:44' },
1612
+ { name: 'index.html', size: '2333', modified: '08-16 08:01' },
1613
+ { name: 'data.csv', size: '991', modified: '08-17 22:13' },
1614
+ ]}
1615
+ />
1616
+ </div>
1617
+
1618
+ {/* ListView 动态增删演示(ref.add / ref.removeByIndex,对齐 Ext store add/remove),无 golden 对应、不入保真循环。 */}
1619
+ <div data-testid="listview-dynamic" style={{ width: 320, marginTop: 24 }}>
1620
+ <DynamicListViewDemo />
1621
+ </div>
1622
+
1623
+ {/* Editor 演示(Ext.Editor:行内编辑浮层,x-editor > x-form-field-wrap > input.x-form-text),与 golden #editor 同参数。 */}
1624
+ <div data-testid="editor" style={{ position: 'relative', width: 300, height: 50 }}>
1625
+ <Editor value="Edit me" boundText="Edit me" width={180} height={22} />
1626
+ </div>
1627
+
1628
+ {/* Editor 动态演示(Ext.Editor):通过 ref 调用句柄运行时 startEdit / setValue / completeEdit / cancelEdit。 */}
1629
+ <EditorDynamicDemo />
1630
+
1631
+ {/* TreeGrid 演示(3 列 Task/Duration/Assigned To,rootVisible:false),与 golden #treegrid 同参数。 */}
1632
+ <div data-testid="treegrid" style={{ width: 500 }}>
1633
+ <TreeGrid
1634
+ title="Core Team Projects"
1635
+ width={500}
1636
+ height={300}
1637
+ columns={[
1638
+ { header: 'Task', dataIndex: 'task', width: 230 },
1639
+ { header: 'Duration', dataIndex: 'duration', width: 100, align: 'center' },
1640
+ { header: 'Assigned To', dataIndex: 'user', width: 150 },
1641
+ ]}
1642
+ data={treegridData}
1643
+ />
1644
+ </div>
1645
+ {/* TreeGrid 动态演示(forwardRef + 句柄运行时增删改节点 / 展开折叠 / 选中)。 */}
1646
+ <div data-testid="treegrid-dynamic" style={{ width: 500, marginTop: 12 }}>
1647
+ <TreeGridDynamicDemo />
1648
+ </div>
1649
+
1650
+ {/* FieldSet 动态字段增删演示(对齐 Ext FieldSet.add/remove):种子 2 个联系人,按钮添加、每行自带删除按钮移除。无 golden 对应,不入保真循环。 */}
1651
+ <DynamicFieldSetDemo />
1652
+ {/* FieldSet 演示(独立 collapsible fieldset,legend 折叠工具 + 两个 textfield),与 golden #fieldset 同参数。 */}
1653
+ <div data-testid="fieldset" style={{ width: 360 }}>
1654
+ <FieldSet title="联系方式" collapsible width={360}>
1655
+ <FormField fieldLabel="姓名">
1656
+ <TextField width={200} />
1657
+ </FormField>
1658
+ <FormField fieldLabel="电话">
1659
+ <TextField width={200} />
1660
+ </FormField>
1661
+ </FieldSet>
1662
+ </div>
1663
+
1664
+ {/* MessageBox 演示(Ext.Msg.show:QUESTION 图标 + Yes/No/Cancel 居中),与 golden .x-window-dlg 同参数。
1665
+ 对话框为绝对定位,置于相对容器避免覆盖其他演示;点击关闭/任意按钮后隐藏(onClose 行为可测)。 */}
1666
+ <div data-testid="messagebox" style={{ position: 'relative', width: 600, height: 170 }}>
1667
+ {showMessageBox && (
1668
+ <MessageBox
1669
+ title="保存修改?"
1670
+ msg="您正在关闭一个包含未保存修改的标签页。是否保存这些修改?"
1671
+ width={400}
1672
+ icon="question"
1673
+ onClose={() => setShowMessageBox(false)}
1674
+ buttons={[
1675
+ { text: 'Yes', onClick: () => setShowMessageBox(false), width: 75 },
1676
+ { text: 'No', onClick: () => setShowMessageBox(false), width: 75 },
1677
+ { text: 'Cancel', onClick: () => setShowMessageBox(false), width: 75 },
1678
+ ]}
1679
+ />
1680
+ )}
1681
+ </div>
1682
+ {/* MessageBox 动态演示(forwardRef + 句柄运行时改写标题/消息/图标与 show/hide)。 */}
1683
+ <div data-testid="messagebox-dynamic" style={{ position: 'relative', width: 600, height: 170, marginTop: 8 }}>
1684
+ <MessageBoxDynamicDemo />
1685
+ </div>
1686
+
1687
+ {/* ToolTip 演示(固定位置 show,x-tip 圆角 + 标题 + 正文),与 golden #tooltip 同参数,供像素对比。 */}
1688
+ <div data-testid="tooltip" style={{ position: 'relative', width: 240, height: 90 }}>
1689
+ <ToolTip title="提示标题" html="这是提示内容,用于说明操作。" width={200} x={0} y={0} />
1690
+ </div>
1691
+ {/* ToolTip hover 触发演示(行为测试用:悬停目标显示 / 移出隐藏) */}
1692
+ <div data-testid="tooltip-hover" style={{ marginTop: 8 }}>
1693
+ <span
1694
+ id="tooltip-trigger"
1695
+ style={{ display: 'inline-block', padding: '4px 10px', border: '1px solid #99bbe8', background: '#fff', cursor: 'help' }}
1696
+ >
1697
+ 悬停查看提示
1698
+ </span>
1699
+ <ToolTip target="#tooltip-trigger" html="悬停触发的提示内容。" showDelay={0} hideDelay={200} />
1700
+ </div>
1701
+ {/* ToolTip 动态演示(forwardRef + 句柄运行时 show/hide/setTitle/setBody)。 */}
1702
+ <div data-testid="tooltip-dynamic" style={{ position: 'relative', marginTop: 8, width: 240, height: 90 }}>
1703
+ <ToolTipDynamicDemo />
1704
+ </div>
1705
+ {/* Accordion 演示(根 Panel + 多个可折叠子 Panel,互斥展开),与 golden #accordion 同参数,供像素对比。 */}
1706
+ <div data-testid="accordion" style={{ position: 'relative', width: 260, height: 300 }}>
1707
+ <Accordion
1708
+ title="Accordion"
1709
+ width={260}
1710
+ height={300}
1711
+ defaultActiveIndex={0}
1712
+ items={[
1713
+ { title: 'Panel 1', html: '<p>Panel 1 content</p>' },
1714
+ { title: 'Panel 2', html: '<p>Panel 2 content</p>' },
1715
+ { title: 'Panel 3', html: '<p>Panel 3 content</p>' },
1716
+ ]}
1717
+ />
1718
+ </div>
1719
+ {/* Accordion 动态:通过句柄运行时增删改子面板 + 切换激活项,验证 forwardRef 单源数组 API。 */}
1720
+ <div data-testid="accordion-dynamic" style={{ position: 'relative', width: 260, height: 300, marginTop: 12 }}>
1721
+ <Accordion
1722
+ ref={accDynRef}
1723
+ title="Accordion (dynamic)"
1724
+ width={260}
1725
+ height={300}
1726
+ defaultActiveIndex={0}
1727
+ items={[
1728
+ { title: 'Panel A', html: '<p>Panel A content</p>' },
1729
+ { title: 'Panel B', html: '<p>Panel B content</p>' },
1730
+ ]}
1731
+ />
1732
+ <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
1733
+ <Button text="句柄加 Panel" onClick={() => accDynRef.current?.addItem({ title: 'Panel C', html: '<p>Panel C content</p>' })} />
1734
+ <Button text="句柄删首个" onClick={() => accDynRef.current?.removeItem(0)} />
1735
+ <Button text="句柄改标题" onClick={() => accDynRef.current?.updateItem(0, { title: 'Panel A*' })} />
1736
+ </div>
1737
+ </div>
1738
+ {/* Accordion 隐藏折叠工具(hideCollapseTool):不渲染 x-tool-toggle,仅标题点击生效。 */}
1739
+ <div data-testid="accordion-hidetool" style={{ position: 'relative', width: 260, height: 300, marginTop: 12 }}>
1740
+ <Accordion
1741
+ title="Accordion (hideCollapseTool)"
1742
+ width={260}
1743
+ height={300}
1744
+ defaultActiveIndex={0}
1745
+ hideCollapseTool
1746
+ items={[
1747
+ { title: 'Panel 1', html: '<p>Panel 1 content</p>' },
1748
+ { title: 'Panel 2', html: '<p>Panel 2 content</p>' },
1749
+ ]}
1750
+ />
1751
+ </div>
1752
+ {/* Accordion 展开项置顶(activeOnTop):展开的子面板冒泡到顶部。 */}
1753
+ <div data-testid="accordion-ontop" style={{ position: 'relative', width: 260, height: 300, marginTop: 12 }}>
1754
+ <Accordion
1755
+ title="Accordion (activeOnTop)"
1756
+ width={260}
1757
+ height={300}
1758
+ defaultActiveIndex={0}
1759
+ activeOnTop
1760
+ items={[
1761
+ { title: 'Panel 1', html: '<p>Panel 1 content</p>' },
1762
+ { title: 'Panel 2', html: '<p>Panel 2 content</p>' },
1763
+ { title: 'Panel 3', html: '<p>Panel 3 content</p>' },
1764
+ ]}
1765
+ />
1766
+ </div>
1767
+ {/* Accordion 受控激活(activeIndex)+ 事件(onActiveChange/per-item onExpand/onCollapse)。 */}
1768
+ <div data-testid="accordion-controlled" style={{ position: 'relative', width: 260, height: 300, marginTop: 12 }}>
1769
+ <Accordion
1770
+ title="Accordion (controlled)"
1771
+ width={260}
1772
+ height={300}
1773
+ activeIndex={activeControlled}
1774
+ onActiveChange={setActiveControlled}
1775
+ items={[
1776
+ { title: 'Panel 1', html: '<p>Panel 1 content</p>', onExpand: () => setAccEvent('1-expand'), onCollapse: () => setAccEvent('1-collapse') },
1777
+ { title: 'Panel 2', html: '<p>Panel 2 content</p>', onExpand: () => setAccEvent('2-expand'), onCollapse: () => setAccEvent('2-collapse') },
1778
+ { title: 'Panel 3', html: '<p>Panel 3 content</p>', onExpand: () => setAccEvent('3-expand'), onCollapse: () => setAccEvent('3-collapse') },
1779
+ ]}
1780
+ />
1781
+ <div style={{ marginTop: 8 }}>
1782
+ <Button text="激活2" onClick={() => setActiveControlled(1)} />
1783
+ <Button text="激活3" onClick={() => setActiveControlled(2)} style={{ marginLeft: 4 }} />
1784
+ </div>
1785
+ <div data-testid="accordion-event-log" style={{ marginTop: 4, fontSize: 11 }}>{accEvent}</div>
1786
+ </div>
1787
+ {/* TimeField 演示(ComboBox 子类,时间点下拉),与 golden #timefield 同参数,供像素对比。 */}
1788
+ <div data-testid="timefield" style={{ position: 'relative', width: 200 }}>
1789
+ <TimeField width={200} increment={30} />
1790
+ </div>
1791
+ {/* TimeField 动态数据源演示(对齐 Ext TimeField 的 store 运行时编辑:
1792
+ 透传 ComboBoxHandle 的 setData/addItem/removeItem)。 */}
1793
+ <div data-testid="timefield-dynamic" style={{ position: 'relative', width: 220, marginTop: 12 }}>
1794
+ <TimeField ref={tfDynRef} width={220} increment={30} onChange={setTfDynValue} value={tfDynValue} />
1795
+ <div style={{ marginTop: 8 }}>
1796
+ <Button text="15 分钟粒度" onClick={() => tfDynRef.current?.setData(genTimes(15))} />
1797
+ <Button text="加 11:45 PM" onClick={() => tfDynRef.current?.addItem({ value: '11:45 PM', text: '11:45 PM' })} />
1798
+ <Button
1799
+ text="移除 12:00 AM"
1800
+ onClick={() => tfDynRef.current?.removeItem('12:00 AM')}
1801
+ style={{ marginLeft: 4 }}
1802
+ />
1803
+ </div>
1804
+ </div>
1805
+ {/* DateField 动态数据源演示(对齐 Ext.DateField 的运行时 setMinValue/setMaxValue/setDisabledDays):
1806
+ 通过 DateFieldHandle 在运行时改写可选范围与禁用星期,验证 DatePicker 的 x-date-disabled 实时刷新。 */}
1807
+ <div data-testid="datefield-dynamic" style={{ position: 'relative', width: 220, marginTop: 12 }}>
1808
+ <DateField ref={dfDynRef} width={220} value="08/15/2026" format="m/d/Y" />
1809
+ <div style={{ marginTop: 8 }}>
1810
+ <Button text="限制 8 月内" onClick={() => { dfDynRef.current?.setMinDate(new Date(2026, 7, 1)); dfDynRef.current?.setMaxDate(new Date(2026, 7, 31)); }} />
1811
+ <Button
1812
+ text="禁周末"
1813
+ onClick={() => dfDynRef.current?.setDisabledDays([0, 6])}
1814
+ style={{ marginLeft: 4 }}
1815
+ />
1816
+ <Button
1817
+ text="解除限制"
1818
+ onClick={() => { dfDynRef.current?.setMinDate(null); dfDynRef.current?.setMaxDate(null); dfDynRef.current?.setDisabledDays([]); }}
1819
+ style={{ marginLeft: 4 }}
1820
+ />
1821
+ </div>
1822
+ </div>
1823
+
1824
+ {/* SplitBar 演示(Ext.SplitBar:被缩放区域 + 分隔条,拖拽改变宽度)。
1825
+ 与 golden #splitbar 同结构(resizing 160×120 + splitter 6×120,grid-split.gif grip,placement LEFT)。
1826
+ testId 直接落在 SplitBar 根(flex 容器),对齐 golden #splitbar。
1827
+ 包裹为整行(width:100%)避免拖拽溢出(resizing 最大 260)压到同排 LoadMask 遮罩层。 */}
1828
+ <div style={{ width: '100%' }}>
1829
+ <SplitBar
1830
+ testId="splitbar"
1831
+ size={160}
1832
+ thickness={120}
1833
+ placement="left"
1834
+ minSize={60}
1835
+ maxSize={260}
1836
+ resizingStyle={{ background: '#e0e8f5' }}
1837
+ resizingContent={<div style={{ padding: 8 }}>左面板内容</div>}
1838
+ />
1839
+ </div>
1840
+ {/* LoadMask 演示(Ext.LoadMask / Ext.Element.mask):300×160 容器 + 遮罩层 + 居中 Loading 框。
1841
+ 与 golden #loadmask 同容器尺寸、同文案、同 x-masked 类,供像素对比。
1842
+ testId 直接落在 LoadMask 根(x-masked 容器),对齐 golden #loadmask。 */}
1843
+ <LoadMask
1844
+ testId="loadmask"
1845
+ loading={true}
1846
+ msg="Loading..."
1847
+ width={300}
1848
+ height={160}
1849
+ >
1850
+ <div style={{ padding: 12, font: '11px tahoma,arial,sans-serif', color: '#333' }}>面板内容(遮罩下方)</div>
1851
+ </LoadMask>
1852
+ {/* 受控演示(仅行为验证,无 golden 对应):loading=false 不应渲染遮罩;自定义文案应生效。 */}
1853
+ <LoadMask testId="loadmask-off" loading={false} width={200} height={100}>
1854
+ <div style={{ padding: 8 }}>无遮罩内容</div>
1855
+ </LoadMask>
1856
+ <LoadMask testId="loadmask-custom" loading={true} msg="数据加载中..." width={200} height={100}>
1857
+ <div style={{ padding: 8 }}>自定义文案</div>
1858
+ </LoadMask>
1859
+ {/* ButtonGroup 演示(Ext.ButtonGroup):frame 圆角分组 + 标题 + table 布局排按钮。
1860
+ 与 golden #buttongroup 同结构(title 'Clipboard',columns 3,3 个文本按钮),供像素对比。
1861
+ 包裹为整行避免与相邻组件溢出重叠。 */}
1862
+ <div style={{ width: '100%' }}>
1863
+ <ButtonGroup
1864
+ testId="buttongroup"
1865
+ title="Clipboard"
1866
+ columns={3}
1867
+ items={[
1868
+ { text: 'Paste' },
1869
+ { text: 'Copy' },
1870
+ { text: 'Cut' },
1871
+ ]}
1872
+ />
1873
+ </div>
1874
+ {/* 无标题变体(仅行为验证):加 x-btn-group-notitle,不渲染标题头。 */}
1875
+ <div style={{ width: '100%' }}>
1876
+ <ButtonGroup
1877
+ testId="buttongroup-notitle"
1878
+ columns={2}
1879
+ items={[
1880
+ { text: 'Bold' },
1881
+ { text: 'Italic' },
1882
+ ]}
1883
+ />
1884
+ </div>
1885
+ {/* StatusBar 演示(Ext.ux.StatusBar):Toolbar 子类 + 贪婪状态文本元素(.x-status-text)。
1886
+ 与 golden #statusbar 同结构(text 'OK' + iconCls 'x-status-valid',items 经 '->' 右对齐)。
1887
+ 包裹整行避免与相邻组件重叠。 */}
1888
+ <div style={{ width: '100%' }}>
1889
+ <StatusBar
1890
+ testId="statusbar"
1891
+ width={520}
1892
+ text="OK"
1893
+ iconCls="x-status-valid"
1894
+ items={[
1895
+ { text: 'A Button' },
1896
+ '-',
1897
+ 'Plain Text',
1898
+ ]}
1899
+ />
1900
+ </div>
1901
+ {/* 右对齐变体(仅行为验证):statusAlign 'right',状态文本落在 .x-toolbar-right-row。 */}
1902
+ <div style={{ width: '100%' }}>
1903
+ <StatusBar
1904
+ testId="statusbar-right"
1905
+ width={520}
1906
+ statusAlign="right"
1907
+ text="就绪"
1908
+ iconCls="x-status-valid"
1909
+ items={[
1910
+ { text: '刷新' },
1911
+ '-',
1912
+ '用户信息',
1913
+ ]}
1914
+ />
1915
+ </div>
1916
+ {/* 动态状态(仅行为验证):按钮切换 showBusy / clearStatus。 */}
1917
+ <div style={{ width: '100%' }}>
1918
+ <DynamicStatusBar />
1919
+ </div>
1920
+ {/* DisplayField 演示(Ext.form.DisplayField):独立渲染为裸 div.x-form-display-field。
1921
+ 与 golden #displayfield 同值('状态:已启用'),容器固定 300px 保证等宽,供像素对比。 */}
1922
+ <div style={{ width: 300 }}>
1923
+ <DisplayField testId="displayfield" value="状态:已启用" />
1924
+ </div>
1925
+ {/* 表单内带标签变体(仅行为验证):由 FormField 包裹为 x-form-item > label + x-form-element。 */}
1926
+ <div style={{ width: 300 }}>
1927
+ <FormField fieldLabel="名称" labelWidth={80}>
1928
+ <DisplayField id="df-name" value="张三" />
1929
+ </FormField>
1930
+ </div>
1931
+ {/* CheckboxGroup 演示(Ext.form.CheckboxGroup):独立渲染为 x-form-check-group > 多列。
1932
+ 与 golden #checkgroup 同结构(columns:2,4 项 篮球/足球/排球/游泳),供像素对比。 */}
1933
+ <div style={{ width: 300 }}>
1934
+ <CheckboxGroup
1935
+ testId="checkgroup"
1936
+ columns={2}
1937
+ items={[
1938
+ { boxLabel: '篮球', inputValue: 'b' },
1939
+ { boxLabel: '足球', inputValue: 'f' },
1940
+ { boxLabel: '排球', inputValue: 'v' },
1941
+ { boxLabel: '游泳', inputValue: 's' },
1942
+ ]}
1943
+ />
1944
+ </div>
1945
+ {/* CheckboxGroup 动态:通过句柄运行时增删改选项,验证 forwardRef 单源数组 API。 */}
1946
+ <div data-testid="checkgroup-dynamic" style={{ width: 300, marginTop: 12 }}>
1947
+ <CheckboxGroup
1948
+ ref={cgDynRef}
1949
+ testId="checkgroup-dyn"
1950
+ columns={2}
1951
+ items={[
1952
+ { boxLabel: '篮球', inputValue: 'b' },
1953
+ { boxLabel: '足球', inputValue: 'f' },
1954
+ ]}
1955
+ />
1956
+ <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
1957
+ <Button text="句柄加项" onClick={() => cgDynRef.current?.addItem({ boxLabel: '排球', inputValue: 'v' })} />
1958
+ <Button text="句柄删首个" onClick={() => cgDynRef.current?.removeItem(0)} />
1959
+ <Button text="句柄改标签" onClick={() => cgDynRef.current?.updateItem(0, { boxLabel: '篮球*' })} />
1960
+ </div>
1961
+ </div>
1962
+ {/* ButtonGroup 动态:通过句柄运行时增删改按钮,验证 forwardRef 单源数组 API。 */}
1963
+ <div data-testid="buttongroup-dynamic" style={{ width: '100%', marginTop: 12 }}>
1964
+ <ButtonGroup
1965
+ ref={bgDynRef}
1966
+ testId="buttongroup-dyn"
1967
+ title="Tools"
1968
+ columns={3}
1969
+ items={[
1970
+ { text: 'New' },
1971
+ { text: 'Open' },
1972
+ ]}
1973
+ />
1974
+ <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
1975
+ <Button text="句柄加按钮" onClick={() => bgDynRef.current?.addItem({ text: 'Save' })} />
1976
+ <Button text="句柄删首个" onClick={() => bgDynRef.current?.removeItem(0)} />
1977
+ <Button text="句柄改文本" onClick={() => bgDynRef.current?.updateItem(0, { text: 'New*' })} />
1978
+ </div>
1979
+ </div>
1980
+ {/* SpinnerField 演示(Ext.ux.form.SpinnerField):TriggerField 子类,wrap 内含 input + spinner-trigger + splitter。
1981
+ 与 golden #spinner 同结构、同宽(194),供像素对比。 */}
1982
+ <div style={{ width: 200 }}>
1983
+ <SpinnerField
1984
+ testId="spinner"
1985
+ width={194}
1986
+ value={5}
1987
+ minValue={0}
1988
+ maxValue={100}
1989
+ incrementValue={1}
1990
+ />
1991
+ </div>
1992
+
1993
+ {/* QuickTip 演示(Ext.QuickTips):展示的 tip 与 ToolTip 同结构(x-tip 9 宫格圆角 + 标题 + 正文)。
1994
+ 保真:#quicktip 容器内强制可见(visible)静态渲染 tip,供像素对比。
1995
+ 行为:下方 #qt-demo 携带 data-qtip/data-qtitle,hover 触发全局 tip(showDelay=0 加速测试)。 */}
1996
+ <div style={{ width: 240, height: 90, position: 'relative' }}>
1997
+ <QuickTip
1998
+ testId="quicktip"
1999
+ title="提示标题"
2000
+ html="这是提示内容,用于说明操作。"
2001
+ width={200}
2002
+ visible
2003
+ x={0}
2004
+ y={0}
2005
+ />
2006
+ </div>
2007
+ <div style={{ width: 200 }}>
2008
+ <div
2009
+ id="qt-demo"
2010
+ data-qtip="QuickTip 内容"
2011
+ data-qtitle="快捷提示"
2012
+ style={{ padding: 8, border: '1px solid #99bbe8', background: '#fff' }}
2013
+ >
2014
+ 悬停我查看 QuickTip
2015
+ </div>
2016
+ <QuickTip target="#qt-demo" title="快捷提示" text="QuickTip 内容" showDelay={0} hideDelay={0} />
2017
+ </div>
2018
+ {/* QuickTip 动态演示:无 target,由 ref 句柄驱动显隐与内容。 */}
2019
+ <QuickTipDynamicDemo />
2020
+ {/* HtmlEditor 演示(Ext.form.HtmlEditor):x-html-editor-wrap > x-html-editor-tb(工具栏) + 隐藏 textarea + iframe。
2021
+ 宽 520、高 200,与 golden #htmleditor 同值,供像素对比。 */}
2022
+ <HtmlEditor
2023
+ testId="htmleditor"
2024
+ width={520}
2025
+ height={200}
2026
+ value="<p>初始内容</p>"
2027
+ />
2028
+ <HtmlEditorDynamicDemo />
2029
+ {/* ColorMenu 演示(Ext.menu.ColorMenu):plain 菜单内含 ColorPalette(40 色)。
2030
+ 触发按钮点击展开浮层 .x-color-menu,与 golden #colormenu 同结构,供像素对比。 */}
2031
+ <div data-testid="colormenu" style={{ position: 'relative' }}>
2032
+ <ColorMenuDemo />
2033
+ </div>
2034
+ <ColorMenuDynamicDemo />
2035
+ {/* TableLayout 演示(Ext.layout.TableLayout):3 列网格,含 colspan/rowspan 跨格。
2036
+ 宽 360、列宽合计=面板宽,与 golden #tablelayout 同结构供像素对比。 */}
2037
+ <TableLayout
2038
+ testId="tablelayout"
2039
+ title="Table Layout"
2040
+ width={360}
2041
+ columns={3}
2042
+ items={[
2043
+ { title: 'A', html: '<div>内容 A</div>', height: 80 },
2044
+ { title: 'B', html: '<div>内容 B</div>', height: 80, colspan: 2 },
2045
+ { title: 'C', html: '<div>内容 C</div>', height: 120, rowspan: 2 },
2046
+ { title: 'D', html: '<div>内容 D</div>', height: 80 },
2047
+ { title: 'E', html: '<div>内容 E</div>', height: 80 },
2048
+ ]}
2049
+ />
2050
+ {/* RowNumberer 演示(Ext.grid.RowNumberer):首列为自动行号列,行号右对齐、列宽 23。
2051
+ 网格宽 400(列宽和 353,右侧留白与 golden 一致),与 golden #rownumberer 同值,供像素对比。 */}
2052
+ <div data-testid="rownumberer" style={{ width: 400 }}>
2053
+ <Grid
2054
+ title="带行号的列表"
2055
+ width={400}
2056
+ columns={[
2057
+ { type: 'rownumberer', header: '', dataIndex: '_' },
2058
+ { header: '姓名', dataIndex: 'name', width: 80 },
2059
+ { header: '代码', dataIndex: 'code', width: 50 },
2060
+ { header: '描述', dataIndex: 'desc', width: 200 },
2061
+ ]}
2062
+ data={[
2063
+ { name: 'Alpha', code: 'A1', desc: '第一行描述' },
2064
+ { name: 'Beta', code: 'B2', desc: '第二行描述' },
2065
+ { name: 'Gamma', code: 'G3', desc: '第三行描述' },
2066
+ { name: 'Delta', code: 'D4', desc: '第四行描述' },
2067
+ ]}
2068
+ />
2069
+ </div>
2070
+ </div>
2071
+ );
2072
+ }
2073
+
2074
+ // 动态字段增删演示:对齐 Ext.form.FieldSet.add / remove。种子 2 个联系人,
2075
+ // 「添加联系人」调用 ref.add 追加一行(行内自带删除按钮,调用 ref.remove 移除自身)。
2076
+ function DynamicFieldSetDemo() {
2077
+ const fsRef = useRef<FieldSetHandle>(null);
2078
+ const counter = useRef(0);
2079
+ const addContact = () => {
2080
+ const id = `contact-${counter.current++}`;
2081
+ fsRef.current?.add(
2082
+ <div
2083
+ key={id}
2084
+ data-dyn-id={id}
2085
+ style={{ display: 'flex', gap: 8, alignItems: 'center' }}
2086
+ >
2087
+ <FormField fieldLabel="联系人" labelWidth={60}>
2088
+ <TextField width={160} emptyText="输入姓名" />
2089
+ </FormField>
2090
+ <Button text="删除" onClick={() => fsRef.current?.remove(id)} />
2091
+ </div>,
2092
+ id,
2093
+ );
2094
+ };
2095
+ return (
2096
+ <div data-testid="form-dynamic" style={{ width: 360 }}>
2097
+ <FieldSet ref={fsRef} title="联系人列表" collapsible width={360}>
2098
+ <div data-dyn-id="seed-0" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
2099
+ <FormField fieldLabel="联系人" labelWidth={60}>
2100
+ <TextField width={160} defaultValue="张三" />
2101
+ </FormField>
2102
+ </div>
2103
+ <div data-dyn-id="seed-1" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
2104
+ <FormField fieldLabel="联系人" labelWidth={60}>
2105
+ <TextField width={160} defaultValue="李四" />
2106
+ </FormField>
2107
+ </div>
2108
+ </FieldSet>
2109
+ <div style={{ marginTop: 8 }}>
2110
+ <Button text="添加联系人" onClick={addContact} />
2111
+ </div>
2112
+ </div>
2113
+ );
2114
+ }
2115
+
2116
+ // Toolbar 动态增删演示:种子 2 个按钮;「添加按钮」追加一个可自删的动态按钮。
2117
+ // Toolbar 动态增删演示:种子 2 个按钮;「添加按钮」追加一个可自删的动态按钮。
2118
+ function DynamicToolbarDemo() {
2119
+ const tbRef = useRef<ToolbarHandle>(null);
2120
+ const counter = useRef(0);
2121
+ const addBtn = () => {
2122
+ const id = `dyn-${counter.current++}`;
2123
+ tbRef.current?.add(
2124
+ { xtype: 'button', text: `动态${counter.current}`, handler: () => tbRef.current?.remove(id) },
2125
+ id,
2126
+ );
2127
+ };
2128
+ return (
2129
+ <div>
2130
+ <Toolbar
2131
+ ref={tbRef}
2132
+ width={600}
2133
+ items={[
2134
+ { xtype: 'tbtext', text: '操作' },
2135
+ { xtype: 'button', text: '刷新' },
2136
+ ]}
2137
+ />
2138
+ <div style={{ marginTop: 8 }}>
2139
+ <Button text="添加按钮" onClick={addBtn} />
2140
+ </div>
2141
+ </div>
2142
+ );
2143
+ }
2144
+
2145
+ // ToolTip 动态演示:通过 ref 调用句柄运行时显示/隐藏/改写标题与正文。
2146
+ function ToolTipDynamicDemo() {
2147
+ const tipRef = useRef<ToolTipHandle>(null);
2148
+ return (
2149
+ <div style={{ position: 'relative' }}>
2150
+ <ToolTip
2151
+ ref={tipRef}
2152
+ title="初始标题"
2153
+ html="初始内容"
2154
+ width={200}
2155
+ x={0}
2156
+ y={0}
2157
+ visible={false}
2158
+ />
2159
+ <div style={{ position: 'absolute', top: 50, left: 0, display: 'flex', gap: 8 }}>
2160
+ <Button text="句柄显示" onClick={() => tipRef.current?.show()} />
2161
+ <Button text="句柄隐藏" onClick={() => tipRef.current?.hide()} />
2162
+ <Button text="改标题" onClick={() => tipRef.current?.setTitle('运行时标题')} />
2163
+ <Button text="改内容" onClick={() => tipRef.current?.setBody('运行时改写的提示内容。')} />
2164
+ </div>
2165
+ </div>
2166
+ );
2167
+ }
2168
+
2169
+ // TreeGrid 动态演示:通过 ref 调用句柄运行时增删改节点 / 展开折叠 / 选中。
2170
+ function TreeGridDynamicDemo() {
2171
+ const gridRef = useRef<TreeGridHandle>(null);
2172
+ const initData: TreeGridNode[] = [
2173
+ {
2174
+ id: 't1',
2175
+ task: '项目 A',
2176
+ duration: '5d',
2177
+ user: '张三',
2178
+ expanded: true,
2179
+ children: [
2180
+ { id: 't1-1', task: '子任务 A1', duration: '2d', user: '李四' },
2181
+ { id: 't1-2', task: '子任务 A2', duration: '3d', user: '王五' },
2182
+ ],
2183
+ },
2184
+ { id: 't2', task: '项目 B', duration: '3d', user: '赵六' },
2185
+ ];
2186
+ return (
2187
+ <div>
2188
+ <TreeGrid
2189
+ ref={gridRef}
2190
+ title="动态项目树"
2191
+ width={500}
2192
+ height={260}
2193
+ columns={[
2194
+ { header: 'Task', dataIndex: 'task', width: 230 },
2195
+ { header: 'Duration', dataIndex: 'duration', width: 100, align: 'center' },
2196
+ { header: 'Assigned To', dataIndex: 'user', width: 150 },
2197
+ ]}
2198
+ data={initData}
2199
+ />
2200
+ <div style={{ marginTop: 8, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
2201
+ <Button text="句柄加顶层节点" onClick={() => gridRef.current?.addNode(null, { id: 't3', task: '项目 C', duration: '1d', user: '钱七' })} />
2202
+ <Button text="句柄改 t2" onClick={() => gridRef.current?.updateNode('t2', { task: '项目 B(改)' })} />
2203
+ <Button text="句柄折叠 t1" onClick={() => gridRef.current?.collapse('t1')} />
2204
+ <Button text="句柄选中 t2" onClick={() => gridRef.current?.select('t2')} />
2205
+ <Button text="句柄删 t1" onClick={() => gridRef.current?.removeNode('t1')} />
2206
+ </div>
2207
+ </div>
2208
+ );
2209
+ }
2210
+
2211
+ // MessageBox 动态演示:通过 ref 调用句柄运行时改写标题/消息/图标与 show/hide。
2212
+ function MessageBoxDynamicDemo() {
2213
+ const mbRef = useRef<MessageBoxHandle>(null);
2214
+ return (
2215
+ <div style={{ position: 'relative' }}>
2216
+ <MessageBox
2217
+ ref={mbRef}
2218
+ title="初始标题"
2219
+ msg="初始消息内容"
2220
+ width={400}
2221
+ icon="info"
2222
+ buttons={[{ text: '确定', width: 75 }]}
2223
+ />
2224
+ <div style={{ position: 'absolute', top: 176, left: 0, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
2225
+ <Button text="改标题" onClick={() => mbRef.current?.setTitle('运行时标题')} />
2226
+ <Button text="改消息" onClick={() => mbRef.current?.setMessage('运行时改写的消息内容。')} />
2227
+ <Button text="改图标" onClick={() => mbRef.current?.setIcon('question')} />
2228
+ <Button text="句柄隐藏" onClick={() => mbRef.current?.hide()} />
2229
+ <Button text="句柄显示" onClick={() => mbRef.current?.show()} />
2230
+ </div>
2231
+ </div>
2232
+ );
2233
+ }
2234
+
2235
+ // Viewport 动态演示:通过 ref 调用句柄运行时增删 region / 改尺寸 / 改 region 内容。
2236
+ function ViewportDynamicDemo() {
2237
+ const vpRef = useRef<ViewportHandle>(null);
2238
+ return (
2239
+ <div style={{ position: 'relative', marginTop: 12 }}>
2240
+ <div style={{ marginBottom: 6, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
2241
+ <Button text="句柄加 East" onClick={() => vpRef.current?.addRegion({ region: 'east', width: 120, title: 'East', html: 'East' })} />
2242
+ <Button text="句柄删 East" onClick={() => vpRef.current?.removeRegion('east')} />
2243
+ <Button text="句柄改 North" onClick={() => vpRef.current?.updateRegion('north', { height: 90, title: 'North*' })} />
2244
+ <Button text="句柄改尺寸" onClick={() => vpRef.current?.setSize(600, 380)} />
2245
+ </div>
2246
+ <Viewport
2247
+ ref={vpRef}
2248
+ testId="viewport-dynamic"
2249
+ width={500}
2250
+ height={300}
2251
+ regions={[
2252
+ { region: 'north', height: 60, title: 'North', html: 'North' },
2253
+ { region: 'west', width: 120, title: 'West', html: 'West' },
2254
+ { region: 'center', title: 'Center', html: 'Center' },
2255
+ ]}
2256
+ />
2257
+ </div>
2258
+ );
2259
+ }
2260
+
2261
+ // QuickTip 动态演示:无 target,由 ref 句柄驱动显隐与内容。
2262
+ function QuickTipDynamicDemo() {
2263
+ const qtRef = useRef<QuickTipHandle>(null);
2264
+ return (
2265
+ <div data-testid="quicktip-dynamic-demo" style={{ position: 'relative', marginTop: 12, minHeight: 170 }}>
2266
+ <div style={{ marginBottom: 6, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
2267
+ <Button text="句柄显示" onClick={() => qtRef.current?.show()} />
2268
+ <Button text="句柄隐藏" onClick={() => qtRef.current?.hide()} />
2269
+ <Button text="句柄改标题" onClick={() => qtRef.current?.setTitle('运行时标题')} />
2270
+ <Button text="句柄改文本" onClick={() => qtRef.current?.setText('运行时改写的提示正文。')} />
2271
+ </div>
2272
+ <QuickTip
2273
+ ref={qtRef}
2274
+ testId="quicktip-dynamic"
2275
+ title="初始标题"
2276
+ text="初始正文"
2277
+ width={200}
2278
+ y={90}
2279
+ />
2280
+ </div>
2281
+ );
2282
+ }
2283
+
2284
+ // Menu 动态增删演示:种子 复制/粘贴/分隔/删除;「添加菜单项」追加一个可自删项。
2285
+ function DynamicMenuDemo() {
2286
+ const menuRef = useRef<MenuHandle>(null);
2287
+ const counter = useRef(0);
2288
+ const [open, setOpen] = useState(false);
2289
+ const addItem = () => {
2290
+ const id = `mi-${counter.current++}`;
2291
+ menuRef.current?.add(
2292
+ { text: `新项${counter.current}`, handler: () => menuRef.current?.remove(id) },
2293
+ id,
2294
+ );
2295
+ };
2296
+ return (
2297
+ <div style={{ position: 'relative' }}>
2298
+ <div style={{ display: 'flex', gap: 8, marginBottom: 4 }}>
2299
+ <Button text="打开菜单" onClick={() => setOpen((o) => !o)} />
2300
+ <Button text="添加菜单项" onClick={addItem} />
2301
+ </div>
2302
+ {open && (
2303
+ <div style={{ position: 'absolute', top: 30, left: 0, zIndex: 15000 }}>
2304
+ <Menu
2305
+ ref={menuRef}
2306
+ items={[
2307
+ { text: '复制' },
2308
+ { text: '粘贴' },
2309
+ { separator: true },
2310
+ { text: '删除' },
2311
+ ]}
2312
+ onItemClick={() => setOpen(false)}
2313
+ />
2314
+ </div>
2315
+ )}
2316
+ </div>
2317
+ );
2318
+ }
2319
+
2320
+ // Menu 级联子菜单动态增删演示:根菜单含「文件」项(带子菜单),用 getSubmenu 对子菜单动态增删,
2321
+ // 等价于 Ext 运行时给某菜单项挂载/编辑级联子菜单。submenuAlwaysOpen 让子菜单常开以便演示与测试。
2322
+ function CascadeMenuDemo() {
2323
+ const menuRef = useRef<MenuHandle>(null);
2324
+ return (
2325
+ <div style={{ position: 'relative' }}>
2326
+ <div style={{ display: 'flex', gap: 8, marginBottom: 4 }}>
2327
+ <Button
2328
+ text="添加子菜单项"
2329
+ onClick={() => menuRef.current?.getSubmenu('seed-0')?.add({ text: '最近文件' }, 'sub-recent')}
2330
+ />
2331
+ <Button
2332
+ text="移除子菜单项"
2333
+ onClick={() => menuRef.current?.getSubmenu('seed-0')?.remove('seed-1')}
2334
+ />
2335
+ </div>
2336
+ <div style={{ position: 'absolute', top: 30, left: 0, zIndex: 15000 }}>
2337
+ <Menu
2338
+ ref={menuRef}
2339
+ submenuAlwaysOpen={0}
2340
+ items={[
2341
+ { text: '文件', menu: [{ text: '新建' }, { text: '打开' }] },
2342
+ { text: '编辑' },
2343
+ { separator: true },
2344
+ { text: '帮助' },
2345
+ ]}
2346
+ onItemClick={() => {}}
2347
+ />
2348
+ </div>
2349
+ </div>
2350
+ );
2351
+ }
2352
+
2353
+ // DatePicker 范围选择演示组件:rangeSelect 下点击两日形成闭区间高亮,回显选中区间文本。
2354
+ function DateRangeDemo() {
2355
+ const [range, setRange] = useState<Date[]>([]);
2356
+ const fmt = (d: Date) => `${d.getMonth() + 1}/${d.getDate()}`;
2357
+ const label = range.length === 2 ? `${fmt(range[0])} – ${fmt(range[1])}` : range.length === 1 ? `起点 ${fmt(range[0])}` : '请选择区间';
2358
+ return (
2359
+ <div>
2360
+ <div style={{ marginBottom: 4, fontSize: 12 }}>范围:{label}</div>
2361
+ <DatePicker rangeSelect selectedDates={[]} onRangeSelect={setRange} />
2362
+ </div>
2363
+ );
2364
+ }
2365
+
2366
+ // TabPanel 动态增删演示:种子 2 个普通 tab;「添加页」追加一个可自删(closable)的动态 tab。
2367
+ function DynamicTabDemo() {
2368
+ const tpRef = useRef<TabPanelHandle>(null);
2369
+ const counter = useRef(0);
2370
+ const seed = [
2371
+ { title: '概览', content: <div style={{ padding: 10 }}>概览内容</div> },
2372
+ { title: '详情', content: <div style={{ padding: 10 }}>详情内容</div> },
2373
+ ];
2374
+ const addTab = () => {
2375
+ const n = ++counter.current;
2376
+ const key = tpRef.current?.add(
2377
+ {
2378
+ title: `新页${n}`,
2379
+ closable: true,
2380
+ content: <div style={{ padding: 10 }}>新页 {n} 内容</div>,
2381
+ },
2382
+ undefined,
2383
+ true,
2384
+ );
2385
+ // 自删:点击 tab 上的 close 由 TabPanel 内部处理;此处额外演示程序化 remove
2386
+ void key;
2387
+ };
2388
+ return (
2389
+ <div>
2390
+ <TabPanel
2391
+ ref={tpRef}
2392
+ width={420}
2393
+ height={140}
2394
+ items={seed}
2395
+ />
2396
+ <div style={{ marginTop: 8 }}>
2397
+ <Button text="添加页" onClick={addTab} />
2398
+ </div>
2399
+ </div>
2400
+ );
2401
+ }
2402
+
2403
+ // ListView 动态增删演示:种子 3 行;「添加一行」追加新记录,「删除选中行」移除当前选中记录。
2404
+ function DynamicListViewDemo() {
2405
+ const lvRef = useRef<ListViewHandle>(null);
2406
+ const nRef = useRef(0);
2407
+ const addRow = () => {
2408
+ const n = ++nRef.current;
2409
+ lvRef.current?.add({
2410
+ name: `new-${n}.doc`,
2411
+ size: String(100 * n),
2412
+ modified: '08-18 10:00',
2413
+ });
2414
+ };
2415
+ const delSel = () => {
2416
+ const sel = lvRef.current?.getSelected() ?? [];
2417
+ if (sel.length) lvRef.current?.removeByIndex(sel[0]);
2418
+ };
2419
+ return (
2420
+ <div>
2421
+ <ListView
2422
+ ref={lvRef}
2423
+ width={320}
2424
+ height={200}
2425
+ multiSelect
2426
+ value={[0]}
2427
+ columns={[
2428
+ { header: 'Name', dataIndex: 'name', width: 0.4 },
2429
+ { header: 'Size', dataIndex: 'size', width: 0.2, align: 'right' },
2430
+ { header: 'Modified', dataIndex: 'modified', width: 0.4 },
2431
+ ]}
2432
+ data={[
2433
+ { name: 'a.txt', size: '12', modified: '08-18 09:00' },
2434
+ { name: 'b.txt', size: '34', modified: '08-18 09:10' },
2435
+ { name: 'c.txt', size: '56', modified: '08-18 09:20' },
2436
+ ]}
2437
+ />
2438
+ <div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
2439
+ <Button text="添加一行" onClick={addRow} />
2440
+ <Button text="删除选中行" onClick={delSel} />
2441
+ </div>
2442
+ </div>
2443
+ );
2444
+ }
2445
+
2446
+ // ColorMenu 演示组件:触发按钮 + 受控 ColorMenu 浮层(对齐 Ext.Button + menu: ColorMenu)。
2447
+ function ColorMenuDemo() {
2448
+ const [open, setOpen] = useState(false);
2449
+ const anchorRef = useRef<HTMLDivElement>(null);
2450
+ return (
2451
+ <div ref={anchorRef} style={{ display: 'inline-block' }}>
2452
+ <Button text="选择颜色" onClick={() => setOpen((o) => !o)} />
2453
+ <ColorMenu open={open} anchorRef={anchorRef} onClose={() => setOpen(false)} onSelect={() => {}} />
2454
+ </div>
2455
+ );
2456
+ }
2457
+
2458
+ // ColorMenu 动态演示:通过 ref 调用句柄运行时增删改色板 / 选中 / 开合浮层。
2459
+ // Editor 动态演示:通过 ref 调用句柄运行时 startEdit / setValue / completeEdit / cancelEdit。
2460
+ function EditorDynamicDemo() {
2461
+ const ref = useRef<EditorHandle>(null);
2462
+ return (
2463
+ <div data-testid="editor-dynamic" style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start', position: 'relative', width: 300, height: 50 }}>
2464
+ <Editor ref={ref} value="Base" boundText="Base" width={180} height={22} editing={false} allowBlur={false} />
2465
+ <Button text="句柄开始编辑" onClick={() => ref.current?.startEdit()} />
2466
+ <Button text="句柄设值" onClick={() => ref.current?.setValue('Dynamic')} />
2467
+ <Button text="句柄完成" onClick={() => ref.current?.completeEdit()} />
2468
+ <Button text="句柄取消" onClick={() => ref.current?.cancelEdit()} />
2469
+ </div>
2470
+ );
2471
+ }
2472
+
2473
+ // EditorGrid 动态演示:通过 ref 调用句柄运行时增删改数据源 / 按坐标启动编辑。
2474
+ function EditorGridDynamicDemo() {
2475
+ const ref = useRef<EditorGridHandle>(null);
2476
+ return (
2477
+ <div data-testid="editorgrid-dynamic" style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start', width: 500 }}>
2478
+ <EditorGrid
2479
+ ref={ref}
2480
+ title="动态用户列表"
2481
+ width={500}
2482
+ columns={[
2483
+ { header: '姓名', dataIndex: 'name', width: 100, editor: true },
2484
+ { header: '邮箱', dataIndex: 'email', width: 180 },
2485
+ { header: '电话', dataIndex: 'phone', width: 140 },
2486
+ { header: '年龄', dataIndex: 'age', width: 80, align: 'right', editor: { type: 'number' } },
2487
+ ]}
2488
+ data={[
2489
+ { name: '张三', email: 'zhangsan@example.com', phone: '13800138000', age: 28 },
2490
+ { name: '李四', email: 'lisi@example.com', phone: '13900139000', age: 32 },
2491
+ { name: '王五', email: 'wangwu@example.com', phone: '13700137000', age: 45 },
2492
+ ]}
2493
+ />
2494
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
2495
+ <Button text="句柄加行" onClick={() => ref.current?.addRecord({ name: '赵六', email: 'zhao@example.com', phone: '13600136000', age: 20 })} />
2496
+ <Button text="句柄删首行" onClick={() => ref.current?.removeRecord(0)} />
2497
+ <Button text="句柄改首行名" onClick={() => ref.current?.updateRecord(0, { name: '改后张三' })} />
2498
+ <Button text="句柄编辑首格" onClick={() => ref.current?.startEditAt(0, 'name')} />
2499
+ <Button text="句柄提交编辑" onClick={() => ref.current?.commitEdit('编辑后姓名')} />
2500
+ <Button text="句柄取消编辑" onClick={() => ref.current?.cancelEdit()} />
2501
+ </div>
2502
+ </div>
2503
+ );
2504
+ }
2505
+
2506
+ // HtmlEditor 动态演示:通过 ref 调用句柄运行时写值 / 切源码 / 禁用。
2507
+ function HtmlEditorDynamicDemo() {
2508
+ const ref = useRef<HtmlEditorHandle>(null);
2509
+ return (
2510
+ <div data-testid="htmleditor-dynamic" style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start' }}>
2511
+ <HtmlEditor ref={ref} testId="htmleditor-dyn" width={400} height={160} value="<p>init</p>" />
2512
+ <Button text="句柄设值" onClick={() => ref.current?.setValue('<p>dynamic</p>')} />
2513
+ <Button text="句柄切源码" onClick={() => ref.current?.toggleSource()} />
2514
+ <Button text="句柄禁用" onClick={() => ref.current?.setDisabled(true)} />
2515
+ </div>
2516
+ );
2517
+ }
2518
+
2519
+ function ColorMenuDynamicDemo() {
2520
+ const ref = useRef<ColorMenuHandle>(null);
2521
+ const anchorRef = useRef<HTMLDivElement>(null);
2522
+ return (
2523
+ <div data-testid="colormenu-dynamic" style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', position: 'relative' }}>
2524
+ <div ref={anchorRef} style={{ display: 'inline-block' }}>
2525
+ <Button
2526
+ text="切换菜单"
2527
+ onClick={() => {
2528
+ if (ref.current?.isOpen()) ref.current.close();
2529
+ else ref.current?.open();
2530
+ }}
2531
+ />
2532
+ </div>
2533
+ <ColorMenu ref={ref} anchorRef={anchorRef} />
2534
+ <Button text="句柄加色" onClick={() => ref.current?.addColor('#123456')} />
2535
+ <Button text="句柄删蓝" onClick={() => ref.current?.removeColor('#123456')} />
2536
+ <Button text="句柄选绿" onClick={() => ref.current?.select('00FF00')} />
2537
+ </div>
2538
+ );
2539
+ }
2540
+
2541
+ // StatusBar 动态状态演示:通过 ref 调用 setStatus / showBusy / clearStatus。
2542
+ function DynamicStatusBar() {
2543
+ const ref = useRef<StatusBarHandle>(null);
2544
+ return (
2545
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center', width: '100%' }}>
2546
+ <StatusBar
2547
+ ref={ref}
2548
+ testId="statusbar-dynamic"
2549
+ width={400}
2550
+ text="就绪"
2551
+ defaultText="就绪"
2552
+ iconCls="x-status-valid"
2553
+ items={[{ text: '动作' }]}
2554
+ />
2555
+ <button type="button" onClick={() => ref.current?.showBusy({ text: '处理中...' })}>
2556
+ 忙碌
2557
+ </button>
2558
+ <button type="button" onClick={() => ref.current?.setStatus({ text: '完成', iconCls: 'x-status-valid' })}>
2559
+ 完成
2560
+ </button>
2561
+ <button type="button" onClick={() => ref.current?.clearStatus()}>
2562
+ 清除
2563
+ </button>
2564
+ </div>
2565
+ );
2566
+ }
2567
+
2568
+ /**
2569
+ * 保真关键:Ext 在启动时(EventManager.initExtCss)向 <body> 注入浏览器嗅探类
2570
+ * (ext-webkit / ext-chrome / ext-mac / ext-linux 等),并向 <html> 注入 ext-strict。
2571
+ * 大量 CSS 规则以这些类为前缀(如 `.ext-webkit .x-small-editor .x-form-text{padding-top:3px}`),
2572
+ * 缺失会导致 toolbar 内表单字段等像素错位。React 端不加载 Ext 运行时,故在此等价的复刻注入。
2573
+ */
2574
+ function applyExtBrowserClasses() {
2575
+ const t = navigator.userAgent.toLowerCase();
2576
+ const isIE = /msie/.test(t) && !/opera/.test(t);
2577
+ const isIE7 = isIE && /msie 7/.test(t);
2578
+ const isIE8 = isIE && /msie 8/.test(t);
2579
+ const isGecko = /gecko/.test(t) && !/webkit/.test(t);
2580
+ const isOpera = /opera/.test(t);
2581
+ const isWebKit = /webkit/.test(t);
2582
+ const isChrome = /chrome/.test(t);
2583
+ const isSafari = /safari/.test(t) && !/chrome/.test(t);
2584
+ const isMac = /macintosh|mac os x/.test(t);
2585
+ const isLinux = /linux/.test(t);
2586
+ const isStrict = document.compatMode === 'CSS1Compat';
2587
+
2588
+ const bodyCls: string[] = [];
2589
+ if (isIE) bodyCls.push('ext-ie', isIE7 ? 'ext-ie7' : isIE8 ? 'ext-ie8' : 'ext-ie6');
2590
+ else if (isGecko) bodyCls.push('ext-gecko', /rv:1\.8/.test(t) ? 'ext-gecko2' : 'ext-gecko3');
2591
+ else if (isOpera) bodyCls.push('ext-opera');
2592
+ else if (isWebKit) bodyCls.push('ext-webkit');
2593
+
2594
+ if (isSafari) bodyCls.push('ext-safari', /version\/2/.test(t) ? 'ext-safari2' : /version\/3/.test(t) ? 'ext-safari3' : 'ext-safari4');
2595
+ else if (isChrome) bodyCls.push('ext-chrome');
2596
+
2597
+ if (isMac) bodyCls.push('ext-mac');
2598
+ if (isLinux) bodyCls.push('ext-linux');
2599
+
2600
+ if (bodyCls.length) document.body.classList.add(...bodyCls);
2601
+ if (isStrict) document.documentElement.classList.add('ext-strict');
2602
+ }
2603
+
2604
+ applyExtBrowserClasses();
2605
+
2606
+ // Grid 列动态增删演示:种子 3 列 + 「添加列」「移除「部门」列」。getColumns().length 断言列数变化。
2607
+ function GridColDynamicDemo() {
2608
+ const gridRef = useRef<GridHandle>(null);
2609
+ const data = [
2610
+ { name: '张三', email: 'zhangsan@example.com', dept: '研发' },
2611
+ { name: '李四', email: 'lisi@example.com', dept: '市场' },
2612
+ { name: '王五', email: 'wangwu@example.com', dept: '研发' },
2613
+ ];
2614
+ const addColumn = () => {
2615
+ gridRef.current?.addColumn(
2616
+ {
2617
+ header: '编号',
2618
+ dataIndex: '__seq__',
2619
+ width: 70,
2620
+ align: 'center',
2621
+ renderer: (_v, row) => `#${data.indexOf(row as (typeof data)[number]) + 1}`,
2622
+ },
2623
+ 'seq',
2624
+ );
2625
+ };
2626
+ const removeDept = () => gridRef.current?.removeColumn('dept');
2627
+ return (
2628
+ <div>
2629
+ <Grid
2630
+ ref={gridRef}
2631
+ title="可增删列的列表"
2632
+ width={480}
2633
+ columns={[
2634
+ { header: '姓名', dataIndex: 'name', width: 120 },
2635
+ { header: '邮箱', dataIndex: 'email', width: 200 },
2636
+ { header: '部门', dataIndex: 'dept', width: 160 },
2637
+ ]}
2638
+ data={data}
2639
+ />
2640
+ <div style={{ marginTop: 8 }}>
2641
+ <Button text="添加「编号」列" onClick={addColumn} />
2642
+ <span style={{ width: 8, display: 'inline-block' }} />
2643
+ <Button text="移除「部门」列" onClick={removeDept} />
2644
+ </div>
2645
+ </div>
2646
+ );
2647
+ }
2648
+
2649
+ createRoot(document.getElementById('root')!).render(<App />);