Time Machine and Audit Trail

目前我正在开发一个web app,用以替代公司目前使用的一个Oracle产品(每年要付给Oracle百万元人民币,实在太厉害了,况且Oracle的这款产品并不是很好用,所以老板决定让我开发一个alternative)。要想替代它,必须做到“人无我有,人有我优”。

Oracle的这款产品特性之一是Audit Trail,就是在表单中的每个field后面都有个小箭头,你点击它,它就显示该字段的所有历史记录。
我觉得这种实现方式对用户来说易用性不够好,因为这样显得很“散”,我要了解整条记录的历史,就得依次点击所有的field后面的小箭头。Apple的Time Machine比这个好多了。得,咱也山寨一个Time Machine吧。

先上图:
1. 时间机器入口:
Time Machine and Audit Trail_第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;">&lt;&lt;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.htmlhttp://www.techspot.com/gallery/data/500/time_machine_wall.jpg, javascript实现部分参考了 http://www.jovianskye.com/jsTimeMachineTable/jstimemachine.html

你可能感兴趣的:(JavaScript,oracle,windows,ext,groovy)