@gm-pc/react 1.29.0-beta.2 → 1.30.0-beta.4

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.29.0-beta.2",
3
+ "version": "1.30.0-beta.4",
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.29.0-beta.2",
27
+ "@gm-pc/locales": "^1.30.0-beta.4",
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": "71157a29a2684a64359ea15e50eb43d64cee12f5"
51
+ "gitHead": "5b1cebbcf101effbeaf2c4f9a49489b59cc4a1c3"
52
52
  }
@@ -4,12 +4,14 @@ import { xor, flatMap, isNil, noop } from 'lodash'
4
4
  import { ListBaseProps } from './types'
5
5
  import { ListDataItem } from '../../types'
6
6
  import { ConfigConsumer } from '../config_provider'
7
+ import SVGOk from '../../svg/ok.svg'
7
8
 
8
9
  class Base<V = any> extends Component<ListBaseProps<V>> {
9
10
  static defaultProps = {
10
11
  onSelect: noop,
11
12
  renderItem: (value: any) => value.text,
12
13
  getItemProps: () => ({}),
14
+ showSelectedIcon: true,
13
15
  }
14
16
 
15
17
  private _listRef = createRef<HTMLDivElement>()
@@ -61,9 +63,9 @@ class Base<V = any> extends Component<ListBaseProps<V>> {
61
63
  }
62
64
  const { multiple, selected, onSelect } = this.props
63
65
  if (multiple) {
64
- onSelect && onSelect(xor(selected, [item.value]))
66
+ onSelect && onSelect(xor(selected, [item.value]), item)
65
67
  } else {
66
- onSelect && onSelect([item.value])
68
+ onSelect && onSelect([item.value], item)
67
69
  }
68
70
  }
69
71
 
@@ -79,6 +81,7 @@ class Base<V = any> extends Component<ListBaseProps<V>> {
79
81
  className,
80
82
  willActiveIndex,
81
83
  getItemProps,
84
+ showSelectedIcon,
82
85
  ...rest
83
86
  } = this.props
84
87
 
@@ -115,6 +118,9 @@ class Base<V = any> extends Component<ListBaseProps<V>> {
115
118
  onClick={() => this._handleSelect(item)}
116
119
  >
117
120
  {renderItem!(item, index)}
121
+ {multiple && showSelectedIcon && selected.includes(item.value) && (
122
+ <SVGOk />
123
+ )}
118
124
  </div>
119
125
  )
120
126
  })}
@@ -15,6 +15,8 @@
15
15
 
16
16
  &.active {
17
17
  color: var(--gm-color-primary-active);
18
+ display: flex;
19
+ justify-content: space-between;
18
20
  }
19
21
 
20
22
  .gmDisabledWith();
@@ -12,12 +12,14 @@ interface CommonListProps<V> {
12
12
  getItemProps?(item: ListDataItem<V>): HTMLAttributes<HTMLDivElement>
13
13
  className?: string
14
14
  style?: CSSProperties
15
+ /** 是否显示选中状态的图标,默认为 true */
16
+ showSelectedIcon?: boolean
15
17
  }
16
18
 
17
19
  interface ListBaseProps<V> extends CommonListProps<V> {
18
20
  data: ListGroupDataItem<V>[]
19
21
  selected: V[]
20
- onSelect?(selected: V[]): void
22
+ onSelect?(selected: V[], item: ListDataItem<V>): void
21
23
  }
22
24
 
23
25
  interface ListProps<V> extends CommonListProps<V> {
@@ -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,20 @@ 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
-
23
+ import { Checkbox } from '../checkbox'
24
+ import { Switch } from '../switch'
24
25
  interface MoreSelectBaseState {
26
+ canClear?: boolean
25
27
  searchValue: string
26
28
  loading: boolean
27
29
  /* keyboard 默认第一个位置 */
28
30
  willActiveIndex: number | null
31
+ isCheckedAll: boolean
32
+ isFilterDelete: boolean
33
+ displayCount: number
34
+ // isPopupJustOpened: boolean
35
+ previousSelected?: MoreSelectDataItem<V>[]
36
+ previousCurrentSelected: MoreSelectDataItem<V>[]
29
37
  }
30
38
 
31
39
  // @todo keydown item disabled
@@ -41,6 +49,11 @@ class MoreSelectBase<V extends string | number = string> extends Component<
41
49
  searchValue: '',
42
50
  loading: false,
43
51
  willActiveIndex: this.props.isKeyboard ? 0 : null,
52
+ isCheckedAll: false,
53
+ isFilterDelete: true,
54
+ displayCount: 0,
55
+ previousSelected: [],
56
+ previousCurrentSelected: [],
44
57
  }
