Signed-off-by: junxiang Mu <1948535941@qq.com>
This commit is contained in:
junxiang Mu
2025-09-22 14:31:07 +08:00
parent 309f541946
commit c019276fcb
122 changed files with 50830 additions and 4841 deletions
+40 -8
View File
@@ -100,21 +100,53 @@ function closeModal(visible = false) {
</script>
<template>
<n-modal v-model:show="modalVisible" :mask-closable="false" preset="card" :title="t('Change current account password')" class="max-w-screen-md" sizer="huge" :segmented="{
content: true,
action: true,
}">
<n-modal
v-model:show="modalVisible"
:mask-closable="false"
preset="card"
:title="t('Change current account password')"
class="max-w-screen-md"
sizer="huge"
:segmented="{
content: true,
action: true,
}"
>
<n-card>
<n-form ref="formRef" label-placement="left" :model="formModel" :rules="rules" label-align="left" :label-width="130">
<n-form
ref="formRef"
label-placement="left"
:model="formModel"
:rules="rules"
label-align="left"
:label-width="130"
>
<n-grid :cols="24" :x-gap="18">
<n-form-item-grid-item :span="24" :label="t('Current Password')" path="current_secret_key">
<n-input v-model:value="formModel.current_secret_key" show-password-on="mousedown" type="password" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('New Password')" path="new_secret_key">
<n-input ref="nPasswordFormItemRef" v-model:value="formModel.new_secret_key" show-password-on="mousedown" type="password" @input="handlePasswordInput" />
<n-input
ref="nPasswordFormItemRef"
v-model:value="formModel.new_secret_key"
show-password-on="mousedown"
type="password"
@input="handlePasswordInput"
/>
</n-form-item-grid-item>
<n-form-item-grid-item ref="rPasswordFormItemRef" :span="24" :label="t('Confirm New Password')" path="re_new_secret_key">
<n-input v-model:value="formModel.re_new_secret_key" :disabled="!formModel.new_secret_key" show-password-on="mousedown" type="password" @keydown.enter.prevent />
<n-form-item-grid-item
ref="rPasswordFormItemRef"
:span="24"
:label="t('Confirm New Password')"
path="re_new_secret_key"
>
<n-input
v-model:value="formModel.re_new_secret_key"
:disabled="!formModel.new_secret_key"
show-password-on="mousedown"
type="password"
@keydown.enter.prevent
/>
</n-form-item-grid-item>
</n-grid>
</n-form>
+54 -42
View File
@@ -1,82 +1,89 @@
<script setup lang="ts">
import { ref } from "vue"
import { useI18n } from "vue-i18n"
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const message = useMessage()
const emit = defineEmits<Emits>()
const { getServiceAccount, updateServiceAccount } = useAccessKeys()
const { $api } = useNuxtApp()
const { t } = useI18n();
const message = useMessage();
const emit = defineEmits<Emits>();
const { getServiceAccount, updateServiceAccount } = useAccessKeys();
const { $api } = useNuxtApp();
const visible = ref(false)
const visible = ref(false);
const defaultFormModal = {
accesskey: "",
secretkey: "",
name: "",
description: "",
accesskey: '',
secretkey: '',
name: '',
description: '',
expiry: null,
policy: "",
status: "on",
}
const formModel = ref({ ...defaultFormModal })
policy: '',
status: 'on',
};
const formModel = ref({ ...defaultFormModal });
const accessKey = ref<string>("")
const accessKey = ref<string>('');
async function openDialog(row: any) {
accessKey.value = row.accessKey
accessKey.value = row.accessKey;
try {
const res = await getServiceAccount(row.accessKey)
formModel.value = res
formModel.value.accesskey = row.accessKey
formModel.value.expiry = res.expiration
formModel.value.status = res.accountStatus
const res = await getServiceAccount(row.accessKey);
formModel.value = res;
formModel.value.accesskey = row.accessKey;
formModel.value.expiry = res.expiration;
formModel.value.status = res.accountStatus;
// const userInfo = await $api.get(`/accountinfo`)
// formModel.value.policy = userInfo.Policy
visible.value = true
visible.value = true;
} catch (error) {
message.error(t("Failed to get data"))
message.error(t('Failed to get data'));
}
}
defineExpose({ openDialog })
defineExpose({ openDialog });
interface Emits {
(e: "search"): void
(e: 'search'): void;
}
function closeModal() {
visible.value = false
visible.value = false;
}
function dateDisabled(ts: number) {
const date = new Date(ts)
return date < new Date()
const date = new Date(ts);
return date < new Date();
}
async function submitForm() {
try {
const res = await updateServiceAccount(accessKey.value, {
newPolicy: formModel.value.policy || "{}", // 可选,新策略
newPolicy: formModel.value.policy || '{}', // 可选,新策略
newStatus: formModel.value.status, // 可选,新状态
newName: formModel.value.name, // 可选,新名称
newDescription: formModel.value.description, // 可选,新描述
newExpiration: new Date(formModel.value.expiry || "").toISOString(), // 可选,新过期时间
})
message.success(t("Updated successfully"))
closeModal()
emit("search")
newExpiration: new Date(formModel.value.expiry || '').toISOString(), // 可选,新过期时间
});
message.success(t('Updated successfully'));
closeModal();
emit('search');
} catch (error) {
message.error(t("Update failed"))
message.error(t('Update failed'));
}
}
</script>
<template>
<n-modal v-model:show="visible" :mask-closable="false" preset="card" :title="t('Edit Key')" class="max-w-screen-md" :segmented="{
content: true,
action: true,
}">
<n-modal
v-model:show="visible"
:mask-closable="false"
preset="card"
:title="t('Edit Key')"
class="max-w-screen-md"
:segmented="{
content: true,
action: true,
}"
>
<n-card>
<n-form label-placement="left" :model="formModel" label-align="right" :label-width="90">
<n-grid :cols="24" :x-gap="18">
@@ -88,7 +95,12 @@ async function submitForm() {
</n-form-item-grid-item>
<!-- TODO: 时间格式有问题 -->
<n-form-item-grid-item :span="24" :label="t('Expiry')" path="expiry">
<n-date-picker v-model:value="formModel.expiry" :is-date-disabled="dateDisabled" type="datetime" clearable />
<n-date-picker
v-model:value="formModel.expiry"
:is-date-disabled="dateDisabled"
type="datetime"
clearable
/>
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('Name')" path="name">
<n-input v-model:value="formModel.name" />
+4 -17
View File
@@ -24,11 +24,7 @@
<n-input v-model:value="formModel.accessKey" />
</n-form-item-gi>
<n-form-item-gi :span="24" :label="t('Secret Key')" path="secretKey">
<n-input
v-model:value="formModel.secretKey"
show-password-on="mousedown"
type="password"
/>
<n-input v-model:value="formModel.secretKey" show-password-on="mousedown" type="password" />
</n-form-item-gi>
<!-- <n-form-item-gi :span="24" label="策略" path="policy">
@@ -56,12 +52,7 @@
<n-form-item-gi :span="24" :label="t('Use main account policy')" path="impliedPolicy">
<n-switch v-model:value="formModel.impliedPolicy" />
</n-form-item-gi>
<n-form-item-gi
v-if="!formModel.impliedPolicy"
:span="24"
:label="t('Current user policy')"
path="policy"
>
<n-form-item-gi v-if="!formModel.impliedPolicy" :span="24" :label="t('Current user policy')" path="policy">
<json-editor v-model="formModel.policy" />
</n-form-item-gi>
</n-grid>
@@ -181,12 +172,8 @@ async function submitForm(e: MouseEvent) {
try {
const res = await createServiceAccount({
...formModel.value,
policy: !formModel.value.impliedPolicy
? JSON.stringify(JSON.parse(formModel.value.policy))
: null,
expiration: formModel.value.expiry
? new Date(formModel.value.expiry).toISOString()
: null,
policy: !formModel.value.impliedPolicy ? JSON.stringify(JSON.parse(formModel.value.policy)) : null,
expiration: formModel.value.expiry ? new Date(formModel.value.expiry).toISOString() : null,
});
message.success(t('Added successfully'));
emit('notice', res);
+26 -10
View File
@@ -1,31 +1,47 @@
<template>
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8 overflow-hidden relative border-r dark:border-none">
<div
class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8 overflow-hidden relative border-r dark:border-none"
>
<div class="max-w-7xl flex flex-col z-10">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords :words="[t('High Performance'), t('Infinite Scaling'), t('Secure & Reliable'), t('Multi-Cloud Storage'), t('S3 Compatible')]" :duration="3000"
class="text-4xl font-semibold !text-primary px-0" />
<FlipWords
:words="[
t('High Performance'),
t('Infinite Scaling'),
t('Secure & Reliable'),
t('Multi-Cloud Storage'),
t('S3 Compatible'),
]"
:duration="3000"
class="text-4xl font-semibold !text-primary px-0"
/>
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
</div>
</div>
</div>
<a href="https://www.rustfs.com" class="z-10 text-primary-500 inline-flex w-max items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<a
href="https://www.rustfs.com"
class="z-10 text-primary-500 inline-flex w-max items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500"
>
<span>{{ t('Visit website') }}</span>
<Icon name="ri:arrow-right-long-fill" class="mr-2" />
</a>
<div class="h-full inset-0 absolute z-0">
<Ripple class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] [mask-image:linear-gradient(to_bottom,white,transparent)]"
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-full" />
<Ripple
class="bg-white/5 -mb-[100vh] h-full w-full -mr-[50vw] [mask-image:linear-gradient(to_bottom,white,transparent)]"
circle-class="border-[hsl(var(--primary))] bg-[#0000]/25 dark:bg-[#fff]/25 rounded-full"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import FlipWords from '~/components/ui/flip-words/FlipWords.vue'
import Ripple from '~/components/ui/ripple/Ripple.vue'
import { useI18n } from 'vue-i18n';
import FlipWords from '~/components/ui/flip-words/FlipWords.vue';
import Ripple from '~/components/ui/ripple/Ripple.vue';
const { t, locale } = useI18n()
const { t, locale } = useI18n();
</script>
+26 -8
View File
@@ -1,29 +1,47 @@
<template>
<div class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8 overflow-hidden relative border-r dark:border-none">
<div
class="w-full bg-gray-50 dark:bg-black h-full p-16 flex flex-col justify-center gap-8 overflow-hidden relative border-r dark:border-none"
>
<div class="max-w-7xl flex flex-col z-10">
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords :words="[t('High Performance'), t('Infinite Scaling'), t('Secure & Reliable'), t('Multi-Cloud Storage'), t('S3 Compatible')]" :duration="3000"
class="text-4xl font-semibold !text-primary px-0" />
<FlipWords
:words="[
t('High Performance'),
t('Infinite Scaling'),
t('Secure & Reliable'),
t('Multi-Cloud Storage'),
t('S3 Compatible'),
]"
:duration="3000"
class="text-4xl font-semibold !text-primary px-0"
/>
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
</div>
</div>
</div>
<a href="https://www.rustfs.com" class="z-10 text-primary-500 inline-flex w-max items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<a
href="https://www.rustfs.com"
class="z-10 text-primary-500 inline-flex w-max items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500"
>
<span>{{ t('Visit website') }}</span>
<Icon name="ri:arrow-right-long-fill" class="mr-2" />
</a>
<div class="h-full inset-0 absolute z-0">
<img src="~/assets/backgrounds/ttten.svg" class="absolute h-full w-full inset-0 z-0 opacity-45 object-cover" alt="" />
<img
src="~/assets/backgrounds/ttten.svg"
class="absolute h-full w-full inset-0 z-0 opacity-45 object-cover"
alt=""
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import FlipWords from '~/components/ui/flip-words/FlipWords.vue'
import { useI18n } from 'vue-i18n';
import FlipWords from '~/components/ui/flip-words/FlipWords.vue';
const { t, locale } = useI18n()
const { t, locale } = useI18n();
</script>
+26 -9
View File
@@ -4,15 +4,32 @@
<img src="~/assets/logo.svg" class="max-w-28" alt="" />
<div class="text-4xl my-6 font-semibold !text-primary px-0">
<span :class="{ 'pr-1': locale !== 'zh' }">{{ t('Rust-based') }} </span>
<FlipWords :words="[t('High Performance'), t('Infinite Scaling'), t('Secure & Reliable'), t('Multi-Cloud Storage'), t('S3 Compatible')]" :duration="3000"
class="text-4xl font-semibold !text-primary px-0" />
<FlipWords
:words="[
t('High Performance'),
t('Infinite Scaling'),
t('Secure & Reliable'),
t('Multi-Cloud Storage'),
t('S3 Compatible'),
]"
:duration="3000"
class="text-4xl font-semibold !text-primary px-0"
/>
<div class="text-muted-foreground mt-2">
{{ t('Reliable distributed file system') }}
</div>
</div>
</div>
<WorldMap class="absolute inset-0" :dots="dots" :map-color="isDark ? '#FFFFFF40' : '#00000040'" :map-bg-color="isDark ? 'black' : '#f9fafb'" />
<a href=" https://www.rustfs.com" class="text-primary-500 inline-flex w-min items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500">
<WorldMap
class="absolute inset-0"
:dots="dots"
:map-color="isDark ? '#FFFFFF40' : '#00000040'"
:map-bg-color="isDark ? 'black' : '#f9fafb'"
/>
<a
href=" https://www.rustfs.com"
class="text-primary-500 inline-flex w-min items-center gap-2 leading-none p-2 px-5 border rounded-full border-blue-500"
>
<span>{{ t('Visit website') }}</span>
<Icon name="ri:arrow-right-long-fill" class="mr-2" />
</a>
@@ -20,11 +37,11 @@
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import FlipWords from "~/components/ui/flip-words/FlipWords.vue"
import WorldMap from "~/components/ui/world-map/WorldMap.vue"
import { useI18n } from 'vue-i18n';
import FlipWords from '~/components/ui/flip-words/FlipWords.vue';
import WorldMap from '~/components/ui/world-map/WorldMap.vue';
const { t, locale } = useI18n()
const { t, locale } = useI18n();
const dots = [
{
@@ -60,5 +77,5 @@ const dots = [
];
const mode = useColorMode();
const isDark = computed(() => mode.value == "dark");
const isDark = computed(() => mode.value == 'dark');
</script>
+8 -4
View File
@@ -460,10 +460,14 @@ const fetchKMSKeys = async () => {
try {
const response = await getKeyList();
if (response && response.keys) {
kmsKeyOptions.value = response.keys.map((key: any) => ({
label: key.keyName || key.keyId,
value: key.keyId || key.keyName,
}));
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)}`;
return {
label: keyName,
value: key.key_id,
};
});
}
} catch (error) {
console.error('Failed to fetch KMS keys:', error);
+2 -10
View File
@@ -21,11 +21,7 @@
<n-input
:placeholder="t('Please enter name')"
v-model:value="objectKey"
:status="
objectKey.length > 0 && (objectKey.length < 3 || objectKey.length > 63)
? 'error'
: undefined
"
:status="objectKey.length > 0 && (objectKey.length < 3 || objectKey.length > 63) ? 'error' : undefined"
/>
</div>
<div class="flex items-center justify-between gap-4">
@@ -55,11 +51,7 @@
<n-space class="w-24">{{ t('Validity') }}*</n-space>
<n-input-group class="justify-end">
<n-input-number v-model:value="retentionPeriod" :min="1" class="w-96" />
<n-select
v-model:value="retentionUnit"
:options="retentionUnitOptions"
class="w-16"
/>
<n-select v-model:value="retentionUnit" :options="retentionUnitOptions" class="w-16" />
</n-input-group>
</div>
</div>
+15 -8
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: {
@@ -26,14 +26,14 @@ const model = defineModel<string>();
const message = useMessage();
const clipboard = new ClipboardJS( props.copyIcon ? '#'+props.id+'btn' : '#'+props.id+'btn2');
clipboard.on('success', function(e) {
const clipboard = new ClipboardJS(props.copyIcon ? '#' + props.id + 'btn' : '#' + props.id + 'btn2');
clipboard.on('success', function (e) {
message.success(t('Copy Success'));
e.clearSelection();
});
// 这里成功的时候也响应error,所以这里也加上
clipboard.on('error', function(e) {
clipboard.on('error', function (e) {
// 如果复制失败,则使用navigator.clipboard.writeText进行复制
if (navigator.clipboard) {
navigator.clipboard.writeText(model.value || '');
@@ -49,10 +49,17 @@ onUnmounted(() => {
<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"
:id="props.id + 'btn'"
:data-clipboard-target="`#${props.id}`"
>
<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 type="primary" v-else :id="props.id + 'btn2'" :data-clipboard-target="`#${props.id}`">{{
t('Copy')
}}</NButton>
</NInputGroup>
</div>
</template>
+29 -28
View File
@@ -8,7 +8,8 @@
:segmented="{
content: true,
action: true,
}">
}"
>
<n-card v-show="!formData.type">
<n-grid x-gap="12" y-gap="12" :cols="2">
<n-gi v-for="item in typeOptions">
@@ -58,8 +59,8 @@
<n-input v-model:value="formData.storageclass" :placeholder="t('Please Enter storage class')" />
</n-form-item>
<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-form>
</n-card>
@@ -67,13 +68,13 @@
</template>
<script setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import MqttIcon from "~/assets/svg/mqtt.svg";
import WebhooksIcon from "~/assets/svg/webhooks.svg";
import KafkaIcon from "~/assets/svg/kafka.svg";
import { NForm, NFormItem, NInput, NSelect, NButton } from "naive-ui";
import { useTiers } from "#imports";
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import MqttIcon from '~/assets/svg/mqtt.svg';
import WebhooksIcon from '~/assets/svg/webhooks.svg';
import KafkaIcon from '~/assets/svg/kafka.svg';
import { NForm, NFormItem, NInput, NSelect, NButton } from 'naive-ui';
import { useTiers } from '#imports';
const { t } = useI18n();
const usetier = useTiers();
@@ -81,28 +82,28 @@ const message = useMessage();
// 支持的存储类型
const typeOptions = [
{ label: t("MQTT"), value: "mqtt", iconUrl: MqttIcon },
{ label: t("Webhook"), value: "webhook", iconUrl: WebhooksIcon },
{ label: t("Kafka"), value: "kafka", iconUrl: KafkaIcon },
{ label: t('MQTT'), value: 'mqtt', iconUrl: MqttIcon },
{ label: t('Webhook'), value: 'webhook', iconUrl: WebhooksIcon },
{ label: t('Kafka'), value: 'kafka', iconUrl: KafkaIcon },
];
const formRef = ref(null);
const formData = ref({
type: "",
name: "",
endpoint: "",
accesskey: "",
secretkey: "",
bucket: "",
prefix: "",
region: "",
storageclass: "STANDARD", // 新增字段,默认值为 STANDARD
type: '',
name: '',
endpoint: '',
accesskey: '',
secretkey: '',
bucket: '',
prefix: '',
region: '',
storageclass: 'STANDARD', // 新增字段,默认值为 STANDARD
});
const rules = {
ruleName: { required: true, message: t("Please enter rule name") },
type: { required: true, message: t("Please select rule type") },
versionType: { required: true, message: t("Please select version type") },
ruleName: { required: true, message: t('Please enter rule name') },
type: { required: true, message: t('Please select rule type') },
versionType: { required: true, message: t('Please select version type') },
};
const visible = ref(false);
@@ -115,9 +116,9 @@ defineExpose({
open,
});
const emmit = defineEmits(["search"]);
const emmit = defineEmits(['search']);
const handleSave = () => {
formRef.value?.validate((errors) => {
formRef.value?.validate(errors => {
if (!errors) {
// {"type":"minio","minio":{"name":"COLDTIER","endpoint":"","bucket":"","prefix":"","region":"","accesskey":"","secretkey":""}}
// 调用保存接口
@@ -146,6 +147,6 @@ const handleSave = () => {
const handleCancel = () => {
visible.value = false;
// 取消逻辑
formData.value.type = "";
formData.value.type = '';
};
</script>
+34 -29
View File
@@ -1,9 +1,15 @@
<template>
<n-modal v-model:show="visible" :mask-closable="false" preset="card" :title="t('Subscribe to event notification') + ` (${t('Bucket')}: ${bucketName})`" class="max-w-screen-md"
<n-modal
v-model:show="visible"
:mask-closable="false"
preset="card"
:title="t('Subscribe to event notification') + ` (${t('Bucket')}: ${bucketName})`"
class="max-w-screen-md"
:segmented="{
content: true,
action: true,
}">
}"
>
<n-card>
<n-form :model="formData" label-placement="left" label-width="120px">
<n-form-item :label="t('Amazon Resource Name')">
@@ -26,7 +32,11 @@
<n-checkbox class="mt-2" value="DELETE" :label="t('DELETE - Object deletion')" />
<n-checkbox class="mt-2" value="REPLICA" :label="t('REPLICA - Object migration')" />
<n-checkbox class="mt-2" value="RESTORE" :label="t('ILM - Object converted')" />
<n-checkbox class="mt-2" value="SCANNER" :label="t('SCANNER - Object has too many versions/prefix has too many subfolders')" />
<n-checkbox
class="mt-2"
value="SCANNER"
:label="t('SCANNER - Object has too many versions/prefix has too many subfolders')"
/>
</n-checkbox-group>
</n-scrollbar>
</n-form-item>
@@ -41,50 +51,45 @@
</template>
<script setup>
import {
NButton,
NForm,
NFormItem,
NInput,
} from 'naive-ui'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { NButton, NForm, NFormItem, NInput } from 'naive-ui';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const formRef = ref(null)
const { t } = useI18n();
const formRef = ref(null);
const formData = ref({
resourceName: '',
prefix: '',
suffix: '',
events: []
})
events: [],
});
const props = defineProps({
bucketName: {
type: String,
required: true
}
})
required: true,
},
});
const visible = ref(false)
const visible = ref(false);
const open = () => {
visible.value = true
}
visible.value = true;
};
defineExpose({
open
})
open,
});
const handleSubmit = () => {
formRef.value?.validate((errors) => {
formRef.value?.validate(errors => {
if (!errors) {
console.log('提交数据:', formData.value)
console.log('提交数据:', formData.value);
// 调用保存接口
}
})
}
});
};
const handleCancel = () => {
// 取消逻辑
visible.value = false
}
visible.value = false;
};
</script>
+2 -2
View File
@@ -5,7 +5,7 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const { t } = useI18n();
</script>
+2 -2
View File
@@ -5,7 +5,7 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const { t } = useI18n();
</script>
+18 -10
View File
@@ -1,32 +1,40 @@
<script setup>
import JsonEditorVue from "json-editor-vue"
import "vanilla-jsoneditor/themes/jse-theme-dark.css"
import { useI18n } from 'vue-i18n'
import JsonEditorVue from 'json-editor-vue';
import 'vanilla-jsoneditor/themes/jse-theme-dark.css';
import { useI18n } from 'vue-i18n';
// import { createAjvValidator } from 'svelte-jsoneditor'
// const validator = createAjvValidator({ schema, schemaDefinitions })
const { t } = useI18n()
const attrs = useAttrs()
const model = defineModel()
const { t } = useI18n();
const attrs = useAttrs();
const model = defineModel();
// const value = ref(model.value)
// 监听model变化
watch(
model,
(newValue) => {
model.value = JSON.stringify(JSON.parse(newValue), null, 2)
newValue => {
model.value = JSON.stringify(JSON.parse(newValue), null, 2);
// value.value = JSON.stringify(JSON.parse(newValue), null, 2)
},
{
deep: true,
immediate: true,
}
)
);
</script>
<template>
<!-- :validator="validator" 验证器 -->
<div class="w-full">
<JsonEditorVue v-bind="attrs" mode="text" v-model="model" :mainMenuBar="false" selection="TextSelection" class="editor w-full jse-theme-dark" :title="t('JSON Editor')" />
<JsonEditorVue
v-bind="attrs"
mode="text"
v-model="model"
:mainMenuBar="false"
selection="TextSelection"
class="editor w-full jse-theme-dark"
:title="t('JSON Editor')"
/>
</div>
</template>
<style scoped>
+16 -16
View File
@@ -10,41 +10,41 @@
</template>
<script setup lang="ts">
import { Icon } from '#components'
import type { DropdownOption } from 'naive-ui'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Icon } from '#components';
import type { DropdownOption } from 'naive-ui';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { locale, setLocale } = useI18n()
const { locale, setLocale } = useI18n();
const languageConfig = {
en: { text: 'English', icon: 'ri:translate' },
zh: { text: '中文', icon: 'ri:translate-2' },
tr: { text: 'Türkçe', icon: 'ri:translate' }
} as const
tr: { text: 'Türkçe', icon: 'ri:translate' },
} as const;
const currentLanguage = computed(() => {
return languageConfig[locale.value as keyof typeof languageConfig] || languageConfig.en
})
return languageConfig[locale.value as keyof typeof languageConfig] || languageConfig.en;
});
const options = ref<DropdownOption[]>([
{
label: 'English',
key: 'en'
key: 'en',
},
{
label: '中文',
key: 'zh'
key: 'zh',
},
{
label: 'Türkçe',
key: 'tr'
}
])
key: 'tr',
},
]);
const handleSelect = async (key: string) => {
await setLocale(key as 'en' | 'zh' | 'tr')
}
await setLocale(key as 'en' | 'zh' | 'tr');
};
</script>
<style scoped>
+5 -21
View File
@@ -12,12 +12,7 @@
>
<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" />
@@ -39,10 +34,7 @@
<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
@@ -103,10 +95,7 @@
<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
@@ -157,8 +146,7 @@ const randomUUID = () => {
};
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } =
useBucket({});
const { putBucketLifecycleConfiguration, getBucketVersioning, getBucketLifecycleConfiguration } = useBucket({});
const { listTiers } = useTiers();
const { t } = useI18n();
const message = useMessage();
@@ -258,11 +246,7 @@ const handleSave = () => {
formRef.value?.validate((errors: any) => {
if (!errors) {
// 额外的参数验证(与表单验证保持一致)
if (
formData.value.days === null ||
formData.value.days === undefined ||
formData.value.days < 1
) {
if (formData.value.days === null || formData.value.days === undefined || formData.value.days < 1) {
message.error(t('Please enter valid days'));
return;
}
+6 -6
View File
@@ -7,13 +7,13 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
import { computed } from 'vue';
import { useDeleteTaskManagerStore } from '~/store/delete-tasks';
const store = useDeleteTaskManagerStore()
const tasks = computed(() => store.tasks)
const store = useDeleteTaskManagerStore();
const tasks = computed(() => store.tasks);
const clearTasks = () => {
store.clearTasks()
}
store.clearTasks();
};
</script>
+14 -8
View File
@@ -4,7 +4,13 @@
<div>{{ task.key }}</div>
<n-button text @click="handleDeleteTask" type="info">{{ t('Delete Record') }}</n-button>
</div>
<n-progress type="line" :height="2" :percentage="task.progress" :show-indicator="false" :processing="task.status == 'deleting'" />
<n-progress
type="line"
:height="2"
:percentage="task.progress"
:show-indicator="false"
:processing="task.status == 'deleting'"
/>
<div class="flex items-center justify-between text-muted-foreground">
<div></div>
<div class="text-muted-foreground">
@@ -19,14 +25,14 @@
</template>
<script setup lang="ts">
import type { DeleteTask } from '~/lib/delete-task-manager'
import { useDeleteTaskManagerStore } from '~/store/delete-tasks'
const { t } = useI18n()
const store = useDeleteTaskManagerStore()
import type { DeleteTask } from '~/lib/delete-task-manager';
import { useDeleteTaskManagerStore } from '~/store/delete-tasks';
const { t } = useI18n();
const store = useDeleteTaskManagerStore();
const props = defineProps<{ task: DeleteTask }>()
const props = defineProps<{ task: DeleteTask }>();
const handleDeleteTask = () => {
store.removeTask(props.task.id)
}
store.removeTask(props.task.id);
};
</script>
+3 -3
View File
@@ -6,7 +6,7 @@
</template>
<script setup lang="ts">
import type { DeleteTask } from '~/lib/delete-task-manager'
const { t } = useI18n()
const props = defineProps<{ tasks: DeleteTask[] }>()
import type { DeleteTask } from '~/lib/delete-task-manager';
const { t } = useI18n();
const props = defineProps<{ tasks: DeleteTask[] }>();
</script>
+8 -31
View File
@@ -40,19 +40,13 @@
<n-descriptions-item :label="t('Object Name')">
<span class="select-all">{{ key }}</span>
</n-descriptions-item>
<n-descriptions-item :label="t('Object Size')">{{
object?.ContentLength
}}</n-descriptions-item>
<n-descriptions-item :label="t('Object Type')">{{
object?.ContentType
}}</n-descriptions-item>
<n-descriptions-item :label="t('Object Size')">{{ object?.ContentLength }}</n-descriptions-item>
<n-descriptions-item :label="t('Object Type')">{{ object?.ContentType }}</n-descriptions-item>
<!-- <n-descriptions-item label="存储类型">{{ object?.StorageClass }}</n-descriptions-item> -->
<n-descriptions-item label="ETag">
<span class="select-all">{{ object?.ETag }}</span>
</n-descriptions-item>
<n-descriptions-item :label="t('Last Modified Time')">{{
object?.LastModified
}}</n-descriptions-item>
<n-descriptions-item :label="t('Last Modified Time')">{{ object?.LastModified }}</n-descriptions-item>
<!-- 合法保留 -->
<n-descriptions-item :label="t('Legal Hold')">
@@ -82,9 +76,7 @@
<!-- 保留 -->
<n-descriptions-item :label="t('Retention') + t('Policy')">
<n-space>
<span>{{
t('Retention Mode') + ': ' + (retentionMode ? retentionMode : t('None'))
}}</span>
<span>{{ t('Retention Mode') + ': ' + (retentionMode ? retentionMode : t('None')) }}</span>
</n-space>
<n-space v-if="retainUntilDate">
<span>{{ t('Retention RetainUntilDate') + ': ' + retainUntilDate }}</span>
@@ -96,9 +88,7 @@
<n-descriptions-item :label="t('Temporary URL')">
<div class="flex items-center gap-2 mt-1">
<n-input v-model:value="signedUrl" id="signedUrl" :placeholder="t('Temporary URL')" />
<n-button id="signedUrlBtn" data-clipboard-target="#signedUrl">{{
t('Copy')
}}</n-button>
<n-button id="signedUrlBtn" data-clipboard-target="#signedUrl">{{ t('Copy') }}</n-button>
</div>
</n-descriptions-item>
</n-descriptions>
@@ -111,13 +101,7 @@
/>
<!-- tagview -->
<n-modal
v-model:show="showTagView"
preset="card"
:title="t('Set Tags')"
draggable
class="max-w-screen-md"
>
<n-modal v-model:show="showTagView" preset="card" :title="t('Set Tags')" draggable class="max-w-screen-md">
<n-card class="max-w-screen-md">
<n-space class="my-4">
<n-tag
@@ -135,10 +119,7 @@
<n-input v-model:value="tagFormValue.Key" :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('Tag Value Placeholder')"
/>
<n-input v-model:value="tagFormValue.Value" :placeholder="t('Tag Value Placeholder')" />
</n-form-item>
<n-form-item>
<n-button type="primary" @click="submitTagForm">{{ t('Add') }}</n-button>
@@ -169,11 +150,7 @@
</n-radio-group>
</n-form-item>
<n-form-item
:label="t('Retention RetainUntilDate')"
path="retainUntilDate"
class="flex-auto"
>
<n-form-item :label="t('Retention RetainUntilDate')" path="retainUntilDate" class="flex-auto">
<n-date-picker
v-model:formatted-value="retainUntilDate"
value-format="yyyy-MM-dd HH:mm:ss"
+9 -27
View File
@@ -94,13 +94,7 @@ 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 { 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';
@@ -181,11 +175,7 @@ const bucketPath = (path?: string | Array<string>) => {
path = path.join('/');
}
return joinRelativeURL(
'/browser',
encodeURIComponent(bucketName.value),
path ? encodeURIComponent(path) : ''
);
return joinRelativeURL('/browser', encodeURIComponent(bucketName.value), path ? encodeURIComponent(path) : '');
};
interface RowData {
@@ -208,10 +198,7 @@ const columns: DataTableColumns<RowData> = [
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;
@@ -395,13 +382,9 @@ function handleBatchDelete() {
// 目录删除
// 递归查询目录下的所有文件,然后删除
if (findOne?.type === 'prefix' && findOne?.Key) {
return await objectApi.mapAllFiles(
bucketName.value,
findOne.Key,
(fileKey: string) => {
deleteTaskStore.addKeys([fileKey], bucketName.value);
}
);
return await objectApi.mapAllFiles(bucketName.value, findOne.Key, (fileKey: string) => {
deleteTaskStore.addKeys([fileKey], bucketName.value);
});
}
return deleteTaskStore.addKeys([String(item)], bucketName.value);
@@ -503,10 +486,9 @@ const downloadMultiple = async () => {
let downloadMsg: any = null;
const updateDownloadMsg = () => {
if (downloadMsg) downloadMsg.destroy();
downloadMsg = message.loading(
`${t('Downloading files')} ${Math.round((finished / total) * 100)}%`,
{ duration: 0 }
);
downloadMsg = message.loading(`${t('Downloading files')} ${Math.round((finished / total) * 100)}%`, {
duration: 0,
});
};
updateDownloadMsg();
const zip = new JSZip();
+18 -18
View File
@@ -3,8 +3,8 @@
<n-card class="max-w-screen-md">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span>{{ t("New Form", { type: displayType }) }}</span>
<n-button size="small" ghost @click="closeModal">{{ t("Close") }}</n-button>
<span>{{ t('New Form', { type: displayType }) }}</span>
<n-button size="small" ghost @click="closeModal">{{ t('Close') }}</n-button>
</div>
</template>
<div class="flex flex-col gap-4">
@@ -14,7 +14,7 @@
</div> -->
<n-alert title="" type="info">
{{ t("Overwrite Warning") }}
{{ t('Overwrite Warning') }}
</n-alert>
<div class="flex items-center gap-4">
@@ -23,7 +23,7 @@
<div class="flex justify-center gap-4">
<n-button type="primary" :disabled="objectKey.trim().length < 1" @click="handlePutObject">
{{ t("Create") }}
{{ t('Create') }}
</n-button>
</div>
</div>
@@ -32,35 +32,35 @@
</template>
<script setup lang="ts">
import { joinRelativeURL } from "ufo";
import { computed, defineEmits, defineProps } from "vue";
import { useI18n } from "vue-i18n";
import { joinRelativeURL } from 'ufo';
import { computed, defineEmits, defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const emit = defineEmits(["update:show"]);
const emit = defineEmits(['update:show']);
const props = defineProps<{ show: boolean; bucketName: string; prefix: string; asPrefix?: boolean }>();
const closeModal = () => emit("update:show", false);
const closeModal = () => emit('update:show', false);
const displayType = computed(() => (props.asPrefix ? t("New Folder") : t("New File")));
const displayType = computed(() => (props.asPrefix ? t('New Folder') : t('New File')));
const objectKey = ref("");
const objectKey = ref('');
const { putObject } = useObject({ bucket: props.bucketName });
const $message = useMessage();
const handlePutObject = () => {
const suffix = props.asPrefix ? "/" : "";
const cleanedKey = objectKey.value.replace(/^\/+|\/+$/g, ""); // 移除开头和结尾的 `/`
putObject(joinRelativeURL(props.prefix, cleanedKey, suffix), "")
const suffix = props.asPrefix ? '/' : '';
const cleanedKey = objectKey.value.replace(/^\/+|\/+$/g, ''); // 移除开头和结尾的 `/`
putObject(joinRelativeURL(props.prefix, cleanedKey, suffix), '')
.then(() => {
emit("update:show", false);
objectKey.value = "";
$message.success(t("Create Success"));
emit('update:show', false);
objectKey.value = '';
$message.success(t('Create Success'));
})
.catch((e) => {
.catch(e => {
$message.error(e.message);
});
};
+31 -19
View File
@@ -1,38 +1,50 @@
<template>
<template v-for="(segment) in displaySegments">
<template v-for="segment in displaySegments">
<span class="text-gray-500">/</span>
<button @click="() => handleOnClick(segment)" :class="{ 'text-blue-500 hover:underline': segment.index > -1, 'cursor-default': segment.index === -1 }">{{ segment.value
}}</button>
<button
@click="() => handleOnClick(segment)"
:class="{ 'text-blue-500 hover:underline': segment.index > -1, 'cursor-default': segment.index === -1 }"
>
{{ segment.value }}
</button>
</template>
</template>
<script lang="ts" setup>
const props = defineProps<{
objectKey: string
onClick: (path: string) => any
}>()
objectKey: string;
onClick: (path: string) => any;
}>();
const segments = computed(() => props.objectKey.split('/').filter(Boolean).map((item, index) => {
return {
value: item,
index
}
}))
const segments = computed(() =>
props.objectKey
.split('/')
.filter(Boolean)
.map((item, index) => {
return {
value: item,
index,
};
})
);
const displaySegments = computed(() => {
if (segments.value.length <= 6) {
return segments.value
return segments.value;
}
return [...segments.value.slice(0, 3), { value: '...', index: -1 }, ...segments.value.slice(-3)]
})
return [...segments.value.slice(0, 3), { value: '...', index: -1 }, ...segments.value.slice(-3)];
});
const handleOnClick = (segment: { value: string; index: number }) => {
if (segment.index === -1) {
return
return;
}
const path = segments.value.slice(0, segment.index + 1).map(item => item.value).join('/')
props.onClick(path)
}
const path = segments.value
.slice(0, segment.index + 1)
.map(item => item.value)
.join('/');
props.onClick(path);
};
</script>
+6 -26
View File
@@ -20,12 +20,7 @@
>
<template v-if="canPreview">
<img v-if="isImage" :src="previewUrl" alt="preview" />
<iframe
v-else-if="isPdf"
:src="previewUrl"
class="w-full min-h-[70vh]"
frameborder="0"
></iframe>
<iframe v-else-if="isPdf" :src="previewUrl" class="w-full min-h-[70vh]" frameborder="0"></iframe>
<pre v-else-if="isText" class="w-full selea">{{ fileContent }}</pre>
<video v-else-if="isVideo" controls class="w-full">
<source :src="previewUrl" type="video/mp4" />
@@ -154,11 +149,7 @@ const fileTypeConfig = {
};
// 文件类型判断函数
function isFileType(
contentType: string | undefined,
fileName: string,
type: keyof typeof fileTypeConfig
): boolean {
function isFileType(contentType: string | undefined, fileName: string, type: keyof typeof fileTypeConfig): boolean {
if (type === 'text') {
return (
contentType?.startsWith('text/') ||
@@ -168,24 +159,15 @@ function isFileType(
}
if (type === 'image') {
return (
contentType?.startsWith('image/') ||
fileTypeConfig.image.extensions.some(ext => fileName.endsWith(ext))
);
return contentType?.startsWith('image/') || fileTypeConfig.image.extensions.some(ext => fileName.endsWith(ext));
}
if (type === 'audio') {
return (
contentType?.startsWith('audio/') ||
fileTypeConfig.audio.extensions.some(ext => fileName.endsWith(ext))
);
return contentType?.startsWith('audio/') || fileTypeConfig.audio.extensions.some(ext => fileName.endsWith(ext));
}
if (type === 'pdf') {
return (
contentType === 'application/pdf' ||
fileTypeConfig.pdf.extensions.some(ext => fileName.endsWith(ext))
);
return contentType === 'application/pdf' || fileTypeConfig.pdf.extensions.some(ext => fileName.endsWith(ext));
}
return false;
@@ -198,9 +180,7 @@ const isAudio = computed(() => isFileType(contentType.value, props.objectKey, 'a
const isPdf = computed(() => isFileType(contentType.value, props.objectKey, 'pdf'));
const isText = computed(() => isFileType(contentType.value, props.objectKey, 'text'));
const canPreview = computed(
() => isImage.value || isPdf.value || isText.value || isVideo.value || isAudio.value
);
const canPreview = computed(() => isImage.value || isPdf.value || isText.value || isVideo.value || isAudio.value);
// 当 show 为 true 时触发加载逻辑
watch(
+2 -6
View File
@@ -75,14 +75,10 @@ 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 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 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'));
+4 -15
View File
@@ -10,14 +10,7 @@
<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>
@@ -75,13 +68,9 @@
<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>
+6 -6
View File
@@ -7,13 +7,13 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useUploadTaskManagerStore } from '~/store/upload-tasks'
import { computed } from 'vue';
import { useUploadTaskManagerStore } from '~/store/upload-tasks';
const store = useUploadTaskManagerStore()
const tasks = computed(() => store.tasks)
const store = useUploadTaskManagerStore();
const tasks = computed(() => store.tasks);
const clearTasks = () => {
store.clearTasks()
}
store.clearTasks();
};
</script>
+4 -18
View File
@@ -50,29 +50,19 @@
<n-descriptions-item :label="t('Object Name')"
><span class="select-all">{{ key }}</span></n-descriptions-item
>
<n-descriptions-item :label="t('Object Size')">{{
object?.ContentLength
}}</n-descriptions-item>
<n-descriptions-item :label="t('Object Size')">{{ object?.ContentLength }}</n-descriptions-item>
<n-descriptions-item :label="t('Object Type')">{{ object?.ContentType }}</n-descriptions-item>
<!-- <n-descriptions-item label="存储类型">{{ object?.StorageClass }}</n-descriptions-item> -->
<n-descriptions-item label="ETag"
><span class="select-all">{{ object?.ETag }}</span></n-descriptions-item
>
<n-descriptions-item :label="t('Last Modified Time')">{{
object?.LastModified
}}</n-descriptions-item>
<n-descriptions-item :label="t('Last Modified Time')">{{ object?.LastModified }}</n-descriptions-item>
</n-descriptions>
<object-preview-modal v-model:show="showPreview" :bucketName="bucketName" :objectKey="key" />
<!-- tagview -->
<n-modal
v-model:show="showTagView"
preset="card"
:title="t('Set Tags')"
draggable
class="max-w-screen-md"
>
<n-modal v-model:show="showTagView" preset="card" :title="t('Set Tags')" draggable class="max-w-screen-md">
<n-card class="max-w-screen-md">
<n-space class="my-4">
<n-tag
@@ -179,11 +169,7 @@ const submitTagForm = async () => {
const objectApi = useObject({ bucket: bucketName.value });
// 在服务端获取数据
const {
data: object,
status,
refresh,
} = useAsyncData(`head-object&${key}`, () => objectApi.headObject(key.value));
const { data: object, status, refresh } = useAsyncData(`head-object&${key}`, () => objectApi.headObject(key.value));
const download = async () => {
const msg = message.loading(t('Getting URL'));
+3 -1
View File
@@ -1,5 +1,7 @@
<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 dark:border-neutral-800 flex justify-between bg-gray-100 dark:bg-neutral-900"
>
<slot name="title"></slot>
<slot name="actions"></slot>
</div>
+18 -13
View File
@@ -7,39 +7,37 @@ const { $api } = useNuxtApp();
const message = useMessage();
interface PolicyItem {
name: string
content: string | object
name: string;
content: string | object;
}
const emit = defineEmits(['update:show', 'saved']);
const props = defineProps<{
show: boolean
policy: PolicyItem | null | undefined
show: boolean;
policy: PolicyItem | null | undefined;
}>();
const name = ref(props.policy?.name || '');
const content = ref(
typeof props.policy?.content === 'object'
? JSON.stringify(props.policy?.content, null, 2)
: (props.policy?.content || '')
: props.policy?.content || ''
);
watch(
() => props.policy,
(newPolicy) => {
newPolicy => {
if (newPolicy) {
name.value = newPolicy.name || '';
content.value =
typeof newPolicy.content === 'object'
? JSON.stringify(newPolicy.content, null, 2)
: (newPolicy.content || '');
typeof newPolicy.content === 'object' ? JSON.stringify(newPolicy.content, null, 2) : newPolicy.content || '';
}
},
{ immediate: true }
);
const closeModal = () => emit('update:show', false)
const closeModal = () => emit('update:show', false);
async function submitForm() {
try {
@@ -55,18 +53,25 @@ async function submitForm() {
</script>
<template>
<n-modal :show="show" @update:show="(val: boolean) => $emit('update:show', val)" :mask-closable="false" preset="card" :title="t('Policy Original')" class="max-w-screen-md"
<n-modal
:show="show"
@update:show="(val: boolean) => $emit('update:show', val)"
:mask-closable="false"
preset="card"
:title="t('Policy Original')"
class="max-w-screen-md"
:segmented="{
content: true,
action: true,
}">
}"
>
<n-form label-placement="top" label-align="left" :label-width="100">
<n-grid :cols="24" :x-gap="18">
<n-form-item-grid-item :span="24" :label="t('Policy Name')" path="name">
<n-input v-model:value="name" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('Policy Original')" path="content">
<n-scrollbar class="w-full max-h-[60vh] "> <json-editor v-model="content" /></n-scrollbar>
<n-scrollbar class="w-full max-h-[60vh]"> <json-editor v-model="content" /></n-scrollbar>
</n-form-item-grid-item>
</n-grid>
</n-form>
+42 -25
View File
@@ -1,9 +1,9 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n"
import { RouterLink } from "vue-router"
import { useSidebarStore } from "~/store/sidebar"
import type { AppConfig, NavItem } from "~/types/app-config"
import type { SiteConfig } from "~/types/config"
import { useI18n } from 'vue-i18n';
import { RouterLink } from 'vue-router';
import { useSidebarStore } from '~/store/sidebar';
import type { AppConfig, NavItem } from '~/types/app-config';
import type { SiteConfig } from '~/types/config';
const { t } = useI18n();
const appConfig = useAppConfig() as unknown as AppConfig;
@@ -24,8 +24,8 @@ try {
endpoint: window.location.origin,
region: 'us-east-1',
accessKeyId: '',
secretAccessKey: ''
}
secretAccessKey: '',
},
};
}
@@ -48,24 +48,24 @@ const navOptions = computed(() => {
nav.to
? nav.target
? h(
"a",
{
href: nav.to,
target: "_blank",
},
{ default: () => t(nav.label) }
)
'a',
{
href: nav.to,
target: '_blank',
},
{ default: () => t(nav.label) }
)
: h(RouterLink, { to: nav.to }, { default: () => t(nav.label) })
: t(nav.label),
icon: nav.icon ? iconRender(nav.icon) : undefined,
type: nav.type,
};
if (nav["children"]) {
item.children = nav["children"].map((child) => {
if (nav['children']) {
item.children = nav['children'].map(child => {
return {
key: child.label,
type: "item",
type: 'item',
label: () => h(RouterLink, { to: child.to || '/' }, { default: () => t(child.label) }),
icon: child.icon ? iconRender(child.icon) : undefined,
};
@@ -73,18 +73,28 @@ const navOptions = computed(() => {
}
return item;
}
);
});
});
// 直接使用计算属性,不需要 readonly 包装
const menuOptions = navOptions;
</script>
<template>
<n-layout-sider bordered class="min-h-full" collapse-mode="width" :collapsed-width="64" :width="240" :native-scrollbar="false" :collapsed="isCollapsed"
v-if="route.path.startsWith('/auth') === false">
<n-layout-sider
bordered
class="min-h-full"
collapse-mode="width"
:collapsed-width="64"
:width="240"
:native-scrollbar="false"
:collapsed="isCollapsed"
v-if="route.path.startsWith('/auth') === false"
>
<div class="flex flex-col h-screen overflow-hidden gap-2 relative">
<div class="border-b dark:border-neutral-800 flex flex-wrap h-16 items-center p-4" :class="isCollapsed ? 'justify-center' : 'justify-between'">
<div
class="border-b dark:border-neutral-800 flex flex-wrap h-16 items-center p-4"
:class="isCollapsed ? 'justify-center' : 'justify-between'"
>
<div>
<n-avatar v-if="isCollapsed" class="text-center text-2xl leading-none">
{{ appConfig.name.substring(0, 1) }}
@@ -100,7 +110,14 @@ const menuOptions = navOptions;
</div>
<div class="overflow-y-auto flex-1">
<n-menu :indent="26" :root-indent="12" :collapsed-width="64" :collapsed-icon-size="22" :options="menuOptions" class="flex-1" />
<n-menu
:indent="26"
:root-indent="12"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
class="flex-1"
/>
</div>
<div v-if="isCollapsed" class="w-full flex items-center justify-center py-4">
@@ -117,7 +134,7 @@ const menuOptions = navOptions;
<Icon :name="isCollapsed ? 'ri:translate-2' : 'ri:translate'" class="text-xl" />
</div>
</template>
{{ t("Language") }}
{{ t('Language') }}
</n-tooltip>
</div>
<language-switcher v-else />
@@ -132,7 +149,7 @@ const menuOptions = navOptions;
<Icon name="ri:contrast-2-line" class="text-xl" />
</div>
</template>
{{ t("Theme") }}
{{ t('Theme') }}
</n-tooltip>
</div>
<theme-switcher v-else />
+37 -38
View File
@@ -1,5 +1,5 @@
<template>
<n-modal
<n-modal
v-model:show="visible"
:mask-closable="false"
preset="card"
@@ -8,14 +8,17 @@
:segmented="{
content: true,
action: true,
}">
}"
>
<n-card :title="t('Add Replication Site')">
<p>{{ t('Note: AccessKey and SecretKey values are required for each site when adding or editing peer sites') }}</p>
<p>
{{ t('Note: AccessKey and SecretKey values are required for each site when adding or editing peer sites') }}
</p>
<n-form ref="currentFormRef" :model="currentSite" :rules="rules">
<!-- 当前站点 -->
<n-flex style="margin-top: 16px;">
<n-card :title="t('Current Site')" >
<n-space direction="vertical" >
<n-flex style="margin-top: 16px">
<n-card :title="t('Current Site')">
<n-space direction="vertical">
<n-form-item :label="t('Site Name')" path="name">
<n-input v-model:value="currentSite.name" :placeholder="t('Site Name')" />
</n-form-item>
@@ -33,12 +36,12 @@
</n-flex>
<!-- 远程站点 -->
<n-flex direction="vertical" style="margin-top: 16px;">
<n-flex direction="vertical" style="margin-top: 16px">
<n-card :title="t('Remote Site')">
<n-space direction="vertical">
<n-dynamic-input :min='1' v-model:value="remoteSite" :on-create="onCreate">
<template #default="{ value }" >
<n-grid x-gap="12" :cols="4">
<n-dynamic-input :min="1" v-model:value="remoteSite" :on-create="onCreate">
<template #default="{ value }">
<n-grid x-gap="12" :cols="4">
<n-gi>
<n-form-item :label="t('Site Name')" path="name">
<n-input v-model:value="value.name" :placeholder="t('Site Name')" />
@@ -50,9 +53,9 @@
</n-form-item>
</n-gi>
<n-gi>
<n-form-item :label="t('Access Key *')" path="accessKey">
<n-input v-model:value="value.accessKey" :placeholder="t('Access Key')" />
</n-form-item>
<n-form-item :label="t('Access Key *')" path="accessKey">
<n-input v-model:value="value.accessKey" :placeholder="t('Access Key')" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item :label="t('Secret Key *')" path="secretKey">
@@ -61,13 +64,13 @@
</n-gi>
</n-grid>
</template>
</n-dynamic-input>
</n-dynamic-input>
</n-space>
</n-card>
</n-flex>
</n-form>
<!-- 按钮 -->
<n-space justify="center" style="margin-top: 16px;">
<n-space justify="center" style="margin-top: 16px">
<n-button type="primary" @click="save">{{ t('Save') }}</n-button>
<n-button @click="cancel">{{ t('Cancel') }}</n-button>
</n-space>
@@ -85,35 +88,31 @@ const currentSite = ref({
name: '',
endpoint: 'http://127.0.0.1:7000',
accessKey: 'rusyfsadmin',
secretKey: ''
secretKey: '',
});
const remoteSite = ref([{
name: '',
endpoint: '',
accessKey: '',
secretKey: ''
}]);
const remoteSite = ref([
{
name: '',
endpoint: '',
accessKey: '',
secretKey: '',
},
]);
const onCreate = () => {
return {
name: '',
endpoint: '',
accessKey: '',
secretKey: ''
secretKey: '',
};
};
const rules = {
endpoint: [
{ required: true, message: t('Endpoint is required'), trigger: 'blur' }
],
accessKey: [
{ required: true, message: t('Access Key is required'), trigger: 'blur' }
],
secretKey: [
{ required: true, message: t('Secret Key is required'), trigger: 'blur' }
]
endpoint: [{ required: true, message: t('Endpoint is required'), trigger: 'blur' }],
accessKey: [{ required: true, message: t('Access Key is required'), trigger: 'blur' }],
secretKey: [{ required: true, message: t('Secret Key is required'), trigger: 'blur' }],
};
const currentFormRef = ref(null);
@@ -129,16 +128,16 @@ const cancel = () => {
const visible = ref(false);
const open = () => {
visible.value = true
}
visible.value = true;
};
defineExpose({
open
})
open,
});
</script>
<style scoped >
:deep(.n-dynamic-input-item__action){
<style scoped>
:deep(.n-dynamic-input-item__action) {
align-self: center !important;
}
</style>
+27 -33
View File
@@ -10,63 +10,57 @@
</template>
<script setup lang="ts">
import { Icon } from '#components'
import { useColorMode } from '@vueuse/core'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Icon } from '#components';
import { useColorMode } from '@vueuse/core';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const { store } = useColorMode()
const { t } = useI18n();
const { store } = useColorMode();
const themeName = computed(() => {
switch (store.value) {
case 'dark':
return 'Dark'
return 'Dark';
case 'light':
return 'Light'
return 'Light';
default:
return 'Auto'
return 'Auto';
}
})
});
const themeIcon = computed(() => {
switch (store.value) {
case 'dark':
return 'ri:moon-fill'
return 'ri:moon-fill';
case 'light':
return 'ri:sun-fill'
return 'ri:sun-fill';
default:
return 'ri:contrast-2-line'
return 'ri:contrast-2-line';
}
})
});
const themeOptions = [
{
label: () => h('div', { class: 'flex items-center' }, [
h(Icon, { name: 'ri:sun-fill', class: 'mr-2' }),
t('Light')
]),
key: 'light'
label: () =>
h('div', { class: 'flex items-center' }, [h(Icon, { name: 'ri:sun-fill', class: 'mr-2' }), t('Light')]),
key: 'light',
},
{
label: () => h('div', { class: 'flex items-center' }, [
h(Icon, { name: 'ri:moon-fill', class: 'mr-2' }),
t('Dark')
]),
key: 'dark'
label: () =>
h('div', { class: 'flex items-center' }, [h(Icon, { name: 'ri:moon-fill', class: 'mr-2' }), t('Dark')]),
key: 'dark',
},
{
label: () => h('div', { class: 'flex items-center' }, [
h(Icon, { name: 'ri:contrast-2-line', class: 'mr-2' }),
t('Auto')
]),
key: 'auto'
}
]
label: () =>
h('div', { class: 'flex items-center' }, [h(Icon, { name: 'ri:contrast-2-line', class: 'mr-2' }), t('Auto')]),
key: 'auto',
},
];
const handleSelect = (key: string) => {
if (key === 'light' || key === 'dark' || key === 'auto') {
store.value = key
store.value = key;
}
}
};
</script>
+24 -22
View File
@@ -8,7 +8,8 @@
:segmented="{
content: true,
action: true,
}">
}"
>
<n-card>
<n-form
ref="formRef"
@@ -16,7 +17,8 @@
:model="formModel"
:rules="rules"
label-align="center"
:label-width="130">
:label-width="130"
>
<n-grid :cols="24" :x-gap="18">
<n-form-item-gi :span="24" :label="t('Access Key')" path="accessKey">
<n-input v-model:value="formModel.accessKey" />
@@ -29,16 +31,16 @@
</n-card>
<template #action>
<n-space justify="center">
<n-button @click="closeModal()">{{ t("Cancel") }}</n-button>
<n-button type="primary" @click="submitForm">{{ t("Submit") }}</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>
</template>
<script setup lang="ts">
import type { FormInst } from "naive-ui";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import type { FormInst } from 'naive-ui';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const usetier = useTiers();
const { t } = useI18n();
@@ -51,8 +53,8 @@ const message = useMessage();
const emit = defineEmits<Emits>();
const defaultFormModal = {
accessKey: "",
secretKey: "",
accessKey: '',
secretKey: '',
};
const formModel = ref({ ...defaultFormModal });
@@ -60,19 +62,19 @@ const formModel = ref({ ...defaultFormModal });
const rules = ref({
accessKey: {
required: true,
trigger: ["blur", "input"],
message: t("Please enter Access Key"),
trigger: ['blur', 'input'],
message: t('Please enter Access Key'),
},
secretKey: {
required: true,
trigger: ["blur", "input"],
message: t("Please enter Secret Key"),
trigger: ['blur', 'input'],
message: t('Please enter Secret Key'),
},
});
interface Emits {
(e: "update:visible", visible: boolean): void;
(e: "search"): void;
(e: 'update:visible', visible: boolean): void;
(e: 'search'): void;
}
const modalVisible = computed({
@@ -84,14 +86,14 @@ const modalVisible = computed({
},
});
function closeModal(visible = false) {
emit("update:visible", visible);
emit('update:visible', visible);
formModel.value = { ...defaultFormModal };
}
const formRef = ref<FormInst | null>(null);
async function submitForm(e: MouseEvent) {
// e.preventDefault()
formRef.value?.validate(async (errors) => {
formRef.value?.validate(async errors => {
if (!errors) {
try {
// const res = await createServiceAccount({
@@ -102,16 +104,16 @@ async function submitForm(e: MouseEvent) {
const res = await usetier.updateTiers(name, {
...formModel.value,
});
message.success(t("Update Success"));
message.success(t('Update Success'));
closeModal();
emit("search");
emit('search');
} catch (error) {
console.log("🚀 ~ submitForm ~ error:", error);
message.error(t("Update Failed"));
console.log('🚀 ~ submitForm ~ error:', error);
message.error(t('Update Failed'));
}
} else {
console.log(errors);
message.error(t("Please fill in the correct format"));
message.error(t('Please fill in the correct format'));
}
});
}
@@ -5,7 +5,7 @@
:class="
cn(
'relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg',
props.class,
props.class
)
"
>
@@ -21,7 +21,7 @@
'aurora-background-gradient-after',
'aurora-gradient-animation',
props.radialGradient &&
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`,
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
)
"
></div>
+23 -15
View File
@@ -1,19 +1,27 @@
<template>
<div class="relative inline-block">
<Transition @after-enter="$emit('animationStart')" @after-leave="$emit('animationComplete')">
<div v-show="isVisible" :class="[
'relative z-10 inline-block text-left text-neutral-900 dark:text-neutral-100',
props.class,
]">
<div
v-show="isVisible"
:class="['relative z-10 inline-block text-left text-neutral-900 dark:text-neutral-100', props.class]"
>
<template v-for="(wordObj, wordIndex) in splitWords" :key="wordObj.word + wordIndex">
<span class="inline-block whitespace-nowrap opacity-0" :style="{
animation: `fadeInWord 0.3s ease forwards`,
animationDelay: `${wordIndex * 0.3}s`,
}">
<span v-for="(letter, letterIndex) in wordObj.letters" :key="wordObj.word + letterIndex" class="inline-block opacity-0" :style="{
animation: `fadeInLetter 0.2s ease forwards`,
animationDelay: `${wordIndex * 0.3 + letterIndex * 0.05}s`,
}">
<span
class="inline-block whitespace-nowrap opacity-0"
:style="{
animation: `fadeInWord 0.3s ease forwards`,
animationDelay: `${wordIndex * 0.3}s`,
}"
>
<span
v-for="(letter, letterIndex) in wordObj.letters"
:key="wordObj.word + letterIndex"
class="inline-block opacity-0"
:style="{
animation: `fadeInLetter 0.2s ease forwards`,
animationDelay: `${wordIndex * 0.3 + letterIndex * 0.05}s`,
}"
>
{{ letter }}
</span>
<span class="inline-block">&nbsp;</span>
@@ -25,7 +33,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
interface Props {
words: string[];
@@ -56,7 +64,7 @@ function startAnimation() {
}
const splitWords = computed(() => {
return currentWord.value.split(' ').map((word) => ({
return currentWord.value.split(' ').map(word => ({
word,
letters: word.split(''),
}));
@@ -78,7 +86,7 @@ onBeforeUnmount(() => {
}
});
watch(isVisible, (newValue) => {
watch(isVisible, newValue => {
if (newValue) {
startTimeout();
}
+6 -22
View File
@@ -1,8 +1,5 @@
<template>
<canvas
ref="githubGlobeRef"
:class="cn('w-96 h-96', props.class)"
></canvas>
<canvas ref="githubGlobeRef" :class="cn('w-96 h-96', props.class)"></canvas>
</template>
<script lang="ts" setup>
@@ -11,15 +8,7 @@
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import ThreeGlobe from 'three-globe';
import {
AmbientLight,
Color,
DirectionalLight,
PerspectiveCamera,
PointLight,
Scene,
WebGLRenderer,
} from 'three';
import { AmbientLight, Color, DirectionalLight, PerspectiveCamera, PointLight, Scene, WebGLRenderer } from 'three';
import contries from './globe.json';
import { cn } from '@/lib/utils';
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
@@ -196,7 +185,7 @@ function initGlobe() {
.showAtmosphere(defaultGlobeConfig.showAtmosphere!)
.atmosphereColor(defaultGlobeConfig.atmosphereColor!)
.atmosphereAltitude(defaultGlobeConfig.atmosphereAltitude!)
.hexPolygonColor((e) => defaultGlobeConfig.polygonColor!);
.hexPolygonColor(e => defaultGlobeConfig.polygonColor!);
globe.rotateY(-Math.PI * (5 / 9));
globe.rotateZ(-Math.PI / 6);
@@ -255,9 +244,7 @@ function startAnimation() {
.ringColor((e: any) => (t: any) => e.color(t))
.ringMaxRadius(defaultGlobeConfig.maxRings!)
.ringPropagationSpeed(3)
.ringRepeatPeriod(
(defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / defaultGlobeConfig.rings!,
);
.ringRepeatPeriod((defaultGlobeConfig.arcTime! * defaultGlobeConfig.arcLength!) / defaultGlobeConfig.rings!);
}
function animate() {
@@ -292,10 +279,7 @@ function buildData() {
// remove duplicates for same lat and lng
const filteredPoints = points.filter(
(v, i, a) =>
a.findIndex((v2) =>
['lat', 'lng'].every((k) => v2[k as 'lat' | 'lng'] === v[k as 'lat' | 'lng']),
) === i,
(v, i, a) => a.findIndex(v2 => ['lat', 'lng'].every(k => v2[k as 'lat' | 'lng'] === v[k as 'lat' | 'lng'])) === i
);
globeData.value = filteredPoints;
@@ -308,7 +292,7 @@ function hexToRgb(color: string) {
if (hex.length === 3) {
hex = hex
.split('')
.map((char) => char + char)
.map(char => char + char)
.join('');
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,7 +8,7 @@
'radiant-animation bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--radiant-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
// Radiant gradient
'bg-gradient-to-r from-transparent via-black via-50% to-transparent dark:via-white',
$props.class,
$props.class
)
"
>
+10 -4
View File
@@ -1,13 +1,19 @@
<template>
<div class="absolute inset-0">
<RippleCircle v-for="index in numberOfCircles" :key="index" :opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
:size="baseCircleSize + index * spaceBetweenCircle" :animation-delay="index * waveSpeed" :border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
:class="circleClass" />
<RippleCircle
v-for="index in numberOfCircles"
:key="index"
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
:size="baseCircleSize + index * spaceBetweenCircle"
:animation-delay="index * waveSpeed"
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
:class="circleClass"
/>
</div>
</template>
<script setup lang="ts">
import RippleCircle from './RippleCircle.vue'
import RippleCircle from './RippleCircle.vue';
interface Props {
baseCircleSize?: number;
baseCircleOpacity?: number;
+2 -2
View File
@@ -28,9 +28,9 @@ const props = withDefaults(defineProps<Props>(), {
width: v-bind('props.size + "px"');
height: v-bind('props.size + "px"');
animation-delay: v-bind('props.animationDelay + "ms"');
opacity: v-bind("props.opacity");
opacity: v-bind('props.opacity');
transform: translate(-50%, -50%) scale(1);
border-style: v-bind("props.borderStyle");
border-style: v-bind('props.borderStyle');
}
@keyframes ripple-effect {
+56 -19
View File
@@ -1,19 +1,34 @@
<template>
<div class="relative aspect-[2/1] w-full rounded-lg bg-white font-sans dark:bg-black">
<NuxtImg :src="`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`"
class="pointer-events-none size-full select-none [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]" alt="world map" height="495"
width="1056" :draggable="false" />
<NuxtImg
:src="`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`"
class="pointer-events-none size-full select-none [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_80%,transparent)]"
alt="world map"
height="495"
width="1056"
:draggable="false"
/>
<svg ref="svgRef" view-box="0 0 800 400" class="pointer-events-none absolute inset-0 size-full select-none">
<g v-for="(dot, i) in props.dots" :key="`path-group-${i}`">
<Motion :key="`start-upper-${i}`" as="path" :d="createCurvedPath(dot)" fill="none" stroke="url(#path-gradient)" stroke-width="1" :initial="{
pathLength: 0,
}" :animate="{
pathLength: 1,
}" :transition="{
duration: 1,
delay: 0.5 * i,
ease: 'easeOut',
}"></Motion>
<Motion
:key="`start-upper-${i}`"
as="path"
:d="createCurvedPath(dot)"
fill="none"
stroke="url(#path-gradient)"
stroke-width="1"
:initial="{
pathLength: 0,
}"
:animate="{
pathLength: 1,
}"
:transition="{
duration: 1,
delay: 0.5 * i,
ease: 'easeOut',
}"
></Motion>
</g>
<defs>
@@ -27,15 +42,37 @@
<g v-for="(dot, i) in props.dots" :key="`points-group-${i}`">
<g :key="`start-${i}`">
<circle :cx="projectPoint(dot.start.lat, dot.start.lng).x" :cy="projectPoint(dot.start.lat, dot.start.lng).y" r="2" :fill="props.lineColor" />
<circle :cx="projectPoint(dot.start.lat, dot.start.lng).x" :cy="projectPoint(dot.start.lat, dot.start.lng).y" r="2" :fill="props.lineColor" opacity="0.5">
<circle
:cx="projectPoint(dot.start.lat, dot.start.lng).x"
:cy="projectPoint(dot.start.lat, dot.start.lng).y"
r="2"
:fill="props.lineColor"
/>
<circle
:cx="projectPoint(dot.start.lat, dot.start.lng).x"
:cy="projectPoint(dot.start.lat, dot.start.lng).y"
r="2"
:fill="props.lineColor"
opacity="0.5"
>
<animate attribute-name="r" from="2" to="8" dur="1.5s" begin="0s" repeat-count="indefinite" />
<animate attribute-name="opacity" from="0.5" to="0" dur="1.5s" begin="0s" repeat-count="indefinite" />
</circle>
</g>
<g :key="`end-${i}`">
<circle :cx="projectPoint(dot.end.lat, dot.end.lng).x" :cy="projectPoint(dot.end.lat, dot.end.lng).y" r="2" :fill="props.lineColor" />
<circle :cx="projectPoint(dot.end.lat, dot.end.lng).x" :cy="projectPoint(dot.end.lat, dot.end.lng).y" r="2" :fill="props.lineColor" opacity="0.5">
<circle
:cx="projectPoint(dot.end.lat, dot.end.lng).x"
:cy="projectPoint(dot.end.lat, dot.end.lng).y"
r="2"
:fill="props.lineColor"
/>
<circle
:cx="projectPoint(dot.end.lat, dot.end.lng).x"
:cy="projectPoint(dot.end.lat, dot.end.lng).y"
r="2"
:fill="props.lineColor"
opacity="0.5"
>
<animate attribute-name="r" from="2" to="8" dur="1.5s" begin="0s" repeat-count="indefinite" />
<animate attribute-name="opacity" from="0.5" to="0" dur="1.5s" begin="0s" repeat-count="indefinite" />
</circle>
@@ -46,8 +83,8 @@
</template>
<script setup lang="ts">
import DottedMap from 'dotted-map'
import { Motion } from 'motion-v'
import DottedMap from 'dotted-map';
import { Motion } from 'motion-v';
interface Dot {
start: { lat: number; lng: number; label?: string };
@@ -74,7 +111,7 @@ const svgMap = computed(() =>
color: props.mapColor,
shape: 'circle',
backgroundColor: props.mapBgColor,
}),
})
);
function projectPoint(lat: number, lng: number) {
+20 -17
View File
@@ -13,30 +13,33 @@
</template>
<script lang="ts" setup>
import { Icon } from '#components'
import { defineProps, withDefaults } from 'vue'
import { useI18n } from 'vue-i18n'
import { Icon } from '#components';
import { defineProps, withDefaults } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const { logout } = useAuth()
const router = useRouter()
const { t } = useI18n();
const { logout } = useAuth();
const router = useRouter();
const handleLogout = async () => {
await logout()
router.push('/auth/login')
}
await logout();
router.push('/auth/login');
};
const handleDropdownClick = (key: string) => {
if (key === 'logout') {
handleLogout()
handleLogout();
}
}
};
const props = withDefaults(defineProps<{
isCollapsed?: boolean
}>(), {
isCollapsed: false
})
const props = withDefaults(
defineProps<{
isCollapsed?: boolean;
}>(),
{
isCollapsed: false,
}
);
const options = [
// {
@@ -49,5 +52,5 @@ const options = [
key: 'logout',
icon: () => icon('ri:logout-box-r-line'),
},
]
];
</script>
+24 -26
View File
@@ -8,19 +8,16 @@
class="max-w-screen-md"
:segmented="{
content: true,
action: true
}">
action: true,
}"
>
<n-card>
<n-tabs type="card">
<n-tab-pane name="users" :tab="t('Members')">
<users-group-members
:group="group"
@search="getGroupData(group.name)"></users-group-members>
<users-group-members :group="group" @search="getGroupData(group.name)"></users-group-members>
</n-tab-pane>
<n-tab-pane name="policy" :tab="t('Policies')">
<users-group-policies
:group="group"
@search="getGroupData(group.name)"></users-group-policies>
<users-group-policies :group="group" @search="getGroupData(group.name)"></users-group-policies>
</n-tab-pane>
<template #suffix>
{{ t('Status') }}
@@ -29,7 +26,8 @@
checked-value="enabled"
unchecked-value="disabled"
v-model:value="group.status"
:on-update:value="handerGroupStatusChange"></n-switch>
:on-update:value="handerGroupStatusChange"
></n-switch>
</template>
</n-tabs>
</n-card>
@@ -38,42 +36,42 @@
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const visible = ref(false)
const { getGroup, updateGroupStatus } = useGroups()
const { t } = useI18n();
const visible = ref(false);
const { getGroup, updateGroupStatus } = useGroups();
interface GroupInfo {
name: string
members: string[]
status: string
name: string;
members: string[];
status: string;
}
const group = ref<GroupInfo>({
name: '',
members: [],
status: 'enabled'
})
status: 'enabled',
});
// 用户组的状态发生变化
const handerGroupStatusChange = async (val: string) => {
await updateGroupStatus(group.value.name, { ...group.value, status: val })
await getGroupData(group.value.name)
}
await updateGroupStatus(group.value.name, { ...group.value, status: val });
await getGroupData(group.value.name);
};
async function openDialog(row: any) {
getGroupData(row.name)
visible.value = true
getGroupData(row.name);
visible.value = true;
}
// 获取用户信息
async function getGroupData(name: string) {
group.value = await getGroup(name)
group.value = await getGroup(name);
}
defineExpose({
openDialog
})
openDialog,
});
</script>
<style lang="scss" scoped></style>
@@ -41,13 +41,7 @@
</template>
<script setup lang="ts">
import {
type DataTableColumns,
type DataTableInst,
type DataTableRowKey,
NButton,
NSpace,
} from 'naive-ui';
import { type DataTableColumns, type DataTableInst, type DataTableRowKey, NButton, NSpace } from 'naive-ui';
const { listPolicies, setUserOrGroupPolicy } = usePolicies();
const { t } = useI18n();
+77 -86
View File
@@ -1,11 +1,6 @@
<template>
<div>
<n-form
class="mb-4 mt-2"
ref="formRef"
:model="searchForm"
label-placement="left"
:show-feedback="false">
<n-form class="mb-4 mt-2" ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false">
<n-flex justify="space-between">
<n-form-item label="" path="name">
<n-input :placeholder="t('Search User Group')" @input="filterName" />
@@ -18,10 +13,7 @@
</template>
删除选中项
</NButton> -->
<NButton
:disabled="!checkedKeys.length"
secondary
@click="allocationPolicy">
<NButton :disabled="!checkedKeys.length" secondary @click="allocationPolicy">
<template #icon>
<Icon name="ri:group-2-fill"></Icon>
</template>
@@ -45,16 +37,15 @@
:bordered="true"
max-height="calc(100vh - 320px)"
:row-key="rowKey"
@update:checked-row-keys="handleCheck" />
@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-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>
ref="policiesRef"
></users-group-set-policies-mutiple>
</div>
</template>
@@ -65,38 +56,38 @@ import {
type DataTableRowKey,
NButton,
NPopconfirm,
NSpace
} from 'naive-ui'
import { Icon } from '#components'
import { useI18n } from 'vue-i18n'
NSpace,
} from 'naive-ui';
import { Icon } from '#components';
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: ''
})
name: '',
});
interface RowData {
name: string
name: string;
}
const columns: DataTableColumns<RowData> = [
{
type: 'selection'
type: 'selection',
},
{
title: t('Name'),
align: 'left',
key: 'name',
filter(value, row) {
return !!row.name.includes(value.toString())
}
return !!row.name.includes(value.toString());
},
},
{
title: t('Actions'),
@@ -107,7 +98,7 @@ const columns: DataTableColumns<RowData> = [
return h(
NSpace,
{
justify: 'center'
justify: 'center',
},
{
default: () => [
@@ -116,11 +107,11 @@ const columns: DataTableColumns<RowData> = [
{
size: 'small',
secondary: true,
onClick: () => openEditItem(row)
onClick: () => openEditItem(row),
},
{
default: () => '',
icon: () => h(Icon, { name: 'ri:edit-2-line' })
icon: () => h(Icon, { name: 'ri:edit-2-line' }),
}
),
h(
@@ -134,102 +125,102 @@ const columns: DataTableColumns<RowData> = [
{ size: 'small', secondary: true },
{
default: () => '',
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' })
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
}
)
),
}
)
]
),
],
}
)
}
}
]
);
},
},
];
// 搜索过滤
const tableRef = ref<DataTableInst>()
const tableRef = ref<DataTableInst>();
function filterName(value: string) {
tableRef.value &&
tableRef.value.filter({
name: [value]
})
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 = []
name: item,
};
}) || [];
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)
if(info.members.length) {
messge.error("请先清空组成员")
return
const info = await group.getGroup(row.name);
if (info.members.length) {
messge.error('请先清空组成员');
return;
}
// 清空组的成员
await group.updateGroupMembers({
group: row.name,
members: info.members,
isRemove: true,
groupStatus: 'enabled'
})
message.success(t('Delete Success'))
getDataList()
groupStatus: 'enabled',
});
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({
@@ -239,16 +230,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>
+67 -59
View File
@@ -36,66 +36,74 @@
:pagination="false"
:bordered="true"
:row-key="rowKey"
@update:checked-row-keys="handleCheck" />
@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 { type DataTableColumns, type DataTableInst, type DataTableRowKey, NButton, NPopconfirm, NSpace } from "naive-ui"
import { Icon } from "#components"
import { useI18n } from 'vue-i18n'
import {
type DataTableColumns,
type DataTableInst,
type DataTableRowKey,
NButton,
NPopconfirm,
NSpace,
} from 'naive-ui';
import { Icon } from '#components';
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: "",
})
accessKey: '',
});
interface RowData {
accessKey: string
accessKey: string;
}
const columns: DataTableColumns<RowData> = [
{
type: "selection",
type: 'selection',
},
{
title: t('Name'),
align: "left",
key: "accessKey",
align: 'left',
key: 'accessKey',
filter(value, row) {
return !!row.accessKey.includes(value.toString())
return !!row.accessKey.includes(value.toString());
},
},
{
title: t('Actions'),
key: "actions",
align: "center",
key: 'actions',
align: 'center',
width: 180,
render: (row: any) => {
return h(
NSpace,
{
justify: "center",
justify: 'center',
},
{
default: () => [
h(
NButton,
{
size: "small",
size: 'small',
secondary: true,
onClick: () => openEditItem(row),
},
{
default: () => "",
icon: () => h(Icon, { name: "ri:edit-2-line" }),
default: () => '',
icon: () => h(Icon, { name: 'ri:edit-2-line' }),
}
),
h(
@@ -106,84 +114,84 @@ const columns: DataTableColumns<RowData> = [
trigger: () =>
h(
NButton,
{ size: "small", secondary: true },
{ size: 'small', secondary: true },
{
default: () => "",
icon: () => h(Icon, { name: "ri:delete-bin-5-line" }),
default: () => '',
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
}
),
}
),
],
}
)
);
},
},
]
];
// 搜索过滤
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 : {}), // 展开用户信息
}))
...(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({
@@ -193,22 +201,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>
+3 -12
View File
@@ -14,16 +14,10 @@
<n-card>
<n-tabs type="card">
<n-tab-pane name="groups" :tab="t('Groups')">
<users-user-groups
:user="user"
@search="getUserData(user.accessKey)"
></users-user-groups>
<users-user-groups :user="user" @search="getUserData(user.accessKey)"></users-user-groups>
</n-tab-pane>
<n-tab-pane name="policy" :tab="t('Policies')">
<users-user-policies
:user="user"
@search="getUserData(user.accessKey)"
></users-user-policies>
<users-user-policies :user="user" @search="getUserData(user.accessKey)"></users-user-policies>
</n-tab-pane>
<n-tab-pane name="accesskey" :tab="t('Account')">
<users-user-account
@@ -43,10 +37,7 @@
></n-switch>
</template>
</n-tabs>
<users-user-notice
ref="noticeRef"
@search="getUserData(user.accessKey)"
></users-user-notice>
<users-user-notice ref="noticeRef" @search="getUserData(user.accessKey)"></users-user-notice>
</n-card>
</n-modal>
</div>
+5 -24
View File
@@ -1,13 +1,7 @@
<template>
<div>
<n-card>
<n-form
ref="formRef"
:model="searchForm"
label-placement="left"
:show-feedback="false"
v-if="!editStatus"
>
<n-form ref="formRef" :model="searchForm" label-placement="left" :show-feedback="false" v-if="!editStatus">
<n-flex justify="space-between">
<n-form-item class="!w-64" label="" path="name">
<n-input :placeholder="t('Search Account')" @input="filterName" />
@@ -45,11 +39,7 @@
<n-input v-model:value="formModel.accessKey" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('Secret Key')" path="secretKey">
<n-input
v-model:value="formModel.secretKey"
show-password-on="mousedown"
type="password"
/>
<n-input v-model:value="formModel.secretKey" show-password-on="mousedown" type="password" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('Expiration')" path="expiry">
<n-date-picker
@@ -73,12 +63,7 @@
<n-form-item-gi :span="24" :label="t('Use main account policy')" path="impliedPolicy">
<n-switch v-model:value="formModel.impliedPolicy" />
</n-form-item-gi>
<n-form-item-gi
v-if="!formModel.impliedPolicy"
:span="24"
:label="t('Current User Policy')"
path="policy"
>
<n-form-item-gi v-if="!formModel.impliedPolicy" :span="24" :label="t('Current User Policy')" path="policy">
<json-editor v-model="formModel.policy" />
</n-form-item-gi>
<!-- <n-form-item-grid-item :span="24" label="状态" v-if="editType == 'edit'" path="accountStatus">
@@ -374,12 +359,8 @@ async function submitForm() {
const res = await createServiceAccount({
...formModel.value,
targetUser: props.user.accessKey,
policy: !formModel.value.impliedPolicy
? JSON.stringify(JSON.parse(formModel.value.policy))
: null,
expiration: formModel.value.expiry
? new Date(formModel.value.expiry).toISOString()
: null,
policy: !formModel.value.impliedPolicy ? JSON.stringify(JSON.parse(formModel.value.policy)) : null,
expiration: formModel.value.expiry ? new Date(formModel.value.expiry).toISOString() : null,
});
message.success(t('Add Success'));
+53 -51
View File
@@ -7,7 +7,8 @@
:model="formModel"
:rules="rules"
label-align="right"
:label-width="130">
:label-width="130"
>
<n-grid :cols="24" :x-gap="18">
<n-form-item-grid-item :span="24" :label="t('Access Key')" path="accessKey">
<n-input v-model:value="formModel.accessKey" :disabled="true" />
@@ -22,7 +23,8 @@
:is-date-disabled="dateDisabled"
value-format="yyyy-MM-ddTkk:mm:SSS"
type="datetime"
clearable />
clearable
/>
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('Name')" path="name">
<n-input v-model:value="formModel.name" />
@@ -56,55 +58,55 @@
</template>
<script setup lang="ts">
import { type FormItemRule, type FormInst, NButton, NSpace } from "naive-ui"
import { useI18n } from 'vue-i18n'
import { type FormItemRule, type FormInst, NButton, NSpace } from 'naive-ui';
import { useI18n } from 'vue-i18n';
const { t } = useI18n()
const { updateServiceAccount } = useAccessKeys()
const { $api } = useNuxtApp()
const { getPolicyByUserName } = usePolicies()
const { t } = useI18n();
const { updateServiceAccount } = useAccessKeys();
const { $api } = useNuxtApp();
const { getPolicyByUserName } = usePolicies();
// 验证
const rules = ref({
accessKey: {
required: true,
trigger: ["blur", "input"],
trigger: ['blur', 'input'],
message: t('Please enter Access Key'),
},
secretKey: {
required: true,
trigger: ["blur", "input"],
trigger: ['blur', 'input'],
message: t('Please enter Secret Key'),
},
expiry: {
required: true,
trigger: ["blur", "change"],
trigger: ['blur', 'change'],
validator(rule: FormItemRule, value: string) {
if (!value) {
return new Error(t('Please select expiration date'))
return new Error(t('Please select expiration date'));
}
return true
return true;
},
},
})
});
const message = useMessage()
const message = useMessage();
const props = defineProps({
user: {
type: Object,
required: true,
},
})
});
const formModel = ref({
accessKey: "",
name: "",
description: "",
accessKey: '',
name: '',
description: '',
impliedPolicy: true,
expiry: null,
policy: "",
accountStatus: "on",
})
policy: '',
accountStatus: 'on',
});
watch(
() => props.user,
@@ -117,42 +119,42 @@ watch(
expiry: props.user.expiration,
policy: props.user.policy,
accountStatus: props.user.accountStatus,
}
};
// 如果没有默认策略,则获取默认策略
if (props.user.impliedPolicy) getPolicie()
if (props.user.impliedPolicy) getPolicie();
},
{
deep: true,
}
)
);
const parentPolicy = ref("")
const parentPolicy = ref('');
// 默认策略原文
const getPolicie = async () => {
parentPolicy.value = JSON.stringify(await getPolicyByUserName(props.user.parentUser))
formModel.value.policy = parentPolicy.value
}
parentPolicy.value = JSON.stringify(await getPolicyByUserName(props.user.parentUser));
formModel.value.policy = parentPolicy.value;
};
function cancelEdit() {
formModel.value = {
accessKey: "",
name: "",
description: "",
accessKey: '',
name: '',
description: '',
impliedPolicy: true,
expiry: null,
policy: "",
accountStatus: "on",
}
emit("search")
policy: '',
accountStatus: 'on',
};
emit('search');
}
interface Emits {
(e: "search"): void
(e: 'search'): void;
}
const emit = defineEmits<Emits>()
const subFormRef = ref<FormInst | null>(null)
const emit = defineEmits<Emits>();
const subFormRef = ref<FormInst | null>(null);
async function submitForm() {
subFormRef.value?.validate(async (errors) => {
subFormRef.value?.validate(async errors => {
if (!errors) {
try {
const res = await updateServiceAccount(props.user.accessKey, {
@@ -161,24 +163,24 @@ async function submitForm() {
newDescription: formModel.value.description,
newPolicy: !formModel.value.impliedPolicy ? JSON.stringify(JSON.parse(formModel.value.policy)) : null,
newExpiration: formModel.value.expiry ? new Date(formModel.value.expiry).toISOString() : null,
})
});
message.success(t('Update Success'))
emit("search")
cancelEdit()
message.success(t('Update Success'));
emit('search');
cancelEdit();
} catch (error) {
console.log(error)
message.error(t('Update Failed'))
console.log(error);
message.error(t('Update Failed'));
}
} else {
console.log(errors)
message.error(t('Please fill in the correct format'))
console.log(errors);
message.error(t('Please fill in the correct format'));
}
})
});
}
function dateDisabled(ts: number) {
const date = new Date(ts)
return date < new Date()
const date = new Date(ts);
return date < new Date();
}
</script>
+1 -6
View File
@@ -32,12 +32,7 @@
</n-form-item-grid-item>
<n-form-item-grid-item :span="24" :label="t('Policy')" path="policies">
<n-select
v-model:value="editForm.policies"
filterable
multiple
:options="policiesList"
/>
<n-select v-model:value="editForm.policies" filterable multiple :options="policiesList" />
</n-form-item-grid-item>
</n-grid>
</n-form>
+2 -12
View File
@@ -15,20 +15,10 @@
<n-form label-placement="left" label-align="right" :label-width="130">
<n-grid :cols="24" :x-gap="18">
<n-form-item-grid-item :span="24" label="Access Key">
<copy-input
class="w-full"
v-model="accessKey"
:readonly="true"
:copy-icon="true"
></copy-input>
<copy-input class="w-full" v-model="accessKey" :readonly="true" :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-input class="w-full" v-model="secretkey" :readonly="true" :copy-icon="true"></copy-input>
</n-form-item-grid-item>
</n-grid>
</n-form>