From 9132fe0386617d53a7d8e23aa5170a297680a788 Mon Sep 17 00:00:00 2001 From: yutent Date: Fri, 1 Dec 2023 17:58:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=8A=E4=BC=A0=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/fs.js | 118 ++++++++++++++++++++++++++++++ src/form/uploader.js | 168 +++++++++++++++++++++++++++++-------------- 2 files changed, 233 insertions(+), 53 deletions(-) create mode 100644 src/base/fs.js diff --git a/src/base/fs.js b/src/base/fs.js new file mode 100644 index 0000000..b380637 --- /dev/null +++ b/src/base/fs.js @@ -0,0 +1,118 @@ +/** + * {文件操作的一些API} + * @author yutent + * @date 2023/11/06 15:46:27 + */ + +// 计算文件指纹 +// 为避免大文件耗时过长, 只取前后各64位, 计算sha1值 +export function getFingerprint(file) { + let head = file.slice(0, 64) + let tail = file.slice(-64) + return Promise.all([head.arrayBuffer(), tail.arrayBuffer()]).then( + ([a1, a2]) => { + let u8a1 = new Uint8Array(a1) + let u8a2 = new Uint8Array(a2) + let u8 = new Uint8Array(u8a1.length + u8a2.length) + u8.set(u8a1) + u8.set(u8a2, u8a1.length) + return u8 + } + ) +} + +export function toBlobURL(url, type) { + return fetch(url) + .then(r => r.blob()) + .then(r => URL.createObjectURL(type ? new Blob([r], { type }) : r)) +} + +export function parseSize(num) { + if (num < 1024) { + return `${num} B` + } else { + num = (num / 1024).toFixed(2) - 0 + + if (num < 1024) { + return `${num} KB` + } else { + num = (num / 1024).toFixed(2) - 0 + return `${num} MB` + } + } +} + +export async function toUint8(bin) { + return new Uint8Array(await bin.arrayBuffer()) +} + +// 路径合并, 同nodejs的 path.join +export function join(p1, p2) { + let tmp1 = p1.split('/') + let tmp2 = p2.split('/') + if (tmp1.at(-1) === '') { + tmp1.pop() + } + while (tmp2.length) { + let tmp = tmp2.shift() + if (tmp === '.' || tmp === '') { + continue + } else if (tmp === '..') { + tmp1.pop() + } else { + tmp1.push(tmp) + } + } + return tmp1.join('/') +} + +// 获取路径的文件名 +export function filename(path) { + let paths = path.split('/') + return paths.pop() +} + +async function getEntry(handle, dir = '') { + let key = join(dir, handle.name) + if (handle.kind === 'directory') { + let entries = await handle.entries() + let item + let list = [] + + while ((item = await entries.next())) { + if (item.done) { + break + } + let tmp = item.value[1] + + if (tmp.kind === 'directory') { + list = list.concat(await getEntry(tmp, key)) + } else { + list.push({ + key: join(key, tmp.name), + file: await tmp.getFile() + }) + } + } + return list + } else { + return { + key, + file: await handle.getFile() + } + } +} + +export async function resolveFiles(items) { + // 无法直接在 getAsFileSystemHandle 这一步就用 await + // 会导致 dataTransfer 后面的数据被清空, 无论选中多少个, 都只能读到第一个 + return Promise.all([...items].map(it => it.getAsFileSystemHandle())).then( + async handles => { + let list = [] + for (let it of handles) { + list = list.concat(await getEntry(it)) + } + return list + } + ) +} diff --git a/src/form/uploader.js b/src/form/uploader.js index 987a443..dd428c4 100644 --- a/src/form/uploader.js +++ b/src/form/uploader.js @@ -5,6 +5,7 @@ */ import { css, html, Component, bind, unbind, styleMap } from 'wkit' +import { resolveFiles, parseSize, filename } from '../base/fs.js' import '../base/icon.js' function uuid() { @@ -42,12 +43,17 @@ class Uploader extends Component { --wc-icon-size: 14px; } - .tips { + .tips, + .error { display: block; - line-height: 2; + line-height: 1.5; font-size: 12px; font-style: normal; } + .error { + color: var(--color-red-1); + background: none; + } .upload-button { position: relative; display: inline-flex; @@ -59,7 +65,8 @@ class Uploader extends Component { border: 1px solid var(--color-grey-2); border-radius: 3px; cursor: pointer; - transition: background 0.1s linear, border-color 0.1s linear; + transition: background 0.1s linear, border-color 0.1s linear, + color 0.1s linear; &:hover { border-color: var(--color-grey-1); @@ -67,6 +74,12 @@ class Uploader extends Component { &:active { background: var(--color-plain-1); } + + &.limited { + background: var(--color-disabled-background); + opacity: 0.6; + cursor: not-allowed; + } } .hidden-input { position: absolute; @@ -91,10 +104,12 @@ class Uploader extends Component { padding: 0 8px; line-height: 24px; gap: 4px; + background: none no-repeat center; + background-size: contain; transition: background 0.15s linear; &:hover { - background: var(--color-plain-1); + background-color: var(--color-plain-1); wc-icon.del { opacity: 1; @@ -188,6 +203,16 @@ class Uploader extends Component { // drag css` :host([drag]) { + .container { + flex-flow: column; + } + .action-area { + width: 100%; + } + .tips { + display: block; + } + .upload-button { flex-direction: column; width: 100%; @@ -199,50 +224,80 @@ class Uploader extends Component { border-color: var(--color-orange-1); background: var(--color-drag-background); } + + &:hover { + color: var(--color-grey-3); + } } } `, - // thumb - css``, // disabled css` :host([disabled]) { - cursor: not-allowed; opacity: 0.6; - .thumb::before { + .upload-button { + background: var(--color-disabled-background); cursor: not-allowed; - transform: unset; } } ` ] #files = [] + #timer + + #toast(msg) { + this.$refs.err.textContent = msg + clearTimeout(this.#timer) + this.#timer = setTimeout(() => { + this.$refs.err.textContent = '' + }, 5000) + } #fileChange(ev) { let files = [...ev.target.files] - let { limit } = this - if (limit > 0) { - files = files.slice(0, limit - this.#files.length) - } - - files = files.map(file => ({ file, id: uuid(), url: '', stat: 0 })) - - this.#files = this.#files.concat(files) - - this.$emit('upload', { - files, - send: args => { - for (let it of args) { - it.stat = 1 - } - this.value = this.#files.map(it => it.url) - } - }) ev.target.value = '' - this.$requestUpdate() + + this.#fetchUpload(files) + } + + #fetchUpload(files) { + let { limit, maxSize } = this + let len = files.length + + if (maxSize > 0) { + files = files.filter(it => it.size <= maxSize) + if (files.length < len) { + len = files.length + this.#toast(`部分文件被忽略, 单文件最大允许 ${parseSize(maxSize)}`) + } + } + + if (limit > 0) { + files = files.slice(0, limit - this.#files.length) + if (files.length < len) { + this.#toast(`部分文件被忽略, 最多选择 ${limit} 个文件`) + } + } + + if (files.length) { + files = files.map(file => ({ file, id: uuid(), url: '', stat: 0 })) + + this.#files = this.#files.concat(files) + + this.$emit('upload', { + files, + send: args => { + for (let it of args) { + it.stat = 1 + } + this.value = this.#files.map(it => it.url) + } + }) + this.$requestUpdate() + } } #remove(ev, it, id) { @@ -256,61 +311,66 @@ class Uploader extends Component { } #dragover() { - this.$refs.drag.classList.toggle('in', true) + if (this.disabled) { + return + } + this.$refs.button.classList.toggle('in', true) } + #dragleave() { - this.$refs.drag.classList.toggle('in', false) + this.$refs.button.classList.toggle('in', false) } - #drop(ev) { - this.$refs.drag.classList.toggle('in', false) + async #drop(ev) { + let files + this.$refs.button.classList.toggle('in', false) - // let files = await resolveFiles(ev.dataTransfer.items) + if (this.disabled) { + return + } - // for (let it of files) { - // if (it.file.type.startsWith('image/')) { - // this.img = it.file - // this.init() - // break - // } - // } + files = await resolveFiles(ev.dataTransfer.items) + + this.#fetchUpload(files.map(it => it.file)) } mounted() {} render() { - let { grid, drag } = this + let { disabled, grid, drag } = this let limited = this.limit > 0 && this.#files.length >= this.limit return html`
-
+
- - ${grid ? '' : '选择文件'} + + ${drag + ? '拖拽文件到此处 或 点击选择文件' + : grid + ? '' + : '选择文件'}
${this.tips} +