openxiangda-cli 2.0.0-alpha.86 → 2.0.0-alpha.90

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 (78) hide show
  1. package/README.md +2 -2
  2. package/bin/run.js +1 -0
  3. package/dist/commands/skill.d.ts +26 -0
  4. package/dist/commands/skill.d.ts.map +1 -0
  5. package/dist/commands/skill.js +43 -0
  6. package/dist/commands/skill.js.map +1 -0
  7. package/dist/create-workspace.d.ts.map +1 -1
  8. package/dist/create-workspace.js +3 -3
  9. package/dist/create-workspace.js.map +1 -1
  10. package/dist/toolchain-capsule.d.ts.map +1 -1
  11. package/dist/toolchain-capsule.js +4 -8
  12. package/dist/toolchain-capsule.js.map +1 -1
  13. package/package.json +6 -7
  14. package/template/AGENTS.md +2 -1
  15. package/template/README.md +5 -5
  16. package/template/apps/server/package.json +1 -1
  17. package/template/apps/server/src/app.module.ts +14 -1
  18. package/template/apps/server/src/main.ts +1 -1
  19. package/template/apps/server/test/smoke.test.ts +3 -1
  20. package/template/apps/web/e2e/client-fixture.ts +5 -0
  21. package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
  22. package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
  23. package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
  24. package/template/apps/web/e2e/resources.spec.ts +20 -181
  25. package/template/apps/web/field-protocol.e2e.html +3 -0
  26. package/template/apps/web/package.json +2 -5
  27. package/template/apps/web/src/main.tsx +12 -60
  28. package/template/apps/web/test/contracts.test.ts +52 -104
  29. package/template/openxiangda.config.ts +2 -2
  30. package/template/package.json +1 -3
  31. package/template/packages/contracts/src/generated.ts +27 -0
  32. package/template/scripts/verify-template-budget.mjs +2 -2
  33. package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
  34. package/template/apps/web/src/FilePreviewPage.tsx +0 -324
  35. package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
  36. package/template/apps/web/src/Shell.tsx +0 -445
  37. package/template/apps/web/src/appearance.tsx +0 -147
  38. package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
  39. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
  40. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
  41. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
  42. package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
  43. package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
  44. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
  45. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
  46. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
  47. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
  48. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
  49. package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
  50. package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
  51. package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
  52. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
  53. package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
  54. package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
  55. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
  56. package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
  57. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
  58. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
  59. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
  60. package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
  61. package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
  62. package/template/apps/web/src/components/resource/resource-import.ts +0 -205
  63. package/template/apps/web/src/data-provider.ts +0 -72
  64. package/template/apps/web/src/platform-client.ts +0 -708
  65. package/template/apps/web/src/runtime-meta.ts +0 -75
  66. package/template/apps/web/src/runtime.tsx +0 -227
  67. package/template/apps/web/src/styles.css +0 -1941
  68. package/template/apps/web/test/address-field.test.ts +0 -66
  69. package/template/apps/web/test/cascade-value.test.ts +0 -68
  70. package/template/apps/web/test/directory-value.test.ts +0 -55
  71. package/template/apps/web/test/field-bounds.test.ts +0 -17
  72. package/template/apps/web/test/field-form-codec.test.ts +0 -85
  73. package/template/apps/web/test/location-field.test.ts +0 -83
  74. package/template/apps/web/test/resource-import.test.ts +0 -143
  75. package/template/apps/web/test/resource-query.test.ts +0 -144
  76. package/template/apps/web/test/rich-json-field.test.ts +0 -53
  77. package/template/apps/web/test/signature-serial-field.test.ts +0 -40
  78. package/template/apps/web/test/subtable-value.test.ts +0 -144
