mirror of
https://github.com/rustfs/console.git
synced 2026-09-24 16:30:56 +08:00
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> </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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user