|
3 | 3 | Describe: 多图片上传组件, 附有预览, 排序, 验证等功能
|
4 | 4 |
|
5 | 5 | todo: 支持 before-upload
|
6 |
| -todo: 支持动态图检测 |
7 | 6 | todo: 图像验证支持验证是否是动图
|
8 | 7 | todo: 文档编写
|
9 |
| -todo: accept 严格性优化 |
10 | 8 | todo: jsDoc 编写
|
| 9 | +todo: 文件判断使用 serveWorker 优化性能 |
11 | 10 | -->
|
12 | 11 |
|
13 | 12 | <template>
|
@@ -91,6 +90,13 @@ todo: jsDoc 编写
|
91 | 90 | </template>
|
92 | 91 |
|
93 | 92 | <script>
|
| 93 | +import { |
| 94 | + getFileType, |
| 95 | + checkIsAnimated, |
| 96 | + isEmptyObj, |
| 97 | + createId, |
| 98 | +} from './utils' |
| 99 | +
|
94 | 100 | /**
|
95 | 101 | * @typedef {Object<string, number, any>} LocalFileInfo 本地图像通过验证后构造的信息对象
|
96 | 102 | * @property {string} localSrc 本地图像预览地址
|
@@ -118,53 +124,6 @@ todo: jsDoc 编写
|
118 | 124 | const ONE_KB = 1024
|
119 | 125 | const ONE_MB = ONE_KB * 1024
|
120 | 126 |
|
121 |
| -// 检测官方文档: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern |
122 |
| -/** 类型检测掩码集合 */ |
123 |
| -const patternMask = [{ |
124 |
| - name: 'image/x-icon', |
125 |
| - mask: [0xFF, 0xFF, 0xFF, 0xFF], |
126 |
| - byte: [0x00, 0x00, 0x01, 0x00], |
127 |
| -}, { |
128 |
| - name: 'image/x-icon', |
129 |
| - mask: [0xFF, 0xFF, 0xFF, 0xFF], |
130 |
| - byte: [0x00, 0x00, 0x02, 0x00], |
131 |
| -}, { |
132 |
| - name: 'image/bmp', |
133 |
| - mask: [0xFF, 0xFF], |
134 |
| - byte: [0x42, 0x4D], |
135 |
| -}, { |
136 |
| - name: 'image/gif', |
137 |
| - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
138 |
| - byte: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], |
139 |
| -}, { |
140 |
| - name: 'image/gif', |
141 |
| - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
142 |
| - byte: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], |
143 |
| -}, { |
144 |
| - name: 'image/webp', |
145 |
| - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
146 |
| - byte: [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50], |
147 |
| -}, { |
148 |
| - name: 'image/png', |
149 |
| - mask: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], |
150 |
| - byte: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], |
151 |
| -}, { |
152 |
| - name: 'image/jpeg', |
153 |
| - mask: [0xFF, 0xFF, 0xFF], |
154 |
| - byte: [0xFF, 0xD8, 0xFF], |
155 |
| -}] |
156 |
| -
|
157 |
| -/** 判断是否是空对象 */ |
158 |
| -function isEmptyObj(data) { |
159 |
| - if (!data) return true |
160 |
| - return (JSON.stringify(data) === '{}') |
161 |
| -} |
162 |
| -
|
163 |
| -/** 生成随机字符串 */ |
164 |
| -function createId() { |
165 |
| - return Math.random().toString(36).substring(2) |
166 |
| -} |
167 |
| -
|
168 | 127 | /**
|
169 | 128 | * 创建项, 如不传入参数则创建空项
|
170 | 129 | * status 状态转换说明:
|
@@ -246,106 +205,6 @@ function getRangeTip(prx, min, max, unit = '') {
|
246 | 205 | return str
|
247 | 206 | }
|
248 | 207 |
|
249 |
| -/** |
250 |
| - * 检测是否是动图 |
251 |
| - * 主要针对 Gif 和 Webp 两种格式 |
252 |
| - * @param {File} file 需要检测的文件 |
253 |
| - * @param {String} fileUrl 文件url |
254 |
| - */ |
255 |
| -async function checkIsAnimated({ file, fileUrl, fileType }) { |
256 |
| - // 参数验证 |
257 |
| - if (!file || !(file instanceof File)) { |
258 |
| - console.error('isAnimated param check fail: param expected to be File object') |
259 |
| - return false |
260 |
| - } |
261 |
| - // 如果不是 gif 和 webp, 默认作为非动图 |
262 |
| - if (fileType !== 'image/webp' && fileType !== 'image/gif') { |
263 |
| - return false |
264 |
| - } |
265 |
| -
|
266 |
| - if (fileType === 'image/webp') { |
267 |
| - return new Promise((resolve) => { |
268 |
| - const request = new XMLHttpRequest() |
269 |
| - request.open('GET', fileUrl, true) |
270 |
| - request.addEventListener('load', () => { |
271 |
| - resolve((request.response.indexOf('ANMF') !== -1)) |
272 |
| - }) |
273 |
| - request.send() |
274 |
| - }) |
275 |
| - } |
276 |
| - if (fileType === 'image/gif') { |
277 |
| - return new Promise((resolve) => { |
278 |
| - const request = new XMLHttpRequest() |
279 |
| - request.open('GET', fileUrl, true) |
280 |
| - request.responseType = 'arraybuffer' |
281 |
| - request.addEventListener('load', () => { |
282 |
| - const arr = new Uint8Array(request.response) |
283 |
| - // make sure it's a gif (GIF8) |
284 |
| - if (arr[0] !== 0x47 || arr[1] !== 0x49 || arr[2] !== 0x46 || arr[3] !== 0x38) { |
285 |
| - resolve(false) |
286 |
| - return |
287 |
| - } |
288 |
| -
|
289 |
| - // ported from php http://www.php.net/manual/en/function.imagecreatefromgif.php#104473 |
290 |
| - // an animated gif contains multiple "frames", with each frame having a |
291 |
| - // header made up of: |
292 |
| - // * a static 4-byte sequence (\x00\x21\xF9\x04) |
293 |
| - // * 4 variable bytes |
294 |
| - // * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?) |
295 |
| - // We read through the file til we reach the end of the file, or we've found |
296 |
| - // at least 2 frame headers |
297 |
| - let frames = 0 |
298 |
| - for (let i = 0, len = arr.length - 9; i < len && frames < 2; ++i) { |
299 |
| - if (arr[i] === 0x00 && arr[i + 1] === 0x21 && arr[i + 2] === 0xF9 && arr[i + 3] === 0x04 && arr[i + 8] === 0x00 && (arr[i + 9] === 0x2C || arr[i + 9] === 0x21)) { |
300 |
| - frames++ |
301 |
| - } |
302 |
| - } |
303 |
| -
|
304 |
| - // if frame count > 1, it's animated |
305 |
| - resolve(frames > 1) |
306 |
| - }) |
307 |
| - request.send() |
308 |
| - }) |
309 |
| - } |
310 |
| -} |
311 |
| -
|
312 |
| -/** |
313 |
| - * 检测文件类型 |
314 |
| - * 使用文件编码进行检测 |
315 |
| - * 支持模式参看: patternMask 定义 |
316 |
| - */ |
317 |
| -async function getFileType(file) { |
318 |
| - if (!(file instanceof File)) { |
319 |
| - return 'unknown' |
320 |
| - } |
321 |
| - return new Promise((resolve) => { |
322 |
| - const fileReader = new FileReader() |
323 |
| - fileReader.onloadend = (e) => { |
324 |
| - const header = (new Uint8Array(e.target.result)).slice(0, 20) |
325 |
| - let type = 'unknown' |
326 |
| -
|
327 |
| - // eslint-disable-next-line arrow-body-style |
328 |
| - const index = patternMask.findIndex((item) => { |
329 |
| - // eslint-disable-next-line arrow-body-style |
330 |
| - return item.mask.every((subItem, subI) => { |
331 |
| - // subItem 掩码标志 |
332 |
| - // item.byte[subI] 规范值 |
333 |
| - // header[subI] 文件实际值 |
334 |
| - // eslint-disable-next-line |
335 |
| - return ((subItem & (header[subI] ^ item.byte[subI])) === 0) |
336 |
| - }) |
337 |
| - }) |
338 |
| -
|
339 |
| - if (index >= 0) { |
340 |
| - type = patternMask[index].name |
341 |
| - } |
342 |
| -
|
343 |
| - resolve(type) |
344 |
| - } |
345 |
| - fileReader.readAsArrayBuffer(file) |
346 |
| - }) |
347 |
| -} |
348 |
| -
|
349 | 208 | /** for originUpload: 一次请求最多的文件数量 */
|
350 | 209 | const uploadLimit = 10
|
351 | 210 | /** for originUpload: 文件对象缓存 */
|
|
0 commit comments