@gm-pc/react 1.27.4-beta.6 → 1.28.0-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gm-pc/react",
3
- "version": "1.27.4-beta.6",
3
+ "version": "1.28.0-beta.0",
4
4
  "description": "观麦前端基础组件库",
5
5
  "author": "liyatang <liyatang@qq.com>",
6
6
  "homepage": "https://github.com/gmfe/gm-pc#readme",
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@gm-common/hooks": "^2.10.0",
26
26
  "@gm-common/tool": "^2.10.0",
27
- "@gm-pc/locales": "^1.27.4-beta.6",
27
+ "@gm-pc/locales": "^1.28.0-beta.0",
28
28
  "big.js": "^6.0.1",
29
29
  "classnames": "^2.2.5",
30
30
  "lodash": "^4.17.19",
@@ -48,5 +48,5 @@
48
48
  "react-router-dom": "^5.2.0",
49
49
  "react-window": "^1.8.5"
50
50
  },
51
- "gitHead": "aaf6aeba25efc7f816af2b2b3c4e17c99b57a157"
51
+ "gitHead": "107461a0dd5c644e10482ed5f3ed8638fee436cc"
52
52
  }
@@ -2,7 +2,7 @@ import { HTMLAttributes, ReactNode } from 'react'
2
2
 
3
3
  interface LoadingProps {
4
4
  size?: string
5
- type: any
5
+ type?: any
6
6
  }
7
7
 
8
8
  interface LoadingChunkProps extends HTMLAttributes<HTMLDivElement> {
@@ -20,12 +20,16 @@ import { getLocale } from '@gm-pc/locales'
20
20
  import { ListBase } from '../list'
21
21
  import { findDOMNode } from 'react-dom'
22
22
  import { ConfigConsumer, ConfigProvider, ConfigProviderProps } from '../config_provider'
23
+ import { Checkbox, Switch } from '@gm-pc/react'
23
24
 
24
25
  interface MoreSelectBaseState {
25
26
  searchValue: string
26
27
  loading: boolean
27
28
  /* keyboard 默认第一个位置 */
28
29
  willActiveIndex: number | null
30
+ isCheckedAll: boolean
31
+ isFilterDelete: boolean
32
+ displayCount: number
29
33
  }
30
34
 
31
35
  // @todo keydown item disabled
@@ -41,6 +45,9 @@ class MoreSelectBase<V extends string | number = string> extends Component<
41
45
  searchValue: '',
42
46
  loading: false,
43
47
  willActiveIndex: this.props.isKeyboard ? 0 : null,
48
+ isCheckedAll: false,
49
+ isFilterDelete: false,
50
+ displayCount: 0,
44
51
  }
45
52
 
46
53
  private _isUnmounted = false
@@ -48,6 +55,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
48
55
  private _selectionRef = createRef<HTMLDivElement>()
49
56
  private _popoverRef = createRef<Popover>()
50
57
  private _inputRef = createRef<HTMLInputElement>()
58
+ private _resizeObserver: ResizeObserver | null = null
51
59
 
52
60
  private _filterData: MoreSelectGroupDataItem<V>[] | undefined
53
61
 
@@ -62,8 +70,44 @@ class MoreSelectBase<V extends string | number = string> extends Component<
62
70
  }
63
71
  }
64
72
 
73
+ componentDidMount() {
74
+ const { maxTagCount } = this.props
75
+ if (maxTagCount === 'responsive' && this._selectionRef.current) {
76
+ // HACK: 首次计算
77
+ setTimeout(() => {
78
+ if (this._selectionRef.current) {
79
+ const { width } = this._selectionRef.current.getBoundingClientRect()
80
+ const omittedTagWidth = 50 // for "+N..."
81
+ const availableWidth = width - omittedTagWidth
82
+ const newDisplayCount = Math.floor(availableWidth / 80)
83
+ if (this.state.displayCount !== newDisplayCount) {
84
+ this.setState({ displayCount: newDisplayCount > 0 ? newDisplayCount : 0 })
85
+ }
86
+ }
87
+ }, 0)
88
+
89
+ this._resizeObserver = new ResizeObserver((entries) => {
90
+ for (const entry of entries) {
91
+ const { width } = entry.contentRect
92
+ // Estimate item width, let's say 80px.
93
+ const omittedTagWidth = 50 // for "+N..."
94
+ const availableWidth = width - omittedTagWidth
95
+ const newDisplayCount = Math.floor(availableWidth / 80)
96
+
97
+ if (this.state.displayCount !== newDisplayCount) {
98
+ this.setState({ displayCount: newDisplayCount > 0 ? newDisplayCount : 0 })
99
+ }
100
+ }
101
+ })
102
+ this._resizeObserver.observe(this._selectionRef.current)
103
+ }
104
+ }
105
+
65
106
  componentWillUnmount() {
66
- this._isUnmounted = false
107
+ this._isUnmounted = true
108
+ if (this._resizeObserver) {
109
+ this._resizeObserver.disconnect()
110
+ }
67
111
  }
