16 changed files with 1020 additions and 2 deletions
@ -0,0 +1,15 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
|
import { ObjectPage } from '@abp/oss'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'Vben5OssObjects', |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page> |
||||
|
<ObjectPage /> |
||||
|
</Page> |
||||
|
</template> |
||||
@ -1 +1,2 @@ |
|||||
export { useContainesApi } from './useContainesApi'; |
export { useContainesApi } from './useContainesApi'; |
||||
|
export { useObjectsApi } from './useObjectsApi'; |
||||
|
|||||
@ -0,0 +1,50 @@ |
|||||
|
import type { |
||||
|
CreateOssObjectInput, |
||||
|
GetOssObjectInput, |
||||
|
OssObjectDto, |
||||
|
} from '../types/objects'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useObjectsApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function createApi(input: CreateOssObjectInput): Promise<OssObjectDto> { |
||||
|
const formData = new window.FormData(); |
||||
|
formData.append('bucket', input.bucket); |
||||
|
formData.append('fileName', input.fileName); |
||||
|
formData.append('overwrite', String(input.overwrite)); |
||||
|
input.expirationTime && |
||||
|
formData.append('expirationTime', input.expirationTime.toString()); |
||||
|
input.path && formData.append('path', input.path); |
||||
|
input.file && formData.append('file', input.file); |
||||
|
return request<OssObjectDto>(`/api/oss-management/objects`, { |
||||
|
data: formData, |
||||
|
headers: { |
||||
|
'Content-Type': 'multipart/form-data;charset=utf-8', |
||||
|
}, |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function generateUrlApi(input: GetOssObjectInput): Promise<string> { |
||||
|
return request<string>('/api/oss-management/objects/generate-url', { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function deleteApi(input: GetOssObjectInput): Promise<void> { |
||||
|
return request('/api/oss-management/objects', { |
||||
|
method: 'DELETE', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
createApi, |
||||
|
deleteApi, |
||||
|
generateUrlApi, |
||||
|
}; |
||||
|
} |
||||
@ -1 +1,2 @@ |
|||||
export { default as ContainerTable } from './containers/ContainerTable.vue'; |
export { default as ContainerTable } from './containers/ContainerTable.vue'; |
||||
|
export { default as ObjectPage } from './objects/ObjectPage.vue'; |
||||
|
|||||
@ -0,0 +1,282 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridListeners, VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { OssContainerDto } from '../../types/containes'; |
||||
|
import type { OssObjectDto } from '../../types/objects'; |
||||
|
|
||||
|
import { defineAsyncComponent, h, nextTick, ref, watch } from 'vue'; |
||||
|
|
||||
|
import { useAccess } from '@vben/access'; |
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { formatToDateTime, isNullOrWhiteSpace } from '@abp/core'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { |
||||
|
DeleteOutlined, |
||||
|
DownloadOutlined, |
||||
|
UploadOutlined, |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { Button, message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useObjectsApi } from '../../api'; |
||||
|
import { useContainesApi } from '../../api/useContainesApi'; |
||||
|
import { OssObjectPermissions } from '../../constants/permissions'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'FileList', |
||||
|
}); |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
bucket: string; |
||||
|
path: string; |
||||
|
}>(); |
||||
|
|
||||
|
const kbUnit = 1 * 1024; |
||||
|
const mbUnit = kbUnit * 1024; |
||||
|
const gbUnit = mbUnit * 1024; |
||||
|
|
||||
|
const { hasAccessByCodes } = useAccess(); |
||||
|
const { cancel, getObjectsApi } = useContainesApi(); |
||||
|
const { deleteApi, generateUrlApi } = useObjectsApi(); |
||||
|
|
||||
|
const selectedKeys = ref<string[]>([]); |
||||
|
|
||||
|
const gridOptions: VxeGridProps<OssContainerDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'name', |
||||
|
minWidth: 150, |
||||
|
title: $t('AbpOssManagement.DisplayName:Name'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'isFolder', |
||||
|
formatter: ({ cellValue }) => { |
||||
|
return cellValue |
||||
|
? $t('AbpOssManagement.DisplayName:Folder') |
||||
|
: $t('AbpOssManagement.DisplayName:Standard'); |
||||
|
}, |
||||
|
minWidth: 150, |
||||
|
title: $t('AbpOssManagement.DisplayName:FileType'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'size', |
||||
|
formatter: ({ cellValue }) => { |
||||
|
const size = Number(cellValue); |
||||
|
if (size > gbUnit) { |
||||
|
let gb = Math.round(size / gbUnit); |
||||
|
if (gb < 1) { |
||||
|
gb = 1; |
||||
|
} |
||||
|
return `${gb} GB`; |
||||
|
} |
||||
|
if (size > mbUnit) { |
||||
|
let mb = Math.round(size / mbUnit); |
||||
|
if (mb < 1) { |
||||
|
mb = 1; |
||||
|
} |
||||
|
return `${mb} MB`; |
||||
|
} |
||||
|
let kb = Math.round(size / kbUnit); |
||||
|
if (kb < 1) { |
||||
|
kb = 1; |
||||
|
} |
||||
|
return `${kb} KB`; |
||||
|
}, |
||||
|
minWidth: 150, |
||||
|
title: $t('AbpOssManagement.DisplayName:Size'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'creationDate', |
||||
|
formatter({ cellValue }) { |
||||
|
return cellValue ? formatToDateTime(cellValue) : ''; |
||||
|
}, |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpOssManagement.DisplayName:CreationDate'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'lastModifiedDate', |
||||
|
formatter({ cellValue }) { |
||||
|
return cellValue ? formatToDateTime(cellValue) : ''; |
||||
|
}, |
||||
|
minWidth: 120, |
||||
|
title: $t('AbpOssManagement.DisplayName:LastModifiedDate'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 200, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async ({ page }) => { |
||||
|
const res = await getObjectsApi({ |
||||
|
bucket: props.bucket, |
||||
|
maxResultCount: page.pageSize, |
||||
|
prefix: props.path, |
||||
|
skipCount: (page.currentPage - 1) * page.pageSize, |
||||
|
}); |
||||
|
return { |
||||
|
totalCount: res.maxKeys, |
||||
|
items: res.objects, |
||||
|
}; |
||||
|
}, |
||||
|
}, |
||||
|
autoLoad: false, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
custom: true, |
||||
|
export: false, |
||||
|
refresh: false, |
||||
|
zoom: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const gridEvents: VxeGridListeners<OssContainerDto> = { |
||||
|
checkboxAll: (params) => { |
||||
|
selectedKeys.value = params.records.map((record) => record.name); |
||||
|
}, |
||||
|
checkboxChange: (params) => { |
||||
|
selectedKeys.value = params.records.map((record) => record.name); |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ |
||||
|
gridEvents, |
||||
|
gridOptions, |
||||
|
}); |
||||
|
|
||||
|
const [FileUploadModal, modalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent( |
||||
|
() => import('./FileUploadModal.vue'), |
||||
|
), |
||||
|
}); |
||||
|
|
||||
|
function onUpload() { |
||||
|
modalApi.setData({ |
||||
|
bucket: props.bucket, |
||||
|
path: props.path, |
||||
|
}); |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
|
||||
|
function onDelete(row: OssObjectDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name]), |
||||
|
onCancel: () => { |
||||
|
cancel(); |
||||
|
}, |
||||
|
onOk: async () => { |
||||
|
await deleteApi({ |
||||
|
bucket: props.bucket, |
||||
|
mD5: false, |
||||
|
object: row.name, |
||||
|
path: row.path, |
||||
|
}); |
||||
|
message.success($t('AbpUi.DeletedSuccessfully')); |
||||
|
await gridApi.query(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function onDownload(row: OssObjectDto) { |
||||
|
const downloadUrl = await generateUrlApi({ |
||||
|
bucket: props.bucket, |
||||
|
mD5: false, |
||||
|
object: row.name, |
||||
|
path: row.path, |
||||
|
}); |
||||
|
const link = document.createElement('a'); |
||||
|
link.style.display = 'none'; |
||||
|
link.href = downloadUrl; |
||||
|
link.setAttribute('download', row.name); |
||||
|
document.body.append(link); |
||||
|
link.click(); |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
() => props.bucket, |
||||
|
(bucket) => { |
||||
|
nextTick(() => { |
||||
|
gridApi.setGridOptions({ |
||||
|
toolbarConfig: { |
||||
|
refresh: !isNullOrWhiteSpace(bucket), |
||||
|
}, |
||||
|
}); |
||||
|
if (!isNullOrWhiteSpace(bucket)) { |
||||
|
gridApi.query(); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
watch( |
||||
|
() => props.path, |
||||
|
(newVal, oldVal) => { |
||||
|
if (newVal !== oldVal) { |
||||
|
nextTick(() => { |
||||
|
gridApi.query(); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Grid :table-title="$t('AbpOssManagement.FileList')"> |
||||
|
<template #toolbar-tools> |
||||
|
<Button |
||||
|
v-if="props.path" |
||||
|
:icon="h(UploadOutlined)" |
||||
|
type="primary" |
||||
|
@click="onUpload" |
||||
|
> |
||||
|
{{ $t('AbpOssManagement.Objects:UploadFile') }} |
||||
|
</Button> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
v-if=" |
||||
|
!row.isFolder && hasAccessByCodes([OssObjectPermissions.Download]) |
||||
|
" |
||||
|
:icon="h(DownloadOutlined)" |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onDownload(row)" |
||||
|
> |
||||
|
{{ $t('AbpOssManagement.Objects:Download') }} |
||||
|
</Button> |
||||
|
<Button |
||||
|
v-if="hasAccessByCodes([OssObjectPermissions.Delete])" |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
danger |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onDelete(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Delete') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
<FileUploadModal @file-uploaded="() => gridApi.query()" /> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,286 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeComponentStyleType } from 'vxe-table'; |
||||
|
|
||||
|
import { h, ref, useTemplateRef, watch } from 'vue'; |
||||
|
import uploader from 'vue-simple-uploader'; |
||||
|
import 'vue-simple-uploader/dist/style.css'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { useRefresh } from '@vben/hooks'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
import { useAccessStore } from '@vben/stores'; |
||||
|
|
||||
|
import { isNullOrWhiteSpace } from '@abp/core'; |
||||
|
import { |
||||
|
CaretRightOutlined, |
||||
|
DeleteOutlined, |
||||
|
PauseOutlined, |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { Button, Tag, Tooltip } from 'ant-design-vue'; |
||||
|
import { VxeColumn, VxeTable } from 'vxe-table'; |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'fileUploaded', file: any): void; |
||||
|
}>(); |
||||
|
const Uploader = uploader.Uploader; |
||||
|
const UploaderDrop = uploader.UploaderDrop; |
||||
|
const UploaderList = uploader.UploaderList; |
||||
|
const UploaderUnsupport = uploader.UploaderUnsupport; |
||||
|
|
||||
|
interface ModalState { |
||||
|
bucket: string; |
||||
|
path: string; |
||||
|
} |
||||
|
|
||||
|
const selectBtn = useTemplateRef<any>('selectBtn'); |
||||
|
const uploaderWrap = useTemplateRef<any>('uploaderWrap'); |
||||
|
|
||||
|
const { refresh } = useRefresh(); |
||||
|
const accessStore = useAccessStore(); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
class: 'w-1/2', |
||||
|
closeOnClickModal: false, |
||||
|
closeOnPressEscape: false, |
||||
|
draggable: true, |
||||
|
footer: false, |
||||
|
onCancel: () => { |
||||
|
uploaderWrap.value?.uploader.cancel(); |
||||
|
}, |
||||
|
onOpenChange: (isOpen) => { |
||||
|
if (isOpen) { |
||||
|
onInit(); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const options = ref({ |
||||
|
chunkRetryInterval: null, |
||||
|
headers: {}, |
||||
|
initialPaused: true, |
||||
|
maxChunkRetries: 3, |
||||
|
permanentErrors: [400, 401, 403, 404, 415, 500, 501], |
||||
|
processParams: (params: any) => params, |
||||
|
processResponse: (response: any, cb: any) => { |
||||
|
if (!isNullOrWhiteSpace(response)) { |
||||
|
const error = JSON.parse(response); |
||||
|
if (error.code !== '0') { |
||||
|
cb(true, error.message); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
cb(null, response); |
||||
|
}, |
||||
|
successStatuses: [200, 201, 202, 204, 205], |
||||
|
target: '/api/oss-management/objects/upload', |
||||
|
testChunks: false, |
||||
|
}); |
||||
|
|
||||
|
function onInit() { |
||||
|
const state = modalApi.getData<ModalState>(); |
||||
|
options.value = { |
||||
|
...options.value, |
||||
|
headers: { |
||||
|
Authorization: accessStore.accessToken, |
||||
|
}, |
||||
|
processParams: (params: any) => { |
||||
|
params.bucket = state.bucket; |
||||
|
params.path = state.path; |
||||
|
return params; |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function onSelectFiles() { |
||||
|
selectBtn.value?.click(); |
||||
|
} |
||||
|
|
||||
|
function onResume(file: any) { |
||||
|
if (file.error) { |
||||
|
file.errorMsg = ''; |
||||
|
file.retry(); |
||||
|
} else { |
||||
|
file.resume(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onPause(file: any) { |
||||
|
file.pause(); |
||||
|
} |
||||
|
|
||||
|
function onDelete(file: any) { |
||||
|
file.cancel(); |
||||
|
} |
||||
|
|
||||
|
function onFileSubmitted(_: any, files: any[]) { |
||||
|
files.forEach((f) => { |
||||
|
f.paused = true; |
||||
|
f.completed = false; |
||||
|
f.progress = 0; |
||||
|
f.progressText = '0 %'; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onUploadProgress(_: any, file: any) { |
||||
|
if (file._prevUploadedSize) { |
||||
|
const progress = Math.floor((file._prevUploadedSize / file.size) * 100); |
||||
|
file.progress = progress; |
||||
|
file.progressText = `${progress} %`; |
||||
|
file.completed = progress === 100; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onUploadError(_rootFile: any, file: any, message: any, chunk: any) { |
||||
|
if (chunk?.xhr?.status === 401) { |
||||
|
// 401 错误代码刷新页面, 由axios拦截器刷新token |
||||
|
refresh(); |
||||
|
} else { |
||||
|
file.errorMsg = message; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onUploadSuccess(_rootFile: any, file: any) { |
||||
|
emits('fileUploaded', file); |
||||
|
} |
||||
|
|
||||
|
function formatSize(size: number) { |
||||
|
if (size < 1024) { |
||||
|
return `${size.toFixed(0)} bytes`; |
||||
|
} else if (size < 1024 * 1024) { |
||||
|
return `${(size / 1024).toFixed(0)} KB`; |
||||
|
} else if (size < 1024 * 1024 * 1024) { |
||||
|
return `${(size / 1024 / 1024).toFixed(1)} MB`; |
||||
|
} else { |
||||
|
return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function rowStyle(params: any): VxeComponentStyleType { |
||||
|
// console.log('上传状态 --->', params.row.status); |
||||
|
if (params.row.error) { |
||||
|
return { |
||||
|
background: '#ffe0e0', |
||||
|
}; |
||||
|
} |
||||
|
const startColor = 'rgba(255, 200, 200, 0.7)'; |
||||
|
const endColor = 'rgba(200, 255, 200, 0.7)'; |
||||
|
return { |
||||
|
background: `linear-gradient( |
||||
|
to right, |
||||
|
${startColor} 0%, |
||||
|
${endColor} ${params.row.progress}%, |
||||
|
transparent ${params.row.progress}%, |
||||
|
transparent 100% |
||||
|
)`, |
||||
|
backgroundSize: '100% 100%', |
||||
|
transition: 'background 0.5s ease', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
() => [selectBtn.value, uploaderWrap.value], |
||||
|
([button, wrap]) => { |
||||
|
if (button && wrap) { |
||||
|
wrap.uploader.assignBrowse(button); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('AbpOssManagement.Objects:UploadFile')"> |
||||
|
<Uploader |
||||
|
:options="options" |
||||
|
ref="uploaderWrap" |
||||
|
@file-error="onUploadError" |
||||
|
@file-progress="onUploadProgress" |
||||
|
@file-success="onUploadSuccess" |
||||
|
@files-submitted="onFileSubmitted" |
||||
|
> |
||||
|
<UploaderUnsupport /> |
||||
|
<UploaderDrop> |
||||
|
<div class="flex flex-row gap-2"> |
||||
|
<input ref="selectBtn" style="display: none" /> |
||||
|
<Button type="primary" @click="onSelectFiles"> |
||||
|
{{ $t('AbpOssManagement.Upload:SelectFile') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</UploaderDrop> |
||||
|
<UploaderList> |
||||
|
<template #default="{ fileList }"> |
||||
|
<VxeTable :data="fileList" :row-style="rowStyle"> |
||||
|
<VxeColumn type="seq" width="70" /> |
||||
|
<VxeColumn |
||||
|
field="name" |
||||
|
:title="$t('AbpOssManagement.DisplayName:Name')" |
||||
|
/> |
||||
|
<VxeColumn |
||||
|
field="size" |
||||
|
:title="$t('AbpOssManagement.DisplayName:Size')" |
||||
|
width="100" |
||||
|
> |
||||
|
<template #default="{ row }"> |
||||
|
<span>{{ formatSize(row.size) }}</span> |
||||
|
</template> |
||||
|
</VxeColumn> |
||||
|
<VxeColumn |
||||
|
field="status" |
||||
|
:title="$t('AbpOssManagement.DisplayName:Status')" |
||||
|
width="180" |
||||
|
> |
||||
|
<template #default="{ row }"> |
||||
|
<Tag v-if="row.completed" color="green"> |
||||
|
{{ $t('AbpOssManagement.Upload:Completed') }} |
||||
|
</Tag> |
||||
|
<Tooltip v-else-if="row.error" :title="row.errorMsg"> |
||||
|
<Tag color="red"> |
||||
|
{{ $t('AbpOssManagement.Upload:Error') }} |
||||
|
</Tag> |
||||
|
</Tooltip> |
||||
|
<Tag v-else-if="row.paused" color="orange"> |
||||
|
{{ $t('AbpOssManagement.Upload:Pause') }} |
||||
|
</Tag> |
||||
|
<span v-else>{{ |
||||
|
`${row.progressText} ${formatSize(row.averageSpeed)}/s` |
||||
|
}}</span> |
||||
|
</template> |
||||
|
</VxeColumn> |
||||
|
<VxeColumn |
||||
|
fixed="right" |
||||
|
field="action" |
||||
|
:title="$t('AbpUi.Actions')" |
||||
|
width="100" |
||||
|
> |
||||
|
<template #default="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<div v-if="!row.completed"> |
||||
|
<Button |
||||
|
v-if="row.paused || row.error" |
||||
|
:icon="h(CaretRightOutlined)" |
||||
|
@click="onResume(row)" |
||||
|
type="link" |
||||
|
/> |
||||
|
<Button |
||||
|
v-else |
||||
|
:icon="h(PauseOutlined)" |
||||
|
@click="onPause(row)" |
||||
|
type="link" |
||||
|
/> |
||||
|
</div> |
||||
|
<Button |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
@click="onDelete(row)" |
||||
|
type="link" |
||||
|
danger |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
</VxeColumn> |
||||
|
</VxeTable> |
||||
|
</template> |
||||
|
</UploaderList> |
||||
|
</Uploader> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,66 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { OssObjectDto } from '../../types/objects'; |
||||
|
|
||||
|
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { message } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useObjectsApi } from '../../api'; |
||||
|
|
||||
|
interface ModalState { |
||||
|
bucket: string; |
||||
|
path?: string; |
||||
|
} |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'change', data: OssObjectDto): void; |
||||
|
}>(); |
||||
|
|
||||
|
const { createApi } = useObjectsApi(); |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'name', |
||||
|
label: $t('AbpOssManagement.DisplayName:Name'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
], |
||||
|
showDefaultActions: false, |
||||
|
}); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
onConfirm: async () => { |
||||
|
await formApi.validateAndSubmitForm(); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
async function onSubmit(values: Record<string, any>) { |
||||
|
try { |
||||
|
const state = modalApi.getData<ModalState>(); |
||||
|
modalApi.setState({ submitting: true }); |
||||
|
const dto = await createApi({ |
||||
|
bucket: state.bucket, |
||||
|
fileName: values.name, |
||||
|
overwrite: false, |
||||
|
path: state.path, |
||||
|
}); |
||||
|
message.success($t('AbpUi.SavedSuccessfully')); |
||||
|
emits('change', dto); |
||||
|
modalApi.close(); |
||||
|
} finally { |
||||
|
modalApi.setState({ submitting: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('AbpOssManagement.Objects:CreateFolder')"> |
||||
|
<Form /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,192 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { EventDataNode, Key } from 'ant-design-vue/es/vc-tree/interface'; |
||||
|
import type { TreeProps } from 'ant-design-vue/es/vc-tree/props'; |
||||
|
|
||||
|
import type { OssContainerDto } from '../../types'; |
||||
|
import type { OssObjectDto } from '../../types/objects'; |
||||
|
|
||||
|
import { defineAsyncComponent, onMounted, ref } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { Button, Card, DirectoryTree, Empty, Select } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useContainesApi } from '../../api'; |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'bucketChange', data: string): void; |
||||
|
(event: 'folderChange', data: string): void; |
||||
|
}>(); |
||||
|
|
||||
|
interface Folder { |
||||
|
children?: Folder[]; |
||||
|
isLeaf?: boolean; |
||||
|
key: string; |
||||
|
name: string; |
||||
|
path?: string; |
||||
|
title: string; |
||||
|
} |
||||
|
|
||||
|
const { getListApi: getContainersApi, getObjectsApi } = useContainesApi(); |
||||
|
|
||||
|
const [FolderModal, modalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent(() => import('./FolderModal.vue')), |
||||
|
}); |
||||
|
|
||||
|
const rootFolder: Folder = { |
||||
|
isLeaf: false, |
||||
|
key: './', |
||||
|
name: './', |
||||
|
path: '', |
||||
|
title: $t('AbpOssManagement.Objects:Root'), |
||||
|
children: [], |
||||
|
}; |
||||
|
|
||||
|
const bucket = ref<string>(''); |
||||
|
const loadedFolders = ref<string[]>([]); |
||||
|
const expandedFolders = ref<string[]>([]); |
||||
|
const selectedFolders = ref<string[]>([]); |
||||
|
const containers = ref<OssContainerDto[]>([]); |
||||
|
const folders = ref<Folder[]>([ |
||||
|
{ |
||||
|
...rootFolder, |
||||
|
}, |
||||
|
]); |
||||
|
|
||||
|
const onLoadChildFolders: TreeProps['loadData'] = async (treeNode) => { |
||||
|
let path = ''; |
||||
|
if (treeNode.dataRef?.path) { |
||||
|
path = path + treeNode.dataRef?.path; |
||||
|
} |
||||
|
if (treeNode.dataRef?.name) { |
||||
|
path = path + treeNode.dataRef?.name; |
||||
|
} |
||||
|
try { |
||||
|
treeNode.dataRef!.children = await getFolders(bucket.value!, path); |
||||
|
} catch { |
||||
|
treeNode.dataRef!.children = []; |
||||
|
} |
||||
|
folders.value = [...folders.value]; |
||||
|
loadedFolders.value = [...loadedFolders.value, treeNode.key.toString()]; |
||||
|
}; |
||||
|
|
||||
|
async function onInit() { |
||||
|
const getContainersRes = await getContainersApi({ |
||||
|
maxResultCount: 1000, |
||||
|
}); |
||||
|
containers.value = getContainersRes.containers; |
||||
|
} |
||||
|
|
||||
|
async function getFolders(bucket: string, path?: string) { |
||||
|
const { objects } = await getObjectsApi({ |
||||
|
bucket, |
||||
|
delimiter: '/', |
||||
|
maxResultCount: 1000, |
||||
|
prefix: path ?? '', |
||||
|
}); |
||||
|
return objects |
||||
|
.filter((f) => f.isFolder) |
||||
|
.map((folder) => { |
||||
|
return { |
||||
|
isLeaf: false, |
||||
|
key: `${folder.path ?? ''}${folder.name}`, |
||||
|
name: folder.name, |
||||
|
path: folder.path, |
||||
|
title: folder.name, |
||||
|
children: [], |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onFolderExpand( |
||||
|
_: Key[], |
||||
|
info: { |
||||
|
expanded: boolean; |
||||
|
node: EventDataNode; |
||||
|
}, |
||||
|
) { |
||||
|
if (!info.expanded) { |
||||
|
const keys = loadedFolders.value; |
||||
|
const findIndex = keys.lastIndexOf(info.node.key.toString()); |
||||
|
findIndex !== -1 && keys.splice(findIndex); |
||||
|
loadedFolders.value = keys; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onFolderChange(selectedKeys: Key[]) { |
||||
|
if (selectedKeys.length === 1) { |
||||
|
emits('folderChange', selectedKeys[0]!.toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function onBucketChange(bucket: string) { |
||||
|
emits('bucketChange', bucket); |
||||
|
expandedFolders.value = []; |
||||
|
loadedFolders.value = []; |
||||
|
folders.value = [ |
||||
|
{ |
||||
|
...rootFolder, |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
function onCreate() { |
||||
|
modalApi.setData({ |
||||
|
bucket: bucket.value, |
||||
|
path: selectedFolders.value[0], |
||||
|
}); |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
|
||||
|
function onFolderCreated(ossObject: OssObjectDto) { |
||||
|
const keys = expandedFolders.value; |
||||
|
const findIndex = keys.lastIndexOf(ossObject.path); |
||||
|
if (findIndex !== -1) { |
||||
|
keys.splice(findIndex); |
||||
|
expandedFolders.value = keys; |
||||
|
loadedFolders.value = []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMounted(onInit); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Card :title="$t('AbpOssManagement.Containers')"> |
||||
|
<div class="flex flex-col gap-2"> |
||||
|
<Select |
||||
|
:placeholder="$t('AbpOssManagement.Containers:Select')" |
||||
|
:options="containers" |
||||
|
:field-names="{ label: 'name', value: 'name' }" |
||||
|
v-model:value="bucket" |
||||
|
@change="(e) => onBucketChange(e!.toString())" |
||||
|
/> |
||||
|
<Button v-if="bucket" block type="primary" ghost @click="onCreate"> |
||||
|
{{ $t('AbpOssManagement.Objects:CreateFolder') }} |
||||
|
</Button> |
||||
|
<DirectoryTree |
||||
|
v-if="bucket" |
||||
|
block-node |
||||
|
v-model:expanded-keys="expandedFolders" |
||||
|
v-model:selected-keys="selectedFolders" |
||||
|
:loaded-keys="loadedFolders" |
||||
|
:tree-data="folders" |
||||
|
:load-data="onLoadChildFolders" |
||||
|
@select="onFolderChange" |
||||
|
@expand="onFolderExpand" |
||||
|
/> |
||||
|
<Empty v-else /> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<FolderModal @change="onFolderCreated" /> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
:deep(.ant-tree) { |
||||
|
.ant-tree-title { |
||||
|
word-break: break-word; |
||||
|
white-space: normal; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,34 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { defineAsyncComponent, ref } from 'vue'; |
||||
|
|
||||
|
const FolderTree = defineAsyncComponent(() => import('./FolderTree.vue')); |
||||
|
const FileList = defineAsyncComponent(() => import('./FileList.vue')); |
||||
|
|
||||
|
const bucket = ref(''); |
||||
|
const path = ref(''); |
||||
|
|
||||
|
function onBucketChange(val: string) { |
||||
|
bucket.value = val; |
||||
|
path.value = ''; |
||||
|
} |
||||
|
|
||||
|
function onFolderChange(val: string) { |
||||
|
path.value = val; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="flex flex-row gap-2"> |
||||
|
<div style="width: 30%"> |
||||
|
<FolderTree |
||||
|
@bucket-change="onBucketChange" |
||||
|
@folder-change="onFolderChange" |
||||
|
/> |
||||
|
</div> |
||||
|
<div style="width: 70%"> |
||||
|
<FileList :bucket="bucket" :path="path" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** 容器权限 */ |
||||
|
export const ContainerPermissions = { |
||||
|
/** 新增 */ |
||||
|
Create: 'AbpOssManagement.Container.Create', |
||||
|
Default: 'AbpOssManagement.Container', |
||||
|
/** 删除 */ |
||||
|
Delete: 'AbpOssManagement.Container.Delete', |
||||
|
}; |
||||
|
/** 容器权限 */ |
||||
|
export const OssObjectPermissions = { |
||||
|
/** 新增 */ |
||||
|
Create: 'AbpOssManagement.OssObject.Create', |
||||
|
Default: 'AbpOssManagement.OssObject', |
||||
|
/** 删除 */ |
||||
|
Delete: 'AbpOssManagement.OssObject.Delete', |
||||
|
/** 下载 */ |
||||
|
Download: 'AbpOssManagement.OssObject.Download', |
||||
|
}; |
||||
@ -0,0 +1 @@ |
|||||
|
declare module 'vue-simple-uploader'; |
||||
@ -0,0 +1,50 @@ |
|||||
|
interface OssObjectDto { |
||||
|
creationDate: Date; |
||||
|
isFolder: boolean; |
||||
|
lastModifiedDate?: Date; |
||||
|
mD5?: string; |
||||
|
metadata: Record<string, string>; |
||||
|
name: string; |
||||
|
path: string; |
||||
|
size: number; |
||||
|
} |
||||
|
|
||||
|
interface CreateOssObjectInput { |
||||
|
bucket: string; |
||||
|
expirationTime?: string; |
||||
|
file?: File; |
||||
|
fileName: string; |
||||
|
overwrite: boolean; |
||||
|
path?: string; |
||||
|
} |
||||
|
|
||||
|
interface GetOssObjectInput { |
||||
|
bucket: string; |
||||
|
mD5: boolean; |
||||
|
object: string; |
||||
|
path?: string; |
||||
|
} |
||||
|
|
||||
|
interface BulkDeleteOssObjectInput { |
||||
|
bucket: string; |
||||
|
object: string; |
||||
|
path?: string; |
||||
|
} |
||||
|
|
||||
|
interface OssObjectsResultDto { |
||||
|
bucket: string; |
||||
|
delimiter?: string; |
||||
|
marker?: string; |
||||
|
maxKeys: number; |
||||
|
nextMarker?: string; |
||||
|
objects: OssObjectDto[]; |
||||
|
prefix?: string; |
||||
|
} |
||||
|
|
||||
|
export type { |
||||
|
BulkDeleteOssObjectInput, |
||||
|
CreateOssObjectInput, |
||||
|
GetOssObjectInput, |
||||
|
OssObjectDto, |
||||
|
OssObjectsResultDto, |
||||
|
}; |
||||
Loading…
Reference in new issue