@baishuyun/ai-design 2.1.0-alpha.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/preview.ts +2 -0
- package/CHANGELOG.md +189 -20
- package/package.json +8 -9
- package/scripts/post-build.cjs +32 -0
- package/src/components/agent-drawer.tsx +119 -0
- package/src/components/agent-helper/agent-entry-card/index.tsx +3 -8
- package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
- package/src/components/agent-helper/app-select/index.tsx +119 -84
- package/src/components/agent-helper/condition-input/index.tsx +18 -21
- package/src/components/agent-helper/condition-item/index.tsx +1 -0
- package/src/components/agent-helper/field-edit-preference-select/index.tsx +31 -4
- package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
- package/src/components/agent-helper/field-value-input/index.tsx +3 -2
- package/src/components/agent-helper/field-value-input/picker-inputs.tsx +20 -18
- package/src/components/agent-helper/form-select/index.tsx +12 -25
- package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
- package/src/components/agent-helper/table/index.tsx +6 -2
- package/src/components/agent-helper/team-wide-form-select/data-adapter.ts +109 -0
- package/src/components/agent-helper/team-wide-form-select/index.tsx +289 -0
- package/src/components/agent-helper/team-wide-form-select/tree-panel.tsx +180 -0
- package/src/components/agent-helper/team-wide-form-select/types.ts +36 -0
- package/src/components/agent-helper/team-wide-form-select/use-form-tree.ts +108 -0
- package/src/components/agent-helper/trigger-select/index.tsx +1 -1
- package/src/components/agent-select.tsx +115 -35
- package/src/components/bot-avatar-name.tsx +1 -1
- package/src/components/bs-entry-select/build-tree.ts +0 -6
- package/src/components/bs-entry-select/entry-item.tsx +7 -11
- package/src/components/bs-entry-select/other-app-section.tsx +2 -4
- package/src/components/bs-entry-select/scroll-slider.tsx +6 -11
- package/src/components/bs-icons.tsx +752 -206
- package/src/components/card.tsx +55 -10
- package/src/components/circle-checker.tsx +18 -3
- package/src/components/collapsible-txt-msg.tsx +18 -13
- package/src/components/fake-bot-msg.tsx +18 -7
- package/src/components/field-checker-list-msg.tsx +4 -3
- package/src/components/fields-design-info-table.tsx +9 -26
- package/src/components/fields-portal.tsx +2 -2
- package/src/components/fields-previewer.tsx +1 -0
- package/src/components/font-icon.tsx +20 -9
- package/src/components/form-info-editor.tsx +21 -6
- package/src/components/legacy-slot.tsx +11 -16
- package/src/components/memoized-markdown.tsx +32 -19
- package/src/components/message-content.tsx +7 -3
- package/src/components/output-config-editor.tsx +39 -25
- package/src/components/primary-confirm-btn.tsx +6 -1
- package/src/components/scroll-to-bottom-btn.tsx +3 -2
- package/src/components/shadow-content.tsx +19 -0
- package/src/components/shadow-portal.tsx +23 -0
- package/src/components/ui/dialog.tsx +4 -1
- package/src/components/ui/drawer.tsx +3 -1
- package/src/components/ui/enhanced-drawer.tsx +134 -0
- package/src/components/ui/popover.tsx +3 -2
- package/src/components/ui/select.tsx +3 -2
- package/src/components/ui/tooltip.tsx +3 -2
- package/src/components/user-txt-msg-bubble.tsx +41 -19
- package/src/components/warning-msg.tsx +29 -12
- package/src/const/shadow.ts +17 -0
- package/src/const/ui.ts +2 -2
- package/src/hooks/use-column-appear.ts +1 -1
- package/src/hooks/use-portal-container.ts +6 -3
- package/src/hooks/use-shadow-boundary.ts +67 -0
- package/src/hooks/use-shadow.ts +24 -7
- package/src/index.ts +21 -0
- package/src/stories/AppSelect.stories.tsx +44 -0
- package/src/stories/FieldValueInput.stories.tsx +6 -3
- package/src/stories/FormSelect.stories.tsx +40 -0
- package/src/stories/IconfontPicker.stories.tsx +106 -0
- package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
- package/src/stories/TeamWideFormSelect.stories.tsx +362 -0
- package/src/stories/UserTextMsgBubble.stories.ts +28 -0
- package/src/style.css +28 -1
- package/style.css +172 -130
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import type { IDeptOption } from '@baishuyun/types';
|
|
3
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
|
|
4
|
+
import { FieldWrapper, type FieldValueInputProps } from './index';
|
|
5
|
+
import { usePortalContainer } from './picker-inputs';
|
|
6
|
+
import { DropdownChevron, ClearIcon, CloseIcon, CheckIcon } from './icons';
|
|
7
|
+
import { SearchIcon } from '../../bs-icons';
|
|
8
|
+
import { ClearAllIcon } from '../../icons/clear-all-icon';
|
|
9
|
+
import { cn } from '../../../lib/utils';
|
|
10
|
+
import { transCls } from '../../../const/ui';
|
|
11
|
+
|
|
12
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type DeptSelectKind = 'dept' | 'deptgroup';
|
|
15
|
+
|
|
16
|
+
export interface DeptSelectProps extends FieldValueInputProps {
|
|
17
|
+
kind: DeptSelectKind;
|
|
18
|
+
multiple?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DeptFlatNode {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
parentId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DeptTreeNode extends DeptFlatNode {
|
|
28
|
+
children?: DeptTreeNode[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Tree utilities ──────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** 将服务返回的选项统一拍平:嵌套 children 与扁平 parentId 两种结构都支持 */
|
|
34
|
+
function flattenOptions(options: IDeptOption[], inheritedParentId?: string): DeptFlatNode[] {
|
|
35
|
+
const out: DeptFlatNode[] = [];
|
|
36
|
+
options.forEach((opt) => {
|
|
37
|
+
out.push({ id: opt.id, name: opt.name, parentId: opt.parentId ?? inheritedParentId });
|
|
38
|
+
if (opt.children?.length) {
|
|
39
|
+
out.push(...flattenOptions(opt.children, opt.id));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 由扁平列表构建部门树;parentId 缺失或找不到父节点时作为根节点 */
|
|
46
|
+
function buildTree(flat: DeptFlatNode[]): DeptTreeNode[] {
|
|
47
|
+
const nodeMap = new Map<string, DeptTreeNode>();
|
|
48
|
+
flat.forEach((f) => nodeMap.set(f.id, { ...f }));
|
|
49
|
+
const roots: DeptTreeNode[] = [];
|
|
50
|
+
flat.forEach((f) => {
|
|
51
|
+
const node = nodeMap.get(f.id)!;
|
|
52
|
+
const parent = f.parentId ? nodeMap.get(f.parentId) : undefined;
|
|
53
|
+
if (parent && parent.id !== node.id) {
|
|
54
|
+
parent.children = parent.children ?? [];
|
|
55
|
+
parent.children.push(node);
|
|
56
|
+
} else {
|
|
57
|
+
roots.push(node);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return roots;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Icons ───────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const TreeChevron = ({ expanded }: { expanded: boolean }) => (
|
|
66
|
+
<svg
|
|
67
|
+
width="14" height="14" viewBox="0 0 14 14" fill="none"
|
|
68
|
+
className="shrink-0 transition-transform duration-200"
|
|
69
|
+
style={{ transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
|
70
|
+
>
|
|
71
|
+
<path d="M5 3L9.5 7L5 11" stroke="#999999" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
72
|
+
</svg>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// ── DeptSelect ──────────────────────────────────────────────────────
|
|
76
|
+
//
|
|
77
|
+
// 等价于旧版 BsyDeptCheckBox:
|
|
78
|
+
// - 打开时一次性加载完整部门列表并缓存,构建组织架构树(可展开/收起)
|
|
79
|
+
// - 搜索时对整棵树按名称过滤,展示扁平结果;清空搜索恢复树形展示
|
|
80
|
+
// - 多选支持「全选」(搜索时为「全选搜索结果」),触发器内以标签展示已选项
|
|
81
|
+
// - 数据无 parentId 时(如部门组)退化为扁平列表
|
|
82
|
+
|
|
83
|
+
export function DeptSelect({
|
|
84
|
+
field,
|
|
85
|
+
value,
|
|
86
|
+
onChange,
|
|
87
|
+
disabled,
|
|
88
|
+
service,
|
|
89
|
+
kind,
|
|
90
|
+
multiple = false,
|
|
91
|
+
showLabel,
|
|
92
|
+
showDescription,
|
|
93
|
+
}: DeptSelectProps) {
|
|
94
|
+
const [open, setOpen] = useState(false);
|
|
95
|
+
const [keyword, setKeyword] = useState('');
|
|
96
|
+
const [loading, setLoading] = useState(false);
|
|
97
|
+
const [tree, setTree] = useState<DeptTreeNode[]>([]);
|
|
98
|
+
const [flatList, setFlatList] = useState<DeptFlatNode[]>([]);
|
|
99
|
+
const [nameMap, setNameMap] = useState<Record<string, string>>({});
|
|
100
|
+
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set());
|
|
101
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
102
|
+
const loadedRef = useRef(false);
|
|
103
|
+
const { portalContainer, setTriggerRef } = usePortalContainer('data-bs-field-value-picker-portal');
|
|
104
|
+
|
|
105
|
+
const fieldName = field.widget?.widgetName || '';
|
|
106
|
+
const searchFn = kind === 'dept' ? service?.searchDepts : service?.searchDeptGroups;
|
|
107
|
+
|
|
108
|
+
const selectedIds: string[] = multiple ? (Array.isArray(value) ? value.map(String) : []) : [];
|
|
109
|
+
const selectedIdSet = new Set(selectedIds);
|
|
110
|
+
const singleId = !multiple && typeof value === 'string' ? value : '';
|
|
111
|
+
|
|
112
|
+
const loadOptions = useCallback(async () => {
|
|
113
|
+
if (!searchFn || loadedRef.current) return;
|
|
114
|
+
loadedRef.current = true;
|
|
115
|
+
setLoading(true);
|
|
116
|
+
try {
|
|
117
|
+
const list = await searchFn('', { field: fieldName });
|
|
118
|
+
const flat = flattenOptions(list);
|
|
119
|
+
const nextTree = buildTree(flat);
|
|
120
|
+
setFlatList(flat);
|
|
121
|
+
setTree(nextTree);
|
|
122
|
+
setNameMap((prev) => {
|
|
123
|
+
const next = { ...prev };
|
|
124
|
+
flat.forEach((f) => {
|
|
125
|
+
next[f.id] = f.name;
|
|
126
|
+
});
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
// 与旧组件一致:默认展开第一个根节点
|
|
130
|
+
const firstWithChildren = nextTree.find((n) => n.children?.length);
|
|
131
|
+
if (firstWithChildren) {
|
|
132
|
+
setExpandedKeys(new Set([firstWithChildren.id]));
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
setLoading(false);
|
|
136
|
+
}
|
|
137
|
+
}, [searchFn, fieldName]);
|
|
138
|
+
|
|
139
|
+
// 首次打开时加载;已有值时提前加载以解析名称
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (open) loadOptions();
|
|
142
|
+
}, [open, loadOptions]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const hasValue = multiple ? selectedIds.length > 0 : !!singleId;
|
|
146
|
+
if (hasValue) loadOptions();
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const searching = !!keyword.trim();
|
|
151
|
+
const searchResults = useMemo(() => {
|
|
152
|
+
if (!searching) return [];
|
|
153
|
+
const kw = keyword.trim();
|
|
154
|
+
return flatList.filter((n) => n.name.includes(kw));
|
|
155
|
+
}, [searching, keyword, flatList]);
|
|
156
|
+
|
|
157
|
+
// 全选作用于当前可见范围:搜索时为搜索结果,否则为全部部门
|
|
158
|
+
const visibleIds = searching ? searchResults.map((n) => n.id) : flatList.map((n) => n.id);
|
|
159
|
+
const isAllSelected =
|
|
160
|
+
multiple && visibleIds.length > 0 && visibleIds.every((id) => selectedIdSet.has(id));
|
|
161
|
+
|
|
162
|
+
const handleToggle = useCallback(
|
|
163
|
+
(node: DeptFlatNode) => {
|
|
164
|
+
if (multiple) {
|
|
165
|
+
const next = selectedIdSet.has(node.id)
|
|
166
|
+
? selectedIds.filter((id) => id !== node.id)
|
|
167
|
+
: [...selectedIds, node.id];
|
|
168
|
+
onChange?.(next);
|
|
169
|
+
} else {
|
|
170
|
+
onChange?.(node.id);
|
|
171
|
+
setNameMap((prev) => ({ ...prev, [node.id]: node.name }));
|
|
172
|
+
setOpen(false);
|
|
173
|
+
setKeyword('');
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[multiple, selectedIds, selectedIdSet, onChange],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const handleSelectAll = useCallback(() => {
|
|
180
|
+
if (!multiple) return;
|
|
181
|
+
if (isAllSelected) {
|
|
182
|
+
const visibleSet = new Set(visibleIds);
|
|
183
|
+
onChange?.(selectedIds.filter((id) => !visibleSet.has(id)));
|
|
184
|
+
} else {
|
|
185
|
+
onChange?.([...new Set([...selectedIds, ...visibleIds])]);
|
|
186
|
+
}
|
|
187
|
+
}, [multiple, isAllSelected, visibleIds, selectedIds, onChange]);
|
|
188
|
+
|
|
189
|
+
const toggleExpand = useCallback((id: string, e: React.MouseEvent) => {
|
|
190
|
+
e.stopPropagation();
|
|
191
|
+
setExpandedKeys((prev) => {
|
|
192
|
+
const next = new Set(prev);
|
|
193
|
+
if (next.has(id)) {
|
|
194
|
+
next.delete(id);
|
|
195
|
+
} else {
|
|
196
|
+
next.add(id);
|
|
197
|
+
}
|
|
198
|
+
return next;
|
|
199
|
+
});
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
const handleClearSingle = useCallback(
|
|
203
|
+
(e: React.MouseEvent) => {
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
onChange?.('');
|
|
206
|
+
},
|
|
207
|
+
[onChange],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const handleRemoveMultiple = useCallback(
|
|
211
|
+
(id: string, e: React.MouseEvent) => {
|
|
212
|
+
e.stopPropagation();
|
|
213
|
+
onChange?.(selectedIds.filter((v) => v !== id));
|
|
214
|
+
},
|
|
215
|
+
[selectedIds, onChange],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const handleClearAllMultiple = useCallback(
|
|
219
|
+
(e: React.MouseEvent) => {
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
onChange?.([]);
|
|
222
|
+
},
|
|
223
|
+
[onChange],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const renderTreeNodes = (nodes: DeptTreeNode[], level: number): React.ReactNode =>
|
|
227
|
+
nodes.map((node) => {
|
|
228
|
+
const hasChildren = !!node.children?.length;
|
|
229
|
+
const expanded = expandedKeys.has(node.id);
|
|
230
|
+
const checked = multiple ? selectedIdSet.has(node.id) : singleId === node.id;
|
|
231
|
+
return (
|
|
232
|
+
<div key={node.id}>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
onClick={() => handleToggle(node)}
|
|
236
|
+
className={cn(
|
|
237
|
+
'flex items-center gap-[6px] w-full h-[34px] pr-[10px] rounded-[4px] text-left',
|
|
238
|
+
checked ? 'bg-[#F0F6FF] text-[#0265FF]' : 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
239
|
+
transCls,
|
|
240
|
+
)}
|
|
241
|
+
style={{ paddingLeft: 10 + level * 20 }}
|
|
242
|
+
>
|
|
243
|
+
{hasChildren ? (
|
|
244
|
+
<span
|
|
245
|
+
role="button"
|
|
246
|
+
tabIndex={-1}
|
|
247
|
+
onClick={(e) => toggleExpand(node.id, e)}
|
|
248
|
+
className="flex items-center justify-center w-[16px] h-[16px] shrink-0"
|
|
249
|
+
>
|
|
250
|
+
<TreeChevron expanded={expanded} />
|
|
251
|
+
</span>
|
|
252
|
+
) : (
|
|
253
|
+
<span className="w-[16px] shrink-0" />
|
|
254
|
+
)}
|
|
255
|
+
{multiple && (
|
|
256
|
+
<div
|
|
257
|
+
className={cn(
|
|
258
|
+
'w-[14px] h-[14px] rounded-[2px] flex items-center justify-center shrink-0 border',
|
|
259
|
+
checked ? 'bg-[#0265FF] border-[#0265FF]' : 'bg-white border-[#CCCCCC]',
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
{checked && <CheckIcon />}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
<span className="truncate text-[14px]">{node.name}</span>
|
|
266
|
+
</button>
|
|
267
|
+
{hasChildren && expanded && renderTreeNodes(node.children!, level + 1)}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const renderSearchResults = () =>
|
|
273
|
+
searchResults.map((node) => {
|
|
274
|
+
const checked = multiple ? selectedIdSet.has(node.id) : singleId === node.id;
|
|
275
|
+
return (
|
|
276
|
+
<button
|
|
277
|
+
key={node.id}
|
|
278
|
+
type="button"
|
|
279
|
+
onClick={() => handleToggle(node)}
|
|
280
|
+
className={cn(
|
|
281
|
+
'flex items-center gap-[10px] h-[34px] px-[10px] rounded-[4px] text-left',
|
|
282
|
+
checked ? 'bg-[#F0F6FF] text-[#0265FF]' : 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
283
|
+
transCls,
|
|
284
|
+
)}
|
|
285
|
+
>
|
|
286
|
+
{multiple && (
|
|
287
|
+
<div
|
|
288
|
+
className={cn(
|
|
289
|
+
'w-[14px] h-[14px] rounded-[2px] flex items-center justify-center shrink-0 border',
|
|
290
|
+
checked ? 'bg-[#0265FF] border-[#0265FF]' : 'bg-white border-[#CCCCCC]',
|
|
291
|
+
)}
|
|
292
|
+
>
|
|
293
|
+
{checked && <CheckIcon />}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
<span className="truncate text-[14px]">{node.name}</span>
|
|
297
|
+
</button>
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const hasSelectionMulti = multiple && selectedIds.length > 0;
|
|
302
|
+
const showClearMulti = hasSelectionMulti && isHovering;
|
|
303
|
+
const singleLabel = singleId ? nameMap[singleId] || singleId : '';
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<FieldWrapper field={field} showLabel={showLabel} showDescription={showDescription}>
|
|
307
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
308
|
+
<PopoverTrigger asChild>
|
|
309
|
+
<button
|
|
310
|
+
ref={setTriggerRef}
|
|
311
|
+
type="button"
|
|
312
|
+
disabled={disabled}
|
|
313
|
+
onMouseEnter={() => setIsHovering(true)}
|
|
314
|
+
onMouseLeave={() => setIsHovering(false)}
|
|
315
|
+
className={cn(
|
|
316
|
+
'flex items-center justify-between w-full h-[34px] gap-2.5',
|
|
317
|
+
'bg-white border rounded-[4px]',
|
|
318
|
+
'py-[7px]',
|
|
319
|
+
(multiple ? hasSelectionMulti : !!singleLabel) ? 'pl-[4px] pr-[10px]' : 'px-[10px]',
|
|
320
|
+
'text-left cursor-pointer',
|
|
321
|
+
'hover:border-[#0265FF]',
|
|
322
|
+
disabled && 'opacity-50 cursor-not-allowed hover:border-[#E0E0E0]',
|
|
323
|
+
open ? 'border-[#0265FF]' : 'border-[#E0E0E0]',
|
|
324
|
+
transCls,
|
|
325
|
+
)}
|
|
326
|
+
>
|
|
327
|
+
{multiple ? (
|
|
328
|
+
hasSelectionMulti ? (
|
|
329
|
+
<>
|
|
330
|
+
<div className="flex items-center gap-[4px] min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
|
331
|
+
{selectedIds.map((id) => (
|
|
332
|
+
<span
|
|
333
|
+
key={id}
|
|
334
|
+
className="flex items-center gap-[4px] bg-[#EEEEEE] rounded-[2px] h-[26px] px-[6px] shrink-0"
|
|
335
|
+
>
|
|
336
|
+
<span className="text-[14px] text-[#030303] leading-[normal]">
|
|
337
|
+
{nameMap[id] || id}
|
|
338
|
+
</span>
|
|
339
|
+
<span
|
|
340
|
+
className="cursor-pointer flex items-center shrink-0"
|
|
341
|
+
onClick={(e) => handleRemoveMultiple(id, e)}
|
|
342
|
+
>
|
|
343
|
+
<CloseIcon />
|
|
344
|
+
</span>
|
|
345
|
+
</span>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
{showClearMulti ? (
|
|
349
|
+
<span
|
|
350
|
+
className="cursor-pointer flex items-center shrink-0"
|
|
351
|
+
onClick={handleClearAllMultiple}
|
|
352
|
+
>
|
|
353
|
+
<ClearAllIcon />
|
|
354
|
+
</span>
|
|
355
|
+
) : (
|
|
356
|
+
<DropdownChevron open={open} />
|
|
357
|
+
)}
|
|
358
|
+
</>
|
|
359
|
+
) : (
|
|
360
|
+
<>
|
|
361
|
+
<span className="text-[14px] text-[#999999] leading-[normal] truncate">
|
|
362
|
+
{field.description ?? `请选择${field.label}`}
|
|
363
|
+
</span>
|
|
364
|
+
<DropdownChevron open={open} />
|
|
365
|
+
</>
|
|
366
|
+
)
|
|
367
|
+
) : (
|
|
368
|
+
<>
|
|
369
|
+
<span className={cn('truncate', !singleLabel && 'text-[#999999]')}>
|
|
370
|
+
{singleLabel || (field.description ?? `请选择${field.label}`)}
|
|
371
|
+
</span>
|
|
372
|
+
<div className="flex items-center gap-[4px] shrink-0">
|
|
373
|
+
{!!singleLabel && (
|
|
374
|
+
<span
|
|
375
|
+
role="button"
|
|
376
|
+
tabIndex={-1}
|
|
377
|
+
onClick={handleClearSingle}
|
|
378
|
+
className="flex items-center justify-center w-[18px] h-[18px] hover:bg-[#EFF0F6] rounded-full"
|
|
379
|
+
>
|
|
380
|
+
<ClearIcon />
|
|
381
|
+
</span>
|
|
382
|
+
)}
|
|
383
|
+
<DropdownChevron open={open} />
|
|
384
|
+
</div>
|
|
385
|
+
</>
|
|
386
|
+
)}
|
|
387
|
+
</button>
|
|
388
|
+
</PopoverTrigger>
|
|
389
|
+
<PopoverContent
|
|
390
|
+
align="start"
|
|
391
|
+
sideOffset={4}
|
|
392
|
+
portalContainer={portalContainer}
|
|
393
|
+
className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[8px]"
|
|
394
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
395
|
+
>
|
|
396
|
+
<div className="flex items-center gap-[8px] px-[6px] mb-[4px] h-[34px] bg-[#F6F7FA] rounded-[4px]">
|
|
397
|
+
<SearchIcon />
|
|
398
|
+
<input
|
|
399
|
+
type="text"
|
|
400
|
+
value={keyword}
|
|
401
|
+
onChange={(e) => setKeyword(e.target.value)}
|
|
402
|
+
placeholder={`搜索${field.label}`}
|
|
403
|
+
className="flex-1 bg-transparent text-[14px] text-[#030303] placeholder:text-[#999999] outline-none border-none"
|
|
404
|
+
autoFocus
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
{multiple && !loading && visibleIds.length > 0 && (
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
onClick={handleSelectAll}
|
|
411
|
+
className={cn(
|
|
412
|
+
'flex items-center gap-[10px] w-full h-[34px] px-[10px] rounded-[4px] text-left',
|
|
413
|
+
'text-[#0265FF] hover:bg-[#EFF0F6]',
|
|
414
|
+
transCls,
|
|
415
|
+
)}
|
|
416
|
+
>
|
|
417
|
+
<div
|
|
418
|
+
className={cn(
|
|
419
|
+
'w-[14px] h-[14px] rounded-[2px] flex items-center justify-center shrink-0 border',
|
|
420
|
+
isAllSelected ? 'bg-[#0265FF] border-[#0265FF]' : 'bg-white border-[#CCCCCC]',
|
|
421
|
+
)}
|
|
422
|
+
>
|
|
423
|
+
{isAllSelected && <CheckIcon />}
|
|
424
|
+
</div>
|
|
425
|
+
<span className="text-[14px]">{searching ? '全选搜索结果' : '全选'}</span>
|
|
426
|
+
</button>
|
|
427
|
+
)}
|
|
428
|
+
<div className="flex flex-col gap-0 max-h-[240px] overflow-y-auto">
|
|
429
|
+
{loading ? (
|
|
430
|
+
<div className="px-[10px] py-[8px] text-[14px] text-[#999999]">加载中...</div>
|
|
431
|
+
) : searching ? (
|
|
432
|
+
searchResults.length === 0 ? (
|
|
433
|
+
<div className="px-[10px] py-[8px] text-[14px] text-[#999999]">暂无数据</div>
|
|
434
|
+
) : (
|
|
435
|
+
renderSearchResults()
|
|
436
|
+
)
|
|
437
|
+
) : tree.length === 0 ? (
|
|
438
|
+
<div className="px-[10px] py-[8px] text-[14px] text-[#999999]">暂无数据</div>
|
|
439
|
+
) : (
|
|
440
|
+
renderTreeNodes(tree, 0)
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
</PopoverContent>
|
|
444
|
+
</Popover>
|
|
445
|
+
</FieldWrapper>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
@@ -5,6 +5,7 @@ import { cn } from '../../../lib/utils';
|
|
|
5
5
|
|
|
6
6
|
import { TextInput, NumberInput, TextareaInput, PasswordInput, DateTimeInput, UploadInput, RangeNumberInput, RangeDateTimeInput } from './inputs';
|
|
7
7
|
import { ComboInput, CheckboxGroupInput, RadioGroupInput, AsyncPicker, CascadingAddressInput, TextMultiSelect } from './picker-inputs';
|
|
8
|
+
import { DeptSelect } from './dept-select';
|
|
8
9
|
import { SeparatorDisplay, ButtonDisplay, SubformPlaceholder } from './display';
|
|
9
10
|
|
|
10
11
|
// ── Props ───────────────────────────────────────────────────────────
|
|
@@ -104,11 +105,11 @@ export const FieldValueInput = (props: FieldValueInputProps) => {
|
|
|
104
105
|
case 'user':
|
|
105
106
|
return <AsyncPicker {...props} kind="user" multiple={operator === 'in' || operator === 'nin'} />;
|
|
106
107
|
case 'dept':
|
|
107
|
-
return <
|
|
108
|
+
return <DeptSelect {...props} kind="dept" multiple={operator === 'in' || operator === 'nin'} />;
|
|
108
109
|
case 'usergroup':
|
|
109
110
|
return <AsyncPicker {...props} kind="usergroup" multiple={operator === 'in' || operator === 'nin' || operator === 'all'} />;
|
|
110
111
|
case 'deptgroup':
|
|
111
|
-
return <
|
|
112
|
+
return <DeptSelect {...props} kind="deptgroup" multiple={operator === 'in' || operator === 'nin' || operator === 'all'} />;
|
|
112
113
|
|
|
113
114
|
// ── Combo / linkdata / linkquery ──
|
|
114
115
|
case 'combo':
|
|
@@ -19,24 +19,21 @@ import { chinaAddressData } from './china-address-data';
|
|
|
19
19
|
const inputBaseCls =
|
|
20
20
|
'h-[34px] px-[10px] py-[7px] rounded-[4px] bg-white border text-[14px] text-[#030303] placeholder:text-[#999999] focus:outline-none focus:border-[#0265FF] w-full disabled:bg-[#EFF0F6] disabled:cursor-not-allowed';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
interface WidgetItem {
|
|
23
23
|
label: string;
|
|
24
24
|
value: string | number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getItems(field: FieldValueInputProps['field']): PickerItem[] {
|
|
28
|
-
return (field.widget?.items ?? [])
|
|
29
|
-
.map((it) => {
|
|
30
|
-
if ('text' in it) {
|
|
31
|
-
return { label: it.text, value: it.value };
|
|
32
|
-
}
|
|
25
|
+
}
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
function getItems(field: FieldValueInputProps['field']): WidgetItem[] {
|
|
28
|
+
return field.widget?.items
|
|
29
|
+
?.filter((it) => typeof it.label === 'string' || typeof it.text === 'string')
|
|
30
|
+
.map((it) => ({
|
|
31
|
+
label: it.label ?? it.text ?? '',
|
|
32
|
+
value: it.value,
|
|
33
|
+
})) as WidgetItem[] ?? [];
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
function usePortalContainer(attr: string) {
|
|
36
|
+
export function usePortalContainer(attr: string) {
|
|
40
37
|
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
41
38
|
const setTriggerRef = useCallback(
|
|
42
39
|
(node: HTMLButtonElement | null) => {
|
|
@@ -134,7 +131,8 @@ export function ComboInput({ field, value, onChange, disabled, service, showLabe
|
|
|
134
131
|
type="button"
|
|
135
132
|
onClick={() => handleSelect(item.value)}
|
|
136
133
|
className={cn(
|
|
137
|
-
|
|
134
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
135
|
+
'shrink-0 h-[34px] px-[10px] rounded-[4px] text-left text-[14px] leading-[normal]',
|
|
138
136
|
value === item.value
|
|
139
137
|
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
140
138
|
: 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
@@ -507,7 +505,8 @@ export function AsyncPicker({ field, value, onChange, disabled, service, kind, m
|
|
|
507
505
|
type="button"
|
|
508
506
|
onClick={() => handleSelect(opt)}
|
|
509
507
|
className={cn(
|
|
510
|
-
|
|
508
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
509
|
+
'shrink-0 flex items-center gap-[10px] h-[34px] px-[10px] rounded-[4px] text-left',
|
|
511
510
|
isSelected
|
|
512
511
|
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
513
512
|
: 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
@@ -687,7 +686,8 @@ export function TextMultiSelect({ field, value, onChange, disabled, service, sho
|
|
|
687
686
|
type="button"
|
|
688
687
|
onClick={() => handleToggle(opt)}
|
|
689
688
|
className={cn(
|
|
690
|
-
|
|
689
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
690
|
+
'shrink-0 flex items-center gap-[10px] h-[34px] px-[10px] rounded-[4px] text-left',
|
|
691
691
|
checked
|
|
692
692
|
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
693
693
|
: 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
@@ -1030,7 +1030,8 @@ export function AddressInput({ field, value, onChange, disabled, service, showLa
|
|
|
1030
1030
|
setKeyword('');
|
|
1031
1031
|
}}
|
|
1032
1032
|
className={cn(
|
|
1033
|
-
|
|
1033
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
1034
|
+
'shrink-0 h-[34px] px-[10px] rounded-[4px] text-left text-[14px] leading-[normal]',
|
|
1034
1035
|
'text-[#030303] hover:bg-[#EFF0F6]',
|
|
1035
1036
|
transCls,
|
|
1036
1037
|
)}
|
|
@@ -1127,7 +1128,8 @@ export function LocationInput({ field, value, onChange, disabled, service, showL
|
|
|
1127
1128
|
setKeyword('');
|
|
1128
1129
|
}}
|
|
1129
1130
|
className={cn(
|
|
1130
|
-
|
|
1131
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
1132
|
+
'shrink-0 h-[34px] px-[10px] rounded-[4px] text-left text-[14px] leading-[normal]',
|
|
1131
1133
|
'text-[#030303] hover:bg-[#EFF0F6]',
|
|
1132
1134
|
transCls,
|
|
1133
1135
|
)}
|
|
@@ -12,6 +12,9 @@ import { cn } from '../../../lib/utils';
|
|
|
12
12
|
import { transCls } from '../../../const/ui';
|
|
13
13
|
import { SolidArrowDownIcon, SearchIcon } from '../../bs-icons';
|
|
14
14
|
import { useSelectPopoverPosition } from '../../../hooks/use-select-popover-position';
|
|
15
|
+
import { FontIcon } from '../../font-icon';
|
|
16
|
+
|
|
17
|
+
const POPOVER_MAX_HEIGHT = 300;
|
|
15
18
|
|
|
16
19
|
const FolderIcon = () => (
|
|
17
20
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
@@ -170,14 +173,6 @@ function buildFormTree(items: FormSelectItem[], searchQuery: string): FormTreeIt
|
|
|
170
173
|
return result;
|
|
171
174
|
}
|
|
172
175
|
|
|
173
|
-
// ── Form Icon ─────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
function getFormIconClass(icon: number): string {
|
|
176
|
-
const isNew = icon >= 25;
|
|
177
|
-
const newStr = isNew ? '-new' : '';
|
|
178
|
-
return `node-icon icon-bsy-form${newStr} icon${newStr}-bsy-form-${icon}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
176
|
// ── Entry Item ────────────────────────────────────────────────────
|
|
182
177
|
|
|
183
178
|
function FormEntryItem({
|
|
@@ -202,10 +197,7 @@ function FormEntryItem({
|
|
|
202
197
|
onClick={onClick}
|
|
203
198
|
>
|
|
204
199
|
<span className="flex items-center gap-[6px] min-w-0 flex-1">
|
|
205
|
-
<
|
|
206
|
-
className={getFormIconClass(item.icon)}
|
|
207
|
-
style={{ fontSize: 16, lineHeight: 1 }}
|
|
208
|
-
/>
|
|
200
|
+
<FontIcon name={item.icon} />
|
|
209
201
|
<span className="text-[14px] leading-[normal] truncate">
|
|
210
202
|
{item.name}
|
|
211
203
|
</span>
|
|
@@ -307,11 +299,12 @@ function FormPopover({
|
|
|
307
299
|
<div
|
|
308
300
|
className={cn(
|
|
309
301
|
'bg-white rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]',
|
|
310
|
-
'flex flex-col gap-[4px]',
|
|
302
|
+
'flex min-h-0 flex-col gap-[4px] overflow-hidden',
|
|
311
303
|
)}
|
|
304
|
+
style={{ maxHeight: POPOVER_MAX_HEIGHT }}
|
|
312
305
|
>
|
|
313
306
|
{/* Search input */}
|
|
314
|
-
<div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
|
|
307
|
+
<div className="flex shrink-0 items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
|
|
315
308
|
<SearchIcon />
|
|
316
309
|
<input
|
|
317
310
|
type="text"
|
|
@@ -337,7 +330,7 @@ function FormPopover({
|
|
|
337
330
|
)}
|
|
338
331
|
|
|
339
332
|
{/* Form list */}
|
|
340
|
-
<div className="flex flex-col gap-0
|
|
333
|
+
<div className="flex min-h-0 flex-col gap-0 overflow-y-auto">
|
|
341
334
|
{tree.length > 0 ? (
|
|
342
335
|
tree.map((node) => (
|
|
343
336
|
<FormTreeItemNode
|
|
@@ -391,7 +384,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
|
|
|
391
384
|
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
392
385
|
|
|
393
386
|
const { popoverStyle, updatePopoverPosition } =
|
|
394
|
-
useSelectPopoverPosition(triggerRef);
|
|
387
|
+
useSelectPopoverPosition(triggerRef, { estimatedHeight: POPOVER_MAX_HEIGHT });
|
|
395
388
|
|
|
396
389
|
const resolvedValue = controlledValue ?? internalValue;
|
|
397
390
|
|
|
@@ -655,10 +648,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
|
|
|
655
648
|
className="flex items-center gap-[4px] bg-[#EEEEEE] rounded-[2px] h-[26px] px-[6px] shrink-0 max-w-[160px]"
|
|
656
649
|
onClick={(e) => handleRemoveChip(form.id, e)}
|
|
657
650
|
>
|
|
658
|
-
<
|
|
659
|
-
className={getFormIconClass(form.icon ?? 0)}
|
|
660
|
-
style={{ fontSize: 14, lineHeight: 1 }}
|
|
661
|
-
/>
|
|
651
|
+
<FontIcon name={form.icon ?? 0} style={{ fontSize: 14 }} />
|
|
662
652
|
<span className="text-[14px] text-[#030303] truncate leading-[normal]">
|
|
663
653
|
{form.name}
|
|
664
654
|
</span>
|
|
@@ -680,10 +670,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
|
|
|
680
670
|
<span className="flex items-center gap-[6px] min-w-0 flex-1">
|
|
681
671
|
{selectedForm ? (
|
|
682
672
|
<>
|
|
683
|
-
<
|
|
684
|
-
className={getFormIconClass(selectedForm.icon ?? 0)}
|
|
685
|
-
style={{ fontSize: 16, lineHeight: 1 }}
|
|
686
|
-
/>
|
|
673
|
+
<FontIcon name={selectedForm.icon ?? 0} />
|
|
687
674
|
<span className="text-[14px] text-[#030303] leading-[normal] truncate">
|
|
688
675
|
{selectedForm.name}
|
|
689
676
|
</span>
|
|
@@ -709,7 +696,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
|
|
|
709
696
|
{/* Popover */}
|
|
710
697
|
{isOpen &&
|
|
711
698
|
createPortal(
|
|
712
|
-
<div ref={popoverRef} style={popoverStyle}>
|
|
699
|
+
<div ref={popoverRef} className="flex flex-col" style={popoverStyle}>
|
|
713
700
|
<FormPopover
|
|
714
701
|
searchQuery={searchQuery}
|
|
715
702
|
onSearchChange={setSearchQuery}
|