会议室选择时间设计
会议室选择时间设计
会议室选择时间,根据效果图来看,样式深度定制。外观第一印象,就像一个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);