45
58
 
46
59
  private _isUnmounted = false
@@ -48,6 +61,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
48
61
  private _selectionRef = createRef<HTMLDivElement>()
49
62
  private _popoverRef = createRef<Popover>()
50
63
  private _inputRef = createRef<HTMLInputElement>()
64
+ private _resizeObserver: ResizeObserver | null = null
51
65
 
52
66
  private _filterData: MoreSelectGroupDataItem<V>[] | undefined
53
67
 
@@ -62,8 +76,42 @@ class MoreSelectBase<V extends string | number = string> extends Component<
62
76
  }
63
77
  }
64
78
 
79
+ componentDidMount() {
80
+ const { maxTagCount, tagItemWidth = 80, omittedTagWidth = 45 } = this.props
81
+ if (maxTagCount === 'responsive' && this._selectionRef.current) {
82
+ // HACK: 首次计算
83
+ setTimeout(() => {
84
+ if (this._selectionRef.current) {
85
+ const { width } = this._selectionRef.current.getBoundingClientRect()
86
+ const availableWidth = width - omittedTagWidth
87
+ const newDisplayCount = Math.floor(availableWidth / tagItemWidth)
88
+ if (this.state.displayCount !== newDisplayCount) {
89
+ this.setState({ displayCount: newDisplayCount > 0 ? newDisplayCount : 0 })
90
+ }
91
+ }
92
+ }, 0)
93
+
94
+ this._resizeObserver = new ResizeObserver((entries) => {
95
+ for (const entry of entries) {
96
+ const { width } = entry.contentRect
97
+ // Estimate item width, let's say 80px.
98
+ const availableWidth = width - omittedTagWidth
99
+ const newDisplayCount = Math.floor(availableWidth / tagItemWidth)
100
+
101
+ if (this.state.displayCount !== newDisplayCount) {
102
+ this.setState({ displayCount: newDisplayCount })
103
+ }
104
+ }
105
+ })
106
+ this._resizeObserver.observe(this._selectionRef.current)
107
+ }
108
+ }
109
+
65
110
  componentWillUnmount() {
66
- this._isUnmounted = false
111
+ this._isUnmounted = true
112
+ if (this._resizeObserver) {
113
+ this._resizeObserver.disconnect()
114
+ }
67
115
  }
68
116
 
69
117
  public apiDoFocus = (): void => {
@@ -104,6 +152,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
104
152
  }
105
153
  })
106
154
  })
