会议室选择时间设计

会议室选择时间设计

会议室选择时间,根据效果图来看,样式深度定制。外观第一印象,就像一个Grid表格。初选方案有两种:grid、dataview。

方案分析

第一种方案,grid,默认的数据都是按行来显示,而效果图则是竖向显示,每天的情况按列显示。不管是grid、dataview都跟数据离不开关系。如果是竖向的展示,那么grid
来显示,还需要将数据横竖方向进行转换,这个难度也大,加上grid表格的是深度定制的样式,选择grid来做,困难很大。
第二种方案,dataview样式可以轻松定制,这个是自定义样式并要处理大量数据的法宝之一。数据的显示的横竖向,都可以通过css样式来定义,而且html骨架都是可控的。基于此,更倾向于选择dataview。

dataview方案深度剖析

  • html骨架选择,考虑到grid本身就是使用table来完成布局的。所以优向考虑使用table来布每一列的显示样式。
  • 在数据来源方面,有两种选择,ajax请求,store来处理。使用store处理的时候,更倾向于后台处理好所有的逻辑,前台负责显示。如果选择store来作为数据源,那么,应当在后台处理所有的逻辑。
  • 如果使用ajax获取数据,那么可以在每次获取数据之后,手动的将数据loadData到store中。考虑到样式,基本上可以看到时间单位基本上为30分钟为粒度。所以在前台处理,也是可行的。(如果考虑减轻后台计算压力的话,那么应该优先选择此方案。而且用户可能在时间选择上操作也比较多,计算在前端确实能省不少事。)
  • 从大的方向来说,也有两种方案来选择,一种时,表格的每一列都是一个record。另外一种是,将每一列进行抽象,作为一个组件来处理。目前,难度不可知,暂不分析。先尝试做。
  • 在选择模型上说,由于选择时间,要选择开始时间、结束时间,属于多选,如果使用原生的选择模型的话,考虑使用multi模式,即多选模式,在细节上进行如下处理:选择不合理,如跨天、跨越占用时间段,默认的将第一个选择清理掉,然后将新的加入进去。(这个跟手机端的设计有差别,手机段是自己要连续选择每一个时间段。)
  • 另外,如何翻页?是否设置专门的翻页处理函数?最好是使用record来隐藏记录,那么这又有一个问题,record隐藏之后,record的粒度是按天还是按时段?按天的话,是否真有必要抽象出按天时间端的组件?

数据格式

选择的时间

[时间1、时间2]

每天的每个具体的时间段,即时间的最小粒度,一条记录。

//record
[时间序列号、date、当前时间段内的可用状态,会议状态信息等,当前是否被选择。]

## 选择

最终觉得当作一个整体来处理,可以更加直接。每一列作为一条record来处理。会议的信息,附加在上面。翻页等于是过滤要见的范围。

时间不可选择,有两种情况,1、时间已过期,2、时间被占用。

选择时,可用时间应经常刷新时间。另外,页面加载的时候,也应该刷新。

后续功能

如用户不但能查看当前选择的会议室,还能查看未选择的会议室的使用情况,如果使用满意,可以直接点击确定,更换会议室。功能看似简单,但是要加很多的业务处理。前期先不考虑。

快速实现

先利用静态数据,快速搭建界面

界面分为左右两个dataview,左侧的为时间段展示区,右侧为最近几天的时间,会议占用情况。布局hbox,允许overflow。右侧的时间端区域,布局为div下table布局,每天的记录,为一个记录,封装在一个div>table里面,这个div是向左浮动的。因为向左浮动了,所以,如果overflow之后,会将一行展示的内容,挤到多行显示,效果非常差。所以就固定右侧dataview的宽度,保证其有充足的宽度。然后就能完全显示。进一步处理,后期如果有时间,可以在dataview的onreize事件中,监听整体的大小,动态的决定右侧的dataview要加载多少条记录。

快速搭建业务

-

  • 对外:与外界的互动,主要分为接收外界消息,初始化选择阶段,将选择记录传递给外界,发消息阶段。另外,要防止初始化阶段重复,或者在修改阶段,被会议室的初始化消息干扰段。
  • 对内:内部事件,主要处理点击事件,分为左右翻页处理。(业务相对简单,点击触发ajax加载数据,但是需要计算时间占用、时间过期,进行标志)点击时间记录事件:1、点击无效点(时间过期)。直接返回;2、点击跨越点,跨越占用的会议、跨越天。先清除第一个选择点。3、有效点,当前是否已选择了其他点。添加进去。

