基于react的影院购票应用

写在最前

这次使用react&redux,来模拟了一个购票app,需要关注的是本次全部数据均为mock实现,不涉及后台。同时其中不会涉及react与redux的语法,只关注到一些模拟原生效果的实现理念。没有接触过react的童鞋们可以关注下阮一峰老师的react入门教程,至于redux,redux中文文档上面也有着详细的说明。不过作者对redux也很感兴趣,打算学习一波源码后(如果整个明白了),可能也会出一个分享,届时欢迎前来交流~ #github地址,捂脸求star

部署

本应用全部运行在开发模式下,开启了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了半天,现在轮到作者自己来实现一个了。

效果图

选座组件

很可惜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 }></img> // 每个座位都是一张小图
    )
})

手势操作

// ./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  (
              <div>
                <FilmSeatTitle location={ location }/>
                <FilmSeat filmSeatList={ filmSeatList } //选座拖拽区域
                          filmBuyList={ filmBuyList }
                          animationTime={ 200 }
                          changeSeatConf={ this.changeSeatConf.bind(this) }/> 
                          //通过这个函数将组件中事件传递到container中,
                          //由container发起action来进行改变state
                <FilmSeatSale filmBuyList= { filmBuyList } //选座信息
                              filmSeatList={ filmSeatList }
                              changeSeatConf={ this.changeSeatConf.bind(this) }/>
              </div>
            )
  }
// ./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这个状态来控制其缩放的系数,有兴趣的童鞋可以自己尝试一下如何计算一个正确的系数来保证大图和预览图缩放后红框移动距离和大图拖拽范围的匹配。

其他功能组件

区域选择组件

区域选择

电影列表组件

电影列表

电影详情组件

电影详情

电影排期组件

电影排期

再次广告github地址,欢迎大家一起交流~~~#另附作者blog仓库,不定期更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容