Browse Source

feat(vben5): 增加文件上传功能

pull/1191/head
colin 9 months ago
parent
commit
90db54b54a
  1. 3
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  2. 3
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  3. 15
      apps/vben5/apps/app-antd/src/views/oss/objects/index.vue
  4. 5
      apps/vben5/packages/@abp/oss/package.json
  5. 1
      apps/vben5/packages/@abp/oss/src/api/index.ts
  6. 15
      apps/vben5/packages/@abp/oss/src/api/useContainesApi.ts
  7. 50
      apps/vben5/packages/@abp/oss/src/api/useObjectsApi.ts
  8. 1
      apps/vben5/packages/@abp/oss/src/components/index.ts
  9. 282
      apps/vben5/packages/@abp/oss/src/components/objects/FileList.vue
  10. 286
      apps/vben5/packages/@abp/oss/src/components/objects/FileUploadModal.vue
  11. 66
      apps/vben5/packages/@abp/oss/src/components/objects/FolderModal.vue
  12. 192
      apps/vben5/packages/@abp/oss/src/components/objects/FolderTree.vue
  13. 34
      apps/vben5/packages/@abp/oss/src/components/objects/ObjectPage.vue
  14. 18
      apps/vben5/packages/@abp/oss/src/constants/permissions.ts
  15. 1
      apps/vben5/packages/@abp/oss/src/global.d.ts
  16. 50
      apps/vben5/packages/@abp/oss/src/types/objects.ts

3
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -132,6 +132,7 @@
}, },
"oss": { "oss": {
"title": "Object storage", "title": "Object storage",
"containers": "Containers" "containers": "Containers",
"objects": "Files"
} }
} }

3
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -132,6 +132,7 @@
}, },
"oss": { "oss": {
"title": "对象存储", "title": "对象存储",
"containers": "容器管理" "containers": "容器管理",
"objects": "文件管理"
} }
} }

15
apps/vben5/apps/app-antd/src/views/oss/objects/index.vue

@ -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>

5
apps/vben5/packages/@abp/oss/package.json

@ -17,6 +17,9 @@
".": { ".": {
"types": "./src/index.ts", "types": "./src/index.ts",
"default": "./src/index.ts" "default": "./src/index.ts"
},
"./global": {
"types": "./global.d.ts"
} }
}, },
"dependencies": { "dependencies": {
@ -32,8 +35,10 @@
"@vben/icons": "workspace:*", "@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*", "@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*", "@vben/locales": "workspace:*",
"@vben/stores": "workspace:*",
"ant-design-vue": "catalog:", "ant-design-vue": "catalog:",
"vue": "catalog:*", "vue": "catalog:*",
"vue-simple-uploader": "catalog:",
"vxe-table": "catalog:" "vxe-table": "catalog:"
} }
} }

1
apps/vben5/packages/@abp/oss/src/api/index.ts

@ -1 +1,2 @@
export { useContainesApi } from './useContainesApi'; export { useContainesApi } from './useContainesApi';
export { useObjectsApi } from './useObjectsApi';

15
apps/vben5/packages/@abp/oss/src/api/useContainesApi.ts

@ -1,8 +1,10 @@
import type { import type {
GetOssContainersInput, GetOssContainersInput,
GetOssObjectsInput,
OssContainerDto, OssContainerDto,
OssContainersResultDto, OssContainersResultDto,
} from '../types/containes'; } from '../types/containes';
import type { OssObjectsResultDto } from '../types/objects';
import { useRequest } from '@abp/request'; import { useRequest } from '@abp/request';
@ -30,6 +32,18 @@ export function useContainesApi() {
}); });
} }
function getObjectsApi(
input: GetOssObjectsInput,
): Promise<OssObjectsResultDto> {
return request<OssObjectsResultDto>(
`/api/oss-management/containes/objects`,
{
method: 'GET',
params: input,
},
);
}
function createApi(name: string): Promise<OssContainerDto> { function createApi(name: string): Promise<OssContainerDto> {
return request<OssContainerDto>(`/api/oss-management/containes/${name}`, { return request<OssContainerDto>(`/api/oss-management/containes/${name}`, {
method: 'POST', method: 'POST',
@ -42,5 +56,6 @@ export function useContainesApi() {
deleteApi, deleteApi,
getApi, getApi,
getListApi, getListApi,
getObjectsApi,
}; };
} }

50
apps/vben5/packages/@abp/oss/src/api/useObjectsApi.ts

@ -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
apps/vben5/packages/@abp/oss/src/components/index.ts

@ -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';

282
apps/vben5/packages/@abp/oss/src/components/objects/FileList.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>

286
apps/vben5/packages/@abp/oss/src/components/objects/FileUploadModal.vue

@ -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 , axiostoken
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>

66
apps/vben5/packages/@abp/oss/src/components/objects/FolderModal.vue

@ -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>

192
apps/vben5/packages/@abp/oss/src/components/objects/FolderTree.vue

@ -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>

34
apps/vben5/packages/@abp/oss/src/components/objects/ObjectPage.vue

@ -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>

18
apps/vben5/packages/@abp/oss/src/constants/permissions.ts

@ -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',
};

1
apps/vben5/packages/@abp/oss/src/global.d.ts

@ -0,0 +1 @@
declare module 'vue-simple-uploader';

50
apps/vben5/packages/@abp/oss/src/types/objects.ts

@ -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…
Cancel
Save