@teamix-evo/ui 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +16 -7
- package/package.json +4 -4
- package/src/_design-system/theme-tokens/stories.tsx +2 -2
- package/src/components/accordion/index.tsx +1 -1
- package/src/components/affix/meta.md +26 -0
- package/src/components/alert/index.tsx +2 -2
- package/src/components/alert-dialog/index.tsx +3 -3
- package/src/components/alert-dialog/meta.md +52 -0
- package/src/components/alert-dialog/stories.tsx +45 -48
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/badge/index.tsx +2 -2
- package/src/components/badge/meta.md +48 -0
- package/src/components/button/index.tsx +2 -2
- package/src/components/button/meta.md +15 -0
- package/src/components/button/stories.tsx +1 -1
- package/src/components/calendar/index.tsx +2 -2
- package/src/components/card/index.tsx +1 -1
- package/src/components/carousel/index.tsx +2 -2
- package/src/components/carousel/meta.md +34 -2
- package/src/components/carousel/stories.tsx +2 -2
- package/src/components/cascader-select/index.tsx +2 -1
- package/src/components/cascader-select/meta.md +46 -0
- package/src/components/checkbox/meta.md +47 -0
- package/src/components/color-picker/index.tsx +3 -3
- package/src/components/color-picker/meta.md +80 -0
- package/src/components/combobox/index.tsx +2 -2
- package/src/components/combobox/meta.md +130 -0
- package/src/components/data-table/index.tsx +3 -3
- package/src/components/data-table/meta.md +419 -0
- package/src/components/data-table/stories.tsx +4 -4
- package/src/components/date-picker/meta.md +91 -0
- package/src/components/descriptions/index.tsx +1 -1
- package/src/components/descriptions/meta.md +245 -0
- package/src/components/dialog/index.tsx +4 -4
- package/src/components/dialog/meta.md +47 -1
- package/src/components/dialog/stories.tsx +38 -41
- package/src/components/dropdown-menu/index.tsx +5 -5
- package/src/components/empty/index.tsx +2 -2
- package/src/components/field/index.tsx +4 -4
- package/src/components/filter-bar/index.tsx +6 -6
- package/src/components/filter-bar/meta.md +323 -0
- package/src/components/float-button/index.tsx +2 -2
- package/src/components/form/index.tsx +1 -1
- package/src/components/form/meta.md +119 -0
- package/src/components/hover-card/index.tsx +1 -1
- package/src/components/hover-card/meta.md +21 -0
- package/src/components/input/meta.md +16 -0
- package/src/components/input-group/index.tsx +1 -1
- package/src/components/input-group/meta.md +118 -0
- package/src/components/input-group/stories.tsx +6 -6
- package/src/components/input-ip/index.tsx +2 -2
- package/src/components/input-ip/meta.md +30 -0
- package/src/components/input-ip/stories.tsx +2 -2
- package/src/components/input-number/index.tsx +3 -2
- package/src/components/input-number/meta.md +67 -0
- package/src/components/input-number/stories.tsx +2 -2
- package/src/components/item/index.tsx +4 -4
- package/src/components/label/meta.md +8 -0
- package/src/components/mentions/meta.md +15 -0
- package/src/components/menubar/index.tsx +4 -4
- package/src/components/navigation-menu/index.tsx +4 -4
- package/src/components/page-header/index.tsx +2 -2
- package/src/components/page-header/meta.md +145 -0
- package/src/components/page-shell/index.tsx +3 -3
- package/src/components/pagination/index.tsx +1 -1
- package/src/components/pagination/meta.md +203 -0
- package/src/components/popconfirm/meta.md +45 -0
- package/src/components/popover/index.tsx +2 -2
- package/src/components/popover/meta.md +47 -0
- package/src/components/progress/index.tsx +1 -1
- package/src/components/progress/meta.md +36 -0
- package/src/components/progress/stories.tsx +1 -1
- package/src/components/radio-group/meta.md +69 -0
- package/src/components/rate/index.tsx +1 -1
- package/src/components/rate/meta.md +50 -0
- package/src/components/resizable/index.tsx +1 -1
- package/src/components/select/index.tsx +2 -2
- package/src/components/select/meta.md +20 -0
- package/src/components/separator/index.tsx +1 -1
- package/src/components/sheet/index.tsx +13 -14
- package/src/components/sheet/meta.md +124 -0
- package/src/components/sheet/stories.tsx +110 -119
- package/src/components/sidebar/index.tsx +5 -5
- package/src/components/sidebar/meta.md +383 -0
- package/src/components/skeleton/meta.md +13 -0
- package/src/components/slider/index.tsx +2 -2
- package/src/components/sonner/meta.md +86 -0
- package/src/components/spinner/meta.md +46 -0
- package/src/components/spinner/stories.tsx +2 -2
- package/src/components/steps/meta.md +20 -0
- package/src/components/steps/stories.tsx +1 -1
- package/src/components/switch/index.tsx +2 -2
- package/src/components/switch/meta.md +33 -0
- package/src/components/table/index.tsx +4 -4
- package/src/components/table/meta.md +11 -0
- package/src/components/tabs/index.tsx +7 -7
- package/src/components/tabs/meta.md +52 -0
- package/src/components/tag/index.tsx +8 -8
- package/src/components/tag/meta.md +194 -0
- package/src/components/textarea/index.tsx +1 -1
- package/src/components/textarea/meta.md +27 -0
- package/src/components/textarea/stories.tsx +1 -1
- package/src/components/time-picker/index.tsx +3 -3
- package/src/components/time-picker/meta.md +76 -0
- package/src/components/timeline/index.tsx +1 -0
- package/src/components/toggle/index.tsx +1 -1
- package/src/components/toggle-group/index.tsx +1 -1
- package/src/components/tooltip/index.tsx +1 -1
- package/src/components/tooltip/meta.md +23 -0
- package/src/components/transfer/index.tsx +2 -2
- package/src/components/transfer/meta.md +97 -0
- package/src/components/tree/index.tsx +245 -15
- package/src/components/tree/meta.md +151 -0
- package/src/components/tree-select/index.tsx +16 -2
- package/src/components/tree-select/meta.md +150 -0
- package/src/components/typography/index.tsx +3 -3
- package/src/components/upload/index.tsx +3 -3
- package/src/components/upload/meta.md +82 -0
- package/src/components/tree/utils.ts +0 -269
- package/src/examples/built-in-assets/stories.tsx +0 -572
- package/src/examples/evaluators/stories.tsx +0 -502
|
@@ -44,6 +44,14 @@
|
|
|
44
44
|
|
|
45
45
|
## 示例
|
|
46
46
|
|
|
47
|
+
### Multiple
|
|
48
|
+
|
|
49
|
+
多选模式:父子联动勾选,半选状态由 indeterminate 表达。
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<TreeSelect treeData={treeData} multiple defaultExpandAll />
|
|
53
|
+
```
|
|
54
|
+
|
|
47
55
|
### CheckStrictly
|
|
48
56
|
|
|
49
57
|
父子独立勾选:勾选父级不影响子级,反之亦然。
|
|
@@ -52,6 +60,103 @@
|
|
|
52
60
|
<TreeSelect treeData={treeData} multiple checkStrictly defaultExpandAll />
|
|
53
61
|
```
|
|
54
62
|
|
|
63
|
+
### CheckedStrategy
|
|
64
|
+
|
|
65
|
+
三种回填策略对比:parent / child / all。
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<div className="flex flex-col gap-3">
|
|
69
|
+
<div className="flex items-center gap-2">
|
|
70
|
+
<span className="w-16 text-xs text-muted-foreground">parent</span>
|
|
71
|
+
<TreeSelect
|
|
72
|
+
treeData={treeData}
|
|
73
|
+
multiple
|
|
74
|
+
showCheckedStrategy="parent"
|
|
75
|
+
value={parentVal}
|
|
76
|
+
onChange={(v) => setParentVal(v as string[])}
|
|
77
|
+
defaultExpandAll
|
|
78
|
+
/>
|
|
79
|
+
<span className="text-xs text-muted-foreground">
|
|
80
|
+
出参:{JSON.stringify(parentVal)}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex items-center gap-2">
|
|
84
|
+
<span className="w-16 text-xs text-muted-foreground">child</span>
|
|
85
|
+
<TreeSelect
|
|
86
|
+
treeData={treeData}
|
|
87
|
+
multiple
|
|
88
|
+
showCheckedStrategy="child"
|
|
89
|
+
value={childVal}
|
|
90
|
+
onChange={(v) => setChildVal(v as string[])}
|
|
91
|
+
defaultExpandAll
|
|
92
|
+
/>
|
|
93
|
+
<span className="text-xs text-muted-foreground">
|
|
94
|
+
出参:{JSON.stringify(childVal)}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
<span className="w-16 text-xs text-muted-foreground">all</span>
|
|
99
|
+
<TreeSelect
|
|
100
|
+
treeData={treeData}
|
|
101
|
+
multiple
|
|
102
|
+
showCheckedStrategy="all"
|
|
103
|
+
value={allVal}
|
|
104
|
+
onChange={(v) => setAllVal(v as string[])}
|
|
105
|
+
defaultExpandAll
|
|
106
|
+
/>
|
|
107
|
+
<span className="text-xs text-muted-foreground">
|
|
108
|
+
出参:{JSON.stringify(allVal)}
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### WithSearch
|
|
115
|
+
|
|
116
|
+
内置搜索:按 label 字段本地过滤。
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<TreeSelect treeData={treeData} showSearch defaultExpandAll />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### AsyncSearch
|
|
123
|
+
|
|
124
|
+
异步搜索:提供 onSearch 由外部接管 treeData,组件不再做本地过滤。
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
const [data, setData] = React.useState<TreeSelectNode[]>([
|
|
128
|
+
{
|
|
129
|
+
value: 'zhejiang',
|
|
130
|
+
label: '浙江',
|
|
131
|
+
children: [{ value: 'sx', label: '绍兴' }],
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
const timer = React.useRef<ReturnType<typeof setTimeout>>();
|
|
135
|
+
return (
|
|
136
|
+
<TreeSelect
|
|
137
|
+
treeData={data}
|
|
138
|
+
showSearch
|
|
139
|
+
defaultExpandAll
|
|
140
|
+
onSearch={(kw) => {
|
|
141
|
+
if (timer.current) clearTimeout(timer.current);
|
|
142
|
+
if (!kw) {
|
|
143
|
+
setData([
|
|
144
|
+
{
|
|
145
|
+
value: 'zhejiang',
|
|
146
|
+
label: '浙江',
|
|
147
|
+
children: [{ value: 'sx', label: '绍兴' }],
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
timer.current = setTimeout(() => {
|
|
153
|
+
setData([{ value: kw, label: kw }]);
|
|
154
|
+
}, 300);
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
55
160
|
### Sizes
|
|
56
161
|
|
|
57
162
|
三档尺寸:sm / default / lg,对齐 Button、Input、Select。
|
|
@@ -76,3 +181,48 @@
|
|
|
76
181
|
allowClear
|
|
77
182
|
/>
|
|
78
183
|
```
|
|
184
|
+
|
|
185
|
+
### Loading
|
|
186
|
+
|
|
187
|
+
加载态:把右侧图标替换为旋转 spinner,与 Select 行为一致。
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
<TreeSelect treeData={treeData} loading />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Disabled
|
|
194
|
+
|
|
195
|
+
整体禁用。
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<TreeSelect treeData={treeData} disabled defaultValue="fe-1" />
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Invalid
|
|
202
|
+
|
|
203
|
+
错误态:通过 aria-invalid 触发 destructive 边框与 ring。
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
<TreeSelect treeData={treeData} aria-invalid />
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Controlled
|
|
210
|
+
|
|
211
|
+
受控 value + 受控 open。
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
<div className="flex flex-col items-start gap-2">
|
|
215
|
+
<TreeSelect
|
|
216
|
+
treeData={treeData}
|
|
217
|
+
multiple
|
|
218
|
+
value={value}
|
|
219
|
+
onChange={(v) => setValue(v as string[])}
|
|
220
|
+
open={open}
|
|
221
|
+
onOpenChange={setOpen}
|
|
222
|
+
defaultExpandAll
|
|
223
|
+
/>
|
|
224
|
+
<span className="text-xs text-muted-foreground">
|
|
225
|
+
value: {JSON.stringify(value)} · open: {String(open)}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
```
|
|
@@ -72,7 +72,7 @@ function Prose({ className, ...props }: ProseProps) {
|
|
|
72
72
|
'[&_pre]:my-4 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-4',
|
|
73
73
|
'[&_pre_code]:bg-transparent [&_pre_code]:p-0',
|
|
74
74
|
// blockquote / hr
|
|
75
|
-
'[&_blockquote]:my-4 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:
|
|
75
|
+
'[&_blockquote]:my-4 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground [&_blockquote]:italic',
|
|
76
76
|
'[&_hr]:my-6 [&_hr]:border-border',
|
|
77
77
|
className,
|
|
78
78
|
)}
|
|
@@ -220,7 +220,7 @@ function Text({
|
|
|
220
220
|
del && 'line-through',
|
|
221
221
|
italic && 'italic',
|
|
222
222
|
underline && 'underline underline-offset-4',
|
|
223
|
-
disabled && 'cursor-not-allowed select-none
|
|
223
|
+
disabled && 'cursor-not-allowed opacity-50 select-none',
|
|
224
224
|
ellipsis && 'inline-block max-w-full truncate align-bottom',
|
|
225
225
|
code && 'rounded bg-muted px-1.5 py-0.5 font-mono text-[0.875em]',
|
|
226
226
|
className,
|
|
@@ -236,7 +236,7 @@ function Text({
|
|
|
236
236
|
type="button"
|
|
237
237
|
onClick={handleCopy}
|
|
238
238
|
aria-label={copied ? '已复制' : '复制'}
|
|
239
|
-
className="cursor-pointer rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:
|
|
239
|
+
className="cursor-pointer rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
240
240
|
>
|
|
241
241
|
{copied ? (
|
|
242
242
|
<Check className="size-3.5 text-success" />
|
|
@@ -476,7 +476,7 @@ function UploadList({
|
|
|
476
476
|
data-status={file.status}
|
|
477
477
|
className={cn(
|
|
478
478
|
'group flex items-center gap-2 rounded-sm px-2 py-1 text-xs transition-colors hover:bg-accent/50',
|
|
479
|
-
listType === 'image' && 'items-center gap-2
|
|
479
|
+
listType === 'image' && 'items-center gap-2 p-1.5',
|
|
480
480
|
isError && 'text-destructive',
|
|
481
481
|
)}
|
|
482
482
|
>
|
|
@@ -686,7 +686,7 @@ function Upload({
|
|
|
686
686
|
<button
|
|
687
687
|
type="button"
|
|
688
688
|
tabIndex={-1}
|
|
689
|
-
className="inline-flex cursor-pointer items-center gap-1.5 rounded-md border border-input bg-card px-2.5
|
|
689
|
+
className="inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-md border border-input bg-card px-2.5 text-xs transition-colors hover:bg-muted hover:text-foreground"
|
|
690
690
|
>
|
|
691
691
|
<UploadIcon className="size-3.5" aria-hidden />
|
|
692
692
|
上传文件
|
|
@@ -813,7 +813,7 @@ function UploadDragger({
|
|
|
813
813
|
handleFiles(e.dataTransfer.files);
|
|
814
814
|
}}
|
|
815
815
|
className={cn(
|
|
816
|
-
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-card px-6 py-8 text-center transition-colors hover:bg-muted/50 focus-visible:
|
|
816
|
+
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-card px-6 py-8 text-center transition-colors hover:bg-muted/50 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none',
|
|
817
817
|
dragOver && 'border-primary bg-primary/5',
|
|
818
818
|
disabled && 'pointer-events-none opacity-50',
|
|
819
819
|
)}
|
|
@@ -44,6 +44,12 @@
|
|
|
44
44
|
|
|
45
45
|
## 示例
|
|
46
46
|
|
|
47
|
+
### Basic
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<Upload customRequest={mockRequest} defaultFileList={[sampleDoc]} />
|
|
51
|
+
```
|
|
52
|
+
|
|
47
53
|
### Drag
|
|
48
54
|
|
|
49
55
|
```tsx
|
|
@@ -54,6 +60,47 @@
|
|
|
54
60
|
/>
|
|
55
61
|
```
|
|
56
62
|
|
|
63
|
+
### ImageList
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Upload listType="image" multiple accept="image/*" customRequest={mockRequest} defaultFileList={[
|
|
67
|
+
sampleImage} {
|
|
68
|
+
uid: 'sample-3'={{
|
|
69
|
+
uid: 'sample-3'} name="cover.jpg" status="uploading" percent={40} thumbUrl="https://img.alicdn.com/tfs/TB1x4QlgxYaK1RjSZFnXXa80pXa-1400-742.png" }={}} {
|
|
70
|
+
uid: 'sample-4'={{
|
|
71
|
+
uid: 'sample-4'} name="broken.png" status="error" error={new Error('上传失败')} }={}} ]={]} />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Multiple
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<Upload multiple accept="image/*" customRequest={mockRequest} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### MaxCountAndSize
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<div className="flex flex-col gap-2">
|
|
84
|
+
<Upload
|
|
85
|
+
multiple
|
|
86
|
+
maxCount={3}
|
|
87
|
+
maxSize={2 * 1024 * 1024}
|
|
88
|
+
customRequest={mockRequest}
|
|
89
|
+
onLimitExceed={(file) =>
|
|
90
|
+
setHint(`已超出最大数量 (3),已忽略 ${file.name}`)
|
|
91
|
+
}
|
|
92
|
+
onSizeExceed={(file) => setHint(`${file.name} 超过 2MB 体积限制`)}
|
|
93
|
+
/>
|
|
94
|
+
{hint ? (
|
|
95
|
+
<span className="text-xs text-destructive">{hint}</span>
|
|
96
|
+
) : (
|
|
97
|
+
<span className="text-xs text-muted-foreground">
|
|
98
|
+
最多 3 个文件,单文件 ≤ 2MB
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
57
104
|
### BeforeUpload
|
|
58
105
|
|
|
59
106
|
```tsx
|
|
@@ -73,6 +120,41 @@
|
|
|
73
120
|
</div>
|
|
74
121
|
```
|
|
75
122
|
|
|
123
|
+
### CustomRequest
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<Upload multiple customRequest={mockRequest} />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Controlled
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<div className="flex flex-col gap-2">
|
|
133
|
+
<Upload
|
|
134
|
+
fileList={fileList}
|
|
135
|
+
onChange={(next) => setFileList(next)}
|
|
136
|
+
customRequest={mockRequest}
|
|
137
|
+
/>
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
139
|
+
<Button variant="outline" onClick={() => setFileList([sampleDoc])}>
|
|
140
|
+
重置
|
|
141
|
+
</Button>
|
|
142
|
+
<Button variant="outline" onClick={() => setFileList([])}>
|
|
143
|
+
清空
|
|
144
|
+
</Button>
|
|
145
|
+
<span className="text-xs text-muted-foreground">
|
|
146
|
+
当前 {fileList.length} 个文件
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Disabled
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<Upload disabled defaultFileList={[sampleDoc]} />
|
|
156
|
+
```
|
|
157
|
+
|
|
76
158
|
### ItemRender
|
|
77
159
|
|
|
78
160
|
```tsx
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree / TreeSelect 共用算法工具。
|
|
3
|
-
*
|
|
4
|
-
* - 抽离自原 `tree-select/index.tsx`,泛化为接受 `getKey` 适配 `key` / `value` 两种字段命名。
|
|
5
|
-
* - 全为纯函数,无副作用,便于在 Tree 与 TreeSelect 间共享。
|
|
6
|
-
*/
|
|
7
|
-
import * as React from 'react';
|
|
8
|
-
|
|
9
|
-
/** 所有树节点共享的可选基础字段。 */
|
|
10
|
-
export interface TreeNodeBase {
|
|
11
|
-
/** 整体禁用:不可选 / 不可勾选 / 不可展开收起。 */
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
/** 仅禁用复选框(多选 / 勾选下生效)。 */
|
|
14
|
-
disableCheckbox?: boolean;
|
|
15
|
-
/** 强制叶子节点:无展开箭头 / 不参与 cascade。 */
|
|
16
|
-
isLeaf?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 在数据树中按 key 查找节点。 */
|
|
20
|
-
export function findNode<T extends { children?: T[] }>(
|
|
21
|
-
data: T[],
|
|
22
|
-
key: string,
|
|
23
|
-
getKey: (n: T) => string,
|
|
24
|
-
): T | undefined {
|
|
25
|
-
for (const node of data) {
|
|
26
|
-
if (getKey(node) === key) return node;
|
|
27
|
-
if (node.children) {
|
|
28
|
-
const found = findNode(node.children, key, getKey);
|
|
29
|
-
if (found) return found;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 收集整棵树所有节点 key(含中间节点)。 */
|
|
36
|
-
export function getAllKeys<T extends { children?: T[] }>(
|
|
37
|
-
data: T[],
|
|
38
|
-
getKey: (n: T) => string,
|
|
39
|
-
): string[] {
|
|
40
|
-
const keys: string[] = [];
|
|
41
|
-
const walk = (nodes: T[]) => {
|
|
42
|
-
for (const node of nodes) {
|
|
43
|
-
keys.push(getKey(node));
|
|
44
|
-
if (node.children) walk(node.children);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
walk(data);
|
|
48
|
-
return keys;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** 收集节点下所有未禁用 / 未 disableCheckbox 的后代 key(含自身)。 */
|
|
52
|
-
export function collectCheckable<T extends TreeNodeBase & { children?: T[] }>(
|
|
53
|
-
node: T,
|
|
54
|
-
getKey: (n: T) => string,
|
|
55
|
-
): string[] {
|
|
56
|
-
const out: string[] = [];
|
|
57
|
-
const walk = (n: T) => {
|
|
58
|
-
if (n.disabled || n.disableCheckbox) return;
|
|
59
|
-
out.push(getKey(n));
|
|
60
|
-
n.children?.forEach(walk);
|
|
61
|
-
};
|
|
62
|
-
walk(node);
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 节点是否 fully checked —— 对齐 antd `rc-tree` `conductCheck` 语义:
|
|
68
|
-
* - 叶子(或所有子均不可勾选):`checked.has(self)`
|
|
69
|
-
* - 非叶子:所有 checkable 子均 fully checked,不依赖 self 在 set 中
|
|
70
|
-
* (避免受控展开后 parent in set 误判为 fully,导致取消子项被压缩推导反吐)。
|
|
71
|
-
*/
|
|
72
|
-
export function isFullyChecked<T extends TreeNodeBase & { children?: T[] }>(
|
|
73
|
-
node: T,
|
|
74
|
-
checked: Set<string>,
|
|
75
|
-
getKey: (n: T) => string,
|
|
76
|
-
): boolean {
|
|
77
|
-
const children = node.children?.filter(
|
|
78
|
-
(c) => !c.disabled && !c.disableCheckbox,
|
|
79
|
-
);
|
|
80
|
-
if (!children || children.length === 0) {
|
|
81
|
-
return checked.has(getKey(node));
|
|
82
|
-
}
|
|
83
|
-
return children.every((c) => isFullyChecked(c, checked, getKey));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** 是否处于半选状态:自身未 fully checked 但有任一后代被选。 */
|
|
87
|
-
export function isIndeterminate<T extends TreeNodeBase & { children?: T[] }>(
|
|
88
|
-
node: T,
|
|
89
|
-
checked: Set<string>,
|
|
90
|
-
getKey: (n: T) => string,
|
|
91
|
-
): boolean {
|
|
92
|
-
if (isFullyChecked(node, checked, getKey)) return false;
|
|
93
|
-
const stack: T[] = node.children ? [...node.children] : [];
|
|
94
|
-
while (stack.length) {
|
|
95
|
-
const cur = stack.pop()!;
|
|
96
|
-
if (checked.has(getKey(cur))) return true;
|
|
97
|
-
if (cur.children) stack.push(...cur.children);
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** 把内部完整 checkedSet 按策略压缩为出参。 */
|
|
103
|
-
export function applyCheckedStrategy<
|
|
104
|
-
T extends TreeNodeBase & { children?: T[] },
|
|
105
|
-
>(
|
|
106
|
-
data: T[],
|
|
107
|
-
checked: Set<string>,
|
|
108
|
-
strategy: 'parent' | 'child' | 'all',
|
|
109
|
-
getKey: (n: T) => string,
|
|
110
|
-
): string[] {
|
|
111
|
-
const out: string[] = [];
|
|
112
|
-
const walk = (nodes: T[]) => {
|
|
113
|
-
for (const node of nodes) {
|
|
114
|
-
const fully = isFullyChecked(node, checked, getKey);
|
|
115
|
-
if (strategy === 'parent') {
|
|
116
|
-
if (fully) {
|
|
117
|
-
out.push(getKey(node));
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (node.children) walk(node.children);
|
|
121
|
-
} else if (strategy === 'child') {
|
|
122
|
-
const isLeaf =
|
|
123
|
-
node.isLeaf ?? (!node.children || node.children.length === 0);
|
|
124
|
-
if (isLeaf && (fully || checked.has(getKey(node)))) {
|
|
125
|
-
out.push(getKey(node));
|
|
126
|
-
}
|
|
127
|
-
if (node.children) walk(node.children);
|
|
128
|
-
} else {
|
|
129
|
-
if (fully || checked.has(getKey(node))) out.push(getKey(node));
|
|
130
|
-
if (node.children) walk(node.children);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
walk(data);
|
|
135
|
-
return out;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** 父子联动模式:勾选 / 取消时同步 ancestor / descendant。 */
|
|
139
|
-
export function toggleWithCascade<T extends TreeNodeBase & { children?: T[] }>(
|
|
140
|
-
data: T[],
|
|
141
|
-
current: Set<string>,
|
|
142
|
-
target: T,
|
|
143
|
-
nextChecked: boolean,
|
|
144
|
-
getKey: (n: T) => string,
|
|
145
|
-
): Set<string> {
|
|
146
|
-
const next = new Set(current);
|
|
147
|
-
// 1. 同步所有可勾选后代(含自身)
|
|
148
|
-
for (const v of collectCheckable(target, getKey)) {
|
|
149
|
-
if (nextChecked) next.add(v);
|
|
150
|
-
else next.delete(v);
|
|
151
|
-
}
|
|
152
|
-
// 2. 找到 target 的祖先链,倒序回算
|
|
153
|
-
const parents: T[] = [];
|
|
154
|
-
const walk = (nodes: T[], chain: T[]): boolean => {
|
|
155
|
-
for (const node of nodes) {
|
|
156
|
-
if (node === target) {
|
|
157
|
-
parents.push(...chain);
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
if (node.children && walk(node.children, [...chain, node])) return true;
|
|
161
|
-
}
|
|
162
|
-
return false;
|
|
163
|
-
};
|
|
164
|
-
walk(data, []);
|
|
165
|
-
for (let i = parents.length - 1; i >= 0; i--) {
|
|
166
|
-
const p = parents[i]!;
|
|
167
|
-
if (isFullyChecked(p, next, getKey)) next.add(getKey(p));
|
|
168
|
-
else next.delete(getKey(p));
|
|
169
|
-
}
|
|
170
|
-
return next;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 收集树中所有节点的半选 key 列表(用于 onCheck info)。 */
|
|
174
|
-
export function collectIndeterminateKeys<
|
|
175
|
-
T extends TreeNodeBase & { children?: T[] },
|
|
176
|
-
>(data: T[], checked: Set<string>, getKey: (n: T) => string): string[] {
|
|
177
|
-
const out: string[] = [];
|
|
178
|
-
const walk = (nodes: T[]) => {
|
|
179
|
-
for (const node of nodes) {
|
|
180
|
-
if (isIndeterminate(node, checked, getKey)) out.push(getKey(node));
|
|
181
|
-
if (node.children) walk(node.children);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
walk(data);
|
|
185
|
-
return out;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ─── 展平 / 自动展开父级 ────────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
/** 扁平节点(虚拟滚动消费)。 */
|
|
191
|
-
export interface FlatTreeNode<T> {
|
|
192
|
-
/** 当前节点。 */
|
|
193
|
-
node: T;
|
|
194
|
-
/** 层级,根 = 0。 */
|
|
195
|
-
level: number;
|
|
196
|
-
/** 父节点 key 链(自顶向下,不含自身)。 */
|
|
197
|
-
parentKeys: string[];
|
|
198
|
-
/** 是否叶子。 */
|
|
199
|
-
isLeaf: boolean;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** 按 expandedKeys 展开当前可见节点列表,root → leaf 顺序。 */
|
|
203
|
-
export function flattenVisible<T extends { children?: T[]; isLeaf?: boolean }>(
|
|
204
|
-
data: T[],
|
|
205
|
-
expandedKeys: Set<string>,
|
|
206
|
-
getKey: (n: T) => string,
|
|
207
|
-
): FlatTreeNode<T>[] {
|
|
208
|
-
const out: FlatTreeNode<T>[] = [];
|
|
209
|
-
const walk = (nodes: T[], level: number, chain: string[]) => {
|
|
210
|
-
for (const node of nodes) {
|
|
211
|
-
const key = getKey(node);
|
|
212
|
-
const hasChildren = !!(node.children && node.children.length > 0);
|
|
213
|
-
const isLeaf = node.isLeaf ?? !hasChildren;
|
|
214
|
-
out.push({ node, level, parentKeys: chain, isLeaf });
|
|
215
|
-
if (hasChildren && expandedKeys.has(key)) {
|
|
216
|
-
walk(node.children!, level + 1, [...chain, key]);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
walk(data, 0, []);
|
|
221
|
-
return out;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** 把命中节点的所有祖先 key 自动并入展开集合(autoExpandParent 用)。 */
|
|
225
|
-
export function expandWithParents<T extends { children?: T[] }>(
|
|
226
|
-
data: T[],
|
|
227
|
-
keys: string[],
|
|
228
|
-
getKey: (n: T) => string,
|
|
229
|
-
): string[] {
|
|
230
|
-
const target = new Set(keys);
|
|
231
|
-
const out = new Set(keys);
|
|
232
|
-
const walk = (nodes: T[], chain: string[]): boolean => {
|
|
233
|
-
let hit = false;
|
|
234
|
-
for (const node of nodes) {
|
|
235
|
-
const key = getKey(node);
|
|
236
|
-
const childHit = node.children
|
|
237
|
-
? walk(node.children, [...chain, key])
|
|
238
|
-
: false;
|
|
239
|
-
if (target.has(key) || childHit) {
|
|
240
|
-
// 把当前节点的祖先加进来
|
|
241
|
-
for (const k of chain) out.add(k);
|
|
242
|
-
hit = true;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return hit;
|
|
246
|
-
};
|
|
247
|
-
walk(data, []);
|
|
248
|
-
return Array.from(out);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ─── React 受控/非受控辅助 ──────────────────────────────────────────────────
|
|
252
|
-
|
|
253
|
-
/** 用受控值 + 默认值计算最终值,并返回 setter(仅在非受控时生效)。 */
|
|
254
|
-
export function useControllableState<T>(
|
|
255
|
-
controlled: T | undefined,
|
|
256
|
-
defaultValue: T,
|
|
257
|
-
): [T, (next: T) => T] {
|
|
258
|
-
const [internal, setInternal] = React.useState<T>(defaultValue);
|
|
259
|
-
const isControlled = controlled !== undefined;
|
|
260
|
-
const value = isControlled ? (controlled as T) : internal;
|
|
261
|
-
const set = React.useCallback(
|
|
262
|
-
(next: T) => {
|
|
263
|
-
if (!isControlled) setInternal(next);
|
|
264
|
-
return next;
|
|
265
|
-
},
|
|
266
|
-
[isControlled],
|
|
267
|
-
);
|
|
268
|
-
return [value, set];
|
|
269
|
-
}
|