ant官网示例

image.pngimage.png

  1. import { Select } from 'antd';
  2. const { Option } = Select;
  3. function handleChange(value) {
  4. console.log(value);
  5. }
  6. //单选
  7. <Select defaultValue="lucy" style={{ width: 120 }} onChange={handleChange}>
  8. <Option value="jack">Jack</Option>
  9. <Option value="lucy">Lucy</Option>
  10. <Option value="disabled" disabled>Disabled</Option>
  11. <Option value="Yiminghe">yiminghe</Option>
  12. </Select>
  13. //多选
  14. <Select mode="multiple" defaultValue={["a10","b11"]} clearable onChange={(val:string)=>valueChange(val)}>
  15. <Option value="a10" label="a10">a10</Option>
  16. <Option value="b11">b11</Option>
  17. <Option value="c12">c12</Option>
  18. <Option value="d13">d13</Option>
  19. <Option value="e14">e14</Option>
  20. </Select>

单选功能

需求分析

组件可传入的属性有:

为了实现预览中的效果,我们需要管理三个状态,先给他们起个名字:列表展开收起 isShowOptions,输入框展示的值 selectedText, 回调给onChange的值 selectedValue。*这个地方可以优化后面mode = multiple时再谈

它的结构分为两部分:展示选中项 & 下拉选项框。
展示选中项点击只需要设置 isShowOptions = true 即可。
下拉选项框中点击某个选项有以下变化:
selectedValue = value
selectedText = label
isShowOptions = false

动手制作

option组件

option组件什么都不需要做,后面再Select组件中会使用props.children获取里面所有属性。

  1. import React from 'react';
  2. interface IOption{
  3. value:any;
  4. label?:any;
  5. children:any;
  6. }
  7. const Option:React.FC<IOption>=(props:IOption)=>{
  8. return (
  9. ""
  10. )
  11. }

Select组件

基础功能:点击切换展示值,触发onChange回调
遍历一下children属性,渲染列表项。点击某一个列表项的时候修改一些状态,并触发onchange事件

  1. import React,{useState, useEffect} from 'react';
  2. interface ISelect{
  3. defaultValue?:any;
  4. children:any;
  5. onChange?:any;
  6. }
  7. interface IOption{
  8. value:any;
  9. label?:any;
  10. children:any;
  11. }
  12. const Select:React.FC<ISelect>=(props:ISelect)=>{
  13. const { children, onChange } = props;
  14. const [isShowOption,setShowStatus] = useState(false)
  15. const [selectedValue,setSelectedValue] = useState(null)
  16. const [selectedText,setselectedText] = useState(null)
  17. return (
  18. <div className="select">
  19. <div className="selectedText">
  20. 展示值:{selectedText}
  21. </div>
  22. <div className="selectOptions">
  23. {children.map((item: any, index: number)=>{
  24. const optionComponentProps:IOption = item.props
  25. return (
  26. <div key={index} onClick={()=>{
  27. setSelectedValue(optionComponentProps.value || "")
  28. setselectedText(optionComponentProps.label || optionComponentProps.children || '')
  29. onChange(optionComponentProps.value)
  30. }}>
  31. {optionComponentProps.label || optionComponentProps.children || ''}
  32. </div>
  33. )
  34. })}
  35. </div>
  36. </div>
  37. )
  38. }
  39. export {Select}

增加功能: 收起展开状态

  1. const OptionList = ()=>{
  2. // 仅当isShowOptions === true且select有子元素时渲染,没有则显示空状态
  3. if(isShowOption){
  4. if(children&&children.length>0){
  5. return (
  6. <div className="selectOptions">
  7. {children.map((item:any,index:number)=>{
  8. const optionComponentProps:IOption = item.props
  9. return (
  10. <div className="optionsClasses" key={index} onClick={
  11. ()=>{
  12. setSelectedValue(optionComponentProps.value || "")
  13. setselectedText(optionComponentProps.label || optionComponentProps.children || '')
  14. onChange(optionComponentProps.value)
  15. setShowStatus(false)
  16. }
  17. }>
  18. {optionComponentProps.label || optionComponentProps.children || ''}
  19. </div>
  20. )
  21. })}
  22. </div>
  23. )
  24. }else{
  25. return (
  26. <div className="selectOptions">
  27. <div>no data</div>
  28. </div>
  29. )
  30. }
  31. }
  32. return ""
  33. }
  34. return (
  35. <div className="select">
  36. <div className="selectedText" onClick={()=>setShowStatus(!isShowOption)}>
  37. 展示值:{selectedText}
  38. </div>
  39. {OptionList()}
  40. </div>
  41. )

