vue-chunk-uploader 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/createPlugin-DZ7dugEN.js +183 -0
- package/dist/createPlugin-DoOjwZfd.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +18 -0
- package/dist/native.cjs +1 -0
- package/dist/native.js +66 -0
- package/dist/quasar.cjs +1 -0
- package/dist/quasar.js +68 -0
- package/dist/vue-chunk-uploader.css +1 -0
- package/dist/vuetify.cjs +1 -0
- package/dist/vuetify.js +65 -0
- package/package.json +82 -0
- package/src/core/chunkUploader.js +151 -0
- package/src/core/createPlugin.js +18 -0
- package/src/core/useChunkUpload.js +133 -0
- package/src/index.js +32 -0
- package/src/ui/native/ChunkUploader.vue +105 -0
- package/src/ui/native/index.js +17 -0
- package/src/ui/quasar/ChunkUploader.vue +50 -0
- package/src/ui/quasar/index.js +17 -0
- package/src/ui/vuetify/ChunkUploader.vue +57 -0
- package/src/ui/vuetify/index.js +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 akbarjoody
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# vue-chunk-uploader
|
|
2
|
+
|
|
3
|
+
A Vue 3 chunked file uploader compatible with the [resumable.js](https://github.com/23/resumable.js) protocol.
|
|
4
|
+
|
|
5
|
+
The upload core is UI-agnostic — you choose which adapter to use.
|
|
6
|
+
|
|
7
|
+
## Choose a UI
|
|
8
|
+
|
|
9
|
+
| Import | UI | Required dependency |
|
|
10
|
+
|--------|----|---------------------|
|
|
11
|
+
| `vue-chunk-uploader/quasar` | Quasar | `quasar` |
|
|
12
|
+
| `vue-chunk-uploader/vuetify` | Vuetify 3 | `vuetify` |
|
|
13
|
+
| `vue-chunk-uploader/native` | Plain HTML | — |
|
|
14
|
+
| `vue-chunk-uploader` | Core only (no UI) | — |
|
|
15
|
+
|
|
16
|
+
> **Note about MUI:** Material UI is for React. In the Vue ecosystem, the closest equivalent is **Vuetify**. For other UIs (Element Plus, Naive UI, MUI/React, etc.), use `native` or the `useChunkUpload` composable and build your own UI.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install vue-chunk-uploader axios vue
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Depending on the UI:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Quasar
|
|
28
|
+
npm install quasar
|
|
29
|
+
|
|
30
|
+
# or Vuetify
|
|
31
|
+
npm install vuetify
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage — Quasar
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { createApp } from 'vue'
|
|
38
|
+
import { Quasar } from 'quasar'
|
|
39
|
+
import VueChunkUploader from 'vue-chunk-uploader/quasar'
|
|
40
|
+
import axios from 'axios'
|
|
41
|
+
|
|
42
|
+
const api = axios.create({ baseURL: 'https://api.example.com', withCredentials: true })
|
|
43
|
+
|
|
44
|
+
const app = createApp(App)
|
|
45
|
+
app.use(Quasar)
|
|
46
|
+
app.use(VueChunkUploader, { httpClient: api })
|
|
47
|
+
app.mount('#app')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```vue
|
|
51
|
+
<template>
|
|
52
|
+
<ChunkUploader
|
|
53
|
+
v-model="file"
|
|
54
|
+
url="/api/document/chunk/upload"
|
|
55
|
+
filled
|
|
56
|
+
bottom-slots
|
|
57
|
+
label="File"
|
|
58
|
+
@onSuccess="onSuccess"
|
|
59
|
+
@onError="onError"
|
|
60
|
+
/>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script setup>
|
|
64
|
+
import { ref } from 'vue'
|
|
65
|
+
// or: import { ChunkUploader } from 'vue-chunk-uploader/quasar'
|
|
66
|
+
|
|
67
|
+
const file = ref(null)
|
|
68
|
+
</script>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage — Vuetify
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import VueChunkUploader from 'vue-chunk-uploader/vuetify'
|
|
75
|
+
|
|
76
|
+
app.use(VueChunkUploader, { httpClient: api })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```vue
|
|
80
|
+
<template>
|
|
81
|
+
<ChunkUploader
|
|
82
|
+
v-model="file"
|
|
83
|
+
url="/api/document/chunk/upload"
|
|
84
|
+
label="File"
|
|
85
|
+
variant="outlined"
|
|
86
|
+
@onSuccess="onSuccess"
|
|
87
|
+
/>
|
|
88
|
+
</template>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage — Native (no UI framework)
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import VueChunkUploader from 'vue-chunk-uploader/native'
|
|
95
|
+
import 'vue-chunk-uploader/style.css'
|
|
96
|
+
|
|
97
|
+
app.use(VueChunkUploader, { httpClient: api })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or build a custom UI with the composable:
|
|
101
|
+
|
|
102
|
+
```vue
|
|
103
|
+
<script setup>
|
|
104
|
+
import { ref } from 'vue'
|
|
105
|
+
import { useChunkUpload } from 'vue-chunk-uploader'
|
|
106
|
+
|
|
107
|
+
const file = ref(null)
|
|
108
|
+
const props = {
|
|
109
|
+
modelValue: file,
|
|
110
|
+
url: '/api/upload',
|
|
111
|
+
autoUpload: true,
|
|
112
|
+
fields: {},
|
|
113
|
+
}
|
|
114
|
+
const emit = (event, payload) => {
|
|
115
|
+
if (event === 'update:modelValue') file.value = payload
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { progress, hasError, uploadFile, clear } = useChunkUpload(props, emit)
|
|
119
|
+
</script>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Manual upload (`autoUpload=false`)
|
|
123
|
+
|
|
124
|
+
```vue
|
|
125
|
+
<template>
|
|
126
|
+
<ChunkUploader
|
|
127
|
+
ref="uploader"
|
|
128
|
+
v-model="file"
|
|
129
|
+
url="/api/document/chunk/upload"
|
|
130
|
+
:auto-upload="false"
|
|
131
|
+
/>
|
|
132
|
+
<button @click="submit">Upload</button>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<script setup>
|
|
136
|
+
import { ref } from 'vue'
|
|
137
|
+
|
|
138
|
+
const file = ref(null)
|
|
139
|
+
const uploader = ref(null)
|
|
140
|
+
|
|
141
|
+
async function submit() {
|
|
142
|
+
// Extra fields are sent only with the last chunk
|
|
143
|
+
await uploader.value.uploadFile({ document_id: 123 })
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Core only (no UI)
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import { chunk, setDefaultHttpClient } from 'vue-chunk-uploader'
|
|
152
|
+
import axios from 'axios'
|
|
153
|
+
|
|
154
|
+
setDefaultHttpClient(axios.create({ baseURL: '/api' }))
|
|
155
|
+
|
|
156
|
+
await chunk(
|
|
157
|
+
'/document/chunk/upload',
|
|
158
|
+
file,
|
|
159
|
+
{ document_id: 1 },
|
|
160
|
+
(percent) => console.log(percent),
|
|
161
|
+
(err) => console.error(err),
|
|
162
|
+
(res) => console.log(res),
|
|
163
|
+
{ chunkSize: 1024 * 1024 },
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Props
|
|
168
|
+
|
|
169
|
+
| Prop | Type | Default | Description |
|
|
170
|
+
|------|------|---------|-------------|
|
|
171
|
+
| `modelValue` / `v-model` | `File` | `null` | Selected file |
|
|
172
|
+
| `url` | `string` | — | Upload endpoint |
|
|
173
|
+
| `autoUpload` | `boolean` | `true` | Upload automatically after selection |
|
|
174
|
+
| `fields` | `object` | `{}` | Extra fields (sent with the last chunk) |
|
|
175
|
+
| `httpClient` | `AxiosInstance` | default / axios | HTTP client |
|
|
176
|
+
| `chunkSize` | `number` | `1MB` | Chunk size |
|
|
177
|
+
| `maxRequestsPerMinute` | `number` | `80` | Client-side rate limit |
|
|
178
|
+
| `rateLimitWindowMs` | `number` | `60000` | Rate-limit window |
|
|
179
|
+
|
|
180
|
+
UI-specific attrs are forwarded as well (e.g. `q-file` or `v-file-input` props).
|
|
181
|
+
|
|
182
|
+
## Events
|
|
183
|
+
|
|
184
|
+
| Event | Payload |
|
|
185
|
+
|-------|---------|
|
|
186
|
+
| `update:modelValue` | `File \| null` |
|
|
187
|
+
| `onSelectFile` | uploader module |
|
|
188
|
+
| `onProgress` | `percent: number` |
|
|
189
|
+
| `onError` | `error` |
|
|
190
|
+
| `onSuccess` | `response` |
|
|
191
|
+
|
|
192
|
+
## Exposed methods
|
|
193
|
+
|
|
194
|
+
| Method | Description |
|
|
195
|
+
|--------|-------------|
|
|
196
|
+
| `uploadFile(extraFormData?)` | Starts the upload; returns a Promise |
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { ref as z, watch as F, unref as d } from "vue";
|
|
2
|
+
import L from "axios";
|
|
3
|
+
let y = null;
|
|
4
|
+
function M(t) {
|
|
5
|
+
y = t;
|
|
6
|
+
}
|
|
7
|
+
function _(t) {
|
|
8
|
+
return t || y || L;
|
|
9
|
+
}
|
|
10
|
+
const R = 1024 * 1024, W = 80, g = 6e4, p = [], q = (t) => new Promise((e) => setTimeout(e, t)), U = async (t, e) => {
|
|
11
|
+
const n = Date.now();
|
|
12
|
+
for (; p.length && n - p[0] > e; )
|
|
13
|
+
p.shift();
|
|
14
|
+
if (p.length >= t) {
|
|
15
|
+
const a = p[0], r = e - (n - a) + 5;
|
|
16
|
+
return await q(r), U(t, e);
|
|
17
|
+
}
|
|
18
|
+
p.push(Date.now());
|
|
19
|
+
}, E = async (t, e, n, a) => {
|
|
20
|
+
const {
|
|
21
|
+
chunkSize: r,
|
|
22
|
+
chunkNumber: i,
|
|
23
|
+
blockCount: l,
|
|
24
|
+
identifier: o,
|
|
25
|
+
httpClient: m,
|
|
26
|
+
maxRequestsPerMinute: h,
|
|
27
|
+
rateLimitWindowMs: b,
|
|
28
|
+
onProgress: f,
|
|
29
|
+
onSuccess: k,
|
|
30
|
+
onError: u
|
|
31
|
+
} = a, C = i * r, S = Math.min(e.size, C + r);
|
|
32
|
+
let w = r;
|
|
33
|
+
i + 1 === l && (w = e.size - C);
|
|
34
|
+
const s = new FormData();
|
|
35
|
+
if (s.append("resumableChunkNumber", i + 1), s.append("resumableChunkSize", w), s.append("resumableCurrentChunkSize", w), s.append("resumableTotalSize", e.size), s.append("resumableType", e.type), s.append("resumableIdentifier", o), s.append("resumableFilename", e.name), s.append("resumableRelativePath", e.name), s.append("resumableTotalChunks", l), s.append("file", e.slice(C, S), e.name), Object.keys(n).length > 0 && i + 1 === l)
|
|
36
|
+
for (const [c, T] of Object.entries(n))
|
|
37
|
+
c !== "file" && s.append(c, T);
|
|
38
|
+
await U(h, b);
|
|
39
|
+
const N = _(m);
|
|
40
|
+
try {
|
|
41
|
+
const c = await N.post(t, s, {
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: "application/json"
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return f == null || f(parseInt(S / e.size * 100, 10), c), S === e.size ? (k == null || k(c), c) : E(t, e, n, {
|
|
47
|
+
...a,
|
|
48
|
+
chunkNumber: i + 1
|
|
49
|
+
});
|
|
50
|
+
} catch (c) {
|
|
51
|
+
throw u == null || u(c), c;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function P(t, e, n = {}, a, r, i, l = {}) {
|
|
55
|
+
const o = l.chunkSize ?? R, m = Math.ceil(e.size / o) || 1, h = `${e.size}-${String(e.name).replace(/\./g, "")}`;
|
|
56
|
+
return E(t, e, n, {
|
|
57
|
+
blockCount: m,
|
|
58
|
+
identifier: h,
|
|
59
|
+
chunkNumber: 0,
|
|
60
|
+
chunkSize: o,
|
|
61
|
+
httpClient: l.httpClient,
|
|
62
|
+
maxRequestsPerMinute: l.maxRequestsPerMinute ?? W,
|
|
63
|
+
rateLimitWindowMs: l.rateLimitWindowMs ?? g,
|
|
64
|
+
onProgress: a,
|
|
65
|
+
onError: r,
|
|
66
|
+
onSuccess: i
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const v = {
|
|
70
|
+
chunk: P,
|
|
71
|
+
setDefaultHttpClient: M
|
|
72
|
+
}, I = {
|
|
73
|
+
modelValue: {
|
|
74
|
+
type: [Object, File, Blob],
|
|
75
|
+
default: null
|
|
76
|
+
},
|
|
77
|
+
autoUpload: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: !0
|
|
80
|
+
},
|
|
81
|
+
url: {
|
|
82
|
+
type: String,
|
|
83
|
+
required: !0
|
|
84
|
+
},
|
|
85
|
+
fields: {
|
|
86
|
+
type: Object,
|
|
87
|
+
default: () => ({})
|
|
88
|
+
},
|
|
89
|
+
httpClient: {
|
|
90
|
+
type: Object,
|
|
91
|
+
default: null
|
|
92
|
+
},
|
|
93
|
+
chunkSize: {
|
|
94
|
+
type: Number,
|
|
95
|
+
default: void 0
|
|
96
|
+
},
|
|
97
|
+
maxRequestsPerMinute: {
|
|
98
|
+
type: Number,
|
|
99
|
+
default: void 0
|
|
100
|
+
},
|
|
101
|
+
rateLimitWindowMs: {
|
|
102
|
+
type: Number,
|
|
103
|
+
default: void 0
|
|
104
|
+
}
|
|
105
|
+
}, j = [
|
|
106
|
+
"update:modelValue",
|
|
107
|
+
"onSelectFile",
|
|
108
|
+
"onSendFile",
|
|
109
|
+
"onProgress",
|
|
110
|
+
"onError",
|
|
111
|
+
"onSuccess"
|
|
112
|
+
];
|
|
113
|
+
function A(t, e) {
|
|
114
|
+
const n = z(!1), a = z(0), r = () => {
|
|
115
|
+
n.value = !1, a.value = 0;
|
|
116
|
+
}, i = async (o = {}) => {
|
|
117
|
+
const m = d(t.modelValue);
|
|
118
|
+
if (!m)
|
|
119
|
+
throw new Error("No file selected");
|
|
120
|
+
const h = {
|
|
121
|
+
...d(t.fields),
|
|
122
|
+
...o
|
|
123
|
+
}, b = {
|
|
124
|
+
httpClient: d(t.httpClient) || void 0,
|
|
125
|
+
chunkSize: d(t.chunkSize),
|
|
126
|
+
maxRequestsPerMinute: d(t.maxRequestsPerMinute),
|
|
127
|
+
rateLimitWindowMs: d(t.rateLimitWindowMs)
|
|
128
|
+
};
|
|
129
|
+
return new Promise((f, k) => {
|
|
130
|
+
P(
|
|
131
|
+
d(t.url),
|
|
132
|
+
m,
|
|
133
|
+
h,
|
|
134
|
+
(u) => {
|
|
135
|
+
a.value = u, e("onProgress", u);
|
|
136
|
+
},
|
|
137
|
+
(u) => {
|
|
138
|
+
n.value = !0, e("onError", u), k(u);
|
|
139
|
+
},
|
|
140
|
+
(u) => {
|
|
141
|
+
e("onSuccess", u), f(u);
|
|
142
|
+
},
|
|
143
|
+
b
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
}, l = () => {
|
|
147
|
+
r(), e("update:modelValue", null);
|
|
148
|
+
};
|
|
149
|
+
return F(
|
|
150
|
+
() => d(t.modelValue),
|
|
151
|
+
async (o, m) => {
|
|
152
|
+
if (o !== m && (r(), e("onSelectFile", v), d(t.autoUpload) && o))
|
|
153
|
+
try {
|
|
154
|
+
await i();
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
), {
|
|
159
|
+
hasError: n,
|
|
160
|
+
progress: a,
|
|
161
|
+
resetState: r,
|
|
162
|
+
uploadFile: i,
|
|
163
|
+
clear: l
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function O(t) {
|
|
167
|
+
return {
|
|
168
|
+
install(e, n = {}) {
|
|
169
|
+
n.httpClient && M(n.httpClient);
|
|
170
|
+
const a = n.componentName || "ChunkUploader";
|
|
171
|
+
e.component(a, t);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
P as a,
|
|
177
|
+
v as b,
|
|
178
|
+
O as c,
|
|
179
|
+
j as d,
|
|
180
|
+
I as e,
|
|
181
|
+
M as s,
|
|
182
|
+
A as u
|
|
183
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const s=require("vue"),F=require("axios");let w=null;function y(t){w=t}function L(t){return t||w||F}const _=1024*1024,R=80,q=6e4,h=[],g=t=>new Promise(e=>setTimeout(e,t)),M=async(t,e)=>{const n=Date.now();for(;h.length&&n-h[0]>e;)h.shift();if(h.length>=t){const u=h[0],i=e-(n-u)+5;return await g(i),M(t,e)}h.push(Date.now())},P=async(t,e,n,u)=>{const{chunkSize:i,chunkNumber:l,blockCount:o,identifier:c,httpClient:p,maxRequestsPerMinute:m,rateLimitWindowMs:C,onProgress:f,onSuccess:k,onError:a}=u,b=l*i,S=Math.min(e.size,b+i);let U=i;l+1===o&&(U=e.size-b);const r=new FormData;if(r.append("resumableChunkNumber",l+1),r.append("resumableChunkSize",U),r.append("resumableCurrentChunkSize",U),r.append("resumableTotalSize",e.size),r.append("resumableType",e.type),r.append("resumableIdentifier",c),r.append("resumableFilename",e.name),r.append("resumableRelativePath",e.name),r.append("resumableTotalChunks",o),r.append("file",e.slice(b,S),e.name),Object.keys(n).length>0&&l+1===o)for(const[d,T]of Object.entries(n))d!=="file"&&r.append(d,T);await M(m,C);const N=L(p);try{const d=await N.post(t,r,{headers:{Accept:"application/json"}});return f==null||f(parseInt(S/e.size*100,10),d),S===e.size?(k==null||k(d),d):P(t,e,n,{...u,chunkNumber:l+1})}catch(d){throw a==null||a(d),d}};function z(t,e,n={},u,i,l,o={}){const c=o.chunkSize??_,p=Math.ceil(e.size/c)||1,m=`${e.size}-${String(e.name).replace(/\./g,"")}`;return P(t,e,n,{blockCount:p,identifier:m,chunkNumber:0,chunkSize:c,httpClient:o.httpClient,maxRequestsPerMinute:o.maxRequestsPerMinute??R,rateLimitWindowMs:o.rateLimitWindowMs??q,onProgress:u,onError:i,onSuccess:l})}const E={chunk:z,setDefaultHttpClient:y},v={modelValue:{type:[Object,File,Blob],default:null},autoUpload:{type:Boolean,default:!0},url:{type:String,required:!0},fields:{type:Object,default:()=>({})},httpClient:{type:Object,default:null},chunkSize:{type:Number,default:void 0},maxRequestsPerMinute:{type:Number,default:void 0},rateLimitWindowMs:{type:Number,default:void 0}},D=["update:modelValue","onSelectFile","onSendFile","onProgress","onError","onSuccess"];function W(t,e){const n=s.ref(!1),u=s.ref(0),i=()=>{n.value=!1,u.value=0},l=async(c={})=>{const p=s.unref(t.modelValue);if(!p)throw new Error("No file selected");const m={...s.unref(t.fields),...c},C={httpClient:s.unref(t.httpClient)||void 0,chunkSize:s.unref(t.chunkSize),maxRequestsPerMinute:s.unref(t.maxRequestsPerMinute),rateLimitWindowMs:s.unref(t.rateLimitWindowMs)};return new Promise((f,k)=>{z(s.unref(t.url),p,m,a=>{u.value=a,e("onProgress",a)},a=>{n.value=!0,e("onError",a),k(a)},a=>{e("onSuccess",a),f(a)},C)})},o=()=>{i(),e("update:modelValue",null)};return s.watch(()=>s.unref(t.modelValue),async(c,p)=>{if(c!==p&&(i(),e("onSelectFile",E),s.unref(t.autoUpload)&&c))try{await l()}catch{}}),{hasError:n,progress:u,resetState:i,uploadFile:l,clear:o}}function I(t){return{install(e,n={}){n.httpClient&&y(n.httpClient);const u=n.componentName||"ChunkUploader";e.component(u,t)}}}exports.chunk=z;exports.chunkUploader=E;exports.chunkUploaderEmits=D;exports.chunkUploaderProps=v;exports.createChunkUploaderPlugin=I;exports.setDefaultHttpClient=y;exports.useChunkUpload=W;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./createPlugin-DoOjwZfd.cjs"),u={chunk:e.chunk,setDefaultHttpClient:e.setDefaultHttpClient,useChunkUpload:e.useChunkUpload,createChunkUploaderPlugin:e.createChunkUploaderPlugin};exports.chunk=e.chunk;exports.chunkUploader=e.chunkUploader;exports.chunkUploaderEmits=e.chunkUploaderEmits;exports.chunkUploaderProps=e.chunkUploaderProps;exports.createChunkUploaderPlugin=e.createChunkUploaderPlugin;exports.setDefaultHttpClient=e.setDefaultHttpClient;exports.useChunkUpload=e.useChunkUpload;exports.default=u;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { c as a, u as e, s, a as o } from "./createPlugin-DZ7dugEN.js";
|
|
2
|
+
import { b as p, d as l, e as d } from "./createPlugin-DZ7dugEN.js";
|
|
3
|
+
const r = {
|
|
4
|
+
chunk: o,
|
|
5
|
+
setDefaultHttpClient: s,
|
|
6
|
+
useChunkUpload: e,
|
|
7
|
+
createChunkUploaderPlugin: a
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
o as chunk,
|
|
11
|
+
p as chunkUploader,
|
|
12
|
+
l as chunkUploaderEmits,
|
|
13
|
+
d as chunkUploaderProps,
|
|
14
|
+
a as createChunkUploaderPlugin,
|
|
15
|
+
r as default,
|
|
16
|
+
s as setDefaultHttpClient,
|
|
17
|
+
e as useChunkUpload
|
|
18
|
+
};
|
package/dist/native.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),t=require("./createPlugin-DoOjwZfd.cjs"),g=(o,l)=>{const a=o.__vccOpts||o;for(const[c,r]of l)a[c]=r;return a},C={class:"vue-chunk-uploader-native","data-ui":"native"},U={class:"vue-chunk-uploader-native__row"},V=["data-error","data-done"],y=["value"],B=Object.assign({inheritAttrs:!1},{__name:"ChunkUploader",props:t.chunkUploaderProps,emits:t.chunkUploaderEmits,setup(o,{expose:l,emit:a}){const c=o,r=a,s=e.ref(null),{hasError:h,progress:n,uploadFile:_,clear:f}=t.useChunkUpload(c,r),m=u=>{var p;const d=((p=u.target.files)==null?void 0:p[0])??null;r("update:modelValue",d)},v=()=>{s.value&&(s.value.value=""),f()};return l({uploadFile:_}),(u,d)=>(e.openBlock(),e.createElementBlock("div",C,[e.createElementVNode("div",U,[e.createElementVNode("input",e.mergeProps({ref_key:"inputRef",ref:s,type:"file"},u.$attrs,{onChange:m}),null,16),u.modelValue?(e.openBlock(),e.createElementBlock("button",{key:0,type:"button",class:"vue-chunk-uploader-native__clear",onClick:v}," × ")):e.createCommentVNode("",!0),e.createElementVNode("span",{class:"vue-chunk-uploader-native__badge","data-error":e.unref(h)||void 0,"data-done":e.unref(n)>=100||void 0},[e.unref(n)<100?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createTextVNode(e.toDisplayString(e.unref(n))+"%",1)],64)):(e.openBlock(),e.createElementBlock(e.Fragment,{key:1},[e.createTextVNode("✓")],64))],8,V)]),e.unref(n)>0&&e.unref(n)<100?(e.openBlock(),e.createElementBlock("progress",{key:0,class:"vue-chunk-uploader-native__progress",value:e.unref(n),max:"100"},null,8,y)):e.createCommentVNode("",!0)]))}}),i=g(B,[["__scopeId","data-v-0be4ee9c"]]),k=t.createChunkUploaderPlugin(i);exports.chunk=t.chunk;exports.chunkUploader=t.chunkUploader;exports.setDefaultHttpClient=t.setDefaultHttpClient;exports.useChunkUpload=t.useChunkUpload;exports.ChunkUploader=i;exports.VueChunkUploader=k;exports.default=k;
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ref as U, openBlock as a, createElementBlock as o, createElementVNode as p, mergeProps as b, createCommentVNode as h, unref as e, Fragment as k, createTextVNode as f, toDisplayString as V } from "vue";
|
|
2
|
+
import { d as x, e as E, u as N, c as P } from "./createPlugin-DZ7dugEN.js";
|
|
3
|
+
import { a as $, b as q, s as z } from "./createPlugin-DZ7dugEN.js";
|
|
4
|
+
const B = (n, l) => {
|
|
5
|
+
const r = n.__vccOpts || n;
|
|
6
|
+
for (const [c, s] of l)
|
|
7
|
+
r[c] = s;
|
|
8
|
+
return r;
|
|
9
|
+
}, D = {
|
|
10
|
+
class: "vue-chunk-uploader-native",
|
|
11
|
+
"data-ui": "native"
|
|
12
|
+
}, F = { class: "vue-chunk-uploader-native__row" }, O = ["data-error", "data-done"], R = ["value"], j = /* @__PURE__ */ Object.assign({ inheritAttrs: !1 }, {
|
|
13
|
+
__name: "ChunkUploader",
|
|
14
|
+
props: E,
|
|
15
|
+
emits: x,
|
|
16
|
+
setup(n, { expose: l, emit: r }) {
|
|
17
|
+
const c = n, s = r, d = U(null), { hasError: v, progress: t, uploadFile: m, clear: g } = N(c, s), C = (u) => {
|
|
18
|
+
var _;
|
|
19
|
+
const i = ((_ = u.target.files) == null ? void 0 : _[0]) ?? null;
|
|
20
|
+
s("update:modelValue", i);
|
|
21
|
+
}, y = () => {
|
|
22
|
+
d.value && (d.value.value = ""), g();
|
|
23
|
+
};
|
|
24
|
+
return l({ uploadFile: m }), (u, i) => (a(), o("div", D, [
|
|
25
|
+
p("div", F, [
|
|
26
|
+
p("input", b({
|
|
27
|
+
ref_key: "inputRef",
|
|
28
|
+
ref: d,
|
|
29
|
+
type: "file"
|
|
30
|
+
}, u.$attrs, { onChange: C }), null, 16),
|
|
31
|
+
u.modelValue ? (a(), o("button", {
|
|
32
|
+
key: 0,
|
|
33
|
+
type: "button",
|
|
34
|
+
class: "vue-chunk-uploader-native__clear",
|
|
35
|
+
onClick: y
|
|
36
|
+
}, " × ")) : h("", !0),
|
|
37
|
+
p("span", {
|
|
38
|
+
class: "vue-chunk-uploader-native__badge",
|
|
39
|
+
"data-error": e(v) || void 0,
|
|
40
|
+
"data-done": e(t) >= 100 || void 0
|
|
41
|
+
}, [
|
|
42
|
+
e(t) < 100 ? (a(), o(k, { key: 0 }, [
|
|
43
|
+
f(V(e(t)) + "%", 1)
|
|
44
|
+
], 64)) : (a(), o(k, { key: 1 }, [
|
|
45
|
+
f("✓")
|
|
46
|
+
], 64))
|
|
47
|
+
], 8, O)
|
|
48
|
+
]),
|
|
49
|
+
e(t) > 0 && e(t) < 100 ? (a(), o("progress", {
|
|
50
|
+
key: 0,
|
|
51
|
+
class: "vue-chunk-uploader-native__progress",
|
|
52
|
+
value: e(t),
|
|
53
|
+
max: "100"
|
|
54
|
+
}, null, 8, R)) : h("", !0)
|
|
55
|
+
]));
|
|
56
|
+
}
|
|
57
|
+
}), w = /* @__PURE__ */ B(j, [["__scopeId", "data-v-0be4ee9c"]]), I = P(w);
|
|
58
|
+
export {
|
|
59
|
+
w as ChunkUploader,
|
|
60
|
+
I as VueChunkUploader,
|
|
61
|
+
$ as chunk,
|
|
62
|
+
q as chunkUploader,
|
|
63
|
+
I as default,
|
|
64
|
+
z as setDefaultHttpClient,
|
|
65
|
+
N as useChunkUpload
|
|
66
|
+
};
|
package/dist/quasar.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),o=require("./createPlugin-DoOjwZfd.cjs"),a=Object.assign({inheritAttrs:!1},{__name:"ChunkUploader",props:o.chunkUploaderProps,emits:o.chunkUploaderEmits,setup(p,{expose:i,emit:d}){const m=p,l=d,{hasError:u,progress:n,uploadFile:k,clear:C}=o.useChunkUpload(m,l);return i({uploadFile:k}),(s,t)=>{const h=e.resolveComponent("q-linear-progress"),r=e.resolveComponent("q-icon"),_=e.resolveComponent("q-btn"),f=e.resolveComponent("q-file");return e.openBlock(),e.createBlock(f,e.mergeProps(s.$attrs,{"model-value":s.modelValue,"onUpdate:modelValue":t[1]||(t[1]=v=>l("update:modelValue",v))}),{hint:e.withCtx(()=>[e.unref(n)>0&&e.unref(n)<100?(e.openBlock(),e.createBlock(h,{key:0,stripe:"",value:e.unref(n)/100,color:e.unref(u)?"negative":"positive",class:"q-mt-sm"},null,8,["value","color"])):e.createCommentVNode("",!0)]),prepend:e.withCtx(()=>[e.createVNode(r,{name:"cloud_upload",onClick:t[0]||(t[0]=e.withModifiers(()=>{},["stop"]))})]),append:e.withCtx(()=>[e.createVNode(_,{rounded:"",color:e.unref(u)?"negative":"positive"},{default:e.withCtx(()=>[e.unref(n)<100?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createTextVNode(e.toDisplayString(e.unref(n))+" % ",1)],64)):(e.openBlock(),e.createBlock(r,{key:1,name:"done_all",color:"white",size:"24px"}))]),_:1},8,["color"]),e.createVNode(r,{name:"close",class:"cursor-pointer",onClick:e.unref(C)},null,8,["onClick"])]),_:1},16,["model-value"])}}}),c=o.createChunkUploaderPlugin(a);exports.chunk=o.chunk;exports.chunkUploader=o.chunkUploader;exports.setDefaultHttpClient=o.setDefaultHttpClient;exports.useChunkUpload=o.useChunkUpload;exports.ChunkUploader=a;exports.VueChunkUploader=c;exports.default=c;
|
package/dist/quasar.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolveComponent as n, openBlock as r, createBlock as a, mergeProps as U, withCtx as l, createVNode as p, unref as e, createElementBlock as V, Fragment as x, createTextVNode as b, toDisplayString as y, withModifiers as w, createCommentVNode as B } from "vue";
|
|
2
|
+
import { d as E, e as N, u as P, c as D } from "./createPlugin-DZ7dugEN.js";
|
|
3
|
+
import { a as O, b as S, s as T } from "./createPlugin-DZ7dugEN.js";
|
|
4
|
+
const F = /* @__PURE__ */ Object.assign({ inheritAttrs: !1 }, {
|
|
5
|
+
__name: "ChunkUploader",
|
|
6
|
+
props: N,
|
|
7
|
+
emits: E,
|
|
8
|
+
setup(m, { expose: d, emit: k }) {
|
|
9
|
+
const _ = m, i = k, { hasError: c, progress: o, uploadFile: f, clear: h } = P(_, i);
|
|
10
|
+
return d({ uploadFile: f }), (u, t) => {
|
|
11
|
+
const C = n("q-linear-progress"), s = n("q-icon"), g = n("q-btn"), q = n("q-file");
|
|
12
|
+
return r(), a(q, U(u.$attrs, {
|
|
13
|
+
"model-value": u.modelValue,
|
|
14
|
+
"onUpdate:modelValue": t[1] || (t[1] = (v) => i("update:modelValue", v))
|
|
15
|
+
}), {
|
|
16
|
+
hint: l(() => [
|
|
17
|
+
e(o) > 0 && e(o) < 100 ? (r(), a(C, {
|
|
18
|
+
key: 0,
|
|
19
|
+
stripe: "",
|
|
20
|
+
value: e(o) / 100,
|
|
21
|
+
color: e(c) ? "negative" : "positive",
|
|
22
|
+
class: "q-mt-sm"
|
|
23
|
+
}, null, 8, ["value", "color"])) : B("", !0)
|
|
24
|
+
]),
|
|
25
|
+
prepend: l(() => [
|
|
26
|
+
p(s, {
|
|
27
|
+
name: "cloud_upload",
|
|
28
|
+
onClick: t[0] || (t[0] = w(() => {
|
|
29
|
+
}, ["stop"]))
|
|
30
|
+
})
|
|
31
|
+
]),
|
|
32
|
+
append: l(() => [
|
|
33
|
+
p(g, {
|
|
34
|
+
rounded: "",
|
|
35
|
+
color: e(c) ? "negative" : "positive"
|
|
36
|
+
}, {
|
|
37
|
+
default: l(() => [
|
|
38
|
+
e(o) < 100 ? (r(), V(x, { key: 0 }, [
|
|
39
|
+
b(y(e(o)) + " % ", 1)
|
|
40
|
+
], 64)) : (r(), a(s, {
|
|
41
|
+
key: 1,
|
|
42
|
+
name: "done_all",
|
|
43
|
+
color: "white",
|
|
44
|
+
size: "24px"
|
|
45
|
+
}))
|
|
46
|
+
]),
|
|
47
|
+
_: 1
|
|
48
|
+
}, 8, ["color"]),
|
|
49
|
+
p(s, {
|
|
50
|
+
name: "close",
|
|
51
|
+
class: "cursor-pointer",
|
|
52
|
+
onClick: e(h)
|
|
53
|
+
}, null, 8, ["onClick"])
|
|
54
|
+
]),
|
|
55
|
+
_: 1
|
|
56
|
+
}, 16, ["model-value"]);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}), A = D(F);
|
|
60
|
+
export {
|
|
61
|
+
F as ChunkUploader,
|
|
62
|
+
A as VueChunkUploader,
|
|
63
|
+
O as chunk,
|
|
64
|
+
S as chunkUploader,
|
|
65
|
+
A as default,
|
|
66
|
+
T as setDefaultHttpClient,
|
|
67
|
+
P as useChunkUpload
|
|
68
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.vue-chunk-uploader-native__row[data-v-0be4ee9c]{display:flex;align-items:center;gap:.5rem}.vue-chunk-uploader-native__badge[data-v-0be4ee9c]{display:inline-flex;align-items:center;justify-content:center;min-width:3rem;padding:.15rem .5rem;border-radius:999px;font-size:.85rem;background:#16a34a;color:#fff}.vue-chunk-uploader-native__badge[data-error][data-v-0be4ee9c]{background:#dc2626}.vue-chunk-uploader-native__clear[data-v-0be4ee9c]{border:0;background:transparent;cursor:pointer;font-size:1.25rem;line-height:1}.vue-chunk-uploader-native__progress[data-v-0be4ee9c]{display:block;width:100%;margin-top:.5rem;height:.5rem}
|
package/dist/vuetify.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),o=require("./createPlugin-DoOjwZfd.cjs"),U={class:"vue-chunk-uploader-vuetify"},u=Object.assign({inheritAttrs:!1},{__name:"ChunkUploader",props:o.chunkUploaderProps,emits:o.chunkUploaderEmits,setup(s,{expose:p,emit:i}){const d=s,t=i,{hasError:l,progress:r,uploadFile:m,clear:k}=o.useChunkUpload(d,t),h=n=>{const c=Array.isArray(n)?n[0]??null:n;t("update:modelValue",c)};return p({uploadFile:m}),(n,c)=>{const _=e.resolveComponent("v-icon"),f=e.resolveComponent("v-chip"),v=e.resolveComponent("v-file-input"),C=e.resolveComponent("v-progress-linear");return e.openBlock(),e.createElementBlock("div",U,[e.createVNode(v,e.mergeProps(n.$attrs,{"model-value":n.modelValue,error:e.unref(l)||void 0,clearable:"","prepend-icon":"mdi-cloud-upload","onUpdate:modelValue":h,"onClick:clear":e.unref(k)}),{append:e.withCtx(()=>[e.createVNode(f,{color:e.unref(l)?"error":e.unref(r)>=100?"success":"primary",size:"small",label:""},{default:e.withCtx(()=>[e.unref(r)<100?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[e.createTextVNode(e.toDisplayString(e.unref(r))+"%",1)],64)):(e.openBlock(),e.createBlock(_,{key:1,icon:"mdi-check-all",size:"small"}))]),_:1},8,["color"])]),_:1},16,["model-value","error","onClick:clear"]),e.unref(r)>0&&e.unref(r)<100?(e.openBlock(),e.createBlock(C,{key:0,class:"mt-1","model-value":e.unref(r),color:e.unref(l)?"error":"success",height:"6",rounded:"",striped:""},null,8,["model-value","color"])):e.createCommentVNode("",!0)])}}}),a=o.createChunkUploaderPlugin(u);exports.chunk=o.chunk;exports.chunkUploader=o.chunkUploader;exports.setDefaultHttpClient=o.setDefaultHttpClient;exports.useChunkUpload=o.useChunkUpload;exports.ChunkUploader=u;exports.VueChunkUploader=a;exports.default=a;
|
package/dist/vuetify.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolveComponent as l, openBlock as n, createElementBlock as c, createVNode as p, mergeProps as V, unref as e, withCtx as i, Fragment as b, createTextVNode as x, toDisplayString as A, createBlock as u, createCommentVNode as B } from "vue";
|
|
2
|
+
import { d as E, e as F, u as N, c as P } from "./createPlugin-DZ7dugEN.js";
|
|
3
|
+
import { a as T, b as $, s as q } from "./createPlugin-DZ7dugEN.js";
|
|
4
|
+
const z = { class: "vue-chunk-uploader-vuetify" }, D = /* @__PURE__ */ Object.assign({ inheritAttrs: !1 }, {
|
|
5
|
+
__name: "ChunkUploader",
|
|
6
|
+
props: F,
|
|
7
|
+
emits: E,
|
|
8
|
+
setup(d, { expose: m, emit: _ }) {
|
|
9
|
+
const h = d, a = _, { hasError: t, progress: o, uploadFile: k, clear: f } = N(h, a), v = (r) => {
|
|
10
|
+
const s = Array.isArray(r) ? r[0] ?? null : r;
|
|
11
|
+
a("update:modelValue", s);
|
|
12
|
+
};
|
|
13
|
+
return m({ uploadFile: k }), (r, s) => {
|
|
14
|
+
const C = l("v-icon"), U = l("v-chip"), g = l("v-file-input"), y = l("v-progress-linear");
|
|
15
|
+
return n(), c("div", z, [
|
|
16
|
+
p(g, V(r.$attrs, {
|
|
17
|
+
"model-value": r.modelValue,
|
|
18
|
+
error: e(t) || void 0,
|
|
19
|
+
clearable: "",
|
|
20
|
+
"prepend-icon": "mdi-cloud-upload",
|
|
21
|
+
"onUpdate:modelValue": v,
|
|
22
|
+
"onClick:clear": e(f)
|
|
23
|
+
}), {
|
|
24
|
+
append: i(() => [
|
|
25
|
+
p(U, {
|
|
26
|
+
color: e(t) ? "error" : e(o) >= 100 ? "success" : "primary",
|
|
27
|
+
size: "small",
|
|
28
|
+
label: ""
|
|
29
|
+
}, {
|
|
30
|
+
default: i(() => [
|
|
31
|
+
e(o) < 100 ? (n(), c(b, { key: 0 }, [
|
|
32
|
+
x(A(e(o)) + "%", 1)
|
|
33
|
+
], 64)) : (n(), u(C, {
|
|
34
|
+
key: 1,
|
|
35
|
+
icon: "mdi-check-all",
|
|
36
|
+
size: "small"
|
|
37
|
+
}))
|
|
38
|
+
]),
|
|
39
|
+
_: 1
|
|
40
|
+
}, 8, ["color"])
|
|
41
|
+
]),
|
|
42
|
+
_: 1
|
|
43
|
+
}, 16, ["model-value", "error", "onClick:clear"]),
|
|
44
|
+
e(o) > 0 && e(o) < 100 ? (n(), u(y, {
|
|
45
|
+
key: 0,
|
|
46
|
+
class: "mt-1",
|
|
47
|
+
"model-value": e(o),
|
|
48
|
+
color: e(t) ? "error" : "success",
|
|
49
|
+
height: "6",
|
|
50
|
+
rounded: "",
|
|
51
|
+
striped: ""
|
|
52
|
+
}, null, 8, ["model-value", "color"])) : B("", !0)
|
|
53
|
+
]);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}), H = P(D);
|
|
57
|
+
export {
|
|
58
|
+
D as ChunkUploader,
|
|
59
|
+
H as VueChunkUploader,
|
|
60
|
+
T as chunk,
|
|
61
|
+
$ as chunkUploader,
|
|
62
|
+
H as default,
|
|
63
|
+
q as setDefaultHttpClient,
|
|
64
|
+
N as useChunkUpload
|
|
65
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-chunk-uploader",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Vue 3 chunked/resumable file uploader with selectable UI adapters (Quasar, Vuetify, native)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./quasar": {
|
|
14
|
+
"import": "./dist/quasar.js",
|
|
15
|
+
"require": "./dist/quasar.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./vuetify": {
|
|
18
|
+
"import": "./dist/vuetify.js",
|
|
19
|
+
"require": "./dist/vuetify.cjs"
|
|
20
|
+
},
|
|
21
|
+
"./native": {
|
|
22
|
+
"import": "./dist/native.js",
|
|
23
|
+
"require": "./dist/native.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./style.css": "./dist/vue-chunk-uploader.css"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": [
|
|
32
|
+
"**/*.css",
|
|
33
|
+
"**/*.vue"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "vite build",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"vue",
|
|
41
|
+
"vue3",
|
|
42
|
+
"quasar",
|
|
43
|
+
"vuetify",
|
|
44
|
+
"uploader",
|
|
45
|
+
"chunk",
|
|
46
|
+
"resumable",
|
|
47
|
+
"file-upload",
|
|
48
|
+
"chunked-upload"
|
|
49
|
+
],
|
|
50
|
+
"author": "akbarjoody",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/akbarjoody/vue-chunk-uploader.git"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/akbarjoody/vue-chunk-uploader/issues"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/akbarjoody/vue-chunk-uploader#readme",
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"axios": "^1.0.0",
|
|
62
|
+
"quasar": "^2.0.0",
|
|
63
|
+
"vue": "^3.2.0",
|
|
64
|
+
"vuetify": "^3.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"quasar": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"vuetify": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
76
|
+
"axios": "^1.9.0",
|
|
77
|
+
"quasar": "^2.18.5",
|
|
78
|
+
"vite": "^6.2.0",
|
|
79
|
+
"vue": "^3.5.13",
|
|
80
|
+
"vuetify": "^3.7.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
|
|
3
|
+
/** @type {import('axios').AxiosInstance | null} */
|
|
4
|
+
let defaultHttpClient = null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set a default HTTP client used when none is passed to `chunk()`.
|
|
8
|
+
* @param {import('axios').AxiosInstance} client
|
|
9
|
+
*/
|
|
10
|
+
export function setDefaultHttpClient(client) {
|
|
11
|
+
defaultHttpClient = client
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {import('axios').AxiosInstance}
|
|
16
|
+
*/
|
|
17
|
+
function resolveHttpClient(httpClient) {
|
|
18
|
+
if (httpClient) return httpClient
|
|
19
|
+
if (defaultHttpClient) return defaultHttpClient
|
|
20
|
+
return axios
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_CHUNK_SIZE = 1024 * 1024
|
|
24
|
+
export const DEFAULT_MAX_REQUESTS_PER_MINUTE = 80
|
|
25
|
+
export const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000
|
|
26
|
+
|
|
27
|
+
const requestTimestamps = []
|
|
28
|
+
|
|
29
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
30
|
+
|
|
31
|
+
const waitForRateLimit = async (maxRequests, windowMs) => {
|
|
32
|
+
const now = Date.now()
|
|
33
|
+
|
|
34
|
+
while (requestTimestamps.length && now - requestTimestamps[0] > windowMs) {
|
|
35
|
+
requestTimestamps.shift()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (requestTimestamps.length >= maxRequests) {
|
|
39
|
+
const earliest = requestTimestamps[0]
|
|
40
|
+
const waitMs = windowMs - (now - earliest) + 5
|
|
41
|
+
await sleep(waitMs)
|
|
42
|
+
return waitForRateLimit(maxRequests, windowMs)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
requestTimestamps.push(Date.now())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const uploadChunk = async (endpoint, file, formData, options) => {
|
|
49
|
+
const {
|
|
50
|
+
chunkSize,
|
|
51
|
+
chunkNumber,
|
|
52
|
+
blockCount,
|
|
53
|
+
identifier,
|
|
54
|
+
httpClient,
|
|
55
|
+
maxRequestsPerMinute,
|
|
56
|
+
rateLimitWindowMs,
|
|
57
|
+
onProgress,
|
|
58
|
+
onSuccess,
|
|
59
|
+
onError,
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
const start = chunkNumber * chunkSize
|
|
63
|
+
const end = Math.min(file.size, start + chunkSize)
|
|
64
|
+
|
|
65
|
+
let currentChunkSize = chunkSize
|
|
66
|
+
if (chunkNumber + 1 === blockCount) {
|
|
67
|
+
currentChunkSize = file.size - start
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const params = new FormData()
|
|
71
|
+
params.append('resumableChunkNumber', chunkNumber + 1)
|
|
72
|
+
params.append('resumableChunkSize', currentChunkSize)
|
|
73
|
+
params.append('resumableCurrentChunkSize', currentChunkSize)
|
|
74
|
+
params.append('resumableTotalSize', file.size)
|
|
75
|
+
params.append('resumableType', file.type)
|
|
76
|
+
params.append('resumableIdentifier', identifier)
|
|
77
|
+
params.append('resumableFilename', file.name)
|
|
78
|
+
params.append('resumableRelativePath', file.name)
|
|
79
|
+
params.append('resumableTotalChunks', blockCount)
|
|
80
|
+
params.append('file', file.slice(start, end), file.name)
|
|
81
|
+
|
|
82
|
+
if (Object.keys(formData).length > 0 && chunkNumber + 1 === blockCount) {
|
|
83
|
+
for (const [key, value] of Object.entries(formData)) {
|
|
84
|
+
if (key !== 'file') {
|
|
85
|
+
params.append(key, value)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await waitForRateLimit(maxRequestsPerMinute, rateLimitWindowMs)
|
|
91
|
+
|
|
92
|
+
const client = resolveHttpClient(httpClient)
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await client.post(endpoint, params, {
|
|
96
|
+
headers: {
|
|
97
|
+
Accept: 'application/json',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
onProgress?.(parseInt((end / file.size) * 100, 10), res)
|
|
102
|
+
|
|
103
|
+
if (end === file.size) {
|
|
104
|
+
onSuccess?.(res)
|
|
105
|
+
return res
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return uploadChunk(endpoint, file, formData, {
|
|
109
|
+
...options,
|
|
110
|
+
chunkNumber: chunkNumber + 1,
|
|
111
|
+
})
|
|
112
|
+
} catch (err) {
|
|
113
|
+
onError?.(err)
|
|
114
|
+
throw err
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Start a chunked upload.
|
|
120
|
+
*/
|
|
121
|
+
export function chunk(
|
|
122
|
+
endpoint,
|
|
123
|
+
file,
|
|
124
|
+
formData = {},
|
|
125
|
+
onProgress,
|
|
126
|
+
onError,
|
|
127
|
+
onSuccess,
|
|
128
|
+
config = {},
|
|
129
|
+
) {
|
|
130
|
+
const chunkSize = config.chunkSize ?? DEFAULT_CHUNK_SIZE
|
|
131
|
+
const blockCount = Math.ceil(file.size / chunkSize) || 1
|
|
132
|
+
const identifier = `${file.size}-${String(file.name).replace(/\./g, '')}`
|
|
133
|
+
|
|
134
|
+
return uploadChunk(endpoint, file, formData, {
|
|
135
|
+
blockCount,
|
|
136
|
+
identifier,
|
|
137
|
+
chunkNumber: 0,
|
|
138
|
+
chunkSize,
|
|
139
|
+
httpClient: config.httpClient,
|
|
140
|
+
maxRequestsPerMinute: config.maxRequestsPerMinute ?? DEFAULT_MAX_REQUESTS_PER_MINUTE,
|
|
141
|
+
rateLimitWindowMs: config.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS,
|
|
142
|
+
onProgress,
|
|
143
|
+
onError,
|
|
144
|
+
onSuccess,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default {
|
|
149
|
+
chunk,
|
|
150
|
+
setDefaultHttpClient,
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { setDefaultHttpClient } from './chunkUploader.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a Vue plugin that registers a UI-specific ChunkUploader component.
|
|
5
|
+
* @param {import('vue').Component} Component
|
|
6
|
+
*/
|
|
7
|
+
export function createChunkUploaderPlugin(Component) {
|
|
8
|
+
return {
|
|
9
|
+
install(app, options = {}) {
|
|
10
|
+
if (options.httpClient) {
|
|
11
|
+
setDefaultHttpClient(options.httpClient)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const name = options.componentName || 'ChunkUploader'
|
|
15
|
+
app.component(name, Component)
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ref, unref, watch } from 'vue'
|
|
2
|
+
import chunkUploader, { chunk } from './chunkUploader.js'
|
|
3
|
+
|
|
4
|
+
export const chunkUploaderProps = {
|
|
5
|
+
modelValue: {
|
|
6
|
+
type: [Object, File, Blob],
|
|
7
|
+
default: null,
|
|
8
|
+
},
|
|
9
|
+
autoUpload: {
|
|
10
|
+
type: Boolean,
|
|
11
|
+
default: true,
|
|
12
|
+
},
|
|
13
|
+
url: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
fields: {
|
|
18
|
+
type: Object,
|
|
19
|
+
default: () => ({}),
|
|
20
|
+
},
|
|
21
|
+
httpClient: {
|
|
22
|
+
type: Object,
|
|
23
|
+
default: null,
|
|
24
|
+
},
|
|
25
|
+
chunkSize: {
|
|
26
|
+
type: Number,
|
|
27
|
+
default: undefined,
|
|
28
|
+
},
|
|
29
|
+
maxRequestsPerMinute: {
|
|
30
|
+
type: Number,
|
|
31
|
+
default: undefined,
|
|
32
|
+
},
|
|
33
|
+
rateLimitWindowMs: {
|
|
34
|
+
type: Number,
|
|
35
|
+
default: undefined,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const chunkUploaderEmits = [
|
|
40
|
+
'update:modelValue',
|
|
41
|
+
'onSelectFile',
|
|
42
|
+
'onSendFile',
|
|
43
|
+
'onProgress',
|
|
44
|
+
'onError',
|
|
45
|
+
'onSuccess',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Shared upload state/logic for all UI adapters.
|
|
50
|
+
* @param {Record<string, any>} props
|
|
51
|
+
* @param {(event: string, ...args: any[]) => void} emit
|
|
52
|
+
*/
|
|
53
|
+
export function useChunkUpload(props, emit) {
|
|
54
|
+
const hasError = ref(false)
|
|
55
|
+
const progress = ref(0)
|
|
56
|
+
|
|
57
|
+
const resetState = () => {
|
|
58
|
+
hasError.value = false
|
|
59
|
+
progress.value = 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const uploadFile = async (extraFormData = {}) => {
|
|
63
|
+
const file = unref(props.modelValue)
|
|
64
|
+
if (!file) {
|
|
65
|
+
throw new Error('No file selected')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const formData = {
|
|
69
|
+
...unref(props.fields),
|
|
70
|
+
...extraFormData,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const config = {
|
|
74
|
+
httpClient: unref(props.httpClient) || undefined,
|
|
75
|
+
chunkSize: unref(props.chunkSize),
|
|
76
|
+
maxRequestsPerMinute: unref(props.maxRequestsPerMinute),
|
|
77
|
+
rateLimitWindowMs: unref(props.rateLimitWindowMs),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
chunk(
|
|
82
|
+
unref(props.url),
|
|
83
|
+
file,
|
|
84
|
+
formData,
|
|
85
|
+
(percent) => {
|
|
86
|
+
progress.value = percent
|
|
87
|
+
emit('onProgress', percent)
|
|
88
|
+
},
|
|
89
|
+
(err) => {
|
|
90
|
+
hasError.value = true
|
|
91
|
+
emit('onError', err)
|
|
92
|
+
reject(err)
|
|
93
|
+
},
|
|
94
|
+
(res) => {
|
|
95
|
+
emit('onSuccess', res)
|
|
96
|
+
resolve(res)
|
|
97
|
+
},
|
|
98
|
+
config,
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const clear = () => {
|
|
104
|
+
resetState()
|
|
105
|
+
emit('update:modelValue', null)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
watch(
|
|
109
|
+
() => unref(props.modelValue),
|
|
110
|
+
async (newVal, oldVal) => {
|
|
111
|
+
if (newVal === oldVal) return
|
|
112
|
+
|
|
113
|
+
resetState()
|
|
114
|
+
emit('onSelectFile', chunkUploader)
|
|
115
|
+
|
|
116
|
+
if (unref(props.autoUpload) && newVal) {
|
|
117
|
+
try {
|
|
118
|
+
await uploadFile()
|
|
119
|
+
} catch {
|
|
120
|
+
// error already emitted via onError
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
hasError,
|
|
128
|
+
progress,
|
|
129
|
+
resetState,
|
|
130
|
+
uploadFile,
|
|
131
|
+
clear,
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chunkUploader, { chunk, setDefaultHttpClient } from './core/chunkUploader.js'
|
|
2
|
+
import {
|
|
3
|
+
useChunkUpload,
|
|
4
|
+
chunkUploaderProps,
|
|
5
|
+
chunkUploaderEmits,
|
|
6
|
+
} from './core/useChunkUpload.js'
|
|
7
|
+
import { createChunkUploaderPlugin } from './core/createPlugin.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Core package entry — no UI framework bundled.
|
|
11
|
+
*
|
|
12
|
+
* Choose a UI adapter explicitly:
|
|
13
|
+
* import ... from 'vue-chunk-uploader/quasar'
|
|
14
|
+
* import ... from 'vue-chunk-uploader/vuetify'
|
|
15
|
+
* import ... from 'vue-chunk-uploader/native'
|
|
16
|
+
*/
|
|
17
|
+
export {
|
|
18
|
+
chunkUploader,
|
|
19
|
+
chunk,
|
|
20
|
+
setDefaultHttpClient,
|
|
21
|
+
useChunkUpload,
|
|
22
|
+
chunkUploaderProps,
|
|
23
|
+
chunkUploaderEmits,
|
|
24
|
+
createChunkUploaderPlugin,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
chunk,
|
|
29
|
+
setDefaultHttpClient,
|
|
30
|
+
useChunkUpload,
|
|
31
|
+
createChunkUploaderPlugin,
|
|
32
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vue-chunk-uploader-native" data-ui="native">
|
|
3
|
+
<div class="vue-chunk-uploader-native__row">
|
|
4
|
+
<input
|
|
5
|
+
ref="inputRef"
|
|
6
|
+
type="file"
|
|
7
|
+
v-bind="$attrs"
|
|
8
|
+
@change="onChange"
|
|
9
|
+
/>
|
|
10
|
+
<button
|
|
11
|
+
v-if="modelValue"
|
|
12
|
+
type="button"
|
|
13
|
+
class="vue-chunk-uploader-native__clear"
|
|
14
|
+
@click="onClear"
|
|
15
|
+
>
|
|
16
|
+
×
|
|
17
|
+
</button>
|
|
18
|
+
<span
|
|
19
|
+
class="vue-chunk-uploader-native__badge"
|
|
20
|
+
:data-error="hasError || undefined"
|
|
21
|
+
:data-done="progress >= 100 || undefined"
|
|
22
|
+
>
|
|
23
|
+
<template v-if="progress < 100">{{ progress }}%</template>
|
|
24
|
+
<template v-else>✓</template>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<progress
|
|
29
|
+
v-if="progress > 0 && progress < 100"
|
|
30
|
+
class="vue-chunk-uploader-native__progress"
|
|
31
|
+
:value="progress"
|
|
32
|
+
max="100"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { ref } from 'vue'
|
|
39
|
+
import {
|
|
40
|
+
chunkUploaderProps,
|
|
41
|
+
chunkUploaderEmits,
|
|
42
|
+
useChunkUpload,
|
|
43
|
+
} from '../../core/useChunkUpload.js'
|
|
44
|
+
|
|
45
|
+
defineOptions({ inheritAttrs: false })
|
|
46
|
+
|
|
47
|
+
const props = defineProps(chunkUploaderProps)
|
|
48
|
+
const emit = defineEmits(chunkUploaderEmits)
|
|
49
|
+
|
|
50
|
+
const inputRef = ref(null)
|
|
51
|
+
const { hasError, progress, uploadFile, clear } = useChunkUpload(props, emit)
|
|
52
|
+
|
|
53
|
+
const onChange = (event) => {
|
|
54
|
+
const file = event.target.files?.[0] ?? null
|
|
55
|
+
emit('update:modelValue', file)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const onClear = () => {
|
|
59
|
+
if (inputRef.value) {
|
|
60
|
+
inputRef.value.value = ''
|
|
61
|
+
}
|
|
62
|
+
clear()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
defineExpose({ uploadFile })
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.vue-chunk-uploader-native__row {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 0.5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.vue-chunk-uploader-native__badge {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
min-width: 3rem;
|
|
80
|
+
padding: 0.15rem 0.5rem;
|
|
81
|
+
border-radius: 999px;
|
|
82
|
+
font-size: 0.85rem;
|
|
83
|
+
background: #16a34a;
|
|
84
|
+
color: #fff;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.vue-chunk-uploader-native__badge[data-error] {
|
|
88
|
+
background: #dc2626;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.vue-chunk-uploader-native__clear {
|
|
92
|
+
border: 0;
|
|
93
|
+
background: transparent;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
font-size: 1.25rem;
|
|
96
|
+
line-height: 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.vue-chunk-uploader-native__progress {
|
|
100
|
+
display: block;
|
|
101
|
+
width: 100%;
|
|
102
|
+
margin-top: 0.5rem;
|
|
103
|
+
height: 0.5rem;
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ChunkUploader from './ChunkUploader.vue'
|
|
2
|
+
import { createChunkUploaderPlugin } from '../../core/createPlugin.js'
|
|
3
|
+
import chunkUploader, { chunk, setDefaultHttpClient } from '../../core/chunkUploader.js'
|
|
4
|
+
import { useChunkUpload } from '../../core/useChunkUpload.js'
|
|
5
|
+
|
|
6
|
+
const VueChunkUploader = createChunkUploaderPlugin(ChunkUploader)
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ChunkUploader,
|
|
10
|
+
VueChunkUploader,
|
|
11
|
+
chunkUploader,
|
|
12
|
+
chunk,
|
|
13
|
+
setDefaultHttpClient,
|
|
14
|
+
useChunkUpload,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default VueChunkUploader
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<q-file
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
:model-value="modelValue"
|
|
5
|
+
@update:model-value="(value) => emit('update:modelValue', value)"
|
|
6
|
+
>
|
|
7
|
+
<template #hint>
|
|
8
|
+
<q-linear-progress
|
|
9
|
+
v-if="progress > 0 && progress < 100"
|
|
10
|
+
stripe
|
|
11
|
+
:value="progress / 100"
|
|
12
|
+
:color="hasError ? 'negative' : 'positive'"
|
|
13
|
+
class="q-mt-sm"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<template #prepend>
|
|
18
|
+
<q-icon name="cloud_upload" @click.stop />
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<template #append>
|
|
22
|
+
<q-btn rounded :color="hasError ? 'negative' : 'positive'">
|
|
23
|
+
<template v-if="progress < 100">
|
|
24
|
+
{{ progress }} %
|
|
25
|
+
</template>
|
|
26
|
+
<template v-else>
|
|
27
|
+
<q-icon name="done_all" color="white" size="24px" />
|
|
28
|
+
</template>
|
|
29
|
+
</q-btn>
|
|
30
|
+
<q-icon name="close" class="cursor-pointer" @click="clear" />
|
|
31
|
+
</template>
|
|
32
|
+
</q-file>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import {
|
|
37
|
+
chunkUploaderProps,
|
|
38
|
+
chunkUploaderEmits,
|
|
39
|
+
useChunkUpload,
|
|
40
|
+
} from '../../core/useChunkUpload.js'
|
|
41
|
+
|
|
42
|
+
defineOptions({ inheritAttrs: false })
|
|
43
|
+
|
|
44
|
+
const props = defineProps(chunkUploaderProps)
|
|
45
|
+
const emit = defineEmits(chunkUploaderEmits)
|
|
46
|
+
|
|
47
|
+
const { hasError, progress, uploadFile, clear } = useChunkUpload(props, emit)
|
|
48
|
+
|
|
49
|
+
defineExpose({ uploadFile })
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ChunkUploader from './ChunkUploader.vue'
|
|
2
|
+
import { createChunkUploaderPlugin } from '../../core/createPlugin.js'
|
|
3
|
+
import chunkUploader, { chunk, setDefaultHttpClient } from '../../core/chunkUploader.js'
|
|
4
|
+
import { useChunkUpload } from '../../core/useChunkUpload.js'
|
|
5
|
+
|
|
6
|
+
const VueChunkUploader = createChunkUploaderPlugin(ChunkUploader)
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ChunkUploader,
|
|
10
|
+
VueChunkUploader,
|
|
11
|
+
chunkUploader,
|
|
12
|
+
chunk,
|
|
13
|
+
setDefaultHttpClient,
|
|
14
|
+
useChunkUpload,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default VueChunkUploader
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vue-chunk-uploader-vuetify">
|
|
3
|
+
<v-file-input
|
|
4
|
+
v-bind="$attrs"
|
|
5
|
+
:model-value="modelValue"
|
|
6
|
+
:error="hasError || undefined"
|
|
7
|
+
clearable
|
|
8
|
+
prepend-icon="mdi-cloud-upload"
|
|
9
|
+
@update:model-value="onFileUpdate"
|
|
10
|
+
@click:clear="clear"
|
|
11
|
+
>
|
|
12
|
+
<template #append>
|
|
13
|
+
<v-chip
|
|
14
|
+
:color="hasError ? 'error' : progress >= 100 ? 'success' : 'primary'"
|
|
15
|
+
size="small"
|
|
16
|
+
label
|
|
17
|
+
>
|
|
18
|
+
<template v-if="progress < 100">{{ progress }}%</template>
|
|
19
|
+
<v-icon v-else icon="mdi-check-all" size="small" />
|
|
20
|
+
</v-chip>
|
|
21
|
+
</template>
|
|
22
|
+
</v-file-input>
|
|
23
|
+
|
|
24
|
+
<v-progress-linear
|
|
25
|
+
v-if="progress > 0 && progress < 100"
|
|
26
|
+
class="mt-1"
|
|
27
|
+
:model-value="progress"
|
|
28
|
+
:color="hasError ? 'error' : 'success'"
|
|
29
|
+
height="6"
|
|
30
|
+
rounded
|
|
31
|
+
striped
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup>
|
|
37
|
+
import {
|
|
38
|
+
chunkUploaderProps,
|
|
39
|
+
chunkUploaderEmits,
|
|
40
|
+
useChunkUpload,
|
|
41
|
+
} from '../../core/useChunkUpload.js'
|
|
42
|
+
|
|
43
|
+
defineOptions({ inheritAttrs: false })
|
|
44
|
+
|
|
45
|
+
const props = defineProps(chunkUploaderProps)
|
|
46
|
+
const emit = defineEmits(chunkUploaderEmits)
|
|
47
|
+
|
|
48
|
+
const { hasError, progress, uploadFile, clear } = useChunkUpload(props, emit)
|
|
49
|
+
|
|
50
|
+
const onFileUpdate = (value) => {
|
|
51
|
+
// Vuetify may give File | File[] | null
|
|
52
|
+
const file = Array.isArray(value) ? value[0] ?? null : value
|
|
53
|
+
emit('update:modelValue', file)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
defineExpose({ uploadFile })
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ChunkUploader from './ChunkUploader.vue'
|
|
2
|
+
import { createChunkUploaderPlugin } from '../../core/createPlugin.js'
|
|
3
|
+
import chunkUploader, { chunk, setDefaultHttpClient } from '../../core/chunkUploader.js'
|
|
4
|
+
import { useChunkUpload } from '../../core/useChunkUpload.js'
|
|
5
|
+
|
|
6
|
+
const VueChunkUploader = createChunkUploaderPlugin(ChunkUploader)
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ChunkUploader,
|
|
10
|
+
VueChunkUploader,
|
|
11
|
+
chunkUploader,
|
|
12
|
+
chunk,
|
|
13
|
+
setDefaultHttpClient,
|
|
14
|
+
useChunkUpload,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default VueChunkUploader
|