refact: 代码重构

This commit is contained in:
overtrue
2025-05-05 17:01:08 +08:00
parent 71f33eeee3
commit ca64d6c6ce
6 changed files with 224 additions and 242 deletions
+8 -77
View File
@@ -1,87 +1,18 @@
<template>
<n-button text v-if="total > 0" @click="toggleDrawer">
<div v-if="pending.length" class="flex items-center gap-2">
<n-spin :size="14" />
<span>{{ t('In Progress', { total: total, deleting: deleting.length, completed: completed.length }) }}</span>
</div>
<div v-else>{{ t('Delete Completed', { completed: completed.length, failed: failed.length }) }}</div>
</n-button>
<n-drawer v-model:show="showDrawer" :width="502">
<n-drawer-content :title="t('Management')" closable>
<div class="flex flex-col gap-4">
<n-alert type="warning" :show-icon="false">
<p><span class="text-red-500">{{ t('Browser Warning') }}</span></p>
<p><span class="text-red-500">{{ t('Cache Warning') }}</span></p>
</n-alert>
<div v-if="total > 0">
<div class="flex items-center justify-between">
<div>{{ total - (pending.length + deleting.length) }}/{{ total }}</div>
<n-button text type="info" @click="clearTasks">{{ t('Clear Records') }}</n-button>
</div>
<n-progress type="line" :height="2" :percentage="percentage" :show-indicator="false" :processing="percentage < 100" />
</div>
<n-tabs type="line" animated v-model:value="tab">
<n-tab-pane :tab="t('Pending', { count: pending.length })" name="pending">
<object-delete-task-list :tasks="pending" />
</n-tab-pane>
<n-tab-pane :tab="t('Deleting', { count: deleting.length })" name="deleting">
<object-delete-task-list :tasks="deleting" />
</n-tab-pane>
<n-tab-pane :tab="t('Completed', { count: completed.length })" name="completed">
<object-delete-task-list :tasks="completed" />
</n-tab-pane>
<n-tab-pane :tab="t('Failed', { count: failed.length })" name="failed">
<object-delete-task-list :tasks="failed" />
</n-tab-pane>
</n-tabs>
</div>
</n-drawer-content>
</n-drawer>
<object-task-stats :tasks="tasks" processing-status="deleting" :on-clear-tasks="clearTasks">
<template #task-list="{ tasks }">
<object-delete-task-list :tasks="tasks" />
</template>
</object-task-stats>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
const { t } = useI18n()
const showDrawer = ref(false)
<script setup lang="ts">
import { computed } from 'vue'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
const store = useDeleteTaskManagerStore()
const tasks = computed(() => store.tasks)
const total = computed(() => tasks.value.length)
const percentage = computed(() => total.value === 0 ? 100 : Math.floor((completed.value.length / total.value) * 100))
const pending = computed(() => tasks.value.filter(task => task.status === 'pending'))
const deleting = computed(() => tasks.value.filter(task => task.status === 'deleting'))
const completed = computed(() => tasks.value.filter(task => task.status === 'completed'))
const failed = computed(() => tasks.value.filter(task => task.status === 'failed'))
const tab = ref('pending')
// 根据任务执行状态切换任务列表,如果有进行中的任务则默认展示进行中,否则展示已完成
watch(percentage, () => {
if (deleting.value.length > 0) {
tab.value = 'deleting'
} else {
tab.value = 'completed'
}
})
// 第一次加入任务时自动打开抽屉
const autoOpened = ref(false)
watch(total, (newVal, oldVal) => {
if (!autoOpened.value && !showDrawer.value && oldVal === 0 && newVal > 0) {
showDrawer.value = true
autoOpened.value = true
}
})
const toggleDrawer = () => {
showDrawer.value = !showDrawer.value
}
const clearTasks = () => {
store.clearTasks()
}
+71 -48
View File
@@ -58,58 +58,81 @@ const loading = ref<boolean>(false)
const message = useMessage()
const textMimes = [
'application/json', 'application/xml',
'application/javascript', 'application/x-sh',
'application/x-csh', 'application/x-python',
'application/x-php', 'application/x-perl',
'application/x-shellscript', 'application/x-ruby',
'application/x-java', 'application/x-cpp',
]
// 文件类型配置
const fileTypeConfig = {
text: {
mimes: [
'application/json', 'application/xml',
'application/javascript', 'application/x-sh',
'application/x-csh', 'application/x-python',
'application/x-php', 'application/x-perl',
'application/x-shellscript', 'application/x-ruby',
'application/x-java', 'application/x-cpp',
],
extensions: [
'.txt', '.json', '.xml', '.js', '.sh', '.csh',
'.py', '.php', '.pl', '.sh', '.rb', '.java',
'.cpp', '.h', '.hpp', '.c', '.cc', '.hh',
'.hxx', '.cxx', '.java', '.kt', '.scala',
'.groovy', '.rs', '.go', '.dart', '.swift',
'.m', '.mm', '.cs', '.ts', '.jsx', '.tsx',
'.html', '.htm', '.css', '.scss', '.sass',
'.less', '.styl', '.yaml', '.yml', '.toml',
'.ini', '.cfg', '.conf', '.properties', '.md',
'.markdown', '.rst', '.r',
]
},
image: {
extensions: [
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg',
]
},
audio: {
extensions: [
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
]
},
pdf: {
extensions: [
'.pdf',
]
}
};
const textExtensions = [
'.txt', '.json', '.xml', '.js', '.sh', '.csh',
'.py', '.php', '.pl', '.sh', '.rb', '.java',
'.cpp', '.h', '.hpp', '.c', '.cc', '.hh',
'.hxx', '.cxx', '.java', '.kt', '.scala',
'.groovy', '.rs', '.go', '.dart', '.swift',
'.m', '.mm', '.cs', '.ts', '.jsx', '.tsx',
'.html', '.htm', '.css', '.scss', '.sass',
'.less', '.styl', '.yaml', '.yml', '.toml',
'.ini', '.cfg', '.conf', '.properties', '.md',
'.markdown', '.rst', '.r',
]
// 文件类型判断函数
function isFileType(contentType: string | undefined, fileName: string, type: keyof typeof fileTypeConfig): boolean {
if (type === 'text') {
return contentType?.startsWith('text/') ||
fileTypeConfig.text.mimes.some(mime => contentType?.includes(mime)) ||
fileTypeConfig.text.extensions.some(ext => fileName.endsWith(ext));
}
const imageExtensions = [
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg',
]
if (type === 'image') {
return contentType?.startsWith('image/') ||
fileTypeConfig.image.extensions.some(ext => fileName.endsWith(ext));
}
const audioExtensions = [
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
]
if (type === 'audio') {
return contentType?.startsWith('audio/') ||
fileTypeConfig.audio.extensions.some(ext => fileName.endsWith(ext));
}
const pdfExtensions = [
'.pdf',
]
if (type === 'pdf') {
return contentType === 'application/pdf' ||
fileTypeConfig.pdf.extensions.some(ext => fileName.endsWith(ext));
}
// MIME 类型判断逻辑
const isImage = computed(() => {
return contentType.value?.startsWith('image/') || imageExtensions.some(ext => props.objectKey.endsWith(ext))
})
const isVideo = computed(() => contentType.value?.startsWith('video/') || false)
const isAudio = computed(() => {
return contentType.value?.startsWith('audio/') || audioExtensions.some(ext => props.objectKey.endsWith(ext))
})
const isPdf = computed(() => {
return contentType.value === 'application/pdf' || pdfExtensions.some(ext => props.objectKey.endsWith(ext))
})
const isText = computed(() => {
// 常见文本类型,可按需扩展
return contentType.value?.startsWith('text/') || textMimes.some(mime => contentType.value?.includes(mime)) ||
textExtensions.some(ext => props.objectKey.endsWith(ext))
})
return false;
}
const canPreview = computed(() => isImage.value || isPdf.value || isText.value || isVideo.value || isAudio.value)
// 使用新的判断函数
const isImage = computed(() => isFileType(contentType.value, props.objectKey, 'image'));
const isVideo = computed(() => contentType.value?.startsWith('video/') || false);
const isAudio = computed(() => isFileType(contentType.value, props.objectKey, 'audio'));
const isPdf = computed(() => isFileType(contentType.value, props.objectKey, 'pdf'));
const isText = computed(() => isFileType(contentType.value, props.objectKey, 'text'));
const canPreview = computed(() => isImage.value || isPdf.value || isText.value || isVideo.value || isAudio.value);
// 当 show 为 true 时触发加载逻辑
watch(() => props.show, async (newVal) => {
@@ -128,12 +151,12 @@ async function loadPreview() {
try {
// 获取预签名URL
const url = await getSignedUrl(objectKey.value)
const url = await getSignedUrl(props.objectKey)
previewUrl.value = url
// 先使用 HEAD 请求获取 Content-Type
const response = await headObject(objectKey.value)
const response = await headObject(props.objectKey)
const ctype = response?.ContentType
contentType.value = ctype || 'application/octet-stream'
+91
View File
@@ -0,0 +1,91 @@
<template>
<n-button text v-if="total > 0" @click="toggleDrawer">
<div v-if="pending.length" class="flex items-center gap-2">
<n-spin :size="14" />
<span>{{ t('In Progress', { total: total, processing: processing.length, completed: completed.length }) }}</span>
</div>
<div v-else>{{ t('Task Completed', { completed: completed.length, failed: failed.length }) }}</div>
</n-button>
<n-drawer v-model:show="showDrawer" :width="502">
<n-drawer-content :title="t('Task Management')" closable>
<div class="flex flex-col gap-4">
<n-alert type="warning" :show-icon="false">
<p><span class="text-red-500">{{ t('Browser Warning') }}</span></p>
<p><span class="text-red-500">{{ t('Cache Warning') }}</span></p>
</n-alert>
<div v-if="total > 0">
<div class="flex items-center justify-between">
<div>{{ total - (pending.length + processing.length) }}/{{ total }}</div>
<n-button text type="info" @click="clearTasks">{{ t('Clear Records') }}</n-button>
</div>
<n-progress type="line" :height="2" :percentage="percentage" :show-indicator="false" :processing="percentage < 100" />
</div>
<n-tabs type="line" animated v-model:value="tab">
<n-tab-pane :tab="t('Pending', { count: pending.length })" name="pending">
<slot name="task-list" :tasks="pending" />
</n-tab-pane>
<n-tab-pane :tab="t('Processing', { count: processing.length })" name="processing">
<slot name="task-list" :tasks="processing" />
</n-tab-pane>
<n-tab-pane :tab="t('Completed', { count: completed.length })" name="completed">
<slot name="task-list" :tasks="completed" />
</n-tab-pane>
<n-tab-pane :tab="t('Failed', { count: failed.length })" name="failed">
<slot name="task-list" :tasks="failed" />
</n-tab-pane>
</n-tabs>
</div>
</n-drawer-content>
</n-drawer>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
tasks: any[]
processingStatus: string
onClearTasks: () => void
}>()
const { t } = useI18n()
const showDrawer = ref(false)
const total = computed(() => props.tasks.length)
const percentage = computed(() => total.value === 0 ? 100 : Math.floor((completed.value.length / total.value) * 100))
const pending = computed(() => props.tasks.filter(task => task.status === 'pending'))
const processing = computed(() => props.tasks.filter(task => task.status === props.processingStatus))
const completed = computed(() => props.tasks.filter(task => task.status === 'completed'))
const failed = computed(() => props.tasks.filter(task => task.status === 'failed'))
const tab = ref('pending')
// 根据任务执行状态切换任务列表
watch(percentage, () => {
if (processing.value.length > 0) {
tab.value = 'processing'
} else {
tab.value = 'completed'
}
})
// 第一次加入任务时自动打开抽屉
const autoOpened = ref(false)
watch(total, (newVal, oldVal) => {
if (!autoOpened.value && !showDrawer.value && oldVal === 0 && newVal > 0) {
showDrawer.value = true
autoOpened.value = true
}
})
const toggleDrawer = () => {
showDrawer.value = !showDrawer.value
}
const clearTasks = () => {
props.onClearTasks()
}
</script>
+8 -77
View File
@@ -1,87 +1,18 @@
<template>
<n-button text v-if="total > 0" @click="toggleDrawer">
<div v-if="pending.length" class="flex items-center gap-2">
<n-spin :size="14" />
<span>{{ t('Task In Progress', { uploading: uploading.length, completed: completed.length }) }}</span>
</div>
<div v-else>{{ t('Upload Completed', { completed: completed.length, failed: failed.length }) }}</div>
</n-button>
<n-drawer v-model:show="showDrawer" :width="502">
<n-drawer-content :title="t('Task Management')" closable>
<div class="flex flex-col gap-4">
<n-alert type="warning" :show-icon="false">
<p>{{ t('Browser Warning') }}</p>
<p>{{ t('Cache Warning') }}</p>
</n-alert>
<div v-if="total > 0">
<div class="flex items-center justify-between">
<div>{{ total - (pending.length + uploading.length) }}/{{ total }}</div>
<n-button text type="info" @click="clearTasks">{{ t('Clear Records') }}</n-button>
</div>
<n-progress type="line" :height="2" :percentage="percentage" :show-indicator="false" :processing="percentage < 100" />
</div>
<n-tabs type="line" animated v-model:value="tab">
<n-tab-pane :tab="t('Pending', { count: pending.length })" name="pending">
<object-upload-task-list :tasks="pending" />
</n-tab-pane>
<n-tab-pane :tab="t('Uploading', { count: uploading.length })" name="uploading">
<object-upload-task-list :tasks="uploading" />
</n-tab-pane>
<n-tab-pane :tab="t('Completed', { count: completed.length })" name="completed">
<object-upload-task-list :tasks="completed" />
</n-tab-pane>
<n-tab-pane :tab="t('Failed', { count: failed.length })" name="failed">
<object-upload-task-list :tasks="failed" />
</n-tab-pane>
</n-tabs>
</div>
</n-drawer-content>
</n-drawer>
<object-task-stats :tasks="tasks" processing-status="uploading" :on-clear-tasks="clearTasks">
<template #task-list="{ tasks }">
<object-upload-task-list :tasks="tasks" />
</template>
</object-task-stats>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
const { t } = useI18n()
const showDrawer = ref(false)
<script setup lang="ts">
import { computed } from 'vue'
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
const store = useUploadTaskManagerStore()
const tasks = computed(() => store.tasks)
const total = computed(() => tasks.value.length)
const percentage = computed(() => total.value === 0 ? 100 : Math.floor((completed.value.length / total.value) * 100))
const pending = computed(() => tasks.value.filter(task => task.status === 'pending'))
const uploading = computed(() => tasks.value.filter(task => task.status === 'uploading'))
const completed = computed(() => tasks.value.filter(task => task.status === 'completed'))
const failed = computed(() => tasks.value.filter(task => task.status === 'failed'))
const tab = ref('pending')
// 根据任务执行状态切换任务列表,如果有进行中的任务则默认展示进行中,否则展示已完成
watch(percentage, () => {
if (uploading.value.length > 0) {
tab.value = 'uploading'
} else {
tab.value = 'completed'
}
})
// 第一次加入任务时自动打开抽屉
const autoOpened = ref(false)
watch(total, (newVal, oldVal) => {
if (!autoOpened.value && !showDrawer.value && oldVal === 0 && newVal > 0) {
showDrawer.value = true
autoOpened.value = true
}
})
const toggleDrawer = () => {
showDrawer.value = !showDrawer.value
}
const clearTasks = () => {
store.clearTasks()
}
+1 -1
View File
@@ -359,7 +359,6 @@
"If no versions remain, delete references to this object": "如果没有剩余版本,则删除此对象的引用",
"Version Cleanup": "版本清理",
"Delete all expired versions": "删除所有过期的版本",
"Expiration": "过期",
"Transition": "转换",
"Add Bucket Replication Rule": "添加存储桶复制规则",
"Definitions": "定义",
@@ -452,6 +451,7 @@
"Search Access Key": "搜索访问密钥",
"Add Access Key": "添加访问密钥",
"Expiration": "过期时间",
"Processing": "进行中",
"Disabled": "已禁用",
"Description": "描述",
"Are you sure you want to delete all selected keys?": "您确定要删除所有选中的密钥吗?",
+45 -39
View File
@@ -59,29 +59,39 @@ class BucketAccessPolicy {
}
}
export function isValidStatement(statement: Statement, bucketName: string): boolean {
if (Array.from(statement.Actions).every(action => !validActions.has(action))) {
return false;
}
// 提取公共的判断逻辑
function hasValidPrincipal(statement: Statement): boolean {
return statement.Principal.AWS?.has('*') || false;
}
if (statement.Effect !== 'Allow') {
return false;
}
function hasValidEffect(statement: Statement): boolean {
return statement.Effect === 'Allow';
}
if (!statement.Principal.AWS || !statement.Principal.AWS.has('*')) {
return false;
}
function hasValidActions(statement: Statement, validActions: Set<string>): boolean {
return Array.from(statement.Actions).every(action => validActions.has(action));
}
function hasValidResource(statement: Statement, bucketName: string): boolean {
const bucketResource = `${awsResourcePrefix}${bucketName}`;
if (statement.Resources.has(bucketResource)) {
return true;
return statement.Resources.has(bucketResource) ||
Array.from(statement.Resources).some(resource => resource.startsWith(bucketResource + '/'));
}
export function isValidStatement(statement: Statement, bucketName: string): boolean {
if (!hasValidActions(statement, validActions)) {
return false;
}
if (Array.from(statement.Resources).some(resource => resource.startsWith(bucketResource + '/'))) {
return true;
if (!hasValidEffect(statement)) {
return false;
}
return false;
if (!hasValidPrincipal(statement)) {
return false;
}
return hasValidResource(statement, bucketName);
}
export function newBucketStatement(policy: BucketPolicy, bucketName: string, prefix: string): Statement[] {
@@ -259,7 +269,7 @@ export function removeStatements(statements: Statement[], bucketName: string, pr
out.push(statement);
continue;
}
if (statement.Resources.has(bucketResource)) {
@@ -378,22 +388,22 @@ export function appendStatement(statements: Statement[], statement: Statement):
function appendStatement(statements: Statement[], statement: Statement): Statement[] {
for (let i = 0; i < statements.length; i++) {
const s = statements[i];
// 第一个合并条件
if (areSetsEqual(s.Actions, statement.Actions) &&
s.Effect === statement.Effect &&
areSetsEqual(s.Principal.AWS, statement.Principal.AWS) &&
deepEqual(s.Conditions, statement.Conditions)) {
statements[i].Resources = new Set([...s.Resources, ...statement.Resources]);
return statements;
}
}
// 第二个合并条件
else if (areSetsEqual(s.Resources, statement.Resources) &&
s.Effect === statement.Effect &&
areSetsEqual(s.Principal.AWS, statement.Principal.AWS) &&
deepEqual(s.Conditions, statement.Conditions)) {
statements[i].Actions = new Set([...s.Actions, ...statement.Actions]);
return statements;
}
@@ -401,17 +411,17 @@ function appendStatement(statements: Statement[], statement: Statement): Stateme
// 处理交集条件
const resourceIntersection = setIntersection(s.Resources, statement.Resources);
const actionIntersection = setIntersection(s.Actions, statement.Actions);
const principalIntersection = setIntersection(s.Principal.AWS as Set<string>, statement.Principal.AWS as Set<string>);
const principalIntersection = setIntersection(s.Principal.AWS as Set<string>, statement.Principal.AWS as Set<string>);
if (areSetsEqual(resourceIntersection, statement.Resources) &&
areSetsEqual(actionIntersection, statement.Actions) &&
s.Effect === statement.Effect &&
areSetsEqual(principalIntersection, statement.Principal.AWS)) {
if (deepEqual(s.Conditions, statement.Conditions)) {
return statements;
}
if (s.Conditions && statement.Conditions) {
if (areSetsEqual(s.Resources, statement.Resources)) {
statements[i].Conditions = mergeConditionMap(s.Conditions, statement.Conditions);
@@ -463,30 +473,26 @@ export function appendStatements(statements: Statement[], appendStatements: Stat
for (const s of appendStatements) {
statements = appendStatement(statements, s);
}
console.log("🚀 ~ appendStatements ~ statements:", statements)
console.log("🚀 ~ appendStatements ~ statements:", statements)
return statements;
}
export function getBucketPolicy(statement: Statement, prefix: string): [boolean, boolean, boolean] {
if (!(statement.Effect === 'Allow' && statement.Principal.AWS?.has('*'))) {
if (!hasValidEffect(statement) || !hasValidPrincipal(statement)) {
return [false, false, false];
}
const commonFound = Array.from(statement.Actions).every(action => commonBucketActions.has(action)) && !statement.Conditions;
const writeOnly = Array.from(statement.Actions).every(action => writeOnlyBucketActions.has(action)) && !statement.Conditions;
const commonFound = hasValidActions(statement, commonBucketActions) && !statement.Conditions;
const writeOnly = hasValidActions(statement, writeOnlyBucketActions) && !statement.Conditions;
let readOnly = false;
if (Array.from(statement.Actions).every(action => readOnlyBucketActions.has(action))) {
if (hasValidActions(statement, readOnlyBucketActions)) {
if (prefix && statement.Conditions) {
const stringEqualsValue = statement.Conditions['StringEquals'];
const values = stringEqualsValue ? stringEqualsValue['s3:prefix'] || new Set() : new Set();
if (values.has(prefix)) {
readOnly = true;
}
} else if (!prefix && !statement.Conditions) {
readOnly = true;
} else if (prefix && !statement.Conditions) {
readOnly = values.has(prefix);
} else if (!prefix || !statement.Conditions) {
readOnly = true;
}
}
@@ -495,9 +501,9 @@ export function getBucketPolicy(statement: Statement, prefix: string): [boolean,
}
export function getObjectPolicy(statement: Statement): [boolean, boolean] {
if (statement.Effect === 'Allow' && statement.Principal.AWS?.has('*') && !statement.Conditions) {
const readOnly = Array.from(statement.Actions).every(action => readOnlyObjectActions.has(action));
const writeOnly = Array.from(statement.Actions).every(action => writeOnlyObjectActions.has(action));
if (hasValidEffect(statement) && hasValidPrincipal(statement) && !statement.Conditions) {
const readOnly = hasValidActions(statement, readOnlyObjectActions);
const writeOnly = hasValidActions(statement, writeOnlyObjectActions);
return [readOnly, writeOnly];
}
return [false, false];
@@ -552,7 +558,7 @@ export function getPolicy(statements: Statement[], bucketName: string, prefix: s
}
let policy: BucketPolicy = BucketPolicyConstants.None;
console.log("🚀 ~ getPolicy ~ bucketCommonFound:", bucketCommonFound)
console.log("🚀 ~ getPolicy ~ bucketCommonFound:", bucketCommonFound)
if (bucketCommonFound) {
if (bucketReadOnly && bucketWriteOnly && objReadOnly && objWriteOnly) {