upload.js 18 KB

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