Commit 7d3ae82b5406bfbb026915e597f044e012374f89

Authored by shaofan
1 parent 9fa70479

Refactor: Enhance role and menu management UI with updated styles, icon support,…

… and improved interactivity.
src/style.css
@@ -745,7 +745,31 @@ a { @@ -745,7 +745,31 @@ a {
745 } 745 }
746 746
747 .list-search { 747 .list-search {
748 - width: 220px; 748 + width: 260px;
  749 +}
  750 +
  751 +.list-search.ant-input-search .ant-input-group {
  752 + display: flex;
  753 + align-items: center;
  754 + gap: 8px;
  755 +}
  756 +
  757 +.list-search.ant-input-search .ant-input-group > .ant-input-affix-wrapper,
  758 +.list-search.ant-input-search .ant-input-group > .ant-input {
  759 + border-radius: 999px !important;
  760 + border-right-width: 1px !important;
  761 +}
  762 +
  763 +.list-search.ant-input-search .ant-input-group > .ant-input-group-addon {
  764 + background: transparent;
  765 + border: none;
  766 + padding: 0;
  767 + width: auto;
  768 +}
  769 +
  770 +.list-search.ant-input-search .ant-input-group > .ant-input-group-addon .ant-btn {
  771 + border-radius: 999px !important;
  772 + border-left-width: 1px !important;
749 } 773 }
750 774
751 .list-table-card .ant-card-head { 775 .list-table-card .ant-card-head {
src/views/system/MenuManage.vue
@@ -3,14 +3,20 @@ @@ -3,14 +3,20 @@
3 <a-card title="菜单管理" :bordered="false" class="list-table-card"> 3 <a-card title="菜单管理" :bordered="false" class="list-table-card">
4 <div class="list-toolbar"> 4 <div class="list-toolbar">
5 <div class="list-toolbar-left"> 5 <div class="list-toolbar-left">
6 - <a-space wrap>  
7 - <a-tag v-if="currentNode">{{ currentNode.menuScope }}</a-tag>  
8 - <a-tag v-if="currentNode" :color="currentNode.visible === 1 ? 'green' : 'default'">{{ currentNode.visible === 1 ? '显示中' : '已隐藏' }}</a-tag>  
9 - <a-tag v-if="currentNode" :color="currentNode.status === 1 ? 'processing' : 'default'">{{ currentNode.status === 1 ? '已启用' : '已停用' }}</a-tag> 6 + <a-space wrap class="top-tags">
  7 + <a-tag v-if="currentNode" :color="currentNode.menuScope === 'PLATFORM' ? 'blue' : currentNode.menuScope === 'SUBSTATION' ? 'cyan' : 'purple'">{{ currentNode.menuScope === 'PLATFORM' ? '平台' : currentNode.menuScope === 'SUBSTATION' ? '分站' : '通用' }}</a-tag>
  8 + <a-tag v-if="currentNode" :color="currentNode.visible === 1 ? 'green' : 'default'">
  9 + <template #icon><EyeOutlined v-if="currentNode.visible === 1" /><EyeInvisibleOutlined v-else /></template>
  10 + {{ currentNode.visible === 1 ? '显示中' : '已隐藏' }}
  11 + </a-tag>
  12 + <a-tag v-if="currentNode" :color="currentNode.status === 1 ? 'blue' : 'error'">
  13 + <template #icon><CheckCircleOutlined v-if="currentNode.status === 1" /><StopOutlined v-else /></template>
  14 + {{ currentNode.status === 1 ? '已启用' : '已停用' }}
  15 + </a-tag>
10 </a-space> 16 </a-space>
11 </div> 17 </div>
12 <div class="list-toolbar-right"> 18 <div class="list-toolbar-right">
13 - <a-button type="primary" @click="openAddRoot">新增根菜单</a-button> 19 + <a-button type="primary" shape="round" @click="openAddRoot">新增根菜单</a-button>
14 </div> 20 </div>
15 </div> 21 </div>
16 22
@@ -31,13 +37,27 @@ @@ -31,13 +37,27 @@
31 :field-names="fieldNames" 37 :field-names="fieldNames"
32 block-node 38 block-node
33 default-expand-all 39 default-expand-all
  40 + :show-line="{ showLeafIcon: false }"
34 @select="handleSelect" 41 @select="handleSelect"
35 > 42 >
36 - <template #title="node">  
37 - <span>{{ node.name }}</span>  
38 - <a-tag style="margin-left: 8px">{{ node.menuScope }}</a-tag>  
39 - <a-tag style="margin-left: 4px" :color="node.visible === 1 ? 'green' : 'default'">{{ node.visible === 1 ? '显' : '隐' }}</a-tag>  
40 - <a-tag style="margin-left: 4px" :color="node.status === 1 ? 'processing' : 'default'">{{ node.status === 1 ? '启' : '停' }}</a-tag> 43 + <template #title="{ dataRef }">
  44 + <div class="menu-tree-node" :class="{ 'is-node-disabled': dataRef.status === 0, 'is-node-hidden': dataRef.visible === 0 }">
  45 + <span class="menu-node-name" :class="{ 'is-disabled': dataRef.status === 0, 'is-hidden': dataRef.visible === 0 }">
  46 + <component :is="resolveIcon(dataRef.icon)" v-if="resolveIcon(dataRef.icon)" class="menu-icon" />
  47 + <span class="menu-name-text">{{ dataRef.name }}</span>
  48 + </span>
  49 + <span class="menu-node-tags">
  50 + <span class="mini-tag tag-scope" :class="dataRef.menuScope?.toLowerCase()">
  51 + {{ dataRef.menuScope === 'PLATFORM' ? '平台' : dataRef.menuScope === 'SUBSTATION' ? '分站' : '通用' }}
  52 + </span>
  53 + <span v-if="dataRef.visible === 0" class="mini-tag tag-warning" title="已隐藏">
  54 + <EyeInvisibleOutlined /> 隐藏
  55 + </span>
  56 + <span v-if="dataRef.status === 0" class="mini-tag tag-error" title="已停用">
  57 + <StopOutlined /> 停用
  58 + </span>
  59 + </span>
  60 + </div>
41 </template> 61 </template>
42 </a-tree> 62 </a-tree>
43 <a-empty v-else description="暂无菜单" /> 63 <a-empty v-else description="暂无菜单" />
@@ -53,21 +73,23 @@ @@ -53,21 +73,23 @@
53 </div> 73 </div>
54 74
55 <div class="plan-toolbar"> 75 <div class="plan-toolbar">
56 - <div class="plan-toolbar-meta">  
57 - <span class="plan-toolbar-eyebrow">当前编辑</span>  
58 - <strong>{{ currentNode.name || '未命名菜单' }}</strong> 76 + <div class="plan-toolbar-left">
  77 + <div class="plan-toolbar-meta">
  78 + <span class="plan-toolbar-eyebrow">当前编辑</span>
  79 + <strong>{{ currentNode.name || '未命名菜单' }}</strong>
  80 + </div>
  81 + <a-space wrap class="plan-toolbar-actions">
  82 + <a-button @click="openAddChild" shape="round">新增子菜单</a-button>
  83 + <a-button @click="toggleVisible" shape="round">{{ form.visible === 1 ? '隐藏菜单' : '显示菜单' }}</a-button>
  84 + <a-button @click="toggleStatus" shape="round">{{ form.status === 1 ? '停用菜单' : '启用菜单' }}</a-button>
  85 + <a-popconfirm title="确认删除当前菜单?" @confirm="handleDelete">
  86 + <a-button danger shape="round">删除菜单</a-button>
  87 + </a-popconfirm>
  88 + </a-space>
59 </div> 89 </div>
60 - <a-space wrap>  
61 - <a-button @click="openAddChild">新增子菜单</a-button>  
62 - <a-button @click="toggleVisible">{{ form.visible === 1 ? '隐藏菜单' : '显示菜单' }}</a-button>  
63 - <a-button @click="toggleStatus">{{ form.status === 1 ? '停用菜单' : '启用菜单' }}</a-button>  
64 - <a-popconfirm title="确认删除当前菜单?" @confirm="handleDelete">  
65 - <a-button danger>删除菜单</a-button>  
66 - </a-popconfirm>  
67 - </a-space>  
68 <div class="plan-toolbar-submit"> 90 <div class="plan-toolbar-submit">
69 <span class="plan-toolbar-tip">保存后重新登录即可看到侧边栏变化</span> 91 <span class="plan-toolbar-tip">保存后重新登录即可看到侧边栏变化</span>
70 - <a-button type="primary" class="plan-save-button" :loading="saving" @click="handleSave">保存菜单</a-button> 92 + <a-button type="primary" class="plan-save-button" shape="round" :loading="saving" @click="handleSave">保存菜单</a-button>
71 </div> 93 </div>
72 </div> 94 </div>
73 </div> 95 </div>
@@ -185,6 +207,21 @@ @@ -185,6 +207,21 @@
185 import { computed, reactive, ref } from 'vue' 207 import { computed, reactive, ref } from 'vue'
186 import { message } from 'ant-design-vue' 208 import { message } from 'ant-design-vue'
187 import { systemMenuApi } from '@/api' 209 import { systemMenuApi } from '@/api'
  210 +import {
  211 + ApiOutlined,
  212 + ApartmentOutlined,
  213 + ControlOutlined,
  214 + GlobalOutlined,
  215 + HomeOutlined,
  216 + ShopOutlined,
  217 + StarOutlined,
  218 + UnorderedListOutlined,
  219 + UserOutlined,
  220 + EyeOutlined,
  221 + EyeInvisibleOutlined,
  222 + CheckCircleOutlined,
  223 + StopOutlined,
  224 +} from '@ant-design/icons-vue'
188 225
189 interface MenuNode { 226 interface MenuNode {
190 id: number 227 id: number
@@ -213,6 +250,23 @@ const iconOptions = [ @@ -213,6 +250,23 @@ const iconOptions = [
213 'ApiOutlined', 250 'ApiOutlined',
214 ] 251 ]
215 252
  253 +const iconMap: Record<string, any> = {
  254 + HomeOutlined,
  255 + GlobalOutlined,
  256 + ApartmentOutlined,
  257 + ShopOutlined,
  258 + UserOutlined,
  259 + StarOutlined,
  260 + UnorderedListOutlined,
  261 + ControlOutlined,
  262 + ApiOutlined,
  263 +}
  264 +
  265 +function resolveIcon(icon?: string) {
  266 + if (!icon) return null
  267 + return iconMap[icon] || null
  268 +}
  269 +
216 const treeData = ref<MenuNode[]>([]) 270 const treeData = ref<MenuNode[]>([])
217 const selectedKeys = ref<number[]>([]) 271 const selectedKeys = ref<number[]>([])
218 const loading = ref(false) 272 const loading = ref(false)
@@ -488,6 +542,12 @@ loadTree() @@ -488,6 +542,12 @@ loadTree()
488 box-shadow: var(--shadow-sm); 542 box-shadow: var(--shadow-sm);
489 } 543 }
490 544
  545 +.plan-toolbar-left {
  546 + display: flex;
  547 + align-items: center;
  548 + gap: 24px;
  549 +}
  550 +
491 .plan-toolbar-meta, 551 .plan-toolbar-meta,
492 .plan-toolbar-submit { 552 .plan-toolbar-submit {
493 display: flex; 553 display: flex;
@@ -568,4 +628,128 @@ loadTree() @@ -568,4 +628,128 @@ loadTree()
568 align-items: flex-start; 628 align-items: flex-start;
569 } 629 }
570 } 630 }
  631 +
  632 +.menu-tree-node {
  633 + display: flex;
  634 + align-items: center;
  635 + justify-content: space-between;
  636 + width: 100%;
  637 + padding: 4px 4px 4px 0;
  638 +}
  639 +
  640 +.menu-node-name {
  641 + flex: 1;
  642 + display: flex;
  643 + align-items: center;
  644 + min-width: 0;
  645 + font-size: 14px;
  646 + transition: color 0.3s;
  647 +}
  648 +
  649 +.menu-name-text {
  650 + overflow: hidden;
  651 + text-overflow: ellipsis;
  652 + white-space: nowrap;
  653 +}
  654 +
  655 +.menu-icon {
  656 + margin-right: 6px;
  657 + font-size: 14px;
  658 + color: var(--text-soft);
  659 + flex-shrink: 0;
  660 +}
  661 +
  662 +.menu-tree-node.is-node-disabled {
  663 + opacity: 0.5;
  664 + filter: grayscale(100%);
  665 +}
  666 +
  667 +.menu-tree-node.is-node-hidden {
  668 + opacity: 0.5;
  669 +}
  670 +
  671 +.menu-node-name.is-disabled {
  672 + color: var(--text-soft);
  673 +}
  674 +
  675 +.menu-node-name.is-hidden {
  676 + color: var(--text-soft);
  677 +}
  678 +
  679 +.menu-node-tags {
  680 + display: flex;
  681 + align-items: center;
  682 + gap: 4px;
  683 + flex-shrink: 0;
  684 +}
  685 +
  686 +.mini-tag {
  687 + font-size: 11px;
  688 + line-height: 18px;
  689 + padding: 0 6px;
  690 + border-radius: 4px;
  691 + background: var(--line);
  692 + color: var(--text-soft);
  693 + font-weight: 500;
  694 +}
  695 +
  696 +.mini-tag.tag-scope.platform {
  697 + color: #1677ff;
  698 + background: #e6f4ff;
  699 +}
  700 +
  701 +.mini-tag.tag-scope.substation {
  702 + color: #52c41a;
  703 + background: #f6ffed;
  704 +}
  705 +
  706 +.mini-tag.tag-scope.both {
  707 + color: #722ed1;
  708 + background: #f9f0ff;
  709 +}
  710 +
  711 +.mini-tag.tag-warning {
  712 + color: #faad14;
  713 + background: #fffbe6;
  714 +}
  715 +
  716 +.mini-tag.tag-error {
  717 + color: #ff4d4f;
  718 + background: #fff2f0;
  719 +}
  720 +:deep(.ant-tree) {
  721 + background: transparent;
  722 +}
  723 +:deep(.ant-tree-node-content-wrapper) {
  724 + border-radius: 8px;
  725 + transition: all 0.2s;
  726 +}
  727 +:deep(.ant-tree-node-content-wrapper:hover) {
  728 + background-color: var(--fill-quaternary, #f5f5f5);
  729 +}
  730 +:deep(.ant-tree-node-selected) {
  731 + background-color: var(--primary-50, #f0e6ff) !important;
  732 +}
  733 +:deep(.ant-tree-node-selected .menu-name-text) {
  734 + color: var(--primary-color, #722ed1);
  735 + font-weight: 600;
  736 +}
  737 +:deep(.ant-tree-node-selected .menu-icon) {
  738 + color: var(--primary-color, #722ed1);
  739 +}
  740 +
  741 +.top-tags :deep(.ant-tag) {
  742 + border-radius: 999px;
  743 + border: none;
  744 + padding: 2px 10px;
  745 + font-weight: 500;
  746 +}
  747 +
  748 +/* Right side inputs styling */
  749 +:deep(.ant-input),
  750 +:deep(.ant-input-number),
  751 +:deep(.ant-select-selector),
  752 +:deep(.ant-tree-select .ant-select-selector) {
  753 + border-radius: 12px !important;
  754 +}
571 </style> 755 </style>
src/views/system/RoleMenuAssign.vue
@@ -20,10 +20,10 @@ @@ -20,10 +20,10 @@
20 > 20 >
21 <div class="plan-item-top"> 21 <div class="plan-item-top">
22 <span class="plan-item-name">{{ role.name }}</span> 22 <span class="plan-item-name">{{ role.name }}</span>
23 - <a-tag>{{ role.roleScope }}</a-tag> 23 + <span class="mini-tag tag-scope" :class="role.roleScope?.toLowerCase()">{{ role.roleScope === 'PLATFORM' ? '平台' : role.roleScope === 'SUBSTATION' ? '分站' : '通用' }}</span>
24 </div> 24 </div>
25 <div class="plan-item-bottom"> 25 <div class="plan-item-bottom">
26 - <span>{{ role.code }}</span> 26 + <span class="role-code">@{{ role.code }}</span>
27 </div> 27 </div>
28 </button> 28 </button>
29 <a-empty v-if="!roles.length" description="暂无角色" /> 29 <a-empty v-if="!roles.length" description="暂无角色" />
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 <template v-if="selectedRole"> 34 <template v-if="selectedRole">
35 <div class="plan-content-top"> 35 <div class="plan-content-top">
36 <div class="soft-note-card plan-note-card"> 36 <div class="soft-note-card plan-note-card">
37 - <strong>分配说明</strong> 37 + <div class="note-title"><InfoCircleOutlined /> 分配说明</div>
38 <p>这里只控制菜单可见性,不做完整接口权限。保存后用户重新登录即可看到最新菜单。</p> 38 <p>这里只控制菜单可见性,不做完整接口权限。保存后用户重新登录即可看到最新菜单。</p>
39 </div> 39 </div>
40 40
@@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
45 </div> 45 </div>
46 <div class="plan-toolbar-submit"> 46 <div class="plan-toolbar-submit">
47 <span class="plan-toolbar-tip">平台角色只能分平台/通用菜单,分站角色只能分分站/通用菜单</span> 47 <span class="plan-toolbar-tip">平台角色只能分平台/通用菜单,分站角色只能分分站/通用菜单</span>
48 - <a-button type="primary" class="plan-save-button" :loading="saving" @click="handleSave">保存分配</a-button> 48 + <a-button type="primary" class="plan-save-button" shape="round" :loading="saving" @click="handleSave">保存分配</a-button>
49 </div> 49 </div>
50 </div> 50 </div>
51 </div> 51 </div>
@@ -53,7 +53,7 @@ @@ -53,7 +53,7 @@
53 <div class="plan-content-body"> 53 <div class="plan-content-body">
54 <a-spin :spinning="loadingTree"> 54 <a-spin :spinning="loadingTree">
55 <div class="soft-note-card" style="margin-bottom: 12px"> 55 <div class="soft-note-card" style="margin-bottom: 12px">
56 - <strong>当前角色范围:{{ selectedRole.roleScope }}</strong> 56 + <div class="note-title"><InfoCircleOutlined /> 当前角色范围:{{ selectedRole.roleScope === 'PLATFORM' ? '平台' : selectedRole.roleScope === 'SUBSTATION' ? '分站' : '通用' }}</div>
57 <p style="margin: 6px 0 0">可勾选节点已经按角色范围过滤;保存后目标账号重新登录即可看到最新菜单。</p> 57 <p style="margin: 6px 0 0">可勾选节点已经按角色范围过滤;保存后目标账号重新登录即可看到最新菜单。</p>
58 </div> 58 </div>
59 <a-tree 59 <a-tree
@@ -61,15 +61,21 @@ @@ -61,15 +61,21 @@
61 checkable 61 checkable
62 block-node 62 block-node
63 check-strictly 63 check-strictly
  64 + default-expand-all
  65 + :show-line="{ showLeafIcon: false }"
64 :tree-data="treeData" 66 :tree-data="treeData"
65 :field-names="fieldNames" 67 :field-names="fieldNames"
66 :checked-keys="checkedKeys" 68 :checked-keys="checkedKeys"
67 @check="handleCheck" 69 @check="handleCheck"
68 > 70 >
69 - <template #title="node">  
70 - <span>{{ node.name }}</span>  
71 - <a-tag style="margin-left: 8px">{{ node.menuScope }}</a-tag>  
72 - <a-tag style="margin-left: 4px" :color="node.checked ? 'processing' : 'default'">{{ node.checked ? '已分配' : '未分配' }}</a-tag> 71 + <template #title="{ dataRef }">
  72 + <span class="menu-node-name">
  73 + <component :is="resolveIcon(dataRef.icon)" v-if="resolveIcon(dataRef.icon)" class="menu-icon" />
  74 + <span class="menu-name-text">{{ dataRef.name }}</span>
  75 + </span>
  76 + <span class="menu-node-tags">
  77 + <span class="mini-tag tag-scope" :class="dataRef.menuScope?.toLowerCase()">{{ dataRef.menuScope === 'PLATFORM' ? '平台' : dataRef.menuScope === 'SUBSTATION' ? '分站' : '通用' }}</span>
  78 + </span>
73 </template> 79 </template>
74 </a-tree> 80 </a-tree>
75 <a-empty v-else description="当前角色暂无可分配菜单" /> 81 <a-empty v-else description="当前角色暂无可分配菜单" />
@@ -89,6 +95,18 @@ import { computed, ref, watch } from &#39;vue&#39; @@ -89,6 +95,18 @@ import { computed, ref, watch } from &#39;vue&#39;
89 import { useRoute } from 'vue-router' 95 import { useRoute } from 'vue-router'
90 import { message } from 'ant-design-vue' 96 import { message } from 'ant-design-vue'
91 import { systemRoleApi } from '@/api' 97 import { systemRoleApi } from '@/api'
  98 +import {
  99 + ApiOutlined,
  100 + ApartmentOutlined,
  101 + ControlOutlined,
  102 + GlobalOutlined,
  103 + HomeOutlined,
  104 + ShopOutlined,
  105 + StarOutlined,
  106 + UnorderedListOutlined,
  107 + UserOutlined,
  108 + InfoCircleOutlined
  109 +} from '@ant-design/icons-vue'
92 110
93 interface RoleVO { 111 interface RoleVO {
94 id: number 112 id: number
@@ -103,9 +121,27 @@ interface MenuTreeNode { @@ -103,9 +121,27 @@ interface MenuTreeNode {
103 code: string 121 code: string
104 menuScope: string 122 menuScope: string
105 checked: boolean 123 checked: boolean
  124 + icon?: string
106 children: MenuTreeNode[] 125 children: MenuTreeNode[]
107 } 126 }
108 127
  128 +const iconMap: Record<string, any> = {
  129 + HomeOutlined,
  130 + GlobalOutlined,
  131 + ApartmentOutlined,
  132 + ShopOutlined,
  133 + UserOutlined,
  134 + StarOutlined,
  135 + UnorderedListOutlined,
  136 + ControlOutlined,
  137 + ApiOutlined,
  138 +}
  139 +
  140 +function resolveIcon(icon?: string) {
  141 + if (!icon) return null
  142 + return iconMap[icon] || null
  143 +}
  144 +
109 const route = useRoute() 145 const route = useRoute()
110 const fieldNames = { title: 'name', key: 'id', children: 'children' } 146 const fieldNames = { title: 'name', key: 'id', children: 'children' }
111 const roles = ref<RoleVO[]>([]) 147 const roles = ref<RoleVO[]>([])
@@ -267,12 +303,28 @@ loadRoles() @@ -267,12 +303,28 @@ loadRoles()
267 text-align: left; 303 text-align: left;
268 cursor: pointer; 304 cursor: pointer;
269 color: inherit; 305 color: inherit;
  306 + transition: all 0.3s;
270 } 307 }
271 308
272 -.plan-item.active {  
273 - border-color: var(--brand); 309 +.plan-item:hover:not(.active) {
  310 + border-color: #d3b4fa;
274 background: var(--panel-tint); 311 background: var(--panel-tint);
275 - box-shadow: var(--shadow-sm); 312 +}
  313 +
  314 +.plan-item.active {
  315 + border-color: #d3b4fa;
  316 + background: var(--primary-50, #f6f0ff);
  317 + box-shadow: none;
  318 +}
  319 +
  320 +.plan-item.active .plan-item-name {
  321 + color: var(--primary-color, #722ed1);
  322 +}
  323 +
  324 +.plan-item-name {
  325 + color: var(--text-dark);
  326 + font-family: var(--font-display);
  327 + transition: color 0.3s;
276 } 328 }
277 329
278 .plan-toolbar { 330 .plan-toolbar {
@@ -308,6 +360,79 @@ loadRoles() @@ -308,6 +360,79 @@ loadRoles()
308 height: 36px; 360 height: 36px;
309 } 361 }
310 362
  363 +.menu-node-name {
  364 + display: flex;
  365 + align-items: center;
  366 + min-width: 0;
  367 + font-size: 14px;
  368 +}
  369 +
  370 +.menu-name-text {
  371 + overflow: hidden;
  372 + text-overflow: ellipsis;
  373 + white-space: nowrap;
  374 +}
  375 +
  376 +.menu-node-tags {
  377 + display: flex;
  378 + align-items: center;
  379 + gap: 4px;
  380 + margin-left: 8px;
  381 + flex-shrink: 0;
  382 +}
  383 +
  384 +.mini-tag {
  385 + display: inline-flex;
  386 + align-items: center;
  387 + gap: 2px;
  388 + padding: 0 8px;
  389 + height: 22px;
  390 + border-radius: 999px;
  391 + font-size: 12px;
  392 + font-weight: 500;
  393 + line-height: 1;
  394 +}
  395 +
  396 +.tag-scope.platform {
  397 + background: #e6f4ff;
  398 + color: #1677ff;
  399 +}
  400 +
  401 +.tag-scope.substation {
  402 + background: #f6ffed;
  403 + color: #389e0d;
  404 +}
  405 +
  406 +.tag-scope.both {
  407 + background: #f9f0ff;
  408 + color: #722ed1;
  409 +}
  410 +
  411 +.tag-processing {
  412 + background: #e6f4ff;
  413 + color: #1677ff;
  414 +}
  415 +
  416 +.note-title {
  417 + display: flex;
  418 + align-items: center;
  419 + gap: 6px;
  420 + font-weight: 600;
  421 + color: var(--text-dark);
  422 +}
  423 +
  424 +.role-code {
  425 + font-family: var(--font-mono, monospace);
  426 + opacity: 0.8;
  427 +}
  428 +
  429 +.menu-icon {
  430 + margin-right: 6px;
  431 + font-size: 14px;
  432 + color: var(--text-soft);
  433 + flex-shrink: 0;
  434 +}
  435 +
311 .plan-content > :deep(.ant-empty) { 436 .plan-content > :deep(.ant-empty) {
312 margin: auto 0; 437 margin: auto 0;
313 } 438 }
@@ -316,6 +441,24 @@ loadRoles() @@ -316,6 +441,24 @@ loadRoles()
316 background: transparent; 441 background: transparent;
317 } 442 }
318 443
  444 +:deep(.ant-tree-node-content-wrapper) {
  445 + display: flex;
  446 + align-items: center;
  447 + border-radius: 8px;
  448 + padding: 4px 8px 4px 0;
  449 + transition: all 0.3s;
  450 +}
  451 +
  452 +:deep(.ant-tree-node-content-wrapper:hover) {
  453 + background-color: var(--panel-tint);
  454 +}
  455 +
  456 +:deep(.ant-tree-title) {
  457 + display: flex;
  458 + align-items: center;
  459 + width: 100%;
  460 +}
  461 +
319 @media (max-width: 960px) { 462 @media (max-width: 960px) {
320 .plan-layout { 463 .plan-layout {
321 grid-template-columns: 1fr; 464 grid-template-columns: 1fr;