上述对内业务处理完毕后,同步到内部的viewModel,然后将消息发送出去。
在快速搭建过程,由于上述过程较多,先快速打通整个业务,然后再完善细节。

1、快速生成右侧的时间。
暂未增加时间冲突、时间过期处理,预留有字段。
2、快速的处理点击事件。

选择 0 - 1 -2 -0

关于时间冲突上面,突然又难住我了。

如果判断一个时间段在一个区间内?首先,两个区间的端点值肯定都是按顺序排列的。
端点是否允许重复?
突然发现,判断两个区间不重叠更容易些。

时间段1的最大值 >  时间段2的最小值。
时间段1的最小值 <  时间段2的最大值。

上述比较,如果都成立,那么则两个时间端不重叠。
思想来源于两个小车模型,一个后面的车追向一个前面的车的过程。相遇则上上面的情况取反。

持续改进

  • 打通,能顺利通过初始化工作,加载修改数据。时间端过滤该会议、绑定到特定的会议室。

主要的工作就是,接受初始化参数,保留要一些必要的会议室的数据。另外,每次选择好时间时,发送消息,保存结果。另外,再发一次消息,告知商品选择模块,选择的时间变化了。

  • 2、顶部结构优化。使table的头能随着页面滚动而固定位置浮动显示。
  • 3、添加点击会议占用、不可用时间的提示消失。 (完成,主要是判断有没有纠正时间,所以需要记录翻页前的日期,然后比对纠正后的结果,如果发现,没有纠正(即一致),则提示消息,并直接返回。不加载数据。)
  • 4、优化代码,让increment起作用 完成。(之前代码已经充分考虑到这个功能,所以基本上没有任何改动,便能胜任)

代码片段

下面的代码的思想,来源于小车追赶其他车辆的模型。虽然里面的if 循环嵌套较复杂。如果理解了,整体代码应该也比较好懂了。

 /************
 * 2019年1月15日
 * 时间冲突判断。
 ************/
isOccupy:function(items,start,stop){
    var me=this;
    var item;
    var fn=function(v){
        return Ext.Date.parse(v,'Y-m-d H:i:s');
    };
    if(items.length>0){
        item=items[0];
        while(start>=fn(item.stoptime)){
            items.shift();
            if(items.length==0){
                return false;
            }
            item=items[0];
        }
        if(stop<=fn(item.starttime)){
            return false;
        }
        return true;
    }
    return false;
},

页面数据重刷机制

ajaxGetOccupy数据刷新的机制。本来ajaxGetOccupy只在initComponent方法里面直接调用。但是接口连通之后,放在meetingorderhomeaddevent初始化事件中,更合理。但是考虑到每次激活该页面的时候,数据都应该刷新一次。所以又将此方法移到active事件中。但是要等到active的时候,数据才开始加载一次,第一次显示的时候特别的丑,没有任何数据,表格都没有完全建立。所以呢,原来的仍保留一份。

initComponent:function(){//先执行
        var me = this;
        me.applySelectTimePanel();
        me.callParent();
        //添加会议时,设置默认项
        ShineMessageHub.on("meetingorderhomeaddevent",function(rdData){
            me.meetingdate = rdData['meeting_date'];
            me.starttime = rdData['starttime'];
            me.stoptime = rdData['stoptime'];
            me.conference_room_id = rdData['conference_room_id'];
            me.meeting_id = rdData['meeting_id'];
            me.ajaxGetOccupy();//仍保留一份,避免直到activate才加载数据,页面太丑了。啥都没有。
        },me);
        //更改会议室,不在这个里面更新数据,是因为这个地方刷新太频繁了。没有必要。
        ShineMessageHub.on('selectroompanelselectionchange',function(roomid){
            me.conference_room_id=roomid;
        },me);
        me.makeTimeList();
        me.displayStart=Ext.Date.format(new Date(),'Y-m-d');
    },
    listeners:{
        "afterrender":function(cmp,opt){
            var me = this;
            var stData=Ext.Array.map(me.timeList,function(v){
                return {period:v+'-'+me.timeAdd(v,me.increment)};
            });
            var st=me.down('dataview').store;
            st.timeAdd=me.timeAdd;
            st.loadData(stData);
        },
        'activate':function(cmp, eOpts){//这个地方才是最好的刷新时机。
            this.ajaxGetOccupy();
        },
        'storeloaddata':function(dataview,store){
            var me=this;
            var indexs=[me.getTimeListIndex(me.starttime),me.getTimeListIndex(me.stoptime)];
            store.each(function(rd){
                if(rd.get('meetingdate')==me.meetingdate){
                    me.setSelectStyle(rd,indexs);
                }
            });
        }
    },

