@baishuyun/ai-design 2.0.47 → 2.0.49

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 (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -4
  3. package/src/components/agent-drawer.tsx +40 -6
  4. package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
  5. package/src/components/agent-helper/app-select/index.tsx +7 -7
  6. package/src/components/agent-helper/condition-item/index.tsx +1 -0
  7. package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
  8. package/src/components/agent-helper/field-value-input/index.tsx +3 -2
  9. package/src/components/agent-helper/field-value-input/picker-inputs.tsx +1 -1
  10. package/src/components/agent-helper/form-select/index.tsx +8 -5
  11. package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
  12. package/src/components/agent-helper/table/index.tsx +6 -2
  13. package/src/components/agent-select.tsx +115 -35
  14. package/src/components/card.tsx +19 -6
  15. package/src/components/fields-portal.tsx +2 -2
  16. package/src/components/fields-previewer.tsx +1 -0
  17. package/src/components/legacy-slot.tsx +11 -16
  18. package/src/components/output-config-editor.tsx +32 -19
  19. package/src/components/shadow-content.tsx +19 -0
  20. package/src/components/shadow-portal.tsx +23 -0
  21. package/src/components/ui/dialog.tsx +4 -1
  22. package/src/components/ui/drawer.tsx +3 -1
  23. package/src/components/ui/popover.tsx +3 -2
  24. package/src/components/ui/select.tsx +3 -2
  25. package/src/components/ui/tooltip.tsx +3 -2
  26. package/src/const/shadow.ts +12 -0
  27. package/src/hooks/use-portal-container.ts +6 -3
  28. package/src/hooks/use-shadow-boundary.ts +64 -0
  29. package/src/hooks/use-shadow.ts +24 -7
  30. package/src/index.ts +2 -0
  31. package/src/stories/AppSelect.stories.tsx +44 -0
  32. package/src/stories/FieldValueInput.stories.tsx +6 -3
  33. package/src/stories/FormSelect.stories.tsx +40 -0
  34. package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
  35. package/src/style.css +28 -1
  36. package/style.css +153 -126
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @baishuyun/ai-design
2
2
 
3
+ ## 2.0.49
4
+
5
+ ### Patch Changes
6
+
7
+ - mobile style
8
+ - Updated dependencies
9
+ - @baishuyun/ai-feat-utils@2.0.44
10
+ - @baishuyun/types@3.0.41
11
+
12
+ ## 2.0.48
13
+
14
+ ### Patch Changes
15
+
16
+ - fix style and other bugs
17
+ - Updated dependencies
18
+ - @baishuyun/ai-feat-utils@2.0.43
19
+ - @baishuyun/types@3.0.40
20
+
3
21
  ## 2.0.47
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/ai-design",
3
- "version": "2.0.47",
3
+ "version": "2.0.49",
4
4
  "description": "AI 功能 UI 组件库 - 所有 AI 功能的 UI 表现唯一真相来源",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -60,8 +60,8 @@
60
60
  "tw-animate-css": "^1.4.0",
61
61
  "vaul": "^1.1.2",
62
62
  "zod": "^4.1.13",
63
- "@baishuyun/ai-feat-utils": "2.0.42",
64
- "@baishuyun/types": "3.0.39"
63
+ "@baishuyun/ai-feat-utils": "2.0.44",
64
+ "@baishuyun/types": "3.0.41"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "react": "^19.0.0",
@@ -84,7 +84,7 @@
84
84
  "vite": "^5.1.4",
85
85
  "vite-plugin-dts": "^4.5.4",
86
86
  "@baishuyun/tailwind-config": "3.0.9",
87
- "@baishuyun/types": "3.0.39",
87
+ "@baishuyun/types": "3.0.41",
88
88
  "@baishuyun/typescript-config": "2.0.9"
89
89
  },
90
90
  "keywords": [],
@@ -17,8 +17,20 @@ export interface AgentDrawerProps {
17
17
  trigger?: React.ReactNode;
18
18
  }
19
19
 