新增功能:在 select 组件外任何地方点击时,一律收起 select 组件。
列表展开时无论再次点击哪里都隐藏列表,元素内点击只需要setShowStatus(false)即可,那节点外的地方呢?
我们引入useRef,给select绑定ref。在点击展示框的时候添加监听事件,判断e.target和ref元素之间的关系确定是否隐藏列表,并在收起列表时取消监听事件

  1. import React,{useState, useEffect, useRef} from 'react';
  2. const selectRef:any = useRef();
  3. const fn = (e:any)=>{
  4. if(selectRef.current !== e.target && !selectRef.current.contains(e.target)){
  5. setShowStatus(false)
  6. }
  7. }
  8. const addListener = ()=>{
  9. document.addEventListener('click', fn, true);
  10. }
  11. const removeListener = ()=>{
  12. document.removeEventListener('click', fn, true);
  13. }
  14. useEffect(()=>{
  15. if(!isShowOption){
  16. removeListener()
  17. }
  18. },[isShowOption])
  19. //输出
  20. <div className="select" ref={selectRef}>
  21. <div className="selectedText" onClick={()=>{
  22. addListener();
  23. setShowStatus(!isShowOption)}}>
  24. 展示值:{selectedText}
  25. </div>
  26. {OptionList()}
  27. </div>

下拉列表直接展示每个option的children, 展示区域则需要做一些判断。
增加Option组件的 label属性,如果没有传入label属性,就使用option的children,考虑到option的children不一定是纯文本,可能像是这样《span》123《/span》。所以要再做一层判断,如果是纯文本使用纯文本,如果不是,使用option的value属性。

  1. interface IOption{
  2. value?:any;
  3. label?:any;
  4. children:any;
  5. }
  6. const getInnerText = (child:any)=>{
  7. if(typeof child.props.children==='string'){
  8. return child.props.children
  9. }else{
  10. return child.props.value
  11. }
  12. }
  13. const OptionList = ()=>{
  14. if(isShowOption){
  15. if(children&&children.length>0){
  16. return (
  17. <div className="selectOptions">
  18. {children.map((item:any,index:number)=>{
  19. let label = item.props.label || getInnerText(item)
  20. return (
  21. <div className="optionsClasses" key={index} onClick={
  22. ()=>{
  23. onChange(item.props.value)
  24. setSelectedValue(item.props.value||"")
  25. setselectedText(label || "")
  26. setShowStatus(false)
  27. }
  28. }>
  29. {item.props.children}
  30. </div>
  31. )
  32. })}
  33. </div>
  34. )
  35. }else{
  36. return (
  37. <div className="selectOptions">
  38. <div>no data</div>
  39. </div>
  40. )
  41. }
  42. }
  43. return ""
  44. }

基础的功能已经完成了,接下来增加CSS样式,以及完善:初始值,清空,禁用项等功能

