Time Machine and Audit Trail

目前我正在開發一個web app,用以替代公司目前使用的一個Oracle產品(每年要付給Oracle百萬元人民幣,實在太厲害了,況且Oracle的這款產品並不是很好用,所以老闆決定讓我開發一個alternative)。要想替代它,必須做到“人無我有,人有我優”。

Oracle的這款產品特性之一是Audit Trail,就是在表單中的每個field後面都有個小箭頭,你點擊它,它就顯示該字段的所有歷史記錄。
我覺得這種實現方式對用戶來說易用性不夠好,因爲這樣顯得很“散”,我要了解整條記錄的歷史,就得依次點擊所有的field後面的小箭頭。Apple的Time Machine比這個好多了。得,咱也山寨一個Time Machine吧。

先上圖:
1. 時間機器入口:
[img]http://sam-ds-chen.iteye.com/upload/picture/pic/68038/78b361d1-0679-33ca-aa47-0e6dce023195.png[/img]

2. 時間機器:
[img]http://dl.iteye.com/upload/picture/pic/68138/8a3576ae-81a8-3af4-b631-b0a1905835f0.png[/img]

再上代碼:
【前端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" οnclick="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.:該時間機器的圖片資源來自[url]http://www.jovianskye.com/jsTimeMachineTable/jstimemachine.html[/url]和[url]http://www.techspot.com/gallery/data/500/time_machine_wall.jpg[/url], javascript實現部分參考了[url]http://www.jovianskye.com/jsTimeMachineTable/jstimemachine.html[/url]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章