mirror of
https://github.com/rustfs/console.git
synced 2026-09-19 01:47:44 +08:00
feat: 创建存储桶 & 删除对象 & 修复上传文件夹时路径拼接错误
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<n-modal :show="show" @update:show="val => $emit('update:show', val)" size="huge">
|
||||
<n-card class="max-w-screen-sm">
|
||||
<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">
|
||||
<div>新建存储桶</div>
|
||||
<div class="text-cyan-600">{{ bucketName }}{{ prefix || '/' }}</div>
|
||||
</div> -->
|
||||
|
||||
<!-- <n-alert title="" type="info">
|
||||
若上传路径中存在同名文件,上传将覆盖原有文件。
|
||||
</n-alert> -->
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<n-input :placeholder="`请输入名称`" v-model:value="objectKey" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<n-button type="primary" :disabled="objectKey.trim().length < 1" @click="handlePutObject">创建</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, defineProps } from 'vue'
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const props = defineProps<{ show: boolean; }>()
|
||||
|
||||
const closeModal = () => emit('update:show', false)
|
||||
|
||||
const objectKey = ref('')
|
||||
|
||||
const { createBucket } = useBucket({})
|
||||
|
||||
const $message = useMessage()
|
||||
|
||||
const handlePutObject = () => {
|
||||
createBucket(objectKey.value).then(() => {
|
||||
emit('update:show', false)
|
||||
objectKey.value = ''
|
||||
$message.success('创建成功')
|
||||
}).catch((e) => {
|
||||
$message.error(e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -101,7 +101,7 @@ const columns = [
|
||||
{
|
||||
key: 'Key', title: '对象', render: (row: { Key: string, type: 'prefix' | 'object' }) => {
|
||||
const displayKey = prefix.value ? row.Key.substring(prefix.value.length) : row.Key
|
||||
let label: string | VNode = displayKey || '根目录'
|
||||
let label: string | VNode = displayKey || '/'
|
||||
|
||||
if (row.type === 'prefix') {
|
||||
label = h('span', { class: 'inline-flex items-center gap-2' }, [icon('ri:folder-line'), label])
|
||||
@@ -109,7 +109,7 @@ const columns = [
|
||||
|
||||
const keyInUri = row.Key
|
||||
|
||||
return h(NuxtLink, { href: bucketPath(keyInUri) }, label)
|
||||
return h(NuxtLink, { href: bucketPath(keyInUri), class: 'block text-cyan-400' }, label)
|
||||
}
|
||||
},
|
||||
{ key: 'Size', title: '大小', render: (row: { Size: number }) => row.Size ? formatBytes(row.Size) : '' },
|
||||
|
||||
@@ -65,16 +65,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { prefix } from 'naive-ui/es/_utils/cssr'
|
||||
import { computed, defineEmits, defineProps, ref } from 'vue'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
|
||||
const uploadTaskManagerStore = useUploadTaskManagerStore()
|
||||
|
||||
interface FileItem {
|
||||
file: File,
|
||||
prefix: string
|
||||
}
|
||||
|
||||
interface SelectedItem {
|
||||
type: 'file' | 'folder'
|
||||
name: string
|
||||
size: number
|
||||
files?: File[]
|
||||
files?: FileItem[]
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
@@ -95,7 +101,9 @@ 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] }))
|
||||
Array.from(input.files).forEach(f => selectedItems.value.push(
|
||||
{ type: 'file', name: f.name, size: f.size, files: [{ file: f, prefix: props.prefix }] }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,24 +112,29 @@ const handleFolderSelect = (e: Event) => {
|
||||
|
||||
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] || '未知文件夹'
|
||||
// 保留完整目录路径(去掉最后的文件名)
|
||||
const pathParts = file.webkitRelativePath.split('/')
|
||||
pathParts.pop() // 移除文件名
|
||||
const folderPath = pathParts.join('/') || '未知文件夹'
|
||||
|
||||
if (!folderMap.has(folderName)) {
|
||||
folderMap.set(folderName, [])
|
||||
if (!folderMap.has(folderPath)) {
|
||||
folderMap.set(folderPath, [])
|
||||
}
|
||||
|
||||
folderMap.get(folderName)?.push(file)
|
||||
folderMap.get(folderPath)?.push(file)
|
||||
}
|
||||
|
||||
// 只展示文件夹名称,实际上传依然包含 folderFiles
|
||||
for (const [folderName, files] of folderMap.entries()) {
|
||||
// 将每个完整路径和对应文件放到 selectedItems
|
||||
for (const [folderPath, files] of folderMap.entries()) {
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0)
|
||||
selectedItems.value.push({ type: 'folder', name: folderName, size: totalSize, files })
|
||||
selectedItems.value.push({
|
||||
type: 'folder',
|
||||
name: folderPath,
|
||||
size: totalSize,
|
||||
files: files.map(file => ({ file, prefix: `${props.prefix}${folderPath}/` }))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +142,9 @@ const handleFolderSelect = (e: Event) => {
|
||||
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] }))
|
||||
droppedFiles.forEach(file => selectedItems.value.push({
|
||||
type: 'file', name: file.name, size: file.size, files: [{ file, prefix: props.prefix }]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +156,9 @@ function removeItem(index: number) {
|
||||
}
|
||||
|
||||
function handleUpload() {
|
||||
uploadTaskManagerStore.addFiles(allFiles.value, props.bucketName, props.prefix)
|
||||
allFiles.value.forEach(fileItem => {
|
||||
uploadTaskManagerStore.addFiles([fileItem.file], props.bucketName, fileItem.prefix)
|
||||
})
|
||||
selectedItems.value = []
|
||||
closeModal()
|
||||
}
|
||||
|
||||
@@ -9,18 +9,32 @@
|
||||
<Icon name="ri:download-line" class="mr-2" />
|
||||
<span>下载</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="() => copySignedUrl()">
|
||||
<Icon name="ri:file-copy-line" class="mr-2" />
|
||||
<span>复制临时链接</span>
|
||||
</n-button>
|
||||
|
||||
<n-button @click="() => showPreview = true">
|
||||
<Icon name="ri:eye-line" class="mr-2" />
|
||||
<span>预览</span>
|
||||
</n-button>
|
||||
|
||||
<n-button>
|
||||
<Icon name="ri:price-tag-3-line" class="mr-2" />
|
||||
<span>标签</span>
|
||||
</n-button>
|
||||
|
||||
<n-popconfirm @positive-click="deleteObject">
|
||||
<template #trigger>
|
||||
<n-button ghost type="error">
|
||||
<Icon name="ri:delete-bin-7-line" class="mr-2" />
|
||||
<span>删除</span>
|
||||
</n-button>
|
||||
</template>
|
||||
删除对象 <span class="select-all">{{ key }}</span> ?
|
||||
</n-popconfirm>
|
||||
|
||||
<n-button @click="() => refresh()">
|
||||
<Icon name="ri:refresh-line" class="mr-2" />
|
||||
<span>刷新</span>
|
||||
@@ -82,6 +96,11 @@ const copySignedUrl = async () => {
|
||||
msg.destroy();
|
||||
message.success('链接已复制到剪贴板');
|
||||
}
|
||||
const deleteObject = async () => {
|
||||
await objectApi.deleteObject(key.value);
|
||||
message.success('删除成功');
|
||||
router.back();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user