完整代码

  1. import React,{useState, useEffect, useRef} from 'react';
  2. import classNames from 'classnames';
  3. import {ThemeContext} from '../../App';
  4. import Icon from '../Icon'
  5. interface ISelect{
  6. defaultValue?:any;
  7. children?:any;
  8. onChange?:any;
  9. }
  10. interface IOption{
  11. children:any;
  12. value?:any;
  13. label?:any;
  14. }
  15. const Option:React.FC<IOption>=(props:IOption)=>{
  16. return (
  17. ""
  18. )
  19. }
  20. const Select:React.FC<ISelect> = (props)=>{
  21. const { children, onChange, defaultValue } = props;
  22. const getInnerText = (child:any)=>{
  23. if(typeof child.props.children==='string'){
  24. return child.props.children
  25. }else{
  26. return child.props.value
  27. }
  28. }
  29. const getDefaultText = (dv:any)=>{
  30. if(!children || children.length<1){
  31. return null
  32. }
  33. for(let i=0;i<children.length;i++){
  34. if(children[i].props.value === dv){
  35. return children[i].props.label || getInnerText(children[i])
  36. }
  37. }
  38. }
  39. //usestate
  40. const [isShowOption,setShowStatus] = useState(false)
  41. const [iconRotate,setIconRotate] = useState({ transform:"rotate(0deg)" })
  42. const [selectedValue,setSelectedValue] = useState( defaultValue )
  43. const [selectedText,setselectedText] = useState( getDefaultText(defaultValue) )
  44. //eventlistener
  45. const selectRef:any = useRef();
  46. const fn = (e:any)=>{
  47. if(selectRef.current!==e.target && !selectRef.current.contains(e.target)){
  48. setShowStatus(false)
  49. }
  50. }
  51. const addListener = ()=>{
  52. document.addEventListener('click', fn, true);
  53. }
  54. const removeListener = ()=>{
  55. document.removeEventListener('click', fn, true);
  56. }
  57. //useeffect
  58. useEffect(()=>{
  59. if(!isShowOption){
  60. setIconRotate({transform:"rotate(0deg)"})
  61. removeListener()
  62. }else{
  63. setIconRotate({transform:"rotate(-180deg)"})
  64. }
  65. },[isShowOption])
  66. //列表
  67. const OptionList = ()=>{
  68. if(isShowOption){
  69. if(children && children.length>0){
  70. return (
  71. <div className="selectOptions">
  72. {children.map((item:any)=>{
  73. const optionsClasses = classNames("optionItem",{"optionItem-selected":selectedValue===item.props.value})
  74. let label = item.props.label || getInnerText(item)
  75. return (
  76. <div className={optionsClasses} key={item.props.value} onClick={
  77. ()=>{
  78. onChange(item.props.value)
  79. setSelectedValue(item.props.value||"")
  80. setselectedText(label || "")
  81. setShowStatus(false)
  82. }
  83. }>
  84. <div style={{flex:"auto"}}>
  85. {item.props.children}
  86. </div>
  87. <div>
  88. <Icon style={{color:"#1890ff"}} icon="icon-select"/>
  89. </div>
  90. </div>
  91. )
  92. })}
  93. </div>
  94. )
  95. }else{
  96. return (
  97. <div className="selectOptions">
  98. <div>no data</div>
  99. </div>
  100. )
  101. }
  102. }
  103. return ""
  104. }
  105. //展示
  106. const displayArea = ()=>{
  107. const selectedClass=classNames("selectedText",{"selectedText-focus":isShowOption})
  108. return (
  109. <div className={selectedClass} onClick={()=>{
  110. addListener();
  111. setShowStatus(!isShowOption)}}>
  112. <div style={{paddingRight:"12px"}}>
  113. {selectedText}
  114. </div>
  115. <span className="suffixIcon" style={iconRotate}>
  116. <Icon style={{fontSize:"16px"}} icon="icon-arrow-down-bold"></Icon>
  117. </span>
  118. </div>
  119. )
  120. }
  121. return (
  122. <div className="select" ref={selectRef}>
  123. {displayArea()}
  124. {OptionList()}
  125. </div>
  126. )
  127. }
  128. export {Select,Option}
  1. .select{
  2. position: relative;
  3. width:200px;
  4. height: 32px;
  5. line-height: 32px;
  6. }
  7. .suffixIcon{
  8. display: inline-block;
  9. font-size:16px;
  10. position: absolute;
  11. height: 100%;
  12. right: 10px;
  13. top: 0;
  14. text-align: center;
  15. color: #c0c4cc;
  16. -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1);
  17. transition: all .3s cubic-bezier(.645,.045,.355,1);
  18. transform-origin: center;
  19. cursor:pointer;
  20. }
  21. .selectedText{
  22. position: relative;
  23. height: 100%;
  24. line-height: inherit;
  25. padding: 0 8px;
  26. width:inherit;
  27. border: 1px solid #d9d9d9;
  28. box-sizing: content-box;
  29. border-radius: 2px;
  30. background-color:#fff;
  31. display: inline-block;
  32. cursor: pointer;
  33. -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1);
  34. transition: all .3s cubic-bezier(.645,.045,.355,1);
  35. .multipleBox{
  36. padding-right:12px;
  37. display: flex;
  38. align-items: center;
  39. height: inherit;
  40. .multipleItem{
  41. background: #f5f5f5;
  42. border: 1px solid #f0f0f0;
  43. border-radius: 2px;
  44. margin-right:8px;
  45. display:inline-block;
  46. height: calc(100% - 4px);
  47. display: flex;
  48. align-items: center;
  49. padding:0 4px 0 8px;
  50. //line-height: 28px;
  51. .item-label{
  52. color:rgba(0,0,0,.85);
  53. margin-right: 4px;
  54. }
  55. .item-icon{
  56. font-size: 12px;
  57. -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1);
  58. transition: all .3s cubic-bezier(.645,.045,.355,1);
  59. color:rgba(0,0,0,.45);
  60. }
  61. .item-icon:hover{
  62. color:rgba(0,0,0,.85);
  63. }
  64. }
  65. }
  66. }
  67. .selectedText:hover{
  68. border-color: #40a9ff;
  69. }
  70. .selectedText-focus{
  71. border-color: #40a9ff;
  72. border-right-width: 1px!important;
  73. outline: 0;
  74. -webkit-box-shadow: 0 0 0 2px rgba(24,144,255,.2);
  75. box-shadow: 0 0 0 2px rgba(24,144,255,.2)
  76. }
  77. .selectOptions{
  78. position: absolute;
  79. z-index:9;
  80. top:45px;
  81. left:0;
  82. //overflow-y: scroll;
  83. width:inherit;
  84. max-height: 274px;
  85. border: 1px solid #e4e7ed;
  86. border-radius: 4px;
  87. background-color: #fff;
  88. box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
  89. box-sizing: content-box;
  90. margin: 5px 0;
  91. height:auto;
  92. .optionItem{
  93. display: flex;
  94. cursor: pointer;
  95. font-size: 14px;
  96. padding:0 20px;
  97. height:34px;
  98. line-height:34px;
  99. color:black;
  100. }
  101. .optionItem-selected{
  102. font-weight: bold;
  103. }
  104. .optionItem:hover{
  105. background-color: #f5f7fa;
  106. }
  107. .optionItem-selected, .optionItem-selected:hover{
  108. background-color:#e6f7ff
  109. }
  110. }

