之前:
【未解决】Preact中用spring-picker去实现弹框选择
但是
(1)虽然手动解决了css中样式(calc无效)的问题
(2)但是还是无法解决,什么原因导致了第二次进入页面后,无法消失掉背景的modal页面
所以只能放弃。
所以只能自己去尝试。
然后就是拷贝粘贴代码,一点点调试。
期间,解决了:
【已解决】[eslint] Using this.refs is deprecated. (react/no-string-refs)
和:
【已解决】Preact中布局样式style.less中的calc不生效
然后再去:
【已解决】ReactJS中阻止点击事件不让Modal窗口消失
期间遇到了:
【未解决】ReactJS中css中div中span的水平居中问题
【总结】
然后最后用代码:
ucowsapp/src/components/base-modal/index.js
import { h, Component } from ‘preact’; import ReactCSSTransitionGroup from ‘react-addons-css-transition-group’; import style from ‘./style.less’; export default class BaseModal extends Component { // state = { // isVisible : false // } constructor(props) { super(props); this.modalOverlayOnClick = this.modalOverlayOnClick.bind(this); this.modalOnClick = this.modalOnClick.bind(this); } // componentWillReceiveProps(nextProps) { // this.setState({ // isVisible: nextProps.visible // }); // console.log(`BaseModal componentWillReceiveProps: nextProps.visible=${nextProps.visible}, this.state.isVisible=${this.state.isVisible}`); // } componentDidUpdate() { // console.log(`BaseModal componentDidUpdate: this.modalOverlay=${this.modalOverlay}`); // if (this.modalOverlay && !this.modalOverlay.onclick) { // // 点击阴影背景时cancel() popup // this.modalOverlay.onclick = (e) => { // e.stopPropagation(); // this.props.onCancel && this.props.onCancel(); // }; // // 点击modal阻止默认行为 // // 原理:react event listener中无法阻止原生事件,所以用原生事件来替代react事件 // this.modal.onclick = (e) => e.stopPropagation(); // console.log(`modal onclick stopPropagation`); // } } modalOverlayOnClick(e){ // e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); // console.log(`BaseModal modalOverlayOnClick:`); // console.log(e); // console.log(this.props.onCancel); if (this.props.onCancel) { this.props.onCancel(); } } modalOnClick(e){ // e.preventDefault(); e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); // console.log(`BaseModal modalOnClick:`); // console.log(e); } // <div class={style.modal_overlay} ref={(mol) => {this.modalOverlay = mol;}}> // <div class={style.modal} ref={(m) => {this.modal = m;}}> // <div class={style.modal_overlay} onClick={this.props.onCancel}> // <div class={style.modal} onClick={this.props.onCancel}> // <div class={style.modal} onClick={this.omitModalClick}> // omitModalClick(e){ // console.log(`omitModalClick: e=${e}`); // } render () { let modal = null; // if (this.state.isVisible) { if (this.props.visible) { modal = ( <div class={style.modal_overlay} onClick={this.modalOverlayOnClick}> <div class={style.modal} onClick={this.modalOnClick}> {this.props.children} </div> </div> ); } return ( <ReactCSSTransitionGroup transitionName="modal-transition" transitionEnterTimeout={120} transitionLeaveTimeout={120}> { modal } </ReactCSSTransitionGroup> ); } } |
/src/components/base-modal/style.less
.modal_overlay { position: fixed; top: 0; left: 0; bottom:0; z-index: 1000; background-color:rgba(11,11,11,0.4); width: 100%; .modal { position: fixed; top: calc(~"100% – 260px"); left: 0; bottom: 0; right: 0; z-index: 1001; background-color: #fff; width: 100%; height: 260px; } } // .modal-transition-enter { // @include fadeIn; // } // .modal-transition-leave { // @include fadeOut; // } // .modal-transition-enter .modal { // @include transitionBottomToUpFn(‘260’,calc(100% – 260px)); // } // .modal-transition-leave .modal { // @include transitionUpToBottomFn(‘260’,calc(100% – 260px)); // } |
src/components/picker/index.js
import { h, Component } from ‘preact’; import _ from ‘lodash’; import style from ‘./style.less’; import cx from "classnames"; const getIndex = (list, item) => { if (list && list.length < 1) { return 0; } let index1 = _.findIndex(list, item); let index2 = list.indexOf(item); let index = Math.max(index1, index2); if (index < 0) { throw new Error(‘list数组中不存在defaultValue’); } return index; }; export default class Picker extends Component { state = { isModalOpen: false, dataList: [ { "name" : "", "value" : 0 } ] }; constructor(props) { super(props); this.startY = 0; this.endY = 0; //当前拖动的Y坐标 this.currentY = 0; this.itemHeight = 36; this.selectedIndex = this.getInitialIndex(); this.state = {style: {}}; this._defaultValue = null; } componentWillReceiveProps(nextProps) { const isEqual = _.isEqual( nextProps.data.defaultValue, this._defaultValue ); if (!isEqual) { this._defaultValue = nextProps.data.defaultValue; this.selectedIndex = this.getReceivePropsIndex(nextProps.data); if (this.selectedIndex === 0) { this.state = { style: { transform: `translate3d(0px, ${this.itemHeight * 2}px, 0px)` } }; } } } componentDidMount () { this.setSelectedValue(this.selectedIndex); } handleWrapperStart (e) { e.preventDefault(); } // 初始化获得selectedIndex getInitialIndex() { let index = getIndex( this.props.data.list, this.props.data.defaultValue ); if (!this.props.data.defaultValue && this.props.data.list.length > 3) { index = Math.floor(this.props.data.list.length / 2); } return index; } getReceivePropsIndex (data) { if (this._defaultValue) { this.selectedIndex = getIndex( data.list, data.defaultValue ); } return this.selectedIndex; } getInitialStyle () { this.currentY = 0; if (this.selectedIndex > 2) { this.currentY = – (this.selectedIndex – 2) * this.itemHeight; } else { this.currentY = (2 – this.selectedIndex) * this.itemHeight; } return `translate3d(0px, ${ this.currentY }px, 0px)`; } handleTouchStart (e) { e.preventDefault(); if (this.props.data.list.length <= 1) { return; } this.startY = e.nativeEvent.changedTouches[0].pageY; } handleTouchEnd (e) { e.preventDefault(); if (this.props.data.list.length <= 1) { return; } this.endY = e.nativeEvent.changedTouches[0].pageY; // 实际滚动距离 let v = parseInt(this.endY – this.startY); let value = v % this.itemHeight; // 计算出每次拖动的36px整倍数 this.currentY += (v – value); // 正数y最大值 const max1 = 2 * this.itemHeight; // 负数y最小值 const max2 = (this.props.data.list.length – 3) * this.itemHeight; if (this.currentY > max1) { this.currentY = max1; } else if (this.currentY > 0 && this.currentY < max1) { this.currentY = this.currentY; } else if (this.currentY === max1) { this.currentY = this.currentY; } else if (Math.abs(this.currentY) > max2) { this.currentY = – max2; } this.countListIndex(this.currentY); this.setState({ style: { transform: `translate3d(0px, ${ this.currentY }px, 0px)` } }); } handleTouchMove (e) { e.preventDefault(); if (this.props.data.list.length <= 1) { return; } const pageY = e.nativeEvent.changedTouches[0].pageY; let value = parseInt(pageY – this.startY); const y = this.currentY + value; let style = `translate3d(0px, ${ y }px, 0px)`; this.setState({ style: { transform: style } }); } // 计算list数组索引 countListIndex (pageY) { let n = pageY / this.itemHeight; n = n > 0 ? 2 – n : Math.abs(n) + 2; this.setSelectedValue(n); } // set选中值 setSelectedValue (index) { const length = this.props.data.list.length; if (length === 0) { this.callback(null); return; } if (index < 0 || index > length -1) { throw new Error(‘滑动取值索引数值出现错误’+ index); } const value = this.props.data.list[index]; this.selectedIndex = index; this.callback(value); } // 回调 callback (value) { this.props.onChange(value); } getSelectedClass (index) { if (this.selectedIndex === index) { return ‘ui_picker_item_selected’; } return ”; } // showCurrentItem(index, displayValue){ // let selectedItemStyles = cx(style.ui_picker_item, style.ui_picker_item_selected); // console.log(selectedItemStyles); // if (this.selectedIndex === index) { // return <div key={index} // class={selectedItemStyles}> // {displayValue} // </div>; // } // return <div key={index} // class={style.ui_picker_item}> // {displayValue} // </div>; // } render () { const curStyle = { transform: this.getInitialStyle() }; let selectedItemStyles = cx(style.ui_picker_item, style.ui_picker_item_selected); return ( <div class={style.ui_picker_wrapper} onTouchStart={this.handleWrapperStart.bind(this)}> <div class={style.ui_picker} style = {this.state.style.transform ? this.state.style : curStyle} onTouchStart={this.handleTouchStart.bind(this)} onTouchMove={this.handleTouchMove.bind(this)} onTouchEnd = {this.handleTouchEnd.bind(this)}> { this.props.data.list.map((data, index) => { const displayValue = this.props.data.displayValue(data); if (this.selectedIndex === index) { return <div key={index} class={selectedItemStyles}> {displayValue} </div>; } return <div key={index} class={style.ui_picker_item}> {displayValue} </div>; }) } </div> <div class={style.ui_picker_center} /> </div> ); } } |
/src/components/picker/style.less
@itemHeight: 36px; .ui_picker_wrapper { width: 100%; height: 180px; overflow: hidden; position: relative; pointer-events: auto; margin-top: 18px; touch-action: none; .ui_picker { width: 100%; transition: transform; transition-duration: .3s; transition-timing-function: ease-out; } .ui_picker_item { height: @itemHeight; line-height: @itemHeight; text-align: center; color: #999; font-size: 18px; white-space:nowrap; text-overflow:ellipsis; overflow:hidden; } .ui_picker_item_selected { color: #000; } .ui_picker_center { height: @itemHeight; box-sizing: border-box; position: absolute; left: 0; width: 100%; top: 50%; z-index: 100; margin-top: -18px; pointer-events: none; border-top: 1px solid #d7d7d7; border-bottom: 1px solid #d7d7d7; } } |
/src/components/popup/index.js
import { h, Component } from ‘preact’; import style from ‘./style.less’; import BaseModal from ‘../base-modal’; export default class Popup extends Component { state = { // modalVisible : false, centerTitle : "" } constructor(props) { super(props); this.state.centerTitle = this.props.centerTitle; this.handleCancel = this.handleCancel.bind(this); this.handleConfirm = this.handleConfirm.bind(this); } // componentWillReceiveProps(nextProps) { // this.setState({ // modalVisible: nextProps.visible // }); // console.log(`Popup componentWillReceiveProps: nextProps.visible=${nextProps.visible}, this.state.modalVisible=${this.state.modalVisible}`); // } handleCancel () { console.log(`handleCancel: this.props.onCancel=${this.props.onCancel}`); if (this.props.onCancel) { this.props.onCancel(); } // this.setState({modalVisible : false}); } handleConfirm() { console.log(`handleConfirm: this.props.onConfirm=${this.props.onConfirm}`); if (this.props.onConfirm) { this.props.onConfirm(); } } // /** // * 使用原生事件替代react事件 // * 当BaseModal modal使用原生事件来阻止冒泡时 // * 完成与取消按钮的react onClick会失效,所以使用原生事件而不使用react事件 // */ // componentDidUpdate () { // console.log(`Popup componentDidUpdate: this.confirmButton=${this.confirmButton},this.cancelButton=${this.cancelButton}`); // if (this.confirmButton && !this.confirmButton.onclick) { // this.confirmButton.onclick = (e) => { // e.stopPropagation(); // this.handleConfirm(); // }; // this.cancelButton.onclick =(e) => { // e.stopPropagation(); // this.handleCancel(); // }; // } // } // <span ref={(cancelBtn) => {this.cancelButton = cancelBtn;}}>{cancelTitle}</span> // <span ref={(confirmBtn) => {this.confirmButton = confirmBtn;}}>{confirmTitle}</span> // visible={this.state.modalVisible}> render () { let cancelTitle = ‘取消’; let confirmTitle = ‘完成’; return ( <BaseModal onCancel={this.handleCancel} visible={this.props.visible}> <div class={style.ui_popup_title}> <span onClick={this.handleCancel}>{cancelTitle}</span> <p>{this.state.centerTitle}</p> <span onClick={this.handleConfirm}>{confirmTitle}</span> </div> <div class={style.ui_popup_content}> {this.props.children} </div> </BaseModal> ); } } |
src/components/popup/style.less
.ui_popup_title { height: 44px; font-size: 17px; background: #ccc; padding: 0 10px; line-height: 44px; > span:first-child { display: inline-block; float: left; color: #007aff; // width: 60px; // text-align: center; // position: absolute; // left: 0; // top: 0; } > span:last-child { display: inline-block; float: right; color: #007aff; // width: 60px; // text-align: center; // position: absolute; // right: 0; // top: 0; } > p { margin-left: 80px; top: 0px; bottom: 0px; // left: 60px; // right: 60px; height: 44px; display: inline-block; // height: 0.84rem; text-align: center; // align: center; // line-height: 0.84rem; // font-size: 0.32rem; color: #272727; font-weight: bold; // position: relative; } } .ui_popup_content { height: 216px; width: 100%; overflow: hidden; -webkit-overflow-scrolling : touch; } |
src/container/estrus-management/index.js
import style from ‘./style.less’; import Popup from ‘../../components/popup’; import Picker from ‘../../components/picker’; export default class EstrusManagement extends Component { state = { pickerVisible: false, selectedValue: {name: ‘确认发情’, value: 2}, }; userData = { list: [ {name: ‘确认发情’, value: 2}, {name: ‘未发情’, value: 3} ], defaultValue: this.state.selectedValue, displayValue(item) { return item.name; } }; constructor(props) { super(props); autoBind(this); this.cancelPick = this.cancelPick.bind(this); this.confirmPick = this.confirmPick.bind(this); this.onPickChange = this.onPickChange.bind(this); this.fetchPending(); } confirmPick() { console.log("confirmPick"); this.setState({pickerVisible: false}); console.log(this.state.selectedValue); const url = `/faqing/faqing/updateStatus`; console.log(url); let postJson = { "id": this.state.activeItem.current_id, "status": this.state.selectedValue.value }; console.log(postJson); alert(JSON.stringify(postJson)); this.props.fetch( url, { method : "POST", headers : { ‘Content-Type’: ‘application/json’, ‘Accept’: ‘application/json’ }, body: JSON.stringify(postJson) }, (data) => { console.log(data); this.setState({ activeSegment: 1}); this.fetchDisposed(); } ); } cancelPick() { console.log("cancelPick"); this.userData.defaultValue = {}; this.setState({ pickerVisible: false, selectedValue: {} }); } onPickChange(data) { console.log("onPickChange"); data = data || {}; this.userData.defaultValue = data; this.setState({selectedValue: data}); console.log(this.state.selectedValue); } render() { const { totalPending, activeSegment } = this.state; return ( 。。。 <div> <Popup onCancel={this.cancelPick.bind(this)} onConfirm={this.confirmPick.bind(this)} centerTitle={"请选择处理状态"} visible={this.state.pickerVisible}> <Picker onChange={this.onPickChange} data={this.userData} /> </Popup> </div> |
实现了对应的效果:
注:
现在有个小问题:
对于modal弹出框中间的标题来说,对其不是绝对的居中
只是此处left的距离是写死的。
后期再想办法优化。