Browse Source

feat(vben5): Add blob management package

pull/1470/head
colin 5 months ago
parent
commit
92ac7fe091
  1. 2
      apps/vben5/apps/app-antd/package.json
  2. 6
      apps/vben5/apps/app-antd/src/views/blob-management/blobs/index.vue
  3. 8
      apps/vben5/apps/app-antd/src/views/blob-management/containers/index.vue
  4. 50
      apps/vben5/packages/@abp/blob-management/package.json
  5. 2
      apps/vben5/packages/@abp/blob-management/src/api/index.ts
  6. 52
      apps/vben5/packages/@abp/blob-management/src/api/useBlobContainersApi.ts
  7. 79
      apps/vben5/packages/@abp/blob-management/src/api/useBlobsApi.ts
  8. 411
      apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFilePreviewModal.vue
  9. 301
      apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFileTable.vue
  10. 339
      apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFileUploadModal.vue
  11. 65
      apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFolderModal.vue
  12. 283
      apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFolderTree.vue
  13. 45
      apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobViewPage.vue
  14. 59
      apps/vben5/packages/@abp/blob-management/src/components/containers/BlobContainerModal.vue
  15. 189
      apps/vben5/packages/@abp/blob-management/src/components/containers/BlobContainerTable.vue
  16. 2
      apps/vben5/packages/@abp/blob-management/src/components/index.ts
  17. 57
      apps/vben5/packages/@abp/blob-management/src/global.d.ts
  18. 3
      apps/vben5/packages/@abp/blob-management/src/index.ts
  19. 78
      apps/vben5/packages/@abp/blob-management/src/types/blobs.ts
  20. 24
      apps/vben5/packages/@abp/blob-management/src/types/containers.ts
  21. 2
      apps/vben5/packages/@abp/blob-management/src/types/index.ts
  22. 6
      apps/vben5/packages/@abp/blob-management/tsconfig.json
  23. 9
      apps/vben5/packages/@abp/request/src/hooks/useErrorFormat.ts
  24. 29
      apps/vben5/packages/@abp/request/src/hooks/useWrapperResult.ts
  25. 5
      apps/vben5/pnpm-workspace.yaml

2
apps/vben5/apps/app-antd/package.json

@ -29,6 +29,7 @@
"@abp/account": "workspace:*",
"@abp/ai-management": "workspace:*",
"@abp/auditing": "workspace:*",
"@abp/blob-management": "workspace:*",
"@abp/cache-management": "workspace:*",
"@abp/components": "workspace:*",
"@abp/core": "workspace:*",
@ -39,7 +40,6 @@
"@abp/localization": "workspace:*",
"@abp/notifications": "workspace:*",
"@abp/openiddict": "workspace:*",
"@abp/oss": "workspace:*",
"@abp/permissions": "workspace:*",
"@abp/platform": "workspace:*",
"@abp/request": "workspace:*",

6
apps/vben5/apps/app-antd/src/views/oss/containers/index.vue → apps/vben5/apps/app-antd/src/views/blob-management/blobs/index.vue

@ -1,15 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { ContainerTable } from '@abp/oss';
import { BlobViewPage } from '@abp/blob-management';
defineOptions({
name: 'Vben5OssContainers',
name: 'Vben5BlobFiles',
});
</script>
<template>
<Page>
<ContainerTable />
<BlobViewPage />
</Page>
</template>

8
apps/vben5/apps/app-antd/src/views/oss/objects/index.vue → apps/vben5/apps/app-antd/src/views/blob-management/containers/index.vue

@ -1,15 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { ObjectPage } from '@abp/oss';
import { BlobContainerTable } from '@abp/blob-management';
defineOptions({
name: 'Vben5OssObjects',
name: 'Vben5BlobContainers',
});
</script>
<template>
<Page auto-content-height>
<ObjectPage />
<Page>
<BlobContainerTable />
</Page>
</template>

50
apps/vben5/packages/@abp/blob-management/package.json

@ -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:"
}
}

2
apps/vben5/packages/@abp/blob-management/src/api/index.ts

@ -0,0 +1,2 @@
export { useBlobContainersApi } from './useBlobContainersApi';
export { useBlobsApi } from './useBlobsApi';

52
apps/vben5/packages/@abp/blob-management/src/api/useBlobContainersApi.ts

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

79
apps/vben5/packages/@abp/blob-management/src/api/useBlobsApi.ts

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

411
apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFilePreviewModal.vue

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

301
apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFileTable.vue

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

339
apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFileUploadModal.vue

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

65
apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFolderModal.vue

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

283
apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobFolderTree.vue

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

45
apps/vben5/packages/@abp/blob-management/src/components/blobs/BlobViewPage.vue

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

59
apps/vben5/packages/@abp/blob-management/src/components/containers/BlobContainerModal.vue

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