多选功能

需求分析

多选功能 select传入mode=”multiple” 列出与单选的区别点:
(1)defaultValue是数组的形式[‘a10’, ‘c12’];
(2)onchange返回值也是数组形式value=[‘a10’, ‘c12’];
(3)输入区域 每点击一个项都会展示该项的小标签,可删除
(4)展示列表
1。只有点击列表外区域才会收起,点击列表内不收起。
2。点击列表内某个项目时更改背景色,屁股后面加个钩。
3。输入框清除标签时,也要清除列表项的样式和钩

思路分析

输出值和展示值都为数组的情况下 那么selectedValue和selectedText也一定是一个数组。selectedText:我们点击列表项“a10”的时候,涉及到一次遍历:如果selectedText没有“a10”元素:添加一个元素,如果有:filter过滤掉该元素,selectedValue 也是一样。
每点击一次列表项的时候,我都要遍历两次数组,麻烦,而且代码有点冗余,我需要一个优化的方案,把他们统一放在一起管理。把selectedText和selectedValue的每一项放进一个对象里,这时候再加一个isSelect字段来管理是否被选中,
这样一来,每次点击列表项只需要遍历一次修改isSelect取反即可,是不是一下子方便了很多。
新增option的key属性,如果没有使用option的value属性。一起存放在对象里,遍历的时候设置key,避免重复。
我们看一下数据结构,在开始的时候需要遍历children处理,输出initData。

  1. const data=[
  2. {
  3. key:"1",
  4. isSelect:false,
  5. label:"Jack",
  6. value:"value",
  7. },
  8. {
  9. key:"2",
  10. isSelect:false,
  11. label:"label2",
  12. value:"value2",
  13. },
  14. ]

开始动手

展示框区域

  1. const getInnerText = (child:any)=>{
  2. if(typeof child.props.children==='string'){
  3. return child.props.children
  4. }else{
  5. return child.props.value
  6. }
  7. }
  8. const setInitData = ()=>{
  9. if(!children || children.length<1){
  10. return []
  11. }
  12. let initData=[]
  13. for(let i=0;i<children.length;i++){
  14. initData.push({
  15. key:children[i].props.key || children[i].props.value,
  16. //通过defaultValue默认值初始化选中状态
  17. isSelect:defaultValue.indexOf(children[i].props.value) > -1 ? true : false,
  18. label:children[i].props.label || getInnerText(children[i]) || "",
  19. value:children[i].props.value,
  20. })
  21. }
  22. return initData
  23. }
  24. const [selectedInfo,setselectedInfo] = useState<any[]>( setInitData() )
  25. //点击列表项的事件
  26. const multipleClick = (item:any)=>{
  27. setselectedInfo(selectedInfo.map((info:any)=>{
  28. if(info.key===(item.key || item.value)){
  29. return {...info,isSelect:!info.isSelect}
  30. }
  31. return info
  32. }))
  33. }
  34. //展示框区域 只要遍历出isSelect为true的对象渲染即可
  35. //每一个项目是一个小标签,有删除按钮可以删除
  36. {selectedInfo.map((item:any)=>{
  37. if(item.isSelect){
  38. return (
  39. <div className="multipleItem" key={item.key}>
  40. <span className="item-label">
  41. {item.label}
  42. </span>
  43. <span onClick={()=>{
  44. multipleClick(item)
  45. }}>
  46. <Icon className="item-icon" icon="icon-close-bold"/>
  47. </span>
  48. </div>
  49. )
  50. }
  51. return ""
  52. })}

