@teamix-evo/ui 0.1.1 → 0.3.0
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/README.md +184 -184
- package/manifest.json +680 -492
- package/package.json +20 -10
- package/src/components/accordion/accordion.meta.md +5 -4
- package/src/components/accordion/accordion.stories.tsx +14 -9
- package/src/components/accordion/accordion.tsx +104 -8
- package/src/components/affix/affix.meta.md +20 -2
- package/src/components/affix/affix.stories.tsx +102 -25
- package/src/components/affix/affix.tsx +79 -9
- package/src/components/alert/alert.meta.md +44 -13
- package/src/components/alert/alert.stories.tsx +66 -21
- package/src/components/alert/alert.tsx +81 -34
- package/src/components/alert-dialog/alert-dialog.meta.md +61 -16
- package/src/components/alert-dialog/alert-dialog.stories.tsx +145 -3
- package/src/components/alert-dialog/alert-dialog.tsx +60 -13
- package/src/components/anchor/anchor.meta.md +8 -3
- package/src/components/anchor/anchor.stories.tsx +3 -3
- package/src/components/anchor/anchor.tsx +2 -2
- package/src/components/app/app.meta.md +9 -4
- package/src/components/app/app.stories.tsx +9 -7
- package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -3
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
- package/src/components/auto-complete/auto-complete.meta.md +14 -6
- package/src/components/auto-complete/auto-complete.stories.tsx +47 -4
- package/src/components/auto-complete/auto-complete.tsx +119 -71
- package/src/components/avatar/avatar.meta.md +6 -7
- package/src/components/avatar/avatar.stories.tsx +21 -3
- package/src/components/avatar/avatar.tsx +24 -23
- package/src/components/badge/badge.meta.md +10 -9
- package/src/components/badge/badge.stories.tsx +2 -2
- package/src/components/badge/badge.tsx +9 -15
- package/src/components/breadcrumb/breadcrumb.meta.md +27 -7
- package/src/components/breadcrumb/breadcrumb.stories.tsx +127 -4
- package/src/components/breadcrumb/breadcrumb.tsx +22 -8
- package/src/components/button/button.meta.md +258 -21
- package/src/components/button/button.stories.tsx +549 -41
- package/src/components/button/button.tsx +335 -33
- package/src/components/button/demo/as-child.tsx +24 -0
- package/src/components/button/demo/basic.tsx +8 -0
- package/src/components/button/demo/block.tsx +16 -0
- package/src/components/button/demo/loading.tsx +19 -0
- package/src/components/button/demo/shapes.tsx +18 -0
- package/src/components/button/demo/sizes.tsx +19 -0
- package/src/components/button/demo/variants.tsx +19 -0
- package/src/components/button/demo/with-icon.tsx +20 -0
- package/src/components/calendar/calendar.meta.md +13 -3
- package/src/components/calendar/calendar.stories.tsx +6 -6
- package/src/components/calendar/calendar.tsx +73 -8
- package/src/components/card/card.meta.md +27 -5
- package/src/components/card/card.stories.tsx +42 -3
- package/src/components/card/card.tsx +146 -63
- package/src/components/carousel/carousel.meta.md +4 -3
- package/src/components/carousel/carousel.stories.tsx +11 -6
- package/src/components/cascader/cascader.meta.md +47 -17
- package/src/components/cascader/cascader.stories.tsx +22 -10
- package/src/components/cascader/cascader.tsx +428 -85
- package/src/components/checkbox/checkbox.meta.md +75 -7
- package/src/components/checkbox/checkbox.stories.tsx +161 -3
- package/src/components/checkbox/checkbox.tsx +77 -9
- package/src/components/collapsible/collapsible.meta.md +14 -6
- package/src/components/collapsible/collapsible.stories.tsx +10 -2
- package/src/components/collapsible/collapsible.tsx +93 -6
- package/src/components/color-picker/color-picker.meta.md +12 -7
- package/src/components/color-picker/color-picker.stories.tsx +86 -7
- package/src/components/color-picker/color-picker.tsx +20 -9
- package/src/components/command/command.meta.md +29 -13
- package/src/components/command/command.stories.tsx +4 -4
- package/src/components/command/command.tsx +19 -8
- package/src/components/context-menu/context-menu.meta.md +11 -8
- package/src/components/context-menu/context-menu.stories.tsx +11 -3
- package/src/components/context-menu/context-menu.tsx +21 -8
- package/src/components/data-table/data-table.meta.md +6 -5
- package/src/components/data-table/data-table.stories.tsx +13 -6
- package/src/components/data-table/data-table.tsx +2 -2
- package/src/components/date-picker/date-picker.meta.md +88 -19
- package/src/components/date-picker/date-picker.stories.tsx +55 -5
- package/src/components/date-picker/date-picker.tsx +1489 -91
- package/src/components/descriptions/descriptions.meta.md +10 -5
- package/src/components/descriptions/descriptions.stories.tsx +3 -3
- package/src/components/descriptions/descriptions.tsx +22 -14
- package/src/components/dialog/dialog.meta.md +76 -13
- package/src/components/dialog/dialog.stories.tsx +182 -20
- package/src/components/dialog/dialog.tsx +67 -15
- package/src/components/dialog/imperative.tsx +252 -0
- package/src/components/drawer/drawer.meta.md +33 -34
- package/src/components/drawer/drawer.stories.tsx +29 -12
- package/src/components/drawer/drawer.tsx +22 -113
- package/src/components/dropdown-menu/dropdown-menu.meta.md +78 -10
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +88 -2
- package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
- package/src/components/ellipsis/ellipsis.meta.md +87 -0
- package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
- package/src/components/ellipsis/ellipsis.tsx +153 -0
- package/src/components/empty/empty.meta.md +9 -4
- package/src/components/empty/empty.stories.tsx +4 -4
- package/src/components/empty/empty.tsx +10 -3
- package/src/components/field/field.meta.md +47 -9
- package/src/components/field/field.stories.tsx +385 -5
- package/src/components/field/field.tsx +263 -35
- package/src/components/filter-bar/filter-bar.meta.md +92 -0
- package/src/components/filter-bar/filter-bar.stories.tsx +1083 -0
- package/src/components/filter-bar/filter-bar.tsx +568 -0
- package/src/components/flex/flex.meta.md +54 -6
- package/src/components/flex/flex.stories.tsx +107 -20
- package/src/components/flex/flex.tsx +27 -4
- package/src/components/float-button/float-button.meta.md +8 -3
- package/src/components/float-button/float-button.stories.tsx +9 -7
- package/src/components/float-button/float-button.tsx +1 -1
- package/src/components/form/form.meta.md +39 -17
- package/src/components/form/form.stories.tsx +350 -3
- package/src/components/form/form.tsx +101 -35
- package/src/components/grid/grid.meta.md +7 -2
- package/src/components/grid/grid.stories.tsx +6 -4
- package/src/components/hover-card/hover-card.meta.md +20 -9
- package/src/components/hover-card/hover-card.stories.tsx +34 -5
- package/src/components/hover-card/hover-card.tsx +51 -13
- package/src/components/icon/DEVELOPMENT.md +809 -0
- package/src/components/icon/icon.meta.md +170 -0
- package/src/components/icon/icon.stories.tsx +344 -0
- package/src/components/icon/icon.tsx +248 -0
- package/src/components/image/image.meta.md +9 -4
- package/src/components/image/image.stories.tsx +3 -3
- package/src/components/image/image.tsx +6 -4
- package/src/components/input/demo/basic.tsx +12 -0
- package/src/components/input/demo/clearable.tsx +21 -0
- package/src/components/input/demo/show-count.tsx +18 -0
- package/src/components/input/demo/sizes.tsx +15 -0
- package/src/components/input/input.meta.md +39 -33
- package/src/components/input/input.stories.tsx +62 -35
- package/src/components/input/input.tsx +97 -98
- package/src/components/input-group/input-group.meta.md +54 -22
- package/src/components/input-group/input-group.stories.tsx +49 -16
- package/src/components/input-group/input-group.tsx +44 -8
- package/src/components/input-number/input-number.meta.md +64 -7
- package/src/components/input-number/input-number.stories.tsx +46 -8
- package/src/components/input-number/input-number.tsx +99 -26
- package/src/components/input-otp/input-otp.meta.md +4 -3
- package/src/components/input-otp/input-otp.stories.tsx +3 -3
- package/src/components/input-otp/input-otp.tsx +1 -1
- package/src/components/item/item.meta.md +8 -3
- package/src/components/item/item.stories.tsx +8 -5
- package/src/components/item/item.tsx +7 -6
- package/src/components/kbd/kbd.meta.md +13 -4
- package/src/components/kbd/kbd.stories.tsx +4 -4
- package/src/components/kbd/kbd.tsx +10 -5
- package/src/components/label/label.meta.md +18 -10
- package/src/components/label/label.stories.tsx +64 -6
- package/src/components/label/label.tsx +91 -19
- package/src/components/masonry/masonry.meta.md +8 -3
- package/src/components/masonry/masonry.stories.tsx +7 -5
- package/src/components/masonry/masonry.tsx +1 -0
- package/src/components/mentions/mentions.meta.md +36 -6
- package/src/components/mentions/mentions.stories.tsx +120 -6
- package/src/components/mentions/mentions.tsx +11 -5
- package/src/components/menubar/menubar.meta.md +30 -12
- package/src/components/menubar/menubar.stories.tsx +62 -2
- package/src/components/menubar/menubar.tsx +9 -9
- package/src/components/native-select/native-select.meta.md +8 -3
- package/src/components/native-select/native-select.stories.tsx +8 -5
- package/src/components/native-select/native-select.tsx +1 -1
- package/src/components/navigation-menu/navigation-menu.meta.md +19 -9
- package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -9
- package/src/components/navigation-menu/navigation-menu.tsx +8 -4
- package/src/components/notification/notification.meta.md +52 -10
- package/src/components/notification/notification.stories.tsx +11 -9
- package/src/components/notification/notification.tsx +36 -21
- package/src/components/page-header/DEVELOPMENT.md +842 -0
- package/src/components/page-header/page-header.meta.md +208 -0
- package/src/components/page-header/page-header.stories.tsx +421 -0
- package/src/components/page-header/page-header.tsx +281 -0
- package/src/components/pagination/pagination.meta.md +140 -37
- package/src/components/pagination/pagination.stories.tsx +232 -10
- package/src/components/pagination/pagination.tsx +355 -63
- package/src/components/popconfirm/popconfirm.meta.md +9 -4
- package/src/components/popconfirm/popconfirm.stories.tsx +3 -4
- package/src/components/popconfirm/popconfirm.tsx +2 -2
- package/src/components/popover/popover.meta.md +62 -5
- package/src/components/popover/popover.stories.tsx +83 -7
- package/src/components/popover/popover.tsx +77 -28
- package/src/components/progress/progress.meta.md +38 -6
- package/src/components/progress/progress.stories.tsx +3 -3
- package/src/components/progress/progress.tsx +24 -16
- package/src/components/radio-group/radio-group.meta.md +79 -7
- package/src/components/radio-group/radio-group.stories.tsx +39 -3
- package/src/components/radio-group/radio-group.tsx +149 -18
- package/src/components/rate/rate.meta.md +35 -4
- package/src/components/rate/rate.stories.tsx +13 -5
- package/src/components/rate/rate.tsx +37 -10
- package/src/components/resizable/resizable.meta.md +7 -4
- package/src/components/resizable/resizable.stories.tsx +6 -6
- package/src/components/resizable/resizable.tsx +1 -1
- package/src/components/result/result.meta.md +7 -2
- package/src/components/result/result.stories.tsx +4 -8
- package/src/components/result/result.tsx +24 -15
- package/src/components/scroll-area/scroll-area.meta.md +4 -3
- package/src/components/scroll-area/scroll-area.stories.tsx +12 -4
- package/src/components/scroll-area/scroll-area.tsx +3 -3
- package/src/components/segmented/segmented.meta.md +7 -4
- package/src/components/segmented/segmented.stories.tsx +37 -8
- package/src/components/segmented/segmented.tsx +15 -7
- package/src/components/select/select.meta.md +197 -52
- package/src/components/select/select.stories.tsx +238 -63
- package/src/components/select/select.tsx +718 -171
- package/src/components/separator/separator.meta.md +4 -3
- package/src/components/separator/separator.stories.tsx +3 -3
- package/src/components/separator/separator.tsx +3 -7
- package/src/components/sheet/sheet.meta.md +32 -16
- package/src/components/sheet/sheet.stories.tsx +116 -10
- package/src/components/sheet/sheet.tsx +116 -29
- package/src/components/sidebar/sidebar.meta.md +37 -18
- package/src/components/sidebar/sidebar.stories.tsx +701 -29
- package/src/components/sidebar/sidebar.tsx +615 -142
- package/src/components/skeleton/skeleton.meta.md +4 -5
- package/src/components/skeleton/skeleton.stories.tsx +4 -4
- package/src/components/skeleton/skeleton.tsx +7 -7
- package/src/components/slider/slider.meta.md +57 -5
- package/src/components/slider/slider.stories.tsx +58 -6
- package/src/components/slider/slider.tsx +154 -13
- package/src/components/sonner/sonner.meta.md +58 -7
- package/src/components/sonner/sonner.stories.tsx +78 -5
- package/src/components/sonner/sonner.tsx +137 -8
- package/src/components/spinner/spinner.meta.md +62 -13
- package/src/components/spinner/spinner.stories.tsx +66 -14
- package/src/components/spinner/spinner.tsx +111 -9
- package/src/components/statistic/statistic.meta.md +7 -2
- package/src/components/statistic/statistic.stories.tsx +3 -7
- package/src/components/statistic/statistic.tsx +5 -6
- package/src/components/steps/steps.meta.md +18 -4
- package/src/components/steps/steps.stories.tsx +43 -3
- package/src/components/steps/steps.tsx +15 -12
- package/src/components/switch/switch.meta.md +51 -5
- package/src/components/switch/switch.stories.tsx +6 -6
- package/src/components/switch/switch.tsx +109 -41
- package/src/components/table/table.meta.md +17 -6
- package/src/components/table/table.stories.tsx +10 -5
- package/src/components/table/table.tsx +4 -4
- package/src/components/tabs/tabs.meta.md +38 -25
- package/src/components/tabs/tabs.stories.tsx +111 -25
- package/src/components/tabs/tabs.tsx +125 -54
- package/src/components/tag/tag.meta.md +105 -40
- package/src/components/tag/tag.stories.tsx +189 -16
- package/src/components/tag/tag.tsx +222 -21
- package/src/components/textarea/textarea.meta.md +35 -19
- package/src/components/textarea/textarea.stories.tsx +32 -6
- package/src/components/textarea/textarea.tsx +33 -9
- package/src/components/time-picker/time-picker.meta.md +124 -32
- package/src/components/time-picker/time-picker.stories.tsx +85 -15
- package/src/components/time-picker/time-picker.tsx +913 -61
- package/src/components/timeline/timeline.meta.md +14 -6
- package/src/components/timeline/timeline.stories.tsx +37 -7
- package/src/components/timeline/timeline.tsx +35 -14
- package/src/components/toggle/toggle.meta.md +5 -4
- package/src/components/toggle/toggle.stories.tsx +4 -4
- package/src/components/toggle/toggle.tsx +4 -3
- package/src/components/toggle-group/toggle-group.meta.md +5 -4
- package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
- package/src/components/toggle-group/toggle-group.tsx +2 -2
- package/src/components/tooltip/tooltip.meta.md +55 -5
- package/src/components/tooltip/tooltip.stories.tsx +42 -5
- package/src/components/tooltip/tooltip.tsx +81 -21
- package/src/components/tour/tour.meta.md +9 -4
- package/src/components/tour/tour.stories.tsx +3 -3
- package/src/components/tour/tour.tsx +4 -4
- package/src/components/transfer/transfer.meta.md +11 -6
- package/src/components/transfer/transfer.stories.tsx +4 -8
- package/src/components/transfer/transfer.tsx +28 -21
- package/src/components/tree/tree.meta.md +63 -5
- package/src/components/tree/tree.stories.tsx +31 -12
- package/src/components/tree/tree.tsx +9 -8
- package/src/components/tree-select/tree-select.meta.md +59 -8
- package/src/components/tree-select/tree-select.stories.tsx +3 -3
- package/src/components/tree-select/tree-select.tsx +42 -7
- package/src/components/typography/typography.meta.md +61 -14
- package/src/components/typography/typography.stories.tsx +12 -11
- package/src/components/typography/typography.tsx +43 -28
- package/src/components/upload/upload.meta.md +49 -4
- package/src/components/upload/upload.stories.tsx +72 -12
- package/src/components/upload/upload.tsx +170 -37
- package/src/components/watermark/watermark.meta.md +7 -2
- package/src/components/watermark/watermark.stories.tsx +101 -9
- package/src/components/watermark/watermark.tsx +1 -0
- package/src/hooks/use-breakpoint.ts +117 -0
- package/src/hooks/use-debounce-callback.ts +52 -0
- package/src/hooks/use-mobile.ts +23 -0
- package/src/stories/theme-tokens.stories.tsx +747 -0
- package/src/utils/trigger-input.ts +53 -0
- package/src/components/button-group/button-group.meta.md +0 -92
- package/src/components/button-group/button-group.stories.tsx +0 -90
- package/src/components/button-group/button-group.tsx +0 -75
- package/src/components/combobox/combobox.meta.md +0 -93
- package/src/components/combobox/combobox.stories.tsx +0 -55
- package/src/components/combobox/combobox.tsx +0 -130
- package/src/components/space/space.meta.md +0 -94
- package/src/components/space/space.stories.tsx +0 -94
- package/src/components/space/space.tsx +0 -106
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Check,
|
|
4
|
+
ChevronDown,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
Loader2,
|
|
7
|
+
Search,
|
|
8
|
+
X,
|
|
9
|
+
} from 'lucide-react';
|
|
3
10
|
|
|
4
11
|
import { cn } from '@/utils/cn';
|
|
5
12
|
import { Button } from '@/components/button/button';
|
|
@@ -9,6 +16,8 @@ import {
|
|
|
9
16
|
PopoverTrigger,
|
|
10
17
|
} from '@/components/popover/popover';
|
|
11
18
|
|
|
19
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
12
21
|
export interface CascaderOption {
|
|
13
22
|
/** 级联节点 value(同级 value 必须唯一)。 */
|
|
14
23
|
value: string;
|
|
@@ -18,45 +27,70 @@ export interface CascaderOption {
|
|
|
18
27
|
children?: CascaderOption[];
|
|
19
28
|
/** 禁用此项。 */
|
|
20
29
|
disabled?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* 显式标记为叶子节点(用于 loadData 模式:当 children 为空但 isLeaf=false 时,
|
|
32
|
+
* 点击该节点触发异步加载;若 isLeaf=true 则直接当叶子处理)。
|
|
33
|
+
* @default 自动推断(无 children 即为叶子)
|
|
34
|
+
*/
|
|
35
|
+
isLeaf?: boolean;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
interface CascaderBaseProps {
|
|
24
39
|
/** 级联选项树(antd `options` 并集)。 */
|
|
25
40
|
options: CascaderOption[];
|
|
26
|
-
/** 受控值(每层 value 数组)。 */
|
|
27
|
-
value?: string[];
|
|
28
|
-
/** uncontrolled 初值。 */
|
|
29
|
-
defaultValue?: string[];
|
|
30
|
-
/** 值变化回调 — 触发条件:点选叶子节点或显式 changeOnSelect。 */
|
|
31
|
-
onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
|
|
32
41
|
/**
|
|
33
42
|
* 每选中一级即触发 onChange(antd `changeOnSelect` 并集) — 默认仅选叶子触发。
|
|
34
43
|
* @default false
|
|
35
44
|
*/
|
|
36
45
|
changeOnSelect?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* 占位文本。
|
|
39
|
-
* @default "请选择"
|
|
40
|
-
*/
|
|
46
|
+
/** 占位文本。 @default "请选择" */
|
|
41
47
|
placeholder?: string;
|
|
42
|
-
/**
|
|
43
|
-
* 路径展示的分隔符。
|
|
44
|
-
* @default " / "
|
|
45
|
-
*/
|
|
48
|
+
/** 路径展示的分隔符。 @default " / " */
|
|
46
49
|
separator?: string;
|
|
47
50
|
/** 整体禁用。 */
|
|
48
51
|
disabled?: boolean;
|
|
52
|
+
/** 触发器 className。 */
|
|
53
|
+
className?: string;
|
|
54
|
+
/** 触发器尺寸。 @default "md" */
|
|
55
|
+
size?: 'sm' | 'md' | 'default' | 'lg';
|
|
49
56
|
/**
|
|
50
|
-
*
|
|
57
|
+
* 启用搜索(面板顶部输入框,过滤匹配路径)。
|
|
58
|
+
* @default false
|
|
51
59
|
*/
|
|
52
|
-
|
|
60
|
+
showSearch?: boolean;
|
|
53
61
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
62
|
+
* 异步加载子节点 — 当点击非叶子且无 children 时触发。
|
|
63
|
+
* 消费者在回调内 fetch 数据后更新 `options` prop。
|
|
56
64
|
*/
|
|
57
|
-
|
|
65
|
+
loadData?: (selectedOptions: CascaderOption[]) => void;
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
export interface CascaderSingleProps extends CascaderBaseProps {
|
|
69
|
+
/** 单选模式(默认)。 */
|
|
70
|
+
multiple?: false | undefined;
|
|
71
|
+
/** 受控值(路径 value 数组)。 */
|
|
72
|
+
value?: string[];
|
|
73
|
+
/** 非受控初值。 */
|
|
74
|
+
defaultValue?: string[];
|
|
75
|
+
/** 值变化回调。 */
|
|
76
|
+
onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CascaderMultipleProps extends CascaderBaseProps {
|
|
80
|
+
/** 多选模式。 */
|
|
81
|
+
multiple: true;
|
|
82
|
+
/** 受控值(多条路径,每条为 value 数组)。 */
|
|
83
|
+
value?: string[][];
|
|
84
|
+
/** 非受控初值。 */
|
|
85
|
+
defaultValue?: string[][];
|
|
86
|
+
/** 值变化回调。 */
|
|
87
|
+
onChange?: (value: string[][], selectedOptions: CascaderOption[][]) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type CascaderProps = CascaderSingleProps | CascaderMultipleProps;
|
|
91
|
+
|
|
92
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
60
94
|
function findPath(
|
|
61
95
|
options: CascaderOption[],
|
|
62
96
|
value: string[],
|
|
@@ -64,7 +98,9 @@ function findPath(
|
|
|
64
98
|
const path: CascaderOption[] = [];
|
|
65
99
|
let cursor: CascaderOption[] | undefined = options;
|
|
66
100
|
for (const v of value) {
|
|
67
|
-
const found: CascaderOption | undefined = cursor?.find(
|
|
101
|
+
const found: CascaderOption | undefined = cursor?.find(
|
|
102
|
+
(o) => o.value === v,
|
|
103
|
+
);
|
|
68
104
|
if (!found) break;
|
|
69
105
|
path.push(found);
|
|
70
106
|
cursor = found.children;
|
|
@@ -72,68 +108,284 @@ function findPath(
|
|
|
72
108
|
return path;
|
|
73
109
|
}
|
|
74
110
|
|
|
111
|
+
function isNodeLeaf(opt: CascaderOption): boolean {
|
|
112
|
+
if (opt.isLeaf !== undefined) return opt.isLeaf;
|
|
113
|
+
return !opt.children || opt.children.length === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 用于 multiple 去重的 path → key。 */
|
|
117
|
+
const pathKey = (p: string[]): string => p.join('\0');
|
|
118
|
+
|
|
119
|
+
/** 将 options 树展开为 [path, labels] 列表(用于搜索)。 */
|
|
120
|
+
function flattenOptions(
|
|
121
|
+
options: CascaderOption[],
|
|
122
|
+
parentPath: CascaderOption[] = [],
|
|
123
|
+
): { path: CascaderOption[]; values: string[] }[] {
|
|
124
|
+
const results: { path: CascaderOption[]; values: string[] }[] = [];
|
|
125
|
+
for (const opt of options) {
|
|
126
|
+
const currentPath = [...parentPath, opt];
|
|
127
|
+
const values = currentPath.map((o) => o.value);
|
|
128
|
+
if (opt.children && opt.children.length > 0) {
|
|
129
|
+
// 有子级则递归
|
|
130
|
+
results.push(...flattenOptions(opt.children, currentPath));
|
|
131
|
+
} else {
|
|
132
|
+
// 叶子节点加入结果
|
|
133
|
+
results.push({ path: currentPath, values });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getLabel(node: React.ReactNode): string {
|
|
140
|
+
if (typeof node === 'string') return node;
|
|
141
|
+
if (typeof node === 'number') return String(node);
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Component ─────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
75
147
|
/**
|
|
76
|
-
* 级联选择 — antd
|
|
77
|
-
*
|
|
148
|
+
* 级联选择 — antd 独有补足。多级联动选择(地区 / 分类 / 组织架构),
|
|
149
|
+
* 支持单选 / 多选(checkbox) / 搜索过滤 / 异步加载(loadData)。
|
|
78
150
|
*/
|
|
79
151
|
const Cascader = React.forwardRef<HTMLButtonElement, CascaderProps>(
|
|
80
|
-
(
|
|
81
|
-
{
|
|
152
|
+
(props, ref) => {
|
|
153
|
+
const {
|
|
82
154
|
options,
|
|
83
|
-
value,
|
|
84
|
-
defaultValue,
|
|
85
|
-
onChange,
|
|
86
155
|
changeOnSelect = false,
|
|
87
156
|
placeholder = '请选择',
|
|
88
157
|
separator = ' / ',
|
|
89
158
|
disabled = false,
|
|
90
159
|
className,
|
|
91
|
-
size = '
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
size = 'md',
|
|
161
|
+
showSearch = false,
|
|
162
|
+
loadData,
|
|
163
|
+
multiple = false,
|
|
164
|
+
} = props;
|
|
165
|
+
|
|
166
|
+
// ─── Single mode state ───────────────────────────────
|
|
167
|
+
const singleValue = !multiple
|
|
168
|
+
? (props as CascaderSingleProps).value
|
|
169
|
+
: undefined;
|
|
170
|
+
const singleDefault = !multiple
|
|
171
|
+
? (props as CascaderSingleProps).defaultValue
|
|
172
|
+
: undefined;
|
|
173
|
+
const singleOnChange = !multiple
|
|
174
|
+
? (props as CascaderSingleProps).onChange
|
|
175
|
+
: undefined;
|
|
176
|
+
|
|
177
|
+
const isSingleControlled = !multiple && singleValue !== undefined;
|
|
178
|
+
const [singleInternal, setSingleInternal] = React.useState<string[]>(
|
|
179
|
+
singleDefault ?? [],
|
|
180
|
+
);
|
|
181
|
+
const currentSingle = isSingleControlled ? singleValue! : singleInternal;
|
|
98
182
|
|
|
183
|
+
// ─── Multiple mode state ─────────────────────────────
|
|
184
|
+
const multiValue = multiple
|
|
185
|
+
? (props as CascaderMultipleProps).value
|
|
186
|
+
: undefined;
|
|
187
|
+
const multiDefault = multiple
|
|
188
|
+
? (props as CascaderMultipleProps).defaultValue
|
|
189
|
+
: undefined;
|
|
190
|
+
const multiOnChange = multiple
|
|
191
|
+
? (props as CascaderMultipleProps).onChange
|
|
192
|
+
: undefined;
|
|
193
|
+
|
|
194
|
+
const isMultiControlled = multiple && multiValue !== undefined;
|
|
195
|
+
const [multiInternal, setMultiInternal] = React.useState<string[][]>(
|
|
196
|
+
multiDefault ?? [],
|
|
197
|
+
);
|
|
198
|
+
const currentMulti = isMultiControlled ? multiValue! : multiInternal;
|
|
199
|
+
|
|
200
|
+
// ─── Shared state ────────────────────────────────────
|
|
99
201
|
const [open, setOpen] = React.useState(false);
|
|
100
|
-
const [activePath, setActivePath] = React.useState<string[]>(
|
|
202
|
+
const [activePath, setActivePath] = React.useState<string[]>(
|
|
203
|
+
multiple ? [] : currentSingle,
|
|
204
|
+
);
|
|
205
|
+
const [searchText, setSearchText] = React.useState('');
|
|
206
|
+
const [loadingKeys, setLoadingKeys] = React.useState<Set<string>>(
|
|
207
|
+
new Set(),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
React.useEffect(() => {
|
|
211
|
+
if (open) {
|
|
212
|
+
setActivePath(multiple ? [] : currentSingle);
|
|
213
|
+
setSearchText('');
|
|
214
|
+
}
|
|
215
|
+
}, [open]);
|
|
101
216
|
|
|
217
|
+
// ─── loadData 响应:当 options 更新时清除对应 loading ──
|
|
102
218
|
React.useEffect(() => {
|
|
103
|
-
if (
|
|
104
|
-
|
|
219
|
+
if (loadingKeys.size === 0) return;
|
|
220
|
+
setLoadingKeys((prev) => {
|
|
221
|
+
const next = new Set(prev);
|
|
222
|
+
for (const key of prev) {
|
|
223
|
+
const parts = key.split('\0');
|
|
224
|
+
const path = findPath(options, parts);
|
|
225
|
+
const last = path[path.length - 1];
|
|
226
|
+
if (last?.children && last.children.length > 0) {
|
|
227
|
+
next.delete(key);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return next.size === prev.size ? prev : next;
|
|
231
|
+
});
|
|
232
|
+
}, [options, loadingKeys]);
|
|
105
233
|
|
|
106
|
-
|
|
234
|
+
// ─── Handlers ────────────────────────────────────────
|
|
107
235
|
|
|
108
236
|
const handleSelect = (level: number, opt: CascaderOption) => {
|
|
109
237
|
if (opt.disabled) return;
|
|
110
238
|
const next = [...activePath.slice(0, level), opt.value];
|
|
111
239
|
setActivePath(next);
|
|
112
|
-
|
|
113
|
-
|
|
240
|
+
|
|
241
|
+
const leaf = isNodeLeaf(opt);
|
|
242
|
+
|
|
243
|
+
// loadData: 非叶子且无子级时触发异步加载
|
|
244
|
+
if (!leaf && (!opt.children || opt.children.length === 0) && loadData) {
|
|
114
245
|
const path = findPath(options, next);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
246
|
+
const key = pathKey(next);
|
|
247
|
+
setLoadingKeys((prev) => new Set(prev).add(key));
|
|
248
|
+
loadData(path);
|
|
249
|
+
return;
|
|
118
250
|
}
|
|
251
|
+
|
|
252
|
+
if (!leaf && !changeOnSelect) return;
|
|
253
|
+
|
|
254
|
+
if (multiple) {
|
|
255
|
+
// multiple: toggle selection
|
|
256
|
+
const existing = currentMulti.findIndex(
|
|
257
|
+
(p) => pathKey(p) === pathKey(next),
|
|
258
|
+
);
|
|
259
|
+
let nextMulti: string[][];
|
|
260
|
+
if (existing >= 0) {
|
|
261
|
+
nextMulti = currentMulti.filter((_, i) => i !== existing);
|
|
262
|
+
} else {
|
|
263
|
+
nextMulti = [...currentMulti, next];
|
|
264
|
+
}
|
|
265
|
+
if (!isMultiControlled) setMultiInternal(nextMulti);
|
|
266
|
+
const paths = nextMulti.map((v) => findPath(options, v));
|
|
267
|
+
multiOnChange?.(nextMulti, paths);
|
|
268
|
+
// 不自动关闭
|
|
269
|
+
} else {
|
|
270
|
+
// single
|
|
271
|
+
const path = findPath(options, next);
|
|
272
|
+
if (!isSingleControlled) setSingleInternal(next);
|
|
273
|
+
singleOnChange?.(next, path);
|
|
274
|
+
if (leaf) setOpen(false);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const handleSearchSelect = (values: string[]) => {
|
|
279
|
+
if (multiple) {
|
|
280
|
+
const existing = currentMulti.findIndex(
|
|
281
|
+
(p) => pathKey(p) === pathKey(values),
|
|
282
|
+
);
|
|
283
|
+
let nextMulti: string[][];
|
|
284
|
+
if (existing >= 0) {
|
|
285
|
+
nextMulti = currentMulti.filter((_, i) => i !== existing);
|
|
286
|
+
} else {
|
|
287
|
+
nextMulti = [...currentMulti, values];
|
|
288
|
+
}
|
|
289
|
+
if (!isMultiControlled) setMultiInternal(nextMulti);
|
|
290
|
+
const paths = nextMulti.map((v) => findPath(options, v));
|
|
291
|
+
multiOnChange?.(nextMulti, paths);
|
|
292
|
+
} else {
|
|
293
|
+
const path = findPath(options, values);
|
|
294
|
+
if (!isSingleControlled) setSingleInternal(values);
|
|
295
|
+
singleOnChange?.(values, path);
|
|
296
|
+
setOpen(false);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleRemoveTag = (idx: number) => {
|
|
301
|
+
const nextMulti = currentMulti.filter((_, i) => i !== idx);
|
|
302
|
+
if (!isMultiControlled) setMultiInternal(nextMulti);
|
|
303
|
+
const paths = nextMulti.map((v) => findPath(options, v));
|
|
304
|
+
multiOnChange?.(nextMulti, paths);
|
|
119
305
|
};
|
|
120
306
|
|
|
121
|
-
//
|
|
307
|
+
// ─── Columns ─────────────────────────────────────────
|
|
122
308
|
const columns: CascaderOption[][] = [options];
|
|
123
309
|
let cursor: CascaderOption[] | undefined = options;
|
|
124
310
|
for (const v of activePath) {
|
|
125
|
-
const found: CascaderOption | undefined = cursor?.find(
|
|
311
|
+
const found: CascaderOption | undefined = cursor?.find(
|
|
312
|
+
(o) => o.value === v,
|
|
313
|
+
);
|
|
126
314
|
if (found?.children && found.children.length > 0) {
|
|
127
315
|
columns.push(found.children);
|
|
128
316
|
cursor = found.children;
|
|
129
317
|
} else break;
|
|
130
318
|
}
|
|
131
319
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
320
|
+
// ─── Search results ──────────────────────────────────
|
|
321
|
+
const searchResults = React.useMemo(() => {
|
|
322
|
+
if (!showSearch || !searchText.trim()) return [];
|
|
323
|
+
const term = searchText.trim().toLowerCase();
|
|
324
|
+
const all = flattenOptions(options);
|
|
325
|
+
return all.filter((item) =>
|
|
326
|
+
item.path.some((opt) =>
|
|
327
|
+
getLabel(opt.label).toLowerCase().includes(term),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
}, [showSearch, searchText, options]);
|
|
331
|
+
|
|
332
|
+
// ─── Display ─────────────────────────────────────────
|
|
333
|
+
let triggerContent: React.ReactNode;
|
|
334
|
+
if (multiple) {
|
|
335
|
+
if (currentMulti.length === 0) {
|
|
336
|
+
triggerContent = (
|
|
337
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
338
|
+
);
|
|
339
|
+
} else {
|
|
340
|
+
triggerContent = (
|
|
341
|
+
<span className="flex flex-wrap items-center gap-1 overflow-hidden">
|
|
342
|
+
{currentMulti.slice(0, 3).map((path, i) => {
|
|
343
|
+
const opts = findPath(options, path);
|
|
344
|
+
const label = opts.map((o) => getLabel(o.label)).join(separator);
|
|
345
|
+
return (
|
|
346
|
+
<span
|
|
347
|
+
key={pathKey(path)}
|
|
348
|
+
className="inline-flex max-w-32 items-center gap-0.5 truncate rounded bg-muted px-1.5 py-0.5 text-xs"
|
|
349
|
+
>
|
|
350
|
+
<span className="truncate">{label}</span>
|
|
351
|
+
<X
|
|
352
|
+
className="size-3 shrink-0 cursor-pointer opacity-60 hover:opacity-100"
|
|
353
|
+
onClick={(e) => {
|
|
354
|
+
e.stopPropagation();
|
|
355
|
+
handleRemoveTag(i);
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
358
|
+
</span>
|
|
359
|
+
);
|
|
360
|
+
})}
|
|
361
|
+
{currentMulti.length > 3 && (
|
|
362
|
+
<span className="text-xs text-muted-foreground">
|
|
363
|
+
+{currentMulti.length - 3}
|
|
364
|
+
</span>
|
|
365
|
+
)}
|
|
366
|
+
</span>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
const selectedPath = findPath(options, currentSingle);
|
|
371
|
+
const display =
|
|
372
|
+
selectedPath.length > 0
|
|
373
|
+
? selectedPath.map((o) => o.label).join(separator)
|
|
374
|
+
: '';
|
|
375
|
+
triggerContent = (
|
|
376
|
+
<span className={cn('truncate', !display && 'text-muted-foreground')}>
|
|
377
|
+
{display || placeholder}
|
|
378
|
+
</span>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ─── selectedSet (multiple) ──────────────────────────
|
|
383
|
+
const selectedSet = React.useMemo(
|
|
384
|
+
() => new Set(currentMulti.map(pathKey)),
|
|
385
|
+
[currentMulti],
|
|
386
|
+
);
|
|
136
387
|
|
|
388
|
+
// ─── Render ──────────────────────────────────────────
|
|
137
389
|
return (
|
|
138
390
|
<Popover open={open} onOpenChange={setOpen}>
|
|
139
391
|
<PopoverTrigger asChild>
|
|
@@ -144,50 +396,141 @@ const Cascader = React.forwardRef<HTMLButtonElement, CascaderProps>(
|
|
|
144
396
|
size={size}
|
|
145
397
|
disabled={disabled}
|
|
146
398
|
className={cn(
|
|
147
|
-
'min-w-
|
|
148
|
-
!display && 'text-muted-foreground',
|
|
399
|
+
'min-w-panel-sm justify-between font-normal',
|
|
149
400
|
className,
|
|
150
401
|
)}
|
|
151
402
|
>
|
|
152
|
-
|
|
403
|
+
{triggerContent}
|
|
153
404
|
<ChevronDown className="ml-2 size-4 shrink-0 opacity-50" />
|
|
154
405
|
</Button>
|
|
155
406
|
</PopoverTrigger>
|
|
156
|
-
<PopoverContent className="
|
|
157
|
-
{
|
|
407
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
408
|
+
{/* Search input */}
|
|
409
|
+
{showSearch && (
|
|
410
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
411
|
+
<Search className="size-4 text-muted-foreground" />
|
|
412
|
+
<input
|
|
413
|
+
type="text"
|
|
414
|
+
value={searchText}
|
|
415
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
416
|
+
placeholder="搜索..."
|
|
417
|
+
className="flex-1 bg-transparent text-xs outline-none placeholder:text-muted-foreground"
|
|
418
|
+
autoFocus
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{/* Search results */}
|
|
424
|
+
{showSearch && searchText.trim() ? (
|
|
158
425
|
<ul
|
|
159
|
-
key={level}
|
|
160
426
|
role="listbox"
|
|
161
|
-
className=
|
|
162
|
-
'min-w-[160px] max-h-72 overflow-auto py-1 text-sm',
|
|
163
|
-
level > 0 && 'border-l',
|
|
164
|
-
)}
|
|
427
|
+
className="max-h-72 min-w-48 overflow-auto p-1 text-xs"
|
|
165
428
|
>
|
|
166
|
-
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
429
|
+
{searchResults.length === 0 ? (
|
|
430
|
+
<li className="px-2 py-4 text-center text-muted-foreground">
|
|
431
|
+
无匹配结果
|
|
432
|
+
</li>
|
|
433
|
+
) : (
|
|
434
|
+
searchResults.map((item) => {
|
|
435
|
+
const key = pathKey(item.values);
|
|
436
|
+
const isSelected = multiple
|
|
437
|
+
? selectedSet.has(key)
|
|
438
|
+
: pathKey(currentSingle) === key;
|
|
439
|
+
return (
|
|
440
|
+
<li
|
|
441
|
+
key={key}
|
|
442
|
+
role="option"
|
|
443
|
+
aria-selected={isSelected}
|
|
444
|
+
onClick={() => handleSearchSelect(item.values)}
|
|
445
|
+
className={cn(
|
|
446
|
+
'flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 transition-colors hover:bg-accent/50',
|
|
447
|
+
isSelected &&
|
|
448
|
+
'bg-accent font-medium text-accent-foreground',
|
|
449
|
+
)}
|
|
450
|
+
>
|
|
451
|
+
{multiple && (
|
|
452
|
+
<span
|
|
453
|
+
className={cn(
|
|
454
|
+
'flex size-4 shrink-0 items-center justify-center rounded border border-primary',
|
|
455
|
+
isSelected && 'bg-primary text-primary-foreground',
|
|
456
|
+
)}
|
|
457
|
+
>
|
|
458
|
+
{isSelected && <Check className="size-3" />}
|
|
459
|
+
</span>
|
|
460
|
+
)}
|
|
461
|
+
<span className="truncate">
|
|
462
|
+
{item.path
|
|
463
|
+
.map((o) => getLabel(o.label))
|
|
464
|
+
.join(separator)}
|
|
465
|
+
</span>
|
|
466
|
+
</li>
|
|
467
|
+
);
|
|
468
|
+
})
|
|
469
|
+
)}
|
|
189
470
|
</ul>
|
|
190
|
-
)
|
|
471
|
+
) : (
|
|
472
|
+
/* Column panels */
|
|
473
|
+
<div className="flex">
|
|
474
|
+
{columns.map((col, level) => (
|
|
475
|
+
<ul
|
|
476
|
+
key={level}
|
|
477
|
+
role="listbox"
|
|
478
|
+
className={cn(
|
|
479
|
+
'min-w-submenu max-h-72 overflow-auto p-1 text-xs',
|
|
480
|
+
level > 0 && 'border-l border-l-border',
|
|
481
|
+
)}
|
|
482
|
+
>
|
|
483
|
+
{col.map((opt) => {
|
|
484
|
+
const isActive = activePath[level] === opt.value;
|
|
485
|
+
const leaf = isNodeLeaf(opt);
|
|
486
|
+
const hasChildren = !leaf;
|
|
487
|
+
const nodeKey = pathKey([
|
|
488
|
+
...activePath.slice(0, level),
|
|
489
|
+
opt.value,
|
|
490
|
+
]);
|
|
491
|
+
const isLoading = loadingKeys.has(nodeKey);
|
|
492
|
+
const isChecked = multiple && selectedSet.has(nodeKey);
|
|
493
|
+
return (
|
|
494
|
+
<li
|
|
495
|
+
key={opt.value}
|
|
496
|
+
role="option"
|
|
497
|
+
aria-selected={isActive}
|
|
498
|
+
onClick={() => handleSelect(level, opt)}
|
|
499
|
+
className={cn(
|
|
500
|
+
'flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 transition-colors',
|
|
501
|
+
!opt.disabled && 'hover:bg-accent/50',
|
|
502
|
+
isActive &&
|
|
503
|
+
!opt.disabled &&
|
|
504
|
+
'bg-accent font-medium text-accent-foreground',
|
|
505
|
+
opt.disabled && 'cursor-not-allowed opacity-50',
|
|
506
|
+
)}
|
|
507
|
+
>
|
|
508
|
+
<span className="flex items-center gap-1.5 truncate">
|
|
509
|
+
{multiple && leaf && (
|
|
510
|
+
<span
|
|
511
|
+
className={cn(
|
|
512
|
+
'flex size-4 shrink-0 items-center justify-center rounded border border-primary',
|
|
513
|
+
isChecked &&
|
|
514
|
+
'bg-primary text-primary-foreground',
|
|
515
|
+
)}
|
|
516
|
+
>
|
|
517
|
+
{isChecked && <Check className="size-3" />}
|
|
518
|
+
</span>
|
|
519
|
+
)}
|
|
520
|
+
<span className="truncate">{opt.label}</span>
|
|
521
|
+
</span>
|
|
522
|
+
{isLoading ? (
|
|
523
|
+
<Loader2 className="size-3.5 shrink-0 animate-spin text-muted-foreground" />
|
|
524
|
+
) : hasChildren ? (
|
|
525
|
+
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
|
|
526
|
+
) : null}
|
|
527
|
+
</li>
|
|
528
|
+
);
|
|
529
|
+
})}
|
|
530
|
+
</ul>
|
|
531
|
+
))}
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
191
534
|
</PopoverContent>
|
|
192
535
|
</Popover>
|
|
193
536
|
);
|