155
+ console.log(data, selected)
107
156
  selected.forEach((item) => {
108
157
  let flag = true // 判断当前已选择的选项中是否存在不在当前data里面的,解决onSearch异步,true则表示都不在data里面
109
158
  data.forEach((group) => {
@@ -113,6 +162,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
113
162
  items.push(item)
114
163
  }
115
164
  })
165
+ console.log(items)
116
166
  onSelect(items)
117
167
 
118
168
  if (!multiple) {
@@ -130,8 +180,18 @@ class MoreSelectBase<V extends string | number = string> extends Component<
130
180
  event: ChangeEvent<HTMLInputElement>,
131
181
  isInitSearch?: boolean
132
182
  ): void => {
183
+ const { onSearch } = this.props
133
184
  const searchValue = event.target.value
134
185
  this.setState({ searchValue })
186
+ this.setState({
187
+ previousSelected: this.props.selected,
188
+ previousCurrentSelected: this.props.selected,
189
+ })
190
+ if (onSearch && !this._isUnmounted) {
191
+ this.setState({
192
+ loading: true,
193
+ })
194
+ }
135
195
  this._debounceDoSearch(searchValue)
136
196
  setTimeout(() => {
137
197
  // eslint-disable-next-line no-unused-expressions
@@ -145,28 +205,37 @@ class MoreSelectBase<V extends string | number = string> extends Component<
145
205
  private _doSearch = (query: string): void => {
146
206
  const { onSearch, data = [] } = this.props
147
207
  if (!this._isUnmounted && onSearch) {
148
- const result = onSearch(query, data)
149
- if (!result) {
150
- return
151
- }
152
208
  this.setState({ loading: true })
153
-
209
+ const result = onSearch(query, data)
154
210
  Promise.resolve(result).finally(() => {
155
- this.setState({ loading: false })
211
+ setTimeout(() => {
212
+ this.setState({ loading: false })
213
+ }, 50)
156
214
  })
157
215
  }
158
216
  }
159
217
 
160
218
  private _debounceDoSearch = _.debounce(this._doSearch, this.props.delay)
161
219
 
162
- private _handleClear = (clearItem: MoreSelectDataItem<V>, event: MouseEvent): void => {
163
- event.stopPropagation()
220
+ private _handleClear = (clearItem: MoreSelectDataItem<V>, event?: MouseEvent): void => {
221
+ if (event) {
222
+ event.stopPropagation()
223
+ }
164
224
  const { onSelect = _.noop, selected = [] } = this.props
165
225
  const willSelected = selected.filter((item) => item.value !== clearItem.value)
166
226
  onSelect(willSelected)
167
227
  }
168
228
 
169
- private _handlePopupKeyDown = (event: KeyboardEvent): void => {
229
+ private _handleClearAll = (event: MouseEvent): void => {
230
+ event.stopPropagation()
231
+ const { onSelect = _.noop } = this.props
232
+ onSelect([])
233
+ this.setState({
234
+ previousCurrentSelected: [],
235
+ })
236
+ }
237
+
238
+ private _handlePopupKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
170
239
  const { onKeyDown } = this.props
171
240
  let willActiveIndex = this.state.willActiveIndex as number
172
241
  if (!onKeyDown) {
@@ -201,9 +270,9 @@ class MoreSelectBase<V extends string | number = string> extends Component<
201
270
  }
202
271
 
203
272
  private _getFilterData = () => {
204
- const { data = [], renderListFilter, renderListFilterType } = this.props
273
+ const { data = [], renderListFilter, renderListFilterType, onSearch } = this.props
205
274
  const { searchValue } = this.state
206
- let filterData: MoreSelectGroupDataItem<V>[]
275
+ let filterData: MoreSelectGroupDataItem<V>[] = []
207
276
  if (renderListFilter) {
208
277
  filterData = renderListFilter(data, searchValue)
209
278
  } else if (renderListFilterType === 'pinyin') {
@@ -232,19 +301,267 @@ class MoreSelectBase<V extends string | number = string> extends Component<
232
301
  )
233
302
  }
234
303
 
235
- private _renderList = (config: ConfigProviderProps): ReactNode => {
304
+ renderBottom = () => {
236
305
  const {
237
306
  selected = [],
238
- multiple,
239
- isGroupList,
240
- renderListItem,
307
+ isShowDeletedSwitch = true,
308
+ isShowCheckedAll = true,
309
+ } = this.props
310
+ const { isCheckedAll, isFilterDelete } = this.state
311
+ const flatFilterData = this._getFlatFilterData()
312
+
313
+ // 根据过滤状态决定是否过滤已删除商品
314
+ const availableData = isFilterDelete
315
+ ? flatFilterData.filter((item) => !item.deleted)
316
+ : flatFilterData
317
+
318
+ // 检查是否所有可用数据都被选中
319
+ const allSelected =
320
+ availableData.length > 0 &&
321
+ availableData.every((item) =>
322
+ selected.some((selectedItem) => selectedItem.value === item.value)
323
+ )
324
+
325
+ return (
326
+ <Flex
327
+ justifyBetween
328
+ className='tw-p-[8px] gm-more-select-default-bottom'
329
+ alignCenter
330
+ >
331
+ {isShowCheckedAll && (
332
+ <Checkbox
333
+ checked={allSelected}
334
+ onChange={(e) => {
335
+ const isChecked = e.target.checked
336
+ this.setState({
337
+ isCheckedAll: isChecked,
338
+ })
339
+
340
+ if (isChecked) {
341
+ // 全选当前过滤后的可用数据
342
+ const valuesToSelect = availableData.map((item) => item.value)
343
+ const prevSelectedValue = selected.map((item) => item.value)
344
+ // console.log(valuesToSelect, selected)
345
+ const newSelected = Array.from(
346
+ new Set([...prevSelectedValue, ...valuesToSelect])
347
+ )
348
+ this._handleSelect(newSelected)
349
+ // 更新 previousCurrentSelected
350
+ this.setState({
351
+ previousCurrentSelected: Array.from(
352
+ new Set([...this.state.previousCurrentSelected, ...availableData])
353
+ ),
354
+ })
355
+ } else {
356
+ // 取消全选 - 只反勾选availableData的数据
357
+ const { selected = [] } = this.props
358
+ const availableValues = availableData.map((item) => item.value)
359
+ const newSelected = selected.filter(
360
+ (item) => !availableValues.includes(item.value)
361
+ )
362
+ this._handleSelect(newSelected.map((item) => item.value))
363
+ // 更新 previousCurrentSelected
364
+ this.setState({
365
+ previousCurrentSelected: this.state.previousCurrentSelected.filter(
366
+ (item) => !availableValues.includes(item.value)
367
+ ),
368
+ })
369
+ }
370
+ }}
371
+ >
372
+ 全选({availableData.length})
373
+ </Checkbox>
374
+ )}
375
+ {isShowDeletedSwitch && (
376
+ <Flex alignCenter>
377
+ <Flex row>
378
+ <Switch
379
+ size='small'
380
+ style={{ width: 32 }}
381
+ checked={isFilterDelete}
382
+ onChange={(open) => {
383
+ this.setState({
384
+ isFilterDelete: open,
385
+ })
386
+ // if (isCheckedAll) {
387
+ // const newAvailableData = open
388
+ // ? flatFilterData.filter((item) => !item.deleted)
389
+ // : flatFilterData
390
+
391
+ // const valuesToSelect = newAvailableData.map((item) => item.value)
392
+ // this._handleSelect(valuesToSelect)
393
+ // }
394
+ }}
395
+ />
396
+ </Flex>
397
+ <span className='gm-margin-left-5'>过滤已删除数据</span>
398
+ </Flex>
399
+ )}
400
+ </Flex>
401
+ )
402
+ }
403
+
404
+ renderContent = () => {
405
+ const { loading, willActiveIndex, isFilterDelete } = this.state
406
+
407
+ if (loading) {
408
+ return (
409
+ <Flex alignCenter justifyCenter className='gm-bg gm-padding-5'>
410
+ <Loading size='20px' />
411
+ </Flex>
412
+ )
413
+ }
414
+
415
+ let filterData = this._getFilterData()
416
+
417
+ // 如果开启了过滤已删除商品功能,需要过滤掉已删除的商品
418
+ if (isFilterDelete) {
419
+ filterData = filterData
420
+ .map((group) => ({
421
+ ...group,
422
+ children: group.children.filter((item) => !item.deleted),
423
+ }))
424
+ .filter((group) => group.children.length > 0)
425
+ }
426
+
427
+ if (!loading && filterData.length === 0) {
428
+ return this._renderEmpty()
429
+ }
430
+
431
+ if (!loading && filterData.length > 0) {
432
+ const {
433
+ selected = [],
434
+ multiple,
435
+ isGroupList,
436
+ renderListItem,
437
+ listHeight,
438
+ showSelectedIcon,
439
+ } = this.props
440
+ const { previousSelected = [], previousCurrentSelected = [] } = this.state
441
+
442
+ const selectedValues = new Set(previousSelected?.map((v) => v.value))
443
+
444
+ // 分离已勾选和未勾选的数据
445
+ const availableGroups: MoreSelectGroupDataItem<V>[] = []
446
+ // 已选中区域直接使用 selected 构建,不受筛选影响
447
+ const selectedGroups: MoreSelectGroupDataItem<V>[] = []
448
+
449
+ if (multiple) {
450
+ filterData.forEach((group) => {
451
+ const availableChildren = group.children.filter((item) => {
452
+ // return true
453
+ return !selectedValues.has(item.value)
454
+ })
455
+
456
+ if (availableChildren.length > 0) {
457
+ availableGroups.push({
458
+ ...group,
459
+ children: availableChildren,
460
+ })
461
+ }
462
+ })
463
+
464
+ if (previousSelected.length > 0) {
465
+ selectedGroups.push({
466
+ label: '',
467
+ children: previousSelected,
468
+ })
469
+ }
470
+ }
471
+
472
+ return (
473
+ <div style={{ height: listHeight, overflow: 'auto' }}>
474
+ {previousSelected.length > 0 && multiple && !this.state.searchValue && (
475
+ <>
476
+ <div className='gm-more-select-section-title gm-padding-5 gm-text-desc gm-text-12'>
477
+ 已选中
478
+ </div>
479
+ <ListBase
480
+ showSelectedIcon={showSelectedIcon}
481
+ selected={previousCurrentSelected.map((v) => v.value)}
482
+ data={selectedGroups}
483
+ multiple={multiple}
484
+ isGroupList={false}
485
+ className='gm-border-0'
486
+ renderItem={renderListItem}
487
+ onSelect={(v, target) => {
488
+ // 判断是勾选还是反选:如果 v 中包含 target.value,说明是勾选操作;否则是反选操作
489
+ const isChecked = v.includes(target.value)
490
+ if (isChecked) {
491
+ // 勾选:添加到 previousCurrentSelected
492
+ this.setState({
493
+ previousCurrentSelected: [
494
+ ...this.state.previousCurrentSelected,
495
+ target,
496
+ ],
497
+ })
498
+ const newSelected = [...selected, target]
499
+
500
+ this.props.onSelect(newSelected)
501
+ } else {
502
+ // 反选:从 previousCurrentSelected 中移除
503
+ this.setState({
504
+ previousCurrentSelected: this.state.previousCurrentSelected?.filter(
505
+ (item) => item.value !== target.value
506
+ ),
507
+ })
508
+ const newSelected = selected.filter(
509
+ (item) => item.value !== target.value
510
+ )
511
+
512
+ this.props.onSelect(newSelected)
513
+ }
514
+ }}
515
+ isScrollTo={false}
516
+ />
517
+ </>
518
+ )}
519
+ <>
520
+ {multiple && !this.state.searchValue && (
521
+ <div className='gm-more-select-section-title gm-padding-5 gm-text-desc gm-text-12'>
522
+ 未选中
523
+ </div>
524
+ )}
525
+
526
+ <ListBase
527
+ showSelectedIcon={showSelectedIcon}
528
+ selected={selected?.map((v) => v.value)}
529
+ data={multiple && !this.state.searchValue ? availableGroups : filterData}
530
+ multiple={multiple}
531
+ isGroupList={isGroupList}
532
+ className='gm-border-0'
533
+ renderItem={renderListItem}
534
+ onSelect={this._handleSelect}
535
+ isScrollTo
536
+ willActiveIndex={willActiveIndex!}
537
+ />
538
+ </>
539
+ </div>
540
+ )
541
+ }
542
+ }
543
+
544
+ private _renderList = (config: ConfigProviderProps): ReactNode => {
545
+ const {
241
546
  searchPlaceholder,
242
547
  listHeight,
243
548
  popupClassName,
244
549
  renderCustomizedBottom,
550
+ isRenderDefaultBottom = false,
245
551
  } = this.props
246
- const { loading, searchValue, willActiveIndex } = this.state
247
- const filterData = this._getFilterData()
552
+ const { loading, searchValue, willActiveIndex, isFilterDelete } = this.state
553
+ let filterData = this._getFilterData()
554
+
555
+ // 如果开启了过滤已删除商品功能,需要过滤掉已删除的商品
556
+ if (isFilterDelete) {
557
+ filterData = filterData
558
+ .map((group) => ({
559
+ ...group,
560
+ children: group.children.filter((item) => !item.deleted),
561
+ }))
562
+ .filter((group) => group.children.length > 0)
563
+ }
564
+
248
565
  return (
249
566
  <ConfigProvider {...config}>
250
567
  <div
@@ -260,32 +577,14 @@ class MoreSelectBase<V extends string | number = string> extends Component<
260
577
  placeholder={searchPlaceholder}
261
578
  />
262
579
  </div>
263
- <div style={{ height: listHeight }}>
264
- {loading && (
265
- <Flex alignCenter justifyCenter className='gm-bg gm-padding-5'>
266
- <Loading size='20px' />
267
- </Flex>
268
- )}
269
- {!loading && !filterData.length && this._renderEmpty()}
270
- {!loading && !!filterData.length && (
271
- <ListBase
272
- selected={selected.map((v) => v.value)}
273
- data={filterData}
274
- multiple={multiple}
275
- isGroupList={isGroupList}
276
- className='gm-border-0'
277
- renderItem={renderListItem}
278
- onSelect={this._handleSelect}
279
- isScrollTo
280
- willActiveIndex={willActiveIndex!}
281
- style={{ height: listHeight }}
282
- />
283
- )}
284
- </div>
580
+ <div style={{ height: listHeight }}>{this.renderContent()}</div>
285
581
  {!loading &&
286
582
  !!filterData.length &&
287
- renderCustomizedBottom &&
288
- renderCustomizedBottom(this._popoverRef)}
583
+ (renderCustomizedBottom
584
+ ? renderCustomizedBottom(this._popoverRef, this.renderBottom)
585
+ : isRenderDefaultBottom
586
+ ? this.renderBottom()
587
+ : null)}
289
588
  </div>
290
589
  </ConfigProvider>
291
590
  )
@@ -299,15 +598,21 @@ class MoreSelectBase<V extends string | number = string> extends Component<
299
598
  }
300
599
 
301
600
  private _handlePopoverVisibleChange = (active: boolean) => {
302
- if (active && this.props.searchOnActive) {
303
- const searchValue = localStorage.getItem('_GM-PC_MORESELECT_SEARCHVALUE')
304
- if (searchValue) {
305
- this.setState({ searchValue })
306
- setTimeout(() => {
307
- // eslint-disable-next-line no-unused-expressions
308
- this._inputRef.current?.select()
309
- this._debounceDoSearch(searchValue)
310
- }, 0)
601
+ if (active) {
602
+ this.setState({
603
+ previousSelected: this.props.selected,
604
+ previousCurrentSelected: this.props.selected,
605
+ })
606
+ if (this.props.searchOnActive) {
607
+ const searchValue = localStorage.getItem('_GM-PC_MORESELECT_SEARCHVALUE')
608
+ if (searchValue) {
609
+ this.setState({ searchValue })
610
+ setTimeout(() => {
611
+ // eslint-disable-next-line no-unused-expressions
612
+ this._inputRef.current?.select()
613
+ this._debounceDoSearch(searchValue)
614
+ }, 0)
615
+ }
311
616
  }
312
617
  }
313
618
  }
@@ -325,7 +630,94 @@ class MoreSelectBase<V extends string | number = string> extends Component<
325
630
  style,
326
631
  popoverType,
327
632
  children,
633
+ maxTagCount,
634
+ maxTagPlaceholder,
328
635
  } = this.props
636
+
637
+ // 处理 maxTagCount 逻辑
638
+ const renderSelectedItems = () => {
639
+ if (!multiple || !maxTagCount || selected.length === 0) {
640
+ return selected.map((item) => (
641
+ <Flex key={item.value as any} className='gm-more-select-selected-item'>
642
+ <Popover
643
+ disabled={!this.props.isKeyboard}
644
+ type='hover'
645
+ popup={<div className='gm-padding-10'>{item.text}</div>}
646
+ >
647
+ <Flex flex column>
648
+ {renderSelected!(item)}
649
+ </Flex>
650
+ </Popover>
651
+ {multiple ? (
652
+ <SVGRemove
653
+ className='gm-cursor gm-more-select-clear-btn'
654
+ onClick={disabled ? _.noop : this._handleClear.bind(this, item)}
655
+ />
656
+ ) : (
657
+ !disabledClose && ( // 是否不限时清除按钮,仅单选可用
658
+ <SVGCloseCircle
659
+ onClick={disabled ? _.noop : this._handleClear.bind(this, item)}
660
+ className='gm-cursor gm-more-select-clear-btn'
661
+ />
662
+ )
663
+ )}
664
+ </Flex>
665
+ ))
666
+ }
667
+
668
+ // 处理 maxTagCount 逻辑
669
+ const isResponsive = maxTagCount === 'responsive'
670
+ let displayCount: number
671
+
672
+ if (isResponsive) {
673
+ displayCount = this.state.displayCount
674
+ } else {
675
+ displayCount = maxTagCount as number
676
+ }
677
+
678
+ const itemsToShow = selected.slice(0, displayCount)
679
+ const omittedItems = selected.slice(displayCount)
680
+ const omittedCount = selected.length - displayCount
681
+
682
+ return (
683
+ <>
684
+ {itemsToShow.map((item) => (
685
+ <Flex key={item.value as any} className='gm-more-select-selected-item'>
686
+ <Popover
687
+ disabled={!this.props.isKeyboard}
688
+ type='hover'
689
+ popup={<div className='gm-padding-10'>{item.text}</div>}
690
+ >
691
+ <Flex flex column>
692
+ {renderSelected!(item)}
693
+ </Flex>
694
+ </Popover>
695
+ <SVGRemove
696
+ className='gm-cursor gm-more-select-clear-btn'
697
+ onClick={disabled ? _.noop : this._handleClear.bind(this, item)}
698
+ />
699
+ </Flex>
700
+ ))}
701
+ {omittedCount > 0 && (
702
+ <Flex
703
+ key='omitted'
704
+ className='gm-more-select-selected-item gm-more-select-omitted-item'
705
+ >
706
+ {maxTagPlaceholder ? (
707
+ maxTagPlaceholder(omittedItems, omittedCount)
708
+ ) : (
709
+ <span className='gm-more-select-omitted-count'>+{omittedCount}...</span>
710
+ )}
711
+ </Flex>
712
+ )}
713
+ <SVGRemove
714
+ className='gm-cursor gm-more-select-clear-btn'
715
+ onClick={disabled ? _.noop : this._handleClearAll}
716
+ />
717
+ </>
718
+ )
719
+ }
720
+
329
721
  return (
330
722
  <ConfigConsumer>
331
723
  {(config) => (
@@ -340,7 +732,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
340
732
  },
341
733
  className
342
734
  )}
343
- style={style}
735
+ style={style as any}
344
736
  >
345
737
  <Popover
346
738
  ref={this._popoverRef}
@@ -358,39 +750,7 @@ class MoreSelectBase<V extends string | number = string> extends Component<
358
750
  className='gm-more-select-selected'
359
751
  >
360
752
  {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
- ))
753
+ renderSelectedItems()
394
754
  ) : (
395
755
  // 加多个 &nbsp; 避免对齐问题,有文本才有对齐
396
756
  <div className='gm-text-placeholder'>{placeholder}&nbsp; </div>
@@ -8,10 +8,17 @@ class MoreSelect<V = any> extends Component<MoreSelectProps<V>> {
8
8
  renderSelected: (item: MoreSelectDataItem<any>) => item.text,
9
9
  renderListItem: (item: MoreSelectDataItem<any>) => item.text,
10
10
  delay: 500,
11
- listHeight: '180px',
11
+ listHeight: '280px',
12
12
  renderListFilterType: 'default',
13
13
  popoverType: 'focus',
14
14
  onKeyDown: _.noop,
15
+ /** 是否展示全选以及过滤已删除商品 */
16
+ isRenderDefaultBottom: false,
17
+ /** 是否展示已删除商品 */
18
+ isShowDeletedSwitch: true,
19
+ /** 是否展示全选 */
20
+ isShowCheckedAll: true,
21
+ showSelectedIcon: true,
15
22
  }
16
23
 
17
24
  private _moreSelectBaseRef = createRef<MoreSelectBase>()
@@ -85,6 +92,8 @@ class MoreSelect<V = any> extends Component<MoreSelectProps<V>> {
85
92
  onSearch,
86
93
  onClick,
87
94
  renderListFilter,
95
+ maxTagCount,
96
+ maxTagPlaceholder,
88
97
  ...rest
89
98
  } = this.props
90
99
  let tempSelect = selected as MoreSelectDataItem<V>[]
@@ -129,6 +138,8 @@ class MoreSelect<V = any> extends Component<MoreSelectProps<V>> {
129
138
  isGroupList={isGroupList}
130
139
  onSearch={onSearch && this._handleSearch}
131
140
  renderListFilter={renderListFilter && this._renderListFilter}
141
+ maxTagCount={maxTagCount}
142
+ maxTagPlaceholder={maxTagPlaceholder}
132
143
  />
133
144
  )
134
145
  }
@@ -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
  }
@@ -70,3 +70,7 @@
70
70
  }
71
71
  }
72
72
  }
73
+
74
+ .gm-more-select-default-bottom {
75
+ border-top: 1px solid var(--gm-color-border);
76
+ }
@@ -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,25 @@ 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
76
+
77
+ /** 是否展示全选以及过滤已删除商品 */
78
+ isRenderDefaultBottom?: boolean
79
+ /** 是否展示已删除商品 */
80
+ isShowDeletedSwitch?: boolean
81
+ /** 是否展示全选 */
82
+ isShowCheckedAll?: boolean
83
+ /** 当设置 maxTagCount 的时候的宽度, 根据这个去计算显示内容 */
84
+ tagItemWidth?: number
85
+ /** +N 显示的宽度 */
86
+ omittedTagWidth?: number
63
87
  }
