feat: 为所有只有图标没有文字的按钮添加适当的文案

- 修复了18个只有图标没有文字的按钮,提升用户体验和可访问性
- 为存储桶配置、对象管理、用户管理、策略管理、事件管理等页面的按钮添加了文字说明
- 所有修改都使用了已存在的翻译键值,确保国际化支持
- 包括编辑、删除、添加、设置等常用操作的按钮文案优化
This commit is contained in:
overtrue
2025-10-21 19:41:38 +08:00
parent ff6dc6415d
commit d22e9e4690
12 changed files with 845 additions and 986 deletions
+193 -245
View File
@@ -7,9 +7,10 @@
<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" />
<span>{{ t('Edit') }}</span>
</n-button>
</template>
{{ policyOptions.find(item => item.value === bucketPolicy)?.label }}
{{policyOptions.find(item => item.value === bucketPolicy)?.label}}
</n-descriptions-item>
<n-descriptions-item>
@@ -17,6 +18,7 @@
<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" />
<span>{{ t('Edit') }}</span>
</n-button>
</template>
<span v-if="encryptFormValue.encrypt === 'disabled'">{{ t('Disabled') }}</span>
@@ -38,42 +40,23 @@
{{ t('Tag') }}
<n-button class="align-middle" round quaternary type="primary" @click="addTag">
<Icon name="ri:add-line" size="16" class="mr-2" />
<span>{{ t('Add') }}</span>
</n-button>
</template>
<n-tag
class="m-2"
v-for="(tag, index) in tags"
type="info"
@click="editTag(index)"
closable
@close="handledeleteTag(index)"
>
<n-tag class="m-2" v-for="(tag, index) in tags" type="info" @click="editTag(index)" closable @close="handledeleteTag(index)">
{{ tag.Key }}:{{ tag.Value }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item>
<template #label>{{ t('Object Lock') }}</template>
<n-switch
:disabled="true"
v-model:value="lockStatus"
:loading="objectLockLoading"
:round="false"
@update:value="handleChangeVersionStatus"
/>
<n-switch :disabled="true" v-model:value="lockStatus" :loading="objectLockLoading" :round="false" @update:value="handleChangeVersionStatus" />
</n-descriptions-item>
<n-descriptions-item>
<template #label>{{ t('Version Control') }}</template>
<n-switch
v-model:value="versioningStatus"
:disabled="lockStatus == true"
checked-value="Enabled"
unchecked-value="Suspended"
:round="false"
:loading="statusLoading"
@update:value="handleChangeVersionStatus"
/>
<n-switch v-model:value="versioningStatus" :disabled="lockStatus == true" checked-value="Enabled" unchecked-value="Suspended" :round="false" :loading="statusLoading"
@update:value="handleChangeVersionStatus" />
</n-descriptions-item>
</n-descriptions>
@@ -111,25 +94,10 @@
</n-drawer-content>
<!-- policy -->
<n-modal
v-model:show="showPolicyModal"
:title="t('Set Policy')"
preset="card"
draggable
:style="{ width: '750px' }"
>
<n-form
ref="policyFormRef"
:inline="policyFormValue.policy !== 'custom'"
:label-width="80"
:model="policyFormValue"
>
<n-modal v-model:show="showPolicyModal" :title="t('Set Policy')" preset="card" draggable :style="{ width: '750px' }">
<n-form ref="policyFormRef" :inline="policyFormValue.policy !== 'custom'" :label-width="80" :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"
/>
<n-select v-model:value="policyFormValue.policy" :placeholder="t('Please select policy')" :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>
@@ -158,27 +126,13 @@
</n-modal>
<!-- Encrypt -->
<n-modal
v-model:show="showEncryptModal"
:title="t('Enable Storage Encryption')"
preset="card"
draggable
:style="{ width: '550px' }"
>
<n-modal v-model:show="showEncryptModal" :title="t('Enable Storage Encryption')" preset="card" draggable :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"
/>
<n-select v-model:value="encryptFormValue.encrypt" :placeholder="t('Please select encryption type')" :options="encryptOptions" />
</n-form-item>
<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="t('Please select KMS key')"
:options="kmsKeyOptions"
/>
<n-select v-model:value="encryptFormValue.kmsKeyId" :placeholder="t('Please select KMS key')" :options="kmsKeyOptions" />
</n-form-item>
<n-form-item>
@@ -189,13 +143,7 @@
</n-modal>
<!-- retention -->
<n-modal
v-model:show="showRetentionModal"
:title="t('Set Retention')"
preset="card"
draggable
:style="{ width: '550px' }"
>
<n-modal v-model:show="showRetentionModal" :title="t('Set Retention')" preset="card" draggable :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">
@@ -225,30 +173,30 @@
</template>
<script setup lang="ts">
import { Icon } from '#components';
const { t } = useI18n();
const dialog = useDialog();
const visibel = ref(false);
const bucketName = ref('');
import { Icon } from '#components'
const { t } = useI18n()
const dialog = useDialog()
const visibel = ref(false)
const bucketName = ref('')
const openDrawer = (bucket: string) => {
visibel.value = true;
bucketName.value = bucket;
visibel.value = true
bucketName.value = bucket
// 在服务端获取数据
getData();
};
getData()
}
defineExpose({
openDrawer,
});
})
const getData = async () => {
await getTags();
await getVersioningStatus();
await getObjectLockConfig();
await getBucketEncryptionFn();
await getbucketPolicy();
};
await getTags()
await getVersioningStatus()
await getObjectLockConfig()
await getBucketEncryptionFn()
await getbucketPolicy()
}
const message = useMessage();
const message = useMessage()
const {
getBucketTagging,
deleteBucket,
@@ -262,115 +210,115 @@ const {
getBucketEncryption,
putBucketEncryption,
deleteBucketEncryption,
} = useBucket({});
} = useBucket({})
// 使用 SSE 相关功能
const { getKeyList } = useSSE();
const { getKeyList } = useSSE()
/**********object lock ***********************/
const lockStatus = ref(false);
const objectLockLoading = ref(false);
const retentionEnabled = ref(false);
const lockStatus = ref(false)
const objectLockLoading = ref(false)
const retentionEnabled = ref(false)
const getObjectLockConfig = async () => {
objectLockLoading.value = true;
objectLockLoading.value = true
getObjectLockConfiguration(bucketName.value)
.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;
retentionEnabled.value = true
retentionFormValue.value.retentionMode = res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Mode || null
retentionFormValue.value.retentionPeriod =
res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Days ||
res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Years ||
null;
null
retentionFormValue.value.retentionUnit = res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Years
? 'Years'
: res.ObjectLockConfiguration?.Rule?.DefaultRetention?.Days
? 'Days'
: '';
: ''
}
} else {
lockStatus.value = false;
retentionEnabled.value = false;
retentionFormValue.value.retentionMode = null;
retentionFormValue.value.retentionPeriod = null;
retentionFormValue.value.retentionUnit = null;
lockStatus.value = false
retentionEnabled.value = false
retentionFormValue.value.retentionMode = null
retentionFormValue.value.retentionPeriod = null
retentionFormValue.value.retentionUnit = null
}
})
.finally(() => {
objectLockLoading.value = false;
});
};
objectLockLoading.value = false
})
}
/**********object lock ***********************/
/******** policy ***********************/
import { setBucketPolicy, getBucketPolicy as getBucketPolicyFn } from '~/utils/bucket-policy';
import { getBucketPolicy as getBucketPolicyFn, setBucketPolicy } from '~/utils/bucket-policy'
const bucketPolicy = ref('public');
const bucketPolicy = ref('public')
const getbucketPolicy = async () => {
try {
const res = await getBucketPolicy(bucketName.value);
const res = await getBucketPolicy(bucketName.value)
if (res.Policy) {
policyFormValue.value.content = res.Policy;
policyFormValue.value.content = res.Policy
bucketPolicy.value = getBucketPolicyFn(JSON.parse(res.Policy).Statement, bucketName.value, '');
policyFormValue.value.policy = bucketPolicy.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';
bucketPolicy.value = 'custom'
policyFormValue.value.policy = 'custom'
}
} else {
bucketPolicy.value = 'public';
bucketPolicy.value = 'public'
}
} catch (error: any) {
// Handle 404 error when no policy exists
console.error('Error fetching bucket policy:', error);
console.error('Error fetching bucket policy:', error)
// Set default values for private policy
bucketPolicy.value = 'private';
policyFormValue.value.policy = 'private';
policyFormValue.value.content = '{}';
bucketPolicy.value = 'private'
policyFormValue.value.policy = 'private'
policyFormValue.value.content = '{}'
}
};
}
const policyFormValue = ref({
policy: 'private',
content: '{}',
});
const showPolicyModal = ref(false);
})
const showPolicyModal = ref(false)
const editPolicy = () => {
showPolicyModal.value = true;
};
showPolicyModal.value = true
}
const submitPolicyForm = () => {
if (policyFormValue.value.policy == 'custom') {
let polictStr = JSON.stringify(JSON.parse(policyFormValue.value.content));
console.log(polictStr);
let polictStr = JSON.stringify(JSON.parse(policyFormValue.value.content))
console.log(polictStr)
putBucketPolicy(bucketName.value, polictStr)
.then(() => {
message.success(t('Edit Success'));
showPolicyModal.value = false;
getbucketPolicy();
message.success(t('Edit Success'))
showPolicyModal.value = false
getbucketPolicy()
})
.catch(error => {
message.error(t('Edit Failed') + ': ' + error.message);
});
message.error(t('Edit Failed') + ': ' + error.message)
})
} else {
const policys = setBucketPolicy([], policyFormValue.value.policy as BucketPolicyType, bucketName.value, '');
console.log('🚀 ~ policys:', 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(t('Edit Success'));
showPolicyModal.value = false;
getbucketPolicy();
message.success(t('Edit Success'))
showPolicyModal.value = false
getbucketPolicy()
})
.catch(error => {
message.error(t('Edit Failed') + ': ' + error.message);
});
message.error(t('Edit Failed') + ': ' + error.message)
})
}
};
}
const policyOptions = [
{
label: t('Public'),
@@ -384,19 +332,19 @@ const policyOptions = [
label: t('Custom'),
value: 'custom',
},
];
]
/******** policy ***********************/
/********Encrypt ***********************/
const showEncryptModal = ref(false);
const showEncryptModal = ref(false)
const encryptFormValue = ref({
encrypt: 'disabled',
kmsKeyId: '',
});
})
// KMS 密钥列表
const kmsKeyOptions = ref([]);
const kmsKeyOptions = ref([])
const encryptOptions = [
{
@@ -411,102 +359,102 @@ const encryptOptions = [
label: 'SSE-S3',
value: 'SSE-S3',
},
];
]
const getBucketEncryptionFn = async () => {
try {
const res = await getBucketEncryption(bucketName.value);
const res = await getBucketEncryption(bucketName.value)
if (
res &&
res.ServerSideEncryptionConfiguration &&
res.ServerSideEncryptionConfiguration.Rules &&
res.ServerSideEncryptionConfiguration.Rules.length > 0
) {
const rule = res.ServerSideEncryptionConfiguration.Rules[0];
const rule = res.ServerSideEncryptionConfiguration.Rules[0]
if (rule.ApplyServerSideEncryptionByDefault) {
const sseAlgorithm = rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm;
const sseAlgorithm = rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm
if (sseAlgorithm) {
// 将AWS S3算法值转换为显示值
if (sseAlgorithm === 'aws:kms') {
encryptFormValue.value.encrypt = 'SSE-KMS';
encryptFormValue.value.encrypt = 'SSE-KMS'
} else if (sseAlgorithm === 'AES256') {
encryptFormValue.value.encrypt = 'SSE-S3';
encryptFormValue.value.encrypt = 'SSE-S3'
} else {
encryptFormValue.value.encrypt = sseAlgorithm;
encryptFormValue.value.encrypt = sseAlgorithm
}
}
const kmsKeyId = rule.ApplyServerSideEncryptionByDefault.KMSMasterKeyID;
const kmsKeyId = rule.ApplyServerSideEncryptionByDefault.KMSMasterKeyID
if (kmsKeyId) {
encryptFormValue.value.kmsKeyId = kmsKeyId;
encryptFormValue.value.kmsKeyId = kmsKeyId
}
}
} else {
encryptFormValue.value.encrypt = 'disabled';
encryptFormValue.value.kmsKeyId = '';
encryptFormValue.value.encrypt = 'disabled'
encryptFormValue.value.kmsKeyId = ''
}
} catch (error: any) {
// 如果没有加密配置,设置为禁用状态
if (error.status === 404) {
encryptFormValue.value.encrypt = 'disabled';
encryptFormValue.value.kmsKeyId = '';
encryptFormValue.value.encrypt = 'disabled'
encryptFormValue.value.kmsKeyId = ''
} else {
console.error('Failed to get bucket encryption:', error);
console.error('Failed to get bucket encryption:', error)
}
}
};
}
// 获取 KMS 密钥列表
const fetchKMSKeys = async () => {
try {
const response = await getKeyList();
const response = await getKeyList()
if (response && response.keys) {
kmsKeyOptions.value = response.keys.map((key: any) => {
// 优先使用 tags.name,然后是 description,最后是 key_id
const keyName = key.tags?.name || key.description || `Key-${key.key_id?.substring(0, 8)}`;
const keyName = key.tags?.name || key.description || `Key-${key.key_id?.substring(0, 8)}`
return {
label: keyName,
value: key.key_id,
};
});
}
})
}
} catch (error) {
console.error('Failed to fetch KMS keys:', error);
message.error(t('Failed to fetch KMS keys'));
console.error('Failed to fetch KMS keys:', error)
message.error(t('Failed to fetch KMS keys'))
}
};
}
// 监听加密类型变化,当选择 SSE-KMS 时自动获取 KMS 密钥列表
watch(
() => encryptFormValue.value.encrypt,
newValue => {
if (newValue === 'SSE-KMS') {
fetchKMSKeys();
fetchKMSKeys()
}
}
);
)
const editEncript = () => {
showEncryptModal.value = true;
showEncryptModal.value = true
// 当选择 SSE-KMS 时,获取 KMS 密钥列表
if (encryptFormValue.value.encrypt === 'SSE-KMS') {
fetchKMSKeys();
fetchKMSKeys()
}
};
}
const submitEncryptForm = () => {
if (encryptFormValue.value.encrypt === 'disabled') {
// 禁用加密
deleteBucketEncryption(bucketName.value)
.then(() => {
message.success(t('Edit Success'));
showEncryptModal.value = false;
message.success(t('Edit Success'))
showEncryptModal.value = false
})
.catch(error => {
message.error(t('Edit Failed') + ': ' + error.message);
});
message.error(t('Edit Failed') + ': ' + error.message)
})
} else if (encryptFormValue.value.encrypt === 'SSE-KMS') {
// 验证是否选择了 KMS Key
if (!encryptFormValue.value.kmsKeyId) {
message.error(t('Please select a KMS key for SSE-KMS encryption'));
return;
message.error(t('Please select a KMS key for SSE-KMS encryption'))
return
}
// 启用 SSE-KMS 加密
@@ -521,12 +469,12 @@ const submitEncryptForm = () => {
],
})
.then(() => {
message.success(t('Edit Success'));
showEncryptModal.value = false;
message.success(t('Edit Success'))
showEncryptModal.value = false
})
.catch(error => {
message.error(t('Edit Failed') + ': ' + error.message);
});
message.error(t('Edit Failed') + ': ' + error.message)
})
} else if (encryptFormValue.value.encrypt === 'SSE-S3') {
// 启用 SSE-S3 加密
putBucketEncryption(bucketName.value, {
@@ -539,104 +487,104 @@ const submitEncryptForm = () => {
],
})
.then(() => {
message.success(t('Edit Success'));
showEncryptModal.value = false;
message.success(t('Edit Success'))
showEncryptModal.value = false
})
.catch(error => {
message.error(t('Edit Failed') + ': ' + error.message);
});
message.error(t('Edit Failed') + ': ' + error.message)
})
}
};
}
/********Encrypt ***********************/
/********versioning ***********************/
const versioningStatus: any = ref('');
const statusLoading = ref(false);
const versioningStatus: any = ref('')
const statusLoading = ref(false)
// 获取版本控制状态
const getVersioningStatus = async () => {
try {
const resp = await getBucketVersioning(bucketName.value);
versioningStatus.value = resp.Status;
const resp = await getBucketVersioning(bucketName.value)
versioningStatus.value = resp.Status
} catch (error) {
console.error('get version fail:', error);
console.error('get version fail:', error)
}
};
}
const handleChangeVersionStatus = async (value: string) => {
statusLoading.value = true;
statusLoading.value = true
putBucketVersioning(bucketName.value, value)
.then(() => {
message.success(t('Edit Success'));
getVersioningStatus();
message.success(t('Edit Success'))
getVersioningStatus()
})
.finally(() => {
statusLoading.value = false;
versioningStatus.value = versioningStatus.value == 'Suspended' ? 'Enabled' : 'Suspended';
});
};
statusLoading.value = false
versioningStatus.value = versioningStatus.value == 'Suspended' ? 'Enabled' : 'Suspended'
})
}
/********versioning ***********************/
/********tag ***********************/
// 定义标签的类型
interface Tag {
Key: string;
Value: string;
Key: string
Value: string
}
const showTagModal = ref(false);
const showTagModal = ref(false)
const tagFormValue = ref({
name: '',
value: '',
});
})
// 获取标签
const tags = ref<Tag[]>([]);
const tags = ref<Tag[]>([])
const getTags = async () => {
const resp: any = await getBucketTagging(bucketName.value);
tags.value = resp.TagSet || [];
};
const resp: any = await getBucketTagging(bucketName.value)
tags.value = resp.TagSet || []
}
const addTag = () => {
nowTagIndex.value = -1;
tagFormValue.value = { name: '', value: '' }; // 清空表单
showTagModal.value = true;
};
nowTagIndex.value = -1
tagFormValue.value = { name: '', value: '' } // 清空表单
showTagModal.value = true
}
const submitTagForm = () => {
if (!tagFormValue.value.name || !tagFormValue.value.value) {
message.error(t('Please fill in complete tag information'));
return;
message.error(t('Please fill in complete tag information'))
return
}
if (nowTagIndex.value === -1) {
tags.value.push({ Key: tagFormValue.value.name, Value: tagFormValue.value.value });
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,
};
}
}
// 调用 putBucketTagging 接口
putBucketTagging(bucketName.value, { TagSet: tags.value })
.then(() => {
showTagModal.value = false; // 关闭模态框
message.success(t('Tag Update Success'));
showTagModal.value = false // 关闭模态框
message.success(t('Tag Update Success'))
})
.catch(error => {
message.error(t('Tag Update Failed') + ': ' + error.message);
});
};
message.error(t('Tag Update Failed') + ': ' + error.message)
})
}
const nowTagIndex = ref(-1);
const nowTagIndex = ref(-1)
// 编辑标签
const editTag = (index: number) => {
nowTagIndex.value = index;
const nowTag = tags.value[index];
tagFormValue.value = { name: nowTag.Key, value: nowTag.Value }; // 填充表单
showTagModal.value = true; // 打开模态框
};
nowTagIndex.value = index
const nowTag = tags.value[index]
tagFormValue.value = { name: nowTag.Key, value: nowTag.Value } // 填充表单
showTagModal.value = true // 打开模态框
}
const handledeleteTag = (index: number) => {
dialog.error({
title: t('Warning'),
@@ -644,53 +592,53 @@ const handledeleteTag = (index: number) => {
positiveText: t('Confirm'),
negativeText: t('Cancel'),
onPositiveClick: async () => {
nowTagIndex.value = index;
tags.value.splice(index, 1); // 从标签列表中删除
nowTagIndex.value = index
tags.value.splice(index, 1) // 从标签列表中删除
// 调用 putBucketTagging 接口
putBucketTagging(bucketName.value, { TagSet: tags.value })
.then(() => {
message.success(t('Tag Update Success'));
message.success(t('Tag Update Success'))
})
.catch(error => {
message.error(t('Tag Delete Failed') + ': ' + error.message);
});
message.error(t('Tag Delete Failed') + ': ' + error.message)
})
},
});
};
})
}
/********tag ***********************/
/********retention ***********************/
interface RetentionFormValue {
retentionMode: string | null;
retentionPeriod: number | null;
retentionUnit: string | null;
retentionMode: string | null
retentionPeriod: number | null
retentionUnit: string | null
}
const showRetentionModal = ref(false);
const showRetentionModal = ref(false)
const retentionFormValue = ref<RetentionFormValue>({
retentionMode: null,
retentionPeriod: null,
retentionUnit: null,
});
})
const editRetention = () => {
if (!lockStatus.value) {
message.error(t('Object lock is not enabled, cannot set retention'));
return;
message.error(t('Object lock is not enabled, cannot set retention'))
return
}
showRetentionModal.value = true;
};
showRetentionModal.value = true
}
const submitRetentionForm = () => {
console.log(retentionFormValue.value);
console.log(retentionFormValue.value)
if (
retentionFormValue.value.retentionMode == null ||
retentionFormValue.value.retentionPeriod == null ||
retentionFormValue.value.retentionUnit == null
) {
message.error(t('Please fill in complete retention information'));
return;
message.error(t('Please fill in complete retention information'))
return
}
putObjectLockConfiguration(bucketName.value, {
@@ -703,10 +651,10 @@ const submitRetentionForm = () => {
},
},
}).then(() => {
message.success(t('Edit Success'));
showRetentionModal.value = false;
getObjectLockConfig();
});
message.success(t('Edit Success'))
showRetentionModal.value = false
getObjectLockConfig()
})
};
/********retention ***********************/
+18 -23
View File
@@ -1,8 +1,8 @@
<script lang="ts" setup>
import ClipboardJS from 'clipboard';
import { useI18n } from 'vue-i18n';
import ClipboardJS from 'clipboard'
import { useI18n } from 'vue-i18n'
const { t } = useI18n();
const { t } = useI18n()
const props = defineProps({
readonly: {
@@ -17,49 +17,44 @@ const props = defineProps({
type: String,
required: false,
default: () => {
return `copy-input-${Math.random().toString(36).substring(2, 15)}`;
return `copy-input-${Math.random().toString(36).substring(2, 15)}`
},
},
});
})
const model = defineModel<string>();
const model = defineModel<string>()
const message = useMessage();
const message = useMessage()
const clipboard = new ClipboardJS(props.copyIcon ? '#' + props.id + 'btn' : '#' + props.id + 'btn2');
const clipboard = new ClipboardJS(props.copyIcon ? '#' + props.id + 'btn' : '#' + props.id + 'btn2')
clipboard.on('success', function (e) {
message.success(t('Copy Success'));
e.clearSelection();
});
message.success(t('Copy Success'))
e.clearSelection()
})
// 这里成功的时候也响应error,所以这里也加上
clipboard.on('error', function (e) {
// 如果复制失败,则使用navigator.clipboard.writeText进行复制
if (navigator.clipboard) {
navigator.clipboard.writeText(model.value || '');
navigator.clipboard.writeText(model.value || '')
}
message.success(t('Copy Success'));
});
message.success(t('Copy Success'))
})
onUnmounted(() => {
clipboard.destroy();
});
clipboard.destroy()
})
</script>
<template>
<div class="h-full">
<NInputGroup>
<n-input v-model:value="model" :readonly="props.readonly" :id="props.id" />
<n-input-group-label
v-if="props.copyIcon"
class="flex items-center"
:id="props.id + 'btn'"
:data-clipboard-target="`#${props.id}`"
>
<n-input-group-label v-if="props.copyIcon" class="flex items-center cursor-pointer" :id="props.id + 'btn'" :data-clipboard-target="`#${props.id}`" :title="t('Copy')">
<Icon :size="25" name="ri:file-copy-line"></Icon>
</n-input-group-label>
<NButton type="primary" v-else :id="props.id + 'btn2'" :data-clipboard-target="`#${props.id}`">{{
t('Copy')
}}</NButton>
}}</NButton>
</NInputGroup>
</div>
</template>
+222 -245
View File
@@ -17,21 +17,17 @@
<Icon name="ri:add-line" class="mr-2" />
<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>
</n-button>
<n-button @click="() => (uploadPickerVisible = true)">
<Icon name="ri:file-add-line" class="mr-2" />
<span>{{ t('Upload File') + '/' + t('Folder') }}</span>
</n-button>
<n-button :disabled="!checkedKeys.length" secondary @click="handleBatchDelete">
<n-button v-show="checkedKeys.length > 0" :disabled="!checkedKeys.length" type="error" @click="handleBatchDelete">
<template #icon>
<Icon name="ri:delete-bin-5-line"></Icon>
</template>
{{ t('Delete Selected') }}
</n-button>
<n-button :disabled="!checkedKeys.length" secondary @click="downloadMultiple">
<n-button v-show="checkedKeys.length > 0" :disabled="!checkedKeys.length" @click="downloadMultiple">
<Icon name="ri:download-cloud-2-line" class="mr-2" />
<span>{{ t('Download') }}</span>
</n-button>
@@ -42,38 +38,20 @@
</div>
</template>
</n-page-header>
<n-data-table
class="border dark:border-neutral-700 rounded overflow-hidden"
:columns="columns"
:data="filteredObjects"
:row-key="rowKey"
@update:checked-row-keys="handleCheck"
:pagination="false"
:bordered="false"
/>
<object-upload-picker
:show="uploadPickerVisible"
@update:show="
val => {
uploadPickerVisible = val;
refresh();
}
"
:bucketName="bucketName"
:prefix="prefix"
/>
<object-new-form
:show="newObjectFormVisible"
:asPrefix="newObjectAsPrefix"
@update:show="
val => {
newObjectFormVisible = val;
refresh();
}
"
:bucketName="bucketName"
:prefix="prefix"
/>
<n-data-table class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="filteredObjects" :row-key="rowKey" @update:checked-row-keys="handleCheck"
:pagination="false" :bordered="false" />
<object-upload-picker :show="uploadPickerVisible" @update:show="
val => {
uploadPickerVisible = val
refresh()
}
" :bucketName="bucketName" :prefix="prefix" />
<object-new-form :show="newObjectFormVisible" :asPrefix="newObjectAsPrefix" @update:show="
val => {
newObjectFormVisible = val
refresh()
}
" :bucketName="bucketName" :prefix="prefix" />
<n-button-group class="ml-auto">
<n-button @click="goToPreviousPage" :disabled="!continuationToken">
<Icon name="ri:arrow-left-s-line" class="mr-2" />
@@ -88,104 +66,103 @@
</template>
<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 { NButton, NSpace, NPopconfirm, type DataTableColumns, type DataTableRowKey } from 'naive-ui';
import { Icon } from '#components';
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 JSZip from 'jszip';
import { saveAs } from 'file-saver';
const { $s3Client } = useNuxtApp()
const { t } = useI18n()
import { useAsyncData, useRoute, useRouter } from '#app'
import { Icon, NuxtLink } from '#components'
import { ListObjectsV2Command, type _Object, type CommonPrefix } from '@aws-sdk/client-s3'
import dayjs from 'dayjs'
import { saveAs } from 'file-saver'
import JSZip from 'jszip'
import { NButton, NPopconfirm, NSpace, type DataTableColumns, type DataTableRowKey } from 'naive-ui'
import { joinRelativeURL } from 'ufo'
import { computed, ref, watch, type VNode } from 'vue'
import { useBucket } from '~/composables/useBucket'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
const route = useRoute();
const router = useRouter();
const dialog = useDialog();
const message = useMessage();
const props = defineProps<{ bucket: string; path: string }>();
const route = useRoute()
const router = useRouter()
const dialog = useDialog()
const message = useMessage()
const props = defineProps<{ bucket: string; path: string }>()
const uploadPickerVisible = ref(false);
const newObjectFormVisible = ref(false);
const newObjectAsPrefix = ref(false);
const searchTerm = ref('');
const uploadPickerVisible = ref(false)
const newObjectFormVisible = ref(false)
const newObjectAsPrefix = ref(false)
const searchTerm = ref('')
// Add debounce function for search
const debounce = (fn: Function, delay: number) => {
let timer: NodeJS.Timeout | null = null;
let timer: NodeJS.Timeout | null = null
return (...args: any[]) => {
if (timer) clearTimeout(timer);
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
fn(...args)
}, delay)
}
}
const handleSearch = debounce(() => {
// Reset pagination when searching
if (continuationToken.value) {
continuationToken.value = undefined;
tokenHistory.value = [];
continuationToken.value = undefined
tokenHistory.value = []
}
}, 300);
}, 300)
// 上传任务
const uploadTaskStore = useUploadTaskManagerStore();
const uploadTasks = computed(() => uploadTaskStore.tasks);
const uploadTaskStore = useUploadTaskManagerStore()
const uploadTasks = computed(() => uploadTaskStore.tasks)
// 删除任务
const deleteTaskStore = useDeleteTaskManagerStore();
const deleteTasks = computed(() => deleteTaskStore.tasks);
const deleteTaskStore = useDeleteTaskManagerStore()
const deleteTasks = computed(() => deleteTaskStore.tasks)
// 当任务变化时,刷新数据
watch(
() => uploadTasks,
() => setTimeout(refresh, 500),
{ deep: true }
);
)
watch(
() => deleteTasks,
() => setTimeout(refresh, 500),
{ deep: true }
);
)
// bucketName
const bucketName = computed(() => props.bucket as string);
const bucketName = computed(() => props.bucket as string)
// 当前路径的前缀, example: '/folder1/folder2/'
const prefix = computed(() => decodeURIComponent(props.path as string));
const prefix = computed(() => decodeURIComponent(props.path as string))
// query 参数
const pageSize = computed(() => parseInt(route.query.pageSize as string, 10));
const pageSize = computed(() => parseInt(route.query.pageSize as string, 10))
// 将 continuationToken 改为 ref
const continuationToken = ref<string | undefined>(undefined);
const continuationToken = ref<string | undefined>(undefined)
// 新建文件夹
const handleNewObject = (asPrefix: boolean) => {
newObjectFormVisible.value = true;
newObjectAsPrefix.value = asPrefix;
};
newObjectFormVisible.value = true
newObjectAsPrefix.value = asPrefix
}
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) : '');
};
interface RowData {
Key: string;
type: 'prefix' | 'object';
Size: number;
LastModified: string;
return joinRelativeURL('/browser', encodeURIComponent(bucketName.value), path ? encodeURIComponent(path) : '')
}
const infoRef = ref();
interface RowData {
Key: string
type: 'prefix' | 'object'
Size: number
LastModified: string
}
const infoRef = ref()
const columns: DataTableColumns<RowData> = [
{
type: 'selection',
@@ -194,27 +171,27 @@ const columns: DataTableColumns<RowData> = [
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 || '/';
const displayKey = prefix.value ? row.Key.substring(prefix.value.length) : row.Key
let label: string | VNode = displayKey || '/'
if (row.type === 'prefix') {
label = h('span', { class: 'inline-flex items-center gap-2' }, [icon('ri:folder-line'), label]);
label = h('span', { class: 'inline-flex items-center gap-2' }, [icon('ri:folder-line'), label])
}
const keyInUri = row.Key;
const keyInUri = row.Key
return h(
NuxtLink,
{
href: row.type === 'prefix' ? bucketPath(keyInUri) : '',
class: 'block text-cyan-400 cursor-pointer',
onClick: (e: MouseEvent) => {
if (row.type === 'prefix') return;
infoRef.value.openDrawer(bucketName.value, row.Key);
return;
if (row.type === 'prefix') return
infoRef.value.openDrawer(bucketName.value, row.Key)
return
},
},
() => label
);
)
},
},
{
@@ -226,14 +203,14 @@ const columns: DataTableColumns<RowData> = [
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') : ''
},
},
{
title: t('Actions'),
key: 'actions',
align: 'center',
width: 150,
width: 250,
render: (row: RowData) => {
return h(
NSpace,
@@ -252,7 +229,7 @@ const columns: DataTableColumns<RowData> = [
NButton,
{ size: 'small', secondary: true },
{
default: () => '',
default: () => t('Download'),
icon: () => h(Icon, { name: 'ri:download-2-line' }),
}
),
@@ -283,7 +260,7 @@ const columns: DataTableColumns<RowData> = [
NButton,
{ size: 'small', type: 'error', secondary: true },
{
default: () => '',
default: () => t('Delete'),
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
}
),
@@ -291,26 +268,26 @@ const columns: DataTableColumns<RowData> = [
),
],
}
);
)
},
},
];
]
interface ListObjectsResponse {
contents: _Object[];
commonPrefixes: CommonPrefix[];
nextContinuationToken: string | null;
isTruncated: boolean;
contents: _Object[]
commonPrefixes: CommonPrefix[]
nextContinuationToken: string | null
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));
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result;
};
const salt = ref(randomString());
return result
}
const salt = ref(randomString())
// 在服务端获取数据
const { data, refresh } = await useAsyncData<ListObjectsResponse>(
`objectsData-${salt.value}&${prefix.value}&${pageSize.value}&${continuationToken.value}`,
@@ -321,30 +298,30 @@ const { data, refresh } = await useAsyncData<ListObjectsResponse>(
Delimiter: '/',
Prefix: prefix.value || undefined,
ContinuationToken: continuationToken.value,
};
}
const result = await $s3Client.send(new ListObjectsV2Command(params));
const result = await $s3Client.send(new ListObjectsV2Command(params))
return {
contents: result.Contents || [],
commonPrefixes: result.CommonPrefixes || [],
nextContinuationToken: result.NextContinuationToken || null,
isTruncated: result.IsTruncated ?? false,
};
}
}
);
)
watch(
continuationToken,
() => {
refresh();
refresh()
},
{ deep: true }
);
)
const contents = computed(() => data.value?.contents || []);
const commonPrefixes = computed(() => data.value?.commonPrefixes || []);
const nextToken = computed(() => data.value?.nextContinuationToken || null);
const contents = computed(() => data.value?.contents || [])
const commonPrefixes = computed(() => data.value?.commonPrefixes || [])
const nextToken = computed(() => data.value?.nextContinuationToken || null)
const objects = computed(() => {
return commonPrefixes.value
@@ -353,7 +330,7 @@ const objects = computed(() => {
Key: prefix.Prefix,
type: 'prefix',
Size: 0,
};
}
})
.concat(
contents.value.map(object => {
@@ -362,31 +339,31 @@ const objects = computed(() => {
type: 'object',
Size: object.Size ?? 0,
LastModified: object.LastModified ?? new Date(0),
};
}
})
);
});
)
})
// New computed property to filter objects based on search term
const filteredObjects = computed(() => {
if (!searchTerm.value.trim()) {
return objects.value;
return objects.value
}
const term = searchTerm.value.toLowerCase();
const term = searchTerm.value.toLowerCase()
return objects.value.filter(obj => {
const displayKey = prefix.value ? obj.Key?.substring(prefix.value.length) : obj.Key;
return displayKey?.toLowerCase().includes(term);
});
});
const displayKey = prefix.value ? obj.Key?.substring(prefix.value.length) : obj.Key
return displayKey?.toLowerCase().includes(term)
})
})
/** ************************************处理对象删除********************************* */
// 处理对象被删除(从版本组件触发)
const handleObjectDeleted = () => {
// 刷新文件列表
salt.value = randomString();
refresh();
};
salt.value = randomString()
refresh()
}
/** ************************************删除所有版本********************************* */
// 删除文件的所有版本或文件夹及其内容
@@ -394,50 +371,50 @@ const handleDeleteAllVersions = async (row: RowData) => {
try {
if (row.type === 'object') {
// 删除文件的所有版本
const { deleteAllVersions } = useObject({ bucket: bucketName.value });
await deleteAllVersions(row.Key);
message.success(t('Delete All Versions Success'));
const { deleteAllVersions } = useObject({ bucket: bucketName.value })
await deleteAllVersions(row.Key)
message.success(t('Delete All Versions Success'))
} else if (row.type === 'prefix') {
// 删除文件夹及其所有内容
const { mapAllFiles } = useObject({ bucket: bucketName.value });
const files: string[] = [];
const { mapAllFiles } = useObject({ bucket: bucketName.value })
const files: string[] = []
// 递归收集文件夹下的所有文件
await mapAllFiles(bucketName.value, row.Key, (fileKey: string) => {
files.push(fileKey);
});
files.push(fileKey)
})
if (files.length > 0) {
// 使用删除任务管理器删除所有文件
deleteTaskStore.addKeys(files, bucketName.value);
message.success(t('Delete task created'));
deleteTaskStore.addKeys(files, bucketName.value)
message.success(t('Delete task created'))
} else {
message.success(t('Delete Success'));
message.success(t('Delete Success'))
}
}
// 刷新数据
salt.value = randomString();
refresh();
salt.value = randomString()
refresh()
} catch (error: any) {
console.error('Delete failed:', error);
message.error(t('Delete Failed'));
console.error('Delete failed:', error)
message.error(t('Delete Failed'))
}
};
}
/** ************************************批量删除********************************* */
function rowKey(row: any): string {
return row.Key || '';
return row.Key || ''
}
const checkedKeys = ref<DataTableRowKey[]>([]);
const checkedKeys = ref<DataTableRowKey[]>([])
function handleCheck(keys: DataTableRowKey[]) {
// 过滤掉 undefined
checkedKeys.value = keys.filter((k): k is string => typeof k === 'string');
return checkedKeys;
checkedKeys.value = keys.filter((k): k is string => typeof k === 'string')
return checkedKeys
}
const objectApi = useObject({ bucket: bucketName.value });
const bucketApi = useBucket({});
const objectApi = useObject({ bucket: bucketName.value })
const bucketApi = useBucket({})
// 批量删除
function handleBatchDelete() {
@@ -448,33 +425,33 @@ function handleBatchDelete() {
negativeText: t('Cancel'),
onPositiveClick: async () => {
if (!checkedKeys.value.length) {
message.error(t('Please select at least one item'));
return;
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);
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);
});
deleteTaskStore.addKeys([fileKey], bucketName.value)
})
}
return deleteTaskStore.addKeys([String(item)], bucketName.value);
return deleteTaskStore.addKeys([String(item)], bucketName.value)
})
);
)
message.success(t('Delete task created'));
salt.value = randomString();
refresh();
message.success(t('Delete task created'))
salt.value = randomString()
refresh()
} catch (error) {
message.error(t('Delete Failed'));
message.error(t('Delete Failed'))
}
},
});
})
}
/** ************************************批量删除********************************* */
@@ -483,106 +460,106 @@ function handleBatchDelete() {
const handledownload = async (item: any) => {
if (item.type === 'object') {
// 单文件下载
const msg = message.loading(t('Getting URL'));
const url = await objectApi.getSignedUrl(item.Key);
msg.destroy();
window.open(url, '_blank');
const msg = message.loading(t('Getting URL'))
const url = await objectApi.getSignedUrl(item.Key)
msg.destroy()
window.open(url, '_blank')
} else if (item.type === 'prefix') {
// 文件夹下载
const msg = message.loading(t('Preparing folder...'));
const msg = message.loading(t('Preparing folder...'))
// 递归获取所有文件
const files: string[] = [];
const files: string[] = []
await objectApi.mapAllFiles(bucketName.value, item.Key, (fileKey: string) => {
files.push(fileKey);
});
files.push(fileKey)
})
if (files.length === 0) {
msg.destroy();
message.warning(t('Folder is empty'));
return;
msg.destroy()
message.warning(t('Folder is empty'))
return
}
msg.destroy();
const zip = new JSZip();
msg.destroy()
const zip = new JSZip()
// 批量获取文件内容并添加到 zip
const downloadMsg = message.loading(t('Downloading files...'));
const downloadMsg = message.loading(t('Downloading files...'))
await Promise.all(
files.map(async fileKey => {
const url = await objectApi.getSignedUrl(fileKey);
const response = await fetch(url);
const blob = await response.blob();
const url = await objectApi.getSignedUrl(fileKey)
const response = await fetch(url)
const blob = await response.blob()
// 保持相对路径
zip.file(fileKey.substring(item.Key.length), blob);
zip.file(fileKey.substring(item.Key.length), blob)
})
);
downloadMsg.destroy();
)
downloadMsg.destroy()
// 生成 zip 并下载
const zipBlob = await zip.generateAsync({ type: 'blob' });
saveAs(zipBlob, `${item.Key.replace(/\/$/, '') || 'folder'}.zip`);
message.success(t('Download ready'));
const zipBlob = await zip.generateAsync({ type: 'blob' })
saveAs(zipBlob, `${item.Key.replace(/\/$/, '') || 'folder'}.zip`)
message.success(t('Download ready'))
}
};
}
// 批量下载
const downloadMultiple = async () => {
if (!checkedKeys.value.length) {
message.warning(t('Please select at least one item'));
return;
message.warning(t('Please select at least one item'))
return
}
// 收集文件 loading
let collectMsg = message.loading(t('Collecting files'));
let collectMsg = message.loading(t('Collecting files'))
// 1. 找到所有选中的对象
const selectedItems = objects.value.filter(obj => checkedKeys.value.includes(obj.Key as string));
const selectedItems = objects.value.filter(obj => checkedKeys.value.includes(obj.Key as string))
// 2. 递归收集所有文件
let allFiles: { key: string; relative: string }[] = [];
let allFiles: { key: string; relative: string }[] = []
for (const item of selectedItems) {
if (item.type === 'object') {
const key = item.Key || '';
const rel = prefix.value ? key.substring(prefix.value.length) : key;
allFiles.push({ key, relative: rel });
const key = item.Key || ''
const rel = prefix.value ? key.substring(prefix.value.length) : key
allFiles.push({ key, relative: rel })
} else if (item.type === 'prefix') {
const key = item.Key || '';
const key = item.Key || ''
await objectApi.mapAllFiles(bucketName.value, key, fileKey => {
const rel = prefix.value ? fileKey.substring(prefix.value.length) : fileKey;
allFiles.push({ key: fileKey, relative: rel });
});
const rel = prefix.value ? fileKey.substring(prefix.value.length) : fileKey
allFiles.push({ key: fileKey, relative: rel })
})
}
}
collectMsg.destroy();
collectMsg.destroy()
if (allFiles.length === 0) {
message.warning(t('No files to download'));
return;
message.warning(t('No files to download'))
return
}
// 下载文件 loading,带进度
let percent = ref(0);
const total = allFiles.length;
let finished = 0;
let downloadMsg: any = null;
let percent = ref(0)
const total = allFiles.length
let finished = 0
let downloadMsg: any = null
const updateDownloadMsg = () => {
if (downloadMsg) downloadMsg.destroy();
if (downloadMsg) downloadMsg.destroy()
downloadMsg = message.loading(`${t('Downloading files')} ${Math.round((finished / total) * 100)}%`, {
duration: 0,
});
};
updateDownloadMsg();
const zip = new JSZip();
})
}
updateDownloadMsg()
const zip = new JSZip()
await Promise.all(
allFiles.map(async ({ key, relative }) => {
const url = await objectApi.getSignedUrl(key);
const response = await fetch(url);
const blob = await response.blob();
zip.file(relative, blob);
finished++;
updateDownloadMsg();
const url = await objectApi.getSignedUrl(key)
const response = await fetch(url)
const blob = await response.blob()
zip.file(relative, blob)
finished++
updateDownloadMsg()
})
);
if (downloadMsg) downloadMsg.destroy();
const zipBlob = await zip.generateAsync({ type: 'blob' });
saveAs(zipBlob, `download.zip`);
message.success(t('Download ready'));
};
)
if (downloadMsg) downloadMsg.destroy()
const zipBlob = await zip.generateAsync({ type: 'blob' })
saveAs(zipBlob, `download.zip`)
message.success(t('Download ready'))
}
/** ************************************下载********************************* */
// 为了实现 "Previous" 功能,需要记录访问过的 token 列表。
@@ -592,40 +569,40 @@ const downloadMultiple = async () => {
// 因此"上一页"可以通过浏览器后退实现,也可以在数据中保存 token 来人工实现。
// 这里演示一个简化版本——在客户端保存 tokenHistory。
// 请注意:刷新后 tokenHistory 会丢失,因为它是前端状态。
const tokenHistory = ref<string[]>([]);
const tokenHistory = ref<string[]>([])
// 当页面加载后,如果 continuationToken 有值,就表示不是第一页
// 将当前 continuationToken 添加到历史中
if (continuationToken.value && !tokenHistory.value.includes(continuationToken.value)) {
tokenHistory.value.push(continuationToken.value);
tokenHistory.value.push(continuationToken.value)
}
// previousToken 根据 tokenHistory 来确定
// tokenHistory 中最后一个是当前页的 token,上一个则是 previousToken
const previousToken = computed(() => {
if (tokenHistory.value.length < 2) return null;
if (tokenHistory.value.length < 2) return null
// 倒数第二个是上一页的 token
return tokenHistory.value[tokenHistory.value.length - 2];
});
return tokenHistory.value[tokenHistory.value.length - 2]
})
// 修改分页方法
function goToNextPage() {
if (nextToken.value) {
continuationToken.value = nextToken.value;
tokenHistory.value.push(nextToken.value);
continuationToken.value = nextToken.value
tokenHistory.value.push(nextToken.value)
}
}
function goToPreviousPage() {
if (previousToken.value) {
continuationToken.value = previousToken.value;
continuationToken.value = previousToken.value
// 将上一页 token 之后的记录删除
const prevIndex = tokenHistory.value.indexOf(previousToken.value);
tokenHistory.value.splice(prevIndex + 1);
const prevIndex = tokenHistory.value.indexOf(previousToken.value)
tokenHistory.value.splice(prevIndex + 1)
} else {
// 回到第一页
continuationToken.value = undefined;
tokenHistory.value = [];
continuationToken.value = undefined
tokenHistory.value = []
}
}
</script>
+1 -3
View File
@@ -1,7 +1,5 @@
<template>
<div
class="p-4 sticky top-0 z-10 px-6 h-16 border-b dark:border-neutral-800 flex justify-between bg-gray-100 dark:bg-neutral-900"
>
<div class="p-4 sticky top-0 z-10 px-6 h-16 border-b bg-background flex justify-between ">
<slot name="title"></slot>
<slot name="actions"></slot>
</div>
+59 -71
View File
@@ -11,8 +11,8 @@
<template #icon>
<Icon name="ri:delete-bin-5-line"></Icon>
</template>
删除选中项
</NButton> -->
删除选中项
</NButton> -->
<NButton :disabled="!checkedKeys.length" secondary @click="allocationPolicy">
<template #icon>
<Icon name="ri:group-2-fill"></Icon>
@@ -29,27 +29,16 @@
</n-space>
</n-flex>
</n-form>
<n-data-table
ref="tableRef"
:columns="columns"
:data="listData"
:pagination="false"
:bordered="true"
max-height="calc(100vh - 320px)"
:row-key="rowKey"
@update:checked-row-keys="handleCheck"
/>
<n-data-table ref="tableRef" :columns="columns" :data="listData" :pagination="false" :bordered="true" max-height="calc(100vh - 320px)" :row-key="rowKey"
@update:checked-row-keys="handleCheck" />
<users-group-edit ref="editItemRef"></users-group-edit>
<users-group-new v-model:visible="newItemVisible" @search="getDataList" ref="newItemRef"></users-group-new>
<users-group-set-policies-mutiple
:checkedKeys="checkedKeys"
@changePoliciesSuccess="changePoliciesSuccess"
ref="policiesRef"
></users-group-set-policies-mutiple>
<users-group-set-policies-mutiple :checkedKeys="checkedKeys" @changePoliciesSuccess="changePoliciesSuccess" ref="policiesRef"></users-group-set-policies-mutiple>
</div>
</template>
<script setup lang="ts">
import { Icon } from '#components'
import {
type DataTableColumns,
type DataTableInst,
@@ -57,24 +46,23 @@ import {
NButton,
NPopconfirm,
NSpace,
} from 'naive-ui';
import { Icon } from '#components';
import { useI18n } from 'vue-i18n';
} from 'naive-ui'
import { useI18n } from 'vue-i18n'
// import { groupEdit, newGroup, setPoliciesMutiple } from '../components';
const messge = useMessage();
const { t } = useI18n();
const messge = useMessage()
const { t } = useI18n()
const { $api } = useNuxtApp();
const dialog = useDialog();
const message = useMessage();
const group = useGroups();
const { $api } = useNuxtApp()
const dialog = useDialog()
const message = useMessage()
const group = useGroups()
const searchForm = reactive({
name: '',
});
})
interface RowData {
name: string;
name: string
}
const columns: DataTableColumns<RowData> = [
@@ -86,7 +74,7 @@ const columns: DataTableColumns<RowData> = [
align: 'left',
key: 'name',
filter(value, row) {
return !!row.name.includes(value.toString());
return !!row.name.includes(value.toString())
},
},
{
@@ -110,7 +98,7 @@ const columns: DataTableColumns<RowData> = [
onClick: () => openEditItem(row),
},
{
default: () => '',
default: () => t('Edit'),
icon: () => h(Icon, { name: 'ri:edit-2-line' }),
}
),
@@ -124,7 +112,7 @@ const columns: DataTableColumns<RowData> = [
NButton,
{ size: 'small', secondary: true },
{
default: () => '',
default: () => t('Delete'),
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
}
),
@@ -132,71 +120,71 @@ const columns: DataTableColumns<RowData> = [
),
],
}
);
)
},
},
];
]
// 搜索过滤
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[]>([])
onMounted(() => {
getDataList();
});
getDataList()
})
// 获取数据
const getDataList = async () => {
try {
const res = await group.listGroup();
const res = await group.listGroup()
listData.value =
res?.map((item: string) => {
return {
name: item,
};
}) || [];
checkedKeys.value = [];
}
}) || []
checkedKeys.value = []
} catch (error) {
message.error(t('Failed to get data'));
message.error(t('Failed to get data'))
}
};
}
/** **********************************添加 */
const newItemRef = ref();
const newItemVisible = ref(false);
const newItemRef = ref()
const newItemVisible = ref(false)
function addUserGroup() {
newItemVisible.value = true;
newItemVisible.value = true
}
/** **********************************分配策略 */
const policiesRef = ref();
const policiesRef = ref()
const allocationPolicy = () => {
policiesRef.value.openDialog();
};
policiesRef.value.openDialog()
}
const changePoliciesSuccess = () => {
checkedKeys.value = [];
};
checkedKeys.value = []
}
/** **********************************修改 */
const editItemRef = ref();
const editItemRef = ref()
function openEditItem(row: any) {
editItemRef.value.openDialog(row);
editItemRef.value.openDialog(row)
}
/** ***********************************删除 */
async function deleteItem(row: any) {
try {
// 获取组的成员
const info = await group.getGroup(row.name);
const info = await group.getGroup(row.name)
if (info.members.length) {
messge.error('请先清空组成员');
return;
messge.error('请先清空组成员')
return
}
// 清空组的成员
await group.updateGroupMembers({
@@ -204,23 +192,23 @@ async function deleteItem(row: any) {
members: info.members,
isRemove: true,
groupStatus: 'enabled',
});
message.success(t('Delete Success'));
getDataList();
})
message.success(t('Delete Success'))
getDataList()
} catch (error) {
message.error(t('Delete Failed'));
message.error(t('Delete Failed'))
}
}
/** ************************************批量删除 */
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
}
function deleteByList() {
dialog.error({
@@ -230,16 +218,16 @@ function deleteByList() {
negativeText: t('Cancel'),
onPositiveClick: () => {
if (!checkedKeys.value.length) {
message.error(t('Please select at least one item'));
return;
message.error(t('Please select at least one item'))
return
}
checkedKeys.value.forEach(async (element: any) => {
const res = await group.removeGroup(element);
});
const res = await group.removeGroup(element)
})
getDataList();
getDataList()
},
});
})
}
</script>
+47 -55
View File
@@ -29,21 +29,14 @@
</n-space>
</n-flex>
</n-form>
<n-data-table
ref="tableRef"
:columns="columns"
:data="listData"
:pagination="false"
:bordered="true"
:row-key="rowKey"
@update:checked-row-keys="handleCheck"
/>
<n-data-table ref="tableRef" :columns="columns" :data="listData" :pagination="false" :bordered="true" :row-key="rowKey" @update:checked-row-keys="handleCheck" />
<users-user-new @search="getDataList" ref="newItemRef"></users-user-new>
<users-user-edit @search="getDataList" :checkedKeys="checkedKeys" ref="editItemRef"></users-user-edit>
</div>
</template>
<script setup lang="ts">
import { Icon } from '#components'
import {
type DataTableColumns,
type DataTableInst,
@@ -51,22 +44,21 @@ import {
NButton,
NPopconfirm,
NSpace,
} from 'naive-ui';
import { Icon } from '#components';
import { useI18n } from 'vue-i18n';
} from 'naive-ui'
import { useI18n } from 'vue-i18n'
const { t } = useI18n();
const { t } = useI18n()
const { $api } = useNuxtApp();
const { listUsers, deleteUser } = useUsers();
const dialog = useDialog();
const message = useMessage();
const { $api } = useNuxtApp()
const { listUsers, deleteUser } = useUsers()
const dialog = useDialog()
const message = useMessage()
const searchForm = reactive({
accessKey: '',
});
})
interface RowData {
accessKey: string;
accessKey: string
}
const columns: DataTableColumns<RowData> = [
@@ -78,7 +70,7 @@ const columns: DataTableColumns<RowData> = [
align: 'left',
key: 'accessKey',
filter(value, row) {
return !!row.accessKey.includes(value.toString());
return !!row.accessKey.includes(value.toString())
},
},
{
@@ -102,7 +94,7 @@ const columns: DataTableColumns<RowData> = [
onClick: () => openEditItem(row),
},
{
default: () => '',
default: () => t('Edit'),
icon: () => h(Icon, { name: 'ri:edit-2-line' }),
}
),
@@ -116,7 +108,7 @@ const columns: DataTableColumns<RowData> = [
NButton,
{ size: 'small', secondary: true },
{
default: () => '',
default: () => t('Delete'),
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
}
),
@@ -124,74 +116,74 @@ const columns: DataTableColumns<RowData> = [
),
],
}
);
)
},
},
];
]
// 搜索过滤
const tableRef = ref<DataTableInst>();
const tableRef = ref<DataTableInst>()
function filterName(value: string) {
tableRef.value &&
tableRef.value.filter({
accessKey: [value],
});
})
}
const listData = ref<any[]>([]);
const listData = ref<any[]>([])
onMounted(() => {
getDataList();
});
getDataList()
})
// 获取数据
const getDataList = async () => {
try {
const res = await listUsers();
const res = await listUsers()
listData.value = Object.entries(res).map(([username, info]) => ({
accessKey: username, // 添加用户名
...(typeof info === 'object' ? info : {}), // 展开用户信息
}));
}))
} catch (error) {
message.error(t('Failed to get data'));
message.error(t('Failed to get data'))
}
};
}
/** **********************************添加 */
const newItemRef = ref();
const newItemRef = ref()
function addUserItem() {
newItemRef.value.openDialog();
newItemRef.value.openDialog()
}
/** **********************************添加到的分组 */
const addToGroup = () => {};
const addToGroup = () => { }
/** **********************************修改 */
const editItemRef = ref();
const editItemRef = ref()
function openEditItem(row: any) {
editItemRef.value.openDialog(row);
editItemRef.value.openDialog(row)
}
/** ***********************************删除 */
async function deleteItem(row: any) {
try {
const res = await deleteUser(row.accessKey);
message.success(t('Delete Success'));
getDataList();
const res = await deleteUser(row.accessKey)
message.success(t('Delete Success'))
getDataList()
} catch (error) {
message.error(t('Delete Failed'));
message.error(t('Delete Failed'))
}
}
/** ************************************批量删除 */
function rowKey(row: any): string {
return row.accessKey;
return row.accessKey
}
const checkedKeys = ref<DataTableRowKey[]>([]);
const checkedKeys = ref<DataTableRowKey[]>([])
function handleCheck(keys: DataTableRowKey[]) {
checkedKeys.value = keys;
return checkedKeys;
checkedKeys.value = keys
return checkedKeys
}
function deleteByList() {
dialog.error({
@@ -201,22 +193,22 @@ function deleteByList() {
negativeText: t('Cancel'),
onPositiveClick: async () => {
if (!checkedKeys.value.length) {
message.error(t('Please select at least one item'));
return;
message.error(t('Please select at least one item'))
return
}
try {
// 循环遍历删除
await Promise.all(checkedKeys.value.map(item => deleteUser(item as string)));
checkedKeys.value = [];
message.success(t('Delete Success'));
await Promise.all(checkedKeys.value.map(item => deleteUser(item as string)))
checkedKeys.value = []
message.success(t('Delete Success'))
nextTick(() => {
getDataList();
});
getDataList()
})
} catch (error) {
message.error(t('Delete Failed'));
message.error(t('Delete Failed'))
}
},
});
})
}
</script>