完成文件上传功能

This commit is contained in:
539943419 committed 2024-08-11 16:02:23 +08:00
1 parent e840e4eb5a
commit 4f477c99d1
14 files changed
+471 -43

No files matched your search

+2
View File
@@ -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",
+76
View File
@@ -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
+60 -6
View File
@@ -1,10 +1,21 @@
<script setup lang="ts">
import { onMounted, reactive } from 'vue';
import { NIcon, NScrollbar } from 'naive-ui';
import { EyeOffOutline, EyeOutline, CloseOutline, CopyOutline, EllipsisVertical } from '@vicons/ionicons5'
import { h, onMounted, reactive, ref, shallowRef } from 'vue';
import { NIcon, NScrollbar, NDropdown, NQrCode } from 'naive-ui';
import { EyeOffOutline, EyeOutline, CloseOutline, CopyOutline, EllipsisVertical, QrCodeOutline } from '@vicons/ionicons5'
import miaoDirectory from './views/miaoDirectory.vue';
import VirtualPage from './class/VirtualPage';
import VirtualDirectory from './class/VirtualDirectory';
import MiaoMask from './components/miaoMask.vue';
import config from './config';
const {baseUrl} = config
const showModal = ref<boolean>(false)
const modalData = shallowRef<{
component: any
props: any
}>()
const views = reactive<VirtualPage[]>([])
@@ -31,6 +42,37 @@ const deleteView = (index: number) => {
}
}
function renderIcon(icon: any) {
return () => {
return h(NIcon, null, {
default: () => h(icon)
})
}
}
const openMenuOption = [
{
label: '分享',
key: 'share',
icon: renderIcon(QrCodeOutline)
}
]
const handleMenuSelect = (key: string) => {
console.log(key)
if(key === 'share') {
showModal.value = true
modalData.value = {
component: NQrCode,
props: {
value: baseUrl,
size: 500,
errorCorrectionLevel: 'H'
}
}
}
}
onMounted(() => {
createView(rootDirectory)
})
@@ -39,6 +81,7 @@ onMounted(() => {
</script>
<template>
<div class="view">
<div class="view-controler">
<n-scrollbar x-scrollable>
<div class="tags-container">
@@ -62,21 +105,31 @@ onMounted(() => {
</n-scrollbar>
<div class="view-controler-menu">
<div class="view-controler-menu-item">
<n-icon class="icon" @click="">
<n-dropdown trigger="click" :options="openMenuOption" @select="handleMenuSelect">
<n-icon class="icon">
<EllipsisVertical class="icon-inner" />
</n-icon>
</n-dropdown>
</div>
</div>
</div>
<div class="view-container">
<div class="view" v-for="(view, index) of views" v-show="view.visible" :key="view.uid">
<div class="view-container-item" v-for="(view, index) of views" v-show="view.visible" :key="view.uid">
<miao-directory v-model:current-directory="view.currentDirectory" :color="view.color" :index="index" @exit="deleteView(index)"></miao-directory>
</div>
</div>
</div>
<MiaoMask v-model:show="showModal" @click="showModal = false" >
<component :is="modalData?.component" v-bind="modalData?.props"></component>
</MiaoMask>
</template>
<style lang="scss" scoped>
$controler-height: 25px;
.view{
position: relative;
height: 100%;
width: 100%;
.view-controler {
display: flex;
@@ -180,7 +233,7 @@ $controler-height: 25px;
height: calc(100% - $controler-height);
overflow-y: overlay;
overflow-x: overlay;
.view {
.view-container-item {
flex: 1;
height: 100%;
}
@@ -203,4 +256,5 @@ $controler-height: 25px;
}
}
}
</style>
+165
View File
@@ -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<any> | 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;
}
}
@@ -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;
+2
View File
@@ -1,7 +1,9 @@
<template>
<transition name="transition">
<div class="mask-body" ref="maskBodyRef" v-show="showMask">
<div @click="e=>e.stopPropagation()">
<slot> {{}} </slot>
</div>
</div>
</transition>
</template>
+4 -2
View File
@@ -4,6 +4,8 @@ export default {
baseUrl,
uploadSizeLimit: 1024 * 1024 * 100,
api: {
get: '/get'
}
get: '/get',
upload: '/upload'
},
enableUpload: true,
};
+1 -1
View File
@@ -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;
-22
View File
@@ -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;
};
}
+95
View File
@@ -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<any>) => {
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<getResponse> {
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;
}
+5
View File
@@ -0,0 +1,5 @@
import UploadQueue from "@/class/UploadQueue";
const uploadQueue = new UploadQueue()
export default () => uploadQueue
+24
View File
@@ -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
+24 -10
View File
@@ -1,6 +1,6 @@
<template>
<div class="miao-directory-item-container" ref="rootDomRef" >
<div class="container-top">
<div class="container-top" :style="{ backgroundColor: props.index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)' }">
<div class="container-top-colorbar" :style="{ backgroundColor: props.color }"></div>
<div class="container-top-breadcrumb">
<!-- <n-scrollbar x-scrollable> -->
@@ -57,16 +57,15 @@ import Config from '@/config'
import dropHandler, { dropHandlerHooks } from '@/hooks/dropHandler';
import miaoMask from '@/components/miaoMask.vue';
import miaoDirectoryItem from '@/components/miaoDirectory/miaoDirectoryItem.vue';
import useMiaoFetch from '@/hooks/useMiaoFetch';
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi';
import useDataBus from '@/hooks/useDataBus';
import { CloudOutline, ChevronBackOutline, ReloadOutline, CloseOutline } from '@vicons/ionicons5'
import useUploadQueue from '@/hooks/useUploadQueue';
const { baseUrl, api } = Config
const dataBus = useDataBus()
const miaoFetch = useMiaoFetch({
retry: 5,
})
const miaoFetchApi = useMiaoFetchApi()
const props = defineProps<{
color: string
@@ -86,15 +85,17 @@ const showModel = ref(0)
// 用于监听拖拽文件事件
const rootDomRef = ref<HTMLDivElement>()
const uploadQueue = useUploadQueue()
// 设置显示的dir
const setCurrentDirectory = async (virtualDirectory: VirtualDirectory) => {
// 若没有缓存数据则同步刷新,有缓存数据则异步刷新
if (virtualDirectory.files === undefined || virtualDirectory.directorys === undefined) {
const data: (file | directory)[] = await (await miaoFetch(`${baseUrl}${api.get}${virtualDirectory.path}`)).json()
const data: (file | directory)[] = await miaoFetchApi.get(virtualDirectory)
virtualDirectory.updateContent(data)
} else {
; (async () => {
const data: (file | directory)[] = await (await miaoFetch(`${baseUrl}${api.get}${virtualDirectory.path}`)).json()
const data: (file | directory)[] = await miaoFetchApi.get(virtualDirectory)
virtualDirectory.updateContent(data)
currentDirectory.value = undefined
currentDirectory.value = virtualDirectory
@@ -106,6 +107,10 @@ const setCurrentDirectory = async (virtualDirectory: VirtualDirectory) => {
// emit("onCurrentDirectoryChange", currentDirectory.value)
}
const reload = () => {
currentDirectory.value && setCurrentDirectory(currentDirectory.value)
}
const openUrl = (href: string, config?: {
download?: string
target?: string
@@ -133,7 +138,7 @@ const handleBack = () => {
}
const handleReload = () => {
currentDirectory.value && setCurrentDirectory(currentDirectory.value)
reload()
}
onMounted(async () => {
@@ -141,8 +146,17 @@ onMounted(async () => {
// 拖动事件处理
const dropFileOption:dropHandlerHooks = {
onDropFiles(files: File[]) {
const dragData = dataBus.get('dragData')
const dragData = dataBus.pop('dragData')
console.log({dragData, files})
if(files && files.length > 0){
currentDirectory.value && uploadQueue.push(files, currentDirectory.value, {
onFinish() {
reload()
},
})
const _f = files[0]
currentDirectory.value && miaoFetchApi.upload(_f, currentDirectory.value)
}
},
onEnter() {
showModel.value += 1
@@ -238,7 +252,7 @@ $bottom-bar-height: 60px;
content: '';
height: 60%;
position: absolute;
right: -1px;
right: -2px;
width: 2px;
background-color: #828282;
}
+6
View File
@@ -17,6 +17,12 @@ export default defineConfig({
target: "http://127.0.0.1:17705",
changeOrigin: true,
rewrite: path => path.replace(/^\/get/, "/get")
},
"/upload": {
target: "http://127.0.0.1:17705",
changeOrigin: true,
rewrite: path => path.replace(/^\/upload/, "/upload")
}
}
},