天天看點

手機端大檔案上傳和斷點續傳的實作

目前,我們正處于一個視訊爆炸的時代,高清智能手機人人都有,各種場景下的視訊應用越來越多,而且視訊分辨率和視訊尺寸越來越大(1080P高清已經普及,4K視訊也即将普及),而且我們又需要和他人之間進行視訊分享,這樣就常常遇到大檔案上傳和斷點續傳的問題。因為檔案過大(比如2GB以上),必須要考慮上傳過程網絡中斷的情況,需要做到斷點續傳,否則我們就需要再次重新上傳。是以我們在手機端上傳内容的時候,需要将大檔案進行分片,比如分成1024*1024B,即将大檔案分成1M的片進行上傳,伺服器端接收後,再将這些分片檔案合并成原始檔案,這就是分片的基本原理。http的網絡請求中已經具備了分片上傳功能,當傳輸的檔案比較大時,http協定自動會将檔案切片(分塊),但這不是我們現在說的重點,我們要做的事是保證在網絡中斷後2G的檔案已上傳的那部分在下次網絡連通時不必再重傳。斷點續傳要求本地要記錄每一片的上傳的狀态,我通過三個狀态進行了标記(wait loading finish),當網絡中斷,再次連接配接後,從斷點處進行上傳。伺服器通過檔案名、總片數判斷該檔案是否已全部上傳完成。

而且,對于大的并發應用而言,我們的上傳伺服器必須支援數百或者數千使用者同時上傳,這樣才能滿足營運級的需要。

為了通用性更強,采用安裝特定的浏覽器上傳插件方式并不可取,最好的實作方式就是通過浏覽器直接上傳,目前已經有很多基于PHP或者Java程式來實作的上傳伺服器,然而在實際使用中你會會發現,這些基于腳本語言實作的上傳功能子產品性能很弱,一是不支援2GB以上的檔案上傳;二是無法支援斷點續傳;三是效率極低,單台伺服器最多支援幾十個并發上傳連接配接,沒有一個可以真正商用的系統。

是以,為了解決這個問題,我們花費了很大精力用C++語言自主實作了這一高性能上傳伺服器,在用戶端隻要調用一段 js 代碼即可,這是一個通用的上傳服務子產品,可以快速內建到第三方應用平台中。

項目位址:

https://github.com/liufeihong/Hyper-Upload-Server

手機端大檔案上傳和斷點續傳的實作
手機端大檔案上傳和斷點續傳的實作
手機端大檔案上傳和斷點續傳的實作
手機端大檔案上傳和斷點續傳的實作

Hyper Upload Server 超級上傳伺服器簡介:

這是一款超級檔案上傳伺服器,采用異步I/O架構,采用C++語言編碼實作。它支援4GB以上超大檔案上傳和斷點續傳,支援Windows和Linux伺服器平台,支援任意格式的檔案上傳,尤其适合大的視訊網站應用。單台伺服器支援1000并發上傳程序,支援PC端和智能手機端主流的浏覽器。

主要特性

1. 伺服器端采用異步I/O架設設計,具有高性能I/O處理能力,尤其适用于超大檔案上傳;

2. 伺服器端采用高效記憶體配置設定技術確定在運作過程中伺服器的記憶體開銷最小化;

3. 完全采用标準協定實作,是以相容幾乎所有的PC端和移動端浏覽器;

4. 伺服器端采用C++語言自主實作,對上傳檔案的尺寸無限制,天生支援超大檔案上傳。

   而基于PHP、JAVA等技術實作的檔案上傳服務天生無法支援超大檔案上傳,無法逾越2GB的最大檔案尺寸瓶頸;

5. 伺服器端采用無緩沖即時寫入方式,上傳資料寫入一步到位。不同于PHP、JAVA等技術實作方式需要兩步寫入;

6. 伺服器端可跨平台編譯運作,支援Windows和Linux平台;

7. 高性能,單台伺服器支援1000個并發上傳程序;

8. 支援4GB以上超大檔案上傳,檔案大小不受限制;

9. 用戶端支援采用HTTP标準協定上傳;

10.支援斷點續傳,斷網、關機重新開機均不受影響;

11.支援HTML5浏覽器上傳進度實時顯示;

12.支援IE8及以上浏覽器上傳進度顯示;

13.支援檢視用戶端線上連接配接, 檢視方法: http://ip:port/lists

14.多浏覽器相容,包括Chrome,Firefox,Safari,IE,Opera,Edge;

 上傳代碼實作

;

(function($){

var rotateLeft = function(lValue, iShiftBits) {

return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));

}

var addUnsigned = function(lX, lY) {

    var lX4, lY4, lX8, lY8, lResult;

    lX8 = (lX & 0x80000000);

    lY8 = (lY & 0x80000000);

    lX4 = (lX & 0x40000000);

    lY4 = (lY & 0x40000000);

    lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);

    if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8);

    if (lX4 | lY4) {

    if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);

    else return (lResult ^ 0x40000000 ^ lX8 ^ lY8);

    } else {

    return (lResult ^ lX8 ^ lY8);

    }

}

var F = function(x, y, z) {

    return (x & y) | ((~ x) & z);

}

var G = function(x, y, z) {

    return (x & z) | (y & (~ z));

}

var H = function(x, y, z) {

    return (x ^ y ^ z);

}

var I = function(x, y, z) {

    return (y ^ (x | (~ z)));

}

var FF = function(a, b, c, d, x, s, ac) {

    a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac));

    return addUnsigned(rotateLeft(a, s), b);

};

var GG = function(a, b, c, d, x, s, ac) {

    a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac));

    return addUnsigned(rotateLeft(a, s), b);

};

var HH = function(a, b, c, d, x, s, ac) {

    a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac));

    return addUnsigned(rotateLeft(a, s), b);

};

var II = function(a, b, c, d, x, s, ac) {

    a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac));

    return addUnsigned(rotateLeft(a, s), b);

};