68
112
 
69
113
  public apiDoFocus = (): void => {
@@ -166,7 +210,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
166
210
  onSelect(willSelected)
167
211
  }
168
212
 
169
- private _handlePopupKeyDown = (event: KeyboardEvent): void => {
213
+ private _handlePopupKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
170
214
  const { onKeyDown } = this.props
171
215
  let willActiveIndex = this.state.willActiveIndex as number
172
216
  if (!onKeyDown) {
@@ -232,6 +276,84 @@ class MoreSelectBase<V extends string | number = string> extends Component<
232
276
  )
233
277
  }
234
278
 
279
+ renderBottom = () => {
280
+ const {
281
+ selected = [],
282
+ isShowDeletedSwitch = true,
283
+ isShowCheckedAll = true,
284
+ } = this.props
285
+ const { isCheckedAll, isFilterDelete } = this.state
286
+ const flatFilterData = this._getFlatFilterData()
287
+
288
+ // 根据过滤状态决定是否过滤已删除商品
289
+ const availableData = isFilterDelete
290
+ ? flatFilterData.filter((item) => !item.deleted)
291
+ : flatFilterData
292
+
293
+ // 检查是否所有可用数据都被选中
294
+ const allSelected =
295
+ availableData.length > 0 &&
296
+ availableData.every((item) =>
297
+ selected.some((selectedItem) => selectedItem.value === item.value)
298
+ )
299
+
300
+ return (
301
+ <Flex
302
+ justifyBetween
303
+ className='tw-p-[8px]'
304
+ alignCenter
305
+ style={{ borderTop: '1px solid #aeaeae' }}
306
+ >
307
+ {isShowCheckedAll && (
308
+ <Checkbox
309
+ checked={allSelected}
310
+ onChange={(e) => {
311
+ const isChecked = e.target.checked
312
+ this.setState({
313
+ isCheckedAll: isChecked,
314
+ })
315
+
316
+ if (isChecked) {
317
+ // 全选当前过滤后的可用数据
318
+ const valuesToSelect = availableData.map((item) => item.value)
319
+ this._handleSelect(valuesToSelect)
320
+ } else {
321
+ // 取消全选
322
+ this._handleSelect([])
323
+ }
324
+ }}
325
+ >
326
+ 全选
327
+ </Checkbox>
328
+ )}
329
+ {isShowDeletedSwitch && (
330
+ <Flex alignCenter>
331
+ <Flex row>
332
+ <Switch
333
+ style={{ width: 48 }}
334
+ checked={isFilterDelete}
335
+ onChange={(open) => {
336
+ this.setState({
337
+ isFilterDelete: open,
338
+ })
339
+ if (isCheckedAll) {
340
+ const newAvailableData = open
341
+ ? flatFilterData.filter((item) => !item.deleted)
342
+ : flatFilterData
343
+
344
+ const valuesToSelect = newAvailableData.map((item) => item.value)
345
+ this._handleSelect(valuesToSelect)
346
+ }
347
+ }}
348
+ />
349
+ </Flex>
350
+ <span className='gm-margin-left-5'>过滤已删除商品</span>
351
+ </Flex>
352
+ )}
353
+ </Flex>
354
+ )
355
+ }
356
+
235
357
  private _renderList = (config: ConfigProviderProps): ReactNode => {
236
358
  const {
237
359
  selected = [],
@@ -242,9 +364,21 @@ class MoreSelectBase<V extends string | number = string> extends Component<
242
364
  listHeight,
243
365
  popupClassName,
244
366
  renderCustomizedBottom,
367
+ isRenderDefaultBottom = false,
245
368
  } = this.props
246
- const { loading, searchValue, willActiveIndex } = this.state
247
- const filterData = this._getFilterData()
369
+ const { loading, searchValue, willActiveIndex, isFilterDelete } = this.state
370
+ let filterData = this._getFilterData()
371
+
372
+ // 如果开启了过滤已删除商品功能,需要过滤掉已删除的商品
373
+ if (isFilterDelete) {
374
+ filterData = filterData
375
+ .map((group) => ({
376
+ ...group,
377
+ children: group.children.filter((item) => !item.deleted),
378
+ }))
379
+ .filter((group) => group.children.length > 0)
380
+ }
381
+
248
382
  return (
249
383
  <ConfigProvider {...config}>
250
384
  <div
@@ -284,8 +418,11 @@ class MoreSelectBase<V extends string | number = string> extends Component<
284
418
  </div>
285
419
  {!loading &&
286
420
  !!filterData.length &&
287
- renderCustomizedBottom &&
288
- renderCustomizedBottom(this._popoverRef)}
421
+ (renderCustomizedBottom
422
+ ? renderCustomizedBottom(this._popoverRef, this.renderBottom)
423
+ : isRenderDefaultBottom
424
+ ? this.renderBottom()
425
+ : null)}
289
426
  </div>
290
427
  </ConfigProvider>
291
428
  )
