feat: 添加对象保留功能及相关界面优化

This commit is contained in:
马登山
2025-06-05 15:28:19 +08:00
parent 0d18c2b040
commit ced0726ed1
7 changed files with 252 additions and 81 deletions
+100 -7
View File
@@ -17,14 +17,10 @@
<span>{{ t("Tags") }}</span>
</n-button>
<!-- <n-button v-if="lockStatus" @click="() => true == true">
<n-button v-if="lockStatus" @click="() => (showRetentionView = true)">
<Icon name="ri:save-2-line" class="mr-2" />
<span>{{ t("Retention") }}</span>
</n-button>
<n-button v-if="lockStatus" @click="() => true == true">
<Icon name="ri:auction-line" class="mr-2" />
<span>{{ t("Legal Hold") }}</span>
</n-button> -->
<n-button id="copyTag" ref="copyRef" @click="copySignUrl">
<Icon name="ri:file-copy-line" class="mr-2" />
@@ -64,7 +60,14 @@
</n-descriptions-item>
<!-- 保留 -->
<n-descriptions-item :label="t('Retention') + t('Policy')">compliance</n-descriptions-item>
<n-descriptions-item :label="t('Retention') + t('Policy')">
<n-space>
<span>{{ t("Retention Type") + ": " + retentionMode }}</span>
</n-space>
<n-space>
<span>{{ t("Retention RetainUntilDate") + ": " + retainUntilDate }}</span>
</n-space>
</n-descriptions-item>
<!-- <n-descriptions-item label="版本ID">{{ object?.VersionId }}</n-descriptions-item>
<n-descriptions-item label="存储类型">{{ object?.StorageClass }}</n-descriptions-item> -->
@@ -105,6 +108,42 @@
</n-form>
</n-card>
</n-modal>
<n-modal
v-model:show="showRetentionView"
preset="card"
:title="t('Retention') + t('Policy')"
class="max-w-screen-md">
<n-card class="flex-col justify-center items-center">
<n-form
ref="retentionFormRef"
:label-width="200"
label-placement="left"
label-align="left"
class="w-[500px]">
<n-form-item :label="t('Retention Mode')" path="retentionMode" class="flex-auto">
<n-radio-group v-model:value="retentionMode">
<n-radio value="COMPLIANCE">Compliance</n-radio>
<n-radio value="GOVERNANCE">Governance</n-radio>
</n-radio-group>
</n-form-item>
<n-form-item :label="t('Retention RetainUntilDate')" path="retainUntilDate" class="flex-auto">
<n-date-picker
v-model:formatted-value="retainUntilDate"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetime"
clearable />
</n-form-item>
<n-form-item>
<n-button type="primary" @click="submitRetention">{{ t("Confirm") }}</n-button>
<n-button class="mx-4" @click="resetRetention">{{ t("Reset") }}</n-button>
<n-button @click="showRetentionView = false">{{ t("Cancel") }}</n-button>
</n-form-item>
</n-form>
</n-card>
</n-modal>
</n-card>
</n-drawer-content>
</n-drawer>
@@ -112,6 +151,7 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
const { t } = useI18n();
const visibel = ref(false);
@@ -178,6 +218,7 @@ const getObjectLockConfig = async () => {
// 如果桶开启了object lock,则获取合法保留状态
if (lockStatus.value) {
getObjectLegalHoldFn();
getObjectRetentionFn();
}
} else {
lockStatus.value = false;
@@ -211,7 +252,59 @@ const handleChangeLegalStatus = () => {
});
};
/****************************************************************** */
/**======================================================= */
// 保留相关
const retentionFormRef = ref();
const retentionMode = ref<string | null>(null);
const retainUntilDate = ref<string | null>(null);
console.log("🚀 ~ retainUntilDate:", retainUntilDate.value);
const retentionLoading = ref(false);
const showRetentionView = ref(false);
// 获取保留状态
const getObjectRetentionFn = () => {
const { getObjectRetention } = useObject({
bucket: bucketName.value,
});
getObjectRetention(key.value).then((res) => {
if (res.Retention) {
retentionMode.value = res.Retention.Mode || "";
retainUntilDate.value = res.Retention.RetainUntilDate
? dayjs(res.Retention.RetainUntilDate).format("YYYY-MM-DD HH:mm:ss")
: null;
} else {
retentionMode.value = "";
}
});
};
const submitRetention = () => {
retentionLoading.value = true;
const { putObjectRetention } = useObject({
bucket: bucketName.value,
});
putObjectRetention(key.value, {
Mode: retentionMode.value,
RetainUntilDate: retainUntilDate.value ? new Date(retainUntilDate.value) : null,
})
.then(() => {
message.success(t("Retention Update Success"));
getObjectRetentionFn();
})
.finally(() => {
retentionLoading.value = false;
showRetentionView.value = false;
});
};
const resetRetention = () => {
retentionMode.value = null;
retainUntilDate.value = null;
};
/**======================================================= */
// 删除标签
const handledeleteTag = async (index: number) => {
+70 -41
View File
@@ -38,10 +38,24 @@
</div>
</template>
</n-page-header>
<n-data-table class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="filteredObjects" :row-key="rowKey" @update:checked-row-keys="handleCheck"
:pagination="false" :bordered="false" />
<object-upload-picker :show="uploadPickerVisible" @update:show="(val: any) => (uploadPickerVisible = val && refresh())" :bucketName="bucketName" :prefix="prefix" />
<object-new-form :show="newObjectFormVisible" :asPrefix="newObjectAsPrefix" @update:show="(val: any) => (newObjectFormVisible = val && refresh())" :bucketName="bucketName"
<n-data-table
class="border dark:border-neutral-700 rounded overflow-hidden"
:columns="columns"
:data="filteredObjects"
:row-key="rowKey"
@update:checked-row-keys="handleCheck"
:pagination="false"
:bordered="false" />
<object-upload-picker
:show="uploadPickerVisible"
@update:show="(val: any) => (uploadPickerVisible = val && refresh())"
:bucketName="bucketName"
:prefix="prefix" />
<object-new-form
:show="newObjectFormVisible"
:asPrefix="newObjectAsPrefix"
@update:show="(val: any) => (newObjectFormVisible = val && refresh())"
:bucketName="bucketName"
:prefix="prefix" />
<n-button-group class="ml-auto">
<n-button @click="goToPreviousPage" :disabled="!continuationToken">
@@ -58,15 +72,15 @@
<script setup lang="ts">
const { $s3Client } = useNuxtApp();
import { useAsyncData, useRoute, useRouter } from "#app"
import { NuxtLink } from "#components"
import { ListObjectsV2Command, type _Object, type CommonPrefix } from "@aws-sdk/client-s3"
import dayjs from "dayjs"
import type { DataTableColumns, DataTableRowKey } from 'naive-ui'
import { joinRelativeURL } from "ufo"
import { computed, ref, watch, type VNode } from "vue"
import { useDeleteTaskManagerStore } from "~/store/delete-tasks"
import { useUploadTaskManagerStore } from "~/store/upload-tasks"
import { useAsyncData, useRoute, useRouter } from "#app";
import { NuxtLink } from "#components";
import { ListObjectsV2Command, type _Object, type CommonPrefix } from "@aws-sdk/client-s3";
import dayjs from "dayjs";
import type { DataTableColumns, DataTableRowKey } from "naive-ui";
import { joinRelativeURL } from "ufo";
import { computed, ref, watch, type VNode } from "vue";
import { useDeleteTaskManagerStore } from "~/store/delete-tasks";
import { useUploadTaskManagerStore } from "~/store/upload-tasks";
const route = useRoute();
const router = useRouter();
@@ -77,7 +91,7 @@ const props = defineProps<{ bucket: string; path: string }>();
const uploadPickerVisible = ref(false);
const newObjectFormVisible = ref(false);
const newObjectAsPrefix = ref(false);
const searchTerm = ref('');
const searchTerm = ref("");
// Add debounce function for search
const debounce = (fn: Function, delay: number) => {
@@ -107,8 +121,16 @@ const deleteTaskStore = useDeleteTaskManagerStore();
const deleteTasks = computed(() => deleteTaskStore.tasks);
// 当任务变化时,刷新数据
watch(() => uploadTasks, () => setTimeout(refresh, 500), { deep: true });
watch(() => deleteTasks, () => setTimeout(refresh, 500), { deep: true });
watch(
() => uploadTasks,
() => setTimeout(refresh, 500),
{ deep: true }
);
watch(
() => deleteTasks,
() => setTimeout(refresh, 500),
{ deep: true }
);
// bucketName
const bucketName = computed(() => props.bucket as string);
@@ -139,7 +161,6 @@ interface RowData {
type: "prefix" | "object";
Size: number;
LastModified: string;
}
const infoRef = ref();
@@ -159,13 +180,19 @@ const columns: DataTableColumns<RowData> = [
}
const keyInUri = row.Key;
return h(NuxtLink, {
href: row.type === "prefix" ? bucketPath(keyInUri) : '', class: "block text-cyan-400 cursor-pointer", onClick: (e: MouseEvent) => {
if (row.type === "prefix") return
infoRef.value.openDrawer(bucketName.value, row.Key)
return
}
}, () => label);
return h(
NuxtLink,
{
href: row.type === "prefix" ? bucketPath(keyInUri) : "",
class: "block text-cyan-400 cursor-pointer",
onClick: (e: MouseEvent) => {
if (row.type === "prefix") return;
infoRef.value.openDrawer(bucketName.value, row.Key);
return;
},
},
() => label
);
},
},
{ key: "Size", title: "大小", render: (row: { Size: number }) => (row.Size ? formatBytes(row.Size) : "") },
@@ -173,7 +200,7 @@ const columns: DataTableColumns<RowData> = [
key: "LastModified",
title: "更新时间",
render: (row: { LastModified: string }) => {
return row.LastModified ? dayjs(row.LastModified).format('YYYY-MM-DD HH:mm:ss') : "";
return row.LastModified ? dayjs(row.LastModified).format("YYYY-MM-DD HH:mm:ss") : "";
},
},
];
@@ -185,14 +212,14 @@ interface ListObjectsResponse {
isTruncated: boolean;
}
const randomString = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
const salt = ref(randomString())
const salt = ref(randomString());
// 在服务端获取数据
const { data, refresh } = await useAsyncData<ListObjectsResponse>(
`objectsData-${salt.value}&${prefix.value}&${pageSize.value}&${continuationToken.value}`,
@@ -256,7 +283,7 @@ const filteredObjects = computed(() => {
}
const term = searchTerm.value.toLowerCase();
return objects.value.filter(obj => {
return objects.value.filter((obj) => {
const displayKey = prefix.value ? obj.Key?.substring(prefix.value.length) : obj.Key;
return displayKey?.toLowerCase().includes(term);
});
@@ -287,20 +314,22 @@ function handleBatchDelete() {
return;
}
try {
await Promise.all(checkedKeys.value.map(async (item) => {
const findOne = objects.value.find((obj) => obj.Key === item);
// 目录删除
// 递归查询目录下的所有文件,然后删除
if (findOne?.type === "prefix" && findOne?.Key) {
return await objectApi.mapAllFiles(bucketName.value, findOne.Key, (fileKey: string) => {
deleteTaskStore.addKeys([fileKey], bucketName.value);
});
}
await Promise.all(
checkedKeys.value.map(async (item) => {
const findOne = objects.value.find((obj) => obj.Key === item);
// 目录删除
// 递归查询目录下的所有文件,然后删除
if (findOne?.type === "prefix" && findOne?.Key) {
return await objectApi.mapAllFiles(bucketName.value, findOne.Key, (fileKey: string) => {
deleteTaskStore.addKeys([fileKey], bucketName.value);
});
}
return deleteTaskStore.addKeys([String(item)], bucketName.value);
}));
return deleteTaskStore.addKeys([String(item)], bucketName.value);
})
);
message.success("删除中");
salt.value = randomString()
salt.value = randomString();
refresh();
} catch (error) {
message.error("删除失败");