mirror of
https://github.com/rustfs/console.git
synced 2026-08-30 17:14:47 +08:00
+180
-117
@@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<n-drawer v-model:show="visibel" :width="502">
|
||||
<n-drawer-content :title="t('Bucket Configuration')+`(${bucketName})`" closable>
|
||||
<n-drawer-content :title="t('Bucket Configuration') + `(${bucketName})`" closable>
|
||||
<n-descriptions :column="2" label-placement="top" bordered label-class="w-1/2">
|
||||
<n-descriptions-item>
|
||||
<template #label>
|
||||
<span>{{ t("Access Policy") }}</span>
|
||||
<span>{{ t('Access Policy') }}</span>
|
||||
<n-button class="align-middle" quaternary round type="primary" @click="editPolicy">
|
||||
<Icon name="ri:edit-2-line" class="mr-2" />
|
||||
</n-button>
|
||||
</template>
|
||||
{{ policyOptions.find((item) => item.value === bucketPolicy)?.label }}
|
||||
{{ policyOptions.find(item => item.value === bucketPolicy)?.label }}
|
||||
</n-descriptions-item>
|
||||
|
||||
<n-descriptions-item>
|
||||
<template #label>
|
||||
<span class="mr-2">{{ t("Encryption") }}</span>
|
||||
<span class="mr-2">{{ t('Encryption') }}</span>
|
||||
<n-button class="align-middle" quaternary round type="primary" @click="editEncript">
|
||||
<Icon name="ri:edit-2-line" class="mr-2" />
|
||||
</n-button>
|
||||
</template>
|
||||
{{ t("Disabled") }}
|
||||
{{ t('Disabled') }}
|
||||
</n-descriptions-item>
|
||||
<!-- <n-descriptions-item>
|
||||
<template #label>
|
||||
@@ -32,7 +32,7 @@
|
||||
</n-descriptions-item> -->
|
||||
<n-descriptions-item class="w-1/2">
|
||||
<template #label>
|
||||
{{ t("Tag") }}
|
||||
{{ t('Tag') }}
|
||||
<n-button class="align-middle" round quaternary type="primary" @click="addTag">
|
||||
<Icon name="ri:add-line" size="16" class="mr-2" />
|
||||
</n-button>
|
||||
@@ -43,23 +43,25 @@
|
||||
type="info"
|
||||
@click="editTag(index)"
|
||||
closable
|
||||
@close="handledeleteTag(index)">
|
||||
@close="handledeleteTag(index)"
|
||||
>
|
||||
{{ tag.Key }}:{{ tag.Value }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
|
||||
<n-descriptions-item>
|
||||
<template #label>{{ t("Object Lock") }}</template>
|
||||
<template #label>{{ t('Object Lock') }}</template>
|
||||
<n-switch
|
||||
:disabled="true"
|
||||
v-model:value="lockStatus"
|
||||
:loading="objectLockLoading"
|
||||
:round="false"
|
||||
@update:value="handleChangeVersionStatus" />
|
||||
@update:value="handleChangeVersionStatus"
|
||||
/>
|
||||
</n-descriptions-item>
|
||||
|
||||
<n-descriptions-item>
|
||||
<template #label>{{ t("Version Control") }}</template>
|
||||
<template #label>{{ t('Version Control') }}</template>
|
||||
<n-switch
|
||||
v-model:value="versioningStatus"
|
||||
:disabled="lockStatus == true"
|
||||
@@ -67,39 +69,40 @@
|
||||
unchecked-value="Suspended"
|
||||
:round="false"
|
||||
:loading="statusLoading"
|
||||
@update:value="handleChangeVersionStatus" />
|
||||
@update:value="handleChangeVersionStatus"
|
||||
/>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<!-- retention -->
|
||||
<n-descriptions :column="2" label-placement="top" bordered label-class="w-1/2">
|
||||
<n-descriptions-item>
|
||||
<template #label>{{ t("Retention") }}</template>
|
||||
<template #label>{{ t('Retention') }}</template>
|
||||
<n-tag type="success" size="small" v-if="retentionEnabled" @click="editRetention">
|
||||
{{ t("Enabled") }}
|
||||
{{ t('Enabled') }}
|
||||
<template #icon>
|
||||
<Icon name="ri:checkbox-circle-fill" />
|
||||
</template>
|
||||
</n-tag>
|
||||
<n-tag type="error" size="small" v-else @click="editRetention">
|
||||
{{ t("Disabled") }}
|
||||
{{ t('Disabled') }}
|
||||
<template #icon>
|
||||
<Icon name="ri:close-circle-fill" />
|
||||
</template>
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item>
|
||||
<template #label>{{ t("Retention Mode") }}</template>
|
||||
{{ t(retentionFormValue.retentionMode || "") }}
|
||||
<template #label>{{ t('Retention Mode') }}</template>
|
||||
{{ t(retentionFormValue.retentionMode || '') }}
|
||||
</n-descriptions-item>
|
||||
|
||||
<n-descriptions-item>
|
||||
<template #label>{{ t("Retention Unit") }}</template>
|
||||
{{ t(retentionFormValue.retentionUnit || "") }}
|
||||
<template #label>{{ t('Retention Unit') }}</template>
|
||||
{{ t(retentionFormValue.retentionUnit || '') }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item>
|
||||
<template #label>{{ t("Retention Period") }}</template>
|
||||
{{ retentionFormValue.retentionPeriod || "" }}
|
||||
<template #label>{{ t('Retention Period') }}</template>
|
||||
{{ retentionFormValue.retentionPeriod || '' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-drawer-content>
|
||||
@@ -110,40 +113,56 @@
|
||||
:title="t('Set Policy')"
|
||||
preset="card"
|
||||
draggable
|
||||
:style="{ width: '750px' }">
|
||||
:style="{ width: '750px' }"
|
||||
>
|
||||
<n-form
|
||||
ref="policyFormRef"
|
||||
:inline="policyFormValue.policy !== 'custom'"
|
||||
:label-width="80"
|
||||
:model="policyFormValue">
|
||||
:model="policyFormValue"
|
||||
>
|
||||
<n-form-item :label="t('Policy')" path="" class="flex-auto">
|
||||
<n-select
|
||||
v-model:value="policyFormValue.policy"
|
||||
:placeholder="t('Please select policy')"
|
||||
:options="policyOptions" />
|
||||
:options="policyOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :span="24" v-if="policyFormValue.policy == 'custom'" :label="t('Policy Content')" path="content">
|
||||
<n-scrollbar class="w-full max-h-[60vh]"><json-editor v-model="policyFormValue.content" /></n-scrollbar>
|
||||
<n-form-item
|
||||
:span="24"
|
||||
v-if="policyFormValue.policy == 'custom'"
|
||||
:label="t('Policy Content')"
|
||||
path="content"
|
||||
>
|
||||
<n-scrollbar class="w-full max-h-[60vh]"
|
||||
><json-editor v-model="policyFormValue.content"
|
||||
/></n-scrollbar>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitPolicyForm">{{ t("Confirm") }}</n-button>
|
||||
<n-button class="mx-4" @click="showPolicyModal = false">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="submitPolicyForm">{{ t('Confirm') }}</n-button>
|
||||
<n-button class="mx-4" @click="showPolicyModal = false">{{ t('Cancel') }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
|
||||
<!-- tag -->
|
||||
<n-modal v-model:show="showTagModal" :title="t('Set Tag')" preset="card" draggable :style="{ width: '550px' }">
|
||||
<n-modal
|
||||
v-model:show="showTagModal"
|
||||
:title="t('Set Tag')"
|
||||
preset="card"
|
||||
draggable
|
||||
:style="{ width: '550px' }"
|
||||
>
|
||||
<n-form ref="formRef" inline :label-width="80" :model="tagFormValue">
|
||||
<n-form-item :label="t('Tag Key')" path="name">
|
||||
<n-input v-model:value="tagFormValue.name" placeholder="输入标签key" />
|
||||
<n-input v-model:value="tagFormValue.name" :placeholder="t('Tag Key Placeholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Tag Value')" path="value">
|
||||
<n-input v-model:value="tagFormValue.value" :placeholder="t('Please enter tag value')" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitTagForm">{{ t("Confirm") }}</n-button>
|
||||
<n-button class="mx-4" @click="showTagModal = false">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="submitTagForm">{{ t('Confirm') }}</n-button>
|
||||
<n-button class="mx-4" @click="showTagModal = false">{{ t('Cancel') }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
@@ -154,21 +173,34 @@
|
||||
:title="t('Enable Storage Encryption')"
|
||||
preset="card"
|
||||
draggable
|
||||
:style="{ width: '550px' }">
|
||||
<n-form ref="encryptFormRef" label-placemen="left" label-width="auto" inline :model="encryptFormValue">
|
||||
:style="{ width: '550px' }"
|
||||
>
|
||||
<n-form
|
||||
ref="encryptFormRef"
|
||||
label-placemen="left"
|
||||
label-width="auto"
|
||||
inline
|
||||
:model="encryptFormValue"
|
||||
>
|
||||
<n-form-item :label="t('Encryption Type')" path="encrypt" class="flex-auto">
|
||||
<n-select
|
||||
v-model:value="encryptFormValue.encrypt"
|
||||
:placeholder="t('Please select encryption type')"
|
||||
:options="encryptOptions" />
|
||||
:options="encryptOptions"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="encryptFormValue.encrypt == 'SSE-KMS'" label="KMS Key ID" path="kmsKeyId" class="flex-auto">
|
||||
<n-form-item
|
||||
v-if="encryptFormValue.encrypt == 'SSE-KMS'"
|
||||
label="KMS Key ID"
|
||||
path="kmsKeyId"
|
||||
class="flex-auto"
|
||||
>
|
||||
<n-select v-model:value="encryptFormValue.kmsKeyId" placeholder="" :options="[]" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitEncryptForm">{{ t("Confirm") }}</n-button>
|
||||
<n-button class="mx-4" @click="showEncryptModal = false">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="submitEncryptForm">{{ t('Confirm') }}</n-button>
|
||||
<n-button class="mx-4" @click="showEncryptModal = false">{{ t('Cancel') }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
@@ -179,19 +211,25 @@
|
||||
:title="t('Set Retention')"
|
||||
preset="card"
|
||||
draggable
|
||||
:style="{ width: '550px' }">
|
||||
<n-form ref="retentionFormRef" label-placemen="left" label-width="auto" :model="retentionFormValue">
|
||||
:style="{ width: '550px' }"
|
||||
>
|
||||
<n-form
|
||||
ref="retentionFormRef"
|
||||
label-placemen="left"
|
||||
label-width="auto"
|
||||
:model="retentionFormValue"
|
||||
>
|
||||
<n-form-item :label="t('Retention Mode')" path="retentionMode" class="flex-auto">
|
||||
<n-radio-group v-model:value="retentionFormValue.retentionMode">
|
||||
<n-radio value="COMPLIANCE">{{ t("COMPLIANCE") }}</n-radio>
|
||||
<n-radio value="GOVERNANCE">{{ t("GOVERNANCE") }}</n-radio>
|
||||
<n-radio value="COMPLIANCE">{{ t('COMPLIANCE') }}</n-radio>
|
||||
<n-radio value="GOVERNANCE">{{ t('GOVERNANCE') }}</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item :label="t('Retention Unit')" path="retentionUnit" class="flex-auto">
|
||||
<n-radio-group v-model:value="retentionFormValue.retentionUnit">
|
||||
<n-radio value="Days">{{ t("DAYS") }}</n-radio>
|
||||
<n-radio value="Years">{{ t("YEARS") }}</n-radio>
|
||||
<n-radio value="Days">{{ t('DAYS') }}</n-radio>
|
||||
<n-radio value="Years">{{ t('YEARS') }}</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
@@ -200,8 +238,8 @@
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="submitRetentionForm">{{ t("Confirm") }}</n-button>
|
||||
<n-button class="mx-4" @click="showRetentionModal = false">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="submitRetentionForm">{{ t('Confirm') }}</n-button>
|
||||
<n-button class="mx-4" @click="showRetentionModal = false">{{ t('Cancel') }}</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
@@ -209,11 +247,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "#components";
|
||||
import { Icon } from '#components';
|
||||
const { t } = useI18n();
|
||||
const dialog = useDialog();
|
||||
const visibel = ref(false);
|
||||
const bucketName = ref("");
|
||||
const bucketName = ref('');
|
||||
const openDrawer = (bucket: string) => {
|
||||
visibel.value = true;
|
||||
bucketName.value = bucket;
|
||||
@@ -256,21 +294,24 @@ const retentionEnabled = ref(false);
|
||||
const getObjectLockConfig = async () => {
|
||||
objectLockLoading.value = true;
|
||||
getObjectLockConfiguration(bucketName.value)
|
||||
.then((res) => {
|
||||
.then(res => {
|
||||
if (res.ObjectLockConfiguration?.ObjectLockEnabled) {
|
||||
lockStatus.value = res.ObjectLockConfiguration?.ObjectLockEnabled == "Enabled" ? true : false;
|
||||
lockStatus.value =
|
||||
res.ObjectLockConfiguration?.ObjectLockEnabled == 'Enabled' ? true : false;
|
||||
if (res.ObjectLockConfiguration?.Rule) {
|
||||
retentionEnabled.value = true;
|
||||
retentionFormValue.value.retentionMode = res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Mode || null;
|
||||
retentionFormValue.value.retentionMode =
|
||||
res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Mode || null;
|
||||
retentionFormValue.value.retentionPeriod =
|
||||
res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Days ||
|
||||
res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Years ||
|
||||
null;
|
||||
retentionFormValue.value.retentionUnit = res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Years
|
||||
? "Years"
|
||||
retentionFormValue.value.retentionUnit = res.ObjectLockConfiguration?.Rule
|
||||
?.DefaultRetention?.Years
|
||||
? 'Years'
|
||||
: res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Days
|
||||
? "Days"
|
||||
: "";
|
||||
? 'Days'
|
||||
: '';
|
||||
}
|
||||
} else {
|
||||
lockStatus.value = false;
|
||||
@@ -288,23 +329,27 @@ const getObjectLockConfig = async () => {
|
||||
/**********object lock ***********************/
|
||||
|
||||
/******** policy ***********************/
|
||||
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from "~/utils/bucket-policy";
|
||||
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from '~/utils/bucket-policy';
|
||||
|
||||
const bucketPolicy = ref("public");
|
||||
const bucketPolicy = ref('public');
|
||||
const getbucketPolicy = async () => {
|
||||
try {
|
||||
const res = await getBucketPolicy(bucketName.value);
|
||||
if (res.Policy) {
|
||||
policyFormValue.value.content = res.Policy;
|
||||
|
||||
bucketPolicy.value = getBucketPolicyFn(JSON.parse(res.Policy).Statement, bucketName.value, "");
|
||||
bucketPolicy.value = getBucketPolicyFn(
|
||||
JSON.parse(res.Policy).Statement,
|
||||
bucketName.value,
|
||||
''
|
||||
);
|
||||
policyFormValue.value.policy = bucketPolicy.value;
|
||||
if (bucketPolicy.value == "none") {
|
||||
bucketPolicy.value = "custom";
|
||||
policyFormValue.value.policy = "custom";
|
||||
if (bucketPolicy.value == 'none') {
|
||||
bucketPolicy.value = 'custom';
|
||||
policyFormValue.value.policy = 'custom';
|
||||
}
|
||||
} else {
|
||||
bucketPolicy.value = "public";
|
||||
bucketPolicy.value = 'public';
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("Error fetching bucket policy:", error)
|
||||
@@ -312,8 +357,8 @@ const getbucketPolicy = async () => {
|
||||
};
|
||||
|
||||
const policyFormValue = ref({
|
||||
policy: "private",
|
||||
content: "{}",
|
||||
policy: 'private',
|
||||
content: '{}',
|
||||
});
|
||||
const showPolicyModal = ref(false);
|
||||
const editPolicy = () => {
|
||||
@@ -321,44 +366,49 @@ const editPolicy = () => {
|
||||
};
|
||||
|
||||
const submitPolicyForm = () => {
|
||||
if (policyFormValue.value.policy == "custom") {
|
||||
if (policyFormValue.value.policy == 'custom') {
|
||||
let polictStr = JSON.stringify(JSON.parse(policyFormValue.value.content));
|
||||
console.log(polictStr);
|
||||
putBucketPolicy(bucketName.value, polictStr)
|
||||
.then(() => {
|
||||
message.success("修改成功");
|
||||
message.success(t('Edit Success'));
|
||||
showPolicyModal.value = false;
|
||||
getbucketPolicy();
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error("修改失败: " + error.message);
|
||||
.catch(error => {
|
||||
message.error(t('Edit Failed') + ': ' + error.message);
|
||||
});
|
||||
} else {
|
||||
const policys = setBucketPolicy([], policyFormValue.value.policy as BucketPolicyType, bucketName.value, "");
|
||||
console.log("🚀 ~ policys:", policys);
|
||||
putBucketPolicy(bucketName.value, JSON.stringify({ Version: "2012-10-17", Statement: policys }))
|
||||
const policys = setBucketPolicy(
|
||||
[],
|
||||
policyFormValue.value.policy as BucketPolicyType,
|
||||
bucketName.value,
|
||||
''
|
||||
);
|
||||
console.log('🚀 ~ policys:', policys);
|
||||
putBucketPolicy(bucketName.value, JSON.stringify({ Version: '2012-10-17', Statement: policys }))
|
||||
.then(() => {
|
||||
message.success("修改成功");
|
||||
message.success(t('Edit Success'));
|
||||
showPolicyModal.value = false;
|
||||
getbucketPolicy();
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error("修改失败: " + error.message);
|
||||
.catch(error => {
|
||||
message.error(t('Edit Failed') + ': ' + error.message);
|
||||
});
|
||||
}
|
||||
};
|
||||
const policyOptions = [
|
||||
{
|
||||
label: t("Public"),
|
||||
value: "public",
|
||||
label: t('Public'),
|
||||
value: 'public',
|
||||
},
|
||||
{
|
||||
label: t("Private"),
|
||||
value: "private",
|
||||
label: t('Private'),
|
||||
value: 'private',
|
||||
},
|
||||
{
|
||||
label: t("Custom"),
|
||||
value: "custom",
|
||||
label: t('Custom'),
|
||||
value: 'custom',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -367,22 +417,22 @@ const policyOptions = [
|
||||
/********Encrypt ***********************/
|
||||
const showEncryptModal = ref(false);
|
||||
const encryptFormValue = ref({
|
||||
encrypt: "disabled",
|
||||
kmsKeyId: "",
|
||||
encrypt: 'disabled',
|
||||
kmsKeyId: '',
|
||||
});
|
||||
|
||||
const encryptOptions = [
|
||||
{
|
||||
label: t("Disabled"),
|
||||
value: "disabled",
|
||||
label: t('Disabled'),
|
||||
value: 'disabled',
|
||||
},
|
||||
{
|
||||
label: "SSE-KMS",
|
||||
value: "SSE-KMS",
|
||||
label: 'SSE-KMS',
|
||||
value: 'SSE-KMS',
|
||||
},
|
||||
{
|
||||
label: "SSE-S3",
|
||||
value: "SSE-S3",
|
||||
label: 'SSE-S3',
|
||||
value: 'SSE-S3',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -417,12 +467,16 @@ const submitEncryptForm = () => {
|
||||
// showEncryptModal.value = false;
|
||||
// });
|
||||
// }
|
||||
if (encryptFormValue.value.encrypt == "SSE-KMS") {
|
||||
message.error("您提供的 XML 格式不正确,或者未根据我们发布的架构进行验证。 (MasterKeyID 未找到 aws:kms)。");
|
||||
} else if (encryptFormValue.value.encrypt == "SSE-S3") {
|
||||
message.error("指定了服务器端加密,但S3未配置。");
|
||||
if (encryptFormValue.value.encrypt == 'SSE-KMS') {
|
||||
message.error(
|
||||
t(
|
||||
'The XML format you provided is incorrect, or has not been validated against our published schema. (MasterKeyID not found aws:kms).'
|
||||
)
|
||||
);
|
||||
} else if (encryptFormValue.value.encrypt == 'SSE-S3') {
|
||||
message.error(t('Server-side encryption is specified, but S3 is not configured.'));
|
||||
} else {
|
||||
message.success("修改成功");
|
||||
message.success(t('Edit Success'));
|
||||
showEncryptModal.value = false;
|
||||
}
|
||||
};
|
||||
@@ -430,7 +484,7 @@ const submitEncryptForm = () => {
|
||||
/********Encrypt ***********************/
|
||||
|
||||
/********versioning ***********************/
|
||||
const versioningStatus: any = ref("");
|
||||
const versioningStatus: any = ref('');
|
||||
const statusLoading = ref(false);
|
||||
// 获取版本控制状态
|
||||
const getVersioningStatus = async () => {
|
||||
@@ -438,7 +492,7 @@ const getVersioningStatus = async () => {
|
||||
const resp = await getBucketVersioning(bucketName.value);
|
||||
versioningStatus.value = resp.Status;
|
||||
} catch (error) {
|
||||
console.error("获取版本控制状态失败:", error);
|
||||
console.error('get version fail:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -446,12 +500,12 @@ const handleChangeVersionStatus = async (value: string) => {
|
||||
statusLoading.value = true;
|
||||
putBucketVersioning(bucketName.value, value)
|
||||
.then(() => {
|
||||
message.success("修改成功");
|
||||
message.success(t('Edit Success'));
|
||||
getVersioningStatus();
|
||||
})
|
||||
.finally(() => {
|
||||
statusLoading.value = false;
|
||||
versioningStatus.value = versioningStatus.value == "Suspended" ? "Enabled" : "Suspended";
|
||||
versioningStatus.value = versioningStatus.value == 'Suspended' ? 'Enabled' : 'Suspended';
|
||||
});
|
||||
};
|
||||
|
||||
@@ -466,8 +520,8 @@ interface Tag {
|
||||
const showTagModal = ref(false);
|
||||
|
||||
const tagFormValue = ref({
|
||||
name: "",
|
||||
value: "",
|
||||
name: '',
|
||||
value: '',
|
||||
});
|
||||
// 获取标签
|
||||
const tags = ref<Tag[]>([]);
|
||||
@@ -478,13 +532,13 @@ const getTags = async () => {
|
||||
|
||||
const addTag = () => {
|
||||
nowTagIndex.value = -1;
|
||||
tagFormValue.value = { name: "", value: "" }; // 清空表单
|
||||
tagFormValue.value = { name: '', value: '' }; // 清空表单
|
||||
showTagModal.value = true;
|
||||
};
|
||||
|
||||
const submitTagForm = () => {
|
||||
if (!tagFormValue.value.name || !tagFormValue.value.value) {
|
||||
message.error("请填写完整的标签信息");
|
||||
message.error(t('Please fill in complete tag information'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -492,16 +546,19 @@ const submitTagForm = () => {
|
||||
tags.value.push({ Key: tagFormValue.value.name, Value: tagFormValue.value.value });
|
||||
}
|
||||
if (nowTagIndex.value !== -1) {
|
||||
tags.value[nowTagIndex.value] = { Key: tagFormValue.value.name, Value: tagFormValue.value.value };
|
||||
tags.value[nowTagIndex.value] = {
|
||||
Key: tagFormValue.value.name,
|
||||
Value: tagFormValue.value.value,
|
||||
};
|
||||
}
|
||||
// 调用 putBucketTagging 接口
|
||||
putBucketTagging(bucketName.value, { TagSet: tags.value })
|
||||
.then(() => {
|
||||
showTagModal.value = false; // 关闭模态框
|
||||
message.success("标签更新成功");
|
||||
message.success(t('Tag Update Success'));
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error("标签更新失败: " + error.message);
|
||||
.catch(error => {
|
||||
message.error(t('Tag Update Failed') + ': ' + error.message);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -515,10 +572,10 @@ const editTag = (index: number) => {
|
||||
};
|
||||
const handledeleteTag = (index: number) => {
|
||||
dialog.error({
|
||||
title: "警告",
|
||||
content: "你确定要删除这个标签吗?",
|
||||
positiveText: "确定",
|
||||
negativeText: "取消",
|
||||
title: t('Warning'),
|
||||
content: t('Delete Tag Confirm'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
nowTagIndex.value = index;
|
||||
tags.value.splice(index, 1); // 从标签列表中删除
|
||||
@@ -526,10 +583,10 @@ const handledeleteTag = (index: number) => {
|
||||
// 调用 putBucketTagging 接口
|
||||
putBucketTagging(bucketName.value, { TagSet: tags.value })
|
||||
.then(() => {
|
||||
message.success("标签更新成功");
|
||||
message.success(t('Tag Update Success'));
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error("删除标签失败: " + error.message);
|
||||
.catch(error => {
|
||||
message.error(t('Tag Delete Failed') + ': ' + error.message);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -552,7 +609,7 @@ const retentionFormValue = ref<RetentionFormValue>({
|
||||
|
||||
const editRetention = () => {
|
||||
if (!lockStatus.value) {
|
||||
message.error("对象锁定未启用,无法设置保留");
|
||||
message.error(t('Object lock is not enabled, cannot set retention'));
|
||||
return;
|
||||
}
|
||||
showRetentionModal.value = true;
|
||||
@@ -565,21 +622,27 @@ const submitRetentionForm = () => {
|
||||
retentionFormValue.value.retentionPeriod == null ||
|
||||
retentionFormValue.value.retentionUnit == null
|
||||
) {
|
||||
message.error("请填写完整的保留信息");
|
||||
message.error(t('Please fill in complete retention information'));
|
||||
return;
|
||||
}
|
||||
|
||||
putObjectLockConfiguration(bucketName.value, {
|
||||
ObjectLockEnabled: "Enabled",
|
||||
ObjectLockEnabled: 'Enabled',
|
||||
Rule: {
|
||||
DefaultRetention: {
|
||||
Mode: retentionFormValue.value.retentionMode,
|
||||
Days: retentionFormValue.value.retentionUnit == "Days" ? retentionFormValue.value.retentionPeriod : null,
|
||||
Years: retentionFormValue.value.retentionUnit == "Years" ? retentionFormValue.value.retentionPeriod : null,
|
||||
Days:
|
||||
retentionFormValue.value.retentionUnit == 'Days'
|
||||
? retentionFormValue.value.retentionPeriod
|
||||
: null,
|
||||
Years:
|
||||
retentionFormValue.value.retentionUnit == 'Years'
|
||||
? retentionFormValue.value.retentionPeriod
|
||||
: null,
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
message.success("修改成功");
|
||||
message.success(t('Edit Success'));
|
||||
showRetentionModal.value = false;
|
||||
getObjectLockConfig();
|
||||
});
|
||||
|
||||
@@ -8,10 +8,16 @@
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-form class="my-4" ref="formRef" :model="formData" :rules="rules">
|
||||
<n-tabs default-value="expire" justify-content="space-evenly" type="line" @update:value="handleUpdateValue">
|
||||
<n-tabs
|
||||
default-value="expire"
|
||||
justify-content="space-evenly"
|
||||
type="line"
|
||||
@update:value="handleUpdateValue"
|
||||
>
|
||||
<n-tab-pane name="expire" :tab="t('Expiration')">
|
||||
<n-form-item :label="t('Object Version')" path="versionType" v-if="versioningStatus">
|
||||
<n-select v-model:value="formData.versionType" :options="versionOptions" />
|
||||
@@ -23,8 +29,9 @@
|
||||
v-model:value="formData.days"
|
||||
:min="1"
|
||||
:placeholder="t('Days')"
|
||||
style="width: 100px" />
|
||||
<span class="ms-4">{{ t("Days After") }}</span>
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span class="ms-4">{{ t('Days After') }}</span>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<!-- 生命周期 -->
|
||||
@@ -32,14 +39,18 @@
|
||||
<n-collapse>
|
||||
<n-collapse-item :title="t('More Configurations')" name="advanced">
|
||||
<n-form-item :label="t('Prefix')">
|
||||
<n-input v-model:value="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
<n-input
|
||||
v-model:value="formData.prefix"
|
||||
:placeholder="t('Please enter prefix')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Tags')">
|
||||
<n-dynamic-input
|
||||
v-model:value="formData.tags"
|
||||
preset="pair"
|
||||
:key-placeholder="t('Tag Name')"
|
||||
:value-placeholder="t('Tag Value')" />
|
||||
:value-placeholder="t('Tag Value')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
@@ -47,11 +58,13 @@
|
||||
<!-- 高级设置 -->
|
||||
<n-card class="my-4" v-if="formData.versionType == 'current'">
|
||||
<n-collapse>
|
||||
<n-collapse-item title="高级设置" name="advanced">
|
||||
<n-form-item label="删除标记处理">
|
||||
<n-collapse-item :title="t('Advanced Settings')" name="advanced">
|
||||
<n-form-item :label="t('Delete Marker Handling')">
|
||||
<n-space>
|
||||
<n-switch v-model:value="formData.expiredDeleteMark" :round="false" />
|
||||
<span class="ml-4 text-gray-500">如果没有留下任何版本,请删除对该对象的引用</span>
|
||||
<span class="ml-4 text-gray-500">{{
|
||||
t('If no versions remain, delete references to this object')
|
||||
}}</span>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
|
||||
@@ -76,8 +89,9 @@
|
||||
v-model:value="formData.days"
|
||||
:min="1"
|
||||
:placeholder="t('Days')"
|
||||
style="width: 100px" />
|
||||
<span class="ms-4">{{ t("Days After") }}</span>
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span class="ms-4">{{ t('Days After') }}</span>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Stroage Type')" path="storageType">
|
||||
@@ -89,14 +103,18 @@
|
||||
<n-collapse>
|
||||
<n-collapse-item :title="t('More Configurations')" name="advanced">
|
||||
<n-form-item :label="t('Prefix')">
|
||||
<n-input v-model:value="formData.prefix" :placeholder="t('Please enter prefix')" />
|
||||
<n-input
|
||||
v-model:value="formData.prefix"
|
||||
:placeholder="t('Please enter prefix')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('Tags')">
|
||||
<n-dynamic-input
|
||||
v-model:value="formData.tags"
|
||||
preset="pair"
|
||||
:key-placeholder="t('Tag Name')"
|
||||
:value-placeholder="t('Tag Value')" />
|
||||
:value-placeholder="t('Tag Value')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
@@ -106,8 +124,8 @@
|
||||
</n-form>
|
||||
|
||||
<n-space justify="center">
|
||||
<n-button @click="handleCancel">{{ t("Cancel") }}</n-button>
|
||||
<n-button type="primary" @click="handleSave">{{ t("Save") }}</n-button>
|
||||
<n-button @click="handleCancel">{{ t('Cancel') }}</n-button>
|
||||
<n-button type="primary" @click="handleSave">{{ t('Save') }}</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
@@ -124,22 +142,23 @@ import {
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NSelect,
|
||||
} from "naive-ui";
|
||||
} from 'naive-ui';
|
||||
// 使用 Web Crypto API 或 Node.js crypto 模块
|
||||
const randomUUID = () => {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// 兜底方案
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({});
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } =
|
||||
useBucket({});
|
||||
const { listTiers } = useTiers();
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
@@ -149,29 +168,29 @@ interface TierItem {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(["search"]);
|
||||
const emit = defineEmits(['search']);
|
||||
const formRef = ref();
|
||||
const formData = ref({
|
||||
ruleName: "",
|
||||
ruleName: '',
|
||||
type: null,
|
||||
versionType: "current",
|
||||
versionType: 'current',
|
||||
days: null,
|
||||
action: "expire",
|
||||
prefix: "",
|
||||
action: 'expire',
|
||||
prefix: '',
|
||||
expiredDeleteMark: false,
|
||||
deleteAllExpired: false,
|
||||
storageType: "",
|
||||
storageType: '',
|
||||
tags: [
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const versionOptions = ref([
|
||||
{ label: t("Current Version"), value: "current" },
|
||||
{ label: t("Non-current Version"), value: "non-current" },
|
||||
{ label: t('Current Version'), value: 'current' },
|
||||
{ label: t('Non-current Version'), value: 'non-current' },
|
||||
]);
|
||||
|
||||
// 表单验证规则
|
||||
@@ -180,21 +199,21 @@ const rules = {
|
||||
required: true,
|
||||
validator: (rule: any, value: any) => {
|
||||
if (value === null || value === undefined || value < 1) {
|
||||
return new Error(t("Please enter valid days"));
|
||||
return new Error(t('Please enter valid days'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
trigger: ["blur", "input", "change"],
|
||||
trigger: ['blur', 'input', 'change'],
|
||||
},
|
||||
storageType: {
|
||||
validator: (rule: any, value: any) => {
|
||||
// 只在 transition 模式下验证 storageType
|
||||
if (formData.value.action === "transition" && (!value || value === "")) {
|
||||
return new Error(t("Please select storage type"));
|
||||
if (formData.value.action === 'transition' && (!value || value === '')) {
|
||||
return new Error(t('Please select storage type'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
trigger: ["blur", "change"],
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -212,7 +231,7 @@ const props = defineProps({
|
||||
|
||||
watch(
|
||||
() => props.bucketName,
|
||||
(newVal) => {
|
||||
newVal => {
|
||||
// 获取是都开启版本管理
|
||||
getVersioningStatus();
|
||||
}
|
||||
@@ -228,10 +247,10 @@ defineExpose({
|
||||
});
|
||||
|
||||
const handleUpdateValue = (value: string) => {
|
||||
if (value === "expire") {
|
||||
formData.value.action = "expire";
|
||||
if (value === 'expire') {
|
||||
formData.value.action = 'expire';
|
||||
} else {
|
||||
formData.value.action = "transition";
|
||||
formData.value.action = 'transition';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -239,32 +258,36 @@ const handleSave = () => {
|
||||
formRef.value?.validate((errors: any) => {
|
||||
if (!errors) {
|
||||
// 额外的参数验证(与表单验证保持一致)
|
||||
if (formData.value.days === null || formData.value.days === undefined || formData.value.days < 1) {
|
||||
message.error(t("Please enter valid days"));
|
||||
if (
|
||||
formData.value.days === null ||
|
||||
formData.value.days === undefined ||
|
||||
formData.value.days < 1
|
||||
) {
|
||||
message.error(t('Please enter valid days'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
formData.value.action === "transition" &&
|
||||
(!formData.value.storageType || formData.value.storageType === "")
|
||||
formData.value.action === 'transition' &&
|
||||
(!formData.value.storageType || formData.value.storageType === '')
|
||||
) {
|
||||
message.error(t("Please select storage type"));
|
||||
message.error(t('Please select storage type'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 先获取当前的生命周期配置
|
||||
getBucketLifecycleConfiguration(props.bucketName)
|
||||
.then((currentConfig) => {
|
||||
.then(currentConfig => {
|
||||
// 创建新的规则
|
||||
const newRule: any = {
|
||||
ID: randomUUID(),
|
||||
Status: "Enabled",
|
||||
Status: 'Enabled',
|
||||
};
|
||||
|
||||
// 设置 Filter 或 Prefix(不能同时使用)
|
||||
console.log("formData.value.prefix:", formData.value.prefix);
|
||||
console.log("formData.value.tags:", formData.value.tags);
|
||||
console.log("formData.value.expiredDeleteMark:", formData.value.expiredDeleteMark);
|
||||
console.log('formData.value.prefix:', formData.value.prefix);
|
||||
console.log('formData.value.tags:', formData.value.tags);
|
||||
console.log('formData.value.expiredDeleteMark:', formData.value.expiredDeleteMark);
|
||||
|
||||
// 检查是否有有效的标签
|
||||
const validTags = formData.value.tags.filter((item: any) => item.key && item.value);
|
||||
@@ -300,8 +323,8 @@ const handleSave = () => {
|
||||
}
|
||||
|
||||
// 根据操作类型添加相应的配置
|
||||
if (formData.value.action === "expire") {
|
||||
if (formData.value.versionType === "non-current") {
|
||||
if (formData.value.action === 'expire') {
|
||||
if (formData.value.versionType === 'non-current') {
|
||||
newRule.NoncurrentVersionExpiration = {
|
||||
NoncurrentDays: formData.value.days,
|
||||
};
|
||||
@@ -322,7 +345,7 @@ const handleSave = () => {
|
||||
}
|
||||
} else {
|
||||
// transition 操作
|
||||
if (formData.value.versionType === "non-current") {
|
||||
if (formData.value.versionType === 'non-current') {
|
||||
newRule.NoncurrentVersionTransitions = [
|
||||
{
|
||||
NoncurrentDays: formData.value.days,
|
||||
@@ -339,12 +362,6 @@ const handleSave = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("formData.value.prefix:", formData.value.prefix);
|
||||
console.log("formData.value.tags:", formData.value.tags);
|
||||
console.log("formData.value.expiredDeleteMark:", formData.value.expiredDeleteMark);
|
||||
console.log("Final newRule:", JSON.stringify(newRule, null, 2));
|
||||
// return;
|
||||
|
||||
// 合并现有规则和新规则
|
||||
const existingRules = currentConfig.Rules || [];
|
||||
const updatedRules = [...existingRules, newRule];
|
||||
@@ -353,33 +370,32 @@ const handleSave = () => {
|
||||
const params = {
|
||||
Rules: updatedRules,
|
||||
};
|
||||
console.log("🚀 ~ .then ~ params:", params);
|
||||
|
||||
return putBucketLifecycleConfiguration(props.bucketName, params);
|
||||
})
|
||||
.then((res) => {
|
||||
.then(res => {
|
||||
visible.value = false;
|
||||
emit("search");
|
||||
message.success(t("Create Success"));
|
||||
emit('search');
|
||||
message.success(t('Create Success'));
|
||||
formData.value = {
|
||||
ruleName: "",
|
||||
ruleName: '',
|
||||
type: null,
|
||||
versionType: "current",
|
||||
versionType: 'current',
|
||||
days: null,
|
||||
action: "expire",
|
||||
action: 'expire',
|
||||
expiredDeleteMark: false,
|
||||
deleteAllExpired: false,
|
||||
storageType: "",
|
||||
prefix: "",
|
||||
storageType: '',
|
||||
prefix: '',
|
||||
tags: [
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
@@ -409,9 +425,9 @@ const versioningStatus: any = ref(false);
|
||||
const getVersioningStatus = async () => {
|
||||
try {
|
||||
const resp = await getBucketVersioning(props.bucketName);
|
||||
versioningStatus.value = resp.Status == "Enabled";
|
||||
versioningStatus.value = resp.Status == 'Enabled';
|
||||
} catch (error) {
|
||||
console.error("获取版本控制状态失败:", error);
|
||||
console.error('获取版本控制状态失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+79
-61
@@ -15,25 +15,25 @@
|
||||
<object-delete-stats />
|
||||
<n-button @click="() => handleNewObject(true)">
|
||||
<Icon name="ri:add-line" class="mr-2" />
|
||||
<span>{{ t("New Folder") }}</span>
|
||||
<span>{{ t('New Folder') }}</span>
|
||||
</n-button>
|
||||
<n-button @click="() => handleNewObject(false)">
|
||||
<Icon name="ri:add-line" class="mr-2" />
|
||||
<span>{{ t("New File") }}</span>
|
||||
<span>{{ t('New File') }}</span>
|
||||
</n-button>
|
||||
<n-button @click="() => (uploadPickerVisible = true)">
|
||||
<Icon name="ri:file-add-line" class="mr-2" />
|
||||
<span>{{ t("Upload File") + "/" + t("Folder") }}</span>
|
||||
<span>{{ t('Upload File') + '/' + t('Folder') }}</span>
|
||||
</n-button>
|
||||
<n-button :disabled="!checkedKeys.length" secondary @click="handleBatchDelete">
|
||||
<template #icon>
|
||||
<Icon name="ri:delete-bin-5-line"></Icon>
|
||||
</template>
|
||||
{{ t("Delete Selected") }}
|
||||
{{ t('Delete Selected') }}
|
||||
</n-button>
|
||||
<n-button @click="() => refresh()">
|
||||
<Icon name="ri:refresh-line" class="mr-2" />
|
||||
<span>{{ t("Refresh") }}</span>
|
||||
<span>{{ t('Refresh') }}</span>
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -45,35 +45,38 @@
|
||||
:row-key="rowKey"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
:pagination="false"
|
||||
:bordered="false" />
|
||||
:bordered="false"
|
||||
/>
|
||||
<object-upload-picker
|
||||
:show="uploadPickerVisible"
|
||||
@update:show="
|
||||
(val) => {
|
||||
val => {
|
||||
uploadPickerVisible = val;
|
||||
refresh();
|
||||
}
|
||||
"
|
||||
:bucketName="bucketName"
|
||||
:prefix="prefix" />
|
||||
:prefix="prefix"
|
||||
/>
|
||||
<object-new-form
|
||||
:show="newObjectFormVisible"
|
||||
:asPrefix="newObjectAsPrefix"
|
||||
@update:show="
|
||||
(val) => {
|
||||
val => {
|
||||
newObjectFormVisible = val;
|
||||
refresh();
|
||||
}
|
||||
"
|
||||
:bucketName="bucketName"
|
||||
:prefix="prefix" />
|
||||
:prefix="prefix"
|
||||
/>
|
||||
<n-button-group class="ml-auto">
|
||||
<n-button @click="goToPreviousPage" :disabled="!continuationToken">
|
||||
<Icon name="ri:arrow-left-s-line" class="mr-2" />
|
||||
<span>{{ t("Previous Page") }}</span>
|
||||
<span>{{ t('Previous Page') }}</span>
|
||||
</n-button>
|
||||
<n-button @click="goToNextPage" :disabled="!nextToken">
|
||||
<span>{{ t("Next Page") }}</span>
|
||||
<span>{{ t('Next Page') }}</span>
|
||||
<Icon name="ri:arrow-right-s-line" class="ml-2" />
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
@@ -83,16 +86,16 @@
|
||||
<script setup lang="ts">
|
||||
const { $s3Client } = useNuxtApp();
|
||||
const { t } = useI18n();
|
||||
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 { useBucket } from "~/composables/useBucket";
|
||||
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 { useBucket } from '~/composables/useBucket';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -103,7 +106,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) => {
|
||||
@@ -162,15 +165,19 @@ const handleNewObject = (asPrefix: boolean) => {
|
||||
|
||||
const bucketPath = (path?: string | Array<string>) => {
|
||||
if (Array.isArray(path)) {
|
||||
path = path.join("/");
|
||||
path = path.join('/');
|
||||
}
|
||||
|
||||
return joinRelativeURL("/browser", encodeURIComponent(bucketName.value), path ? encodeURIComponent(path) : "");
|
||||
return joinRelativeURL(
|
||||
'/browser',
|
||||
encodeURIComponent(bucketName.value),
|
||||
path ? encodeURIComponent(path) : ''
|
||||
);
|
||||
};
|
||||
|
||||
interface RowData {
|
||||
Key: string;
|
||||
type: "prefix" | "object";
|
||||
type: 'prefix' | 'object';
|
||||
Size: number;
|
||||
LastModified: string;
|
||||
}
|
||||
@@ -178,27 +185,30 @@ interface RowData {
|
||||
const infoRef = ref();
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
type: "selection",
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
key: "Key",
|
||||
title: t("Object"),
|
||||
render: (row: { Key: string; type: "prefix" | "object" }) => {
|
||||
key: 'Key',
|
||||
title: t('Object'),
|
||||
render: (row: { Key: string; type: 'prefix' | 'object' }) => {
|
||||
const displayKey = prefix.value ? row.Key.substring(prefix.value.length) : row.Key;
|
||||
let label: string | VNode = displayKey || "/";
|
||||
let label: string | VNode = displayKey || '/';
|
||||
|
||||
if (row.type === "prefix") {
|
||||
label = h("span", { class: "inline-flex items-center gap-2" }, [icon("ri:folder-line"), label]);
|
||||
if (row.type === 'prefix') {
|
||||
label = h('span', { class: 'inline-flex items-center gap-2' }, [
|
||||
icon('ri:folder-line'),
|
||||
label,
|
||||
]);
|
||||
}
|
||||
|
||||
const keyInUri = row.Key;
|
||||
return h(
|
||||
NuxtLink,
|
||||
{
|
||||
href: row.type === "prefix" ? bucketPath(keyInUri) : "",
|
||||
class: "block text-cyan-400 cursor-pointer",
|
||||
href: row.type === 'prefix' ? bucketPath(keyInUri) : '',
|
||||
class: 'block text-cyan-400 cursor-pointer',
|
||||
onClick: (e: MouseEvent) => {
|
||||
if (row.type === "prefix") return;
|
||||
if (row.type === 'prefix') return;
|
||||
infoRef.value.openDrawer(bucketName.value, row.Key);
|
||||
return;
|
||||
},
|
||||
@@ -207,12 +217,16 @@ const columns: DataTableColumns<RowData> = [
|
||||
);
|
||||
},
|
||||
},
|
||||
{ key: "Size", title: t("Size"), render: (row: { Size: number }) => (row.Size ? formatBytes(row.Size) : "") },
|
||||
{
|
||||
key: "LastModified",
|
||||
title: t("Update Time"),
|
||||
key: 'Size',
|
||||
title: t('Size'),
|
||||
render: (row: { Size: number }) => (row.Size ? formatBytes(row.Size) : ''),
|
||||
},
|
||||
{
|
||||
key: 'LastModified',
|
||||
title: t('Update Time'),
|
||||
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') : '';
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -224,8 +238,8 @@ 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));
|
||||
}
|
||||
@@ -239,7 +253,7 @@ const { data, refresh } = await useAsyncData<ListObjectsResponse>(
|
||||
const params = {
|
||||
Bucket: bucketName.value,
|
||||
MaxKeys: pageSize.value || 25,
|
||||
Delimiter: "/",
|
||||
Delimiter: '/',
|
||||
Prefix: prefix.value || undefined,
|
||||
ContinuationToken: continuationToken.value,
|
||||
};
|
||||
@@ -269,18 +283,18 @@ const nextToken = computed(() => data.value?.nextContinuationToken || null);
|
||||
|
||||
const objects = computed(() => {
|
||||
return commonPrefixes.value
|
||||
.map((prefix) => {
|
||||
.map(prefix => {
|
||||
return {
|
||||
Key: prefix.Prefix,
|
||||
type: "prefix",
|
||||
type: 'prefix',
|
||||
Size: 0,
|
||||
};
|
||||
})
|
||||
.concat(
|
||||
contents.value.map((object) => {
|
||||
contents.value.map(object => {
|
||||
return {
|
||||
Key: object.Key,
|
||||
type: "object",
|
||||
type: 'object',
|
||||
Size: object.Size ?? 0,
|
||||
LastModified: object.LastModified ?? new Date(0),
|
||||
};
|
||||
@@ -295,7 +309,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);
|
||||
});
|
||||
@@ -317,36 +331,40 @@ const bucketApi = useBucket({});
|
||||
// 批量删除
|
||||
function handleBatchDelete() {
|
||||
dialog.error({
|
||||
title: "警告",
|
||||
content: "你确定要删除所有选中的对象吗?",
|
||||
positiveText: "确定",
|
||||
negativeText: "取消",
|
||||
title: t('Warning'),
|
||||
content: t('Are you sure you want to delete all selected objects?'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
if (!checkedKeys.value.length) {
|
||||
message.error("请至少选择一项");
|
||||
message.error(t('Please select at least one item'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Promise.all(
|
||||
checkedKeys.value.map(async (item) => {
|
||||
const findOne = objects.value.find((obj) => obj.Key === item);
|
||||
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);
|
||||
});
|
||||
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);
|
||||
})
|
||||
);
|
||||
|
||||
message.success("删除任务已创建");
|
||||
message.success(t('Delete task created'));
|
||||
salt.value = randomString();
|
||||
refresh();
|
||||
} catch (error) {
|
||||
message.error("删除失败");
|
||||
message.error(t('Delete Failed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,24 +2,42 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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" />
|
||||
<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">
|
||||
@@ -34,6 +52,9 @@
|
||||
<n-tab-pane :tab="t('Failed', { count: failed.length })" name="failed">
|
||||
<slot name="task-list" :tasks="failed" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane :tab="t('Paused', { count: paused.length })" name="paused">
|
||||
<slot name="task-list" :tasks="paused" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
@@ -41,51 +62,56 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
tasks: any[]
|
||||
processingStatus: string
|
||||
onClearTasks: () => void
|
||||
}>()
|
||||
tasks: any[];
|
||||
processingStatus: string;
|
||||
onClearTasks: () => void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n()
|
||||
const showDrawer = ref(false)
|
||||
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 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 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 paused = computed(() => props.tasks.filter(task => task.status === 'paused'));
|
||||
|
||||
const tab = ref('pending')
|
||||
const tab = ref('pending');
|
||||
|
||||
// 根据任务执行状态切换任务列表
|
||||
watch(percentage, () => {
|
||||
if (processing.value.length > 0) {
|
||||
tab.value = 'processing'
|
||||
tab.value = 'processing';
|
||||
} else {
|
||||
tab.value = 'completed'
|
||||
tab.value = 'completed';
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 第一次加入任务时自动打开抽屉
|
||||
const autoOpened = ref(false)
|
||||
const autoOpened = ref(false);
|
||||
watch(total, (newVal, oldVal) => {
|
||||
if (!autoOpened.value && !showDrawer.value && oldVal === 0 && newVal > 0) {
|
||||
showDrawer.value = true
|
||||
autoOpened.value = true
|
||||
showDrawer.value = true;
|
||||
autoOpened.value = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const toggleDrawer = () => {
|
||||
showDrawer.value = !showDrawer.value
|
||||
}
|
||||
showDrawer.value = !showDrawer.value;
|
||||
};
|
||||
|
||||
const clearTasks = () => {
|
||||
props.onClearTasks()
|
||||
}
|
||||
props.onClearTasks();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<n-modal :show="show" @update:show="(val: boolean) => $emit('update:show', val)" size="huge">
|
||||
<n-card class="max-w-screen-md">
|
||||
<template #header>
|
||||
<div style="display:flex; justify-content: space-between; align-items:center;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span>{{ t('Upload File') }}</span>
|
||||
<n-button size="small" ghost @click="closeModal">{{ t('Close') }}</n-button>
|
||||
</div>
|
||||
@@ -10,7 +10,14 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="file" ref="fileInput" multiple hidden @change="handleFileSelect" />
|
||||
<input type="file" ref="folderInput" webkitdirectory directory hidden @change="handleFolderSelect" />
|
||||
<input
|
||||
type="file"
|
||||
ref="folderInput"
|
||||
webkitdirectory
|
||||
directory
|
||||
hidden
|
||||
@change="handleFolderSelect"
|
||||
/>
|
||||
<n-button type="primary" @click="selectFile">{{ t('Select File') }}</n-button>
|
||||
<div>{{ t('Or') }}</div>
|
||||
<n-button @click="selectFolder">{{ t('Select Folder') }}</n-button>
|
||||
@@ -23,16 +30,24 @@
|
||||
</n-alert>
|
||||
|
||||
<div @dragover.prevent @drop.prevent="handleDrop">
|
||||
<div v-if="selectedItems.length === 0" class="flex flex-col gap-6 items-center justify-center border border-dashed border-muted rounded p-4 h-[40vh]">
|
||||
<div
|
||||
v-if="selectedItems.length === 0"
|
||||
class="flex flex-col gap-6 items-center justify-center border border-dashed border-muted rounded p-4 h-[40vh]"
|
||||
>
|
||||
<div class="text-gray-500 font-bold">{{ t('No Selection') }}</div>
|
||||
<div class="text-muted-foreground text-center">
|
||||
{{ t('Drag Drop Info') }}<br>
|
||||
{{ t('Drag Drop Info') }}<br />
|
||||
{{ t('File Size Limit') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 虚拟滚动文件与文件夹列表 -->
|
||||
<n-virtual-list v-if="selectedItems.length" :items="selectedItems" :item-size="40" class="h-[40vh] overflow-y-auto border rounded">
|
||||
<n-virtual-list
|
||||
v-if="selectedItems.length"
|
||||
:items="selectedItems"
|
||||
:item-size="40"
|
||||
class="h-[40vh] overflow-y-auto border rounded"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div class="flex items-center gap-2 w-full border-b py-1">
|
||||
<div class="flex-1">
|
||||
@@ -48,15 +63,25 @@
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
|
||||
<n-progress v-if="isAdding" :percentage="addProgress" type="line" :show-indicator="true" style="margin-bottom: 16px;">
|
||||
<template #default>
|
||||
{{ t('Adding to Upload Queue') }} ({{ addProgress }}%)
|
||||
</template>
|
||||
<n-progress
|
||||
v-if="isAdding"
|
||||
:percentage="addProgress"
|
||||
type="line"
|
||||
:show-indicator="true"
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<template #default> {{ t('Adding to Upload Queue') }} ({{ addProgress }}%) </template>
|
||||
</n-progress>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<n-button type="default" :disabled="!hasFiles || isAdding">{{ t('Configure') }}</n-button>
|
||||
<n-button type="primary" :disabled="!hasFiles || isAdding" :loading="isAdding" @click="handleUpload">{{ t('Start Upload') }}</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!hasFiles || isAdding"
|
||||
:loading="isAdding"
|
||||
@click="handleUpload"
|
||||
>{{ t('Start Upload') }}</n-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
@@ -64,98 +89,108 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, defineProps, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
import { computed, defineEmits, defineProps, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks';
|
||||
|
||||
const { t } = useI18n()
|
||||
const uploadTaskManagerStore = useUploadTaskManagerStore()
|
||||
const { t } = useI18n();
|
||||
const uploadTaskManagerStore = useUploadTaskManagerStore();
|
||||
|
||||
interface FileItem {
|
||||
file: File,
|
||||
prefix: string
|
||||
file: File;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
interface SelectedItem {
|
||||
type: 'file' | 'folder'
|
||||
name: string
|
||||
size: number
|
||||
files?: FileItem[]
|
||||
type: 'file' | 'folder';
|
||||
name: string;
|
||||
size: number;
|
||||
files?: FileItem[];
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:show', 'submit'])
|
||||
const emit = defineEmits(['update:show', 'submit']);
|
||||
|
||||
const props = defineProps<{ show: boolean; bucketName: string; prefix: string }>()
|
||||
const props = defineProps<{ show: boolean; bucketName: string; prefix: string }>();
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const folderInput = ref<HTMLInputElement | null>(null)
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const folderInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// 待上传的文件/文件夹列表
|
||||
const selectedItems = ref<SelectedItem[]>([])
|
||||
const selectedItems = ref<SelectedItem[]>([]);
|
||||
|
||||
const isAdding = ref(false)
|
||||
const addProgress = ref(0)
|
||||
const isAdding = ref(false);
|
||||
const addProgress = ref(0);
|
||||
|
||||
const closeModal = () => emit('update:show', false)
|
||||
const selectFile = () => fileInput.value?.click()
|
||||
const selectFolder = () => folderInput.value?.click()
|
||||
const closeModal = () => emit('update:show', false);
|
||||
const selectFile = () => fileInput.value?.click();
|
||||
const selectFolder = () => folderInput.value?.click();
|
||||
|
||||
const handleFileSelect = (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files) {
|
||||
// 更新展示列表
|
||||
Array.from(input.files).forEach(f => selectedItems.value.push(
|
||||
{ type: 'file', name: f.name, size: f.size, files: [{ file: f, prefix: props.prefix }] }
|
||||
))
|
||||
Array.from(input.files).forEach(f =>
|
||||
selectedItems.value.push({
|
||||
type: 'file',
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
files: [{ file: f, prefix: props.prefix }],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderSelect = (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const input = e.target as HTMLInputElement;
|
||||
|
||||
if (input.files) {
|
||||
const folderFiles = Array.from(input.files)
|
||||
const folderMap = new Map<string, File[]>()
|
||||
const folderFiles = Array.from(input.files);
|
||||
const folderMap = new Map<string, File[]>();
|
||||
|
||||
for (const file of folderFiles) {
|
||||
// 保留完整目录路径(去掉最后的文件名)
|
||||
const pathParts = file.webkitRelativePath.split('/')
|
||||
pathParts.pop() // 移除文件名
|
||||
const folderPath = pathParts.join('/') || t('Unknown Folder')
|
||||
const pathParts = file.webkitRelativePath.split('/');
|
||||
pathParts.pop(); // 移除文件名
|
||||
const folderPath = pathParts.join('/') || t('Unknown Folder');
|
||||
|
||||
if (!folderMap.has(folderPath)) {
|
||||
folderMap.set(folderPath, [])
|
||||
folderMap.set(folderPath, []);
|
||||
}
|
||||
folderMap.get(folderPath)?.push(file)
|
||||
folderMap.get(folderPath)?.push(file);
|
||||
}
|
||||
|
||||
// 将每个完整路径和对应文件放到 selectedItems
|
||||
for (const [folderPath, files] of folderMap.entries()) {
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0)
|
||||
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
|
||||
selectedItems.value.push({
|
||||
type: 'folder',
|
||||
name: folderPath,
|
||||
size: totalSize,
|
||||
files: files.map(file => ({ file, prefix: `${props.prefix}${folderPath}/` }))
|
||||
})
|
||||
files: files.map(file => ({ file, prefix: `${props.prefix}${folderPath}/` })),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
if (e.dataTransfer?.files) {
|
||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
||||
droppedFiles.forEach(file => selectedItems.value.push({
|
||||
type: 'file', name: file.name, size: file.size, files: [{ file, prefix: props.prefix }]
|
||||
}))
|
||||
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||
droppedFiles.forEach(file =>
|
||||
selectedItems.value.push({
|
||||
type: 'file',
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
files: [{ file, prefix: props.prefix }],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasFiles = computed(() => selectedItems.value.length > 0)
|
||||
const allFiles = computed(() => selectedItems.value.flatMap(item => item.files || []))
|
||||
const hasFiles = computed(() => selectedItems.value.length > 0);
|
||||
const allFiles = computed(() => selectedItems.value.flatMap(item => item.files || []));
|
||||
|
||||
function removeItem(index: number) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
selectedItems.value.splice(index, 1);
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
|
||||
@@ -2,9 +2,23 @@
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>{{ task.file.name }}</div>
|
||||
<n-button text @click="handleDeleteTask" type="info">{{ t('Delete Record') }}</n-button>
|
||||
<div class="flex gap-2">
|
||||
<n-button v-if="task.status === 'uploading'" text @click="handlePauseTask" type="warning">{{
|
||||
t('Pause')
|
||||
}}</n-button>
|
||||
<n-button v-if="task.status === 'paused'" text @click="handleResumeTask" type="success">{{
|
||||
t('Resume')
|
||||
}}</n-button>
|
||||
<n-button text @click="handleDeleteTask" type="info">{{ t('Delete Record') }}</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-progress type="line" :height="2" :percentage="task.progress" :show-indicator="false" :processing="task.status == 'uploading'" />
|
||||
<n-progress
|
||||
type="line"
|
||||
:height="2"
|
||||
:percentage="task.progress"
|
||||
:show-indicator="false"
|
||||
:processing="task.status == 'uploading'"
|
||||
/>
|
||||
<div class="flex items-center justify-between text-muted-foreground">
|
||||
<div>{{ formatBytes(task.file.size) }}</div>
|
||||
<div class="text-muted-foreground">
|
||||
@@ -12,20 +26,27 @@
|
||||
<span v-else-if="task.status === 'uploading'">{{ t('Uploading Status') }}</span>
|
||||
<span v-else-if="task.status === 'completed'">{{ t('Success Status') }}</span>
|
||||
<span v-else-if="task.status === 'failed'">{{ t('Failed Status') }}</span>
|
||||
<span v-else-if="task.status === 'paused'">{{ t('Paused') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UploadTask } from '~/lib/upload-task-manager'
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
|
||||
const { t } = useI18n()
|
||||
const store = useUploadTaskManagerStore()
|
||||
import type { UploadTask } from '~/lib/upload-task-manager';
|
||||
import { useUploadTaskManagerStore } from '~/store/upload-tasks';
|
||||
const { t } = useI18n();
|
||||
const store = useUploadTaskManagerStore();
|
||||
|
||||
const props = defineProps<{ task: UploadTask }>()
|
||||
const props = defineProps<{ task: UploadTask }>();
|
||||
|
||||
const handleDeleteTask = () => {
|
||||
store.removeTask(props.task.id)
|
||||
}
|
||||
store.removeTask(props.task.id);
|
||||
};
|
||||
const handlePauseTask = () => {
|
||||
store.pauseTask(props.task.id);
|
||||
};
|
||||
const handleResumeTask = () => {
|
||||
store.resumeTask(props.task.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div v-if="tasks.length <= 0" class="mx-auto text-muted-foreground text-center">{{ t('No Tasks') }}</div>
|
||||
<div v-if="tasks.length <= 0" class="mx-auto text-muted-foreground text-center">
|
||||
{{ t('No Tasks') }}
|
||||
</div>
|
||||
<object-upload-task-item v-for="task in tasks" :key="task.id" :task="task" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { UploadTask } from '~/lib/upload-task-manager'
|
||||
const { t } = useI18n()
|
||||
defineProps<{ tasks: UploadTask[] }>()
|
||||
import type { UploadTask } from '~/lib/upload-task-manager';
|
||||
const { t } = useI18n();
|
||||
defineProps<{ tasks: UploadTask[] }>();
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<n-form ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false">
|
||||
<n-flex justify="space-between" v-if="!editStatus">
|
||||
<n-form-item class="!w-64" label="" path="name">
|
||||
<n-input placeholder="搜索用户" @input="filterName" />
|
||||
<n-input :placeholder="t('Search User')" @input="filterName" />
|
||||
</n-form-item>
|
||||
|
||||
<n-space>
|
||||
@@ -13,18 +13,18 @@
|
||||
<template #icon>
|
||||
<Icon name="ri:add-line"></Icon>
|
||||
</template>
|
||||
编辑成员
|
||||
{{ t('Edit User') }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" v-else>
|
||||
<n-form-item class="!w-96" label="选择用户组的成员" path="members">
|
||||
<n-form-item class="!w-96" :label="t('Select user group members')" path="members">
|
||||
<n-select v-model:value="members" filterable multiple :options="users" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="changeMebers">提交</NButton>
|
||||
<NButton secondary @click="changeMebers">{{ t('Submit') }}</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
@@ -36,91 +36,93 @@
|
||||
:columns="columns"
|
||||
:data="listData"
|
||||
:pagination="false"
|
||||
:bordered="false" />
|
||||
:bordered="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type DataTableColumns, type DataTableInst, NButton, NSpace } from "naive-ui"
|
||||
const { listUsers } = useUsers()
|
||||
const { updateGroupMembers } = useGroups()
|
||||
import { type DataTableColumns, type DataTableInst, NButton, NSpace } from 'naive-ui';
|
||||
const { listUsers } = useUsers();
|
||||
const { updateGroupMembers } = useGroups();
|
||||
const { t } = useI18n();
|
||||
|
||||
const messge = useMessage()
|
||||
const messge = useMessage();
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: "",
|
||||
})
|
||||
name: '',
|
||||
});
|
||||
interface RowData {
|
||||
name: string
|
||||
name: string;
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
title: "名称",
|
||||
align: "left",
|
||||
key: "name",
|
||||
title: '名称',
|
||||
align: 'left',
|
||||
key: 'name',
|
||||
filter(value, row) {
|
||||
return !!row.name.includes(value.toString())
|
||||
return !!row.name.includes(value.toString());
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
// 搜索过滤
|
||||
const tableRef = ref<DataTableInst>()
|
||||
const tableRef = ref<DataTableInst>();
|
||||
function filterName(value: string) {
|
||||
tableRef.value &&
|
||||
tableRef.value.filter({
|
||||
name: [value],
|
||||
})
|
||||
});
|
||||
}
|
||||
const listData = computed(() => {
|
||||
return (
|
||||
props.group.members.map((item: string) => {
|
||||
return {
|
||||
name: item,
|
||||
}
|
||||
};
|
||||
}) || []
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/********************编辑****************/
|
||||
const editStatus = ref(false)
|
||||
const editStatus = ref(false);
|
||||
// 用户列表
|
||||
const users = ref<any[]>([])
|
||||
const users = ref<any[]>([]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "search"): void
|
||||
}>()
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
const getUserList = async () => {
|
||||
const res = await listUsers()
|
||||
const res = await listUsers();
|
||||
users.value = Object.entries(res).map(([username, info]) => ({
|
||||
label: username,
|
||||
value: username,
|
||||
...(typeof info === "object" ? info : {}), // 展开用户信息
|
||||
}))
|
||||
}
|
||||
getUserList()
|
||||
...(typeof info === 'object' ? info : {}), // 展开用户信息
|
||||
}));
|
||||
};
|
||||
getUserList();
|
||||
|
||||
const members = ref([...props.group.members])
|
||||
const members = ref([...props.group.members]);
|
||||
const changeMebers = async () => {
|
||||
try {
|
||||
// 删除不存在的
|
||||
const nowRemoveMembers = props.group.members.filter((item: string) => {
|
||||
return !members.value.includes(item)
|
||||
})
|
||||
return !members.value.includes(item);
|
||||
});
|
||||
if (nowRemoveMembers.length) {
|
||||
await updateGroupMembers({
|
||||
group: props.group.name,
|
||||
members: nowRemoveMembers,
|
||||
isRemove: true,
|
||||
groupStatus: "enabled",
|
||||
})
|
||||
groupStatus: 'enabled',
|
||||
});
|
||||
}
|
||||
|
||||
// 修改组的成员
|
||||
@@ -128,16 +130,16 @@ const changeMebers = async () => {
|
||||
group: props.group.name,
|
||||
members: members.value,
|
||||
isRemove: false,
|
||||
groupStatus: "enabled",
|
||||
})
|
||||
groupStatus: 'enabled',
|
||||
});
|
||||
|
||||
messge.success("修改成功")
|
||||
editStatus.value = false
|
||||
emit("search")
|
||||
messge.success('修改成功');
|
||||
editStatus.value = false;
|
||||
emit('search');
|
||||
} catch {
|
||||
messge.error("修改失败")
|
||||
messge.error('修改失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,65 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
visible: boolean;
|
||||
}
|
||||
const { visible } = defineProps<Props>()
|
||||
const message = useMessage()
|
||||
const { $api } = useNuxtApp()
|
||||
const group = useGroups()
|
||||
const user = useUsers()
|
||||
const { visible } = defineProps<Props>();
|
||||
const message = useMessage();
|
||||
const { $api } = useNuxtApp();
|
||||
const group = useGroups();
|
||||
const user = useUsers();
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const emit = defineEmits<Emits>();
|
||||
const defaultFormModal = {
|
||||
group: "",
|
||||
group: '',
|
||||
members: [],
|
||||
}
|
||||
const formModel = ref({ ...defaultFormModal })
|
||||
};
|
||||
const formModel = ref({ ...defaultFormModal });
|
||||
|
||||
interface Emits {
|
||||
(e: "update:visible", visible: boolean): void
|
||||
(e: "search"): void
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
// 用户列表
|
||||
const users = ref<any[]>([])
|
||||
const users = ref<any[]>([]);
|
||||
const getUserList = async () => {
|
||||
const res = await user.listUsers()
|
||||
const res = await user.listUsers();
|
||||
|
||||
users.value = Object.entries(res).map(([username, info]) => ({
|
||||
label: username, // 添加用户名
|
||||
value: username, // 添加用户名
|
||||
}))
|
||||
}
|
||||
getUserList()
|
||||
}));
|
||||
};
|
||||
getUserList();
|
||||
|
||||
const modalVisible = computed({
|
||||
get() {
|
||||
return visible
|
||||
return visible;
|
||||
},
|
||||
set(visible) {
|
||||
closeModal(visible)
|
||||
closeModal(visible);
|
||||
},
|
||||
})
|
||||
});
|
||||
function closeModal(visible = false) {
|
||||
formModel.value = { ...defaultFormModal }
|
||||
emit("update:visible", visible)
|
||||
formModel.value = { ...defaultFormModal };
|
||||
emit('update:visible', visible);
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (formModel.value.group.trim() === "") {
|
||||
message.error("请输入用户组名称")
|
||||
return
|
||||
if (formModel.value.group.trim() === '') {
|
||||
message.error(t('Please enter user group name'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await group.updateGroupMembers({ ...formModel.value, groupStatus: "enabled", isRemove: false })
|
||||
const res = await group.updateGroupMembers({
|
||||
...formModel.value,
|
||||
groupStatus: 'enabled',
|
||||
isRemove: false,
|
||||
});
|
||||
|
||||
message.success("添加成功")
|
||||
closeModal()
|
||||
emit("search")
|
||||
message.success(t('Add success'));
|
||||
closeModal();
|
||||
emit('search');
|
||||
} catch (error) {
|
||||
message.error("添加失败")
|
||||
message.error(t('Add failed'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -69,19 +75,20 @@ async function submitForm() {
|
||||
v-model:show="modalVisible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
title="添加用户组"
|
||||
:title="t('Add group members')"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-form label-placement="left" :model="formModel" label-align="right" :label-width="130">
|
||||
<n-grid :cols="24" :x-gap="18">
|
||||
<n-form-item-grid-item :span="24" label="名称" path="group" required>
|
||||
<n-form-item-grid-item :span="24" :label="t('Name')" path="group" required>
|
||||
<n-input v-model:value="formModel.group" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" label="用户" path="members">
|
||||
<n-form-item-grid-item :span="24" :label="t('Users')" path="members">
|
||||
<n-select v-model:value="formModel.members" filterable multiple :options="users" />
|
||||
</n-form-item-grid-item>
|
||||
</n-grid>
|
||||
@@ -89,8 +96,8 @@ async function submitForm() {
|
||||
</n-card>
|
||||
<template #action>
|
||||
<n-space justify="center">
|
||||
<n-button @click="closeModal()">取消</n-button>
|
||||
<n-button type="primary" @click="submitForm">提交</n-button>
|
||||
<n-button @click="closeModal()">{{ t('Cancel') }}</n-button>
|
||||
<n-button type="primary" @click="submitForm">{{ t('Submit') }}</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="searchForm"
|
||||
label-placement="left"
|
||||
:show-feedback="false">
|
||||
<n-form ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false">
|
||||
<n-flex justify="space-between" v-if="!editStatus">
|
||||
<n-form-item class="!w-64" label="" path="name">
|
||||
<n-input placeholder="搜索策略" @input="filterName" />
|
||||
<n-input :placeholder="t('Search Policy')" @input="filterName" />
|
||||
</n-form-item>
|
||||
|
||||
<n-space>
|
||||
@@ -17,22 +13,18 @@
|
||||
<template #icon>
|
||||
<Icon name="ri:add-line"></Icon>
|
||||
</template>
|
||||
编辑策略
|
||||
{{ t('Edit Policy') }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<n-flex justify="space-between" v-else>
|
||||
<n-form-item class="!w-96" label="选择用户组的策略" path="policies">
|
||||
<n-select
|
||||
v-model:value="name"
|
||||
filterable
|
||||
multiple
|
||||
:options="polices" />
|
||||
<n-form-item class="!w-96" :label="t('Select user group policies')" path="policies">
|
||||
<n-select v-model:value="name" filterable multiple :options="polices" />
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="changePolicies">提交</NButton>
|
||||
<NButton secondary @click="changePolicies">{{ t('Submit') }}</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
@@ -44,32 +36,29 @@
|
||||
:columns="columns"
|
||||
:data="listData"
|
||||
:pagination="false"
|
||||
:bordered="false" />
|
||||
:bordered="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type DataTableColumns,
|
||||
type DataTableInst,
|
||||
NButton,
|
||||
NSpace
|
||||
} from 'naive-ui'
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
|
||||
import { type DataTableColumns, type DataTableInst, NButton, NSpace } from 'naive-ui';
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies();
|
||||
const { t } = useI18n();
|
||||
|
||||
const messge = useMessage()
|
||||
const messge = useMessage();
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: ''
|
||||
})
|
||||
name: '',
|
||||
});
|
||||
interface RowData {
|
||||
name: string
|
||||
name: string;
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
@@ -78,66 +67,68 @@ const columns: DataTableColumns<RowData> = [
|
||||
align: 'left',
|
||||
key: 'name',
|
||||
filter(value, row) {
|
||||
return !!row.name.includes(value.toString())
|
||||
}
|
||||
}
|
||||
]
|
||||
return !!row.name.includes(value.toString());
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 搜索过滤
|
||||
const tableRef = ref<DataTableInst>()
|
||||
const tableRef = ref<DataTableInst>();
|
||||
function filterName(value: string) {
|
||||
tableRef.value &&
|
||||
tableRef.value.filter({
|
||||
name: [value]
|
||||
})
|
||||
name: [value],
|
||||
});
|
||||
}
|
||||
const listData = computed(() => {
|
||||
if (!props.group?.policy) return []
|
||||
if (!props.group?.policy) return [];
|
||||
return (
|
||||
props.group?.policy?.split(',').map((item: string) => {
|
||||
return {
|
||||
name: item
|
||||
}
|
||||
name: item,
|
||||
};
|
||||
}) || []
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/********************编辑****************/
|
||||
const editStatus = ref(false)
|
||||
const editStatus = ref(false);
|
||||
// 策略列表
|
||||
const polices = ref<any[]>([])
|
||||
const polices = ref<any[]>([]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search'): void
|
||||
}>()
|
||||
(e: 'search'): void;
|
||||
}>();
|
||||
const getPoliciesList = async () => {
|
||||
const res = await listPolicies()
|
||||
polices.value = Object.keys(res).sort((a,b)=>a.localeCompare(b)).map((key) => {
|
||||
return {
|
||||
label: key,
|
||||
value: key
|
||||
}
|
||||
})
|
||||
}
|
||||
getPoliciesList()
|
||||
const res = await listPolicies();
|
||||
polices.value = Object.keys(res)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => {
|
||||
return {
|
||||
label: key,
|
||||
value: key,
|
||||
};
|
||||
});
|
||||
};
|
||||
getPoliciesList();
|
||||
|
||||
// const name = ref(props.group?.policy?.split(',') || [])
|
||||
const name = ref(props.group?.policy ? props.group?.policy?.split(',') : [])
|
||||
const name = ref(props.group?.policy ? props.group?.policy?.split(',') : []);
|
||||
const changePolicies = async () => {
|
||||
try {
|
||||
await setUserOrGroupPolicy({
|
||||
policyName: name.value || '',
|
||||
userOrGroup: encodeURIComponent(props.group.name),
|
||||
isGroup: true
|
||||
})
|
||||
isGroup: true,
|
||||
});
|
||||
|
||||
messge.success('修改成功')
|
||||
editStatus.value = false
|
||||
emit('search')
|
||||
messge.success('修改成功');
|
||||
editStatus.value = false;
|
||||
emit('search');
|
||||
} catch {
|
||||
messge.error('修改失败')
|
||||
messge.error('修改失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -4,22 +4,23 @@
|
||||
v-model:show="visible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
title="批量分配策略"
|
||||
:title="t('Batch allocation policies')"
|
||||
class="w-1/2"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-form ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false">
|
||||
<n-flex justify="space-between">
|
||||
<n-form-item class="!w-64" label="" path="name">
|
||||
<n-input placeholder="搜索策略" @input="filterName" />
|
||||
<n-input :placeholder="t('Search Policy')" @input="filterName" />
|
||||
</n-form-item>
|
||||
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="changePolicies">提交</NButton>
|
||||
<NButton secondary @click="changePolicies">{{ t('Submit') }}</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
@@ -33,104 +34,114 @@
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
:row-key="rowKey"
|
||||
@update:checked-row-keys="handleCheck" />
|
||||
@update:checked-row-keys="handleCheck"
|
||||
/>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type DataTableColumns, type DataTableInst, type DataTableRowKey, NButton, NSpace } from "naive-ui"
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies()
|
||||
const {t} = useI18n()
|
||||
import {
|
||||
type DataTableColumns,
|
||||
type DataTableInst,
|
||||
type DataTableRowKey,
|
||||
NButton,
|
||||
NSpace,
|
||||
} from 'naive-ui';
|
||||
const { listPolicies, setUserOrGroupPolicy } = usePolicies();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
checkedKeys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const messge = useMessage()
|
||||
});
|
||||
const messge = useMessage();
|
||||
|
||||
const visible = ref(false)
|
||||
const visible = ref(false);
|
||||
async function openDialog() {
|
||||
visible.value = true
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
})
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: "",
|
||||
})
|
||||
name: '',
|
||||
});
|
||||
interface RowData {
|
||||
name: string
|
||||
name: string;
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
type: "selection",
|
||||
type: 'selection',
|
||||
},
|
||||
{
|
||||
title: "策略",
|
||||
align: "left",
|
||||
key: "name",
|
||||
title: '策略',
|
||||
align: 'left',
|
||||
key: 'name',
|
||||
filter(value, row) {
|
||||
return !!row.name.includes(value.toString())
|
||||
return !!row.name.includes(value.toString());
|
||||
},
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
// 搜索过滤
|
||||
const tableRef = ref<DataTableInst>()
|
||||
const tableRef = ref<DataTableInst>();
|
||||
function filterName(value: string) {
|
||||
tableRef.value &&
|
||||
tableRef.value.filter({
|
||||
name: [value],
|
||||
})
|
||||
});
|
||||
}
|
||||
const listData = ref<any[]>([])
|
||||
const listData = ref<any[]>([]);
|
||||
const getPoliciesList = async () => {
|
||||
const res = await listPolicies()
|
||||
listData.value = Object.keys(res).sort((a,b)=>a.localeCompare(b)).map((key) => {
|
||||
return {
|
||||
name: key,
|
||||
content: res[key],
|
||||
}
|
||||
})
|
||||
}
|
||||
getPoliciesList()
|
||||
const res = await listPolicies();
|
||||
listData.value = Object.keys(res)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(key => {
|
||||
return {
|
||||
name: key,
|
||||
content: res[key],
|
||||
};
|
||||
});
|
||||
};
|
||||
getPoliciesList();
|
||||
|
||||
/********************编辑****************/
|
||||
function rowKey(row: any): string {
|
||||
return row.name
|
||||
return row.name;
|
||||
}
|
||||
|
||||
const checkedKeys = ref<DataTableRowKey[]>([])
|
||||
const checkedKeys = ref<DataTableRowKey[]>([]);
|
||||
function handleCheck(keys: DataTableRowKey[]) {
|
||||
checkedKeys.value = keys
|
||||
return checkedKeys
|
||||
checkedKeys.value = keys;
|
||||
return checkedKeys;
|
||||
}
|
||||
|
||||
|
||||
const emit = defineEmits(['changePoliciesSuccess'])
|
||||
const emit = defineEmits(['changePoliciesSuccess']);
|
||||
const changePolicies = async () => {
|
||||
Promise.all(
|
||||
props.checkedKeys.map((item) => {
|
||||
return setUserOrGroupPolicy({
|
||||
policyName: checkedKeys.value,
|
||||
userOrGroup: item,
|
||||
isGroup: true,
|
||||
})
|
||||
})
|
||||
).then(()=>{
|
||||
visible.value = false
|
||||
messge.success(t('Edit Success'))
|
||||
emit('changePoliciesSuccess')
|
||||
}).catch(()=>{
|
||||
messge.error(t('Edit Failed'))
|
||||
Promise.all(
|
||||
props.checkedKeys.map(item => {
|
||||
return setUserOrGroupPolicy({
|
||||
policyName: checkedKeys.value,
|
||||
userOrGroup: item,
|
||||
isGroup: true,
|
||||
});
|
||||
})
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
visible.value = false;
|
||||
messge.success(t('Edit Success'));
|
||||
emit('changePoliciesSuccess');
|
||||
})
|
||||
.catch(() => {
|
||||
messge.error(t('Edit Failed'));
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -366,17 +366,17 @@ async function submitForm() {
|
||||
: null,
|
||||
});
|
||||
|
||||
message.success('添加成功');
|
||||
message.success(t('Add Success'));
|
||||
cancelAdd();
|
||||
emit('notice', res);
|
||||
getUserList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
message.error('添加失败');
|
||||
message.error(t('Add Failed'));
|
||||
}
|
||||
} else {
|
||||
console.log(errors);
|
||||
message.error('请填写正确的格式');
|
||||
message.error(t('Please fill in the correct format'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -386,11 +386,11 @@ async function submitForm() {
|
||||
policy: formModel.value.policy || '{}',
|
||||
expiry: new Date(formModel.value.expiry || 0).toISOString(),
|
||||
});
|
||||
message.success('修改成功');
|
||||
message.success(t('Edit Success'));
|
||||
cancelAdd();
|
||||
getUserList();
|
||||
} catch (error) {
|
||||
message.error('修改失败');
|
||||
message.error(t('Edit Failed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,10 +402,10 @@ function dateDisabled(ts: number) {
|
||||
async function deleteItem(row: any) {
|
||||
try {
|
||||
const res = await deleteServiceAccount(row.accessKey);
|
||||
message.success('删除成功');
|
||||
message.success(t('Delete Success'));
|
||||
getUserList();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
message.error(t('Delete Failed'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,13 +421,13 @@ function handleCheck(keys: DataTableRowKey[]) {
|
||||
}
|
||||
function deleteByList() {
|
||||
dialog.error({
|
||||
title: '警告',
|
||||
content: '你确定要删除所有选中的秘钥吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
title: t('Warning'),
|
||||
content: t('Are you sure you want to delete all selected keys?'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
if (!checkedKeys.value.length) {
|
||||
message.error('请至少选择一项');
|
||||
message.error(t('Please select at least one item'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -436,9 +436,9 @@ function deleteByList() {
|
||||
// })
|
||||
checkedKeys.value = [];
|
||||
getUserList();
|
||||
message.success('删除成功');
|
||||
message.success(t('Delete Success'));
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
message.error(t('Delete Failed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
v-model:show="visible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
title="新用户已创建"
|
||||
:title="t('New user has been created')"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true
|
||||
}">
|
||||
action: true,
|
||||
}"
|
||||
>
|
||||
<n-card>
|
||||
<n-form label-placement="left" label-align="right" :label-width="130">
|
||||
<n-grid :cols="24" :x-gap="18">
|
||||
@@ -18,22 +19,24 @@
|
||||
class="w-full"
|
||||
v-model="accessKey"
|
||||
:readonly="true"
|
||||
:copy-icon="true"></copy-input>
|
||||
:copy-icon="true"
|
||||
></copy-input>
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" label="secretkey">
|
||||
<copy-input
|
||||
class="w-full"
|
||||
v-model="secretkey"
|
||||
:readonly="true"
|
||||
:copy-icon="true"></copy-input>
|
||||
:copy-icon="true"
|
||||
></copy-input>
|
||||
</n-form-item-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</n-card>
|
||||
<template #action>
|
||||
<n-space justify="center">
|
||||
<n-button @click="closeModal()">取消</n-button>
|
||||
<n-button type="primary" @click="exportFile">导出</n-button>
|
||||
<n-button @click="closeModal()">{{ t('Cancel') }}</n-button>
|
||||
<n-button type="primary" @click="exportFile">{{ t('Export') }}</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
@@ -41,29 +44,30 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { download } from '@/utils/export-file'
|
||||
const visible = ref(false)
|
||||
import { download } from '@/utils/export-file';
|
||||
const visible = ref(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
const accessKey = ref('')
|
||||
const secretkey = ref('')
|
||||
const url = ref('')
|
||||
const accessKey = ref('');
|
||||
const secretkey = ref('');
|
||||
const url = ref('');
|
||||
function openDialog(data: any) {
|
||||
accessKey.value = data.credentials.accessKey
|
||||
secretkey.value = data.credentials.secretKey
|
||||
url.value = data.url
|
||||
visible.value = true
|
||||
accessKey.value = data.credentials.accessKey;
|
||||
secretkey.value = data.credentials.secretKey;
|
||||
url.value = data.url;
|
||||
visible.value = true;
|
||||
}
|
||||
const emit = defineEmits(['search'])
|
||||
const emit = defineEmits(['search']);
|
||||
function closeModal() {
|
||||
visible.value = false
|
||||
accessKey.value = ''
|
||||
secretkey.value = ''
|
||||
emit('search')
|
||||
visible.value = false;
|
||||
accessKey.value = '';
|
||||
secretkey.value = '';
|
||||
emit('search');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDialog
|
||||
})
|
||||
openDialog,
|
||||
});
|
||||
|
||||
function exportFile() {
|
||||
download(
|
||||
@@ -73,9 +77,9 @@ function exportFile() {
|
||||
accessKey: accessKey.value,
|
||||
secretKey: secretkey.value,
|
||||
api: 's3v4',
|
||||
path: 'auto'
|
||||
path: 'auto',
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<NFlex>
|
||||
<NButton secondary @click="changeMebers">提交</NButton>
|
||||
<NButton secondary @click="changeMebers">{{ t('Submit') }}</NButton>
|
||||
</NFlex>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
@@ -119,11 +119,11 @@ const changeMebers = async () => {
|
||||
userOrGroup: encodeURIComponent(props.user.accessKey),
|
||||
isGroup: false,
|
||||
});
|
||||
messge.success('修改成功');
|
||||
messge.success(t('Edit Success'));
|
||||
editStatus.value = false;
|
||||
emit('search');
|
||||
} catch {
|
||||
messge.error('修改失败');
|
||||
messge.error(t('Edit Failed'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
+14
-7
@@ -165,10 +165,14 @@
|
||||
"Browser Warning": "Browser Warning",
|
||||
"Cache Warning": "Cache Warning",
|
||||
"Clear Records": "Clear Records",
|
||||
"Pending": "Pending",
|
||||
"Uploading": "Uploading",
|
||||
"Completed": "Completed",
|
||||
"Failed": "Failed",
|
||||
"Pending": "Pending({count})",
|
||||
"Processing": "Processing({count})",
|
||||
"Uploading": "Uploading({count})",
|
||||
"Completed": "Completed({count})",
|
||||
"Failed": "Failed({count})",
|
||||
"Paused": "Paused({count})",
|
||||
"Pause": "Pause",
|
||||
"Resume": "Resume",
|
||||
"Delete Record": "Delete Record",
|
||||
"Waiting": "Waiting",
|
||||
"Uploading Status": "Uploading Status",
|
||||
@@ -402,7 +406,7 @@
|
||||
"Search Account": "Search Account",
|
||||
"Edit policy": "Edit policy",
|
||||
"Search Policy": "Search Policy",
|
||||
"Bucket Configuration":"Bucket Configuration",
|
||||
"Bucket Configuration": "Bucket Configuration",
|
||||
"Adding to Upload Queue": "Adding to Upload Queue",
|
||||
"Adding to Delete Queue": "Adding to Delete Queue",
|
||||
|
||||
@@ -540,10 +544,13 @@
|
||||
"Search Access Key": "Search Access Key",
|
||||
"Add Access Key": "Add Access Key",
|
||||
"Expiration": "Expiration",
|
||||
"Processing": "Processing",
|
||||
"Disabled": "Disabled",
|
||||
"Description": "Description",
|
||||
"Are you sure you want to delete all selected keys?": "Are you sure you want to delete all selected keys?",
|
||||
"Please select at least one item": "Please select at least one item",
|
||||
"Get Data Failed": "Get Data Failed"
|
||||
"Get Data Failed": "Get Data Failed",
|
||||
"New user has been created": "New user has been created",
|
||||
"Export": "Export",
|
||||
"Login expired, please login again": "Login expired, please login again",
|
||||
"Batch allocation policies": "Batch allocation policies"
|
||||
}
|
||||
|
||||
+14
-5
@@ -27,8 +27,8 @@
|
||||
"Name": "名称",
|
||||
"Search": "搜索",
|
||||
"Edit": "编辑",
|
||||
"Edit Success": "编辑成功",
|
||||
"Edit Failed": "编辑失败",
|
||||
"Edit Success": "修改成功",
|
||||
"Edit Failed": "修改失败",
|
||||
"Add Success": "添加成功",
|
||||
"Add Failed": "添加失败",
|
||||
"Delete Failed": "删除失败",
|
||||
@@ -191,8 +191,12 @@
|
||||
"Clear Records": "清空记录",
|
||||
"Pending": "待处理({count})",
|
||||
"Uploading": "进行中({count})",
|
||||
"Processing": "进行中({count})",
|
||||
"Completed": "已完成({count})",
|
||||
"Failed": "已失败({count})",
|
||||
"Paused": "已暂停({count})",
|
||||
"Pause": "暂停",
|
||||
"Resume": "恢复",
|
||||
"Delete Record": "删除记录",
|
||||
"Waiting": "等待上传",
|
||||
"Uploading Status": "上传中",
|
||||
@@ -406,6 +410,8 @@
|
||||
"Storage Class": "存储类型",
|
||||
"Version ID": "版本ID",
|
||||
"Submit Data": "提交数据",
|
||||
"New user has been created": "新用户已创建",
|
||||
"Export": "导出",
|
||||
"Get Version Control Status Failed": "获取版本控制状态失败",
|
||||
"The XML format you provided is incorrect, or has not been validated against our published schema. (MasterKeyID not found aws:kms).": "您提供的XML格式不正确,或者未根据我们发布的架构进行验证。(未找到MasterKeyID aws:kms)。",
|
||||
"Server-side encryption is specified, but S3 is not configured.": "指定了服务器端加密,但S3未配置。",
|
||||
@@ -512,7 +518,6 @@
|
||||
"Search Access Key": "搜索访问密钥",
|
||||
"Add Access Key": "添加访问密钥",
|
||||
"Expiration": "过期时间",
|
||||
"Processing": "进行中",
|
||||
"Disabled": "已禁用",
|
||||
"Description": "描述",
|
||||
"Are you sure you want to delete all selected keys?": "您确定要删除所有选中的密钥吗?",
|
||||
@@ -631,7 +636,11 @@
|
||||
"Search Account": "搜索账户",
|
||||
"Edit policy": "编辑策略",
|
||||
"Search Policy": "搜索策略",
|
||||
"Bucket Configuration":"桶配置",
|
||||
"Bucket Configuration": "桶配置",
|
||||
"Adding to Upload Queue": "添加到上传队列",
|
||||
"Adding to Delete Queue": "添加到删除队列"
|
||||
"Adding to Delete Queue": "添加到删除队列",
|
||||
"Login expired, please login again": "登录信息已过期,请重新登录",
|
||||
"Batch allocation policies": "批量分配策略",
|
||||
"Add group members": "添加组成员",
|
||||
"Search User Group": "搜索用户组"
|
||||
}
|
||||
|
||||
+24
-21
@@ -1,7 +1,7 @@
|
||||
import { joinURL } from "ufo";
|
||||
import { parseApiError } from "~/utils/error-handler";
|
||||
import { logger } from "~/utils/logger";
|
||||
import type { AwsClient } from "./aws4fetch";
|
||||
import { joinURL } from 'ufo';
|
||||
import { parseApiError } from '~/utils/error-handler';
|
||||
import { logger } from '~/utils/logger';
|
||||
import type { AwsClient } from './aws4fetch';
|
||||
|
||||
interface ApiClientOptions {
|
||||
baseUrl?: string;
|
||||
@@ -36,12 +36,12 @@ class ApiClient {
|
||||
delete options.params; // 删除params,以免影响fetch的options
|
||||
}
|
||||
|
||||
logger.log("[request] url:", url);
|
||||
logger.log("[request] options:", options);
|
||||
logger.log('[request] url:', url);
|
||||
logger.log('[request] options:', options);
|
||||
|
||||
const response = await this.$api.fetch(url, options);
|
||||
|
||||
logger.log("[request] response:", response);
|
||||
logger.log('[request] response:', response);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMsg = await parseApiError(response);
|
||||
@@ -51,15 +51,18 @@ class ApiClient {
|
||||
// 处理401
|
||||
if (response.status === 401) {
|
||||
const message = useMessage();
|
||||
message.error("登录信息已过期,请重新登录");
|
||||
// 清除登录信息
|
||||
await useAuth().logout();
|
||||
window.location.href = "/auth/login";
|
||||
window.location.href = '/auth/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// 204 or body length is 0
|
||||
if (response.status === 204 || response.headers.get("content-length") === "0" || !response.body) {
|
||||
if (
|
||||
response.status === 204 ||
|
||||
response.headers.get('content-length') === '0' ||
|
||||
!response.body
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -74,11 +77,11 @@ class ApiClient {
|
||||
const response = await this.request(url, options, false);
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("No response body");
|
||||
throw new Error('No response body');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
@@ -88,40 +91,40 @@ class ApiClient {
|
||||
const data = JSON.parse(chunk);
|
||||
yield data; // 使用 yield 返回数据
|
||||
} catch (error) {
|
||||
logger.error("Failed to parse chunk:", error);
|
||||
logger.error('Failed to parse chunk:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async get(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "GET", ...options });
|
||||
return this.request(url, { method: 'GET', ...options });
|
||||
}
|
||||
|
||||
async post(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: "POST", body, ...options });
|
||||
return this.request(url, { method: 'POST', body, ...options });
|
||||
}
|
||||
|
||||
async delete(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "DELETE", ...options });
|
||||
return this.request(url, { method: 'DELETE', ...options });
|
||||
}
|
||||
|
||||
async put(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: "PUT", body, ...options });
|
||||
return this.request(url, { method: 'PUT', body, ...options });
|
||||
}
|
||||
|
||||
async patch(url: string, body: any, options?: RequestOptions) {
|
||||
return this.request(url, { method: "PATCH", body, ...options });
|
||||
return this.request(url, { method: 'PATCH', body, ...options });
|
||||
}
|
||||
|
||||
async head(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "HEAD", ...options });
|
||||
return this.request(url, { method: 'HEAD', ...options });
|
||||
}
|
||||
|
||||
async options(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "OPTIONS", ...options });
|
||||
return this.request(url, { method: 'OPTIONS', ...options });
|
||||
}
|
||||
|
||||
async trace(url: string, options?: RequestOptions) {
|
||||
return this.request(url, { method: "TRACE", ...options });
|
||||
return this.request(url, { method: 'TRACE', ...options });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+94
-31
@@ -4,6 +4,7 @@ import {
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
UploadPartCommand,
|
||||
ListPartsCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
|
||||
export interface UploadTask {
|
||||
@@ -11,11 +12,14 @@ export interface UploadTask {
|
||||
file: File;
|
||||
bucketName: string;
|
||||
prefix?: string;
|
||||
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'canceled';
|
||||
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'canceled' | 'paused';
|
||||
progress: number;
|
||||
error?: string;
|
||||
abortController?: AbortController;
|
||||
retryCount?: number;
|
||||
uploadId?: string;
|
||||
completedParts?: { ETag: string; PartNumber: number }[];
|
||||
_pauseRequested?: boolean;
|
||||
}
|
||||
|
||||
export interface TaskManagerConfig {
|
||||
@@ -96,6 +100,7 @@ class UploadTaskManager {
|
||||
cancelTask(taskId: string) {
|
||||
const task = this.tasks.find(task => task.id === taskId);
|
||||
if (task) {
|
||||
task._pauseRequested = false;
|
||||
if (task.abortController) {
|
||||
task.abortController.abort();
|
||||
}
|
||||
@@ -129,6 +134,58 @@ class UploadTaskManager {
|
||||
this.isStarted = false;
|
||||
}
|
||||
|
||||
pauseTask(taskId: string) {
|
||||
const task = this.tasks.find(t => t.id === taskId);
|
||||
if (task && task.status === 'uploading') {
|
||||
task._pauseRequested = true;
|
||||
task.abortController?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async resumeTask(taskId: string) {
|
||||
const task = this.tasks.find(t => t.id === taskId);
|
||||
if (task && task.status === 'paused') {
|
||||
task._pauseRequested = false; // 恢复前清除暂停标记
|
||||
// 1. 校验 uploadId 是否有效
|
||||
try {
|
||||
if (task.uploadId) {
|
||||
const listParts = await this.s3Client.send(
|
||||
new ListPartsCommand({
|
||||
Bucket: task.bucketName,
|
||||
Key: (task.prefix || '') + task.file.name,
|
||||
UploadId: task.uploadId,
|
||||
})
|
||||
);
|
||||
|
||||
// 2. 以 S3 返回的分片为准,更新 task.completedParts
|
||||
task.completedParts = (listParts.Parts || []).map((p: any) => ({
|
||||
ETag: p.ETag,
|
||||
PartNumber: p.PartNumber,
|
||||
}));
|
||||
|
||||
// 3. 开始断点续传
|
||||
this.startUpload(task);
|
||||
} else {
|
||||
// 没有 uploadId,直接重新上传
|
||||
task.completedParts = [];
|
||||
this.startUpload(task);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.name === 'NoSuchUpload' ||
|
||||
(typeof e.message === 'string' && e.message.includes('Invalid upload id'))
|
||||
) {
|
||||
// uploadId 已失效,重新发起分片上传
|
||||
task.uploadId = undefined;
|
||||
task.completedParts = [];
|
||||
this.startUpload(task);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的队列处理逻辑
|
||||
private async processQueue() {
|
||||
if (!this.isStarted) {
|
||||
@@ -169,8 +226,17 @@ class UploadTaskManager {
|
||||
task.status = 'completed';
|
||||
task.progress = 100;
|
||||
} catch (error: any) {
|
||||
// 检查是否是取消操作
|
||||
// 判断是否为“暂停”操作
|
||||
if (
|
||||
task._pauseRequested &&
|
||||
(error.message === 'Upload paused' || error.message === 'Request aborted')
|
||||
) {
|
||||
task.status = 'paused';
|
||||
return;
|
||||
}
|
||||
// 只有真正取消时才清理
|
||||
if (error.name === 'AbortError' || error.message?.includes('canceled')) {
|
||||
// 这里才调用 AbortMultipartUploadCommand
|
||||
task.status = 'canceled';
|
||||
return;
|
||||
}
|
||||
@@ -262,39 +328,38 @@ class UploadTaskManager {
|
||||
task.abortController = abortController;
|
||||
|
||||
const Key = prefix + file.name;
|
||||
let uploadId: string | undefined;
|
||||
let uploadId: string | undefined = task.uploadId;
|
||||
let completedParts: { ETag: string; PartNumber: number }[] = task.completedParts || [];
|
||||
|
||||
try {
|
||||
// 创建分片上传
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
ContentType: file.type || 'application/octet-stream',
|
||||
});
|
||||
|
||||
const createResponse = await this.s3Client.send(createCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
uploadId = createResponse.UploadId;
|
||||
|
||||
if (!uploadId) {
|
||||
// 创建分片上传
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
ContentType: file.type || 'application/octet-stream',
|
||||
});
|
||||
const createResponse = await this.s3Client.send(createCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
uploadId = createResponse.UploadId;
|
||||
task.uploadId = uploadId;
|
||||
task.completedParts = [];
|
||||
}
|
||||
if (!uploadId) {
|
||||
throw new Error('Failed to create multipart upload');
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
||||
const completedParts = [];
|
||||
|
||||
// 上传分片
|
||||
for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
|
||||
// 检查是否被取消
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Upload canceled');
|
||||
throw new Error('Upload paused');
|
||||
}
|
||||
|
||||
// 跳过已完成分片
|
||||
if (completedParts.some(p => p.PartNumber === partNumber)) continue;
|
||||
const start = (partNumber - 1) * this.chunkSize;
|
||||
const end = Math.min(start + this.chunkSize, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const uploadPartCommand = new UploadPartCommand({
|
||||
Bucket: bucketName,
|
||||
Key: Key,
|
||||
@@ -302,19 +367,16 @@ class UploadTaskManager {
|
||||
PartNumber: partNumber,
|
||||
Body: chunk,
|
||||
});
|
||||
|
||||
const { ETag } = await this.s3Client.send(uploadPartCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!ETag) {
|
||||
throw new Error(`Failed to upload part ${partNumber}`);
|
||||
}
|
||||
|
||||
completedParts.push({ ETag, PartNumber: partNumber });
|
||||
task.progress = Math.round((partNumber / totalChunks) * 100);
|
||||
task.completedParts = completedParts;
|
||||
task.progress = Math.round((completedParts.length / totalChunks) * 100);
|
||||
}
|
||||
|
||||
// 完成分片上传
|
||||
const completeCommand = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
@@ -322,13 +384,15 @@ class UploadTaskManager {
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: { Parts: completedParts },
|
||||
});
|
||||
|
||||
await this.s3Client.send(completeCommand, {
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
// 上传完成后清理 uploadId 和 completedParts
|
||||
task.uploadId = undefined;
|
||||
task.completedParts = undefined;
|
||||
} catch (error: any) {
|
||||
// 如果上传失败,尝试清理分片上传
|
||||
if (uploadId) {
|
||||
// 只在“非暂停”情况下才调用 AbortMultipartUploadCommand
|
||||
if (!task._pauseRequested) {
|
||||
try {
|
||||
const { AbortMultipartUploadCommand } = await import('@aws-sdk/client-s3');
|
||||
const abortCommand = new AbortMultipartUploadCommand({
|
||||
@@ -341,7 +405,6 @@ class UploadTaskManager {
|
||||
console.warn('Failed to cleanup multipart upload:', cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-14
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import UploadTaskManager from "~/lib/upload-task-manager";
|
||||
import { defineStore } from 'pinia';
|
||||
import UploadTaskManager from '~/lib/upload-task-manager';
|
||||
|
||||
export const useUploadTaskManagerStore = defineStore("uploadTaskManager", {
|
||||
export const useUploadTaskManagerStore = defineStore('uploadTaskManager', {
|
||||
state: () => ({
|
||||
taskManager: new UploadTaskManager(useNuxtApp().$s3Client, {
|
||||
chunkSize: 16, // 16MB chunks for better performance
|
||||
@@ -11,15 +11,19 @@ export const useUploadTaskManagerStore = defineStore("uploadTaskManager", {
|
||||
}),
|
||||
}),
|
||||
getters: {
|
||||
tasks: (state) => state.taskManager.getTasks(),
|
||||
pendingTasks: (state) => state.taskManager.getTasks().filter(task => task.status === "pending"),
|
||||
uploadingTasks: (state) => state.taskManager.getTasks().filter(task => task.status === "uploading"),
|
||||
completedTasks: (state) => state.taskManager.getTasks().filter(task => task.status === "completed"),
|
||||
failedTasks: (state) => state.taskManager.getTasks().filter(task => task.status === "failed"),
|
||||
tasks: state => state.taskManager.getTasks(),
|
||||
pendingTasks: state => state.taskManager.getTasks().filter(task => task.status === 'pending'),
|
||||
uploadingTasks: state =>
|
||||
state.taskManager.getTasks().filter(task => task.status === 'uploading'),
|
||||
completedTasks: state =>
|
||||
state.taskManager.getTasks().filter(task => task.status === 'completed'),
|
||||
failedTasks: state => state.taskManager.getTasks().filter(task => task.status === 'failed'),
|
||||
canceledTasks: state => state.taskManager.getTasks().filter(task => task.status === 'canceled'),
|
||||
pausedTasks: state => state.taskManager.getTasks().filter(task => task.status === 'paused'),
|
||||
},
|
||||
actions: {
|
||||
addFiles(files: File[], bucketName: string, prefix?: string) {
|
||||
console.log("addFiles", files.length, "files to bucket", bucketName, "prefix:", prefix);
|
||||
console.log('addFiles', files.length, 'files to bucket', bucketName, 'prefix:', prefix);
|
||||
|
||||
// 对于大量文件,分批处理
|
||||
const batchSize = 50;
|
||||
@@ -54,16 +58,24 @@ export const useUploadTaskManagerStore = defineStore("uploadTaskManager", {
|
||||
this.taskManager.clearTasks();
|
||||
},
|
||||
|
||||
pauseTask(taskId: string) {
|
||||
this.taskManager.pauseTask(taskId);
|
||||
},
|
||||
resumeTask(taskId: string) {
|
||||
this.taskManager.resumeTask(taskId);
|
||||
},
|
||||
|
||||
// 获取统计信息
|
||||
getStats() {
|
||||
const tasks = this.tasks;
|
||||
return {
|
||||
total: tasks.length,
|
||||
pending: tasks.filter(task => task.status === "pending").length,
|
||||
uploading: tasks.filter(task => task.status === "uploading").length,
|
||||
completed: tasks.filter(task => task.status === "completed").length,
|
||||
failed: tasks.filter(task => task.status === "failed").length,
|
||||
canceled: tasks.filter(task => task.status === "canceled").length,
|
||||
pending: tasks.filter(task => task.status === 'pending').length,
|
||||
uploading: tasks.filter(task => task.status === 'uploading').length,
|
||||
completed: tasks.filter(task => task.status === 'completed').length,
|
||||
failed: tasks.filter(task => task.status === 'failed').length,
|
||||
canceled: tasks.filter(task => task.status === 'canceled').length,
|
||||
paused: tasks.filter(task => task.status === 'paused').length,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user