@baishuyun/ai-design 2.0.48 → 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.
- package/CHANGELOG.md +9 -0
- package/package.json +4 -4
- package/src/components/agent-drawer.tsx +40 -6
- package/src/components/agent-helper/condition-item/index.tsx +1 -0
- 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 +1 -1
- 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
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baishuyun/ai-design",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
64
|
-
"@baishuyun/types": "3.0.
|
|
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.
|
|
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
|
-
*
|
|
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="
|
|
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="
|
|
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={{
|
|
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>
|
|
@@ -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) => {
|