64
88
 
65
89
  interface MoreSelectBaseProps<V extends string | number = string>
@@ -69,7 +93,7 @@ interface MoreSelectBaseProps<V extends string | number = string>
69
93
  onSelect(selected: MoreSelectDataItem<V>[]): void
70
94
 
71
95
  /** 搜索回调 */
72
- onSearch?(searchWord: string, data: MoreSelectGroupDataItem<V>[]): Promise<void> | void
96
+ onSearch?(searchWord: string, data: MoreSelectGroupDataItem<V>[]): Promise<any> | any
73
97
  /** 点击回调 */
74
98
  onClick?(selected: MoreSelectSelected<V>[]): void
75
99
 
@@ -80,6 +104,13 @@ interface MoreSelectBaseProps<V extends string | number = string>
80
104
  ): MoreSelectGroupDataItem<V>[]
81
105
  /** 是否在active的时候搜索,订单业务相关,searchValue放在localstorage */
82
106
  searchOnActive?: boolean
107
+ /** 是否展示全选以及过滤已删除商品 */
108
+ isRenderDefaultBottom?: boolean
109
+ /** 是否展示已删除商品 */
110
+ isShowDeletedSwitch?: boolean
111
+ /** 是否展示全选 */
112
+ isShowCheckedAll?: boolean
113
+ showSelectedIcon?: boolean
83
114
  }
