upload.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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.type = this.option.safe ? 'local' : this.option.elem.attr('data-uptype') || '';
  13. /*! 查找表单元素, 如果没有找到将不会自动写值 */
  14. if (!this.option.elem.data('input') && this.option.elem.data('field')) {
  15. this.$input = $('input[name="' + this.option.elem.data('field') + '"]:not([type=file])');
  16. this.option.elem.data('input', this.$input.size() > 0 ? this.$input.get(0) : null);
  17. }
  18. /*! 文件选择筛选,使用 MIME 规则过滤文件列表 */
  19. $((this.option.elem.data('type') || '').split(',')).map(function (i, e) {
  20. if (allowMime[e]) that.option.exts.push(e), that.option.mimes.push(allowMime[e]);
  21. });
  22. /*! 初始化上传组件 */
  23. this.adapter = new Adapter(this.option, layui.upload.render({
  24. 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 (object) {
  25. object.files = object.pushFile();
  26. that.adapter.event('upload.choose', object.files);
  27. that.adapter.upload(object.files, done), layui.each(object.files, function (index) {
  28. delete object.files[index];
  29. });
  30. }
  31. }));
  32. })(elem, done)
  33. }
  34. // 创建对象
  35. UploadAdapter.adapter = window.AdminUploadAdapter = Adapter;
  36. // 上传文件
  37. function Adapter(option, uploader) {
  38. this.uploader = uploader, this.config = function (option) {
  39. return (this.option = Object.assign({}, this.option || {}, option || {})), this;
  40. }, this.init = function (option) {
  41. this.uploader && this.uploader.config.elem.next().val('');
  42. this.files = {}, this.loader = 0, this.count = {total: 0, error: 0, success: 0};
  43. return this.config(option).config({safe: this.option.safe || 0, type: this.option.type || ''});
  44. }, this.init(option);
  45. }
  46. // 文件推送
  47. Adapter.prototype.upload = function (files, done) {
  48. var that = this.init();
  49. layui.each(files, function (index, file) {
  50. that.count.total++, file.index = index, that.files[index] = file;
  51. if (that.option.size && file.size > that.option.size) {
  52. that.count.error++, file.xstate = -1, file.xstats = '大小超限';
  53. return $.msg.tips('文件大小超出限制!');
  54. }
  55. if (!that.option.hide) {
  56. file.notify = new NotifyExtend(file);
  57. }
  58. }), layui.each(files, function (index, file) {
  59. that.hash(file).then(function (file) {
  60. that.event('upload.hash', file).request(file, done);
  61. });
  62. });
  63. };
  64. // 文件上传
  65. Adapter.prototype.request = function (file, done) {
  66. var that = this, data = {key: file.xkey, safe: that.option.safe, uptype: that.option.type};
  67. data.size = file.size, data.name = file.name, data.hash = file.xmd5;
  68. jQuery.ajax("{:url('admin/api.upload/state')}", {
  69. data: data, method: 'post', success: function (ret) {
  70. file.xurl = ret.data.url, file.xsafe = ret.data.safe, file.xpath = ret.data.key, file.xtype = ret.data.uptype;
  71. if (parseInt(ret.code) === 404) {
  72. var uploader = {};
  73. uploader.url = ret.data.server;
  74. uploader.form = new FormData();
  75. uploader.form.append('key', ret.data.key);
  76. uploader.form.append('safe', ret.data.safe);
  77. uploader.form.append('uptype', ret.data.uptype);
  78. if (ret.data.uptype === 'qiniu') {
  79. uploader.form.append('token', ret.data.token);
  80. } else if (ret.data.uptype === 'alioss') {
  81. uploader.form.append('policy', ret.data['policy']);
  82. uploader.form.append('signature', ret.data['signature']);
  83. uploader.form.append('OSSAccessKeyId', ret.data['OSSAccessKeyId']);
  84. uploader.form.append('success_action_status', '200');
  85. uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
  86. } else if (ret.data.uptype === 'txcos') {
  87. uploader.form.append('q-ak', ret.data['q-ak']);
  88. uploader.form.append('policy', ret.data['policy']);
  89. uploader.form.append('q-key-time', ret.data['q-key-time']);
  90. uploader.form.append('q-signature', ret.data['q-signature']);
  91. uploader.form.append('q-sign-algorithm', ret.data['q-sign-algorithm']);
  92. uploader.form.append('success_action_status', '200');
  93. uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
  94. } else if (ret.data.uptype === 'upyun') {
  95. uploader.form.delete('key');
  96. uploader.form.delete('safe');
  97. uploader.form.delete('uptype');
  98. uploader.form.append('save-key', ret.data['key']);
  99. uploader.form.append('policy', ret.data['policy']);
  100. uploader.form.append('authorization', ret.data['authorization']);
  101. uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
  102. }
  103. uploader.form.append('file', file), jQuery.ajax({
  104. url: uploader.url, data: uploader.form, type: 'post', xhr: function (xhr) {
  105. xhr = new XMLHttpRequest();
  106. return xhr.upload.addEventListener('progress', function (event) {
  107. file.xtotal = event.total, file.xloaded = event.loaded || 0;
  108. that.progress((file.xloaded / file.xtotal * 100).toFixed(2), file)
  109. }), xhr;
  110. }, contentType: false, error: function () {
  111. that.event('upload.error', {file: file}, file, '接口异常');
  112. }, processData: false, success: function (ret) {
  113. // 兼容数据格式
  114. if (typeof ret === 'string' && ret.length > 0) try {
  115. ret = JSON.parse(ret) || ret;
  116. } catch (e) {
  117. console.log(e)
  118. }
  119. if (typeof ret !== 'object') {
  120. ret = {code: 1, url: file.xurl, info: '上传成功'};
  121. }
  122. /*! 检查单个文件上传返回的结果 */
  123. if (typeof ret === 'object' && ret.code < 1) {
  124. that.event('upload.error', {file: file}, file, ret.info || '上传失败');
  125. } else {
  126. that.done(ret, file.index, file, done, '上传成功');
  127. }
  128. }
  129. });
  130. } else if (parseInt(ret.code) === 200) {
  131. (file.xurl = ret.data.url), that.progress('100.00', file);
  132. that.done({code: 1, url: file.xurl, info: file.xstats}, file.index, file, done, '秒传成功');
  133. } else {
  134. that.event('upload.error', {file: file}, file, ret.info || ret.error.message || '上传出错!');
  135. }
  136. }
  137. });
  138. };
  139. // 上传进度
  140. Adapter.prototype.progress = function (number, file) {
  141. this.event('upload.progress', {number: number, file: file});
  142. if (file.notify) file.notify.setProgress(number);
  143. };
  144. // 上传结果
  145. Adapter.prototype.done = function (ret, idx, file, done, message) {
  146. /*! 检查单个文件上传返回的结果 */
  147. if (ret.code < 1) return $.msg.tips(ret.info || '文件上传失败!');
  148. if (typeof file.xurl !== 'string') return $.msg.tips('无效的文件上传对象!');
  149. /*! 单个文件上传成功结果处理 */
  150. if (typeof done === 'function') {
  151. done.call(this.option.elem, file.xurl, this.files['id']);
  152. } else if (this.option.mult < 1 && this.option.elem.data('input')) {
  153. $(this.option.elem.data('input')).val(file.xurl).trigger('change', file);
  154. }
  155. // 文件上传成功事件
  156. this.event('upload.done', {file: file, data: ret}, file, message);
  157. /*! 所有文件上传完成后结果处理 */
  158. if (this.count.success + this.count.error >= this.count.total) {
  159. this.option.hide || $.msg.close(this.loader);
  160. if (this.option.mult > 0 && this.option.elem.data('input')) {
  161. var urls = this.option.elem.data('input').value || [];
  162. if (typeof urls === 'string') urls = urls.split('|');
  163. for (var i in this.files) urls.push(this.files[i].xurl);
  164. $(this.option.elem.data('input')).val(urls.join('|')).trigger('change', files);
  165. }
  166. this.event('upload.complete', {file: this.files}, file).init().uploader && this.uploader.reload();
  167. }
  168. };
  169. /*! 触发事件过程 */
  170. Adapter.prototype.event = function (name, data, file, message) {
  171. if (name === 'upload.error') {
  172. this.count.error++, file.xstate = -1, file.xstats = message;
  173. if (file.notify) file.notify.setError(message || file.xstats || '');
  174. } else if (name === 'upload.done') {
  175. this.count.success++, file.xstate = 1, file.xstats = message;
  176. if (file.notify) file.notify.setSuccess(message || file.xstats || '')
  177. }
  178. if (this.option.elem) {
  179. this.option.elem.triggerHandler(name, data);
  180. }
  181. return this;
  182. };
  183. /*! 计算文件 HASH 值 */
  184. Adapter.prototype.hash = function (file) {
  185. var defer = jQuery.Deferred();
  186. file.xext = file.name.indexOf('.') > -1 ? file.name.split('.').pop() : 'tmp';
  187. /*! 兼容不能计算文件 HASH 的情况 */
  188. var IsDate = '{$nameType|default=""}'.indexOf('date') > -1;
  189. if (!window.FileReader || IsDate) return jQuery.when((function (xmd5, chars) {
  190. while (xmd5.length < 32) xmd5 += chars.charAt(Math.floor(Math.random() * chars.length));
  191. return SetFileXdata(file, xmd5, 6), defer.promise();
  192. })(layui.util.toDateString(Date.now(), 'yyyyMMddHHmmss-'), '0123456789'));
  193. /*! 读取文件并计算 HASH 值 */
  194. return new LoadNextChunk(file).ReadAsChunk();
  195. function SetFileXdata(file, xmd5, slice) {
  196. file.xmd5 = xmd5, file.xstate = 0, file.xstats = '';
  197. file.xkey = file.xmd5.substring(0, slice || 2) + '/' + file.xmd5.substring(slice || 2) + '.' + file.xext;
  198. return defer.resolve(file, file.xmd5, file.xkey), file;
  199. }
  200. function LoadNextChunk(file) {
  201. var that = this, reader = new FileReader(), spark = new SparkMD5.ArrayBuffer();
  202. var slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  203. this.chunkIdx = 0, this.chunkSize = 2097152, this.chunkTotal = Math.ceil(file.size / this.chunkSize);
  204. reader.onload = function (event) {
  205. spark.append(event.target.result);
  206. ++that.chunkIdx < that.chunkTotal ? that.ReadAsChunk() : SetFileXdata(file, spark.end());
  207. }, reader.onerror = function () {
  208. defer.reject();
  209. }, this.ReadAsChunk = function () {
  210. this.start = that.chunkIdx * that.chunkSize;
  211. this.loaded = this.start + that.chunkSize >= file.size ? file.size : this.start + that.chunkSize;
  212. reader.readAsArrayBuffer(slice.call(file, this.start, this.loaded));
  213. defer.notify(file, (this.loaded / file.size * 100).toFixed(2));
  214. return defer.promise();
  215. };
  216. }
  217. };
  218. return UploadAdapter;
  219. /*! Base64 内容转 File 对象 */
  220. function Base64ToFile(base64, filename) {
  221. var arr = base64.split(',');
  222. var mime = arr[0].match(/:(.*?);/)[1], suffix = mime.split('/')[1];
  223. var bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
  224. while (n--) u8arr[n] = bstr.charCodeAt(n);
  225. return new File([u8arr], filename + '.' + suffix, {type: mime});
  226. }
  227. /*! File 对象转 Base64 内容 */
  228. function FileToBase64(file) {
  229. var defer = jQuery.Deferred(), reader = new FileReader();
  230. reader.onload = function () {
  231. defer.resolve(this.result);
  232. };
  233. return reader.readAsDataURL(file), defer.promise();
  234. }
  235. /*! 图片压缩处理 */
  236. function ImageToThumb(url, quality) {
  237. var deferred = jQuery.Deferred();
  238. var maxWidth = 500, maxHeight = 400, image = new Image();
  239. image.src = url, image.onload = function () {
  240. var canvas = document.createElement('canvas');
  241. var context = canvas.getContext('2d');
  242. // 原始尺寸
  243. var originWidth = this.width, originHeight = this.height;
  244. // 目标尺寸
  245. var targetWidth = originWidth, targetHeight = originHeight;
  246. // 图片尺寸超过最大值的限制
  247. if (originWidth > maxWidth || originHeight > maxHeight) {
  248. if (originWidth / originHeight > maxWidth / maxHeight) {
  249. targetWidth = maxWidth;
  250. targetHeight = Math.round(maxWidth * (originHeight / originWidth));
  251. } else {
  252. targetHeight = maxHeight;
  253. targetWidth = Math.round(maxHeight * (originWidth / originHeight));
  254. }
  255. }
  256. canvas.width = targetWidth, canvas.height = targetHeight;
  257. context.clearRect(0, 0, targetWidth, targetHeight);
  258. context.drawImage(this, 0, 0, targetWidth, targetHeight);
  259. deferred.resolve(canvas.toDataURL('image/jpeg', quality || 0.92));
  260. };
  261. }
  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. });