refactor: migrate lifecycle management to shadcn components

This commit is contained in:
overtrue
2025-10-26 09:39:56 +08:00
parent 32ce9c6afa
commit d7fd4fd56b
2 changed files with 582 additions and 577 deletions
+407 -389
View File
@@ -1,425 +1,443 @@
<template>
<n-modal
v-model:show="visible"
:mask-closable="false"
preset="card"
:title="t('Add Lifecycle Rule') + ` (${t('Bucket')}: ${bucketName})`"
class="max-w-screen-md"
:segmented="{
content: true,
action: true,
}"
<AppModal
v-model="visible"
:title="`${t('Add Lifecycle Rule')} (${t('Bucket')}: ${bucketName})`"
size="lg"
:close-on-backdrop="false"
>
<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-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" />
</n-form-item>
<n-form-item :label="t('Time Cycle')" path="days">
<div class="w-full flex items-center justify-between">
<n-input-number
class="flex-auto"
v-model:value="formData.days"
:min="1"
<div class="space-y-6">
<Tabs v-model="activeTab" class="flex flex-col gap-4">
<TabsList class="w-full justify-start overflow-x-auto">
<TabsTrigger value="expire">{{ t('Expiration') }}</TabsTrigger>
<TabsTrigger value="transition">{{ t('Transition') }}</TabsTrigger>
</TabsList>
<TabsContent value="expire" class="mt-0 space-y-6">
<AppCard padded class="space-y-4">
<div v-if="versioningStatus" class="grid gap-2">
<Label>{{ t('Object Version') }}</Label>
<AppSelect v-model="formData.versionType" :options="versionOptions" />
</div>
<div class="grid gap-2">
<Label>{{ t('Time Cycle') }}</Label>
<div class="flex items-center gap-3">
<AppInput
v-model="formData.days"
type="number"
min="1"
class="w-32"
:placeholder="t('Days')"
style="width: 100px"
/>
<span class="ms-4">{{ t('Days After') }}</span>
<span class="text-sm text-muted-foreground">{{ t('Days After') }}</span>
</div>
</n-form-item>
<!-- 生命周期 -->
<n-card>
<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-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')"
/>
</n-form-item>
</n-collapse-item>
</n-collapse>
</n-card>
<!-- 高级设置 -->
<n-card class="my-4" v-if="formData.versionType == 'current'">
<n-collapse>
<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">{{
t('If no versions remain, delete references to this object')
}}</span>
</n-space>
</n-form-item>
</div>
</AppCard>
<!-- <n-form-item label="版本清理">
<n-space>
<n-switch v-model:value="formData.deleteAllExpired" :round="false" />
<span class="ml-4 text-gray-500">删除所有过期版本</span>
</n-space>
</n-form-item> -->
</n-collapse-item>
</n-collapse>
</n-card>
</n-tab-pane>
<n-tab-pane name="transition" :tab="t('Transition')">
<n-form-item :label="t('Object Version')" path="versionType" v-if="versioningStatus">
<n-select v-model:value="formData.versionType" :options="versionOptions" />
</n-form-item>
<n-form-item :label="t('Time Cycle')" path="days">
<div class="w-full flex items-center justify-between">
<n-input-number
class="flex-auto"
v-model:value="formData.days"
:min="1"
<AppCard padded class="space-y-4">
<details>
<summary class="cursor-pointer text-sm font-medium text-primary">{{ t('More Configurations') }}</summary>
<div class="mt-4 space-y-4">
<div class="grid gap-2">
<Label>{{ t('Prefix') }}</Label>
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
<AppButton variant="outline" size="sm" @click="addTag">
<Icon name="ri:add-line" class="size-4" />
{{ t('Add Tag') }}
</AppButton>
</div>
<div v-if="formData.tags.length" class="space-y-3">
<div
v-for="(tag, index) in formData.tags"
:key="index"
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
>
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
<div class="flex items-center gap-2">
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<AppButton
variant="ghost"
size="sm"
class="text-destructive"
:disabled="formData.tags.length === 1"
@click="removeTag(index)"
>
<Icon name="ri:delete-bin-line" class="size-4" />
</AppButton>
</div>
</div>
</div>
</div>
</div>
</details>
</AppCard>
<AppCard v-if="formData.versionType === 'current'" padded>
<details>
<summary class="cursor-pointer text-sm font-medium text-primary">{{ t('Advanced Settings') }}</summary>
<div class="mt-4 flex items-center justify-between">
<div>
<p class="text-sm font-medium">{{ t('Delete Marker Handling') }}</p>
<p class="text-xs text-muted-foreground">
{{ t('If no versions remain, delete references to this object') }}
</p>
</div>
<AppSwitch v-model="formData.expiredDeleteMark" />
</div>
</details>
</AppCard>
</TabsContent>
<TabsContent value="transition" class="mt-0 space-y-6">
<AppCard padded class="space-y-4">
<div v-if="versioningStatus" class="grid gap-2">
<Label>{{ t('Object Version') }}</Label>
<AppSelect v-model="formData.versionType" :options="versionOptions" />
</div>
<div class="grid gap-2">
<Label>{{ t('Time Cycle') }}</Label>
<div class="flex items-center gap-3">
<AppInput
v-model="formData.days"
type="number"
min="1"
class="w-32"
:placeholder="t('Days')"
style="width: 100px"
/>
<span class="ms-4">{{ t('Days After') }}</span>
<span class="text-sm text-muted-foreground">{{ t('Days After') }}</span>
</div>
</n-form-item>
<n-form-item :label="t('Storage Type')" path="storageType">
<n-select v-model:value="formData.storageType" :options="tiers" />
</n-form-item>
</div>
<!-- 生命周期 -->
<n-card>
<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-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')"
/>
</n-form-item>
</n-collapse-item>
</n-collapse>
</n-card>
</n-tab-pane>
</n-tabs>
</n-form>
<div class="grid gap-2">
<Label>{{ t('Storage Type') }}</Label>
<AppSelect
v-model="formData.storageType"
:options="tiers"
:placeholder="t('Please select storage type')"
/>
</div>
</AppCard>
<n-space justify="center">
<n-button @click="handleCancel">{{ t('Cancel') }}</n-button>
<n-button type="primary" @click="handleSave">{{ t('Save') }}</n-button>
</n-space>
</n-card>
</n-modal>
<AppCard padded class="space-y-4">
<details>
<summary class="cursor-pointer text-sm font-medium text-primary">{{ t('More Configurations') }}</summary>
<div class="mt-4 space-y-4">
<div class="grid gap-2">
<Label>{{ t('Prefix') }}</Label>
<AppInput v-model="formData.prefix" :placeholder="t('Please enter prefix')" />
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<Label class="text-sm font-medium">{{ t('Tags') }}</Label>
<AppButton variant="outline" size="sm" @click="addTag">
<Icon name="ri:add-line" class="size-4" />
{{ t('Add Tag') }}
</AppButton>
</div>
<div v-if="formData.tags.length" class="space-y-3">
<div
v-for="(tag, index) in formData.tags"
:key="index"
class="grid gap-2 rounded-md border border-border/60 p-3 md:grid-cols-2 md:items-center md:gap-4"
>
<AppInput v-model="tag.key" :placeholder="t('Tag Name')" />
<div class="flex items-center gap-2">
<AppInput v-model="tag.value" :placeholder="t('Tag Value')" class="flex-1" />
<AppButton
variant="ghost"
size="sm"
class="text-destructive"
:disabled="formData.tags.length === 1"
@click="removeTag(index)"
>
<Icon name="ri:delete-bin-line" class="size-4" />
</AppButton>
</div>
</div>
</div>
</div>
</div>
</details>
</AppCard>
</TabsContent>
</Tabs>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<AppButton variant="outline" @click="handleCancel">{{ t('Cancel') }}</AppButton>
<AppButton variant="primary" :loading="submitting" @click="handleSave">{{ t('Save') }}</AppButton>
</div>
</template>
</AppModal>
</template>
<script setup lang="ts">
import {
NButton,
NCollapse,
NCollapseItem,
NDynamicInput,
NForm,
NFormItem,
NInput,
NInputNumber,
NSelect,
} from 'naive-ui';
// 使用 Web Crypto API 或 Node.js crypto 模块
const randomUUID = () => {
if (typeof crypto !== 'undefined' && 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({});
const { listTiers } = useTiers();
const { t } = useI18n();
const message = useMessage();
// 定义 tiers 数据结构的接口
interface TierItem {
label: string;
value: string;
}
import { Icon } from '#components'
import { AppButton, AppCard, AppInput, AppModal, AppSelect, AppSwitch } from '@/components/app'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Label } from '@/components/ui/label'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const emit = defineEmits(['search']);
const formRef = ref();
const formData = ref({
ruleName: '',
type: null,
const { t } = useI18n()
const message = useMessage()
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({})
const { listTiers } = useTiers()
const props = defineProps<{
bucketName: string
}>()
const emit = defineEmits<{
(e: 'search'): void
}>()
const visible = ref(false)
const activeTab = ref<'expire' | 'transition'>('expire')
const submitting = ref(false)
const versioningStatus = ref(false)
const versionOptions = computed(() => [
{ label: t('Current Version'), value: 'current' },
{ label: t('Non-current Version'), value: 'non-current' },
])
const formData = reactive({
versionType: 'current',
days: null,
action: 'expire',
days: 1,
storageType: '',
prefix: '',
expiredDeleteMark: false,
deleteAllExpired: false,
storageType: '',
tags: [
{
key: '',
value: '',
},
],
});
] as Array<{ key: string; value: string }>,
})
const versionOptions = ref([
{ label: t('Current Version'), value: 'current' },
{ label: t('Non-current Version'), value: 'non-current' },
]);
watch(
() => activeTab.value,
value => {
formState.action = value === 'expire' ? 'expire' : 'transition'
},
{ immediate: true }
)
// 表单验证规则
const rules = {
days: {
required: true,
validator: (rule: any, value: any) => {
if (value === null || value === undefined || value < 1) {
return new Error(t('Please enter valid days'));
}
return true;
},
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'));
}
return true;
},
trigger: ['blur', 'change'],
},
};
const formState = reactive({
action: 'expire' as 'expire' | 'transition',
})
const props = defineProps({
bucketName: {
type: String,
required: true,
},
// 已经存在的生命周期
lifecycleList: {
type: Array,
default: () => [],
},
});
const tiers = ref<Array<{ label: string; value: string }>>([])
const loadTiers = async () => {
const res = await listTiers()
if (res) {
tiers.value = res.map((item: any) => ({
label: item[item.type].name,
value: item[item.type].name,
}))
}
if (!formData.storageType && tiers.value.length > 0) {
formData.storageType = tiers.value[0].value
}
}
const loadVersioningStatus = async () => {
if (!props.bucketName) return
try {
const resp = await getBucketVersioning(props.bucketName)
versioningStatus.value = resp.Status === 'Enabled'
} catch (error) {
versioningStatus.value = false
}
}
watch(
() => props.bucketName,
newVal => {
// 获取是都开启版本管理
getVersioningStatus();
() => {
loadVersioningStatus()
}
);
)
const visible = ref(false);
const open = () => {
visible.value = true;
};
const addTag = () => {
formData.tags.push({ key: '', value: '' })
}
const removeTag = (index: number) => {
if (formData.tags.length === 1) return
formData.tags.splice(index, 1)
}
const resetForm = () => {
activeTab.value = 'expire'
formState.action = 'expire'
formData.versionType = 'current'
formData.days = 1
formData.storageType = tiers.value[0]?.value ?? ''
formData.prefix = ''
formData.expiredDeleteMark = false
formData.tags = [{ key: '', value: '' }]
}
const validate = () => {
if (!formData.days || Number(formData.days) < 1) {
message.error(t('Please enter valid days'))
return false
}
if (
formState.action === 'transition' &&
(!formData.storageType || formData.storageType === '')
) {
message.error(t('Please select storage type'))
return false
}
if (!formData.tags.some(tag => tag.key && tag.value)) {
// allow but no error
}
return true
}
const randomUUID = () => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
const buildFilter = () => {
const validTags = formData.tags.filter(tag => tag.key && tag.value)
if (!formData.prefix && validTags.length === 0) {
return undefined
}
const filter: any = {}
if (validTags.length === 1 && !formData.prefix) {
filter.Tag = {
Key: validTags[0].key,
Value: validTags[0].value,
}
} else if (validTags.length > 0) {
filter.And = {
Tags: validTags.map(tag => ({
Key: tag.key,
Value: tag.value,
})),
}
if (formData.prefix) {
filter.And.Prefix = formData.prefix
}
} else if (formData.prefix) {
filter.Prefix = formData.prefix
}
if (formData.prefix && validTags.length === 1 && !filter.And) {
filter.Prefix = formData.prefix
filter.Tag = {
Key: validTags[0].key,
Value: validTags[0].value,
}
} else if (formData.prefix && validTags.length === 0) {
filter.Prefix = formData.prefix
}
return Object.keys(filter).length ? filter : undefined
}
const handleSave = async () => {
if (!validate()) return
submitting.value = true
try {
const currentConfig = await getBucketLifecycleConfiguration(props.bucketName)
const newRule: any = {
ID: randomUUID(),
Status: 'Enabled',
Filter: buildFilter(),
}
const daysValue = Number(formData.days)
if (formState.action === 'expire') {
if (formData.versionType === 'non-current') {
newRule.NoncurrentVersionExpiration = {
NoncurrentDays: daysValue,
}
if (formData.expiredDeleteMark) {
newRule.ExpiredObjectDeleteMarker = true
}
} else {
newRule.Expiration = {
Days: daysValue,
}
if (formData.expiredDeleteMark) {
newRule.Expiration.ExpiredObjectDeleteMarker = true
}
}
} else {
if (formData.versionType === 'non-current') {
newRule.NoncurrentVersionTransitions = [
{
NoncurrentDays: daysValue,
StorageClass: formData.storageType,
},
]
} else {
newRule.Transitions = [
{
Days: daysValue,
StorageClass: formData.storageType,
},
]
}
}
const existingRules = currentConfig.Rules || []
const payload = {
Rules: [...existingRules, newRule],
}
await putBucketLifecycleConfiguration(props.bucketName, payload)
message.success(t('Create Success'))
emit('search')
handleCancel()
} catch (error: any) {
message.error(error?.message || t('Failed to create rule'))
} finally {
submitting.value = false
}
}
const handleCancel = () => {
visible.value = false
resetForm()
}
defineExpose({
open,
});
const handleUpdateValue = (value: string) => {
if (value === 'expire') {
formData.value.action = 'expire';
} else {
formData.value.action = 'transition';
}
};
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'));
return;
}
if (
formData.value.action === 'transition' &&
(!formData.value.storageType || formData.value.storageType === '')
) {
message.error(t('Please select storage type'));
return;
}
// 先获取当前的生命周期配置
getBucketLifecycleConfiguration(props.bucketName)
.then(currentConfig => {
// 创建新的规则
const newRule: any = {
ID: randomUUID(),
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);
// 检查是否有有效的标签
const validTags = formData.value.tags.filter((item: any) => item.key && item.value);
const hasValidTags = validTags.length > 0;
if (formData.value.prefix || hasValidTags) {
newRule.Filter = {};
if (hasValidTags) {
if (validTags.length === 1) {
newRule.Filter.Tag = {
Key: validTags[0].key,
Value: validTags[0].value,
};
if (formData.value.prefix) {
newRule.Filter.Prefix = formData.value.prefix;
}
} else {
newRule.Filter.And = {
Tags: validTags.map((item: any) => ({
Key: item.key,
Value: item.value,
})),
};
if (formData.value.prefix) {
newRule.Filter.And.Prefix = formData.value.prefix;
}
}
} else {
// 只有前缀时,直接添加到 Filter
newRule.Filter.Prefix = formData.value.prefix;
}
}
// 根据操作类型添加相应的配置
if (formData.value.action === 'expire') {
if (formData.value.versionType === 'non-current') {
newRule.NoncurrentVersionExpiration = {
NoncurrentDays: formData.value.days,
};
// 如果启用了删除标记处理
if (formData.value.expiredDeleteMark) {
newRule.ExpiredObjectDeleteMarker = true;
}
} else {
newRule.Expiration = {
Days: formData.value.days,
};
// 如果启用了删除标记处理(当前版本也支持)
if (formData.value.expiredDeleteMark) {
newRule.Expiration.ExpiredObjectDeleteMarker = true;
}
}
} else {
// transition 操作
if (formData.value.versionType === 'non-current') {
newRule.NoncurrentVersionTransitions = [
{
NoncurrentDays: formData.value.days,
StorageClass: formData.value.storageType,
},
];
} else {
newRule.Transitions = [
{
Days: formData.value.days,
StorageClass: formData.value.storageType,
},
];
}
}
// 合并现有规则和新规则
const existingRules = currentConfig.Rules || [];
const updatedRules = [...existingRules, newRule];
// 调用保存接口
const params = {
Rules: updatedRules,
};
return putBucketLifecycleConfiguration(props.bucketName, params);
})
.then(res => {
visible.value = false;
emit('search');
message.success(t('Create Success'));
formData.value = {
ruleName: '',
type: null,
versionType: 'current',
days: null,
action: 'expire',
expiredDeleteMark: false,
deleteAllExpired: false,
storageType: '',
prefix: '',
tags: [
{
key: '',
value: '',
},
],
};
})
.catch(e => {
message.error(e.message);
});
}
});
};
// 分层列表
const tiers = ref<TierItem[]>([]);
const getTiers = async () => {
const res = await listTiers();
if (res) {
tiers.value = res.map((item: any) => {
return {
label: item[item.type].name,
value: item[item.type].name,
};
});
}
if (tiers.value.length) {
formData.value.storageType = tiers.value[0].value;
}
};
getTiers();
// 获取版本控制状态
const versioningStatus: any = ref(false);
const getVersioningStatus = async () => {
try {
const resp = await getBucketVersioning(props.bucketName);
versioningStatus.value = resp.Status == 'Enabled';
} catch (error) {
console.error('获取版本控制状态失败:', error);
}
};
open: async () => {
await Promise.all([loadTiers(), loadVersioningStatus()])
resetForm()
visible.value = true
},
})
onMounted(() => {
getVersioningStatus();
});
const handleCancel = () => {
// 取消逻辑
visible.value = false;
};
loadTiers()
loadVersioningStatus()
})
</script>
+175 -188
View File
@@ -1,248 +1,235 @@
<template>
<div>
<div class="flex flex-col gap-6">
<page-header>
<template #title>
<h1 class="text-2xl font-bold">{{ t('Lifecycle') }}</h1>
</template>
</page-header>
<page-content class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div style="width: 300px">
<n-form-item :label="t('Bucket')" path="" class="flex-auto" label-placement="left">
<n-select
filterable
v-model:value="bucketName"
:placeholder="t('Please select bucket')"
:options="bucketList"
/>
</n-form-item>
</div>
<div class="flex items-center gap-4">
<n-button @click="() => handleNew()">
<Icon name="ri:add-line" class="mr-2" />
<page-content class="flex flex-col gap-4">
<div
class="flex flex-col gap-4 rounded-lg border border-border/60 bg-background/80 p-4 shadow-sm md:flex-row md:items-end md:justify-between"
>
<div class="w-full max-w-sm">
<Label class="mb-2 block text-sm font-medium text-muted-foreground">{{ t('Bucket') }}</Label>
<AppSelect
v-model="bucketName"
:options="bucketList"
:placeholder="t('Please select bucket')"
class="w-full"
/>
</div>
<div class="flex flex-wrap items-center justify-end gap-2">
<AppButton variant="secondary" @click="handleNew">
<Icon name="ri:add-line" class="size-4" />
<span>{{ t('Add Lifecycle Rule') }}</span>
</n-button>
<n-button @click="refresh">
<Icon name="ri:refresh-line" class="mr-2" />
</AppButton>
<AppButton variant="outline" @click="refresh">
<Icon name="ri:refresh-line" class="size-4" />
<span>{{ t('Refresh') }}</span>
</n-button>
</AppButton>
</div>
</div>
<n-data-table
class="border dark:border-neutral-700 rounded overflow-hidden"
v-if="pageData.length > 0"
:columns="columns"
:data="pageData"
:pagination="false"
:bordered="false"
/>
<AppCard padded class="border border-border/60">
<AppDataTable
:table="table"
:is-loading="loading"
:empty-title="t('No Data')"
:empty-description="t('Create lifecycle rules to automate object transitions and expiration.')"
/>
</AppCard>
<n-card class="flex flex-center" style="height: 400px" v-else>
<n-empty :description="t('No Data')"></n-empty>
</n-card>
<!-- <n-data-table class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="pageData" :pagination="false" :bordered="false" /> -->
<lifecycle-new-form ref="newRef" :bucketName="bucketName" @search="refresh"></lifecycle-new-form>
<lifecycle-new-form ref="newRef" :bucketName="bucketName" @search="refresh" />
</page-content>
</div>
</template>
<script lang="ts" setup>
import { Icon } from '#components';
import { NButton, NSpace, NPopconfirm, type DataTableColumns } from 'naive-ui';
import { useI18n } from 'vue-i18n';
import { Icon } from '#components'
import type { ColumnDef } from '@tanstack/vue-table'
import { AppButton, AppCard, AppSelect, AppTag } from '@/components/app'
import { AppDataTable, useDataTable } from '@/components/app/data-table'
import { Label } from '@/components/ui/label'
import { h, computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n();
const message = useMessage();
const { t } = useI18n()
const message = useMessage()
const dialog = useDialog()
const { listBuckets, getBucketLifecycleConfiguration, deleteBucketLifecycle, putBucketLifecycleConfiguration } =
useBucket({});
const formVisible = ref(false);
useBucket({})
interface RowData {
Status?: string;
ID?: string;
NoncurrentVersionExpiration?: {
NoncurrentDays?: number;
};
Expiration?: {
StorageClass?: string;
Days?: number;
Date?: string;
ExpiredObjectDeleteMarker?: boolean;
};
interface LifecycleRule {
ID?: string
Status?: string
Filter?: {
Prefix?: string;
};
Transitions?: {
Days?: number;
StorageClass?: string;
}[];
Prefix?: string
Tag?: { Key: string; Value: string }
And?: {
Prefix?: string
Tags?: Array<{ Key: string; Value: string }>
}
}
Expiration?: {
Days?: number
Date?: string
StorageClass?: string
ExpiredObjectDeleteMarker?: boolean
}
NoncurrentVersionExpiration?: {
NoncurrentDays?: number
}
Transitions?: Array<{ Days?: number; StorageClass?: string }>
NoncurrentVersionTransitions?: Array<{ NoncurrentDays?: number; StorageClass?: string }>
}
const columns: DataTableColumns<RowData> = [
const columns: ColumnDef<LifecycleRule>[] = [
{
title: t('Type'),
key: '',
render: row => {
return h(NSpace, {}, () => {
console.log(row);
if (row?.Transitions) {
return t('Transition');
} else {
return t('Expire');
}
});
},
header: () => t('Type'),
accessorFn: row => (row.Transitions || row.NoncurrentVersionTransitions ? 'Transition' : 'Expire'),
},
{
title: t('Version'),
key: 'Transition',
render: row => {
return h(NSpace, {}, row?.NoncurrentVersionExpiration ? t('Non-current Version') : t('Current Version'));
},
header: () => t('Version'),
accessorFn: row => (row.NoncurrentVersionExpiration || row.NoncurrentVersionTransitions ? t('Non-current Version') : t('Current Version')),
},
{
title: t('Expiration Delete Mark'),
key: '',
render: row => {
return h(NSpace, {}, row?.Expiration?.ExpiredObjectDeleteMarker ? t('On') : t('Off'));
},
header: () => t('Expiration Delete Mark'),
accessorFn: row => (row.Expiration?.ExpiredObjectDeleteMarker ? t('On') : t('Off')),
},
{
title: t('Tier'),
key: '',
render: row => {
return h(NSpace, {}, row?.Transitions?.[0]?.StorageClass || '--');
},
header: () => t('Tier'),
accessorFn: row => row.Transitions?.[0]?.StorageClass || row.NoncurrentVersionTransitions?.[0]?.StorageClass || '--',
},
{
title: t('Prefix'),
key: 'Filter',
render: row => {
return h(NSpace, {}, row?.Filter?.Prefix || '');
},
},
{
title: t('Time Cycle') + '(' + t('Days') + ')',
key: 'NoncurrentVersionExpiration',
render: row => {
return h(
NSpace,
{},
row?.Expiration?.Days || row?.NoncurrentVersionExpiration?.NoncurrentDays || row?.Transitions?.[0]?.Days || ''
);
},
header: () => t('Prefix'),
accessorFn: row => row.Filter?.Prefix || row.Filter?.And?.Prefix || '',
},
{
title: t('Status'),
key: 'Status',
header: () => `${t('Time Cycle')} (${t('Days')})`,
accessorFn: row =>
row.Expiration?.Days ||
row.NoncurrentVersionExpiration?.NoncurrentDays ||
row.Transitions?.[0]?.Days ||
row.NoncurrentVersionTransitions?.[0]?.NoncurrentDays ||
'',
},
{
title: t('Actions'),
key: 'actions',
align: 'center',
width: 100,
render: (row: RowData) => {
return h(
NSpace,
header: () => t('Status'),
accessorFn: row => row.Status || '-',
cell: ({ row }) =>
h(
AppTag,
{
justify: 'center',
tone: row.original.Status === 'Enabled' ? 'success' : 'danger',
},
{
default: () => [
h(
NPopconfirm,
{ onPositiveClick: () => handleRowDelete(row) },
{
default: () => t('Confirm Delete'),
trigger: () =>
h(
NButton,
{ size: 'small', secondary: true },
{
default: () => '',
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
}
),
}
),
],
}
);
},
() => row.original.Status || '-'
),
},
];
{
id: 'actions',
header: () => t('Actions'),
enableSorting: false,
cell: ({ row }) =>
h('div', { class: 'flex justify-center gap-2' }, [
h(
AppButton,
{
variant: 'outline',
size: 'sm',
onClick: () => confirmDelete(row.original),
},
() => [h(Icon, { name: 'ri:delete-bin-5-line', class: 'size-4' }), h('span', t('Delete'))]
),
]),
},
]
// 获取桶列表
const { data } = await useAsyncData(
'buckets',
const bucketOptions = await useAsyncData(
'lifecycle-buckets',
async () => {
const response = await listBuckets();
const response = await listBuckets()
return (
response.Buckets?.sort((a: any, b: any) => {
return a.Name.localeCompare(b.Name);
return a.Name.localeCompare(b.Name)
}) || []
);
)
},
{ default: () => [] }
);
)
const bucketList = computed(() => {
return data.value.map(bucket => ({
const bucketList = computed(() =>
bucketOptions.data.value.map(bucket => ({
label: bucket.Name,
value: bucket.Name,
}));
});
}))
)
const bucketName = ref<string>(bucketList.value.length > 0 ? bucketList.value[0]?.value ?? '' : '')
const pageData = ref<LifecycleRule[]>([])
const loading = ref(false)
const { table } = useDataTable<LifecycleRule>({
data: pageData,
columns,
getRowId: row => row.ID ?? JSON.stringify(row),
})
const fetchLifecycle = async () => {
if (!bucketName.value) {
pageData.value = []
return
}
loading.value = true
try {
const response = await getBucketLifecycleConfiguration(bucketName.value)
pageData.value = response.Rules?.sort((a: any, b: any) => (a.ID || '').localeCompare(b.ID || '')) ?? []
} catch (error) {
pageData.value = []
} finally {
loading.value = false
}
}
const refresh = () => fetchLifecycle()
const bucketName = ref<string>(bucketList.value.length > 0 ? (bucketList.value[0]?.value ?? '') : '');
const pageData = ref<any[]>([]);
const loading = ref<boolean>(false);
watch(
() => bucketName.value,
async newVal => {
if (!newVal) return;
loading.value = true;
try {
refresh();
} catch (error) {
pageData.value = [];
} finally {
loading.value = false;
}
() => {
fetchLifecycle()
},
{ immediate: true }
);
)
const handleRowDelete = (row: RowData) => {
const params = pageData.value.filter(item => item.ID !== row.ID);
console.log(params);
if (params.length === 0) {
deleteBucketLifecycle(bucketName.value).then(async res => {
message.success(t('Delete Success'));
refresh();
});
} else {
putBucketLifecycleConfiguration(bucketName.value, { Rules: params })
.then(async res => {
message.success(t('Delete Success'));
refresh();
})
.catch(e => {
message.error(e.message);
});
const confirmDelete = (row: LifecycleRule) => {
dialog.error({
title: t('Warning'),
content: t('Are you sure you want to delete this rule?'),
positiveText: t('Confirm'),
negativeText: t('Cancel'),
onPositiveClick: () => handleRowDelete(row),
})
}
const handleRowDelete = async (row: LifecycleRule) => {
const remaining = pageData.value.filter(item => item.ID !== row.ID)
try {
if (remaining.length === 0) {
await deleteBucketLifecycle(bucketName.value)
} else {
await putBucketLifecycleConfiguration(bucketName.value, { Rules: remaining })
}
message.success(t('Delete Success'))
fetchLifecycle()
} catch (error: any) {
message.error(error?.message || t('Delete Failed'))
}
};
}
const refresh = async () => {
const response = await getBucketLifecycleConfiguration(bucketName.value);
pageData.value = response.Rules?.sort((a: any, b: any) => a.ID.localeCompare(b.ID)) || [];
};
const newRef = ref();
const newRef = ref<{ open: () => void }>()
const handleNew = () => {
newRef.value.open();
};
newRef.value?.open()
}
</script>