var convertToWordArray = function(string) {

    var lWordCount;

    var lMessageLength = string.length;

    var lNumberOfWordsTempOne = lMessageLength + 8;

    var lNumberOfWordsTempTwo = (lNumberOfWordsTempOne - (lNumberOfWordsTempOne % 64)) / 64;

    var lNumberOfWords = (lNumberOfWordsTempTwo + 1) * 16;

    var lWordArray = Array(lNumberOfWords - 1);

    var lBytePosition = 0;

    var lByteCount = 0;

    while (lByteCount < lMessageLength) {

        lWordCount = (lByteCount - (lByteCount % 4)) / 4;

        lBytePosition = (lByteCount % 4) * 8;

        lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));

        lByteCount++;

    }

    lWordCount = (lByteCount - (lByteCount % 4)) / 4;

    lBytePosition = (lByteCount % 4) * 8;

    lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);

    lWordArray[lNumberOfWords - 2] = lMessageLength << 3;

    lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;

    return lWordArray;

};

var wordToHex = function(lValue) {

    var WordToHexValue = "", WordToHexValueTemp = "", lByte, lCount;

    for (lCount = 0; lCount <= 3; lCount++) {

    lByte = (lValue >>> (lCount * 8)) & 255;

    WordToHexValueTemp = "0" + lByte.toString(16);

    WordToHexValue = WordToHexValue + WordToHexValueTemp.substr(WordToHexValueTemp.length - 2, 2);

    }

    return WordToHexValue;

};

var uTF8Encode = function(string) {

    string = string.replace(/\x0d\x0a/g, "\x0a");

    var output = "";

    for (var n = 0; n < string.length; n++) {

    var c = string.charCodeAt(n);

    if (c < 128) {

    output += String.fromCharCode(c);

    } else if ((c > 127) && (c < 2048)) {

    output += String.fromCharCode((c >> 6) | 192);

    output += String.fromCharCode((c & 63) | 128);

    } else {

    output += String.fromCharCode((c >> 12) | 224);

    output += String.fromCharCode(((c >> 6) & 63) | 128);

    output += String.fromCharCode((c & 63) | 128);

    }

    }

    return output;

};

$.extend({

md5: function(string) {

var x = Array();

var k, AA, BB, CC, DD, a, b, c, d;

var S11=7, S12=12, S13=17, S14=22;

var S21=5, S22=9 , S23=14, S24=20;

var S31=4, S32=11, S33=16, S34=23;

var S41=6, S42=10, S43=15, S44=21;

string = uTF8Encode(string);

x = convertToWordArray(string);

a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;

for (k = 0; k < x.length; k += 16) {

AA = a; BB = b; CC = c; DD = d;

a = FF(a, b, c, d, x[k+0], S11, 0xD76AA478);

d = FF(d, a, b, c, x[k+1], S12, 0xE8C7B756);

c = FF(c, d, a, b, x[k+2], S13, 0x242070DB);

b = FF(b, c, d, a, x[k+3], S14, 0xC1BDCEEE);

a = FF(a, b, c, d, x[k+4], S11, 0xF57C0FAF);

d = FF(d, a, b, c, x[k+5], S12, 0x4787C62A);

c = FF(c, d, a, b, x[k+6], S13, 0xA8304613);

b = FF(b, c, d, a, x[k+7], S14, 0xFD469501);

a = FF(a, b, c, d, x[k+8], S11, 0x698098D8);

d = FF(d, a, b, c, x[k+9], S12, 0x8B44F7AF);

c = FF(c, d, a, b, x[k+10], S13, 0xFFFF5BB1);

b = FF(b, c, d, a, x[k+11], S14, 0x895CD7BE);

a = FF(a, b, c, d, x[k+12], S11, 0x6B901122);

d = FF(d, a, b, c, x[k+13], S12, 0xFD987193);

c = FF(c, d, a, b, x[k+14], S13, 0xA679438E);

b = FF(b, c, d, a, x[k+15], S14, 0x49B40821);

a = GG(a, b, c, d, x[k+1], S21, 0xF61E2562);

d = GG(d, a, b, c, x[k+6], S22, 0xC040B340);

c = GG(c, d, a, b, x[k+11], S23, 0x265E5A51);

b = GG(b, c, d, a, x[k+0], S24, 0xE9B6C7AA);

a = GG(a, b, c, d, x[k+5], S21, 0xD62F105D);

d = GG(d, a, b, c, x[k+10], S22, 0x2441453);

c = GG(c, d, a, b, x[k+15], S23, 0xD8A1E681);

b = GG(b, c, d, a, x[k+4], S24, 0xE7D3FBC8);

a = GG(a, b, c, d, x[k+9], S21, 0x21E1CDE6);

d = GG(d, a, b, c, x[k+14], S22, 0xC33707D6);

c = GG(c, d, a, b, x[k+3], S23, 0xF4D50D87);

b = GG(b, c, d, a, x[k+8], S24, 0x455A14ED);

a = GG(a, b, c, d, x[k+13], S21, 0xA9E3E905);

d = GG(d, a, b, c, x[k+2], S22, 0xFCEFA3F8);

c = GG(c, d, a, b, x[k+7], S23, 0x676F02D9);

b = GG(b, c, d, a, x[k+12], S24, 0x8D2A4C8A);

a = HH(a, b, c, d, x[k+5], S31, 0xFFFA3942);

d = HH(d, a, b, c, x[k+8], S32, 0x8771F681);

c = HH(c, d, a, b, x[k+11], S33, 0x6D9D6122);

b = HH(b, c, d, a, x[k+14], S34, 0xFDE5380C);

a = HH(a, b, c, d, x[k+1], S31, 0xA4BEEA44);

d = HH(d, a, b, c, x[k+4], S32, 0x4BDECFA9);

c = HH(c, d, a, b, x[k+7], S33, 0xF6BB4B60);

b = HH(b, c, d, a, x[k+10], S34, 0xBEBFBC70);

a = HH(a, b, c, d, x[k+13], S31, 0x289B7EC6);

d = HH(d, a, b, c, x[k+0], S32, 0xEAA127FA);

c = HH(c, d, a, b, x[k+3], S33, 0xD4EF3085);

b = HH(b, c, d, a, x[k+6], S34, 0x4881D05);

a = HH(a, b, c, d, x[k+9], S31, 0xD9D4D039);

d = HH(d, a, b, c, x[k+12], S32, 0xE6DB99E5);

c = HH(c, d, a, b, x[k+15], S33, 0x1FA27CF8);

b = HH(b, c, d, a, x[k+2], S34, 0xC4AC5665);

a = II(a, b, c, d, x[k+0], S41, 0xF4292244);

d = II(d, a, b, c, x[k+7], S42, 0x432AFF97);

c = II(c, d, a, b, x[k+14], S43, 0xAB9423A7);

b = II(b, c, d, a, x[k+5], S44, 0xFC93A039);

a = II(a, b, c, d, x[k+12], S41, 0x655B59C3);

d = II(d, a, b, c, x[k+3], S42, 0x8F0CCC92);

c = II(c, d, a, b, x[k+10], S43, 0xFFEFF47D);

b = II(b, c, d, a, x[k+1], S44, 0x85845DD1);

a = II(a, b, c, d, x[k+8], S41, 0x6FA87E4F);

d = II(d, a, b, c, x[k+15], S42, 0xFE2CE6E0);

c = II(c, d, a, b, x[k+6], S43, 0xA3014314);

b = II(b, c, d, a, x[k+13], S44, 0x4E0811A1);

a = II(a, b, c, d, x[k+4], S41, 0xF7537E82);

d = II(d, a, b, c, x[k+11], S42, 0xBD3AF235);

c = II(c, d, a, b, x[k+2], S43, 0x2AD7D2BB);

b = II(b, c, d, a, x[k+9], S44, 0xEB86D391);

a = addUnsigned(a, AA);

b = addUnsigned(b, BB);

c = addUnsigned(c, CC);

d = addUnsigned(d, DD);

}

var tempValue = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);

return tempValue.toLowerCase();

}

});

})(jQuery); 

