完成文件上传功能

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

No files matched your search

+187 -133
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,168 +81,180 @@ onMounted(() => {
</script>
<template>
<div class="view-controler">
<n-scrollbar x-scrollable>
<div class="tags-container">
<div class="tag" v-for="(view, index) of views" :key="view.uid">
<div class="tag-title">{{ view.title }}</div>
<div class="tag-point" :style="{ backgroundColor: view.color } "></div>
<div class="tag-control">
<n-icon class="tag-control-icon icon" @click="view.switchShow()">
<EyeOutline v-show="view.visible" class="icon-inner" />
<EyeOffOutline v-show="!view.visible" class="icon-inner" />
</n-icon>
<n-icon class="tag-control-icon icon" @click="createView(view.currentDirectory, index + 1)">
<CopyOutline />
</n-icon>
<n-icon class="tag-control-icon icon" @click="deleteView(index)">
<CloseOutline class="icon-inner" />
</n-icon>
<div class="view">
<div class="view-controler">
<n-scrollbar x-scrollable>
<div class="tags-container">
<div class="tag" v-for="(view, index) of views" :key="view.uid">
<div class="tag-title">{{ view.title }}</div>
<div class="tag-point" :style="{ backgroundColor: view.color } "></div>
<div class="tag-control">
<n-icon class="tag-control-icon icon" @click="view.switchShow()">
<EyeOutline v-show="view.visible" class="icon-inner" />
<EyeOffOutline v-show="!view.visible" class="icon-inner" />
</n-icon>
<n-icon class="tag-control-icon icon" @click="createView(view.currentDirectory, index + 1)">
<CopyOutline />
</n-icon>
<n-icon class="tag-control-icon icon" @click="deleteView(index)">
<CloseOutline class="icon-inner" />
</n-icon>
</div>
</div>
</div>
</n-scrollbar>
<div class="view-controler-menu">
<div class="view-controler-menu-item">
<n-dropdown trigger="click" :options="openMenuOption" @select="handleMenuSelect">
<n-icon class="icon">
<EllipsisVertical class="icon-inner" />
</n-icon>
</n-dropdown>
</div>
</div>
</n-scrollbar>
<div class="view-controler-menu">
<div class="view-controler-menu-item">
<n-icon class="icon" @click="">
<EllipsisVertical class="icon-inner" />
</n-icon>
</div>
<div class="view-container">
<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>
<div class="view-container">
<div class="view" 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>
<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-controler {
display: flex;
height: $controler-height;
.view{
position: relative;
height: 100%;
width: 100%;
background-color: #dadada;
// overflow: hidden;
.tags-container {
.view-controler {
display: flex;
margin-left: 5px;
flex: 1;
// overflow-x: scroll;
.tag {
position: relative;
width: 170px;
margin: 0 5px;
// flex: 1;
height: $controler-height;
width: 100%;
background-color: #dadada;
// overflow: hidden;
.tags-container {
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tag-title {
margin-left: 4px;
// width: 70%;
user-select: none;
}
.tag-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-left: 5px;
}
.tag-control {
position: absolute;
right: 10px;
margin-left: 5px;
flex: 1;
// overflow-x: scroll;
.tag {
position: relative;
width: 170px;
margin: 0 5px;
// flex: 1;
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tag-control-icon:not(:last-child) {
margin-right: 5px;
.tag-title {
margin-left: 4px;
// width: 70%;
user-select: none;
}
.tag-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-left: 5px;
}
.tag-control {
position: absolute;
right: 10px;
display: flex;
align-items: center;
.tag-control-icon:not(:last-child) {
margin-right: 5px;
}
}
&:not(:last-child)::after {
content: '';
height: calc($controler-height*0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
right: 0px;
}
}
}
&:not(:last-child)::after {
content: '';
height: calc($controler-height*0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
right: 0px;
.view-controler-menu {
position: relative;
display: flex;
align-items: center;
padding: 0 5px;
.view-controler-menu-item {
height: 100%;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
&::before {
content: '';
height: calc($controler-height*0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
}
}
}
.view-controler-menu {
position: relative;
display: flex;
align-items: center;
padding: 0 5px;
.view-controler-menu-item {
height: 100%;
aspect-ratio: 1;
.icon {
height: calc($controler-height * 0.7);
width: calc($controler-height * 0.7);
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
}
&::before {
content: '';
height: calc($controler-height*0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
cursor: pointer;
background-color: rgba(128, 128, 128, 0);
transition: background-color 0.3s;
&:hover {
background-color: rgba(128, 128, 128, 0.4);
}
}
}
.icon {
height: calc($controler-height * 0.7);
width: calc($controler-height * 0.7);
border-radius: 5px;
.view-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: rgba(128, 128, 128, 0);
transition: background-color 0.3s;
&:hover {
background-color: rgba(128, 128, 128, 0.4);
// flex-direction: column;
height: calc(100% - $controler-height);
overflow-y: overlay;
overflow-x: overlay;
.view-container-item {
flex: 1;
height: 100%;
}
&::-webkit-scrollbar {
position: absolute;
width: 4px;
}
// 滚动条轨道
&::-webkit-scrollbar-track {
// background: rgb(239, 239, 239);
border-radius: 2px;
}
// 小滑块
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
// border-radius: 5px;
}
&::-webkit-scrollbar-thumb:hover {
background: #999999;
}
}
}
.view-container {
position: relative;
display: flex;
// flex-direction: column;
height: calc(100% - $controler-height);
overflow-y: overlay;
overflow-x: overlay;
.view {
flex: 1;
height: 100%;
}
&::-webkit-scrollbar {
position: absolute;
width: 4px;
}
// 滚动条轨道
&::-webkit-scrollbar-track {
// background: rgb(239, 239, 239);
border-radius: 2px;
}
// 小滑块
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
// border-radius: 5px;
}
&::-webkit-scrollbar-thumb:hover {
background: #999999;
}
}
</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;
+3 -1
View File
@@ -1,7 +1,9 @@
<template>
<transition name="transition">
<div class="mask-body" ref="maskBodyRef" v-show="showMask">
<slot> {{}} </slot>
<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
+25 -11
View File
@@ -1,6 +1,6 @@
<template>
<div class="miao-directory-item-container" ref="rootDomRef">
<div class="container-top">
<div class="miao-directory-item-container" ref="rootDomRef" >
<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;
}