@baishuyun/ai-design 2.0.48 → 2.0.50
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/CHANGELOG.md +18 -0
- package/package.json +4 -4
- package/src/components/agent-drawer.tsx +40 -6
- 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 +11 -6
- 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-select.tsx +115 -35
- package/src/components/card.tsx +19 -6
- package/src/components/output-config-editor.tsx +23 -17
- package/src/stories/FieldValueInput.stories.tsx +6 -3
- package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
- package/style.css +147 -126
|
@@ -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':
|
|
@@ -33,7 +33,7 @@ function getItems(field: FieldValueInputProps['field']): WidgetItem[] {
|
|
|
33
33
|
})) as WidgetItem[] ?? [];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function usePortalContainer(attr: string) {
|
|
36
|
+
export function usePortalContainer(attr: string) {
|
|
37
37
|
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
38
38
|
const setTriggerRef = useCallback(
|
|
39
39
|
(node: HTMLButtonElement | null) => {
|
|
@@ -131,7 +131,8 @@ export function ComboInput({ field, value, onChange, disabled, service, showLabe
|
|
|
131
131
|
type="button"
|
|
132
132
|
onClick={() => handleSelect(item.value)}
|
|
133
133
|
className={cn(
|
|
134
|
-
|
|
134
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
135
|
+
'shrink-0 h-[34px] px-[10px] rounded-[4px] text-left text-[14px] leading-[normal]',
|
|
135
136
|
value === item.value
|
|
136
137
|
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
137
138
|
: 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
@@ -504,7 +505,8 @@ export function AsyncPicker({ field, value, onChange, disabled, service, kind, m
|
|
|
504
505
|
type="button"
|
|
505
506
|
onClick={() => handleSelect(opt)}
|
|
506
507
|
className={cn(
|
|
507
|
-
|
|
508
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
509
|
+
'shrink-0 flex items-center gap-[10px] h-[34px] px-[10px] rounded-[4px] text-left',
|
|
508
510
|
isSelected
|
|
509
511
|
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
510
512
|
: 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
@@ -684,7 +686,8 @@ export function TextMultiSelect({ field, value, onChange, disabled, service, sho
|
|
|
684
686
|
type="button"
|
|
685
687
|
onClick={() => handleToggle(opt)}
|
|
686
688
|
className={cn(
|
|
687
|
-
|
|
689
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
690
|
+
'shrink-0 flex items-center gap-[10px] h-[34px] px-[10px] rounded-[4px] text-left',
|
|
688
691
|
checked
|
|
689
692
|
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
690
693
|
: 'text-[#030303] hover:bg-[#EFF0F6]',
|
|
@@ -1027,7 +1030,8 @@ export function AddressInput({ field, value, onChange, disabled, service, showLa
|
|
|
1027
1030
|
setKeyword('');
|
|
1028
1031
|
}}
|
|
1029
1032
|
className={cn(
|
|
1030
|
-
|
|
1033
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
1034
|
+
'shrink-0 h-[34px] px-[10px] rounded-[4px] text-left text-[14px] leading-[normal]',
|
|
1031
1035
|
'text-[#030303] hover:bg-[#EFF0F6]',
|
|
1032
1036
|
transCls,
|
|
1033
1037
|
)}
|
|
@@ -1124,7 +1128,8 @@ export function LocationInput({ field, value, onChange, disabled, service, showL
|
|
|
1124
1128
|
setKeyword('');
|
|
1125
1129
|
}}
|
|
1126
1130
|
className={cn(
|
|
1127
|
-
|
|
1131
|
+
// shrink-0 防止 flex 容器 max-height 下子项收缩坍缩,改走 overflow 滚动
|
|
1132
|
+
'shrink-0 h-[34px] px-[10px] rounded-[4px] text-left text-[14px] leading-[normal]',
|
|
1128
1133
|
'text-[#030303] hover:bg-[#EFF0F6]',
|
|
1129
1134
|
transCls,
|
|
1130
1135
|
)}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import type { relationalOperator } from '@baishuyun/types';
|
|
3
|
-
import {
|
|
2
|
+
import type { FieldType, relationalOperator } from '@baishuyun/types';
|
|
3
|
+
import { getOperatorLabel } from '@baishuyun/ai-feat-utils';
|
|
4
4
|
import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
|
|
5
5
|
import { cn } from '../../../lib/utils';
|
|
6
6
|
import { transCls } from '../../../const/ui';
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
const ALL_OPERATORS: relationalOperator[] = [
|
|
9
|
+
'eq',
|
|
10
|
+
'neq',
|
|
11
|
+
'gt',
|
|
12
|
+
'lt',
|
|
13
|
+
'gte',
|
|
14
|
+
'lte',
|
|
15
|
+
'contains',
|
|
16
|
+
'not_contains',
|
|
17
|
+
'in',
|
|
18
|
+
'nin',
|
|
19
|
+
'all',
|
|
20
|
+
'empty',
|
|
21
|
+
'not_empty',
|
|
22
|
+
'range',
|
|
23
|
+
'like',
|
|
24
|
+
'unlike',
|
|
25
25
|
];
|
|
26
26
|
|
|
27
27
|
export interface IRelationalOperatorSelectProps {
|
|
@@ -29,6 +29,13 @@ export interface IRelationalOperatorSelectProps {
|
|
|
29
29
|
defaultValue?: relationalOperator;
|
|
30
30
|
onChange?: (value: relationalOperator) => void;
|
|
31
31
|
operators?: relationalOperator[];
|
|
32
|
+
/**
|
|
33
|
+
* 字段类型:同一操作符在不同字段类型下描述不同
|
|
34
|
+
* (如部门多选的 in 为「包含任意一个」),未提供时使用默认文案
|
|
35
|
+
*/
|
|
36
|
+
fieldType?: FieldType;
|
|
37
|
+
/** 逐操作符的文案覆盖,优先级高于 fieldType 推导 */
|
|
38
|
+
operatorLabels?: Partial<Record<relationalOperator, string>>;
|
|
32
39
|
borderless?: boolean;
|
|
33
40
|
disabled?: boolean;
|
|
34
41
|
className?: string;
|
|
@@ -39,6 +46,8 @@ export const RelationalOperatorSelect = ({
|
|
|
39
46
|
defaultValue = 'eq',
|
|
40
47
|
onChange,
|
|
41
48
|
operators,
|
|
49
|
+
fieldType,
|
|
50
|
+
operatorLabels,
|
|
42
51
|
borderless = false,
|
|
43
52
|
disabled = false,
|
|
44
53
|
className,
|
|
@@ -48,10 +57,15 @@ export const RelationalOperatorSelect = ({
|
|
|
48
57
|
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
49
58
|
|
|
50
59
|
const options = useMemo(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const list =
|
|
61
|
+
!operators || operators.length === 0
|
|
62
|
+
? ALL_OPERATORS
|
|
63
|
+
: ALL_OPERATORS.filter((o) => new Set(operators).has(o));
|
|
64
|
+
return list.map((value) => ({
|
|
65
|
+
value,
|
|
66
|
+
label: operatorLabels?.[value] ?? getOperatorLabel(value, fieldType),
|
|
67
|
+
}));
|
|
68
|
+
}, [operators, fieldType, operatorLabels]);
|
|
55
69
|
|
|
56
70
|
const resolvedValue = (() => {
|
|
57
71
|
const v = controlledValue !== undefined ? controlledValue : internalValue;
|
|
@@ -47,9 +47,13 @@ export function DataTable<TData>({
|
|
|
47
47
|
return (
|
|
48
48
|
<div
|
|
49
49
|
className={cn(
|
|
50
|
-
'rounded-[10px] border border-[#e0e0e0] overflow-
|
|
50
|
+
'rounded-[10px] border border-[#e0e0e0] overflow-auto bg-white',
|
|
51
51
|
className,
|
|
52
52
|
)}
|
|
53
|
+
// 抽屉(vaul/Radix Dialog)经 react-remove-scroll 在 document 级 preventDefault
|
|
54
|
+
// wheel 事件;shadow DOM 下其 contains 判定失效,内部滚动会被误杀。
|
|
55
|
+
// 阻断冒泡使其不可见,恢复触摸板滚动(同 form-select 的处理先例)。
|
|
56
|
+
onWheel={(e) => e.stopPropagation()}
|
|
53
57
|
>
|
|
54
58
|
<table className="w-full h-full border-collapse">
|
|
55
59
|
<thead>
|
|
@@ -59,7 +63,7 @@ export function DataTable<TData>({
|
|
|
59
63
|
<th
|
|
60
64
|
key={header.id}
|
|
61
65
|
className={cn(
|
|
62
|
-
'bg-[#f5f7fa] h-[34px] px-[10px] text-left text-[14px] font-normal text-[#030303] border-b border-[#e0e0e0] align-middle whitespace-nowrap',
|
|
66
|
+
'sticky top-0 z-[1] bg-[#f5f7fa] h-[34px] px-[10px] text-left text-[14px] font-normal text-[#030303] border-b border-[#e0e0e0] align-middle whitespace-nowrap',
|
|
63
67
|
index !== 0 && 'border-l border-[#e0e0e0]',
|
|
64
68
|
)}
|
|
65
69
|
style={{
|