upload.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
  2. allowMime = JSON.parse('{$exts|raw}');
  3. function UploadAdapter(elem, done) {
  4. return new (function (elem, done, that) {
  5. /*! 初始化变量 */
  6. that = this;
  7. this.option = {elem: $(elem), exts: [], mimes: []};
  8. this.option.size = this.option.elem.data('size') || 0;
  9. this.option.safe = this.option.elem.data('safe') ? 1 : 0;
  10. this.option.hide = this.option.elem.data('hload') ? 1 : 0;
  11. this.option.mult = this.option.elem.data('multiple') > 0;
  12. this.option.path = (this.option.elem.data('path') || '').replace(/\W/g, '');
  13. this.option.type = this.option.safe ? 'local' : this.option.elem.attr('data-uptype') || '';
  14. this.option.quality = parseFloat(this.option.elem.data('quality') || '1.0');
  15. this.option.maxWidth = parseInt(this.option.elem.data('max-width') || '0');
  16. this.option.maxHeight = parseInt(this.option.elem.data('max-height') || '0');
  17. this.option.cutWidth = parseInt(this.option.elem.data('cut-width') || '0');
  18. this.option.cutHeight = parseInt(this.option.elem.data('cut-height') || '0');
  19. /*! 查找表单元素, 如果没有找到将不会自动写值 */
  20. if (!this.option.elem.data('input') && this.option.elem.data('field')) {
  21. this.$input = $('input[name="' + this.option.elem.data('field') + '"]:not([type=file])');
  22. this.option.elem.data('input', this.$input.size() > 0 ? this.$input.get(0) : null);
  23. }
  24. /*! 文件选择筛选,使用 MIME 规则过滤文件列表 */
  25. $((this.option.elem.data('type') || '').split(',')).map(function (i, e) {
  26. if (allowMime[e]) that.option.exts.push(e), that.option.mimes.push(allowMime[e]);
  27. });
  28. /*! 初始化上传组件 */
  29. this.adapter = new Adapter(this.option, layui.upload.render({
  30. url: '{:url("admin/api.upload/file")}', auto: false, elem: elem, accept: 'file', multiple: this.option.mult, exts: this.option.exts.join('|'), acceptMime: this.option.mimes.join(','), choose: function (obj) {
  31. obj.items = [], obj.files = obj.pushFile();
  32. layui.each(obj.files, function (idx, file) {
  33. obj.items.push(file);
  34. file.path = that.option.path;
  35. file.quality = that.option.quality;
  36. file.maxWidth = that.option.maxWidth;
  37. file.maxHeight = that.option.maxHeight;
  38. file.cutWidth = that.option.cutWidth;
  39. file.cutHeight = that.option.cutHeight;
  40. });
  41. that.adapter.event('upload.choose', obj.items);
  42. that.adapter.upload(obj.items, done);
  43. layui.each(obj.files, function (idx) {
  44. delete obj.files[idx];
  45. });
  46. }
  47. }));
  48. })(elem, done)
  49. }
  50. // 创建对象
  51. UploadAdapter.adapter = window.AdminUploadAdapter = Adapter;
  52. // 上传文件
  53. function Adapter(option, uploader) {
  54. this.uploader = uploader, this.config = function (option) {
  55. return (this.option = Object.assign({}, this.option || {}, option || {})), this;
  56. }, this.init = function (option) {
  57. this.uploader && this.uploader.config.elem.next().val('');
  58. this.files = {}, this.loader = 0, this.count = {total: 0, error: 0, success: 0};
  59. return this.config(option).config({safe: this.option.safe || 0, type: this.option.type || ''});
  60. }, this.init(option);
  61. }
  62. // 文件推送
  63. Adapter.prototype.upload = function (files, done) {
  64. var that = this.init();
  65. layui.each(files, function (index, file) {
  66. that.count.total++, file.index = index, that.files[index] = file;
  67. if (that.option.size && file.size > that.option.size) {
  68. that.event('upload.error', {file: file}, file, '大小超限');
  69. } else if (!that.option.hide) {
  70. file.notify = new NotifyExtend(file);
  71. }
  72. }), layui.each(files, function (index, file) {
  73. // 禁传异常状态文件
  74. if (typeof file.xstate === 'number' && file.xstate === -1) return;
  75. // 图片限宽限高压缩
  76. if (/^image\//.test(file.type) && (file.maxWidth + file.maxHeight + file.cutWidth + file.cutHeight > 0 || file.quality !== 1)) {
  77. require(['compressor'], function (Compressor) {
  78. new Compressor(file, {
  79. quality: file.quality, resize: 'cover', width: file.cutWidth || 0, height: file.cutHeight || 0, maxWidth: file.maxWidth, maxHeight: file.maxHeight, success(blob) {
  80. blob.index = file.index, blob.notify = file.notify, blob.path = file.path, files[index] = blob;
  81. that.hash(files[index]).then(function (file) {
  82. that.event('upload.hash', file).request(file, done);
  83. });
  84. }, error: function () {
  85. that.event('upload.error', {file: file}, file, '压缩失败');
  86. }
  87. });
  88. });
  89. } else {
  90. that.hash(file).then(function (file) {
  91. that.event('upload.hash', file).request(file, done);
  92. });
  93. }
  94. });
  95. };
  96. // 文件上传
  97. Adapter.prototype.request = function (file, done) {
  98. var that = this, data = {key: file.xkey, safe: that.option.safe, uptype: that.option.type};
  99. data.size = file.size, data.name = file.name, data.hash = file.xmd5, data.mime = file.type, data.xext = file.xext;
  100. jQuery.ajax("{:url('admin/api.upload/state')}", {
  101. data: data, method: 'post', success: function (ret) {
  102. file.id = ret.data.id || 0, file.xurl = ret.data.url;
  103. file.xsafe = ret.data.safe, file.xpath = ret.data.key, file.xtype = ret.data.uptype;
  104. if (parseInt(ret.code) === 404) {
  105. var uploader = {};
  106. uploader.url = ret.data.server;
  107. uploader.form = new FormData();
  108. uploader.form.append('key', ret.data.key);
  109. uploader.form.append('safe', ret.data.safe);
  110. uploader.form.append('uptype', ret.data.uptype);
  111. if (ret.data.uptype === 'qiniu') {
  112. uploader.form.append('token', ret.data.token);
  113. } else if (ret.data.uptype === 'alioss') {
  114. uploader.form.append('policy', ret.data['policy']);
  115. uploader.form.append('signature', ret.data['signature']);
  116. uploader.form.append('OSSAccessKeyId', ret.data['OSSAccessKeyId']);
  117. uploader.form.append('success_action_status', '200');
  118. uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
  119. } else if (ret.data.uptype === 'txcos') {
  120. uploader.form.append('q-ak', ret.data['q-ak']);
  121. uploader.form.append('policy', ret.data['policy']);
  122. uploader.form.append('q-key-time', ret.data['q-key-time']);
  123. uploader.form.append('q-signature', ret.data['q-signature']);
  124. uploader.form.append('q-sign-algorithm', ret.data['q-sign-algorithm']);
  125. uploader.form.append('success_action_status', '200');
  126. uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
  127. } else if (ret.data.uptype === 'upyun') {
  128. uploader.form.delete('key');
  129. uploader.form.delete('safe');
  130. uploader.form.delete('uptype');
  131. uploader.form.append('save-key', ret.data['key']);
  132. uploader.form.append('policy', ret.data['policy']);
  133. uploader.form.append('authorization', ret.data['authorization']);
  134. uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
  135. }
  136. uploader.form.append('file', file, file.name), jQuery.ajax({
  137. url: uploader.url, data: uploader.form, type: 'post', xhr: function (xhr) {
  138. xhr = new XMLHttpRequest();
  139. return xhr.upload.addEventListener('progress', function (event) {
  140. file.xtotal = event.total, file.xloaded = event.loaded || 0;
  141. that.progress((file.xloaded / file.xtotal * 100).toFixed(2), file)
  142. }), xhr;
  143. }, contentType: false, error: function () {
  144. that.event('upload.error', {file: file}, file, '接口异常');
  145. }, processData: false, success: function (ret) {
  146. // 兼容数据格式
  147. if (typeof ret === 'string' && ret.length > 0) try {
  148. ret = JSON.parse(ret) || ret;
  149. } catch (e) {
  150. console.log(e)
  151. }
  152. if (typeof ret !== 'object') {
  153. ret = {code: 1, url: file.xurl, info: '上传成功'};
  154. }
  155. /*! 检查单个文件上传返回的结果 */
  156. if (typeof ret === 'object' && ret.code < 1) {
  157. that.event('upload.error', {file: file}, file, ret.info || '上传失败');
  158. } else {
  159. that.done(ret, file.index, file, done, '上传成功');
  160. }
  161. }
  162. });
  163. } else if (parseInt(ret.code) === 200) {
  164. (file.xurl = ret.data.url), that.progress('100.00', file);
  165. that.done({code: 1, url: file.xurl, info: file.xstats}, file.index, file, done, '秒传成功');
  166. } else {
  167. that.event('upload.error', {file: file}, file, ret.info || ret.error.message || '上传出错!');
  168. }
  169. }
  170. });
  171. };
  172. // 上传进度
  173. Adapter.prototype.progress = function (number, file) {
  174. this.event('upload.progress', {number: number, file: file});
  175. if (file.notify) file.notify.setProgress(number);
  176. };
  177. // 上传结果
  178. Adapter.prototype.done = function (ret, idx, file, done, message) {
  179. /*! 检查单个文件上传返回的结果 */
  180. if (ret.code < 1) return $.msg.tips(ret.info || '文件上传失败!');
  181. if (typeof file.xurl !== 'string') return $.msg.tips('无效的文件上传对象!');
  182. jQuery.post("{:url('admin/api.upload/done')}", {id: file.id, hash: file.xmd5});
  183. /*! 单个文件上传成功结果处理 */
  184. if (typeof done === 'function') {
  185. done.call(this.option.elem, file.xurl, this.files['id']);
  186. } else if (this.option.mult < 1 && this.option.elem.data('input')) {
  187. $(this.option.elem.data('input')).val(file.xurl).trigger('change', file);
  188. }
  189. // 文件上传成功事件
  190. this.event('push', file.xurl).event('upload.done', {file: file, data: ret}, file, message);
  191. /*! 所有文件上传完成后结果处理 */
  192. if (this.count.success + this.count.error >= this.count.total) {
  193. this.option.hide || $.msg.close(this.loader);
  194. if (this.option.mult > 0 && this.option.elem.data('input')) {
  195. var urls = this.option.elem.data('input').value || [];
  196. if (typeof urls === 'string') urls = urls.split('|');
  197. for (var i in this.files) urls.push(this.files[i].xurl);
  198. $(this.option.elem.data('input')).val(urls.join('|')).trigger('change', this.files);
  199. }
  200. this.event('upload.complete', {file: this.files}, file).init().uploader && this.uploader.reload();
  201. }
  202. };
  203. /*! 触发事件过程 */
  204. Adapter.prototype.event = function (name, data, file, message) {
  205. if (name === 'upload.error') {
  206. this.count.error++, file.xstate = -1, file.xstats = message;
  207. if (file.notify) file.notify.setError(message || file.xstats || '');
  208. } else if (name === 'upload.done') {
  209. this.count.success++, file.xstate = 1, file.xstats = message;
  210. if (file.notify) file.notify.setSuccess(message || file.xstats || '')
  211. }
  212. if (this.option.elem) {
  213. this.option.elem.triggerHandler(name, data);
  214. }
  215. return this;
  216. };
  217. /**
  218. * 计算文件 HASH 值
  219. * @param {File} file 文件对象
  220. * @return {Promise}
  221. */
  222. Adapter.prototype.hash = function (file) {
  223. var defer = jQuery.Deferred();
  224. file.xext = file.name.indexOf('.') > -1 ? file.name.split('.').pop() : 'tmp';
  225. /*! 兼容不能计算文件 HASH 的情况 */
  226. var IsDate = '{$nameType|default=""}'.indexOf('date') > -1;
  227. if (!window.FileReader || IsDate) return jQuery.when((function (xmd5, chars) {
  228. while (xmd5.length < 32) xmd5 += chars.charAt(Math.floor(Math.random() * chars.length));
  229. return SetFileXdata(file, xmd5, 6), defer.promise();
  230. })(layui.util.toDateString(Date.now(), 'yyyyMMddHHmmss-'), '0123456789'));
  231. /*! 读取文件并计算 HASH 值 */
  232. return new LoadNextChunk(file).ReadAsChunk();
  233. function SetFileXdata(file, xmd5, slice) {
  234. file.xmd5 = xmd5, file.xstate = 0, file.xstats = '';
  235. file.xkey = file.xmd5.substring(0, slice || 2) + '/' + file.xmd5.substring(slice || 2) + '.' + file.xext;
  236. if (file.path) file.xkey = file.path + '/' + file.xkey;
  237. return defer.resolve(file, file.xmd5, file.xkey), file;
  238. }
  239. function LoadNextChunk(file) {
  240. var that = this, reader = new FileReader(), spark = new SparkMD5.ArrayBuffer();
  241. var slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  242. this.chunkIdx = 0, this.chunkSize = 2097152, this.chunkTotal = Math.ceil(file.size / this.chunkSize);
  243. reader.onload = function (event) {
  244. spark.append(event.target.result);
  245. ++that.chunkIdx < that.chunkTotal ? that.ReadAsChunk() : SetFileXdata(file, spark.end());
  246. }, reader.onerror = function () {
  247. defer.reject();
  248. }, this.ReadAsChunk = function () {
  249. this.start = that.chunkIdx * that.chunkSize;
  250. this.loaded = this.start + that.chunkSize >= file.size ? file.size : this.start + that.chunkSize;
  251. reader.readAsArrayBuffer(slice.call(file, this.start, this.loaded));
  252. defer.notify(file, (this.loaded / file.size * 100).toFixed(2));
  253. return defer.promise();
  254. };
  255. }
  256. };
  257. return UploadAdapter;
  258. /**
  259. * 上传状态提示扩展插件
  260. * @param {File} file 文件对象
  261. * @constructor
  262. */
  263. function NotifyExtend(file) {
  264. var that = this;
  265. this.notify = Notify.notify({width: 260, title: file.name, showProgress: true, description: '上传进度 <span data-upload-progress>0%</span>', type: 'default', position: 'top-right', closeTimeout: 0});
  266. this.$elem = $(this.notify.notification.nodes);
  267. this.$elem.find('.growl-notification__progress').addClass('is-visible');
  268. this.$elem.find('.growl-notification__progress-bar').addClass('transition');
  269. this.setProgress = function (number) {
  270. this.$elem.find('[data-upload-progress]').html(number + '%');
  271. this.$elem.find('.growl-notification__progress-bar').css({width: number + '%'});
  272. return this;
  273. }, this.setError = function (message) {
  274. this.$elem.find('.growl-notification__desc').html(message || '文件上传失败!');
  275. this.$elem.removeClass('growl-notification--default').addClass('growl-notification--error')
  276. return this.close();
  277. }, this.setSuccess = function (message) {
  278. this.setProgress('100.00');
  279. this.$elem.find('.growl-notification__desc').html(message || '文件上传成功!');
  280. this.$elem.removeClass('growl-notification--default').addClass('growl-notification--success');
  281. return this.close();
  282. }, this.close = function (timeout) {
  283. return setTimeout(function () {
  284. that.notify.close();
  285. }, timeout || 2000), this;
  286. };
  287. }
  288. });