function toast(msg,duration)

{

    var toastDiv = document.getElementById('hyupload-toast');

    if (msg) {

        toastDiv.style.display = 'block';

        toastDiv.innerHTML = msg;

        duration = parseInt(duration);

        if (!duration) duration = 3000;

        if (!window.uploadtoast) {

            window.clearTimeout(window.uploadtoast);

        }

        window.uploadtoast = window.setTimeout(function() {

            toastDiv.style.display = 'none';

            delete window.uploadtoast;

        },duration);

    }

    else {

        toastDiv.style.display = 'none';

        toastDiv.innerHTML = '';

    }

}

Array.prototype.in_array=function(e){

    var S=String.fromCharCode(2);

    var r=new RegExp(S+e+S);

    return (r.test(S+this.join(S)+S));

};

var HYFileUploader = function (options) {

    var _options = {

        filelist:'hyupload-filelist',

        notice_url:'',

        savepaths:{'video':'.mp4,.mkv,.avi,.wmv','document':'.zip,.rar','image':'.jpg,.jpeg,.png,.gif','audio':'.aac,.mp3'},

        ontaskfinish:function(taskobj) {

        },

        ontaskprogress:function(taskobj) {

        },

        ontaskprogress:function(progress) {

        },

        ontaskstart:function(taskobj) {

            return true;

        }

    };

    $.extend(_options, options);

    //簡單的Cookie幫助函數

    var setCookie = function(cname,cvalue,exdays) {

        var d = new Date();

        d.setTime(d.getTime()+(exdays*24*60*60*1000));

        var expires = "expires="+d.toGMTString();

        document.cookie = cname + "=" + cvalue + "; " + expires;

    };

    var getCookie = function(cname) {

        var name = cname + "=";

        var ca = document.cookie.split(';');

        for(var i=0; i<ca.length; i++) 

        {

            var c = ca[i].trim();

            if (c.indexOf(name)==0) return c.substring(name.length,c.length);

        }

        return "";

    };

    //

    //簡單的檔案HASH值計算,如果您不是十分考究,應該可以用于産品。

    //由于計算檔案HASH值用到了多種資料,是以在HYFileUploader系統範圍内發生HASH沖突的可能性應該非常小,應該可以放心使用。

    //擷取檔案的ID可以用任何算法來實作,隻要保證做到同一檔案的ID是相同的即可,擷取的ID長度不要超過32位元組

    //

    var getFileId = function (file) {

        //給浏覽器授予一個唯一的ID用于區分不同的浏覽器執行個體

        var clientid = getCookie("HUAYIUPLOAD");

        if (clientid == "") {

            //用一個随機值來做浏覽器的ID,将作為檔案HASH值的一部分

            var rand = parseInt(Math.random() * 1000);

            var t = (new Date()).getTime();

            clientid =rand+'T'+t;

            setCookie("HUAYIUPLOAD",clientid,365);

        }

        var info = clientid;

        if (file.lastModified)

            info += file.lastModified;

        if (file.name)

            info += file.name;

        if (file.size)

            info += file.size;

        //https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.min.js

        var fileid = $.md5(info);

        return fileid;

    };

    var get_file_mimetype = function(filename) {

        var mimeType;

        var extPos = filename.lastIndexOf('.');

        if (extPos != -1) {

            var extName = filename.substr(extPos).toLowerCase();

            if (extName == '.rm' || extName == '.rmvb') {

                mimeType = 'video/realvideo';

            }

            else if (extName == '.dat') {

                mimeType = 'video/vcd';

            }

            else if (extName == '.ra') {

                mimeType = 'audio/realaudio';

            }

        }

        if (!mimeType) mimeType = 'bin/unknown';

        return mimeType;

    };

    //savepaths:{'video':'.mp4,*.mkv,.avi,.wmv','document':'.zip,*.rar','image':'.jpg,.jpeg,.png,.gif','audio':'.aac,.mp3'}

    var get_save_path = function(filename,savepaths) {

        var extPos = filename.lastIndexOf('.');

        if (extPos != -1) {

            var extName = filename.substr(extPos).toLowerCase();

            for (path in savepaths) {

                var extList = savepaths[path].split(',');

                if (extList.indexOf(extName) != -1) {

                    return path;

                }

            }

        }

        return '';

    }

    var bytesToSize = function (bytes) {

        if (bytes === 0) return '0 B';

        if (bytes < 1024) return (bytes + 'B');

        var k = 1024,sizes = ['B','KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

        var i = Math.floor(Math.log(bytes) / Math.log(k));

        return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];

    }

    var _addFileToList = function (listViewer,fileobj) {

        var fileSources = '';

        var fileid = '';

        var filename = '',filesize = 0;

        var fileitemId = '';

        var $listViewer = $(listViewer);

        if (fileobj instanceof Array) {

            fileSources = '<filesources>';

            for(var i = 0; i < fileobj.length; i ++) {

                if (filename != '') filename += '|';

                var file = fileobj[i];

                filename += file.name;

                filesize += file.size;

                fileid = getFileId(file);

                fileSources += '<file id="'+fileid+'" fname="'+file.name+'" fsize="'+file.size+'"></file>';

                fileitemId += fileid;

                file.fileid = fileid;

            }

            fileSources += '</filesources>';

            fileitemId = $.md5(fileitemId);

        }

        else {

            var typename = typeof fileobj;

            if (typename != 'object') {

                return false;

            }

            if (fileobj.size == 0) {

                toast('檔案 “'+fileobj.name+'” 長度為0無法上傳,忽略此檔案。');

                return false;

            }

            fileid = getFileId(fileobj);

            filename = fileobj.name;

            filesize = fileobj.size;

            fileSources = '<filesources><file id="'+fileid+'" fname="'+fileobj.name+'" fsize="'+fileobj.size+'"></file></filesources>';

            fileitemId = fileid;

            fileobj.fileid = fileid;

        }

        var file_existed = false;

        $listViewer.children('div.upload-fileitem').each(function(index,obj) {

            if (obj.id == fileitemId) {

                file_existed = true;

                return false;

            }

        });

        if (file_existed) {

            toast('添加的檔案或檔案組合 “'+filename+'” 已經存在,不重複添加');

            return false;

        }

        if (!listViewer.uploadOptions) {

            toast('添加的檔案 “'+filename+'” 失敗,檔案清單未綁定上傳對象。');

            return false;

        }

        //移除提示元素

        $listViewer.children('div.fileupload-help').remove();

        $listViewer.children('.notice').remove();

        var server = listViewer.uploadOptions.server;

        var cid = listViewer.uploadOptions.cid;

        var serverid = listViewer.uploadOptions.serverid;

        var userid = listViewer.uploadOptions.userid;

        var html = '<div class="upload-fileitem" id="'+fileitemId+'" data-server="'+server+'" data-cid="'+cid+'" data-serverid="'+serverid+'" data-userid="'+userid+'">';

        html += fileSources;

        html += '<a class="remove_icon enabled" href="javascript:void(0);" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" title="移除"></a><a class="edit_icon enabled" href="javascript:void(0);" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" title="編輯"><i class="icon-edit"></i></a><i class="icon-ok-circle"></i>';

        html += '<div class="fileinfo"><div class="filename"><span title="'+filename+'">'+filename+'</span></div>';

        html += '<div class="upload-status"><div class="item"><span class="title">長度:</span><span class="value filesize" data-id="filesize">';

        html += bytesToSize(filesize);

        html += '</span></div>';

        html += '<div class="item"><span class="title">已傳:</span><span class="value finish" data-id="finish" title="實際已經上傳的檔案資料長度">0</span></div>';

        html += '<div class="item"><span class="title">速率:</span><span class="value bitrate" data-id="bitrate">0</span></div>';

        html += '</div></div>';

        html += '<div class="progressbar"><div class="xprogressbar progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">';

        html += '<div class="progress-track progress-success" style="width: 0%;"></div></div><span class="percent-label text" data-id="percent-label">0%</span></div></div>';

        var $fileItem = $(html);

        $fileItem.appendTo($listViewer);

        //$('#upload-filelistview').mCustomScrollbar('update');

        $fileItem.find('a.remove_icon').click(function(evt) {

            evt.preventDefault();

            if (!$(this).hasClass('enabled')) {

                return;

            }

            var $item = $(this).parent();

            if ($item.attr('data-upload')=='finished') {

                toast('檔案已經上傳完成,不必移除,如要清空隊列,請強制重新整理浏覽器。');

                return;

            }

            //remove_icon 的外層 upload-fileitem

            var fileItemDomObj = $item.get(0);

            var fileViewer = $item.get(0).fileViewer;

            if (fileViewer.uploader) {

                var uploader = fileViewer.uploader;

                if ($item.attr('data-upload')=='uploading') {

                    if (!uploader.options.forceremove) {

                        if (!confirm('視訊檔案正在上傳,确定要放棄嗎?')) {

                            return;

                        }

                    }

                    //再次檢查是否還在上傳,如果沒有啟動上傳uploader 屬性無值

                    if (uploader.curFileitemElement && uploader.curFileitemElement == fileItemDomObj) {

                        //僅僅執行取消上傳操作

                        if (uploader.currentFile) {

                            if (uploader.currentXHR && typeof uploader.currentXHR.abort == 'function') {

                                if (uploader.options.forceremove) {

                                    uploader.taskIsForceRemove = true;

                                }

                                uploader.currentXHR.abort();

                                return ;

                            }

                        }

                    }

                }

            }

            if (fileItemDomObj.fileObject) {

                fileItemDomObj.fileObject = null;

                delete fileItemDomObj.fileObject;

            }

            $item.remove();

            var $fileitems = $(fileViewer).children('.upload-fileitem');

            if ($fileitems.length == 0) {

                $(fileViewer).html('<p class="notice">暫無上傳任務</p>');

            }

            //$('#upload-filelistview').mCustomScrollbar('update');

        });

        $fileItem.find('a.edit_icon').click(function(evt) {

            evt.preventDefault();

        });

        var fileItemDom = $fileItem.get(0);

        var fileObject = {

            taskid:fileitemId,

            files:fileobj,

            totalfilesize:filesize,

            uploadedsize:0,

            finished:0

        }

        fileItemDom.fileObject = fileObject;

        fileItemDom.fileViewer = listViewer;

        return fileid;

    };

    //fileInfo 是上傳伺服器傳回的

    var _onUploadedFileFinished = function(fileInfo) {

        var uploader = this;

        //fileInfo 對象的值可以用于儲存到CMS裡,而不僅僅是用于顯示

        if (uploader.options.show_output && uploader.uploadResult) {

            var html = '<div class="file-object" >';

                html += '<div class="info-row"><span class="rlabel">檔案名:</span><span class="rvalue">'+fileInfo.name+'</span></div>';

                html += '<div class="info-row"><span class="rlabel">儲存路徑:</span><span class="rvalue">'+fileInfo.path+'</span></div>';

                html += '<div class="info-row"><span class="rlabel">檔案尺寸:</span><span class="rvalue">'+fileInfo.filesize+'</span></div>';

                html += '<div class="info-row"><span class="rlabel">通路URL:</span><span class="rvalue">'+fileInfo.url+'</span></div>';

            html += '</div>';

            //将檔案資訊顯示在清單裡

            uploader.uploadResult.innerHTML +=html;

        }

        uploader.currentXHR = null;

        if (uploader.curFileitemElement) {

            var $fileNode = $(uploader.curFileitemElement);

            var cid = $fileNode.attr('data-cid');

            var serverid = $fileNode.attr('data-serverid');

            var userid = $fileNode.attr('data-userid');

            uploader.currentFile.uploadInfo = fileInfo;

            var taskid = uploader.curFileitemElement.id;

            var mimeType=mimeType = uploader.currentFile.type;

            if (!mimeType) {

                mimeType = get_file_mimetype(fileInfo.name);

            }

            var title = uploader.curFileitemElement.videotitle ? curFileitemElement.videotitle:'';

            var desc = uploader.curFileitemElement.videodesc ? curFileitemElement.videodesc:'';

            //記錄檔案完成上傳

            var uploadEvent = {

                "event":"filefinished",

                "taskid":uploader.curFileitemElement.id,

                "fileid":uploader.currentFile.fileid,

                "cid":cid,

                "serverid":serverid,

                "userid":userid,

                "mimetype":mimeType,

                "filepath":fileInfo.path,

                "filename":fileInfo.name,

                "filesize":fileInfo.filesize,

                "url":fileInfo.url,

                "title":title,

                "desc":desc

            };

            if (uploader.options.notice_url) {

                $.post(uploader.options.notice_url,uploadEvent,function(data) {

                    if (data.code != 1) {

                        toast(data.msg);

                    }

                },'json');

            }

            if (typeof uploader.options.onevent == 'function') {

                uploader.options.onevent.call(uploader, uploadEvent);

            }

            var isTaskfinish = 1;

            if (uploader.curFileitemElement.fileObject.files instanceof Array) {

                for(var i = 0; i < uploader.curFileitemElement.fileObject.files.length; i ++) {

                    if (!uploader.curFileitemElement.fileObject.files[i].uploadInfo) {

                        isTaskfinish = 0;

                        break;

                    }

                }

            }

            else {

                isTaskfinish = 1;

            }

            uploader.currentFile = null;

            uploader.upload_start = false;

            if (isTaskfinish) {

                $fileNode.attr('data-upload','finished').addClass('finished');

                $fileNode.find('.remove_icon').removeClass('enabled');

                uploader.curFileitemElement.fileObject.finished = 1;

                if (typeof uploader.options.ontaskfinish == 'function') {

                    uploader.options.ontaskfinish.call(uploader,uploader.curFileitemElement.fileObject);

                }

                uploader.curFileitemElement = null;

                //接着啟動下一個任務

                if (!_startNextTaskupload.call(uploader)) {

                    if (uploader.startbutton) {

                        uploader.startbutton.disabled=false;

                        $(uploader.startbutton).removeClass('disabled');

                        uploader.startbutton = null;

                    }

                }

            }

            else {

                //繼續上傳本任務的下一個檔案

                _startFileupload.call(uploader,false);

            }                

        }

    }

    var _do_upload_file = function (fileObj,start_offset,fileid) {

        var uploader = this;

        var xhr = new XMLHttpRequest();

        var formData = new FormData();

        var blobfile;

        if(start_offset >= fileObj.size){

            return false;

        }

        var bitrateDiv = null;

        var finishDiv = null;

        var progressBar = null;

        var progressDiv = null;

        if (uploader.curFileitemElement) {

            var $fileItem = $(uploader.curFileitemElement);

            bitrateDiv = $fileItem.find('.bitrate').get(0);

            finishDiv = $fileItem.find('.finish').get(0);

            progressBar = $fileItem.find('.progress-track').get(0);

            progressDiv = $fileItem.find('.percent-label').get(0);

        }

        else {

            bitrateDiv = document.getElementById("bitrate");

            finishDiv = document.getElementById("finish");

            progressBar = document.getElementById('progressbar');

            progressDiv = document.getElementById('percent-label');

        }

        var oldTimestamp = 0;

        var oldLoadsize = 0;

        var totalFilesize = uploader.curFileitemElement.fileObject.totalfilesize;

        if (totalFilesize == 0) {

            return false;

        }

        //将fileObj 設定為目前正在上傳的檔案

        uploader.currentFile = fileObj;

        //已經完成了的檔案的總尺寸

        var totalUploadedsize = uploader.curFileitemElement.fileObject.uploadedsize;

        var uploadProgress = function (evt) {

            if (evt.lengthComputable) {

                var uploadedSize = totalUploadedsize + evt.loaded + start_offset; 

                var percentComplete = Math.round(uploadedSize * 100 / totalFilesize);

                var timestamp = (new Date()).valueOf();

                var isFinish = evt.loaded == evt.total;

                if (timestamp > oldTimestamp || isFinish) {

                    var duration = timestamp - oldTimestamp;

                    if (duration > 500 || isFinish) {

                        var size =  evt.loaded - oldLoadsize;

                        var bitrate = (size * 8 / duration /1024) * 1000; //kbps

                        if (bitrate > 1000)

                            bitrate = Math.round(bitrate / 1000) + 'Mbps';

                        else

                            bitrate = Math.round(bitrate) + 'Kbps';

                        var finish = totalUploadedsize + evt.loaded + start_offset;

                        if (finish > 1048576)

                            finish = (Math.round(finish / (1048576/100)) / 100).toString() + 'MB';

                        else

                            finish = (Math.round(finish / (1024/100) ) / 100).toString() + 'KB';

                        progressBar.style.width = percentComplete+'%';

                        progressDiv.innerHTML = percentComplete.toString() + '%';

                        bitrateDiv.innerHTML = bitrate;

                        finishDiv.innerHTML = finish;

                        oldTimestamp = timestamp;

                        oldLoadsize = evt.loaded;

                        if (typeof uploader.options.ontaskprogress == 'function') {

                            var progress = {

                                'bitrate':bitrate,

                                'finish':finish,

                                'percent':percentComplete

                            };

                            uploader.options.ontaskprogress.call(uploader,progress);

                        }

                    }

                }

            }

            else {

                progressDiv.innerHTML = 'N/A';

            }

        }

        xhr.onreadystatechange = function(){

            if (xhr.readyState == 4 && xhr.status == 200) {

                if (console)

                    console.log( xhr.responseText );

            }

            else if (xhr.status == 400) {

            }

        };

        var uploadComplete = function (evt) {

            progressDiv.innerHTML = '100%';

            uploader.curFileitemElement.fileObject.uploadedsize += uploader.currentFile.size;

            var result = JSON.parse(evt.target.responseText);

            if (result.result == 'success') {

                _onUploadedFileFinished.call(uploader,result.files[0]);

            }

            else {

                uploader.currentFile = null;

                uploader.upload_start = false;

                toast(result.msg);

            }

        }

        var uploadFailed = function (evt) {

            if (!uploader.currentFile) return;

            if (uploader.reconnectId) return;

            uploader.upload_start = false;

            toast("檢測到網絡故障!兩秒後嘗試重連...");

            uploader.reconnectId = window.setTimeout(function() {

                _startFileupload.call(uploader,true);

            },2000);

            if (typeof uploader.options.ontaskfail == 'function') {

                uploader.options.ontaskfail.call(uploader,uploader.curFileitemElement.fileObject);

            }

        }

        var uploadCanceled = function (evt) {

            if (uploader.curFileitemElement) {

                $(uploader.curFileitemElement).attr('data-upload','aborted');

                var fileid = uploader.currentFile.fileid;

                //記錄檔案取消上傳

                var uploadEvent = {

                    "event":'abortupload',

                    "taskid":uploader.curFileitemElement.id,

                    "fileid":fileid

                };

                if (uploader.options.notice_url) {

                    $.post(uploader.options.notice_url,uploadEvent,function(data) {

                        if (data.code != 1) {

                            toast(data.msg);

                        }

                    },'json');

                }

                if (typeof uploader.options.onevent == 'function') {

                    uploader.options.onevent.call(uploader, uploadEvent);

                }

                if (uploader.taskIsForceRemove) {

                    delete uploader.curFileitemElement.fileObject;

                    $(uploader.curFileitemElement).remove();

                    uploader.curFileitemElement = null;

                    var $fileitems = $(uploader.fileViewer).children('.upload-fileitem');

                    if ($fileitems.length == 0) {

                        $(uploader.fileViewer).html('<p class="notice">暫無上傳任務</p>');

                    }

                }

            }

            uploader.currentFile = null;

            uploader.upload_start = false;

            if (uploader.startbutton) {

                uploader.startbutton.disabled=false;

                $(uploader.startbutton).removeClass('disabled');

                uploader.startbutton = null;

            }

            if (uploader.taskIsForceRemove) {

                uploader.currentXHR = null;

                delete uploader.taskIsForceRemove;

                //在可以強制删除的情況下繼續執行下一個任務,但不提示toast

                _startNextTaskupload.call(uploader,true);

            }

            else {

                toast("上傳已經被暫停取消或者浏覽器斷開了連接配接!");

                if (typeof uploader.options.onpause == 'function') {

                    uploader.options.onpause.call(uploader);

                }

            }

        }

        //設定逾時時間,由于是上傳大檔案,是以千萬不要設定逾時

        //xhr.timeout = 20000;

        //xhr.ontimeout = function(event){

        //        alert('檔案上傳時間太長,伺服器在規定的時間内沒有響應!');

        //}         

        xhr.overrideMimeType("application/octet-stream"); 

        var mimeType = fileObj.type;

        if (!mimeType) {

            mimeType = get_file_mimetype(fileObj.name);

        }

        //附加的檔案資料應該放在請求的前面

        var cid = uploader.curFileitemElement.getAttribute('data-cid');

        var serverid = uploader.curFileitemElement.getAttribute('data-serverid');

        formData.append('channelid', cid);

        formData.append('filename', fileObj.name);

        //必須将fileid資訊傳送給伺服器,伺服器隻有在獲得了fileid資訊後才對檔案做斷點續傳處理

        formData.append('fileid', fileid);

        //請将檔案資料放在最後的域

        var filesize = fileObj.size;

        var blob = fileObj.slice(start_offset,filesize);

        if('msSaveOrOpenBlob' in navigator){

            formData.append("file",blob, fileObj.name);

        }

        else {

            var fileOfBlob = new File([blob], fileObj.name);

            formData.append('file', fileOfBlob);

        }

        xhr.upload.addEventListener("progress", uploadProgress, false);

        xhr.addEventListener("load", uploadComplete, false);

        xhr.addEventListener("error", uploadFailed, false);

        xhr.addEventListener("abort", uploadCanceled, false);

        var upload_url_full = uploader.upload_file_url + get_save_path(fileObj.name,uploader.options.savepaths);

        xhr.open('POST', upload_url_full);

        //

        xhr.send(formData);

        uploader.currentXHR = xhr;

        uploader.upload_start = true;

        toast('');

        if (start_offset == 0) {

            var uploadEvent = {

                "event":'startupload',

                "fileid":fileid,

                "cid":cid,

                "serverid":serverid,

                "filename":fileObj.name,

                "mimetype":mimeType

            };

            //開始檔案上傳

            if (uploader.options.notice_url) {

                $.post(uploader.options.notice_url,uploadEvent,function(data) {

                    if (data.code != 1) {

                        toast(data.msg);

                    }

                },'json');

            }

            if (typeof uploader.options.onevent == 'function') {

                uploader.options.onevent.call(uploader, uploadEvent);

            }

        }

        else {

            //記錄檔案斷點續傳

            var uploadEvent = {

                "event":'resumeupload',

                "fileid":fileid,

                "cid":cid,

                "serverid":serverid,

                "filename":fileObj.name,

                "mimetype":mimeType

            };

            if (uploader.options.notice_url) {

                $.post(uploader.options.notice_url,uploadEvent,function(data) {

                    if (data.code != 1) {

                        toast(data.msg);

                    }

                },'json');

            }

            if (typeof uploader.options.onevent == 'function') {

                uploader.options.onevent.call(uploader, uploadEvent);

            }

        }

    }

    var _do_finish_file = function (fileObj,fileInfo) {

        var uploader = this;

        var bitrateDiv = null;

        var finishDiv = null;

        var progressBar = null;

        var progressDiv = null;

        if (!uploader.curFileitemElement) {

            return;

        }

        var $fileItem = $(uploader.curFileitemElement);

        bitrateDiv = $fileItem.find('.bitrate').get(0);

        finishDiv = $fileItem.find('.finish').get(0);

        progressBar = $fileItem.find('.progress-track').get(0);

        progressDiv = $fileItem.find('.percent-label').get(0);

        //模拟進度完成

        var totalFilesize = uploader.curFileitemElement.fileObject.totalfilesize;

        if (totalFilesize == 0) {

            return false;

        }

        //将fileObj 設定為目前正在上傳的檔案

        uploader.currentFile = fileObj;

        //已經完成了的檔案的總尺寸

        var totalUploadedsize = uploader.curFileitemElement.fileObject.uploadedsize;

        var uploadedSize = totalUploadedsize + fileObj.size; 

        var percentComplete = Math.round(uploadedSize * 100 / totalFilesize);

        var finish = uploadedSize;

        if (finish > 1048576)

            finish = (Math.round(finish / (1048576/100)) / 100).toString() + 'MB';

        else

            finish = (Math.round(finish / (1024/100) ) / 100).toString() + 'KB';

        progressBar.style.width = percentComplete+'%';

        progressDiv.innerHTML = percentComplete.toString() + '%';

        bitrateDiv.innerHTML = '';

        finishDiv.innerHTML = finish;

        uploader.curFileitemElement.fileObject.uploadedsize += fileObj.size;

        _onUploadedFileFinished.call(uploader,fileInfo);

    }

    //doupload

    var _startFileupload = function (bReconnect) {

        var uploader = this;

        if (!this.options) {

            alert("發生未知錯誤,沒有正确綁定上傳對象。")

            return false;

        }

        if (!this.curFileitemElement) {

            alert("請選擇檔案後再試!")

            return false;

        }

        if (!bReconnect) {

            if (uploader.currentFile) {

                alert("上傳對象已經有檔案在上傳了,不要重複調用!")

                return false;

            }

        }

        var fileObject = this.curFileitemElement.fileObject;

        if (fileObject.finished) {

            return false;

        }

        var fileObj = null;

        if (!this.currentFile) {

            if (this.curFileitemElement.fileObject.files instanceof Array) {

                for(var i = 0; i < this.curFileitemElement.fileObject.files.length; i ++) {

                    if (!this.curFileitemElement.fileObject.files[i].uploadInfo) {

                        fileObj = this.curFileitemElement.fileObject.files[i];

                        break;

                    }

                }

            }

            else {

                if (!this.curFileitemElement.fileObject.files.uploadInfo) {

                    fileObj = this.curFileitemElement.fileObject.files;

                }

            }

            if (!fileObj) {

                alert("任務檔案已經上傳完畢!")

                return false;

            }

            //this.currentFile = fileObj;

        }

        else {

            fileObj = this.currentFile;

        }

        var fileid = getFileId(fileObj);

        var t = (new Date()).getTime();

        //通過以下URL擷取檔案的斷點續傳資訊,必須的參數為fileid,後面追加t參數是避免浏覽器緩存

        var url = this.resume_info_url + '?fileid='+fileid + '&t='+t;

        var ajax = new XMLHttpRequest();

        ajax.onerror = function (e) {

            if (uploader.reconnectId) 

                return;

            uploader.reconnectId = window.setTimeout(function() {

                uploader.currentXHR = null;

                _startFileupload.call(uploader,true);

            },2000);

        };

        ajax.onabort = function (evt) {

            uploader.currentFile = null;

            uploader.upload_start = false;

            if (uploader.curFileitemElement) {

                $(uploader.curFileitemElement).attr('data-upload','aborted');

                var fileid = uploader.curFileitemElement.id;

                uploader.curFileitemElement = null;

            }

            if (uploader.startbutton) {

                uploader.startbutton.disabled=false;

                $(uploader.startbutton).removeClass('disabled');

                uploader.startbutton = null;

            }

            toast("上傳被取消或者浏覽器斷開了連接配接!");

        }

        ajax.onreadystatechange = function () {

            if(this.readyState == 4){

                if (bReconnect) {

                    //目前是重連狀态,清除重連标志

                    uploader.reconnectId = 0;

                }

                if (this.status == 200){

                    var response = this.responseText;

                    var result = JSON.parse(response);

                    if (!result) {

                        alert('伺服器傳回的資料不正确,可能是不相容的伺服器,上傳無法繼續!');

                        return;

                    }

                    //斷點續傳資訊傳回的檔案對象包含已經上傳的尺寸

                    var uploadedBytes = result.file && result.file.size;

                    if (!result.file.finished && uploadedBytes < fileObj.size) {

                        _do_upload_file.call(uploader,fileObj,uploadedBytes,fileid);

                    }

                    else {

                        //檔案已經上傳完成了,就不要再上傳了,直接傳回結果就可以了

                        _do_finish_file.call(uploader,fileObj,result.file);

                    }

                }else {

                    uploader.currentFile = null;

                    uploader.currentXHR = false;

                    toast('擷取檔案斷點續傳資訊失敗,伺服器錯誤碼:'+this.status+', 上傳無法繼續!');

                }  

            }

        }

        if (typeof this.options.ontaskstart == 'function') {

            if (false === this.options.ontaskstart.call(this,this.curFileitemElement.fileObject)) {

                return false;

            }

        }

        ajax.open('get',url,true);

        ajax.send(null);

        this.currentXHR = ajax;

        return true;

    }

    //任務上傳完成後,開啟下一個任務

    function _startNextTaskupload(isAfterAbort) {

        var uploader = this;

        var $fileitems = $(this.fileViewer).children('.upload-fileitem');

        $fileitems.each(function(index,elm) {

            if ($(elm).attr('data-upload') != 'finished') {

                if (elm.fileObject) {

                    $(elm).attr('data-upload','uploading');

                    uploader.curFileitemElement = elm;

                    return false;

                }

            }

        });

        if (!uploader.curFileitemElement) {

            if (typeof this.options.onend == 'function') {

                this.options.onend.call(this);

            }

            if (!isAfterAbort) {            

                toast('所有檔案已經上傳完畢!');

            }

            return false;

        }

        _startFileupload.call(uploader,false);

        return true;

    }

    var fileViewer = document.getElementById(_options.filelist);

    if (!fileViewer) {

        return null;

    }

    fileViewer.uploadOptions = _options;

    if (_options.enabledrop) {

        //禁止浏覽器打開檔案的預設行為

        document.addEventListener("drop",function(e){//拖離 

            e.preventDefault();

        });

        document.addEventListener("dragleave",function(e){//拖後放 

            e.preventDefault();

        });

        document.addEventListener("dragenter",function(e){//拖進

            e.preventDefault();

        });

        document.addEventListener("dragover",function(e){//拖來拖去  

            e.preventDefault();

        });

        fileViewer.addEventListener("drop",function (e) {

            var fileList = e.dataTransfer.files; //擷取檔案對象

            if (console) console.log(fileList)

            //檢測是否是拖拽檔案到頁面的操作

            if(fileList.length == 0){

                return false;

            }

            for(var i = 0; i < fileList.length; i++) {

                _addFileToList(this,fileList[i]);

            }

        },false);

    }

    if (_options.fileselector) {

        var fileSelector = document.getElementById(_options.fileselector);

        if (fileSelector) {

            fileSelector.fileViewer = fileViewer;

            fileSelector.addEventListener("change",function (e) {

                var fileViewer = this.fileViewer;

                if (window.File && window.FileList) {

                    if (this.files.length) {

                        for(var i = 0; i < this.files.length; i++) {

                            _addFileToList(fileViewer,this.files[i]);

                        }

                        this.value = '';

                    }

                }

                else {

                    window.alert('抱歉,你的浏覽器不支援FileAPI,請更新浏覽器!');

                }

            },false);

        }

    }

    return {

        options:_options,

        fileViewer: fileViewer,

        resume_info_url:'',

        upload_file_url:'',

        upload_start:false,

        startbutton:null,

        currentFile: null,  //目前正在上傳的檔案對象

        curFileitemElement : null, //目前正在上傳的 檔案 DOM元素

        start:function(btnObj) {

            if (this.upload_start) {

                alert("檔案上傳正在進行中,請稍候再點選重複長傳!")

                return false;

            }

            var $fileitems = $(this.fileViewer).children('.upload-fileitem');

            if ($fileitems.length == 0) {

                toast('沒有任何可以上傳的檔案,請先添加檔案再試!');

                return false;

            }

            var uploader = this;

            this.fileViewer.uploader = this;

            $fileitems.each(function(index,elm) {

                if ($(elm).attr('data-upload') != 'finished') {

                    if (elm.fileObject) {

                        $(elm).attr('data-upload','uploading');

                        uploader.curFileitemElement = elm;

                        var uploadsrv_addr = 'http://'+uploader.options.server;

                        uploader.resume_info_url = uploadsrv_addr+'/resume/';

                        uploader.upload_file_url = uploadsrv_addr+'/upload/';

                        return false;

                    }

                }

            });

            if (!this.curFileitemElement) {

                toast('所有檔案已經上傳完畢,請添加新檔案後再試!');

                return false;

            }

            this.startbutton = btnObj;

            if (this.options.authize_url) {

                //如果提供了授權驗證URL,則向CMS平台進行驗證

                $.get(this.options.authize_url,{"t":(new Date).getTime()},function(result) {

                    if (result.code != 1) {

                        if (typeof this.options.onend == 'function') {

                            this.options.onend.call(this);

                        }

                        toast(result.msg);

                        return;

                    }

                    if (_startFileupload.call(uploader,false)) {

                        if (uploader.startbutton) {

                            uploader.startbutton.disabled=true;

                            $(uploader.startbutton).addClass('disabled');

                        }

                    }

                    else {

                        if (typeof this.options.onend == 'function') {

                            this.options.onend.call(this);

                        }

                    }

                },'json');

                return true;

            }

            else {

                if (_startFileupload.call(uploader,false)) {

                    if (uploader.startbutton) {

                        uploader.startbutton.disabled=true;

                        $(uploader.startbutton).addClass('disabled');

                    }

                    return true;

                }

                return false;

            }

        },

        pause:function () {

            if (this.currentFile) {

                if (this.currentXHR && typeof this.currentXHR.abort == 'function') {

                    this.currentXHR.abort();

                }

            }

        },

        addtask:function(fileobjs) {

            _addFileToList(this.fileViewer,fileobjs);

        },

        removetask:function(taskid) {

            var $fileItemElm = $('#'+taskid);

            if ($fileItemElm.length == 1 && $fileItemElm.hasClass('upload-fileitem')) {

                if ($fileItemElm.attr('data-upload')=='uploading') {

                    alert('任務正在進行中,請停止任務後再删除!');

                    return;

                }

                var itemDom = $fileItemElm.get(0);

                if (itemDom.fileObject) {

                    delete itemDom.fileObject;

                }

                $fileItemElm.remove();

            }

        },

        clear:function() {

            var $fileitems = $(this.fileViewer).children('.upload-fileitem');

            if ($fileitems.length == 0) {

                return false;

            }

            if (!confirm('确實要清除任務清單嗎?')) {

                return false;

            }

            if (this.currentFile) {

                if (this.currentXHR && typeof this.currentXHR.abort == 'function') {

                    this.currentXHR.abort();

                }

                if (typeof this.options.onend == 'function') {

                    this.options.onend.call(this);

                }

            }

            $fileitems.each(function(index,elm) {

                if (elm.fileObject) delete elm.fileObject;

            });

            $fileitems.remove();

            this.curFileitemElement = null;

            this.currentFile = null;

            this.currentXHR = null;

            $(this.fileViewer).html('<p class="notice">暫無上傳任務</p>');

        },

        version: "2.0"

    };

};

技術交流

- QQ:1918098288

- Mail: [email protected]

繼續閱讀