@@ -325,7 +462,91 @@ class MoreSelectBase<V extends string | number = string> extends Component<
325
462
  style,
326
463
  popoverType,
327
464
  children,
465
+ maxTagCount,
466
+ maxTagPlaceholder,
467
+ isRenderDefaultBottom = false,
328
468
  } = this.props
469
+
470
+ // 处理 maxTagCount 逻辑
471
+ const renderSelectedItems = () => {
472
+ if (!multiple || !maxTagCount || selected.length === 0) {
473
+ return selected.map((item) => (
474
+ <Flex key={item.value as any} className='gm-more-select-selected-item'>
475
+ <Popover
476
+ disabled={!this.props.isKeyboard}
477
+ type='hover'
478
+ popup={<div className='gm-padding-10'>{item.text}</div>}
479
+ >
480
+ <Flex flex column>
481
+ {renderSelected!(item)}
482
+ </Flex>
483
+ </Popover>
484
+ {multiple ? (
485
+ <SVGRemove
486
+ className='gm-cursor gm-more-select-clear-btn'
487
+ onClick={disabled ? _.noop : this._handleClear.bind(this, item)}
488
+ />
489
+ ) : (
490
+ !disabledClose && ( // 是否不限时清除按钮,仅单选可用
491
+ <SVGCloseCircle
492
+ onClick={disabled ? _.noop : this._handleClear.bind(this, item)}
493
+ className='gm-cursor gm-more-select-clear-btn'
494
+ />
495
+ )
496
+ )}
497
+ </Flex>
498
+ ))
499
+ }
500
+
501
+ // 处理 maxTagCount 逻辑
502
+ const isResponsive = maxTagCount === 'responsive'
503
+ let displayCount: number
504
+
505
+ if (isResponsive) {
506
+ displayCount = this.state.displayCount
507
+ } else {
508
+ displayCount = maxTagCount as number
509
+ }
510
+
511
+ const itemsToShow = selected.slice(0, displayCount)
512
+ const omittedItems = selected.slice(displayCount)
513
+ const omittedCount = selected.length - displayCount
514
+
515
+ return (
516
+ <>
517
+ {itemsToShow.map((item) => (
518
+ <Flex key={item.value as any} className='gm-more-select-selected-item'>
519
+ <Popover
520
+ disabled={!this.props.isKeyboard}
521
+ type='hover'
522
+ popup={<div className='gm-padding-10'>{item.text}</div>}
523
+ >
524
+ <Flex flex column>
525
+ {renderSelected!(item)}
526
+ </Flex>
527
+ </Popover>
528
+ <SVGRemove
529
+ className='gm-cursor gm-more-select-clear-btn'
530
+ onClick={disabled ? _.noop : this._handleClear.bind(this, item)}
531
+ />
532
+ </Flex>
533
+ ))}
534
+ {omittedCount > 0 && (
535
+ <Flex
536
+ key='omitted'
537
+ className='gm-more-select-selected-item gm-more-select-omitted-item'
538
+ >
539
+ {maxTagPlaceholder ? (
540
+ maxTagPlaceholder(omittedItems, omittedCount)
541
+ ) : (
542
+ <span className='gm-more-select-omitted-count'>+{omittedCount}...</span>
543
+ )}
544
+ </Flex>
545
+ )}
546
+ </>
547
+ )
548
+ }
549
+
329
550
  return (
330
551
  <ConfigConsumer>
331
552
  {(config) => (
@@ -340,7 +561,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
340
561
  },
341
562
  className
342
563
  )}
