@baishuyun/ai-design 1.0.1 → 1.0.3

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 (111) hide show
  1. package/.turbo/turbo-build.log +16 -14
  2. package/CHANGELOG.md +15 -0
  3. package/dist/ai-design.js +6255 -5314
  4. package/dist/ai-design.js.map +1 -1
  5. package/dist/ai-design.umd.cjs +67 -67
  6. package/dist/ai-design.umd.cjs.map +1 -1
  7. package/dist/src/components/agent-scroll-selector.d.ts +2 -1
  8. package/dist/src/components/agent-scroll-selector.d.ts.map +1 -1
  9. package/dist/src/components/bs-entry-select/app-group-folder.d.ts +6 -0
  10. package/dist/src/components/bs-entry-select/app-group-folder.d.ts.map +1 -0
  11. package/dist/src/components/bs-entry-select/bs-entry-select.d.ts +4 -0
  12. package/dist/src/components/bs-entry-select/bs-entry-select.d.ts.map +1 -0
  13. package/dist/src/components/bs-entry-select/build-tree.d.ts +9 -0
  14. package/dist/src/components/bs-entry-select/build-tree.d.ts.map +1 -0
  15. package/dist/src/components/bs-entry-select/entry-item.d.ts +8 -0
  16. package/dist/src/components/bs-entry-select/entry-item.d.ts.map +1 -0
  17. package/dist/src/components/bs-entry-select/entry-popover.d.ts +17 -0
  18. package/dist/src/components/bs-entry-select/entry-popover.d.ts.map +1 -0
  19. package/dist/src/components/bs-entry-select/index.d.ts +3 -0
  20. package/dist/src/components/bs-entry-select/index.d.ts.map +1 -0
  21. package/dist/src/components/bs-entry-select/other-app-section.d.ts +11 -0
  22. package/dist/src/components/bs-entry-select/other-app-section.d.ts.map +1 -0
  23. package/dist/src/components/bs-entry-select/scroll-slider.d.ts +10 -0
  24. package/dist/src/components/bs-entry-select/scroll-slider.d.ts.map +1 -0
  25. package/dist/src/components/bs-entry-select/search-input.d.ts +5 -0
  26. package/dist/src/components/bs-entry-select/search-input.d.ts.map +1 -0
  27. package/dist/src/components/bs-entry-select/tree-item-node.d.ts +9 -0
  28. package/dist/src/components/bs-entry-select/tree-item-node.d.ts.map +1 -0
  29. package/dist/src/components/{bs-entry-select.d.ts → bs-entry-select/types.d.ts} +12 -2
  30. package/dist/src/components/bs-entry-select/types.d.ts.map +1 -0
  31. package/dist/src/components/bs-icons.d.ts +2 -0
  32. package/dist/src/components/bs-icons.d.ts.map +1 -1
  33. package/dist/src/components/collapsible.d.ts +6 -0
  34. package/dist/src/components/collapsible.d.ts.map +1 -0
  35. package/dist/src/components/field-checker-list-msg.d.ts +19 -0
  36. package/dist/src/components/field-checker-list-msg.d.ts.map +1 -0
  37. package/dist/src/components/field-checker.d.ts +19 -0
  38. package/dist/src/components/field-checker.d.ts.map +1 -0
  39. package/dist/src/components/field-icon.d.ts +5 -0
  40. package/dist/src/components/field-icon.d.ts.map +1 -0
  41. package/dist/src/components/field-value-checker.d.ts +16 -0
  42. package/dist/src/components/field-value-checker.d.ts.map +1 -0
  43. package/dist/src/components/legacy-slot.d.ts.map +1 -1
  44. package/dist/src/components/office-container.d.ts +5 -0
  45. package/dist/src/components/office-container.d.ts.map +1 -1
  46. package/dist/src/components/subform-fields-value-checker.d.ts +22 -0
  47. package/dist/src/components/subform-fields-value-checker.d.ts.map +1 -0
  48. package/dist/src/const/ui.d.ts +3 -0
  49. package/dist/src/const/ui.d.ts.map +1 -1
  50. package/dist/src/hooks/use-column-appear.d.ts +22 -0
  51. package/dist/src/hooks/use-column-appear.d.ts.map +1 -0
  52. package/dist/src/hooks/use-resizable-width.d.ts +3 -1
  53. package/dist/src/hooks/use-resizable-width.d.ts.map +1 -1
  54. package/dist/src/index.d.ts +11 -2
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/lib/utils.d.ts +3 -0
  57. package/dist/src/lib/utils.d.ts.map +1 -1
  58. package/dist/src/stories/BsEntrySelect.stories.d.ts +4 -0
  59. package/dist/src/stories/BsEntrySelect.stories.d.ts.map +1 -1
  60. package/dist/src/stories/OfficeContainer.stories.d.ts +5 -0
  61. package/dist/src/stories/OfficeContainer.stories.d.ts.map +1 -1
  62. package/dist/src/stories/collapsible.stories.d.ts +8 -0
  63. package/dist/src/stories/collapsible.stories.d.ts.map +1 -0
  64. package/dist/src/stories/field-checker-list-msg.stories.d.ts +11 -0
  65. package/dist/src/stories/field-checker-list-msg.stories.d.ts.map +1 -0
  66. package/dist/src/stories/field-checker.stories.d.ts +13 -0
  67. package/dist/src/stories/field-checker.stories.d.ts.map +1 -0
  68. package/dist/src/stories/field-icon.stories.d.ts +15 -0
  69. package/dist/src/stories/field-icon.stories.d.ts.map +1 -0
  70. package/dist/src/stories/field-value-checker.stories.d.ts +13 -0
  71. package/dist/src/stories/field-value-checker.stories.d.ts.map +1 -0
  72. package/dist/src/stories/subform-fields-value-checker.stories.d.ts +9 -0
  73. package/dist/src/stories/subform-fields-value-checker.stories.d.ts.map +1 -0
  74. package/package.json +7 -5
  75. package/src/components/agent-scroll-selector.tsx +10 -3
  76. package/src/components/bs-entry-select/app-group-folder.tsx +61 -0
  77. package/src/components/bs-entry-select/bs-entry-select.tsx +271 -0
  78. package/src/components/bs-entry-select/build-tree.ts +144 -0
  79. package/src/components/bs-entry-select/entry-item.tsx +43 -0
  80. package/src/components/bs-entry-select/entry-popover.tsx +97 -0
  81. package/src/components/bs-entry-select/index.ts +2 -0
  82. package/src/components/bs-entry-select/other-app-section.tsx +96 -0
  83. package/src/components/bs-entry-select/scroll-slider.tsx +455 -0
  84. package/src/components/bs-entry-select/search-input.tsx +32 -0
  85. package/src/components/bs-entry-select/tree-item-node.tsx +84 -0
  86. package/src/components/bs-entry-select/types.ts +30 -0
  87. package/src/components/bs-icons.tsx +36 -1
  88. package/src/components/card.tsx +1 -1
  89. package/src/components/collapsible.tsx +9 -0
  90. package/src/components/field-checker-list-msg.tsx +146 -0
  91. package/src/components/field-checker.tsx +182 -0
  92. package/src/components/field-icon.tsx +13 -0
  93. package/src/components/field-value-checker.tsx +118 -0
  94. package/src/components/legacy-slot.tsx +7 -0
  95. package/src/components/office-container.tsx +111 -28
  96. package/src/components/subform-fields-value-checker.tsx +185 -0
  97. package/src/const/ui.ts +67 -0
  98. package/src/hooks/use-column-appear.ts +78 -0
  99. package/src/hooks/use-resizable-width.ts +10 -2
  100. package/src/index.ts +11 -2
  101. package/src/lib/utils.ts +67 -0
  102. package/src/stories/BsEntrySelect.stories.tsx +45 -2
  103. package/src/stories/OfficeContainer.stories.tsx +150 -1
  104. package/src/stories/collapsible.stories.tsx +47 -0
  105. package/src/stories/field-checker-list-msg.stories.tsx +95 -0
  106. package/src/stories/field-checker.stories.tsx +117 -0
  107. package/src/stories/field-icon.stories.tsx +70 -0
  108. package/src/stories/field-value-checker.stories.tsx +113 -0
  109. package/src/stories/subform-fields-value-checker.stories.tsx +98 -0
  110. package/dist/src/components/bs-entry-select.d.ts.map +0 -1
  111. package/src/components/bs-entry-select.tsx +0 -724
