seven-design-ui 0.0.1 → 0.0.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.
Files changed (40) hide show
  1. package/docs/components/cascader.mdx +413 -0
  2. package/docs/doc_build/404.html +1 -1
  3. package/docs/doc_build/components/button.html +2 -2
  4. package/docs/doc_build/components/cascader.html +45 -0
  5. package/docs/doc_build/components/input.html +2 -2
  6. package/docs/doc_build/components/pagination.html +3 -3
  7. package/docs/doc_build/components/switch.html +3 -3
  8. package/docs/doc_build/guide/introduction.html +2 -2
  9. package/docs/doc_build/guide/quick-start.html +1 -1
  10. package/docs/doc_build/guide/theme.html +1 -1
  11. package/docs/doc_build/index.html +1 -1
  12. package/docs/doc_build/static/css/{styles.5a3e7113.css → styles.fc43cac8.css} +1 -1
  13. package/docs/doc_build/static/js/414.7fd9a017.js +6 -0
  14. package/docs/doc_build/static/js/async/166.656253c5.js +2 -0
  15. package/docs/doc_build/static/js/async/252.c7b023ea.js +1 -0
  16. package/docs/doc_build/static/js/async/{637.cb5d76c9.js → 637.9d5c88dd.js} +1 -1
  17. package/docs/doc_build/static/js/index.366ff9c0.js +1 -0
  18. package/docs/doc_build/static/search_index.69d54449.json +1 -0
  19. package/docs/guide/introduction.md +1 -1
  20. package/docs/package.json +2 -1
  21. package/docs/rspress.config.ts +5 -1
  22. package/package.json +2 -2
  23. package/packages/components/package.json +1 -1
  24. package/packages/components/src/cascader/Cascader.tsx +638 -0
  25. package/packages/components/src/cascader/cascader.css +381 -0
  26. package/packages/components/src/cascader/index.ts +1 -0
  27. package/packages/components/src/index.ts +1 -0
  28. package/play/src/App.tsx +74 -1
  29. package/play/src/index.css +2 -0
  30. package/play/src/options/basic.ts +32 -0
  31. package/play/src/options/disabled.ts +25 -0
  32. package/play/src/options/hover.ts +18 -0
  33. package/play/src/options/index.ts +4 -0
  34. package/play/src/options/multiple.ts +39 -0
  35. package/docs/doc_build/static/js/414.04bb58dd.js +0 -6
  36. package/docs/doc_build/static/js/async/166.f43be01a.js +0 -2
  37. package/docs/doc_build/static/js/index.0991c749.js +0 -1
  38. package/docs/doc_build/static/search_index.72c9c372.json +0 -1
  39. /package/docs/doc_build/static/js/{414.04bb58dd.js.LICENSE.txt → 414.7fd9a017.js.LICENSE.txt} +0 -0
  40. /package/docs/doc_build/static/js/async/{166.f43be01a.js.LICENSE.txt → 166.656253c5.js.LICENSE.txt} +0 -0
