diff --git a/package.json b/package.json index 2c0ebde..529c8ee 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "@vicons/ionicons5": "^0.12.0", + "axios": "^1.7.3", "naive-ui": "^2.39.0", "sass": "^1.77.8", "vue": "^3.4.35" }, "devDependencies": { + "@types/lodash": "^4.17.7", "@types/node": "^22.1.0", "@vitejs/plugin-vue": "^5.1.2", "typescript": "^5.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cca68d..32d8790 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@vicons/ionicons5': specifier: ^0.12.0 version: 0.12.0 + axios: + specifier: ^1.7.3 + version: 1.7.3 naive-ui: specifier: ^2.39.0 version: 2.39.0(vue@3.4.37(typescript@5.5.4)) @@ -21,6 +24,9 @@ importers: specifier: ^3.4.35 version: 3.4.37(typescript@5.5.4) devDependencies: + '@types/lodash': + specifier: ^4.17.7 + version: 4.17.7 '@types/node': specifier: ^22.1.0 version: 22.1.0 @@ -390,6 +396,12 @@ packages: async-validator@4.2.5: resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.7.3: + resolution: {integrity: sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -408,6 +420,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + computeds@0.0.1: resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} @@ -432,6 +448,10 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + entities@5.0.0: resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} engines: {node: '>=0.12'} @@ -451,6 +471,19 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -500,6 +533,14 @@ packages: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -535,6 +576,9 @@ packages: resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} engines: {node: ^10 || ^12 || >=14} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -916,6 +960,16 @@ snapshots: async-validator@4.2.5: {} + asynckit@0.4.0: {} + + axios@1.7.3: + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} binary-extensions@2.3.0: {} @@ -940,6 +994,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + computeds@0.0.1: {} css-render@0.15.14: @@ -961,6 +1019,8 @@ snapshots: de-indent@1.0.2: {} + delayed-stream@1.0.0: {} + entities@5.0.0: {} esbuild@0.21.5: @@ -997,6 +1057,14 @@ snapshots: dependencies: to-regex-range: 5.0.1 + follow-redirects@1.15.6: {} + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true @@ -1035,6 +1103,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -1080,6 +1154,8 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + proxy-from-env@1.1.0: {} + readdirp@3.6.0: dependencies: picomatch: 2.3.1 diff --git a/src/App.vue b/src/App.vue index 61a0e72..89eb32f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,10 +1,21 @@ diff --git a/src/class/UploadQueue.ts b/src/class/UploadQueue.ts new file mode 100644 index 0000000..ffc5dc0 --- /dev/null +++ b/src/class/UploadQueue.ts @@ -0,0 +1,165 @@ +import generateId from "@/hooks/generateId"; +import useMiaoFetchApi from "@/hooks/useMiaoFetchApi"; +import VirtualDirectory from "./VirtualDirectory"; + +const miaoFetchApi = useMiaoFetchApi(); + +export class UploadQueueItem { + constructor( + file: File, + targetDirectory: VirtualDirectory, + config?: { + onSuccess?: () => void; + onFailed?: () => void; + } + ) { + this.file = file; + this.targetDirectory = targetDirectory; + this.uid = generateId(); + this.status = "waiting"; + this.process = 0; + this.onSuccess = config && config.onSuccess; + this.onFailed = config && config.onFailed; + } + uid: number; + file: File; + targetDirectory: VirtualDirectory; + status: "waiting" | "uploading" | "success" | "failed"; + process: number; + failedReason: unknown | undefined; + + onSuccess?: () => void; + onFailed?: () => void; + + _abort: (() => void | undefined) | undefined; + _status: Promise | undefined; + + /** + * 开始上传文件 + */ + startUpload() { + if (this.status === "uploading") { + return; + } + const { abort, status } = miaoFetchApi.upload(this.file, this.targetDirectory, { + onProcess: percent => { + this.process = percent; + } + }); + this._abort = abort; + this._status = status; + this.status = "uploading"; + status + .then(() => { + this.status = "success"; + this.onSuccess && this.onSuccess(); + this._abort = undefined; + this._status = undefined; + }) + .catch(e => { + this.status = "failed"; + this.failedReason = e; + this.onFailed && this.onFailed(); + }); + } + + /** + * 终止上传文件 + */ + abortUpload() { + this._abort && this._abort(); + this.status = "failed"; + this.failedReason = "abort"; + } +} + +export default class UploadQueue { + constructor() { + this.uid = generateId(); + this.queue = []; + this.mode = "serial"; + } + uid: number; + queue: UploadQueueItem[]; + current: UploadQueueItem | undefined; + + // 未来再去实现,目前就是串行的 + // 好像也不用去实现 + mode: "serial" | "parallel"; + get status() { + for (let item of this.queue) { + if (item.status === "uploading") { + return "busy"; + } + } + return "free"; + } + + /** + * 获取下一个需要被上传的item + */ + get nextUplpadItem() { + for (let item of this.queue) { + if (item.status === "waiting") { + return item; + } + } + } + + /** + * 开始串行的上传循环 + */ + loop() { + if (this.status === "busy") { + return; + } + if (this.nextUplpadItem) { + this.nextUplpadItem.startUpload(); + } + } + + /** + * 向上传文件队列输送一批新的文件 + * @param {any} files 需要上传的文件 + * @param {any} targetDirectory 目标文件夹 + * @param {any} option 每个文件上传完成以后都会触发一遍的回调函数 + * @returns {any} + */ + push( + files: File[], + targetDirectory: VirtualDirectory, + option?: { + /** + * 每个文件上传完成以后都会触发一遍的回调函数 + */ + onFinish?: () => void; + /** + * 是否立即开始上传,若否则按队列顺序一个一个上传 + */ + immediately?: boolean; + } + ) { + const onFinish = option?.onFinish; + const immediately = option?.immediately ?? false; + const uploadQueue = files.map(file => { + return new UploadQueueItem(file, targetDirectory, { + onSuccess: () => { + this.loop(); + onFinish && onFinish(); + }, + onFailed: () => { + this.loop(); + } + }); + }); + this.queue.push(...uploadQueue); + if (!immediately) { + this.loop(); + } else { + uploadQueue.forEach(item => { + item.startUpload(); + }); + } + return uploadQueue; + } +} diff --git a/src/components/miaoDirectory/miaoDirectoryItem.vue b/src/components/miaoDirectory/miaoDirectoryItem.vue index f31e718..b821c99 100644 --- a/src/components/miaoDirectory/miaoDirectoryItem.vue +++ b/src/components/miaoDirectory/miaoDirectoryItem.vue @@ -50,9 +50,13 @@ const time = computed(()=>{ }) const dropDownOptions = computed(()=> { - const options = [] + const options = [{ + label: '删除', + key: 'delete' + }] if(itemType.value === 'file'){ - options.push(...[{ + options.push(...[ + { label: '下载', key: 'download', } @@ -135,6 +139,7 @@ const handleDragStart = () => { .item-main-icon { margin-left: 10px; height: 80%; + color: #00403e; } .item-main-info { margin-left: 10px; diff --git a/src/components/miaoMask.vue b/src/components/miaoMask.vue index 9145ca9..fc55001 100644 --- a/src/components/miaoMask.vue +++ b/src/components/miaoMask.vue @@ -1,7 +1,9 @@ diff --git a/src/config.ts b/src/config.ts index d3ee6e1..cff41ac 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,8 @@ export default { baseUrl, uploadSizeLimit: 1024 * 1024 * 100, api: { - get: '/get' - } + get: '/get', + upload: '/upload' + }, + enableUpload: true, }; diff --git a/src/hooks/useDataBus.ts b/src/hooks/useDataBus.ts index 30721cc..23bbb73 100644 --- a/src/hooks/useDataBus.ts +++ b/src/hooks/useDataBus.ts @@ -6,7 +6,7 @@ export default function useDataBus() { dataStorage[key] = value; return value; }, - get(key: string | number) { + pop(key: string | number) { const value = dataStorage[key]; dataStorage[key] = undefined; return value; diff --git a/src/hooks/useMiaoFetch.ts b/src/hooks/useMiaoFetch.ts deleted file mode 100644 index f82f5cc..0000000 --- a/src/hooks/useMiaoFetch.ts +++ /dev/null @@ -1,22 +0,0 @@ -type miaoFetchConfig = { - retry: number; -}; - -export default function useMiaoFetch(config: miaoFetchConfig) { - const { retry } = config; - - return async (input: RequestInfo | URL, init?: RequestInit) => { - let retryTimes = 0; - let err: any; - while (retryTimes <= retry) { - try { - const response = await fetch(input, init); - return response; - } catch (e) { - err = e; - retryTimes += 1; - } - } - throw err; - }; -} diff --git a/src/hooks/useMiaoFetchApi.ts b/src/hooks/useMiaoFetchApi.ts new file mode 100644 index 0000000..1255037 --- /dev/null +++ b/src/hooks/useMiaoFetchApi.ts @@ -0,0 +1,95 @@ +import VirtualDirectory from "@/class/VirtualDirectory"; +import { getOption, getResponse, miaoFetchConfig, uploadOption } from "@/types/fetch"; +import Config from "@/config"; +import axios, { AxiosRequestConfig } from "axios"; + +export const useMiaoFetch = (option?: miaoFetchConfig) => { + const retry = option?.retry ?? 0; + const controller = new AbortController(); + let flag = true; + return { + miaoFetch: async (config: AxiosRequestConfig) => { + let counter = 0; + let response: any; + let errList = []; + while (flag && counter <= retry) { + try { + response = await axios({ + ...config, + signal: controller.signal + }); + return response; + } catch (_e) { + errList.push(_e); + counter += 1; + } + } + throw errList; + }, + /** + * 强制终止miaoFetch + */ + abortMiaoFetch: () => { + flag = false; + controller.abort(); + } + }; +}; + +const miaoFetchApi = { + /** + * 获取对应文件夹下的数据 + */ + async get(vDirectory: VirtualDirectory, option?: getOption): Promise { + const { baseUrl, api } = Config; + const url = `${baseUrl}${api.get}${vDirectory.path}`; + const retry = option?.retry ?? 0; + + const { miaoFetch } = useMiaoFetch({ + retry + }); + const response = await miaoFetch({ + url + }); + return response.data; + }, + + /** + * 上传单个文件到对应文件夹 + */ + upload(file: File, targetVDir: VirtualDirectory, option?: uploadOption) { + const { baseUrl, api } = Config; + const url = `${baseUrl}${api.upload}${targetVDir.path}`; + const retry = option?.retry ?? 0; + const { miaoFetch, abortMiaoFetch } = useMiaoFetch({ + retry + }); + const sizeTotal = file.size + + const opType = option?.type ?? 'write' + + const data = new FormData() + + data.append('file', file) + data.append('oprateType', opType) + + return { + abort: () => abortMiaoFetch(), + status: new Promise(async (resolve)=>{ + const response = await miaoFetch({ + url, + method: 'post', + data: data, + onUploadProgress(progressEvent) { + option && option.onProcess && option.onProcess(progressEvent.loaded / sizeTotal) + }, + }) + resolve(response.data) + }) + }; + } +}; + +export default function useMiaoFetchApi() { + return miaoFetchApi; +} diff --git a/src/hooks/useUploadQueue.ts b/src/hooks/useUploadQueue.ts new file mode 100644 index 0000000..a30a819 --- /dev/null +++ b/src/hooks/useUploadQueue.ts @@ -0,0 +1,5 @@ +import UploadQueue from "@/class/UploadQueue"; + +const uploadQueue = new UploadQueue() + +export default () => uploadQueue \ No newline at end of file diff --git a/src/types/fetch.ts b/src/types/fetch.ts new file mode 100644 index 0000000..1825f8b --- /dev/null +++ b/src/types/fetch.ts @@ -0,0 +1,24 @@ +import { directory, file } from "./type"; + +export type miaoFetchConfig = { + retry?: number +} + +export type getOption = { +} & miaoFetchConfig + +export type getResponse = (file | directory)[] + +export type uploadOption = { + /** + * 上传进度回调函数 + * @param {any} percent 进度百分比,float类型 + */ + onProcess?: (percent: number) => void + /** + * 上传文件执行的操作 + * write即为覆盖写 + * append即为增量写 + */ + type?: 'write' | 'append' +} & miaoFetchConfig \ No newline at end of file diff --git a/src/views/miaoDirectory.vue b/src/views/miaoDirectory.vue index c9b6557..5f7cf97 100644 --- a/src/views/miaoDirectory.vue +++ b/src/views/miaoDirectory.vue @@ -1,6 +1,6 @@