├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── css │ ├── 427.e58885ad.css │ ├── 836.322c633c.css │ ├── app.42eccd68.css │ └── chunk-vendors.00935754.css ├── favicon.ico ├── fonts │ ├── element-icons.f1a45d74.ttf │ └── element-icons.ff18efd1.woff ├── img │ ├── image-icon.28a7d8a0.png │ ├── text-icon.c556116e.png │ └── video-icon.8fd1d080.png ├── index.html └── js │ ├── 427.3d2044d6.js │ ├── 836.7620d65a.js │ ├── app.700990af.js │ └── chunk-vendors.61dfd1fe.js ├── jsconfig.json ├── package.json ├── public ├── favicon.ico └── index.html ├── server ├── .gitignore ├── app.js ├── package.json ├── tmp │ └── .gitkeep ├── uploader-node.js └── uploads │ └── .gitkeep ├── src ├── App.vue ├── api │ └── index.js ├── assets │ └── logo.png ├── main.js ├── router │ └── index.js ├── styles │ └── index.scss └── views │ ├── About.vue │ └── Home.vue ├── vue-simple-uploader ├── GlobalUploader.vue ├── README.md ├── images │ ├── audio-icon.png │ ├── image-icon.png │ ├── text-icon.png │ ├── video-icon.png │ └── zip.png └── js │ ├── bus.js │ └── config.js ├── vue-webuploader ├── page.vue └── upload.vue └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = crlf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | "plugin:vue/essential", 8 | "eslint:recommended", 9 | "plugin:prettier/recommended", 10 | ], 11 | parserOptions: { 12 | parser: "@babel/eslint-parser", 13 | }, 14 | rules: { 15 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 16 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | package-lock.json 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, //一行的字符数,如果超过会进行换行,默认为80 3 | tabWidth: 2, //一个tab代表几个空格数 4 | useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减 5 | singleQuote: true, //字符串是否使用单引号,默认为false,使用双引号 6 | semi: false, //行位是否使用分号,默认为true 7 | trailingComma: 'none', //是否使用尾逗号,有三个可选值"" 8 | bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar } 9 | arrowFunctionParentheses: true // 箭头函数的参数是否需要被括号包裹 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2 | 3 | "Anti 996" License Version 1.0 (Draft) 4 | 5 | Permission is hereby granted to any individual or legal entity 6 | obtaining a copy of this licensed work (including the source code, 7 | documentation and/or related items, hereinafter collectively referred 8 | to as the "licensed work"), free of charge, to deal with the licensed 9 | work for any purpose, including without limitation, the rights to use, 10 | reproduce, modify, prepare derivative works of, distribute, publish 11 | and sublicense the licensed work, subject to the following conditions: 12 | 13 | 1. The individual or the legal entity must conspicuously display, 14 | without modification, this License and the notice on each redistributed 15 | or derivative copy of the Licensed Work. 16 | 17 | 2. The individual or the legal entity must strictly comply with all 18 | applicable laws, regulations, rules and standards of the jurisdiction 19 | relating to labor and employment where the individual is physically 20 | located or where the individual was born or naturalized; or where the 21 | legal entity is registered or is operating (whichever is stricter). In 22 | case that the jurisdiction has no such laws, regulations, rules and 23 | standards or its laws, regulations, rules and standards are 24 | unenforceable, the individual or the legal entity are required to 25 | comply with Core International Labor Standards. 26 | 27 | 3. The individual or the legal entity shall not induce, metaphor or force 28 | its employee(s), whether full-time or part-time, or its independent 29 | contractor(s), in any methods, to agree in oral or written form, to 30 | directly or indirectly restrict, weaken or relinquish his or her 31 | rights or remedies under such laws, regulations, rules and standards 32 | relating to labor and employment as mentioned above, no matter whether 33 | such written or oral agreements are enforceable under the laws of the 34 | said jurisdiction, nor shall such individual or the legal entity 35 | limit, in any methods, the rights of its employee(s) or independent 36 | contractor(s) from reporting or complaining to the copyright holder or 37 | relevant authorities monitoring the compliance of the license about 38 | its violation(s) of the said license. 39 | 40 | THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 43 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, 44 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 45 | OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE 46 | LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue uploader solutions 2 | 3 | Vue上传的解决方案 4 | 5 | 目前主要处理 `vue-simple-uploader` 的方案,基于 `vue-simple-uploader` 封装了可以分片、秒传及断点续传的上传插件 6 | 7 | 预览:[https://shady-xia.github.io/vue-uploader-solutions](https://shady-xia.github.io/vue-uploader-solutions) 8 | 9 | 基于vue3的版本:[https://github.com/shady-xia/vue-uploader-solutions-next](https://github.com/shady-xia/vue-uploader-solutions-next) 10 | 11 | ## 文章 12 | 13 | 该仓库有对应的文章进行分析: 14 | 15 | [基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件](https://www.cnblogs.com/xiahj/p/vue-simple-uploader.html) 16 | 17 | [vue-simple-uploader 常见问题整理](https://www.cnblogs.com/xiahj/p/15950975.html) 18 | 19 | ## 本地调试 20 | 21 | **前端服务**: 22 | ```bash 23 | npm install 24 | 25 | npm start 26 | # or 27 | npm run serve 28 | ``` 29 | 30 | 前端服务默认打开8080端口 31 | 32 | **node服务端** 33 | 34 | ```bash 35 | cd server 36 | npm install 37 | npm run server 38 | ``` 39 | 40 | node服务会打开3000端口,临时文件存在 tmp 目录下,上传成功的文件存在 uploads 目录下 41 | 42 | ## 插件的封装及使用 43 | 44 | ### GlobalUploader 45 | 46 | `GlobalUploader.vue` 为基于 `vue-simple-uploader` 二次封装的上传插件,源码路径为 `/vue-simple-uploader/GlobalUploader.vue` 47 | 48 | 它有两种使用方式: 49 | 50 | ### 通过bus作为全局组件使用 51 | 52 | 作为全局上传组件使用时,将组件注册在`App.vue`中,通过 `Event Bus` 的方式调用插件。 53 | 54 | 使用场景为:上传器为整个网站级别的,切换路由时不打断上传流程,上传窗口始终存在于网站右下角。 55 | 56 | **打开上传器** 57 | 58 | 调用`Bus.$emit('openUploader')`,打开上传器,弹出选择文件窗口,该函数有两个参数: 59 | 60 | * params:传给服务端的额外参数 61 | * options:上传选项,目前支持 target、testChunks、mergeFn、accept 62 | 63 | ```js 64 | Bus.$emit('openUploader', { 65 | params: { 66 | page: 'home' 67 | }, 68 | options: { 69 | target: 'http://10.0.0.10' 70 | } 71 | }) 72 | ``` 73 | 74 | **Bus Events** 75 | 76 | * fileAdded:文件选择后的回调 77 | * fileSuccess:文件上传成功的回调 78 | 79 | ```js 80 | // 文件选择后的回调 81 | Bus.$on('fileAdded', () => { 82 | console.log('文件已选择') 83 | }) 84 | 85 | // 文件上传成功的回调 86 | Bus.$on('fileSuccess', () => { 87 | console.log('文件上传成功') 88 | }) 89 | ``` 90 | 91 | ### 作为普通组件在单个页面中使用 92 | 93 | 使用场景为:在单个页面中使用上传器 94 | 95 | props: 96 | * global:请务必设置为 `false`,代表非全局 97 | * params:(同用法一)传给服务端的额外参数 98 | * options:(同用法一)上传选项,目前支持 target、testChunks、mergeFn、accept 99 | 100 | events: 101 | * fileAdded:文件选择后的回调 102 | * fileSuccess:文件上传成功的回调 103 | 104 | ```html 105 | 111 | ``` 112 | 113 | ## 其他上传器 114 | 115 | ### vue-webuploader(已不推荐) 116 | 117 | 见文章: 118 | [Vue2.0结合webuploader实现文件分片上传](https://www.cnblogs.com/xiahj/p/8529545.html) 119 | 120 | ## License 121 | 122 | 请捍卫自己作为劳动者的权利 123 | 124 | [![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) 125 | [![996.icu](https://img.shields.io/badge/link-996.icu-red.svg)](https://996.icu) 126 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/css/427.e58885ad.css: -------------------------------------------------------------------------------- 1 | .page-home[data-v-46138898]{height:100%}.file-box[data-v-46138898]{position:relative;margin-top:20px;padding:30px;flex:1;background-color:#fff}.file-box .title[data-v-46138898]{padding-left:10px;font-size:15px;border-left:4px solid #1989fa}.file-box .empty[data-v-46138898]{position:absolute;top:45%;left:50%;transform:translate(-50%,-50%)}.file-box .empty .upload[data-v-46138898]{color:#1989fa;cursor:pointer} -------------------------------------------------------------------------------- /docs/css/836.322c633c.css: -------------------------------------------------------------------------------- 1 | .text[data-v-baddbe00]{margin-top:20px} -------------------------------------------------------------------------------- /docs/css/app.42eccd68.css: -------------------------------------------------------------------------------- 1 | #global-uploader:not(.global-uploader-single){position:fixed;z-index:20;right:15px;bottom:15px;box-sizing:border-box}#global-uploader .uploader-app{width:520px}#global-uploader .file-panel{background-color:#fff;border:1px solid #e2e2e2;border-radius:7px 7px 0 0;box-shadow:0 0 10px rgba(0,0,0,.2)}#global-uploader .file-panel .file-title{display:flex;height:40px;line-height:40px;padding:0 15px;border-bottom:1px solid #ddd}#global-uploader .file-panel .file-title .operate{flex:1;text-align:right}#global-uploader .file-panel .file-title .operate i{font-size:18px}#global-uploader .file-panel .file-list{position:relative;height:240px;overflow-x:hidden;overflow-y:auto;background-color:#fff;transition:all .3s}#global-uploader .file-panel .file-list .file-item{background-color:#fff}#global-uploader .file-panel.collapse .file-title{background-color:#e7ecf2}#global-uploader .file-panel.collapse .file-list{height:0}#global-uploader .no-file{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:16px}#global-uploader .uploader-file.md5 .uploader-file-resume{display:none}#global-uploader .uploader-file-icon:before{content:""!important}#global-uploader .uploader-file-icon[icon=image]{background:url(/vue-uploader-solutions/img/image-icon.28a7d8a0.png)}#global-uploader .uploader-file-icon[icon=audio]{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACL0lEQVRYR2NkgILPN9XqGRgY0/4zMEjBxCihGRnZL3By+veyyDYvwWcOI9wBt9Su/P/PqE2Jpch6IQ7wM2BgYEhhkW2Zi8tcuAM+3VT/Ty3LQeYgOYCBkZEhi1mmZTo28+niALCDGBjzmWWbJ6E7gm4OADviP0MJs1xLL0pUwTjUjwKW65ycgZqYPv5fySzb2gETp1kIgCxgYZY5wMZmoMnAyCmO4uv/DHXMci3NkKiBAmqHAKEEzad+E2w3RQ5g4XG+wCHRyvD9ecnjv1+P+BKyFFmeag7glJpq8P/PmyNf7tnYDJgDQBb/+bzjwPfnBQ7EOoK6IfDvwwVGJgGD//8+XPhyxwJU+hEEVHfAv99vvjKzq1j//X7i4LfHCfaEXEB1B4B8ziW36hATm6rYlzuGGgPiACYWiZfcSvtZPt/SFB4gB4i94lba/+fzLW2CVTpNooCZ0+Iqq0DI6x/PSwjmBpo4gENywgFQyUZMdqTYAYwMDBeYeZwZwAURCdkPljbIdgCbSMFhdqEUMQYGFrn//78/YWTkVKWbA7jkVh1m5tCzRU/hdHMAr9r15wwMjJLoDvj5ftGhX6/b7AhlPYorI27F3cuZWGUjkQ36++Py1m+PQr1JsRykltw08IBLZtFVJg416X+/P3759WH+uz8fV/iRajklDiDHLqx6yA2BUQeMhgDtQuAzlTun+FzKyPj/Kq/aLR2UZjm1u+e4HMDIwPCMgeH/LF71W40gNQBMREcwqeJwNQAAAABJRU5ErkJggg==);background-size:contain}#global-uploader .uploader-file-icon[icon=video]{background:url(/vue-uploader-solutions/img/video-icon.8fd1d080.png)}#global-uploader .uploader-file-icon[icon=document]{background:url(/vue-uploader-solutions/img/text-icon.c556116e.png)}#global-uploader .uploader-file-icon[icon=unknown]{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAAAvCAYAAABkBQNlAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAK8AAACvAAsAdHPAAAAB3RJTUUH4wEJAy0K3tWoHgAABfxJREFUWMPFmEtsJFcVhr9bj66qfla7y+625+HGnoFhMgkDiQISEQIhJRIBCcECdpEQCzassmTNlgWKArNikIKYTZRFlA0SKxZImSBgHprYFsngV7fdbvfb3dXVXYeF7WGc8WuqrMm/rYe++s9/zz231F//9neAaeBnwHeBC4DGMRIRHiwtaLXtOt955RXthatX1Z2PP+b+0hK9TifsdbtyrjQtl8pzaqvVWNpqNd8Mw/DujDdFMAr46Q9eP+71GMA54PfA64DilJI9OAXomoZSBx8VAYViNB6XgT+ahvGLSr32YaPT5tb7HxwLpgE/f1qgfSmlWKlU+Mf9+yyvrtLtdBgOh3sX//9GEflqKPIbwzDKhZzL2laNW+9/cCzUj6MA7WujXmfhk0+o1mr0ez1GQXC4syLfDMPwhqa0smNZx4JpwMWoQAJcmp3lWy+/zBfn5sjl81i2ffT9Iq+GEt7QNK1sJxKUS+e4+e57h0KZUaEQwc1muVAqkXddLNtG1/UTHpFXwzD8g65pX7u9cI83fvTDJxzTRCQylFKKyuYmdxcXWatU6HU6BEeU7zNg3w5DuWEa5vzv/nzriVJqnLD8T9Jatcq/HzxgZX19F2o/6CeBIS+FYfj2YRmLBQQwe/48X79+nbnZWTKuS8KyDlzXlHbkMnqUsT2wn3z/e9x89714UCLC1MQEl8tlpjwPJ5nEMIx9KwBImCbqmMX9WPjn33rnT0wXJjHiQCmlWK5UGAOrlQqddvuJ8qVsB8tMMAj8I+H2wv+OaZpv/Gd9ZTEWFEC1VqPV69HrdNjpdg90dhHBNAyKEwU2G9sMhj6CPHLxM2DfEJHfisgvY0NdnJmhfPEiD1dWWF5bwx8MGPr+gXuSls35ySl6gz5+EBCGIUcE7TVD196KBSUilDyPL8/PM/B9tppNwvH4CSgAQzdwU9ldp47XpdiZWt/cRF9YOFWfOgUQQBi7fKvVKlvt9qGZivqtsftUaXKS5y5fZqZUIplOY5jRd619xc7Uxelprl+7hq5pdHZ26B7SFp5WsZxSSrFZr7P46ads1Grs9HqMRqPP1ymAh2trbDQa9DodemeTqehQSikE8PJ5zs/MsFatUhUhGA4JgiAWXCQopRS2ZYEICcPghStXME2TVrf7qHyObe+Cy6nawAFFypRSinzOxTRNKhsb/Hd1FTebJZ/JoADbsnCz2WfrlIiQy2T5ytXnmCvPMuV5GIZByfNYrVRYWV0n6SQjuRQZCsC2ba586RKpZJL+YECn1yOVTDI3O0shP8Hy8iqj8fjZOuXmMiQdh3a3y+07d2i0WxQLHi89/zyZdIp0Jk2j0YwU+MiZsveCvFmvU2800ZXOxtYWjVYLTdNwbCvyCoy8+jRt93uyqTSzpWl0TSMUIek4gJx4qjlzKABEEBEKeRcJQ7rdHrlclmw6TcR8x4OSx4YQpRRTXoFJr4BSxAaCMzjNAPhBQLvbJQji73uRnXpcO/0+t+/epdFqUfQ8Xrx2DSuR+PycUkqxub3NxtYWo9GItY0NGq1W7E05tlPhOKSYL5C0bHqD/pmUMBqU7M/bQspxyKXSaEphmib2XumibjEQsXwigu8PQcCxLLS9HmXoOknHQUToDwaRwSJDNdttxmFIOp3CK0zg2DZTkx7JpMNoNKLRbkd2KlL5NE2jWquRy2aYKRaZmS4yntqdFESE5UqFRrOFm848O6jd35mKj+7d4wvNJjPFIlYiQb/dZnl9nZVKBS/rouB0J72zgBIgk0zR7e9wf2mJxYcPMQ2DYRAwGo1w0xlSthMJCHYzFelZXdMoTXgUci4SCoPBAAUU8wWm3Il4M3oo4UhXeiTHDF3Hy7m4qQzjcIyu65jRXvVI4/FYtL7vx+p2CoVpGDgJKzYQQG/QR9tutxrDIDj2b9tpFHc4UCj6vk+93aoZO/7gn+v12gUv55K07DM5TD6tQhG6/R3q7SZ+EHxoAG8Phv6L6/XauYRhYsSYGKNqNB7hBwEi8kDBTQP4C/BmGIa/7g/9+TOZ0p5WSomCj4BfAf/6HzQmyAX0AQdiAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE5LTAxLTA5VDAzOjQ1OjEwKzA4OjAwPK9v8AAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOS0wMS0wOVQwMzo0NToxMCswODowME3y10wAAABDdEVYdHNvZnR3YXJlAC91c3IvbG9jYWwvaW1hZ2VtYWdpY2svc2hhcmUvZG9jL0ltYWdlTWFnaWNrLTcvL2luZGV4Lmh0bWy9tXkKAAAAGHRFWHRUaHVtYjo6RG9jdW1lbnQ6OlBhZ2VzADGn/7svAAAAGHRFWHRUaHVtYjo6SW1hZ2U6OkhlaWdodAAyMjRIjLPyAAAAF3RFWHRUaHVtYjo6SW1hZ2U6OldpZHRoADE3Nz1F+AkAAAAZdEVYdFRodW1iOjpNaW1ldHlwZQBpbWFnZS9wbmc/slZOAAAAF3RFWHRUaHVtYjo6TVRpbWUAMTU0Njk3NjcxMHAIjaYAAAARdEVYdFRodW1iOjpTaXplADk1NjJCr9FpAwAAAGJ0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL2hvbWUvd3d3cm9vdC9uZXdzaXRlL3d3dy5lYXN5aWNvbi5uZXQvY2RuLWltZy5lYXN5aWNvbi5jbi9maWxlcy8xMTgvMTE4NTYyNS5wbmfTFu+zAAAAAElFTkSuQmCC) no-repeat 50%;background-size:contain}#global-uploader .uploader-file-actions>span{margin-right:6px}#global-uploader .custom-status{position:absolute;top:0;left:0;right:0;bottom:0;z-index:1}#global-uploader-btn{position:absolute;clip:rect(0,0,0,0)}.global-uploader-single #global-uploader-btn{position:relative}#app{display:flex;flex-direction:column}#app .app-nav{background-color:#fff}#app .app-nav .app-nav-inner{display:flex;justify-content:space-between;align-items:center;width:1140px;margin:0 auto}#app .app-nav .app-nav-title{color:#1989fa;font-weight:600}#app .app-main{flex:1}#app .app-main>.container{height:100%}*{margin:0;padding:0;box-sizing:border-box}body,html{height:100%}body{font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Arial,sans-serif;background:#f2f3f8;color:#252525;font-size:14px;-webkit-tap-highlight-color:transparent}#app{height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.container{width:1140px;margin:0 auto}.page{display:flex;flex-direction:column;padding:20px 0} -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/docs/favicon.ico -------------------------------------------------------------------------------- /docs/fonts/element-icons.f1a45d74.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/docs/fonts/element-icons.f1a45d74.ttf -------------------------------------------------------------------------------- /docs/fonts/element-icons.ff18efd1.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/docs/fonts/element-icons.ff18efd1.woff -------------------------------------------------------------------------------- /docs/img/image-icon.28a7d8a0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/docs/img/image-icon.28a7d8a0.png -------------------------------------------------------------------------------- /docs/img/text-icon.c556116e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/docs/img/text-icon.c556116e.png -------------------------------------------------------------------------------- /docs/img/video-icon.8fd1d080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/docs/img/video-icon.8fd1d080.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Vue Uploader Solutions
-------------------------------------------------------------------------------- /docs/js/427.3d2044d6.js: -------------------------------------------------------------------------------- 1 | "use strict";(self["webpackChunkvue_uploader_solutions"]=self["webpackChunkvue_uploader_solutions"]||[]).push([[427],{1427:function(e,t,s){s.r(t),s.d(t,{default:function(){return p}});var l=function(){var e=this,t=e._self._c;return t("div",{staticClass:"page page-home"},[t("div",{staticClass:"btn-group"},[t("el-button",{attrs:{type:"primary",size:"medium"},on:{click:e.upload}},[e._v("上传")])],1),t("div",{staticClass:"file-box"},[t("p",{staticClass:"title"},[e._v("文件资源库")]),e.fileList.length?e._e():t("div",{staticClass:"empty"},[t("el-empty",{scopedSlots:e._u([{key:"description",fn:function(){return[t("p",[e._v("暂无文件,请先"),t("a",{staticClass:"upload",on:{click:e.upload}},[e._v("上传")])])]},proxy:!0}],null,!1,1751674375)})],1)])])},o=[],i=s(8558),a={name:"home",data(){return{fileList:[]}},mounted(){i.Z.$on("fileAdded",(()=>{console.log("文件已选择")})),i.Z.$on("fileSuccess",(()=>{console.log("文件上传成功")}))},beforeDestroy(){i.Z.$off("fileAdded"),i.Z.$off("fileSuccess")},methods:{upload(){i.Z.$emit("openUploader",{params:{page:"home"}})}}},n=a,u=s(1001),c=(0,u.Z)(n,l,o,!1,null,"46138898",null),p=c.exports}}]); -------------------------------------------------------------------------------- /docs/js/836.7620d65a.js: -------------------------------------------------------------------------------- 1 | "use strict";(self["webpackChunkvue_uploader_solutions"]=self["webpackChunkvue_uploader_solutions"]||[]).push([[836],{7836:function(t,s,u){u.r(s),u.d(s,{default:function(){return r}});var a=function(){var t=this;t._self._c;return t._m(0)},e=[function(){var t=this,s=t._self._c;return s("div",{staticClass:"page page-about"},[s("div",{staticClass:"about"},[s("h1",[t._v("这是关于页面")]),s("p",{staticClass:"text"},[t._v("只是为了模拟上传器的全局效果而已")])])])}],n={name:"about"},l=n,i=u(1001),o=(0,i.Z)(l,a,e,!1,null,"baddbe00",null),r=o.exports}}]); -------------------------------------------------------------------------------- /docs/js/app.700990af.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";var e={9253:function(e,t,n){var o=n(6369),i=function(){var e=this,t=e._self._c;return t("div",{attrs:{id:"app"}},[t("div",{staticClass:"app-nav"},[t("div",{staticClass:"app-nav-inner"},[t("el-menu",{staticClass:"app-nav-menu",attrs:{mode:"horizontal","active-text-color":"#1989fa","default-active":e.activeRoute,router:""}},[t("el-menu-item",{attrs:{index:"/home"}},[e._v("vue-simple-uploader")]),t("el-menu-item",{attrs:{index:"/about"}},[e._v("关于")])],1),t("div",{staticClass:"app-nav-title"},[e._v("Vue Uploader Solutions")])],1)]),t("div",{staticClass:"app-main"},[t("div",{staticClass:"container"},[t("router-view")],1)]),t("global-uploader")],1)},r=[],s=function(){var e=this,t=e._self._c;return t("div",{class:{"global-uploader-single":!e.global},attrs:{id:"global-uploader"}},[t("uploader",{ref:"uploader",staticClass:"uploader-app",attrs:{options:e.initOptions,fileStatusText:e.fileStatusText,autoStart:!1},on:{"file-added":e.onFileAdded,"file-success":e.onFileSuccess,"file-progress":e.onFileProgress,"file-error":e.onFileError}},[t("uploader-unsupport"),t("uploader-btn",{ref:"uploadBtn",attrs:{id:"global-uploader-btn"}},[e._v("选择文件")]),t("uploader-list",{directives:[{name:"show",rawName:"v-show",value:e.panelShow,expression:"panelShow"}],scopedSlots:e._u([{key:"default",fn:function(n){return t("div",{staticClass:"file-panel",class:{collapse:e.collapse}},[t("div",{staticClass:"file-title"},[t("div",{staticClass:"title"},[e._v("文件列表")]),t("div",{staticClass:"operate"},[t("el-button",{attrs:{type:"text",title:e.collapse?"展开":"折叠"},on:{click:function(t){e.collapse=!e.collapse}}},[t("i",{staticClass:"iconfont",class:e.collapse?"el-icon-full-screen":"el-icon-minus"})]),t("el-button",{attrs:{type:"text",title:"关闭"},on:{click:e.close}},[t("i",{staticClass:"el-icon-close"})])],1)]),t("ul",{staticClass:"file-list"},[e._l(n.fileList,(function(n){return t("li",{key:n.id,staticClass:"file-item"},[t("uploader-file",{ref:"files",refInFor:!0,class:["file_"+n.id,e.customStatus],attrs:{file:n,list:!0}})],1)})),n.fileList.length?e._e():t("div",{staticClass:"no-file"},[t("i",{staticClass:"iconfont icon-empty-file"}),e._v(" 暂无待上传文件 ")])],2)])}}])})],1)],1)},a=[];const l={image:[".png",".jpg",".jpeg",".gif",".bmp"],video:[".mp4",".rmvb",".mkv",".wmv",".flv"],document:[".doc",".docx",".xls",".xlsx",".ppt",".pptx",".pdf",".txt",".tif",".tiff"],getAll(){return[...this.image,...this.video,...this.document]}};var u=n(8558),c=n(7335),d=n.n(c);function p(){return Promise.resolve()}var f={props:{global:{type:Boolean,default:!0},params:{type:Object},options:{type:Object}},data(){return{initOptions:{target:"http://localhost:3000/upload",chunkSize:"2048000",fileParameterName:"file",maxChunkRetries:3,testChunks:!0,checkChunkUploadedByResponse:function(e,t){let n=!1;try{let o=JSON.parse(t);n=!!o.skipUpload||(o.uploaded||[]).indexOf(e.offset+1)>=0}catch(o){}return n},query:(e,t)=>({...e.params})},fileStatusText:{success:"上传成功",error:"上传失败",uploading:"上传中",paused:"已暂停",waiting:"等待上传"},panelShow:!1,collapse:!1,customParams:{},customStatus:""}},watch:{params:{handler(e){e&&(this.customParams=e)},immediate:!0},options:{handler(e){e&&setTimeout((()=>{this.customizeOptions(e)}),0)},immediate:!0}},mounted(){u.Z.$on("openUploader",(({params:e={},options:t={}})=>{this.customParams=e,this.customizeOptions(t),this.$refs.uploadBtn&&this.$refs.uploadBtn.$el.click()}))},computed:{uploader(){return this.$refs.uploader.uploader}},methods:{customizeOptions(e){e.target&&(this.uploader.opts.target=e.target),void 0!==e.testChunks&&(this.uploader.opts.testChunks=e.testChunks),this.mergeFn=e.mergeFn||p;let t=document.querySelector("#global-uploader-btn input"),n=e.accept||l.getAll();t.setAttribute("accept",n.join())},onFileAdded(e){this.panelShow=!0,this.emit("fileAdded"),e.params=this.customParams,this.computeMD5(e).then((e=>this.startUpload(e)))},computeMD5(e){let t=new FileReader,n=(new Date).getTime(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,i=0;const r=1024e4;let s=Math.ceil(e.size/r),a=new(d().ArrayBuffer);return this.statusSet(e.id,"md5"),e.pause(),l(),new Promise(((o,r)=>{t.onload=t=>{if(a.append(t.target.result),i{const t="校验MD5 "+(i/s*100).toFixed(0)+"%";document.querySelector(`.custom-status-${e.id}`).innerText=t}));else{let t=a.end();o({md5:t,file:e}),console.log(`MD5计算完毕:${e.name} \nMD5:${t} \n分片:${s} 大小:${e.size} 用时:${(new Date).getTime()-n} ms`)}},t.onerror=function(){this.error(`文件${e.name}读取出错,请检查该文件`),e.cancel(),r()}}));function l(){let n=i*r,s=n+r>=e.size?e.size:n+r;t.readAsArrayBuffer(o.call(e.file,n,s))}},startUpload({md5:e,file:t}){t.uniqueIdentifier=e,t.resume(),this.statusRemove(t.id)},onFileSuccess(e,t,n,o){let i=JSON.parse(n);if(!i.result)return this.error(i.message),void this.statusSet(t.id,"failed");i.needMerge?(this.statusSet(t.id,"merging"),this.mergeFn({tempName:i.tempName,fileName:t.name,...t.params}).then((e=>{this.emit("fileSuccess"),this.statusRemove(t.id)})).catch((e=>{}))):(this.emit("fileSuccess"),console.log("上传成功"))},onFileProgress(e,t,n){console.log(`上传中 ${t.name},chunk:${n.startByte/1024/1024} ~ ${n.endByte/1024/1024}`)},onFileError(e,t,n,o){this.error(n)},close(){this.uploader.cancel(),this.panelShow=!1},statusSet(e,t){let n={md5:{text:"校验MD5",bgc:"#fff"},merging:{text:"合并中",bgc:"#e2eeff"},transcoding:{text:"转码中",bgc:"#e2eeff"},failed:{text:"上传失败",bgc:"#e2eeff"}};this.customStatus=t,this.$nextTick((()=>{const o=document.createElement("p");o.className=`custom-status-${e} custom-status`,o.innerText=n[t].text,o.style.backgroundColor=n[t].bgc;const i=document.querySelector(`.file_${e} .uploader-file-status`);i.appendChild(o)}))},statusRemove(e){this.customStatus="",this.$nextTick((()=>{const t=document.querySelector(`.custom-status-${e}`);t.remove()}))},emit(e){u.Z.$emit(e),this.$emit(e)},error(e){this.$notify({title:"错误",message:e,type:"error",duration:2e3})}}},m=f,h=n(1001),v=(0,h.Z)(m,s,a,!1,null,null,null),g=v.exports,b={name:"app",components:{GlobalUploader:g},data(){return{activeRoute:""}},watch:{"$route.path":{handler(e){this.activeRoute=e},immediate:!1}}},y=b,S=(0,h.Z)(y,i,r,!1,null,null,null),C=S.exports,w=n(2631);o["default"].use(w.Z);var k=new w.Z({base:"/vue-uploader-solutions/",routes:[{path:"/",redirect:"/home"},{path:"/home",name:"home",component:()=>n.e(427).then(n.bind(n,1427))},{path:"/about",name:"about",component:()=>n.e(836).then(n.bind(n,7836))},{path:"*",redirect:"/"}]}),x=n(3103),$=n.n(x),_=n(8499),O=n.n(_);o["default"].use($()),o["default"].use(O()),o["default"].config.productionTip=!1,new o["default"]({router:k,render:e=>e(C)}).$mount("#app")},8558:function(e,t,n){var o=n(6369);t["Z"]=new o["default"]}},t={};function n(o){var i=t[o];if(void 0!==i)return i.exports;var r=t[o]={exports:{}};return e[o].call(r.exports,r,r.exports,n),r.exports}n.m=e,function(){var e=[];n.O=function(t,o,i,r){if(!o){var s=1/0;for(c=0;c=r)&&Object.keys(n.O).every((function(e){return n.O[e](o[l])}))?o.splice(l--,1):(a=!1,r0&&e[c-1][2]>r;c--)e[c]=e[c-1];e[c]=[o,i,r]}}(),function(){n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,{a:t}),t}}(),function(){n.d=function(e,t){for(var o in t)n.o(t,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})}}(),function(){n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce((function(t,o){return n.f[o](e,t),t}),[]))}}(),function(){n.u=function(e){return"js/"+e+"."+{427:"3d2044d6",836:"7620d65a"}[e]+".js"}}(),function(){n.miniCssF=function(e){return"css/"+e+"."+{427:"e58885ad",836:"322c633c"}[e]+".css"}}(),function(){n.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()}(),function(){n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}}(),function(){var e={},t="vue-uploader-solutions:";n.l=function(o,i,r,s){if(e[o])e[o].push(i);else{var a,l;if(void 0!==r)for(var u=document.getElementsByTagName("script"),c=0;c 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue Uploader Solutions 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | !tmp/ .gitkeep 3 | uploads/* 4 | !uploads/ .gitkeep 5 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | process.env.TMPDIR = 'tmp' // to avoid the EXDEV rename error, see http://stackoverflow.com/q/21071303/76173 2 | 3 | const express = require('express') 4 | const multipart = require('connect-multiparty') 5 | const multipartMiddleware = multipart() 6 | const uploader = require('./uploader-node.js')('tmp') 7 | const app = express() 8 | const fs = require('fs') 9 | 10 | // Configure access control allow origin header stuff 11 | const ACCESS_CONTROLL_ALLOW_ORIGIN = true 12 | 13 | // Host 14 | // app.use(express.static(__dirname + '/../docs')) 15 | 16 | // Handle uploads through Uploader.js 17 | app.post('/upload', multipartMiddleware, function (req, res) { 18 | uploader.post(req, function (status, filename, original_filename, identifier) { 19 | console.log('POST', status, original_filename, identifier) 20 | if (ACCESS_CONTROLL_ALLOW_ORIGIN) { 21 | res.header('Access-Control-Allow-Origin', '*') 22 | res.header('Access-Control-Allow-Headers', 'content-type') 23 | } 24 | 25 | if (status === 'done') { 26 | const s = fs.createWriteStream('./uploads/' + filename) 27 | s.on('finish', function () { 28 | res.status(200) 29 | }) 30 | 31 | uploader.write(identifier, s, { end: true }) 32 | } else { 33 | res.status(/^(partly_done|done)$/.test(status) ? 200 : 500) 34 | } 35 | 36 | const data = { 37 | result: true, 38 | message: '' 39 | } 40 | res.send(data) 41 | }) 42 | }) 43 | 44 | app.options('/upload', function (req, res) { 45 | console.log('OPTIONS') 46 | if (ACCESS_CONTROLL_ALLOW_ORIGIN) { 47 | res.header('Access-Control-Allow-Origin', '*') 48 | res.header('Access-Control-Allow-Headers', 'content-type') 49 | } 50 | res.status(200).send() 51 | }) 52 | 53 | // Handle status checks on chunks through Uploader.js 54 | app.get('/upload', function (req, res) { 55 | uploader.get(req, function (status, filename, original_filename, identifier) { 56 | console.log('GET', status) 57 | if (ACCESS_CONTROLL_ALLOW_ORIGIN) { 58 | res.header('Access-Control-Allow-Origin', '*') 59 | } 60 | 61 | if (status == 'found') { 62 | status = 200 63 | } else { 64 | status = 204 65 | } 66 | 67 | res.status(status).send() 68 | }) 69 | }) 70 | 71 | app.get('/download/:identifier', function (req, res) { 72 | uploader.write(req.params.identifier, res) 73 | }) 74 | 75 | app.listen(3000, function () { 76 | console.log('Server started...') 77 | }) 78 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "scripts": { 6 | "server": "node app.js" 7 | }, 8 | "dependencies": { 9 | "connect-multiparty": "^2.2.0", 10 | "express": "^4.3.1", 11 | "mv": "^2.1.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/server/tmp/.gitkeep -------------------------------------------------------------------------------- /server/uploader-node.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | util = require('util'), 4 | mv = require('mv'), 5 | Stream = require('stream').Stream 6 | 7 | module.exports = function (temporaryFolder) { 8 | var $ = this 9 | $.temporaryFolder = temporaryFolder 10 | $.maxFileSize = null 11 | $.fileParameterName = 'file' 12 | 13 | try { 14 | fs.mkdirSync($.temporaryFolder) 15 | } catch (e) {} 16 | 17 | function cleanIdentifier(identifier) { 18 | return identifier.replace(/[^0-9A-Za-z_-]/g, '') 19 | } 20 | 21 | function getChunkFilename(chunkNumber, identifier) { 22 | // Clean up the identifier 23 | identifier = cleanIdentifier(identifier) 24 | // What would the file name be? 25 | return path.resolve($.temporaryFolder, './uploader-' + identifier + '.' + chunkNumber) 26 | } 27 | 28 | function validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename, fileSize) { 29 | // Clean up the identifier 30 | identifier = cleanIdentifier(identifier) 31 | 32 | // Check if the request is sane 33 | if ( 34 | chunkNumber == 0 || 35 | chunkSize == 0 || 36 | totalSize == 0 || 37 | identifier.length == 0 || 38 | filename.length == 0 39 | ) { 40 | return 'non_uploader_request' 41 | } 42 | var numberOfChunks = Math.max(Math.floor(totalSize / (chunkSize * 1.0)), 1) 43 | if (chunkNumber > numberOfChunks) { 44 | return 'invalid_uploader_request1' 45 | } 46 | 47 | // Is the file too big? 48 | if ($.maxFileSize && totalSize > $.maxFileSize) { 49 | return 'invalid_uploader_request2' 50 | } 51 | 52 | if (typeof fileSize != 'undefined') { 53 | if (chunkNumber < numberOfChunks && fileSize != chunkSize) { 54 | // The chunk in the POST request isn't the correct size 55 | return 'invalid_uploader_request3' 56 | } 57 | if ( 58 | numberOfChunks > 1 && 59 | chunkNumber == numberOfChunks && 60 | fileSize != (totalSize % chunkSize) + parseInt(chunkSize) 61 | ) { 62 | // The chunks in the POST is the last one, and the fil is not the correct size 63 | return 'invalid_uploader_request4' 64 | } 65 | if (numberOfChunks == 1 && fileSize != totalSize) { 66 | // The file is only a single chunk, and the data size does not fit 67 | return 'invalid_uploader_request5' 68 | } 69 | } 70 | 71 | return 'valid' 72 | } 73 | 74 | //'found', filename, original_filename, identifier 75 | //'not_found', null, null, null 76 | $.get = function (req, callback) { 77 | var chunkNumber = req.param('chunkNumber', 0) 78 | var chunkSize = req.param('chunkSize', 0) 79 | var totalSize = req.param('totalSize', 0) 80 | var identifier = req.param('identifier', '') 81 | var filename = req.param('filename', '') 82 | 83 | if (validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename) == 'valid') { 84 | var chunkFilename = getChunkFilename(chunkNumber, identifier) 85 | fs.exists(chunkFilename, function (exists) { 86 | if (exists) { 87 | callback('found', chunkFilename, filename, identifier) 88 | } else { 89 | callback('not_found', null, null, null) 90 | } 91 | }) 92 | } else { 93 | callback('not_found', null, null, null) 94 | } 95 | } 96 | 97 | //'partly_done', filename, original_filename, identifier 98 | //'done', filename, original_filename, identifier 99 | //'invalid_uploader_request', null, null, null 100 | //'non_uploader_request', null, null, null 101 | $.post = function (req, callback) { 102 | var fields = req.body 103 | var files = req.files 104 | 105 | var chunkNumber = fields['chunkNumber'] 106 | var chunkSize = fields['chunkSize'] 107 | var totalSize = fields['totalSize'] 108 | var identifier = cleanIdentifier(fields['identifier']) 109 | var filename = fields['filename'] 110 | 111 | if (!files[$.fileParameterName] || !files[$.fileParameterName].size) { 112 | callback('invalid_uploader_request', null, null, null) 113 | return 114 | } 115 | 116 | var original_filename = files[$.fileParameterName]['originalFilename'] 117 | var validation = validateRequest( 118 | chunkNumber, 119 | chunkSize, 120 | totalSize, 121 | identifier, 122 | filename, 123 | files[$.fileParameterName].size 124 | ) 125 | if (validation == 'valid') { 126 | var chunkFilename = getChunkFilename(chunkNumber, identifier) 127 | 128 | // Save the chunk (TODO: OVERWRITE) 129 | mv(files[$.fileParameterName].path, chunkFilename, function (err) { 130 | if (err) { 131 | console.error('err:', err) 132 | } 133 | 134 | // Do we have all the chunks? 135 | var currentTestChunk = 1 136 | var numberOfChunks = Math.max(Math.floor(totalSize / (chunkSize * 1.0)), 1) 137 | var testChunkExists = function () { 138 | fs.exists(getChunkFilename(currentTestChunk, identifier), function (exists) { 139 | if (exists) { 140 | currentTestChunk++ 141 | if (currentTestChunk > numberOfChunks) { 142 | callback('done', filename, original_filename, identifier) 143 | } else { 144 | // Recursion 145 | testChunkExists() 146 | } 147 | } else { 148 | callback('partly_done', filename, original_filename, identifier) 149 | } 150 | }) 151 | } 152 | testChunkExists() 153 | }) 154 | } else { 155 | callback(validation, filename, original_filename, identifier) 156 | } 157 | } 158 | 159 | // Pipe chunks directly in to an existsing WritableStream 160 | // r.write(identifier, response); 161 | // r.write(identifier, response, {end:false}); 162 | // 163 | // var stream = fs.createWriteStream(filename); 164 | // r.write(identifier, stream); 165 | // stream.on('data', function(data){...}); 166 | // stream.on('finish', function(){...}); 167 | $.write = function (identifier, writableStream, options) { 168 | options = options || {} 169 | options.end = typeof options['end'] == 'undefined' ? true : options['end'] 170 | 171 | // Iterate over each chunk 172 | var pipeChunk = function (number) { 173 | var chunkFilename = getChunkFilename(number, identifier) 174 | fs.exists(chunkFilename, function (exists) { 175 | if (exists) { 176 | // If the chunk with the current number exists, 177 | // then create a ReadStream from the file 178 | // and pipe it to the specified writableStream. 179 | var sourceStream = fs.createReadStream(chunkFilename) 180 | sourceStream.pipe(writableStream, { 181 | end: false 182 | }) 183 | sourceStream.on('end', function () { 184 | // When the chunk is fully streamed, 185 | // jump to the next one 186 | pipeChunk(number + 1) 187 | }) 188 | } else { 189 | // When all the chunks have been piped, end the stream 190 | if (options.end) writableStream.end() 191 | if (options.onDone) options.onDone() 192 | } 193 | }) 194 | } 195 | pipeChunk(1) 196 | } 197 | 198 | $.clean = function (identifier, options) { 199 | options = options || {} 200 | 201 | // Iterate over each chunk 202 | var pipeChunkRm = function (number) { 203 | var chunkFilename = getChunkFilename(number, identifier) 204 | 205 | //console.log('removing pipeChunkRm ', number, 'chunkFilename', chunkFilename); 206 | fs.exists(chunkFilename, function (exists) { 207 | if (exists) { 208 | console.log('exist removing ', chunkFilename) 209 | fs.unlink(chunkFilename, function (err) { 210 | if (err && options.onError) options.onError(err) 211 | }) 212 | 213 | pipeChunkRm(number + 1) 214 | } else { 215 | if (options.onDone) options.onDone() 216 | } 217 | }) 218 | } 219 | pipeChunkRm(1) 220 | } 221 | 222 | return $ 223 | } 224 | -------------------------------------------------------------------------------- /server/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/server/uploads/.gitkeep -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 53 | 54 | 85 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | export function mergeSimpleUpload() { 2 | return Promise.resolve() 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/src/assets/logo.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import uploader from 'vue-simple-uploader' 5 | import Element from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | import './styles/index.scss' 8 | 9 | Vue.use(uploader) 10 | Vue.use(Element) 11 | 12 | Vue.config.productionTip = false 13 | 14 | new Vue({ 15 | router, 16 | render: (h) => h(App) 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | base: process.env.BASE_URL, 8 | routes: [ 9 | { 10 | path: '/', 11 | redirect: '/home' 12 | }, 13 | { 14 | path: '/home', 15 | name: 'home', 16 | component: () => import('../views/Home.vue') 17 | }, 18 | { 19 | path: '/about', 20 | name: 'about', 21 | component: () => import('../views/About.vue') 22 | }, 23 | { 24 | path: '*', 25 | redirect: '/' 26 | } 27 | ] 28 | }) 29 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; 13 | background: #f2f3f8; 14 | color: #252525; 15 | font-size: 14px; 16 | -webkit-tap-highlight-color: transparent; 17 | } 18 | 19 | #app { 20 | height: 100%; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | .container { 26 | width: 1140px; 27 | margin: 0 auto; 28 | } 29 | 30 | .page { 31 | display: flex; 32 | flex-direction: column; 33 | padding: 20px 0; 34 | } 35 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 64 | 65 | 96 | -------------------------------------------------------------------------------- /vue-simple-uploader/GlobalUploader.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 394 | 395 | 521 | -------------------------------------------------------------------------------- /vue-simple-uploader/README.md: -------------------------------------------------------------------------------- 1 | # 基于`vue-simple-uploader`封装插件 2 | 3 | `GlobalUploader.vue` 为基于 `vue-simple-uploader` 二次封装的上传插件 4 | 5 | 它有两种使用方式: 6 | 7 | ## 通过bus作为全局组件使用 8 | 9 | 作为全局上传组件使用时,将组件注册在`App.vue`中,通过 `Event Bus` 的方式调用插件。 10 | 11 | 使用场景为:上传器为整个网站级别的,切换路由时不打断上传流程,上传窗口始终存在于网站右下角。 12 | 13 | **打开上传器** 14 | 15 | 调用`Bus.$emit('openUploader')`,打开上传器,弹出选择文件窗口,该函数有两个参数: 16 | 17 | * params:传给服务端的额外参数 18 | * options:上传选项,目前支持 target、testChunks、mergeFn、accept 19 | 20 | ```js 21 | Bus.$emit('openUploader', { 22 | params: { 23 | page: 'home' 24 | }, 25 | options: { 26 | target: 'http://10.0.0.10' 27 | } 28 | }) 29 | ``` 30 | 31 | **Bus Events** 32 | 33 | * fileAdded:文件选择后的回调 34 | * fileSuccess:文件上传成功的回调 35 | 36 | ```js 37 | // 文件选择后的回调 38 | Bus.$on('fileAdded', () => { 39 | console.log('文件已选择') 40 | }) 41 | 42 | // 文件上传成功的回调 43 | Bus.$on('fileSuccess', () => { 44 | console.log('文件上传成功') 45 | }) 46 | ``` 47 | 48 | ## 作为普通组件在单个页面中使用 49 | 50 | 使用场景为:在单个页面中使用上传器 51 | 52 | props: 53 | * global:请务必设置为 `false`,代表非全局 54 | * params:(同用法一)传给服务端的额外参数 55 | * options:(同用法一)上传选项,目前支持 target、testChunks、mergeFn、accept 56 | 57 | events: 58 | * fileAdded:文件选择后的回调 59 | * fileSuccess:文件上传成功的回调 60 | 61 | ```html 62 | 68 | ``` 69 | -------------------------------------------------------------------------------- /vue-simple-uploader/images/audio-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/vue-simple-uploader/images/audio-icon.png -------------------------------------------------------------------------------- /vue-simple-uploader/images/image-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/vue-simple-uploader/images/image-icon.png -------------------------------------------------------------------------------- /vue-simple-uploader/images/text-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/vue-simple-uploader/images/text-icon.png -------------------------------------------------------------------------------- /vue-simple-uploader/images/video-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/vue-simple-uploader/images/video-icon.png -------------------------------------------------------------------------------- /vue-simple-uploader/images/zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shady-xia/vue-uploader-solutions/ccec5291ebe8fcb78026c53883da8d5125f4559a/vue-simple-uploader/images/zip.png -------------------------------------------------------------------------------- /vue-simple-uploader/js/bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default new Vue(); 4 | -------------------------------------------------------------------------------- /vue-simple-uploader/js/config.js: -------------------------------------------------------------------------------- 1 | export const ACCEPT_CONFIG = { 2 | image: ['.png', '.jpg', '.jpeg', '.gif', '.bmp'], 3 | video: ['.mp4', '.rmvb', '.mkv', '.wmv', '.flv'], 4 | document: ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.tif', '.tiff'], 5 | getAll(){ 6 | return [...this.image, ...this.video, ...this.document] 7 | }, 8 | }; -------------------------------------------------------------------------------- /vue-webuploader/page.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 137 | 138 | 234 | -------------------------------------------------------------------------------- /vue-webuploader/upload.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 173 | 174 | 204 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("@vue/cli-service"); 2 | 3 | module.exports = defineConfig({ 4 | publicPath: process.env.NODE_ENV === 'production' ? '/vue-uploader-solutions/' : '/', 5 | outputDir: 'docs', 6 | transpileDependencies: true, 7 | lintOnSave: false, 8 | productionSourceMap: false, 9 | }); 10 | --------------------------------------------------------------------------------