openxiangda-cli 2.0.0-alpha.57 → 2.0.0-alpha.59

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