559 lines
16 KiB
Vue
559 lines
16 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import {
|
|||
|
|
CheckOutlined,
|
|||
|
|
DeleteOutlined,
|
|||
|
|
EditOutlined,
|
|||
|
|
LockOutlined,
|
|||
|
|
PlusOutlined,
|
|||
|
|
SafetyCertificateOutlined,
|
|||
|
|
SaveOutlined,
|
|||
|
|
TeamOutlined,
|
|||
|
|
} from '@ant-design/icons-vue';
|
|||
|
|
import { message, Modal } from 'ant-design-vue';
|
|||
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|||
|
|
import { toFarsiDigits } from '@lucky-wheel/shared-types';
|
|||
|
|
import { useAuthStore } from '../../stores/auth.store';
|
|||
|
|
import { apiClient } from '../../services/api/client';
|
|||
|
|
|
|||
|
|
const authStore = useAuthStore();
|
|||
|
|
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const saving = ref(false);
|
|||
|
|
const roles = ref<any[]>([]);
|
|||
|
|
const permissions = ref<any[]>([]);
|
|||
|
|
const selectedRoleId = ref<string | null>(null);
|
|||
|
|
|
|||
|
|
// Form state for creating a new role
|
|||
|
|
const isCreateModalOpen = ref(false);
|
|||
|
|
const isSubmittingNewRole = ref(false);
|
|||
|
|
const newRoleForm = reactive({
|
|||
|
|
code: '',
|
|||
|
|
name: '',
|
|||
|
|
description: '',
|
|||
|
|
permissionIds: [] as string[],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Permission IDs assigned to the currently selected role in edit matrix
|
|||
|
|
const activeRolePermIds = ref<string[]>([]);
|
|||
|
|
const isRoleDirty = ref(false);
|
|||
|
|
|
|||
|
|
const selectedRole = computed(() => {
|
|||
|
|
return roles.value.find((r) => r.id === selectedRoleId.value) || null;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Group permissions by module
|
|||
|
|
const moduleLabels: Record<string, string> = {
|
|||
|
|
campaigns: 'مدیریت کمپینها',
|
|||
|
|
prizes: 'مدیریت جوایز',
|
|||
|
|
users: 'کاربران سامانه',
|
|||
|
|
chances: 'تنظیم دستی شانسها',
|
|||
|
|
spins: 'تاریخچه اسپینها',
|
|||
|
|
winners: 'مدیریت برندگان و کدهای هدیه',
|
|||
|
|
sms: 'لاگ و ارسال پیامک',
|
|||
|
|
audit: 'لاگ بازرسی (Audit)',
|
|||
|
|
reports: 'گزارشها و KPI',
|
|||
|
|
admins: 'مدیریت مدیران و همکاران',
|
|||
|
|
roles: 'مدیریت نقشها و دسترسیها',
|
|||
|
|
workspaces: 'فضاهای کاری سازمانی',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const groupedPermissions = computed(() => {
|
|||
|
|
const groups: Record<string, any[]> = {};
|
|||
|
|
permissions.value.forEach((p) => {
|
|||
|
|
const mod = p.module || 'other';
|
|||
|
|
if (!groups[mod]) groups[mod] = [];
|
|||
|
|
groups[mod].push(p);
|
|||
|
|
});
|
|||
|
|
return groups;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function loadData() {
|
|||
|
|
loading.value = true;
|
|||
|
|
try {
|
|||
|
|
const [rolesRes, permsRes] = await Promise.all([
|
|||
|
|
apiClient.get('/admin/roles'),
|
|||
|
|
apiClient.get('/admin/permissions'),
|
|||
|
|
]);
|
|||
|
|
roles.value = Array.isArray(rolesRes.data) ? rolesRes.data : rolesRes.data?.data || [];
|
|||
|
|
permissions.value = Array.isArray(permsRes.data) ? permsRes.data : permsRes.data?.data || [];
|
|||
|
|
|
|||
|
|
if (!selectedRoleId.value && roles.value.length > 0) {
|
|||
|
|
selectRole(roles.value[0].id);
|
|||
|
|
} else if (selectedRoleId.value) {
|
|||
|
|
selectRole(selectedRoleId.value);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectRole(roleId: string) {
|
|||
|
|
selectedRoleId.value = roleId;
|
|||
|
|
const role = roles.value.find((r) => r.id === roleId);
|
|||
|
|
if (role) {
|
|||
|
|
activeRolePermIds.value = role.permissions ? role.permissions.map((rp: any) => rp.permissionId || rp.permission?.id) : [];
|
|||
|
|
} else {
|
|||
|
|
activeRolePermIds.value = [];
|
|||
|
|
}
|
|||
|
|
isRoleDirty.value = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function togglePermission(permId: string) {
|
|||
|
|
if (selectedRole.value?.isSystem && selectedRole.value?.code === 'SUPER_ADMIN') {
|
|||
|
|
message.warning('مجوزهای نقش مدیر ارشد به صورت ذاتی کامل و غیرقابل تغییر است.');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const idx = activeRolePermIds.value.indexOf(permId);
|
|||
|
|
if (idx > -1) {
|
|||
|
|
activeRolePermIds.value.splice(idx, 1);
|
|||
|
|
} else {
|
|||
|
|
activeRolePermIds.value.push(permId);
|
|||
|
|
}
|
|||
|
|
isRoleDirty.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleModuleAll(mod: string) {
|
|||
|
|
if (selectedRole.value?.isSystem && selectedRole.value?.code === 'SUPER_ADMIN') return;
|
|||
|
|
|
|||
|
|
const perms = groupedPermissions.value[mod] || [];
|
|||
|
|
const ids = perms.map((p) => p.id);
|
|||
|
|
const allSelected = ids.every((id) => activeRolePermIds.value.includes(id));
|
|||
|
|
|
|||
|
|
if (allSelected) {
|
|||
|
|
activeRolePermIds.value = activeRolePermIds.value.filter((id) => !ids.includes(id));
|
|||
|
|
} else {
|
|||
|
|
ids.forEach((id) => {
|
|||
|
|
if (!activeRolePermIds.value.includes(id)) {
|
|||
|
|
activeRolePermIds.value.push(id);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
isRoleDirty.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveRolePermissions() {
|
|||
|
|
if (!selectedRoleId.value) return;
|
|||
|
|
|
|||
|
|
saving.value = true;
|
|||
|
|
try {
|
|||
|
|
await apiClient.patch(`/admin/roles/${selectedRoleId.value}`, {
|
|||
|
|
permissionIds: activeRolePermIds.value,
|
|||
|
|
});
|
|||
|
|
message.success('مجوزهای نقش با موفقیت بهروزرسانی شد.');
|
|||
|
|
isRoleDirty.value = false;
|
|||
|
|
loadData();
|
|||
|
|
} finally {
|
|||
|
|
saving.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openCreateModal() {
|
|||
|
|
newRoleForm.code = '';
|
|||
|
|
newRoleForm.name = '';
|
|||
|
|
newRoleForm.description = '';
|
|||
|
|
newRoleForm.permissionIds = [];
|
|||
|
|
isCreateModalOpen.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function handleCreateRole() {
|
|||
|
|
if (!newRoleForm.name.trim()) {
|
|||
|
|
message.error('نام فارسی نقش الزامی است.');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!newRoleForm.code.trim()) {
|
|||
|
|
message.error('کد انگلیسی نقش الزامی است.');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isSubmittingNewRole.value = true;
|
|||
|
|
try {
|
|||
|
|
const res = await apiClient.post('/admin/roles', {
|
|||
|
|
name: newRoleForm.name.trim(),
|
|||
|
|
code: newRoleForm.code.trim().toUpperCase(),
|
|||
|
|
description: newRoleForm.description?.trim() || undefined,
|
|||
|
|
permissionIds: newRoleForm.permissionIds,
|
|||
|
|
});
|
|||
|
|
message.success('نقش جدید با موفقیت ایجاد شد.');
|
|||
|
|
isCreateModalOpen.value = false;
|
|||
|
|
await loadData();
|
|||
|
|
if (res.data?.id) selectRole(res.data.id);
|
|||
|
|
} finally {
|
|||
|
|
isSubmittingNewRole.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function confirmDeleteRole(role: any) {
|
|||
|
|
Modal.confirm({
|
|||
|
|
title: 'حذف نقش سفارشی',
|
|||
|
|
content: `آیا از حذف نقش «${role.name}» اطمینان دارید؟`,
|
|||
|
|
okText: 'حذف نقش',
|
|||
|
|
okType: 'danger',
|
|||
|
|
cancelText: 'انصراف',
|
|||
|
|
onOk: async () => {
|
|||
|
|
try {
|
|||
|
|
await apiClient.delete(`/admin/roles/${role.id}`);
|
|||
|
|
message.success('نقش با موفقیت حذف شد.');
|
|||
|
|
selectedRoleId.value = null;
|
|||
|
|
loadData();
|
|||
|
|
} catch {
|
|||
|
|
// Handled
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadData();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="roles-matrix-page">
|
|||
|
|
<div class="page-header-title">
|
|||
|
|
<div>
|
|||
|
|
<h1 class="title">مدیریت نقشها و ماتریس مجوزها</h1>
|
|||
|
|
<p class="subtitle">تعریف نقشهای سازمانی و انتساب دانهبندیشده مجوزهای دسترسی به ماژولهای سامانه</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="page-actions" v-if="authStore.can('roles:manage')">
|
|||
|
|
<a-button type="primary" size="large" @click="openCreateModal">
|
|||
|
|
<template #icon><PlusOutlined /></template>
|
|||
|
|
تعریف نقش جدید
|
|||
|
|
</a-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<a-spin :spinning="loading">
|
|||
|
|
<a-row :gutter="20">
|
|||
|
|
<!-- سمت راست: لیست نقشها -->
|
|||
|
|
<a-col :xs="24" :md="8" :lg="7">
|
|||
|
|
<a-card :bordered="false" class="roles-sidebar-card" title="نقشهای سامانه">
|
|||
|
|
<div class="roles-list">
|
|||
|
|
<div
|
|||
|
|
v-for="r in roles"
|
|||
|
|
:key="r.id"
|
|||
|
|
class="role-item"
|
|||
|
|
:class="{ 'is-active': r.id === selectedRoleId }"
|
|||
|
|
@click="selectRole(r.id)"
|
|||
|
|
>
|
|||
|
|
<div class="role-item-header">
|
|||
|
|
<div class="role-title">
|
|||
|
|
<SafetyCertificateOutlined
|
|||
|
|
:style="{ color: r.isSystem ? '#7c3aed' : '#2563eb', marginLeft: '6px' }"
|
|||
|
|
/>
|
|||
|
|
<strong>{{ r.name }}</strong>
|
|||
|
|
</div>
|
|||
|
|
<a-tag v-if="r.isSystem" color="purple" size="small">سیستمی</a-tag>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="role-code">{{ r.code }}</div>
|
|||
|
|
<div class="role-desc">{{ r.description || 'بدون توضیح' }}</div>
|
|||
|
|
|
|||
|
|
<div class="role-footer">
|
|||
|
|
<span class="user-count">
|
|||
|
|
<TeamOutlined style="margin-left: 4px" />
|
|||
|
|
{{ toFarsiDigits(r._count?.admins ?? 0) }} مدیر منتسب
|
|||
|
|
</span>
|
|||
|
|
|
|||
|
|
<a-button
|
|||
|
|
v-if="!r.isSystem && authStore.can('roles:manage')"
|
|||
|
|
type="text"
|
|||
|
|
danger
|
|||
|
|
size="small"
|
|||
|
|
@click.stop="confirmDeleteRole(r)"
|
|||
|
|
>
|
|||
|
|
<DeleteOutlined />
|
|||
|
|
</a-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</a-card>
|
|||
|
|
</a-col>
|
|||
|
|
|
|||
|
|
<!-- سمت چپ: ماتریس مجوزهای نقش انتخابشده -->
|
|||
|
|
<a-col :xs="24" :md="16" :lg="17">
|
|||
|
|
<a-card :bordered="false" class="matrix-card">
|
|||
|
|
<template #title>
|
|||
|
|
<div class="matrix-header">
|
|||
|
|
<div>
|
|||
|
|
<h3 style="margin: 0; font-size: 16px">
|
|||
|
|
ماتریس دسترسیهای:
|
|||
|
|
<span style="color: #2563eb">{{ selectedRole?.name || 'انتخاب کنید' }}</span>
|
|||
|
|
</h3>
|
|||
|
|
<span style="font-size: 12px; color: #64748b">
|
|||
|
|
{{ selectedRole?.code }}
|
|||
|
|
{{ selectedRole?.isSystem ? '• نقش سیستمی پایه' : '• نقش سفارشی' }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<a-button
|
|||
|
|
v-if="authStore.can('roles:manage') && !selectedRole?.isSystem"
|
|||
|
|
type="primary"
|
|||
|
|
:loading="saving"
|
|||
|
|
:disabled="!isRoleDirty"
|
|||
|
|
@click="saveRolePermissions"
|
|||
|
|
>
|
|||
|
|
<template #icon><SaveOutlined /></template>
|
|||
|
|
ذخیره تغییرات مجوزها
|
|||
|
|
</a-button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div v-if="selectedRole?.code === 'SUPER_ADMIN'" class="super-admin-notice">
|
|||
|
|
<SafetyCertificateOutlined style="font-size: 20px; color: #7c3aed" />
|
|||
|
|
<div>
|
|||
|
|
<strong>نقش مدیر ارشد سامانه (Super Admin)</strong>
|
|||
|
|
<p style="margin: 4px 0 0; color: #6b7280; font-size: 13px">
|
|||
|
|
تمامی مجوزهای سامانه به صورت خودکار و دائمی برای مدیر ارشد فعال است و نیازی به تغییر دستی ندارد.
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="modules-grid">
|
|||
|
|
<div
|
|||
|
|
v-for="(perms, modKey) in groupedPermissions"
|
|||
|
|
:key="modKey"
|
|||
|
|
class="module-perm-block"
|
|||
|
|
>
|
|||
|
|
<div class="module-header">
|
|||
|
|
<div class="module-title">
|
|||
|
|
<h4>{{ moduleLabels[modKey] || modKey }}</h4>
|
|||
|
|
<span class="count-label">
|
|||
|
|
({{ toFarsiDigits(perms.filter((p) => activeRolePermIds.includes(p.id)).length) }} از {{ toFarsiDigits(perms.length) }})
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<a-button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
:disabled="selectedRole?.code === 'SUPER_ADMIN' || !authStore.can('roles:manage')"
|
|||
|
|
@click="toggleModuleAll(modKey)"
|
|||
|
|
>
|
|||
|
|
تغییر انتخاب همه
|
|||
|
|
</a-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="perms-list">
|
|||
|
|
<div
|
|||
|
|
v-for="p in perms"
|
|||
|
|
:key="p.id"
|
|||
|
|
class="perm-checkbox-item"
|
|||
|
|
:class="{ 'is-checked': activeRolePermIds.includes(p.id) }"
|
|||
|
|
@click="togglePermission(p.id)"
|
|||
|
|
>
|
|||
|
|
<a-checkbox
|
|||
|
|
:checked="activeRolePermIds.includes(p.id)"
|
|||
|
|
:disabled="selectedRole?.code === 'SUPER_ADMIN' || !authStore.can('roles:manage')"
|
|||
|
|
>
|
|||
|
|
<strong class="perm-name">{{ p.name }}</strong>
|
|||
|
|
</a-checkbox>
|
|||
|
|
<div class="perm-code">{{ p.code }}</div>
|
|||
|
|
<div class="perm-desc" v-if="p.description">{{ p.description }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</a-card>
|
|||
|
|
</a-col>
|
|||
|
|
</a-row>
|
|||
|
|
</a-spin>
|
|||
|
|
|
|||
|
|
<!-- Create Role Modal -->
|
|||
|
|
<a-modal
|
|||
|
|
v-model:open="isCreateModalOpen"
|
|||
|
|
title="تعریف نقش سازمانی جدید"
|
|||
|
|
okText="ایجاد نقش"
|
|||
|
|
cancelText="انصراف"
|
|||
|
|
:confirmLoading="isSubmittingNewRole"
|
|||
|
|
@ok="handleCreateRole"
|
|||
|
|
>
|
|||
|
|
<a-form layout="vertical" style="margin-top: 16px">
|
|||
|
|
<a-form-item label="نام فارسی نقش" required>
|
|||
|
|
<a-input
|
|||
|
|
v-model:value="newRoleForm.name"
|
|||
|
|
placeholder="مثال: سرپرست بازاریابی"
|
|||
|
|
size="large"
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
|
|||
|
|
<a-form-item label="کد انگلیسی نقش (فقط حروف بزرگ و خط زیر)" required>
|
|||
|
|
<a-input
|
|||
|
|
v-model:value="newRoleForm.code"
|
|||
|
|
placeholder="MARKETING_SUPERVISOR"
|
|||
|
|
dir="ltr"
|
|||
|
|
size="large"
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
|
|||
|
|
<a-form-item label="توضیحات و شرح اختیارات">
|
|||
|
|
<a-textarea
|
|||
|
|
v-model:value="newRoleForm.description"
|
|||
|
|
placeholder="شرح وظایف و اختیارات این نقش..."
|
|||
|
|
:rows="3"
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
</a-form>
|
|||
|
|
</a-modal>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.roles-matrix-page {
|
|||
|
|
padding-bottom: 24px;
|
|||
|
|
}
|
|||
|
|
.page-header-title {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
.title {
|
|||
|
|
font-size: 22px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #1e293b;
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
.subtitle {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #64748b;
|
|||
|
|
margin-top: 4px;
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
.roles-sidebar-card,
|
|||
|
|
.matrix-card {
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
.roles-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
.role-item {
|
|||
|
|
padding: 12px;
|
|||
|
|
border: 1px solid #e2e8f0;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
.role-item:hover {
|
|||
|
|
border-color: #93c5fd;
|
|||
|
|
background: #f8fafc;
|
|||
|
|
}
|
|||
|
|
.role-item.is-active {
|
|||
|
|
border-color: #2563eb;
|
|||
|
|
background: #eff6ff;
|
|||
|
|
}
|
|||
|
|
.role-item-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
.role-code {
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: #94a3b8;
|
|||
|
|
direction: ltr;
|
|||
|
|
text-align: right;
|
|||
|
|
margin: 2px 0 6px;
|
|||
|
|
}
|
|||
|
|
.role-desc {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #64748b;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
}
|
|||
|
|
.role-footer {
|
|||
|
|
margin-top: 10px;
|
|||
|
|
padding-top: 8px;
|
|||
|
|
border-top: 1px dashed #e2e8f0;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #64748b;
|
|||
|
|
}
|
|||
|
|
.matrix-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
.super-admin-notice {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
align-items: center;
|
|||
|
|
background: #f5f3ff;
|
|||
|
|
border: 1px solid #ddd6fe;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
.modules-grid {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
.module-perm-block {
|
|||
|
|
border: 1px solid #e2e8f0;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.module-header {
|
|||
|
|
background: #f8fafc;
|
|||
|
|
padding: 10px 14px;
|
|||
|
|
border-bottom: 1px solid #e2e8f0;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
.module-title h4 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
display: inline-block;
|
|||
|
|
color: #1e293b;
|
|||
|
|
}
|
|||
|
|
.count-label {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #64748b;
|
|||
|
|
margin-right: 6px;
|
|||
|
|
}
|
|||
|
|
.perms-list {
|
|||
|
|
padding: 12px;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
.perm-checkbox-item {
|
|||
|
|
padding: 8px 10px;
|
|||
|
|
border: 1px solid #f1f5f9;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
transition: all 0.15s ease;
|
|||
|
|
cursor: pointer;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
.perm-checkbox-item:hover {
|
|||
|
|
border-color: #cbd5e1;
|
|||
|
|
}
|
|||
|
|
.perm-checkbox-item.is-checked {
|
|||
|
|
background: #eff6ff;
|
|||
|
|
border-color: #bfdbfe;
|
|||
|
|
}
|
|||
|
|
.perm-name {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #1e293b;
|
|||
|
|
}
|
|||
|
|
.perm-code {
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: #94a3b8;
|
|||
|
|
direction: ltr;
|
|||
|
|
text-align: right;
|
|||
|
|
margin: 2px 24px 0 0;
|
|||
|
|
}
|
|||
|
|
.perm-desc {
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: #64748b;
|
|||
|
|
margin: 2px 24px 0 0;
|
|||
|
|
}
|
|||
|
|
</style>
|