UNPKG

@wiajs/ui

Version:

wia app ui packages

1,029 lines (1,026 loc) 38.2 kB
/** @jsxImportSource @wiajs/core */ // import {Event} from '@wiajs/core' // import {Page} from '@wiajs/core' import { jsx as _jsx, jsxs as _jsxs } from "@wiajs/core/jsx-runtime"; import Compress from '@wiajs/lib/compress'; // @ts-ignore import * as css from './index.less'; import { log as Log } from '@wiajs/util'; const log = Log({ m: 'uploader' }) // 创建日志实例 ; /** * @typedef {import('jquery')} $ * @typedef {JQuery} Dom */ /** @typedef {object} FileType * @prop {number} idx:数组索引 * @prop {Blob} rawFile * @prop {string} name * @prop {string} type * @prop {string} ext * @prop {number} size * @prop {string} status * @prop {HTMLCanvasElement} canvas * @prop {boolean} compress * @prop {string} url * @prop {string} id - 数据库中的附件id,上传时服务端返回或从数据库加载,用于修改 */ /** @typedef {object} Opts * @prop {string} url - 'https://lianlian.pub/img/upload', // 图片上传服务接口 * @prop {string} dir - 'slcj/contract', 图片存储路径,格式: 所有者/应用名称/分类,结尾不要带/ * dir: 'lianlian/esign/test', 图片存储路径,格式: 所有者/应用名称/分类 * @prop {Dom} el - $('.wia_uploader'), 容器 * @prop {boolean} [multiple] 缺省:true, 同时选择多个文件 * @prop {number} [limit] - 缺省:0 不限制数量 * @prop {boolean} [upload] - 缺省:true, 自动上传 * @prop {string} [accept] - 'image/jpg,image/jpeg,image/png,image/gif', // 选择文件类型 * accept: '*', // 不限类型 * @prop {boolean} [preview] - 缺省:true, 点击图片是否预览,图片可提供大图预览,其他文件可在preview事件中提供预览功能 * @prop {number} [left] - 0, // 预览偏移,master page * @prop {boolean} [compress] - 缺省:true, 启用压缩 * @prop {number} [maxSize] - 缺省:0, 压缩后最大尺寸 * @prop {number} [quality] - 缺省:0.6, 压缩比 * @prop {number} [width] - 缺省:0, 指定压缩后宽 * @prop {number} [height] - 缺省:0, 指定压缩后高 * @prop {string} [resize] - 缺省:'none', 与 width、height 一起使用,改变图像尺寸压缩 none|contain|cover' * @prop {number} [aspectRatio] - 缺省:0, 设置宽高比,0 关闭 * @prop {string} [crop] - 缺省:'img/crop', 不符合比例,进入裁剪 * @prop {*} [img] = null, // 指定 img,对于图片,如指定img,则使用img展示图片缩略图,否则自动在上传容器中加载缩略图 * @prop {*} [input] - null, // 上传成功后的url填入输入框,便于表单数据提交 * @prop {*} [choose] - 点击触发选择文件,默认为上传容器 * @prop {*} [data] - 请求体参数 * @prop {*} [header] - 请求头,传 token、bucket 等 * @prop {boolean} [withCredentials] - 缺省:false, */ const def = { url: 'https://lianlian.pub/img/upload', // dir: 'slcj/contract', // 图片存储路径,格式: 所有者/应用名称/分类,结尾不要带/ el: $('.wia_uploader'), multiple: true, limit: 0, upload: true, // accept: '*', // 不限类型 accept: 'image/jpg,image/jpeg,image/png,image/gif', // dir: 'lianlian/esign/test', // 图片存储路径,格式: 所有者/应用名称/分类 preview: true, left: 0, compress: true, maxSize: 0, quality: 0.6, width: 0, height: 0, resize: 'none', aspectRatio: 0, // crop: 'img/crop', // 不符合比例,进入裁剪 /** @type {JQuery} */ img: null, /** @type JQuery} */ input: null, /** @type JQuery} */ choose: null, header: {}, data: {}, withCredentials: false }; /** * 解析服务器返回失败消息 * @param {*} xhr * @returns */ const parseError = (xhr)=>{ let msg = ''; const { responseText, responseType, status, statusText } = xhr; if (responseText && responseType === 'text') { try { msg = JSON.parse(responseText); } catch (error) { msg = responseText; } } else { msg = `${status} ${statusText}`; } const err = new Error(msg); err.status = status; return err; }; /** * 解析服务器返回成功消息 * @param {*} rs * @returns */ const parseSuccess = (rs)=>{ if (rs) { try { return JSON.parse(rs); } catch (ex) { console.log('parseSuccess', { exp: ex.message }); } } return rs; }; let Uploader = class Uploader { /** * 构造函数 * @param {Opts} opts */ constructor(opts){ this.idx = 1; // super(opts, [page]) const _ = this; const opt = { ...def, ...opts }; _.opt = opt; _.el = opt.el; // _.page = page if (!opt.accept.startsWith('image/')) { opt.compress = false // 关闭压缩 ; opt.quality = 1 // 压缩比 ; opt.preview = false // 非图形,不提供内部预览 ; opt.aspectRatio = 0 // 设置宽高比,0 关闭 ; } // if (this.opt.dir) this.opt.dir = this.opt.dir.trim; this.init(opt); } /** * 初始化,可被调用 * @param {{el:*}} opt */ init(opt) { const _ = this; _.opt = opt; _.el = _.opt.el; /** @type {*[]} */ _.files = []; _.input = this.initInput(opt); _.page = opt.el.parentNode('.page'); _.el.removeClass('wia_uploader').addClass(`${css.wia_uploader} wia_uploader`) // 内置样式已改名 ; _.el.class('_input').removeClass('_input').addClass(`${css._input} _input`) // 内置样式已改名 ; this.bind(); } /** * 创建并返回 file input 组件保存到input中,用于选择文件 * input.click 可触发文件选择 * @param {*} opt */ initInput(opt) { const _ = this; // 选择文件后返回 this.changeHandler = async (e)=>{ let { files } = e.target; // console.log('Input', {files}) const type = Object.prototype.toString.call(files); if (type === '[object FileList]') { files = [].slice.call(files); } else if (type === '[object Object]' || type === '[object File]') { files = [ files ]; } _.hideChoose(); // 外部可干预,返回false或者文件数组 const ret = await _.callEvent('choose', files); // const ret = this.emit('local::choose', files) if (ret !== false) _.loadFiles(ret || files); }; /** @type{*} */ const el = document.createElement('input'); for (const [key, value] of Object.entries({ type: 'file', accept: opt.accept, multiple: opt.multiple, hidden: true })){ el[key] = value; } el.addEventListener('change', this.changeHandler); opt.el.append(el); return el; } // 大图浏览 getGallery() { const _ = this; if (!this.opt.preview) return null; let gal = this.page.class(`${css.wia_gallery}`); if (!gal || !gal.length) { const tmpl = /*#__PURE__*/ _jsxs("div", { class: css.wia_gallery, style: `display: none; left: ${_.opt.left}px`, children: [ /*#__PURE__*/ _jsx("span", { class: css._img }), /*#__PURE__*/ _jsx("div", { class: `flex-center ${css._opr}`, children: /*#__PURE__*/ _jsx("a", { href: "javascript:;", name: "delete", children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) }) ] }); gal = $(tmpl); gal.insertBefore(this.page.class('page-content')); // 图片预览 gal.click(/** @param {*}ev */ (ev)=>{ ev.stopPropagation() // 阻止冒泡,避免上层 choose再次触发 ; ev.preventDefault(); gal.hide(); // $gallery.fadeOut(100); }); gal.name('delete').click(/** @param {*}ev */ (ev)=>{ const idx = gal.class(`${css._img}`).data('fileid'); this.remove(idx); }); gal = this.page.class(`${css.wia_gallery}`); this.gallery = gal; } return gal; } /** * 事件绑定 */ bind() { const _ = this; const { opt } = _; // const self = this; // ontouchstart/addEventListener 有时无法触发文件选择 // opt.input.dom.onclick = ev => { // this.chooseFile(); // }; // /** * 外部更改input时,显示图片,如:模板视图加载时 * [{id, url}], [url], url */ opt.input.change(/** @param {*}ev */ (ev)=>{ try { // 优先获取 data let p = opt.input.dom.data; const val = opt.input.val(); // 字符串转对象 if ($.isEmpty(p) && val) { // json if (/^\{[\s\S]+\}/.test(val)) p = JSON.parse(val); else { p = { dir: '' }; p.url = val.split(','); } } // 加载 url if (p?.url) { _.clear(); _.files = p.url.map((v)=>{ // const {dir} = p; // const host = dir.replace(`/${opt.dir}`, ''); const f = { idx: this.idx++, // dir 可选 url: p.dir ? `${p.dir}/${v}` : v, status: 'upload', id: p.id }; return f; }); _.load(); } } catch (ex) { console.error(`input value exp:${ex.message}`); } }); // 点击容器,没有点图片则选择图片,点图片则预览, if (opt.el) { opt.el.click(/** @param {*}ev */ (ev)=>{ const file = $(ev.target).closest(`.${css._file}`); // console.log('el click', {file, ev, _file: styles._file}); // 点击图片,预览或裁剪 if (file.length > 0) { const f = this.getFile(file.data('fileid')); // 进入裁剪页面 if (f && f.status === 'crop' && opt.crop) { // el 上设置,手机可以触发选择文件,pc失效 ev.stopPropagation() // 阻止冒泡,避免上层 choose再次触发 ; ev.preventDefault() // 阻止缺省行为,可能导致层缺省行为无效 ; $.go(opt.crop, { src: 'crop', idx: f.idx, url: f.url, aspectRatio: opt.aspectRatio }); } else if (opt.preview) { // el 上设置,手机可以触发选择文件,pc失效 ev.stopPropagation() // 阻止冒泡,避免上层 choose再次触发 ; ev.preventDefault() // 阻止缺省行为,可能导致层缺省行为无效 ; this.showGallery(file); this.callEvent('preview', f); } } else if (!opt.choose) { ev.stopPropagation() // 阻止冒泡,避免上层 choose再次触发 ; _.chooseFile(); } }); } // 如指定文件选择器choose,点击则选择文件 opt.choose?.click((ev)=>{ ev.stopPropagation() // 阻止事件冒泡 ; _.chooseFile() // 触发文件选择 ; }); } hideChoose() { const { opt } = this; const el = opt.choose; const wrap = el.upper('._choose'); if (wrap.dom) wrap.hide(); else el.hide(); } showChoose() { const { opt } = this; const el = opt.choose; const wrap = el.upper('._choose'); if (wrap.dom) wrap.show(); else el.show(); } /** * * @param {Opts} opts */ config(opts) { this.opt = { ...this.opt, ...opts }; } /** * 图片显示 * @param {*} file */ showGallery(file) { if (file.length > 0) { const gal = this.getGallery(); if (gal.length) { gal.class(`${css._img}`).attr('style', file.attr('style')).data('fileid', file.data('fileid')); gal.show(); } } // $gallery.fadeIn(100); } /** * 响应事件[choose, load, success, error, exceed, change, progress] * @param {*} evt * @param {*} cb * @returns */ on(evt, cb) { if (evt && typeof cb === 'function') { this['on' + evt] = cb; } return this; } /** * 调用外部响应事件 * @param {*} evt * @param {...any} args * @returns */ callEvent(evt, ...args) { if (evt && this['on' + evt]) { return this['on' + evt].apply(this, args); } } /** * 利用隐藏的文件输入组件实现文件选择 */ chooseFile() { console.log('chooseFile'); this.input.value = ''; this.input.click() // 弹出文件选择 ; } /** * 加载文件,选择或外部传入的文件数组 * 注意,如果设置了 limit,则只能保留该数量文件 * @param {File|FileType[]} files * @returns {boolean} */ loadFiles(files) { const _ = this; try { if (!files) return false; const { opt } = _; if (opt.limit > 0 && files.length && files.length + _.files.length > opt.limit) { if (opt.limit === 1) _.clear(); else { _.callEvent('exceed', files); return false; } } _.files = _.files.concat(files.map((file)=>{ if (file.idx && file.rawFile) return file; // 任意后缀 const rg = /(\.(?:\w+))$/i.exec(file.name); let name = file.name; // 后缀改为小写 name = name.replace(/\.[^.]*$/, (ext)=>ext.toLowerCase()); return { idx: _.idx++, rawFile: file, mimeType: file.type, type: getFileType(file.type), name, ext: rg && rg[1], size: file.size, status: 'choose' }; })); _.callEvent('change', this.files) // 文件列表改变 ; _.load(); } catch (e) { log.err(e, 'loadFiles'); } return true; } /** * * @param {*} idx * @returns */ getFile(idx) { return this.files.find((f)=>f.idx == idx); } /** * 裁剪后,更新文件 * @param {number|string} idx * @param {Blob} blob */ async update(idx, blob) { const file = this.files.find((f)=>f.idx == idx); if (file && blob) { file.status = 'croped'; // @ts-ignore blob.name = file.name.replace(/\.\w+$/i, '.jpg'); file.ext = '.jpg'; file.size = blob.size; file.rawFile = blob; file.url = URL.createObjectURL(blob); this.load(); } } /** * 加载文件图标 * 从文件系统加载文件到上传容器,非图形文件用文件后缀图标表示,图片用缩略图表示 */ async load() { const _ = this; const { opt } = _; // const fs = this.files.filter(f => f.status === 'choose'); for (const f of _.files ?? []){ let src; let tp; const { ext } = f; if (f.status === 'choose') { f.status = 'load'; if (/\.(jpeg|jpg|png|gif)/i.test(ext)) { const URL1 = window.URL || window.webkitURL || window.mozURL; src = URL1 && f.rawFile && URL1.createObjectURL(f.rawFile); } else src = getThumb(ext); tp = /*#__PURE__*/ _jsx("div", { name: `img${f.idx}`, title: f.name, "data-fileid": f.idx, class: `flex-center ${css._file} _file ${css._status}`, style: `background-image: url(${src}); background-size: contain`, children: /*#__PURE__*/ _jsx("div", { class: css._content, children: "50%" }) }); if (opt.label || opt.delete) tp = /*#__PURE__*/ _jsxs("div", { class: `${css._wrap} _wrap`, children: [ tp, opt.label && /*#__PURE__*/ _jsx("p", { children: "上传中" }), opt.delete && /*#__PURE__*/ _jsx("div", { class: "attach-delete", children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) ] }); // 指定宽高比 if (opt.aspectRatio) { const img = await loadImg(src); if (Math.round(img.naturalWidth * 100 / img.naturalHeight) / 100 !== this.opt.aspectRatio) { f.status = 'crop'; f.img = img; f.url = src; tp = /*#__PURE__*/ _jsx("div", { name: `img${f.idx}`, title: f.name, "data-fileid": f.idx, class: `flex-center ${css._file} _file ${css._status}`, style: `background-image: url(${src}); background-size: contain`, children: /*#__PURE__*/ _jsx("div", { class: `flex-center ${css._content}`, children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) }); if (opt.label || opt.delete) tp = /*#__PURE__*/ _jsxs("div", { class: `${css._wrap} _wrap`, children: [ tp, opt.label && /*#__PURE__*/ _jsx("p", { children: "需裁剪" }), opt.delete && /*#__PURE__*/ _jsx("div", { class: "attach-delete", children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) ] }); } } } else if (f.status === 'croped') { // 裁剪后的文件,重新加载,准备自动上传 opt.el.name(`img${f.idx}`).remove(); f.status = 'load'; src = f.url; tp = /*#__PURE__*/ _jsx("div", { name: `img${f.idx}`, "data-fileid": f.idx, title: f.name, class: `flex-center ${css._file} _file ${css._status}`, style: `background-image: url(${src}); background-size: contain`, children: /*#__PURE__*/ _jsx("div", { class: css._content, children: "50%" }) }); if (opt.label || opt.delete) tp = /*#__PURE__*/ _jsxs("div", { class: `${css._wrap} _wrap`, children: [ tp, opt.label && /*#__PURE__*/ _jsx("p", { children: "上传中" }), opt.delete && /*#__PURE__*/ _jsx("div", { class: "attach-delete", children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) ] }); } else if (f.status === 'upload') { // 已上传 const n = opt.el.name(`img${f.idx}`); // 是否在内部显示图标,是否已加载 if (!n.dom && !opt.img) src = `${f.url}`; // 重新加载图标 if (src) { tp = /*#__PURE__*/ _jsx("div", { name: `img${f.idx}`, title: f.name, "data-fileid": f.idx, "data-src": src, class: `flex-center ${css._file} _file _upload`, style: `background-image: url(${src}); background-size: contain` }); if (opt.label || opt.delete) tp = /*#__PURE__*/ _jsxs("div", { class: `${css._wrap} _wrap`, children: [ tp, opt.label && /*#__PURE__*/ _jsx("p", { children: "上传成功" }), opt.delete && /*#__PURE__*/ _jsx("div", { class: "attach-delete", children: /*#__PURE__*/ _jsx("i", { class: "icon wiaicon", children: "" }) }) ] }); } } // 加载图标 if (src) { if (tp) { $(tp).insertBefore(opt.input); } else if (opt.img) { // 上传成功 let { img } = opt; if (img.dom.tagName !== 'IMG') img = img.find('img'); src = getThumb(ext, src); img.attr('src', src); opt.img.show(); } _.callEvent('load', f, _.files); console.log({ f, files: _.files }, 'load'); opt.upload && _.upload(); } } } /** * 压缩 * @param {FileType} file * @returns {Promise<FileType>} */ async compress(file) { let R; const _ = this; const { opt } = _; const { quality, maxSize, width, height, resize } = opt; if (!file || file.compress || file.status === 'upload' || file.status === 'crop') return; const com = new Compress(file.rawFile, { quality, maxSize, width, height, resize }); const blob = await com.press(); if (blob) { // The third parameter is required for server // formData.append('file', r, r.name); console.log('compress', { name: blob.name, rate: `${Math.round(blob.size * 100 / file.size)}%` }); file.rawFile = blob; file.size = blob.size; file.name = blob.name // png -> jpg ; file.ext = /(\.(?:\w+))$/i.exec(file.name)?.[1]; file.compress = true; // console.log('compress', {r}); R = file; // if (cb) cb.call(_, file); } return R; } /** * 上传成功的文件 [{url、id}] 以json 字符串写入 input * 不触发 change * 多个文件,每个文件单独触发! */ updateInput() { const _ = this; const { opt, files } = _; try { // 已上传成功文件 const fs = files.filter((f)=>f.status === 'upload'); // console.log({fs}, 'updateInput') const el = opt.input; el.dom.uploadData = []; if (fs.length > 0) { const rs = fs.map((f)=>({ id: f.id, url: f.url })); el.val(JSON.stringify(rs)); opt.val = rs; for (const f of fs){ const { id, type, name, url } = f; let { ext } = f; ext = ext.replace('.', ''); const { abb } = opt.data || {}; // 方便点击浏览 el.dom.uploadData.push({ id, url, type, ext, name, abb }); } } else this.opt.input.val(''); } catch (e) { log.err(e, 'updateInput'); } } /** * 清除内部文件 */ clear() { const _ = this; try { _.idx = 1; _.files = []; _.opt.el.find(`.${css._wrap}`).remove(); _.opt.el.find(`.${css._file}`).remove(); _.updateInput(); _.callEvent('change', this.files); } catch (e) { log.err(e, 'clear'); } } /** * 删除文件 * @param {*} file */ removeFile(file) { const idx = file.idx || file; this.remove(idx); } /** * 删除文件 * @param {{idx?:number, id?: number, url?:string}} opts */ remove(opts) { const _ = this; try { let { idx } = opts; const { id, url } = opts; if (typeof opts === 'number') idx = opts; let i; if (idx >= 0) i = _.files.findIndex((f)=>f.idx == idx); else if (id >= 0) i = _.files.findIndex((f)=>f.id == id); else if (url) i = _.files.findIndex((f)=>f.url === url); ({ idx } = _.files[i]); if (i > -1) { _.files.splice(i, 1); _.callEvent('change', _.files); } const el = _.opt.el.name(`img${idx}`); el.upper(`.${css._wrap}`).remove(); el.remove(); _.updateInput(); } catch (e) { log.err(e, 'remove'); } } /** * 非裁剪、上传状态文件,进入上传流程 * @param {*} file * @returns */ upload(file) { if (!this.files.length && !file) return; if (file) { const target = this.files.find((item)=>item.idx === file.idx || item.idx === file); target && target.status !== 'upload' && target.status !== 'crop' && this.prePost(target); } else { const fs = this.files.filter((f)=>f.status !== 'upload' && f.status !== 'crop'); fs.forEach((f)=>{ this.prePost(f); }); } } /** * 压缩文件 * @param {*} file */ async prePost(file) { const _ = this; if (_.opt.compress) { const f = await _.compress(file); _.post(f); } else this.post(file); } /** * 图片上传,将图片转成二进制Blob对象,装入formdata上传 * 文件上传浏览器自动设置 content-type: multipart/form-data; 分片上传 * @param {*} file */ async post(file) { const _ = this; const { opt } = _; if (!file.rawFile || file.status === 'upload') return; console.log({ file }, 'post'); let percent = 0; /** @type {NodeJS.Timer} */ let timer = null; const ls = opt.input.parent(); // 数据后50%用模拟进度 function mockProgress() { if (timer) return; timer = setInterval(()=>{ percent += 5; // $li.find(".progress span").css('width', percent + "%"); const f = ls.name(`img${file.idx}`); const content = f.class(`${css._content}`); content.html(`${percent}%`); // self.opt.input // .parent() // .name(file.name) // .class(`${styles._content}`) // .html(`${percent}%`); // console.log(`... ${percent}%`); if (percent >= 99) { clearInterval(timer); f.removeClass(`${css._status}`); content.remove(); } }, 50); } const { data, withCredentials, header } = opt; const fd = new FormData(); // 传入路径、文件数据和文件名称 // const name = `${file.idx}${file.ext}` // idx.文件扩展名,不可重复 const name = file.name // idx.文件扩展名,不可重复 ; fd.append(opt.dir, file.rawFile, name); if (data) for (const k of Object.keys(data)){ fd.append(k, data[k]); } const xhr = new XMLHttpRequest(); xhr.withCredentials = !!withCredentials; xhr.open('POST', this.opt.url); // 添加自定义 header if (header) for (const k of Object.keys(header)){ xhr.setRequestHeader(k, header[k]); } xhr.onreadystatechange = ()=>{ if (xhr.readyState === 4) if (xhr.status === 200) { // {code: 200, data:{}} const rs = parseSuccess(xhr.responseText); // 上传成功,返回文件名对象 if (rs.code === 200 && rs.data[name]) { file.status = 'upload' // 上传成功状态 ; // 返回数据: // { // '3.jpg': { // dir: 'img/req/', // file: 'a42f5e9265e42064d169c76700209d4f.jpg', // host: 'https://fin.wia.pub', // len: 55834, // name: '3.jpg', // url: 'https://fin.wia.pub/img/req/a42f5e9265e42064d169c76700209d4f.jpg', // id: 4523, // } // } const r = rs.data[name]; // 服务器返回存储路径、文件名称 if (r.url) { // const idx = r.name.replace(/\.\w+/i, '') // 去掉末尾 / 字符 r.dir = r.dir.replace(/\/$/, ''); // 不支持多文件、多次不同路径上传 file.url = r.url //`${r.host}/${r.dir}/${r.file}` ; file.id = r.id; let uf = ls.name(`img${file.idx}`); if (opt.label) uf = uf.parent(); // 上传成功,更新图片缩略图 if (opt.label) { const abb = opt.data?.abb ?? '上传成功'; uf.find('p').html(abb); } const src = getThumb(file.ext, file.url); let { img } = opt; if (img) { if (img.dom.tagName !== 'IMG') img = img.find('img'); img.attr('src', src); uf.remove() // 删除上传显示 ; opt.img.show(); } else { const n = opt.el.name(`img${file.idx}`); n.css('background-image', `url(${src})`); n.data('src', file.url); n.addClass('_upload'); } } _.showChoose(); // 填入 input,方便客户读取 _.updateInput(); // 上传成功事件 _.callEvent('success', rs.data, file, this.files); // _.emit('local::success', rs.data, file, this.files) } } else { file.status = 'error'; _.callEvent('error', parseError(xhr), file, this.files); } }; xhr.onerror = (e)=>{ file.status = 'error'; _.callEvent('error', parseError(xhr), file, _.files); _.showChoose(); }; // 数据发送进度,前50%展示该进度,后50%使用模拟进度! xhr.upload.onprogress = (e)=>{ if (timer) return; const { total, loaded } = e; percent = total > 0 ? 100 * loaded / total / 2 : 0; console.log(`... ${percent}%`); if (percent >= 50) mockProgress(); else { let n = opt.input.parent(); n = n.name(file.name); n = n.class(`${css._content}`); n.html(`${percent}%`); } e.percent = percent; _.callEvent('progress', e, file, _.files); }; console.log({ xhr, url: opt.url }, 'post'); xhr.send(fd); } destroy() { this.input.removeEventHandler('change', this.changeHandler); $(this.input).remove(); this.gallery.remove(); } }; /** * * @param {*} url * @returns */ function loadImg(url) { return new Promise((res, rej)=>{ // 不能使用页面中的img,页面中的img会压缩图片,得不到图片真实大小! const img = new Image(); img.src = url; if (img.complete) { res(img); } else { img.onload = ()=>res(img); } }); } /** * * @param {*} header * @returns */ function getHeader(header) { const R = { 'content-type': `multipart/form-data; boundary=${getBoundary()}` }; Object.keys(header).forEach((k)=>{ R[k.toLowerCase()] = header[k]; }); return R; } function getBoundary() { // This generates a 50 character boundary similar to those used by Firefox. // They are optimized for boyer-moore parsing. let R = '----'; for(let i = 0; i < 24; i++){ R += Math.floor(Math.random() * 10).toString(16); } return R; } /** * 获得文件类型 * @param {*} mimeType * @returns {string} */ function getFileType(mimeType) { let R; try { if (!mimeType || typeof mimeType !== 'string') return; if (mimeType.startsWith('image/')) R = 'img'; if (mimeType.startsWith('audio/')) R = 'audio'; if (mimeType.startsWith('video/')) R = 'video'; const docTypes = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'text/plain', 'text/csv' ]; if (docTypes.includes(mimeType)) R = 'doc'; } catch (e) { log.err(e, 'getFileType'); } return R; } /** * 获取上传文件缩略图标 * @param {string} ext * @param {string} [url] * @returns {string} */ function getThumb(ext, url) { let R; try { ext = `.${ext}`; if (ext.endsWith('.docx') || ext.endsWith('.docm')) ext = '.doc'; else if (ext.endsWith('.pptx')) ext = '.ppt'; else if (ext.endsWith('.xlsx') || ext.endsWith('.xlsm') || ext.endsWith('.xlsb')) ext = '.xls'; ext = ext.replace(/^\.+/, '.'); if (/\.(pdf|xls|doc|csv|txt|zip|rar|ppt|avi|mov|mp3)/i.test(ext)) R = `https://cos.wia.pub/wiajs/img/uploader/${ext.substring(1)}.png`; else R = url ?? 'https://cos.wia.pub/wiajs/img/uploader/raw.png'; } catch (e) { log.err(e, 'getThumb'); } return R; } export { Uploader as default, getFileType, getThumb };