1859 lines
53 KiB
Vue
1859 lines
53 KiB
Vue
<template>
|
||
<section
|
||
class="system-permission-page"
|
||
:class="{ 'system-permission-page--maintenance': permissionMainTab === 'maintenance' }"
|
||
@click="closeContextMenu"
|
||
>
|
||
<header class="system-permission-hero compact-hero page-hero-card system-permission-hero-card">
|
||
<div class="page-hero-copy">
|
||
<p class="eyebrow">系统管理</p>
|
||
<div class="system-permission-title-row">
|
||
<h3>系统权限管理</h3>
|
||
<div class="system-permission-main-tabs admin-tab-switcher" role="tablist" aria-label="系统权限管理页签">
|
||
<button
|
||
type="button"
|
||
:class="{ active: permissionMainTab === 'org' }"
|
||
@click="permissionMainTab = 'org'"
|
||
>
|
||
组织架构
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ active: permissionMainTab === 'maintenance' }"
|
||
@click="permissionMainTab = 'maintenance'"
|
||
>
|
||
角色维护
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p class="page-hero-subtitle">组织架构负责公司层级、人员授权和系统账号;角色维护负责菜单权限和业务身份。</p>
|
||
</div>
|
||
</header>
|
||
|
||
<section
|
||
v-if="permissionMainTab === 'org'"
|
||
class="system-permission-map-card full-width-card"
|
||
:class="{ 'tree-layout-card': orgViewMode === 'tree' }"
|
||
>
|
||
<div class="section-head">
|
||
<div>
|
||
<p class="eyebrow">组织架构</p>
|
||
<h3>权限组织架构</h3>
|
||
<p class="permission-note">脑图和组织树共用同一套组织数据;脑图模式下员工节点默认收拢,人员授权和移除都在组织架构里处理。</p>
|
||
</div>
|
||
<div class="mindmap-card-actions">
|
||
<div class="org-view-switch" role="tablist" aria-label="组织架构展示模式">
|
||
<button
|
||
type="button"
|
||
:class="{ active: orgViewMode === 'mindmap' }"
|
||
@click.stop="orgViewMode = 'mindmap'"
|
||
>
|
||
脑图模式
|
||
</button>
|
||
<button
|
||
type="button"
|
||
:class="{ active: orgViewMode === 'tree' }"
|
||
@click.stop="orgViewMode = 'tree'"
|
||
>
|
||
组织树模式
|
||
</button>
|
||
</div>
|
||
<span class="status-pill">{{ orgNodeCount }} 个组织节点</span>
|
||
<div v-if="orgViewMode === 'mindmap'" class="mindmap-toolbar" aria-label="脑图工具栏">
|
||
<button type="button" title="放大" @click.stop="zoomMindMap(0.1)">放大</button>
|
||
<button type="button" title="缩小" @click.stop="zoomMindMap(-0.1)">缩小</button>
|
||
<button type="button" title="一屏展示" @click.stop="fitMindMapToScreen">一屏展示</button>
|
||
<button type="button" title="刷新" @click.stop="refreshMindMap">刷新</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<OrgMindMap
|
||
v-if="orgViewMode === 'mindmap'"
|
||
:nodes="orgNodes"
|
||
:zoom-scale="mindMapScale"
|
||
:fit-request-key="mindMapFitRequestKey"
|
||
@select-node="selectOrgNode"
|
||
@node-contextmenu="openContextMenu"
|
||
@toggle-employee-leaves="selectOrgNode"
|
||
@fit-scale="applyMindMapFitScale"
|
||
/>
|
||
<OrgPermissionTree
|
||
v-else
|
||
:nodes="orgNodes"
|
||
:selected-node-id="selectedOrgNodeId"
|
||
@select-node="selectOrgNode"
|
||
@node-contextmenu="openContextMenu"
|
||
@add-employee="openOrgEmployeeDrawer"
|
||
@authorize-employee="openAuthorizeDrawer"
|
||
@remove-employee="removeOrgEmployee"
|
||
/>
|
||
</section>
|
||
|
||
<section v-if="permissionMainTab === 'maintenance'" class="system-permission-maintenance-card full-width-card">
|
||
<div class="section-head">
|
||
<div>
|
||
<p class="eyebrow">角色权限</p>
|
||
<h3>角色维护</h3>
|
||
<p class="permission-note">角色用于配置菜单权限和业务身份;人员授权请回到组织架构中对人员节点操作。</p>
|
||
</div>
|
||
<div class="row-actions">
|
||
<button
|
||
class="primary-button"
|
||
type="button"
|
||
@click="openRoleDrawer()"
|
||
>
|
||
新增角色
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="maintenance-table-panel">
|
||
<div class="table-wrap">
|
||
<table class="data-table compact-table">
|
||
<thead>
|
||
<tr>
|
||
<th>角色名称</th>
|
||
<th>角色编码</th>
|
||
<th>系统人员数</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="role in paginatedRoles" :key="role.role_id || role.role_code || role.role_name">
|
||
<td>{{ role.role_name || role.name || "未命名角色" }}</td>
|
||
<td>{{ role.role_code || "未设置编码" }}</td>
|
||
<td>{{ role.user_count || 0 }} 人</td>
|
||
<td><StatusBadge :value="role.status" /></td>
|
||
<td class="action-row">
|
||
<button class="ghost-button" type="button" @click="openRoleDrawer(role)">编辑</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!roles.length">
|
||
<td colspan="5" class="empty-row">暂无角色数据</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<PaginationBar v-model:page="rolePage" v-model:page-size="rolePageSize" :total="roles.length" />
|
||
</div>
|
||
</section>
|
||
|
||
<div
|
||
v-if="contextMenu.visible"
|
||
class="org-context-menu"
|
||
:style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }"
|
||
@click.stop
|
||
>
|
||
<button
|
||
v-for="action in contextMenuActions"
|
||
:key="action.key"
|
||
type="button"
|
||
@click="runNodeAction(action.key, contextMenu.node)"
|
||
>
|
||
{{ action.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="showAuthorizeDrawer" class="workflow-drawer-mask system-permission-drawer-mask" @click.self="showAuthorizeDrawer = false">
|
||
<aside class="form-drawer system-permission-drawer authorize-drawer">
|
||
<div class="drawer-header authorize-drawer-header">
|
||
<div>
|
||
<p class="eyebrow">人员授权</p>
|
||
<h2>{{ authorizingEmployee?.employee_name || "人员授权" }}</h2>
|
||
<p class="authorize-header-note">为该人员创建或更新系统账号,并分配可访问的系统角色。</p>
|
||
</div>
|
||
<button class="ghost-button authorize-close-button" type="button" @click="showAuthorizeDrawer = false">关闭窗口</button>
|
||
</div>
|
||
|
||
<section class="authorize-profile-card" aria-label="授权对象">
|
||
<div class="authorize-avatar">{{ authorizingEmployeeInitial }}</div>
|
||
<div class="authorize-profile-main">
|
||
<strong>{{ authorizingEmployee?.employee_name || "未命名人员" }}</strong>
|
||
<span>{{ authorizingOrganizationPath }}</span>
|
||
</div>
|
||
<div class="authorize-profile-meta">
|
||
<span>电话 / 账号:{{ authorizingEmployeePhone || "未设置电话号码" }}</span>
|
||
<span>账号状态:<StatusBadge :value="authorizingSystemUser?.status" :text="authorizingSystemUser ? accountStatusLabel(authorizingSystemUser.status) : '未创建账号'" /></span>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="authorize-section authorize-role-section" aria-label="角色授权">
|
||
<div class="authorize-section-head">
|
||
<div>
|
||
<p class="eyebrow">角色授权</p>
|
||
<h3>选择系统角色</h3>
|
||
</div>
|
||
<span class="authorize-selected-count">已选 {{ selectedAuthorizeRoles.length }} 个角色</span>
|
||
</div>
|
||
<div class="authorize-selected-strip">
|
||
<span v-if="selectedAuthorizeRoleNames.length">{{ selectedAuthorizeRoleNames.join("、") }}</span>
|
||
<span v-else>尚未选择角色,至少选择一个角色后才能保存。</span>
|
||
</div>
|
||
<div class="authorize-role-grid">
|
||
<button
|
||
v-for="role in roles"
|
||
:key="role.role_id"
|
||
type="button"
|
||
class="authorize-role-card"
|
||
:class="{ active: isAuthorizeRoleSelected(role.role_id), danger: isHighRiskRole(role) }"
|
||
@click="toggleAuthorizeRole(role.role_id)"
|
||
>
|
||
<span class="authorize-role-check" aria-hidden="true">{{ isAuthorizeRoleSelected(role.role_id) ? "✓" : "" }}</span>
|
||
<span class="authorize-role-body">
|
||
<strong>{{ roleDisplayName(role) }}</strong>
|
||
<small>{{ roleDisplayCode(role) }}</small>
|
||
<em>{{ roleRiskText(role) }}</em>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="authorize-section authorize-account-section" aria-label="账号设置">
|
||
<div class="authorize-section-head">
|
||
<div>
|
||
<p class="eyebrow">账号设置</p>
|
||
<h3>{{ authorizingSystemUser ? "账号状态" : "创建账号" }}</h3>
|
||
</div>
|
||
</div>
|
||
<label v-if="!authorizingSystemUser" class="form-field authorize-password-field">
|
||
<span>初始密码</span>
|
||
<input v-model="authorizeForm.password" type="password" placeholder="首次创建账号必填,最少6位" />
|
||
</label>
|
||
<div v-else class="authorize-account-note">
|
||
该人员已创建系统账号;本次仅更新角色和账号启停状态,如需修改密码请使用右上角账号重置入口。
|
||
</div>
|
||
<div class="authorize-status-switch" role="group" aria-label="账号状态">
|
||
<button type="button" :class="{ active: authorizeForm.status === 'ACTIVE' }" @click="authorizeForm.status = 'ACTIVE'">启用账号</button>
|
||
<button type="button" :class="{ active: authorizeForm.status === 'INACTIVE' }" @click="authorizeForm.status = 'INACTIVE'">停用账号</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="authorize-save-summary">
|
||
将为 <strong>{{ authorizingEmployee?.employee_name || "该人员" }}</strong>
|
||
授权 <strong>{{ selectedAuthorizeRoles.length }}</strong> 个角色,账号状态为
|
||
<strong>{{ accountStatusLabel(authorizeForm.status) }}</strong>
|
||
</div>
|
||
|
||
<div class="drawer-actions authorize-actions">
|
||
<button class="primary-button" type="button" :disabled="!authorizeForm.role_ids.length" @click="submitAuthorizeUser">保存授权</button>
|
||
<button class="ghost-button" type="button" @click="showAuthorizeDrawer = false">取消</button>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<div v-if="showRoleDrawer" class="workflow-drawer-mask system-permission-drawer-mask" @click.self="showRoleDrawer = false">
|
||
<aside class="form-drawer system-permission-drawer">
|
||
<div class="drawer-header">
|
||
<div>
|
||
<p class="eyebrow">角色权限</p>
|
||
<h2>{{ editingRole ? "编辑角色" : "新增角色" }}</h2>
|
||
</div>
|
||
<button class="ghost-button" type="button" @click="showRoleDrawer = false">关闭</button>
|
||
</div>
|
||
|
||
<label class="form-field">
|
||
<span>角色编码</span>
|
||
<input v-model="roleForm.role_code" type="text" :readonly="Boolean(editingRole)" placeholder="请输入角色编码(大写字母、数字或下划线)" />
|
||
</label>
|
||
<label class="form-field">
|
||
<span>角色名称</span>
|
||
<input v-model="roleForm.role_name" type="text" placeholder="请输入角色名称" />
|
||
</label>
|
||
<label class="form-field">
|
||
<span>角色范围</span>
|
||
<select v-model="roleForm.role_scope">
|
||
<option value="SYSTEM">系统角色</option>
|
||
<option v-if="roleForm.role_scope && roleForm.role_scope !== 'SYSTEM'" :value="roleForm.role_scope">自定义范围</option>
|
||
</select>
|
||
</label>
|
||
<label class="form-field">
|
||
<span>状态</span>
|
||
<select v-model="roleForm.status">
|
||
<option value="ACTIVE">启用</option>
|
||
<option value="INACTIVE">停用</option>
|
||
</select>
|
||
</label>
|
||
<label class="form-field">
|
||
<span>备注</span>
|
||
<textarea v-model="roleForm.remark" rows="3" placeholder="请输入备注"></textarea>
|
||
</label>
|
||
|
||
<div class="form-field">
|
||
<span>菜单权限</span>
|
||
<div class="permission-tree">
|
||
<template v-for="group in permissionTree" :key="group.permission_code">
|
||
<label class="permission-check" :class="{ partial: isPermissionIndeterminate(group) }">
|
||
<input
|
||
type="checkbox"
|
||
:checked="isPermissionChecked(group)"
|
||
:indeterminate="isPermissionIndeterminate(group)"
|
||
@change="togglePermissionNode(group, $event.target.checked)"
|
||
/>
|
||
<strong>{{ group.permission_name }}</strong>
|
||
<span v-if="isPermissionIndeterminate(group)" class="partial-note">部分已选</span>
|
||
</label>
|
||
<label
|
||
v-for="child in group.children || []"
|
||
:key="child.permission_code"
|
||
class="permission-check child"
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
:checked="isPermissionChecked(child)"
|
||
:indeterminate="isPermissionIndeterminate(child)"
|
||
@change="togglePermissionNode(child, $event.target.checked)"
|
||
/>
|
||
<span>{{ child.permission_name }}</span>
|
||
</label>
|
||
</template>
|
||
<p v-if="!permissionTree.length" class="empty-state">暂无菜单权限数据。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="drawer-actions">
|
||
<button class="primary-button" type="button" @click="submitRole">保存</button>
|
||
<button class="ghost-button" type="button" @click="showRoleDrawer = false">取消</button>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<div v-if="showOrgDrawer" class="workflow-drawer-mask system-permission-drawer-mask" @click.self="showOrgDrawer = false">
|
||
<aside class="form-drawer system-permission-drawer">
|
||
<div class="drawer-header">
|
||
<div>
|
||
<p class="eyebrow">组织脑图</p>
|
||
<h2>{{ orgDrawerTitle }}</h2>
|
||
</div>
|
||
<button class="ghost-button" type="button" @click="showOrgDrawer = false">关闭</button>
|
||
</div>
|
||
|
||
<div v-if="orgDrawerMode === 'create' || orgDrawerMode === 'rename'">
|
||
<p v-if="orgDrawerMode === 'create'" class="permission-note">
|
||
将在「{{ orgForm.parent_label }}」下新增{{ nodeTypeLabel(orgForm.node_type) }},只需要填写名称。
|
||
</p>
|
||
<label class="form-field">
|
||
<span>{{ nodeTypeLabel(orgForm.node_type) }}名称</span>
|
||
<input v-model="orgForm.node_label" type="text" placeholder="请输入名称" />
|
||
</label>
|
||
</div>
|
||
|
||
<div v-else-if="orgDrawerMode === 'manager'">
|
||
<p class="permission-note">支持绑定多位主管,直接点击人员卡片即可选中或取消,不需要按键盘组合键。</p>
|
||
<div class="form-field">
|
||
<span>多位主管</span>
|
||
<div class="selected-manager-tags">
|
||
<span
|
||
v-for="manager in selectedManagerOptions"
|
||
:key="manager.employee_id"
|
||
class="selected-manager-tag"
|
||
>
|
||
{{ manager.employee_name }}
|
||
<button type="button" @click="toggleOrgManager(manager.employee_id)">×</button>
|
||
</span>
|
||
<em v-if="!selectedManagerOptions.length">暂未选择主管</em>
|
||
</div>
|
||
<label class="manager-search-box">
|
||
<span>搜索主管</span>
|
||
<input
|
||
v-model.trim="managerSearchKeyword"
|
||
type="search"
|
||
placeholder="搜索主管姓名或手机号,也可输入部门/岗位"
|
||
/>
|
||
</label>
|
||
<div class="manager-option-grid">
|
||
<button
|
||
v-for="employee in filteredManagerOptions"
|
||
:key="employee.employee_id"
|
||
type="button"
|
||
:class="{ active: isOrgManagerSelected(employee.employee_id) }"
|
||
@click="toggleOrgManager(employee.employee_id)"
|
||
>
|
||
<strong>{{ employee.employee_name }}</strong>
|
||
<span>{{ employee.mobile || "无电话" }}</span>
|
||
<span>{{ employee.dept_name || "未分部门" }}{{ employee.job_title ? ` · ${employee.job_title}` : "" }}</span>
|
||
</button>
|
||
</div>
|
||
<p v-if="!filteredManagerOptions.length" class="manager-empty-state">
|
||
{{ managerSearchKeyword ? "没有匹配的主管,请换个关键词试试。" : "暂无可选人员。" }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="orgDrawerMode === 'employee'">
|
||
<p class="permission-note">新增人员是把基础资料人员挂到当前脑图节点,不会修改小程序人员主数据。</p>
|
||
<label class="form-field">
|
||
<span>人员</span>
|
||
<select v-model="orgEmployeeForm.employee_id">
|
||
<option value="">请选择人员</option>
|
||
<option v-for="employee in employeeOptions" :key="employee.employee_id" :value="employee.employee_id">
|
||
{{ employee.employee_name }} · {{ employee.mobile || "无电话" }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="drawer-actions">
|
||
<button class="primary-button" type="button" @click="submitOrgNode">保存</button>
|
||
<button class="ghost-button" type="button" @click="showOrgDrawer = false">取消</button>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
||
|
||
import OrgMindMap from "../components/OrgMindMap.vue";
|
||
import OrgPermissionTree from "../components/OrgPermissionTree.vue";
|
||
import PaginationBar from "../components/PaginationBar.vue";
|
||
import StatusBadge from "../components/StatusBadge.vue";
|
||
import { deleteResource, fetchResource, postResource, putResource } from "../services/api";
|
||
import { notifyError, notifySuccess } from "../utils/notifications";
|
||
import { usePagination } from "../utils/pagination";
|
||
|
||
const orgTree = ref([]);
|
||
const roles = ref([]);
|
||
const users = ref([]);
|
||
const employeeOptions = ref([]);
|
||
const permissionTree = ref([]);
|
||
const permissionMainTab = ref("org");
|
||
const orgViewMode = ref("mindmap");
|
||
const selectedNode = ref(null);
|
||
const mindMapScale = ref(1);
|
||
const mindMapFitRequestKey = ref(0);
|
||
const showAuthorizeDrawer = ref(false);
|
||
const showRoleDrawer = ref(false);
|
||
const showOrgDrawer = ref(false);
|
||
const editingRole = ref(null);
|
||
const editingOrgNode = ref(null);
|
||
const orgDrawerMode = ref("create");
|
||
const managerSearchKeyword = ref("");
|
||
const authorizingEmployee = ref(null);
|
||
|
||
const contextMenu = reactive({
|
||
visible: false,
|
||
x: 0,
|
||
y: 0,
|
||
node: null
|
||
});
|
||
|
||
const authorizeForm = reactive({
|
||
role_ids: [],
|
||
password: "",
|
||
status: "ACTIVE"
|
||
});
|
||
|
||
const roleForm = reactive({
|
||
role_code: "",
|
||
role_name: "",
|
||
role_scope: "SYSTEM",
|
||
status: "ACTIVE",
|
||
remark: "",
|
||
permission_codes: []
|
||
});
|
||
|
||
const orgForm = reactive({
|
||
parent_id: "",
|
||
parent_label: "",
|
||
node_type: "DEPARTMENT",
|
||
node_label: "",
|
||
dept_code: "",
|
||
dept_type: "ADMIN",
|
||
manager_employee_id: "",
|
||
manager_employee_ids: [],
|
||
sort_no: 0,
|
||
remark: "",
|
||
status: "ACTIVE"
|
||
});
|
||
|
||
const orgEmployeeForm = reactive({
|
||
employee_id: "",
|
||
sort_no: 0
|
||
});
|
||
|
||
const orgNodes = computed(() => {
|
||
if (Array.isArray(orgTree.value)) {
|
||
return orgTree.value;
|
||
}
|
||
return orgTree.value ? [orgTree.value] : [];
|
||
});
|
||
|
||
const orgNodeCount = computed(() => flattenOrgNodes(orgNodes.value).length);
|
||
const selectedOrgNodeId = computed(() => selectedNode.value?.node_id || orgNodes.value[0]?.node_id || "");
|
||
|
||
const authorizingSystemUser = computed(() => {
|
||
const employeeId = authorizingEmployee.value?.employee_id;
|
||
if (!employeeId) {
|
||
return null;
|
||
}
|
||
return users.value.find((user) => String(user.employee_id) === String(employeeId)) || null;
|
||
});
|
||
|
||
const authorizingEmployeePhone = computed(() =>
|
||
authorizingEmployee.value?.mobile || authorizingSystemUser.value?.mobile || authorizingSystemUser.value?.username || ""
|
||
);
|
||
|
||
const authorizingOrganizationPath = computed(() =>
|
||
authorizingEmployee.value?.organization_path || authorizingEmployee.value?.parent_node_label || "未标记组织"
|
||
);
|
||
|
||
const authorizingEmployeeInitial = computed(() => {
|
||
const name = authorizingEmployee.value?.employee_name || authorizingEmployee.value?.display_name || authorizingEmployee.value?.username || "人";
|
||
return String(name).slice(0, 1);
|
||
});
|
||
|
||
const selectedAuthorizeRoles = computed(() => {
|
||
const selectedIds = new Set(authorizeForm.role_ids.map(String));
|
||
return roles.value.filter((role) => selectedIds.has(String(role.role_id)));
|
||
});
|
||
|
||
const selectedAuthorizeRoleNames = computed(() => selectedAuthorizeRoles.value.map((role) => roleDisplayName(role)));
|
||
|
||
const orgDrawerTitle = computed(() => {
|
||
if (orgDrawerMode.value === "rename") {
|
||
return "重命名";
|
||
}
|
||
if (orgDrawerMode.value === "manager") {
|
||
return "添加该节点的主管";
|
||
}
|
||
if (orgDrawerMode.value === "employee") {
|
||
return "新增人员";
|
||
}
|
||
return `新增${nodeTypeLabel(orgForm.node_type)}`;
|
||
});
|
||
|
||
const contextMenuActions = computed(() => (contextMenu.node ? nodeActions(contextMenu.node) : []));
|
||
const selectedManagerOptions = computed(() => {
|
||
const selectedIds = new Set(orgForm.manager_employee_ids.map(String));
|
||
return employeeOptions.value.filter((employee) => selectedIds.has(String(employee.employee_id)));
|
||
});
|
||
|
||
const filteredManagerOptions = computed(() => {
|
||
const keyword = managerSearchKeyword.value.trim().toLowerCase();
|
||
if (!keyword) {
|
||
return employeeOptions.value;
|
||
}
|
||
return employeeOptions.value.filter((employee) => {
|
||
const searchableText = [
|
||
employee.employee_name,
|
||
employee.mobile,
|
||
employee.phone,
|
||
employee.dept_name,
|
||
employee.job_title,
|
||
employee.position,
|
||
employee.employee_code
|
||
]
|
||
.filter(Boolean)
|
||
.join(" ")
|
||
.toLowerCase();
|
||
return searchableText.includes(keyword);
|
||
});
|
||
});
|
||
|
||
const roleRowsForTable = computed(() => roles.value);
|
||
|
||
const {
|
||
page: rolePage,
|
||
pageSize: rolePageSize,
|
||
rows: paginatedRoles
|
||
} = usePagination(roleRowsForTable);
|
||
|
||
async function loadAll() {
|
||
const [orgRows, roleRows, userRows, employeeRows, menuPermissionRows] = await Promise.all([
|
||
fetchResource("/system-permissions/org-tree", []),
|
||
fetchResource("/system-permissions/roles", []),
|
||
fetchResource("/system-permissions/users", []),
|
||
fetchResource("/system-permissions/employee-options", []),
|
||
fetchResource("/system-permissions/menu-permission-tree", [])
|
||
]);
|
||
orgTree.value = Array.isArray(orgRows) ? orgRows : [];
|
||
roles.value = Array.isArray(roleRows) ? roleRows : [];
|
||
users.value = Array.isArray(userRows) ? userRows : [];
|
||
employeeOptions.value = Array.isArray(employeeRows) ? employeeRows : [];
|
||
permissionTree.value = Array.isArray(menuPermissionRows) ? menuPermissionRows : [];
|
||
}
|
||
|
||
function flattenOrgNodes(nodes) {
|
||
const result = [];
|
||
for (const node of nodes || []) {
|
||
result.push(node);
|
||
result.push(...flattenOrgNodes(node.children || []));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function selectOrgNode(node) {
|
||
selectedNode.value = node;
|
||
}
|
||
|
||
function zoomMindMap(delta) {
|
||
const nextScale = Number((mindMapScale.value + delta).toFixed(1));
|
||
setMindMapScale(nextScale);
|
||
}
|
||
|
||
function setMindMapScale(scale) {
|
||
const numericScale = Number(scale || 1);
|
||
mindMapScale.value = Math.min(1.8, Math.max(0.12, numericScale));
|
||
}
|
||
|
||
function applyMindMapFitScale(scale) {
|
||
const numericScale = Number(scale || 1);
|
||
mindMapScale.value = Math.min(1, Math.max(0.12, numericScale));
|
||
}
|
||
|
||
async function requestMindMapFitToScreen({ showToast = false, closeMenu = false } = {}) {
|
||
if (closeMenu) {
|
||
closeContextMenu();
|
||
}
|
||
await nextTick();
|
||
if (permissionMainTab.value !== "org" || orgViewMode.value !== "mindmap") {
|
||
return;
|
||
}
|
||
mindMapFitRequestKey.value += 1;
|
||
if (showToast) {
|
||
notifySuccess("脑图已按当前展开状态适配一屏");
|
||
}
|
||
}
|
||
|
||
function fitMindMapToScreen() {
|
||
requestMindMapFitToScreen({ showToast: true, closeMenu: true });
|
||
}
|
||
|
||
async function refreshMindMap() {
|
||
closeContextMenu();
|
||
await loadAll();
|
||
notifySuccess("脑图已刷新");
|
||
}
|
||
|
||
function nodeTypeLabel(type) {
|
||
return {
|
||
COMPANY: "总公司",
|
||
BRANCH: "分公司",
|
||
DEPARTMENT: "部门",
|
||
GROUP: "小组",
|
||
EMPLOYEE: "人员"
|
||
}[type] || "组织";
|
||
}
|
||
|
||
function isOrgManagerSelected(employeeId) {
|
||
return orgForm.manager_employee_ids.map(String).includes(String(employeeId));
|
||
}
|
||
|
||
function toggleOrgManager(employeeId) {
|
||
const targetId = String(employeeId);
|
||
if (isOrgManagerSelected(targetId)) {
|
||
orgForm.manager_employee_ids = orgForm.manager_employee_ids.filter((id) => String(id) !== targetId);
|
||
} else {
|
||
orgForm.manager_employee_ids = [...orgForm.manager_employee_ids, targetId];
|
||
}
|
||
}
|
||
|
||
function defaultDeptType(type) {
|
||
return {
|
||
COMPANY: "ADMIN",
|
||
BRANCH: "ADMIN",
|
||
DEPARTMENT: "ADMIN",
|
||
GROUP: "ADMIN"
|
||
}[type] || "ADMIN";
|
||
}
|
||
|
||
function isAuthorizeRoleSelected(roleId) {
|
||
return authorizeForm.role_ids.map(String).includes(String(roleId));
|
||
}
|
||
|
||
function toggleAuthorizeRole(roleId) {
|
||
const targetId = String(roleId);
|
||
if (isAuthorizeRoleSelected(targetId)) {
|
||
authorizeForm.role_ids = authorizeForm.role_ids.filter((id) => String(id) !== targetId);
|
||
} else {
|
||
authorizeForm.role_ids = [...authorizeForm.role_ids, targetId];
|
||
}
|
||
}
|
||
|
||
function roleDisplayName(role) {
|
||
return role?.role_name || role?.name || role?.role_code || "未命名角色";
|
||
}
|
||
|
||
function roleDisplayCode(role) {
|
||
return role?.role_code ? `角色编码:${role.role_code}` : "未设置角色编码";
|
||
}
|
||
|
||
function isHighRiskRole(role) {
|
||
const text = [role?.role_code, roleDisplayName(role)].filter(Boolean).join(" ").toUpperCase();
|
||
return text.includes("ADMIN") || text.includes("超级") || text.includes("管理员");
|
||
}
|
||
|
||
function roleRiskText(role) {
|
||
if (isHighRiskRole(role)) {
|
||
return "高权限角色,请谨慎授权";
|
||
}
|
||
return "业务角色,可按岗位职责授权";
|
||
}
|
||
|
||
function accountStatusLabel(status) {
|
||
if (status === "INACTIVE") {
|
||
return "停用";
|
||
}
|
||
return "启用";
|
||
}
|
||
|
||
function nodeActions(node) {
|
||
if (!node) {
|
||
return [];
|
||
}
|
||
if (node.employee_id) {
|
||
return [
|
||
{ key: "authorizeEmployee", label: "授权" },
|
||
{ key: "removeEmployee", label: "移除" }
|
||
];
|
||
}
|
||
const actions = [
|
||
{ key: "rename", label: "重命名" },
|
||
{ key: "manager", label: "添加该节点的主管" }
|
||
];
|
||
if (node.node_type === "COMPANY") {
|
||
actions.push({ key: "addBranch", label: "新增分公司节点" });
|
||
actions.push({ key: "addDepartment", label: "新增直属部门" });
|
||
}
|
||
if (node.node_type === "BRANCH") {
|
||
actions.push({ key: "addDepartment", label: "新增部门节点" });
|
||
}
|
||
if (node.node_type === "DEPARTMENT") {
|
||
actions.push({ key: "addGroup", label: "新增小组节点" });
|
||
actions.push({ key: "addEmployee", label: "新增人员" });
|
||
}
|
||
if (node.node_type === "GROUP") {
|
||
actions.push({ key: "addEmployee", label: "新增人员" });
|
||
}
|
||
if (node.dept_code !== "ORG_ROOT") {
|
||
actions.push({ key: "delete", label: "删除节点" });
|
||
}
|
||
return actions;
|
||
}
|
||
|
||
function openContextMenu(eventPayload) {
|
||
selectedNode.value = eventPayload.node;
|
||
const menuWidth = 220;
|
||
const menuHeight = 280;
|
||
const viewportWidth = window.innerWidth || 1280;
|
||
const viewportHeight = window.innerHeight || 720;
|
||
contextMenu.visible = true;
|
||
contextMenu.x = Math.max(12, Math.min(Number(eventPayload.x || 12), viewportWidth - menuWidth - 12));
|
||
contextMenu.y = Math.max(12, Math.min(Number(eventPayload.y || 12), viewportHeight - menuHeight - 12));
|
||
contextMenu.node = eventPayload.node;
|
||
}
|
||
|
||
function closeContextMenu() {
|
||
contextMenu.visible = false;
|
||
}
|
||
|
||
function runNodeAction(actionKey, node) {
|
||
closeContextMenu();
|
||
if (!node) {
|
||
return;
|
||
}
|
||
if (actionKey === "rename") {
|
||
openOrgRenameDrawer(node);
|
||
} else if (actionKey === "manager") {
|
||
openOrgManagerDrawer(node);
|
||
} else if (actionKey === "addBranch") {
|
||
openOrgCreateDrawer(node, "BRANCH");
|
||
} else if (actionKey === "addDepartment") {
|
||
openOrgCreateDrawer(node, "DEPARTMENT");
|
||
} else if (actionKey === "addGroup") {
|
||
openOrgCreateDrawer(node, "GROUP");
|
||
} else if (actionKey === "addEmployee") {
|
||
openOrgEmployeeDrawer(node);
|
||
} else if (actionKey === "authorizeEmployee") {
|
||
openAuthorizeDrawer(node);
|
||
} else if (actionKey === "removeEmployee") {
|
||
removeOrgEmployee(node);
|
||
} else if (actionKey === "delete") {
|
||
removeOrgNode(node);
|
||
}
|
||
}
|
||
|
||
function fillOrgFormFromNode(node) {
|
||
editingOrgNode.value = node;
|
||
orgForm.parent_id = node?.parent_id ?? "";
|
||
orgForm.parent_label = "";
|
||
orgForm.node_type = node?.node_type || "DEPARTMENT";
|
||
orgForm.node_label = node?.node_label || "";
|
||
orgForm.dept_code = node?.dept_code || "";
|
||
orgForm.dept_type = node?.dept_type || "ADMIN";
|
||
orgForm.manager_employee_id = node?.manager_employee_id ?? "";
|
||
orgForm.manager_employee_ids = Array.isArray(node?.manager_employee_ids) && node.manager_employee_ids.length
|
||
? node.manager_employee_ids.map(String)
|
||
: node?.manager_employee_id
|
||
? [String(node.manager_employee_id)]
|
||
: [];
|
||
orgForm.sort_no = node?.sort_no ?? 0;
|
||
orgForm.remark = node?.remark || "";
|
||
orgForm.status = node?.status || "ACTIVE";
|
||
}
|
||
|
||
function openOrgCreateDrawer(parent, nodeType) {
|
||
editingOrgNode.value = null;
|
||
orgDrawerMode.value = "create";
|
||
orgForm.parent_id = parent?.node_id ?? "";
|
||
orgForm.parent_label = parent?.node_label || "当前节点";
|
||
orgForm.node_type = nodeType;
|
||
orgForm.node_label = "";
|
||
orgForm.dept_code = "";
|
||
orgForm.dept_type = defaultDeptType(nodeType);
|
||
orgForm.manager_employee_id = "";
|
||
orgForm.manager_employee_ids = [];
|
||
orgForm.sort_no = 0;
|
||
orgForm.remark = "";
|
||
orgForm.status = "ACTIVE";
|
||
showOrgDrawer.value = true;
|
||
}
|
||
|
||
function openOrgRenameDrawer(node) {
|
||
orgDrawerMode.value = "rename";
|
||
fillOrgFormFromNode(node);
|
||
showOrgDrawer.value = true;
|
||
}
|
||
|
||
function openOrgManagerDrawer(node) {
|
||
orgDrawerMode.value = "manager";
|
||
fillOrgFormFromNode(node);
|
||
managerSearchKeyword.value = "";
|
||
showOrgDrawer.value = true;
|
||
}
|
||
|
||
function openOrgEmployeeDrawer(node) {
|
||
orgDrawerMode.value = "employee";
|
||
fillOrgFormFromNode(node);
|
||
orgEmployeeForm.employee_id = "";
|
||
orgEmployeeForm.sort_no = (node?.employees?.length || 0) + 1;
|
||
showOrgDrawer.value = true;
|
||
}
|
||
|
||
function openOrgDrawer(parent = null, node = null) {
|
||
if (node) {
|
||
openOrgRenameDrawer(node);
|
||
return;
|
||
}
|
||
openOrgCreateDrawer(parent || orgNodes.value[0], parent?.node_type === "COMPANY" ? "BRANCH" : "DEPARTMENT");
|
||
}
|
||
|
||
async function submitOrgNode() {
|
||
try {
|
||
if (orgDrawerMode.value === "employee") {
|
||
if (!orgEmployeeForm.employee_id) {
|
||
notifyError("请选择人员");
|
||
return;
|
||
}
|
||
await postResource(`/system-permissions/org-nodes/${editingOrgNode.value.node_id}/employees`, {
|
||
employee_id: Number(orgEmployeeForm.employee_id),
|
||
sort_no: Number(orgEmployeeForm.sort_no || 0)
|
||
});
|
||
notifySuccess("人员已加入组织节点");
|
||
} else {
|
||
if (!orgForm.node_label.trim() && orgDrawerMode.value !== "manager") {
|
||
notifyError("请输入名称");
|
||
return;
|
||
}
|
||
const updatePayload = {
|
||
node_label: orgForm.node_label.trim() || editingOrgNode.value?.node_label || "",
|
||
dept_type: orgForm.dept_type || "ADMIN",
|
||
manager_employee_id: orgForm.manager_employee_ids.length ? Number(orgForm.manager_employee_ids[0]) : null,
|
||
manager_employee_ids: orgForm.manager_employee_ids.map(Number),
|
||
sort_no: Number(orgForm.sort_no || 0),
|
||
remark: orgForm.remark,
|
||
status: orgForm.status
|
||
};
|
||
const createPayload = {
|
||
parent_id: orgForm.parent_id ? Number(orgForm.parent_id) : null,
|
||
node_type: orgForm.node_type,
|
||
dept_code: orgForm.dept_code.trim() || null,
|
||
...updatePayload
|
||
};
|
||
|
||
if (editingOrgNode.value) {
|
||
await putResource(`/system-permissions/org-nodes/${editingOrgNode.value.node_id}`, updatePayload);
|
||
notifySuccess(orgDrawerMode.value === "manager" ? "主管已更新" : "节点已更新");
|
||
} else {
|
||
await postResource("/system-permissions/org-nodes", createPayload);
|
||
notifySuccess("组织节点已新增");
|
||
}
|
||
}
|
||
|
||
showOrgDrawer.value = false;
|
||
editingOrgNode.value = null;
|
||
await loadAll();
|
||
} catch (error) {
|
||
notifyError(error.message || "组织架构保存失败");
|
||
}
|
||
}
|
||
|
||
async function removeOrgNode(node) {
|
||
if (!window.confirm(`确认删除「${node.node_label}」吗?有下级或人员时系统会阻止删除。`)) {
|
||
return;
|
||
}
|
||
try {
|
||
await deleteResource(`/system-permissions/org-nodes/${node.node_id}`);
|
||
notifySuccess("组织节点已删除");
|
||
selectedNode.value = null;
|
||
await loadAll();
|
||
} catch (error) {
|
||
notifyError(error.message || "组织节点删除失败");
|
||
}
|
||
}
|
||
|
||
async function removeOrgEmployee(node) {
|
||
const parentNodeId = node.parent_node_id;
|
||
if (!parentNodeId) {
|
||
notifyError("缺少上级组织节点,无法移除人员");
|
||
return;
|
||
}
|
||
if (!window.confirm(`确认从组织架构中移除「${node.employee_name}」吗?不会删除人员主数据。`)) {
|
||
return;
|
||
}
|
||
try {
|
||
await deleteResource(`/system-permissions/org-nodes/${parentNodeId}/employees/${node.employee_id}`);
|
||
notifySuccess("人员已从组织架构移除");
|
||
selectedNode.value = null;
|
||
await loadAll();
|
||
} catch (error) {
|
||
notifyError(error.message || "人员移除失败");
|
||
}
|
||
}
|
||
|
||
function openAuthorizeDrawer(employee) {
|
||
const existingUser = users.value.find((user) => String(user.employee_id) === String(employee?.employee_id));
|
||
authorizingEmployee.value = employee;
|
||
authorizeForm.role_ids = existingUser?.role_ids?.length
|
||
? existingUser.role_ids.map(String)
|
||
: (employee?.role_ids || []).map(String);
|
||
authorizeForm.password = "";
|
||
authorizeForm.status = existingUser?.status || "ACTIVE";
|
||
showAuthorizeDrawer.value = true;
|
||
}
|
||
|
||
async function submitAuthorizeUser() {
|
||
if (!authorizingEmployee.value?.employee_id) {
|
||
notifyError("请选择有效人员");
|
||
return;
|
||
}
|
||
if (!authorizingEmployeePhone.value) {
|
||
notifyError("该人员没有电话号码,不能作为账号");
|
||
return;
|
||
}
|
||
if (!authorizeForm.role_ids.length) {
|
||
notifyError("请选择角色");
|
||
return;
|
||
}
|
||
if (!authorizingSystemUser.value && (!authorizeForm.password || authorizeForm.password.length < 6)) {
|
||
notifyError("密码最少6位");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (authorizingSystemUser.value) {
|
||
await putResource(`/system-permissions/users/${authorizingSystemUser.value.user_id}/roles`, {
|
||
role_ids: authorizeForm.role_ids.map(Number)
|
||
});
|
||
if (authorizingSystemUser.value.status !== authorizeForm.status) {
|
||
await putResource(`/system-permissions/users/${authorizingSystemUser.value.user_id}/status`, {
|
||
status: authorizeForm.status
|
||
});
|
||
}
|
||
notifySuccess("人员角色授权已更新");
|
||
} else {
|
||
await postResource("/system-permissions/users", {
|
||
employee_id: Number(authorizingEmployee.value.employee_id),
|
||
role_ids: authorizeForm.role_ids.map(Number),
|
||
password: authorizeForm.password,
|
||
status: authorizeForm.status
|
||
});
|
||
notifySuccess("系统账号已创建并授权");
|
||
}
|
||
showAuthorizeDrawer.value = false;
|
||
authorizingEmployee.value = null;
|
||
await loadAll();
|
||
} catch (error) {
|
||
notifyError(error.message || "人员授权失败");
|
||
}
|
||
}
|
||
|
||
function openRoleDrawer(role = null) {
|
||
editingRole.value = role;
|
||
roleForm.role_code = role?.role_code || "";
|
||
roleForm.role_name = role?.role_name || "";
|
||
roleForm.role_scope = role?.role_scope || "SYSTEM";
|
||
roleForm.status = role?.status || "ACTIVE";
|
||
roleForm.remark = role?.remark || "";
|
||
roleForm.permission_codes = [...(role?.permission_codes || [])];
|
||
showRoleDrawer.value = true;
|
||
}
|
||
|
||
function collectPermissionCodes(node) {
|
||
const codes = node?.permission_code ? [node.permission_code] : [];
|
||
for (const child of node?.children || []) {
|
||
codes.push(...collectPermissionCodes(child));
|
||
}
|
||
return codes;
|
||
}
|
||
|
||
function collectPersistablePermissionCodes(node) {
|
||
const children = node?.children || [];
|
||
if (children.length) {
|
||
return children.flatMap((child) => collectPersistablePermissionCodes(child));
|
||
}
|
||
return node?.permission_code ? [node.permission_code] : [];
|
||
}
|
||
|
||
function isPermissionChecked(node) {
|
||
const codes = collectPersistablePermissionCodes(node);
|
||
if (!codes.length) {
|
||
return false;
|
||
}
|
||
const selectedCodes = new Set(roleForm.permission_codes);
|
||
return codes.every((code) => selectedCodes.has(code));
|
||
}
|
||
|
||
function isPermissionIndeterminate(node) {
|
||
const descendantCodes = (node?.children || []).flatMap((child) => collectPersistablePermissionCodes(child));
|
||
if (!descendantCodes.length) {
|
||
return false;
|
||
}
|
||
const selectedCodes = new Set(roleForm.permission_codes);
|
||
const selectedDescendantCount = descendantCodes.filter((code) => selectedCodes.has(code)).length;
|
||
return selectedDescendantCount > 0 && selectedDescendantCount < descendantCodes.length;
|
||
}
|
||
|
||
function togglePermissionNode(node, checked) {
|
||
const next = new Set(roleForm.permission_codes);
|
||
for (const code of collectPersistablePermissionCodes(node)) {
|
||
if (checked) {
|
||
next.add(code);
|
||
} else {
|
||
next.delete(code);
|
||
}
|
||
}
|
||
roleForm.permission_codes = Array.from(next);
|
||
}
|
||
|
||
async function submitRole() {
|
||
if (!roleForm.role_name.trim()) {
|
||
notifyError("请输入角色名称");
|
||
return;
|
||
}
|
||
if (!editingRole.value && !roleForm.role_code.trim()) {
|
||
notifyError("请输入角色编码");
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
role_name: roleForm.role_name.trim(),
|
||
role_scope: roleForm.role_scope.trim() || "SYSTEM",
|
||
status: roleForm.status,
|
||
remark: roleForm.remark,
|
||
permission_codes: roleForm.permission_codes
|
||
};
|
||
|
||
try {
|
||
if (editingRole.value) {
|
||
await putResource(`/system-permissions/roles/${editingRole.value.role_id}`, payload);
|
||
} else {
|
||
await postResource("/system-permissions/roles", {
|
||
role_code: roleForm.role_code.trim(),
|
||
...payload
|
||
});
|
||
}
|
||
notifySuccess("角色已保存");
|
||
showRoleDrawer.value = false;
|
||
await loadAll();
|
||
} catch (error) {
|
||
notifyError(error.message || "角色保存失败");
|
||
}
|
||
}
|
||
|
||
watch(
|
||
[permissionMainTab, orgViewMode],
|
||
([mainTab, viewMode]) => {
|
||
if (mainTab === "org" && viewMode === "mindmap") {
|
||
requestMindMapFitToScreen();
|
||
}
|
||
}
|
||
);
|
||
|
||
onMounted(async () => {
|
||
await loadAll();
|
||
await requestMindMapFitToScreen();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.system-permission-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
position: relative;
|
||
}
|
||
|
||
.system-permission-hero,
|
||
.system-permission-map-card,
|
||
.system-permission-side-card {
|
||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||
border-radius: 28px;
|
||
background: rgba(255, 255, 255, 0.88);
|
||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
.system-permission-hero {
|
||
padding: 20px 24px;
|
||
background:
|
||
radial-gradient(circle at 12% 20%, rgba(96, 165, 250, 0.2), transparent 28%),
|
||
linear-gradient(135deg, #f8fafc, #eff6ff);
|
||
}
|
||
|
||
.compact-hero {
|
||
min-height: unset;
|
||
}
|
||
|
||
.system-permission-hero h2,
|
||
.section-head h3,
|
||
.drawer-header h2 {
|
||
margin: 0;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.system-permission-hero p {
|
||
margin: 8px 0 0;
|
||
color: #475569;
|
||
}
|
||
|
||
.system-permission-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.system-permission-title-row .system-permission-main-tabs {
|
||
display: inline-flex;
|
||
width: auto;
|
||
max-width: 100%;
|
||
grid-template-columns: none;
|
||
gap: 4px;
|
||
padding: 4px;
|
||
}
|
||
|
||
.system-permission-title-row .system-permission-main-tabs button {
|
||
min-width: clamp(58px, 8vw, 132px);
|
||
padding: 7px clamp(6px, 0.9vw, 12px);
|
||
font-size: clamp(13px, 0.8vw, 14px);
|
||
line-height: 1.15;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.system-permission-main-tabs {
|
||
display: inline-grid;
|
||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||
gap: 8px;
|
||
align-self: flex-start;
|
||
padding: 6px;
|
||
border: 1px solid rgba(37, 99, 235, 0.12);
|
||
border-radius: 18px;
|
||
background: #eef6ff;
|
||
}
|
||
|
||
.system-permission-main-tabs button {
|
||
border: 0;
|
||
border-radius: 14px;
|
||
padding: 12px 18px;
|
||
background: transparent;
|
||
color: #475569;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.system-permission-main-tabs button.active {
|
||
background: #ffffff;
|
||
color: #1d4ed8;
|
||
box-shadow: 0 12px 24px rgba(37, 99, 235, 0.14);
|
||
}
|
||
|
||
.system-permission-maintenance-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.maintenance-table-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.full-width-card,
|
||
.system-permission-side-card {
|
||
padding: 22px;
|
||
}
|
||
|
||
.section-head,
|
||
.drawer-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-head.compact {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.eyebrow {
|
||
margin: 0 0 6px;
|
||
color: #2563eb;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
padding: 6px 10px;
|
||
background: #dbeafe;
|
||
color: #1d4ed8;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.permission-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
.permission-list li {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||
border-radius: 16px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.permission-list li > div:first-child {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.permission-list span,
|
||
.permission-note,
|
||
.empty-state {
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.row-actions,
|
||
.drawer-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.drawer-actions {
|
||
justify-content: flex-end;
|
||
margin-top: 18px;
|
||
}
|
||
|
||
.primary-button,
|
||
.ghost-button {
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 10px 16px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.primary-button {
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.ghost-button {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.mindmap-card-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.org-view-switch {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
padding: 6px;
|
||
border: 1px solid rgba(37, 99, 235, 0.12);
|
||
border-radius: 999px;
|
||
background: rgba(239, 246, 255, 0.92);
|
||
}
|
||
|
||
.org-view-switch button {
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 8px 12px;
|
||
background: transparent;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.org-view-switch button.active {
|
||
background: #ffffff;
|
||
color: #1d4ed8;
|
||
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.14);
|
||
}
|
||
|
||
.mindmap-toolbar {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
padding: 6px;
|
||
border: 1px solid rgba(37, 99, 235, 0.12);
|
||
border-radius: 999px;
|
||
background: rgba(239, 246, 255, 0.92);
|
||
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.12);
|
||
}
|
||
|
||
.mindmap-toolbar button {
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 8px 12px;
|
||
background: #ffffff;
|
||
color: #1d4ed8;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mindmap-toolbar button:hover {
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.org-context-menu {
|
||
position: fixed;
|
||
z-index: 120;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 180px;
|
||
padding: 8px;
|
||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||
border-radius: 16px;
|
||
background: rgba(255, 255, 255, 0.98);
|
||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.18);
|
||
}
|
||
|
||
.org-context-menu button {
|
||
border: 0;
|
||
border-radius: 10px;
|
||
padding: 10px 12px;
|
||
background: transparent;
|
||
color: #0f172a;
|
||
text-align: left;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.org-context-menu button:hover {
|
||
background: #eff6ff;
|
||
color: #1d4ed8;
|
||
}
|
||
|
||
.system-permission-drawer-mask {
|
||
z-index: 90;
|
||
}
|
||
|
||
.system-permission-drawer {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
width: min(520px, calc(100vw - 32px));
|
||
max-height: calc(100vh - 32px);
|
||
overflow-y: auto;
|
||
padding: 24px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.authorize-drawer {
|
||
width: min(760px, calc(100vw - 32px));
|
||
gap: 16px;
|
||
}
|
||
|
||
.authorize-drawer-header {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.authorize-header-note {
|
||
margin: 6px 0 0;
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.authorize-close-button {
|
||
min-width: 108px;
|
||
}
|
||
|
||
.authorize-profile-card,
|
||
.authorize-section,
|
||
.authorize-save-summary {
|
||
border: 1px solid rgba(190, 205, 224, 0.95);
|
||
border-radius: 20px;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
.authorize-profile-card {
|
||
display: grid;
|
||
grid-template-columns: 58px minmax(0, 1fr) minmax(220px, 0.72fr);
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 16px;
|
||
background:
|
||
linear-gradient(135deg, rgba(239, 246, 255, 0.96), rgba(255, 255, 255, 0.92));
|
||
}
|
||
|
||
.authorize-avatar {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 54px;
|
||
height: 54px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(135deg, #2563eb, #38bdf8);
|
||
color: #ffffff;
|
||
font-size: 24px;
|
||
font-weight: 900;
|
||
box-shadow: 0 12px 26px rgba(37, 99, 235, 0.24);
|
||
}
|
||
|
||
.authorize-profile-main,
|
||
.authorize-profile-meta {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.authorize-profile-main strong {
|
||
color: #0f172a;
|
||
font-size: 22px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.authorize-profile-main span,
|
||
.authorize-profile-meta span {
|
||
overflow: hidden;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 750;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.authorize-section {
|
||
display: grid;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.authorize-section-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.authorize-section-head h3 {
|
||
margin: 0;
|
||
color: #0f172a;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.authorize-selected-count {
|
||
flex: 0 0 auto;
|
||
border-radius: 999px;
|
||
padding: 6px 10px;
|
||
background: #dbeafe;
|
||
color: #1d4ed8;
|
||
font-size: 12px;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.authorize-selected-strip {
|
||
min-height: 36px;
|
||
border: 1px dashed rgba(96, 165, 250, 0.5);
|
||
border-radius: 14px;
|
||
padding: 9px 12px;
|
||
background: #f8fbff;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.authorize-role-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.authorize-role-card {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: 26px minmax(0, 1fr);
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
min-height: 96px;
|
||
border: 1px solid rgba(190, 205, 224, 0.95);
|
||
border-radius: 18px;
|
||
padding: 14px;
|
||
background: linear-gradient(180deg, #ffffff, #f8fafc);
|
||
color: #0f172a;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease, background 0.16s ease;
|
||
}
|
||
|
||
.authorize-role-card:hover {
|
||
border-color: rgba(37, 99, 235, 0.42);
|
||
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.08);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.authorize-role-card.active {
|
||
border-color: rgba(37, 99, 235, 0.78);
|
||
background: linear-gradient(180deg, #eff6ff, #ffffff);
|
||
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.18), 0 14px 28px rgba(37, 99, 235, 0.12);
|
||
}
|
||
|
||
.authorize-role-card.danger.active {
|
||
border-color: rgba(249, 115, 22, 0.86);
|
||
background: linear-gradient(180deg, #fff7ed, #ffffff);
|
||
box-shadow: inset 0 0 0 1px rgba(249, 115, 22, 0.2), 0 14px 28px rgba(249, 115, 22, 0.12);
|
||
}
|
||
|
||
.authorize-role-check {
|
||
display: inline-grid;
|
||
place-items: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
border: 1px solid rgba(148, 163, 184, 0.72);
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
color: #2563eb;
|
||
font-size: 14px;
|
||
font-weight: 950;
|
||
}
|
||
|
||
.authorize-role-card.active .authorize-role-check {
|
||
border-color: #2563eb;
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.authorize-role-card.danger.active .authorize-role-check {
|
||
border-color: #f97316;
|
||
background: #f97316;
|
||
}
|
||
|
||
.authorize-role-body {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.authorize-role-body strong,
|
||
.authorize-role-body small,
|
||
.authorize-role-body em {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.authorize-role-body strong {
|
||
color: #0f172a;
|
||
font-size: 16px;
|
||
font-style: normal;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.authorize-role-body small {
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.authorize-role-body em {
|
||
width: fit-content;
|
||
max-width: 100%;
|
||
border-radius: 999px;
|
||
padding: 3px 8px;
|
||
background: #eef6ff;
|
||
color: #2563eb;
|
||
font-size: 11px;
|
||
font-style: normal;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.authorize-role-card.danger .authorize-role-body em {
|
||
background: #fff7ed;
|
||
color: #c2410c;
|
||
}
|
||
|
||
.authorize-account-section {
|
||
background:
|
||
linear-gradient(135deg, rgba(248, 250, 252, 0.98), rgba(255, 255, 255, 0.92));
|
||
}
|
||
|
||
.authorize-password-field {
|
||
margin: 0;
|
||
}
|
||
|
||
.authorize-account-note {
|
||
border: 1px solid rgba(148, 163, 184, 0.26);
|
||
border-radius: 14px;
|
||
padding: 11px 12px;
|
||
background: #f8fafc;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 750;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.authorize-status-switch {
|
||
display: inline-flex;
|
||
width: fit-content;
|
||
border: 1px solid rgba(190, 205, 224, 0.95);
|
||
border-radius: 999px;
|
||
padding: 4px;
|
||
background: #f8fafc;
|
||
gap: 4px;
|
||
}
|
||
|
||
.authorize-status-switch button {
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 8px 16px;
|
||
background: transparent;
|
||
color: #64748b;
|
||
font-weight: 850;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.authorize-status-switch button.active {
|
||
background: #2563eb;
|
||
color: #ffffff;
|
||
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.22);
|
||
}
|
||
|
||
.authorize-save-summary {
|
||
padding: 13px 16px;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.authorize-save-summary strong {
|
||
color: #0f172a;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.authorize-actions {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.permission-tree {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-height: 260px;
|
||
overflow-y: auto;
|
||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||
border-radius: 16px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.authorize-summary {
|
||
display: grid;
|
||
gap: 8px;
|
||
border: 1px solid rgba(37, 99, 235, 0.12);
|
||
border-radius: 16px;
|
||
padding: 12px;
|
||
background: #f8fbff;
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.permission-check {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #0f172a;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.permission-check.child {
|
||
padding-left: 24px;
|
||
color: #475569;
|
||
}
|
||
|
||
.selected-manager-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
min-height: 38px;
|
||
padding: 8px;
|
||
border: 1px dashed rgba(37, 99, 235, 0.24);
|
||
border-radius: 14px;
|
||
background: #f8fbff;
|
||
}
|
||
|
||
.selected-manager-tags em {
|
||
color: #94a3b8;
|
||
font-size: 13px;
|
||
font-style: normal;
|
||
}
|
||
|
||
.selected-manager-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border-radius: 999px;
|
||
padding: 7px 10px;
|
||
background: #dbeafe;
|
||
color: #1d4ed8;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.selected-manager-tag button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 18px;
|
||
height: 18px;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
background: #ffffff;
|
||
color: #2563eb;
|
||
cursor: pointer;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.manager-search-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.manager-search-box span {
|
||
color: #475569;
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.manager-search-box input {
|
||
width: 100%;
|
||
border: 1px solid rgba(15, 23, 42, 0.1);
|
||
border-radius: 14px;
|
||
padding: 10px 12px;
|
||
background: #ffffff;
|
||
color: #0f172a;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||
}
|
||
|
||
.manager-search-box input:focus {
|
||
border-color: rgba(37, 99, 235, 0.62);
|
||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.manager-option-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 10px;
|
||
max-height: 280px;
|
||
overflow-y: auto;
|
||
padding: 4px;
|
||
}
|
||
|
||
.manager-option-grid button {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||
border-radius: 16px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
color: #0f172a;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.manager-option-grid button.active {
|
||
border-color: rgba(37, 99, 235, 0.7);
|
||
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||
box-shadow: inset 4px 0 0 #2563eb;
|
||
}
|
||
|
||
.manager-option-grid button strong {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.manager-option-grid button span {
|
||
color: #64748b;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.manager-empty-state {
|
||
margin: 0;
|
||
border: 1px dashed rgba(148, 163, 184, 0.5);
|
||
border-radius: 14px;
|
||
padding: 12px;
|
||
background: #f8fafc;
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.section-head,
|
||
.mindmap-card-actions {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.system-permission-main-tabs {
|
||
width: 100%;
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|