mirror of
https://github.com/rustfs/console.git
synced 2026-08-28 19:47:21 +08:00
refactor: migrate access keys ui to shadcn components
This commit is contained in:
@@ -1,167 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormItemRule } from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AppButton, AppCard, AppInput, AppModal } from '@/components/app'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
}
|
||||
const { visible } = defineProps<Props>();
|
||||
const { visible } = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const defaultFormModal = {
|
||||
const emit = defineEmits<Emits>()
|
||||
const formModel = reactive({
|
||||
current_secret_key: '',
|
||||
new_secret_key: '',
|
||||
re_new_secret_key: '',
|
||||
};
|
||||
const formModel = ref({ ...defaultFormModal });
|
||||
// 验证规则
|
||||
const rules = {
|
||||
current_secret_key: [
|
||||
{
|
||||
required: true,
|
||||
message: t('Please enter current password'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
new_secret_key: [
|
||||
{
|
||||
required: true,
|
||||
message: t('Please enter new password'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
re_new_secret_key: [
|
||||
{
|
||||
required: true,
|
||||
message: t('Please enter new password again'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
validator: validatePasswordSame,
|
||||
message: t('The two passwords are inconsistent'),
|
||||
trigger: ['blur', 'password-input'],
|
||||
},
|
||||
],
|
||||
};
|
||||
// 再次输入密码的时候验证两次输入的密码是否一致
|
||||
function validatePasswordSame(rule: FormItemRule, value: string): boolean {
|
||||
return value === formModel.value.new_secret_key;
|
||||
}
|
||||
})
|
||||
|
||||
// 输入密码时候验证与下发已经输入的重复密码是否一致
|
||||
const rPasswordFormItemRef = ref();
|
||||
function handlePasswordInput() {
|
||||
if (formModel.value.re_new_secret_key) {
|
||||
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' });
|
||||
}
|
||||
}
|
||||
const errors = reactive({
|
||||
current_secret_key: '',
|
||||
new_secret_key: '',
|
||||
re_new_secret_key: '',
|
||||
})
|
||||
|
||||
// 提交确认
|
||||
const formRef = ref();
|
||||
const { $api } = useNuxtApp();
|
||||
const message = useMessage();
|
||||
function submitForm(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
formRef.value?.validate(async (errors: any) => {
|
||||
if (errors) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await $api.post('/account/change-password', {
|
||||
current_secret_key: formModel.value.current_secret_key,
|
||||
new_secret_key: formModel.value.new_secret_key,
|
||||
});
|
||||
message.success(t('Updated successfully'));
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
message.error(t('Update failed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}
|
||||
const submitting = ref(false)
|
||||
const message = useMessage()
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const modalVisible = computed({
|
||||
get() {
|
||||
return visible;
|
||||
return visible
|
||||
},
|
||||
set(visible) {
|
||||
closeModal(visible);
|
||||
set(value) {
|
||||
closeModal(value)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
formModel.current_secret_key = ''
|
||||
formModel.new_secret_key = ''
|
||||
formModel.re_new_secret_key = ''
|
||||
Object.keys(errors).forEach(key => {
|
||||
errors[key as keyof typeof errors] = ''
|
||||
})
|
||||
}
|
||||
|
||||
function closeModal(visible = false) {
|
||||
emit('update:visible', visible);
|
||||
emit('update:visible', visible)
|
||||
if (!visible) {
|
||||
clearForm()
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function validate() {
|
||||
errors.current_secret_key = formModel.current_secret_key ? '' : t('Please enter current password')
|
||||
errors.new_secret_key = formModel.new_secret_key ? '' : t('Please enter new password')
|
||||
|
||||
if (!formModel.re_new_secret_key) {
|
||||
errors.re_new_secret_key = t('Please enter new password again')
|
||||
} else if (formModel.re_new_secret_key !== formModel.new_secret_key) {
|
||||
errors.re_new_secret_key = t('The two passwords are inconsistent')
|
||||
} else {
|
||||
errors.re_new_secret_key = ''
|
||||
}
|
||||
|
||||
return !errors.current_secret_key && !errors.new_secret_key && !errors.re_new_secret_key
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!validate()) {
|
||||
message.error(t('Please fill in the correct format'))
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await $api.post('/account/change-password', {
|
||||
current_secret_key: formModel.current_secret_key,
|
||||
new_secret_key: formModel.new_secret_key,
|
||||
})
|
||||
message.success(t('Updated successfully'))
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error(t('Update failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="modalVisible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
<AppModal
|
||||
v-model="modalVisible"
|
||||
:title="t('Change current account password')"
|
||||
class="max-w-screen-md"
|
||||
sizer="huge"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
size="md"
|
||||
:close-on-backdrop="false"
|
||||
>
|
||||
<n-card>
|
||||
<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-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>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</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-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="password-current">{{ t('Current Password') }}</Label>
|
||||
<AppInput
|
||||
id="password-current"
|
||||
v-model="formModel.current_secret_key"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p v-if="errors.current_secret_key" class="text-sm text-destructive">
|
||||
{{ errors.current_secret_key }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
.n-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password-new">{{ t('New Password') }}</Label>
|
||||
<AppInput
|
||||
id="password-new"
|
||||
v-model="formModel.new_secret_key"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p v-if="errors.new_secret_key" class="text-sm text-destructive">
|
||||
{{ errors.new_secret_key }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password-new-confirm">{{ t('Confirm New Password') }}</Label>
|
||||
<AppInput
|
||||
id="password-new-confirm"
|
||||
v-model="formModel.re_new_secret_key"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:disabled="!formModel.new_secret_key"
|
||||
/>
|
||||
<p v-if="errors.re_new_secret_key" class="text-sm text-destructive">
|
||||
{{ errors.re_new_secret_key }}
|
||||
</p>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
@@ -1,130 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import AppDateTimePicker from '@/components/app/AppDateTimePicker.vue'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, reactive, 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 visible = ref(false);
|
||||
const visible = ref(false)
|
||||
|
||||
const defaultFormModal = {
|
||||
const formModel = reactive({
|
||||
accesskey: '',
|
||||
secretkey: '',
|
||||
policy: '',
|
||||
expiry: null as string | null,
|
||||
name: '',
|
||||
description: '',
|
||||
expiry: null,
|
||||
policy: '',
|
||||
status: 'on',
|
||||
};
|
||||
const formModel = ref({ ...defaultFormModal });
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const accessKey = ref('')
|
||||
|
||||
const statusBoolean = computed({
|
||||
get: () => formModel.status === 'on',
|
||||
set: value => {
|
||||
formModel.status = value ? 'on' : 'off'
|
||||
},
|
||||
})
|
||||
|
||||
const minExpiry = computed(() => dayjs().toISOString())
|
||||
|
||||
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 userInfo = await $api.get(`/accountinfo`)
|
||||
// formModel.value.policy = userInfo.Policy
|
||||
visible.value = true;
|
||||
const res = await getServiceAccount(row.accessKey)
|
||||
formModel.accesskey = row.accessKey
|
||||
const policyValue = typeof res.policy === 'string' ? res.policy : JSON.stringify(res.policy ?? {})
|
||||
formModel.policy = policyValue
|
||||
formModel.expiry = res.expiration ? dayjs(res.expiration).toISOString() : null
|
||||
formModel.name = res.name ?? ''
|
||||
formModel.description = res.description ?? ''
|
||||
formModel.status = res.accountStatus ?? 'on'
|
||||
visible.value = true
|
||||
} catch (error) {
|
||||
message.error(t('Failed to get data'));
|
||||
console.error(error)
|
||||
message.error(t('Failed to get data'))
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openDialog });
|
||||
defineExpose({ openDialog })
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
(e: 'search'): void
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function dateDisabled(ts: number) {
|
||||
const date = new Date(ts);
|
||||
return date < new Date();
|
||||
visible.value = false
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!accessKey.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await updateServiceAccount(accessKey.value, {
|
||||
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');
|
||||
await updateServiceAccount(accessKey.value, {
|
||||
newPolicy: formModel.policy || '{}',
|
||||
newStatus: formModel.status,
|
||||
newName: formModel.name,
|
||||
newDescription: formModel.description,
|
||||
newExpiration: formModel.expiry ? dayjs(formModel.expiry).toISOString() : undefined,
|
||||
})
|
||||
message.success(t('Updated successfully'))
|
||||
closeModal()
|
||||
emit('search')
|
||||
} catch (error) {
|
||||
message.error(t('Update failed'));
|
||||
console.error(error)
|
||||
message.error(t('Update failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="visible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
<AppModal
|
||||
v-model="visible"
|
||||
:title="t('Edit Key')"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
size="lg"
|
||||
:close-on-backdrop="false"
|
||||
>
|
||||
<n-card>
|
||||
<n-form label-placement="left" :model="formModel" label-align="right" :label-width="90">
|
||||
<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 />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" :label="t('Policy')" path="policy">
|
||||
<json-editor v-model="formModel.policy" />
|
||||
</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-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" :label="t('Name')" path="name">
|
||||
<n-input v-model:value="formModel.name" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" :label="t('Description')" path="description">
|
||||
<n-input v-model:value="formModel.description" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="24" :label="t('Status')" path="status">
|
||||
<n-switch v-model:value="formModel.status" checked-value="on" unchecked-value="off" />
|
||||
</n-form-item-grid-item>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</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-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Access Key') }}</Label>
|
||||
<AppInput v-model="formModel.accesskey" disabled />
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
.n-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Policy') }}</Label>
|
||||
<json-editor v-model="formModel.policy" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Expiry') }}</Label>
|
||||
<AppDateTimePicker v-model="formModel.expiry" :min="minExpiry" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Name') }}</Label>
|
||||
<AppInput v-model="formModel.name" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>{{ t('Description') }}</Label>
|
||||
<AppTextarea v-model="formModel.description" :rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-md border border-border/60 p-3">
|
||||
<span class="text-sm font-medium">{{ t('Status') }}</span>
|
||||
<AppSwitch v-model="statusBoolean" />
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
+178
-162
@@ -1,202 +1,218 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="modalVisible"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
<AppModal
|
||||
v-model="modalVisible"
|
||||
:title="t('Create Key')"
|
||||
class="max-w-screen-md"
|
||||
:segmented="{
|
||||
content: true,
|
||||
action: true,
|
||||
}"
|
||||
size="lg"
|
||||
:close-on-backdrop="false"
|
||||
>
|
||||
<n-card>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
label-placement="left"
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
label-align="center"
|
||||
: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" />
|
||||
</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-form-item-gi>
|
||||
<AppCard padded class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-access-key">{{ t('Access Key') }}</Label>
|
||||
<AppInput id="create-access-key" v-model="formModel.accessKey" autocomplete="off" />
|
||||
<p v-if="errors.accessKey" class="text-sm text-destructive">{{ errors.accessKey }}</p>
|
||||
</div>
|
||||
|
||||
<!-- <n-form-item-gi :span="24" label="策略" path="policy">
|
||||
<n-select v-model:value="formModel.policy" filterable multiple :options="polices" />
|
||||
</n-form-item-gi> -->
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-secret-key">{{ t('Secret Key') }}</Label>
|
||||
<AppInput id="create-secret-key" v-model="formModel.secretKey" type="password" autocomplete="off" />
|
||||
<p v-if="errors.secretKey" class="text-sm text-destructive">{{ errors.secretKey }}</p>
|
||||
</div>
|
||||
|
||||
<n-form-item-gi :span="24" :label="t('Expiry')" path="expiry">
|
||||
<n-date-picker
|
||||
class="!w-full"
|
||||
v-model:value="formModel.expiry"
|
||||
:is-date-disabled="dateDisabled"
|
||||
type="datetime"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="24" :label="t('Name')" path="name">
|
||||
<n-input v-model:value="formModel.name" />
|
||||
</n-form-item-gi>
|
||||
<!-- <n-form-item-gi :span="24" label="描述" path="comment">
|
||||
<n-input v-model:value="formModel.comment" />
|
||||
</n-form-item-gi> -->
|
||||
<n-form-item-gi :span="24" :label="t('Description')" path="description">
|
||||
<n-input v-model:value="formModel.description" />
|
||||
</n-form-item-gi>
|
||||
<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">
|
||||
<json-editor v-model="formModel.policy" />
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</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-space>
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-expiry">{{ t('Expiry') }}</Label>
|
||||
<AppDateTimePicker
|
||||
id="create-expiry"
|
||||
v-model="formModel.expiry"
|
||||
:min="minExpiry"
|
||||
:placeholder="t('Please select expiry date')"
|
||||
/>
|
||||
<p v-if="errors.expiry" class="text-sm text-destructive">{{ errors.expiry }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-name">{{ t('Name') }}</Label>
|
||||
<AppInput id="create-name" v-model="formModel.name" autocomplete="off" />
|
||||
<p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="create-description">{{ t('Description') }}</Label>
|
||||
<AppTextarea id="create-description" v-model="formModel.description" :rows="3" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-start justify-between gap-3 rounded-md border border-border/60 p-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium">{{ t('Use main account policy') }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t('Automatically inherit the main account policy when enabled.') }}
|
||||
</p>
|
||||
</div>
|
||||
<AppSwitch v-model="formModel.impliedPolicy" />
|
||||
</div>
|
||||
|
||||
<div v-if="!formModel.impliedPolicy" class="grid gap-2">
|
||||
<Label>{{ t('Current user policy') }}</Label>
|
||||
<json-editor v-model="formModel.policy" />
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="outline" @click="closeModal()">
|
||||
{{ t('Cancel') }}
|
||||
</AppButton>
|
||||
<AppButton variant="primary" :loading="submitting" @click="submitForm">
|
||||
{{ t('Submit') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</AppModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInst, FormItemRule } from 'naive-ui';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { makeRandomString } from '~/utils/functions';
|
||||
import { AppButton, AppCard, AppInput, AppModal, AppSwitch, AppTextarea } from '@/components/app'
|
||||
import AppDateTimePicker from '@/components/app/AppDateTimePicker.vue'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { makeRandomString } from '~/utils/functions'
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
visible: boolean
|
||||
}
|
||||
const { visible } = defineProps<Props>();
|
||||
const message = useMessage();
|
||||
const { $api } = useNuxtApp();
|
||||
const { createServiceAccount } = useAccessKeys();
|
||||
const { visible } = defineProps<Props>()
|
||||
const message = useMessage()
|
||||
const { $api } = useNuxtApp()
|
||||
const { createServiceAccount } = useAccessKeys()
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const defaultFormModal = {
|
||||
const emit = defineEmits<Emits>()
|
||||
const defaultFormModal = () => ({
|
||||
accessKey: makeRandomString(20),
|
||||
secretKey: makeRandomString(40),
|
||||
name: '',
|
||||
description: '',
|
||||
// comment: "",
|
||||
expiry: null,
|
||||
expiry: null as string | null,
|
||||
policy: '',
|
||||
impliedPolicy: true,
|
||||
};
|
||||
const formModel = ref({ ...defaultFormModal });
|
||||
})
|
||||
const formModel = reactive(defaultFormModal())
|
||||
|
||||
// 验证
|
||||
const rules = ref({
|
||||
accessKey: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error(t('Please enter Access Key'));
|
||||
}
|
||||
if (value.length < 3 || value.length > 20) {
|
||||
return new Error(t('Access Key length must be between 3 and 20 characters'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
secretKey: {
|
||||
required: true,
|
||||
trigger: ['blur', 'input'],
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error(t('Please enter Secret Key'));
|
||||
}
|
||||
if (value.length < 8 || value.length > 40) {
|
||||
return new Error(t('Secret Key length must be between 8 and 40 characters'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
expiry: {
|
||||
required: true,
|
||||
trigger: ['blur', 'change'],
|
||||
// message: "请选择有效期",
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
if (!value) {
|
||||
return new Error(t('Please select expiry date'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
const errors = reactive({
|
||||
accessKey: '',
|
||||
secretKey: '',
|
||||
expiry: '',
|
||||
name: '',
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
(e: 'search'): void;
|
||||
(e: 'notice', data: object): void;
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'search'): void
|
||||
(e: 'notice', data: object): void
|
||||
}
|
||||
|
||||
const modalVisible = computed({
|
||||
get() {
|
||||
return visible;
|
||||
return visible
|
||||
},
|
||||
set(visible) {
|
||||
closeModal(visible);
|
||||
closeModal(visible)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const minExpiry = computed(() => new Date().toISOString())
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formModel, defaultFormModal())
|
||||
formModel.policy = JSON.stringify(parentPolicy.value)
|
||||
clearErrors()
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
errors.accessKey = ''
|
||||
errors.secretKey = ''
|
||||
errors.expiry = ''
|
||||
errors.name = ''
|
||||
}
|
||||
|
||||
function closeModal(visible = false) {
|
||||
emit('update:visible', visible);
|
||||
formModel.value = {
|
||||
...defaultFormModal,
|
||||
accessKey: makeRandomString(20),
|
||||
secretKey: makeRandomString(40),
|
||||
policy: JSON.stringify(parentPolicy.value),
|
||||
};
|
||||
emit('update:visible', visible)
|
||||
if (!visible) {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
function dateDisabled(ts: number) {
|
||||
const date = new Date(ts);
|
||||
return date < new Date();
|
||||
function validate() {
|
||||
clearErrors()
|
||||
|
||||
if (!formModel.accessKey) {
|
||||
errors.accessKey = t('Please enter Access Key')
|
||||
} else if (formModel.accessKey.length < 3 || formModel.accessKey.length > 20) {
|
||||
errors.accessKey = t('Access Key length must be between 3 and 20 characters')
|
||||
}
|
||||
|
||||
if (!formModel.secretKey) {
|
||||
errors.secretKey = t('Please enter Secret Key')
|
||||
} else if (formModel.secretKey.length < 8 || formModel.secretKey.length > 40) {
|
||||
errors.secretKey = t('Secret Key length must be between 8 and 40 characters')
|
||||
}
|
||||
|
||||
if (!formModel.expiry) {
|
||||
errors.expiry = t('Please select expiry date')
|
||||
}
|
||||
|
||||
if (!formModel.name) {
|
||||
errors.name = t('Please enter name')
|
||||
}
|
||||
|
||||
return !errors.accessKey && !errors.secretKey && !errors.expiry && !errors.name
|
||||
}
|
||||
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
async function submitForm(e: MouseEvent) {
|
||||
// e.preventDefault()
|
||||
formRef.value?.validate(async errors => {
|
||||
if (!errors) {
|
||||
async function submitForm() {
|
||||
if (!validate()) {
|
||||
message.error(t('Please fill in the correct format'))
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
let customPolicy: string | null = null
|
||||
if (!formModel.impliedPolicy) {
|
||||
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,
|
||||
});
|
||||
message.success(t('Added successfully'));
|
||||
emit('notice', res);
|
||||
closeModal();
|
||||
emit('search');
|
||||
customPolicy = JSON.stringify(JSON.parse(formModel.policy || '{}'))
|
||||
} catch (error) {
|
||||
message.error(t('Add failed'));
|
||||
message.error(t('Policy format invalid'))
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
console.log(errors);
|
||||
message.error(t('Please fill in the correct format'));
|
||||
}
|
||||
});
|
||||
|
||||
const payload = {
|
||||
...formModel,
|
||||
policy: customPolicy,
|
||||
expiration: formModel.expiry,
|
||||
}
|
||||
|
||||
const res = await createServiceAccount(payload)
|
||||
message.success(t('Added successfully'))
|
||||
emit('notice', res)
|
||||
closeModal()
|
||||
emit('search')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error(t('Add failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const parentPolicy = ref('');
|
||||
// 默认策略原文
|
||||
const getPolicie = async () => {
|
||||
const userInfo = await $api.get(`/accountinfo`);
|
||||
parentPolicy.value = userInfo.Policy;
|
||||
formModel.value.policy = JSON.stringify(userInfo.Policy);
|
||||
};
|
||||
getPolicie();
|
||||
const parentPolicy = ref('')
|
||||
const getPolicy = async () => {
|
||||
const userInfo = await $api.get(`/accountinfo`)
|
||||
parentPolicy.value = userInfo.Policy
|
||||
formModel.policy = JSON.stringify(userInfo.Policy)
|
||||
}
|
||||
getPolicy()
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string | null
|
||||
placeholder?: string
|
||||
min?: string
|
||||
max?: string
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
name?: string
|
||||
id?: string
|
||||
required?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: null,
|
||||
placeholder: '',
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
disabled: false,
|
||||
class: undefined,
|
||||
name: undefined,
|
||||
id: undefined,
|
||||
required: false,
|
||||
}
|
||||
)
|
||||
|
||||
const modelValue = defineModel<string | null>({ default: null })
|
||||
|
||||
const inputValue = computed({
|
||||
get: () => {
|
||||
const value = modelValue.value ?? props.modelValue
|
||||
if (!value) return ''
|
||||
const date = dayjs(value)
|
||||
if (!date.isValid()) return ''
|
||||
return date.format('YYYY-MM-DDTHH:mm')
|
||||
},
|
||||
set: value => {
|
||||
if (!value) {
|
||||
modelValue.value = null
|
||||
return
|
||||
}
|
||||
const date = dayjs(value)
|
||||
modelValue.value = date.isValid() ? date.toISOString() : null
|
||||
},
|
||||
})
|
||||
|
||||
const minValue = computed(() => {
|
||||
if (!props.min) return undefined
|
||||
const date = dayjs(props.min)
|
||||
return date.isValid() ? date.format('YYYY-MM-DDTHH:mm') : undefined
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
if (!props.max) return undefined
|
||||
const date = dayjs(props.max)
|
||||
return date.isValid() ? date.format('YYYY-MM-DDTHH:mm') : undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="inputValue"
|
||||
type="datetime-local"
|
||||
:id="id"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:class="cn('flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -9,6 +9,7 @@ export { default as AppSwitch } from './AppSwitch.vue'
|
||||
export { default as AppCheckbox } from './AppCheckbox.vue'
|
||||
export { default as AppCheckboxGroup } from './AppCheckboxGroup.vue'
|
||||
export { default as AppRadioGroup } from './AppRadioGroup.vue'
|
||||
export { default as AppDateTimePicker } from './AppDateTimePicker.vue'
|
||||
export { default as AppModal } from './AppModal.vue'
|
||||
export { default as AppDrawer } from './AppDrawer.vue'
|
||||
export { default as AppEmpty } from './AppEmpty.vue'
|
||||
|
||||
+170
-187
@@ -1,267 +1,250 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<page-header>
|
||||
<template #title>
|
||||
<h1 class="text-2xl font-bold">{{ t('Access Keys') }}</h1>
|
||||
</template>
|
||||
<template #actions></template>
|
||||
</page-header>
|
||||
|
||||
<page-content>
|
||||
<n-form class="mb-4" 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 Access Key')" @input="filterName" />
|
||||
</n-form-item>
|
||||
<!-- <n-button @click="() => refresh()">
|
||||
<Icon name="ri:refresh-line" class="mr-2" />
|
||||
<span>刷新</span>
|
||||
</n-button> -->
|
||||
<NFlex>
|
||||
<NButton :disabled="!checkedKeys.length" secondary @click="deleteByList">
|
||||
<template #icon>
|
||||
<Icon name="ri:delete-bin-5-line"></Icon>
|
||||
</template>
|
||||
{{ t('Delete Selected') }}
|
||||
</NButton>
|
||||
<!-- <NButton secondary @click="changePassword">
|
||||
<template #icon>
|
||||
<Icon name="ri:key-2-line"></Icon>
|
||||
</template>
|
||||
修改秘钥
|
||||
</NButton> -->
|
||||
<NButton secondary @click="addItem">
|
||||
<template #icon>
|
||||
<Icon name="ri:add-line"></Icon>
|
||||
</template>
|
||||
{{ t('Add Access Key') }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</n-flex>
|
||||
</n-form>
|
||||
<page-content class="flex flex-col gap-4">
|
||||
<div class="rounded-lg border border-border/60 bg-background/80 p-4 shadow-sm">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex w-full max-w-sm items-center gap-2">
|
||||
<Icon name="ri:search-line" class="size-4 text-muted-foreground" />
|
||||
<AppInput
|
||||
v-model="searchTerm"
|
||||
:placeholder="t('Search Access Key')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<AppButton variant="outline" @click="changePasswordVisible = true">
|
||||
<Icon name="ri:key-2-line" class="size-4" />
|
||||
<span>{{ t('Change Password') }}</span>
|
||||
</AppButton>
|
||||
<AppButton
|
||||
variant="outline"
|
||||
:disabled="!selectedKeys.length"
|
||||
@click="deleteSelected"
|
||||
>
|
||||
<Icon name="ri:delete-bin-5-line" class="size-4" />
|
||||
<span>{{ t('Delete Selected') }}</span>
|
||||
</AppButton>
|
||||
<AppButton variant="secondary" @click="addItem">
|
||||
<Icon name="ri:add-line" class="size-4" />
|
||||
<span>{{ t('Add Access Key') }}</span>
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-data-table ref="tableRef" :columns="columns" :data="listData" :pagination="false" :bordered="true" :row-key="rowKey" @update:checked-row-keys="handleCheck" />
|
||||
<div class="rounded-lg border border-border/60 bg-background/60 p-2">
|
||||
<AppDataTable
|
||||
:table="table"
|
||||
:is-loading="loading"
|
||||
:empty-title="t('No Access Keys')"
|
||||
:empty-description="t('Create a new access key to get started.')"
|
||||
class="overflow-hidden"
|
||||
table-class="min-w-full"
|
||||
/>
|
||||
<AppDataTablePagination :table="table" class="px-2 py-3" />
|
||||
</div>
|
||||
</page-content>
|
||||
<NewItem ref="newItemRef" v-model:visible="newItemVisible" @search="getDataList" @notice="noticeDialog" />
|
||||
<EditItem ref="editItemRef" @search="getDataList" />
|
||||
<ChangePassword ref="changePasswordModalRef" v-model:visible="changePasswordVisible" @search="getDataList" />
|
||||
<users-user-notice ref="noticeRef" @search="getDataList"></users-user-notice>
|
||||
|
||||
<NewItem ref="newItemRef" v-model:visible="newItemVisible" @search="refresh" @notice="noticeDialog" />
|
||||
<EditItem ref="editItemRef" @search="refresh" />
|
||||
<ChangePassword ref="changePasswordModalRef" v-model:visible="changePasswordVisible" />
|
||||
<users-user-notice ref="noticeRef" @search="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '#components'
|
||||
import {
|
||||
type DataTableColumns,
|
||||
type DataTableInst,
|
||||
type DataTableRowKey,
|
||||
NButton,
|
||||
NPopconfirm,
|
||||
NSpace,
|
||||
} from 'naive-ui'
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AppButton, AppInput, AppTag } from '@/components/app'
|
||||
import { AppDataTable, AppDataTablePagination, useDataTable } from '@/components/app/data-table'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { ChangePassword, EditItem, NewItem } from '~/components/access-keys'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { $api } = useNuxtApp()
|
||||
const dialog = useDialog()
|
||||
const message = useMessage()
|
||||
const { listUserServiceAccounts, deleteServiceAccount } = useAccessKeys()
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
})
|
||||
interface RowData {
|
||||
accessKey: string
|
||||
expiration: string
|
||||
expiration: string | null
|
||||
name: string
|
||||
description: string
|
||||
accountStatus: string
|
||||
actions: string
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
const data = ref<RowData[]>([])
|
||||
const loading = ref(false)
|
||||
const searchTerm = ref('')
|
||||
|
||||
const openEditItem = (row: RowData) => {
|
||||
editItemRef.value?.openDialog(row)
|
||||
}
|
||||
|
||||
const confirmDeleteSingle = (row: RowData) => {
|
||||
dialog.error({
|
||||
title: t('Warning'),
|
||||
content: t('Are you sure you want to delete this key?'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: () => deleteItem(row.accessKey),
|
||||
})
|
||||
}
|
||||
|
||||
const columns: ColumnDef<RowData>[] = [
|
||||
{
|
||||
type: 'selection',
|
||||
id: 'select',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
header: ({ table }) =>
|
||||
h(Checkbox, {
|
||||
checked: table.getIsAllPageRowsSelected(),
|
||||
indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
|
||||
'onUpdate:checked': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
|
||||
class: 'translate-y-[2px]'
|
||||
}),
|
||||
cell: ({ row }) =>
|
||||
h(Checkbox, {
|
||||
checked: row.getIsSelected(),
|
||||
'onUpdate:checked': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
|
||||
class: 'translate-y-[2px]'
|
||||
}),
|
||||
size: 48,
|
||||
},
|
||||
{
|
||||
title: t('Access Key'),
|
||||
align: 'center',
|
||||
key: 'accessKey',
|
||||
filter(value, row) {
|
||||
return !!row.accessKey.includes(value.toString())
|
||||
},
|
||||
accessorKey: 'accessKey',
|
||||
header: () => t('Access Key'),
|
||||
cell: ({ row }) => h('span', { class: 'font-mono text-sm' }, row.original.accessKey),
|
||||
filterFn: 'includesString',
|
||||
},
|
||||
{
|
||||
title: t('Expiration'),
|
||||
align: 'center',
|
||||
key: 'expiration',
|
||||
accessorKey: 'expiration',
|
||||
header: () => t('Expiration'),
|
||||
cell: ({ row }) =>
|
||||
h('span', row.original.expiration ? dayjs(row.original.expiration).format('YYYY-MM-DD HH:mm') : '-'),
|
||||
},
|
||||
{
|
||||
title: t('Status'),
|
||||
align: 'center',
|
||||
key: 'accountStatus',
|
||||
render: (row: any) => {
|
||||
return row.accountStatus === 'on' ? t('Available') : t('Disabled')
|
||||
},
|
||||
accessorKey: 'accountStatus',
|
||||
header: () => t('Status'),
|
||||
cell: ({ row }) =>
|
||||
h(AppTag, { tone: row.original.accountStatus === 'on' ? 'success' : 'danger' }, () =>
|
||||
row.original.accountStatus === 'on' ? t('Available') : t('Disabled')),
|
||||
},
|
||||
{
|
||||
title: t('Name'),
|
||||
align: 'center',
|
||||
key: 'name',
|
||||
accessorKey: 'name',
|
||||
header: () => t('Name'),
|
||||
cell: ({ row }) => h('span', row.original.name || '-'),
|
||||
},
|
||||
{
|
||||
title: t('Description'),
|
||||
align: 'center',
|
||||
key: 'description',
|
||||
accessorKey: 'description',
|
||||
header: () => t('Description'),
|
||||
cell: ({ row }) => h('span', row.original.description || '-'),
|
||||
},
|
||||
{
|
||||
title: t('Actions'),
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: (row: any) => {
|
||||
return h(
|
||||
NSpace,
|
||||
{
|
||||
justify: 'center',
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
secondary: true,
|
||||
onClick: () => openEditItem(row),
|
||||
},
|
||||
{
|
||||
default: () => t('Edit'),
|
||||
icon: () => h(Icon, { name: 'ri:edit-2-line' }),
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => deleteItem(row) },
|
||||
{
|
||||
default: () => t('Confirm Delete'),
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'small', secondary: true },
|
||||
{
|
||||
default: () => t('Delete'),
|
||||
icon: () => h(Icon, { name: 'ri:delete-bin-5-line' }),
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
},
|
||||
id: 'actions',
|
||||
header: () => t('Actions'),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) =>
|
||||
h('div', { class: 'flex justify-center gap-2' }, [
|
||||
h(AppButton, {
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
onClick: () => openEditItem(row.original),
|
||||
}, () => [h(Icon, { name: 'ri:edit-2-line', class: 'size-4' }), h('span', t('Edit'))]),
|
||||
h(AppButton, {
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
onClick: () => confirmDeleteSingle(row.original),
|
||||
}, () => [h(Icon, { name: 'ri:delete-bin-5-line', class: 'size-4' }), h('span', t('Delete'))])
|
||||
]),
|
||||
},
|
||||
]
|
||||
|
||||
// 搜索过滤
|
||||
const tableRef = ref<DataTableInst>()
|
||||
function filterName(value: string) {
|
||||
tableRef.value &&
|
||||
tableRef.value.filter({
|
||||
accessKey: [value],
|
||||
})
|
||||
}
|
||||
const listData = ref<any[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
const { table } = useDataTable<RowData>({
|
||||
data,
|
||||
columns,
|
||||
getRowId: row => row.accessKey,
|
||||
})
|
||||
// 获取数据
|
||||
const getDataList = async () => {
|
||||
|
||||
watch(searchTerm, value => {
|
||||
table.getColumn('accessKey')?.setFilterValue(value || undefined)
|
||||
})
|
||||
|
||||
const selectedKeys = computed(() => table.getSelectedRowModel().rows.map(row => row.original.accessKey))
|
||||
|
||||
const listUserAccounts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listUserServiceAccounts({})
|
||||
listData.value = res.accounts || []
|
||||
data.value = res.accounts || []
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error(t('Get Data Failed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
onMounted(listUserAccounts)
|
||||
|
||||
const refresh = () => {
|
||||
getDataList()
|
||||
listUserAccounts()
|
||||
}
|
||||
|
||||
/** **********************************添加 */
|
||||
const newItemRef = ref()
|
||||
const newItemVisible = ref(false)
|
||||
const editItemRef = ref()
|
||||
const changePasswordModalRef = ref()
|
||||
const changePasswordVisible = ref(false)
|
||||
const noticeRef = ref()
|
||||
|
||||
function addItem() {
|
||||
newItemVisible.value = true
|
||||
}
|
||||
|
||||
// 添加之后的反馈弹窗
|
||||
const noticeRef = ref()
|
||||
function noticeDialog(data: any) {
|
||||
console.log(data)
|
||||
noticeRef.value.openDialog(data)
|
||||
noticeRef.value?.openDialog(data)
|
||||
}
|
||||
|
||||
/** **********************************修改 */
|
||||
const editItemRef = ref()
|
||||
function openEditItem(row: any) {
|
||||
editItemRef.value.openDialog(row)
|
||||
}
|
||||
/** **********************************修改密码 */
|
||||
const changePasswordModalRef = ref()
|
||||
const changePasswordVisible = ref(false)
|
||||
|
||||
function changePassword() {
|
||||
changePasswordVisible.value = true
|
||||
async function deleteItem(accessKey: string) {
|
||||
try {
|
||||
await deleteServiceAccount(accessKey)
|
||||
message.success(t('Delete Success'))
|
||||
await listUserAccounts()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error(t('Delete Failed'))
|
||||
}
|
||||
}
|
||||
|
||||
/** ***********************************删除 */
|
||||
async function deleteItem(row: any) {
|
||||
deleteServiceAccount(row.accessKey)
|
||||
.then(res => {
|
||||
message.success(t('Delete Success'))
|
||||
getDataList()
|
||||
})
|
||||
.catch(error => {
|
||||
message.error(t('Delete Failed'))
|
||||
})
|
||||
}
|
||||
function deleteSelected() {
|
||||
if (!selectedKeys.value.length) {
|
||||
message.error(t('Please select at least one item'))
|
||||
return
|
||||
}
|
||||
|
||||
/** ************************************批量删除 */
|
||||
function rowKey(row: any): string {
|
||||
return row.accessKey
|
||||
}
|
||||
|
||||
const checkedKeys = ref<DataTableRowKey[]>([])
|
||||
function handleCheck(keys: DataTableRowKey[]) {
|
||||
checkedKeys.value = keys
|
||||
return checkedKeys
|
||||
}
|
||||
// 批量删除
|
||||
function deleteByList() {
|
||||
dialog.error({
|
||||
title: t('Warning'),
|
||||
content: t('Are you sure you want to delete all selected keys?'),
|
||||
positiveText: t('Confirm'),
|
||||
negativeText: t('Cancel'),
|
||||
onPositiveClick: async () => {
|
||||
if (!checkedKeys.value.length) {
|
||||
message.error(t('Please select at least one item'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
Promise.all(checkedKeys.value.map(item => deleteServiceAccount(item as string))).then(() => {
|
||||
message.success(t('Delete Success'))
|
||||
checkedKeys.value = []
|
||||
nextTick(() => {
|
||||
getDataList()
|
||||
})
|
||||
})
|
||||
await Promise.all(selectedKeys.value.map(key => deleteServiceAccount(key)))
|
||||
message.success(t('Delete Success'))
|
||||
table.resetRowSelection()
|
||||
await listUserAccounts()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
message.error(t('Delete Failed'))
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user