Rewrote Frontend
This commit is contained in:
@@ -1,52 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { Status } from "naive-ui/es/progress/src/interface";
|
||||
import { defineProps, defineExpose, ref } from "vue";
|
||||
import { isErrorResponse, FS } from "@/api";
|
||||
import { NProgress } from "naive-ui";
|
||||
import filesize from "filesize";
|
||||
import type { Status } from 'naive-ui/es/progress/src/interface';
|
||||
import type { UploadFile } from '@/api';
|
||||
import { ref } from 'vue';
|
||||
import { isErrorResponse, FS } from '@/api';
|
||||
import { NProgress } from 'naive-ui';
|
||||
import filesize from 'filesize';
|
||||
|
||||
const props = defineProps<{
|
||||
file: File;
|
||||
file: UploadFile;
|
||||
}>();
|
||||
|
||||
const progress = ref(0);
|
||||
const percentage = ref(0);
|
||||
const err = ref("");
|
||||
const status = ref<Status>("info");
|
||||
const err = ref('');
|
||||
const status = ref<Status>('info');
|
||||
const shown = ref(true);
|
||||
|
||||
async function startUpload(parent: number, token: string) {
|
||||
const resp = await FS.upload_file(token, parent, props.file, (e) => {
|
||||
progress.value = e.loaded;
|
||||
percentage.value = (e.loaded / e.total) * 100;
|
||||
});
|
||||
percentage.value = 100;
|
||||
if (isErrorResponse(resp)) {
|
||||
err.value = resp.message ?? "Error";
|
||||
status.value = "error";
|
||||
} else status.value = "success";
|
||||
async function startUpload(token: string, done: () => void) {
|
||||
const resp = await FS.upload_file(token, props.file, (e) => {
|
||||
progress.value = e.loaded;
|
||||
percentage.value = (e.loaded / e.total) * 100;
|
||||
if (e.loaded == e.total) done();
|
||||
});
|
||||
percentage.value = 100;
|
||||
if (isErrorResponse(resp)) {
|
||||
err.value = resp.message ?? 'Error';
|
||||
status.value = 'error';
|
||||
} else {
|
||||
status.value = 'success';
|
||||
shown.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
startUpload,
|
||||
startUpload
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="percentage < 100">
|
||||
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
|
||||
{{ Math.floor(percentage * 1000) / 1000 }}%
|
||||
</div>
|
||||
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
|
||||
<div v-else>{{ file.name }} - Completed</div>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="percentage"
|
||||
:height="20"
|
||||
:status="status"
|
||||
border-radius="10px 0"
|
||||
fill-border-radius="10px 0"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
<Transition name="slide-up">
|
||||
<div class="container" v-show="shown">
|
||||
<div v-if="percentage < 100">
|
||||
{{ file.fullName }} -
|
||||
{{
|
||||
filesize(progress, {
|
||||
base: 2,
|
||||
standard: 'jedec'
|
||||
})
|
||||
}}
|
||||
/
|
||||
{{
|
||||
filesize(file.file.size, {
|
||||
base: 2,
|
||||
standard: 'jedec'
|
||||
})
|
||||
}}
|
||||
- {{ Math.floor(percentage * 1000) / 1000 }}%
|
||||
</div>
|
||||
<div v-else-if="err !== ''">
|
||||
{{ file.fullName }} - Error: {{ err }}
|
||||
</div>
|
||||
<div v-else>{{ file.fullName }} - Completed</div>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="percentage"
|
||||
:height="20"
|
||||
:status="status"
|
||||
border-radius="10px 0"
|
||||
fill-border-radius="10px 0"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
height: 60px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.slide-up-leave-active {
|
||||
transition: all 2s ease-out;
|
||||
}
|
||||
|
||||
.slide-up-leave-to {
|
||||
height: 0;
|
||||
padding: 0 8px;
|
||||
opacity: 0;
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
</style>
|
||||
|
||||
203
frontend/src/components/UploadDialog/UploadField.vue
Normal file
203
frontend/src/components/UploadDialog/UploadField.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import type { TokenInjectType, Responses, UploadFile } from '@/api';
|
||||
import { inject, ref } from 'vue';
|
||||
import { useMessage, NModal, NText, NIcon } from 'naive-ui';
|
||||
import { CloudUpload } from '@vicons/carbon';
|
||||
import { FS, check_token, isErrorResponse } from '@/api';
|
||||
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue';
|
||||
import { loadingMsgWrapper } from '@/utils';
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const props = defineProps<{
|
||||
node: Responses.GetNode;
|
||||
}>();
|
||||
|
||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'reloadNode'): void;
|
||||
}>();
|
||||
|
||||
const uploadArea = ref<HTMLDivElement>();
|
||||
const fileInput = ref<HTMLInputElement>();
|
||||
const uploadDialog = ref();
|
||||
const uploadDialogShow = ref(false);
|
||||
|
||||
const files = ref<UploadFile[]>([]);
|
||||
|
||||
function startDrag() {
|
||||
uploadArea.value?.classList.add('uploadActive');
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
uploadArea.value?.classList.remove('uploadActive');
|
||||
}
|
||||
|
||||
function openBrowser() {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
function browserChanged(event: InputEvent) {
|
||||
files.value = Array.from(
|
||||
(event.target as HTMLInputElement).files ?? []
|
||||
).map((file) => {
|
||||
return {
|
||||
parent: props.node.id,
|
||||
fullName: file.name,
|
||||
file
|
||||
};
|
||||
});
|
||||
uploadFiles();
|
||||
}
|
||||
|
||||
interface FileSystemDirectoryReader {
|
||||
readEntries(
|
||||
successCallback: (entries: FileSystemEntry[]) => void,
|
||||
errorCallback?: (err: DOMException) => void
|
||||
): void;
|
||||
}
|
||||
|
||||
interface FileSystemEntry {
|
||||
readonly fullPath: string;
|
||||
readonly isDirectory: boolean;
|
||||
readonly isFile: boolean;
|
||||
readonly name: string;
|
||||
file(
|
||||
successCallback: (file: File) => void,
|
||||
errorCallback?: (err: DOMException) => void
|
||||
): void;
|
||||
createReader(): FileSystemDirectoryReader;
|
||||
}
|
||||
|
||||
const asyncReadEntries = async (
|
||||
reader: FileSystemDirectoryReader
|
||||
): Promise<FileSystemEntry[]> =>
|
||||
new Promise((resolve, reject) => reader.readEntries(resolve, reject));
|
||||
|
||||
const getFile = async (entry: FileSystemEntry): Promise<File> =>
|
||||
new Promise((resolve, reject) => entry.file(resolve, reject));
|
||||
|
||||
async function processDirOrFile(
|
||||
entry: FileSystemEntry,
|
||||
parent: number,
|
||||
token: string
|
||||
) {
|
||||
if (entry.isDirectory) {
|
||||
const resp = await FS.create_folder(token, parent, entry.name);
|
||||
if (isErrorResponse(resp)) return;
|
||||
if ('exists' in resp && resp.isFile) return;
|
||||
const reader = entry.createReader();
|
||||
let entries = [];
|
||||
do {
|
||||
try {
|
||||
entries = await asyncReadEntries(reader);
|
||||
entries.forEach((e) => processDirOrFile(e, resp.id, token));
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
} while (entries.length != 0);
|
||||
} else
|
||||
files.value.push({
|
||||
parent: parent,
|
||||
fullName: entry.fullPath.slice(1),
|
||||
file: await getFile(entry)
|
||||
});
|
||||
}
|
||||
|
||||
const filesDropped = loadingMsgWrapper(message, async (event: DragEvent) => {
|
||||
stopDrag();
|
||||
if (!event.dataTransfer) return;
|
||||
const token = await check_token(jwt);
|
||||
if (!token) return;
|
||||
files.value = [];
|
||||
for (const file of event.dataTransfer.items) {
|
||||
const entry = file.webkitGetAsEntry();
|
||||
if (entry)
|
||||
await processDirOrFile(
|
||||
entry as unknown as FileSystemEntry,
|
||||
props.node.id,
|
||||
token
|
||||
);
|
||||
}
|
||||
uploadFiles();
|
||||
});
|
||||
|
||||
function uploadFiles() {
|
||||
if (files.value.length == 0) return;
|
||||
uploadDialogShow.value = true;
|
||||
}
|
||||
|
||||
async function uploadFilesDialogOpen() {
|
||||
await uploadDialog.value?.startUpload();
|
||||
uploadDialogShow.value = false;
|
||||
if (fileInput.value) fileInput.value.value = '';
|
||||
emit('reloadNode');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="uploadArea"
|
||||
ref="uploadArea"
|
||||
@drop.prevent
|
||||
@dragenter.prevent
|
||||
@dragover.prevent
|
||||
@dragleave.prevent
|
||||
@dragend.prevent
|
||||
@click="openBrowser"
|
||||
@drop="filesDropped"
|
||||
@dragenter="startDrag"
|
||||
@dragover="startDrag"
|
||||
@dragleave="stopDrag"
|
||||
@dragend="stopDrag"
|
||||
>
|
||||
<input type="file" ref="fileInput" multiple @input="browserChanged" />
|
||||
<div>
|
||||
<n-icon size="2em">
|
||||
<CloudUpload />
|
||||
</n-icon>
|
||||
</div>
|
||||
<n-text>
|
||||
Click or drag here to upload files
|
||||
</n-text>
|
||||
</div>
|
||||
<n-modal
|
||||
v-model:show="uploadDialogShow"
|
||||
:close-on-esc="false"
|
||||
:mask-closable="false"
|
||||
:on-after-enter="uploadFilesDialogOpen"
|
||||
>
|
||||
<UploadFileDialog ref="uploadDialog" :files="files" />
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.uploadArea {
|
||||
border: 1px dashed #ddd;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background-color: rgb(250, 250, 252);
|
||||
|
||||
text-align: center;
|
||||
|
||||
transition: border-color 250ms ease-out, background-color 250ms ease-out;
|
||||
|
||||
padding: 20px;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.uploadArea:hover {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
.uploadActive {
|
||||
background-color: rgb(240, 252, 240);
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { TokenInjectType } from "@/api";
|
||||
import { defineProps, defineExpose, ref, inject } from "vue";
|
||||
import { check_token } from "@/api";
|
||||
import UploadEntry from "@/components/UploadDialog/UploadEntry.vue";
|
||||
import { NCard } from "naive-ui";
|
||||
import type { TokenInjectType, UploadFile } from '@/api';
|
||||
import { ref, inject } from 'vue';
|
||||
import { check_token } from '@/api';
|
||||
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue';
|
||||
import { NCard } from 'naive-ui';
|
||||
|
||||
const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
|
||||
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType;
|
||||
|
||||
const entries = ref<typeof UploadEntry[]>([]);
|
||||
const done = ref(false);
|
||||
let canCloseResolve: (value: unknown) => void = () => null;
|
||||
const canClose = new Promise((r) => (canCloseResolve = r));
|
||||
|
||||
async function startUpload(parent: number) {
|
||||
const token = await check_token(jwt);
|
||||
if (!token) return;
|
||||
await Promise.all(
|
||||
entries.value.map((entry) => entry.startUpload(parent, token))
|
||||
);
|
||||
done.value = true;
|
||||
await canClose;
|
||||
async function startUpload() {
|
||||
const token = await check_token(jwt);
|
||||
if (!token) return;
|
||||
const ents: typeof UploadEntry[] = entries.value;
|
||||
const allProms: Promise<void>[] = [];
|
||||
for (const entry of ents) {
|
||||
await new Promise<void>((resolve) =>
|
||||
allProms.push(entry.startUpload(token, resolve))
|
||||
);
|
||||
}
|
||||
await Promise.all(allProms);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
startUpload,
|
||||
startUpload
|
||||
});
|
||||
defineProps<{
|
||||
files: File[];
|
||||
files: UploadFile[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="Upload Files">
|
||||
<div>
|
||||
<UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
|
||||
</div>
|
||||
<div>
|
||||
<button v-if="done" @click="canCloseResolve(null)">Close</button>
|
||||
</div>
|
||||
</n-card>
|
||||
<n-card title="Uploading files" style="margin: 20px">
|
||||
<UploadEntry
|
||||
v-for="f in files"
|
||||
:key="f.file.name"
|
||||
ref="entries"
|
||||
:file="f"
|
||||
/>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user