基于naive ui重构设置-团队页面及弹窗
This commit is contained in:
parent
8b617d833b
commit
345d3db483
@ -1,88 +1,162 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: $t('Add New Member'),
|
||||
actions: [
|
||||
{
|
||||
label: $t('Invite Member'),
|
||||
variant: 'solid',
|
||||
onClick: inviteMember,
|
||||
},
|
||||
],
|
||||
}"
|
||||
v-model="show"
|
||||
<n-modal
|
||||
v-model:show="showDialog"
|
||||
preset="card"
|
||||
:title="$t('Add New Member')"
|
||||
:style="modalStyle"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
class="invite-member-modal"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="space-y-4">
|
||||
<FormControl :label="$t('Username')" v-model="username" />
|
||||
<div
|
||||
v-if="$resources.roles.data?.length > 0"
|
||||
class="flex items-center space-x-2"
|
||||
>
|
||||
<FormControl
|
||||
class="w-full"
|
||||
type="autocomplete"
|
||||
:label="$t('Select Role')"
|
||||
:options="roleOptions"
|
||||
v-model="selectedRole"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('Add')"
|
||||
icon-left="plus"
|
||||
<template #header>
|
||||
<span class="text-lg font-semibold">{{ $t('Add New Member') }}</span>
|
||||
</template>
|
||||
<n-space vertical :size="20">
|
||||
<n-form-item :label="$t('Username')" :required="true">
|
||||
<n-input
|
||||
v-model:value="username"
|
||||
placeholder=""
|
||||
:size="inputSize"
|
||||
class="w-full"
|
||||
/>
|
||||
</n-form-item>
|
||||
<div v-if="$resources.roles.data?.length > 0" class="space-y-4">
|
||||
<div class="flex items-end gap-2">
|
||||
<n-form-item :label="$t('Select Role')" class="flex-1">
|
||||
<n-select
|
||||
v-model:value="selectedRole"
|
||||
:options="roleOptions"
|
||||
:placeholder="$t('Select a role')"
|
||||
:size="inputSize"
|
||||
class="w-full"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedRole"
|
||||
@click="addRole"
|
||||
class="mt-5"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedRoles.length > 0"
|
||||
class="divide-y rounded border border-gray-300 px-1.5"
|
||||
>
|
||||
<div
|
||||
class="flex w-full items-center space-x-2 py-1.5"
|
||||
v-for="role in selectedRoles"
|
||||
:size="buttonSize"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between px-3 py-2">
|
||||
<div class="text-base text-gray-800">{{ role.label }}</div>
|
||||
</div>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="ghost"
|
||||
icon="x"
|
||||
<template #icon>
|
||||
<n-icon><PlusIcon /></n-icon>
|
||||
</template>
|
||||
{{ $t('Add') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div v-if="selectedRoles.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="role in selectedRoles"
|
||||
:key="role.value"
|
||||
class="flex items-center justify-between p-3 rounded border border-gray-200 bg-gray-50"
|
||||
>
|
||||
<div class="text-base text-gray-800">{{ role.label }}</div>
|
||||
<n-button
|
||||
tertiary
|
||||
circle
|
||||
size="small"
|
||||
@click="removeRole(role.value)"
|
||||
/>
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><XIcon /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-alert
|
||||
v-if="$team.inviteTeamMember.error"
|
||||
type="error"
|
||||
:title="getErrorMessage($team.inviteTeamMember.error)"
|
||||
/>
|
||||
</n-space>
|
||||
<template #action>
|
||||
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
|
||||
<n-button @click="hide" :block="isMobile" :size="buttonSize">
|
||||
{{ $t('Cancel') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="$team.inviteTeamMember.loading"
|
||||
@click="inviteMember"
|
||||
:block="isMobile"
|
||||
:size="buttonSize"
|
||||
>
|
||||
{{ $t('Invite Member') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</Dialog>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
NModal,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSpace,
|
||||
NAlert,
|
||||
NButton,
|
||||
NSelect,
|
||||
NIcon,
|
||||
} from 'naive-ui';
|
||||
import { toast } from 'vue-sonner';
|
||||
import PlusIcon from '~icons/lucide/plus';
|
||||
import XIcon from '~icons/lucide/x';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NModal,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSpace,
|
||||
NAlert,
|
||||
NButton,
|
||||
NSelect,
|
||||
NIcon,
|
||||
PlusIcon,
|
||||
XIcon,
|
||||
},
|
||||
emits: ['success', 'update:modelValue'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
show: true,
|
||||
selectedRoles: [],
|
||||
selectedRole: null,
|
||||
windowWidth: window.innerWidth,
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
roles() {
|
||||
computed: {
|
||||
showDialog: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
},
|
||||
isMobile() {
|
||||
return this.windowWidth <= 768;
|
||||
},
|
||||
modalStyle() {
|
||||
return {
|
||||
type: 'list',
|
||||
pagetype: 'Jcloud Role',
|
||||
fields: ['name', 'title'],
|
||||
initialData: [],
|
||||
auto: true,
|
||||
width: this.isMobile ? '95vw' : '700px',
|
||||
maxWidth: this.isMobile ? '95vw' : '90vw',
|
||||
};
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inputSize() {
|
||||
return this.isMobile ? 'medium' : 'large';
|
||||
},
|
||||
buttonSize() {
|
||||
return this.isMobile ? 'medium' : 'medium';
|
||||
},
|
||||
roleOptions() {
|
||||
if (!this.$resources.roles.data) return [];
|
||||
return this.$resources.roles.data
|
||||
.filter((role) => {
|
||||
return !this.selectedRoles.some(
|
||||
@ -95,11 +169,36 @@ export default {
|
||||
}));
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
getErrorMessage(error) {
|
||||
if (!error) return '';
|
||||
if (typeof error === 'string') return error;
|
||||
if (error.message) return error.message;
|
||||
if (error.messages && Array.isArray(error.messages)) {
|
||||
return error.messages.join('\n');
|
||||
}
|
||||
return String(error);
|
||||
},
|
||||
addRole() {
|
||||
if (this.selectedRole) {
|
||||
this.selectedRoles.push(this.selectedRole);
|
||||
this.selectedRole = null;
|
||||
const role = this.$resources.roles.data.find(r => r.name === this.selectedRole);
|
||||
if (role) {
|
||||
this.selectedRoles.push({
|
||||
label: role.title,
|
||||
value: role.name,
|
||||
});
|
||||
this.selectedRole = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
removeRole(roleToRemove) {
|
||||
@ -115,19 +214,87 @@ export default {
|
||||
if (this.$team.inviteTeamMember.loading) return;
|
||||
|
||||
toast.success(this.$t('Member added to team'), { duration: 2000 });
|
||||
this.show = false;
|
||||
this.$team.inviteTeamMember.submit({
|
||||
username: this.username,
|
||||
roles: this.selectedRoles.map((role) => role.value),
|
||||
});
|
||||
this.username = '';
|
||||
this.selectedRoles = [];
|
||||
this.hide();
|
||||
setTimeout(() => {
|
||||
if (this.$team?.getTeamMembers) {
|
||||
this.$team.getTeamMembers.submit();
|
||||
}
|
||||
this.$emit('success');
|
||||
}, 500);
|
||||
},
|
||||
hide() {
|
||||
this.showDialog = false;
|
||||
this.username = '';
|
||||
this.selectedRoles = [];
|
||||
this.selectedRole = null;
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
roles() {
|
||||
return {
|
||||
type: 'list',
|
||||
pagetype: 'Jcloud Role',
|
||||
fields: ['name', 'title'],
|
||||
initialData: [],
|
||||
auto: true,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.invite-member-modal .n-card) {
|
||||
width: 700px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-input),
|
||||
:deep(.invite-member-modal .n-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-card__action) {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.invite-member-modal .n-card) {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
margin: 20px auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-card-body) {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-card__header) {
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-card__action) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.invite-member-modal .n-button) {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,22 +1,129 @@
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<ObjectList :options="teamMembersListOptions"> </ObjectList>
|
||||
<div class="team-settings-container">
|
||||
<n-card :title="$t('Team Members')" class="settings-card">
|
||||
<ObjectList :options="teamMembersListOptions" />
|
||||
</n-card>
|
||||
|
||||
<!-- 团队设置弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showTeamSettingsDialog"
|
||||
preset="card"
|
||||
:title="$t('Settings')"
|
||||
:style="modalStyle"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
class="team-settings-modal"
|
||||
>
|
||||
<template #header>
|
||||
<span class="text-lg font-semibold">{{ $t('Settings') }}</span>
|
||||
</template>
|
||||
<n-space vertical :size="20">
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex-1">
|
||||
<div class="text-base font-medium text-gray-900 mb-1">
|
||||
{{ $t('Enforce Two-Factor Authentication') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ $t('Require all team members to enable two-factor authentication') }}
|
||||
</div>
|
||||
</div>
|
||||
<n-switch
|
||||
:value="enforce2FA"
|
||||
@update:value="handleEnforce2FAChange"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<!-- 邀请成员弹窗 -->
|
||||
<InviteTeamMemberDialog
|
||||
v-if="showInviteMemberDialog"
|
||||
v-model="showInviteMemberDialog"
|
||||
@success="handleInviteSuccess"
|
||||
/>
|
||||
|
||||
<!-- 移除成员确认弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showRemoveMemberDialog"
|
||||
preset="dialog"
|
||||
:title="$t('Remove Member')"
|
||||
:positive-text="$t('Remove')"
|
||||
:positive-button-props="{ type: 'error' }"
|
||||
:loading="team.removeTeamMember.loading"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
@positive-click="handleRemoveMember"
|
||||
>
|
||||
<div class="py-4">
|
||||
<p class="text-base">
|
||||
{{ $t('Are you sure you want to remove') }} <strong>{{ selectedMember?.full_name || '' }}</strong> {{ $t('from the team?') }}
|
||||
</p>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineAsyncComponent, h, ref, getCurrentInstance } from 'vue';
|
||||
import {
|
||||
NCard,
|
||||
NModal,
|
||||
NSpace,
|
||||
NFormItem,
|
||||
NSwitch,
|
||||
NButton,
|
||||
} from 'naive-ui';
|
||||
import { defineAsyncComponent, h, ref, getCurrentInstance, computed, onMounted } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { getTeam } from '../../data/team';
|
||||
import { confirmDialog, renderDialog } from '../../utils/components';
|
||||
import { icon } from '../../utils/components';
|
||||
import ObjectList from '../ObjectList.vue';
|
||||
import UserWithAvatarCell from '../UserWithAvatarCell.vue';
|
||||
import InviteTeamMemberDialog from './InviteTeamMemberDialog.vue';
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||
|
||||
const team = getTeam();
|
||||
team.getTeamMembers.submit();
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const showTeamSettingsDialog = ref(false);
|
||||
const showInviteMemberDialog = ref(false);
|
||||
const showRemoveMemberDialog = ref(false);
|
||||
const selectedMember = ref(null);
|
||||
|
||||
const isMobile = computed(() => windowWidth.value <= 768);
|
||||
const modalStyle = computed(() => ({
|
||||
width: isMobile.value ? '95vw' : '700px',
|
||||
maxWidth: isMobile.value ? '95vw' : '90vw',
|
||||
}));
|
||||
const buttonSize = computed(() => (isMobile.value ? 'medium' : 'medium'));
|
||||
|
||||
const enforce2FA = computed(() => {
|
||||
return Boolean(team.pg?.enforce_2fa);
|
||||
});
|
||||
|
||||
function handleEnforce2FAChange(value) {
|
||||
team.setValue.submit({ enforce_2fa: value });
|
||||
}
|
||||
|
||||
function handleInviteSuccess() {
|
||||
showInviteMemberDialog.value = false;
|
||||
team.getTeamMembers.submit();
|
||||
}
|
||||
|
||||
function handleRemoveMember() {
|
||||
if (!selectedMember.value) return;
|
||||
team.removeTeamMember.submit({ member: selectedMember.value.name });
|
||||
toast.success($t('Member removed'), { duration: 2000 });
|
||||
setTimeout(() => {
|
||||
team.getTeamMembers.submit();
|
||||
showRemoveMemberDialog.value = false;
|
||||
selectedMember.value = null;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const teamMembersListOptions = ref({
|
||||
onRowClick: () => {},
|
||||
rowHeight: 50,
|
||||
@ -36,7 +143,6 @@ const teamMembersListOptions = ref({
|
||||
}
|
||||
],
|
||||
rowActions({ row }) {
|
||||
let team = getTeam();
|
||||
if (row.name === team.pg.user || row.name === team.pg.user_info?.name)
|
||||
return [];
|
||||
return [
|
||||
@ -44,47 +150,105 @@ const teamMembersListOptions = ref({
|
||||
label: $t('Remove Member'),
|
||||
condition: () => row.name !== team.pg.user,
|
||||
onClick() {
|
||||
if (team.removeTeamMember.loading) return;
|
||||
confirmDialog({
|
||||
title: $t('Remove Member'),
|
||||
message: $t('Are you sure you want to remove <b>{name}</b> from the team?', { name: row.full_name }),
|
||||
onSuccess({ hide }) {
|
||||
toast.success($t('Member removed'), { duration: 2000 });
|
||||
hide();
|
||||
team.removeTeamMember.submit({ member: row.name });
|
||||
setTimeout(() => {
|
||||
team.getTeamMembers.submit();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
selectedMember.value = row;
|
||||
showRemoveMemberDialog.value = true;
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
actions() {
|
||||
return [
|
||||
{
|
||||
label: $t('Settings'),
|
||||
iconLeft: 'settings',
|
||||
onClick() {
|
||||
const TeamSettingsDialog = defineAsyncComponent(() =>
|
||||
import('./TeamSettingsDialog.vue')
|
||||
);
|
||||
renderDialog(h(TeamSettingsDialog));
|
||||
}
|
||||
},
|
||||
{
|
||||
label: $t('Add Member'),
|
||||
variant: 'solid',
|
||||
iconLeft: 'plus',
|
||||
onClick() {
|
||||
const InviteTeamMemberDialog = defineAsyncComponent(() =>
|
||||
import('./InviteTeamMemberDialog.vue')
|
||||
);
|
||||
renderDialog(h(InviteTeamMemberDialog));
|
||||
}
|
||||
secondaryAction() {
|
||||
return {
|
||||
label: $t('Settings'),
|
||||
icon: 'settings',
|
||||
onClick: () => {
|
||||
showTeamSettingsDialog.value = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
},
|
||||
primaryAction() {
|
||||
return {
|
||||
label: $t('Add Member'),
|
||||
slots: { prefix: icon('plus') },
|
||||
onClick: () => {
|
||||
showInviteMemberDialog.value = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
function handleResize() {
|
||||
windowWidth.value = window.innerWidth;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
team.getTeamMembers.submit();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-settings-container {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card) {
|
||||
width: 700px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card__action) {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.team-settings-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card) {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
margin: 20px auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card-body) {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card__header) {
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-card__action) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.team-settings-modal .n-button) {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,27 +4,30 @@
|
||||
$account.user.name === $account.team.user ||
|
||||
$account.user.user_type === 'System User'
|
||||
"
|
||||
class="space-y-5"
|
||||
class="team-settings-view"
|
||||
>
|
||||
<AccountMembers />
|
||||
<AccountGroups />
|
||||
<TeamSettings />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccountGroups from './AccountGroups.vue';
|
||||
import AccountMembers from './AccountMembers.vue';
|
||||
import TeamSettings from '../../components/settings/TeamSettings.vue';
|
||||
|
||||
export default {
|
||||
name: 'AccountSettings',
|
||||
name: 'TeamSettingsView',
|
||||
pageMeta() {
|
||||
return {
|
||||
title: 'Settings - Team'
|
||||
};
|
||||
},
|
||||
components: {
|
||||
AccountMembers,
|
||||
AccountGroups
|
||||
TeamSettings
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-settings-view {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -843,6 +843,8 @@ Username is required,用户名为必填项,
|
||||
Member added to team,已添加成员到团队,
|
||||
Remove Member,移除成员,
|
||||
Are you sure you want to remove <b>{name}</b> from the team?,确定要将 <b>{name}</b> 从团队中移除吗?,
|
||||
Are you sure you want to remove,确定要将,
|
||||
from the team?,从团队中移除吗?,
|
||||
Member removed,成员已被删除,
|
||||
Add Member,添加成员,
|
||||
Enforce Two-Factor Authentication,强制启用双因素认证,
|
||||
|
||||
|
Can't render this file because it has a wrong number of fields in line 400.
|
Loading…
x
Reference in New Issue
Block a user