32 KiB
System Permission Org View Authorization Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add 脑图模式 / 组织树模式 to system permission organization management, move personnel authorization/account creation onto org personnel nodes, and simplify the maintenance tab into role-only maintenance.
Architecture: Keep the existing backend permission APIs and refactor the frontend around two organization views that share one data source. SystemPermissionView.vue owns data loading, drawers, authorization submission, and role maintenance; a new focused OrgPermissionTree.vue renders the Ruoyi-like left-tree/right-personnel-list organization tree mode.
Tech Stack: Vue 3 <script setup>, existing fetchResource/postResource/putResource/deleteResource, existing PaginationBar, existing backend FastAPI/SQLAlchemy permission services.
File Structure
- Modify:
frontend/scripts/test-system-permission-management-ui.mjs- Update static assertions to describe the new UI contract before implementation.
- Create:
frontend/src/components/OrgPermissionTree.vue- Render organization tree mode: left organization tree, right selected-node personnel table, right-click org node menu emit, personnel
授权/移除, add-person button, search, pagination.
- Render organization tree mode: left organization tree, right selected-node personnel table, right-click org node menu emit, personnel
- Modify:
frontend/src/views/SystemPermissionView.vue- Add
orgViewModeswitch. - Render
OrgMindMaponly in mind-map mode andOrgPermissionTreein tree mode. - Replace
人员与角色维护with role-only角色维护. - Add personnel authorization drawer used by both modes.
- Rework employee node context menu to
授权/移除.
- Add
- No backend file changes expected.
- Existing routes already support org tree, employee binding, user creation, and role update.
Task 1: Update Static UI Contract Test
Files:
-
Modify:
frontend/scripts/test-system-permission-management-ui.mjs -
Step 1: Replace old personnel-maintenance expectations with new contract
Change the assertions around SystemPermissionView.vue so they require the new labels and reject the old standalone personnel tab.
Use this replacement block after assert.match(systemPermissionSource, /系统权限管理/, ...):
assert.match(systemPermissionSource, /组织架构/, "SystemPermissionView should expose organization structure tab");
assert.match(systemPermissionSource, /角色维护/, "SystemPermissionView should expose role-only maintenance tab");
assert.doesNotMatch(systemPermissionSource, /人员与角色维护/, "personnel and role maintenance should be renamed");
assert.doesNotMatch(systemPermissionSource, /maintenanceSubTab/, "role maintenance should not keep nested personnel tabs");
assert.doesNotMatch(systemPermissionSource, /system-permission-sub-tabs/, "role maintenance should not render nested sub tabs");
assert.doesNotMatch(
systemPermissionSource,
/v-if="maintenanceSubTab === 'users'"/,
"standalone system users table should be removed from maintenance page"
);
assert.doesNotMatch(
systemPermissionSource,
/v-if="maintenanceSubTab === 'roles'"/,
"roles should render directly in the role maintenance tab"
);
assert.match(systemPermissionSource, /orgViewMode/, "organization tab should keep organization view mode state");
assert.match(systemPermissionSource, /脑图模式/, "organization tab should expose mind-map mode switch");
assert.match(systemPermissionSource, /组织树模式/, "organization tab should expose organization-tree mode switch");
assert.match(
systemPermissionSource,
/<OrgPermissionTree/,
"SystemPermissionView should render organization tree mode through OrgPermissionTree"
);
assert.match(
systemPermissionSource,
/showAuthorizeDrawer/,
"SystemPermissionView should expose a personnel authorization drawer"
);
assert.match(systemPermissionSource, /openAuthorizeDrawer/, "personnel authorization should open from org personnel nodes");
assert.match(systemPermissionSource, /submitAuthorizeUser/, "authorization drawer should save account and roles");
assert.match(systemPermissionSource, /putResource\(`\/system-permissions\/users\/\$\{[^`]+\.user_id\}\/roles`/, "existing users should update roles");
assert.match(systemPermissionSource, /postResource\(\"\/system-permissions\/users\"/, "missing users should be created from authorization drawer");
assert.doesNotMatch(systemPermissionSource, /function openUserDrawer/, "standalone add-user drawer should be removed");
assert.doesNotMatch(systemPermissionSource, /function submitCreateUser/, "standalone add-user submit should be removed");
assert.doesNotMatch(systemPermissionSource, /paginatedUsers/, "standalone users table pagination should be removed");
assert.doesNotMatch(systemPermissionSource, /reset-password/, "standalone password reset action should not be exposed on this page");
- Step 2: Add component import test
Add this read near the existing component reads:
const orgPermissionTreeSource = readFileSync(new URL("../src/components/OrgPermissionTree.vue", import.meta.url), "utf8");
Add these assertions near the OrgMindMap assertions:
assert.match(orgPermissionTreeSource, /组织树/, "OrgPermissionTree should render organization tree UI");
assert.match(orgPermissionTreeSource, /@contextmenu\.prevent\.stop/, "OrgPermissionTree should support right-click org node actions");
assert.match(orgPermissionTreeSource, /授权/, "OrgPermissionTree should expose personnel authorization action");
assert.match(orgPermissionTreeSource, /移除/, "OrgPermissionTree should expose personnel removal action");
assert.match(orgPermissionTreeSource, /新增人员/, "OrgPermissionTree should expose add-person action for selected org node");
assert.match(orgPermissionTreeSource, /PaginationBar/, "OrgPermissionTree should use global pagination bar");
assert.match(orgPermissionTreeSource, /搜索人员姓名或手机号/, "OrgPermissionTree should support personnel search");
- Step 3: Run test and verify it fails
Run:
node frontend/scripts/test-system-permission-management-ui.mjs
Expected result:
AssertionError
The failure should mention one of the new requirements such as OrgPermissionTree missing or 人员与角色维护 still present.
Task 2: Create Organization Tree Component
Files:
-
Create:
frontend/src/components/OrgPermissionTree.vue -
Step 1: Add the component template
Create frontend/src/components/OrgPermissionTree.vue with this structure:
<template>
<div class="org-permission-tree-card">
<aside class="org-tree-panel">
<div class="org-tree-head">
<div>
<p class="eyebrow">组织树</p>
<h4>组织节点</h4>
</div>
<span class="status-pill">{{ flatOrgNodes.length }} 个节点</span>
</div>
<div class="org-tree-list">
<OrgTreeNode
v-for="node in nodes"
:key="node.node_id"
:node="node"
:level="0"
:selected-node-id="selectedNodeId"
:expanded-node-ids="expandedNodeIds"
@select="selectNode"
@toggle="toggleNode"
@node-contextmenu="emitNodeContextMenu"
/>
</div>
<p v-if="!nodes.length" class="empty-state">暂无组织节点。</p>
</aside>
<section class="org-tree-people-panel">
<div class="org-tree-people-head">
<div>
<p class="eyebrow">节点人员</p>
<h4>{{ selectedOrgNode?.node_label || "请选择组织节点" }}</h4>
<p class="permission-note">在当前组织节点内授权人员、创建账号或移除人员。</p>
</div>
<button
class="primary-button"
type="button"
:disabled="!selectedOrgNode || !canBindEmployees(selectedOrgNode)"
@click="emit('add-employee', selectedOrgNode)"
>
新增人员
</button>
</div>
<label class="tree-search-box">
<span>搜索人员</span>
<input v-model.trim="personKeyword" type="search" placeholder="搜索人员姓名或手机号" />
</label>
<div class="table-wrap">
<table class="data-table compact-table">
<thead>
<tr>
<th>姓名</th>
<th>电话号码 / 账号</th>
<th>岗位</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in paginatedEmployees" :key="employee.employee_id">
<td>{{ employee.employee_name }}</td>
<td>{{ employee.mobile || "未设置电话号码" }}</td>
<td>{{ employee.job_title || "未设置岗位" }}</td>
<td>{{ formatRoleNames(employee) }}</td>
<td>{{ employee.status === "ACTIVE" ? "启用" : "停用" }}</td>
<td class="action-row">
<button class="ghost-button" type="button" @click="emit('authorize-employee', withParent(employee))">授权</button>
<button class="ghost-button danger" type="button" @click="emit('remove-employee', withParent(employee))">移除</button>
</td>
</tr>
<tr v-if="selectedOrgNode && !filteredEmployees.length">
<td colspan="6" class="empty-row">当前节点暂无匹配人员</td>
</tr>
<tr v-if="!selectedOrgNode">
<td colspan="6" class="empty-row">请先选择左侧组织节点</td>
</tr>
</tbody>
</table>
</div>
<PaginationBar v-model:page="employeePage" v-model:page-size="employeePageSize" :total="filteredEmployees.length" />
</section>
</div>
</template>
- Step 2: Add script with tree flattening, selection, search, and pagination
Add this <script setup> block below the template:
<script setup>
import { computed, defineComponent, h, ref, watch } from "vue";
import PaginationBar from "./PaginationBar.vue";
import { usePagination } from "../utils/pagination";
const props = defineProps({
nodes: {
type: Array,
default: () => []
},
selectedNodeId: {
type: [Number, String],
default: ""
}
});
const emit = defineEmits([
"select-node",
"node-contextmenu",
"add-employee",
"authorize-employee",
"remove-employee"
]);
const expandedNodeIds = ref(new Set());
const personKeyword = ref("");
const flatOrgNodes = computed(() => flattenOrgNodes(props.nodes));
const selectedOrgNode = computed(() => {
const selectedId = String(props.selectedNodeId || "");
return flatOrgNodes.value.find((node) => String(node.node_id) === selectedId) || props.nodes[0] || null;
});
const filteredEmployees = computed(() => {
const employees = selectedOrgNode.value?.employees || [];
const keyword = personKeyword.value.trim().toLowerCase();
if (!keyword) {
return employees;
}
return employees.filter((employee) =>
[
employee.employee_name,
employee.mobile,
employee.username,
employee.job_title,
...(employee.role_names || [])
]
.filter(Boolean)
.join(" ")
.toLowerCase()
.includes(keyword)
);
});
const {
page: employeePage,
pageSize: employeePageSize,
rows: paginatedEmployees
} = usePagination(filteredEmployees);
watch(
() => props.nodes,
() => {
const next = new Set(expandedNodeIds.value);
for (const node of props.nodes || []) {
if (node?.node_id) {
next.add(String(node.node_id));
}
}
expandedNodeIds.value = next;
},
{ immediate: true }
);
watch(
() => props.selectedNodeId,
() => {
personKeyword.value = "";
employeePage.value = 1;
}
);
function flattenOrgNodes(nodes) {
const result = [];
for (const node of nodes || []) {
result.push(node);
result.push(...flattenOrgNodes(node.children || []));
}
return result;
}
function selectNode(node) {
emit("select-node", node);
}
function toggleNode(node) {
if (!node?.node_id) {
return;
}
const key = String(node.node_id);
const next = new Set(expandedNodeIds.value);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
expandedNodeIds.value = next;
}
function emitNodeContextMenu(payload) {
emit("node-contextmenu", payload);
}
function canBindEmployees(node) {
return ["DEPARTMENT", "GROUP"].includes(node?.node_type);
}
function formatRoleNames(employee) {
const roleNames = Array.isArray(employee.role_names) ? employee.role_names : [];
return roleNames.length ? roleNames.join("、") : "未授权";
}
function withParent(employee) {
return {
...employee,
parent_node_id: selectedOrgNode.value?.node_id || null,
parent_node_label: selectedOrgNode.value?.node_label || ""
};
}
const OrgTreeNode = defineComponent({
name: "OrgTreeNode",
props: {
node: { type: Object, required: true },
level: { type: Number, default: 0 },
selectedNodeId: { type: [Number, String], default: "" },
expandedNodeIds: { type: Object, required: true }
},
emits: ["select", "toggle", "node-contextmenu"],
setup(nodeProps, { emit: nodeEmit }) {
return () => {
const children = nodeProps.node.children || [];
const hasChildren = children.length > 0;
const isExpanded = nodeProps.expandedNodeIds.has(String(nodeProps.node.node_id));
const isSelected = String(nodeProps.selectedNodeId || "") === String(nodeProps.node.node_id);
return h("div", { class: "org-tree-node-wrap" }, [
h(
"div",
{
class: ["org-tree-node", { active: isSelected }],
style: { paddingLeft: `${12 + nodeProps.level * 18}px` },
onClick: () => nodeEmit("select", nodeProps.node),
onContextmenu: (event) => {
event.preventDefault();
event.stopPropagation();
nodeEmit("node-contextmenu", { node: nodeProps.node, x: event.clientX, y: event.clientY });
}
},
[
h(
"button",
{
class: "org-tree-toggle",
disabled: !hasChildren,
onClick: (event) => {
event.stopPropagation();
nodeEmit("toggle", nodeProps.node);
}
},
hasChildren ? (isExpanded ? "−" : "+") : "·"
),
h("span", { class: "org-tree-node-type" }, nodeTypeLabel(nodeProps.node.node_type)),
h("strong", nodeProps.node.node_label),
h("em", formatManagers(nodeProps.node))
]
),
hasChildren && isExpanded
? h(
"div",
{ class: "org-tree-children" },
children.map((child) =>
h(OrgTreeNode, {
key: child.node_id,
node: child,
level: nodeProps.level + 1,
selectedNodeId: nodeProps.selectedNodeId,
expandedNodeIds: nodeProps.expandedNodeIds,
onSelect: (node) => nodeEmit("select", node),
onToggle: (node) => nodeEmit("toggle", node),
onNodeContextmenu: (payload) => nodeEmit("node-contextmenu", payload)
})
)
)
: null
]);
};
}
});
function nodeTypeLabel(type) {
return {
COMPANY: "总公司",
BRANCH: "分公司",
DEPARTMENT: "部门",
GROUP: "小组"
}[type] || "组织";
}
function formatManagers(node) {
const names = Array.isArray(node?.manager_names) ? node.manager_names : [];
if (names.length) {
return names.join("、");
}
return node?.manager_name || "未设置主管";
}
</script>
- Step 3: Add scoped styles
Add this style block:
<style scoped>
.org-permission-tree-card {
display: grid;
grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
gap: 16px;
min-height: 520px;
}
.org-tree-panel,
.org-tree-people-panel {
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 22px;
background: rgba(248, 250, 252, 0.9);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.org-tree-panel {
overflow: hidden;
}
.org-tree-head,
.org-tree-people-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px;
}
.org-tree-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 460px;
overflow: auto;
padding: 0 10px 14px;
}
.org-tree-node {
display: grid;
grid-template-columns: 28px auto minmax(0, 1fr);
align-items: center;
gap: 8px;
border-radius: 14px;
padding: 9px 10px;
color: #0f172a;
cursor: pointer;
}
.org-tree-node:hover,
.org-tree-node.active {
background: #eff6ff;
color: #1d4ed8;
}
.org-tree-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: 0;
border-radius: 8px;
background: #ffffff;
color: #2563eb;
font-weight: 900;
cursor: pointer;
}
.org-tree-toggle:disabled {
background: transparent;
color: #94a3b8;
cursor: default;
}
.org-tree-node-type {
border-radius: 999px;
padding: 4px 8px;
background: #dbeafe;
color: #1d4ed8;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.org-tree-node strong {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-tree-node em {
grid-column: 3 / 4;
color: #64748b;
font-size: 12px;
font-style: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-tree-people-panel {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
padding: 16px;
}
.tree-search-box {
display: flex;
align-items: center;
gap: 10px;
}
.tree-search-box span {
color: #475569;
font-size: 13px;
font-weight: 800;
}
.tree-search-box input {
width: min(360px, 100%);
border: 1px solid rgba(15, 23, 42, 0.1);
border-radius: 999px;
padding: 10px 14px;
background: #ffffff;
outline: none;
}
.danger {
background: #fef2f2;
color: #b91c1c;
}
.empty-state,
.permission-note {
color: #64748b;
font-size: 13px;
}
@media (max-width: 960px) {
.org-permission-tree-card {
grid-template-columns: 1fr;
}
}
</style>
- Step 4: Run static test and verify current next failure
Run:
node frontend/scripts/test-system-permission-management-ui.mjs
Expected result:
AssertionError
The component-specific assertions should pass; failures should now point to SystemPermissionView.vue still using the old page structure.
Task 3: Refactor SystemPermissionView Page Structure
Files:
-
Modify:
frontend/src/views/SystemPermissionView.vue -
Step 1: Import the new component
Add the import beside OrgMindMap:
import OrgPermissionTree from "../components/OrgPermissionTree.vue";
- Step 2: Replace top copy and tabs
Change the hero copy to:
<p>组织架构负责公司层级、人员授权和系统账号;角色维护负责菜单权限和业务身份。</p>
Change the top tab labels to:
组织架构
角色维护
Keep permissionMainTab values as "org" and "maintenance" to minimize routing/state churn.
- Step 3: Add organization mode switch
Add this state near permissionMainTab:
const orgViewMode = ref("mindmap");
Inside the organization section header, add this switch before the status pill:
<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>
- Step 4: Render mind map or tree mode
Wrap the existing OrgMindMap with:
<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"
/>
Add this computed near orgNodeCount:
const selectedOrgNodeId = computed(() => selectedNode.value?.node_id || orgNodes.value[0]?.node_id || "");
- Step 5: Hide mind-map-only toolbar in tree mode
Change the toolbar container to:
<div v-if="orgViewMode === 'mindmap'" class="mindmap-toolbar" aria-label="脑图工具栏">
Keep 放大、缩小、一屏展示、刷新 unchanged.
- Step 6: Replace maintenance page with role-only page
Remove the nested system-permission-sub-tabs block and remove the maintenanceSubTab === 'users' table panel.
The maintenance section header should become:
<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">
<!-- keep the existing roles table and PaginationBar here -->
</div>
</section>
Remove:
const maintenanceSubTab = ref("users");
const userRowsForTable = computed(() => users.value);
const {
page: userPage,
pageSize: userPageSize,
rows: paginatedUsers
} = usePagination(userRowsForTable);
Keep:
const users = ref([]);
because authorization needs it to detect existing system accounts.
- Step 7: Run static test and verify authorization assertions still fail
Run:
node frontend/scripts/test-system-permission-management-ui.mjs
Expected result:
AssertionError
The remaining failure should be around missing authorization drawer functions/state.
Task 4: Add Personnel Authorization Drawer and Logic
Files:
-
Modify:
frontend/src/views/SystemPermissionView.vue -
Step 1: Add authorization state
Add these declarations near existing drawer state:
const showAuthorizeDrawer = ref(false);
const authorizingEmployee = ref(null);
const authorizeForm = reactive({
role_ids: [],
password: "",
status: "ACTIVE"
});
Add these computed values near selectedEmployee:
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 || ""
);
- Step 2: Add authorization drawer template
Insert this drawer before the role drawer:
<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">
<div>
<p class="eyebrow">人员授权</p>
<h2>{{ authorizingEmployee?.employee_name || "人员授权" }}</h2>
</div>
<button class="ghost-button" type="button" @click="showAuthorizeDrawer = false">关闭</button>
</div>
<div class="authorize-summary">
<span>电话 / 账号:{{ authorizingEmployeePhone || "未设置电话号码" }}</span>
<span>所属组织:{{ authorizingEmployee?.parent_node_label || "未标记组织" }}</span>
<span>账号状态:{{ authorizingSystemUser ? (authorizingSystemUser.status === "ACTIVE" ? "已启用" : "已停用") : "未创建账号" }}</span>
</div>
<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>
<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>
</div>
</aside>
</div>
- Step 3: Add open and submit functions
Add these functions near removeOrgEmployee:
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 || "人员授权失败");
}
}
- Step 4: Update employee context menu actions
Change nodeActions(node) employee branch to:
if (node.employee_id) {
return [
{ key: "authorizeEmployee", label: "授权" },
{ key: "removeEmployee", label: "移除" }
];
}
Add this branch to runNodeAction:
} else if (actionKey === "authorizeEmployee") {
openAuthorizeDrawer(node);
- Step 5: Remove standalone user drawer code
Delete the old drawer controlled by:
showUserDrawer
createUserForm
openUserDrawer
submitCreateUser
syncSelectedEmployeePhone
resetPassword
toggleUserStatus
formatRoleNames(user)
Do not delete users, loadAll() user loading, or employeeOptions.
- Step 6: Run static test
Run:
node frontend/scripts/test-system-permission-management-ui.mjs
Expected result:
all assertions pass
If the script prints no output and exits 0, treat it as pass.
Task 5: Add Styling for Switch, Tree Layout, and Authorization Summary
Files:
-
Modify:
frontend/src/views/SystemPermissionView.vue -
Step 1: Add view switch styles
Add this CSS near .mindmap-toolbar:
.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);
}
- Step 2: Add authorization summary styles
Add this CSS near drawer styles:
.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;
}
- Step 3: Remove obsolete sub-tab styles
Delete CSS blocks for:
.system-permission-sub-tabs
.system-permission-sub-tabs button
.system-permission-sub-tabs button.active
Also remove the mobile media override for .system-permission-sub-tabs.
- Step 4: Run frontend build
Run:
cd frontend && npm run build
Expected result:
✓ built
Chunk-size warnings are acceptable if the build exits 0.
Task 6: Backend Regression Verification
Files:
-
Test only:
backend/tests/test_system_permission_management.py -
Step 1: Run backend permission tests
Run:
cd backend && python -m pytest tests/test_system_permission_management.py -q
Expected result:
passed
- Step 2: If tests fail due to local environment only, capture the exact blocker
Acceptable environment blockers include missing Python packages or unavailable virtual environment. Record the exact error text in the execution summary. Do not change backend code unless a real regression is identified.
Task 7: Browser Verification
Files:
-
No file changes.
-
Step 1: Start or reuse frontend dev server
Run:
cd frontend && npm run dev -- --host 127.0.0.1 --port 5173
Expected result:
Local: http://127.0.0.1:5173/
- Step 2: Verify page structure in browser
Open:
http://127.0.0.1:5173/system-permissions
Verify:
-
Top tabs are
组织架构and角色维护. -
组织架构has脑图模式 / 组织树模式. -
脑图模式still shows mind-map toolbar. -
组织树模式shows left organization tree and right personnel table. -
角色维护does not show standalone system personnel table. -
Step 3: Verify personnel authorization interaction
In browser:
- In mind-map mode, expand a node with personnel, right-click a personnel leaf, verify menu shows
授权and移除. - Click
授权, verify drawer displays phone/account, organization, account status, role selector, and password only when no account exists. - In tree mode, select an org node with personnel, verify row actions show
授权and移除.
Expected result:
Both modes can reach the same authorization drawer.
Self-Review
- Spec coverage:
- Organization view switch: Task 3.
- Tree mode UI: Task 2 and Task 7.
- Mind-map employee right-click
授权/移除: Task 4. - Role-only maintenance tab: Task 3.
- Account creation and role update from personnel node: Task 4.
- Tests and verification: Tasks 1, 5, 6, 7.
- Placeholder scan:
- No
TBD,TODO, or vague implementation-only placeholders remain.
- No
- Type consistency:
orgViewMode,showAuthorizeDrawer,authorizingEmployee,authorizeForm, andselectedOrgNodeIdare introduced before use.OrgPermissionTreeemitsselect-node,node-contextmenu,add-employee,authorize-employee,remove-employee;SystemPermissionView.vuelistens to the same names.- Existing backend endpoint names match
backend/app/api/routes/system_permissions.py.