Commit 7d3ae82b5406bfbb026915e597f044e012374f89
1 parent
9fa70479
Refactor: Enhance role and menu management UI with updated styles, icon support,…
… and improved interactivity.
Showing
3 changed files
with
386 additions
and
35 deletions
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 'vue' | @@ -89,6 +95,18 @@ import { computed, ref, watch } from 'vue' | ||
| 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; |