优化组织人员路径与授权弹窗
This commit is contained in:
parent
30fb7af159
commit
9b60aa3e0d
@ -24,3 +24,21 @@ describe("OrgPermissionTree hierarchy lines", () => {
|
||||
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;/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -54,9 +54,19 @@
|
||||
|
||||
<div class="table-wrap">
|
||||
<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>
|
||||
<tr>
|
||||
<th>姓名</th>
|
||||
<th>组织路径</th>
|
||||
<th>电话号码 / 账号</th>
|
||||
<th>岗位</th>
|
||||
<th>角色</th>
|
||||
@ -67,6 +77,10 @@
|
||||
<tbody>
|
||||
<tr v-for="employee in paginatedEmployees" :key="employeeKey(employee)">
|
||||
<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>{{ employeePosition(employee) }}</td>
|
||||
<td>{{ employeeRoles(employee) }}</td>
|
||||
@ -74,12 +88,14 @@
|
||||
<StatusBadge :value="employee?.status" :text="employeeStatus(employee)" />
|
||||
</td>
|
||||
<td class="action-row">
|
||||
<button class="ghost-button" type="button" @click="authorizeEmployee(employee)">授权</button>
|
||||
<button class="ghost-button danger" type="button" @click="removeEmployee(employee)">移除</button>
|
||||
<div class="employee-actions">
|
||||
<button class="ghost-button" type="button" @click="authorizeEmployee(employee)">人员授权</button>
|
||||
<button class="ghost-button danger" type="button" @click="removeEmployee(employee)">移除记录</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!filteredEmployees.length">
|
||||
<td colspan="6" class="empty-row">{{ selectedEmployees.length ? "没有符合条件的人员" : "当前节点暂无人员" }}</td>
|
||||
<td colspan="7" class="empty-row">{{ selectedEmployees.length ? "没有符合条件的人员" : "当前节点暂无人员" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -237,7 +253,7 @@ const filteredEmployees = computed(() => {
|
||||
return selectedEmployees.value;
|
||||
}
|
||||
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)
|
||||
.join(" ")
|
||||
.toLowerCase()
|
||||
@ -393,6 +409,15 @@ function employeePosition(employee) {
|
||||
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) {
|
||||
if (Array.isArray(employee?.role_names) && employee.role_names.length) {
|
||||
return employee.role_names.join("、");
|
||||
@ -764,9 +789,23 @@ function removeEmployee(employee) {
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.employee-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
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 {
|
||||
@ -779,6 +818,67 @@ function removeEmployee(employee) {
|
||||
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) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@ -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 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 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 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 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 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 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 {
|
||||
@ -12096,9 +12101,17 @@ body .system-permission-page .tree-layout-card .org-employee-table .action-row {
|
||||
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 {
|
||||
min-height: 30px !important;
|
||||
padding: 0 8px !important;
|
||||
padding: 0 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ export function collectOrganizationEmployees(node) {
|
||||
const employees = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
collectFromNode(node, node, employees, seenKeys);
|
||||
collectFromNode(node, node, employees, seenKeys, []);
|
||||
return employees;
|
||||
}
|
||||
|
||||
@ -10,40 +10,48 @@ export function countOrganizationEmployees(node) {
|
||||
return collectOrganizationEmployees(node).length;
|
||||
}
|
||||
|
||||
function collectFromNode(node, parentNode, employees, seenKeys) {
|
||||
function collectFromNode(node, parentNode, employees, seenKeys, pathParts) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEmployeeNode(node)) {
|
||||
appendEmployee(node, parentNode, employees, seenKeys);
|
||||
appendEmployee(node, parentNode, employees, seenKeys, pathParts);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPathParts = [...pathParts, nodeLabel(node)].filter(Boolean);
|
||||
|
||||
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 : []) {
|
||||
if (isOrganizationNode(child)) {
|
||||
collectFromNode(child, child, employees, seenKeys);
|
||||
collectFromNode(child, child, employees, seenKeys, currentPathParts);
|
||||
} 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);
|
||||
if (!key || seenKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const organizationPathParts = Array.isArray(employee.organization_path_parts) && employee.organization_path_parts.length
|
||||
? employee.organization_path_parts
|
||||
: pathParts;
|
||||
|
||||
seenKeys.add(key);
|
||||
employees.push({
|
||||
...employee,
|
||||
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(" / ")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,9 @@ describe("organization tree employee aggregation", () => {
|
||||
assert.deepEqual(employees.map((employee) => employee.employee_name), ["直属主管", "张三", "李四", "王五"]);
|
||||
assert.equal(employees[1].parent_node_id, 11);
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@ -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;/);
|
||||
});
|
||||
});
|
||||
|
||||
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"/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -156,42 +156,87 @@
|
||||
</div>
|
||||
|
||||
<div v-if="showAuthorizeDrawer" class="workflow-drawer-mask system-permission-drawer-mask" @click.self="showAuthorizeDrawer = false">
|
||||
<aside class="form-drawer system-permission-drawer">
|
||||
<div class="drawer-header">
|
||||
<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" type="button" @click="showAuthorizeDrawer = false">关闭</button>
|
||||
<button class="ghost-button authorize-close-button" type="button" @click="showAuthorizeDrawer = false">关闭窗口</button>
|
||||
</div>
|
||||
|
||||
<div class="authorize-summary">
|
||||
<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>所属组织:{{ authorizingEmployee?.parent_node_label || "未标记组织" }}</span>
|
||||
<span>账号状态:<StatusBadge :value="authorizingSystemUser?.status" :text="authorizingSystemUser ? '' : '未创建账号'" /></span>
|
||||
<span>账号状态:<StatusBadge :value="authorizingSystemUser?.status" :text="authorizingSystemUser ? accountStatusLabel(authorizingSystemUser.status) : '未创建账号'" /></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<label class="form-field">
|
||||
<span>角色</span>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@ -450,6 +495,22 @@ 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 "重命名";
|
||||
@ -598,6 +659,46 @@ function defaultDeptType(type) {
|
||||
}[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 [];
|
||||
@ -1291,6 +1392,294 @@ onMounted(async () => {
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user