@@ -1,724 +0,0 @@
1
- import {
2
- forwardRef,
3
- useState,
4
- useRef,
5
- useEffect,
6
- useMemo,
7
- useCallback,
8
- } from 'react';
9
- import { createPortal } from 'react-dom';
10
- import { cn } from '../lib/utils';
11
- import { transCls } from '../const/ui';
12
- import type { IAppDetail, IEntryDetail } from '@baishuyun/types';
13
- import { SolidArrowDownIcon as ChevronDownIcon } from './bs-icons';
14
- const ChevronRightIcon = ({ size }: { size?: number }) => (
15
- <ChevronDownIcon className="-rotate-90" size={size} />
16
- )
17
-
18
- export interface BsEntrySelectChangeResult {
19
- entry: IEntryDetail;
20
- appId: string;
21
- section: 'current' | 'other';
22
- }
23
-
24
- export interface BsEntrySelectProps {
25
- appDetails: Record<string, IAppDetail>;
26
- currentAppId?: string;
27
- value?: string;
28
- defaultValue?: string;
29
- onChange?: (result: BsEntrySelectChangeResult | null) => void;
30
- placeholder?: string;
31
- disabled?: boolean;
32
- className?: string;
33
- }
34
-
35
- interface EntryTreeItem {
36
- id: string;
37
- name: string;
38
- icon: number;
39
- type: string;
40
- children: EntryTreeItem[];
41
- depth: number;
42
- isSelectable: boolean;
43
- entry: IEntryDetail | null;
44
- }
45
-
46
- const DEPTH_PADDING: Record<number, string> = {
47
- 0: 'pl-[10px]',
48
- 1: 'pl-[32px]',
49
- 2: 'pl-[32px]',
50
- 3: 'pl-[54px]',
51
- 4: 'pl-[78px]',
52
- 5: 'pl-[102px]',
53
- };
54
-
55
- function getDepthPadding(depth: number): string {
56
- return DEPTH_PADDING[Math.min(depth, 5)] ?? DEPTH_PADDING[5];
57
- }
58
-
59
- function getEntryIconClass(entry: IEntryDetail): string {
60
- const isNew = entry.icon >= 25;
61
- const newStr = isNew ? '-new' : '';
62
- return `node-icon icon-bsy-form${newStr} icon${newStr}-bsy-form-${entry.icon}`;
63
- }
64
-
65
- function buildEntryTreeFromAliasMap(
66
- appDetail: IAppDetail,
67
- searchQuery: string,
68
- ): EntryTreeItem[] {
69
- const { entryIdAliasMap, entryMap } = appDetail;
70
- const aliasKeys = Object.keys(entryIdAliasMap);
71
- if (aliasKeys.length === 0) return [];
72
-
73
- const childrenByParent = new Map<string, string[]>();
74
- for (const id of aliasKeys) {
75
- const aliasItem = entryIdAliasMap[id];
76
- const parentKey = aliasItem.parent || '0';
77
- if (!childrenByParent.has(parentKey)) {
78
- childrenByParent.set(parentKey, []);
79
- }
80
- childrenByParent.get(parentKey)!.push(id);
81
- }
82
-
83
- // Root parents: any parent value that is not itself a key in entryIdAliasMap.
84
- // Also include the sentinel '0' for entries with empty/falsy parent.
85
- const allParentValues = new Set(childrenByParent.keys());
86
- const rootParents = Array.from(allParentValues).filter(
87
- (p) => p === '0' || !entryIdAliasMap[p],
88
- );
89
-
90
- const lowerQuery = searchQuery.toLowerCase().trim();
91
-
92
- function buildRecursive(parentId: string, depth: number): EntryTreeItem[] {
93
- const childIds = childrenByParent.get(parentId) || [];
94
- const nodes: EntryTreeItem[] = [];
95
-
96
- for (const id of childIds) {
97
- const aliasItem = entryIdAliasMap[id];
98
- const entryDetail = entryMap[id] || null;
99
- const name = entryDetail?.name || id;
100
- const icon = entryDetail?.icon || 0;
101
- const isGroup = aliasItem.type === 'group';
102
-
103
- const children = buildRecursive(id, depth + 1);
104
- const selfMatch = !lowerQuery || name.toLowerCase().includes(lowerQuery);
105
-
106
- if (isGroup) {
107
- if (selfMatch || children.length > 0) {
108
- nodes.push({
109
- id,
110
- name,
111
- icon,
112
- type: 'group',
113
- children,
114
- depth,
115
- isSelectable: false,
116
- entry: entryDetail,
117
- });
118
- }
119
- } else {
120
- if (selfMatch) {
121
- nodes.push({
122
- id,
123
- name,
124
- icon,
125
- type: aliasItem.type,
126
- children: [],
127
- depth,
128
- isSelectable: true,
129
- entry: entryDetail,
130
- });
131
- }
132
- }
133
- }
134
-
135
- return nodes;
136
- }
137
-
138
- // Build from each root parent, all at depth 0
139
- const result: EntryTreeItem[] = [];
140
- for (const rootId of rootParents) {
141
- result.push(...buildRecursive(rootId, 0));
142
- }
143
- return result;
144
- }
145
-
146
- function findSelected(
147
- appDetails: Record<string, IAppDetail>,
148
- entryId: string,
149
- currentAppId?: string,
150
- ): BsEntrySelectChangeResult | null {
151
- for (const [appId, detail] of Object.entries(appDetails)) {
152
- if (detail.entryMap[entryId]) {
153
- return {
154
- entry: detail.entryMap[entryId],
155
- appId,
156
- section: appId === currentAppId ? 'current' : 'other',
157
- };
158
- }
159
- }
160
- return null;
161
- }
162
-
163
- function SearchInput({
164
- value,
165
- onChange,
166
- }: {
167
- value: string;
168
- onChange: (v: string) => void;
169
- }) {
170
- return (
171
- <div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
172
- <svg
173
- width="16"
174
- height="16"
175
- viewBox="0 0 16 16"
176
- fill="none"
177
- stroke="currentColor"
178
- strokeWidth="1.5"
179
- strokeLinecap="round"
180
- className="text-[#999999] shrink-0"
181
- >
182
- <circle cx="7" cy="7" r="4.5" />
183
- <path d="M10.5 10.5L14 14" />
184
- </svg>
185
- <input
186
- type="text"
187
- value={value}
188
- onChange={(e) => onChange(e.target.value)}
189
- placeholder="输入表单名称来搜索"
190
- className="flex-1 outline-none text-[14px] placeholder:text-[#999999] bg-transparent min-w-0 text-[#030303]"
191
- />
192
- </div>
193
- );
194
- }
195
-
196
- function EntryItem({
197
- item,
198
- depth,
199
- isSelected,
200
- onClick,
201
- }: {
202
- item: EntryTreeItem;
203
- depth: number;
204
- isSelected: boolean;
205
- onClick: () => void;
206
- }) {
207
- return (
208
- <div
209
- className={cn(
210
- 'flex items-center justify-between h-[34px] px-[10px] cursor-pointer rounded-[4px]',
211
- getDepthPadding(depth),
212
- 'hover:bg-[#EFF0F6]',
213
- isSelected && 'bg-[#EFF0F6]',
214
- transCls,
215
- )}
216
- onClick={onClick}
217
- >
218
- <span className="flex items-center gap-[6px] min-w-0 flex-1">
219
- <i
220
- className={
221
- item.entry
222
- ? getEntryIconClass(item.entry)
223
- : 'node-icon icon-bsy-form icon-new-bsy-form-25'
224
- }
225
- style={{ fontSize: 16, lineHeight: 1 }}
226
- />
227
- <span className="text-[14px] text-[#030303] truncate">
228
- {item.name}
229
- </span>
230
- </span>
231
- </div>
232
- );
233
- }
234
-
235
- function TreeItemNode({
236
- item,
237
- selectedEntryId,
238
- appId,
239
- section,
240
- onSelect,
241
- }: {
242
- item: EntryTreeItem;
243
- selectedEntryId: string | undefined;
244
- appId: string;
245
- section: 'current' | 'other';
246
- onSelect: (result: BsEntrySelectChangeResult) => void;
247
- }) {
248
- const [expanded, setExpanded] = useState(true);
249
-
250
- if (item.type === 'group') {
251
- return (
252
- <div>
253
- <div
254
- className={cn(
255
- 'flex items-center gap-[6px] h-[34px] px-[10px] cursor-pointer rounded-[4px]',
256
- getDepthPadding(item.depth),
257
- 'hover:bg-[#EFF0F6]',
258
- transCls,
259
- )}
260
- onClick={() => setExpanded(!expanded)}
261
- >
262
- <span
263
- className={cn(
264
- 'shrink-0 flex items-center justify-center w-[16px] h-[16px]',
265
- transCls,
266
- expanded && 'rotate-90',
267
- )}
268
- >
269
- <ChevronRightIcon size={16} />
270
- </span>
271
- <i
272
- className="icon-bsy-folder-close"
273
- style={{ fontSize: 16, lineHeight: 1 }}
274
- />
275
- <span className="text-[14px] text-[#030303] truncate">
276
- {item.name}
277
- </span>
278
- </div>
279
- {expanded &&
280
- item.children.map((child) => (
281
- <TreeItemNode
282
- key={child.id}
283
- item={child}
284
- selectedEntryId={selectedEntryId}
285
- appId={appId}
286
- section={section}
287
- onSelect={onSelect}
288
- />
289
- ))}
290
- </div>
291
- );
292
- }
293
-
294
- return (
295
- <EntryItem
296
- item={item}
297
- depth={item.depth}
298
- isSelected={selectedEntryId === item.id}
299
- onClick={() => {
300
- if (item.entry) {
301
- onSelect({ entry: item.entry, appId, section });
302
- }
303
- }}
304
- />
305
- );
306
- }
307
-
308
- function AppGroupFolder({
309
- label,
310
- searchQuery,
311
- children,
312
- }: {
313
- label: string;
314
- searchQuery: string;
315
- children: React.ReactNode;
316
- }) {
317
- const [expanded, setExpanded] = useState(true);
318
- const hasSearch = searchQuery.trim().length > 0;
319
-
320
- useEffect(() => {
321
- if (hasSearch) {
322
- setExpanded(true);
323
- }
324
- }, [hasSearch]);
325
-
326
- return (
327
- <div>
328
- <div
329
- className={cn(
330
- 'flex items-center gap-[6px] h-[32px] cursor-pointer rounded-[4px] pr-[10px]',
331
- 'hover:bg-[#EFF0F6]',
332
- 'border-b border-[#E0E0E0]',
333
- 'rounded-b-none',
334
- transCls,
335
- )}
336
- style={{ paddingLeft: 10, paddingTop: 9 }}
337
- onClick={() => setExpanded(!expanded)}
338
- >
339
- <span
340
- className={cn(
341
- 'shrink-0 flex items-center justify-center w-[16px] h-[16px]',
342
- transCls,
343
- expanded && 'rotate-90',
344
- )}
345
- >
346
- <ChevronRightIcon size={16} />
347
- </span>
348
- <i
349
- className="icon-bsy-folder-close"
350
- style={{ fontSize: 16, lineHeight: 1 }}
351
- />
352
- <span className="text-[12px] text-[#666666]">{label}</span>
353
- </div>
354
- {expanded && (
355
- <div className="flex flex-col">{children}</div>
356
- )}
357
- </div>
358
- );
359
- }
360
-
361
- function OtherAppSection({
362
- appId,
363
- appDetail,
364
- searchQuery,
365
- selectedEntryId,
366
- section,
367
- onSelect,
368
- }: {
369
- appId: string;
370
- appDetail: IAppDetail;
371
- searchQuery: string;
372
- selectedEntryId: string | undefined;
373
- section: 'current' | 'other';
374
- onSelect: (result: BsEntrySelectChangeResult) => void;
375
- }) {
376
- const [expanded, setExpanded] = useState(true);
377
- const hasSearch = searchQuery.trim().length > 0;
378
-
379
- useEffect(() => {
380
- if (hasSearch) {
381
- setExpanded(true);
382
- }
383
- }, [hasSearch]);
384
-
385
- const tree = useMemo(() => {
386
- return buildEntryTreeFromAliasMap(appDetail, searchQuery);
387
- }, [appDetail, searchQuery]);
388
-
389
- return (
390
- <div>
391
- <div
392
- className={cn(
393
- 'flex items-center gap-[6px] h-[32px] cursor-pointer rounded-[4px] pr-[10px]',
394
- 'hover:bg-[#EFF0F6]',
395
- transCls,
396
- )}
397
- style={{ paddingLeft: 15 }}
398
- onClick={() => setExpanded(!expanded)}
399
- >
400
- <span
401
- className={cn(
402
- 'shrink-0 flex items-center justify-center w-[18px] h-[18px]',
403
- transCls,
404
- expanded && 'rotate-90',
405
- )}
406
- >
407
- <ChevronRightIcon size={18} />
408
- </span>
409
- <i
410
- className="node-icon icon-bsy-form icon-new-bsy-form-0"
411
- style={{ fontSize: 16, lineHeight: 1 }}
412
- />
413
- <span className="text-[14px] text-[#030303] truncate">
414
- {appDetail.name}
415
- </span>
416
- </div>
417
- {expanded && tree.length > 0 && (
418
- <div className="flex flex-col" style={{ paddingLeft: 15 }}>
419
- {tree.map((node) => (
420
- <TreeItemNode
421
- key={node.id}
422
- item={node}
423
- selectedEntryId={selectedEntryId}
424
- appId={appId}
425
- section={section}
426
- onSelect={onSelect}
427
- />
428
- ))}
429
- </div>
430
- )}
431
- {expanded && tree.length === 0 && hasSearch && (
432
- <div className="text-[12px] text-[#999999] py-[8px] pl-[32px]">
433
- 无匹配结果
434
- </div>
435
- )}
436
- {expanded && tree.length === 0 && !hasSearch && (
437
- <div className="text-[12px] text-[#999999] py-[8px] pl-[32px]">
438
- 暂无表单
439
- </div>
440
- )}
441
- </div>
442
- );
443
- }
444
-
445
- export const BsEntrySelect = forwardRef<HTMLDivElement, BsEntrySelectProps>(
446
- (
447
- {
448
- appDetails,
449
- currentAppId,
450
- value: controlledValue,
451
- defaultValue,
452
- onChange,
453
- placeholder = '请选择',
454
- disabled = false,
455
- className,
456
- },
457
- ref,
458
- ) => {
459
- const [isOpen, setIsOpen] = useState(false);
460
- const [searchQuery, setSearchQuery] = useState('');
461
- const [internalValue, setInternalValue] = useState<string | undefined>(
462
- defaultValue,
463
- );
464
- const triggerRef = useRef<HTMLDivElement>(null);
465
- const popoverRef = useRef<HTMLDivElement>(null);
466
- const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
467
- const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
468
-
469
- const selectedEntryId = controlledValue ?? internalValue;
470
-
471
- const selectedResult = useMemo(() => {
472
- if (!selectedEntryId) return null;
473
- return findSelected(appDetails, selectedEntryId, currentAppId);
474
- }, [appDetails, selectedEntryId, currentAppId]);
475
-
476
- const otherAppIds = useMemo(() => {
477
- return Object.keys(appDetails).filter((id) => id !== currentAppId);
478
- }, [appDetails, currentAppId]);
479
-
480
- const updatePopoverPosition = useCallback(() => {
481
- if (!triggerRef.current) return;
482
- const rect = triggerRef.current.getBoundingClientRect();
483
- setPopoverStyle({
484
- position: 'fixed',
485
- top: rect.bottom + 4,
486
- left: rect.left,
487
- minWidth: Math.max(rect.width, 240),
488
- zIndex: 9999,
489
- });
490
- }, []);
491
-
492
- useEffect(() => {
493
- const triggerEl = triggerRef.current;
494
- if (!triggerEl) return;
495
- const rootNode = triggerEl.getRootNode();
496
- if (rootNode instanceof ShadowRoot) {
497
- const existing = rootNode.querySelector<HTMLElement>(
498
- '[data-bs-entry-select-portal]',
499
- );
500
- if (existing) {
501
- setPortalContainer(existing);
502
- return;
503
- }
504
- const container = document.createElement('div');
505
- container.setAttribute('data-bs-entry-select-portal', 'true');
506
- rootNode.appendChild(container);
507
- setPortalContainer(container);
508
- return;
509
- }
510
- setPortalContainer(null);
511
- }, []);
512
-
513
- const handleOpen = useCallback(() => {
514
- if (disabled) return;
515
- setIsOpen(true);
516
- updatePopoverPosition();
517
- }, [disabled, updatePopoverPosition]);
518
-
519
- const handleClose = useCallback(() => {
520
- setIsOpen(false);
521
- setSearchQuery('');
522
- if (triggerRef.current) {
523
- triggerRef.current.style.backgroundColor = '';
524
- }
525
- }, []);
526
-
527
- const handleSelect = useCallback(
528
- (result: BsEntrySelectChangeResult) => {
529
- setInternalValue(result.entry.entryId);
530
- onChange?.(result);
531
- handleClose();
532
- },
533
- [onChange, handleClose],
534
- );
535
-
536
- useEffect(() => {
537
- if (!isOpen) return;
538
-
539
- const handleClickOutside = (e: MouseEvent) => {
540
- const path = e.composedPath();
541
- if (
542
- (popoverRef.current && path.includes(popoverRef.current)) ||
543
- (triggerRef.current && path.includes(triggerRef.current))
544
- ) {
545
- return;
546
- }
547
- handleClose();
548
- };
549
-
550
- const handleScroll = () => {
551
- updatePopoverPosition();
552
- };
553
-
554
- const handleResize = () => {
555
- updatePopoverPosition();
556
- };
557
-
558
- document.addEventListener('mousedown', handleClickOutside);
559
- window.addEventListener('scroll', handleScroll, true);
560
- window.addEventListener('resize', handleResize);
561
-
562
- return () => {
563
- document.removeEventListener('mousedown', handleClickOutside);
564
- window.removeEventListener('scroll', handleScroll, true);
565
- window.removeEventListener('resize', handleResize);
566
- };
567
- }, [isOpen, handleClose, updatePopoverPosition]);
568
-
569
- useEffect(() => {
570
- if (!isOpen) return;
571
-
572
- const handleEscape = (e: KeyboardEvent) => {
573
- if (e.key === 'Escape') {
574
- handleClose();
575
- }
576
- };
577
-
578
- document.addEventListener('keydown', handleEscape);
579
- return () => document.removeEventListener('keydown', handleEscape);
580
- }, [isOpen, handleClose]);
581
-
582
- const hasCurrentApp = Boolean(currentAppId && appDetails[currentAppId]);
583
- const hasOtherApps = otherAppIds.length > 0;
584
- const hasAnyContent = hasCurrentApp || hasOtherApps;
585
-
586
- const currentAppTree = useMemo(() => {
587
- if (!currentAppId || !appDetails[currentAppId]) return [];
588
- return buildEntryTreeFromAliasMap(appDetails[currentAppId], searchQuery);
589
- }, [appDetails, currentAppId, searchQuery]);
590
-
591
- const hasSearch = searchQuery.trim().length > 0;
592
-
593
- return (
594
- <div ref={ref} className={cn('relative inline-block', className)}>
595
- {/* Trigger */}
596
- <div
597
- ref={triggerRef}
598
- className={cn(
599
- 'flex items-center gap-[10px] px-[10px] py-[7px] rounded-[4px] cursor-pointer select-none',
600
- transCls,
601
- isOpen && 'bg-[#EFF0F6]',
602
- disabled && 'opacity-50 cursor-not-allowed',
603
- )}
604
- onClick={handleOpen}
605
- onMouseEnter={(e) => {
606
- if (!isOpen && !disabled) {
607
- (e.currentTarget as HTMLElement).style.backgroundColor =
608
- '#EFF0F6';
609
- }
610
- }}
611
- onMouseLeave={(e) => {
612
- if (!isOpen) {
613
- (e.currentTarget as HTMLElement).style.backgroundColor = '';
614
- }
615
- }}
616
- >
617
- <span className="flex items-center gap-[10px] min-w-0 flex-1">
618
- {selectedResult ? (
619
- <>
620
- <i
621
- className={getEntryIconClass(selectedResult.entry)}
622
- style={{ fontSize: 16, lineHeight: 1 }}
623
- />
624
- <span className="text-[14px] text-[#030303] truncate">
625
- {selectedResult.entry.name}
626
- </span>
627
- </>
628
- ) : (
629
- <span className="text-[14px] text-[#999999] truncate">
630
- {placeholder}
631
- </span>
632
- )}
633
- </span>
634
- <span
635
- className={cn(
636
- 'shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
637
- transCls,
638
- isOpen && 'rotate-180',
639
- )}
640
- >
641
- <ChevronDownIcon size={16} />
642
- </span>
643
- </div>
644
-
645
- {/* Popover */}
646
- {isOpen &&
647
- createPortal(
648
- <div
649
- ref={popoverRef}
650
- style={popoverStyle}
651
- className={cn(
652
- 'bg-white rounded-[10px] shadow-[0px_0px_10px_0px_rgba(0,0,0,0.04)] p-[4px]',
653
- 'flex flex-col gap-[4px]',
654
- transCls,
655
- )}
656
- >
657
- <SearchInput
658
- value={searchQuery}
659
- onChange={setSearchQuery}
660
- />
661
-
662
- <div className="flex flex-col gap-[4px] max-h-[400px] overflow-y-auto">
663
- {hasCurrentApp && (
664
- <AppGroupFolder
665
- label="当前应用表单"
666
- searchQuery={searchQuery}
667
- >
668
- {currentAppTree.length > 0 ? (
669
- currentAppTree.map((node) => (
670
- <TreeItemNode
671
- key={node.id}
672
- item={node}
673
- selectedEntryId={selectedEntryId}
674
- appId={currentAppId!}
675
- section="current"
676
- onSelect={handleSelect}
677
- />
678
- ))
679
- ) : hasSearch ? (
680
- <div className="text-[12px] text-[#999999] py-[8px] pl-[22px]">
681
- 无匹配结果
682
- </div>
683
- ) : (
684
- <div className="text-[12px] text-[#999999] py-[8px] pl-[22px]">
685
- 暂无表单
686
- </div>
687
- )}
688
- </AppGroupFolder>
689
- )}
690
-
691
- {hasOtherApps && (
692
- <AppGroupFolder
693
- label="其他应用表单"
694
- searchQuery={searchQuery}
695
- >
696
- {otherAppIds.map((appId) => (
697
- <OtherAppSection
698
- key={appId}
699
- appId={appId}
700
- appDetail={appDetails[appId]}
701
- searchQuery={searchQuery}
702
- selectedEntryId={selectedEntryId}
703
- section="other"
704
- onSelect={handleSelect}
705
- />
706
- ))}
707
- </AppGroupFolder>
708
- )}
709
-
710
- {!hasAnyContent && (
711
- <div className="text-[14px] text-[#999999] py-[12px] text-center">
712
- 暂无应用
713
- </div>
714
- )}
715
- </div>
716
- </div>,
717
- portalContainer || document.body,
718
- )}
719
- </div>
720
- );
721
- },
722
- );
723
-
724
- BsEntrySelect.displayName = 'BsEntrySelect';