此处有一个问题,点击小标签的删除,会触发外部输入框的下拉事件,我们不想点击删除的时候还顺便展示或者收起菜单,这是典型的事件冒泡,只要在小标签的事件上取消冒泡即可event.stopPropagation()

  1. const multipleClick = (item:any,e:any)=>{
  2. e.stopPropagation()
  3. setselectedInfo(selectedInfo.map((info:any)=>{
  4. if(info.key===(item.key || item.value)){
  5. return {...info,isSelect:!info.isSelect}
  6. }
  7. return info
  8. }))
  9. }
  10. //元素上加上参数别忘了
  11. onClick={(e)=>multipleClick(item,e)}

标签长度超出了输入框长度时我们希望他能自动换行,我们使用css实现即可

  1. .autoBreak{
  2. min-height: 32px;
  3. -webkit-box-flex: 1;
  4. -ms-flex: auto;
  5. flex: auto;
  6. -ms-flex-wrap: wrap;
  7. flex-wrap: wrap;
  8. max-width: 100%;
  9. }

由于列表使用绝对定位,当输入框高度发生变化时,列表的top值也要发生变化。新增一个状态管理列表top值,绑定在列表的style属性上。使用useEffect每次selectinfo状态发生变化时,获得输入框的高度,如果变高增加相应的top值。

  1. const [top,setTop] = useState(40) //初始高度40
  2. useEffect(()=>{
  3. let height = document.getElementsByClassName("multipleBox")[0].clientHeight //30
  4. setTop(10+height)
  5. },[selectedInfo])
  6. //绑定在绝对定位的元素上 style={{top:top+'px'}}
  7. //顺便把其top属性删除

下拉列表区域

输入框搞定了,再来看看展示列表吧,之前我们直接循环select.children,很不优雅,现在我们循环selectedInfo即可,isSelect为true增加打钩的icon

  1. {selectedInfo.map((info:any)=>{
  2. const optionsClasses = classNames("optionItem",{"optionItem-selected":info.isSelect})
  3. const iconArea = info.isSelect?(<div><Icon style={{color:"#1890ff"}} icon="icon-select"/></div>):""
  4. return (
  5. <div className={optionsClasses} key={info.key} onClick={()=>multipleClick(info)}>
  6. <div style={{flex:"auto"}}>
  7. {info.label}
  8. </div>
  9. {iconArea}
  10. </div>
  11. )
  12. })}

当子元素太多,展示列表长度会把遮住页面上的信息很不友好,所以我们给他一个最大高度,超出时用滚动条。用 overflow-y:scroll 加上设置 max-height。

onchange事件:

触发select上的onchange事件上的参数,单选返回字符串格式,mode=”multiple” 返回数组形式,默认值也是如此
一开始我想在列表元素点击的事件中触发onchange事件的,后来发现在事件绑定中修改state是异步的。

  1. //错误示例
  2. const multipleClick = (item:any,e:any)=>{
  3. e.stopPropagation()
  4. if(mode==="multiple"){
  5. //此处更新为异步
  6. setselectedInfo(selectedInfo.map((info:ISelectInfo)=>{
  7. if(info.key===(item.key || item.value)){
  8. return {...info,isSelect:!info.isSelect}
  9. }
  10. return info
  11. }))
  12. //把选中元素回调到onChange
  13. //这里并不能获取最新的selectInfo
  14. onChange(selectedInfo.filter((item:any)=>item.isSelect))
  15. }
  16. }

只能根据selectinfo的变化在effect中触发onchange了。

  1. let flag=useRef(true)
  2. useEffect(()=>{
  3. //为了让他第一次进来不调用
  4. if(flag.current){
  5. flag.current=false
  6. return
  7. }
  8. if(mode==="multiple"){
  9. let emitVal:any[]=[]
  10. selectedInfo.map((item:any)=>{
  11. if(item.isSelect) emitVal.push(item.value)
  12. })
  13. onChange(emitVal)
  14. }
  15. },[selectedInfo])

