25 changed files with 2093 additions and 13 deletions
@ -1,15 +1,15 @@ |
|||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||
import { Page } from '@vben/common-ui'; |
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
import { ContainerTable } from '@abp/oss'; |
import { BlobViewPage } from '@abp/blob-management'; |
||||
|
|
||||
defineOptions({ |
defineOptions({ |
||||
name: 'Vben5OssContainers', |
name: 'Vben5BlobFiles', |
||||
}); |
}); |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<Page> |
<Page> |
||||
<ContainerTable /> |
<BlobViewPage /> |
||||
</Page> |
</Page> |
||||
</template> |
</template> |
||||
@ -1,15 +1,15 @@ |
|||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||
import { Page } from '@vben/common-ui'; |
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
import { ObjectPage } from '@abp/oss'; |
import { BlobContainerTable } from '@abp/blob-management'; |
||||
|
|
||||
defineOptions({ |
defineOptions({ |
||||
name: 'Vben5OssObjects', |
name: 'Vben5BlobContainers', |
||||
}); |
}); |
||||
</script> |
</script> |
||||
|
|
||||
<template> |
<template> |
||||
<Page auto-content-height> |
<Page> |
||||
<ObjectPage /> |
<BlobContainerTable /> |
||||
</Page> |
</Page> |
||||
</template> |
</template> |
||||
@ -0,0 +1,50 @@ |
|||||
|
{ |
||||
|
"name": "@abp/blob-management", |
||||
|
"version": "10.0.2", |
||||
|
"homepage": "https://github.com/colinin/abp-next-admin", |
||||
|
"bugs": "https://github.com/colinin/abp-next-admin/issues", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "git+https://github.com/colinin/abp-next-admin.git", |
||||
|
"directory": "packages/@abp/blob-management" |
||||
|
}, |
||||
|
"license": "MIT", |
||||
|
"type": "module", |
||||
|
"sideEffects": [ |
||||
|
"**/*.css" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./src/index.ts", |
||||
|
"default": "./src/index.ts" |
||||
|
}, |
||||
|
"./global": { |
||||
|
"types": "./global.d.ts" |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@abp/components": "workspace:*", |
||||
|
"@abp/core": "workspace:*", |
||||
|
"@abp/features": "workspace:*", |
||||
|
"@abp/request": "workspace:*", |
||||
|
"@abp/ui": "workspace:*", |
||||
|
"@ant-design/icons-vue": "catalog:", |
||||
|
"@vben/access": "workspace:*", |
||||
|
"@vben/common-ui": "workspace:*", |
||||
|
"@vben/hooks": "workspace:*", |
||||
|
"@vben/icons": "workspace:*", |
||||
|
"@vben/layouts": "workspace:*", |
||||
|
"@vben/locales": "workspace:*", |
||||
|
"@vben/stores": "workspace:*", |
||||
|
"@vben/utils": "workspace:*", |
||||
|
"@vue-office/docx": "catalog:", |
||||
|
"@vue-office/excel": "catalog:", |
||||
|
"@vue-office/pdf": "catalog:", |
||||
|
"@vue-office/pptx": "catalog:", |
||||
|
"ant-design-vue": "catalog:", |
||||
|
"vue": "catalog:", |
||||
|
"vue-demi": "catalog:", |
||||
|
"vue-simple-uploader": "catalog:", |
||||
|
"vxe-table": "catalog:" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { useBlobContainersApi } from './useBlobContainersApi'; |
||||
|
export { useBlobsApi } from './useBlobsApi'; |
||||
@ -0,0 +1,52 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { |
||||
|
BlobContainerCreateDto, |
||||
|
BlobContainerDto, |
||||
|
BlobContainerGetPagedListInput, |
||||
|
} from '../types/containers'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useBlobContainersApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function deleteApi(id: string): Promise<void> { |
||||
|
return request(`/api/blob-management/containers/${id}`, { |
||||
|
method: 'DELETE', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getApi(id: string): Promise<BlobContainerDto> { |
||||
|
return request<BlobContainerDto>(`/api/blob-management/containers/${id}`, { |
||||
|
method: 'GET', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getPagedListApi( |
||||
|
input?: BlobContainerGetPagedListInput, |
||||
|
): Promise<PagedResultDto<BlobContainerDto>> { |
||||
|
return request<PagedResultDto<BlobContainerDto>>( |
||||
|
`/api/blob-management/containers`, |
||||
|
{ |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function createApi(input: BlobContainerCreateDto): Promise<BlobContainerDto> { |
||||
|
return request<BlobContainerDto>(`/api/blob-management/containers`, { |
||||
|
method: 'POST', |
||||
|
data: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
createApi, |
||||
|
deleteApi, |
||||
|
getApi, |
||||
|
getPagedListApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
import type { PagedResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { |
||||
|
BlobDto, |
||||
|
BlobFolderCreateDto, |
||||
|
BlobGetPagedListInput, |
||||
|
} from '../types/blobs'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
import { BlobType } from '../types/blobs'; |
||||
|
|
||||
|
export function useBlobsApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
function createFolderApi(input: BlobFolderCreateDto): Promise<BlobDto> { |
||||
|
return request<BlobDto>(`/api/blob-management/blobs/folder`, { |
||||
|
method: 'POST', |
||||
|
data: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function deleteApi(id: string): Promise<void> { |
||||
|
return request(`/api/blob-management/blobs/${id}`, { |
||||
|
method: 'DELETE', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getApi(id: string): Promise<BlobDto> { |
||||
|
return request<BlobDto>(`/api/blob-management/blobs/${id}`, { |
||||
|
method: 'GET', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getContentApi(id: string): Promise<Blob> { |
||||
|
return request<Blob>(`/api/blob-management/blobs/uid/${id}/content`, { |
||||
|
method: 'GET', |
||||
|
responseType: 'blob', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getFolderPagedListApi( |
||||
|
input: BlobGetPagedListInput, |
||||
|
): Promise<PagedResultDto<BlobDto>> { |
||||
|
return getPagedListApi({ |
||||
|
...input, |
||||
|
type: BlobType.Folder, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getFilePagedListApi( |
||||
|
input: BlobGetPagedListInput, |
||||
|
): Promise<PagedResultDto<BlobDto>> { |
||||
|
return getPagedListApi({ |
||||
|
...input, |
||||
|
type: BlobType.File, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function getPagedListApi( |
||||
|
input: BlobGetPagedListInput, |
||||
|
): Promise<PagedResultDto<BlobDto>> { |
||||
|
return request<PagedResultDto<BlobDto>>(`/api/blob-management/blobs`, { |
||||
|
method: 'GET', |
||||
|
params: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
createFolderApi, |
||||
|
deleteApi, |
||||
|
getApi, |
||||
|
getContentApi, |
||||
|
getFolderPagedListApi, |
||||
|
getFilePagedListApi, |
||||
|
getPagedListApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,411 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { BlobDto } from '../../types/blobs'; |
||||
|
|
||||
|
import { defineAsyncComponent, ref, watch } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
import { downloadFileFromUrl } from '@vben/utils'; |
||||
|
|
||||
|
import { Button, Empty, message, Spin } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBlobsApi } from '../../api/useBlobsApi'; |
||||
|
|
||||
|
type FileType = 'excel' | 'image' | 'pdf' | 'ppt' | 'text' | 'word'; |
||||
|
|
||||
|
const VueOfficeDocx = defineAsyncComponent({ |
||||
|
loader: async () => { |
||||
|
const res = await import('@vue-office/docx/lib/v3/index.js'); |
||||
|
import('@vue-office/docx/lib/v3/index.css'); |
||||
|
return res.default; |
||||
|
}, |
||||
|
loadingComponent: Spin, |
||||
|
delay: 200, |
||||
|
}); |
||||
|
|
||||
|
const VueOfficeExcel = defineAsyncComponent({ |
||||
|
loader: async () => { |
||||
|
const res = await import('@vue-office/excel/lib/v3/index.js'); |
||||
|
import('@vue-office/excel/lib/v3/index.css'); |
||||
|
return res.default; |
||||
|
}, |
||||
|
loadingComponent: Spin, |
||||
|
delay: 200, |
||||
|
}); |
||||
|
|
||||
|
const VueOfficePdf = defineAsyncComponent({ |
||||
|
loader: () => import('@vue-office/pdf/lib/v3/index.js'), |
||||
|
loadingComponent: Spin, |
||||
|
delay: 200, |
||||
|
}); |
||||
|
|
||||
|
const VueOfficePptx = defineAsyncComponent({ |
||||
|
loader: () => import('@vue-office/pptx/lib/v3/index.js'), |
||||
|
loadingComponent: Spin, |
||||
|
delay: 200, |
||||
|
}); |
||||
|
|
||||
|
const Image = defineAsyncComponent({ |
||||
|
loader: async () => { |
||||
|
const res = await import('ant-design-vue'); |
||||
|
return res.Image; |
||||
|
}, |
||||
|
loadingComponent: Spin, |
||||
|
delay: 200, |
||||
|
}); |
||||
|
|
||||
|
const modalTitle = ref(''); |
||||
|
const isPreview = ref(false); |
||||
|
const isLoading = ref(false); |
||||
|
const blob = ref<BlobDto>(); |
||||
|
const blobData = ref<string>(); |
||||
|
const blobText = ref<string>(); |
||||
|
const blobType = ref<FileType>(); |
||||
|
const errorMessage = ref<string>(''); |
||||
|
const excelComponentKey = ref(0); // 只对 Excel 组件使用 key |
||||
|
|
||||
|
const { getContentApi } = useBlobsApi(); |
||||
|
|
||||
|
// 文件类型配置 |
||||
|
const allowFiles: Record<FileType, string[]> = { |
||||
|
pdf: ['.pdf'], |
||||
|
word: ['.doc', '.docx'], |
||||
|
excel: ['.xls', '.xlsx'], |
||||
|
ppt: ['.ppt', '.pptx'], |
||||
|
text: ['.txt', '.text', '.log', '.md', '.json', '.xml', '.yaml', '.yml'], |
||||
|
image: [ |
||||
|
'.bmp', |
||||
|
'.png', |
||||
|
'.jpg', |
||||
|
'.jpeg', |
||||
|
'.webp', |
||||
|
'.tif', |
||||
|
'.tiff', |
||||
|
'.gif', |
||||
|
'.svg', |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
// 文件大小限制(默认50MB) |
||||
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; |
||||
|
|
||||
|
// 判断文件类型 |
||||
|
function getFileType(): FileType | undefined { |
||||
|
const fileName = blob.value!.name.toLocaleLowerCase(); |
||||
|
for (const [type, extensions] of Object.entries(allowFiles)) { |
||||
|
if (extensions.some((ext) => fileName.endsWith(ext))) { |
||||
|
return type as FileType; |
||||
|
} |
||||
|
} |
||||
|
return undefined; |
||||
|
} |
||||
|
|
||||
|
// 检查文件是否允许预览 |
||||
|
function isAllowPreview(): boolean { |
||||
|
return !!getFileType(); |
||||
|
} |
||||
|
|
||||
|
// 检查文件大小 |
||||
|
function isFileSizeValid(content: Blob): boolean { |
||||
|
if (content.size > MAX_FILE_SIZE) { |
||||
|
errorMessage.value = $t('BlobManagement.Blobs:FileTooLarge', { |
||||
|
size: MAX_FILE_SIZE / 1024 / 1024, |
||||
|
}); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
function onError(error?: string) { |
||||
|
message.error(`${$t('BlobManagement.Blobs:PreviewError')}: ${error}`); |
||||
|
} |
||||
|
|
||||
|
// 清理资源 |
||||
|
function cleanup() { |
||||
|
if (blobData.value) { |
||||
|
URL.revokeObjectURL(blobData.value); |
||||
|
blobData.value = undefined; |
||||
|
} |
||||
|
blobText.value = ''; |
||||
|
blobType.value = undefined; |
||||
|
isPreview.value = false; |
||||
|
errorMessage.value = ''; |
||||
|
} |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
showCancelButton: false, |
||||
|
showConfirmButton: false, |
||||
|
fullscreen: true, |
||||
|
fullscreenButton: false, |
||||
|
onCancel: () => { |
||||
|
cleanup(); |
||||
|
}, |
||||
|
onOpenChange: async (isOpen) => { |
||||
|
if (isOpen) { |
||||
|
await onInit(); |
||||
|
} else { |
||||
|
cleanup(); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
async function onInit() { |
||||
|
try { |
||||
|
isLoading.value = true; |
||||
|
isPreview.value = false; |
||||
|
errorMessage.value = ''; |
||||
|
modalApi.lock(); |
||||
|
|
||||
|
// 重置状态 |
||||
|
cleanup(); |
||||
|
|
||||
|
// 获取数据 |
||||
|
blob.value = modalApi.getData<BlobDto>(); |
||||
|
if (!blob.value) { |
||||
|
errorMessage.value = $t('BlobManagement.Blobs:NoData'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
modalTitle.value = `${$t('BlobManagement.Blobs:Preview')} - ${blob.value.name}`; |
||||
|
|
||||
|
// 检查是否支持预览 |
||||
|
if (!isAllowPreview()) { |
||||
|
errorMessage.value = $t('BlobManagement.BlobCanNotPreviewMessage'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 获取文件内容 |
||||
|
const content = await getContentApi(blob.value.id); |
||||
|
|
||||
|
// 检查文件大小 |
||||
|
if (!isFileSizeValid(content)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 创建对象URL |
||||
|
blobData.value = URL.createObjectURL(content); |
||||
|
|
||||
|
// 设置文件类型并处理内容 |
||||
|
blobType.value = getFileType(); |
||||
|
|
||||
|
await processFileContent(content); |
||||
|
isPreview.value = true; |
||||
|
} catch (error) { |
||||
|
console.error('Preview error:', error); |
||||
|
errorMessage.value = $t('BlobManagement.Blobs:PreviewError'); |
||||
|
message.error(errorMessage.value); |
||||
|
} finally { |
||||
|
isLoading.value = false; |
||||
|
modalApi.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function processFileContent(content: Blob) { |
||||
|
// 处理文本文件 |
||||
|
if (blobType.value === 'text') { |
||||
|
try { |
||||
|
blobText.value = await content.text(); |
||||
|
// 限制文本显示长度(避免大文件导致页面卡顿) |
||||
|
const maxLength = 100_000; // 10万字符 |
||||
|
if (blobText.value.length > maxLength) { |
||||
|
blobText.value = `${blobText.value.slice(0, Math.max(0, maxLength))}\n\n... ${$t( |
||||
|
'BlobManagement.Blobs:TextTruncated', |
||||
|
)}`; |
||||
|
message.warning($t('BlobManagement.Blobs:TextTooLarge')); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('Read text error:', error); |
||||
|
throw new Error('Failed to read text file'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 下载原始文件 |
||||
|
function handleDownload() { |
||||
|
if (!blob.value) return; |
||||
|
downloadFileFromUrl({ |
||||
|
fileName: blob.value.name, |
||||
|
source: `/api/blob-management/blobs/uid/${blob.value.id}/content`, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 监听blobData变化,确保资源清理 |
||||
|
watch(blobData, (newVal, oldVal) => { |
||||
|
if (oldVal && oldVal !== newVal) { |
||||
|
URL.revokeObjectURL(oldVal); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="modalTitle"> |
||||
|
<div class="preview-container"> |
||||
|
<!-- 加载状态 --> |
||||
|
<div v-if="isLoading" class="flex h-96 items-center justify-center"> |
||||
|
<Spin :tip="$t('AbpUi.LoadingWithThreeDot')" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 错误提示 --> |
||||
|
<div v-else-if="errorMessage" class="error-message"> |
||||
|
<Empty :description="errorMessage"> |
||||
|
<template #image> |
||||
|
<span class="text-6xl">📄</span> |
||||
|
</template> |
||||
|
</Empty> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 预览内容 --> |
||||
|
<template v-else> |
||||
|
<!-- 不支持预览 --> |
||||
|
<Empty |
||||
|
v-if="!isPreview" |
||||
|
:description="$t('BlobManagement.BlobCanNotPreviewMessage')" |
||||
|
> |
||||
|
<template #image> |
||||
|
<span class="text-6xl">🔍</span> |
||||
|
</template> |
||||
|
</Empty> |
||||
|
|
||||
|
<!-- Word预览 --> |
||||
|
<div v-else-if="blobType === 'word'" class="office-preview-wrapper"> |
||||
|
<VueOfficeDocx :src="blobData" @error="onError" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Excel预览 - 单独处理,添加key强制重新渲染 --> |
||||
|
<div v-else-if="blobType === 'excel'" class="excel-preview-wrapper"> |
||||
|
<VueOfficeExcel |
||||
|
:key="excelComponentKey" |
||||
|
:src="blobData" |
||||
|
@error="onError" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- PDF预览 - 不做任何处理,保持原样 --> |
||||
|
<VueOfficePdf |
||||
|
v-else-if="blobType === 'pdf'" |
||||
|
:src="blobData" |
||||
|
@error="onError" |
||||
|
/> |
||||
|
|
||||
|
<!-- PPT预览 --> |
||||
|
<div v-else-if="blobType === 'ppt'" class="office-preview-wrapper"> |
||||
|
<VueOfficePptx :src="blobData" @error="onError" /> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 图片预览 --> |
||||
|
<div v-else-if="blobType === 'image'" class="image-preview"> |
||||
|
<Image |
||||
|
:src="blobData" |
||||
|
:alt="blob?.name" |
||||
|
class="max-h-[80vh] max-w-full object-contain" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 文本预览 --> |
||||
|
<div v-else-if="blobType === 'text'" class="text-preview"> |
||||
|
<div |
||||
|
class="whitespace-pre-wrap rounded bg-gray-50 p-4 font-mono text-sm dark:bg-gray-800" |
||||
|
> |
||||
|
{{ blobText }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<!-- 下载按钮 --> |
||||
|
<div v-if="blob" class="download-button"> |
||||
|
<Button type="primary" @click="handleDownload"> |
||||
|
{{ $t('BlobManagement.Blobs:Download') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
.preview-container { |
||||
|
position: relative; |
||||
|
min-height: 400px; |
||||
|
|
||||
|
.error-message { |
||||
|
padding: 40px 20px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.office-preview-wrapper { |
||||
|
width: 100%; |
||||
|
|
||||
|
:deep(> div) { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Excel 单独包装器样式 |
||||
|
.excel-preview-wrapper { |
||||
|
width: 100%; |
||||
|
min-height: 500px; |
||||
|
|
||||
|
:deep(> div) { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-height: 500px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.image-preview { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
min-height: 400px; |
||||
|
overflow: auto; |
||||
|
background-color: #f5f5f5; |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
.text-preview { |
||||
|
max-height: 70vh; |
||||
|
overflow: auto; |
||||
|
|
||||
|
:deep(.whitespace-pre-wrap) { |
||||
|
word-break: normal; |
||||
|
overflow-wrap: break-word; |
||||
|
white-space: pre-wrap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.download-button { |
||||
|
position: sticky; |
||||
|
right: 20px; |
||||
|
bottom: 20px; |
||||
|
z-index: 10; |
||||
|
float: right; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.vben-modal { |
||||
|
.vben-modal__body { |
||||
|
height: auto !important; |
||||
|
max-height: calc(90vh - 110px); |
||||
|
padding: 20px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.excel-preview-wrapper { |
||||
|
.vue-office-excel { |
||||
|
width: 100% !important; |
||||
|
height: 100% !important; |
||||
|
min-height: 500px !important; |
||||
|
|
||||
|
iframe, |
||||
|
canvas, |
||||
|
.office-viewer { |
||||
|
width: 100% !important; |
||||
|
height: 100% !important; |
||||
|
min-height: 500px !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,301 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridListeners, VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { BlobDto } from '../../types/blobs'; |
||||
|
|
||||
|
import { defineAsyncComponent, h, nextTick, ref, watch } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
import { downloadFileFromUrl } from '@vben/utils'; |
||||
|
|
||||
|
import { formatToDateTime, isNullOrWhiteSpace } from '@abp/core'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { |
||||
|
DeleteOutlined, |
||||
|
DownloadOutlined, |
||||
|
EyeOutlined, |
||||
|
UploadOutlined, |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { Button, Card, message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBlobsApi } from '../../api/useBlobsApi'; |
||||
|
import { BlobType } from '../../types/blobs'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'FileList', |
||||
|
}); |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
containerId: string; |
||||
|
folderId?: string; |
||||
|
}>(); |
||||
|
|
||||
|
const kbUnit = 1 * 1024; |
||||
|
const mbUnit = kbUnit * 1024; |
||||
|
const gbUnit = mbUnit * 1024; |
||||
|
|
||||
|
const { deleteApi, getPagedListApi } = useBlobsApi(); |
||||
|
|
||||
|
const selectedKeys = ref<string[]>([]); |
||||
|
|
||||
|
const gridOptions: VxeGridProps<BlobDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'name', |
||||
|
minWidth: 150, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:Name'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'type', |
||||
|
formatter: ({ row }) => { |
||||
|
if (row.type === BlobType.Folder) { |
||||
|
return $t('BlobManagement.BlobType:Folder'); |
||||
|
} |
||||
|
return $t('BlobManagement.BlobType:File'); |
||||
|
}, |
||||
|
minWidth: 150, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:BlobType'), |
||||
|
}, |
||||
|
{ |
||||
|
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, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:Size'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'creationTime', |
||||
|
formatter({ cellValue }) { |
||||
|
return cellValue ? formatToDateTime(cellValue) : ''; |
||||
|
}, |
||||
|
minWidth: 120, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:CreationTime'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'lastModificationTime', |
||||
|
formatter({ cellValue }) { |
||||
|
return cellValue ? formatToDateTime(cellValue) : ''; |
||||
|
}, |
||||
|
minWidth: 120, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:LastModificationTime'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 260, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async ({ page, sort }) => { |
||||
|
const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; |
||||
|
return await getPagedListApi({ |
||||
|
containerId: props.containerId, |
||||
|
maxResultCount: page.pageSize, |
||||
|
parentId: props.folderId, |
||||
|
sorting, |
||||
|
skipCount: (page.currentPage - 1) * page.pageSize, |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
autoLoad: false, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
sortConfig: { |
||||
|
remote: true, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
custom: true, |
||||
|
export: false, |
||||
|
refresh: false, |
||||
|
zoom: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const gridEvents: VxeGridListeners<BlobDto> = { |
||||
|
checkboxAll: (params) => { |
||||
|
selectedKeys.value = params.records.map((record) => record.name); |
||||
|
}, |
||||
|
checkboxChange: (params) => { |
||||
|
selectedKeys.value = params.records.map((record) => record.name); |
||||
|
}, |
||||
|
sortChange: () => { |
||||
|
gridApi.query(); |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const [BlobFileUploadModal, uploadModalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent( |
||||
|
() => import('./BlobFileUploadModal.vue'), |
||||
|
), |
||||
|
}); |
||||
|
const [BlobFilePreviewModal, previewModalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent( |
||||
|
() => import('./BlobFilePreviewModal.vue'), |
||||
|
), |
||||
|
}); |
||||
|
|
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ |
||||
|
gridEvents, |
||||
|
gridOptions, |
||||
|
}); |
||||
|
|
||||
|
function onUpload() { |
||||
|
uploadModalApi.setData({ |
||||
|
containerId: props.containerId, |
||||
|
folderId: props.folderId, |
||||
|
}); |
||||
|
uploadModalApi.open(); |
||||
|
} |
||||
|
|
||||
|
function onDownload(row: BlobDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('BlobManagement.BlobWellDownloadMessage', [row.name]), |
||||
|
onCancel: () => {}, |
||||
|
onOk: () => { |
||||
|
downloadFileFromUrl({ |
||||
|
fileName: row.name, |
||||
|
source: `/api/blob-management/blobs/uid/${row.id}/content`, |
||||
|
}); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onPreview(row: BlobDto) { |
||||
|
previewModalApi.setData(row); |
||||
|
previewModalApi.open(); |
||||
|
} |
||||
|
|
||||
|
function onDelete(row: BlobDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name]), |
||||
|
onCancel: () => {}, |
||||
|
onOk: async () => { |
||||
|
await deleteApi(row.id); |
||||
|
message.success($t('AbpUi.DeletedSuccessfully')); |
||||
|
await gridApi.query(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
() => props.containerId, |
||||
|
(containerId) => { |
||||
|
nextTick(() => { |
||||
|
gridApi.setGridOptions({ |
||||
|
toolbarConfig: { |
||||
|
refresh: !isNullOrWhiteSpace(containerId), |
||||
|
}, |
||||
|
}); |
||||
|
if (!isNullOrWhiteSpace(containerId)) { |
||||
|
gridApi.query(); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
watch( |
||||
|
() => props.folderId, |
||||
|
(_) => { |
||||
|
nextTick(() => { |
||||
|
if (!isNullOrWhiteSpace(props.containerId)) { |
||||
|
gridApi.query(); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Card :title="$t('BlobManagement.Blobs:Files')"> |
||||
|
<Grid> |
||||
|
<template #toolbar-tools> |
||||
|
<Button |
||||
|
v-if="props.containerId" |
||||
|
:icon="h(UploadOutlined)" |
||||
|
type="primary" |
||||
|
@click="onUpload" |
||||
|
> |
||||
|
{{ $t('BlobManagement.Blobs:UploadFile') }} |
||||
|
</Button> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
v-if="row.type === BlobType.File" |
||||
|
:icon="h(EyeOutlined)" |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onPreview(row)" |
||||
|
> |
||||
|
{{ $t('BlobManagement.Blobs:Preview') }} |
||||
|
</Button> |
||||
|
<Button |
||||
|
v-if="row.type === BlobType.File" |
||||
|
:icon="h(DownloadOutlined)" |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onDownload(row)" |
||||
|
> |
||||
|
{{ $t('BlobManagement.Blobs:Download') }} |
||||
|
</Button> |
||||
|
<Button |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
danger |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onDelete(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Delete') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
</Card> |
||||
|
<BlobFilePreviewModal /> |
||||
|
<BlobFileUploadModal @file-uploaded="() => gridApi.query()" /> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,339 @@ |
|||||
|
<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 { |
||||
|
containerId: string; |
||||
|
folderId?: 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, _file: any) => {}, |
||||
|
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/blob-management/blobs/file/chunk', |
||||
|
testChunks: false, |
||||
|
withCredentials: false, |
||||
|
}); |
||||
|
|
||||
|
function onInit() { |
||||
|
const state = modalApi.getData<ModalState>(); |
||||
|
options.value = { |
||||
|
...options.value, |
||||
|
headers: { |
||||
|
Authorization: accessStore.accessToken, |
||||
|
}, |
||||
|
processParams: (params: any, file: any) => { |
||||
|
params.containerId = state.containerId; |
||||
|
params.name = file.name; |
||||
|
if (state.folderId) { |
||||
|
params.parentId = state.folderId; |
||||
|
} |
||||
|
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 { |
||||
|
if (params.row.error) { |
||||
|
return { |
||||
|
background: '#fff1f0', |
||||
|
color: '#f5222d', |
||||
|
fontWeight: '500', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const progress = Math.min(100, Math.max(0, params.row.progress || 0)); |
||||
|
|
||||
|
const getGradientByProgress = (progress: number) => { |
||||
|
if (progress === 0) { |
||||
|
return { |
||||
|
start: 'rgba(64, 169, 255, 0.1)', |
||||
|
end: 'rgba(64, 169, 255, 0.05)', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (progress <= 30) { |
||||
|
return { |
||||
|
start: 'rgba(24, 144, 255, 0.15)', |
||||
|
end: 'rgba(24, 144, 255, 0.05)', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (progress <= 70) { |
||||
|
return { |
||||
|
start: 'rgba(0, 199, 228, 0.15)', |
||||
|
end: 'rgba(0, 199, 228, 0.05)', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (progress < 100) { |
||||
|
return { |
||||
|
start: 'rgba(82, 196, 26, 0.2)', |
||||
|
end: 'rgba(82, 196, 26, 0.05)', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
start: 'rgba(82, 196, 26, 0.25)', |
||||
|
end: 'rgba(82, 196, 26, 0.1)', |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const { start, end } = getGradientByProgress(progress); |
||||
|
|
||||
|
if (progress === 100 && !params.row.error) { |
||||
|
return { |
||||
|
background: |
||||
|
'linear-gradient(135deg, rgba(82, 196, 26, 0.15) 0%, rgba(82, 196, 26, 0.05) 100%)', |
||||
|
position: 'relative', |
||||
|
transition: 'all 0.3s ease', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
background: `linear-gradient( |
||||
|
90deg, |
||||
|
${start} 0%, |
||||
|
${end} ${progress}%, |
||||
|
transparent ${progress}%, |
||||
|
transparent 100% |
||||
|
)`, |
||||
|
backgroundSize: '100% 100%', |
||||
|
transition: 'background 0.3s cubic-bezier(0.4, 0, 0.2, 1)', |
||||
|
position: 'relative', |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
() => [selectBtn.value, uploaderWrap.value], |
||||
|
([button, wrap]) => { |
||||
|
if (button && wrap) { |
||||
|
wrap.uploader.assignBrowse(button); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('BlobManagement.Blobs: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('BlobManagement.Blobs:SelectFile') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</UploaderDrop> |
||||
|
<UploaderList> |
||||
|
<template #default="{ fileList }"> |
||||
|
<VxeTable :data="fileList" :row-style="rowStyle"> |
||||
|
<VxeColumn type="seq" width="70" /> |
||||
|
<VxeColumn |
||||
|
field="name" |
||||
|
:title="$t('BlobManagement.DisplayName:Name')" |
||||
|
/> |
||||
|
<VxeColumn |
||||
|
field="size" |
||||
|
:title="$t('BlobManagement.DisplayName:Size')" |
||||
|
width="100" |
||||
|
> |
||||
|
<template #default="{ row }"> |
||||
|
<span>{{ formatSize(row.size) }}</span> |
||||
|
</template> |
||||
|
</VxeColumn> |
||||
|
<VxeColumn |
||||
|
field="status" |
||||
|
:title="$t('BlobManagement.DisplayName:UploadStatus')" |
||||
|
width="180" |
||||
|
> |
||||
|
<template #default="{ row }"> |
||||
|
<Tooltip v-if="row.error" :title="row.errorMsg"> |
||||
|
<Tag color="red"> |
||||
|
{{ $t('BlobManagement.UploadStatus:Error') }} |
||||
|
</Tag> |
||||
|
</Tooltip> |
||||
|
<Tag v-else-if="row.paused" color="orange"> |
||||
|
{{ $t('BlobManagement.UploadStatus:Pause') }} |
||||
|
</Tag> |
||||
|
<Tag v-else-if="row.completed" color="green"> |
||||
|
{{ $t('BlobManagement.UploadStatus:Completed') }} |
||||
|
</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,65 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { BlobDto } from '../../types/blobs'; |
||||
|
|
||||
|
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { message } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBlobsApi } from '../../api/useBlobsApi'; |
||||
|
|
||||
|
interface ModalState { |
||||
|
containerId: string; |
||||
|
parentId?: string; |
||||
|
} |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'change', data: BlobDto): void; |
||||
|
}>(); |
||||
|
|
||||
|
const { createFolderApi } = useBlobsApi(); |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'name', |
||||
|
label: $t('BlobManagement.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 createFolderApi({ |
||||
|
containerId: state.containerId, |
||||
|
parentId: state.parentId, |
||||
|
name: values.name, |
||||
|
}); |
||||
|
message.success($t('AbpUi.SavedSuccessfully')); |
||||
|
emits('change', dto); |
||||
|
modalApi.close(); |
||||
|
} finally { |
||||
|
modalApi.setState({ submitting: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('BlobManagement.Blobs:CreateFolder')"> |
||||
|
<Form /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,283 @@ |
|||||
|
<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 { BlobDto } from '../../types'; |
||||
|
import type { BlobContainerDto } from '../../types/containers'; |
||||
|
|
||||
|
import { defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { Button, Card, DirectoryTree, Empty, Select } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBlobContainersApi } from '../../api/useBlobContainersApi'; |
||||
|
import { useBlobsApi } from '../../api/useBlobsApi'; |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'containerChange', id: string): void; |
||||
|
(event: 'folderChange', id?: string): void; |
||||
|
}>(); |
||||
|
|
||||
|
interface Folder { |
||||
|
children?: Folder[]; |
||||
|
id: string; |
||||
|
isLeaf?: boolean; |
||||
|
key: string; |
||||
|
name: string; |
||||
|
path: string; |
||||
|
title: string; |
||||
|
} |
||||
|
|
||||
|
const { getPagedListApi: getContainersApi } = useBlobContainersApi(); |
||||
|
const { getFolderPagedListApi: getFoldersApi } = useBlobsApi(); |
||||
|
|
||||
|
const rootFolder: Folder = { |
||||
|
id: '', |
||||
|
isLeaf: false, |
||||
|
key: './', |
||||
|
name: './', |
||||
|
path: '/', |
||||
|
title: $t('BlobManagement.Blobs:RootFolder'), |
||||
|
children: [], |
||||
|
}; |
||||
|
|
||||
|
const containerId = ref<string>(''); |
||||
|
const loadedFolders = ref<string[]>([]); |
||||
|
const expandedFolders = ref<string[]>([]); |
||||
|
const selectedFolders = ref<string[]>([]); |
||||
|
const containers = ref<BlobContainerDto[]>([]); |
||||
|
const folders = ref<Folder[]>([ |
||||
|
{ |
||||
|
...rootFolder, |
||||
|
}, |
||||
|
]); |
||||
|
|
||||
|
const isLoadingContainers = ref(false); |
||||
|
const isLoadingFolders = ref(false); |
||||
|
|
||||
|
const [BlobFolderModal, modalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent( |
||||
|
() => import('./BlobFolderModal.vue'), |
||||
|
), |
||||
|
}); |
||||
|
|
||||
|
const clearTreeState = () => { |
||||
|
folders.value = [ |
||||
|
{ |
||||
|
...rootFolder, |
||||
|
}, |
||||
|
]; |
||||
|
loadedFolders.value = []; |
||||
|
expandedFolders.value = []; |
||||
|
selectedFolders.value = []; |
||||
|
isLoadingFolders.value = false; |
||||
|
}; |
||||
|
|
||||
|
const onLoadChildFolders: TreeProps['loadData'] = async (treeNode) => { |
||||
|
if (isLoadingContainers.value || !containerId.value) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const nodeKey = treeNode.key?.toString(); |
||||
|
if (!nodeKey) return; |
||||
|
|
||||
|
if (loadedFolders.value.includes(nodeKey)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
isLoadingFolders.value = true; |
||||
|
const children = await getFolders(containerId.value, treeNode.dataRef?.id); |
||||
|
|
||||
|
if (treeNode.dataRef && containerId.value) { |
||||
|
treeNode.dataRef.children = children; |
||||
|
folders.value = [...folders.value]; |
||||
|
loadedFolders.value = [...loadedFolders.value, nodeKey]; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('Failed to load folders:', error); |
||||
|
if (treeNode.dataRef) { |
||||
|
treeNode.dataRef.children = []; |
||||
|
} |
||||
|
} finally { |
||||
|
isLoadingFolders.value = false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
async function onInit() { |
||||
|
try { |
||||
|
isLoadingContainers.value = true; |
||||
|
const { items } = await getContainersApi({ |
||||
|
maxResultCount: 1000, |
||||
|
}); |
||||
|
containers.value = items; |
||||
|
} catch (error) { |
||||
|
console.error('Failed to load containers:', error); |
||||
|
} finally { |
||||
|
isLoadingContainers.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function getFolders(containerId: string, parentId?: string) { |
||||
|
if (!containerId) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const { items } = await getFoldersApi({ |
||||
|
containerId, |
||||
|
parentId, |
||||
|
}); |
||||
|
return items.map((folder) => { |
||||
|
return { |
||||
|
isLeaf: false, |
||||
|
id: folder.id, |
||||
|
key: folder.id, |
||||
|
name: folder.name, |
||||
|
path: folder.path, |
||||
|
title: folder.name, |
||||
|
children: [], |
||||
|
}; |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
console.error('Failed to get folders:', error); |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onFolderExpand( |
||||
|
_: Key[], |
||||
|
info: { |
||||
|
expanded: boolean; |
||||
|
node: EventDataNode; |
||||
|
}, |
||||
|
) { |
||||
|
if (!info.expanded) { |
||||
|
const keys = [...loadedFolders.value]; |
||||
|
const findIndex = keys.lastIndexOf(info.node?.key?.toString()); |
||||
|
if (findIndex !== -1) { |
||||
|
keys.splice(findIndex, 1); |
||||
|
loadedFolders.value = keys; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function onFolderChange(folderId?: string) { |
||||
|
emits('folderChange', folderId); |
||||
|
} |
||||
|
|
||||
|
async function onContainerChange(newContainerId: string) { |
||||
|
clearTreeState(); |
||||
|
emits('containerChange', newContainerId); |
||||
|
await nextTick(); |
||||
|
} |
||||
|
|
||||
|
function onCreate() { |
||||
|
if (!containerId.value) { |
||||
|
return; |
||||
|
} |
||||
|
modalApi.setData({ |
||||
|
containerId: containerId.value, |
||||
|
parentId: selectedFolders.value[0], |
||||
|
}); |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
|
||||
|
function onFolderCreated(blob: BlobDto) { |
||||
|
const parentId = selectedFolders.value[0]; |
||||
|
if (parentId) { |
||||
|
const index = loadedFolders.value.indexOf(parentId); |
||||
|
if (index !== -1) { |
||||
|
loadedFolders.value.splice(index, 1); |
||||
|
} |
||||
|
|
||||
|
if (expandedFolders.value.includes(parentId)) { |
||||
|
const parentNode = findNodeById(folders.value, parentId); |
||||
|
if (parentNode && parentNode.children) { |
||||
|
parentNode.children = []; |
||||
|
folders.value = [...folders.value]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
emits('folderChange', blob.id); |
||||
|
} |
||||
|
|
||||
|
function findNodeById(nodes: Folder[], id: string): Folder | null { |
||||
|
for (const node of nodes) { |
||||
|
if (node.id === id) { |
||||
|
return node; |
||||
|
} |
||||
|
if (node.children) { |
||||
|
const found = findNodeById(node.children, id); |
||||
|
if (found) return found; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
watch(containerId, (newVal, oldVal) => { |
||||
|
if (newVal !== oldVal && newVal) { |
||||
|
onContainerChange(newVal); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
onMounted(onInit); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Card :title="$t('BlobManagement.Blobs:Folder')" class="h-full"> |
||||
|
<div class="flex h-full flex-col gap-2"> |
||||
|
<Select |
||||
|
:placeholder="$t('BlobManagement.Blobs:SelectContainer')" |
||||
|
:options="containers" |
||||
|
:field-names="{ label: 'name', value: 'id' }" |
||||
|
:loading="isLoadingContainers" |
||||
|
v-model:value="containerId" |
||||
|
@change="(e) => onContainerChange(e!.toString())" |
||||
|
/> |
||||
|
<Button |
||||
|
v-if="containerId" |
||||
|
block |
||||
|
type="primary" |
||||
|
ghost |
||||
|
:disabled="isLoadingFolders" |
||||
|
@click="onCreate" |
||||
|
> |
||||
|
{{ $t('BlobManagement.Blobs:CreateFolder') }} |
||||
|
</Button> |
||||
|
<div class="h-[calc(100%-100px)] overflow-auto"> |
||||
|
<!-- 添加 key 属性强制重新渲染树组件 --> |
||||
|
<DirectoryTree |
||||
|
v-if="containerId" |
||||
|
:key="containerId" |
||||
|
block-node |
||||
|
:expand-action="false" |
||||
|
v-model:expanded-keys="expandedFolders" |
||||
|
v-model:selected-keys="selectedFolders" |
||||
|
:loaded-keys="loadedFolders" |
||||
|
:tree-data="folders" |
||||
|
:load-data="onLoadChildFolders" |
||||
|
@select="(_, info) => onFolderChange(info.node?.dataRef?.id)" |
||||
|
@expand="onFolderExpand" |
||||
|
/> |
||||
|
<Empty v-else /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<BlobFolderModal @change="onFolderCreated" /> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
:deep(.ant-tree) { |
||||
|
.ant-tree-title { |
||||
|
white-space: normal; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-card-body) { |
||||
|
height: calc(100% - 56px); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,45 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { reactive, ref } from 'vue'; |
||||
|
|
||||
|
import { ColPage } from '@vben/common-ui'; |
||||
|
|
||||
|
import BlobFileTable from './BlobFileTable.vue'; |
||||
|
import BlobFolderTree from './BlobFolderTree.vue'; |
||||
|
|
||||
|
const containerId = ref<string>(''); |
||||
|
const folderId = ref<string>(); |
||||
|
|
||||
|
const props = reactive({ |
||||
|
leftCollapsible: false, |
||||
|
leftMaxWidth: 30, |
||||
|
leftMinWidth: 20, |
||||
|
leftWidth: 30, |
||||
|
resizable: true, |
||||
|
rightWidth: 70, |
||||
|
splitHandle: true, |
||||
|
splitLine: true, |
||||
|
}); |
||||
|
|
||||
|
function onContainerChange(val: string) { |
||||
|
containerId.value = val; |
||||
|
folderId.value = undefined; |
||||
|
} |
||||
|
|
||||
|
function onFolderChange(val?: string) { |
||||
|
folderId.value = val; |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<ColPage auto-content-height v-bind="props"> |
||||
|
<template #left> |
||||
|
<BlobFolderTree |
||||
|
@container-change="onContainerChange" |
||||
|
@folder-change="onFolderChange" |
||||
|
/> |
||||
|
</template> |
||||
|
<BlobFileTable :container-id="containerId" :folder-id="folderId" /> |
||||
|
</ColPage> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,59 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { BlobContainerDto } from '../../types/containers'; |
||||
|
|
||||
|
import { useVbenForm, useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { message } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBlobContainersApi } from '../../api/useBlobContainersApi'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'BlobContainerModal', |
||||
|
}); |
||||
|
|
||||
|
const emits = defineEmits<{ |
||||
|
(event: 'change', data: BlobContainerDto): void; |
||||
|
}>(); |
||||
|
|
||||
|
const { cancel, createApi } = useBlobContainersApi(); |
||||
|
|
||||
|
const [Form, formApi] = useVbenForm({ |
||||
|
handleSubmit: onSubmit, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
fieldName: 'name', |
||||
|
label: $t('BlobManagement.DisplayName:Name'), |
||||
|
rules: 'required', |
||||
|
}, |
||||
|
], |
||||
|
showDefaultActions: false, |
||||
|
}); |
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
onCancel: cancel, |
||||
|
onConfirm: async () => { |
||||
|
await formApi.validateAndSubmitForm(); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
async function onSubmit(values: Record<string, any>) { |
||||
|
try { |
||||
|
modalApi.setState({ submitting: true }); |
||||
|
const dto = await createApi({ name: values.name }); |
||||
|
message.success($t('AbpUi.SavedSuccessfully')); |
||||
|
emits('change', dto); |
||||
|
modalApi.close(); |
||||
|
} finally { |
||||
|
modalApi.setState({ submitting: false }); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal :title="$t('BlobManagement.BlobContainers:Create')"> |
||||
|
<Form /> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,189 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridListeners, VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { VbenFormProps } from '@vben/common-ui'; |
||||
|
|
||||
|
import type { BlobContainerDto } from '../../types/containers'; |
||||
|
|
||||
|
import { defineAsyncComponent, h, ref } from 'vue'; |
||||
|
|
||||
|
import { useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { formatToDateTime } from '@abp/core'; |
||||
|
import { useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'; |
||||
|
import { Button, message, Modal } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useBlobContainersApi } from '../../api/useBlobContainersApi'; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'BlobContainerTable', |
||||
|
}); |
||||
|
|
||||
|
const { cancel, deleteApi, getPagedListApi } = useBlobContainersApi(); |
||||
|
|
||||
|
const selectedKeys = ref<string[]>([]); |
||||
|
|
||||
|
const formOptions: VbenFormProps = { |
||||
|
collapsed: true, |
||||
|
collapsedRows: 2, |
||||
|
commonConfig: { |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
}, |
||||
|
}, |
||||
|
schema: [ |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
allowClear: true, |
||||
|
}, |
||||
|
fieldName: 'filter', |
||||
|
formItemClass: 'col-span-3 items-baseline', |
||||
|
label: $t('AbpUi.Search'), |
||||
|
}, |
||||
|
], |
||||
|
// 控制表单是否显示折叠按钮 |
||||
|
showCollapseButton: true, |
||||
|
// 按下回车时是否提交表单 |
||||
|
submitOnEnter: true, |
||||
|
}; |
||||
|
|
||||
|
const gridOptions: VxeGridProps<BlobContainerDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'name', |
||||
|
minWidth: 150, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:Name'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'creationTime', |
||||
|
formatter({ cellValue }) { |
||||
|
return cellValue ? formatToDateTime(cellValue) : ''; |
||||
|
}, |
||||
|
minWidth: 120, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:CreationTime'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'lastModificationTime', |
||||
|
formatter({ cellValue }) { |
||||
|
return cellValue ? formatToDateTime(cellValue) : ''; |
||||
|
}, |
||||
|
minWidth: 120, |
||||
|
sortable: true, |
||||
|
title: $t('BlobManagement.DisplayName:LastModificationTime'), |
||||
|
}, |
||||
|
{ |
||||
|
field: 'action', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'action' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 150, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async ({ page, sort }, formValues) => { |
||||
|
const sorting = sort.order ? `${sort.field} ${sort.order}` : undefined; |
||||
|
return await getPagedListApi({ |
||||
|
sorting, |
||||
|
maxResultCount: page.pageSize, |
||||
|
skipCount: (page.currentPage - 1) * page.pageSize, |
||||
|
...formValues, |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
custom: true, |
||||
|
export: true, |
||||
|
refresh: { |
||||
|
code: 'query', |
||||
|
}, |
||||
|
zoom: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const gridEvents: VxeGridListeners<BlobContainerDto> = { |
||||
|
checkboxAll: (params) => { |
||||
|
selectedKeys.value = params.records.map((record) => record.name); |
||||
|
}, |
||||
|
checkboxChange: (params) => { |
||||
|
selectedKeys.value = params.records.map((record) => record.name); |
||||
|
}, |
||||
|
sortChange: () => { |
||||
|
gridApi.query(); |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ |
||||
|
formOptions, |
||||
|
gridEvents, |
||||
|
gridOptions, |
||||
|
}); |
||||
|
|
||||
|
const [BlobContainerModal, modalApi] = useVbenModal({ |
||||
|
connectedComponent: defineAsyncComponent( |
||||
|
() => import('./BlobContainerModal.vue'), |
||||
|
), |
||||
|
}); |
||||
|
|
||||
|
function onCreate() { |
||||
|
modalApi.setData({}); |
||||
|
modalApi.open(); |
||||
|
} |
||||
|
|
||||
|
function onDelete(row: BlobContainerDto) { |
||||
|
Modal.confirm({ |
||||
|
centered: true, |
||||
|
content: $t('AbpUi.ItemWillBeDeletedMessageWithFormat', [row.name]), |
||||
|
onCancel: () => { |
||||
|
cancel(); |
||||
|
}, |
||||
|
onOk: async () => { |
||||
|
await deleteApi(row.id); |
||||
|
message.success($t('AbpUi.DeletedSuccessfully')); |
||||
|
await gridApi.query(); |
||||
|
}, |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Grid :table-title="$t('BlobManagement.BlobContainers')"> |
||||
|
<template #toolbar-tools> |
||||
|
<Button :icon="h(PlusOutlined)" type="primary" @click="onCreate"> |
||||
|
{{ $t('BlobManagement.BlobContainers:Create') }} |
||||
|
</Button> |
||||
|
</template> |
||||
|
<template #action="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
danger |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onDelete(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Delete') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
<BlobContainerModal @change="() => gridApi.query()" /> |
||||
|
</template> |
||||
|
|
||||
|
<style lang="scss" scoped></style> |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { default as BlobViewPage } from './blobs/BlobViewPage.vue'; |
||||
|
export { default as BlobContainerTable } from './containers/BlobContainerTable.vue'; |
||||
@ -0,0 +1,57 @@ |
|||||
|
declare module 'vue-simple-uploader'; |
||||
|
|
||||
|
declare module '@vue-office/docx' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/docx/lib/v3/index.js' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/excel' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/excel/lib/v3/index.js' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/pdf' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/pdf/lib/v3/index.js' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/pptx' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
|
|
||||
|
declare module '@vue-office/pptx/lib/v3/index.js' { |
||||
|
import { DefineComponent } from 'vue'; |
||||
|
|
||||
|
const component: DefineComponent<any>; |
||||
|
export default component; |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
export * from './api'; |
||||
|
export * from './components'; |
||||
|
export * from './types'; |
||||
@ -0,0 +1,78 @@ |
|||||
|
import type { |
||||
|
ExtensibleAuditedEntityDto, |
||||
|
PagedAndSortedResultRequestDto, |
||||
|
} from '@abp/core'; |
||||
|
|
||||
|
export enum BlobType { |
||||
|
File = 1, |
||||
|
Folder = 0, |
||||
|
} |
||||
|
|
||||
|
interface BlobDto extends ExtensibleAuditedEntityDto<string> { |
||||
|
contentType?: string; |
||||
|
downloadCount: number; |
||||
|
expirationTime?: Date; |
||||
|
name: string; |
||||
|
path: string; |
||||
|
size: number; |
||||
|
type: BlobType; |
||||
|
} |
||||
|
|
||||
|
interface BlobFolderCreateBaseDto { |
||||
|
name: string; |
||||
|
parentId?: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobFileCreateBaseDto { |
||||
|
compareMd5?: string; |
||||
|
file: Blob | File; |
||||
|
name: string; |
||||
|
parentId?: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobFileChunkCreateBaseDto extends BlobFileCreateBaseDto { |
||||
|
chunkNumber: number; |
||||
|
chunkSize: number; |
||||
|
currentChunkSize: number; |
||||
|
totalChunks: number; |
||||
|
} |
||||
|
|
||||
|
interface BlobFileCreateDto extends BlobFileCreateBaseDto { |
||||
|
containerId: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobFileChunkCreateDto extends BlobFileChunkCreateBaseDto { |
||||
|
containerId: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobFolderCreateDto extends BlobFolderCreateBaseDto { |
||||
|
containerId: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobDownloadByNameInput { |
||||
|
blobName: string; |
||||
|
containerName: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobGetPagedListInputBase extends PagedAndSortedResultRequestDto { |
||||
|
contentType?: string; |
||||
|
parentId?: string; |
||||
|
type?: BlobType; |
||||
|
} |
||||
|
|
||||
|
interface BlobGetPagedListInput extends BlobGetPagedListInputBase { |
||||
|
containerId: string; |
||||
|
} |
||||
|
|
||||
|
export type { |
||||
|
BlobDownloadByNameInput, |
||||
|
BlobDto, |
||||
|
BlobFileChunkCreateBaseDto, |
||||
|
BlobFileChunkCreateDto, |
||||
|
BlobFileCreateBaseDto, |
||||
|
BlobFileCreateDto, |
||||
|
BlobFolderCreateBaseDto, |
||||
|
BlobFolderCreateDto, |
||||
|
BlobGetPagedListInput, |
||||
|
BlobGetPagedListInputBase, |
||||
|
}; |
||||
@ -0,0 +1,24 @@ |
|||||
|
import type { |
||||
|
ExtensibleAuditedEntityDto, |
||||
|
IHasConcurrencyStamp, |
||||
|
PagedAndSortedResultRequestDto, |
||||
|
} from '@abp/core'; |
||||
|
|
||||
|
interface BlobContainerDto |
||||
|
extends ExtensibleAuditedEntityDto<string>, IHasConcurrencyStamp { |
||||
|
name: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobContainerCreateDto { |
||||
|
name: string; |
||||
|
} |
||||
|
|
||||
|
interface BlobContainerGetPagedListInput extends PagedAndSortedResultRequestDto { |
||||
|
filter?: string; |
||||
|
} |
||||
|
|
||||
|
export type { |
||||
|
BlobContainerCreateDto, |
||||
|
BlobContainerDto, |
||||
|
BlobContainerGetPagedListInput, |
||||
|
}; |
||||
@ -0,0 +1,2 @@ |
|||||
|
export * from './blobs'; |
||||
|
export * from './containers'; |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/tsconfig", |
||||
|
"extends": "@vben/tsconfig/web.json", |
||||
|
"include": ["src"], |
||||
|
"exclude": ["node_modules"] |
||||
|
} |
||||
Loading…
Reference in new issue