至此代码基本上完成。

/******************
 * 2019年1月11日
 * 选择时间
 * "meeting_date":"",//开会日期
 * "starttime":"",//开始时间
 * "stoptime":"",//结束时间
 ******************/
Ext.define('DesktopOrder.view.meetingorder.SelectTimePanel', {
    extend: 'Ext.panel.Panel',
    xtype:"selecttimepanel",
    layout:"hbox",
    scrollable:true,
    minValue:'08:00',//设置当前的最早时间
    maxValue:'22:00',//设置当前的最晚时间
    increment:30,//设置时间的增长步长
    timeList:null,//左侧显示的时间段
    displayStart:'',//起始展示日期 2019-01-14形式
    displayLength:6,//展示最近6天的
    orderLimit:15,//允许预定在多少天内的。
    servertime:null,//服务器时间
    conference_room_id:0,//会议室id
    meeting_id:0,//会议id
    conference_room_name:'',//会议室名称
    conference_room_cost:'',//会议室费用

    meetingdate:'',//已选日期
    starttime:'',//开始时间
    stoptime:'',//结束时间
    idx:-1,//上次选择
    requires: [
        'Ext.window.Toast'
    ],
    constructor:function(config){//后执行
		var me = this;
        me.superclass.constructor.call(me, config); 
    },
    initComponent:function(){//先执行
        var me = this;
        me.callParent();
        //添加会议时,设置默认项
        ShineMessageHub.on("meetingorderhomeaddevent",function(rdData){
            me.meetingdate = rdData['meeting_date'];
            me.starttime = rdData['starttime'];
            me.stoptime = rdData['stoptime'];
            me.conference_room_id = rdData['conference_room_id'];
            me.meeting_id = rdData['meeting_id'];
            me.ajaxGetOccupy();
        },me);
        //更改会议室
        ShineMessageHub.on('selectroompanelselectionchange',function(roomid,screen_type,template_id,data){
            me.conference_room_id=roomid;
            me.conference_room_name=data.conference_room_name;
            me.conference_room_cost=data.conference_room_cost;
        },me);
        me.makeTimeList();
        me.displayStart=Ext.Date.format(new Date(),'Y-m-d');
    },
    listeners:{
        "afterrender":function(cmp,opt){
            var me = this;
            var stData=Ext.Array.map(me.timeList,function(v){
                return {period:v+'-'+me.timeAdd(v,me.increment)};
            });
            var st=me.down('dataview').store;
            st.timeAdd=me.timeAdd;
            st.loadData(stData);
        },
        'activate':function(cmp, eOpts){
            this.ajaxGetOccupy();
        },
        'storeloaddata':function(dataview,store){
            var me=this;
            var indexs=[me.getTimeListIndex(me.starttime),me.getTimeListIndex(me.stoptime)];
            store.each(function(rd){
                if(rd.get('meetingdate')==me.meetingdate){
                    me.setSelectStyle(rd,indexs);
                }
            });
        }
    },
    tbar:[{
        xtype:'label',
        text:'查看会议室使用情况',//请更改对应的设置
        flex:1,
        style: {
            color:'black',
            fontSize:'20px',
            lineHeight:'20px'
        }
    },{
        xtype:'label',
        text:'表示已被占用',//请更改对应的设置
        width:120,
        style: {
            color:'red',
            background:'url(resources/meetingorder/images/time/circle.png) no-repeat  0px 0px/16px 16px;',
            textAlign:'center'
        }
    },{
        xtype:'label',
        text:'空白表格表示该会议室可以预定',//请更改对应的设置
        width:270,
        style: {
            color:'black',
            textAlign:'center'
        }
    }],
    items:[{
        xtype:'dataview',
        width:125,
        itemSelector:'tr.selecttimepanelperioditemCls',
        store:{
            fields:[
                {name:"period",type:"string"}
            ],
        },
        tpl:[
            '<div class="selecttimepanel-period-box">',
                '<table>',
                    '<tr><th>时间|日期</th></tr>',
                    '<tpl for=".">',
                    '<tr class="selecttimepanelperioditemCls">',
                        '<td>{period}</td>',
                    '</tr>',
                    '</tpl>',
                '</table>',
            '<div>',
        ],
    },{
        xtype:'dataview',
        itemSelector:'div.selecttimepanelchoseitemCls',
        store:{
        },
        tpl:[
            '<div class="selecttimepanel-right-box">',
                '<div class="arrow left-arrow"></div>',
                '<tpl for=".">',
                '<div class="selecttimepanelchoseitemCls"><table>',
                    '<tr><th>{displaydate}</th></tr>',
                    '<tpl for="timelist">',
                    '<tr>',
                        '<td dataref={index} {[values.selected?"class=selecttimepanel-selected":""]} {[values.status==1?"class=selecttimepanel-expired":""]} {[values.status==2?"class=selecttimepanel-occupy":""]}></td>',
                    '</tr>',
                    '</tpl>',
                '</table></div>',
                '</tpl>',
                '<div class="arrow right-arrow"></div>',
            '</div>',
        ],
        listeners:{
            'containerclick':function(cmp, e, eOpts){
                var me=this.up('selecttimepanel');
                //添加左右箭头的点击事件
                var clickClass=e.target.className.trim();
                if(clickClass==='arrow left-arrow'){
                    me.turnPage(-1);
                }
                if(clickClass==='arrow right-arrow'){
                    me.turnPage(1);
                }
            },
            'itemclick':function(cmp, record, item, index, e, eOpts){
                var me=this.up('selecttimepanel');
                var idx=e.target.getAttribute('dataref');
                var cell;
                if(idx){//点击表格
                    idx=parseInt(idx);
                    cell=record.get('timelist')[idx];
                    //无效点,直接返回
                    if(cell['status']==1||cell['status']==2){
                        Ext.toast('当前时间段不可用。');
                        return ;
                    }
                    //跨越点,先清除上次选择
                    if(me.meetingdate){
                        if(me.meetingdate!==record.get('meetingdate')){
                            me.idx=-1;
                            me.setUnSelectStyleByDate(me.meetingdate);
                            me.meetingdate=record.get('meetingdate');
                        }
                        if(me.idx>-1){
                            var i=Math.min(me.idx,idx);
                            var tmp=record.get('timelist');
                            while(++i<Math.max(me.idx,idx)){
                                if(tmp[i]['status']){
                                    me.idx=-1;
                                }
                            }
                        }
                    }
                    //有效点
                    var indexs=me.idx>idx?[idx,me.idx]:[me.idx,idx];
                    console.log(indexs);
                    me.setSelectStyle(record,indexs);
                    me.meetingdate=record.get('meetingdate');

                    if(indexs[0]>-1){//已选择两个点
                        me.idx=-1;
                        me.starttime=me.timeList[indexs[0]];
                        me.stoptime=me.timeAdd(me.timeList[indexs[1]],me.increment);
                    }else{//已选择一个点
                        me.idx=idx;
                        me.starttime=me.timeList[idx];
                        me.stoptime=me.timeAdd(me.timeList[idx],me.increment);
                    }
                    //保存选择
                    ShineMessageHub.fireEvent('meetingordermenusetviewmodelevent',{
                        meeting_date:record.get('meetingdate'),
                        starttime:me.starttime,
                        stoptime:me.stoptime
                    });
                    //发送数据变化消息
                    var count=Math.ceil(me.timeDiff(me.starttime,me.stoptime)/30)*0.5;
                    var o={commodity_id:-1,commodity_name:me.conference_room_name,commodity_price:me.conference_room_cost,count:count};
                    ShineMessageHub.fireEvent('selecttimepanelchangetime',o);
                }
            } 
        }
    }],
    /************
     * 2019年1月16日
     * 计算时间差,string 08:00   返回分钟
     ************/
    timeDiff:function(min,max){
        max=Ext.Date.parse('2018-07-18 '+max,"Y-m-d H:i");
        min=Ext.Date.parse('2018-07-18 '+min,"Y-m-d H:i");
        return Ext.Date.diff(min,max,Ext.Date.MINUTE);
    },
    /************
     * 2019年1月14日
     * 时间相加,单位分钟
     * @params v string 08:00  interval 分钟
     ************/
    timeAdd:function(v,interval){
        var st=Ext.Date.parse('2018-07-18 '+v,"Y-m-d H:i");
        var ret=Ext.Date.add(st,Ext.Date.SECOND,interval*60);
        return Ext.Date.format(ret,'H:i');
    },
    /************
     * 2019年1月14日
     * 日期相加 
     * @params v string 2019-01-14
     * @params interval 天
     ************/
    dateAdd:function(v,interval){
        var ret=Ext.Date.add(Ext.Date.parse(v,'Y-m-d'),Ext.Date.DAY,interval);
        return Ext.Date.format(ret,'Y-m-d');
    },
    /************
     * 2019年1月15日
     * 解析字符串,并转换格式
     * @params v string 2019-01-14  为1月14日
     ************/
    dateFormat:function(v,format){
        if(!format){
            format='m月d日';
        }
        var ret=Ext.Date.parse(v,'Y-m-d');
        return Ext.Date.format(ret,format);
    },
    /************
     * 2019年1月14日
     * 生成时间序列
     ************/
    makeTimeList:function(){
        var me=this;
        var start=Ext.Date.parse('2018-07-18 '+me.minValue,"Y-m-d H:i");
        var end=Ext.Date.parse('2018-07-18 '+me.maxValue,"Y-m-d H:i");
        var cur=start;
        var ret=[];
        while(cur<=end){
            ret.push(Ext.Date.format(cur,'H:i'));
            cur=Ext.Date.add(cur,Ext.Date.MINUTE,me.increment);
        }
        ret.pop();
        console.log(ret);
        me.timeList=ret;
    },
    /************
     * 2019年1月14日
     * Ajax获取当前会议室时间段占用情况
     ************/
    ajaxGetOccupy:function(){
        var me=this;
        //计算显示结束时间
        var displayStop=me.dateAdd(me.displayStart,me.displayLength);
        console.log(me.displayStart);
        console.log(displayStop);
		Ext.Ajax.request({
			url: SHINEVMSHTTP +'/admin/desktop/meetingorder/getoccupy',
			params:{"startdate":me.displayStart,"stopdate":displayStop,meeting_id:me.meeting_id,conference_room_id:me.conference_room_id},
			success: function(response, opts) {
                var obj = Ext.decode(response.responseText);
                me.makeLoadData(obj.items,obj.servertime);
			},
			failure: function(response, opts) {
                Ext.Msg.hide();
				Ext.Msg.show({
					title:"操作提示",
					msg:"编辑失败,请检查网络!",
					icon:Ext.Msg.ERROR,
					buttons:Ext.Msg.OK,
					fn:function(){
						me.store.reload();						
					}
				});
			}
		}); 
    },
    /************
     * 2019年1月14日
     * 为store生成loadData数据
     ************/
    makeLoadData:function(items,servertime){
        var me=this;
        var data=[];
        var rd;
        var curDate;
        var svTime=new Date(servertime);
        me.servertime=svTime;//保存服务器时间
        for(var i=0;i<me.displayLength;i++){
            rd={};//生成每一条记录
            curDate=me.dateAdd(me.displayStart,i);
            rd['meetingdate']=curDate;
            rd['displaydate']=me.dateFormat(curDate);//格式化当前日期
            rd['timelist']=Ext.Array.map(me.timeList,function(v,i){
                //计算时间是否过期 status 1时间过期 2被占用
                var start=Ext.Date.parse(curDate+' '+v,'Y-m-d H:i');
                var stop=Ext.Date.add(start,Ext.Date.MINUTE,30);
                var status=start<svTime?1:0;
                //计算时间是否占用
                if(status===0){
                    status=me.isOccupy(items,start,stop)?2:0;
                }
                return {st:v,status:status,index:i,selected:false};
            });
            //保存结果
            data.push(rd);
        }
        console.log(data);
        //store加载数据
        me.storeLoadData(data);
    },
    /************
     * 2019年1月15日
     * 时间冲突判断。
     ************/
    isOccupy:function(items,start,stop){
        var me=this;
        var item;
        var fn=function(v){
            return Ext.Date.parse(v,'Y-m-d H:i:s');
        };
        if(items.length>0){
            item=items[0];
            while(start>=fn(item.stoptime)){
                items.shift();
                if(items.length==0){
                    return false;
                }
                item=items[0];
            }
            if(stop<=fn(item.starttime)){
                return false;
            }
            return true;
        }
        return false;
    },
    /************
     * 2019年1月15日
     * store loaddate
     ************/
    storeLoadData:function(data){
        var me=this;
        var dv=me.items.items[1];
        var st=dv.store;
        st.loadData(data);
        me.fireEvent('storeloaddata',dv,st);
    },
    /************
     * 2019年1月15日
     * 日期翻页 
     ************/
    turnPage:function(direction){
        var me=this;
        var direction=direction>0?1:-1;
        var pass=me.displayStart;
        me.displayStart=me.dateAdd(me.displayStart,direction*me.displayLength);
        var cur=Ext.Date.parse(me.displayStart,'Y-m-d');
        //负向纠正值
        if(direction<0&&cur<me.servertime){
            me.displayStart=Ext.Date.format(me.servertime,'Y-m-d');
            if(me.displayStart==pass){
                Ext.toast('不允许选择过去的时间段');
                return ;
            }
        }
        //正向纠正值
        var limit=Ext.Date.add(me.servertime,Ext.Date.DAY,me.orderLimit-me.displayLength+1);//最大可预定几天内的
        if(direction>0&&cur>limit){
            me.displayStart=Ext.Date.format(limit,'Y-m-d');
            if(me.displayStart==pass){
                Ext.toast('只允许预约'+me.orderLimit+'之内的会议。');
                return ;
            }
        }
        me.ajaxGetOccupy();
    },
    /************
     * 2019年1月15日
     * 设置选择样式
     ************/
    setSelectStyle:function(record,indexs){
        var me=this;
        if(indexs[0]==-1){
            for(var i=0;i<me.timeList.length;i++){
                record.data.timelist[i].selected= (i==indexs[1]);
            }
        }else{
            for(var i=0;i<me.timeList.length;i++){
                record.data.timelist[i].selected= (i>=indexs[0]&&i<=indexs[1]);
            }
        }
        record.commit();
    },
    /************
     * 2019年1月15日
     * 设置取消样式
     ************/
    setUnSelectStyleByDate:function(meetingdate){
        var me=this;
        var dv=me.items.items[1];
        var st=dv.store;
        st.each(function(rd){
            if(rd.get('meetingdate')===meetingdate){//找到当前记录
                for(var i=0;i<me.timeList.length;i++){
                    rd.data.timelist[i].selected= false;
                }
                rd.commit();
                return false;
            }
        });
    },
    /************
     * 2019年1月16日
     * 获取timeList中的index
     ************/
    getTimeListIndex:function(time){
        var me=this;
        for(var i=0;i<me.timeList.length;i++){
            if(time==me.timeList[i]){
                return i;
            }
        }
        return me.timeList.length+10;
    }
});

代码改进:

将发送给商品模块的消息独立成函数

这样在时间选择、会议室变化之后,都会发送消息。

/************
 * 2019年1月17日
 * 发送消息给商品管理模块
 ************/
sendMsgToProduct:function(){
    var me=this;
    var count=Math.ceil(me.timeDiff(me.starttime,me.stoptime)/30)*0.5;
    var o={commodity_id:-1,commodity_name:me.conference_room_name,commodity_price:me.conference_room_cost,count:count};
    ShineMessageHub.fireEvent('selecttimepanelchangetime',o);
}

    //更改会议室
    ShineMessageHub.on('selectroompanelselectionchange',function(roomid,screen_type,template_id,data){
        me.conference_room_id=roomid;
        me.conference_room_name=data.conference_room_name;
        me.conference_room_cost=data.conference_room_cost;
        if(me.starttime&&me.stoptime){
            me.sendMsgToProduct();//如果已经选择时间了,在这块,将会议室更改的消息中转出去。
        }
    },me);