在页面中使用多个select组件对应会生成多个className为”multipleBox”,document.getElementsByClassName(“multipleBox”)[0]会有错误
在”multipleBox”上再加个id根据isshowoption判断。true的话id为calcHeight,否则为空,再使用document.getElementById(“calcHeight”)

  1. let doc:HTMLElement | null = document.getElementById("calcHeight")
  2. let height:number = doc?doc.clientHeight:32
  3. <div className="multipleBox" id={isShowOption?"calcHeight":""} >

完整代码

兼容单选和多选

  1. import React,{useState, useEffect, useRef} from 'react';
  2. import classNames from 'classnames';
  3. import Icon from '../Icon'
  4. interface ISelect{
  5. mode?:string;
  6. defaultValue?:any;
  7. clearable?:boolean;
  8. children:any;
  9. onChange?:any;
  10. }
  11. interface IOption{
  12. children:any;
  13. value?:any;
  14. label?:any;
  15. }
  16. interface ISelectInfo{
  17. key:string;
  18. isSelect:boolean;
  19. value:any;
  20. label:any;
  21. }
  22. //util工具方法
  23. const getInnerText = (child:any)=>{
  24. if(typeof child.props.children==='string'){
  25. return child.props.children
  26. }else{
  27. return child.props.value
  28. }
  29. }
  30. const isSelect = (dv:any,cv:any)=>{
  31. if(typeof dv==="string"){
  32. return dv===cv
  33. }else if(typeof dv==="object"){
  34. return dv.indexOf(cv)>-1
  35. }
  36. return false
  37. }
  38. const Option:React.FC<IOption>=(props:IOption)=>{
  39. return (
  40. <div>
  41. <p>{props.children}</p>
  42. </div>
  43. )
  44. }
  45. const Select:React.FC<ISelect> = (props)=>{
  46. const { children, onChange, clearable, defaultValue=[], mode } = props;
  47. // init
  48. const setInitData = ()=>{
  49. if(!children || children.length<1){
  50. return []
  51. }
  52. let initData:ISelectInfo[]=[]
  53. for(let i=0;i<children.length;i++){
  54. initData.push({
  55. key:children[i].props.key || children[i].props.value,
  56. isSelect:isSelect(defaultValue,children[i].props.value),
  57. label:children[i].props.label || getInnerText(children[i]) || "",
  58. value:children[i].props.value,
  59. })
  60. }
  61. return initData
  62. }
  63. //usestate
  64. const [isShowOption,setShowStatus] = useState(false)
  65. const [iconRotate,setIconRotate] = useState({ transform:"rotate(0deg)" })
  66. const [top,setTop] = useState(40)
  67. const [selectedInfo,setselectedInfo] = useState<ISelectInfo[]>( setInitData() )
  68. //点击事件监听
  69. const selectRef:any = useRef();
  70. const fn = (e:any)=>{
  71. if(selectRef.current!==e.target && !selectRef.current.contains(e.target)){
  72. setShowStatus(false)
  73. }
  74. }
  75. const addListener = ()=>{
  76. document.addEventListener('click', fn, true);
  77. }
  78. const removeListener = ()=>{
  79. document.removeEventListener('click', fn, true);
  80. }
  81. //useeffect
  82. useEffect(()=>{
  83. if(!isShowOption){
  84. setIconRotate({transform:"rotate(0deg)"})
  85. removeListener()
  86. }else{
  87. setIconRotate({transform:"rotate(-180deg)"})
  88. }
  89. },[isShowOption])
  90. let flag=useRef(true)
  91. useEffect(()=>{
  92. let doc:HTMLElement | null = document.getElementById("calcHeight")
  93. let height:number = doc?doc.clientHeight:32
  94. setTop(10+height)
  95. if(flag.current){
  96. flag.current=false
  97. return
  98. }
  99. if(mode==="multiple"){
  100. let emitVal:any[]=[]
  101. selectedInfo.map((item:ISelectInfo)=>{
  102. if(item.isSelect) emitVal.push(item.value)
  103. })
  104. onChange(emitVal)
  105. }else{
  106. selectedInfo.map((item:ISelectInfo)=>{
  107. if(item.isSelect) onChange(item.value)
  108. })
  109. setShowStatus(false)
  110. }
  111. },[selectedInfo])
  112. //onclick事件: 清空,下拉,选择
  113. const handleClear = (e:any)=>{
  114. e.stopPropagation();
  115. setselectedInfo(selectedInfo.map((info:ISelectInfo)=>{
  116. return {...info,isSelect:false}
  117. }))
  118. }
  119. const dropDown = ()=>{
  120. addListener();
  121. setShowStatus(!isShowOption)
  122. }
  123. const multipleClick = (item:any,e:any)=>{
  124. e.stopPropagation()
  125. if(mode==="multiple"){
  126. //此处更新为异步
  127. setselectedInfo(selectedInfo.map((info:ISelectInfo)=>{
  128. if(info.key===(item.key || item.value)){
  129. return {...info,isSelect:!info.isSelect}
  130. }
  131. return info
  132. }))
  133. }else{
  134. setselectedInfo(selectedInfo.map((info:ISelectInfo)=>{
  135. if(info.key===(item.key || item.value)){
  136. return {...info,isSelect:true}
  137. }
  138. return {...info,isSelect:false}
  139. }))
  140. }
  141. }
  142. //展示框
  143. const displayArea = ()=>{
  144. const selectedClass=classNames("selectedText",{"selectedText-focus":isShowOption})
  145. const dropDownIcon = mode!=="multiple"
  146. ?(<Icon className="suffixIcon" style={iconRotate} icon="icon-arrow-down-bold"/>)
  147. :"";
  148. const clearIcon = clearable && selectedInfo.some((item:ISelectInfo)=>item.isSelect)
  149. ?(<span className="clearIcon" onClick={(e)=>handleClear(e)}>
  150. <Icon style={{fontSize:"14px"}} icon="icon-close-bold"/>
  151. </span>)
  152. :"";
  153. const displayInline =(item:any)=>{
  154. if(item.isSelect){
  155. return mode==="multiple"
  156. ?(
  157. <div className="multipleItem" key={item.key}>
  158. <span className="item-label">
  159. {item.label}
  160. </span>
  161. <span onClick={(e)=>multipleClick(item,e)}>
  162. <Icon className="item-icon" icon="icon-close-bold"/>
  163. </span>
  164. </div>
  165. )
  166. :(
  167. <div style={{paddingRight:"12px"}} key={item.key}>
  168. {item.label}
  169. </div>
  170. )
  171. }
  172. return ""
  173. }
  174. return (
  175. <div className={selectedClass} onClick={()=>dropDown()}>
  176. <div className="multipleBox" id={isShowOption?"calcHeight":""}>
  177. {selectedInfo.map((item:ISelectInfo)=>{
  178. return displayInline(item)
  179. })}
  180. </div>
  181. {dropDownIcon}
  182. {clearIcon}
  183. </div>
  184. )
  185. }
  186. //列表
  187. const OptionList = ()=>{
  188. if(isShowOption){
  189. if(children && children.length>0){
  190. const row = (info:any)=>{
  191. const optionsClasses = classNames("optionItem",{"optionItem-selected":info.isSelect})
  192. const iconArea = info.isSelect?(<div><Icon style={{color:"#1890ff"}} icon="icon-select"/></div>):""
  193. return mode==="multiple"
  194. ?(<div className={optionsClasses} key={info.key} onClick={(e)=>multipleClick(info,e)}>
  195. <div style={{flex:"auto",userSelect: "none"}}>
  196. {info.label}
  197. </div>
  198. {iconArea}
  199. </div>)
  200. :(<div className={optionsClasses} key={info.key} onClick={(e)=>multipleClick(info,e)}>
  201. {info.label}
  202. </div>)
  203. }
  204. return (
  205. <div className="selectOptions" style={{top:top+'px'}}>
  206. {selectedInfo.map((info:ISelectInfo)=>{
  207. {return row(info)}
  208. })}
  209. </div>
  210. )
  211. }else{
  212. return (
  213. <div className="selectOptions">
  214. <div>no data</div>
  215. </div>
  216. )
  217. }
  218. }
  219. return ""
  220. }
  221. return (
  222. <div className="select" ref={selectRef}>
  223. {displayArea()}
  224. {OptionList()}
  225. </div>
  226. )
  227. }
  228. export {Select,Option}

