mirror of
https://github.com/rustfs/console.git
synced 2026-08-29 03:52:28 +08:00
feat: 上传
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/* 实现列表头滚动固定 */
|
||||
.n-list.sticky-header {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.n-list.sticky-header .n-list__header {
|
||||
@apply bg-neutral-900/60 sticky top-0 z-10;
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -7,6 +7,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
@@ -17,6 +18,8 @@ declare module 'vue' {
|
||||
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
@@ -28,12 +31,15 @@ declare module 'vue' {
|
||||
NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
<template #extra>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<object-upload-stats />
|
||||
<n-button>
|
||||
<Icon name="ri:add-line" class="mr-2" />
|
||||
<span>新建目录</span>
|
||||
</n-button>
|
||||
<n-button>
|
||||
<n-button @click="() => uploadPickerVisible = true">
|
||||
<Icon name="ri:file-add-line" class="mr-2" />
|
||||
<span>上传文件/文件夹</span>
|
||||
</n-button>
|
||||
@@ -38,6 +39,7 @@
|
||||
</template>
|
||||
</n-page-header>
|
||||
<n-data-table class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="objects" :pagination="false" :bordered="false" />
|
||||
<object-upload-picker :show="uploadPickerVisible" @update:show="val => (uploadPickerVisible = val)" :bucketName="bucketName" :prefix="prefix" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -48,12 +50,24 @@ import { NuxtLink } from '#components'
|
||||
import { ListObjectsV2Command, type _Object, type CommonPrefix } from '@aws-sdk/client-s3'
|
||||
import { joinRelativeURL } from 'ufo'
|
||||
import { computed, ref, type VNode } from 'vue'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps<{ bucket: string; path: string }>()
|
||||
|
||||
const uploadPickerVisible = ref(false)
|
||||
|
||||
// 上传任务
|
||||
const uploadTaskStore = useUploadTaskManagerStore()
|
||||
const tasks = computed(() => uploadTaskStore.tasks)
|
||||
|
||||
// 当任务变化时,刷新数据
|
||||
watch(() => tasks, () => {
|
||||
setTimeout(refresh, 500);
|
||||
}, { deep: true })
|
||||
|
||||
// bucketName
|
||||
const bucketName = computed(() => props.bucket as string)
|
||||
// 当前路径的前缀, example: '/folder1/folder2/'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<template v-for="(segment) in displaySegments">
|
||||
<span class="text-gray-500">/</span>
|
||||
<button @click="() => handleOnClick(segment)" :class="{ 'text-blue-500 hover:underline': segment.index > -1 }">{{ segment.value }}</button>
|
||||
<button @click="() => handleOnClick(segment)" :class="{ 'text-blue-500 hover:underline': segment.index > -1, 'cursor-default': segment.index === -1 }">{{ segment.value
|
||||
}}</button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<n-modal :show="show" @update:show="val => $emit('update:show', val)" size="huge">
|
||||
<template #header>
|
||||
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||
<span>对象预览: {{ objectKey }}</span>
|
||||
<n-button size="small" ghost @click="closeModal">关闭</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<n-card class="max-w-screen-md">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-4 truncate">
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<n-modal :show="show" @update:show="val => $emit('update:show', val)" size="huge">
|
||||
<n-card class="max-w-screen-md">
|
||||
<template #header>
|
||||
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||
<span>上传文件</span>
|
||||
<n-button size="small" ghost @click="closeModal">关闭</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="file" ref="fileInput" multiple hidden @change="handleFileSelect" />
|
||||
<input type="file" ref="folderInput" webkitdirectory directory hidden @change="handleFolderSelect" />
|
||||
<n-button type="primary" @click="selectFile">选择文件</n-button>
|
||||
<div>或</div>
|
||||
<n-button @click="selectFolder">选择文件夹</n-button>
|
||||
<div>上传至</div>
|
||||
<div class="text-cyan-600">{{ bucketName }}{{ prefix || '/' }}</div>
|
||||
</div>
|
||||
|
||||
<n-alert title="" type="info">
|
||||
若上传路径中存在同名文件,上传将覆盖原有文件。
|
||||
</n-alert>
|
||||
|
||||
<div @dragover.prevent @drop.prevent="handleDrop">
|
||||
<div v-if="selectedItems.length === 0" class="flex flex-col gap-6 items-center justify-center border border-dashed border-muted rounded p-4 h-[40vh]">
|
||||
<div class="text-gray-500 font-bold">未选择文件/文件夹</div>
|
||||
<div class="text-muted-foreground text-center">
|
||||
Chrome 和 FireFox 支持拖拽到此区域上传,支持选择多个文件/文件夹<br>
|
||||
单个文件最大支持 512GB,上传更大的文件请使用命令行工具
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展示文件与文件夹 -->
|
||||
<n-list v-if="selectedItems.length" bordered class="h-[40vh] overflow-y-auto sticky-header relative">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2 font-bold">
|
||||
<div class="flex-1">已选择文件/文件夹</div>
|
||||
<div class="w-32 text-center">大小</div>
|
||||
<div class="w-32 text-center">操作</div>
|
||||
</div>
|
||||
</template>
|
||||
<n-list-item v-for="(item, index) in selectedItems" :key="index">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<div class="flex-1">
|
||||
<span v-if="item.type === 'folder'">{{ item.name }}/</span>
|
||||
<span v-else>{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="w-32 text-center">{{ formatBytes(item.size) }}</div>
|
||||
<div class="w-32 text-center">
|
||||
<n-button text type="info" @click="removeItem(index)">删除</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<n-button type="default" :disabled="!hasFiles">参数配置</n-button>
|
||||
<n-button type="primary" :disabled="!hasFiles" @click="handleUpload">开始上传</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, defineProps, ref } from 'vue'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
|
||||
const uploadTaskManagerStore = useUploadTaskManagerStore()
|
||||
|
||||
interface SelectedItem {
|
||||
type: 'file' | 'folder'
|
||||
name: string
|
||||
size: number
|
||||
files?: File[]
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const props = defineProps<{ show: boolean; bucketName: string; prefix: string }>()
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const folderInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// 待上传的文件/文件夹列表
|
||||
const selectedItems = ref<SelectedItem[]>([])
|
||||
|
||||
const closeModal = () => emit('update:show', false)
|
||||
const selectFile = () => fileInput.value?.click()
|
||||
const selectFolder = () => folderInput.value?.click()
|
||||
|
||||
const handleFileSelect = (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input.files) {
|
||||
// 更新展示列表
|
||||
Array.from(input.files).forEach(f => selectedItems.value.push({ type: 'file', name: f.name, size: f.size, files: [f] }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFolderSelect = (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
|
||||
if (input.files) {
|
||||
const folderFiles = Array.from(input.files)
|
||||
|
||||
// 找到每一个文件的 top-level 文件夹
|
||||
const folderMap = new Map<string, File[]>()
|
||||
for (const file of folderFiles) {
|
||||
const parts = file.webkitRelativePath.split('/')
|
||||
const folderName = parts[0] || '未知文件夹'
|
||||
|
||||
if (!folderMap.has(folderName)) {
|
||||
folderMap.set(folderName, [])
|
||||
}
|
||||
|
||||
folderMap.get(folderName)?.push(file)
|
||||
}
|
||||
|
||||
// 只展示文件夹名称,实际上传依然包含 folderFiles
|
||||
for (const [folderName, files] of folderMap.entries()) {
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0)
|
||||
selectedItems.value.push({ type: 'folder', name: folderName, size: totalSize, files })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
if (e.dataTransfer?.files) {
|
||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
||||
droppedFiles.forEach(file => selectedItems.value.push({ type: 'file', name: file.name, size: file.size, files: [file] }))
|
||||
}
|
||||
}
|
||||
|
||||
const hasFiles = computed(() => selectedItems.value.length > 0)
|
||||
const allFiles = computed(() => selectedItems.value.flatMap(item => item.files || []))
|
||||
|
||||
function removeItem(index: number) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
uploadTaskManagerStore.addFiles(allFiles.value, props.bucketName, props.prefix)
|
||||
selectedItems.value = []
|
||||
closeModal()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<n-button text v-if="total > 0" @click="toggleDrawer">
|
||||
<div v-if="pending.length">
|
||||
<n-spin size="small" />
|
||||
<span>{{ total }} 任务进行中(进行中 {{ uploading.length }} 个,已成功 {{ completed.length }} 个)</span>
|
||||
</div>
|
||||
<div v-else>上传完成(成功 {{ completed.length }} 个,失败 {{ failed.length }} 个)</div>
|
||||
</n-button>
|
||||
|
||||
<n-drawer v-model:show="showDrawer" :width="502">
|
||||
<n-drawer-content title="任务管理" closable>
|
||||
<div class="flex flex-col gap-4">
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
<p><span class="text-red-500">刷新/关闭浏览器</span> 将取消当前所有任务。</p>
|
||||
<p><span class="text-red-500">清空浏览器缓存、session过期</span>等情况会导致任务中断或丢失,请谨慎操作。</p>
|
||||
</n-alert>
|
||||
<div v-if="total > 0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ total - (pending.length + uploading.length) }}/{{ total }}</div>
|
||||
<n-button text type="info" @click="clearTasks">清空记录</n-button>
|
||||
</div>
|
||||
<n-progress type="line" :height="2" :percentage="percentage" :show-indicator="false" :processing="percentage < 100" />
|
||||
</div>
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane :tab="`待处理(${pending.length})`" name="pending">
|
||||
<object-upload-task-list :tasks="pending" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane :tab="`进行中(${uploading.length})`" name="uploading">
|
||||
<object-upload-task-list :tasks="uploading" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane :tab="`已完成(${completed.length})`" name="completed">
|
||||
<object-upload-task-list :tasks="completed" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane :tab="`已失败(${failed.length})`" name="failed">
|
||||
<object-upload-task-list :tasks="failed" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
|
||||
const showDrawer = ref(false)
|
||||
|
||||
const store = useUploadTaskManagerStore()
|
||||
|
||||
const tasks = computed(() => store.tasks)
|
||||
const total = computed(() => tasks.value.length)
|
||||
const percentage = computed(() => total.value === 0 ? 100 : Math.floor((completed.value.length / total.value) * 100))
|
||||
|
||||
const pending = computed(() => tasks.value.filter(task => task.status === 'pending'))
|
||||
const uploading = computed(() => tasks.value.filter(task => task.status === 'uploading'))
|
||||
const completed = computed(() => tasks.value.filter(task => task.status === 'completed'))
|
||||
const failed = computed(() => tasks.value.filter(task => task.status === 'failed'))
|
||||
|
||||
const toggleDrawer = () => {
|
||||
showDrawer.value = !showDrawer.value
|
||||
}
|
||||
|
||||
const clearTasks = () => {
|
||||
store.clearTasks()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ task.file.name }}</div>
|
||||
<n-button text @click="handleDeleteTask" type="info">删除记录</n-button>
|
||||
</div>
|
||||
<n-progress type="line" :height="2" :percentage="task.progress" :show-indicator="false" :processing="task.status == 'uploading'" />
|
||||
<div class="flex items-center justify-between text-muted-foreground">
|
||||
<div>{{ formatBytes(task.file.size) }}</div>
|
||||
<div class="text-muted-foreground">
|
||||
<span v-if="task.status === 'pending'">等待上传</span>
|
||||
<span v-else-if="task.status === 'uploading'">上传中</span>
|
||||
<span v-else-if="task.status === 'completed'">上传成功</span>
|
||||
<span v-else-if="task.status === 'failed'">上传失败</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UploadTask } from '~/lib/upload-task-manager'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
|
||||
const store = useUploadTaskManagerStore()
|
||||
|
||||
const props = defineProps<{ task: UploadTask }>()
|
||||
|
||||
const handleDeleteTask = () => {
|
||||
store.removeTask(props.task.id)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-if="tasks.length <= 0" class="mx-auto text-muted-foreground text-center">暂无任务</div>
|
||||
<object-upload-task-item v-for="task in tasks" :key="task.id" :task="task" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UploadTask } from '~/lib/upload-task-manager'
|
||||
defineProps<{ tasks: UploadTask[] }>()
|
||||
</script>
|
||||
@@ -0,0 +1,168 @@
|
||||
import { CompleteMultipartUploadCommand, CreateMultipartUploadCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
export interface UploadTask {
|
||||
id: string;
|
||||
file: File;
|
||||
bucketName: string;
|
||||
prefix?: string;
|
||||
status: "pending" | "uploading" | "completed" | "failed" | "canceled";
|
||||
progress: number;
|
||||
error?: string;
|
||||
abortController?: AbortController;
|
||||
}
|
||||
|
||||
export interface TaskManagerConfig {
|
||||
chunkSize?: number; // in MB
|
||||
maxConcurrentUploads?: number;
|
||||
}
|
||||
|
||||
class UploadTaskManager {
|
||||
private tasks = reactive<UploadTask[]>([]);
|
||||
private maxConcurrentUploads: number;
|
||||
private chunkSize: number;
|
||||
private s3Client: S3Client;
|
||||
private activeUploads: number = 0;
|
||||
|
||||
constructor(s3Client: S3Client, config: TaskManagerConfig) {
|
||||
this.s3Client = s3Client;
|
||||
this.chunkSize = (config.chunkSize ?? 5) * 1024 * 1024; // Default 5MB
|
||||
this.maxConcurrentUploads = config.maxConcurrentUploads ?? 6;
|
||||
}
|
||||
|
||||
addFiles(files: File[], bucketName: string, prefix?: string) {
|
||||
files.forEach((file) => {
|
||||
const task: UploadTask = {
|
||||
id: `${Date.now()}-${file.name}`,
|
||||
file,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
bucketName,
|
||||
prefix: prefix ?? "",
|
||||
};
|
||||
this.tasks.push(task);
|
||||
});
|
||||
|
||||
console.debug("Added files to upload queue", files);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.tasks
|
||||
.filter((task) => task.status === "pending")
|
||||
.forEach((task) => {
|
||||
task.status = "canceled";
|
||||
});
|
||||
}
|
||||
|
||||
cancelTask(taskId: string) {
|
||||
const task = this.tasks.find((task) => task.id === taskId);
|
||||
if (task && task.abortController) {
|
||||
task.abortController.abort();
|
||||
task.status = "canceled";
|
||||
}
|
||||
}
|
||||
|
||||
removeTask(taskId: string) {
|
||||
const taskIndex = this.tasks.findIndex((task) => task.id === taskId);
|
||||
if (taskIndex !== -1) {
|
||||
this.tasks.splice(taskIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
getTasks() {
|
||||
return this.tasks;
|
||||
}
|
||||
|
||||
clearTasks() {
|
||||
this.tasks = [];
|
||||
}
|
||||
|
||||
private async processQueue() {
|
||||
while (this.activeUploads < this.maxConcurrentUploads) {
|
||||
const nextTask = this.tasks.find((task) => task.status === "pending");
|
||||
if (!nextTask) { break; }
|
||||
|
||||
this.activeUploads++;
|
||||
nextTask.status = "uploading";
|
||||
|
||||
try {
|
||||
if (nextTask.file.size > this.chunkSize) {
|
||||
await this.multipartUpload(nextTask);
|
||||
} else {
|
||||
await this.putObject(nextTask);
|
||||
}
|
||||
nextTask.status = "completed";
|
||||
} catch (error: any) {
|
||||
nextTask.status = "failed";
|
||||
nextTask.error = error.message;
|
||||
} finally {
|
||||
this.activeUploads--;
|
||||
this.processQueue(); // Continue with the next task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async putObject(task: UploadTask) {
|
||||
const { file, bucketName, prefix } = task;
|
||||
const abortController = new AbortController();
|
||||
task.abortController = abortController;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: prefix + task.file.name,
|
||||
Body: file,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command, { abortSignal: abortController.signal });
|
||||
task.progress = 100;
|
||||
}
|
||||
|
||||
private async multipartUpload(task: UploadTask) {
|
||||
const { file, bucketName, prefix } = task;
|
||||
const abortController = new AbortController();
|
||||
task.abortController = abortController;
|
||||
|
||||
const Key = prefix + task.file.name;
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key
|
||||
});
|
||||
const { UploadId } = await this.s3Client.send(createCommand);
|
||||
|
||||
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
||||
let completedParts = [];
|
||||
|
||||
for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
|
||||
const start = (partNumber - 1) * this.chunkSize;
|
||||
const end = Math.min(start + this.chunkSize, file.size);
|
||||
|
||||
const chunk = file.slice(start, end);
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
UploadId,
|
||||
PartNumber: partNumber,
|
||||
Body: chunk,
|
||||
});
|
||||
|
||||
const { ETag } = await this.s3Client.send(uploadPartCommand, { abortSignal: abortController.signal });
|
||||
completedParts.push({ ETag, PartNumber: partNumber });
|
||||
|
||||
task.progress = Math.round((partNumber / totalChunks) * 100);
|
||||
}
|
||||
|
||||
const completeCommand = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
UploadId,
|
||||
MultipartUpload: { Parts: completedParts },
|
||||
});
|
||||
|
||||
await this.s3Client.send(completeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
export default UploadTaskManager;
|
||||
+4
-1
@@ -17,7 +17,10 @@ export default defineNuxtConfig({
|
||||
'nuxtjs-naive-ui',
|
||||
'@vueuse/nuxt'
|
||||
],
|
||||
plugins: ['~/plugins/api.ts', '~/plugins/s3.ts'],
|
||||
plugins: [
|
||||
'~/plugins/api.ts',
|
||||
'~/plugins/s3.ts',
|
||||
],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
api: {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import UploadTaskManager from '~/lib/upload-task-manager'
|
||||
|
||||
export const useUploadTaskManagerStore = defineStore('uploadTaskManager', {
|
||||
state: () => ({
|
||||
taskManager: new UploadTaskManager(useNuxtApp().$s3Client, {
|
||||
chunkSize: 5,
|
||||
maxConcurrentUploads: 6
|
||||
})
|
||||
}),
|
||||
getters: {
|
||||
tasks: (state) => state.taskManager.getTasks()
|
||||
},
|
||||
actions: {
|
||||
addFiles(files: File[], bucketName: string, prefix?: string) {
|
||||
this.taskManager.addFiles(files, bucketName, prefix)
|
||||
this.taskManager.start()
|
||||
},
|
||||
getTasks() {
|
||||
return this.taskManager.getTasks()
|
||||
},
|
||||
|
||||
removeTask(taskId: string) {
|
||||
this.taskManager.removeTask(taskId)
|
||||
},
|
||||
|
||||
clearTasks() {
|
||||
this.taskManager.clearTasks()
|
||||
}
|
||||
}
|
||||
})
|
||||
+35
-1
@@ -12,7 +12,41 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: 'rgba(var(--n-primary-color))',
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
|
||||
// overwrite the naive-ui primary color
|
||||
'primary-1': 'rgba(var(--n-primary-color-1))',
|
||||
'primary-2': 'rgba(var(--n-primary-color-2))',
|
||||
'primary-3': 'rgba(var(--n-primary-color-3))',
|
||||
|
||||
Reference in New Issue
Block a user