xfl-select.vue 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
  1. <template>
  2. <div class="show-box" :class="{disabled: disabled, active: isShowList}" :style="style_Container">
  3. <!-- 输入框,仅在可输入模式下使用 -->
  4. <input
  5. v-if="showInput" class="input" placeholder-style="color: #bbb;"
  6. type="text" v-model="selectText" :placeholder="placeholder"
  7. @focus="onFocus" @blur="onBlur" @input="onInput" @confirm="$emit('confirm', $event)"
  8. >
  9. <!-- 显示框 -->
  10. <div v-else class="input" :class="{placeholder: selectText === placeholder}" @click="onUpperClick" >{{selectText}}</div>
  11. <!-- 右侧的小三角图标 -->
  12. <span
  13. @click="onUpperClick"
  14. class="iconfont iconarrowBottom-fill right-arrow"
  15. :class="{isRotate: isRotate}"
  16. ></span>
  17. <!-- 清除按钮图标 -->
  18. <span
  19. v-if="clearable && selectText && selectText != placeholder"
  20. class="right-arrow" @click="onClear"
  21. >
  22. <span class="iconfont iconshanchu1 clear"></span>
  23. </span>
  24. <!-- 列表框 -->
  25. <div class="list-container"
  26. @click.stop="onListClick"
  27. :style="'top:' + listTop__ + 'px;'" v-show="isShowList">
  28. <span class="popper__arrow"></span> <!-- 列表框左上角的空心小三角 -->
  29. <scroll-view
  30. class="list" style="background-color: #fff;"
  31. :style="'max-height: ' + listBoxHeight__ +'em;'"
  32. scroll-y=true scroll-x=true
  33. >
  34. <div
  35. class="item" @click="onClickItem(index, item.value)"
  36. v-for="(item, index) in innerList" :key="index"
  37. :class="{active: activeIndex == index, disabled: item.disabled}"
  38. >
  39. <div>{{item.value}}</div>
  40. </div>
  41. <div v-show="innerList.length==0" class="data-state item">无数据</div>
  42. <!-- <slot></slot> -->
  43. </scroll-view>
  44. </div>
  45. </div>
  46. </template>
  47. <script>
  48. /**
  49. * v1.1.1
  50. * 最后修改: 2019.7.29
  51. * 创建: 2019.6.27
  52. */
  53. import Vue from 'vue';
  54. Vue.__xfl_select = Vue.__xfl_select || new Vue(); // 这个实例专门用来做xfl-select多个实例之间的通信中间站
  55. export default {
  56. name: 'xfl-select',
  57. props: {
  58. list: { // 原始数据
  59. type: Array,
  60. default: function(){
  61. return [];
  62. }
  63. },
  64. focusShowList: null, // 当input获取焦点时,是否自动弹出列表框
  65. initValue: null, // 选择框的初始值
  66. isCanInput: { // 选择框是否可以输入值
  67. type: Boolean,
  68. default: false,
  69. },
  70. selectHideType: { // 本选择框与其它选择框之间的关系
  71. type: String,
  72. default: 'hideAll', // 'independent' - 是独立的,与其它选择框互不影响 'hideAll' - 任何一个选择框展开时,隐藏所有其它选择框
  73. // 'hideOthers'- 当本选择框展开时,隐藏其它的选择框。 当其它选择框展开时,不隐藏本选择框。
  74. // 'hideSelf' - 当本选择框展开时,不隐藏其它的选择框。当其它选择框展开时,隐藏本选择框。
  75. },
  76. placeholder: { // 选择框的placeholder
  77. type: String,
  78. default: '请选择',
  79. },
  80. style_Container: { // 最外层的样式
  81. type: String,
  82. default: ''
  83. },
  84. disabled: { // 是否禁用整个选择框
  85. type: Boolean,
  86. default: false,
  87. },
  88. showItemNum: { // 显示列表框的窗口高度,数字表示能显示几个列表项
  89. type: Number,
  90. default: 5
  91. },
  92. listShow: { // 是否显示列表框
  93. type: Boolean,
  94. default: false
  95. },
  96. clearable: { // 是否显示右侧的清除按钮
  97. type: Boolean,
  98. default: true
  99. },
  100. },
  101. data() {
  102. return {
  103. isShowList: false, // 是否显示列表框
  104. selectText: '', // 已经选择的内容
  105. activeIndex: -1, // 列表中当前活动的索引号
  106. isRotate: false, // 右侧的小三角是否旋转
  107. listTop__: 50, // 列表框的top位置,在初始时,根据input节点的高度来调整
  108. };
  109. },
  110. // 进行监听的话,在组件外改变这个值,组件内就能响应变化
  111. watch: { // 监听变化 ,注意,初始的值是不会被监听到的,只有在mounted回调中手动赋值
  112. listShow(newVal, oldVal){
  113. this.onDataChange_listShow(newVal, oldVal);
  114. },
  115. },
  116. computed:{
  117. focusShowList__(){ // 是否在输入框获得焦点时,自动弹出列表框
  118. if(this.focusShowList == null ){
  119. // 应该是判断在 pc端还是移动端
  120. // #ifdef H5
  121. return isPC();
  122. // #endif
  123. // #ifndef H5
  124. return false;
  125. // #endif
  126. }else{
  127. return this.focusShowList;
  128. }
  129. },
  130. listBoxHeight__(){ // 列表框的总高度
  131. const itemHeight = 2; // 每个列表项的高度(em), 默认为2个文字高
  132. return this.showItemNum*itemHeight;
  133. },
  134. showInput(){ // 是否显示输入框
  135. return this.isCanInput && !this.disabled;
  136. },
  137. innerList(){ // 转换列表的数据格式
  138. const arr = [], orginArr = this.list;
  139. orginArr.forEach((val, index)=>{
  140. let value = typeof val === 'object' && 'value' in val ? val.value : val;
  141. let isDisabled = typeof val === 'object' && val.disabled == true;
  142. arr.push({
  143. isActive: false,
  144. value: value,
  145. disabled: isDisabled,
  146. });
  147. });
  148. return arr;
  149. },
  150. },
  151. mounted(){
  152. Vue.__xfl_select.$on('open', this.onOtherXflSelectOpen);
  153. this.switchMgr = new Switch(this.onListShow, this.onListHide); // 创建开关对象
  154. this.onDataChange_listShow(this.listShow, null); // 由于 watch 不到初始值,所以需要在这里手动调用一次
  155. this.init(); //进行初始化
  156. },
  157. beforeDestroy(){
  158. Vue.__xfl_select.$off('open', this.onOtherXflSelectOpen);
  159. },
  160. methods: {
  161. onOtherXflSelectOpen(component){ //当本组件的其它实例展开时的回调
  162. if(this.selectHideType === 'independent' || this.selectHideType === 'hideOthers'){
  163. return;
  164. }
  165. component !== this && this.switchMgr.close(100);
  166. },
  167. /************************** 初始化函数 ****************************/
  168. //进行初始化
  169. init(){
  170. this.clearInput(); // 清空输入框中的显示,主要是设置placeholder
  171. this.setInput(this.initValue); // 在输入框中显示初始值
  172. this.changeActiveIndex(this.initValue); // 根据初始值设置列表框中的活动项
  173. this.getInputBoxHeight(); // 初始化列表框的top值
  174. },
  175. // 获取输入框的总高度 px
  176. getInputBoxHeight(){
  177. let component = this;
  178. // #ifdef H5
  179. component = undefined; // 在h5中传入了component反而拿不到数据
  180. // #endif
  181. getNodeInfo('.show-box', component, (data)=>{
  182. if(data){
  183. const trangleHeight = 6; //列表框左上角的小的空心三角形的高度(px)
  184. this.listTop__ = data[0].height + trangleHeight;
  185. }
  186. })
  187. },
  188. /************************** 初始化函数 ****************************/
  189. /************************** 数据 ****************************/
  190. getIndex(value){ // 将值转换为索引
  191. let activeIndex = searchIndex(
  192. this.innerList, value, 'value')
  193. return activeIndex; // 转换失败,则返回-1
  194. },
  195. itemIsDisabled(index){ // 某个列表项是否已经被禁用了
  196. return this.innerList[index].disabled;
  197. },
  198. itemIsActive(index){ // 某个列表项是否是被选中的(活动的)
  199. return index === this.activeIndex;
  200. },
  201. // listShow 这个字段的值变化时的回调
  202. onDataChange_listShow(newVal = false, oldVal){
  203. newVal ? this.switchMgr.open() : this.switchMgr.close(100);
  204. },
  205. /************************** 数据 ****************************/
  206. /************************** “输入框”的操作 ****************************/
  207. // 输入框获得焦点时
  208. onFocus(event){
  209. this.focusShowList__ && this.switchMgr.open();
  210. this.$emit('focus', event);
  211. },
  212. // 输入框失去焦点时
  213. onBlur(event){
  214. // 失去焦点时隐藏,在电脑上很好,但在移动端体验不好,因为会弹出数字键盘,然后隐藏键盘时会失去焦点
  215. this.focusShowList__ && this.switchMgr.close(100);
  216. this.$emit('blur', event);
  217. },
  218. //当显示的不是输入框时,上面的点击事件
  219. onUpperClick(){
  220. if(this.disabled){
  221. return;
  222. }
  223. this.switchMgr.toggle('auto', -1, 100);
  224. this.$emit('input-click');
  225. },
  226. //清空已经选择的内容
  227. onClear(){
  228. this.clearItemActive(); // 清空列表框中的所有活动项
  229. this.clearInput(); // 清空输入框中的显示
  230. this.$emit('clear');
  231. },
  232. // 输入框的值变化时
  233. onInput(event){
  234. const inputVal = event.detail.value;
  235. this.changeActiveIndex(inputVal);
  236. this.$emit('input', event);
  237. },
  238. // 清空input中显示的内容
  239. clearInput(placeholder = null){
  240. this.placeholder = placeholder== null ? this.placeholder : placeholder;
  241. this.selectText = this.showInput ? '' : this.placeholder;
  242. },
  243. // 设置input中显示的内容
  244. setInput(text = null){
  245. if(text == null){
  246. return;
  247. }
  248. this.selectText = text;
  249. },
  250. /************************** “输入框”的操作 ****************************/
  251. /************************** 列表的操作(显示/隐藏/点击) ****************************/
  252. /**
  253. * 传入数字表示索引,其它值表示value, 会自动去搜索对应的索引
  254. * 注意:
  255. * 1. 如果没有找到对应的索引,则什么也不会做
  256. * 2. 如果找到了,只会把对应项设置为活动的,并不会清除其它的活动项
  257. */
  258. changeActiveIndex(value_index){ //改变列表中的活动项
  259. if(value_index == null){
  260. return;
  261. }
  262. let activeIndex = value_index, value = value_index;
  263. if(typeof value_index !== 'number'){ //认为是值,否则就是索引
  264. activeIndex = this.getIndex(value); // 搜索对应的值所在的索引
  265. }else{
  266. value = this.innerList[activeIndex].value;
  267. }
  268. if(activeIndex > -1){
  269. !this.itemIsActive(activeIndex) && this.setItemActive(activeIndex, value);
  270. }else{
  271. this.clearItemActive();
  272. }
  273. this.setInput(value); // 更改输入框的值
  274. },
  275. clearItemActive(index = -1){ // 设置为不选中
  276. if(index < 0){ // 清空全部
  277. this.activeIndex = -1;
  278. }
  279. },
  280. setItemActive(index, value){ //选中某一项,必须传入索引和对应的值
  281. if(this.itemIsDisabled(index)){
  282. return;
  283. }
  284. this.activeIndex = index;
  285. },
  286. // 整个列表框上的点击事件
  287. onListClick(){
  288. },
  289. onClickItem(index, value){ // 列表项上的点击事件
  290. if( this.itemIsDisabled(index) ){
  291. this.switchMgr.open(); // 点在禁用项上,就不隐藏
  292. return;
  293. }
  294. this.switchMgr.close(100); // 开始隐藏,因为会延迟隐藏,所以可以写在这里
  295. if(this.disabled){ //如果本项被禁用 或 整个列表框被禁用
  296. return;
  297. }
  298. if( !this.itemIsActive(index) ){ //如果点在非选中项上
  299. this.clearItemActive(); // 清空其它的选中的列表项
  300. this.setItemActive(index, value); // 将这一项设置为选中项
  301. this.$emit('change', {newVal: value, oldVal: this.selectText,
  302. index: index, orignItem: this.list[index]});
  303. this.setInput(value); // 更改输入框的值
  304. }
  305. },
  306. onListHide(){ //列表隐藏时的回调
  307. this.isRotate = false;
  308. this.isShowList = false;
  309. this.$emit('visible-change', false);
  310. },
  311. onListShow(){ //列表显示时的回调
  312. this.isShowList = true;
  313. this.isRotate = true;
  314. this.$emit('visible-change', true);
  315. if(this.selectHideType === 'independent' || this.selectHideType === 'hideSelf'){
  316. return;
  317. }
  318. Vue.__xfl_select.$emit('open', this);
  319. }
  320. /************************** 列表的操作(显示/隐藏/点击) ****************************/
  321. }
  322. }
  323. /************************** uniapp libs ****************************/
  324. /**
  325. * 是否是web的移动端
  326. * @public
  327. * @returns {boolean} true表示当前环境是web,并且是移动端,false表示非web或是pc端
  328. */
  329. function isMobile(){
  330. try{ // 可能不存在window对象
  331. let reg = /iPhone|iPad|iPod|iOS|Android|SymbianOS|Windows Phone|coolpad|mmp|smartphone|midp|wap|xoom|symbian|j2me|blackberry|wince/i;
  332. return reg.test(navigator.userAgent);
  333. }catch(e){
  334. return false;
  335. }
  336. }
  337. /**
  338. * 是否是web的pc端
  339. * @public
  340. * @returns {boolean} true表示当前环境是web,并且是pc端,false表示非web或是移动端
  341. */
  342. function isPC(){
  343. try{ // 可能不存在window对象
  344. let reg = /iPhone|iPad|iPod|iOS|Android|SymbianOS|Windows Phone|coolpad|mmp|smartphone|midp|wap|xoom|symbian|j2me|blackberry|wince/i;
  345. return !reg.test(navigator.userAgent);
  346. }catch(e){
  347. return false;
  348. }
  349. }
  350. /**
  351. * 获取指定元素的样式
  352. * 注意:
  353. * 1. 必须在使用这个函数的文件中 导入 import Vue from 'vue'
  354. * 2. 自定义组件编译模式(默认模式)时, 必须传入component参数。(h5中测试时不管传不传都能正常获取,但wx中必须传入才行)
  355. * @public
  356. * @param {Object|string} options - 配置对象,如果传入一个字符串,则识别为selector
  357. * selector - dom元素的选择器,仅支持以下选择器:
  358. * 1. ID选择器:'#the-id'
  359. 2. class选择器(可以连续指定多个):'.a-class.another-class'
  360. 3. 子元素选择器:'.the-parent > .the-child'
  361. 4. 后代选择器:'.the-ancestor .the-descendant'
  362. 5. 跨自定义组件的后代选择器:'.the-ancestor >>> .the-descendant'
  363. 6. 多选择器的并集:'#a-node, .some-other-nodes'
  364. 7. 传入 'viewport' 表示获取视口对象,有点类似于选中window。
  365. * @param {function|component} [callback=null] - 如果传入一个函数,则识别为获取到样式后的回调,也可以传入一个组件,
  366. 回调的第一个参数如下:
  367. // 获取信息成功时,是对象数组,
  368. // 对象根据options的配置而有不同的字段
  369. {
  370. id: '', // String 节点的 ID, 经测试,这个id并不一定正确(特别是选中多个节点时)
  371. dataset: null, // Object 节点的 dataset
  372. left: 0, // Number 节点的包围盒的左边界坐标(px)(page元素的左上角为坐标原点)
  373. right: 0, // Number 节点的包围盒的右边界坐标(px)
  374. top: 0, // Number 节点的包围盒的上边界坐标(px)
  375. bottom: 0, // Number 节点的包围盒的下边界坐标(px)
  376. width: 0, // Number 节点的宽度(px)
  377. height: 0, // Number 节点的高度(px)
  378. scrollLeft: 0, // Number 节点的水平滚动位置(px)
  379. scrollTop: 0, // Number 节点的竖直滚动位置(px)
  380. context: {} || null, // Object节点对应的Context对象(如VideoContext、CanvasContext、和MapContext)
  381. ... // properties 数组中指定的属性值和computedStyle数组中指定的样式值
  382. }
  383. // 当获取信息失败,则为null
  384. * @param {any} [thisObj=null] 回调中的this, 可能位于第三个参数或第四个参数。
  385. * @return {undefined|promise} 当没有callback时,则返回promise,否则返回undefined
  386. * @example
  387. * 1. 传入选择器,返回promise
  388. * getNodeInfo('#aa').then((data)=>{ console.log(data);});
  389. *
  390. * 2. 传入选择器和component, 返回promise
  391. * getNodeInfo('#aa', this).then((data)=>{ console.log(data);});
  392. *
  393. * 3. 传入选择器和callback, 返回undefined
  394. * getNodeInfo('#aa', (data)=>{ console.log(data);});
  395. *
  396. * 4. 传入配置对象和callback, 返回undefined
  397. * getNodeInfo({selector: '#aa', component: this}, (data)=>{ console.log(data);});
  398. */
  399. function getNodeInfo({
  400. selector = 'selector', // 选择器
  401. component = null, // 选择器所在的组件,不传入的话,相当于是在整个当前页面中选择
  402. attemptSpaceTime = 16, // 尝试获取节点信息的时间间隔(ms): 16 24 36 54 81 122 183 275 413
  403. attemptSpaceRate = 1.5, // 时间间隔的增长系数
  404. totalAttemptNum = 8, // 如果获取信息失败,再次进行尝试获的最大次数
  405. // 以下为获取到的结果字段的配置
  406. id = true, // Boolean 是否返回节点 id
  407. dataset = true, // Boolean 是否返回节点 dataset
  408. rect = true, // Boolean 是否返回节点布局位置(left right top bottom)
  409. size = true, // Boolean 是否返回节点尺寸(width height)
  410. scrollOffset = true, //Boolean 是否返回节点的 scrollLeft scrollTop
  411. // 以下三个 仅 App 和微信小程序支持
  412. properties = [], // Array<string> 指定属性名列表,返回节点对应属性名的当前属性值
  413. // 只能获得组件文档中标注的常规属性值,
  414. // id class style 和事件绑定的属性值不可获取
  415. computedStyle = [], //Array<string>指定样式名列表,返回节点对应样式名的当前值
  416. context = true, // Boolean 是否返回节点对应的 Context 对象
  417. } = {}, callback = null, thisObj = null){
  418. // arguments 始终会记录最原始的传进来的参数,而不管这些默认值会怎么转换
  419. // 因为传入一个对象或非字符串会报错,强制转换为字符串
  420. const args = arguments;
  421. selector = typeof args[0] === 'string' ? args[0] : String(selector);
  422. if(typeof args[1] !== 'function'){
  423. component = args[1]; callback = args[2]; thisObj = args[3];
  424. }
  425. !component instanceof Vue && (component = null); //传入非组件对象,会报错
  426. // 不能把 component 字符添加到这个对象上,否则在wx中会报循环引用的错误
  427. const options = { selector, attemptSpaceTime, totalAttemptNum, attemptSpaceRate,
  428. id, dataset, rect, size, scrollOffset, properties, computedStyle, context };
  429. const selectorQuery = uni.createSelectorQuery();
  430. component && selectorQuery.in(component);
  431. const nodesRef = selector === 'viewport' ? selectorQuery.selectViewport() : selectorQuery.selectAll(selector);
  432. nodesRef.fields(options); // 注意,只注册了这一个命令
  433. let result; // 必须把创建promise的代码放在前面,否则在h5端会出现exec先执行完成的情况
  434. if(typeof callback !== 'function'){
  435. result = new Promise(resolve=>callback = resolve);
  436. }
  437. stepRunFunc((next, currNum)=>{
  438. selectorQuery.exec( ([data]) => { // 开始查询页面中的节点
  439. data && data.length === 0 && (data = null);
  440. data || totalAttemptNum <= currNum ? typeof callback === 'function' && callback.call(thisObj, data) : next(attemptSpaceTime);
  441. attemptSpaceTime = Math.round( attemptSpaceTime * attemptSpaceRate );
  442. });
  443. })(); // 立即执行一次
  444. return result;
  445. }
  446. /************************** uniapp libs ****************************/
  447. /************************** js libs ****************************/
  448. /**
  449. * 开关类,管理两个状态的切换
  450. * 特点是: 状态的切换可能是延迟进行的。
  451. * @class
  452. */
  453. class Switch{
  454. constructor(onopen = null, onclose = null){
  455. this.onopen = onopen; // 打开后的回调
  456. this.onclose = onclose; // 关闭后的回调
  457. this.isOpen = false; // 初始时状态是关闭的
  458. }
  459. toggle(toState = 'auto', ...args){ //切换开关的状态
  460. if( !(toState === 'close' || toState === 'open') ){
  461. toState = this.isOpen ? 'close' : 'open';
  462. }
  463. let delayTime_open, delayTime_close, cancelType_open, cancelType_close;
  464. for(let i=0, arg; i<args.length; i++){
  465. arg = args[i];
  466. switch(typeof arg){
  467. case 'number': delayTime_open == null ? (delayTime_open = arg) : (delayTime_close = arg); break;
  468. case 'string': cancelType_open == null ? (cancelType_open = arg) : (cancelType_close = arg); break;
  469. }
  470. }
  471. const delayTime = toState === 'open' ? delayTime_open : delayTime_close;
  472. const cancelType = toState === 'open' ? cancelType_open : cancelType_close;
  473. this.change(toState, delayTime == null ? -1 : delayTime, cancelType == null ? 'both' : cancelType);
  474. }
  475. open(delayTime = -1, cancelType = 'both'){ // 打开
  476. this.change('open', delayTime, cancelType);
  477. }
  478. close(delayTime = -1, cancelType = 'both'){ // 关闭
  479. this.change('close', delayTime, cancelType);
  480. }
  481. cancel(type = 'both'){ // 取消定时器
  482. if(type === 'open'){
  483. clearTimeout(this.openTimer); this.openTimer = null;
  484. }else if(type === 'close'){
  485. clearTimeout(this.closeTimer); this.closeTimer = null;
  486. }else if(type === 'both'){
  487. clearTimeout(this.closeTimer); this.closeTimer = null;
  488. clearTimeout(this.openTimer); this.openTimer = null;
  489. }
  490. }
  491. change(toState, delayTime = -1, cancelType = 'both' ){ // 改变到指定的状态
  492. this.cancel(cancelType); // 取消定时器
  493. if(this.isOpen && toState === 'open' || !this.isOpen && toState === 'close'){
  494. return;
  495. }
  496. const funcName = 'on' + toState;
  497. if(delayTime < 0){
  498. this.isOpen = toState === 'open';
  499. typeof this[funcName] === 'function' && this[funcName]();
  500. }else{
  501. this[toState + 'Timer'] = setTimeout(()=>{
  502. this.isOpen = toState === 'open';
  503. typeof this[funcName] === 'function' && this[funcName]();
  504. }, delayTime)
  505. }
  506. }
  507. }
  508. /**
  509. * 从一个数组中进行搜索,返回一个索引, 主要特点是可以深层搜索
  510. * 依赖: forEach props 这两个函数
  511. * @public
  512. * @param {Array} arr - 要搜索的数组或类数组或普通对象
  513. * @param {any} searchVal - 要搜索的值
  514. * @param {string|Array} [propPath=''] - 要搜索的值的路径, 如 'aa.bb.cc' 或 ['aa', 'bb', 'cc']
  515. * @param {function} [compareFunc=null] - 比较函数 compareFunc(val, searchVal, arrElem, index, orignArr)
  516. * 省略时,表示进行全等比较。
  517. * @example
  518. * 1. 简单的使用
  519. * searchIndex([1, 2, 3], 2); // => 1
  520. *
  521. * 2. 使用自定义的比较函数
  522. * searchIndex([1, 2, 3], '2', '', (val, searchVal)=>val==searchVal); // => 1
  523. *
  524. * 3. 指定用值的路径
  525. * searchIndex([1, {aa: 3}, {aa: {bb: 3}}, {aa: {bb: 4}], 3, 'aa.bb'); // => 1
  526. */
  527. function searchIndex(arr, searchVal, propPath = '', compareFunc = null){
  528. let result_index= -1;
  529. if(propPath){
  530. if(typeof propPath === 'string'){
  531. propPath = propPath.split(/\s*[\,\.]\s*/);
  532. }else if( !Array.isArray(propPath) ){
  533. propPath = '';
  534. }
  535. }
  536. forEach(arr, (val, index, orignArr)=>{
  537. if(propPath){
  538. val = props(val, propPath);
  539. }
  540. if(
  541. typeof compareFunc === 'function'
  542. ? compareFunc(val, searchVal, arrElem, index, orignArr)
  543. : val === searchVal
  544. ){
  545. result_index = index;
  546. return false;
  547. }
  548. });
  549. return result_index;
  550. }
  551. /**
  552. * 遍历数组或类数组或普通对象
  553. * 跟原生的forEach的差别是: 可以遍历普通对象,也可以中途可以退出。
  554. * 注意,类数组只会遍历其中的数字属性。
  555. * @public
  556. * @param {object|Array} obj - 要遍历的对象
  557. * @param {function} func - 回调 func.call(thisObj, value, prop, obj);
  558. * @param {any} [thisObj=null] - 回调中的this
  559. * @example
  560. * 1. forEach({a: 3, b: 4}, (val, prop, obj)=>{ // 遍历普通对象
  561. * return false; //返回false 表示退出循环
  562. * });
  563. *
  564. * 2. forEach([3, 4], (val, index, obj)=>{ // 遍历数组
  565. * return false; //返回false 表示退出循环
  566. * });
  567. *
  568. * 3. forEach({1: 3, 5: 10, a: 'aa', length: 20}, (val, index, obj)=>{ // 遍历类数组
  569. * return false; //返回false 表示退出循环
  570. * });
  571. */
  572. function forEach(obj, func, thisObj = null) {
  573. if (obj == null || typeof obj === 'function' || typeof func !== 'function') {
  574. return obj;
  575. }
  576. //对象自身的(不含继承的)所有可遍历(enumerable)属性
  577. let keys = Object.keys(obj);
  578. const length = obj.length;
  579. const isArrayLike = typeof length == 'number' && length > -1 && length % 1 == 0 && length <= 9007199254740991;
  580. //如果是类数组或数组,只遍历其中的数字属性
  581. if (isArrayLike) {
  582. const reg = /^(?:0|[1-9]\d*)$/,
  583. maxNum = 9007199254740991,
  584. numPropArr = [];
  585. for (let i = 0, val; i < keys.length; i++) {
  586. val = keys[i];
  587. if (reg.test(val) && +val <= maxNum) {
  588. numPropArr.push(val);
  589. }
  590. }
  591. keys = numPropArr;
  592. }
  593. // 开始遍历所有的数字属性
  594. for (let i = 0; i < keys.length; i++) {
  595. if ( func.call(thisObj, obj[keys[i]], keys[i], obj) === false ) { break; }
  596. }
  597. return obj;
  598. }
  599. /**
  600. * 从一个对象上取指定的属性 或 设置属性的值
  601. * @public
  602. * @param {Object} obj - 对象, 当设置时,会更改这个对象
  603. * @param {Array} propArr - 属性名称的数组,指出要操作的属性的路径
  604. * @param {any} [val=undefined] - 要设置的值 省略时表示获取,否则就是设置
  605. * @param {Boolean} [fource=false] - 在设置时,如果不存在对应的属性,是否创建
  606. * @returns {any|undefined} 设置时一定返回undefined, 获取时,返回对应的值,如果不存在则返回undefined
  607. * @example
  608. * 1. props({}, ['aa', 'bb', 'cc'], 5); // => undefined 什么也没做
  609. * 2. props({}, ['aa', 'bb', 'cc'], 5, true); // => undefined 在空对象上创建了多层属性 {aa: {bb: {cc: 5} }}
  610. * 3. props({}, ['aa', 'bb', 'cc']); // => undefined
  611. * 4. props({aa: {bb: 77}}, ['aa', 'bb']); // => 77
  612. * 5. props({aa: 3}, ['aa', 'bb', 'cc'], 5); // => undefined 什么也没做
  613. * 6. props({aa: 3}, ['aa'], 5); // => undefined 设置了 aa 的值为5
  614. * 7. props({aa: 3}, [], 5); // => undefined 什么也没做
  615. */
  616. function props(obj, propArr, val = undefined, fource = false){
  617. for(let i=0, subObj = obj, len = propArr.length, propName; i<len; i++){
  618. if(!subObj || typeof subObj !== 'object'){
  619. return;
  620. }
  621. propName = propArr[i];
  622. if(i === len -1 ){
  623. if(val === undefined){
  624. return subObj[ propName ];
  625. }else{
  626. subObj[ propName ] = val;
  627. }
  628. }else{
  629. if( !(subObj[ propName ] && typeof subObj[ propName ] === 'object') ){
  630. if(fource && val !== undefined){
  631. subObj[ propName ] = {};
  632. }else{
  633. return;
  634. }
  635. }
  636. subObj = subObj[ propName ];
  637. }
  638. }
  639. }
  640. /**
  641. * 分次执行某个函数
  642. * 使用场景: 异步执行某个操作,这个操作可能会失败,所以当失败时,需要再尝试几次,直到成功或尝试次数用完
  643. * @public
  644. * @param {function} callback - 要执行的函数 callback.call(thisObj, next, currCount, timers)
  645. * @param {any} [thisObj=null] - callback中的this
  646. * @returns {function} 返回next函数,next函数可以传入以下两个参数
  647. * {any} [delayTime=-1] - 延迟多久(ms)再执行下一次callback回调
  648. * 负数、NaN、Infinite表示立即同步调用,其它值表示延迟执行
  649. * {string} [type='both'] - 当调用next时,如果其它地方也调用了next并且还没有完成,此时该保留哪次调用
  650. * 'new' - 保留本次的,清除所有原来的
  651. * 'old' - 保留所有原来的,舍弃本次的
  652. * 'both' - 两个都保留
  653. * @example
  654. * 1. 最简单的使用
  655. * stepRunFunc((next, currCount, timers)=>{
  656. * console.log('执行第' + currCount + '次');
  657. * currCount <= 2 && next(2000);
  658. * })();
  659. * // => 会立即执行第一次,然后2s后再执行第二次
  660. *
  661. * 2. next()函数的第二个参数,是考虑到,用户可能会在短时间内连续调用多次,此时应该怎么处理这些next调用之间的关系
  662. * stepRunFunc((next, currCount, timers)=>{
  663. * console.log('执行第' + currCount + '次');
  664. * if(currCount <= 2 ){
  665. * next(3000);
  666. * setTimeout(()=>{next(1000, 'old')}, 1000); // 这一次next调用将不起作用
  667. * }
  668. * })();
  669. * // => 会立即执行第一次,然后3s后再执行第二次
  670. */
  671. function stepRunFunc(callback, thisObj = null){
  672. const getDelayTime = (delayTime)=>{ // 转换delayTime的格式
  673. delayTime = parseInt(delayTime);
  674. if(isNaN(delayTime) || !isFinite(delayTime)){
  675. delayTime = -1;
  676. }
  677. return delayTime;
  678. }
  679. const timers = []; // 记录所有正在执行的计时器
  680. const clearTimer = (oneTimer)=>{ // 清除定时器
  681. if(oneTimer == null){
  682. for(let i=0; i<timers.length; i++){
  683. clearTimeout(timers[i]);
  684. }
  685. timers.length = 0;
  686. }else{
  687. const index = timers.indexOf(oneTimer);
  688. if(index > -1){
  689. clearTimeout(timers[index]);
  690. timers.splice(index, 1);
  691. }
  692. }
  693. }
  694. let currCount = 0; // 记录callback当前已经执行了的次数
  695. const next = function(delayTime = -1, type = 'both'){
  696. if(type === 'new'){ // 如果只保留最新的next回调
  697. clearTimer();
  698. }else if(type === 'old' && timers.length > 0){ // 保留以前的next回调,忽略本次next回调
  699. return;
  700. }
  701. delayTime = getDelayTime(delayTime);
  702. if(delayTime < 0){
  703. callback.call(thisObj, next, ++currCount, timers);
  704. }else{
  705. const oneTimer = setTimeout(()=>{
  706. clearTimer(oneTimer);
  707. callback.call(thisObj, next, ++currCount, timers);
  708. }, delayTime);
  709. timers.push(oneTimer);
  710. }
  711. }
  712. return next;
  713. }
  714. /************************** js libs ****************************/
  715. </script>
  716. <style scoped lang="less">
  717. @normal-color: #606266; //正常情况下的字体颜色
  718. @hover-color: #c0c4cc; //边框的颜色
  719. @active-color: #409eff; //活动的颜色
  720. @mouse-move-color: #f5f7fa; //在列表项上按下时的列表项的背景色
  721. @padding-left: 5%; //两侧的边距
  722. @arrowWidth: 12%; //右边的小三角按钮区域的宽度
  723. .placeholder11{
  724. color: red; top: 10px;
  725. }
  726. .show-box{
  727. &.active{
  728. border-color: @active-color;
  729. }
  730. // &:hover{
  731. // border-color: @normal-color;
  732. // &.active{
  733. // border-color: @active-color;
  734. // }
  735. // }
  736. &.disabled{
  737. background-color: #f0f0f0;
  738. }
  739. text-align: left;
  740. -webkit-appearance: none;
  741. background-color: #fff;
  742. background-image: none;
  743. border-radius: 4px;
  744. border: 1px solid @hover-color;
  745. box-sizing: border-box;
  746. color: @normal-color;
  747. display: inline-block;
  748. font-size: inherit;
  749. height: 3em;
  750. line-height: inherit;
  751. outline: none;
  752. padding: 0 @arrowWidth 0 @padding-left;
  753. transition: border-color .2s cubic-bezier(.645,.045,.355,1);
  754. width: 100%;
  755. position: relative;
  756. .input{
  757. width: 100%; height: 100%;
  758. display: flex; align-items: center; justify-content: flex-start;
  759. }
  760. .placeholder{
  761. color: #bbb;
  762. }
  763. //*************************** 右侧的小箭头 ***************************
  764. .right-arrow{
  765. &.isRotate{
  766. transform: rotate(180deg);
  767. }
  768. transition: transform .2s cubic-bezier(.645,.045,.355,1);
  769. position: absolute; font-size: 1em; right: 0px; display: flex;
  770. top: 0;
  771. align-items: center; color: @hover-color; height: 100%;
  772. font-weight: 100; width: @arrowWidth; justify-content: center;
  773. }
  774. .clear{
  775. color: #fff; line-height: 1;
  776. background-color: @hover-color; border-radius: 50%; padding: 2px;
  777. }
  778. /****** 列表框部分样式 *****/
  779. .list-container{
  780. position: absolute; width: 100%; left: 0; top: 50px;
  781. box-sizing: border-box; z-index: 100;
  782. //*************************** 弹出框上面的小三角 ***************************
  783. .popper__arrow{
  784. transform: translateX(-400%);
  785. position: absolute;
  786. display: block;
  787. width: 0;
  788. height: 0;
  789. border-color: transparent;
  790. border-style: solid;
  791. border-width: 6px;
  792. filter: drop-shadow(0 2px 12px rgba(0,0,0,.03));
  793. left: 30%;
  794. margin-right: 3px;
  795. border-top-width: 0;
  796. border-bottom-color: #dcdfe6;
  797. top: -5px;
  798. &:after{
  799. content: " ";
  800. border-width: 6px;
  801. position: absolute;
  802. display: block;
  803. width: 0;
  804. height: 0;
  805. border-color: transparent;
  806. border-style: solid;
  807. top: 1px;
  808. margin-left: -6px;
  809. border-top-width: 0;
  810. border-bottom-color: #fff;
  811. }
  812. }
  813. .list{
  814. border-radius: 4px;
  815. border: 1px solid #dcdfe6;
  816. width: 100%;
  817. max-height: 10em;
  818. background-color: #fff;
  819. box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
  820. padding: 5px 0;
  821. //*************************** 弹出框中每一项样式 ***************************
  822. .item{
  823. &:hover{
  824. background-color: @mouse-move-color;
  825. &.disabled{
  826. background-color: transparent;
  827. }
  828. }
  829. &.active{
  830. color: @active-color;
  831. font-weight: 500;
  832. background-color: @mouse-move-color;
  833. }
  834. &.disabled{
  835. color: @hover-color;
  836. }
  837. padding: 0 @padding-left;
  838. line-height: 2;
  839. }
  840. .data-state{
  841. color: @hover-color;
  842. }
  843. }
  844. }
  845. }
  846. //************************************** 以下为字体 ****************************************
  847. @font-face {font-family: "iconfont";
  848. src:
  849. url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAM8AAsAAAAAB1gAAALvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDHAqCEIFsATYCJAMQCwoABCAFhG0HSxthBhEVlKdkPwvsmHgLNqmwEc2pDxvYjI1gkX0f4uFrv9dz3+772RAqQJV8FbKANj5RiB1f1q0ioyorK1THs2Qj0gAJVYn///3mxT27TKyJJ63gD/KkYhr/9woe4ghtLxKJk5AWd7icc+CiJuQLU5SVQ48+ST+l0H2/pM2sm89zOb2VZYHMb1luYy3a0496AWYLKLA9sQ0UaAEFxC2yi7gTF3GaQJtRTbFxcfcIRYYmBeKyjDJQCiFZNrJFaDSszOI11Ep1IQZeRd+P/zAXcip1gmbuHJ/nYeWX9redqtuqPU6AYj4vjHUkNJGJ08bUviQMXtL2m2wJRVHxS/sz/N1+2CZOdizDemP/eBXRgCo7wIKcTvzSUnlmGMoSgt/tChX8EEOBlNvCLsQdpgv8HuNG8wuia9YA1Tfni5TZR1QthTxh8ZM2VCAHtiBtzfWtz1RtObA8IXowr5rzRK4/sRYpfjm1FBA9nrPl/qNAJRZLKJNsUumMKdb3dkIlkqjEtt8VrbNjZgnB48fG1XqNHax98/uI4xs768DFXVceFql2do6594N/t9vl/tw+ZlhKP6ngFjorHQq/AOmpcAlI98L7Pz/KG7P0OqU7+SuqQ7d8OXhYRvZsnLHcTCD4zwpgXfZVyJGzq6byIJiNgyZUaNOGv5ujz885jIPgWkIxOCLYYiRDUkyTmdNErd0CGopltJm1vb5dv3tJ5DDjpYTQ4wMqXT4h6fGZzJwfqA2R/SGlDxGUnsO0o4onyuKUUDLWoDbodPCGuFjE1U9sJispr4r4X6Sxi0IRiZWzD/RIc8wZ56ZkNmAoOLhL56G1ASKFHjWnLXOssmix6UWpDm4nnCJIYqgGlA3oaIFneHMmKp9/Qo2JJVEHqyf9hcio6x0UUjmAfOg9iHUvl4xmjRJjBjBI4IC7NAxZVgBi87Ae0liqHZGIKhluZKD6dH2j+8Jd0AY9MUcVKXLU5I9a6XU7FUcUppMkCss5MAeXmM7a3Q4A') format('woff2'),
  850. url('data:application/x-font-woff;charset=utf-8;base64,d09GMgABAAAAAAM8AAsAAAAAB1gAAALvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDHAqCEIFsATYCJAMQCwoABCAFhG0HSxthBhEVlKdkPwvsmHgLNqmwEc2pDxvYjI1gkX0f4uFrv9dz3+772RAqQJV8FbKANj5RiB1f1q0ioyorK1THs2Qj0gAJVYn///3mxT27TKyJJ63gD/KkYhr/9woe4ghtLxKJk5AWd7icc+CiJuQLU5SVQ48+ST+l0H2/pM2sm89zOb2VZYHMb1luYy3a0496AWYLKLA9sQ0UaAEFxC2yi7gTF3GaQJtRTbFxcfcIRYYmBeKyjDJQCiFZNrJFaDSszOI11Ep1IQZeRd+P/zAXcip1gmbuHJ/nYeWX9redqtuqPU6AYj4vjHUkNJGJ08bUviQMXtL2m2wJRVHxS/sz/N1+2CZOdizDemP/eBXRgCo7wIKcTvzSUnlmGMoSgt/tChX8EEOBlNvCLsQdpgv8HuNG8wuia9YA1Tfni5TZR1QthTxh8ZM2VCAHtiBtzfWtz1RtObA8IXowr5rzRK4/sRYpfjm1FBA9nrPl/qNAJRZLKJNsUumMKdb3dkIlkqjEtt8VrbNjZgnB48fG1XqNHax98/uI4xs768DFXVceFql2do6594N/t9vl/tw+ZlhKP6ngFjorHQq/AOmpcAlI98L7Pz/KG7P0OqU7+SuqQ7d8OXhYRvZsnLHcTCD4zwpgXfZVyJGzq6byIJiNgyZUaNOGv5ujz885jIPgWkIxOCLYYiRDUkyTmdNErd0CGopltJm1vb5dv3tJ5DDjpYTQ4wMqXT4h6fGZzJwfqA2R/SGlDxGUnsO0o4onyuKUUDLWoDbodPCGuFjE1U9sJispr4r4X6Sxi0IRiZWzD/RIc8wZ56ZkNmAoOLhL56G1ASKFHjWnLXOssmix6UWpDm4nnCJIYqgGlA3oaIFneHMmKp9/Qo2JJVEHqyf9hcio6x0UUjmAfOg9iHUvl4xmjRJjBjBI4IC7NAxZVgBi87Ae0liqHZGIKhluZKD6dH2j+8Jd0AY9MUcVKXLU5I9a6XU7FUcUppMkCss5MAeXmM7a3Q4A') format('woff')
  851. }
  852. .iconfont {
  853. font-family: "iconfont" !important;
  854. font-size: 16px;
  855. font-style: normal;
  856. -webkit-font-smoothing: antialiased;
  857. -moz-osx-font-smoothing: grayscale;
  858. }
  859. .iconshanchu1:before {
  860. content: "\e68c";
  861. }
  862. .icongou:before {
  863. content: "\e786";
  864. }
  865. .iconarrowBottom-fill:before {
  866. content: "\e60e";
  867. }
  868. </style>