343
- style={style}
564
+ style={style as any}
344
565
  >
345
566
  <Popover
346
567
  ref={this._popoverRef}
@@ -358,39 +579,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
358
579
  className='gm-more-select-selected'
359
580
  >
360
581
  {selected.length !== 0 ? (
361
- selected.map((item) => (
362
- <Flex
363
- key={item.value as any}
364
- className='gm-more-select-selected-item'
365
- >
366
- <Popover
367
- disabled={!this.props.isKeyboard}
368
- type='hover'
369
- popup={<div className='gm-padding-10'>{item.text}</div>}
370
- >
371
- <Flex flex column>
372
- {renderSelected!(item)}
373
- </Flex>
374
- </Popover>
375
- {multiple ? (
376
- <SVGRemove
377
- className='gm-cursor gm-more-select-clear-btn'
378
- onClick={
379
- disabled ? _.noop : this._handleClear.bind(this, item)
380
- }
381
- />
382
- ) : (
383
- !disabledClose && ( // 是否不限时清除按钮,仅单选可用
384
- <SVGCloseCircle
385
- onClick={
386
- disabled ? _.noop : this._handleClear.bind(this, item)
387
- }
388
- className='gm-cursor gm-more-select-clear-btn'
389
- />
390
- )
391
- )}
392
- </Flex>
393
- ))
582
+ renderSelectedItems()
394
583
  ) : (
395
584
  // 加多个 &nbsp; 避免对齐问题,有文本才有对齐
396
585
  <div className='gm-text-placeholder'>{placeholder}&nbsp; </div>
@@ -85,6 +85,8 @@ class MoreSelect<V = any> extends Component<MoreSelectProps<V>> {
85
85
  onSearch,
86
86
  onClick,
87
87
  renderListFilter,
88
+ maxTagCount,
89
+ maxTagPlaceholder,
88
90
  ...rest
89
91
  } = this.props
90
92
  let tempSelect = selected as MoreSelectDataItem<V>[]
@@ -129,6 +131,8 @@ class MoreSelect<V = any> extends Component<MoreSelectProps<V>> {
129
131
  isGroupList={isGroupList}
130
132
  onSearch={onSearch && this._handleSearch}
131
133
  renderListFilter={renderListFilter && this._renderListFilter}
134
+ maxTagCount={maxTagCount}
135
+ maxTagPlaceholder={maxTagPlaceholder}
132
136
  />
133
137
  )
134
138
  }
@@ -318,6 +318,98 @@ export const ComMoreSelectWithIsGroupListMultiple = () => (
318
318
  />
319
319
  )
320
320
 
321
+ export const ComMoreSelectWithMaxTagCount = () => {
322
+ // 创建一个包含多个选项的数据集
323
+ const manyOptions = [
324
+ { value: 1, text: '选项1' },
325
+ { value: 2, text: '选项2' },
326
+ { value: 3, text: '选项3' },
327
+ { value: 4, text: '选项4' },
328
+ { value: 5, text: '选项5' },
329
+ { value: 6, text: '选项6' },
330
+ { value: 7, text: '选项7' },
331
+ { value: 8, text: '选项8' },
332
+ ]
333
+
334
+ // 预选多个选项
335
+ const preSelected = manyOptions.slice(0, 6)
336
+
337
+ return (
338
+ <div style={{ width: '300px' }}>
339
+ <h3>maxTagCount 示例</h3>
340
+
341
+ <div style={{ marginBottom: '20px' }}>
342
+ <h4>不使用 maxTagCount(显示所有选项)</h4>
343
+ <MoreSelect<number>
344
+ multiple
345
+ data={manyOptions}
346
+ selected={preSelected}
347
+ onSelect={(selected) => {
348
+ console.log('不限制显示数量:', selected)
349
+ }}
350
+ />
351
+ </div>
352
+
353
+ <div style={{ marginBottom: '20px' }}>
354
+ <h4>maxTagCount={2}(最多显示2个选项)</h4>
355
+ <MoreSelect<number>
356
+ multiple
357
+ maxTagCount={2}
358
+ data={manyOptions}
359
+ selected={preSelected}
360
+ onSelect={(selected) => {
361
+ console.log('最多显示2个:', selected)
362
+ }}
363
+ />
364
+ </div>
365
+
366
+ <div style={{ marginBottom: '20px' }}>
367
+ <h4>maxTagCount={3}(最多显示3个选项)</h4>
368
+ <MoreSelect<number>
369
+ multiple
370
+ maxTagCount={3}
371
+ data={manyOptions}
372
+ selected={preSelected}
373
+ onSelect={(selected) => {
374
+ console.log('最多显示3个:', selected)
375
+ }}
376
+ />
377
+ </div>
378
+
379
+ <div style={{ marginBottom: '20px' }}>
380
+ <h4>maxTagCount=responsive(响应式模式)</h4>
381
+ <MoreSelect<number>
382
+ multiple
383
+ maxTagCount='responsive'
384
+ data={manyOptions}
385
+ selected={preSelected}
386
+ onSelect={(selected) => {
387
+ console.log('响应式模式:', selected)
388
+ }}
389
+ />
390
+ </div>
391
+
392
+ <div style={{ marginBottom: '20px' }}>
393
+ <h4>自定义 maxTagPlaceholder</h4>
394
+ <MoreSelect<number>
395
+ multiple
396
+ maxTagCount={2}
397
+ maxTagPlaceholder={(omittedValues, omittedCount) => (
398
+ <span style={{ color: '#1890ff', fontWeight: 'bold' }}>
399
+ 还有{omittedCount}项未显示
400
+ </span>
401
+ )}
402
+ data={manyOptions}
403
+ selected={preSelected}
404
+ onSelect={(selected) => {
405
+ console.log('自定义占位符:', selected)
406
+ }}
407
+ />
408
+ </div>
409
+ </div>
410
+ )
411
+ }
412
+
321
413
  export default {
322
414
  title: '表单/MoreSelect',
323
415
  }
@@ -1,11 +1,13 @@
1
- import { Popover } from '@gm-pc/react'
2
1
  import { CSSProperties, ReactNode, KeyboardEvent } from 'react'
2
+ import { Popover } from '../popover'
3
3
 
4
4
  /** 普通的数据格式 */
5
5
  interface MoreSelectDataItem<V extends string | number = string> {
6
6
  value: V
7
7
  text: string
8
8
  disabled?: boolean
9
+ /** 是否已删除 */
10
+ deleted?: boolean
9
11
  [key: string]: any
10
12
  }
11
13
 
@@ -36,10 +38,13 @@ interface MoreSelectCommonProps<V extends string | number = string> {
36
38
  renderListItem?(value: MoreSelectDataItem<V>, index: number): ReactNode
37
39
 
38
40
  /** 自定义popup底部渲染 */
39
- renderCustomizedBottom?(ref: React.RefObject<Popover>): ReactNode
41
+ renderCustomizedBottom?(
42
+ ref: React.RefObject<Popover>,
43
+ defaultBottom: () => ReactNode
44
+ ): ReactNode
40
45
 
41
46
  /**
42
- * 自定义“空状态”渲染
47
+ * 自定义"空状态"渲染
43
48
  *
44
49
  * 若函数返回 undefined 则使用默认的空状态
45
50
  */
@@ -60,6 +65,14 @@ interface MoreSelectCommonProps<V extends string | number = string> {
60
65
  /** 目前为了 keyboard */
61
66
  isKeyboard?: boolean
62
67
  onKeyDown?(event: KeyboardEvent): void
68
+
69
+ /** 最多显示的选中项数量,超出部分会折叠 */
70
+ maxTagCount?: number | 'responsive'
71
+ /** 自定义超出 maxTagCount 时显示的内容 */
72
+ maxTagPlaceholder?: (
73
+ omittedValues: MoreSelectDataItem<V>[],
74
+ omittedCount: number
75
+ ) => ReactNode
63
76
  }
64
77
 
65
78
  interface MoreSelectBaseProps<V extends string | number = string>
@@ -80,6 +93,12 @@ interface MoreSelectBaseProps<V extends string | number = string>
80
93
  ): MoreSelectGroupDataItem<V>[]
81
94
  /** 是否在active的时候搜索,订单业务相关,searchValue放在localstorage */
82
95
  searchOnActive?: boolean
96
+ /** 是否展示全选以及过滤已删除商品 */
97
+ isRenderDefaultBottom?: boolean
98
+ /** 是否展示已删除商品 */
99
+ isShowDeletedSwitch?: boolean
100
+ /** 是否展示全选 */
101
+ isShowCheckedAll?: boolean
83
102
  }
84
103
 
85
104
  type MoreSelectData<V extends string | number = string> =