20
+ /** 关闭图标(设计稿 40434:34111,20×20,#121111) */
21
+ const CloseIcon = () => (
22
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
23
+ <path
24
+ d="M11.2584 9.99867L17.874 3.37975C18.0348 3.21757 18.125 2.99839 18.125 2.76996C18.125 2.54153 18.0348 2.32235 17.874 2.16017L17.8388 2.12495C17.6764 1.96522 17.4578 1.87572 17.23 1.87572C17.0023 1.87572 16.7837 1.96522 16.6213 2.12495L10.0027 8.75414L3.38708 2.12495C3.22461 1.96478 3.00568 1.875 2.77759 1.875C2.54951 1.875 2.33057 1.96478 2.1681 2.12495L2.13143 2.16017C2.05026 2.23946 1.98576 2.33419 1.94172 2.43878C1.89769 2.54338 1.875 2.65573 1.875 2.76923C1.875 2.88272 1.89769 2.99507 1.94172 3.09967C1.98576 3.20427 2.05026 3.299 2.13143 3.37828L8.74706 9.99867L2.13143 16.6205C1.97109 16.7826 1.88115 17.0015 1.88115 17.2296C1.88115 17.4577 1.97109 17.6765 2.13143 17.8386L2.1681 17.8739C2.33014 18.0343 2.5489 18.1243 2.77686 18.1243C3.00482 18.1243 3.22358 18.0343 3.38561 17.8739L10.0027 11.2549L16.6183 17.8739C16.7804 18.0347 16.9995 18.125 17.2278 18.125C17.4561 18.125 17.6752 18.0347 17.8373 17.8739L17.874 17.8386C18.0343 17.6765 18.1243 17.4577 18.1243 17.2296C18.1243 17.0015 18.0343 16.7826 17.874 16.6205L11.2598 9.99867H11.2584Z"
25
+ fill="#121111"
26
+ />
27
+ </svg>
28
+ );
29
+
20
30
  /**
21
- * 智能体选择抽屉(默认从右侧滑入),复用 AgentSelect 列表,选中后自动关闭。
31
+ * 智能体选择抽屉(移动端从底部滑出),复用 AgentSelect 列表,选中后自动关闭。
32
+ * 视觉对齐设计稿 40434:34107:顶部圆角面板 +「全部智能体」18px 标题 +
33
+ * 关闭按钮 + 搜索框 + 全宽列表行。
22
34
  * shadow DOM 兼容逻辑统一由 EnhancedDrawer 处理。
23
35
  */
