@zjlab-fe/data-hub-ui 0.14.2 → 0.15.1

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.
@@ -0,0 +1,4436 @@
1
+ # @zjlab-fe/data-hub-ui 组件库 Skill 参考文档
2
+
3
+ > **版本**:0.14.2 | **包名**:`@zjlab-fe/data-hub-ui` | **自动生成,请勿手动编辑**
4
+ >
5
+ > 本文档包含所有公开组件的 API 参考与使用说明,供 AI 编码助手作为 skill 参考。
6
+ > 安装:`npm install @zjlab-fe/data-hub-ui`
7
+
8
+ ## 目录
9
+
10
+ - [apply-perm-modal(申请权限弹窗)](#apply-perm-modal)
11
+ - [auth-tag(权限标签)](#auth-tag)
12
+ - [bench-card(卡片)](#bench-card)
13
+ - [confirm-again(二次确认)](#confirm-again)
14
+ - [copy(复制)](#copy)
15
+ - [feature-card(功能点卡片)](#feature-card)
16
+ - [file-preview(文件预览)](#file-preview)
17
+ - [file-uploader(文件上传)](#file-uploader)
18
+ - [floating-label-input(浮动标签输入框)](#floating-label-input)
19
+ - [floating-layer(浮动操作框)](#floating-layer)
20
+ - [header(头部导航)](#header)
21
+ - [input-tag(标签/关键字输入)](#input-tag)
22
+ - [menu(菜单)](#menu)
23
+ - [operator-chain(卡片式操作链)](#operator-chain)
24
+ - [permission-editor(权限编辑器)](#permission-editor)
25
+ - [popover-select(带描述浮层下拉框)](#popover-select)
26
+ - [radio-card(单选卡片)](#radio-card)
27
+ - [section-heading(区块标题)](#section-heading)
28
+ - [status-tag(状态标签)](#status-tag)
29
+ - [tip-tap(富文本编辑器)](#tip-tap)
30
+
31
+ ---
32
+
33
+ ## apply-perm-modal(申请权限弹窗)
34
+
35
+ **导入方式:**
36
+
37
+ ```ts
38
+ import { ApplyPermModal } from '@zjlab-fe/data-hub-ui';
39
+ ```
40
+
41
+ antd modal二次封装,仅用于申请权限
42
+
43
+ ### 代码演示
44
+
45
+ **示例代码(index.tsx):**
46
+
47
+ ```tsx
48
+ import React from 'react';
49
+ import { Button, Space } from 'antd';
50
+ import ApplyPermModal from '..';
51
+
52
+ export default function Demo() {
53
+ const [open, setOpen] = React.useState(false);
54
+
55
+ return (
56
+ <>
57
+ # 申请权限弹窗使用方法
58
+ ### 1. 组件式
59
+ <Button onClick={() => setOpen(true)}>打开</Button>
60
+ <ApplyPermModal open={open} onCancel={() => setOpen(false)} />
61
+ ### 2. 命令式
62
+ <Space>
63
+ <Button onClick={() => ApplyPermModal.confirm()}>打开(中文)</Button>
64
+ <Button onClick={() => ApplyPermModal.confirm({ locale: 'en' })}>打开(英文)</Button>
65
+ </Space>
66
+
67
+ # 非弹窗形式展示申请权限内容
68
+ <ApplyPermModal noModal />
69
+ </>
70
+ );
71
+ }
72
+ ```
73
+ ### API
74
+
75
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
76
+ | --- | --- | --- | --- | --- | --- |
77
+ | locale | 语言 | `'zh' \| 'en'` | `'zh'` | - | |
78
+ | noModal | 是否直接展示申请权限内容,而非弹窗形式,默认以弹窗形式展示 | `boolean` | `false` | - | |
79
+ | open | 对话框是否可见 | `boolean` | - | - | |
80
+ | onCancel | 取消时的回调 | `(e: React.MouseEvent<HTMLButtonElement>) => void;` | - | - | |
81
+
82
+ ---
83
+
84
+ ## auth-tag(权限标签)
85
+
86
+ **导入方式:**
87
+
88
+ ```ts
89
+ import { AuthTag } from '@zjlab-fe/data-hub-ui';
90
+ ```
91
+
92
+ > 该组件暂无 README 文档,以下为 demo 示例代码,可参考用法。
93
+
94
+ ### 示例代码(index.tsx)
95
+
96
+ ```tsx
97
+ import AuthTag from '@/components/auth-tag';
98
+ export default function Demo() {
99
+ return (
100
+ <>
101
+ <AuthTag type="confidential">限制公开</AuthTag>
102
+ <AuthTag type="public">公开</AuthTag>
103
+ <AuthTag type="private">非公开</AuthTag>
104
+ </>
105
+ );
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## bench-card(卡片)
112
+
113
+ **导入方式:**
114
+
115
+ ```ts
116
+ import { BenchCard } from '@zjlab-fe/data-hub-ui';
117
+ ```
118
+
119
+ 提供两种 BenchCard 包括工作流式和组件式
120
+
121
+ ### 代码演示
122
+
123
+ **示例代码(index.tsx):**
124
+
125
+ ```tsx
126
+ import BenchCard from '@/components/bench-card';
127
+ export default function Demo() {
128
+ return (
129
+ <>
130
+ <BenchCard type="workflow">模板中心 -工作流-card</BenchCard>
131
+ <BenchCard type="component">模板中心 -组件-card</BenchCard>
132
+ <BenchCard type="workflow" size="small">
133
+ 工作空间 -工作流-card
134
+ </BenchCard>
135
+ <BenchCard type="component" size="small">
136
+ 工作空间 -组件-card
137
+ </BenchCard>
138
+ <BenchCard type="workflow" size="small" width={800} height={300}>
139
+ 工作空间 -工作流-card-可控制 长度
140
+ </BenchCard>
141
+ </>
142
+ );
143
+ }
144
+ ```
145
+ ### API
146
+
147
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
148
+ | --- | --- | --- | --- | --- | --- |
149
+ | type | 类型 | `workflow \| component` | - | - | 是 |
150
+ | size | 尺寸 | `small \| middle` | `middle` | -| 否 |
151
+ | children | 插槽 |`React.ReactNode` | - | - | - |
152
+ | width | 宽度 | `string \| number` | - | - | 否 |
153
+ | height | 高度 | `string \| number` | - | - | 否 |
154
+
155
+ ---
156
+
157
+ ## confirm-again(二次确认)
158
+
159
+ **导入方式:**
160
+
161
+ ```ts
162
+ import { ConfirmAgain } from '@zjlab-fe/data-hub-ui';
163
+ ```
164
+
165
+ antd modal二次封装,主要是覆盖了Modal部分默认样式,整体垂直居中展示
166
+
167
+ ### 代码演示
168
+
169
+ **示例代码(index.tsx):**
170
+
171
+ ```tsx
172
+ import React, { useState } from 'react';
173
+ import ConfirmAgain from '@/components/confirm-again';
174
+ import { Button } from 'antd';
175
+
176
+ export default function Demo() {
177
+ const [open, setOpen] = useState(false);
178
+ const openModal = function () {
179
+ setOpen(true);
180
+ };
181
+ const handleOk = function () {
182
+ console.log('ok');
183
+ setOpen(false);
184
+ };
185
+ const handleCancel = function () {
186
+ console.log('cancel');
187
+ setOpen(false);
188
+ };
189
+ const openFunModal = function () {
190
+ ConfirmAgain.confirm({
191
+ title: '命令式-二次确认弹窗',
192
+ wrapClassName: 'test',
193
+ content: <div style={{ width: 300 }}>子内容</div>,
194
+ onOk: () => {
195
+ console.log('ok');
196
+ },
197
+ onCancel: () => {
198
+ console.log('cancel');
199
+ },
200
+ open: true,
201
+ });
202
+ };
203
+ return (
204
+ <>
205
+ # 二次确认弹窗使用方法
206
+ ### 1. 组件式
207
+ <Button onClick={() => openModal()}>打开</Button>
208
+ <ConfirmAgain
209
+ title="组件式-二次确认弹窗"
210
+ wrapClassName="test"
211
+ open={open}
212
+ onOk={handleOk}
213
+ onCancel={handleCancel}
214
+ >
215
+ <div>子内容</div>
216
+ </ConfirmAgain>
217
+ ### 2. 命令式
218
+ <Button onClick={() => openFunModal()}>打开</Button>
219
+ </>
220
+ );
221
+ }
222
+ ```
223
+ ### API
224
+
225
+ 组件式API与antd modal API一致,函数命令式的参数与Modal.method()的参数一致
226
+
227
+ ---
228
+
229
+ ## copy(复制)
230
+
231
+ **导入方式:**
232
+
233
+ ```ts
234
+ import { Copy, copy } from '@zjlab-fe/data-hub-ui';
235
+ ```
236
+ ```ts
237
+ // 类型导入
238
+ import type { ICopy } from '@zjlab-fe/data-hub-ui';
239
+ ```
240
+
241
+ 使用:`import { Copy } from '@zjlab-fe/data-hub-ui';`
242
+
243
+ ### 何时使用
244
+
245
+ 展示复制组件 or 复制文本到系统粘贴板
246
+
247
+ ### 代码演示
248
+
249
+ **示例代码(index.tsx):**
250
+
251
+ ```tsx
252
+ import Copy, { copy } from '@/components/copy';
253
+ export default function Demo() {
254
+ return (
255
+ <>
256
+ <p>方式一:传入待复制的文本</p>
257
+ <Copy text="haha test" />
258
+ <p>方式二:不传入待复制的文本,仅展示UI组件,在onClick回调中手动调用copy</p>
259
+ <Copy
260
+ onClick={() => {
261
+ copy('haha test');
262
+ }}
263
+ />
264
+ </>
265
+ );
266
+ }
267
+ ```
268
+ ### API
269
+
270
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
271
+ | --- | --- | --- | --- | --- | --- |
272
+ | text | 待复制到粘贴板的文本,如果传入为null or undefined,将不会自动复制到粘贴板 | `string` | - | - | - |
273
+ | showFeedback | 是否展示复制成功后的反馈信息 | `boolean` | `true` | - | - |
274
+ | locale | 语言 | `zh \| en` | `zh` | - | - |
275
+ | onClick | 点击回调 | `() => void` | - | - | - |
276
+
277
+ #### copy
278
+
279
+ 单独使用copy方法,用于复制文本
280
+
281
+ ```js
282
+ import { copy } from '@zjlab-fe/data-hub-ui';
283
+ copy('test')
284
+ ```
285
+
286
+ copy函数入参如下:
287
+
288
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
289
+ | --- | --- | --- | --- | --- | --- |
290
+ | text | 待复制到粘贴板的文本 | `string` | - | - | 是 |
291
+ | options | 其它选项 | `object` | - | - | - |
292
+
293
+ `options`具体属性如下:
294
+
295
+ | 属性名 | 说明 | 类型 | 默认值 | 版本 | 必须 |
296
+ | --- | --- | --- | --- | --- | --- |
297
+ | showFeedback | 是否展示复制成功后的反馈信息 | `boolean` | `true` | - | - |
298
+ | locale | 语言 | `zh \| en` | `zh` | - | - |
299
+
300
+ ---
301
+
302
+ ## feature-card(功能点卡片)
303
+
304
+ **导入方式:**
305
+
306
+ ```ts
307
+ import { FeatureCard } from '@zjlab-fe/data-hub-ui';
308
+ ```
309
+ ```ts
310
+ // 类型导入
311
+ import type { FeatureCardProps, FeatureCardRootProps } from '@zjlab-fe/data-hub-ui';
312
+ ```
313
+
314
+ 用于展示功能点/卖点的卡片(icon + title + description)。
315
+
316
+ - 支持常规 props 方式
317
+ - 支持类似 shadcn/ui 的组合式用法(`FeatureCard.Root/Icon/Title/Description/Content`)
318
+ - 使用纯 CSS,并通过 `@scope` + 根类名做样式隔离
319
+
320
+ ### 代码演示
321
+
322
+ **示例代码(index.tsx):**
323
+
324
+ ```tsx
325
+ import React from 'react';
326
+ import FeatureCard from '@/components/feature-card';
327
+ import { Card, RadioGroup } from '@/demo/shared';
328
+ import Moha from './moha';
329
+ import { ComparisonVsSemanticDom } from '@/demo/shared/ComparisonVsSemanticDom';
330
+ import './index.css';
331
+
332
+ type Align = 'left' | 'center' | 'right';
333
+ type Size = 'sm' | 'md' | 'lg';
334
+ type IconVariant = 'default' | 'full' | 'square';
335
+
336
+ function Controls({
337
+ align,
338
+ size,
339
+ iconVariant,
340
+ onChange,
341
+ }: {
342
+ align: Align;
343
+ size: Size;
344
+ iconVariant: IconVariant;
345
+ onChange: (next: { align?: Align; size?: Size; iconVariant?: IconVariant }) => void;
346
+ }) {
347
+ return (
348
+ <div style={{ display: 'grid', gap: 16 }}>
349
+ <RadioGroup<Align>
350
+ label="对齐方式 Align"
351
+ value={align}
352
+ options={[
353
+ { value: 'left', label: 'left', badge: 'Default' },
354
+ { value: 'center', label: 'center' },
355
+ { value: 'right', label: 'right' },
356
+ ]}
357
+ onChange={(val) => onChange({ align: val })}
358
+ />
359
+ <RadioGroup<Size>
360
+ label="尺寸 Size"
361
+ value={size}
362
+ options={[
363
+ { value: 'sm', label: 'sm' },
364
+ { value: 'md', label: 'md' },
365
+ { value: 'lg', label: 'lg', badge: 'Default' },
366
+ ]}
367
+ onChange={(val) => onChange({ size: val })}
368
+ />
369
+ <RadioGroup<IconVariant>
370
+ label="图标样式 Icon Variant"
371
+ value={iconVariant}
372
+ options={[
373
+ { value: 'default', label: 'default' },
374
+ { value: 'full', label: 'full' },
375
+ { value: 'square', label: 'square' },
376
+ ]}
377
+ onChange={(val) => onChange({ iconVariant: val })}
378
+ />
379
+ </div>
380
+ );
381
+ }
382
+
383
+ export default function Demo() {
384
+ const [align, setAlign] = React.useState<Align>('left');
385
+ const [size, setSize] = React.useState<Size>('lg');
386
+ const [iconVariant, setIconVariant] = React.useState<IconVariant>('default');
387
+
388
+ const shared = {
389
+ icon: <Moha />,
390
+ cardTitle: 'Too young, too simple, sometimes naïve',
391
+ description: '你们问我支不支持,我说支持也不行,不支持也不行',
392
+ } as const;
393
+
394
+ return (
395
+ <div style={{ padding: 24, display: 'grid', gap: 24, maxWidth: 1280, margin: '0 auto' }}>
396
+ {/* Interactive Props Playground */}
397
+ <Card
398
+ title={<span>Interactive Playground</span>}
399
+ sourceCode={`<div
400
+ style={{
401
+ padding: 24,
402
+ display: 'grid',
403
+ gap: 16,
404
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
405
+ }}
406
+ >
407
+ <FeatureCard
408
+ icon={<Moha />}
409
+ cardTitle="${shared.cardTitle}"
410
+ description="${shared.description}"
411
+ align="${align}"
412
+ size="${size}"
413
+ iconVariant="${iconVariant}"
414
+ />
415
+ <FeatureCard
416
+ icon={<Moha />}
417
+ cardTitle="${shared.cardTitle}"
418
+ description="${shared.description}。${shared.description}。${shared.description}"
419
+ align="${align}"
420
+ size="${size}"
421
+ iconVariant="${iconVariant}"
422
+ />
423
+ <FeatureCard.Root align="${align}" size="${size}" iconVariant="${iconVariant}">
424
+ <FeatureCard.Icon>
425
+ <Moha />
426
+ </FeatureCard.Icon>
427
+ <FeatureCard.Body>
428
+ <FeatureCard.Title>${shared.cardTitle}</FeatureCard.Title>
429
+ <FeatureCard.Description>${shared.description}</FeatureCard.Description>
430
+ </FeatureCard.Body>
431
+ </FeatureCard.Root>
432
+ </div>`}
433
+ >
434
+ <div style={{ display: 'grid', gap: 16 }}>
435
+ <Controls
436
+ align={align}
437
+ size={size}
438
+ iconVariant={iconVariant}
439
+ onChange={(next) => {
440
+ if (next.align) setAlign(next.align);
441
+ if (next.size) setSize(next.size);
442
+ if (next.iconVariant) setIconVariant(next.iconVariant);
443
+ }}
444
+ />
445
+
446
+ <div
447
+ style={{
448
+ padding: 24,
449
+ display: 'grid',
450
+ gap: 16,
451
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
452
+ }}
453
+ >
454
+ <FeatureCard
455
+ icon={shared.icon}
456
+ cardTitle={shared.cardTitle}
457
+ description={shared.description}
458
+ align={align}
459
+ size={size}
460
+ iconVariant={iconVariant}
461
+ />
462
+ <FeatureCard
463
+ icon={shared.icon}
464
+ cardTitle={shared.cardTitle}
465
+ description={shared.description + '。' + shared.description + '。' + shared.description}
466
+ align={align}
467
+ size={size}
468
+ iconVariant={iconVariant}
469
+ />
470
+ <FeatureCard.Root align={align} size={size} iconVariant={iconVariant}>
471
+ <FeatureCard.Icon>
472
+ <Moha />
473
+ </FeatureCard.Icon>
474
+ <FeatureCard.Body>
475
+ <FeatureCard.Title>{shared.cardTitle}</FeatureCard.Title>
476
+ <FeatureCard.Description>{shared.description}</FeatureCard.Description>
477
+ </FeatureCard.Body>
478
+ </FeatureCard.Root>
479
+ </div>
480
+ </div>
481
+ </Card>
482
+
483
+ <ComparisonVsSemanticDom />
484
+
485
+ {/* Semantic API */}
486
+ <Card
487
+ title={<span>Semantic API</span>}
488
+ sourceCode={`<FeatureCard
489
+ icon={<Moha />}
490
+ cardTitle="${shared.cardTitle}"
491
+ description="${shared.description}"
492
+ align="left"
493
+ size="lg"
494
+ classNames={{
495
+ root: 'dhu-bg-gradient-to-br dhu-from-purple-50 dhu-to-blue-50 dhu-border-purple-200',
496
+ icon: 'dhu-bg-gradient-to-br dhu-from-purple-500 dhu-to-blue-500 dhu-text-white',
497
+ title: 'dhu-text-purple-900',
498
+ description: 'dhu-text-purple-700',
499
+ }}
500
+ />`}
501
+ >
502
+ <FeatureCard
503
+ icon={shared.icon}
504
+ cardTitle={shared.cardTitle}
505
+ description={shared.description}
506
+ align="left"
507
+ size="lg"
508
+ classNames={{
509
+ root: 'dhu-bg-gradient-to-br dhu-from-purple-50 dhu-to-blue-50 dhu-border-purple-200',
510
+ icon: 'dhu-bg-gradient-to-br dhu-from-purple-500 dhu-to-blue-500 dhu-text-white',
511
+ title: 'dhu-text-purple-900',
512
+ description: 'dhu-text-purple-700',
513
+ }}
514
+ />
515
+ </Card>
516
+
517
+ {/* Composition API */}
518
+ <Card
519
+ title={<span>Composition API</span>}
520
+ sourceCode={`<FeatureCard.Root align="left" size="lg">
521
+ <FeatureCard.Icon>
522
+ <Moha />
523
+ </FeatureCard.Icon>
524
+ <FeatureCard.Body>
525
+ <FeatureCard.Title>Too young, too simple, sometimes naïve</FeatureCard.Title>
526
+ <FeatureCard.Description>
527
+ 你们问我支不支持,我说支持也不行,不支持也不行
528
+ </FeatureCard.Description>
529
+ </FeatureCard.Body>
530
+ <div style={{
531
+ display: 'flex',
532
+ gap: 8,
533
+ alignItems: 'center',
534
+ marginTop: 16,
535
+ paddingTop: 16,
536
+ borderTop: '1px solid #e5e7eb'
537
+ }}>
538
+ <span style={{
539
+ padding: '4px 12px',
540
+ background: '#f3f4f6',
541
+ borderRadius: 12,
542
+ fontSize: 12,
543
+ color: '#6b7280'
544
+ }}>
545
+ Featured
546
+ </span>
547
+ <button style={{
548
+ padding: '6px 16px',
549
+ background: '#3b82f6',
550
+ color: 'white',
551
+ border: 'none',
552
+ borderRadius: 6,
553
+ fontSize: 14,
554
+ cursor: 'pointer'
555
+ }}>
556
+ Learn More
557
+ </button>
558
+ </div>
559
+ </FeatureCard.Root>`}
560
+ >
561
+ <FeatureCard.Root align="left" size="lg">
562
+ <FeatureCard.Icon>
563
+ <Moha />
564
+ </FeatureCard.Icon>
565
+ <FeatureCard.Body>
566
+ <FeatureCard.Title className="text-red">{shared.cardTitle}</FeatureCard.Title>
567
+ <FeatureCard.Description>{shared.description}</FeatureCard.Description>
568
+ </FeatureCard.Body>
569
+ <div
570
+ style={{
571
+ display: 'flex',
572
+ gap: 8,
573
+ alignItems: 'center',
574
+ marginTop: 16,
575
+ paddingTop: 16,
576
+ borderTop: '1px solid #e5e7eb',
577
+ }}
578
+ >
579
+ <span
580
+ style={{
581
+ padding: '4px 12px',
582
+ background: '#f3f4f6',
583
+ borderRadius: 12,
584
+ fontSize: 12,
585
+ color: '#6b7280',
586
+ }}
587
+ >
588
+ Featured
589
+ </span>
590
+ <button
591
+ style={{
592
+ padding: '6px 16px',
593
+ background: '#3b82f6',
594
+ color: 'white',
595
+ border: 'none',
596
+ borderRadius: 6,
597
+ fontSize: 14,
598
+ cursor: 'pointer',
599
+ }}
600
+ >
601
+ Learn More
602
+ </button>
603
+ </div>
604
+ </FeatureCard.Root>
605
+ </Card>
606
+ </div>
607
+ );
608
+ }
609
+ ```
610
+ ### API
611
+
612
+ #### FeatureCard (默认导出)
613
+
614
+ | 参数 | 说明 | 类型 | 默认值 | 必须 |
615
+ | --- | --- | --- | --- | --- |
616
+ | icon | 图标区域内容 | `React.ReactNode` | - | 否 |
617
+ | cardTitle | 标题(避免与 HTML title 属性冲突) | `React.ReactNode` | - | 否 |
618
+ | description | 描述 | `React.ReactNode` | - | 否 |
619
+ | titleAs | 标题标签 | `keyof JSX.IntrinsicElements` | `'h3'` | 否 |
620
+ | className | 自定义类名 | `string` | - | 否 |
621
+
622
+ #### 组合式子组件
623
+
624
+ - `FeatureCard.Root`
625
+ - `FeatureCard.Header`
626
+ - `FeatureCard.Icon`
627
+ - `FeatureCard.Title`
628
+ - `FeatureCard.Description`
629
+ - `FeatureCard.Content`
630
+
631
+ 子组件支持 `asChild`:传入自定义标签作为 children(需为单个 ReactElement)。
632
+
633
+ ---
634
+
635
+ ## file-preview(文件预览)
636
+
637
+ **导入方式:**
638
+
639
+ ```ts
640
+ import { FilePreview } from '@zjlab-fe/data-hub-ui';
641
+ ```
642
+ ```ts
643
+ // 类型导入
644
+ import type { IFilePreviewProps } from '@zjlab-fe/data-hub-ui';
645
+ ```
646
+
647
+ 用于文件预览
648
+
649
+ ### 支持的文件格式
650
+
651
+ 包括:
652
+
653
+ - xlsx/csv
654
+ - json
655
+ - pdf
656
+ - jpg/jpeg/png
657
+ - md
658
+ - txt
659
+
660
+ ### 代码演示
661
+
662
+ **示例代码(index.tsx):**
663
+
664
+ ```tsx
665
+ import FilePreview from '@/components/file-preview';
666
+ import { useState } from 'react';
667
+ import { Tabs } from 'antd';
668
+ import type { TabsProps } from 'antd';
669
+ export default function Demo() {
670
+ const [show, setShow] = useState(true);
671
+
672
+ const fileMap = {
673
+ // 'xlsx/csv': 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/test.xlsx',
674
+ 'xlsx/csv': 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/method_Caledonian.csv',
675
+ pdf: 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/1.pdf',
676
+ image: 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/figure9.png',
677
+ markdown: 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/b1047.md',
678
+ txt: 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/test.txt',
679
+ json: 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/test.json',
680
+ jsonl: 'https://haina-datahub.zero2x.org/ossRoute/frontend/test/ocr_test.jsonl',
681
+ parquet: 'https://haina-datahub.zero2x.org/ossRoute/frontend/resources/test/test.parquet',
682
+ };
683
+ const items: TabsProps['items'] = [
684
+ {
685
+ key: 'table',
686
+ label: 'table格式数据',
687
+ children: (
688
+ <div style={{ width: '80%', height: '500px' }}>
689
+ <FilePreview
690
+ openInModal={false}
691
+ tableConfig={{
692
+ rowCount: 100,
693
+ maxLines: 2,
694
+ header: [
695
+ 'id',
696
+ 'title',
697
+ 'title_new',
698
+ 'translation',
699
+ 'explanation',
700
+ 'classification_4',
701
+ 'explanation_4',
702
+ 'classification_14',
703
+ 'explanation_14',
704
+ 'ds',
705
+ ],
706
+ rows: [
707
+ [
708
+ 'aacid__upload_files_degruyter__20240526T001320Z__eTkNyDZB9Q8CsRz3SYorHg',
709
+ 'DeGruyter Journals/Journal of Optical Communications/Volume 33 (2012)/Issue 4/006_An Approach of Multiple-Quality Segment Protection (10.1515_joc-2012-0059).pdf',
710
+ 'degruyter journals/journal of optical communications/volume 33 (2012)/issue 4/006_an approach of multiple-quality segment protection (10.1515_joc-2012-0059).pdf',
711
+ 'degruyter journals/journal of optical communications/volume 33 (2012)/issue 4/006_an approach of multiple-quality segment protection (10.1515_joc-2012-0059).pdf',
712
+ '标题看起来像一个学术期刊文章的文件名,包含具体的卷号、期号和文章标题,无实际意义,无法翻译,因此保留原样。',
713
+ '其他',
714
+ '该标题是一个学术期刊文章的引用,虽然涉及光学通信领域的专业内容,但作为一个单独的标题,它更适合作为学术文献而非教育或科普书籍。因此,归类为“其他”。',
715
+ '物理学',
716
+ '该标题涉及“optical communications(光通信)”,这是物理学中的一个分支领域,主要研究光信号的传输与处理。因此,归类为物理学。',
717
+ '2025-04-16',
718
+ ],
719
+ [
720
+ 'aacid__upload_files_acm__20240525T232722Z__Es24jwWahzfMHbUzQdoBES',
721
+ 'Curve Fitting with Conic Splines',
722
+ 'curve fitting with conic splines',
723
+ '用圆锥样条进行曲线拟合',
724
+ '标题描述了技术内容,翻译为“用圆锥样条进行曲线拟合”准确且符合中文表达习惯。',
725
+ '专业教育',
726
+ '“curve fitting with conic splines” 是一个与数学和计算机科学相关的专业主题,通常在高等教育或专业培训中涉及。因此,它被归类为“专业教育”。',
727
+ '数学',
728
+ '“Curve fitting with conic splines”涉及数学中的曲线拟合和样条函数,这些是数学领域中的重要概念和方法,因此归类为数学。',
729
+ '2025-04-16',
730
+ ],
731
+ [
732
+ 'aacid__upload_files_duxiu_main__20240526T021914Z__UXkiaAn54FAH4Yo3KGVyTY',
733
+ 'v/djvu/33152228_普通物理力学_王正清高等教育.djvu',
734
+ 'v/djvu/33152228_普通物理力学_王正清高等教育.djvu',
735
+ 'v/djvu/33152228_普通物理力学_王正清高等教育.djvu',
736
+ '标题看起来像一个文件路径,包含文件名和扩展名,无实际意义,无法翻译,因此保留原样。',
737
+ '专业教育',
738
+ '标题中的“普通物理力学”和“高等教育”表明这是一本大学水平的物理教科书,适用于高等教育或职业培训,因此归类为“专业教育”。',
739
+ '物理学',
740
+ '标题中的“普通物理力学”明确指向物理学中的力学部分,属于物理学(物理学)的范畴。',
741
+ '2025-04-16',
742
+ ],
743
+ [
744
+ 'aacid__upload_files_bpb9v_cadal__20240510T045544Z__R5oLfdRocAST7YfzYGKGRz',
745
+ 'ca33_03/33087932_電測儀表初級工_山西省電力工業局編中國電力.djvu',
746
+ 'ca33_03/33087932_電測儀表初級工_山西省電力工業局編中國電力.djvu',
747
+ 'ca33_03/33087932_電測儀表初級工_山西省電力工業局編中國電力.djvu',
748
+ '标题看起来像一个文件名,包含中文和数字,无实际意义,无法翻译,因此保留原样。',
749
+ '专业教育',
750
+ '标题中的“電測儀表初級工”(初级电工仪表)和“山西省電力工業局編”(山西省电力工业局编)表明这是一本关于电工仪表的专业培训书籍,适用于职业培训或高等教育。因此,归类为“专业教育”。',
751
+ '工程学',
752
+ '标题中的“電測儀表初級工”涉及电气测量仪表的基础知识,属于工程学中的电气工程领域。因此,该书应归类为工程学。',
753
+ '2025-04-16',
754
+ ],
755
+ [
756
+ '28392707',
757
+ '职场女性健康新视角',
758
+ '职场女性健康新视角',
759
+ '职场女性健康新视角',
760
+ '输入已经是中文,无需翻译,直接保留。',
761
+ '专业教育',
762
+ '标题“职场女性健康新视角”涉及职业健康和女性健康,这通常属于专业教育范畴,特别是针对职场人士的健康管理和职业培训。因此,归类为“专业教育”。',
763
+ '医学',
764
+ '该书标题“职场女性健康新视角”涉及女性在职场中的健康问题,属于医学领域中的职业健康或女性健康研究,因此归类为医学。',
765
+ '2025-04-16',
766
+ ],
767
+ [
768
+ 'xinandiquzaoleiz00shiz',
769
+ 'xi nan di qu zao lei zi yuan kao cha zhuan ji 西南地区藻类资源考察专集',
770
+ 'xi nan di qu zao lei zi yuan kao cha zhuan ji 西南地区藻类资源考察专集',
771
+ '西南地区藻类资源考察专集',
772
+ '输入已经是中文,无需翻译,直接保留。',
773
+ '科普知识',
774
+ '标题“西南地区藻类资源考察专集”涉及对特定地区的藻类资源进行科学考察和研究。虽然内容较为专业,但它面向的是普通读者和科普爱好者,而不是高等教育或职业培训。因此,归类为“科普知识”。',
775
+ '生物学',
776
+ '该书标题“西南地区藻类资源考察专集”涉及藻类资源的调查与研究,藻类属于生物学中的一个分支,因此归类为生物学。',
777
+ '2025-04-16',
778
+ ],
779
+ [
780
+ 'aacid__upload_files_acm__20240525T230805Z__84PrZimQiEaA9wLTn8WK5W',
781
+ 'Decision Procedures for Epistemic Logic Exploiting Belief Bases',
782
+ 'decision procedures for epistemic logic exploiting belief bases',
783
+ '用于知识逻辑的决策程序:利用信念库',
784
+ '标题中的“decision procedures for epistemic logic”被翻译为“用于知识逻辑的决策程序”,准确传达了主题内容。“exploiting belief bases”则翻译为“利用信念库”,进一步明确了方法和手段。',
785
+ '专业教育',
786
+ '标题“decision procedures for epistemic logic exploiting belief bases”涉及逻辑学和认知科学的高级主题,通常在大学或研究生水平的课程中教授。因此,它属于“专业教育”类别。',
787
+ '其他',
788
+ '标题涉及“epistemic logic(认识逻辑)”和“belief bases(信念基础)”,这些内容属于哲学和逻辑学领域,不在给定的14个学术学科范围内,因此归类为“其他”。',
789
+ '2025-04-16',
790
+ ],
791
+ [
792
+ '27854311',
793
+ '正切、弧度、渐开线函数秒表',
794
+ '正切、弧度、渐开线函数秒表',
795
+ '正切、弧度、渐开线函数秒表',
796
+ '输入已经是中文,无需翻译,直接保留。',
797
+ '科普知识',
798
+ '标题中的“正切、弧度、渐开线函数”涉及数学和科学概念,但“秒表”表明这可能是一本介绍这些概念在实际应用中的书籍。整体来看,这本书更倾向于面向普通读者,提供科学知识的普及,因此归类为“科普知识”。',
799
+ '数学',
800
+ '标题中的“正切、弧度、渐开线函数”都是数学中的概念,特别是与三角函数和解析几何相关的内容。因此,该书应归类为数学。',
801
+ '2025-04-16',
802
+ ],
803
+ [
804
+ '31851026',
805
+ '黄家驷外科学 第8版 下册_吴孟超,吴在德主编2021年',
806
+ '黄家驷外科学 第8版 下册_吴孟超,吴在德主编2021年',
807
+ '黄家驷外科学 第8版 下册_吴孟超,吴在德主编2021年',
808
+ '输入已经是中文,无需翻译,直接保留。',
809
+ '专业教育',
810
+ '《黄家驷外科学》是一本经典的医学教科书,通常用于医学专业学生的高等教育或职业培训。因此,它属于“专业教育”类别。',
811
+ '医学',
812
+ '该书为《黄家驷外科学》的第8版,由吴孟超和吴在德主编,内容涉及外科医学,属于医学类。外科学是医学的一个重要分支,专注于外科手术和相关疾病的治疗。',
813
+ '2025-04-16',
814
+ ],
815
+ [
816
+ 'aacid__upload_files_acm__20240525T223731Z__RhvjUeLyf5Ly67cbS2Dpob',
817
+ 'Video enhancement leveraging high-quality depth maps',
818
+ 'video enhancement leveraging high-quality depth maps',
819
+ '利用高质量深度图进行视频增强',
820
+ '标题描述了利用高质量深度图技术进行视频增强的内容,翻译为“利用高质量深度图进行视频增强”既准确又符合中文表达习惯。',
821
+ '专业教育',
822
+ '标题涉及视频增强和深度图技术,这些内容通常与高等教育或专业培训相关,特别是计算机科学和图像处理领域。因此,归类为“专业教育”。',
823
+ '计算机科学',
824
+ '该书标题涉及视频增强技术,利用高质量深度图进行处理,这属于计算机视觉和图像处理的范畴,是计算机科学的一个重要分支。因此,归类为计算机科学。',
825
+ '2025-04-16',
826
+ ],
827
+ ],
828
+ }}
829
+ />
830
+ </div>
831
+ ),
832
+ },
833
+ {
834
+ key: 'xlsx/csv',
835
+ label: 'xlsx/csv',
836
+ children: (
837
+ <div style={{ width: '80%', height: '500px' }}>
838
+ <FilePreview
839
+ openInModal={false}
840
+ modalProps={{ show, onCancel: () => setShow(false) }}
841
+ filePath={fileMap['xlsx/csv']}
842
+ />
843
+ </div>
844
+ ),
845
+ },
846
+ {
847
+ key: 'pdf',
848
+ label: 'pdf',
849
+ children: (
850
+ <div style={{ width: '80%', height: '500px' }}>
851
+ <FilePreview
852
+ openInModal={false}
853
+ modalProps={{ show, onCancel: () => setShow(false) }}
854
+ filePath={fileMap['pdf']}
855
+ />
856
+ </div>
857
+ ),
858
+ },
859
+ {
860
+ key: 'image',
861
+ label: '图片',
862
+ children: (
863
+ <div style={{ width: '80%', height: '500px' }}>
864
+ <FilePreview
865
+ openInModal={false}
866
+ modalProps={{ show, onCancel: () => setShow(false) }}
867
+ filePath={fileMap['image']}
868
+ />
869
+ </div>
870
+ ),
871
+ },
872
+ {
873
+ key: 'markdown',
874
+ label: 'markdown',
875
+ children: (
876
+ <div style={{ width: '80%', height: '500px' }}>
877
+ <FilePreview
878
+ openInModal={false}
879
+ modalProps={{ show, onCancel: () => setShow(false) }}
880
+ filePath={fileMap['markdown']}
881
+ />
882
+ </div>
883
+ ),
884
+ },
885
+ {
886
+ key: 'txt',
887
+ label: 'txt',
888
+ children: (
889
+ <div style={{ width: '80%', height: '500px' }}>
890
+ <FilePreview
891
+ openInModal={false}
892
+ modalProps={{ show, onCancel: () => setShow(false) }}
893
+ filePath={fileMap['txt']}
894
+ />
895
+ </div>
896
+ ),
897
+ },
898
+ {
899
+ key: 'jsonl',
900
+ label: 'jsonl',
901
+ children: (
902
+ <div style={{ width: '80%', height: '500px' }}>
903
+ <FilePreview
904
+ openInModal={false}
905
+ modalProps={{ show, onCancel: () => setShow(false) }}
906
+ filePath={fileMap['jsonl']}
907
+ // jsonConfig={{
908
+ // data: {
909
+ // name: 'John',
910
+ // age: 30,
911
+ // },
912
+ // }}
913
+ />
914
+ </div>
915
+ ),
916
+ },
917
+ {
918
+ key: 'json',
919
+ label: 'json',
920
+ children: (
921
+ <div style={{ width: '80%', height: '500px' }}>
922
+ <FilePreview
923
+ openInModal={false}
924
+ modalProps={{ show, onCancel: () => setShow(false) }}
925
+ filePath={fileMap['json']}
926
+ // jsonConfig={{
927
+ // data: {
928
+ // name: 'John',
929
+ // age: 30,
930
+ // },
931
+ // }}
932
+ />
933
+ </div>
934
+ ),
935
+ },
936
+ {
937
+ key: 'parquet',
938
+ label: 'parquet',
939
+ children: (
940
+ <div style={{ width: '80%', height: '500px' }}>
941
+ <FilePreview
942
+ openInModal={false}
943
+ modalProps={{ show, onCancel: () => setShow(false) }}
944
+ filePath={fileMap['parquet']}
945
+ />
946
+ </div>
947
+ ),
948
+ },
949
+ ];
950
+ return <Tabs items={items} destroyInactiveTabPane />;
951
+ }
952
+ ```
953
+ ### API
954
+
955
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
956
+ | --- | --- | --- | --- | --- | --- |
957
+ | filePath | 文件url(从文件url可以判断文件类型) | `string` | - | - | filePath/tableConfig 二选一 |
958
+ | tableConfig | 使用方已获取预览数据,使用表格展示 | [tableConfig](#tableConfig) | - | - | filePath/tableConfig 二选一 |
959
+ | openInModal | 是否在[Modal](https://ant.design/components/modal-cn)组件中展示 | `boolean` | - | - | 是 |
960
+ | modalProps | 如果在Modal中展示,需要进一步配置该属性|[modalProps](#modalProps)| - | - | |
961
+ | onError | 文件加载异常时的回调 | `(Error) => void` | - | - | |
962
+ | onSuccess | 文件加载成功时的回调 | `() => void` | - | - | |
963
+
964
+ #### modalProps
965
+
966
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
967
+ | --- | --- | --- | --- | --- | --- |
968
+ | show | 控制是否展示 | `boolean` | - | - | 是 |
969
+ | title | 标题 | `string` | - | - | |
970
+ | onCancel | Modal关闭时的回调 |`() => void`| - | - | 是|
971
+
972
+ #### tableConfig
973
+
974
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
975
+ | --- | --- | --- | --- | --- | --- |
976
+ | header | 表头 | `string[]` | - | - | 是 |
977
+ | rows | 行数据 | `string[][]` | - | - | 是 |
978
+ | rowCount | 数据总行数,如果传入,内部会判断`rows.length`和`rowCount`的大小,决定是否展示预览提示信息(请下载查看完整内容) | `number` | - | - | 否 |
979
+ | maxLines | 单元格内容最多显示多少行,默认固定显示2行,超出显示省略号 | `number` | - | - | 否 |
980
+
981
+ ---
982
+
983
+ ## file-uploader(文件上传)
984
+
985
+ **导入方式:**
986
+
987
+ ```ts
988
+ // 该组件通过 export * 导出,具体导出项见下方文档
989
+ import { UploadProvider, FileUploader, useUpload /* ... */ } from '@zjlab-fe/data-hub-ui';
990
+ ```
991
+
992
+ 一个支持分片上传、进度追踪的文件上传组件。断点续传 is still in progress
993
+
994
+ ### 目录
995
+
996
+ - [导出清单](#导出清单)
997
+ - [代码演示](#代码演示)
998
+ - [快速开始](#快速开始)
999
+ - [基础用法](#基础用法)
1000
+ - [自定义 UI](#自定义-ui)
1001
+ - [UploadProvider 详解](#uploadprovider-详解)
1002
+ - [它负责什么?](#它负责什么)
1003
+ - [放在哪里?](#放在哪里)
1004
+ - [配置项](#配置项)
1005
+ - [小贴士](#小贴士)
1006
+ - [FileUploader 组件详解](#fileuploader-组件详解)
1007
+ - [Props](#props)
1008
+ - [Handlers(上传回调)](#handlers上传回调)
1009
+ - [HandlerContext(处理器上下文)](#HandlerContext处理器上下文)
1010
+ - [错误处理](#错误处理)
1011
+ - [自定义样式](#自定义样式)
1012
+ - [使用场景](#使用场景)
1013
+ - [Hooks 详解](#hooks-详解)
1014
+ - [useUpload](#useupload)
1015
+ - [useUploadFiles](#useuploadfiles)
1016
+ - [useUploadOperations](#useuploadoperations)
1017
+ - [useUploadControl](#useuploadcontrol)
1018
+ - [useUploadBatch](#useuploadbatch)
1019
+ - [如何选择 Hook?](#如何选择-hook)
1020
+ - [子组件详解](#子组件详解)
1021
+ - [UploaderDropZone](#uploaderdropzone)
1022
+ - [UploaderFileListLayout](#uploaderfilelistlayout)
1023
+ - [UploaderFileItem](#uploaderfileitem)
1024
+ - [组合使用完整示例](#组合使用完整示例)
1025
+
1026
+ ### 导出清单
1027
+
1028
+ #### Provider
1029
+
1030
+ | 导出 | 说明 |
1031
+ | --- | --- |
1032
+ | `UploadProvider` | 状态管理 Provider,负责管理上传状态和全局配置。在 App 或页面根部包裹使用。 |
1033
+
1034
+ #### 组件
1035
+
1036
+ | 导出 | 说明 |
1037
+ | --- | --- |
1038
+ | `FileUploader` | 开箱即用的完整上传器,包含拖拽区和文件列表,文件选择后自动开始上传。 |
1039
+ | `UploaderDropZone` | 拖拽上传区域,支持点击选择、拖拽文件、文件夹上传。 |
1040
+ | `UploaderFileListLayout` | 文件列表容器,显示文件数量和空状态。 |
1041
+ | `UploaderFileItem` | 单个文件项,展示文件名、状态、进度,以及暂停/继续/取消按钮。 |
1042
+
1043
+ #### Hooks
1044
+
1045
+ | 导出 | 说明 |
1046
+ | --- | --- |
1047
+ | `useUpload` | **主 Hook**,整合了下面所有 Hook 的功能,适合需要完整上传能力的场景。 |
1048
+ | `useUploadFiles` | 只读文件状态。返回 `files`、`pendingFiles`、`uploadingFiles`、`successFiles`、`failedFiles`、`pausedFiles` 和 `getFileById`。 |
1049
+ | `useUploadOperations` | 文件队列管理。提供 `addFiles`、`removeFile`、`clearSucceeded`、`clearAll`。 |
1050
+ | `useUploadControl` | 单文件控制。提供 `start`、`pause`、`resume`、`cancel`。 |
1051
+ | `useUploadBatch` | 批量操作。提供 `startAll`、`pauseAll`、`resumeAll`、`cancelAll`。 |
1052
+
1053
+ #### 类型
1054
+
1055
+ | 导出 | 说明 |
1056
+ | --- | --- |
1057
+ | `UploadFile` | 文件状态对象类型 |
1058
+ | `UploadHandlers` | 上传回调处理器类型 |
1059
+ | `HandlerContext` | 处理器上下文类型,用于在回调中获取最新的 files 和操作函数,避免闭包陷阱 |
1060
+ | `UploadConfig` | Provider 配置类型 |
1061
+ | `AddFilesOptions` | addFiles 选项类型 |
1062
+
1063
+ ### 代码演示
1064
+
1065
+ **示例代码(index.tsx):**
1066
+
1067
+ ```tsx
1068
+ import { FileUploader, UploaderDropZone, UploadProvider } from '../index';
1069
+
1070
+ function App() {
1071
+ return (
1072
+ <UploadProvider>
1073
+ <FileUploader
1074
+ classNames={{
1075
+ container: 'dark-mode',
1076
+ }}
1077
+ handlers={{
1078
+ async onGetUploadUrls(fileName) {
1079
+ console.log('Getting upload URLs for', fileName);
1080
+ return {
1081
+ success: true,
1082
+ uploadInfo: {
1083
+ uploadUrls: [],
1084
+ uploadId: '',
1085
+ },
1086
+ };
1087
+ },
1088
+ async onFileComplete(fileName) {
1089
+ console.log('Completing upload for', fileName);
1090
+ return {
1091
+ success: true,
1092
+ };
1093
+ },
1094
+ async onPartComplete(fileName, uploadId, partNum) {
1095
+ console.log(`Part ${partNum} completed for ${fileName}`);
1096
+ return {
1097
+ success: true,
1098
+ };
1099
+ },
1100
+ onError(file, error) {
1101
+ console.error('Upload error for file', file.fileName, error);
1102
+ },
1103
+ }}
1104
+ />
1105
+ <UploaderDropZone onDrop={() => {}} />
1106
+ </UploadProvider>
1107
+ );
1108
+ }
1109
+
1110
+ export default App;
1111
+ ```
1112
+ ### 快速开始
1113
+
1114
+ #### 基础用法
1115
+
1116
+ 最简单的方式是使用 `FileUploader` 组件,它已经帮你把拖拽区和文件列表都搞定了:
1117
+
1118
+ ```tsx
1119
+ import { UploadProvider, FileUploader } from '@/components/file-uploader';
1120
+
1121
+ function MyPage() {
1122
+ const handlers = {
1123
+ // 必填:获取上传地址
1124
+ onGetUploadUrls: async (fileName, partCount) => {
1125
+ const res = await api.getUploadUrls(fileName, partCount);
1126
+ return { success: true, uploadInfo: res };
1127
+ },
1128
+ // 必填:单个分片上传完成
1129
+ onPartComplete: async (fileName, uploadId, partNum) => {
1130
+ await api.onPartComplete(fileName, uploadId, partNum);
1131
+ return { success: true };
1132
+ },
1133
+ // 必填:整个文件上传完成
1134
+ onFileComplete: async (fileName, uploadId) => {
1135
+ await api.onFileComplete(fileName, uploadId);
1136
+ return { success: true };
1137
+ },
1138
+ };
1139
+
1140
+ return (
1141
+ <UploadProvider>
1142
+ <FileUploader handlers={handlers} />
1143
+ </UploadProvider>
1144
+ );
1145
+ }
1146
+ ```
1147
+
1148
+ #### 自定义 UI
1149
+
1150
+ 如果你想自己控制 UI,可以直接用 Hook:
1151
+
1152
+ ```tsx
1153
+ import { UploadProvider, useUpload, UploaderDropZone } from '@/components/file-uploader';
1154
+
1155
+ function CustomUploader({ datasetId }: { datasetId: string }) {
1156
+ const { files, addFiles, start, pause, cancel } = useUpload();
1157
+
1158
+ const handleDrop = (droppedFiles: File[]) => {
1159
+ addFiles(droppedFiles, {
1160
+ handlers: {
1161
+ onGetUploadUrls: (fileName, partCount) => api.getUploadUrls(datasetId, fileName, partCount),
1162
+ onPartComplete: (fileName, uploadId, partNum) => api.onPartComplete(datasetId, uploadId, partNum),
1163
+ onFileComplete: (fileName, uploadId) => api.onFileComplete(datasetId, uploadId),
1164
+ },
1165
+ });
1166
+ };
1167
+
1168
+ return (
1169
+ <div>
1170
+ <UploaderDropZone onDrop={handleDrop} />
1171
+ <ul>
1172
+ {files.map((file) => (
1173
+ <li key={file.id}>
1174
+ {file.fileName} - {file.status} ({file.progress}%)
1175
+ <button onClick={() => start(file.id)}>开始</button>
1176
+ <button onClick={() => pause(file.id)}>暂停</button>
1177
+ <button onClick={() => cancel(file.id)}>取消</button>
1178
+ </li>
1179
+ ))}
1180
+ </ul>
1181
+ </div>
1182
+ );
1183
+ }
1184
+
1185
+ // 记得在外层包 Provider
1186
+ function App() {
1187
+ return (
1188
+ <UploadProvider>
1189
+ <CustomUploader datasetId="123" />
1190
+ </UploadProvider>
1191
+ );
1192
+ }
1193
+ ```
1194
+
1195
+ ### UploadProvider 详解
1196
+
1197
+ `UploadProvider` 是整个上传系统的核心,所有上传组件和 Hook 都依赖它。如果你在没有 Provider 的情况下使用 Hook,会直接报错提醒你。
1198
+
1199
+ #### 它负责什么?
1200
+
1201
+ - **状态管理**:统一管理所有文件的上传状态(pending、uploading、paused、success、failed 等)
1202
+ - **上传队列**:内置 QueueManager,自动控制并发上传,不用担心同时传太多文件把带宽打满
1203
+ - **全局配置**:分片大小、并发数等配置在这里统一设置
1204
+
1205
+ #### 放在哪里?
1206
+
1207
+ 建议放在 App 根组件或页面根部。同一个 Provider 下的所有上传组件会**共享状态**,这意味着:
1208
+
1209
+ ```tsx
1210
+ // ✅ 推荐:App 级别,整个应用共享上传状态
1211
+ function App() {
1212
+ return (
1213
+ <UploadProvider>
1214
+ <Router>
1215
+ <Routes />
1216
+ </Router>
1217
+ </UploadProvider>
1218
+ );
1219
+ }
1220
+
1221
+ // ✅ 也可以:页面级别,该页面内共享
1222
+ function DatasetPage() {
1223
+ return (
1224
+ <UploadProvider>
1225
+ <DatasetUploader />
1226
+ <SomeOtherComponent /> {/* 这里也能通过 useUpload 访问上传状态 */}
1227
+ </UploadProvider>
1228
+ );
1229
+ }
1230
+
1231
+ // ⚠️ 注意:多个 Provider 会隔离状态
1232
+ <UploadProvider>
1233
+ <UploaderA /> {/* 这两个上传器的文件列表是分开的 */}
1234
+ </UploadProvider>
1235
+ <UploadProvider>
1236
+ <UploaderB /> {/* 互不影响 */}
1237
+ </UploadProvider>
1238
+ ```
1239
+
1240
+ #### 配置项
1241
+
1242
+ 通过 `config` prop 可以调整全局上传行为:
1243
+
1244
+ ```tsx
1245
+ <UploadProvider
1246
+ config={{
1247
+ maxChunkSize: 100 * 1024 * 1024, // 最大分片大小,默认 100MB
1248
+ minChunkSize: 5 * 1024 * 1024, // 最小分片大小,默认 5MB
1249
+ concurrentChunks: 2, // 单文件并发分片数,默认 2
1250
+ concurrentFiles: 1, // 同时上传文件数,默认 1
1251
+ }}
1252
+ >
1253
+ <App />
1254
+ </UploadProvider>
1255
+ ```
1256
+
1257
+ | 配置项 | 类型 | 默认值 | 说明 |
1258
+ | --- | --- | --- | --- |
1259
+ | `maxChunkSize` | `number` | `100MB` | 单个分片的最大大小 |
1260
+ | `minChunkSize` | `number` | `5MB` | 单个分片的最小大小 |
1261
+ | `concurrentChunks` | `number` | `2` | 每个文件同时上传的分片数 |
1262
+ | `concurrentFiles` | `number` | `1` | 同时上传的文件数量 |
1263
+
1264
+ #### 小贴士
1265
+
1266
+ - 分片大小会根据文件大小自动计算,在 `minChunkSize` 和 `maxChunkSize` 之间动态调整
1267
+ - `concurrentFiles` 设置为 1 时是串行上传,适合带宽有限或服务端有限制的场景
1268
+ - 如果需要在不同页面使用完全独立的上传队列,可以用多个 Provider 隔离
1269
+ - **并发数建议**:由于浏览器对同一域名有并发请求数限制(通常 6 个),建议 `concurrentChunks × concurrentFiles < 5`,给其他请求留点余量
1270
+
1271
+ ### FileUploader 组件详解
1272
+
1273
+ `FileUploader` 是开箱即用的完整上传组件,内部集成了拖拽区(`UploaderDropZone`)和文件列表(`UploaderFileListLayout` + `UploaderFileItem`)。选择文件后会自动开始上传,适合大多数场景直接使用。
1274
+
1275
+ #### Props
1276
+
1277
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
1278
+ | --- | --- | --- | --- | --- |
1279
+ | `handlers` | `UploadHandlers` | - | ✅ | 上传生命周期回调,详见下方 Handlers 说明 |
1280
+ | `accept` | `string` | `''` | | 允许的文件类型,如 `"image/*"` 或 `".pdf,.doc"` |
1281
+ | `directory` | `boolean` | `false` | | 是否支持文件夹上传 |
1282
+ | `maxFiles` | `number` | - | | 最大文件数量限制 |
1283
+ | `maxSize` | `number` | `10GB` | | 单文件大小限制(字节) |
1284
+ | `dragAreaDescription` | `ReactNode` | `'点击或拖拽上传文件'` | | 拖拽区提示文案 |
1285
+ | `fileValidation` | `(file: File) => boolean \| Promise<boolean>` | - | | 自定义文件校验函数,在文件进入上传队列**之前**执行 |
1286
+ | `onDropZoneError` | `(error: DropZoneError) => void` | - | | 文件进入上传队列**之前**的校验错误回调(如文件过大、数量超限等) |
1287
+ | `classNames` | `object` | - | | 自定义样式类名 |
1288
+
1289
+ #### Handlers(上传回调)
1290
+
1291
+ `handlers` 是上传的核心配置,用于对接你的后端 API。
1292
+
1293
+ ##### 必填回调
1294
+
1295
+ | 回调 | 参数 | 返回值 | 说明 |
1296
+ | --- | --- | --- | --- |
1297
+ | `onGetUploadUrls` | `(fileName, partCount)` | `Promise<GetUploadUrlsResult>` | 获取分片上传地址 |
1298
+ | `onPartComplete` | `(fileName, uploadId, partNum)` | `Promise<CallbackResult>` | 单个分片上传完成后通知服务端 |
1299
+ | `onFileComplete` | `(fileName, uploadId)` | `Promise<CallbackResult>` | 所有分片上传完成后通知服务端(通常触发合并) |
1300
+
1301
+ **返回值类型说明:**
1302
+
1303
+ ```ts
1304
+ // onGetUploadUrls 返回值
1305
+ type GetUploadUrlsResult =
1306
+ | { success: false; reason?: string }
1307
+ | { success: true; uploadInfo: { uploadUrls: UploadUrl[]; uploadId: string } };
1308
+
1309
+ // uploadUrls 数组中每个元素的结构
1310
+ interface UploadUrl {
1311
+ uploadUrl: string; // 分片上传地址
1312
+ partNum: number; // 分片序号
1313
+ }
1314
+
1315
+ // onPartComplete / onFileComplete 返回值
1316
+ type CallbackResult =
1317
+ | { success: true }
1318
+ | { success: false; reason?: string };
1319
+ ```
1320
+
1321
+ **示例:**
1322
+
1323
+ ```tsx
1324
+ const handlers = {
1325
+ onGetUploadUrls: async (fileName, partCount) => {
1326
+ const res = await api.getUploadUrls(fileName, partCount);
1327
+ // 返回成功
1328
+ return { success: true, uploadInfo: { uploadUrls: res.urls, uploadId: res.uploadId } };
1329
+ // 或返回失败
1330
+ // return { success: false, reason: '获取上传地址失败' };
1331
+ },
1332
+
1333
+ onPartComplete: async (fileName, uploadId, partNum) => {
1334
+ await api.onPartComplete(fileName, uploadId, partNum);
1335
+ return { success: true };
1336
+ },
1337
+
1338
+ onFileComplete: async (fileName, uploadId) => {
1339
+ await api.onFileComplete(fileName, uploadId);
1340
+ return { success: true };
1341
+ },
1342
+ };
1343
+ ```
1344
+
1345
+ ##### 可选回调
1346
+
1347
+ | 回调 | 参数 | 返回值 | 说明 |
1348
+ | --- | --- | --- | --- |
1349
+ | `onBeforeUpload` | `(file: UploadFile, ctx: HandlerContext)` | `Promise<CallbackResult>` | 上传前校验,返回 `{ success: false }` 可取消上传 |
1350
+ | `onUploadStart` | `(file: UploadFile, ctx: HandlerContext)` | `void` | 开始上传时触发(获取到上传地址后) |
1351
+ | `onProgress` | `(file: UploadFile, progress: number, ctx: HandlerContext)` | `void` | 进度更新(0-100) |
1352
+ | `onPause` | `(file: UploadFile, ctx: HandlerContext)` | `void` | 暂停时触发 |
1353
+ | `onResume` | `(file: UploadFile, ctx: HandlerContext)` | `void` | 恢复时触发 |
1354
+ | `onUploadSuccess` | `(file: UploadFile, ctx: HandlerContext)` | `void` | 上传成功时触发 |
1355
+ | `onUploadCancel` | `(fileName, uploadId)` | `Promise<CallbackResult>` | 取消上传,通知服务端清理资源 |
1356
+ | `onDeleteFile` | `(file: UploadFile, ctx: HandlerContext)` | `Promise<CallbackResult>` | 从队列中删除文件时触发 |
1357
+ | `onRetry` | `(file: UploadFile, retryType: 'smart' \| 'full', ctx: HandlerContext)` | `void` | 重试上传时触发 |
1358
+ | `onError` | `(file: UploadFile, error: Error, ctx: HandlerContext)` | `void` | 上传出错时触发 |
1359
+
1360
+ ##### HandlerContext(处理器上下文)
1361
+
1362
+ > **设计初衷**:在用户触发上传的那一刻,捕获当时的状态(如当前选中的文件夹),
1363
+ > 每个文件都有自己专属的 handlers
1364
+ >
1365
+ > 但在实际场景中,你可能希望拿到某些最新的数据
1366
+ >
1367
+ > 以下是**临时解决方案**,我们会在后续版本中探索更优雅的解决方案。
1368
+
1369
+ > ⚠️ **重要提示:避免闭包陷阱!**
1370
+ >
1371
+ > 由于 React 的闭包特性,在 handlers 中直接使用 `files`、`removeFile` 等变量会导致**值过期**问题。
1372
+ > 这些值在 handler 注册时被"捕获",之后即使状态更新,handler 中的值也不会更新。
1373
+
1374
+ **错误示例:**
1375
+
1376
+ ```tsx
1377
+ // ❌ 错误:files 和 removeFile 是注册时的旧值,永远不会更新!
1378
+ const { files, removeFile } = useUpload();
1379
+
1380
+ addFiles(selectedFiles, {
1381
+ getHandlers: () => ({
1382
+ onUploadSuccess: (file) => {
1383
+ console.log(files.length); // 始终是 0 或旧值
1384
+ removeFile(file.id); // 使用的是旧版本的 removeFile
1385
+ }
1386
+ })
1387
+ });
1388
+ ```
1389
+
1390
+ **正确示例:**
1391
+
1392
+ ```tsx
1393
+ // ✅ 正确:使用 ctx 参数获取最新值
1394
+ addFiles(selectedFiles, {
1395
+ getHandlers: () => ({
1396
+ onUploadSuccess: (file, ctx) => {
1397
+ console.log(ctx.files.length); // 始终是最新值
1398
+ ctx.removeFile(file.id); // 使用最新版本的 removeFile
1399
+ },
1400
+ onError: (file, error, ctx) => {
1401
+ console.log('上传失败,剩余文件:', ctx.files.length);
1402
+ // 可以使用 ctx.start, ctx.pause, ctx.resume, ctx.cancel 等
1403
+ }
1404
+ })
1405
+ });
1406
+ ```
1407
+
1408
+ **HandlerContext 结构:**
1409
+
1410
+ ```ts
1411
+ interface HandlerContext {
1412
+ files: UploadFile[]; // 当前所有文件(始终最新)
1413
+ removeFile: (id: string) => Promise<void>; // 移除文件
1414
+ start: (id: string) => Promise<void>; // 开始上传
1415
+ pause: (id: string) => void; // 暂停上传
1416
+ resume: (id: string) => Promise<void>; // 恢复上传
1417
+ cancel: (id: string) => void; // 取消上传
1418
+ getFileById: (id: string) => UploadFile | undefined; // 获取指定文件
1419
+ }
1420
+ ```
1421
+
1422
+ ##### 处理自定义状态的闭包问题
1423
+
1424
+ `HandlerContext` 只能解决上传组件内部状态的闭包问题。如果你在 handler 中使用了**自己组件的状态**,同样会遇到闭包陷阱:
1425
+
1426
+ ```tsx
1427
+ // ❌ 错误:selectedFolder 也会过期!
1428
+ const [selectedFolder, setSelectedFolder] = useState('folder-1');
1429
+
1430
+ addFiles(files, {
1431
+ getHandlers: () => ({
1432
+ onUploadSuccess: (file, ctx) => {
1433
+ api.moveToFolder(file.id, selectedFolder); // selectedFolder 永远是 'folder-1'
1434
+ }
1435
+ })
1436
+ });
1437
+ ```
1438
+
1439
+ **临时解决方案:使用 useRef 保存最新值**
1440
+
1441
+ ```tsx
1442
+ const [selectedFolder, setSelectedFolder] = useState('folder-1');
1443
+ const selectedFolderRef = useRef(selectedFolder);
1444
+
1445
+ // 保持 ref 始终是最新值
1446
+ useEffect(() => {
1447
+ selectedFolderRef.current = selectedFolder;
1448
+ }, [selectedFolder]);
1449
+
1450
+ addFiles(files, {
1451
+ getHandlers: () => ({
1452
+ onUploadSuccess: (file, ctx) => {
1453
+ api.moveToFolder(file.id, selectedFolderRef.current); // ✅ 始终是最新值
1454
+ }
1455
+ })
1456
+ });
1457
+ ```
1458
+
1459
+ #### 错误处理
1460
+
1461
+ 当文件校验失败时(比如文件太大、数量超限),会触发 `onDropZoneError`:
1462
+
1463
+ ```tsx
1464
+ <FileUploader
1465
+ handlers={handlers}
1466
+ maxSize={100 * 1024 * 1024} // 100MB
1467
+ maxFiles={10}
1468
+ onDropZoneError={(error) => {
1469
+ // error.type: 'FILE_TOO_LARGE' | 'TOO_MANY_FILES' | 'INVALID_TYPE' | 'DROP_FAILED'
1470
+ // error.message: 错误描述
1471
+ // error.rejectedFiles: 被拒绝的文件列表
1472
+ message.error(error.message);
1473
+ }}
1474
+ />
1475
+ ```
1476
+
1477
+ #### 自定义样式
1478
+
1479
+ 通过 `classNames` 可以覆盖各部分的样式, 基本包含了组件中出现所有html元素:
1480
+
1481
+ ```tsx
1482
+ <FileUploader
1483
+ handlers={handlers}
1484
+ classNames={{
1485
+ container: 'my-uploader', // 最外层容器
1486
+ dragZone: {
1487
+ container: 'my-drop-zone', // 拖拽区容器
1488
+ dragArea: 'my-drag-area', // 拖拽区域
1489
+ dragging: 'my-dragging', // 拖拽中状态
1490
+ disabled: 'my-disabled', // 禁用状态
1491
+ icon: 'my-icon', // 图标
1492
+ description: 'my-description', // 提示文字
1493
+ input: 'my-input', // 文件输入框
1494
+ },
1495
+ fileList: {
1496
+ container: 'my-file-list', // 文件列表容器
1497
+ title: 'my-file-list-title', // 文件列表标题
1498
+ listWrapper: 'my-list-wrapper', // 文件列表包装器
1499
+ empty: 'my-empty-state', // 空状态
1500
+ },
1501
+ fileItem: {
1502
+ container: 'my-file-item', // 单个文件项
1503
+ info: 'my-file-info', // 文件信息区域
1504
+ icon: 'my-file-icon', // 文件图标
1505
+ name: 'my-file-name', // 文件名
1506
+ relativePath: 'my-relative-path', // 相对路径
1507
+ status: 'my-status', // 状态区域
1508
+ statusText: 'my-status-text', // 状态文字
1509
+ progress: 'my-progress', // 进度条
1510
+ pauseBtn: 'my-pause-btn', // 暂停按钮
1511
+ resumeBtn: 'my-resume-btn', // 继续按钮
1512
+ closeBtn: 'my-close-btn', // 关闭按钮
1513
+ },
1514
+ }}
1515
+ />
1516
+ ```
1517
+
1518
+ #### 使用场景
1519
+
1520
+ ```tsx
1521
+ // 场景 1:基础上传
1522
+ <FileUploader handlers={handlers} />
1523
+
1524
+ // 场景 2:只允许上传图片,最多 5 张
1525
+ <FileUploader
1526
+ handlers={handlers}
1527
+ accept="image/*"
1528
+ maxFiles={5}
1529
+ directory={false}
1530
+ />
1531
+
1532
+ // 场景 3:大文件上传,限制 50GB
1533
+ <FileUploader
1534
+ handlers={handlers}
1535
+ maxSize={50 * 1024 * 1024 * 1024}
1536
+ dragAreaDescription="支持上传最大 50GB 的文件"
1537
+ />
1538
+
1539
+ // 场景 4:自定义校验(只允许特定命名格式)
1540
+ <FileUploader
1541
+ handlers={handlers}
1542
+ fileValidation={(file) => /^data_\d+\.csv$/.test(file.name)}
1543
+ onDropZoneError={(e) => message.error('文件名必须是 data_数字.csv 格式')}
1544
+ />
1545
+ ```
1546
+
1547
+ ### Hooks 详解
1548
+
1549
+ 如果你需要自定义 UI 或更细粒度的控制,可以直接使用 Hooks。我们提供了 5 个 Hook,按需选用:
1550
+
1551
+ #### useUpload
1552
+
1553
+ **主 Hook**,整合了下面 4 个 Hook 的所有功能。如果你不确定用哪个,直接用这个就对了。
1554
+
1555
+ ```tsx
1556
+ const {
1557
+ // 来自 useUploadFiles
1558
+ files,
1559
+ getFileById,
1560
+ pendingFiles,
1561
+ uploadingFiles,
1562
+ successFiles,
1563
+ failedFiles,
1564
+ pausedFiles,
1565
+
1566
+ // 来自 useUploadOperations
1567
+ addFiles,
1568
+ removeFile,
1569
+ clearSucceeded,
1570
+ clearAll,
1571
+
1572
+ // 来自 useUploadControl
1573
+ start,
1574
+ pause,
1575
+ resume,
1576
+ cancel,
1577
+
1578
+ // 来自 useUploadBatch
1579
+ startAll,
1580
+ pauseAll,
1581
+ resumeAll,
1582
+ cancelAll,
1583
+ } = useUpload();
1584
+ ```
1585
+
1586
+ ---
1587
+
1588
+ #### useUploadFiles
1589
+
1590
+ 只读文件状态,适合只需要展示文件列表、不需要操作的场景。
1591
+
1592
+ ```tsx
1593
+ const {
1594
+ files, // UploadFile[] - 所有文件
1595
+ getFileById, // (id: string) => UploadFile | undefined
1596
+ pendingFiles, // UploadFile[] - 等待上传的文件
1597
+ uploadingFiles, // UploadFile[] - 正在上传的文件
1598
+ successFiles, // UploadFile[] - 上传完成的文件
1599
+ failedFiles, // UploadFile[] - 上传失败的文件
1600
+ pausedFiles, // UploadFile[] - 已暂停的文件
1601
+ } = useUploadFiles();
1602
+ ```
1603
+
1604
+ **UploadFile 结构:**
1605
+
1606
+ ```ts
1607
+ interface UploadFile {
1608
+ id: string; // 文件唯一标识
1609
+ file: File; // 原始 File 对象
1610
+ fileName: string; // 文件名
1611
+ fileSize: number; // 文件大小(字节)
1612
+ status: UploadStatusType; // 状态:pending | preparing | uploading | paused | merging | success | failed | cancelled
1613
+ progress: number; // 上传进度(0-100)
1614
+ uploadId: string; // 服务端返回的上传 ID
1615
+ chunks: ChunkState[]; // 分片状态列表
1616
+ error?: string; // 错误信息
1617
+ createdAt: number; // 添加时间戳
1618
+ }
1619
+ ```
1620
+
1621
+ ---
1622
+
1623
+ #### useUploadOperations
1624
+
1625
+ 文件队列管理,用于添加、删除、清理文件。
1626
+
1627
+ ```tsx
1628
+ const {
1629
+ addFiles, // 添加文件到队列
1630
+ removeFile, // 移除单个文件
1631
+ clearSucceeded, // 清理所有已完成的文件
1632
+ clearAll, // 清理所有文件
1633
+ } = useUploadOperations();
1634
+ ```
1635
+
1636
+ **addFiles 用法:**
1637
+
1638
+ ```tsx
1639
+ // 方式 1:所有文件使用相同的 handlers
1640
+ addFiles(fileList, {
1641
+ handlers: {
1642
+ onGetUploadUrls: ...,
1643
+ onPartComplete: ...,
1644
+ onFileComplete: ...,
1645
+ },
1646
+ });
1647
+
1648
+ // 方式 2:每个文件使用不同的 handlers(比如上传到不同目录)
1649
+ addFiles(fileList, {
1650
+ getHandlers: (file) => ({
1651
+ onGetUploadUrls: (fileName, partCount) =>
1652
+ api.getUploadUrls(getFolderIdFromFileName(file.name), fileName, partCount),
1653
+ onPartComplete: ...,
1654
+ onFileComplete: ...,
1655
+ }),
1656
+ });
1657
+ ```
1658
+
1659
+ **其他方法:**
1660
+
1661
+ ```tsx
1662
+ removeFile(fileId); // 移除文件,会触发 handlers.onDeleteFile
1663
+ clearSucceeded(); // 清理所有 status === 'success' 的文件
1664
+ clearAll(); // 清理所有文件(不管状态)
1665
+ ```
1666
+
1667
+ ---
1668
+
1669
+ #### useUploadControl
1670
+
1671
+ 单文件控制,用于控制单个文件的上传流程。
1672
+
1673
+ ```tsx
1674
+ const {
1675
+ start, // (id: string) => Promise<void> - 开始上传
1676
+ pause, // (id: string) => void - 暂停上传
1677
+ resume, // (id: string) => Promise<void> - 恢复上传
1678
+ cancel, // (id: string) => void - 取消上传
1679
+ } = useUploadControl();
1680
+ ```
1681
+
1682
+ **示例:**
1683
+
1684
+ ```tsx
1685
+ function FileActions({ file }: { file: UploadFile }) {
1686
+ const { start, pause, resume, cancel } = useUploadControl();
1687
+
1688
+ return (
1689
+ <div>
1690
+ {file.status === 'pending' && (
1691
+ <button onClick={() => start(file.id)}>开始</button>
1692
+ )}
1693
+ {file.status === 'uploading' && (
1694
+ <button onClick={() => pause(file.id)}>暂停</button>
1695
+ )}
1696
+ {file.status === 'paused' && (
1697
+ <button onClick={() => resume(file.id)}>继续</button>
1698
+ )}
1699
+ {['pending', 'uploading', 'paused'].includes(file.status) && (
1700
+ <button onClick={() => cancel(file.id)}>取消</button>
1701
+ )}
1702
+ </div>
1703
+ );
1704
+ }
1705
+ ```
1706
+
1707
+ ---
1708
+
1709
+ #### useUploadBatch
1710
+
1711
+ 批量操作,一次控制所有文件。
1712
+
1713
+ ```tsx
1714
+ const {
1715
+ startAll, // () => Promise<void> - 开始所有 pending 文件
1716
+ pauseAll, // () => void - 暂停所有 uploading 文件
1717
+ resumeAll, // () => Promise<void> - 恢复所有 paused 文件
1718
+ cancelAll, // () => void - 取消所有未完成的文件
1719
+ } = useUploadBatch();
1720
+ ```
1721
+
1722
+ **示例:**
1723
+
1724
+ ```tsx
1725
+ function BatchControls() {
1726
+ const { pendingFiles, uploadingFiles, pausedFiles } = useUploadFiles();
1727
+ const { startAll, pauseAll, resumeAll, cancelAll } = useUploadBatch();
1728
+
1729
+ return (
1730
+ <div>
1731
+ {pendingFiles.length > 0 && (
1732
+ <button onClick={startAll}>全部开始 ({pendingFiles.length})</button>
1733
+ )}
1734
+ {uploadingFiles.length > 0 && (
1735
+ <button onClick={pauseAll}>全部暂停 ({uploadingFiles.length})</button>
1736
+ )}
1737
+ {pausedFiles.length > 0 && (
1738
+ <button onClick={resumeAll}>全部继续 ({pausedFiles.length})</button>
1739
+ )}
1740
+ <button onClick={cancelAll}>全部取消</button>
1741
+ </div>
1742
+ );
1743
+ }
1744
+ ```
1745
+
1746
+ ---
1747
+
1748
+ #### 如何选择 Hook?
1749
+
1750
+ | 场景 | 推荐 Hook |
1751
+ | --- | --- |
1752
+ | 完整功能,不想分开引入 | `useUpload` |
1753
+ | 只需要展示文件列表 | `useUploadFiles` |
1754
+ | 只需要添加/删除文件 | `useUploadOperations` |
1755
+ | 只需要控制单个文件 | `useUploadControl` |
1756
+ | 只需要批量操作 | `useUploadBatch` |
1757
+ | 组合使用,按需引入 | 分别引入需要的 Hook |
1758
+
1759
+ ### 子组件详解
1760
+
1761
+ 如果你不想用 `Uploader` 一体化组件,可以自由组合这三个子组件来构建自定义 UI。
1762
+
1763
+ #### UploaderDropZone
1764
+
1765
+ 拖拽上传区域,支持点击选择、拖拽文件、文件夹上传。这是一个纯 UI 组件,不依赖上传状态。
1766
+
1767
+ ##### Props
1768
+
1769
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
1770
+ | --- | --- | --- | --- | --- |
1771
+ | `onDrop` | `(files: File[]) => void` | - | ✅ | 文件选择/拖入后的回调 |
1772
+ | `accept` | `string` | `''` | | 允许的文件类型,如 `"image/*"` 或 `".pdf,.doc"` |
1773
+ | `maxSize` | `number` | - | | 单文件大小限制(字节) |
1774
+ | `maxFiles` | `number` | - | | 最大文件数量 |
1775
+ | `directory` | `boolean` | `false` | | 是否支持文件夹上传 |
1776
+ | `disabled` | `boolean` | `false` | | 禁用状态 |
1777
+ | `dragAreaDescription` | `ReactNode` | `'点击或拖拽上传文件'` | | 提示文案 |
1778
+ | `icon` | `ReactNode \| string` | - | | 自定义图标(组件或图片路径) |
1779
+ | `fileValidation` | `(file: File) => boolean \| Promise<boolean>` | - | | 自定义文件校验 |
1780
+ | `onError` | `(error: DropZoneError) => void` | - | | 校验失败回调 |
1781
+ | `classNames` | `UploaderDropZoneClassNames` | - | | 自定义样式 |
1782
+
1783
+ ##### classNames 结构
1784
+
1785
+ ```ts
1786
+ interface UploaderDropZoneClassNames {
1787
+ container?: string; // 容器
1788
+ dragArea?: string; // 拖拽区域
1789
+ dragging?: string; // 拖拽中状态
1790
+ disabled?: string; // 禁用状态
1791
+ icon?: string; // 图标
1792
+ description?: string; // 提示文字
1793
+ input?: string; // 隐藏的 input
1794
+ }
1795
+ ```
1796
+
1797
+ ##### 示例
1798
+
1799
+ ```tsx
1800
+ import { UploaderDropZone } from '@/components/file-uploader';
1801
+
1802
+ function MyDropZone() {
1803
+ const handleDrop = (files: File[]) => {
1804
+ console.log('选中的文件:', files);
1805
+ // 通常这里调用 addFiles
1806
+ };
1807
+
1808
+ return (
1809
+ <UploaderDropZone
1810
+ onDrop={handleDrop}
1811
+ accept="image/*"
1812
+ maxSize={10 * 1024 * 1024}
1813
+ dragAreaDescription="拖入图片或点击上传"
1814
+ onError={(error) => console.error(error.message)}
1815
+ />
1816
+ );
1817
+ }
1818
+ ```
1819
+
1820
+ ---
1821
+
1822
+ #### UploaderFileListLayout
1823
+
1824
+ 文件列表容器,显示文件数量标题和空状态。内部使用 `useUploadFiles` 获取文件列表长度。
1825
+
1826
+ ##### Props
1827
+
1828
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
1829
+ | --- | --- | --- | --- | --- |
1830
+ | `children` | `ReactNode` | - | ✅ | 子元素(通常是 `UploaderFileItem` 列表) |
1831
+ | `emptyText` | `string` | `'暂无文件'` | | 空状态提示文案 |
1832
+ | `classNames` | `FileListClassNames` | - | | 自定义样式 |
1833
+
1834
+ ##### classNames 结构
1835
+
1836
+ ```ts
1837
+ interface FileListClassNames {
1838
+ container?: string; // 列表容器
1839
+ empty?: string; // 空状态容器
1840
+ }
1841
+ ```
1842
+
1843
+ ##### 示例
1844
+
1845
+ ```tsx
1846
+ import { UploaderFileListLayout, UploaderFileItem, useUploadFiles } from '@/components/file-uploader';
1847
+
1848
+ function MyFileList() {
1849
+ const { files } = useUploadFiles();
1850
+
1851
+ return (
1852
+ <UploaderFileListLayout emptyText="还没有上传任何文件">
1853
+ {files.map((file) => (
1854
+ <UploaderFileItem key={file.id} file={file} />
1855
+ ))}
1856
+ </UploaderFileListLayout>
1857
+ );
1858
+ }
1859
+ ```
1860
+
1861
+ ---
1862
+
1863
+ #### UploaderFileItem
1864
+
1865
+ 单个文件项,显示文件名、状态、进度圈、操作按钮。这是一个受控组件,需要传入 `file` 和操作回调。
1866
+
1867
+ ##### Props
1868
+
1869
+ | 属性 | 类型 | 默认值 | 必填 | 说明 |
1870
+ | --- | --- | --- | --- | --- |
1871
+ | `file` | `UploadFile` | - | ✅ | 文件对象 |
1872
+ | `onCancel` | `(id: string) => void` | - | | 取消上传回调 |
1873
+ | `onRemove` | `(id: string) => void` | - | | 移除文件回调(用于已完成/失败/已取消的文件) |
1874
+ | `onPause` | `(id: string) => void` | - | | 暂停上传回调 |
1875
+ | `onResume` | `(id: string) => void` | - | | 恢复上传回调 |
1876
+ | `classNames` | `UploaderFileItemClassNames` | - | | 自定义样式 |
1877
+
1878
+ ##### classNames 结构
1879
+
1880
+ ```ts
1881
+ interface UploaderFileItemClassNames {
1882
+ container?: string; // 根容器
1883
+ info?: string; // 文件信息区域
1884
+ icon?: string; // 文件图标
1885
+ name?: string; // 文件名
1886
+ status?: string; // 状态区域
1887
+ statusText?: string; // 状态文字
1888
+ progress?: string; // 进度圈容器
1889
+ pauseBtn?: string; // 暂停按钮
1890
+ resumeBtn?: string; // 继续按钮
1891
+ closeBtn?: string; // 关闭/取消按钮
1892
+ }
1893
+ ```
1894
+
1895
+ ##### 状态显示逻辑
1896
+
1897
+ - **关闭按钮**:始终显示。已完成/失败/已取消时调用 `onRemove`,其他状态调用 `onCancel`
1898
+ - **暂停按钮**:上传中且传入了 `onPause` 时显示
1899
+ - **继续按钮**:已暂停且传入了 `onResume` 时显示
1900
+
1901
+ ##### 示例
1902
+
1903
+ ```tsx
1904
+ import { UploaderFileItem, useUploadFiles, useUploadControl } from '@/components/file-uploader';
1905
+
1906
+ function MyFileItem({ file }: { file: UploadFile }) {
1907
+ const { pause, resume, cancel } = useUploadControl();
1908
+ const { removeFile } = useUploadOperations();
1909
+
1910
+ return (
1911
+ <UploaderFileItem
1912
+ file={file}
1913
+ onPause={pause}
1914
+ onResume={resume}
1915
+ onCancel={cancel}
1916
+ onRemove={removeFile}
1917
+ />
1918
+ );
1919
+ }
1920
+ ```
1921
+
1922
+ ---
1923
+
1924
+ #### 组合使用完整示例
1925
+
1926
+ ```tsx
1927
+ import {
1928
+ UploadProvider,
1929
+ UploaderDropZone,
1930
+ UploaderFileListLayout,
1931
+ UploaderFileItem,
1932
+ useUpload,
1933
+ } from '@/components/file-uploader';
1934
+
1935
+ function CustomUploader({ datasetId }: { datasetId: string }) {
1936
+ const { files, addFiles, pause, resume, cancel, removeFile } = useUpload();
1937
+
1938
+ const handlers = {
1939
+ onGetUploadUrls: (fileName, partCount) => api.getUploadUrls(datasetId, fileName, partCount),
1940
+ onPartComplete: (fileName, uploadId, partNum) => api.onPartComplete(datasetId, uploadId, partNum),
1941
+ onFileComplete: (fileName, uploadId) => api.onFileComplete(datasetId, uploadId),
1942
+ };
1943
+
1944
+ return (
1945
+ <div className="my-uploader">
1946
+ {/* 拖拽区 */}
1947
+ <UploaderDropZone
1948
+ onDrop={(droppedFiles) => addFiles(droppedFiles, { handlers })}
1949
+ accept="image/*,.pdf"
1950
+ maxSize={100 * 1024 * 1024}
1951
+ directory
1952
+ />
1953
+
1954
+ {/* 文件列表 */}
1955
+ <UploaderFileListLayout emptyText="拖入文件开始上传">
1956
+ {files.map((file) => (
1957
+ <UploaderFileItem
1958
+ key={file.id}
1959
+ file={file}
1960
+ onPause={pause}
1961
+ onResume={resume}
1962
+ onCancel={cancel}
1963
+ onRemove={removeFile}
1964
+ />
1965
+ ))}
1966
+ </UploaderFileListLayout>
1967
+ </div>
1968
+ );
1969
+ }
1970
+
1971
+ // 使用
1972
+ function App() {
1973
+ return (
1974
+ <UploadProvider>
1975
+ <CustomUploader datasetId="123" />
1976
+ </UploadProvider>
1977
+ );
1978
+ }
1979
+ ```
1980
+
1981
+ ---
1982
+
1983
+ ## floating-label-input(浮动标签输入框)
1984
+
1985
+ **导入方式:**
1986
+
1987
+ ```ts
1988
+ import { FloatingLabelInput } from '@zjlab-fe/data-hub-ui';
1989
+ ```
1990
+
1991
+ 使用:`import { FloatingLabelInput } from '@zjlab-fe/data-hub-ui';`
1992
+
1993
+ ### 代码演示
1994
+
1995
+ **示例代码(index.tsx):**
1996
+
1997
+ ```tsx
1998
+ import React from 'react';
1999
+ import FloatingLabelInput from '../index';
2000
+ import { Button, Checkbox, Form, Input } from 'antd';
2001
+ import type { FormProps } from 'antd';
2002
+ import * as styles from '../index.module.scss';
2003
+
2004
+ type FieldType = {
2005
+ testtest?: string;
2006
+ passwor2d?: string;
2007
+ remember?: string;
2008
+ };
2009
+
2010
+ export default function Demo() {
2011
+ const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
2012
+ console.log('Success:', values);
2013
+ };
2014
+
2015
+ const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => {
2016
+ console.log('Failed:', errorInfo);
2017
+ };
2018
+ return (
2019
+ <>
2020
+ ### 普通使用
2021
+ <FloatingLabelInput
2022
+ placeholder="请输入"
2023
+ onChange={(e) => {
2024
+ console.log(e.target.value);
2025
+ }}
2026
+ onBlur={(e) => {
2027
+ console.log('onBlur', e.target.value);
2028
+ }}
2029
+ ></FloatingLabelInput>
2030
+ <br />
2031
+ ### 在form表单里使用
2032
+ <Form
2033
+ name="basic"
2034
+ style={{ maxWidth: 600 }}
2035
+ initialValues={{ remember: true }}
2036
+ onFinish={onFinish}
2037
+ onFinishFailed={onFinishFailed}
2038
+ autoComplete="off"
2039
+ >
2040
+ <Form.Item<FieldType>
2041
+ label="Testtest"
2042
+ name="testtest"
2043
+ rules={[{ required: true, message: 'Please input your username!' }]}
2044
+ >
2045
+ <FloatingLabelInput
2046
+ placeholder="请输入"
2047
+ onChange={(e) => {
2048
+ console.log(e.target.value);
2049
+ }}
2050
+ onBlur={(e) => {
2051
+ console.log('onBlur', e.target.value);
2052
+ }}
2053
+ wrapperStyle={{
2054
+ width: 200,
2055
+ }}
2056
+ ></FloatingLabelInput>
2057
+ </Form.Item>
2058
+
2059
+ <Form.Item<FieldType>
2060
+ label="Passwo2rd"
2061
+ name="passwor2d"
2062
+ rules={[{ required: true, message: 'Please input your password!' }]}
2063
+ >
2064
+ <FloatingLabelInput
2065
+ placeholder="请输入"
2066
+ onChange={(e) => {
2067
+ console.log(e.target.value);
2068
+ }}
2069
+ onBlur={(e) => {
2070
+ console.log('onBlur', e.target.value);
2071
+ }}
2072
+ wrapperStyle={{
2073
+ width: 200,
2074
+ }}
2075
+ ></FloatingLabelInput>
2076
+ </Form.Item>
2077
+
2078
+ <Form.Item<FieldType> name="remember" valuePropName="checked" label={null}>
2079
+ <Checkbox>Remember me</Checkbox>
2080
+ </Form.Item>
2081
+
2082
+ <Form.Item label={null}>
2083
+ <Button type="primary" htmlType="submit">
2084
+ Submit
2085
+ </Button>
2086
+ </Form.Item>
2087
+ </Form>
2088
+ <br />
2089
+ ### 设置inline
2090
+ <FloatingLabelInput
2091
+ placeholder="请输入"
2092
+ wrapperStyle={{
2093
+ width: 200,
2094
+ }}
2095
+ inline
2096
+ ></FloatingLabelInput>
2097
+ <FloatingLabelInput
2098
+ placeholder="请输入"
2099
+ wrapperStyle={{
2100
+ width: 200,
2101
+ }}
2102
+ inline
2103
+ ></FloatingLabelInput>
2104
+ <br />
2105
+ ### 设置wrapperClassName
2106
+ <FloatingLabelInput
2107
+ placeholder="请输入"
2108
+ wrapperClassName={styles['slide-input-wrapper']}
2109
+ ></FloatingLabelInput>
2110
+ </>
2111
+ );
2112
+ }
2113
+ ```
2114
+ ### API
2115
+
2116
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
2117
+ | --- | --- | --- | --- | --- | --- |
2118
+ | id | 绑定在输入框根节点的id| `string` | - | - | 否 |
2119
+ | placeholder | 输入框提示文字 | `string` | - | - | |
2120
+ | inline | 是否显示为行内元素 | `boolean` | `false` | - | |
2121
+ | wrapperStyle | 包装器的样式 | `React.CSSProperties` | `{}` | - | |
2122
+ | wrapperClassName | 包装器的类名 | `string` | `''` | - | |
2123
+ | onFocus | 输入框获得焦点时的回调 | `(e: React.FocusEvent<HTMLInputElement>) => void` | - | - | |
2124
+ | onBlur | 输入框失去焦点时的回调 | `(e: React.FocusEvent<HTMLInputElement>) => void` | - | - | |
2125
+ | 其他属性 | 支持原生Input组件的其他所有属性 | - | - | - | |
2126
+
2127
+ ---
2128
+
2129
+ ## floating-layer(浮动操作框)
2130
+
2131
+ **导入方式:**
2132
+
2133
+ ```ts
2134
+ import { FloatingLayer } from '@zjlab-fe/data-hub-ui';
2135
+ ```
2136
+
2137
+ ### 代码演示
2138
+
2139
+ **示例代码(index.tsx):**
2140
+
2141
+ ```tsx
2142
+ import { useState } from 'react';
2143
+ import FloatingLayer from '@/components/floating-layer';
2144
+ import { Button } from 'antd';
2145
+
2146
+ export default function Demo() {
2147
+ const [open, setOpen] = useState(false);
2148
+ const openFloat = function () {
2149
+ setOpen(true);
2150
+ };
2151
+ const onClose = () => {
2152
+ setOpen(false);
2153
+ };
2154
+ return (
2155
+ <>
2156
+ <Button onClick={() => openFloat()}>打开浮层</Button>
2157
+ <FloatingLayer visiable={open} onClose={onClose}>
2158
+ <div>这里是插槽</div>
2159
+ </FloatingLayer>
2160
+ </>
2161
+ );
2162
+ }
2163
+ ```
2164
+ ### API
2165
+
2166
+
2167
+
2168
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
2169
+ | --- | --- | --- | --- | --- | --- |
2170
+ | visiable | 展示浮层 | `boolean` | - | - | 是 |
2171
+ | onClose | 关闭浮层时的回调 | `() => void` | - | - | 是|
2172
+ | children | 浮层内子节点 | `React.ReactNode` | - | - | |
2173
+ | className | 如传入,将添加到根节点上 | `string` | - | >= 0.11.2| |
2174
+ | style | 如传入,将添加到根节点上 | `React.CSSProperties` | - | >= 0.11.2 | |
2175
+
2176
+ ---
2177
+
2178
+ ## header(头部导航)
2179
+
2180
+ **导入方式:**
2181
+
2182
+ ```ts
2183
+ import { Header } from '@zjlab-fe/data-hub-ui';
2184
+ ```
2185
+
2186
+ > 该组件暂无 README 文档,以下为 demo 示例代码,可参考用法。
2187
+
2188
+ ### 示例代码(index.tsx)
2189
+
2190
+ ```tsx
2191
+ import React from 'react';
2192
+ import Header from '@/components/header';
2193
+
2194
+ export default function Demo() {
2195
+ return <Header showLogin />;
2196
+ }
2197
+ ```
2198
+
2199
+ ---
2200
+
2201
+ ## input-tag(标签/关键字输入)
2202
+
2203
+ **导入方式:**
2204
+
2205
+ ```ts
2206
+ import { InputTag } from '@zjlab-fe/data-hub-ui';
2207
+ ```
2208
+ ```ts
2209
+ // 类型导入
2210
+ import type { IProps as InputTagProps } from '@zjlab-fe/data-hub-ui';
2211
+ ```
2212
+
2213
+ 用于输入多个标签/关键字,回车将输入内容添加为标签或者关键字
2214
+
2215
+ ### 代码演示
2216
+
2217
+ #### 示例1
2218
+
2219
+ 期望传回字符串
2220
+
2221
+ **示例代码(index.tsx):**
2222
+
2223
+ ```tsx
2224
+ import React, { useState } from 'react';
2225
+ import InputTag, { IProps } from '@/components/input-tag';
2226
+ export default function Demo() {
2227
+ const [value, setValue] = useState('abc;def');
2228
+ const onChange: IProps['onChange'] = (v) => {
2229
+ console.log('+++ value', v);
2230
+ setValue(v as string);
2231
+ };
2232
+ return <InputTag valueType="string" value={value} onChange={onChange} maxLength={20} placeholder="标签" />;
2233
+ }
2234
+ ```
2235
+ #### 示例2
2236
+
2237
+ 期望传回数组
2238
+
2239
+ **示例代码(index2.tsx):**
2240
+
2241
+ ```tsx
2242
+ import React, { useState } from 'react';
2243
+ import InputTag, { IProps } from '@/components/input-tag';
2244
+ export default function Demo() {
2245
+ const [value, setValue] = useState(['abc', 'def']);
2246
+ const onChange: IProps['onChange'] = (v) => {
2247
+ console.log('+++ value', v);
2248
+ setValue(v as string[]);
2249
+ };
2250
+ return <InputTag valueType="array" value={value} onChange={onChange} maxLength={20} placeholder="标签" />;
2251
+ }
2252
+ ```
2253
+ ### API
2254
+
2255
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
2256
+ | --- | --- | --- | --- | --- | --- |
2257
+ | valueType | 值变更回调时,期望传回的值的类型,如果期望传回字符串,则传回的值是英文分号分隔的字符串 | `array \| string` | - | - | 是 |
2258
+ | value | 当前值 | `string[] \| string \| undefined` | | | |
2259
+ | onChange | 内容变化时的回调 | `(value: string[] \| string \| undefined) => void` | | |
2260
+ | placeholder | 占位内容 | `string` | 请输入关键字 | | |
2261
+ | maxLength | 可输入的字符个数 | `number` | | | |
2262
+ | size | 组件大小 | `large \| middle \| small` | middle | | |
2263
+ | disabled | 是否禁止操作 | `boolean` | `false` | | |
2264
+ | deduplicated | 是否去除重复 | `boolean` | `true` | | |
2265
+
2266
+ ---
2267
+
2268
+ ## menu(菜单)
2269
+
2270
+ **导入方式:**
2271
+
2272
+ ```ts
2273
+ import { Menu } from '@zjlab-fe/data-hub-ui';
2274
+ ```
2275
+
2276
+ 符合团队设计规范的menu菜单
2277
+
2278
+ ### 代码演示
2279
+
2280
+ **示例代码(index.tsx):**
2281
+
2282
+ ```tsx
2283
+ import { useState } from 'react';
2284
+ import { DatabaseOutlined, TagsOutlined, FilterOutlined } from '@ant-design/icons';
2285
+ import Menu from '../index';
2286
+ const menus = [
2287
+ {
2288
+ label: '数据增强',
2289
+ key: 'data-enhance',
2290
+ icon: <DatabaseOutlined></DatabaseOutlined>,
2291
+ },
2292
+ {
2293
+ label: '数据管理',
2294
+ key: 'data-management',
2295
+ icon: <TagsOutlined />,
2296
+ },
2297
+ { label: '模型管理', key: 'model-management', icon: <FilterOutlined /> },
2298
+ ];
2299
+
2300
+ export default function Demo() {
2301
+ const [collapsed, setCollapsed] = useState(false);
2302
+
2303
+ return (
2304
+ <>
2305
+ <Menu title="数据增强" menus={menus} collapsed={collapsed} onCollapse={setCollapsed}></Menu>
2306
+ </>
2307
+ );
2308
+ }
2309
+ ```
2310
+ ### API
2311
+
2312
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
2313
+ | --- | --- | --- | --- | --- | --- |
2314
+ | menus | 菜单项目 |
2315
+ | title | 菜单标题 |
2316
+ | collapsed | 是否折叠 | `boolean` | - | - | |
2317
+ | onCollapse | 折叠的回调 | `(e: React.MouseEvent<HTMLButtonElement>) => void;` | - | - | |
2318
+
2319
+ ---
2320
+
2321
+ ## operator-chain(卡片式操作链)
2322
+
2323
+ **导入方式:**
2324
+
2325
+ ```ts
2326
+ import { OperatorChain } from '@zjlab-fe/data-hub-ui';
2327
+ ```
2328
+ ```ts
2329
+ // 类型导入
2330
+ import type { OperatorChainProps, SingleCardItem, OperatorChainRef } from '@zjlab-fe/data-hub-ui';
2331
+ ```
2332
+
2333
+ 用于展示和管理算子流程的链式组件
2334
+ 支持数据导入导出节点
2335
+ 支持是否可编辑模式
2336
+ 符合团队样式规范
2337
+
2338
+ ### 代码演示
2339
+
2340
+ **示例代码(index.tsx):**
2341
+
2342
+ ```tsx
2343
+ import { Button, Flex, message } from 'antd';
2344
+ import OperatorChain, { OperatorChainRef, SingleCardItem } from '../index';
2345
+ import { useRef } from 'react';
2346
+
2347
+ const cards = [
2348
+ {
2349
+ id: '1',
2350
+ title: 'Operator 1',
2351
+ description:
2352
+ '这里随便写一句长文本,测试一下是否能正常显示,要很长很长很长,超过2行,是否能正常显示省略号。',
2353
+ },
2354
+ {
2355
+ id: '2',
2356
+ title: 'Operator 2',
2357
+ description: 'Description 2',
2358
+ },
2359
+ {
2360
+ id: '3',
2361
+ title: 'Operator 3',
2362
+ description: 'Description 3',
2363
+ },
2364
+ ];
2365
+
2366
+ const memu = [
2367
+ {
2368
+ id: '4',
2369
+ title: 'Operator 4',
2370
+ description: 'Description 4',
2371
+ },
2372
+ {
2373
+ id: '5',
2374
+ title: 'Operator 5',
2375
+ description: 'Description 5',
2376
+ },
2377
+ {
2378
+ id: '6',
2379
+ title: 'Operator 6',
2380
+ description: 'Description 6',
2381
+ },
2382
+ {
2383
+ id: '7',
2384
+ title: 'Operator 7',
2385
+ description: 'Description 7',
2386
+ },
2387
+ {
2388
+ id: '8',
2389
+ title: 'Operator 8',
2390
+ description: 'Description 8',
2391
+ },
2392
+ {
2393
+ id: '9',
2394
+ title: 'Operator 9',
2395
+ description: 'Description 9',
2396
+ },
2397
+ ];
2398
+
2399
+ export default function Demo() {
2400
+ const chainRef = useRef<OperatorChainRef>(null);
2401
+
2402
+ const handleClick = (id: string) => {
2403
+ message.info(`当前选择的卡片id: ${id}`);
2404
+ };
2405
+
2406
+ const handleCardsChange = (latestCards: SingleCardItem[]) => {
2407
+ console.log('子组件最新的cards:', latestCards);
2408
+ };
2409
+
2410
+ const handleActive = (id: string | null) => {
2411
+ console.log('子组件最新的激活卡片id:', id);
2412
+ };
2413
+
2414
+ return (
2415
+ <div>
2416
+ <div style={{ display: 'flex', gap: 100, width: '100%' }}>
2417
+ <div>
2418
+ ### dataIO 模式
2419
+ #### 输入数据集/ 输出数据集 卡片id 固定为 IMPORT/ EXPORT
2420
+
2421
+ <OperatorChain
2422
+ type="dataIO"
2423
+ defaultValues={cards}
2424
+ menu={memu}
2425
+ onChange={handleCardsChange}
2426
+ onClick={handleClick}
2427
+ onActive={handleActive}
2428
+ isEdit={true}
2429
+ ref={chainRef} // 绑定ref
2430
+ dropdownStyle={{
2431
+ width: '250px',
2432
+ maxHeight: '250px',
2433
+ }}
2434
+ />
2435
+
2436
+ <Flex align="center" gap={5}>
2437
+ <Button type="primary" onClick={() => console.log('获取cards:', chainRef.current?.getCards())}>
2438
+ 获取组件当前状态
2439
+ </Button>
2440
+ <Button
2441
+ type="primary"
2442
+ onClick={() => console.log('获取当前激活卡片id:', chainRef.current?.getActiveCardId())}
2443
+ >
2444
+ 获取当前激活卡片id
2445
+ </Button>
2446
+ </Flex>
2447
+ </div>
2448
+
2449
+ <div>
2450
+ ### 默认可编辑模式
2451
+ <OperatorChain
2452
+ defaultValues={cards}
2453
+ menu={memu}
2454
+ onChange={handleCardsChange}
2455
+ onClick={handleClick}
2456
+ isEdit={true}
2457
+ />
2458
+ </div>
2459
+
2460
+ <div>
2461
+ ### 禁止编辑模式
2462
+ <OperatorChain defaultValues={cards} menu={memu} isEdit={false} width="240px" />
2463
+ </div>
2464
+ </div>
2465
+ </div>
2466
+ );
2467
+ }
2468
+ ```
2469
+ ### API
2470
+
2471
+ #### OperatorChain
2472
+
2473
+ | 参数 | 说明 | 类型 | 默认值 | 必须 |
2474
+ | --- | --- | --- | --- | --- |
2475
+ | type | 组件类型 | `'default' \| 'dataIO'` | `'default'` | 否 |
2476
+ | isEdit | 是否处于编辑模式 | `boolean` | `false` | 否 |
2477
+ | defaultValues | 默认卡片数据 | `SingleCardItem[]` | `[]` | 否 |
2478
+ | menu | 可选的算子菜单 | `SingleCardItem[]` | `[]` | 否 |
2479
+ | onClick | 点击卡片时的回调 | `(id: string) => void` | - | 否 |
2480
+ | onChange | 卡片数据变化时的回调 | `(values: SingleCardItem[]) => void` | - | 否 |
2481
+ | onActive | 激活卡片变化时的回调 | `(id: string \| null) => void` | - | 否 |
2482
+ | width | 组件宽度 | `string` | `'240px'` | 否 |
2483
+ | dropdownStyle | 下拉菜单样式 | `{ width?: string \| number; maxHeight?: string \| number; overflowY?: React.CSSProperties['overflowY']; }` | `{ width: '200px', maxHeight: '200px', overflowY: 'auto' }` | 否 |
2484
+
2485
+ ### 接口定义
2486
+
2487
+ #### SingleCardItem 接口
2488
+
2489
+ | 属性 | 说明 | 类型 | 必须 |
2490
+ | --- | --- | --- | --- |
2491
+ | id | 卡片唯一标识 | `string` | 是 |
2492
+ | title | 卡片标题 | `string` | 否 |
2493
+ | description | 卡片描述 | `string` | 否 |
2494
+
2495
+ #### OperatorChainRef 接口
2496
+
2497
+ | 属性 | 说明 | 类型 |
2498
+ | --- | --- | --- |
2499
+ | getCards | 获取当前所有卡片数据 | `() => SingleCardItem[]` |
2500
+ | getActiveCardId | 获取当前激活卡片id | `() => string \| null` |
2501
+
2502
+ ### 使用说明
2503
+
2504
+ #### 1. 基础用法(dataIO 模式)
2505
+
2506
+ ```tsx
2507
+ import { OperatorChain } from '@/components/operator-chain';
2508
+
2509
+ const defaultValues = [
2510
+ { id: '1', title: '数据清洗', description: '清洗原始数据' },
2511
+ { id: '2', title: '数据转换', description: '转换数据格式' },
2512
+ ];
2513
+
2514
+ <OperatorChain
2515
+ defaultValues={defaultValues}
2516
+ onClick={(id) => console.log('点击了:', id)}
2517
+ onChange={(values) => console.log('当前数据:', values)}
2518
+ />
2519
+ ```
2520
+
2521
+ #### 2. 编辑模式用法
2522
+
2523
+ ```tsx
2524
+ import { OperatorChain } from '@/components/operator-chain';
2525
+
2526
+ const menu = [
2527
+ { id: 'filter', title: '数据过滤', description: '过滤符合条件的数据' },
2528
+ { id: 'transform', title: '数据转换', description: '转换数据格式' },
2529
+ { id: 'aggregate', title: '数据聚合', description: '聚合统计数据' },
2530
+ ];
2531
+
2532
+ <OperatorChain
2533
+ isEdit={true}
2534
+ menu={menu}
2535
+ defaultValues={[{ id: '1', title: '初始算子', description: '默认算子' }]}
2536
+ onChange={(values) => console.log('当前数据:', values)}
2537
+ />
2538
+ ```
2539
+
2540
+ #### 3. 使用 ref 获取数据
2541
+
2542
+ ```tsx
2543
+ import { useRef } from 'react';
2544
+ import { OperatorChain, OperatorChainRef } from '@/components/operator-chain';
2545
+
2546
+ const operatorRef = useRef<OperatorChainRef>(null);
2547
+
2548
+ const handleGetData = () => {
2549
+ const cards = operatorRef.current?.getCards();
2550
+ console.log('当前卡片数据:', cards);
2551
+ };
2552
+
2553
+ <OperatorChain
2554
+ ref={operatorRef}
2555
+ defaultValues={[{ id: '1', title: '示例算子' }]}
2556
+ />
2557
+ ```
2558
+
2559
+ #### 4. 自定义下拉菜单样式
2560
+
2561
+ ```tsx
2562
+ import { OperatorChain } from '@/components/operator-chain';
2563
+
2564
+ const menu = [
2565
+ { id: 'filter', title: '数据过滤', description: '过滤符合条件的数据' },
2566
+ { id: 'transform', title: '数据转换', description: '转换数据格式' },
2567
+ { id: 'aggregate', title: '数据聚合', description: '聚合统计数据' },
2568
+ ];
2569
+
2570
+ <OperatorChain
2571
+ isEdit={true}
2572
+ menu={menu}
2573
+ dropdownStyle={{
2574
+ width: '250px',
2575
+ maxHeight: '300px',
2576
+ overflowY: 'auto'
2577
+ }}
2578
+ defaultValues={[{ id: '1', title: '初始算子', description: '默认算子' }]}
2579
+ />
2580
+ ```
2581
+
2582
+ ### 注意事项
2583
+
2584
+ 1. `dataIO` 模式下,组件会自动在首尾添加 `IMPORT` 和 `EXPORT` 节点
2585
+ 2. `default` 模式下,不会自动添加导入导出节点
2586
+ 3. 编辑模式下需要传入 `menu` 属性才能使用添加功能
2587
+ 4. 非编辑模式下,添加按钮和删除按钮不会显示
2588
+
2589
+ ---
2590
+
2591
+ ## permission-editor(权限编辑器)
2592
+
2593
+ **导入方式:**
2594
+
2595
+ ```ts
2596
+ import { PermissionEditor, PermissionEditModal, PermissionViewPopover } from '@zjlab-fe/data-hub-ui';
2597
+ ```
2598
+
2599
+ 一个用于编辑用户权限的 React 组件,支持分层级的权限选择,包含父级权限和子级权限的联动逻辑。
2600
+
2601
+ ### 功能特性
2602
+
2603
+ - 🏷️ **分层级权限管理**:支持父子级权限结构
2604
+ - 🔒 **只读权限控制**:部分权限可设为只读状态
2605
+ - 🔄 **智能联动**:父子级权限自动联动选择
2606
+ - ✏️ **可编辑控制**:支持整体编辑状态的控制
2607
+ - 📱 **响应式布局**:自适应不同屏幕尺寸
2608
+
2609
+ ### 使用方法
2610
+
2611
+ #### 基础用法
2612
+
2613
+ ```tsx
2614
+ import React, { useState } from 'react';
2615
+ import PermissionEditor from './components/permission-editor/permissionEditor';
2616
+
2617
+ const App = () => {
2618
+ const [permissions, setPermissions] = useState([
2619
+ {
2620
+ module: 'workbench',
2621
+ grentedList: ['1', '3'], // 部分子权限被选中
2622
+ },
2623
+ {
2624
+ module: 'agentic',
2625
+ grentedList: ['13'],
2626
+ },
2627
+ ]);
2628
+
2629
+ const handlePermissionChange = (newPermissions) => {
2630
+ console.log('权限变更:', newPermissions);
2631
+ setPermissions(newPermissions);
2632
+ };
2633
+
2634
+ return (
2635
+ <div style={{ padding: '20px' }}>
2636
+ ## 用户权限设置
2637
+ <PermissionEditor
2638
+ initialPermissions={permissions}
2639
+ onChange={handlePermissionChange}
2640
+ editable={true}
2641
+ />
2642
+ </div>
2643
+ );
2644
+ };
2645
+
2646
+ export default App;
2647
+ ```
2648
+
2649
+ #### 只读模式
2650
+
2651
+ ```tsx
2652
+ <PermissionEditor
2653
+ initialPermissions={userPermissions}
2654
+ editable={false}
2655
+ />
2656
+ ```
2657
+ ### 代码演示
2658
+
2659
+ **示例代码(index.tsx):**
2660
+
2661
+ ```tsx
2662
+ import PermissionEditor from '@/components/permission-editor';
2663
+ import { PermissionEditModal, PermissionViewPopover } from '@/components/permission-editor';
2664
+ import React, { useState } from 'react';
2665
+ import { Button } from 'antd';
2666
+
2667
+ export default function Demo() {
2668
+ const [permissionEditShow, setPermissionEditShow] = useState<boolean>(false);
2669
+
2670
+ return (
2671
+ <>
2672
+ <div style={{ marginBottom: 30 }}>
2673
+ ### 权限编辑(PermissionEditor)
2674
+ <div style={{ width: 500, border: '1px solid #171313', marginBottom: 10, padding: 10 }}>
2675
+ <PermissionEditor
2676
+ initialPermissions={[
2677
+ {
2678
+ module: 'agentic',
2679
+ grentedList: ['13'],
2680
+ },
2681
+ {
2682
+ module: 'workbench',
2683
+ grentedList: ['1', '3'],
2684
+ },
2685
+ ]}
2686
+ onChange={(permissions) => {
2687
+ console.log('selected permissions: ', permissions);
2688
+ }}
2689
+ />
2690
+ </div>
2691
+ </div>
2692
+ <div style={{ marginBottom: 30 }}>
2693
+ ### 权限查看气泡卡片(PermissionViewPopover)
2694
+ <PermissionViewPopover
2695
+ initialPermissions={[
2696
+ {
2697
+ module: 'workbench',
2698
+ grentedList: ['1', '3'],
2699
+ },
2700
+ ]}
2701
+ >
2702
+ <Button type="primary">Hover me</Button>
2703
+ </PermissionViewPopover>
2704
+ </div>
2705
+ <div style={{ marginBottom: 30 }}>
2706
+ ### 权限编辑弹窗(PermissionEditModal)
2707
+ <Button type="primary" onClick={() => setPermissionEditShow(true)}>
2708
+ Click me
2709
+ </Button>
2710
+ </div>
2711
+ <PermissionEditModal
2712
+ open={permissionEditShow}
2713
+ onCancel={() => {
2714
+ setPermissionEditShow(false);
2715
+ }}
2716
+ onSave={(permissions) => {
2717
+ console.log('saved permissions: ', permissions);
2718
+ setPermissionEditShow(false);
2719
+ }}
2720
+ initialPermissions={[
2721
+ {
2722
+ module: 'agentic',
2723
+ grentedList: [],
2724
+ },
2725
+ {
2726
+ module: 'workbench',
2727
+ grentedList: ['1', '3'],
2728
+ },
2729
+ ]}
2730
+ />
2731
+ </>
2732
+ );
2733
+ }
2734
+ ```
2735
+ ### API
2736
+
2737
+ #### Props
2738
+
2739
+ | 参数 | 类型 | 默认值 | 说明 |
2740
+ |------|------|--------|------|
2741
+ | `initialPermissions` | `PermissionModule[]` | `[]` | 初始权限数据 |
2742
+ | `onChange` | `(permissions: PermissionModule[]) => void` | - | 权限变化时的回调函数 |
2743
+ | `editable` | `boolean` | `true` | 是否可编辑,为 `false` 时组件为只读状态 |
2744
+
2745
+ ### 🗂 数据结构
2746
+
2747
+ #### PermissionModule 接口
2748
+
2749
+ ```typescript
2750
+ interface PermissionModule {
2751
+ module: string; // 权限模块标识符
2752
+ grentedList: string[]; // 已授权的子权限列表
2753
+ }
2754
+ ```
2755
+
2756
+ #### 权限配置结构
2757
+ 如需修改页面中的权限内容,如新增或删减,请修改以下配置内容
2758
+ ```typescript
2759
+ const permissionTags = [
2760
+ {
2761
+ label: '智能体权限', // 显示名称
2762
+ value: 'agentic', // 权限标识符
2763
+ readOnly: false, // 是否只读
2764
+ children: [], // 子权限(空数组表示无子权限)
2765
+ },
2766
+ {
2767
+ label: '工作台权限',
2768
+ value: 'workbench',
2769
+ readOnly: false,
2770
+ children: [
2771
+ {
2772
+ label: '可视化工作流',
2773
+ value: '1',
2774
+ readOnly: false,
2775
+ },
2776
+ {
2777
+ label: 'Notebook',
2778
+ value: '2',
2779
+ readOnly: true, // 只读权限,用户无法修改
2780
+ },
2781
+ // ... 更多子权限
2782
+ ],
2783
+ },
2784
+ ];
2785
+ ```
2786
+
2787
+ #### 数据示例
2788
+
2789
+ ```typescript
2790
+ // 输入数据格式
2791
+ const initialPermissions = [
2792
+ {
2793
+ module: 'workbench',
2794
+ grentedList: ['1', '3', '4'], // 选中了部分子权限
2795
+ },
2796
+ {
2797
+ module: 'agentic',
2798
+ grentedList: [], // 无子权限的模块,父级被选中
2799
+ },
2800
+ ];
2801
+
2802
+ // 输出数据格式(onChange 回调)
2803
+ const outputPermissions = [
2804
+ {
2805
+ module: 'workbench',
2806
+ grentedList: ['1', '3', '4', '5'], // 新增了权限 '5'
2807
+ },
2808
+ {
2809
+ module: 'agentic',
2810
+ grentedList: [], // 依然保持为无子权限状态
2811
+ },
2812
+ ];
2813
+ ```
2814
+
2815
+ ### 🔄 交互逻辑
2816
+
2817
+ #### 父子级联动规则
2818
+
2819
+ 1. **选中父级权限**:
2820
+ - 自动选中所有非只读的子级权限
2821
+ - 只读子权限保持原状态不变
2822
+
2823
+ 2. **取消父级权限**:
2824
+ - 自动取消所有子级权限(包括只读)
2825
+ - 该模块从权限列表中移除
2826
+
2827
+ 3. **选中子级权限**:
2828
+ - 如果所有可选子级都被选中,自动选中父级
2829
+ - 如果部分子级被选中,父级保持未选中状态
2830
+
2831
+ 4. **取消子级权限**:
2832
+ - 如果父级原本被选中,自动取消父级选中状态
2833
+ - 如果所有子级都被取消,该模块从权限列表中移除
2834
+
2835
+ ### 相关组件
2836
+
2837
+ #### PermissionEditModal 权限编辑弹窗
2838
+
2839
+ 基于 PermissionEditor 构建的模态框组件,提供完整的权限编辑界面。
2840
+
2841
+ ##### 功能特性
2842
+
2843
+ - 📋 **模态框形式**:以弹窗方式展示权限编辑器
2844
+ - 💾 **确认/取消操作**:提供保存和取消按钮
2845
+ - 🔄 **状态管理**:内置权限变更的状态管理
2846
+
2847
+ ##### 使用方法
2848
+
2849
+ ```tsx
2850
+ import React, { useState } from 'react';
2851
+ import { Button } from 'antd';
2852
+ import PermissionEditModal from '../permissionEditModal';
2853
+
2854
+ const UserManagement = () => {
2855
+ const [modalVisible, setModalVisible] = useState(false);
2856
+ const [userPermissions, setUserPermissions] = useState([
2857
+ {
2858
+ module: 'workbench',
2859
+ grentedList: ['1', '3'],
2860
+ }
2861
+ ]);
2862
+
2863
+ const handleSave = (permissions) => {
2864
+ setUserPermissions(permissions);
2865
+ setModalVisible(false);
2866
+ console.log('保存权限:', permissions);
2867
+ };
2868
+
2869
+ const handleCancel = () => {
2870
+ setModalVisible(false);
2871
+ };
2872
+
2873
+ return (
2874
+ <>
2875
+ <Button onClick={() => setModalVisible(true)}>
2876
+ 编辑权限
2877
+ </Button>
2878
+
2879
+ <PermissionEditModal
2880
+ open={modalVisible}
2881
+ initialPermissions={userPermissions}
2882
+ onSave={handleSave}
2883
+ onCancel={handleCancel}
2884
+ />
2885
+ </>
2886
+ );
2887
+ };
2888
+ ```
2889
+
2890
+ ##### API
2891
+
2892
+ | 参数 | 类型 | 默认值 | 说明 |
2893
+ |------|------|--------|------|
2894
+ | `open` | `boolean` | `false` | 控制弹窗显示/隐藏 |
2895
+ | `initialPermissions` | `PermissionModule[]` | `[]` | 初始权限列表 |
2896
+ | `onSave` | `(permissions: PermissionModule[]) => void` | - | 保存权限时的回调 |
2897
+ | `onCancel` | `() => void` | - | 取消编辑时的回调 |
2898
+
2899
+ ---
2900
+
2901
+ #### PermissionViewPopover 权限查看气泡卡片
2902
+
2903
+ 用于展示用户权限的轻量级气泡组件,支持快速查看权限详情。
2904
+
2905
+ ##### 功能特性
2906
+
2907
+ - 👁️ **只读展示**:以只读模式展示用户权限
2908
+ - 🎈 **气泡形式**:轻量级的气泡卡片展示
2909
+ - 📱 **紧凑布局**:适合在列表、表格中使用
2910
+
2911
+ ##### 使用方法
2912
+
2913
+ ```tsx
2914
+ import React from 'react';
2915
+ import { Button, Tag } from 'antd';
2916
+ import PermissionViewPopover from '../permissionViewPopover';
2917
+
2918
+ const UserList = () => {
2919
+ const userPermissions = [
2920
+ {
2921
+ module: 'workbench',
2922
+ grentedList: ['1', '3', '4'],
2923
+ },
2924
+ {
2925
+ module: 'agentic',
2926
+ grentedList: [],
2927
+ }
2928
+ ];
2929
+
2930
+ return (
2931
+ <div>
2932
+ {/* 按钮触发方式 */}
2933
+ <PermissionViewPopover permissions={userPermissions}>
2934
+ <Button type="link">查看权限</Button>
2935
+ </PermissionViewPopover>
2936
+
2937
+ {/* 标签触发方式 */}
2938
+ <PermissionViewPopover permissions={userPermissions}>
2939
+ <Tag color="blue">权限详情</Tag>
2940
+ </PermissionViewPopover>
2941
+ </div>
2942
+ );
2943
+ };
2944
+ ```
2945
+
2946
+ ##### API
2947
+
2948
+ | 参数 | 类型 | 默认值 | 说明 |
2949
+ |------|------|--------|------|
2950
+ | `initialPermissions` | `PermissionModule[]` | `[]` | 要展示的权限列表 |
2951
+ | `children` | `React.ReactNode` | - | 触发元素 |
2952
+ | `title` | `string` | `"权限详情"` | 气泡标题 |
2953
+
2954
+ ---
2955
+
2956
+ ## popover-select(带描述浮层下拉框)
2957
+
2958
+ **导入方式:**
2959
+
2960
+ ```ts
2961
+ import { PopoverSelect } from '@zjlab-fe/data-hub-ui';
2962
+ ```
2963
+
2964
+ 使用:`import { PopoverSelect } from '@zjlab-fe/data-hub-ui';`
2965
+
2966
+
2967
+
2968
+ ### 代码演示
2969
+
2970
+
2971
+ **示例代码(index.tsx):**
2972
+
2973
+ ```tsx
2974
+ import React from 'react';
2975
+ import PopoverSelect from '../index';
2976
+ import { Form, Button } from 'antd';
2977
+
2978
+ const mockData = [
2979
+ {
2980
+ value: '24',
2981
+ label: 'EmmaE',
2982
+ desc: (
2983
+ <div>
2984
+ ### 产品设计部门
2985
+ <p>产品设计部门负责用户界面设计,确保产品符合用户需求和使用习惯。</p>
2986
+ <span>联系电话:13800000000</span>
2987
+ </div>
2988
+ ),
2989
+ },
2990
+ { value: '56', label: 'Liam', desc: '后端开发工程师,擅长Java技术栈' },
2991
+ { value: '18', label: 'Olivia', desc: '市场部专员,负责品牌推广' },
2992
+ { value: '73', label: 'Noah', desc: '数据分析师,专注用户行为研究' },
2993
+ { value: '39', label: 'Ava', desc: '人力资源主管,负责招聘与培训' },
2994
+ { value: '82', label: 'Elijah', desc: '前端开发,精通React框架' },
2995
+ { value: '45', label: 'Sophia', desc: '内容编辑,擅长文案创作' },
2996
+ { value: '67', label: 'Oliver', desc: 'DevOps工程师,负责系统部署' },
2997
+ { value: '29', label: 'Isabella', desc: '客服经理,客户关系维护' },
2998
+ { value: '91', label: 'William', desc: '产品经理,主导产品路线规划' },
2999
+ { value: '53', label: 'Mia', desc: '财务专员,负责账目管理' },
3000
+ { value: '37', label: 'James', desc: 'UI设计师,专注用户体验优化' },
3001
+ { value: '78', label: 'Charlotte', desc: '测试工程师,保障产品质量' },
3002
+ { value: '61', label: 'Benjamin', desc: '运维工程师,系统稳定性保障' },
3003
+ { value: '42', label: 'Amelia', desc: '销售代表,负责客户拓展' },
3004
+ { value: '89', label: 'Lucas', desc: '数据库管理员,数据安全维护' },
3005
+ { value: '33', label: 'Harper', desc: '行政专员,办公室日常管理' },
3006
+ { value: '59', label: 'Alexander', desc: '算法工程师,优化搜索推荐' },
3007
+ { value: '74', label: 'Abigail', desc: '新媒体运营,社交媒体管理' },
3008
+ { value: '26', label: 'Ethan', desc: '架构师,系统架构设计' },
3009
+ ];
3010
+
3011
+ export default function Demo() {
3012
+ return (
3013
+ <>
3014
+ ### 基础使用
3015
+ <PopoverSelect options={mockData} />
3016
+ <br />
3017
+ ### 行内元素
3018
+ <PopoverSelect options={mockData} style={{ width: 400 }} inline />
3019
+ <PopoverSelect options={mockData} style={{ width: 400 }} inline />
3020
+ <br />
3021
+ ### 分组
3022
+ <PopoverSelect
3023
+ options={[
3024
+ {
3025
+ label: <span>manager</span>,
3026
+ title: 'manager',
3027
+ options: [
3028
+ { label: <span>Jack</span>, value: 'Jack', desc: 'manager' },
3029
+ { label: <span>Lucy</span>, value: 'Lucy', desc: 'manager' },
3030
+ ],
3031
+ },
3032
+ {
3033
+ label: <span>engineer</span>,
3034
+ title: 'engineer',
3035
+ options: [
3036
+ { label: <span>Chloe</span>, value: 'Chloe', desc: 'engineer' },
3037
+ { label: <span>Lucas</span>, value: 'Lucas', desc: 'engineer' },
3038
+ ],
3039
+ },
3040
+ ]}
3041
+ style={{ width: 400 }}
3042
+ />
3043
+ <br />
3044
+ ### 表单元素
3045
+ <Form>
3046
+ <Form.Item name="select" label="选择" rules={[{ required: true, message: '请选择' }]}>
3047
+ <PopoverSelect options={mockData} />
3048
+ </Form.Item>
3049
+ <Form.Item>
3050
+ <Button type="primary" htmlType="submit">
3051
+ 提交
3052
+ </Button>
3053
+ </Form.Item>
3054
+ </Form>
3055
+ </>
3056
+ );
3057
+ }
3058
+ ```
3059
+ ### API
3060
+
3061
+
3062
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
3063
+ | --- | --- | --- | --- | --- | --- |
3064
+ | id | 绑定select根节点的id | `string` | - | - | - |
3065
+ | options | 选项数组,每个选项可以包含 desc 属性用于显示描述文本 | `Array<DefaultOptionType & { desc?: React.ReactNode }>` | - | - | ✓ |
3066
+ | inline | 是否显示为行内元素 | `boolean` | `false` | - | |
3067
+ | wrapperStyle | 包装器的样式 | `React.CSSProperties` | `{}` | - | |
3068
+ | wrapperClassName | 包装器的类名 | `string` | `''` | - | |
3069
+ | style | 选择框的样式 | `React.CSSProperties` | `{ width: 200 }` | - | |
3070
+ | 其他属性 | 支持原生 Select 组件的其他所有属性 (optionRender除外)| - | - | - | |
3071
+
3072
+ ---
3073
+
3074
+ ## radio-card(单选卡片)
3075
+
3076
+ **导入方式:**
3077
+
3078
+ ```ts
3079
+ import { RadioCard } from '@zjlab-fe/data-hub-ui';
3080
+ ```
3081
+ ```ts
3082
+ // 类型导入
3083
+ import type { RadioCardProps, RadioOption } from '@zjlab-fe/data-hub-ui';
3084
+ ```
3085
+
3086
+ 对antd radio的二次封装
3087
+ 单选卡片
3088
+ 符合团队样式规范
3089
+
3090
+ ### 代码演示
3091
+
3092
+ **示例代码(index.tsx):**
3093
+
3094
+ ```tsx
3095
+ import { useState } from 'react';
3096
+ import RadioCard from '../index';
3097
+
3098
+ function Home() {
3099
+ const [value, setValue] = useState<number>(1);
3100
+ const [value2, setValue2] = useState<number>(2);
3101
+ const [value3, setValue3] = useState<number>(2);
3102
+
3103
+ const onChange = (value: number | string) => {
3104
+ console.log('radio checked', value);
3105
+ setValue(Number(value));
3106
+ };
3107
+
3108
+ const onChange2 = (value: number | string) => {
3109
+ console.log('radio checked with content', value);
3110
+ setValue2(Number(value));
3111
+ };
3112
+ const onChange3 = (value: number | string) => {
3113
+ console.log('radio checked with mixed', value);
3114
+ setValue3(Number(value));
3115
+ };
3116
+
3117
+ const options = [
3118
+ {
3119
+ value: 1,
3120
+ title: 'SFT 微调训练',
3121
+ desc: '有监督微调,增强方指令跟踪的能力,提供全参和高效训练方指令跟踪的能力',
3122
+ },
3123
+ {
3124
+ value: 2,
3125
+ title: 'CPT 继续预训练',
3126
+ desc: '通过无标注数据进行无监督继续预训练,强化或新增模型特定能力',
3127
+ },
3128
+ {
3129
+ value: 3,
3130
+ title: 'TEST 继续预训练',
3131
+ desc: '通过无标注数据进行无监督继续预训练,强化或新增模型特定能力',
3132
+ },
3133
+ ];
3134
+
3135
+ const optionsWithContent = [
3136
+ {
3137
+ value: 1,
3138
+ content: (
3139
+ <div style={{ padding: '10px' }}>
3140
+ ### 自定义内容 1
3141
+ <p style={{ margin: '10px 0 0 0', fontSize: '12px', color: '#666' }}>
3142
+ 这是一个完全自定义的内容区域,可以包含任何 React 组件
3143
+ </p>
3144
+ <div style={{ marginTop: '10px', display: 'flex', gap: '8px' }}>
3145
+ <span
3146
+ style={{ padding: '4px 8px', background: '#f0f0f0', borderRadius: '4px', fontSize: '12px' }}
3147
+ >
3148
+ Tag 1
3149
+ </span>
3150
+ <span
3151
+ style={{ padding: '4px 8px', background: '#f0f0f0', borderRadius: '4px', fontSize: '12px' }}
3152
+ >
3153
+ Tag 2
3154
+ </span>
3155
+ </div>
3156
+ </div>
3157
+ ),
3158
+ },
3159
+ {
3160
+ value: 2,
3161
+ content: (
3162
+ <div style={{ padding: '10px' }}>
3163
+ ### 自定义内容 2
3164
+ <p style={{ margin: '10px 0 0 0', fontSize: '12px', color: '#666' }}>
3165
+ 第2个自定义内容,展示更复杂的布局
3166
+ </p>
3167
+ <div style={{ marginTop: '10px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
3168
+ <div
3169
+ style={{
3170
+ padding: '6px',
3171
+ background: '#f6ffed',
3172
+ border: '1px solid #b7eb8f',
3173
+ borderRadius: '4px',
3174
+ }}
3175
+ >
3176
+ <div style={{ fontSize: '10px', color: '#52c41a' }}>特性 A</div>
3177
+ <div style={{ fontSize: '14px', fontWeight: 'bold' }}>100%</div>
3178
+ </div>
3179
+ <div
3180
+ style={{
3181
+ padding: '6px',
3182
+ background: '#fff7e6',
3183
+ border: '1px solid #ffd591',
3184
+ borderRadius: '4px',
3185
+ }}
3186
+ >
3187
+ <div style={{ fontSize: '10px', color: '#fa8c16' }}>特性 B</div>
3188
+ <div style={{ fontSize: '14px', fontWeight: 'bold' }}>85%</div>
3189
+ </div>
3190
+ </div>
3191
+ </div>
3192
+ ),
3193
+ },
3194
+ ];
3195
+
3196
+ return (
3197
+ <div style={{ padding: '20px' }}>
3198
+ ## 用法一:使用 title 和 desc及其内置样式
3199
+ <RadioCard options={options} value={value} onChange={onChange} />
3200
+
3201
+ ## 用法二:使用 content传入ReactNode,自定义卡片
3202
+ <RadioCard options={optionsWithContent} value={value2} onChange={onChange2} />
3203
+
3204
+ ## 用法三:混合使用(部分使用 content,部分使用 title/desc)
3205
+ <RadioCard
3206
+ options={[...options.slice(0, 1), ...optionsWithContent.slice(1), ...options.slice(2, 3)]}
3207
+ value={value3}
3208
+ onChange={onChange3}
3209
+ />
3210
+ </div>
3211
+ );
3212
+ }
3213
+
3214
+ export default Home;
3215
+ ```
3216
+ ### API
3217
+
3218
+ #### RadioCard
3219
+
3220
+ | 参数 | 说明 | 类型 | 默认值 | 必须 |
3221
+ | --- | --- | --- | --- | --- |
3222
+ | options | 选项数组 | `RadioOption[]` | - | 是 |
3223
+ | value | 当前选中的值 | `number \| string` | - | 否 |
3224
+ | onChange | 值变化时的回调函数 | `(value: number \| string) => void` | - | 否 |
3225
+
3226
+ ### 接口定义
3227
+
3228
+ #### RadioOption 类型(联合类型)
3229
+ `RadioOption` 是一个联合类型,必须是 `TitleRadioOption` 或 `ContentRadioOption` 中的一种:
3230
+
3231
+ ```typescript
3232
+ type RadioOption = TitleRadioOption | ContentRadioOption;
3233
+ ```
3234
+
3235
+ #### TitleRadioOption 接口(使用标题)
3236
+ | 属性 | 说明 | 类型 | 必须 |
3237
+ | --- | --- | --- | --- |
3238
+ | value | 选项的值 | `number \| string` | 是 |
3239
+ | title | 选项的标题 | `string` | 是 |
3240
+ | desc | 选项的描述 | `string` | 否 |
3241
+ | content | **禁止使用** | `never` | - |
3242
+
3243
+ #### ContentRadioOption 接口(使用自定义内容)
3244
+ | 属性 | 说明 | 类型 | 必须 |
3245
+ | --- | --- | --- | --- |
3246
+ | value | 选项的值 | `number \| string` | 是 |
3247
+ | content | 自定义内容 | `React.ReactNode` | 是 |
3248
+ | desc | 选项的描述 | `string` | 否 |
3249
+ | title | **禁止使用** | `never` | - |
3250
+
3251
+ ### 使用说明
3252
+
3253
+ #### 1. 传统用法(使用 title 和 desc)
3254
+ ```tsx
3255
+ import { RadioCard, TitleRadioOption } from '@/components/radio-card';
3256
+
3257
+ const options: TitleRadioOption[] = [
3258
+ {
3259
+ value: 1,
3260
+ title: '选项一',
3261
+ desc: '选项一的描述',
3262
+ },
3263
+ {
3264
+ value: 2,
3265
+ title: '选项二',
3266
+ desc: '选项二的描述',
3267
+ },
3268
+ ];
3269
+
3270
+ <RadioCard options={options} value={value} onChange={onChange} />
3271
+ ```
3272
+
3273
+ #### 2. 自定义内容用法(使用 content)
3274
+ ```tsx
3275
+ import { RadioCard, ContentRadioOption } from '@/components/radio-card';
3276
+
3277
+ const options: ContentRadioOption[] = [
3278
+ {
3279
+ value: 1,
3280
+ content: (
3281
+ <div>
3282
+ ### 自定义标题
3283
+ <p>自定义描述内容</p>
3284
+ </div>
3285
+ ),
3286
+ },
3287
+ {
3288
+ value: 2,
3289
+ content: (
3290
+ <div>
3291
+ ### 另一个自定义标题
3292
+ <p>另一个自定义描述</p>
3293
+ </div>
3294
+ ),
3295
+ },
3296
+ ];
3297
+
3298
+ <RadioCard options={options} value={value} onChange={onChange} />
3299
+ ```
3300
+
3301
+ #### 3. 混合用法(TypeScript 自动推断)
3302
+ ```tsx
3303
+ import { RadioCard } from '@/components/radio-card';
3304
+
3305
+ const options = [
3306
+ {
3307
+ value: 1,
3308
+ title: '传统选项',
3309
+ desc: '使用 title 和 desc',
3310
+ },
3311
+ {
3312
+ value: 2,
3313
+ content: <div>自定义内容选项</div>,
3314
+ },
3315
+ ] as const;
3316
+
3317
+ <RadioCard options={options} value={value} onChange={onChange} />
3318
+ ```
3319
+
3320
+ ### 注意事项
3321
+
3322
+ 1. 每个选项必须提供 `title` 或 `content` 中的一个(不能两者都没有)
3323
+ 2. `title` 和 `content` 不能同时存在(互斥)
3324
+
3325
+ ---
3326
+
3327
+ ## section-heading(区块标题)
3328
+
3329
+ **导入方式:**
3330
+
3331
+ ```ts
3332
+ import { SectionHeading } from '@zjlab-fe/data-hub-ui';
3333
+ ```
3334
+ ```ts
3335
+ // 类型导入
3336
+ import type { SectionHeadingProps, SectionHeadingRootProps } from '@zjlab-fe/data-hub-ui';
3337
+ ```
3338
+
3339
+ 用于页面区块标题(kicker + title + description)。
3340
+
3341
+ - 支持常规 props 方式
3342
+ - 支持类似 shadcn/ui 的组合式用法(`SectionHeading.Root/Title/Description`)
3343
+
3344
+ ### 代码演示
3345
+
3346
+ **示例代码(index.tsx):**
3347
+
3348
+ ```tsx
3349
+ import React, { type JSX } from 'react';
3350
+ import SectionHeading from '@/components/section-heading';
3351
+ import { Card, RadioGroup } from '@/demo/shared';
3352
+ import { ComparisonVsSemanticDom } from '../../../demo/shared/ComparisonVsSemanticDom';
3353
+
3354
+ type Align = 'left' | 'center' | 'right';
3355
+ type Size = 'sm' | 'md' | 'lg';
3356
+ type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4';
3357
+
3358
+ function Controls({
3359
+ align,
3360
+ size,
3361
+ titleAs,
3362
+ onChange,
3363
+ }: {
3364
+ align: Align;
3365
+ size: Size;
3366
+ titleAs: HeadingTag;
3367
+ onChange: (next: { align?: Align; size?: Size; titleAs?: HeadingTag }) => void;
3368
+ }) {
3369
+ return (
3370
+ <div style={{ display: 'grid', gap: 16 }}>
3371
+ <RadioGroup<Align>
3372
+ label="对齐方式 Align"
3373
+ value={align}
3374
+ options={[
3375
+ { value: 'left', label: 'left' },
3376
+ { value: 'center', label: 'center', badge: 'Default' },
3377
+ { value: 'right', label: 'right' },
3378
+ ]}
3379
+ onChange={(val) => onChange({ align: val })}
3380
+ />
3381
+ <RadioGroup<Size>
3382
+ label="尺寸 Size"
3383
+ value={size}
3384
+ options={[
3385
+ { value: 'sm', label: 'sm' },
3386
+ { value: 'md', label: 'md' },
3387
+ { value: 'lg', label: 'lg', badge: 'Default' },
3388
+ ]}
3389
+ onChange={(val) => onChange({ size: val })}
3390
+ />
3391
+ <RadioGroup<HeadingTag>
3392
+ label="标题标签 Title Tag"
3393
+ value={titleAs}
3394
+ options={[
3395
+ { value: 'h1', label: 'h1' },
3396
+ { value: 'h2', label: 'h2', badge: 'Default' },
3397
+ { value: 'h3', label: 'h3' },
3398
+ { value: 'h4', label: 'h4' },
3399
+ ]}
3400
+ onChange={(val) => onChange({ titleAs: val })}
3401
+ />
3402
+ </div>
3403
+ );
3404
+ }
3405
+
3406
+ export default function Demo() {
3407
+ const [align, setAlign] = React.useState<Align>('center');
3408
+ const [size, setSize] = React.useState<Size>('lg');
3409
+ const [titleAs, setTitleAs] = React.useState<HeadingTag>('h2');
3410
+
3411
+ const shared = {
3412
+ kicker: '你们呀,不要想搞个大新闻',
3413
+ mainTitle: 'Too young, too simple, sometimes naïve',
3414
+ description: '你们问我支不支持,我说支持也不行,不支持也不行',
3415
+ } as const;
3416
+
3417
+ return (
3418
+ <div style={{ padding: 24, display: 'grid', gap: 24, maxWidth: 1280, margin: '0 auto' }}>
3419
+ {/* Interactive Props Playground */}
3420
+ <Card
3421
+ title={<span>Interactive Playground</span>}
3422
+ sourceCode={`<SectionHeading
3423
+ kicker="你们呀,不要想搞个大新闻"
3424
+ mainTitle="Too young, too simple, sometimes naïve"
3425
+ description="你们问我支不支持,我说支持也不行,不支持也不行"
3426
+ align="${align}"
3427
+ size="${size}"
3428
+ titleAs="${titleAs}"
3429
+ />`}
3430
+ >
3431
+ <div style={{ display: 'grid', gap: 16 }}>
3432
+ <Controls
3433
+ align={align}
3434
+ size={size}
3435
+ titleAs={titleAs}
3436
+ onChange={(next) => {
3437
+ if (next.align) setAlign(next.align);
3438
+ if (next.size) setSize(next.size);
3439
+ if (next.titleAs) setTitleAs(next.titleAs);
3440
+ }}
3441
+ />
3442
+
3443
+ <SectionHeading
3444
+ kicker={shared.kicker}
3445
+ mainTitle={shared.mainTitle}
3446
+ description={shared.description}
3447
+ align={align}
3448
+ size={size}
3449
+ titleAs={titleAs as keyof JSX.IntrinsicElements}
3450
+ />
3451
+ </div>
3452
+ </Card>
3453
+
3454
+ {/* Comparison Card */}
3455
+ <ComparisonVsSemanticDom />
3456
+
3457
+ {/* Semantic Dom - Simple Props API */}
3458
+ <Card
3459
+ title={
3460
+ <div style={{ display: 'grid', gap: 4 }}>
3461
+ <span>Semantic Dom</span>
3462
+ <span style={{ fontSize: 14, fontWeight: 400, color: '#6b7280' }}>
3463
+ Simple and powerful. Use this when you don&apos;t need to customize the DOM structure.
3464
+ </span>
3465
+ </div>
3466
+ }
3467
+ sourceCode={`// With custom styles
3468
+ <SectionHeading
3469
+ kicker="你们呀,不要想搞个大新闻"
3470
+ mainTitle="Too young, too simple, sometimes naïve"
3471
+ description="你们问我支不支持,我说支持也不行,不支持也不行"
3472
+ align="left"
3473
+ size="lg"
3474
+ titleAs="h2"
3475
+ styles={{
3476
+ root: {
3477
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
3478
+ padding: '32px',
3479
+ borderRadius: '12px',
3480
+ color: 'white',
3481
+ },
3482
+ kicker: {
3483
+ color: '#fbbf24',
3484
+ fontWeight: 600,
3485
+ textTransform: 'uppercase',
3486
+ letterSpacing: '1px',
3487
+ },
3488
+ title: {
3489
+ color: 'white',
3490
+ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
3491
+ },
3492
+ description: {
3493
+ color: 'rgba(255, 255, 255, 0.9)',
3494
+ lineHeight: 1.6,
3495
+ },
3496
+ }}
3497
+ />`}
3498
+ >
3499
+ <SectionHeading
3500
+ kicker={shared.kicker}
3501
+ mainTitle={shared.mainTitle}
3502
+ description={shared.description}
3503
+ align="left"
3504
+ size="lg"
3505
+ titleAs="h2"
3506
+ styles={{
3507
+ root: {
3508
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
3509
+ padding: '32px',
3510
+ borderRadius: '12px',
3511
+ color: 'white',
3512
+ },
3513
+ kicker: {
3514
+ color: '#fbbf24',
3515
+ fontWeight: 600,
3516
+ textTransform: 'uppercase',
3517
+ letterSpacing: '1px',
3518
+ },
3519
+ title: {
3520
+ color: 'white',
3521
+ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
3522
+ },
3523
+ description: {
3524
+ color: 'rgba(255, 255, 255, 0.9)',
3525
+ lineHeight: 1.6,
3526
+ },
3527
+ }}
3528
+ />
3529
+ </Card>
3530
+
3531
+ {/* Composition - Flexible DOM Structure */}
3532
+ <Card
3533
+ title={
3534
+ <div style={{ display: 'grid', gap: 4 }}>
3535
+ <span>Composition</span>
3536
+ <span style={{ fontSize: 14, fontWeight: 400, color: '#6b7280' }}>
3537
+ Use this when you need to add extra DOM elements or customize the structure. Note: Semantic Dom
3538
+ and Composition cannot be used together — choose one approach.
3539
+ </span>
3540
+ </div>
3541
+ }
3542
+ sourceCode={`<SectionHeading.Root align="left" size="md">
3543
+ <SectionHeading.Kicker>你们呀,不要想搞个大新闻</SectionHeading.Kicker>
3544
+ {/* Extra DOM element between kicker and title */}
3545
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
3546
+ <span style={{ fontSize: 16 }}>🎯</span>
3547
+ <span
3548
+ style={{
3549
+ fontSize: 11,
3550
+ padding: '2px 8px',
3551
+ background: '#dbeafe',
3552
+ color: '#1e40af',
3553
+ borderRadius: 12,
3554
+ fontWeight: 600,
3555
+ }}
3556
+ >
3557
+ NEW
3558
+ </span>
3559
+ </div>
3560
+ <SectionHeading.Title asChild>
3561
+ ### Too young, too simple, sometimes naïve
3562
+ </SectionHeading.Title>
3563
+ {/* Extra DOM element between title and description */}
3564
+ <hr style={{ margin: '12px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
3565
+ <SectionHeading.Description>
3566
+ 你们问我支不支持,我说支持也不行,不支持也不行
3567
+ </SectionHeading.Description>
3568
+ </SectionHeading.Root>`}
3569
+ >
3570
+ <SectionHeading.Root align="left" size="md">
3571
+ <SectionHeading.Kicker>{shared.kicker}</SectionHeading.Kicker>
3572
+ {/* Extra DOM element between kicker and title */}
3573
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
3574
+ <span style={{ fontSize: 16 }}>🎯</span>
3575
+ <span
3576
+ style={{
3577
+ fontSize: 11,
3578
+ padding: '2px 8px',
3579
+ background: '#dbeafe',
3580
+ color: '#1e40af',
3581
+ borderRadius: 12,
3582
+ fontWeight: 600,
3583
+ }}
3584
+ >
3585
+ NEW
3586
+ </span>
3587
+ </div>
3588
+ <SectionHeading.Title asChild>
3589
+ ### {shared.mainTitle}
3590
+ </SectionHeading.Title>
3591
+ {/* Extra DOM element between title and description */}
3592
+ <hr style={{ margin: '12px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
3593
+ <SectionHeading.Description>{shared.description}</SectionHeading.Description>
3594
+ </SectionHeading.Root>
3595
+ </Card>
3596
+ </div>
3597
+ );
3598
+ }
3599
+ ```
3600
+ ### API
3601
+
3602
+ #### SectionHeading (默认导出)
3603
+
3604
+ | 参数 | 说明 | 类型 | 默认值 | 必须 |
3605
+ | --- | --- | --- | --- | --- |
3606
+ | kicker | 上方小标题 | `React.ReactNode` | - | 否 |
3607
+ | title | 主标题 | `React.ReactNode` | - | 否 |
3608
+ | description | 描述 | `React.ReactNode` | - | 否 |
3609
+ | align | 对齐方式 | `'left' \| 'center' \| 'right'` | `'left'` | 否 |
3610
+ | size | 标题尺寸 | `'sm' \| 'md' \| 'lg'` | `'lg'` | 否 |
3611
+ | titleAs | 标题标签 | `keyof JSX.IntrinsicElements` | `'h2'` | 否 |
3612
+ | className | 自定义类名 | `string` | - | 否 |
3613
+
3614
+ #### 组合式子组件
3615
+
3616
+ - `SectionHeading.Root`
3617
+ - `SectionHeading.Kicker`
3618
+ - `SectionHeading.Title`
3619
+ - `SectionHeading.Description`
3620
+
3621
+ 子组件支持 `asChild`:传入自定义标签作为 children(需为单个 ReactElement)。
3622
+
3623
+ ---
3624
+
3625
+ ## status-tag(状态标签)
3626
+
3627
+ **导入方式:**
3628
+
3629
+ ```ts
3630
+ import { StatusTag } from '@zjlab-fe/data-hub-ui';
3631
+ ```
3632
+
3633
+ > 该组件暂无 README 文档,以下为 demo 示例代码,可参考用法。
3634
+
3635
+ ### 示例代码(index.tsx)
3636
+
3637
+ ```tsx
3638
+ import StatusTag from '@/components/status-tag';
3639
+ export default function Demo() {
3640
+ return (
3641
+ <>
3642
+ <StatusTag type="running">进行状态</StatusTag>
3643
+ <StatusTag type="failed">失败状态</StatusTag>
3644
+ <StatusTag type="success">成功状态</StatusTag>
3645
+ <StatusTag type="stopped">停止状态</StatusTag>
3646
+ <StatusTag type="pending">等待状态</StatusTag>
3647
+ </>
3648
+ );
3649
+ }
3650
+ ```
3651
+
3652
+ ---
3653
+
3654
+ ## tip-tap(富文本编辑器)
3655
+
3656
+ **导入方式:**
3657
+
3658
+ ```ts
3659
+ import { Tiptap, TipTapReader } from '@zjlab-fe/data-hub-ui';
3660
+ ```
3661
+
3662
+ tip-tap 是一个基于 [TipTap](https://tiptap.dev/) 和 React 构建的强大富文本编辑器组件。当需要提供富文本编辑功能时使用,支持文本格式化、图片插入、表格编辑、Markdown 支持等功能。组件提供了可编辑的编辑器(TipTapEditor)和只读的阅读器(TipTapReader)两种模式。
3663
+
3664
+ ### 主要特性
3665
+
3666
+ 1. **文本格式化**:支持粗体、斜体、下划线、删除线
3667
+ 2. **标题支持**:支持 H1、H2、H3、H4 标题
3668
+ 3. **列表**:支持有序列表和无序列表
3669
+ 4. **文本对齐**:支持左对齐、居中、右对齐
3670
+ 5. **颜色**:支持文本颜色和背景高亮颜色
3671
+ 6. **链接**:支持插入和编辑超链接
3672
+ 7. **代码**:支持行内代码和代码块(带语法高亮)
3673
+ 8. **引用**:支持引用块
3674
+ 9. **图片**:支持上传和插入图片(base64 支持)
3675
+ 10. **表格**:支持插入和编辑可调整大小的表格(带表头)
3676
+ 11. **Markdown**:完整的 Markdown 支持,粘贴时自动检测
3677
+ 12. **工具栏**:顶部工具栏提供所有格式化选项
3678
+ 13. **气泡菜单**:选中文本时出现的上下文菜单
3679
+ 14. **浮动菜单**:空行时出现的快速插入菜单
3680
+ 15. **多种格式**:支持获取 HTML、JSON 或 Markdown 格式的内容
3681
+ 16. **Ref API**:提供命令式 API 用于程序化控制
3682
+
3683
+ ### 代码演示
3684
+
3685
+ **示例代码(index.tsx):**
3686
+
3687
+ ```tsx
3688
+ import { useRef, useState } from 'react';
3689
+ import TipTapEditor, { TipTapReader } from '../index';
3690
+ import type { TipTapEditorHandle } from '../editor';
3691
+
3692
+ const initialContent = `
3693
+ <!--- this is file is already parsed --># 数据简介
3694
+ <p>先进星载热辐射和反射辐射计 (ASTER) 是一种多光谱成像仪,于 1999 年 12 月搭载于 NASA 的 Terra 航天器上。ASTER 可以收集从可见光到热红外的 14 个光谱带的数据,每个景覆盖 60 x 60 千米的区域。该数据集包含全球范围内2000年3月4日至2007年12月31日的 ASTER L1T 数据,L1T 数据由 L1A 通过重采样生成,且已完成大气校正、辐射定标及精准地形校正,包括三个可见光及近红外波段、六个短波红外波段和五个热红外波段。</p>
3695
+ ## 波段说明
3696
+ <table width="375.75" border="0" cellpadding="0" cellspacing="0" style="width:375.75pt;border-collapse:collapse;table-layout:fixed;">
3697
+ <colgroup><col width="134.15" style="mso-width-source:userset;mso-width-alt:6541;">
3698
+ <col width="73.30" style="mso-width-source:userset;mso-width-alt:3574;">
3699
+ <col width="89.15" style="mso-width-source:userset;mso-width-alt:4346;">
3700
+ <col width="79.15" style="mso-width-source:userset;mso-width-alt:3859;">
3701
+ </colgroup><tbody><tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3702
+ <td class="xl65" height="34.75" width="134.15" style="height:34.75pt;width:134.15pt;" x:str="">谱段</td>
3703
+ <td class="xl66" width="73.30" style="width:73.30pt;" x:str="">波段名称</td>
3704
+ <td class="xl66" width="89.15" style="width:89.15pt;" x:str="">波长范围</td>
3705
+ <td class="xl66" width="79.15" style="width:79.15pt;" x:str="">空间分辨率</td>
3706
+ </tr>
3707
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3708
+ <td class="xl67" height="104.25" rowspan="3" style="height:104.25pt;border-right:.5pt solid windowtext;border-bottom:.5pt solid windowtext;" x:str="">可见光近红外(VNIR)</td>
3709
+ <td class="xl68" x:str="">Band 1</td>
3710
+ <td class="xl69" x:str="">0.520-0.600µm</td>
3711
+ <td class="xl68" x:str="">15 m</td>
3712
+ </tr>
3713
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3714
+ <td class="xl68" x:str="">Band 2</td>
3715
+ <td class="xl69" x:str="">0.630-0.690µm</td>
3716
+ <td class="xl68" x:str="">15 m</td>
3717
+ </tr>
3718
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3719
+ <td class="xl68" x:str="">Band 3</td>
3720
+ <td class="xl69" x:str="">0.780-0.860µm</td>
3721
+ <td class="xl68" x:str="">15 m</td>
3722
+ </tr>
3723
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3724
+ <td class="xl67" height="208.50" rowspan="6" style="height:208.50pt;border-right:.5pt solid windowtext;border-bottom:.5pt solid windowtext;" x:str="">短波红外(SWIR)</td>
3725
+ <td class="xl68" x:str="">Band 4</td>
3726
+ <td class="xl69" x:str="">1.600-1.700µm</td>
3727
+ <td class="xl68" x:str="">30 m</td>
3728
+ </tr>
3729
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3730
+ <td class="xl68" x:str="">Band 5</td>
3731
+ <td class="xl69" x:str="">2.145-2.185µm</td>
3732
+ <td class="xl68" x:str="">30 m</td>
3733
+ </tr>
3734
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3735
+ <td class="xl68" x:str="">Band 6</td>
3736
+ <td class="xl69" x:str="">2.185-2.225µm</td>
3737
+ <td class="xl68" x:str="">30 m</td>
3738
+ </tr>
3739
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3740
+ <td class="xl68" x:str="">Band 7</td>
3741
+ <td class="xl69" x:str="">2.235-2.285µm</td>
3742
+ <td class="xl68" x:str="">30 m</td>
3743
+ </tr>
3744
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3745
+ <td class="xl68" x:str="">Band 8</td>
3746
+ <td class="xl69" x:str="">2.295-2.365µm</td>
3747
+ <td class="xl68" x:str="">30 m</td>
3748
+ </tr>
3749
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3750
+ <td class="xl68" x:str="">Band 9</td>
3751
+ <td class="xl69" x:str="">2.360-2.430µm</td>
3752
+ <td class="xl68" x:str="">30 m</td>
3753
+ </tr>
3754
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3755
+ <td class="xl67" height="173.75" rowspan="5" style="height:173.75pt;border-right:.5pt solid windowtext;border-bottom:.5pt solid windowtext;" x:str="">热红外(TIR)</td>
3756
+ <td class="xl68" x:str="">Band 10</td>
3757
+ <td class="xl69" x:str="">8.125-8.475µm</td>
3758
+ <td class="xl68" x:str="">90 m</td>
3759
+ </tr>
3760
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3761
+ <td class="xl68" x:str="">Band 11</td>
3762
+ <td class="xl69" x:str="">8.475-8.825µm</td>
3763
+ <td class="xl68" x:str="">90 m</td>
3764
+ </tr>
3765
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3766
+ <td class="xl68" x:str="">Band 12</td>
3767
+ <td class="xl69" x:str="">8.925-9.275µm</td>
3768
+ <td class="xl68" x:str="">90 m</td>
3769
+ </tr>
3770
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3771
+ <td class="xl68" x:str="">Band 13</td>
3772
+ <td class="xl69" x:str="">10.250-10.950µm</td>
3773
+ <td class="xl68" x:str="">90 m</td>
3774
+ </tr>
3775
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3776
+ <td class="xl68" x:str="">Band 14</td>
3777
+ <td class="xl69" x:str="">10.950-11.650µm</td>
3778
+ <td class="xl68" x:str="">90 m</td>
3779
+ </tr>
3780
+ <!--[if supportMisalignedColumns]-->
3781
+ <tr width="0" style="display:none;">
3782
+ <td width="134" style="width:134;"></td>
3783
+ <td width="73" style="width:73;"></td>
3784
+ <td width="89" style="width:89;"></td>
3785
+ <td width="79" style="width:79;"></td>
3786
+ </tr>
3787
+ <!--[endif]-->
3788
+ </tbody></table>
3789
+
3790
+ ## 数据属性
3791
+ <table width="667.35" border="0" cellpadding="0" cellspacing="0" style="width:667.35pt;border-collapse:collapse;table-layout:fixed;">
3792
+ <colgroup><col width="214.10" style="mso-width-source:userset;mso-width-alt:10439;">
3793
+ <col width="132.45" style="mso-width-source:userset;mso-width-alt:6458;">
3794
+ <col width="320.80" style="mso-width-source:userset;mso-width-alt:15642;">
3795
+ </colgroup><tbody><tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3796
+ <td class="xl65" height="34.75" width="214.10" style="height:34.75pt;width:214.10pt;" x:str="">字段名称</td>
3797
+ <td class="xl65" width="132.45" style="width:132.45pt;" x:str="">数据类型</td>
3798
+ <td class="xl65" width="320.80" style="width:320.80pt;" x:str="">说明</td>
3799
+ </tr>
3800
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3801
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Granule_UR</td>
3802
+ <td class="xl66" x:str="">String</td>
3803
+ <td class="xl66" x:str="">数据 UR 或唯一标识符</td>
3804
+ </tr>
3805
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3806
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Short_Name</td>
3807
+ <td class="xl66" x:str="">String</td>
3808
+ <td class="xl66" x:str="">数据集缩写</td>
3809
+ </tr>
3810
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3811
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Version_ID</td>
3812
+ <td class="xl66" x:str="">String</td>
3813
+ <td class="xl66" x:str="">数据集版本</td>
3814
+ </tr>
3815
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3816
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">File_Name</td>
3817
+ <td class="xl66" x:str="">String</td>
3818
+ <td class="xl66" x:str="">文件名</td>
3819
+ </tr>
3820
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3821
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">File_Size</td>
3822
+ <td class="xl66" x:str="">Double</td>
3823
+ <td class="xl66" x:str="">每一种文件大小,以字节为单位</td>
3824
+ </tr>
3825
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3826
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Reprocessing_Actual</td>
3827
+ <td class="xl66" x:str="">Datetime</td>
3828
+ <td class="xl66" x:str="">实际重新处理的次数</td>
3829
+ </tr>
3830
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3831
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Granule_ID</td>
3832
+ <td class="xl66" x:str="">String</td>
3833
+ <td class="xl66" x:str="">产品命名,即 L 1 T 数据名称</td>
3834
+ </tr>
3835
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3836
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Day_Night_Flag</td>
3837
+ <td class="xl66" x:str="">String</td>
3838
+ <td class="xl66" x:str="">白天还是夜晚的标志,这里是“Day”,表示这是白天的数据</td>
3839
+ </tr>
3840
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3841
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Time_of_Day</td>
3842
+ <td class="xl66" x:str="">Datetime</td>
3843
+ <td class="xl66" x:str="">观测时间</td>
3844
+ </tr>
3845
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3846
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Calendar_Date</td>
3847
+ <td class="xl66" x:str="">Datetime</td>
3848
+ <td class="xl66" x:str="">观测日期</td>
3849
+ </tr>
3850
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3851
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Point_Longitude</td>
3852
+ <td class="xl66" x:str="">Double</td>
3853
+ <td class="xl66" x:str="">某一景地理范围的左上/右上/右下/左下角点经度</td>
3854
+ </tr>
3855
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3856
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Point_Latitude</td>
3857
+ <td class="xl66" x:str="">Double</td>
3858
+ <td class="xl66" x:str="">某一景地理范围的左上/右上/右下/左下角点纬度</td>
3859
+ </tr>
3860
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3861
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">QA_Percent_Missing_Data</td>
3862
+ <td class="xl66" x:str="">Double</td>
3863
+ <td class="xl66" x:str="">存储缺失数据占全部数据百分比</td>
3864
+ </tr>
3865
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3866
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">QA_Percent_Out_of_Bounds_Data</td>
3867
+ <td class="xl66" x:str="">Double</td>
3868
+ <td class="xl66" x:str="">存储错误数据占全部数据百分比</td>
3869
+ </tr>
3870
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3871
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">QA_Percent_Interpolated_Data</td>
3872
+ <td class="xl66" x:str="">Double</td>
3873
+ <td class="xl66" x:str="">内插数据百分比</td>
3874
+ </tr>
3875
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3876
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">QA_Percent_Cloud_Cover</td>
3877
+ <td class="xl66" x:str="">Double</td>
3878
+ <td class="xl66" x:str="">云覆盖百分比</td>
3879
+ </tr>
3880
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3881
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Automatic_Qualit_Flag</td>
3882
+ <td class="xl66" x:str="">String</td>
3883
+ <td class="xl66" x:str="">自动质量检查</td>
3884
+ </tr>
3885
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3886
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">ASTER_Map_Projection</td>
3887
+ <td class="xl66" x:str="">String</td>
3888
+ <td class="xl66" x:str="">地图投影方法</td>
3889
+ </tr>
3890
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3891
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Scene_Cloud_Coverage</td>
3892
+ <td class="xl66" x:str="">Double</td>
3893
+ <td class="xl66" x:str="">场景云覆盖率</td>
3894
+ </tr>
3895
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3896
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Upper_Left_Quad_Cloud_Coverage</td>
3897
+ <td class="xl66" x:str="">Double</td>
3898
+ <td class="xl66" x:str="">左上角云覆盖率</td>
3899
+ </tr>
3900
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3901
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Upper_Right_Quad_Cloud_Coverage</td>
3902
+ <td class="xl66" x:str="">Double</td>
3903
+ <td class="xl66" x:str="">右上角云覆盖率</td>
3904
+ </tr>
3905
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3906
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Lower_Left_Quad_Cloud_Coverage</td>
3907
+ <td class="xl66" x:str="">Double</td>
3908
+ <td class="xl66" x:str="">左下角云覆盖率</td>
3909
+ </tr>
3910
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3911
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Lower_Right_Quad_Cloud_Coverage</td>
3912
+ <td class="xl66" x:str="">Double</td>
3913
+ <td class="xl66" x:str="">右下角云覆盖率</td>
3914
+ </tr>
3915
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3916
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">VNIR 1_Observation_Mode</td>
3917
+ <td class="xl66" x:str="">String</td>
3918
+ <td class="xl66" x:str="">VNIR 1 观测模式</td>
3919
+ </tr>
3920
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3921
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">VNIR 2_Observation_Mode</td>
3922
+ <td class="xl66" x:str="">String</td>
3923
+ <td class="xl66" x:str="">VNIR 2 观测模式</td>
3924
+ </tr>
3925
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3926
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">SWIR_Observation_Mode</td>
3927
+ <td class="xl66" x:str="">String</td>
3928
+ <td class="xl66" x:str="">SWIR 观测模式</td>
3929
+ </tr>
3930
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3931
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">TIR_Observation_Mode</td>
3932
+ <td class="xl66" x:str="">String</td>
3933
+ <td class="xl66" x:str="">TIR 观测模式</td>
3934
+ </tr>
3935
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3936
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 1_Available</td>
3937
+ <td class="xl66" x:str="">String</td>
3938
+ <td class="xl66" x:str="">波段 1 可用</td>
3939
+ </tr>
3940
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3941
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 2_Available</td>
3942
+ <td class="xl66" x:str="">String</td>
3943
+ <td class="xl66" x:str="">波段 2 可用</td>
3944
+ </tr>
3945
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3946
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 3_Available</td>
3947
+ <td class="xl66" x:str="">String</td>
3948
+ <td class="xl66" x:str="">波段 3 可用</td>
3949
+ </tr>
3950
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3951
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 4_Available</td>
3952
+ <td class="xl66" x:str="">String</td>
3953
+ <td class="xl66" x:str="">波段 4 可用</td>
3954
+ </tr>
3955
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3956
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 5_Available</td>
3957
+ <td class="xl66" x:str="">String</td>
3958
+ <td class="xl66" x:str="">波段 5 可用</td>
3959
+ </tr>
3960
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3961
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 6_Available</td>
3962
+ <td class="xl66" x:str="">String</td>
3963
+ <td class="xl66" x:str="">波段 6 可用</td>
3964
+ </tr>
3965
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3966
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 7_Available</td>
3967
+ <td class="xl66" x:str="">String</td>
3968
+ <td class="xl66" x:str="">波段 7 可用</td>
3969
+ </tr>
3970
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3971
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 8_Available</td>
3972
+ <td class="xl66" x:str="">String</td>
3973
+ <td class="xl66" x:str="">波段 8 可用</td>
3974
+ </tr>
3975
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3976
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 9_Available</td>
3977
+ <td class="xl66" x:str="">String</td>
3978
+ <td class="xl66" x:str="">波段 9 可用</td>
3979
+ </tr>
3980
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3981
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 10_Available</td>
3982
+ <td class="xl66" x:str="">String</td>
3983
+ <td class="xl66" x:str="">波段 10 可用</td>
3984
+ </tr>
3985
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3986
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 11_Available</td>
3987
+ <td class="xl66" x:str="">String</td>
3988
+ <td class="xl66" x:str="">波段 11 可用</td>
3989
+ </tr>
3990
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3991
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 12_Available</td>
3992
+ <td class="xl66" x:str="">String</td>
3993
+ <td class="xl66" x:str="">波段 12 可用</td>
3994
+ </tr>
3995
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
3996
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 13_Available</td>
3997
+ <td class="xl66" x:str="">String</td>
3998
+ <td class="xl66" x:str="">波段 13 可用</td>
3999
+ </tr>
4000
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4001
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Band 14_Available</td>
4002
+ <td class="xl66" x:str="">String</td>
4003
+ <td class="xl66" x:str="">波段 14 可用</td>
4004
+ </tr>
4005
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4006
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Solar_Azimuth_Angle</td>
4007
+ <td class="xl66" x:str="">Double</td>
4008
+ <td class="xl66" x:str="">太阳方位角</td>
4009
+ </tr>
4010
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4011
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">Solar_Elevation_Angle</td>
4012
+ <td class="xl66" x:str="">Double</td>
4013
+ <td class="xl66" x:str="">太阳仰角</td>
4014
+ </tr>
4015
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4016
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">ASTER_Map_Orientation_Angle</td>
4017
+ <td class="xl66" x:str="">Double</td>
4018
+ <td class="xl66" x:str="">地图定向角</td>
4019
+ </tr>
4020
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4021
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">ASTER_VNIR_Pointing_Angle</td>
4022
+ <td class="xl66" x:str="">Double</td>
4023
+ <td class="xl66" x:str="">VNIR 指向角</td>
4024
+ </tr>
4025
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4026
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">ASTER_SWIR_Pointing_Angle</td>
4027
+ <td class="xl66" x:str="">Double</td>
4028
+ <td class="xl66" x:str="">SWIR 指向角</td>
4029
+ </tr>
4030
+ <tr height="34.75" style="height:34.75pt;mso-height-source:userset;mso-height-alt:695;">
4031
+ <td class="xl66" height="34.75" style="height:34.75pt;" x:str="">ASTER_TIR_Pointing_Angle</td>
4032
+ <td class="xl66" x:str="">Double</td>
4033
+ <td class="xl66" x:str="">TIR 指向角</td>
4034
+ </tr>
4035
+ <!--[if supportMisalignedColumns]-->
4036
+ <tr width="0" style="display:none;">
4037
+ <td width="214" style="width:214;"></td>
4038
+ <td width="132" style="width:132;"></td>
4039
+ <td width="321" style="width:321;"></td>
4040
+ </tr>
4041
+ <!--[endif]-->
4042
+ </tbody></table>
4043
+
4044
+ ## API 参考
4045
+ <p>方式一:使用 OSS Browser 进行数据访问,参数如下:
4046
+ Endpoint =<a href="http://oss-cn-hangzhou-zjy-d01-a.ops.cloud.zhejianglab.com/">http://oss-cn-hangzhou-zjy-d01-a.ops.cloud.zhejianglab.com/</a>
4047
+ 预设OSS路径:oss://geocloud/aster
4048
+ AccessKeyId和AccessKeySecret请联系管理员获取,<a href="mailto:ysx@zhejianglab.com">ysx@zhejianglab.com</a></p>
4049
+ <p>方式二:使用OSS Python SDK进行数据访问
4050
+ 1、鉴权及初始化</p>
4051
+ <pre><code class="language-python">import oss2 auth = oss2.Auth(&#39;&lt;AccessKey_ID&gt;&#39;, &#39;&lt;AccessKey_Secret&gt;&#39;)
4052
+ ## endpoint 区域 例如 region = &#39;cn-hangzhou&#39;
4053
+ region = &#39;&lt;region&gt;&#39;
4054
+ bucket = oss2.Bucket(auth, &#39;&lt;Endpoint&gt;&#39;, &#39;&lt;Bucket_Name&gt;&#39;,region=region)</code></pre>
4055
+ <p>2、查询文件 key bucket 文件列表,读取文件,下载文件</p>
4056
+ <pre><code class="language-python">from itertools import islice
4057
+
4058
+ #### 查询文件对应的key 查询列表 这里只展示10条
4059
+ for b in islice(oss2.ObjectIterator(bucket), 10):
4060
+ print(b.key)
4061
+ ## 可以根据查询的列表中的key查询对应文件
4062
+ key = &#39;&lt;FILE_KEY&gt;&#39;
4063
+ #### 获取文件
4064
+ file = bucket.get_object(key)
4065
+ print(file.read())
4066
+
4067
+ ## 下载文件 Local_file_DIR 包括文件存储路径以及存储对应的文件名和后缀 例如/home/opt/xxx.xml
4068
+ bucket.get_object_to_file(key,&#39;&lt;LOCAL_FILE_DIR_WITH_FILE_SUFFIX&gt;&#39;)</code></pre>
4069
+
4070
+ `;
4071
+
4072
+ export default function TipTapEditorDemo() {
4073
+ const editorRef = useRef<TipTapEditorHandle>(null);
4074
+ const [mode, setMode] = useState<'edit' | 'read'>('edit');
4075
+ const [savedContent, setSavedContent] = useState(initialContent);
4076
+
4077
+ const handleSave = () => {
4078
+ const content = editorRef.current?.getContent() || '';
4079
+ setSavedContent(content);
4080
+ alert('内容已保存!');
4081
+ };
4082
+
4083
+ const handleClear = () => {
4084
+ editorRef.current?.setContent('');
4085
+ };
4086
+
4087
+ return (
4088
+ <div
4089
+ style={{
4090
+ minHeight: '100vh',
4091
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
4092
+ padding: '40px 20px',
4093
+ }}
4094
+ >
4095
+ <div
4096
+ style={{
4097
+ maxWidth: '1000px',
4098
+ margin: '0 auto',
4099
+ }}
4100
+ >
4101
+ {/* Header */}
4102
+ <div
4103
+ style={{
4104
+ textAlign: 'center',
4105
+ marginBottom: '32px',
4106
+ color: 'white',
4107
+ }}
4108
+ >
4109
+ <h1
4110
+ style={{
4111
+ fontSize: '2.5rem',
4112
+ fontWeight: 700,
4113
+ marginBottom: '8px',
4114
+ textShadow: '0 2px 4px rgba(0,0,0,0.2)',
4115
+ }}
4116
+ >
4117
+ TipTap Editor
4118
+ </h1>
4119
+ <p
4120
+ style={{
4121
+ fontSize: '1.1rem',
4122
+ opacity: 0.9,
4123
+ }}
4124
+ >
4125
+ 下一代富文本编辑器
4126
+ </p>
4127
+ </div>
4128
+
4129
+ {/* Controls */}
4130
+ <div
4131
+ style={{
4132
+ display: 'flex',
4133
+ justifyContent: 'center',
4134
+ gap: '12px',
4135
+ marginBottom: '24px',
4136
+ }}
4137
+ >
4138
+ <button
4139
+ onClick={() => setMode('edit')}
4140
+ style={{
4141
+ padding: '10px 24px',
4142
+ border: 'none',
4143
+ borderRadius: '8px',
4144
+ background: mode === 'edit' ? 'white' : 'rgba(255,255,255,0.2)',
4145
+ color: mode === 'edit' ? '#6366f1' : 'white',
4146
+ fontWeight: 600,
4147
+ cursor: 'pointer',
4148
+ transition: 'all 0.2s',
4149
+ }}
4150
+ >
4151
+ 编辑模式
4152
+ </button>
4153
+ <button
4154
+ onClick={() => setMode('read')}
4155
+ style={{
4156
+ padding: '10px 24px',
4157
+ border: 'none',
4158
+ borderRadius: '8px',
4159
+ background: mode === 'read' ? 'white' : 'rgba(255,255,255,0.2)',
4160
+ color: mode === 'read' ? '#6366f1' : 'white',
4161
+ fontWeight: 600,
4162
+ cursor: 'pointer',
4163
+ transition: 'all 0.2s',
4164
+ }}
4165
+ >
4166
+ 阅读模式
4167
+ </button>
4168
+ </div>
4169
+
4170
+ {/* Editor Container */}
4171
+ <div
4172
+ style={{
4173
+ background: 'white',
4174
+ borderRadius: '16px',
4175
+ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
4176
+ }}
4177
+ >
4178
+ {mode === 'edit' ? (
4179
+ <>
4180
+ <TipTapEditor
4181
+ ref={editorRef}
4182
+ content={savedContent}
4183
+ placeholder="开始创作你的内容..."
4184
+ onChange={(html) => {
4185
+ console.log('Content changed:', html.slice(0, 100) + '...');
4186
+ }}
4187
+ />
4188
+
4189
+ {/* Action Buttons */}
4190
+ <div
4191
+ style={{
4192
+ display: 'flex',
4193
+ justifyContent: 'flex-end',
4194
+ gap: '12px',
4195
+ padding: '16px 24px',
4196
+ borderTop: '1px solid #e2e8f0',
4197
+ background: '#f8fafc',
4198
+ }}
4199
+ >
4200
+ <button
4201
+ onClick={handleClear}
4202
+ style={{
4203
+ padding: '10px 20px',
4204
+ border: '1px solid #e2e8f0',
4205
+ borderRadius: '8px',
4206
+ background: 'white',
4207
+ color: '#64748b',
4208
+ fontWeight: 500,
4209
+ cursor: 'pointer',
4210
+ transition: 'all 0.2s',
4211
+ }}
4212
+ >
4213
+ 清空内容
4214
+ </button>
4215
+ <button
4216
+ onClick={handleSave}
4217
+ style={{
4218
+ padding: '10px 24px',
4219
+ border: 'none',
4220
+ borderRadius: '8px',
4221
+ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
4222
+ color: 'white',
4223
+ fontWeight: 600,
4224
+ cursor: 'pointer',
4225
+ transition: 'all 0.2s',
4226
+ boxShadow: '0 4px 14px rgba(99, 102, 241, 0.4)',
4227
+ }}
4228
+ >
4229
+ 保存内容
4230
+ </button>
4231
+ </div>
4232
+ </>
4233
+ ) : (
4234
+ <div style={{ padding: '24px' }}>
4235
+ <TipTapReader content={savedContent} />
4236
+ </div>
4237
+ )}
4238
+ </div>
4239
+
4240
+ {/* Footer */}
4241
+ <div
4242
+ style={{
4243
+ textAlign: 'center',
4244
+ marginTop: '32px',
4245
+ color: 'rgba(255,255,255,0.7)',
4246
+ fontSize: '14px',
4247
+ }}
4248
+ >
4249
+ Built with TipTap & React
4250
+ </div>
4251
+ </div>
4252
+ </div>
4253
+ );
4254
+ }
4255
+ ```
4256
+ ### 使用方法
4257
+
4258
+ #### 基本用法
4259
+
4260
+ ##### TipTapEditor
4261
+
4262
+ ```tsx
4263
+ import { useRef } from 'react';
4264
+ import TipTapEditor, { type TipTapEditorHandle } from '@/components/tip-tap';
4265
+
4266
+ function MyComponent() {
4267
+ const editorRef = useRef<TipTapEditorHandle>(null);
4268
+
4269
+ const handleSave = () => {
4270
+ const content = editorRef.current?.getContent();
4271
+ console.log('保存的内容:', content);
4272
+ };
4273
+
4274
+ return (
4275
+ <TipTapEditor
4276
+ ref={editorRef}
4277
+ content="<p>初始内容</p>"
4278
+ placeholder="开始输入内容..."
4279
+ onChange={(html) => {
4280
+ console.log('内容变化:', html);
4281
+ }}
4282
+ />
4283
+ );
4284
+ }
4285
+ ```
4286
+
4287
+ ##### TipTapReader
4288
+
4289
+ ```tsx
4290
+ import { TipTapReader } from '@/components/tip-tap';
4291
+
4292
+ function MyComponent() {
4293
+ const content = '# 标题<p>一些内容</p>';
4294
+
4295
+ return (
4296
+ <TipTapReader content={content} />
4297
+ );
4298
+ }
4299
+ ```
4300
+
4301
+ ### API
4302
+
4303
+ #### TipTapEditor
4304
+
4305
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
4306
+ | --- | --- | --- | --- | --- | --- |
4307
+ | content | 初始 HTML 内容 | `string` | `''` | - | 否 |
4308
+ | placeholder | 编辑器为空时的占位文本 | `string` | `'开始输入内容...'` | - | 否 |
4309
+ | editable | 是否可编辑 | `boolean` | `true` | - | 否 |
4310
+ | autofocus | 是否自动聚焦 | `boolean` | `false` | - | 否 |
4311
+ | className | 编辑器容器的额外 CSS 类名 | `string` | `''` | - | 否 |
4312
+ | toolbarClassName | 工具栏的额外 CSS 类名 | `string` | `''` | - | 否 |
4313
+ | onChange | 内容变化时的回调函数 | `(html: string) => void` | - | - | 否 |
4314
+
4315
+ #### TipTapEditor Ref 方法
4316
+
4317
+ 通过 `ref` 可以访问以下方法:
4318
+
4319
+ | 方法 | 说明 | 返回值 |
4320
+ | --- | --- | --- |
4321
+ | getContent | 获取 HTML 格式的内容 | `string` |
4322
+ | getJSON | 获取 JSON 格式的内容 | `Record<string, unknown>` |
4323
+ | getMarkdown | 获取 Markdown 格式的内容 | `string` |
4324
+ | setContent | 设置 HTML 内容 | `void` |
4325
+ | setMarkdown | 设置 Markdown 内容 | `void` |
4326
+ | focus | 聚焦编辑器 | `void` |
4327
+ | isEmpty | 检查编辑器是否为空 | `boolean` |
4328
+
4329
+ #### TipTapReader
4330
+
4331
+ | 参数 | 说明 | 类型 | 默认值 | 版本 | 必须 |
4332
+ | --- | --- | --- | --- | --- | --- |
4333
+ | content | 要显示的 HTML 内容 | `string` | - | - | 是 |
4334
+ | className | 额外的 CSS 类名 | `string` | `''` | - | 否 |
4335
+
4336
+ ### 高级用法
4337
+
4338
+ #### 程序化内容操作
4339
+
4340
+ ```tsx
4341
+ const editorRef = useRef<TipTapEditorHandle>(null);
4342
+
4343
+ // 获取不同格式的内容
4344
+ const html = editorRef.current?.getContent();
4345
+ const json = editorRef.current?.getJSON();
4346
+ const markdown = editorRef.current?.getMarkdown();
4347
+
4348
+ // 设置内容
4349
+ editorRef.current?.setContent('<p>新内容</p>');
4350
+ editorRef.current?.setMarkdown('# 标题\n\n内容');
4351
+
4352
+ // 检查是否为空
4353
+ const isEmpty = editorRef.current?.isEmpty();
4354
+
4355
+ // 聚焦编辑器
4356
+ editorRef.current?.focus();
4357
+ ```
4358
+
4359
+ #### 在编辑和只读模式之间切换
4360
+
4361
+ ```tsx
4362
+ const [isEditing, setIsEditing] = useState(true);
4363
+ const [content, setContent] = useState('');
4364
+
4365
+ return (
4366
+ <>
4367
+ {isEditing ? (
4368
+ <TipTapEditor
4369
+ content={content}
4370
+ onChange={setContent}
4371
+ />
4372
+ ) : (
4373
+ <TipTapReader content={content} />
4374
+ )}
4375
+ <button onClick={() => setIsEditing(!isEditing)}>
4376
+ {isEditing ? '查看' : '编辑'}
4377
+ </button>
4378
+ </>
4379
+ );
4380
+ ```
4381
+
4382
+ #### 只读编辑器
4383
+
4384
+ ```tsx
4385
+ <TipTapEditor
4386
+ content={content}
4387
+ editable={false}
4388
+ />
4389
+ ```
4390
+
4391
+ ### 注意事项
4392
+
4393
+ 1. 图片上传支持 base64 格式,大图片可能会导致性能问题,建议使用图片上传服务。
4394
+
4395
+ 2. 使用 `getContent()` 获取的内容是 HTML 格式,可以直接保存到数据库或用于显示。
4396
+
4397
+ 3. 使用 `getMarkdown()` 可以获取 Markdown 格式的内容,便于版本控制和纯文本编辑。
4398
+
4399
+ 4. 组件支持粘贴 Markdown 内容,会自动转换为富文本格式。
4400
+
4401
+ 5. 通过 CSS 变量可以自定义样式,相关变量定义在 `styles/variables.css` 中。
4402
+
4403
+ ### 内部实现
4404
+
4405
+ tip-tap 组件内部使用了以下关键技术和方法:
4406
+
4407
+ 1. **TipTap 编辑器核心**:基于 `@tiptap/react` 构建,使用 `useEditor` hook 管理编辑器实例。
4408
+
4409
+ 2. **扩展系统**:通过 TipTap 扩展系统实现各种功能:
4410
+ - StarterKit:核心编辑功能
4411
+ - TextAlign:文本对齐
4412
+ - Highlight:文本高亮
4413
+ - Color:文本颜色
4414
+ - Image:图片插入
4415
+ - Table:表格支持
4416
+ - CodeBlockLowlight:语法高亮的代码块
4417
+ - Markdown:Markdown 解析和序列化
4418
+
4419
+ 3. **菜单系统**:
4420
+ - 工具栏(Toolbar):顶部固定工具栏
4421
+ - 气泡菜单(BubbleMenu):选中文本时显示
4422
+ - 浮动菜单(FloatingMenu):空行时显示
4423
+
4424
+ 4. **样式系统**:使用 CSS 模块化,包含:
4425
+ - `editor.css`:编辑器样式
4426
+ - `content.css`:内容样式
4427
+ - `toolbar.css`:工具栏样式
4428
+ - `menus.css`:菜单样式
4429
+ - `animations.css`:动画样式
4430
+ - `variables.css`:CSS 变量
4431
+
4432
+ 5. **Ref API**:通过 `useImperativeHandle` 暴露命令式 API,方便外部程序化控制编辑器。
4433
+
4434
+ 通过这些实现,tip-tap 组件提供了强大的富文本编辑功能,同时保持了良好的用户体验和性能。
4435
+
4436
+ ---