@@ -1,673 +0,0 @@
1
- import {
2
- ApartmentOutlined,
3
- CloseOutlined,
4
- SearchOutlined,
5
- UserOutlined,
6
- } from '@ant-design/icons';
7
- import {
8
- Avatar,
9
- Button,
10
- Checkbox,
11
- Drawer,
12
- Empty,
13
- Grid,
14
- Input,
15
- Modal,
16
- Space,
17
- Spin,
18
- Tag,
19
- Tree,
20
- Typography,
21
- type TreeDataNode,
22
- } from 'antd';
23
- import type {
24
- DirectoryEntry,
25
- } from 'openxiangda-contracts/browser';
26
- import {
27
- useCallback,
28
- useEffect,
29
- useMemo,
30
- useRef,
31
- useState,
32
- type Key,
33
- } from 'react';
34
- import {
35
- browseDepartmentTree,
36
- browseDepartmentUsers,
37
- searchDirectory,
38
- type DirectoryKind,
39
- } from '../../platform-client';
40
- import {
41
- directoryEntryFromStored,
42
- directoryStoredValueFromEntries,
43
- directoryStoredValues,
44
- type DirectoryStoredValue,
45
- } from './directory-value';
46
-
47
- type DepartmentNode = DirectoryEntry & { children?: DepartmentNode[] };
48
-
49
- function replaceChildren(
50
- nodes: DepartmentNode[],
51
- parentId: string,
52
- children: DepartmentNode[]
53
- ): DepartmentNode[] {
54
- return nodes.map(node =>
55
- node.id === parentId
56
- ? { ...node, children }
57
- : node.children
58
- ? { ...node, children: replaceChildren(node.children, parentId, children) }
59
- : node
60
- );
61
- }
62
-
63
- function departmentTreeData(nodes: DepartmentNode[]): TreeDataNode[] {
64
- return nodes.map(node => ({
65
- key: node.id,
66
- title: node.label,
67
- isLeaf: !node.hasChildren,
68
- children: node.children ? departmentTreeData(node.children) : undefined,
69
- }));
70
- }
71
-
72
- function displayDescription(entry: DirectoryEntry) {
73
- if (entry.description) return entry.description;
74
- if (entry.path?.length) return entry.path.map(item => item.label).join(' / ');
75
- return '';
76
- }
77
-
78
- export function PlatformDirectoryPicker({
79
- kind,
80
- value,
81
- onChange,
82
- multiple = false,
83
- disabled = false,
84
- placeholder,
85
- id,
86
- }: {
87
- kind: DirectoryKind;
88
- value?: DirectoryStoredValue | DirectoryStoredValue[];
89
- onChange?: (value: DirectoryStoredValue | DirectoryStoredValue[] | undefined) => void;
90
- multiple?: boolean;
91
- disabled?: boolean;
92
- placeholder: string;
93
- id?: string;
94
- }) {
95
- const screens = Grid.useBreakpoint();
96
- const mobile = !screens.md;
97
- const values = useMemo(() => directoryStoredValues(value), [value]);
98
- const [open, setOpen] = useState(false);
99
- const [selected, setSelected] = useState<DirectoryEntry[]>(() =>
100
- values.map(item => directoryEntryFromStored(kind, item))
101
- );
102
-
103
- useEffect(() => {
104
- setSelected(values.map(item => directoryEntryFromStored(kind, item)));
105
- }, [kind, values]);
106
-
107
- const remove = (entryId: string) => {
108
- const next = selected.filter(entry => entry.id !== entryId);
109
- setSelected(next);
110
- onChange?.(directoryStoredValueFromEntries(next, multiple));
111
- };
112
- const confirm = (items: DirectoryEntry[]) => {
113
- setSelected(items);
114
- onChange?.(directoryStoredValueFromEntries(items, multiple));
115
- setOpen(false);
116
- };
117
- const panel = (
118
- <DirectoryPickerPanel
119
- kind={kind}
120
- multiple={multiple}
121
- onCancel={() => setOpen(false)}
122
- onConfirm={confirm}
123
- value={selected}
124
- />
125
- );
126
-
127
- return (
128
- <>
129
- <div
130
- aria-label={placeholder}
131
- aria-disabled={disabled}
132
- className={`oxa-directory-trigger${disabled ? ' is-disabled' : ''}`}
133
- id={id}
134
- onClick={() => {
135
- if (!disabled) setOpen(true);
136
- }}
137
- onKeyDown={event => {
138
- if (!disabled && (event.key === 'Enter' || event.key === ' ')) {
139
- event.preventDefault();
140
- setOpen(true);
141
- }
142
- }}
143
- role="button"
144
- tabIndex={disabled ? -1 : 0}
145
- >
146
- <span className="oxa-directory-trigger-icon">
147
- {kind === 'user' ? <UserOutlined /> : <ApartmentOutlined />}
148
- </span>
149
- <span className="oxa-directory-trigger-values">
150
- {selected.length ? (
151
- selected.map(entry => (
152
- <Tag
153
- closable={!disabled}
154
- key={entry.id}
155
- onClose={event => {
156
- event.preventDefault();
157
- event.stopPropagation();
158
- remove(entry.id);
159
- }}
160
- >
161
- {entry.label}
162
- </Tag>
163
- ))
164
- ) : (
165
- <span className="oxa-directory-placeholder">{placeholder}</span>
166
- )}
167
- </span>
168
- <Button
169
- disabled={disabled}
170
- onClick={event => {
171
- event.stopPropagation();
172
- setOpen(true);
173
- }}
174
- size="small"
175
- type="link"
176
- >
177
- 选择
178
- </Button>
179
- </div>
180
- {mobile ? (
181
- <Drawer
182
- className="oxa-directory-mobile-drawer"
183
- destroyOnHidden
184
- onClose={() => setOpen(false)}
185
- open={open}
186
- placement="bottom"
187
- size="90vh"
188
- title={kind === 'user' ? '选择成员' : '选择部门'}
189
- >
190
- {panel}
191
- </Drawer>
192
- ) : (
193
- <Modal
194
- destroyOnHidden
195
- footer={null}
196
- onCancel={() => setOpen(false)}
197
- open={open}
198
- title={kind === 'user' ? '选择成员' : '选择部门'}
199
- width={kind === 'user' ? 980 : 760}
200
- >
201
- {panel}
202
- </Modal>
203
- )}
204
- </>
205
- );
206
- }
207
-
208
- function DirectoryPickerPanel({
209
- kind,
210
- value,
211
- multiple,
212
- onConfirm,
213
- onCancel,
214
- }: {
215
- kind: DirectoryKind;
216
- value: DirectoryEntry[];
217
- multiple: boolean;
218
- onConfirm: (items: DirectoryEntry[]) => void;
219
- onCancel: () => void;
220
- }) {
221
- const [selected, setSelected] = useState(value);
222
- useEffect(() => setSelected(value), [value]);
223
- return kind === 'user' ? (
224
- <MemberPanel
225
- multiple={multiple}
226
- onCancel={onCancel}
227
- onConfirm={onConfirm}
228
- selected={selected}
229
- setSelected={setSelected}
230
- />
231
- ) : (
232
- <DepartmentPanel
233
- multiple={multiple}
234
- onCancel={onCancel}
235
- onConfirm={onConfirm}
236
- selected={selected}
237
- setSelected={setSelected}
238
- />
239
- );
240
- }
241
-
242
- function useDepartmentTree() {
243
- const [nodes, setNodes] = useState<DepartmentNode[]>([]);
244
- const [loading, setLoading] = useState(false);
245
- const [error, setError] = useState('');
246
- const entries = useRef(new Map<string, DirectoryEntry>());
247
- const remember = useCallback((items: DirectoryEntry[]) => {
248
- for (const item of items) entries.current.set(item.id, item);
249
- }, []);
250
- const loadRoots = useCallback(async () => {
251
- setLoading(true);
252
- setError('');
253
- try {
254
- const page = await browseDepartmentTree({});
255
- remember(page.items);
256
- setNodes(page.items as DepartmentNode[]);
257
- } catch (reason) {
258
- setError(reason instanceof Error ? reason.message : String(reason));
259
- } finally {
260
- setLoading(false);
261
- }
262
- }, [remember]);
263
- const loadChildren = useCallback(
264
- async (parentId: string) => {
265
- const page = await browseDepartmentTree({ parentId });
266
- remember(page.items);
267
- setNodes(current =>
268
- replaceChildren(current, parentId, page.items as DepartmentNode[])
269
- );
270
- },
271
- [remember]
272
- );
273
- return { nodes, loading, error, entries, remember, loadRoots, loadChildren };
274
- }
275
-
276
- function MemberPanel({
277
- selected,
278
- setSelected,
279
- multiple,
280
- onConfirm,
281
- onCancel,
282
- }: {
283
- selected: DirectoryEntry[];
284
- setSelected: (items: DirectoryEntry[]) => void;
285
- multiple: boolean;
286
- onConfirm: (items: DirectoryEntry[]) => void;
287
- onCancel: () => void;
288
- }) {
289
- const tree = useDepartmentTree();
290
- const [departmentId, setDepartmentId] = useState('');
291
- const [keyword, setKeyword] = useState('');
292
- const [members, setMembers] = useState<DirectoryEntry[]>([]);
293
- const [loading, setLoading] = useState(false);
294
- const [error, setError] = useState('');
295
- const [page, setPage] = useState(1);
296
- const [searchCursors, setSearchCursors] = useState<Array<string | undefined>>([
297
- undefined,
298
- ]);
299
- const [nextCursor, setNextCursor] = useState<string | null>(null);
300
- const selectedIds = new Set(selected.map(item => item.id));
301
-
302
- useEffect(() => {
303
- void tree.loadRoots();
304
- }, [tree.loadRoots]);
305
- useEffect(() => {
306
- if (!departmentId && tree.nodes[0]) setDepartmentId(tree.nodes[0].id);
307
- }, [departmentId, tree.nodes]);
308
- useEffect(() => {
309
- setPage(1);
310
- setSearchCursors([undefined]);
311
- }, [departmentId, keyword]);
312
- useEffect(() => {
313
- const query = keyword.trim();
314
- if (query && query.length < 2) {
315
- setMembers([]);
316
- setNextCursor(null);
317
- return;
318
- }
319
- if (!query && !departmentId) return;
320
- let active = true;
321
- const timer = window.setTimeout(() => {
322
- setLoading(true);
323
- setError('');
324
- const request = query
325
- ? searchDirectory('user', {
326
- keyword: query,
327
- cursor: searchCursors[page - 1],
328
- })
329
- : browseDepartmentUsers(departmentId, page);
330
- void request
331
- .then(result => {
332
- if (!active) return;
333
- setMembers(result.items);
334
- setNextCursor(result.nextCursor);
335
- })
336
- .catch(reason => {
337
- if (active)
338
- setError(reason instanceof Error ? reason.message : String(reason));
339
- })
340
- .finally(() => {
341
- if (active) setLoading(false);
342
- });
343
- }, query ? 300 : 0);
344
- return () => {
345
- active = false;
346
- window.clearTimeout(timer);
347
- };
348
- }, [departmentId, keyword, page, searchCursors]);
349
-
350
- const toggle = (entry: DirectoryEntry, checked: boolean) => {
351
- if (multiple) {
352
- setSelected(
353
- checked
354
- ? selectedIds.has(entry.id)
355
- ? selected
356
- : [...selected, entry]
357
- : selected.filter(item => item.id !== entry.id)
358
- );
359
- } else {
360
- setSelected(checked ? [entry] : []);
361
- }
362
- };
363
- const next = () => {
364
- if (!nextCursor) return;
365
- if (keyword.trim()) {
366
- setSearchCursors(current => {
367
- const copy = [...current];
368
- copy[page] = nextCursor;
369
- return copy;
370
- });
371
- }
372
- setPage(value => value + 1);
373
- };
374
-
375
- return (
376
- <div className="oxa-directory-panel oxa-member-panel">
377
- <div className="oxa-directory-main">
378
- <section className="oxa-directory-tree-pane">
379
- <div className="oxa-directory-pane-title">组织架构</div>
380
- <Spin spinning={tree.loading}>
381
- {tree.error ? (
382
- <Typography.Text type="danger">{tree.error}</Typography.Text>
383
- ) : (
384
- <Tree
385
- height={430}
386
- loadData={node => tree.loadChildren(String(node.key))}
387
- onSelect={keys => setDepartmentId(String(keys[0] || ''))}
388
- selectedKeys={departmentId ? [departmentId] : []}
389
- treeData={departmentTreeData(tree.nodes)}
390
- />
391
- )}
392
- </Spin>
393
- </section>
394
- <section className="oxa-directory-member-pane">
395
- <Input
396
- allowClear
397
- prefix={<SearchOutlined />}
398
- placeholder="搜索成员姓名或工号"
399
- value={keyword}
400
- onChange={event => setKeyword(event.target.value)}
401
- />
402
- <Spin spinning={loading}>
403
- <div className="oxa-directory-members">
404
- {members.length ? (
405
- members.map(entry => (
406
- <label className="oxa-directory-member-row" key={entry.id}>
407
- <Checkbox
408
- checked={selectedIds.has(entry.id)}
409
- disabled={!entry.selectable}
410
- onChange={event => toggle(entry, event.target.checked)}
411
- />
412
- <Avatar size={36}>{entry.label.slice(0, 1)}</Avatar>
413
- <span>
414
- <strong>{entry.label}</strong>
415
- {displayDescription(entry) && (
416
- <small>{displayDescription(entry)}</small>
417
- )}
418
- </span>
419
- </label>
420
- ))
421
- ) : (
422
- <Empty
423
- image={Empty.PRESENTED_IMAGE_SIMPLE}
424
- description={
425
- error ||
426
- (keyword.trim().length === 1
427
- ? '请输入至少 2 个字符'
428
- : '暂无成员')
429
- }
430
- />
431
- )}
432
- </div>
433
- </Spin>
434
- <div className="oxa-directory-pagination">
435
- <span>第 {page} 页</span>
436
- <Space>
437
- <Button disabled={page === 1} onClick={() => setPage(page - 1)}>
438
- 上一页
439
- </Button>
440
- <Button disabled={!nextCursor} onClick={next}>
441
- 下一页
442
- </Button>
443
- </Space>
444
- </div>
445
- </section>
446
- <SelectedPane items={selected} onChange={setSelected} unit="人" />
447
- </div>
448
- <PickerFooter onCancel={onCancel} onConfirm={() => onConfirm(selected)} />
449
- </div>
450
- );
451
- }
452
-
453
- function DepartmentPanel({
454
- selected,
455
- setSelected,
456
- multiple,
457
- onConfirm,
458
- onCancel,
459
- }: {
460
- selected: DirectoryEntry[];
461
- setSelected: (items: DirectoryEntry[]) => void;
462
- multiple: boolean;
463
- onConfirm: (items: DirectoryEntry[]) => void;
464
- onCancel: () => void;
465
- }) {
466
- const tree = useDepartmentTree();
467
- const [keyword, setKeyword] = useState('');
468
- const [searching, setSearching] = useState(false);
469
- const [searchResults, setSearchResults] = useState<DirectoryEntry[]>([]);
470
- const [searchError, setSearchError] = useState('');
471
- const selectedIds = selected.map(item => item.id);
472
- useEffect(() => {
473
- void tree.loadRoots();
474
- }, [tree.loadRoots]);
475
- useEffect(() => {
476
- const query = keyword.trim();
477
- if (query.length < 2) {
478
- setSearchResults([]);
479
- setSearchError('');
480
- return;
481
- }
482
- let active = true;
483
- const timer = window.setTimeout(() => {
484
- setSearching(true);
485
- void searchDirectory('department', { keyword: query })
486
- .then(page => {
487
- if (active) {
488
- tree.remember(page.items);
489
- setSearchResults(page.items);
490
- }
491
- })
492
- .catch(reason => {
493
- if (active)
494
- setSearchError(
495
- reason instanceof Error ? reason.message : String(reason)
496
- );
497
- })
498
- .finally(() => {
499
- if (active) setSearching(false);
500
- });
501
- }, 300);
502
- return () => {
503
- active = false;
504
- window.clearTimeout(timer);
505
- };
506
- }, [keyword, tree.remember]);
507
-
508
- const select = (entry: DirectoryEntry, checked = true) => {
509
- if (multiple) {
510
- setSelected(
511
- checked
512
- ? selectedIds.includes(entry.id)
513
- ? selected
514
- : [...selected, entry]
515
- : selected.filter(item => item.id !== entry.id)
516
- );
517
- } else setSelected(checked ? [entry] : []);
518
- };
519
- const selectKeys = (ids: Key[]) => {
520
- const items = ids
521
- .map(key => tree.entries.current.get(String(key)))
522
- .filter((item): item is DirectoryEntry => Boolean(item));
523
- setSelected(items);
524
- };
525
-
526
- return (
527
- <div className="oxa-directory-panel">
528
- <Input
529
- allowClear
530
- prefix={<SearchOutlined />}
531
- placeholder="搜索部门"
532
- value={keyword}
533
- onChange={event => setKeyword(event.target.value)}
534
- />
535
- <div className="oxa-directory-main oxa-department-main">
536
- <section className="oxa-directory-tree-pane">
537
- <Spin spinning={tree.loading || searching}>
538
- {keyword.trim().length >= 2 ? (
539
- <div className="oxa-directory-results">
540
- {searchResults.length ? (
541
- searchResults.map(entry => (
542
- <div
543
- className="oxa-directory-department-row"
544
- key={entry.id}
545
- onClick={() => select(entry)}
546
- onKeyDown={event => {
547
- if (event.key === 'Enter' || event.key === ' ') {
548
- event.preventDefault();
549
- select(entry);
550
- }
551
- }}
552
- role="button"
553
- tabIndex={0}
554
- >
555
- <ApartmentOutlined />
556
- <span>
557
- <strong>{entry.label}</strong>
558
- {displayDescription(entry) && (
559
- <small>{displayDescription(entry)}</small>
560
- )}
561
- </span>
562
- <Checkbox
563
- checked={selectedIds.includes(entry.id)}
564
- onClick={event => event.stopPropagation()}
565
- onChange={event => select(entry, event.target.checked)}
566
- />
567
- </div>
568
- ))
569
- ) : (
570
- <Empty
571
- image={Empty.PRESENTED_IMAGE_SIMPLE}
572
- description={searchError || '暂无部门'}
573
- />
574
- )}
575
- </div>
576
- ) : tree.error ? (
577
- <Typography.Text type="danger">{tree.error}</Typography.Text>
578
- ) : (
579
- <Tree
580
- checkable={multiple}
581
- checkedKeys={multiple ? selectedIds : undefined}
582
- height={460}
583
- loadData={node => tree.loadChildren(String(node.key))}
584
- onCheck={keys =>
585
- selectKeys(Array.isArray(keys) ? keys : keys.checked)
586
- }
587
- onSelect={keys => {
588
- if (!multiple) selectKeys(keys);
589
- }}
590
- selectedKeys={multiple ? undefined : selectedIds}
591
- treeData={departmentTreeData(tree.nodes)}
592
- />
593
- )}
594
- </Spin>
595
- </section>
596
- <SelectedPane items={selected} onChange={setSelected} unit="个" />
597
- </div>
598
- <PickerFooter onCancel={onCancel} onConfirm={() => onConfirm(selected)} />
599
- </div>
600
- );
601
- }
602
-
603
- function SelectedPane({
604
- items,
605
- onChange,
606
- unit,
607
- }: {
608
- items: DirectoryEntry[];
609
- onChange: (items: DirectoryEntry[]) => void;
610
- unit: string;
611
- }) {
612
- return (
613
- <aside className="oxa-directory-selected-pane">
614
- <div className="oxa-directory-pane-title">
615
- <span>
616
- 已选 {items.length} {unit}
617
- </span>
618
- <Button disabled={!items.length} onClick={() => onChange([])} type="link">
619
- 清空
620
- </Button>
621
- </div>
622
- {items.length ? (
623
- <div className="oxa-directory-selected-list">
624
- {items.map(item => (
625
- <div className="oxa-directory-selected-item" key={item.id}>
626
- {item.kind === 'user' ? (
627
- <Avatar size={30}>{item.label.slice(0, 1)}</Avatar>
628
- ) : (
629
- <ApartmentOutlined />
630
- )}
631
- <span>
632
- <strong>{item.label}</strong>
633
- {displayDescription(item) && (
634
- <small>{displayDescription(item)}</small>
635
- )}
636
- </span>
637
- <Button
638
- aria-label={`移除${item.label}`}
639
- icon={<CloseOutlined />}
640
- onClick={() =>
641
- onChange(items.filter(entry => entry.id !== item.id))
642
- }
643
- size="small"
644
- type="text"
645
- />
646
- </div>
647
- ))}
648
- </div>
649
- ) : (
650
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无选择" />
651
- )}
652
- </aside>
653
- );
654
- }
655
-
656
- function PickerFooter({
657
- onCancel,
658
- onConfirm,
659
- }: {
660
- onCancel: () => void;
661
- onConfirm: () => void;
662
- }) {
663
- return (
664
- <div className="oxa-directory-footer">
665
- <Space>
666
- <Button onClick={onCancel}>取消</Button>
667
- <Button onClick={onConfirm} type="primary">
668
- 确定
669
- </Button>
670
- </Space>
671
- </div>
672
- );
673
- }