189
apps/vben5/packages/@abp/blob-management/src/components/containers/BlobContainerTable.vue

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

2
apps/vben5/packages/@abp/blob-management/src/components/index.ts

@ -0,0 +1,2 @@
export { default as BlobViewPage } from './blobs/BlobViewPage.vue';
export { default as BlobContainerTable } from './containers/BlobContainerTable.vue';

57
apps/vben5/packages/@abp/blob-management/src/global.d.ts

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

3
apps/vben5/packages/@abp/blob-management/src/index.ts

@ -0,0 +1,3 @@
export * from './api';
export * from './components';
export * from './types';

78
apps/vben5/packages/@abp/blob-management/src/types/blobs.ts

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

24
apps/vben5/packages/@abp/blob-management/src/types/containers.ts

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

2
apps/vben5/packages/@abp/blob-management/src/types/index.ts

@ -0,0 +1,2 @@
export * from './blobs';
export * from './containers';

6
apps/vben5/packages/@abp/blob-management/tsconfig.json

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"],
"exclude": ["node_modules"]
}

9
apps/vben5/packages/@abp/request/src/hooks/useErrorFormat.ts

@ -17,6 +17,12 @@ export function useErrorFormat(response: AxiosResponse) {
/** 如果请求错误,抛出异常 */
function throwIfError(): void {
if (!hasError()) return;
throw Object.assign({}, response, { message: getErrorMessage(), response });
}
/** 获取错误消息 */
function getErrorMessage(): string | undefined {
if (!hasError()) return undefined;
const errorJson = data.error as RemoteServiceErrorInfo;
let errorMessage = errorJson.message;
if (errorJson.validationErrors) {
@ -24,11 +30,12 @@ export function useErrorFormat(response: AxiosResponse) {
.map((error) => error.message)
.join('\n');
}
throw Object.assign({}, response, { message: errorMessage, response });
return errorMessage;
}
return {
hasError,
getErrorMessage,
throwIfError,
};
}

29
apps/vben5/packages/@abp/request/src/hooks/useWrapperResult.ts

@ -3,9 +3,20 @@ import type { AxiosResponse } from 'axios';
import { useErrorFormat } from './useErrorFormat';
export function useWrapperResult(response: AxiosResponse) {
const { hasError, throwIfError: throwIfAbpError } = useErrorFormat(response);
const {
hasError: hasAbpError,
getErrorMessage: getAbpErrorMessage,
throwIfError: throwIfAbpError,
} = useErrorFormat(response);
const _defaultWrapperHeaderKey: string = '_abpwrapresult';
const { data, headers } = response;
/** 是否请求错误 */
function hasError(): boolean {
if (hasAbpError()) return true;
return Reflect.has(data, 'code') && data.code !== '0';
}
/** 是否已包装结果 */
function hasWrapResult(): boolean {
if (
@ -14,7 +25,7 @@ export function useWrapperResult(response: AxiosResponse) {
) {
return String(headers[_defaultWrapperHeaderKey]).includes('true');
}
return hasError();
return hasAbpError();
}
/** 获取包装结果 */
@ -26,8 +37,7 @@ export function useWrapperResult(response: AxiosResponse) {
/** 如果请求错误,抛出异常 */
function throwIfError(): void {
throwIfAbpError();
const hasSuccess = data && Reflect.has(data, 'code') && data.code === '0';
if (!hasSuccess) {
if (hasError()) {
const content = data.details || data.message;
throw Object.assign({}, response, {
response: {
@ -41,8 +51,19 @@ export function useWrapperResult(response: AxiosResponse) {
}
}
/** 获取错误消息 */
function getErrorMessage(): string | undefined {
if (!hasWrapResult()) return undefined;
const errorMessage = getAbpErrorMessage();
if (errorMessage) return errorMessage;
if (!hasError()) return undefined;
return data.message;
}
return {
getData,
getErrorMessage,
hasError,
hasWrapResult,
};
}

5
apps/vben5/pnpm-workspace.yaml

@ -83,6 +83,10 @@ catalog:
'@vite-pwa/vitepress': ^1.1.0
'@vitejs/plugin-vue': ^6.0.4
'@vitejs/plugin-vue-jsx': ^5.1.4
'@vue-office/docx': ^1.6.3
'@vue-office/excel': ^1.7.14
'@vue-office/pdf': ^2.0.10
'@vue-office/pptx': ^1.0.1
'@vue/shared': ^3.5.27
'@vue/test-utils': ^2.4.6
'@vueuse/core': ^14.1.0
@ -229,6 +233,7 @@ catalog:
vitepress-plugin-group-icons: ^1.7.1
vitest: ^3.2.4
vue: ^3.5.27
vue-demi: ^0.14.6
vue-eslint-parser: ^10.2.0
vue-i18n: ^11.2.8
vue-json-viewer: ^3.0.4

Loading…
Cancel
Save