目前我正在开发一个web app,用以替代公司目前使用的一个Oracle产品(每年要付给Oracle百万元人民币,实在太厉害了,况且Oracle的这款产品并不是很好用,所以老板决定让我开发一个alternative)。要想替代它,必须做到“人无我有,人有我优”。
Oracle的这款产品特性之一是Audit Trail,就是在表单中的每个field后面都有个小箭头,你点击它,它就显示该字段的所有历史记录。
我觉得这种实现方式对用户来说易用性不够好,因为这样显得很“散”,我要了解整条记录的历史,就得依次点击所有的field后面的小箭头。Apple的Time Machine比这个好多了。得,咱也山寨一个Time Machine吧。
先上图:
1. 时间机器入口:
2. 时间机器:
再上代码:
【前端HTML片段】
<div id="id-time-machine-mask" style="z-index:9000;visibility:hidden;position:absolute; top:0px;left:0px;width:100%;height:100%;" >
<div style="position:absolute;top:0px;left:0px;width:100%;height:100%;"><img src="${resource(dir:'images',file:'time-machine.jpg')}" width="100%" style="position:absolute;top:0px;left:0px"/></div>
<div style="position:absolute;left:0px;width:100%;height:100%;color:white;bottom:0px;">
<span style="position:absolute;top:20px;left:20px;"><a href="#" title="Back to SmartCTMS" onclick="SCTMS.dlg.timeMachine.hide();return false;"><<Back</a></span>
<img src="${resource(dir:'images',file:'time-machine-toggle.png')}" style="position:absolute;right:30px;bottom:160px;" onClick="timemachine.forward()" usemap="#controls" />
<map id="controls" name="controls">
<area shape="poly" coords="0,0, 39,7, 22,33, 0,48" onClick="SCTMS.dlg.timeMachine.forward()" />
<area shape="poly" coords="41,9, 73,0, 73,48, 25,35" onClick="SCTMS.dlg.timeMachine.backward()" />
</map>
</div>
</div>
【前端javascript片段】
Ext.ns('SCTMS.dlg');
SCTMS.dlg.TimeMachine = Ext.extend(Ext.util.Observable, {
settings : {
win_size: [1200, 500],
offsetTop: 250, //How far down to start
decay_constant: 0.15 //decay (shrink) of the boxes
}
, timeMachineLoad : function(mid) {
this.mid = mid;
Ext.get('id-time-machine-mask').show();
Ext.get('id-time-machine-loading-indicator').show();
SCTMS.dispatcher.dispatch('timeMachine/countRevisions', {mid:mid}, function(r){
this.revisionCount = r.count;this.visibleWindowCount = r.count;
if(!this.windows) {
this.windows = [];
}
var more = this.revisionCount-this.windows.length;
var _this = this;
for(var i=0;i<more;i++) {
var nw = new Ext.Window({
index: this.windows.length
, animateTarget: 'id-time-machine-mask'
, loaded: false
, border: false
, title:'Revision #'+(this.windows.length+1)
, closable: false
, resizable:false
, draggable:false
, layout : "border"
, items:[new Ext.Panel({region:'center',autoScroll:true})]
, listeners: {'render': function(w){
w.el.on('click', function(evt, el, obj){
_this.visibleWindowCount = w.index + 1;
_this.resizeWindows();
_this.doLoad(w.index);
_this.hideOtherWindows();
});
}}});
this.windows.push(nw);
}
this.resizeWindows({resetTitle:true});
this.doLoad(this.visibleWindowCount-1);
this.hideOtherWindows();
}, this)
}
, resizeWindows : function(A) {
if(!A)A={};
var baseSize = this.settings.win_size;
// change the offsetTop to put it in the center:
this.settings.offsetTop = (document.body.clientHeight-baseSize[1])/2;
var basePosition = [(document.body.clientWidth-baseSize[0])/2, this.settings.offsetTop];
for(var i=0;i<this.visibleWindowCount;i++) {
this.windows[i].show();
var winSize = [baseSize[0] * Math.pow(1-this.settings.decay_constant, (this.revisionCount - (i+this.revisionCount-this.visibleWindowCount) -1)), baseSize[1] * Math.pow(1-this.settings.decay_constant, (this.revisionCount - (i+this.revisionCount-this.visibleWindowCount) -1))];
this.windows[i].setSize(winSize[0], winSize[1]);
this.windows[i].setPosition((document.body.clientWidth-winSize[0])/2, this.settings.offsetTop * Math.pow(1-this.settings.decay_constant, (this.revisionCount - (i+this.revisionCount-this.visibleWindowCount) -1)));
if(A.resetTitle===true) {
var title = 'Revision #'+(i+1);
this.windows[i].setTitle(title);
}
}
}
, backward : function() {
if(this.loading === true)return;this.loading = true;
if(this.visibleWindowCount<this.revisionCount) {
this.visibleWindowCount++;
}
for(var i=this.visibleWindowCount;i<this.revisionCount;i++){this.windows[i].hide();}
this.resizeWindows();
this.doLoad(this.visibleWindowCount-1);
}
, forward : function() {
if(this.loading === true)return;this.loading = true;
if(this.visibleWindowCount>0) {
this.visibleWindowCount--;
}
for(var i=this.visibleWindowCount;i<this.revisionCount;i++){this.windows[i].hide();}
this.resizeWindows();
this.doLoad(this.visibleWindowCount-1);
}
, go2Revision : function(v) {
if(isNaN(v))return;
if(v===-1) {
v = this.revisionCount;
} else if(v<1){
v = 1;
} else if(v>this.revisionCount) {
v = this.revisionCount;
}
this.visibleWindowCount = v;
this.resizeWindows();
this.doLoad(v-1);
this.hideOtherWindows();
}
, hideOtherWindows : function() {
for(var i=this.visibleWindowCount;i<this.windows.length;i++) {
this.windows[i].hide();
}
}
, doLoad : function(index) {
if(index<0 || index>=this.visibleWindowCount){
this.loading = false;
return;
}
this.topWinIndex = index;
if(this.windows[index].loaded===true) {
this.loading = false;
return;
}
log('Loading revision ' + index);
Ext.get('id-time-machine-loading-indicator').show();
this.windows[index].items.items[0].body.load({
url: "rpc/timeMachine/revision",
params: {mid:this.mid, view:true, version:index, fid:'time-machine-form_'+this.mid+'_'+index},
scripts: true,
callback: this.processDataForm, scope: this,
nocache: true
});
}
, processDataForm : function() {
this.windows[this.topWinIndex].loaded = true;
var formId = 'time-machine-form_'+this.mid+'_'+this.topWinIndex;
var F = Ext.get(formId);
if(F) {
log('building time machine form ' + formId);
var frm = GRS.form.FormEngine.buildForm(formId);
var values = GRS.form.FormDataAccessor.getValues(formId);
var dateCreated = values.dateCreated, createdBy = values.createdBy;
var winTitle = 'Revision #'+(this.topWinIndex+1)+' [created by '+createdBy+' on '+dateCreated+']';
if(this.topWinIndex === this.revisionCount - 1) {
winTitle += ' <font color="red">[latested]</font>';
}
this.windows[this.topWinIndex].setTitle(winTitle);
if(this.revisionCount-1===this.topWinIndex) {
var w = new Number(F.getAttribute('_width')), h = new Number(F.getAttribute('_height'));
if(h > 500) {
h = 500;
w += 30;
} else {
w += 15;
//h += 5;
}
// change win size to fit the form
this.settings.win_size = [w, h];
this.resizeWindows();
}
Ext.get('id-time-machine-loading-indicator').hide();
this.loading = false;
}
}
, hide : function() {
for(var i=0;i<this.windows.length;i++){
this.windows[i].hide();
this.windows[i].loaded=false;
}
Ext.get('id-time-machine-mask').hide();
}
});
【后端groovy】
class TimeMachineService {
static transactional = true
def formService
def countRevisions(params) {
def ids = params.mid.split(':')
def mid = ids[0], oid = ids[1]
def clazz = Module.find(id:mid).clazz
[count: clazz.findAll(sort:'dateCreated',all:true){it.descend('id').constrain(oid).like()}.size()]
}
def revision(params) {
def ids = params.mid.split(':')
def mid = ids[0], oid = ids[1]
def clazz = Module.find(id:mid).clazz
def f = new File(params.servletContext.getRealPath("WEB-INF/db/formdesign/${clazz.name}"))
def form = JSON.parse((f.exists()?f.text:null)?:formService.buildFormScaffold(clazz))
def instance = clazz.find(all:true){
it.descend('id').constrain(oid).like()
it.descend('version').constrain(new Integer(params.version))
}
[instance: instance
,form: form, fid:params.fid]
}
}
P.S.:该时间机器的图片资源来自
http://www.jovianskye.com/jsTimeMachineTable/jstimemachine.html和
http://www.techspot.com/gallery/data/500/time_machine_wall.jpg, javascript实现部分参考了
http://www.jovianskye.com/jsTimeMachineTable/jstimemachine.html