基于naive ui重构设置-团队页面及弹窗

This commit is contained in:
jingrow 2025-12-30 01:17:10 +08:00
parent 8b617d833b
commit 345d3db483
4 changed files with 451 additions and 115 deletions

View File

@ -1,88 +1,162 @@
<template> <template>
<Dialog <n-modal
:options="{ v-model:show="showDialog"
title: $t('Add New Member'), preset="card"
actions: [ :title="$t('Add New Member')"
{ :style="modalStyle"
label: $t('Invite Member'), :mask-closable="true"
variant: 'solid', :close-on-esc="true"
onClick: inviteMember, class="invite-member-modal"
},
],
}"
v-model="show"
> >
<template #body-content> <template #header>
<div class="space-y-4"> <span class="text-lg font-semibold">{{ $t('Add New Member') }}</span>
<FormControl :label="$t('Username')" v-model="username" /> </template>
<div <n-space vertical :size="20">
v-if="$resources.roles.data?.length > 0" <n-form-item :label="$t('Username')" :required="true">
class="flex items-center space-x-2" <n-input
> v-model:value="username"
<FormControl placeholder=""
class="w-full" :size="inputSize"
type="autocomplete" class="w-full"
:label="$t('Select Role')" />
:options="roleOptions" </n-form-item>
v-model="selectedRole" <div v-if="$resources.roles.data?.length > 0" class="space-y-4">
/> <div class="flex items-end gap-2">
<Button <n-form-item :label="$t('Select Role')" class="flex-1">
:label="$t('Add')" <n-select
icon-left="plus" 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" :disabled="!selectedRole"
@click="addRole" @click="addRole"
class="mt-5" :size="buttonSize"
/>
</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"
> >
<div class="flex w-full items-center justify-between px-3 py-2"> <template #icon>
<div class="text-base text-gray-800">{{ role.label }}</div> <n-icon><PlusIcon /></n-icon>
</div> </template>
<Button {{ $t('Add') }}
class="ml-auto" </n-button>
variant="ghost" </div>
icon="x" <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)" @click="removeRole(role.value)"
/> >
<template #icon>
<n-icon><XIcon /></n-icon>
</template>
</n-button>
</div> </div>
</div> </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> </template>
</Dialog> </n-modal>
</template> </template>
<script> <script>
import {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSelect,
NIcon,
} from 'naive-ui';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import PlusIcon from '~icons/lucide/plus';
import XIcon from '~icons/lucide/x';
export default { export default {
components: {
NModal,
NFormItem,
NInput,
NSpace,
NAlert,
NButton,
NSelect,
NIcon,
PlusIcon,
XIcon,
},
emits: ['success', 'update:modelValue'],
props: {
modelValue: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
username: '', username: '',
show: true,
selectedRoles: [], selectedRoles: [],
selectedRole: null, selectedRole: null,
windowWidth: window.innerWidth,
}; };
}, },
resources: { computed: {
roles() { showDialog: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
},
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return { return {
type: 'list', width: this.isMobile ? '95vw' : '700px',
pagetype: 'Jcloud Role', maxWidth: this.isMobile ? '95vw' : '90vw',
fields: ['name', 'title'],
initialData: [],
auto: true,
}; };
}, },
}, inputSize() {
computed: { return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
roleOptions() { roleOptions() {
if (!this.$resources.roles.data) return [];
return this.$resources.roles.data return this.$resources.roles.data
.filter((role) => { .filter((role) => {
return !this.selectedRoles.some( 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: { 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() { addRole() {
if (this.selectedRole) { if (this.selectedRole) {
this.selectedRoles.push(this.selectedRole); const role = this.$resources.roles.data.find(r => r.name === this.selectedRole);
this.selectedRole = null; if (role) {
this.selectedRoles.push({
label: role.title,
value: role.name,
});
this.selectedRole = null;
}
} }
}, },
removeRole(roleToRemove) { removeRole(roleToRemove) {
@ -115,19 +214,87 @@ export default {
if (this.$team.inviteTeamMember.loading) return; if (this.$team.inviteTeamMember.loading) return;
toast.success(this.$t('Member added to team'), { duration: 2000 }); toast.success(this.$t('Member added to team'), { duration: 2000 });
this.show = false;
this.$team.inviteTeamMember.submit({ this.$team.inviteTeamMember.submit({
username: this.username, username: this.username,
roles: this.selectedRoles.map((role) => role.value), roles: this.selectedRoles.map((role) => role.value),
}); });
this.username = ''; this.hide();
this.selectedRoles = [];
setTimeout(() => { setTimeout(() => {
if (this.$team?.getTeamMembers) { if (this.$team?.getTeamMembers) {
this.$team.getTeamMembers.submit(); this.$team.getTeamMembers.submit();
} }
this.$emit('success');
}, 500); }, 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>

View File

@ -1,22 +1,129 @@
<template> <template>
<div class="p-5"> <div class="team-settings-container">
<ObjectList :options="teamMembersListOptions"> </ObjectList> <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> </div>
</template> </template>
<script setup> <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 { toast } from 'vue-sonner';
import { getTeam } from '../../data/team'; import { getTeam } from '../../data/team';
import { confirmDialog, renderDialog } from '../../utils/components'; import { icon } from '../../utils/components';
import ObjectList from '../ObjectList.vue'; import ObjectList from '../ObjectList.vue';
import UserWithAvatarCell from '../UserWithAvatarCell.vue'; import UserWithAvatarCell from '../UserWithAvatarCell.vue';
import InviteTeamMemberDialog from './InviteTeamMemberDialog.vue';
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key); const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
const team = getTeam(); 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({ const teamMembersListOptions = ref({
onRowClick: () => {}, onRowClick: () => {},
rowHeight: 50, rowHeight: 50,
@ -36,7 +143,6 @@ const teamMembersListOptions = ref({
} }
], ],
rowActions({ row }) { rowActions({ row }) {
let team = getTeam();
if (row.name === team.pg.user || row.name === team.pg.user_info?.name) if (row.name === team.pg.user || row.name === team.pg.user_info?.name)
return []; return [];
return [ return [
@ -44,47 +150,105 @@ const teamMembersListOptions = ref({
label: $t('Remove Member'), label: $t('Remove Member'),
condition: () => row.name !== team.pg.user, condition: () => row.name !== team.pg.user,
onClick() { onClick() {
if (team.removeTeamMember.loading) return; selectedMember.value = row;
confirmDialog({ showRemoveMemberDialog.value = true;
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);
}
});
} }
} }
]; ];
}, },
actions() { secondaryAction() {
return [ return {
{ label: $t('Settings'),
label: $t('Settings'), icon: 'settings',
iconLeft: 'settings', onClick: () => {
onClick() { showTeamSettingsDialog.value = true;
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));
}
} }
]; };
},
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>

View File

@ -4,27 +4,30 @@
$account.user.name === $account.team.user || $account.user.name === $account.team.user ||
$account.user.user_type === 'System User' $account.user.user_type === 'System User'
" "
class="space-y-5" class="team-settings-view"
> >
<AccountMembers /> <TeamSettings />
<AccountGroups />
</div> </div>
</template> </template>
<script> <script>
import AccountGroups from './AccountGroups.vue'; import TeamSettings from '../../components/settings/TeamSettings.vue';
import AccountMembers from './AccountMembers.vue';
export default { export default {
name: 'AccountSettings', name: 'TeamSettingsView',
pageMeta() { pageMeta() {
return { return {
title: 'Settings - Team' title: 'Settings - Team'
}; };
}, },
components: { components: {
AccountMembers, TeamSettings
AccountGroups
} }
}; };
</script> </script>
<style scoped>
.team-settings-view {
width: 100%;
}
</style>

View File

@ -843,6 +843,8 @@ Username is required,用户名为必填项,
Member added to team,已添加成员到团队, Member added to team,已添加成员到团队,
Remove Member,移除成员, 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 <b>{name}</b> from the team?,确定要将 <b>{name}</b> 从团队中移除吗?,
Are you sure you want to remove,确定要将,
from the team?,从团队中移除吗?,
Member removed,成员已被删除, Member removed,成员已被删除,
Add Member,添加成员, Add Member,添加成员,
Enforce Two-Factor Authentication,强制启用双因素认证, Enforce Two-Factor Authentication,强制启用双因素认证,

Can't render this file because it has a wrong number of fields in line 400.