样式代码

  1. .select{
  2. position: relative;
  3. width:220px;
  4. min-height: 32px;
  5. }
  6. .suffixIcon{
  7. display: inline-block;
  8. font-size:14px;
  9. line-height: 32px;
  10. position: absolute;
  11. z-index: 2020;
  12. height: 100%;
  13. right: 9px;
  14. top: 0;
  15. text-align: center;
  16. color: #c0c4cc;
  17. -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1);
  18. transition: all .3s cubic-bezier(.645,.045,.355,1);
  19. cursor:pointer;
  20. }
  21. .selectedText{
  22. position: relative;
  23. height: 100%;
  24. line-height: inherit;
  25. padding: 0 8px;
  26. width:inherit;
  27. border: 1px solid #d9d9d9;
  28. box-sizing: content-box;
  29. border-radius: 2px;
  30. background-color:#fff;
  31. cursor: pointer;
  32. -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1);
  33. transition: all .3s cubic-bezier(.645,.045,.355,1);
  34. .multipleBox{
  35. padding-right:12px;
  36. display: flex;
  37. align-items: center;
  38. height: inherit;
  39. /********* 高度自动撑开 **************/
  40. min-height: 32px;
  41. -webkit-box-flex: 1;
  42. -ms-flex: auto;
  43. flex: auto;
  44. -ms-flex-wrap: wrap;
  45. flex-wrap: wrap;
  46. max-width: 100%;
  47. .multipleItem{
  48. background: #f5f5f5;
  49. border: 1px solid #f0f0f0;
  50. border-radius: 2px;
  51. margin:2px 8px 2px 0;
  52. display:inline-block;
  53. display: flex;
  54. align-items: center;
  55. padding:0 4px 0 8px;
  56. //line-height: 28px;
  57. .item-label{
  58. color:rgba(0,0,0,.85);
  59. margin-right: 4px;
  60. user-select: none;
  61. }
  62. .item-icon{
  63. font-size: 12px;
  64. -webkit-transition: all .3s cubic-bezier(.645,.045,.355,1);
  65. transition: all .3s cubic-bezier(.645,.045,.355,1);
  66. color:rgba(0,0,0,.45);
  67. }
  68. .item-icon:hover{
  69. color:rgba(0,0,0,.85);
  70. }
  71. }
  72. }
  73. .clearIcon{
  74. position: absolute;
  75. top: 50%;
  76. right: 11px;
  77. z-index: 2021;
  78. display: inline-block;
  79. width: 12px;
  80. height: 12px;
  81. margin-top: -6px;
  82. color: rgba(0,0,0,.25);
  83. font-size: 12px;
  84. font-style: normal;
  85. line-height: 1;
  86. text-align: center;
  87. text-transform: none;
  88. background: #fff;
  89. cursor: pointer;
  90. opacity: 0;
  91. -webkit-transition: color .3s ease,opacity .15s ease;
  92. transition: color .3s ease,opacity .15s ease;
  93. text-rendering: auto;
  94. &:hover{
  95. color: rgba(0,0,0,.65);
  96. }
  97. }
  98. }
  99. .selectedText:hover{
  100. border-color: #40a9ff;
  101. }
  102. .selectedText:hover .clearIcon{
  103. opacity: 1;
  104. }
  105. .selectedText-focus{
  106. border-color: #40a9ff;
  107. border-right-width: 1px!important;
  108. outline: 0;
  109. -webkit-box-shadow: 0 0 0 2px rgba(24,144,255,.2);
  110. box-shadow: 0 0 0 2px rgba(24,144,255,.2)
  111. }
  112. .selectOptions{
  113. position: absolute;
  114. z-index:9;
  115. //top:45px; 通过js设置
  116. left:0;
  117. overflow-y: scroll;
  118. width:inherit;
  119. max-height: 274px;
  120. border: 1px solid #e4e7ed;
  121. border-radius: 4px;
  122. background-color: #fff;
  123. box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
  124. box-sizing: content-box;
  125. margin: 5px 0;
  126. height:auto;
  127. .optionItem{
  128. display: flex;
  129. cursor: pointer;
  130. font-size: 14px;
  131. padding:0 20px;
  132. height:34px;
  133. line-height:34px;
  134. color:black;
  135. user-select: none;
  136. }
  137. .optionItem-selected{
  138. font-weight: bold;
  139. }
  140. .optionItem:hover{
  141. background-color: #f5f7fa;
  142. }
  143. .optionItem-selected, .optionItem-selected:hover{
  144. background-color:#e6f7ff
  145. }
  146. }

肯定还有很多不完善的地方。欢迎评论或者直接联系我,谢谢!

image.png