基于naive ui重构设置-权限页面及弹窗
This commit is contained in:
parent
b0172b9aa8
commit
81f6620bf1
@ -15,7 +15,7 @@
|
||||
<n-form-item :label="$t('Endpoint')" :required="true">
|
||||
<n-input
|
||||
v-model:value="endpoint"
|
||||
:placeholder="$t('Enter webhook endpoint URL')"
|
||||
placeholder=""
|
||||
:size="inputSize"
|
||||
class="w-full"
|
||||
/>
|
||||
@ -23,7 +23,7 @@
|
||||
<n-form-item :label="$t('Secret Key')">
|
||||
<n-input
|
||||
v-model:value="secret"
|
||||
:placeholder="$t('Enter secret key (optional)')"
|
||||
placeholder=""
|
||||
:size="inputSize"
|
||||
class="w-full"
|
||||
>
|
||||
|
||||
@ -1,138 +1,198 @@
|
||||
<template>
|
||||
<Dialog
|
||||
<n-modal
|
||||
v-if="role"
|
||||
:options="{ title: `${role.title}`, size: 'xl' }"
|
||||
v-model="show"
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="role.title"
|
||||
:style="modalStyle"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
class="role-configure-modal"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<FTabs
|
||||
:tabs="[
|
||||
{
|
||||
label: $t('Members'),
|
||||
value: 'members',
|
||||
},
|
||||
{
|
||||
label: $t('Settings'),
|
||||
value: 'settings',
|
||||
},
|
||||
]"
|
||||
v-model="tabIndex"
|
||||
>
|
||||
<TabList v-slot="{ tab, selected }" class="pl-0">
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-1.5 border-b border-transparent py-3 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900 focus:outline-none focus:transition-none [&>div]:pl-0"
|
||||
:class="{ 'text-gray-900': selected }"
|
||||
>
|
||||
<span>{{ tab.label }}</span>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel v-slot="{ tab }">
|
||||
<div v-if="tab.value === 'members'" class="text-base">
|
||||
<div class="my-4 flex gap-2">
|
||||
<div class="flex-1">
|
||||
<Autocomplete
|
||||
:options="autoCompleteList"
|
||||
v-model="member"
|
||||
:placeholder="$t('Select member to add')"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
:label="$t('Add Member')"
|
||||
:disabled="!member?.value"
|
||||
:loading="$resources.role.addUser?.loading"
|
||||
@click="() => addUser(member.value)"
|
||||
<template #header>
|
||||
<span class="text-lg font-semibold">{{ role.title }}</span>
|
||||
</template>
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<n-tab-pane name="members" :tab="$t('Members')">
|
||||
<n-space vertical :size="20">
|
||||
<div class="flex gap-2 items-end">
|
||||
<n-select
|
||||
v-model:value="selectedMember"
|
||||
:options="autoCompleteList"
|
||||
:placeholder="$t('Select member to add')"
|
||||
:size="inputSize"
|
||||
class="flex-1"
|
||||
filterable
|
||||
/>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedMember"
|
||||
:loading="$resources.role.addUser?.loading"
|
||||
@click="() => addUser(selectedMember)"
|
||||
:size="buttonSize"
|
||||
>
|
||||
{{ $t('Add Member') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="rounded border px-3">
|
||||
<div class="mt-2 text-gray-600">{{ $t('Members') }}</div>
|
||||
<n-card>
|
||||
<div class="mb-3 text-gray-600 font-medium">{{ $t('Members') }}</div>
|
||||
<div
|
||||
v-if="roleUsers.length === 0"
|
||||
class="p-6 text-center text-gray-500"
|
||||
>
|
||||
<span>{{ $t('No members have been added to this role yet.') }}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col divide-y">
|
||||
<div
|
||||
v-for="user in roleUsers"
|
||||
class="flex justify-between py-3"
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="user in roleUsers"
|
||||
:key="user.user"
|
||||
class="flex justify-between items-center py-2 border-b border-gray-100 last:border-0"
|
||||
>
|
||||
<UserWithAvatarCell
|
||||
:avatarImage="user.user_image"
|
||||
:fullName="user.full_name"
|
||||
:email="user.user"
|
||||
/>
|
||||
<n-button
|
||||
tertiary
|
||||
circle
|
||||
size="small"
|
||||
@click="() => removeUser(user.user)"
|
||||
>
|
||||
<UserWithAvatarCell
|
||||
:avatarImage="user.user_image"
|
||||
:fullName="user.full_name"
|
||||
:email="user.user"
|
||||
:key="user.user"
|
||||
/>
|
||||
<Button variant="ghost" @click="() => removeUser(user.user)">
|
||||
<template #icon>
|
||||
<i-lucide-x class="h-4 w-4 text-gray-600" />
|
||||
</template>
|
||||
</Button>
|
||||
<template #icon>
|
||||
<n-icon><XIcon /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="settings" :tab="$t('Settings')">
|
||||
<n-space vertical :size="20">
|
||||
<n-card>
|
||||
<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('Admin Access') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ $t('Grant members permissions similar to team owner. Includes access to all pages and settings.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab.value === 'settings'" class="mt-4 text-base">
|
||||
<div class="space-y-3">
|
||||
<div class="rounded border p-4">
|
||||
<Switch
|
||||
class="ml-2"
|
||||
v-model="adminAccess"
|
||||
:label="$t('Admin Access')"
|
||||
:description="$t('Grant members permissions similar to team owner. Includes access to all pages and settings.')"
|
||||
<n-switch
|
||||
:value="adminAccess"
|
||||
@update:value="(val) => adminAccess = val"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1 rounded border p-4">
|
||||
<h2 class="mb-2 ml-2 font-semibold">{{ $t('Page Access Permissions') }}</h2>
|
||||
<Switch
|
||||
v-model="allowBilling"
|
||||
:label="$t('Allow Billing Access')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
<Switch
|
||||
v-model="allowApps"
|
||||
:label="$t('Allow Apps Access')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
<Switch
|
||||
v-if="$team.pg.jerp_partner"
|
||||
v-model="allowPartner"
|
||||
:label="$t('Allow Partner Access')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
<Switch
|
||||
v-model="allowSiteCreation"
|
||||
:label="$t('Allow Site Creation')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
<Switch
|
||||
v-model="allowBenchCreation"
|
||||
:label="$t('Allow Release Group Creation')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
<Switch
|
||||
v-model="allowServerCreation"
|
||||
:label="$t('Allow Server Creation')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
<Switch
|
||||
v-model="allowWebhookConfiguration"
|
||||
:label="$t('Allow Webhook Configuration')"
|
||||
:disabled="adminAccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</FTabs>
|
||||
</template>
|
||||
</Dialog>
|
||||
</n-form-item>
|
||||
</n-card>
|
||||
<n-card>
|
||||
<h2 class="mb-4 text-base font-semibold">{{ $t('Page Access Permissions') }}</h2>
|
||||
<n-space vertical :size="16">
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Billing Access') }}</span>
|
||||
<n-switch
|
||||
:value="allowBilling"
|
||||
@update:value="(val) => allowBilling = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Apps Access') }}</span>
|
||||
<n-switch
|
||||
:value="allowApps"
|
||||
@update:value="(val) => allowApps = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="$team.pg.jerp_partner">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Partner Access') }}</span>
|
||||
<n-switch
|
||||
:value="allowPartner"
|
||||
@update:value="(val) => allowPartner = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Site Creation') }}</span>
|
||||
<n-switch
|
||||
:value="allowSiteCreation"
|
||||
@update:value="(val) => allowSiteCreation = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Release Group Creation') }}</span>
|
||||
<n-switch
|
||||
:value="allowBenchCreation"
|
||||
@update:value="(val) => allowBenchCreation = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Server Creation') }}</span>
|
||||
<n-switch
|
||||
:value="allowServerCreation"
|
||||
@update:value="(val) => allowServerCreation = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-base">{{ $t('Allow Webhook Configuration') }}</span>
|
||||
<n-switch
|
||||
:value="allowWebhookConfiguration"
|
||||
@update:value="(val) => allowWebhookConfiguration = val"
|
||||
:disabled="adminAccess"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Switch, Tabs, TabList, TabPanel } from 'jingrow-ui';
|
||||
import {
|
||||
NModal,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NSpace,
|
||||
NSelect,
|
||||
NButton,
|
||||
NIcon,
|
||||
NCard,
|
||||
NFormItem,
|
||||
NSwitch,
|
||||
} from 'naive-ui';
|
||||
import { toast } from 'vue-sonner';
|
||||
import UserWithAvatarCell from '../UserWithAvatarCell.vue';
|
||||
import XIcon from '~icons/lucide/x';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -140,16 +200,24 @@ export default {
|
||||
},
|
||||
components: {
|
||||
UserWithAvatarCell,
|
||||
FTabs: Tabs,
|
||||
TabPanel,
|
||||
TabList,
|
||||
Switch,
|
||||
NModal,
|
||||
NTabs,
|
||||
NTabPane,
|
||||
NSpace,
|
||||
NSelect,
|
||||
NButton,
|
||||
NIcon,
|
||||
NCard,
|
||||
NFormItem,
|
||||
NSwitch,
|
||||
XIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
member: {},
|
||||
selectedMember: null,
|
||||
show: true,
|
||||
tabIndex: 0,
|
||||
activeTab: 'members',
|
||||
windowWidth: window.innerWidth,
|
||||
};
|
||||
},
|
||||
resources: {
|
||||
@ -180,6 +248,21 @@ export default {
|
||||
?.filter(({ user }) => isNotGroupMember(user))
|
||||
.map(({ user }) => ({ label: user, value: user }));
|
||||
},
|
||||
isMobile() {
|
||||
return this.windowWidth <= 768;
|
||||
},
|
||||
modalStyle() {
|
||||
return {
|
||||
width: this.isMobile ? '95vw' : '900px',
|
||||
maxWidth: this.isMobile ? '95vw' : '90vw',
|
||||
};
|
||||
},
|
||||
inputSize() {
|
||||
return this.isMobile ? 'medium' : 'large';
|
||||
},
|
||||
buttonSize() {
|
||||
return this.isMobile ? 'medium' : 'medium';
|
||||
},
|
||||
adminAccess: {
|
||||
get() {
|
||||
return !!this.role?.admin_access;
|
||||
@ -286,14 +369,16 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
},
|
||||
addUser(user) {
|
||||
if (!user) return;
|
||||
if (this.$resources.role.addUser.loading) return;
|
||||
|
||||
toast.success(this.$t('Added {user} to {role}', { user, role: this.role.title }), { duration: 2000 });
|
||||
this.member = {};
|
||||
this.selectedMember = null;
|
||||
this.$resources.role.addUser.submit({ user });
|
||||
// 刷新角色数据
|
||||
setTimeout(() => {
|
||||
if (this.$resources.role) {
|
||||
this.$resources.role.reload();
|
||||
@ -306,7 +391,6 @@ export default {
|
||||
|
||||
toast.success(this.$t('Removed {user} from {role}', { user, role: this.role.title }), { duration: 2000 });
|
||||
this.$resources.role.removeUser.submit({ user });
|
||||
// 刷新角色数据
|
||||
setTimeout(() => {
|
||||
if (this.$resources.role) {
|
||||
this.$resources.role.reload();
|
||||
@ -314,5 +398,57 @@ export default {
|
||||
}, 500);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.role-configure-modal .n-card) {
|
||||
width: 900px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-tabs) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-tab-pane) {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.role-configure-modal .n-card) {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
margin: 20px auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-card-body) {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-card__header) {
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.role-configure-modal .n-tab-pane) {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,20 +1,86 @@
|
||||
<template>
|
||||
<ObjectList :options="listOptions" />
|
||||
<div class="role-list-container">
|
||||
<n-card class="settings-card">
|
||||
<ObjectList :options="listOptions" />
|
||||
</n-card>
|
||||
|
||||
<!-- 创建角色弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showCreateRoleDialog"
|
||||
preset="card"
|
||||
:title="$t('Create Role')"
|
||||
:style="modalStyle"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
class="create-role-modal"
|
||||
>
|
||||
<template #header>
|
||||
<span class="text-lg font-semibold">{{ $t('Create Role') }}</span>
|
||||
</template>
|
||||
<n-space vertical :size="20">
|
||||
<n-form-item :label="$t('Role')" :required="true">
|
||||
<n-input
|
||||
v-model:value="newRoleTitle"
|
||||
placeholder=""
|
||||
:size="inputSize"
|
||||
class="w-full"
|
||||
@keyup.enter="handleCreateRole"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
<template #action>
|
||||
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
|
||||
<n-button @click="showCreateRoleDialog = false" :block="isMobile" :size="buttonSize">
|
||||
{{ $t('Cancel') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="createRoleLoading"
|
||||
:disabled="!newRoleTitle"
|
||||
@click="handleCreateRole"
|
||||
:block="isMobile"
|
||||
:size="buttonSize"
|
||||
>
|
||||
{{ $t('Confirm') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 删除角色确认弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showDeleteRoleDialog"
|
||||
preset="dialog"
|
||||
:title="$t('Delete Role')"
|
||||
:positive-text="$t('Delete')"
|
||||
:positive-button-props="{ type: 'error' }"
|
||||
:loading="deleteRoleLoading"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
@positive-click="handleDeleteRole"
|
||||
>
|
||||
<div class="py-4">
|
||||
<p class="text-base" v-html="$t('Are you sure you want to delete role <b>{role}</b>?', { role: selectedRole?.title || '' })"></p>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="jsx">
|
||||
import { h, ref, getCurrentInstance } from 'vue';
|
||||
import { h, ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { FeatherIcon } from 'jingrow-ui';
|
||||
import { icon, renderDialog, confirmDialog } from '../../utils/components';
|
||||
import { NCard, NModal, NSpace, NFormItem, NInput, NButton } from 'naive-ui';
|
||||
import { icon, renderDialog } from '../../utils/components';
|
||||
import ObjectList from '../ObjectList.vue';
|
||||
import RoleConfigureDialog from './RoleConfigureDialog.vue';
|
||||
import router from '../../router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import UserAvatarGroup from '../AvatarGroup.vue';
|
||||
import { getToastErrorMessage } from '../../utils/toast';
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||
const router = useRouter();
|
||||
|
||||
const listOptions = ref({
|
||||
pagetype: 'Jcloud Role',
|
||||
@ -75,22 +141,9 @@ const listOptions = ref({
|
||||
label: $t('Delete Role'),
|
||||
onClick() {
|
||||
if (roleListResource.delete.loading) return;
|
||||
confirmDialog({
|
||||
title: $t('Delete Role'),
|
||||
message: $t('Are you sure you want to delete role <b>{role}</b>?', { role: row.title }),
|
||||
onSuccess({ hide }) {
|
||||
if (roleListResource.delete.loading) return;
|
||||
toast.promise(roleListResource.delete.submit(row.name), {
|
||||
loading: $t('Deleting role...'),
|
||||
success: () => {
|
||||
roleListResource.reload();
|
||||
hide();
|
||||
return $t('Role {role} deleted', { role: row.title });
|
||||
},
|
||||
error: (e) => getToastErrorMessage(e),
|
||||
});
|
||||
},
|
||||
});
|
||||
selectedRole.value = row;
|
||||
roleListResourceRef.value = roleListResource;
|
||||
showDeleteRoleDialog.value = true;
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -102,42 +155,158 @@ const listOptions = ref({
|
||||
};
|
||||
},
|
||||
primaryAction({ listResource: groups }) {
|
||||
roleListResourceRef.value = groups;
|
||||
return {
|
||||
label: $t('New Role'),
|
||||
variant: 'solid',
|
||||
slots: {
|
||||
prefix: icon('plus'),
|
||||
},
|
||||
onClick() {
|
||||
confirmDialog({
|
||||
title: $t('Create Role'),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'title',
|
||||
label: $t('Role'),
|
||||
autocomplete: 'off',
|
||||
},
|
||||
],
|
||||
primaryAction: {
|
||||
label: $t('Confirm'),
|
||||
variant: 'solid'
|
||||
},
|
||||
onSuccess({ hide, values }) {
|
||||
if (values.title) {
|
||||
return groups.insert.submit(
|
||||
{ title: values.title },
|
||||
{ onSuccess: hide },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
newRoleTitle.value = '';
|
||||
showCreateRoleDialog.value = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const showCreateRoleDialog = ref(false);
|
||||
const showDeleteRoleDialog = ref(false);
|
||||
const newRoleTitle = ref('');
|
||||
const selectedRole = ref(null);
|
||||
const roleListResourceRef = ref(null);
|
||||
const createRoleLoading = ref(false);
|
||||
const deleteRoleLoading = ref(false);
|
||||
|
||||
const isMobile = computed(() => windowWidth.value <= 768);
|
||||
const modalStyle = computed(() => ({
|
||||
width: isMobile.value ? '95vw' : '700px',
|
||||
maxWidth: isMobile.value ? '95vw' : '90vw',
|
||||
}));
|
||||
const inputSize = computed(() => isMobile.value ? 'medium' : 'large');
|
||||
const buttonSize = computed(() => isMobile.value ? 'medium' : 'medium');
|
||||
|
||||
function handleResize() {
|
||||
windowWidth.value = window.innerWidth;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
async function handleCreateRole() {
|
||||
if (!newRoleTitle.value || !roleListResourceRef.value) return;
|
||||
if (createRoleLoading.value) return;
|
||||
|
||||
createRoleLoading.value = true;
|
||||
try {
|
||||
await roleListResourceRef.value.insert.submit({ title: newRoleTitle.value });
|
||||
toast.success($t('Role created successfully'));
|
||||
showCreateRoleDialog.value = false;
|
||||
newRoleTitle.value = '';
|
||||
} catch (error) {
|
||||
toast.error(getToastErrorMessage(error));
|
||||
} finally {
|
||||
createRoleLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteRole() {
|
||||
if (!selectedRole.value || !roleListResourceRef.value) return;
|
||||
if (deleteRoleLoading.value) return;
|
||||
|
||||
const roleTitle = selectedRole.value.title;
|
||||
deleteRoleLoading.value = true;
|
||||
try {
|
||||
await toast.promise(
|
||||
roleListResourceRef.value.delete.submit(selectedRole.value.name),
|
||||
{
|
||||
loading: $t('Deleting role...'),
|
||||
success: () => {
|
||||
roleListResourceRef.value.reload();
|
||||
showDeleteRoleDialog.value = false;
|
||||
selectedRole.value = null;
|
||||
return $t('Role {role} deleted', { role: roleTitle });
|
||||
},
|
||||
error: (e) => getToastErrorMessage(e),
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
deleteRoleLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function configureRole(row) {
|
||||
renderDialog(h(RoleConfigureDialog, { roleId: row.name }));
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-list-container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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(.create-role-modal .n-card) {
|
||||
width: 700px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-card__action) {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.create-role-modal .n-card) {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
margin: 20px auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-card-body) {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-card__header) {
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-card__action) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.create-role-modal .n-button) {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,58 +1,158 @@
|
||||
<template>
|
||||
<div class="mb-5 flex items-center gap-2">
|
||||
<Tooltip :text="$t('All Roles')">
|
||||
<Button :route="{ name: 'SettingsPermissionRoles' }">
|
||||
<template #icon>
|
||||
<i-lucide-arrow-left class="h-4 w-4 text-gray-700" />
|
||||
<div class="role-permissions-container">
|
||||
<div class="mb-5 flex items-center gap-2">
|
||||
<n-tooltip>
|
||||
<template #trigger>
|
||||
<n-button
|
||||
quaternary
|
||||
@click="router.push({ name: 'SettingsPermissionRoles' })"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><ArrowLeftIcon /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{{ role.pg?.title }}
|
||||
</h3>
|
||||
<Tooltip :text="$t('Admin Role')" v-if="role.pg.admin_access">
|
||||
<FeatherIcon name="shield" class="h-5 w-5 text-gray-700" />
|
||||
</Tooltip>
|
||||
{{ $t('All Roles') }}
|
||||
</n-tooltip>
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
{{ role.pg?.title }}
|
||||
</h3>
|
||||
<n-tooltip v-if="role.pg?.admin_access">
|
||||
<template #trigger>
|
||||
<n-icon class="h-5 w-5 text-gray-700">
|
||||
<ShieldIcon />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ $t('Admin Role') }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
<n-card class="settings-card">
|
||||
<ObjectList
|
||||
:options="rolePermissions"
|
||||
@update:selections="(e) => (selectedItems = e)"
|
||||
>
|
||||
<template #header-left="{ listResource }">
|
||||
<n-dropdown :options="getDropdownOptions(listResource)" trigger="click">
|
||||
<n-button>
|
||||
<template #icon>
|
||||
<n-icon><AppWindowIcon /></n-icon>
|
||||
</template>
|
||||
{{ currentDropdownOption.label }}
|
||||
<template #suffix>
|
||||
<n-icon><ChevronDownIcon /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
</ObjectList>
|
||||
</n-card>
|
||||
|
||||
<!-- 添加权限弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showAddPermissionDialog"
|
||||
preset="card"
|
||||
:title="$t('Add Permission')"
|
||||
:style="modalStyle"
|
||||
:mask-closable="true"
|
||||
:close-on-esc="true"
|
||||
class="add-permission-modal"
|
||||
>
|
||||
<template #header>
|
||||
<span class="text-lg font-semibold">{{ $t('Add Permission') }}</span>
|
||||
</template>
|
||||
<n-space vertical :size="20">
|
||||
<n-form-item :label="$t('Select {type}', { type: currentDropdownOption.pagetype === 'Release Group' ? $t('Release Group') : currentDropdownOption.pagetype })">
|
||||
<!-- 这里需要使用 LinkControl 或类似的组件来选择文档 -->
|
||||
<n-input
|
||||
v-model:value="selectedDocument"
|
||||
:placeholder="$t('Select {type}', { type: currentDropdownOption.pagetype === 'Release Group' ? $t('Release Group') : currentDropdownOption.pagetype })"
|
||||
:size="inputSize"
|
||||
class="w-full"
|
||||
readonly
|
||||
@click="showDocumentSelector = true"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-space>
|
||||
<template #action>
|
||||
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
|
||||
<n-button @click="showAddPermissionDialog = false" :block="isMobile" :size="buttonSize">
|
||||
{{ $t('Cancel') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="addPermissionLoading"
|
||||
:disabled="!selectedDocument"
|
||||
@click="handleAddPermission"
|
||||
:block="isMobile"
|
||||
:size="buttonSize"
|
||||
>
|
||||
{{ $t('Add') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
<ObjectList
|
||||
:options="rolePermissions"
|
||||
@update:selections="(e) => (selectedItems = e)"
|
||||
>
|
||||
<template #header-left="{ listResource }">
|
||||
<Dropdown :options="getDropdownOptions(listResource)">
|
||||
<Button>
|
||||
<template #prefix>
|
||||
<LucideAppWindow class="h-4 w-4 text-gray-500" />
|
||||
</template>
|
||||
{{ currentDropdownOption.label }}
|
||||
<template #suffix>
|
||||
<FeatherIcon name="chevron-down" class="h-4 w-4 text-gray-500" />
|
||||
</template>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</ObjectList>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
createDocumentResource,
|
||||
createResource,
|
||||
} from 'jingrow-ui';
|
||||
import { computed, h, ref, getCurrentInstance } from 'vue';
|
||||
import LucideAppWindow from '~icons/lucide/app-window';
|
||||
import { computed, h, ref, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue';
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
NDropdown,
|
||||
NTooltip,
|
||||
NIcon,
|
||||
NModal,
|
||||
NSpace,
|
||||
NFormItem,
|
||||
NInput,
|
||||
} from 'naive-ui';
|
||||
import ArrowLeftIcon from '~icons/lucide/arrow-left';
|
||||
import ShieldIcon from '~icons/lucide/shield';
|
||||
import AppWindowIcon from '~icons/lucide/app-window';
|
||||
import ChevronDownIcon from '~icons/lucide/chevron-down';
|
||||
import ObjectList from '../ObjectList.vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { getToastErrorMessage } from '../../utils/toast';
|
||||
import { confirmDialog, icon, renderDialog } from '../../utils/components';
|
||||
import { icon, renderDialog } from '../../utils/components';
|
||||
import RoleConfigureDialog from './RoleConfigureDialog.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
|
||||
const router = useRouter();
|
||||
|
||||
let selectedItems = ref(new Set());
|
||||
const windowWidth = ref(window.innerWidth);
|
||||
const showAddPermissionDialog = ref(false);
|
||||
const showDocumentSelector = ref(false);
|
||||
const selectedDocument = ref('');
|
||||
const addPermissionLoading = ref(false);
|
||||
const permissionsResourceRef = ref(null);
|
||||
|
||||
const isMobile = computed(() => windowWidth.value <= 768);
|
||||
const modalStyle = computed(() => ({
|
||||
width: isMobile.value ? '95vw' : '700px',
|
||||
maxWidth: isMobile.value ? '95vw' : '90vw',
|
||||
}));
|
||||
const inputSize = computed(() => isMobile.value ? 'medium' : 'large');
|
||||
const buttonSize = computed(() => isMobile.value ? 'medium' : 'medium');
|
||||
|
||||
function handleResize() {
|
||||
windowWidth.value = window.innerWidth;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
roleId: { type: String, required: true },
|
||||
@ -86,7 +186,8 @@ const currentDropdownOption = ref(dropdownOptions[0]);
|
||||
function getDropdownOptions(listResource) {
|
||||
return dropdownOptions.map((option) => {
|
||||
return {
|
||||
...option,
|
||||
label: option.label,
|
||||
key: option.pagetype,
|
||||
onClick: () => {
|
||||
currentDropdownOption.value = option;
|
||||
let filters = {
|
||||
@ -179,59 +280,109 @@ const rolePermissions = ref({
|
||||
prefix: icon('plus'),
|
||||
},
|
||||
onClick() {
|
||||
const pagetypeLabel = currentDropdownOption.value.pagetype === 'Release Group'
|
||||
? $t('Release Group')
|
||||
: currentDropdownOption.value.pagetype;
|
||||
confirmDialog({
|
||||
title: $t('Add Permission'),
|
||||
message: '',
|
||||
fields: [
|
||||
{
|
||||
label: $t('Select {type}', { type: pagetypeLabel }),
|
||||
type: 'link',
|
||||
fieldname: 'document_name',
|
||||
options: {
|
||||
pagetype: currentDropdownOption.value.pagetype,
|
||||
filters: {
|
||||
name: [
|
||||
'not in',
|
||||
permissions.data.map(
|
||||
(p) => p[currentDropdownOption.value.fieldname],
|
||||
) || '',
|
||||
],
|
||||
status: ['!=', 'Archived'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
primaryAction: {
|
||||
label: $t('Add'),
|
||||
onClick({ values }) {
|
||||
let key = currentDropdownOption.value.fieldname;
|
||||
|
||||
toast.promise(
|
||||
docInsert.submit({
|
||||
pg: {
|
||||
pagetype: 'Jcloud Role Permission',
|
||||
role: props.roleId,
|
||||
[key]: values.document_name,
|
||||
},
|
||||
}),
|
||||
{
|
||||
loading: $t('Adding permission...'),
|
||||
success() {
|
||||
permissions.reload();
|
||||
return $t('Permission added successfully');
|
||||
},
|
||||
error: (e) => getToastErrorMessage(e),
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
permissionsResourceRef.value = permissions;
|
||||
selectedDocument.value = '';
|
||||
showAddPermissionDialog.value = true;
|
||||
},
|
||||
},
|
||||
].filter((action) => (action.condition ? action.condition() : true));
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
async function handleAddPermission() {
|
||||
if (!selectedDocument.value || !permissionsResourceRef.value) return;
|
||||
if (addPermissionLoading.value) return;
|
||||
|
||||
const key = currentDropdownOption.value.fieldname;
|
||||
addPermissionLoading.value = true;
|
||||
try {
|
||||
await toast.promise(
|
||||
docInsert.submit({
|
||||
pg: {
|
||||
pagetype: 'Jcloud Role Permission',
|
||||
role: props.roleId,
|
||||
[key]: selectedDocument.value,
|
||||
},
|
||||
}),
|
||||
{
|
||||
loading: $t('Adding permission...'),
|
||||
success() {
|
||||
permissionsResourceRef.value.reload();
|
||||
showAddPermissionDialog.value = false;
|
||||
selectedDocument.value = '';
|
||||
return $t('Permission added successfully');
|
||||
},
|
||||
error: (e) => getToastErrorMessage(e),
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
addPermissionLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-permissions-container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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(.add-permission-modal .n-card) {
|
||||
width: 700px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-card-body) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-card__action) {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.add-permission-modal .n-card) {
|
||||
width: 95vw !important;
|
||||
max-width: 95vw !important;
|
||||
margin: 20px auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-card-body) {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-card__header) {
|
||||
padding: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-card__action) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
:deep(.add-permission-modal .n-button) {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div class="permissions-settings-container">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
<script setup></script>
|
||||
|
||||
<style scoped>
|
||||
.permissions-settings-container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user