@@ -0,0 +1,638 @@
1
+ import { forwardRef, useState, useCallback, useMemo, useRef, useEffect, type MutableRefObject } from 'react'
2
+ import { classnames } from '@seven-design-ui/core'
3
+ import './cascader.css'
4
+
5
+ // 级联选择器的选项类型
6
+ export interface CascaderOption {
7
+ /** 选项的值 */
8
+ value: string | number
9
+ /** 选项的显示文本 */
10
+ label: string
11
+ /** 子选项 */
12
+ children?: CascaderOption[]
13
+ /** 是否禁用 */
14
+ disabled?: boolean
15
+ /** 是否禁用复选框(多选模式下) */
16
+ disableCheckbox?: boolean
17
+ }
18
+
19
+ // 级联选择器的属性类型
20
+ export interface CascaderProps {
21
+ /** 选项数据 */
22
+ options: CascaderOption[]
23
+ /** 展开触发方式 */
24
+ expandTrigger?: 'click' | 'hover'
25
+ /** 是否可清空 */
26
+ clearable?: boolean
27
+ /** 是否禁用 */
28
+ disabled?: boolean
29
+ /** 默认值 */
30
+ defaultValue?: (string | number)[] | (string | number)
31
+ /** 当前值(受控) */
32
+ value?: (string | number)[] | (string | number)
33
+ /** 值改变时的回调 */
34
+ onChange?: (value: (string | number)[] | (string | number) | undefined, selectedOptions: CascaderOption[] | CascaderOption | undefined) => void
35
+ /** 是否显示完整路径 */
36
+ showAllLevels?: boolean
37
+ /** 是否支持多选 */
38
+ multiple?: boolean
39
+ /** 是否支持搜索 */
40
+ showSearch?: boolean
41
+ /** 自定义搜索函数 */
42
+ filterOption?: (inputValue: string, option: CascaderOption) => boolean
43
+ /** 占位符 */
44
+ placeholder?: string
45
+ /** 搜索占位符 */
46
+ searchPlaceholder?: string
47
+ /** 自定义类名 */
48
+ className?: string
49
+ /** 自定义样式 */
50
+ style?: React.CSSProperties
51
+ }
52
+
53
+ // 内部使用的选中项类型
54
+ interface SelectedItem {
55
+ value: string | number
56
+ label: string
57
+ path: CascaderOption[]
58
+ }
59
+
60
+ export const Cascader = forwardRef<HTMLDivElement, CascaderProps>((props, ref) => {
61
+ const {
62
+ options,
63
+ expandTrigger = 'click',
64
+ clearable = false,
65
+ disabled = false,
66
+ defaultValue,
67
+ value: controlledValue,
68
+ onChange,
69
+ showAllLevels = true,
70
+ multiple = false,
71
+ showSearch = false,
72
+ filterOption,
73
+ placeholder = '请选择',
74
+ searchPlaceholder = '搜索选项',
75
+ className,
76
+ style,
77
+ ...rest
78
+ } = props
79
+
80
+ // 内部状态管理
81
+ const [internalValue, setInternalValue] = useState<(string | number)[] | (string | number) | undefined>(defaultValue)
82
+ const [isOpen, setIsOpen] = useState(false)
83
+ const [activePath, setActivePath] = useState<CascaderOption[]>([])
84
+ const [hoveredPath, setHoveredPath] = useState<CascaderOption[]>([])
85
+ const [focusedIndex, setFocusedIndex] = useState<number>(-1)
86
+ const [focusedLevel, setFocusedLevel] = useState<number>(0)
87
+ const [searchValue, setSearchValue] = useState<string>('')
88
+
89
+ // 引用:容器包含触发器+下拉面板,用于点击外部检测;inputRef 仅触发器
90
+ const containerRef = useRef<HTMLDivElement>(null)
91
+ const inputRef = useRef<HTMLDivElement>(null)
92
+
93
+ // 当前值(受控或内部)
94
+ const value = controlledValue !== undefined ? controlledValue : internalValue
95
+
96
+ // 将值转换为数组格式(便于处理)
97
+ const valueArray = useMemo(() => {
98
+ if (multiple) {
99
+ return Array.isArray(value) ? value : (value ? [value] : [])
100
+ } else {
101
+ return value ? (Array.isArray(value) ? [value[0]] : [value]) : []
102
+ }
103
+ }, [value, multiple])
104
+
105
+ // 根据值找到对应的选项路径
106
+ const selectedItems = useMemo((): SelectedItem[] => {
107
+ const findOptionsByValues = (values: (string | number)[]): SelectedItem[] => {
108
+ const result: SelectedItem[] = []
109
+
110
+ const findPath = (options: CascaderOption[], targetValue: string | number, currentPath: CascaderOption[] = []): CascaderOption[] | null => {
111
+ for (const option of options) {
112
+ const newPath = [...currentPath, option]
113
+ if (option.value === targetValue) {
114
+ return newPath
115
+ }
116
+ if (option.children) {
117
+ const childPath = findPath(option.children, targetValue, newPath)
118
+ if (childPath) {
119
+ return childPath
120
+ }
121
+ }
122
+ }
123
+ return null
124
+ }
125
+
126
+ for (const val of values) {
127
+ const path = findPath(options, val)
128
+ if (path) {
129
+ const lastOption = path[path.length - 1]
130
+ result.push({
131
+ value: val,
132
+ label: lastOption.label,
133
+ path: path
134
+ })
135
+ }
136
+ }
137
+
138
+ return result
139
+ }
140
+
141
+ return findOptionsByValues(valueArray)
142
+ }, [valueArray, options])
143
+
144
+ // 显示文本
145
+ const displayText = useMemo(() => {
146
+ if (selectedItems.length === 0) {
147
+ return ''
148
+ }
149
+
150
+ if (multiple) {
151
+ // 多选模式:显示所有选中项的标签
152
+ return selectedItems.map(item => item.label).join(', ')
153
+ } else {
154
+ // 单选模式:根据 showAllLevels 决定显示完整路径还是最后一级
155
+ const item = selectedItems[0]
156
+ if (showAllLevels) {
157
+ return item.path.map(opt => opt.label).join(' / ')
158
+ } else {
159
+ return item.label
160
+ }
161
+ }
162
+ }, [selectedItems, multiple, showAllLevels])
163
+
164
+ // 处理选项点击
165
+ const handleOptionClick = useCallback((option: CascaderOption, path: CascaderOption[]) => {
166
+ if (option.disabled) return
167
+
168
+ if (multiple) {
169
+ // 多选模式
170
+ const currentValues = valueArray
171
+ const isSelected = currentValues.includes(option.value)
172
+
173
+ let newValues: (string | number)[]
174
+ if (isSelected) {
175
+ // 取消选中
176
+ newValues = currentValues.filter(v => v !== option.value)
177
+ } else {
178
+ // 添加选中
179
+ newValues = [...currentValues, option.value]
180
+ }
181
+
182
+ const newValue = multiple ? newValues : newValues[0]
183
+ if (controlledValue === undefined) {
184
+ setInternalValue(newValue)
185
+ }
186
+
187
+ // 直接构造选中的选项,避免使用过时的selectedItems
188
+ const selectedOptions = newValues.map(val => {
189
+ const findOption = (options: CascaderOption[]): CascaderOption | null => {
190
+ for (const opt of options) {
191
+ if (opt.value === val) return opt
192
+ if (opt.children) {
193
+ const childResult = findOption(opt.children)
194
+ if (childResult) return childResult
195
+ }
196
+ }
197
+ return null
198
+ }
199
+ return findOption(options)
200
+ }).filter(Boolean) as CascaderOption[]
201
+
202
+ onChange?.(newValue, selectedOptions.length > 0 ? selectedOptions : (multiple ? [] : undefined))
203
+
204
+ // 如果有子选项,展开子菜单
205
+ if (option.children && !isSelected) {
206
+ setActivePath(path)
207
+ }
208
+ } else {
209
+ // 单选模式
210
+ if (option.children) {
211
+ // 有子选项,展开
212
+ setActivePath(path)
213
+ } else {
214
+ // 没有子选项,选择并关闭
215
+ const newValue = option.value
216
+ if (controlledValue === undefined) {
217
+ setInternalValue(newValue)
218
+ }
219
+ onChange?.(newValue, option)
220
+ setIsOpen(false)
221
+ setActivePath([])
222
+ }
223
+ }
224
+ }, [multiple, valueArray, controlledValue, onChange, options])
225
+
226
+ // 处理鼠标悬停
227
+ const handleOptionHover = useCallback((option: CascaderOption, path: CascaderOption[]) => {
228
+ if (expandTrigger === 'hover' && option.children) {
229
+ setHoveredPath(path)
230
+ }
231
+ }, [expandTrigger])
232
+
233
+ // 当前激活的路径(用于显示子菜单)
234
+ const currentActivePath = useMemo(() => {
235
+ if (expandTrigger === 'hover') {
236
+ return hoveredPath
237
+ } else {
238
+ return activePath
239
+ }
240
+ }, [expandTrigger, hoveredPath, activePath])
241
+
242
+
243
+ // 处理输入框点击
244
+ const handleInputClick = useCallback(() => {
245
+ if (disabled) return
246
+ setIsOpen(!isOpen)
247
+ }, [disabled, isOpen])
248
+
249
+ // 处理清空
250
+ const handleClear = useCallback((e: React.MouseEvent) => {
251
+ e.stopPropagation()
252
+ const newValue = multiple ? [] : undefined
253
+ if (controlledValue === undefined) {
254
+ setInternalValue(newValue)
255
+ }
256
+ onChange?.(newValue, multiple ? [] : undefined)
257
+ setIsOpen(false)
258
+ setActivePath([])
259
+ }, [multiple, controlledValue, onChange])
260
+
261
+ // 处理标签关闭(多选模式)
262
+ const handleTagClose = useCallback((tagValue: string | number, e: React.MouseEvent) => {
263
+ e.stopPropagation()
264
+ const newValues = valueArray.filter(v => v !== tagValue)
265
+ const newValue = multiple ? newValues : newValues[0] || undefined
266
+ if (controlledValue === undefined) {
267
+ setInternalValue(newValue)
268
+ }
269
+ const selectedOptions = selectedItems.filter(item => newValues.includes(item.value)).map(item => item.path[item.path.length - 1])
270
+ onChange?.(newValue, selectedOptions.length > 0 ? selectedOptions : (multiple ? [] : undefined))
271
+ }, [valueArray, multiple, controlledValue, onChange, selectedItems])
272
+
273
+ // 默认搜索过滤函数
274
+ const defaultFilterOption = useCallback((inputValue: string, option: CascaderOption): boolean => {
275
+ return option.label.toLowerCase().includes(inputValue.toLowerCase())
276
+ }, [])
277
+
278
+ // 搜索过滤选项
279
+ const filterOptions = useCallback((options: CascaderOption[], searchValue: string): CascaderOption[] => {
280
+ if (!searchValue) return options
281
+
282
+ const filter = filterOption || defaultFilterOption
283
+
284
+ const filterRecursively = (opts: CascaderOption[]): CascaderOption[] => {
285
+ return opts
286
+ .map(option => {
287
+ // 检查当前选项是否匹配
288
+ const currentMatch = filter(searchValue, option)
289
+
290
+ // 递归检查子选项
291
+ let filteredChildren: CascaderOption[] = []
292
+ if (option.children) {
293
+ filteredChildren = filterRecursively(option.children)
294
+ }
295
+
296
+ // 如果当前选项或任何子选项匹配,则包含此选项
297
+ if (currentMatch || filteredChildren.length > 0) {
298
+ return {
299
+ ...option,
300
+ children: filteredChildren.length > 0 ? filteredChildren : option.children
301
+ }
302
+ }
303
+
304
+ return null
305
+ })
306
+ .filter(Boolean) as CascaderOption[]
307
+ }
308
+
309
+ return filterRecursively(options)
310
+ }, [filterOption, defaultFilterOption])
311
+
312
+ // 获取当前级别的选项(支持搜索)
313
+ const getCurrentLevelOptions = useCallback((level: number): CascaderOption[] => {
314
+ let baseOptions = options
315
+ if (searchValue) {
316
+ baseOptions = filterOptions(options, searchValue)
317
+ }
318
+
319
+ if (level === 0) return baseOptions
320
+ if (currentActivePath.length >= level) {
321
+ return currentActivePath[level - 1]?.children || []
322
+ }
323
+ return []
324
+ }, [options, currentActivePath, searchValue, filterOptions])
325
+
326
+ // 处理键盘导航
327
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
328
+ if (!isOpen) return
329
+
330
+ const currentOptions = getCurrentLevelOptions(focusedLevel)
331
+
332
+ switch (event.key) {
333
+ case 'Escape':
334
+ setIsOpen(false)
335
+ setActivePath([])
336
+ setHoveredPath([])
337
+ setFocusedIndex(-1)
338
+ setFocusedLevel(0)
339
+ setSearchValue('')
340
+ break
341
+
342
+ case 'ArrowDown':
343
+ event.preventDefault()
344
+ if (currentOptions.length > 0) {
345
+ const nextIndex = focusedIndex < currentOptions.length - 1 ? focusedIndex + 1 : 0
346
+ setFocusedIndex(nextIndex)
347
+ // 模拟悬停效果
348
+ const option = currentOptions[nextIndex]
349
+ if (option && expandTrigger === 'hover') {
350
+ const path = focusedLevel === 0 ? [option] : [...currentActivePath.slice(0, focusedLevel), option]
351
+ setHoveredPath(path)
352
+ }
353
+ }
354
+ break
355
+
356
+ case 'ArrowUp':
357
+ event.preventDefault()
358
+ if (currentOptions.length > 0) {
359
+ const nextIndex = focusedIndex > 0 ? focusedIndex - 1 : currentOptions.length - 1
360
+ setFocusedIndex(nextIndex)
361
+ // 模拟悬停效果
362
+ const option = currentOptions[nextIndex]
363
+ if (option && expandTrigger === 'hover') {
364
+ const path = focusedLevel === 0 ? [option] : [...currentActivePath.slice(0, focusedLevel), option]
365
+ setHoveredPath(path)
366
+ }
367
+ }
368
+ break
369
+
370
+ case 'ArrowRight':
371
+ event.preventDefault()
372
+ if (focusedIndex >= 0 && currentOptions[focusedIndex]?.children) {
373
+ // 移动到子菜单
374
+ const option = currentOptions[focusedIndex]
375
+ const path = focusedLevel === 0 ? [option] : [...currentActivePath.slice(0, focusedLevel), option]
376
+ setActivePath(path)
377
+ setFocusedLevel(focusedLevel + 1)
378
+ setFocusedIndex(0)
379
+ }
380
+ break
381
+
382
+ case 'ArrowLeft':
383
+ event.preventDefault()
384
+ if (focusedLevel > 0) {
385
+ // 移动到父菜单
386
+ setActivePath(currentActivePath.slice(0, focusedLevel - 1))
387
+ setFocusedLevel(focusedLevel - 1)
388
+ setFocusedIndex(-1)
389
+ }
390
+ break
391
+
392
+ case 'Enter':
393
+ event.preventDefault()
394
+ if (focusedIndex >= 0 && currentOptions[focusedIndex]) {
395
+ const option = currentOptions[focusedIndex]
396
+ const path = focusedLevel === 0 ? [] : currentActivePath.slice(0, focusedLevel)
397
+ handleOptionClick(option, [...path, option])
398
+ }
399
+ break
400
+ }
401
+ }, [isOpen, focusedIndex, focusedLevel, getCurrentLevelOptions, expandTrigger, currentActivePath, handleOptionClick, setFocusedIndex, setFocusedLevel, setActivePath, setIsOpen, setHoveredPath, setSearchValue])
402
+
403
+ // 点击外部关闭下拉框(必须用容器 ref:面板与触发器是兄弟,不在 inputRef 内)
404
+ useEffect(() => {
405
+ const handleClickOutside = (event: MouseEvent) => {
406
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
407
+ setIsOpen(false)
408
+ setActivePath([])
409
+ setHoveredPath([])
410
+ setFocusedIndex(-1)
411
+ setFocusedLevel(0)
412
+ setSearchValue('')
413
+ }
414
+ }
415
+
416
+ if (isOpen) {
417
+ document.addEventListener('mousedown', handleClickOutside)
418
+ document.addEventListener('keydown', handleKeyDown)
419
+ return () => {
420
+ document.removeEventListener('mousedown', handleClickOutside)
421
+ document.removeEventListener('keydown', handleKeyDown)
422
+ }
423
+ }
424
+ }, [isOpen, handleKeyDown])
425
+
426
+ // 渲染菜单项
427
+ const renderMenuItem = useCallback((
428
+ option: CascaderOption,
429
+ path: CascaderOption[],
430
+ levelIndex: number,
431
+ itemIndex: number
432
+ ) => {
433
+ const isSelected = valueArray.includes(option.value)
434
+ const hasChildren = option.children && option.children.length > 0
435
+ const isInActivePath = activePath.some(activeOption => activeOption.value === option.value)
436
+ const isFocused = focusedLevel === levelIndex && focusedIndex === itemIndex
437
+
438
+ const itemClasses = classnames('sd-cascader__menu-item', {
439
+ 'is-active': isSelected,
440
+ 'is-disabled': option.disabled,
441
+ 'in-active-path': isInActivePath,
442
+ 'is-focused': isFocused
443
+ })
444
+
445
+ return (
446
+ <li
447
+ key={option.value}
448
+ className={itemClasses}
449
+ onClick={(e) => {
450
+ e.stopPropagation()
451
+ handleOptionClick(option, [...path, option])
452
+ }}
453
+ onMouseEnter={() => {
454
+ handleOptionHover(option, [...path, option])
455
+ // 清除键盘聚焦状态
456
+ setFocusedIndex(-1)
457
+ }}
458
+ >
459
+ {multiple && (
460
+ <div className="sd-cascader__menu-item-prefix">
461
+ <div
462
+ className={classnames('sd-cascader__menu-item-checkbox', {
463
+ 'is-checked': isSelected,
464
+ 'is-disabled': option.disableCheckbox || option.disabled
465
+ })}
466
+ >
467
+ {isSelected && <span></span>}
468
+ </div>
469
+ </div>
470
+ )}
471
+ <span className="sd-cascader__menu-item-label">{option.label}</span>
472
+ {hasChildren && (
473
+ <div className="sd-cascader__menu-item-postfix">
474
+ <span className="sd-cascader__menu-item-arrow">›</span>
475
+ </div>
476
+ )}
477
+ </li>
478
+ )
479
+ }, [valueArray, activePath, multiple, focusedLevel, focusedIndex, handleOptionClick, handleOptionHover])
480
+
481
+ // 渲染菜单
482
+ const renderMenu = useCallback((
483
+ options: CascaderOption[],
484
+ path: CascaderOption[] = [],
485
+ levelIndex: number = 0
486
+ ) => {
487
+ if (!options || options.length === 0) {
488
+ return (
489
+ <div className="sd-cascader__empty">
490
+ 无数据
491
+ </div>
492
+ )
493
+ }
494
+
495
+ return (
496
+ <div className="sd-cascader__menu">
497
+ <ul className="sd-cascader__menu-list">
498
+ {options.map((option, itemIndex) => renderMenuItem(option, path, levelIndex, itemIndex))}
499
+ </ul>
500
+ </div>
501
+ )
502
+ }, [renderMenuItem])
503
+
504
+
505
+ // CSS 类名
506
+ const containerClasses = classnames('sd-cascader', className)
507
+ const inputClasses = classnames('sd-cascader__input', {
508
+ 'is-disabled': disabled,
509
+ 'is-expanded': isOpen
510
+ })
511
+
512
+ // 合并容器 ref 与 forwarded ref,供点击外部检测与父组件使用
513
+ const setContainerRef = useCallback(
514
+ (el: HTMLDivElement | null) => {
515
+ (containerRef as MutableRefObject<HTMLDivElement | null>).current = el
516
+ if (typeof ref === 'function') ref(el)
517
+ else if (ref) (ref as MutableRefObject<HTMLDivElement | null>).current = el
518
+ },
519
+ [ref]
520
+ )
521
+
522
+ return (
523
+ <div
524
+ ref={setContainerRef}
525
+ className={containerClasses}
526
+ style={style}
527
+ {...rest}
528
+ >
529
+ <div
530
+ ref={inputRef}
531
+ className={inputClasses}
532
+ onClick={handleInputClick}
533
+ >
534
+ {multiple ? (
535
+ // 多选模式:显示标签
536
+ <>
537
+ {selectedItems.map(item => (
538
+ <span key={item.value} className="sd-cascader__tag">
539
+ <span className="sd-cascader__tag-content">{item.label}</span>
540
+ <span
541
+ className="sd-cascader__tag-close"
542
+ onClick={(e) => handleTagClose(item.value, e)}
543
+ >
544
+ ×
545
+ </span>
546
+ </span>
547
+ ))}
548
+ {selectedItems.length === 0 && (
549
+ <span className="sd-cascader__placeholder">{placeholder}</span>
550
+ )}
551
+ </>
552
+ ) : (
553
+ // 单选模式:显示文本
554
+ displayText ? (
555
+ <span className="sd-cascader__selected-text">{displayText}</span>
556
+ ) : (
557
+ <span className="sd-cascader__placeholder">{placeholder}</span>
558
+ )
559
+ )}
560
+
561
+ <div className="sd-cascader__suffix">
562
+ {clearable && valueArray.length > 0 && !disabled && (
563
+ <span className="sd-cascader__clear" onClick={handleClear}>
564
+ ×
565
+ </span>
566
+ )}
567
+ <span className={classnames('sd-cascader__arrow', { 'is-expanded': isOpen })}>
568
+
569
+ </span>
570
+ </div>
571
+ </div>
572
+
573
+ {/* 下拉面板 */}
574
+ {isOpen && (
575
+ <div
576
+ className={classnames('sd-cascader__panel', 'is-visible')}
577
+ style={{
578
+ position: 'absolute',
579
+ top: '100%',
580
+ left: 0,
581
+ right: 0,
582
+ zIndex: 9999
583
+ }}
584
+ >
585
+ {/* 搜索框 */}
586
+ {showSearch && (
587
+ <div className="sd-cascader__search">
588
+ <input
589
+ type="text"
590
+ className="sd-cascader__search-input"
591
+ placeholder={searchPlaceholder}
592
+ value={searchValue}
593
+ onChange={(e) => {
594
+ setSearchValue(e.target.value)
595
+ // 重置活动路径和聚焦状态
596
+ setActivePath([])
597
+ setFocusedIndex(-1)
598
+ setFocusedLevel(0)
599
+ }}
600
+ onKeyDown={(e) => {
601
+ // 阻止搜索框的键盘事件冒泡到全局处理
602
+ e.stopPropagation()
603
+ }}
604
+ />
605
+ </div>
606
+ )}
607
+
608
+ {/* 在搜索模式下,只显示第一级过滤结果 */}
609
+ {!searchValue ? (
610
+ <>
611
+ <div className="sd-cascader__menu">
612
+ {renderMenu(options, [], 0)}
613
+ </div>
614
+ {currentActivePath.map((activeOption, index) => {
615
+ const currentOptions = activeOption.children || []
616
+ const currentPath = currentActivePath.slice(0, index + 1)
617
+
618
+ if (currentOptions.length === 0) return null
619
+
620
+ return (
621
+ <div key={index + 1} className="sd-cascader__menu">
622
+ {renderMenu(currentOptions, currentPath, index + 1)}
623
+ </div>
624
+ )
625
+ })}
626
+ </>
627
+ ) : (
628
+ <div className="sd-cascader__menu">
629
+ {renderMenu(getCurrentLevelOptions(0), [], 0)}
630
+ </div>
631
+ )}
632
+ </div>
633
+ )}
634
+ </div>
635
+ )
636
+ })
637
+
638
+ Cascader.displayName = 'Cascader'