84
115
 
85
116
  type MoreSelectData<V extends string | number = string> =
@@ -98,7 +129,7 @@ interface MoreSelectProps<V extends string | number = string>
98
129
  onSelect?(selected?: MoreSelectSelected<V>): void
99
130
  onChange?(value: V | V[]): void
100
131
  /** 搜索回调 */
101
- onSearch?(searchWord: string, data: MoreSelectData<V>): Promise<void> | void
132
+ onSearch?(searchWord: string, data: MoreSelectData<V>): Promise<void | any> | any
102
133
  /** 点击回调 */
103
134
  onClick?(selected: MoreSelectSelected<V>[]): void
104
135
 
@@ -13,7 +13,6 @@ const Nav: FC<NavProps> = ({
13
13
  onPushCreate,
14
14
  showActive,
15
15
  other,
16
- otherFirst,
17
16
  className,
18
17
  style,
19
18
  footerImage,
@@ -79,8 +78,6 @@ const Nav: FC<NavProps> = ({
79
78
  >
80
79
  <div className='gm-nav-logo'>{logo}</div>
81
80
  <Flex flex column className='gm-nav-content'>
82
- {otherFirst}
83
-
84
81
  {data.map((one) => (
85
82
  <NavItem
86
83
  key={one.link}
@@ -50,7 +50,6 @@ interface NavProps extends NavExtraProps {
50
50
  /** 控制 浮层的线上,如商品库传 merchandise */
51
51
  showActive?: string
52
52
  other?: ReactNode
53
- otherFirst?: ReactNode
54
53
  className?: string
55
54
  style?: CSSProperties
56
55
  /** 底部 iot 图片 & 数据 */
@@ -6,6 +6,10 @@
6
6
  border-radius: calc(var(--gm-switch-size-height) / 2);
7
7
  background-color: var(--gm-color-bg-switch);
8
8
 
9
+ &.gm-switch-small {
10
+ --gm-switch-size-height: 16px;
11
+ }
12
+
9
13
  &::before {
10
14
  position: absolute;
11
15
  content: '';
@@ -11,6 +11,7 @@ interface SwitchProps {
11
11
  onChange?(checked: boolean): void
12
12
  className?: string
13
13
  style?: CSSProperties
14
+ size?: 'small' | 'middle'
14
15
  }
15
16
 
16
17
  interface SwitchState {
@@ -26,10 +27,11 @@ class Switch extends Component<SwitchProps, SwitchState> {
26
27
  on: '',
27
28
  off: '',
28
29
  onChange: _.noop,
30
+ size: 'middle',
29
31
  }
30
32
 
31
33
  readonly state: SwitchState = {
32
- checked: this.props.checked,
34
+ checked: !!this.props.checked,
33
35
  labelWidth: null,
34
36
  isReady: false,
35
37
  }
@@ -53,7 +55,7 @@ class Switch extends Component<SwitchProps, SwitchState> {
53
55
  UNSAFE_componentWillReceiveProps(nextProps: Readonly<SwitchProps>) {
54
56
  if ('checked' in nextProps) {
55
57
  this.setState({
56
- checked: nextProps.checked,
58
+ checked: !!nextProps.checked,
57
59
  })
58
60
  }
59
61
  }
@@ -82,6 +84,7 @@ class Switch extends Component<SwitchProps, SwitchState> {
82
84
  disabled,
83
85
  on,
84
86
  off,
87
+ size,
85
88
  ...rest
86
89
  } = this.props
87
90
 
@@ -101,8 +104,9 @@ class Switch extends Component<SwitchProps, SwitchState> {
101
104
  ref={this._inputOffRef}
102
105
  className={classNames('gm-switch gm-switch-' + type, className, {
103
106
  disabled,
107
+ [`gm-switch-${size}`]: size,
104
108
  })}
105
- style={style}
109
+ style={style as any}
106
110
  data-attr={this.state.labelWidth}
107
111
  disabled={disabled}
108
112
  type='checkbox'