优化组织人员路径与授权弹窗

This commit is contained in:
汤学会 2026-06-15 03:27:51 +08:00
parent 30fb7af159
commit 9b60aa3e0d
7 changed files with 605 additions and 49 deletions

View File

@ -24,3 +24,21 @@ describe("OrgPermissionTree hierarchy lines", () => {
assert.doesNotMatch(source, /background:\s*var\(--tree-connector-color\)/); assert.doesNotMatch(source, /background:\s*var\(--tree-connector-color\)/);
}); });
}); });
describe("OrgPermissionTree employee list", () => {
it("shows each employee organization path with full-value hover text", () => {
assert.match(source, /<th>组织路径<\/th>/);
assert.match(source, /employeeOrganizationPath\(employee\)/);
assert.match(source, /class="org-path-value"\s+:title="employeeOrganizationPath\(employee\)"/);
assert.match(source, /class="org-path-tail"\s+:title="employeeOrganizationPath\(employee\)"/);
assert.match(source, /function employeeOrganizationTail\(employee\)/);
assert.doesNotMatch(source, /org-path-pill/);
assert.match(source, /<td colspan="7" class="empty-row"/);
});
it("keeps row action buttons inside an inner flex container instead of flexing the table cell", () => {
assert.match(source, /<td class="action-row">\s*<div class="employee-actions">/);
assert.match(source, /\.employee-actions\s*\{[\s\S]*?display:\s*inline-flex;/);
assert.doesNotMatch(source, /\.action-row\s*\{[^}]*display:\s*flex;/);
});
});

View File

@ -54,9 +54,19 @@
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table compact-table org-employee-table"> <table class="data-table compact-table org-employee-table">
<colgroup>
<col class="employee-col-name" />
<col class="employee-col-path" />
<col class="employee-col-phone" />
<col class="employee-col-position" />
<col class="employee-col-role" />
<col class="employee-col-status" />
<col class="employee-col-action" />
</colgroup>
<thead> <thead>
<tr> <tr>
<th>姓名</th> <th>姓名</th>
<th>组织路径</th>
<th>电话号码 / 账号</th> <th>电话号码 / 账号</th>
<th>岗位</th> <th>岗位</th>
<th>角色</th> <th>角色</th>
@ -67,6 +77,10 @@
<tbody> <tbody>
<tr v-for="employee in paginatedEmployees" :key="employeeKey(employee)"> <tr v-for="employee in paginatedEmployees" :key="employeeKey(employee)">
<td>{{ employeeName(employee) }}</td> <td>{{ employeeName(employee) }}</td>
<td class="org-path-cell">
<span class="org-path-value" :title="employeeOrganizationPath(employee)">{{ employeeOrganizationPath(employee) }}</span>
<span class="org-path-tail" :title="employeeOrganizationPath(employee)">{{ employeeOrganizationTail(employee) }}</span>
</td>
<td>{{ employeePhone(employee) }}</td> <td>{{ employeePhone(employee) }}</td>
<td>{{ employeePosition(employee) }}</td> <td>{{ employeePosition(employee) }}</td>
<td>{{ employeeRoles(employee) }}</td> <td>{{ employeeRoles(employee) }}</td>
@ -74,12 +88,14 @@
<StatusBadge :value="employee?.status" :text="employeeStatus(employee)" /> <StatusBadge :value="employee?.status" :text="employeeStatus(employee)" />
</td> </td>
<td class="action-row"> <td class="action-row">
<button class="ghost-button" type="button" @click="authorizeEmployee(employee)">授权</button> <div class="employee-actions">
<button class="ghost-button danger" type="button" @click="removeEmployee(employee)">移除</button> <button class="ghost-button" type="button" @click="authorizeEmployee(employee)">人员授权</button>
<button class="ghost-button danger" type="button" @click="removeEmployee(employee)">移除记录</button>
</div>
</td> </td>
</tr> </tr>
<tr v-if="!filteredEmployees.length"> <tr v-if="!filteredEmployees.length">
<td colspan="6" class="empty-row">{{ selectedEmployees.length ? "没有符合条件的人员" : "当前节点暂无人员" }}</td> <td colspan="7" class="empty-row">{{ selectedEmployees.length ? "没有符合条件的人员" : "当前节点暂无人员" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -237,7 +253,7 @@ const filteredEmployees = computed(() => {
return selectedEmployees.value; return selectedEmployees.value;
} }
return selectedEmployees.value.filter((employee) => return selectedEmployees.value.filter((employee) =>
[employeeName(employee), employeePhone(employee), employee.employee_code, employee.username] [employeeName(employee), employeeOrganizationPath(employee), employeePhone(employee), employee.employee_code, employee.username]
.filter(Boolean) .filter(Boolean)
.join(" ") .join(" ")
.toLowerCase() .toLowerCase()
@ -393,6 +409,15 @@ function employeePosition(employee) {
return employee?.job_title || employee?.position || employee?.post_name || employee?.title || "未设置岗位"; return employee?.job_title || employee?.position || employee?.post_name || employee?.title || "未设置岗位";
} }
function employeeOrganizationPath(employee) {
return employee?.organization_path || employee?.parent_node_label || "未设置组织";
}
function employeeOrganizationTail(employee) {
const pathParts = Array.isArray(employee?.organization_path_parts) ? employee.organization_path_parts.filter(Boolean) : [];
return pathParts.length ? pathParts[pathParts.length - 1] : employeeOrganizationPath(employee);
}
function employeeRoles(employee) { function employeeRoles(employee) {
if (Array.isArray(employee?.role_names) && employee.role_names.length) { if (Array.isArray(employee?.role_names) && employee.role_names.length) {
return employee.role_names.join("、"); return employee.role_names.join("、");
@ -764,9 +789,23 @@ function removeEmployee(employee) {
} }
.action-row { .action-row {
display: flex; overflow: visible;
flex-wrap: wrap; white-space: nowrap;
}
.employee-actions {
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 8px; gap: 8px;
min-width: max-content;
}
.employee-actions .ghost-button {
flex: 0 0 auto;
min-height: 30px;
padding: 0 10px;
font-size: 12px;
} }
.empty-row { .empty-row {
@ -779,6 +818,67 @@ function removeEmployee(employee) {
table-layout: fixed; table-layout: fixed;
} }
.org-employee-table .employee-col-name {
width: 10%;
}
.org-employee-table .employee-col-path {
width: 30%;
}
.org-employee-table .employee-col-phone {
width: 13%;
}
.org-employee-table .employee-col-position {
width: 10%;
}
.org-employee-table .employee-col-role {
width: 11%;
}
.org-employee-table .employee-col-status {
width: 7%;
}
.org-employee-table .employee-col-action {
width: 19%;
}
.org-path-cell {
min-width: 0;
}
.org-path-value,
.org-path-tail {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-path-value {
color: #334155;
font-size: 13px;
font-weight: 700;
line-height: 1.35;
}
.org-path-tail {
width: fit-content;
max-width: 100%;
margin-top: 3px;
border-radius: 999px;
padding: 2px 7px;
background: #eef6ff;
color: #2563eb;
font-size: 11px;
font-weight: 750;
line-height: 1.2;
}
:deep(.org-tree-branch) { :deep(.org-tree-branch) {
position: relative; position: relative;
display: flex; display: flex;

View File

@ -12062,32 +12062,37 @@ body .system-permission-page .tree-layout-card .org-employee-table td {
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(1), body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(1),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(1) { body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(1) {
width: 13% !important; width: 10% !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(2), body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(2),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(2) { body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(2) {
width: 20% !important; width: 30% !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(3), body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(3),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(3) { body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(3) {
width: 14% !important; width: 13% !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(4), body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(4),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(4) { body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(4) {
width: 17% !important; width: 10% !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(5), body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(5),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(5) { body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(5) {
width: 12% !important; width: 11% !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(6), body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(6),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(6) { body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(6) {
width: 24% !important; width: 7% !important;
}
body .system-permission-page .tree-layout-card .org-employee-table th:nth-child(7),
body .system-permission-page .tree-layout-card .org-employee-table td:nth-child(7) {
width: 19% !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table .action-row { body .system-permission-page .tree-layout-card .org-employee-table .action-row {
@ -12096,9 +12101,17 @@ body .system-permission-page .tree-layout-card .org-employee-table .action-row {
white-space: nowrap !important; white-space: nowrap !important;
} }
body .system-permission-page .tree-layout-card .org-employee-table .employee-actions {
display: inline-flex !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 8px !important;
min-width: max-content !important;
}
body .system-permission-page .tree-layout-card .org-employee-table .action-row button { body .system-permission-page .tree-layout-card .org-employee-table .action-row button {
min-height: 30px !important; min-height: 30px !important;
padding: 0 8px !important; padding: 0 10px !important;
font-size: 12px !important; font-size: 12px !important;
} }

View File

@ -2,7 +2,7 @@ export function collectOrganizationEmployees(node) {
const employees = []; const employees = [];
const seenKeys = new Set(); const seenKeys = new Set();
collectFromNode(node, node, employees, seenKeys); collectFromNode(node, node, employees, seenKeys, []);
return employees; return employees;
} }
@ -10,40 +10,48 @@ export function countOrganizationEmployees(node) {
return collectOrganizationEmployees(node).length; return collectOrganizationEmployees(node).length;
} }
function collectFromNode(node, parentNode, employees, seenKeys) { function collectFromNode(node, parentNode, employees, seenKeys, pathParts) {
if (!node) { if (!node) {
return; return;
} }
if (isEmployeeNode(node)) { if (isEmployeeNode(node)) {
appendEmployee(node, parentNode, employees, seenKeys); appendEmployee(node, parentNode, employees, seenKeys, pathParts);
return; return;
} }
const currentPathParts = [...pathParts, nodeLabel(node)].filter(Boolean);
for (const employee of Array.isArray(node.employees) ? node.employees : []) { for (const employee of Array.isArray(node.employees) ? node.employees : []) {
appendEmployee(employee, node, employees, seenKeys); appendEmployee(employee, node, employees, seenKeys, currentPathParts);
} }
for (const child of Array.isArray(node.children) ? node.children : []) { for (const child of Array.isArray(node.children) ? node.children : []) {
if (isOrganizationNode(child)) { if (isOrganizationNode(child)) {
collectFromNode(child, child, employees, seenKeys); collectFromNode(child, child, employees, seenKeys, currentPathParts);
} else if (isEmployeeNode(child)) { } else if (isEmployeeNode(child)) {
appendEmployee(child, node, employees, seenKeys); appendEmployee(child, node, employees, seenKeys, currentPathParts);
} }
} }
} }
function appendEmployee(employee, parentNode, employees, seenKeys) { function appendEmployee(employee, parentNode, employees, seenKeys, pathParts) {
const key = employeeKey(employee); const key = employeeKey(employee);
if (!key || seenKeys.has(key)) { if (!key || seenKeys.has(key)) {
return; return;
} }
const organizationPathParts = Array.isArray(employee.organization_path_parts) && employee.organization_path_parts.length
? employee.organization_path_parts
: pathParts;
seenKeys.add(key); seenKeys.add(key);
employees.push({ employees.push({
...employee, ...employee,
parent_node_id: employee.parent_node_id ?? nodeId(parentNode), parent_node_id: employee.parent_node_id ?? nodeId(parentNode),
parent_node_label: employee.parent_node_label ?? nodeLabel(parentNode) parent_node_label: employee.parent_node_label ?? nodeLabel(parentNode),
organization_path_parts: organizationPathParts,
organization_path: employee.organization_path || organizationPathParts.join(" / ")
}); });
} }

View File

@ -40,6 +40,9 @@ describe("organization tree employee aggregation", () => {
assert.deepEqual(employees.map((employee) => employee.employee_name), ["直属主管", "张三", "李四", "王五"]); assert.deepEqual(employees.map((employee) => employee.employee_name), ["直属主管", "张三", "李四", "王五"]);
assert.equal(employees[1].parent_node_id, 11); assert.equal(employees[1].parent_node_id, 11);
assert.equal(employees[1].parent_node_label, "旭升加工部"); assert.equal(employees[1].parent_node_label, "旭升加工部");
assert.deepEqual(employees[1].organization_path_parts, ["旭升车间", "旭升加工部"]);
assert.equal(employees[1].organization_path, "旭升车间 / 旭升加工部");
assert.equal(employees[3].organization_path, "旭升车间 / 旭升加工部 / 冲压小组");
assert.equal(countOrganizationEmployees(branch), 4); assert.equal(countOrganizationEmployees(branch), 4);
}); });

View File

@ -46,3 +46,28 @@ describe("SystemPermissionView maintenance scrolling", () => {
assert.match(mainStyles, /\.system-permission-page\.system-permission-page--maintenance\s+\.system-permission-maintenance-card\s*\{[\s\S]*?overflow:\s*visible;/); assert.match(mainStyles, /\.system-permission-page\.system-permission-page--maintenance\s+\.system-permission-maintenance-card\s*\{[\s\S]*?overflow:\s*visible;/);
}); });
}); });
describe("SystemPermissionView organization employee table columns", () => {
it("keeps the organization employee table aligned after adding the organization path column", () => {
assert.match(mainStyles, /\.tree-layout-card\s+\.org-employee-table th:nth-child\(7\),[\s\S]*?td:nth-child\(7\)\s*\{[\s\S]*?width:\s*19%\s*!important;/);
assert.match(mainStyles, /\.tree-layout-card\s+\.org-employee-table \.action-row\s*\{[\s\S]*?display:\s*table-cell\s*!important;/);
assert.match(mainStyles, /\.tree-layout-card\s+\.org-employee-table \.employee-actions\s*\{[\s\S]*?display:\s*inline-flex\s*!important;[\s\S]*?min-width:\s*max-content\s*!important;/);
});
});
describe("SystemPermissionView authorize drawer", () => {
it("uses card-style role selection instead of a native multi-select", () => {
const authorizeStart = source.indexOf('v-if="showAuthorizeDrawer"');
const authorizeEnd = source.indexOf('v-if="showRoleDrawer"', authorizeStart);
const authorizeSource = source.slice(authorizeStart, authorizeEnd);
assert.doesNotMatch(authorizeSource, /<select\s+v-model="authorizeForm\.role_ids"\s+multiple>/);
assert.match(authorizeSource, /class="authorize-role-grid"/);
assert.match(authorizeSource, /v-for="role in roles"/);
assert.match(authorizeSource, /@click="toggleAuthorizeRole\(role\.role_id\)"/);
assert.match(authorizeSource, /isAuthorizeRoleSelected\(role\.role_id\)/);
assert.match(authorizeSource, /selectedAuthorizeRoleNames/);
assert.match(authorizeSource, /roleRiskText\(role\)/);
assert.match(authorizeSource, /:disabled="!authorizeForm\.role_ids\.length"\s+@click="submitAuthorizeUser"/);
});
});

View File

@ -156,42 +156,87 @@
</div> </div>
<div v-if="showAuthorizeDrawer" class="workflow-drawer-mask system-permission-drawer-mask" @click.self="showAuthorizeDrawer = false"> <div v-if="showAuthorizeDrawer" class="workflow-drawer-mask system-permission-drawer-mask" @click.self="showAuthorizeDrawer = false">
<aside class="form-drawer system-permission-drawer"> <aside class="form-drawer system-permission-drawer authorize-drawer">
<div class="drawer-header"> <div class="drawer-header authorize-drawer-header">
<div> <div>
<p class="eyebrow">人员授权</p> <p class="eyebrow">人员授权</p>
<h2>{{ authorizingEmployee?.employee_name || "人员授权" }}</h2> <h2>{{ authorizingEmployee?.employee_name || "人员授权" }}</h2>
<p class="authorize-header-note">为该人员创建或更新系统账号并分配可访问的系统角色</p>
</div> </div>
<button class="ghost-button" type="button" @click="showAuthorizeDrawer = false">关闭</button> <button class="ghost-button authorize-close-button" type="button" @click="showAuthorizeDrawer = false">关闭窗口</button>
</div> </div>
<div class="authorize-summary"> <section class="authorize-profile-card" aria-label="授权对象">
<span>电话 / 账号{{ authorizingEmployeePhone || "未设置电话号码" }}</span> <div class="authorize-avatar">{{ authorizingEmployeeInitial }}</div>
<span>所属组织{{ authorizingEmployee?.parent_node_label || "未标记组织" }}</span> <div class="authorize-profile-main">
<span>账号状态<StatusBadge :value="authorizingSystemUser?.status" :text="authorizingSystemUser ? '' : '未创建账号'" /></span> <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>
<label class="form-field"> <div class="drawer-actions authorize-actions">
<span>角色</span> <button class="primary-button" type="button" :disabled="!authorizeForm.role_ids.length" @click="submitAuthorizeUser">保存授权</button>
<select v-model="authorizeForm.role_ids" multiple>
<option v-for="role in roles" :key="role.role_id" :value="role.role_id">
{{ role.role_name || role.role_code }}
</option>
</select>
</label>
<label v-if="!authorizingSystemUser" class="form-field">
<span>密码</span>
<input v-model="authorizeForm.password" type="password" placeholder="首次创建账号必填最少6位" />
</label>
<label class="form-field">
<span>账号状态</span>
<select v-model="authorizeForm.status">
<option value="ACTIVE">启用</option>
<option value="INACTIVE">停用</option>
</select>
</label>
<div class="drawer-actions">
<button class="primary-button" type="button" @click="submitAuthorizeUser">保存授权</button>
<button class="ghost-button" type="button" @click="showAuthorizeDrawer = false">取消</button> <button class="ghost-button" type="button" @click="showAuthorizeDrawer = false">取消</button>
</div> </div>
</aside> </aside>
@ -450,6 +495,22 @@ const authorizingEmployeePhone = computed(() =>
authorizingEmployee.value?.mobile || authorizingSystemUser.value?.mobile || authorizingSystemUser.value?.username || "" 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(() => { const orgDrawerTitle = computed(() => {
if (orgDrawerMode.value === "rename") { if (orgDrawerMode.value === "rename") {
return "重命名"; return "重命名";
@ -598,6 +659,46 @@ function defaultDeptType(type) {
}[type] || "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) { function nodeActions(node) {
if (!node) { if (!node) {
return []; return [];
@ -1291,6 +1392,294 @@ onMounted(async () => {
background: #ffffff; 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 { .permission-tree {
display: flex; display: flex;
flex-direction: column; flex-direction: column;