24
36
  export const AgentDrawer = ({
@@ -60,14 +72,35 @@ export const AgentDrawer = ({
60
72
  <EnhancedDrawer
61
73
  open={open}
62
74
  onOpenChange={setOpen}
63
- direction="right"
64
- width={300}
75
+ direction="bottom"
65
76
  title="选择智能体"
77
+ // DrawerContent 内联 style 强制 borderRadius: 0(会压掉方向圆角),
78
+ // 用 !important 任意值恢复设计稿的顶部 10px 圆角
79
+ className="rounded-t-[10px]!"
66
80
  >
67
- <div className="h-full overflow-hidden p-[12px]">
81
+ <div className="flex flex-col">
82
+ {/* 头部:标题 18px Bold + 关闭(设计稿 40434:34109,上 20、下 20、左右 12) */}
83
+ <div className="flex items-center justify-between px-[12px] pt-[20px] pb-[20px]">
84
+ <span className="text-[18px] font-bold text-[#030303] leading-[normal]">
85
+ 全部智能体
86
+ </span>
87
+ <button
88
+ type="button"
89
+ aria-label="关闭"
90
+ className="shrink-0 size-[20px] border-none bg-transparent p-0 text-[#121111] cursor-pointer"
91
+ onClick={() => setOpen(false)}
92
+ >
93
+ <CloseIcon />
94
+ </button>
95
+ </div>
96
+ {/* 搜索 + 列表(样式见 AgentSelect large;85px ≈ 头部块高度;
97
+ 底部 16px 内边距,设计稿 40654:39900) */}
68
98
  <div
69
- className="overflow-y-auto"
70
- style={{ maxHeight: 'calc(100vh - 60px)', scrollbarWidth: 'none' }}
99
+ className="overflow-y-auto pb-[16px]"
100
+ style={{
101
+ maxHeight: 'calc(100vh - 52px - 85px)',
102
+ scrollbarWidth: 'none',
103
+ }}
71
104
  data-vaul-no-drag
72
105
  >
73
106
  <AgentSelect
@@ -76,6 +109,7 @@ export const AgentDrawer = ({
76
109
  onSelect={handleSelect}
77
110
  showSearch={showSearch}
78
111
  showAllItems
112
+ large
79
113
  />
80
114
  </div>
81
115
  </div>
@@ -118,11 +118,11 @@ export const AgentHelperCard = ({
118
118
  )}
119
119
  >
120
120
  {/* Header */}
121
- <div className="grid-rows-[max-content] inline-grid leading-[0] place-items-start shrink-0 w-full">
121
+ <div className="grid-cols-1 grid-rows-[max-content] inline-grid leading-[0] place-items-start shrink-0 w-full">
122
122
  <div className="col-1 content-stretch flex h-[42px] items-center justify-between ml-0 mt-0 px-[12px] row-1 w-full">
123
- <div className="content-stretch flex gap-[6px] items-center shrink-0 min-w-0">
123
+ <div className="content-stretch flex gap-[6px] items-center min-w-0">
124
124
  <TriggerTypeIcon triggerType={helper.triggerType} size={18} />
125
- <p className="leading-[normal] shrink-0 text-[#030303] text-[14px] truncate">
125
+ <p className="leading-[normal] text-[#030303] text-[14px] truncate">
126
126
  {helper.helperName}
127
127
  </p>
128
128
  </div>
@@ -440,9 +440,9 @@ export const AppSelect = ({
440
440
  const hasMultiSelection = mode === 'multiple' && multiSelectedLabels.length > 0;
441
441
 
442
442
  const popoverContent = (
443
- <div className={classNames('flex flex-col gap-[4px]', contentCls)}>
443
+ <div className={classNames('flex min-h-0 flex-col gap-[4px]', contentCls)}>
444
444
  {/* Search */}
445
- <div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
445
+ <div className="flex shrink-0 items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
446
446
  <SearchIcon />
447
447
  <input
448
448
  type="text"
@@ -462,7 +462,7 @@ export const AppSelect = ({
462
462
  setOpen(false);
463
463
  }}
464
464
  className={cn(
465
- 'flex items-center w-full h-[34px] rounded-[4px] text-left',
465
+ 'flex shrink-0 items-center w-full h-[34px] rounded-[4px] text-left',
466
466
  'px-[10px]',
467
467
  transCls,
468
468
  'text-[#030303] hover:bg-[#EFF0F6]'
@@ -481,7 +481,7 @@ export const AppSelect = ({
481
481
  setOpen(false);
482
482
  }}
483
483
  className={cn(
484
- 'flex items-center w-full h-[34px] rounded-[4px] text-left',
484
+ 'flex shrink-0 items-center w-full h-[34px] rounded-[4px] text-left',
485
485
  'px-[10px]',
486
486
  transCls,
487
487
  selectedIds.has(ALL_APPS_VALUE)
@@ -494,7 +494,7 @@ export const AppSelect = ({
494
494
  )}
495
495
 
496
496
  {/* Tree */}
497
- <div className={classNames('overflow-y-auto', maxTreeHeightCls)}>
497
+ <div className={classNames('min-h-0 overflow-y-auto', maxTreeHeightCls)}>
498
498
  {filteredNodes.length > 0 ? (
499
499
  filteredNodes.map((node) => renderNodeRow(node, 0))
500
500
  ) : (
@@ -583,8 +583,8 @@ export const AppSelect = ({
583
583
  align="start"
584
584
  sideOffset={4}
585
585
  portalContainer={portalContainer}
586
- className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
587
- style={{ minWidth: triggerWidth || undefined }}
586
+ className="flex flex-col overflow-hidden !border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
587
+ style={{ minWidth: triggerWidth || undefined, maxHeight: 300 }}
588
588
  >
589
589
  {popoverContent}
590
590
  </PopoverContent>
@@ -183,6 +183,7 @@ export const ConditionItem = ({
183
183
  value={currentOperator}
184
184
  onChange={handleOperatorChange}
185
185
  operators={availableOperators}
186
+ fieldType={fieldType}
186
187
  borderless
187
188
  disabled={disabled}
188
189
  className="w-[120px] shrink-0"
@@ -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 <AsyncPicker {...props} kind="dept" multiple={operator === 'in' || operator === 'nin'} />;
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 <AsyncPicker {...props} kind="deptgroup" multiple={operator === 'in' || operator === 'nin' || operator === 'all'} />;
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) => {