refactor: migrate access keys ui to shadcn components

This commit is contained in:
overtrue
2025-10-26 00:05:21 +08:00
parent 65d4c931f9
commit 81ca5a1590
6 changed files with 668 additions and 589 deletions
+135 -143
View File
@@ -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>
+106 -97
View File
@@ -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
View File
@@ -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>
+78
View File
@@ -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>
+1
View File
@@ -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
View File
@@ -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'))
}
},