@baishuyun/ai-design 2.0.41 → 2.0.42
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 +9 -0
- package/package.json +4 -4
- package/src/components/agent-drawer.tsx +85 -0
- 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/font-icon.tsx +13 -10
- package/src/components/ui/enhanced-drawer.tsx +134 -0
- package/src/index.ts +18 -0
- package/src/stories/IconfontPicker.stories.tsx +106 -0
- package/src/stories/TeamWideFormSelect.stories.tsx +362 -0
- package/style.css +6 -0
package/.storybook/preview.ts
CHANGED
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.42",
|
|
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.37",
|
|
64
|
+
"@baishuyun/types": "3.0.34"
|
|
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.34",
|
|
88
88
|
"@baishuyun/typescript-config": "2.0.9"
|
|
89
89
|
},
|
|
90
90
|
"keywords": [],
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { EnhancedDrawer } from './ui/enhanced-drawer';
|
|
3
|
+
import { AgentSelect } from './agent-select';
|
|
4
|
+
import type { AgentSelectAgent } from './agent-select';
|
|
5
|
+
|
|
6
|
+
export interface AgentDrawerProps {
|
|
7
|
+
agents: Array<AgentSelectAgent>;
|
|
8
|
+
activeAgent?: { id: string } | null;
|
|
9
|
+
onSelect: (agent: AgentSelectAgent) => void | Promise<void>;
|
|
10
|
+
/** 受控开关 */
|
|
11
|
+
open?: boolean;
|
|
12
|
+
/** 非受控默认开关 */
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
showSearch?: boolean;
|
|
16
|
+
/** 触发器(可选;不传则完全由 open 控制) */
|
|
17
|
+
trigger?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 智能体选择抽屉(默认从右侧滑入),复用 AgentSelect 列表,选中后自动关闭。
|
|
22
|
+
* shadow DOM 兼容逻辑统一由 EnhancedDrawer 处理。
|
|
23
|
+
*/
|
|
24
|
+
export const AgentDrawer = ({
|
|
25
|
+
agents,
|
|
26
|
+
activeAgent,
|
|
27
|
+
onSelect,
|
|
28
|
+
open: controlledOpen,
|
|
29
|
+
defaultOpen,
|
|
30
|
+
onOpenChange,
|
|
31
|
+
showSearch = true,
|
|
32
|
+
trigger,
|
|
33
|
+
}: AgentDrawerProps) => {
|
|
34
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
|
|
35
|
+
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
|
36
|
+
|
|
37
|
+
const setOpen = useCallback(
|
|
38
|
+
(next: boolean) => {
|
|
39
|
+
if (controlledOpen === undefined) setInternalOpen(next);
|
|
40
|
+
onOpenChange?.(next);
|
|
41
|
+
},
|
|
42
|
+
[controlledOpen, onOpenChange]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const handleSelect = useCallback(
|
|
46
|
+
async (agent: AgentSelectAgent) => {
|
|
47
|
+
await onSelect(agent);
|
|
48
|
+
setOpen(false);
|
|
49
|
+
},
|
|
50
|
+
[onSelect, setOpen]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
{trigger && (
|
|
56
|
+
<span onClick={() => setOpen(true)} style={{ display: 'contents' }}>
|
|
57
|
+
{trigger}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
<EnhancedDrawer
|
|
61
|
+
open={open}
|
|
62
|
+
onOpenChange={setOpen}
|
|
63
|
+
direction="right"
|
|
64
|
+
width={300}
|
|
65
|
+
title="选择智能体"
|
|
66
|
+
>
|
|
67
|
+
<div className="h-full overflow-hidden p-[12px]">
|
|
68
|
+
<div
|
|
69
|
+
className="overflow-y-auto"
|
|
70
|
+
style={{ maxHeight: 'calc(100vh - 60px)', scrollbarWidth: 'none' }}
|
|
71
|
+
data-vaul-no-drag
|
|
72
|
+
>
|
|
73
|
+
<AgentSelect
|
|
74
|
+
agents={agents}
|
|
75
|
+
activeAgent={activeAgent}
|
|
76
|
+
onSelect={handleSelect}
|
|
77
|
+
showSearch={showSearch}
|
|
78
|
+
showAllItems
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</EnhancedDrawer>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IAllAppsAndFormsData,
|
|
3
|
+
IAppFormListEntry,
|
|
4
|
+
IAppWithFormList,
|
|
5
|
+
} from '@baishuyun/types';
|
|
6
|
+
import type { TeamWideFormTreeNode, TeamWideFormTreeSection } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 数据适配:/dashboard/app/form/list 返回 → 树形结构。
|
|
10
|
+
*
|
|
11
|
+
* 树层级:应用分组 → 应用 → 表单分组 → 表单(表单分组可嵌套)。
|
|
12
|
+
* 传入 currentAppId 时拆成「当前应用表单 / 其他应用表单」两个 section,
|
|
13
|
+
* 当前应用的条目不再带应用/分组层级(平铺到 section 根)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 条目(含嵌套分组)→ 树节点 */
|
|
17
|
+
function entryToNode(entry: IAppFormListEntry): TeamWideFormTreeNode {
|
|
18
|
+
const isGroup = entry.type === 'group';
|
|
19
|
+
return {
|
|
20
|
+
id: entry.entryId,
|
|
21
|
+
label: entry.name,
|
|
22
|
+
type: isGroup ? 'formGroup' : 'form',
|
|
23
|
+
formIcon: entry.icon,
|
|
24
|
+
data: isGroup ? undefined : entry,
|
|
25
|
+
children: entry.child?.map(entryToNode),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 应用 formList → 表单/分组树节点列表(聚合 form/dash/aggregate/etl 各类) */
|
|
30
|
+
function appFormNodes(app: IAppWithFormList): TeamWideFormTreeNode[] {
|
|
31
|
+
const fl = app.formList;
|
|
32
|
+
if (!fl) return [];
|
|
33
|
+
|
|
34
|
+
const nodes: TeamWideFormTreeNode[] = [];
|
|
35
|
+
const pushEntries = (entries: IAppFormListEntry[] | null) => {
|
|
36
|
+
if (entries?.length) {
|
|
37
|
+
nodes.push(...entries.map(entryToNode));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
pushEntries(fl.form);
|
|
42
|
+
pushEntries(fl.dash);
|
|
43
|
+
pushEntries(fl.aggregate_table);
|
|
44
|
+
pushEntries(fl.etl_table);
|
|
45
|
+
|
|
46
|
+
return nodes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 应用 → 树节点(含其子树) */
|
|
50
|
+
function appToNode(app: IAppWithFormList): TeamWideFormTreeNode {
|
|
51
|
+
return {
|
|
52
|
+
id: app._id,
|
|
53
|
+
label: app.appname,
|
|
54
|
+
type: 'app',
|
|
55
|
+
appIcon: app.style?.icon,
|
|
56
|
+
children: appFormNodes(app),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 全量数据 → 树形 section 列表。
|
|
62
|
+
* - 不传 currentAppId:单个 'all' section,全量树(应用分组→应用→表单分组→表单)
|
|
63
|
+
* - 传 currentAppId:'current'(当前应用条目,无应用层级)+ 'other'(其余应用全量树)
|
|
64
|
+
*/
|
|
65
|
+
export function appsAndFormsToTree(
|
|
66
|
+
data: IAllAppsAndFormsData | null | undefined,
|
|
67
|
+
currentAppId?: string
|
|
68
|
+
): TeamWideFormTreeSection[] {
|
|
69
|
+
const groups = data?.items ?? [];
|
|
70
|
+
|
|
71
|
+
if (!currentAppId) {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
key: 'all',
|
|
75
|
+
nodes: groups.map((group) => ({
|
|
76
|
+
id: group._id,
|
|
77
|
+
label: group.name,
|
|
78
|
+
type: 'appGroup' as const,
|
|
79
|
+
children: (group.apps ?? []).map(appToNode),
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 当前应用:只取它的条目树(无应用/分组层级)
|
|
86
|
+
let currentNodes: TeamWideFormTreeNode[] = [];
|
|
87
|
+
for (const group of groups) {
|
|
88
|
+
const target = (group.apps ?? []).find((a) => a._id === currentAppId);
|
|
89
|
+
if (target) {
|
|
90
|
+
currentNodes = appFormNodes(target);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 其他应用:全量树,剔除当前应用(空应用分组保留)
|
|
96
|
+
const otherNodes: TeamWideFormTreeNode[] = groups.map((group) => ({
|
|
97
|
+
id: group._id,
|
|
98
|
+
label: group.name,
|
|
99
|
+
type: 'appGroup' as const,
|
|
100
|
+
children: (group.apps ?? [])
|
|
101
|
+
.filter((a) => a._id !== currentAppId)
|
|
102
|
+
.map(appToNode),
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
{ key: 'current', title: '当前应用表单', nodes: currentNodes },
|
|
107
|
+
{ key: 'other', title: '其他应用表单', nodes: otherNodes },
|
|
108
|
+
];
|
|
109
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { cn } from '../../../lib/utils';
|
|
3
|
+
import { transCls } from '../../../const/ui';
|
|
4
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
|
|
5
|
+
import { EnhancedDrawer } from '../../ui/enhanced-drawer';
|
|
6
|
+
import { appsAndFormsToTree } from './data-adapter';
|
|
7
|
+
import { TeamWideFormTreePanel } from './tree-panel';
|
|
8
|
+
import type { IAllAppsAndFormsData } from '@baishuyun/types';
|
|
9
|
+
import type {
|
|
10
|
+
TeamWideFormSelectChangeResult,
|
|
11
|
+
TeamWideFormTreeNode,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
// ── Shared ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** 下拉箭头(trigger 用) */
|
|
17
|
+
const DropdownChevron = ({ open }: { open: boolean }) => (
|
|
18
|
+
<svg
|
|
19
|
+
width="16" height="16" viewBox="0 0 16 16" fill="none"
|
|
20
|
+
className={cn('shrink-0', transCls)}
|
|
21
|
+
style={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
|
22
|
+
>
|
|
23
|
+
<path d="M4 6L8 10L12 6" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** 在 sections 里按 entryId 找选中节点的 label / formIcon */
|
|
28
|
+
function findSelected(
|
|
29
|
+
sections: ReturnType<typeof appsAndFormsToTree>,
|
|
30
|
+
entryId?: string
|
|
31
|
+
): { label: string; formIcon?: number } | null {
|
|
32
|
+
if (!entryId) return null;
|
|
33
|
+
let found: { label: string; formIcon?: number } | null = null;
|
|
34
|
+
const walk = (nodes: TeamWideFormTreeNode[]) => {
|
|
35
|
+
for (const n of nodes) {
|
|
36
|
+
if (found) return;
|
|
37
|
+
if (n.type === 'form' && n.id === entryId) {
|
|
38
|
+
found = { label: n.label, formIcon: n.formIcon };
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (n.children) walk(n.children);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
sections.forEach((s) => walk(s.nodes));
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface BaseProps {
|
|
49
|
+
/** 全量应用+表单数据(/dashboard/app/form/list 返回的 data 字段) */
|
|
50
|
+
data: IAllAppsAndFormsData | null | undefined;
|
|
51
|
+
/** 当前应用 ID:传入后按「当前应用表单 / 其他应用表单」分组展示 */
|
|
52
|
+
currentAppId?: string;
|
|
53
|
+
/** 选中的 entryId(受控) */
|
|
54
|
+
value?: string;
|
|
55
|
+
/** 选中回调(返回 appId + entryId + 原始条目) */
|
|
56
|
+
onChange?: (result: TeamWideFormSelectChangeResult) => void;
|
|
57
|
+
searchPlaceholder?: string;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── 1. 下拉选择 ───────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export interface TeamWideFormSelectProps extends BaseProps {
|
|
64
|
+
/** 非受控默认值 */
|
|
65
|
+
defaultValue?: string;
|
|
66
|
+
placeholder?: string;
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 全量表单选择(下拉形式):数据源为一次拉全的应用+表单接口,
|
|
72
|
+
* 树形展示 应用分组→应用→表单分组→表单。
|
|
73
|
+
*/
|
|
74
|
+
export const TeamWideFormSelect = ({
|
|
75
|
+
data,
|
|
76
|
+
currentAppId,
|
|
77
|
+
value: controlledValue,
|
|
78
|
+
defaultValue,
|
|
79
|
+
onChange,
|
|
80
|
+
placeholder = '请选择表单',
|
|
81
|
+
searchPlaceholder,
|
|
82
|
+
disabled = false,
|
|
83
|
+
className,
|
|
84
|
+
}: TeamWideFormSelectProps) => {
|
|
85
|
+
const [open, setOpen] = useState(false);
|
|
86
|
+
const [internalValue, setInternalValue] = useState(defaultValue ?? '');
|
|
87
|
+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
88
|
+
const [triggerWidth, setTriggerWidth] = useState(0);
|
|
89
|
+
const triggerBtnRef = useRef<HTMLButtonElement | null>(null);
|
|
90
|
+
|
|
91
|
+
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
92
|
+
const sections = useMemo(
|
|
93
|
+
() => appsAndFormsToTree(data, currentAppId),
|
|
94
|
+
[data, currentAppId]
|
|
95
|
+
);
|
|
96
|
+
const selected = useMemo(
|
|
97
|
+
() => findSelected(sections, value),
|
|
98
|
+
[sections, value]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
|
|
102
|
+
triggerBtnRef.current = node;
|
|
103
|
+
if (node) setTriggerWidth(node.offsetWidth);
|
|
104
|
+
if (!node) {
|
|
105
|
+
setPortalContainer(null);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rootNode = node.getRootNode();
|
|
109
|
+
if (rootNode instanceof ShadowRoot) {
|
|
110
|
+
const existing = rootNode.querySelector<HTMLElement>(
|
|
111
|
+
'[data-bs-team-form-select-portal]'
|
|
112
|
+
);
|
|
113
|
+
if (existing) {
|
|
114
|
+
setPortalContainer(existing);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const container = document.createElement('div');
|
|
118
|
+
container.setAttribute('data-bs-team-form-select-portal', 'true');
|
|
119
|
+
rootNode.appendChild(container);
|
|
120
|
+
setPortalContainer(container);
|
|
121
|
+
} else {
|
|
122
|
+
setPortalContainer(null);
|
|
123
|
+
}
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const handleChange = useCallback(
|
|
127
|
+
(result: TeamWideFormSelectChangeResult) => {
|
|
128
|
+
if (controlledValue === undefined) setInternalValue(result.entryId);
|
|
129
|
+
onChange?.(result);
|
|
130
|
+
setOpen(false);
|
|
131
|
+
},
|
|
132
|
+
[controlledValue, onChange]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={cn('relative', className)}>
|
|
137
|
+
<Popover
|
|
138
|
+
open={open}
|
|
139
|
+
onOpenChange={(next) => {
|
|
140
|
+
if (disabled) return;
|
|
141
|
+
setOpen(next);
|
|
142
|
+
if (next && triggerBtnRef.current) {
|
|
143
|
+
setTriggerWidth(triggerBtnRef.current.offsetWidth);
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<PopoverTrigger asChild>
|
|
148
|
+
<button
|
|
149
|
+
ref={setTriggerRef}
|
|
150
|
+
type="button"
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
className={cn(
|
|
153
|
+
'flex items-center justify-between w-full h-[34px] gap-2.5 px-[10px]',
|
|
154
|
+
'bg-white border border-[#E0E0E0] rounded-[4px]',
|
|
155
|
+
'text-left cursor-pointer hover:border-[#0265FF]',
|
|
156
|
+
disabled && 'opacity-50 cursor-not-allowed hover:border-[#E0E0E0]',
|
|
157
|
+
open && 'border-[#0265FF]',
|
|
158
|
+
transCls
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
<span
|
|
162
|
+
className={cn(
|
|
163
|
+
'text-[14px] leading-[normal] truncate',
|
|
164
|
+
selected ? 'text-[#030303]' : 'text-[#999999]'
|
|
165
|
+
)}
|
|
166
|
+
>
|
|
167
|
+
{selected?.label ?? placeholder}
|
|
168
|
+
</span>
|
|
169
|
+
<DropdownChevron open={open} />
|
|
170
|
+
</button>
|
|
171
|
+
</PopoverTrigger>
|
|
172
|
+
<PopoverContent
|
|
173
|
+
align="start"
|
|
174
|
+
sideOffset={4}
|
|
175
|
+
portalContainer={portalContainer}
|
|
176
|
+
className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[12px]"
|
|
177
|
+
style={{ minWidth: triggerWidth || undefined }}
|
|
178
|
+
>
|
|
179
|
+
<TeamWideFormTreePanel
|
|
180
|
+
sections={sections}
|
|
181
|
+
value={value}
|
|
182
|
+
onSelect={handleChange}
|
|
183
|
+
searchPlaceholder={searchPlaceholder}
|
|
184
|
+
/>
|
|
185
|
+
</PopoverContent>
|
|
186
|
+
</Popover>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ── 2. 左侧滑入抽屉 ───────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
export interface TeamWideFormDrawerProps extends BaseProps {
|
|
194
|
+
/** 受控开关 */
|
|
195
|
+
open?: boolean;
|
|
196
|
+
/** 非受控默认开关 */
|
|
197
|
+
defaultOpen?: boolean;
|
|
198
|
+
onOpenChange?: (open: boolean) => void;
|
|
199
|
+
/** 触发器(可选;不传则完全由 open 控制) */
|
|
200
|
+
trigger?: React.ReactNode;
|
|
201
|
+
/** 数据加载状态:加载中/失败时替代树区域展示(不传则按 data 直接渲染树) */
|
|
202
|
+
status?: 'loading' | 'error';
|
|
203
|
+
/** status === 'error' 时展示重试按钮,点击回调 */
|
|
204
|
+
onRetry?: () => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 全量表单选择(左侧滑入抽屉):选中表单后自动关闭。
|
|
209
|
+
* 视觉对齐 Figma:左侧 300px 白色面板 + 半透明蒙版。
|
|
210
|
+
*/
|
|
211
|
+
export const TeamWideFormDrawer = ({
|
|
212
|
+
data,
|
|
213
|
+
currentAppId,
|
|
214
|
+
value,
|
|
215
|
+
onChange,
|
|
216
|
+
open: controlledOpen,
|
|
217
|
+
defaultOpen,
|
|
218
|
+
onOpenChange,
|
|
219
|
+
searchPlaceholder,
|
|
220
|
+
trigger,
|
|
221
|
+
status,
|
|
222
|
+
onRetry,
|
|
223
|
+
}: TeamWideFormDrawerProps) => {
|
|
224
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
|
|
225
|
+
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
|
226
|
+
|
|
227
|
+
const sections = useMemo(
|
|
228
|
+
() => appsAndFormsToTree(data, currentAppId),
|
|
229
|
+
[data, currentAppId]
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const setOpen = useCallback(
|
|
233
|
+
(next: boolean) => {
|
|
234
|
+
if (controlledOpen === undefined) setInternalOpen(next);
|
|
235
|
+
onOpenChange?.(next);
|
|
236
|
+
},
|
|
237
|
+
[controlledOpen, onOpenChange]
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const handleChange = useCallback(
|
|
241
|
+
(result: TeamWideFormSelectChangeResult) => {
|
|
242
|
+
onChange?.(result);
|
|
243
|
+
setOpen(false);
|
|
244
|
+
},
|
|
245
|
+
[onChange, setOpen]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<>
|
|
250
|
+
{trigger && <span onClick={() => setOpen(true)}>{trigger}</span>}
|
|
251
|
+
<EnhancedDrawer
|
|
252
|
+
open={open}
|
|
253
|
+
onOpenChange={setOpen}
|
|
254
|
+
direction="left"
|
|
255
|
+
width={300}
|
|
256
|
+
title="切换表单"
|
|
257
|
+
>
|
|
258
|
+
<div className="h-full overflow-hidden p-[12px]">
|
|
259
|
+
{status === 'error' ? (
|
|
260
|
+
<div className="flex flex-col items-center justify-center h-full gap-[12px]">
|
|
261
|
+
<span className="text-[12px] text-[#999999]">应用/表单加载失败</span>
|
|
262
|
+
{onRetry && (
|
|
263
|
+
<button
|
|
264
|
+
type="button"
|
|
265
|
+
onClick={onRetry}
|
|
266
|
+
className="h-[30px] px-[16px] rounded-[4px] border border-[#E0E0E0] text-[14px] text-[#0265FF] active:bg-[#F0F6FF]"
|
|
267
|
+
>
|
|
268
|
+
重试
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
) : status === 'loading' ? (
|
|
273
|
+
<div className="flex items-center justify-center h-full text-[12px] text-[#999999]">
|
|
274
|
+
加载中…
|
|
275
|
+
</div>
|
|
276
|
+
) : (
|
|
277
|
+
<TeamWideFormTreePanel
|
|
278
|
+
sections={sections}
|
|
279
|
+
value={value}
|
|
280
|
+
onSelect={handleChange}
|
|
281
|
+
searchPlaceholder={searchPlaceholder}
|
|
282
|
+
maxHeight="calc(100vh - 60px)"
|
|
283
|
+
/>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</EnhancedDrawer>
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
};
|