Open Aaaaaaaty opened 7 years ago
这次使用react&redux,来模拟了一个购票app,需要关注的是本次全部数据均为mock实现,不涉及后台。同时其中不会涉及react与redux的语法,只关注到一些模拟原生效果的实现理念。没有接触过react的童鞋们可以关注下阮一峰老师的react入门教程,至于redux,redux中文文档上面也有着详细的说明。不过作者对redux也很感兴趣,打算学习一波源码后(如果整个明白了),可能也会出一个分享,届时欢迎前来交流~ #github地址,捂脸求star
本应用全部运行在开发模式下,开启了devserver,没有进行过生产环境测试,如果出现问题大家可以留言~ git clone https://github.com/Aaaaaaaty/react_movie
本应用全部运行在开发模式下,开启了devserver,没有进行过生产环境测试,如果出现问题大家可以留言~
git clone https://github.com/Aaaaaaaty/react_movie
cd react_movie
cnpm i || npm i
将./data及./src/images 文件 拷贝进dist //项目依赖的图片及假数据
npm start
### 重点实现 —— 一个电影选座组件 本次分享的重点是一个基于react的选座组件demo。作者在开发这个组件的时候有观察过微信和支付宝内嵌的影院选座功能。但是无奈看不到代码,一切纯平臆想,说错勿喷。个人感觉微信里面外包的微票儿内的选座模块里面的手势功能为原生浏览器自带的缩放,那么控制上会相对粗暴,缩放上面相对没有支付宝精细。而支付宝上面不仅缩放手感好同时包含了左上方小窗预览功能,可谓用户体验良好(我不是阿里脑残粉hhhhh,虽然事实如此?),所以作者并没有感觉出来这个是混合开发的组件还是原生的还是什么的。。。好了bb了半天,现在轮到作者自己来实现一个了。 ### 效果图 ![选座组件](https://dn-mhke0kuv.qbox.me/247556ed695bcbdb2b91.gif) 很可惜chrome的模拟器下无法演示手势的操作。其实这里面实现了缩放功能,以及在选座界面放大的时候左侧上方的预览图中的红色标示线则会相应的缩小来指出你选中的范围在整个影院中的位置。这次作者使用了react来书写这个组件,所有的移动缩放全部通过js计算,在真机测试中页面会有些许卡顿。不过作者相信如果进行防抖和节流的优化,在手机浏览器中的体验应该可以更优秀一些。 ### 核心思路 1. 按照后端接口mock数据 2. 渲染座位 3. 增加手势操作 4. 管理选座信息 5. 渲染预览小图 ### mock数据
// ./dist/data/filmSeat.json { "seatId":"0000002-1-1", "rowId": 1, //行index "columnId": 1, //列index "xAxis":3, //行绝对定位 "yAxis":1, //列绝对定位 ... "isSold":false //是否卖出(用于渲染座位颜色) }
在这里需要注意的是:行和列的index值与其绝对定位的区别。我们在电影院中座位摆放的地理位置是千奇百怪的,但是索引序号一定是从1到X。从而就有了如上的四个属性。在渲染座位布局的时候一定是采用```xAxis & yAxis ```才能达到展示影厅座位排布的效果。如果还有点懵请看上图的演示中的座位的排布。 ### 渲染座位 在这里我们先假设要渲染一个占设备视口80%宽的区域来摆放我们的座椅。那么由此就会有一个问题就是我们不确定座椅的数量。故座椅的宽是不能定死的(方便起见,让座椅为正方形,宽高相等),即宽度应为 **视口宽*80% / 座椅数量**。 当然如果座椅太少那么就会导致宽太大这种情况这些极端条件如果有兴趣可以后期再进行判断
// ./src/Components/FilmSeat/FilmSeat.js let list = seatList.map((item, index) => { let style = { position: 'absolute', left: ${seatWidth * item.xAxis + seatWidth / 2 }rem, top: ${seatWidth * item.yAxis}rem, // 根据数据中的绝对定位来动态渲染座位位置 width: ${seatWidth}rem } return ( <img key={ 'seatId' + index } style={ style } src={ .\/images\/${isSoldUrl[index]}.png } onTouchTap={ this.changeSeat.bind(this, isSoldUrl, index, item) } className={ styles.seatItem }> // 每个座位都是一张小图 ) })
${seatWidth * item.xAxis + seatWidth / 2 }rem
${seatWidth * item.yAxis}rem
${seatWidth}rem
.\/images\/${isSoldUrl[index]}.png
### 手势操作
// ./src/Components/FilmSeat/FilmSeat.js <div ... onTouchStart={ this.onTouchStart.bind(this) } onTouchMove={ this.onTouchMove.bind(this) } onTouchEnd={ this.onTouchEnd.bind(this) }>
对于手势操作,采用了浏览器的三个原生触摸事件。下面主要说明如何使用react实现一个原生的拖拽效果:
// ./src/Components/FilmSeat/FilmSeat.js onTouchStart(e) { //三个事件均会传入event事件 e.preventDefault() let { left, top... } = this.state ... if(e.touches.length === 1) { //判断是否为一个手指触摸 let startX = e.touches[0].clientX //得到起始横坐标 let startY = e.touches[0].clientY //得到起始纵坐标 state = { startX: startX, startY: startY, lastDisX: left, //记录上一次横轴偏移量 lastDisY: top, //记录上一次纵轴偏移量 ... } } ... this.setState(state) } onTouchMove(e) { e.preventDefault() let { startX, startY ... } = this.state if(e.touches.length === 1) { let moveX = e.touches[0].clientX //记录当前的位置 let moveY = e.touches[0].clientY let disX = moveX - startX + lastDisX //记录现在手指相对屏幕左侧距离 let disY = moveY - startY + lastDisY ... this.setState({ moveX: moveX, moveY: moveY, left: disX, top: disY, }) } else if(e.touches.length === 2) { ... } } onTouchEnd(e) { e.preventDefault() ... //主要做一些拖拽完成之后的判断,重置初始值等等 }
总结来说核心思路是,``` e.touches[0].clientX/Y```可以提供手指在屏幕中的绝对距离,我们滑动中可以记录到滑动了的相对距离。那么在下次滑动前就需要记录下上一次的相对距离,下次滑动时就要加上上次的距离。不然每次重新拖拽就会从0,0点重新开始。 ### 管理选座信息 通过效果图我们可以知道,在组件中同时需要渲染座位的选取,下方弹出/关闭座位信息等效果。虽然效果多样但是基本可以看为两个状态即座位是否选中,这就使用到了redux来作为状态管理。通过redux来抽象出公共状态,让不同的效果渲染都基于同一个状态,从而达到效果联动。
// ./src/Container/FilmChooseSeat.js changeSeatConf(item, isSoldUrl, type) { const { changeFilmBuySeatList } = this.props // 拿到store中传出来的方法 let data = { item: item, //座位信息 isSoldUrl: isSoldUrl, //所有座位颜色列表 type: type } changeFilmBuySeatList(data) } render() { let { filmSeatList, filmBuyList, location } = this.props ... return (
)
} // ./src/Redux/Store/Store.js export const mapStateToProps =(state)=> { return { ... filmSeatList:state.filmChooseSeatReducer.filmSeatList,//电影座位列表 filmBuyList:state.filmChooseSeatReducer.filmBuyList,//电影选座列表 } } export const mapDispatchToProps=(dispatch)=> { return { ... getFilmSeatList:(url,data)=>dispatch(FilmChooseSeatActions.fetchFilmSeatList(url,data)),//获取电影座位列表 changeFilmBuySeatList:(data)=>dispatch(FilmChooseSeatActions.changeFilmBuySeatList(data))//选中座位购票 } }
发起action后,在reducer中改变维护的```filmBuyList ```数组状态,就可以同时渲染好整个界面的变化。
// ./src/Redux/Reducer/FilmChooseSeatReducer.js export const filmBuyList = (state = {item:[],isSoldUrl:{},type:''}, action={})=>{ switch(action.type){ case FilmChooseSeatActions.CHANGE_FILM_BUYSEAT: let _state = Object.assign({}, state) if(action.text.type === 'add') { _state.item.push(action.text.item) } else { let index = _state.item.indexOf(action.text.item) _state.item.splice(index, 1) } _state.isSoldUrl = action.text.isSoldUrl _state.type = action.text.type return _state default: return state } }
### 渲染预览小图 当完成了大图的渲染以及选座状态切换的工作之后,只需要复制一份大图的渲染的那段jsx修改css样式就可以完成一个预览小图。在这期间你不需要做任何事就可以看到小图上面同样会存在选座状态的切换,这就是状态管理的好处。只要你的界面效果和状态进行了绑定,那么在之后的工作中你就不需要再去关注效果而只需要关注状态是否正确即可。在这其中唯一有一点问题的地方是预览图中红色提示框的缩放和大图的缩放是成反比的。大图放大预览图中的红色框应该缩小,同时大图可拖拽的范围应该和红框的移动范围有一个比例系数。在这次的实现中作者用了``` scaleNum```这个状态来控制其缩放的系数,有兴趣的童鞋可以自己尝试一下如何计算一个正确的系数来保证大图和预览图缩放后红框移动距离和大图拖拽范围的匹配。 ## 其他功能组件 ### 区域选择组件 ![区域选择](https://dn-mhke0kuv.qbox.me/bbd349f5eea3aaf661ae) ### 电影列表组件 ![电影列表](https://dn-mhke0kuv.qbox.me/48d55570a6113a5c74bf) ### 电影详情组件 ![电影详情](https://dn-mhke0kuv.qbox.me/8a548a80b0f4a1d9ac36) ### 电影排期组件 ![电影排期](https://dn-mhke0kuv.qbox.me/4e113ca5799e239c89f8) ### 再次广告[github地址](https://github.com/Aaaaaaaty/react_movie),欢迎大家一起交流~~~#另附作者[blog仓库](https://github.com/Aaaaaaaty/Blog),不定期更新
写在最前
这次使用react&redux,来模拟了一个购票app,需要关注的是本次全部数据均为mock实现,不涉及后台。同时其中不会涉及react与redux的语法,只关注到一些模拟原生效果的实现理念。没有接触过react的童鞋们可以关注下阮一峰老师的react入门教程,至于redux,redux中文文档上面也有着详细的说明。不过作者对redux也很感兴趣,打算学习一波源码后(如果整个明白了),可能也会出一个分享,届时欢迎前来交流~ #github地址,捂脸求star
部署
cd react_movie
cnpm i || npm i
将./data及./src/images 文件 拷贝进dist //项目依赖的图片及假数据
npm start
// ./dist/data/filmSeat.json { "seatId":"0000002-1-1", "rowId": 1, //行index "columnId": 1, //列index "xAxis":3, //行绝对定位 "yAxis":1, //列绝对定位 ... "isSold":false //是否卖出(用于渲染座位颜色) }
// ./src/Components/FilmSeat/FilmSeat.js let list = seatList.map((item, index) => { let style = { position: 'absolute', left:
${seatWidth * item.xAxis + seatWidth / 2 }rem
, top:${seatWidth * item.yAxis}rem
, // 根据数据中的绝对定位来动态渲染座位位置 width:${seatWidth}rem
} return ( <img key={ 'seatId' + index } style={ style } src={.\/images\/${isSoldUrl[index]}.png
} onTouchTap={ this.changeSeat.bind(this, isSoldUrl, index, item) } className={ styles.seatItem }> // 每个座位都是一张小图 ) })// ./src/Components/FilmSeat/FilmSeat.js <div ... onTouchStart={ this.onTouchStart.bind(this) } onTouchMove={ this.onTouchMove.bind(this) } onTouchEnd={ this.onTouchEnd.bind(this) }>
// ./src/Components/FilmSeat/FilmSeat.js onTouchStart(e) { //三个事件均会传入event事件 e.preventDefault() let { left, top... } = this.state ... if(e.touches.length === 1) { //判断是否为一个手指触摸 let startX = e.touches[0].clientX //得到起始横坐标 let startY = e.touches[0].clientY //得到起始纵坐标 state = { startX: startX, startY: startY, lastDisX: left, //记录上一次横轴偏移量 lastDisY: top, //记录上一次纵轴偏移量 ... } } ... this.setState(state) } onTouchMove(e) { e.preventDefault() let { startX, startY ... } = this.state if(e.touches.length === 1) { let moveX = e.touches[0].clientX //记录当前的位置 let moveY = e.touches[0].clientY let disX = moveX - startX + lastDisX //记录现在手指相对屏幕左侧距离 let disY = moveY - startY + lastDisY ... this.setState({ moveX: moveX, moveY: moveY, left: disX, top: disY, }) } else if(e.touches.length === 2) { ... } } onTouchEnd(e) { e.preventDefault() ... //主要做一些拖拽完成之后的判断,重置初始值等等 }
// ./src/Container/FilmChooseSeat.js changeSeatConf(item, isSoldUrl, type) { const { changeFilmBuySeatList } = this.props // 拿到store中传出来的方法 let data = { item: item, //座位信息 isSoldUrl: isSoldUrl, //所有座位颜色列表 type: type } changeFilmBuySeatList(data) } render() { let { filmSeatList, filmBuyList, location } = this.props ... return (
} // ./src/Redux/Store/Store.js export const mapStateToProps =(state)=> { return { ... filmSeatList:state.filmChooseSeatReducer.filmSeatList,//电影座位列表 filmBuyList:state.filmChooseSeatReducer.filmBuyList,//电影选座列表 } } export const mapDispatchToProps=(dispatch)=> { return { ... getFilmSeatList:(url,data)=>dispatch(FilmChooseSeatActions.fetchFilmSeatList(url,data)),//获取电影座位列表 changeFilmBuySeatList:(data)=>dispatch(FilmChooseSeatActions.changeFilmBuySeatList(data))//选中座位购票 } }
// ./src/Redux/Reducer/FilmChooseSeatReducer.js export const filmBuyList = (state = {item:[],isSoldUrl:{},type:''}, action={})=>{ switch(action.type){ case FilmChooseSeatActions.CHANGE_FILM_BUYSEAT: let _state = Object.assign({}, state) if(action.text.type === 'add') { _state.item.push(action.text.item) } else { let index = _state.item.indexOf(action.text.item) _state.item.splice(index, 1) } _state.isSoldUrl = action.text.isSoldUrl _state.type = action.text.type return _state default: return state } }